robotframework-openapitools 0.3.0__py3-none-any.whl → 1.0.0__py3-none-any.whl

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 (63) hide show
  1. OpenApiDriver/__init__.py +45 -41
  2. OpenApiDriver/openapi_executors.py +83 -49
  3. OpenApiDriver/openapi_reader.py +114 -116
  4. OpenApiDriver/openapidriver.libspec +209 -133
  5. OpenApiDriver/openapidriver.py +31 -296
  6. OpenApiLibCore/__init__.py +39 -13
  7. OpenApiLibCore/annotations.py +10 -0
  8. OpenApiLibCore/data_generation/__init__.py +10 -0
  9. OpenApiLibCore/data_generation/body_data_generation.py +250 -0
  10. OpenApiLibCore/data_generation/data_generation_core.py +233 -0
  11. OpenApiLibCore/data_invalidation.py +294 -0
  12. OpenApiLibCore/dto_base.py +75 -129
  13. OpenApiLibCore/dto_utils.py +125 -85
  14. OpenApiLibCore/localized_faker.py +88 -0
  15. OpenApiLibCore/models.py +723 -0
  16. OpenApiLibCore/oas_cache.py +14 -13
  17. OpenApiLibCore/openapi_libcore.libspec +363 -322
  18. OpenApiLibCore/openapi_libcore.py +388 -1903
  19. OpenApiLibCore/parameter_utils.py +97 -0
  20. OpenApiLibCore/path_functions.py +215 -0
  21. OpenApiLibCore/path_invalidation.py +42 -0
  22. OpenApiLibCore/protocols.py +38 -0
  23. OpenApiLibCore/request_data.py +246 -0
  24. OpenApiLibCore/resource_relations.py +55 -0
  25. OpenApiLibCore/validation.py +380 -0
  26. OpenApiLibCore/value_utils.py +216 -481
  27. openapi_libgen/__init__.py +3 -0
  28. openapi_libgen/command_line.py +75 -0
  29. openapi_libgen/generator.py +82 -0
  30. openapi_libgen/parsing_utils.py +30 -0
  31. openapi_libgen/spec_parser.py +154 -0
  32. openapi_libgen/templates/__init__.jinja +3 -0
  33. openapi_libgen/templates/library.jinja +30 -0
  34. robotframework_openapitools-1.0.0.dist-info/METADATA +249 -0
  35. robotframework_openapitools-1.0.0.dist-info/RECORD +40 -0
  36. {robotframework_openapitools-0.3.0.dist-info → robotframework_openapitools-1.0.0.dist-info}/WHEEL +1 -1
  37. robotframework_openapitools-1.0.0.dist-info/entry_points.txt +3 -0
  38. roboswag/__init__.py +0 -9
  39. roboswag/__main__.py +0 -3
  40. roboswag/auth.py +0 -44
  41. roboswag/cli.py +0 -80
  42. roboswag/core.py +0 -85
  43. roboswag/generate/__init__.py +0 -1
  44. roboswag/generate/generate.py +0 -121
  45. roboswag/generate/models/__init__.py +0 -0
  46. roboswag/generate/models/api.py +0 -219
  47. roboswag/generate/models/definition.py +0 -28
  48. roboswag/generate/models/endpoint.py +0 -68
  49. roboswag/generate/models/parameter.py +0 -25
  50. roboswag/generate/models/response.py +0 -8
  51. roboswag/generate/models/tag.py +0 -16
  52. roboswag/generate/models/utils.py +0 -60
  53. roboswag/generate/templates/api_init.jinja +0 -15
  54. roboswag/generate/templates/models.jinja +0 -7
  55. roboswag/generate/templates/paths.jinja +0 -68
  56. roboswag/logger.py +0 -33
  57. roboswag/validate/__init__.py +0 -6
  58. roboswag/validate/core.py +0 -3
  59. roboswag/validate/schema.py +0 -21
  60. roboswag/validate/text_response.py +0 -14
  61. robotframework_openapitools-0.3.0.dist-info/METADATA +0 -41
  62. robotframework_openapitools-0.3.0.dist-info/RECORD +0 -41
  63. {robotframework_openapitools-0.3.0.dist-info → robotframework_openapitools-1.0.0.dist-info}/LICENSE +0 -0
OpenApiDriver/__init__.py CHANGED
@@ -1,41 +1,45 @@
1
- """
2
- The OpenApiDriver package is intended to be used as a Robot Framework library.
3
- The following classes and constants are exposed to be used by the library user:
4
- - OpenApiDriver: The class to be used as a Library in the *** Settings *** section
5
- - IdDependency, IdReference, PathPropertiesConstraint, PropertyValueConstraint,
6
- UniquePropertyValueConstraint: Classes to be subclassed by the library user
7
- when implementing a custom mapping module (advanced use).
8
- - Dto, Relation: Base classes that can be used for type annotations.
9
- - IGNORE: A special constant that can be used as a value in the PropertyValueConstraint.
10
- """
11
-
12
- from importlib.metadata import version
13
-
14
- from OpenApiDriver.openapidriver import OpenApiDriver
15
- from OpenApiLibCore.dto_base import (
16
- Dto,
17
- IdDependency,
18
- IdReference,
19
- PathPropertiesConstraint,
20
- PropertyValueConstraint,
21
- Relation,
22
- UniquePropertyValueConstraint,
23
- )
24
- from OpenApiLibCore.value_utils import IGNORE
25
-
26
- try:
27
- __version__ = version("robotframework-openapidriver")
28
- except Exception: # pragma: no cover
29
- pass
30
-
31
- __all__ = [
32
- "Dto",
33
- "IdDependency",
34
- "IdReference",
35
- "PathPropertiesConstraint",
36
- "PropertyValueConstraint",
37
- "Relation",
38
- "UniquePropertyValueConstraint",
39
- "IGNORE",
40
- "OpenApiDriver",
41
- ]
1
+ # pylint: disable=invalid-name
2
+ """
3
+ The OpenApiDriver package is intended to be used as a Robot Framework library.
4
+ The following classes and constants are exposed to be used by the library user:
5
+ - OpenApiDriver: The class to be used as a Library in the *** Settings *** section
6
+ - IdDependency, IdReference, PathPropertiesConstraint, PropertyValueConstraint,
7
+ UniquePropertyValueConstraint: Classes to be subclassed by the library user
8
+ when implementing a custom mapping module (advanced use).
9
+ - Dto, Relation: Base classes that can be used for type annotations.
10
+ - IGNORE: A special constant that can be used as a value in the PropertyValueConstraint.
11
+ """
12
+
13
+ from importlib.metadata import version
14
+
15
+ from OpenApiDriver.openapidriver import OpenApiDriver
16
+ from OpenApiLibCore.dto_base import (
17
+ Dto,
18
+ IdDependency,
19
+ IdReference,
20
+ PathPropertiesConstraint,
21
+ PropertyValueConstraint,
22
+ ResourceRelation,
23
+ UniquePropertyValueConstraint,
24
+ )
25
+ from OpenApiLibCore.validation import ValidationLevel
26
+ from OpenApiLibCore.value_utils import IGNORE
27
+
28
+ try:
29
+ __version__ = version("robotframework-openapidriver")
30
+ except Exception: # pragma: no cover pylint: disable=broad-exception-caught
31
+ pass
32
+
33
+
34
+ __all__ = [
35
+ "IGNORE",
36
+ "Dto",
37
+ "IdDependency",
38
+ "IdReference",
39
+ "OpenApiDriver",
40
+ "PathPropertiesConstraint",
41
+ "PropertyValueConstraint",
42
+ "ResourceRelation",
43
+ "UniquePropertyValueConstraint",
44
+ "ValidationLevel",
45
+ ]
@@ -1,52 +1,70 @@
1
1
  """Module containing the classes to perform automatic OpenAPI contract validation."""
2
2
 
3
- from logging import getLogger
3
+ from collections.abc import Mapping, MutableMapping
4
+ from http import HTTPStatus
5
+ from os import getenv
4
6
  from pathlib import Path
5
7
  from random import choice
6
- from typing import Any, Dict, List, Optional, Tuple, Union
8
+ from types import MappingProxyType
7
9
 
8
10
  from requests import Response
9
11
  from requests.auth import AuthBase
10
12
  from requests.cookies import RequestsCookieJar as CookieJar
11
- from robot.api import SkipExecution
13
+ from robot.api import logger
12
14
  from robot.api.deco import keyword, library
15
+ from robot.api.exceptions import SkipExecution
13
16
  from robot.libraries.BuiltIn import BuiltIn
14
17
 
15
- from OpenApiLibCore import OpenApiLibCore, RequestData, RequestValues, ValidationLevel
18
+ from OpenApiLibCore import (
19
+ KEYWORD_NAMES as LIBCORE_KEYWORD_NAMES,
20
+ )
21
+ from OpenApiLibCore import (
22
+ OpenApiLibCore,
23
+ RequestData,
24
+ RequestValues,
25
+ ValidationLevel,
26
+ )
27
+ from OpenApiLibCore.annotations import JSON
16
28
 
17
29
  run_keyword = BuiltIn().run_keyword
30
+ default_str_mapping: Mapping[str, str] = MappingProxyType({})
18
31
 
19
32
 
20
- logger = getLogger(__name__)
33
+ KEYWORD_NAMES = [
34
+ "test_unauthorized",
35
+ "test_forbidden",
36
+ "test_invalid_url",
37
+ "test_endpoint",
38
+ ]
21
39
 
22
40
 
23
41
  @library(scope="SUITE", doc_format="ROBOT")
24
- class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-attributes
42
+ class OpenApiExecutors(OpenApiLibCore):
25
43
  """Main class providing the keywords and core logic to perform endpoint validations."""
26
44
 
27
- def __init__( # pylint: disable=too-many-arguments
45
+ def __init__( # noqa: PLR0913, pylint: disable=dangerous-default-value
28
46
  self,
29
47
  source: str,
30
48
  origin: str = "",
31
49
  base_path: str = "",
32
50
  response_validation: ValidationLevel = ValidationLevel.WARN,
33
51
  disable_server_validation: bool = True,
34
- mappings_path: Union[str, Path] = "",
52
+ mappings_path: str | Path = "",
35
53
  invalid_property_default_response: int = 422,
36
54
  default_id_property_name: str = "id",
37
- faker_locale: Optional[Union[str, List[str]]] = None,
55
+ faker_locale: str | list[str] = "",
38
56
  require_body_for_invalid_url: bool = False,
39
57
  recursion_limit: int = 1,
40
- recursion_default: Any = {},
58
+ recursion_default: JSON = {},
41
59
  username: str = "",
42
60
  password: str = "",
43
61
  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,
62
+ auth: AuthBase | None = None,
63
+ cert: str | tuple[str, str] = "",
64
+ verify_tls: bool | str = True,
65
+ extra_headers: Mapping[str, str] = default_str_mapping,
66
+ cookies: MutableMapping[str, str] | CookieJar | None = None,
67
+ proxies: MutableMapping[str, str] | None = None,
50
68
  ) -> None:
51
69
  super().__init__(
52
70
  source=source,
@@ -84,13 +102,13 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
84
102
  > Note: No headers or (json) body are send with the request. For security
85
103
  reasons, the authorization validation should be checked first.
86
104
  """
87
- url: str = run_keyword("get_valid_url", path, method)
105
+ url: str = run_keyword("get_valid_url", path)
88
106
  response = self.session.request(
89
107
  method=method,
90
108
  url=url,
91
109
  verify=False,
92
110
  )
93
- if response.status_code != 401:
111
+ if response.status_code != int(HTTPStatus.UNAUTHORIZED):
94
112
  raise AssertionError(f"Response {response.status_code} was not 401.")
95
113
 
96
114
  @keyword
@@ -105,9 +123,9 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
105
123
  > Note: No headers or (json) body are send with the request. For security
106
124
  reasons, the access rights validation should be checked first.
107
125
  """
108
- url: str = run_keyword("get_valid_url", path, method)
126
+ url: str = run_keyword("get_valid_url", path)
109
127
  response: Response = run_keyword("authorized_request", url, method)
110
- if response.status_code != 403:
128
+ if response.status_code != int(HTTPStatus.FORBIDDEN):
111
129
  raise AssertionError(f"Response {response.status_code} was not 403.")
112
130
 
113
131
  @keyword
@@ -118,8 +136,9 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
118
136
  Perform a request for the provided 'path' and 'method' where the url for
119
137
  the `path` is invalidated.
120
138
 
121
- This keyword will be `SKIPPED` if the path contains no parts that
122
- can be invalidated.
139
+ This keyword will be `SKIPPED` if the path contains no parts
140
+ that can be invalidated and there is no mapping for a
141
+ PathPropertiesConstraint for the `expected_status_code`.
123
142
 
124
143
  The optional `expected_status_code` parameter (default: 404) can be set to the
125
144
  expected status code for APIs that do not return a 404 on invalid urls.
@@ -129,9 +148,17 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
129
148
  parameters are send with the request. The `require_body_for_invalid_url`
130
149
  parameter can be set to `True` if needed.
131
150
  """
132
- valid_url: str = run_keyword("get_valid_url", path, method)
151
+ valid_url: str = run_keyword("get_valid_url", path)
152
+
153
+ try:
154
+ url = run_keyword(
155
+ "get_invalidated_url", valid_url, path, expected_status_code
156
+ )
157
+ except Exception as exception:
158
+ message = getattr(exception, "message", "")
159
+ if not message.startswith("ValueError"):
160
+ raise exception # pragma: no cover
133
161
 
134
- if not (url := run_keyword("get_invalidated_url", valid_url)):
135
162
  raise SkipExecution(
136
163
  f"Path {path} does not contain resource references that "
137
164
  f"can be invalidated."
@@ -139,7 +166,7 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
139
166
 
140
167
  params, headers, json_data = None, None, None
141
168
  if self.require_body_for_invalid_url:
142
- request_data = self.get_request_data(method=method, endpoint=path)
169
+ request_data: RequestData = run_keyword("get_request_data", path, method)
143
170
  params = request_data.params
144
171
  headers = request_data.headers
145
172
  dto = request_data.dto
@@ -164,11 +191,11 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
164
191
  The keyword calls other keywords to generate the neccesary data to perform
165
192
  the desired operation and validate the response against the openapi document.
166
193
  """
167
- json_data: Optional[Dict[str, Any]] = None
168
- original_data = None
194
+ json_data: dict[str, JSON] = {}
195
+ original_data = {}
169
196
 
170
- url: str = run_keyword("get_valid_url", path, method)
171
- request_data: RequestData = self.get_request_data(method=method, endpoint=path)
197
+ url: str = run_keyword("get_valid_url", path)
198
+ request_data: RequestData = run_keyword("get_request_data", path, method)
172
199
  params = request_data.params
173
200
  headers = request_data.headers
174
201
  if request_data.has_body:
@@ -177,10 +204,10 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
177
204
  if method == "PATCH":
178
205
  original_data = self.get_original_data(url=url)
179
206
  # in case of a status code indicating an error, ensure the error occurs
180
- if status_code >= 400:
207
+ if status_code >= int(HTTPStatus.BAD_REQUEST):
181
208
  invalidation_keyword_data = {
182
- "get_invalid_json_data": [
183
- "get_invalid_json_data",
209
+ "get_invalid_body_data": [
210
+ "get_invalid_body_data",
184
211
  url,
185
212
  method,
186
213
  status_code,
@@ -194,14 +221,14 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
194
221
  }
195
222
  invalidation_keywords = []
196
223
 
197
- if request_data.dto.get_relations_for_error_code(status_code):
198
- invalidation_keywords.append("get_invalid_json_data")
224
+ if request_data.dto.get_body_relations_for_error_code(status_code):
225
+ invalidation_keywords.append("get_invalid_body_data")
199
226
  if request_data.dto.get_parameter_relations_for_error_code(status_code):
200
227
  invalidation_keywords.append("get_invalidated_parameters")
201
228
  if invalidation_keywords:
202
229
  if (
203
230
  invalidation_keyword := choice(invalidation_keywords)
204
- ) == "get_invalid_json_data":
231
+ ) == "get_invalid_body_data":
205
232
  json_data = run_keyword(
206
233
  *invalidation_keyword_data[invalidation_keyword]
207
234
  )
@@ -219,13 +246,13 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
219
246
  params, headers = run_keyword(
220
247
  *invalidation_keyword_data["get_invalidated_parameters"]
221
248
  )
222
- if request_data.dto_schema:
249
+ if request_data.body_schema:
223
250
  json_data = run_keyword(
224
- *invalidation_keyword_data["get_invalid_json_data"]
251
+ *invalidation_keyword_data["get_invalid_body_data"]
225
252
  )
226
- elif request_data.dto_schema:
253
+ elif request_data.body_schema:
227
254
  json_data = run_keyword(
228
- *invalidation_keyword_data["get_invalid_json_data"]
255
+ *invalidation_keyword_data["get_invalid_body_data"]
229
256
  )
230
257
  else:
231
258
  raise SkipExecution(
@@ -248,20 +275,20 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
248
275
  ),
249
276
  original_data,
250
277
  )
251
- if status_code < 300 and (
278
+ if status_code < int(HTTPStatus.MULTIPLE_CHOICES) and (
252
279
  request_data.has_optional_properties
253
280
  or request_data.has_optional_params
254
281
  or request_data.has_optional_headers
255
282
  ):
256
283
  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)
284
+ url = run_keyword("get_valid_url", path)
285
+ request_data = run_keyword("get_request_data", path, method)
259
286
  params = request_data.get_required_params()
260
287
  headers = request_data.get_required_headers()
261
288
  json_data = (
262
- request_data.get_minimal_body_dict() if request_data.has_body else None
289
+ request_data.get_minimal_body_dict() if request_data.has_body else {}
263
290
  )
264
- original_data = None
291
+ original_data = {}
265
292
  if method == "PATCH":
266
293
  original_data = self.get_original_data(url=url)
267
294
  run_keyword(
@@ -278,15 +305,15 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
278
305
  original_data,
279
306
  )
280
307
 
281
- def get_original_data(self, url: str) -> Optional[Dict[str, Any]]:
308
+ def get_original_data(self, url: str) -> dict[str, JSON]:
282
309
  """
283
310
  Attempt to GET the current data for the given url and return it.
284
311
 
285
- If the GET request fails, None is returned.
312
+ If the GET request fails, an empty dict is returned.
286
313
  """
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")
314
+ original_data = {}
315
+ path = self.get_parameterized_path_from_url(url)
316
+ get_request_data: RequestData = run_keyword("get_request_data", path, "GET")
290
317
  get_params = get_request_data.params
291
318
  get_headers = get_request_data.headers
292
319
  response: Response = run_keyword(
@@ -295,3 +322,10 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
295
322
  if response.ok:
296
323
  original_data = response.json()
297
324
  return original_data
325
+
326
+ @staticmethod
327
+ def get_keyword_names() -> list[str]:
328
+ """Curated keywords for libdoc and libspec."""
329
+ if getenv("HIDE_INHERITED_KEYWORDS") == "true":
330
+ return KEYWORD_NAMES
331
+ return KEYWORD_NAMES + LIBCORE_KEYWORD_NAMES
@@ -1,116 +1,114 @@
1
- """Module holding the OpenApiReader reader_class implementation."""
2
-
3
- from typing import Any, Dict, List, Union
4
-
5
- from DataDriver.AbstractReaderClass import AbstractReaderClass
6
- from DataDriver.ReaderConfig import TestCaseData
7
-
8
-
9
- # pylint: disable=too-few-public-methods
10
- class Test:
11
- """
12
- Helper class to support ignoring endpoint responses when generating the test cases.
13
- """
14
-
15
- def __init__(self, path: str, method: str, response: Union[str, int]):
16
- self.path = path
17
- self.method = method.lower()
18
- self.response = str(response)
19
-
20
- def __eq__(self, other: Any) -> bool:
21
- if not isinstance(other, type(self)):
22
- return False
23
- return (
24
- self.path == other.path
25
- and self.method == other.method
26
- and self.response == other.response
27
- )
28
-
29
-
30
- class OpenApiReader(AbstractReaderClass):
31
- """Implementation of the reader_class used by DataDriver."""
32
-
33
- def get_data_from_source(self) -> List[TestCaseData]:
34
- test_data: List[TestCaseData] = []
35
-
36
- read_paths_method = getattr(self, "read_paths_method")
37
- paths: Dict[str, Any] = read_paths_method()
38
- self._filter_paths(paths)
39
-
40
- ignored_responses_ = [
41
- str(response) for response in getattr(self, "ignored_responses", [])
42
- ]
43
-
44
- ignored_tests = [Test(*test) for test in getattr(self, "ignored_testcases", [])]
45
-
46
- for path, path_item in paths.items():
47
- # by reseversing the items, post/put operations come before get and delete
48
- for item_name, item_data in reversed(path_item.items()):
49
- # this level of the OAS also contains data that's not related to a
50
- # path operation
51
- if item_name not in ["get", "put", "post", "delete", "patch"]:
52
- continue
53
- method, method_data = item_name, item_data
54
- tags_from_spec = method_data.get("tags", [])
55
- for response in method_data.get("responses"):
56
- # 'default' applies to all status codes that are not specified, in
57
- # which case we don't know what to expect and therefore can't verify
58
- if (
59
- response == "default"
60
- or response in ignored_responses_
61
- or Test(path, method, response) in ignored_tests
62
- ):
63
- continue
64
-
65
- tag_list = _get_tag_list(
66
- tags=tags_from_spec, method=method, response=response
67
- )
68
- test_data.append(
69
- TestCaseData(
70
- arguments={
71
- "${path}": path,
72
- "${method}": method.upper(),
73
- "${status_code}": response,
74
- },
75
- tags=tag_list,
76
- ),
77
- )
78
- return test_data
79
-
80
- def _filter_paths(self, paths: Dict[str, Any]) -> None:
81
- def matches_include_pattern(path: str) -> bool:
82
- for included_path in included_paths:
83
- if path == included_path:
84
- return True
85
- if included_path.endswith("*"):
86
- wildcard_include, _, _ = included_path.partition("*")
87
- if path.startswith(wildcard_include):
88
- return True
89
- return False
90
-
91
- def matches_ignore_pattern(path: str) -> bool:
92
- for ignored_path in ignored_paths:
93
- if path == ignored_path:
94
- return True
95
-
96
- if ignored_path.endswith("*"):
97
- wildcard_ignore, _, _ = ignored_path.partition("*")
98
- if path.startswith(wildcard_ignore):
99
- return True
100
- return False
101
-
102
- if included_paths := getattr(self, "included_paths", ()):
103
- path_list = list(paths.keys())
104
- for path in path_list:
105
- if not matches_include_pattern(path):
106
- paths.pop(path)
107
-
108
- if ignored_paths := getattr(self, "ignored_paths", ()):
109
- path_list = list(paths.keys())
110
- for path in path_list:
111
- if matches_ignore_pattern(path):
112
- paths.pop(path)
113
-
114
-
115
- def _get_tag_list(tags: List[str], method: str, response: str) -> List[str]:
116
- return [*tags, f"Method: {method.upper()}", f"Response: {response}"]
1
+ """Module holding the OpenApiReader reader_class implementation."""
2
+
3
+ from typing import Sequence
4
+
5
+ from DataDriver.AbstractReaderClass import AbstractReaderClass
6
+ from DataDriver.ReaderConfig import TestCaseData
7
+
8
+ from OpenApiLibCore.models import PathItemObject
9
+
10
+
11
+ class Test:
12
+ """
13
+ Helper class to support ignoring endpoint responses when generating the test cases.
14
+ """
15
+
16
+ def __init__(self, path: str, method: str, response: str | int) -> None:
17
+ self.path = path
18
+ self.method = method.lower()
19
+ self.response = str(response)
20
+
21
+ def __eq__(self, other: object) -> bool:
22
+ if not isinstance(other, type(self)):
23
+ return False
24
+ return (
25
+ self.path == other.path
26
+ and self.method == other.method
27
+ and self.response == other.response
28
+ )
29
+
30
+
31
+ class OpenApiReader(AbstractReaderClass):
32
+ """Implementation of the reader_class used by DataDriver."""
33
+
34
+ def get_data_from_source(self) -> list[TestCaseData]:
35
+ test_data: list[TestCaseData] = []
36
+
37
+ read_paths_method = getattr(self, "read_paths_method")
38
+ paths: dict[str, PathItemObject] = read_paths_method()
39
+ self._filter_paths(paths)
40
+
41
+ ignored_responses_ = [
42
+ str(response) for response in getattr(self, "ignored_responses", [])
43
+ ]
44
+
45
+ ignored_tests = [Test(*test) for test in getattr(self, "ignored_testcases", [])]
46
+
47
+ for path, path_item in paths.items():
48
+ path_operations = path_item.get_operations()
49
+
50
+ # by reseversing the items, post/put operations come before get and delete
51
+ for method, operation_data in reversed(path_operations.items()):
52
+ tags_from_spec = operation_data.tags
53
+ for response in operation_data.responses.keys():
54
+ # 'default' applies to all status codes that are not specified, in
55
+ # which case we don't know what to expect and therefore can't verify
56
+ if (
57
+ response == "default"
58
+ or response in ignored_responses_
59
+ or Test(path, method, response) in ignored_tests
60
+ ):
61
+ continue
62
+
63
+ tag_list = _get_tag_list(
64
+ tags=tags_from_spec, method=method, response=response
65
+ )
66
+ test_data.append(
67
+ TestCaseData(
68
+ arguments={
69
+ "${path}": path,
70
+ "${method}": method.upper(),
71
+ "${status_code}": response,
72
+ },
73
+ tags=tag_list,
74
+ ),
75
+ )
76
+ return test_data
77
+
78
+ def _filter_paths(self, paths: dict[str, PathItemObject]) -> None:
79
+ def matches_include_pattern(path: str) -> bool:
80
+ for included_path in included_paths:
81
+ if path == included_path:
82
+ return True
83
+ if included_path.endswith("*"):
84
+ wildcard_include, _, _ = included_path.partition("*")
85
+ if path.startswith(wildcard_include):
86
+ return True
87
+ return False
88
+
89
+ def matches_ignore_pattern(path: str) -> bool:
90
+ for ignored_path in ignored_paths:
91
+ if path == ignored_path:
92
+ return True
93
+
94
+ if ignored_path.endswith("*"):
95
+ wildcard_ignore, _, _ = ignored_path.partition("*")
96
+ if path.startswith(wildcard_ignore):
97
+ return True
98
+ return False
99
+
100
+ if included_paths := getattr(self, "included_paths", ()):
101
+ path_list = list(paths.keys())
102
+ for path in path_list:
103
+ if not matches_include_pattern(path):
104
+ paths.pop(path)
105
+
106
+ if ignored_paths := getattr(self, "ignored_paths", ()):
107
+ path_list = list(paths.keys())
108
+ for path in path_list:
109
+ if matches_ignore_pattern(path):
110
+ paths.pop(path)
111
+
112
+
113
+ def _get_tag_list(tags: Sequence[str], method: str, response: str) -> list[str]:
114
+ return [*tags, f"Method: {method.upper()}", f"Response: {response}"]