robotframework-openapitools 0.2.1__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/PKG-INFO +1 -1
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/pyproject.toml +140 -141
- robotframework_openapitools-0.2.3/src/OpenApiDriver/openapi_executors.py +297 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiDriver/openapidriver.libspec +107 -54
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiLibCore/__init__.py +54 -48
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiLibCore/dto_base.py +3 -3
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiLibCore/openapi_libcore.libspec +340 -68
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiLibCore/openapi_libcore.py +472 -2
- robotframework_openapitools-0.2.1/src/OpenApiDriver/openapi_executors.py +0 -764
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/LICENSE +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/docs/README.md +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiDriver/__init__.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiDriver/openapi_reader.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiDriver/openapidriver.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiDriver/py.typed +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiLibCore/dto_utils.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiLibCore/oas_cache.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiLibCore/py.typed +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiLibCore/value_utils.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/__init__.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/__main__.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/auth.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/cli.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/core.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/__init__.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/generate.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/models/__init__.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/models/api.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/models/definition.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/models/endpoint.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/models/parameter.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/models/response.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/models/tag.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/models/utils.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/templates/api_init.jinja +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/templates/models.jinja +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/templates/paths.jinja +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/logger.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/validate/__init__.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/validate/core.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/validate/schema.py +0 -0
- {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/validate/text_response.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: robotframework-openapitools
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.3
|
4
4
|
Summary: A set of Robot Framework libraries to test APIs for which the OAS is available.
|
5
5
|
Home-page: https://github.com/MarketSquare/robotframework-openapitools
|
6
6
|
License: Apache-2.0
|
@@ -1,142 +1,141 @@
|
|
1
|
-
[tool.poetry]
|
2
|
-
name="robotframework-openapitools"
|
3
|
-
version = "0.2.
|
4
|
-
description = "A set of Robot Framework libraries to test APIs for which the OAS is available."
|
5
|
-
license = "Apache-2.0"
|
6
|
-
authors = [
|
7
|
-
"Bartlomiej Hirsz <bartek.hirsz@gmail.com>",
|
8
|
-
"Mateusz Nojek <matnojek@gmail.com>",
|
9
|
-
"Robin Mackaij <r.a.mackaij@gmail.com>"
|
10
|
-
]
|
11
|
-
maintainers = ["Robin Mackaij <r.a.mackaij@gmail.com>"]
|
12
|
-
readme = "./docs/README.md"
|
13
|
-
homepage = "https://github.com/MarketSquare/robotframework-openapitools"
|
14
|
-
classifiers = [
|
15
|
-
"Programming Language :: Python :: 3",
|
16
|
-
"Programming Language :: Python :: 3.8",
|
17
|
-
"License :: OSI Approved :: Apache Software License",
|
18
|
-
"Operating System :: OS Independent",
|
19
|
-
"Topic :: Software Development :: Testing",
|
20
|
-
"Topic :: Software Development :: Testing :: Acceptance",
|
21
|
-
"Framework :: Robot Framework",
|
22
|
-
]
|
23
|
-
packages = [
|
24
|
-
{include = "OpenApiDriver", from = "src"},
|
25
|
-
{include = "OpenApiLibCore", from = "src"},
|
26
|
-
{include = "roboswag", from = "src"},
|
27
|
-
|
28
|
-
]
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
robotframework = ">=
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
types-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
[
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
[
|
76
|
-
|
77
|
-
"
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
[
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
[
|
92
|
-
|
93
|
-
"
|
94
|
-
"
|
95
|
-
"
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
[
|
116
|
-
|
117
|
-
|
118
|
-
[
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
[
|
132
|
-
|
133
|
-
|
134
|
-
"
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
"missing-doc-
|
139
|
-
"missing-doc-
|
140
|
-
"
|
141
|
-
"too-few-calls-in-test-case"
|
1
|
+
[tool.poetry]
|
2
|
+
name="robotframework-openapitools"
|
3
|
+
version = "0.2.3"
|
4
|
+
description = "A set of Robot Framework libraries to test APIs for which the OAS is available."
|
5
|
+
license = "Apache-2.0"
|
6
|
+
authors = [
|
7
|
+
"Bartlomiej Hirsz <bartek.hirsz@gmail.com>",
|
8
|
+
"Mateusz Nojek <matnojek@gmail.com>",
|
9
|
+
"Robin Mackaij <r.a.mackaij@gmail.com>"
|
10
|
+
]
|
11
|
+
maintainers = ["Robin Mackaij <r.a.mackaij@gmail.com>"]
|
12
|
+
readme = "./docs/README.md"
|
13
|
+
homepage = "https://github.com/MarketSquare/robotframework-openapitools"
|
14
|
+
classifiers = [
|
15
|
+
"Programming Language :: Python :: 3",
|
16
|
+
"Programming Language :: Python :: 3.8",
|
17
|
+
"License :: OSI Approved :: Apache Software License",
|
18
|
+
"Operating System :: OS Independent",
|
19
|
+
"Topic :: Software Development :: Testing",
|
20
|
+
"Topic :: Software Development :: Testing :: Acceptance",
|
21
|
+
"Framework :: Robot Framework",
|
22
|
+
]
|
23
|
+
packages = [
|
24
|
+
{include = "OpenApiDriver", from = "src"},
|
25
|
+
{include = "OpenApiLibCore", from = "src"},
|
26
|
+
{include = "roboswag", from = "src"},
|
27
|
+
]
|
28
|
+
include = ["*.libspec"]
|
29
|
+
|
30
|
+
[tool.poetry.dependencies]
|
31
|
+
python = "^3.8"
|
32
|
+
robotframework = ">=6.0.0, !=7.0.0"
|
33
|
+
robotframework-datadriver = ">=1.10.0"
|
34
|
+
requests = "^2.31.0"
|
35
|
+
prance = {version = "^23", extras = ["CLI"]}
|
36
|
+
Faker = ">=23.1.0"
|
37
|
+
rstr = "^3.2.0"
|
38
|
+
openapi-core = "^0.19.0"
|
39
|
+
rich_click = "^1.7.0"
|
40
|
+
black = ">=24.1.0"
|
41
|
+
Jinja2 = "^3.1.2"
|
42
|
+
|
43
|
+
[tool.poetry.group.dev.dependencies]
|
44
|
+
invoke = ">=2.2.0"
|
45
|
+
robotframework-stacktrace = ">=0.4.0"
|
46
|
+
uvicorn = ">=0.27.0"
|
47
|
+
fastapi = ">=0.109.0"
|
48
|
+
coverage = {version = ">=7.2.0", extras = ["toml"]}
|
49
|
+
|
50
|
+
[tool.poetry.group.formatting.dependencies]
|
51
|
+
isort = ">=5.13.0"
|
52
|
+
robotframework-tidy = ">=4.9.0"
|
53
|
+
|
54
|
+
[tool.poetry.group.type-checking.dependencies]
|
55
|
+
mypy = ">=1.8.0"
|
56
|
+
pyright = ">=1.1.350"
|
57
|
+
types-requests = ">=2.31.0"
|
58
|
+
types-invoke = ">=2.0.0.0"
|
59
|
+
|
60
|
+
[tool.poetry.group.linting.dependencies]
|
61
|
+
pylint = ">=3.0.0"
|
62
|
+
ruff = ">=0.2.0"
|
63
|
+
robotframework-robocop = ">=5.0.0"
|
64
|
+
|
65
|
+
[build-system]
|
66
|
+
requires = ["poetry-core>=1.0.0"]
|
67
|
+
build-backend = "poetry.core.masonry.api"
|
68
|
+
|
69
|
+
[tool.coverage.run]
|
70
|
+
branch = true
|
71
|
+
parallel = true
|
72
|
+
source = ["src"]
|
73
|
+
|
74
|
+
[tool.coverage.report]
|
75
|
+
exclude_lines = [
|
76
|
+
"pragma: no cover",
|
77
|
+
"@abstract"
|
78
|
+
]
|
79
|
+
|
80
|
+
[tool.mypy]
|
81
|
+
plugins = ["pydantic.mypy"]
|
82
|
+
warn_redundant_casts = true
|
83
|
+
warn_unused_ignores = true
|
84
|
+
disallow_any_generics = true
|
85
|
+
check_untyped_defs = true
|
86
|
+
disallow_untyped_defs = true
|
87
|
+
strict = true
|
88
|
+
show_error_codes = true
|
89
|
+
|
90
|
+
[[tool.mypy.overrides]]
|
91
|
+
module = [
|
92
|
+
"prance.*",
|
93
|
+
"invoke",
|
94
|
+
"uvicorn",
|
95
|
+
"rstr"
|
96
|
+
]
|
97
|
+
ignore_missing_imports = true
|
98
|
+
|
99
|
+
[tool.black]
|
100
|
+
line-length = 88
|
101
|
+
target-version = ["py38"]
|
102
|
+
|
103
|
+
[tool.isort]
|
104
|
+
profile = "black"
|
105
|
+
py_version=38
|
106
|
+
src_paths = [
|
107
|
+
"src"
|
108
|
+
]
|
109
|
+
|
110
|
+
[tool.ruff]
|
111
|
+
line-length = 120
|
112
|
+
src = ["src/OpenApiDriver"]
|
113
|
+
|
114
|
+
[tool.ruff.lint]
|
115
|
+
select = ["E", "F", "PL"]
|
116
|
+
|
117
|
+
[tool.pylint.'MESSAGES CONTROL']
|
118
|
+
disable = ["logging-fstring-interpolation", "missing-class-docstring"]
|
119
|
+
|
120
|
+
[tool.pylint.'FORMAT CHECKER']
|
121
|
+
max-line-length=120
|
122
|
+
|
123
|
+
[tool.pylint.'SIMILARITIES CHECKER']
|
124
|
+
ignore-imports="yes"
|
125
|
+
|
126
|
+
[tool.robotidy]
|
127
|
+
line_length = 120
|
128
|
+
spacecount = 4
|
129
|
+
|
130
|
+
[tool.robocop]
|
131
|
+
filetypes = [".robot", ".resource"]
|
132
|
+
configure = [
|
133
|
+
"line-too-long:line_length:120",
|
134
|
+
"too-many-calls-in-test-case:max_calls:15"
|
135
|
+
]
|
136
|
+
exclude = [
|
137
|
+
"missing-doc-suite",
|
138
|
+
"missing-doc-test-case",
|
139
|
+
"missing-doc-keyword",
|
140
|
+
"too-few-calls-in-test-case"
|
142
141
|
]
|
@@ -0,0 +1,297 @@
|
|
1
|
+
"""Module containing the classes to perform automatic OpenAPI contract validation."""
|
2
|
+
|
3
|
+
from logging import getLogger
|
4
|
+
from pathlib import Path
|
5
|
+
from random import choice
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
7
|
+
|
8
|
+
from requests import Response
|
9
|
+
from requests.auth import AuthBase
|
10
|
+
from requests.cookies import RequestsCookieJar as CookieJar
|
11
|
+
from robot.api import SkipExecution
|
12
|
+
from robot.api.deco import keyword, library
|
13
|
+
from robot.libraries.BuiltIn import BuiltIn
|
14
|
+
|
15
|
+
from OpenApiLibCore import OpenApiLibCore, RequestData, RequestValues, ValidationLevel
|
16
|
+
|
17
|
+
run_keyword = BuiltIn().run_keyword
|
18
|
+
|
19
|
+
|
20
|
+
logger = getLogger(__name__)
|
21
|
+
|
22
|
+
|
23
|
+
@library(scope="TEST SUITE", doc_format="ROBOT")
|
24
|
+
class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-attributes
|
25
|
+
"""Main class providing the keywords and core logic to perform endpoint validations."""
|
26
|
+
|
27
|
+
def __init__( # pylint: disable=too-many-arguments
|
28
|
+
self,
|
29
|
+
source: str,
|
30
|
+
origin: str = "",
|
31
|
+
base_path: str = "",
|
32
|
+
response_validation: ValidationLevel = ValidationLevel.WARN,
|
33
|
+
disable_server_validation: bool = True,
|
34
|
+
mappings_path: Union[str, Path] = "",
|
35
|
+
invalid_property_default_response: int = 422,
|
36
|
+
default_id_property_name: str = "id",
|
37
|
+
faker_locale: Optional[Union[str, List[str]]] = None,
|
38
|
+
require_body_for_invalid_url: bool = False,
|
39
|
+
recursion_limit: int = 1,
|
40
|
+
recursion_default: Any = {},
|
41
|
+
username: str = "",
|
42
|
+
password: str = "",
|
43
|
+
security_token: str = "",
|
44
|
+
auth: Optional[AuthBase] = None,
|
45
|
+
cert: Optional[Union[str, Tuple[str, str]]] = None,
|
46
|
+
verify_tls: Optional[Union[bool, str]] = True,
|
47
|
+
extra_headers: Optional[Dict[str, str]] = None,
|
48
|
+
cookies: Optional[Union[Dict[str, str], CookieJar]] = None,
|
49
|
+
proxies: Optional[Dict[str, str]] = None,
|
50
|
+
) -> None:
|
51
|
+
super().__init__(
|
52
|
+
source=source,
|
53
|
+
origin=origin,
|
54
|
+
base_path=base_path,
|
55
|
+
mappings_path=mappings_path,
|
56
|
+
default_id_property_name=default_id_property_name,
|
57
|
+
faker_locale=faker_locale,
|
58
|
+
recursion_limit=recursion_limit,
|
59
|
+
recursion_default=recursion_default,
|
60
|
+
username=username,
|
61
|
+
password=password,
|
62
|
+
security_token=security_token,
|
63
|
+
auth=auth,
|
64
|
+
cert=cert,
|
65
|
+
verify_tls=verify_tls,
|
66
|
+
extra_headers=extra_headers,
|
67
|
+
cookies=cookies,
|
68
|
+
proxies=proxies,
|
69
|
+
)
|
70
|
+
self.response_validation = response_validation
|
71
|
+
self.disable_server_validation = disable_server_validation
|
72
|
+
self.require_body_for_invalid_url = require_body_for_invalid_url
|
73
|
+
self.invalid_property_default_response = invalid_property_default_response
|
74
|
+
|
75
|
+
@keyword
|
76
|
+
def test_unauthorized(self, path: str, method: str) -> None:
|
77
|
+
"""
|
78
|
+
Perform a request for `method` on the `path`, with no authorization.
|
79
|
+
|
80
|
+
This keyword only passes if the response code is 401: Unauthorized.
|
81
|
+
|
82
|
+
Any authorization parameters used to initialize the library are
|
83
|
+
ignored for this request.
|
84
|
+
> Note: No headers or (json) body are send with the request. For security
|
85
|
+
reasons, the authorization validation should be checked first.
|
86
|
+
"""
|
87
|
+
url: str = run_keyword("get_valid_url", path, method)
|
88
|
+
response = self.session.request(
|
89
|
+
method=method,
|
90
|
+
url=url,
|
91
|
+
verify=False,
|
92
|
+
)
|
93
|
+
if response.status_code != 401:
|
94
|
+
raise AssertionError(f"Response {response.status_code} was not 401.")
|
95
|
+
|
96
|
+
@keyword
|
97
|
+
def test_forbidden(self, path: str, method: str) -> None:
|
98
|
+
"""
|
99
|
+
Perform a request for `method` on the `path`, with the provided authorization.
|
100
|
+
|
101
|
+
This keyword only passes if the response code is 403: Forbidden.
|
102
|
+
|
103
|
+
For this keyword to pass, the authorization parameters used to initialize the
|
104
|
+
library should grant insufficient access rights to the target endpoint.
|
105
|
+
> Note: No headers or (json) body are send with the request. For security
|
106
|
+
reasons, the access rights validation should be checked first.
|
107
|
+
"""
|
108
|
+
url: str = run_keyword("get_valid_url", path, method)
|
109
|
+
response: Response = run_keyword("authorized_request", url, method)
|
110
|
+
if response.status_code != 403:
|
111
|
+
raise AssertionError(f"Response {response.status_code} was not 403.")
|
112
|
+
|
113
|
+
@keyword
|
114
|
+
def test_invalid_url(
|
115
|
+
self, path: str, method: str, expected_status_code: int = 404
|
116
|
+
) -> None:
|
117
|
+
"""
|
118
|
+
Perform a request for the provided 'path' and 'method' where the url for
|
119
|
+
the `path` is invalidated.
|
120
|
+
|
121
|
+
This keyword will be `SKIPPED` if the path contains no parts that
|
122
|
+
can be invalidated.
|
123
|
+
|
124
|
+
The optional `expected_status_code` parameter (default: 404) can be set to the
|
125
|
+
expected status code for APIs that do not return a 404 on invalid urls.
|
126
|
+
|
127
|
+
> Note: Depending on API design, the url may be validated before or after
|
128
|
+
validation of headers, query parameters and / or (json) body. By default, no
|
129
|
+
parameters are send with the request. The `require_body_for_invalid_url`
|
130
|
+
parameter can be set to `True` if needed.
|
131
|
+
"""
|
132
|
+
valid_url: str = run_keyword("get_valid_url", path, method)
|
133
|
+
|
134
|
+
if not (url := run_keyword("get_invalidated_url", valid_url)):
|
135
|
+
raise SkipExecution(
|
136
|
+
f"Path {path} does not contain resource references that "
|
137
|
+
f"can be invalidated."
|
138
|
+
)
|
139
|
+
|
140
|
+
params, headers, json_data = None, None, None
|
141
|
+
if self.require_body_for_invalid_url:
|
142
|
+
request_data = self.get_request_data(method=method, endpoint=path)
|
143
|
+
params = request_data.params
|
144
|
+
headers = request_data.headers
|
145
|
+
dto = request_data.dto
|
146
|
+
json_data = dto.as_dict()
|
147
|
+
response: Response = run_keyword(
|
148
|
+
"authorized_request", url, method, params, headers, json_data
|
149
|
+
)
|
150
|
+
if response.status_code != expected_status_code:
|
151
|
+
raise AssertionError(
|
152
|
+
f"Response {response.status_code} was not {expected_status_code}"
|
153
|
+
)
|
154
|
+
|
155
|
+
@keyword
|
156
|
+
def test_endpoint(self, path: str, method: str, status_code: int) -> None:
|
157
|
+
"""
|
158
|
+
Validate that performing the `method` operation on `path` results in a
|
159
|
+
`status_code` response.
|
160
|
+
|
161
|
+
This is the main keyword to be used in the `Test Template` keyword when using
|
162
|
+
the OpenApiDriver.
|
163
|
+
|
164
|
+
The keyword calls other keywords to generate the neccesary data to perform
|
165
|
+
the desired operation and validate the response against the openapi document.
|
166
|
+
"""
|
167
|
+
json_data: Optional[Dict[str, Any]] = None
|
168
|
+
original_data = None
|
169
|
+
|
170
|
+
url: str = run_keyword("get_valid_url", path, method)
|
171
|
+
request_data: RequestData = self.get_request_data(method=method, endpoint=path)
|
172
|
+
params = request_data.params
|
173
|
+
headers = request_data.headers
|
174
|
+
if request_data.has_body:
|
175
|
+
json_data = request_data.dto.as_dict()
|
176
|
+
# when patching, get the original data to check only patched data has changed
|
177
|
+
if method == "PATCH":
|
178
|
+
original_data = self.get_original_data(url=url)
|
179
|
+
# in case of a status code indicating an error, ensure the error occurs
|
180
|
+
if status_code >= 400:
|
181
|
+
invalidation_keyword_data = {
|
182
|
+
"get_invalid_json_data": [
|
183
|
+
"get_invalid_json_data",
|
184
|
+
url,
|
185
|
+
method,
|
186
|
+
status_code,
|
187
|
+
request_data,
|
188
|
+
],
|
189
|
+
"get_invalidated_parameters": [
|
190
|
+
"get_invalidated_parameters",
|
191
|
+
status_code,
|
192
|
+
request_data,
|
193
|
+
],
|
194
|
+
}
|
195
|
+
invalidation_keywords = []
|
196
|
+
|
197
|
+
if request_data.dto.get_relations_for_error_code(status_code):
|
198
|
+
invalidation_keywords.append("get_invalid_json_data")
|
199
|
+
if request_data.dto.get_parameter_relations_for_error_code(status_code):
|
200
|
+
invalidation_keywords.append("get_invalidated_parameters")
|
201
|
+
if invalidation_keywords:
|
202
|
+
if (
|
203
|
+
invalidation_keyword := choice(invalidation_keywords)
|
204
|
+
) == "get_invalid_json_data":
|
205
|
+
json_data = run_keyword(
|
206
|
+
*invalidation_keyword_data[invalidation_keyword]
|
207
|
+
)
|
208
|
+
else:
|
209
|
+
params, headers = run_keyword(
|
210
|
+
*invalidation_keyword_data[invalidation_keyword]
|
211
|
+
)
|
212
|
+
# if there are no relations to invalide and the status_code is the default
|
213
|
+
# response_code for invalid properties, invalidate properties instead
|
214
|
+
elif status_code == self.invalid_property_default_response:
|
215
|
+
if (
|
216
|
+
request_data.params_that_can_be_invalidated
|
217
|
+
or request_data.headers_that_can_be_invalidated
|
218
|
+
):
|
219
|
+
params, headers = run_keyword(
|
220
|
+
*invalidation_keyword_data["get_invalidated_parameters"]
|
221
|
+
)
|
222
|
+
if request_data.dto_schema:
|
223
|
+
json_data = run_keyword(
|
224
|
+
*invalidation_keyword_data["get_invalid_json_data"]
|
225
|
+
)
|
226
|
+
elif request_data.dto_schema:
|
227
|
+
json_data = run_keyword(
|
228
|
+
*invalidation_keyword_data["get_invalid_json_data"]
|
229
|
+
)
|
230
|
+
else:
|
231
|
+
raise SkipExecution(
|
232
|
+
"No properties or parameters can be invalidated."
|
233
|
+
)
|
234
|
+
else:
|
235
|
+
raise AssertionError(
|
236
|
+
f"No Dto mapping found to cause status_code {status_code}."
|
237
|
+
)
|
238
|
+
run_keyword(
|
239
|
+
"perform_validated_request",
|
240
|
+
path,
|
241
|
+
status_code,
|
242
|
+
RequestValues(
|
243
|
+
url=url,
|
244
|
+
method=method,
|
245
|
+
params=params,
|
246
|
+
headers=headers,
|
247
|
+
json_data=json_data,
|
248
|
+
),
|
249
|
+
original_data,
|
250
|
+
)
|
251
|
+
if status_code < 300 and (
|
252
|
+
request_data.has_optional_properties
|
253
|
+
or request_data.has_optional_params
|
254
|
+
or request_data.has_optional_headers
|
255
|
+
):
|
256
|
+
logger.info("Performing request without optional properties and parameters")
|
257
|
+
url = run_keyword("get_valid_url", path, method)
|
258
|
+
request_data = self.get_request_data(method=method, endpoint=path)
|
259
|
+
params = request_data.get_required_params()
|
260
|
+
headers = request_data.get_required_headers()
|
261
|
+
json_data = (
|
262
|
+
request_data.get_minimal_body_dict() if request_data.has_body else None
|
263
|
+
)
|
264
|
+
original_data = None
|
265
|
+
if method == "PATCH":
|
266
|
+
original_data = self.get_original_data(url=url)
|
267
|
+
run_keyword(
|
268
|
+
"perform_validated_request",
|
269
|
+
path,
|
270
|
+
status_code,
|
271
|
+
RequestValues(
|
272
|
+
url=url,
|
273
|
+
method=method,
|
274
|
+
params=params,
|
275
|
+
headers=headers,
|
276
|
+
json_data=json_data,
|
277
|
+
),
|
278
|
+
original_data,
|
279
|
+
)
|
280
|
+
|
281
|
+
def get_original_data(self, url: str) -> Optional[Dict[str, Any]]:
|
282
|
+
"""
|
283
|
+
Attempt to GET the current data for the given url and return it.
|
284
|
+
|
285
|
+
If the GET request fails, None is returned.
|
286
|
+
"""
|
287
|
+
original_data = None
|
288
|
+
path = self.get_parameterized_endpoint_from_url(url)
|
289
|
+
get_request_data = self.get_request_data(endpoint=path, method="GET")
|
290
|
+
get_params = get_request_data.params
|
291
|
+
get_headers = get_request_data.headers
|
292
|
+
response: Response = run_keyword(
|
293
|
+
"authorized_request", url, "GET", get_params, get_headers
|
294
|
+
)
|
295
|
+
if response.ok:
|
296
|
+
original_data = response.json()
|
297
|
+
return original_data
|