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.
Files changed (42) hide show
  1. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/PKG-INFO +1 -1
  2. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/pyproject.toml +140 -141
  3. robotframework_openapitools-0.2.3/src/OpenApiDriver/openapi_executors.py +297 -0
  4. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiDriver/openapidriver.libspec +107 -54
  5. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiLibCore/__init__.py +54 -48
  6. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiLibCore/dto_base.py +3 -3
  7. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiLibCore/openapi_libcore.libspec +340 -68
  8. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiLibCore/openapi_libcore.py +472 -2
  9. robotframework_openapitools-0.2.1/src/OpenApiDriver/openapi_executors.py +0 -764
  10. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/LICENSE +0 -0
  11. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/docs/README.md +0 -0
  12. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiDriver/__init__.py +0 -0
  13. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiDriver/openapi_reader.py +0 -0
  14. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiDriver/openapidriver.py +0 -0
  15. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiDriver/py.typed +0 -0
  16. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiLibCore/dto_utils.py +0 -0
  17. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiLibCore/oas_cache.py +0 -0
  18. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiLibCore/py.typed +0 -0
  19. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/OpenApiLibCore/value_utils.py +0 -0
  20. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/__init__.py +0 -0
  21. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/__main__.py +0 -0
  22. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/auth.py +0 -0
  23. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/cli.py +0 -0
  24. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/core.py +0 -0
  25. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/__init__.py +0 -0
  26. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/generate.py +0 -0
  27. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/models/__init__.py +0 -0
  28. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/models/api.py +0 -0
  29. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/models/definition.py +0 -0
  30. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/models/endpoint.py +0 -0
  31. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/models/parameter.py +0 -0
  32. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/models/response.py +0 -0
  33. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/models/tag.py +0 -0
  34. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/models/utils.py +0 -0
  35. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/templates/api_init.jinja +0 -0
  36. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/templates/models.jinja +0 -0
  37. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/generate/templates/paths.jinja +0 -0
  38. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/logger.py +0 -0
  39. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/validate/__init__.py +0 -0
  40. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/validate/core.py +0 -0
  41. {robotframework_openapitools-0.2.1 → robotframework_openapitools-0.2.3}/src/roboswag/validate/schema.py +0 -0
  42. {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.1
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.1"
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
- include = ["*.libspec"]
30
-
31
- [tool.poetry.dependencies]
32
- python = "^3.8"
33
- robotframework = ">=6.0.0, !=7.0.0"
34
- robotframework-datadriver = ">=1.10.0"
35
- requests = "^2.31.0"
36
- prance = {version = "^23", extras = ["CLI"]}
37
- Faker = ">=23.1.0"
38
- rstr = "^3.2.0"
39
- openapi-core = "^0.19.0"
40
- rich_click = "^1.7.0"
41
- black = ">=24.1.0"
42
- Jinja2 = "^3.1.2"
43
-
44
- [tool.poetry.group.dev.dependencies]
45
- invoke = ">=2.2.0"
46
- robotframework-stacktrace = ">=0.4.0"
47
- uvicorn = ">=0.27.0"
48
- fastapi = ">=0.109.0"
49
- coverage = {version = ">=7.2.0", extras = ["toml"]}
50
-
51
- [tool.poetry.group.formatting.dependencies]
52
- isort = ">=5.13.0"
53
- robotframework-tidy = ">=4.9.0"
54
-
55
- [tool.poetry.group.type-checking.dependencies]
56
- mypy = ">=1.8.0"
57
- pyright = ">=1.1.350"
58
- types-requests = ">=2.31.0"
59
- types-invoke = ">=2.0.0.0"
60
-
61
- [tool.poetry.group.linting.dependencies]
62
- pylint = ">=3.0.0"
63
- ruff = ">=0.2.0"
64
- robotframework-robocop = ">=5.0.0"
65
-
66
- [build-system]
67
- requires = ["poetry-core>=1.0.0"]
68
- build-backend = "poetry.core.masonry.api"
69
-
70
- [tool.coverage.run]
71
- branch = true
72
- parallel = true
73
- source = ["src"]
74
-
75
- [tool.coverage.report]
76
- exclude_lines = [
77
- "pragma: no cover",
78
- "@abstract"
79
- ]
80
-
81
- [tool.mypy]
82
- plugins = ["pydantic.mypy"]
83
- warn_redundant_casts = true
84
- warn_unused_ignores = true
85
- disallow_any_generics = true
86
- check_untyped_defs = true
87
- disallow_untyped_defs = true
88
- strict = true
89
- show_error_codes = true
90
-
91
- [[tool.mypy.overrides]]
92
- module = [
93
- "prance.*",
94
- "invoke",
95
- "uvicorn",
96
- "rstr"
97
- ]
98
- ignore_missing_imports = true
99
-
100
- [tool.black]
101
- line-length = 88
102
- target-version = ["py38"]
103
-
104
- [tool.isort]
105
- profile = "black"
106
- py_version=38
107
- src_paths = [
108
- "src"
109
- ]
110
-
111
- [tool.ruff]
112
- line-length = 120
113
- src = ["src/OpenApiDriver"]
114
-
115
- [tool.ruff.lint]
116
- select = ["E", "F", "PL"]
117
-
118
- [tool.pylint.'MESSAGES CONTROL']
119
- disable = ["logging-fstring-interpolation", "missing-class-docstring"]
120
-
121
- [tool.pylint.'FORMAT CHECKER']
122
- max-line-length=120
123
-
124
- [tool.pylint.'SIMILARITIES CHECKER']
125
- ignore-imports="yes"
126
-
127
- [tool.robotidy]
128
- line_length = 120
129
- spacecount = 4
130
-
131
- [tool.robocop]
132
- filetypes = [".robot", ".resource"]
133
- configure = [
134
- "line-too-long:line_length:120",
135
- "too-many-calls-in-test-case:max_calls:15"
136
- ]
137
- exclude = [
138
- "missing-doc-suite",
139
- "missing-doc-test-case",
140
- "missing-doc-keyword",
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