Skip to content
This repository has been archived by the owner on Aug 9, 2024. It is now read-only.

Commit

Permalink
Merge pull request #5 from MarketSquare/feature/support_array_in_data…
Browse files Browse the repository at this point in the history
…_generation

Feature/support array in data generation
  • Loading branch information
robinmackaij authored Apr 28, 2022
2 parents 489a457 + 6c95160 commit 07f210c
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 36 deletions.
6 changes: 6 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ Validates OK as OpenAPI 3.0.2!
You'll have to change the url or file reference to the location of the openapi
document for your API.

> Note: if you encounter a `Recursion reached limit ...` error there is a circular
reference somewhere in your OpenAPI document.
Although recursion is technically allowed under the OAS, tool support is limited
and changing the API to not use recursion is recommended.
At present OpenApiLibCore does not support recursion in the OpenAPI document.

If the openapi document passes this validation, the next step is trying to do a test
run with a minimal test suite.
The example below can be used, with `source`, `origin` and 'endpoint' altered to
Expand Down
2 changes: 1 addition & 1 deletion docs/openapi_libcore.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name="robotframework-openapi-libcore"
version = "1.1.2"
version = "1.2.0"
description = "A Robot Framework library to facilitate library development for OpenAPI / Swagger APIs."
license = "Apache-2.0"
authors = ["Robin Mackaij"]
Expand Down
30 changes: 15 additions & 15 deletions src/OpenApiLibCore/openapi_libcore.libspec
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<keywordspec name="OpenApiLibCore" type="LIBRARY" format="HTML" scope="SUITE" generated="2022-04-02T16:06:44Z" specversion="4" source="C:\GitHub\robotframework-openapi-libcore\src\OpenApiLibCore\openapi_libcore.py" lineno="306">
<version>1.1.2</version>
<keywordspec name="OpenApiLibCore" type="LIBRARY" format="HTML" scope="SUITE" generated="2022-04-28T14:44:09Z" specversion="4" source="C:\GitHub\robotframework-openapi-libcore\src\OpenApiLibCore\openapi_libcore.py" lineno="314">
<version>1.2.0</version>
<doc>&lt;p&gt;Main class providing the keywords and core logic to interact with an OpenAPI server.&lt;/p&gt;
&lt;p&gt;Visit the &lt;a href="https://github.com/MarketSquare/robotframework-openapi-libcore"&gt;library page&lt;/a&gt; for an introduction.&lt;/p&gt;</doc>
<tags>
</tags>
<inits>
<init name="__init__" lineno="314">
<init name="__init__" lineno="322">
<arguments repr="source: str, origin: str = , base_path: str = , mappings_path: str | Path = , username: str = , password: str = , security_token: str = , auth: AuthBase | None = None, extra_headers: Dict[str, str] | None = None, invalid_property_default_response: int = 422">
<arg kind="POSITIONAL_OR_NAMED" required="true" repr="source: str">
<name>source</name>
Expand Down Expand Up @@ -85,7 +85,7 @@
</init>
</inits>
<keywords>
<kw name="Authorized Request" lineno="1094">
<kw name="Authorized Request" lineno="1101">
<arguments repr="url: str, method: str, params: Dict[str, Any] | None = None, headers: Dict[str, str] | None = None, json_data: Dict[str, Any] | None = None">
<arg kind="POSITIONAL_OR_NAMED" required="true" repr="url: str">
<name>url</name>
Expand Down Expand Up @@ -118,7 +118,7 @@
&lt;p&gt;&amp;gt; Note: provided username / password or auth objects take precedence over token based security&lt;/p&gt;</doc>
<shortdoc>Perform a request using the security token or authentication set in the library.</shortdoc>
</kw>
<kw name="Ensure In Use" lineno="998">
<kw name="Ensure In Use" lineno="1005">
<arguments repr="url: str, resource_relation: IdReference">
<arg kind="POSITIONAL_OR_NAMED" required="true" repr="url: str">
<name>url</name>
Expand All @@ -132,7 +132,7 @@
<doc>&lt;p&gt;Ensure that the (right-most) &lt;span class="name"&gt;id&lt;/span&gt; of the resource referenced by the &lt;span class="name"&gt;url&lt;/span&gt; is used by the resource defined by the &lt;span class="name"&gt;resource_relation&lt;/span&gt;.&lt;/p&gt;</doc>
<shortdoc>Ensure that the (right-most) `id` of the resource referenced by the `url` is used by the resource defined by the `resource_relation`.</shortdoc>
</kw>
<kw name="Get Ids From Url" lineno="531">
<kw name="Get Ids From Url" lineno="539">
<arguments repr="url: str">
<arg kind="POSITIONAL_OR_NAMED" required="true" repr="url: str">
<name>url</name>
Expand All @@ -142,7 +142,7 @@
<doc>&lt;p&gt;Perform a GET request on the &lt;span class="name"&gt;url&lt;/span&gt; and return the list of resource &lt;span class="name"&gt;ids&lt;/span&gt; from the response.&lt;/p&gt;</doc>
<shortdoc>Perform a GET request on the `url` and return the list of resource `ids` from the response.</shortdoc>
</kw>
<kw name="Get Invalid Json Data" lineno="826">
<kw name="Get Invalid Json Data" lineno="833">
<arguments repr="url: str, method: str, status_code: int, request_data: RequestData">
<arg kind="POSITIONAL_OR_NAMED" required="true" repr="url: str">
<name>url</name>
Expand All @@ -165,7 +165,7 @@
&lt;p&gt;&amp;gt; Note: applicable UniquePropertyValueConstraint and IdReference Relations are considered before changes to &lt;span class="name"&gt;json_data&lt;/span&gt; are made.&lt;/p&gt;</doc>
<shortdoc>Return `json_data` based on the `dto` on the `request_data` that will cause the provided `status_code` for the `method` operation on the `url`.</shortdoc>
</kw>
<kw name="Get Invalidated Parameters" lineno="874">
<kw name="Get Invalidated Parameters" lineno="881">
<arguments repr="status_code: int, request_data: RequestData">
<arg kind="POSITIONAL_OR_NAMED" required="true" repr="status_code: int">
<name>status_code</name>
Expand All @@ -179,7 +179,7 @@
<doc>&lt;p&gt;Returns a version of &lt;span class="name"&gt;params, headers&lt;/span&gt; as present on &lt;span class="name"&gt;request_data&lt;/span&gt; that has been modified to cause the provided &lt;span class="name"&gt;status_code&lt;/span&gt;.&lt;/p&gt;</doc>
<shortdoc>Returns a version of `params, headers` as present on `request_data` that has been modified to cause the provided `status_code`.</shortdoc>
</kw>
<kw name="Get Invalidated Url" lineno="791">
<kw name="Get Invalidated Url" lineno="799">
<arguments repr="valid_url: str">
<arg kind="POSITIONAL_OR_NAMED" required="true" repr="valid_url: str">
<name>valid_url</name>
Expand All @@ -190,7 +190,7 @@
&lt;p&gt;Raises ValueError if the valid_url cannot be invalidated.&lt;/p&gt;</doc>
<shortdoc>Return an url with all the path parameters in the `valid_url` replaced by a random UUID.</shortdoc>
</kw>
<kw name="Get Json Data For Dto Class" lineno="704">
<kw name="Get Json Data For Dto Class" lineno="712">
<arguments repr="schema: Dict[str, Any], dto_class: Dto | Type[Dto], operation_id: str">
<arg kind="POSITIONAL_OR_NAMED" required="true" repr="schema: Dict[str, Any]">
<name>schema</name>
Expand All @@ -209,7 +209,7 @@
<doc>&lt;p&gt;Generate a valid (json-compatible) dict for all the &lt;span class="name"&gt;dto_class&lt;/span&gt; properties.&lt;/p&gt;</doc>
<shortdoc>Generate a valid (json-compatible) dict for all the `dto_class` properties.</shortdoc>
</kw>
<kw name="Get Json Data With Conflict" lineno="1042">
<kw name="Get Json Data With Conflict" lineno="1049">
<arguments repr="url: str, method: str, dto: Dto, conflict_status_code: int">
<arg kind="POSITIONAL_OR_NAMED" required="true" repr="url: str">
<name>url</name>
Expand All @@ -231,7 +231,7 @@
<doc>&lt;p&gt;Return &lt;span class="name"&gt;json_data&lt;/span&gt; based on the &lt;span class="name"&gt;UniquePropertyValueConstraint&lt;/span&gt; that must be returned by the &lt;span class="name"&gt;get_relations&lt;/span&gt; implementation on the &lt;span class="name"&gt;dto&lt;/span&gt; for the given &lt;span class="name"&gt;conflict_status_code&lt;/span&gt;.&lt;/p&gt;</doc>
<shortdoc>Return `json_data` based on the `UniquePropertyValueConstraint` that must be returned by the `get_relations` implementation on the `dto` for the given `conflict_status_code`.</shortdoc>
</kw>
<kw name="Get Parameterized Endpoint From Url" lineno="814">
<kw name="Get Parameterized Endpoint From Url" lineno="821">
<arguments repr="url: str">
<arg kind="POSITIONAL_OR_NAMED" required="true" repr="url: str">
<name>url</name>
Expand All @@ -241,7 +241,7 @@
<doc>&lt;p&gt;Return the endpoint as found in the &lt;span class="name"&gt;paths&lt;/span&gt; section based on the given &lt;span class="name"&gt;url&lt;/span&gt;.&lt;/p&gt;</doc>
<shortdoc>Return the endpoint as found in the `paths` section based on the given `url`.</shortdoc>
</kw>
<kw name="Get Request Data" lineno="556">
<kw name="Get Request Data" lineno="564">
<arguments repr="endpoint: str, method: str">
<arg kind="POSITIONAL_OR_NAMED" required="true" repr="endpoint: str">
<name>endpoint</name>
Expand All @@ -255,7 +255,7 @@
<doc>&lt;p&gt;Return an object with valid request data for body, headers and query params.&lt;/p&gt;</doc>
<shortdoc>Return an object with valid request data for body, headers and query params.</shortdoc>
</kw>
<kw name="Get Valid Id For Endpoint" lineno="456">
<kw name="Get Valid Id For Endpoint" lineno="464">
<arguments repr="endpoint: str, method: str">
<arg kind="POSITIONAL_OR_NAMED" required="true" repr="endpoint: str">
<name>endpoint</name>
Expand All @@ -270,7 +270,7 @@
&lt;p&gt;To prevent resource conflicts with other test cases, a new resource is created (POST) if possible.&lt;/p&gt;</doc>
<shortdoc>Support keyword that returns the `id` for an existing resource at `endpoint`.</shortdoc>
</kw>
<kw name="Get Valid Url" lineno="418">
<kw name="Get Valid Url" lineno="426">
<arguments repr="endpoint: str, method: str">
<arg kind="POSITIONAL_OR_NAMED" required="true" repr="endpoint: str">
<name>endpoint</name>
Expand Down
9 changes: 8 additions & 1 deletion src/OpenApiLibCore/openapi_libcore.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@
You'll have to change the url or file reference to the location of the openapi
document for your API.
> Note: if you encounter a `Recursion reached limit ...` error there is a circular
reference somewhere in your OpenAPI document.
Although recursion is technically allowed under the OAS, tool support is limited
and changing the API to not use recursion is recommended.
At present OpenApiLibCore does not support recursion in the OpenAPI document.
If the openapi document passes this validation, the next step is trying to do a test
run with a minimal test suite.
The example below can be used, with `source`, `origin` and 'endpoint' altered to
Expand Down Expand Up @@ -271,6 +277,8 @@ def headers_that_can_be_invalidated(self) -> Set[str]:
"exclusiveMaximum",
"minLength",
"maxLength",
"minItems",
"maxItems",
}
):
result.add(header["name"])
Expand Down Expand Up @@ -807,7 +815,6 @@ def get_invalidated_url(self, valid_url: str) -> Optional[str]:
valid_url_parts.reverse()
invalid_url = "/".join(valid_url_parts)
return invalid_url
# TODO: add support for query parameters that can be invalidated
raise ValueError(f"{parameterized_endpoint} could not be invalidated.")

@keyword
Expand Down
24 changes: 24 additions & 0 deletions src/OpenApiLibCore/value_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ def get_valid_value(value_schema: Dict[str, Any]) -> Any:
return get_random_float(value_schema=value_schema)
if value_type == "string":
return get_random_string(value_schema=value_schema)
if value_type == "array":
return get_random_array(value_schema=value_schema)
raise NotImplementedError(f"Type '{value_type}' is currently not supported")


Expand Down Expand Up @@ -136,6 +138,20 @@ def get_random_string(value_schema: Dict[str, Any]) -> str:
return value


def get_random_array(value_schema: Dict[str, Any]) -> List[Any]:
"""Generate a list with random elements as specified by the schema."""
minimum = value_schema.get("minItems", 0)
maximum = value_schema.get("maxItems", 1)
if minimum > maximum:
maximum = minimum
items_schema = value_schema["items"]
value = []
for _ in range(maximum):
item_value = get_valid_value(items_schema)
value.append(item_value)
return value


def get_invalid_value_from_constraint(
values_from_constraint: List[Any], value_type: str
) -> Any:
Expand Down Expand Up @@ -218,6 +234,14 @@ def get_value_out_of_bounds(value_schema: Dict[str, Any], current_value: Any) ->
return exclusive_minimum
if exclusive_maximum := value_schema.get("exclusiveMaximum"):
return exclusive_maximum
if value_type == "array":
if minimum := value_schema.get("minItems", 0) > 0:
return current_value[0 : minimum - 1]
if maximum := value_schema.get("maxItems"):
invalid_value = current_value if current_value else ["x"]
while len(invalid_value) <= maximum:
invalid_value.append(choice(invalid_value))
return invalid_value
if value_type == "string":
# if there is a minimum length, send 1 character less
if minimum := value_schema.get("minLength", 0):
Expand Down
16 changes: 13 additions & 3 deletions tasks.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# pylint: disable=missing-function-docstring
# pylint: disable=missing-function-docstring, unused-argument
import pathlib
import subprocess
from importlib.metadata import version
Expand All @@ -13,8 +13,18 @@

@task
def testserver(context):
testserver_path = f"{ROOT}/tests/server/testserver.py"
subprocess.run(f"python {testserver_path}", shell=True, check=False)
cmd = [
"python",
"-m",
"uvicorn",
"testserver:app",
f"--app-dir {ROOT}/tests/server",
"--host 0.0.0.0",
"--port 8000",
"--reload",
f"--reload-dir {ROOT}/tests/server",
]
subprocess.run(" ".join(cmd), shell=True, check=False)


@task
Expand Down
19 changes: 9 additions & 10 deletions tests/server/testserver.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# pylint: disable="missing-class-docstring", "missing-function-docstring"
import datetime
from enum import Enum
from sys import float_info
from typing import Callable, Dict, List, Optional
from uuid import uuid4

import uvicorn
from fastapi import FastAPI, Header, HTTPException, Path, Query, Request, Response
from pydantic import BaseModel, confloat, conint, constr

Expand Down Expand Up @@ -149,7 +149,14 @@ def get_message(

# deliberate trailing /
@app.get("/events/", status_code=200, response_model=List[Event])
def get_events() -> List[Event]:
def get_events(
search_strings: Optional[List[str]] = Query(None),
) -> List[Event]:
if search_strings:
result: List[Event] = []
for search_string in search_strings:
result.extend([e for e in EVENTS if search_string in e.message.message])
return result
return EVENTS


Expand Down Expand Up @@ -338,11 +345,3 @@ def get_available_employees(weekday: WeekDay = Query(...)) -> List[EmployeeDetai
return [
e for e in EMPLOYEES.values() if getattr(e, "parttime_day", None) != weekday
]


def main():
uvicorn.run(app, host="0.0.0.0", port=8000)


if __name__ == "__main__":
main()
Loading

0 comments on commit 07f210c

Please sign in to comment.