schemathesis 3.25.6__py3-none-any.whl → 3.39.7__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 (146) hide show
  1. schemathesis/__init__.py +6 -6
  2. schemathesis/_compat.py +2 -2
  3. schemathesis/_dependency_versions.py +4 -2
  4. schemathesis/_hypothesis.py +369 -56
  5. schemathesis/_lazy_import.py +1 -0
  6. schemathesis/_override.py +5 -4
  7. schemathesis/_patches.py +21 -0
  8. schemathesis/_rate_limiter.py +7 -0
  9. schemathesis/_xml.py +75 -22
  10. schemathesis/auths.py +78 -16
  11. schemathesis/checks.py +21 -9
  12. schemathesis/cli/__init__.py +783 -432
  13. schemathesis/cli/__main__.py +4 -0
  14. schemathesis/cli/callbacks.py +58 -13
  15. schemathesis/cli/cassettes.py +233 -47
  16. schemathesis/cli/constants.py +8 -2
  17. schemathesis/cli/context.py +22 -5
  18. schemathesis/cli/debug.py +2 -1
  19. schemathesis/cli/handlers.py +4 -1
  20. schemathesis/cli/junitxml.py +103 -22
  21. schemathesis/cli/options.py +15 -4
  22. schemathesis/cli/output/default.py +258 -112
  23. schemathesis/cli/output/short.py +23 -8
  24. schemathesis/cli/reporting.py +79 -0
  25. schemathesis/cli/sanitization.py +6 -0
  26. schemathesis/code_samples.py +5 -3
  27. schemathesis/constants.py +1 -0
  28. schemathesis/contrib/openapi/__init__.py +1 -1
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
  30. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  31. schemathesis/contrib/unique_data.py +3 -3
  32. schemathesis/exceptions.py +76 -65
  33. schemathesis/experimental/__init__.py +35 -0
  34. schemathesis/extra/_aiohttp.py +1 -0
  35. schemathesis/extra/_flask.py +4 -1
  36. schemathesis/extra/_server.py +1 -0
  37. schemathesis/extra/pytest_plugin.py +17 -25
  38. schemathesis/failures.py +77 -9
  39. schemathesis/filters.py +185 -8
  40. schemathesis/fixups/__init__.py +1 -0
  41. schemathesis/fixups/fast_api.py +2 -2
  42. schemathesis/fixups/utf8_bom.py +1 -2
  43. schemathesis/generation/__init__.py +20 -36
  44. schemathesis/generation/_hypothesis.py +59 -0
  45. schemathesis/generation/_methods.py +44 -0
  46. schemathesis/generation/coverage.py +931 -0
  47. schemathesis/graphql.py +0 -1
  48. schemathesis/hooks.py +89 -12
  49. schemathesis/internal/checks.py +84 -0
  50. schemathesis/internal/copy.py +22 -3
  51. schemathesis/internal/deprecation.py +6 -2
  52. schemathesis/internal/diff.py +15 -0
  53. schemathesis/internal/extensions.py +27 -0
  54. schemathesis/internal/jsonschema.py +2 -1
  55. schemathesis/internal/output.py +68 -0
  56. schemathesis/internal/result.py +1 -1
  57. schemathesis/internal/transformation.py +11 -0
  58. schemathesis/lazy.py +138 -25
  59. schemathesis/loaders.py +7 -5
  60. schemathesis/models.py +318 -211
  61. schemathesis/parameters.py +4 -0
  62. schemathesis/runner/__init__.py +50 -15
  63. schemathesis/runner/events.py +65 -5
  64. schemathesis/runner/impl/context.py +104 -0
  65. schemathesis/runner/impl/core.py +388 -177
  66. schemathesis/runner/impl/solo.py +19 -29
  67. schemathesis/runner/impl/threadpool.py +70 -79
  68. schemathesis/runner/probes.py +11 -9
  69. schemathesis/runner/serialization.py +150 -17
  70. schemathesis/sanitization.py +5 -1
  71. schemathesis/schemas.py +170 -102
  72. schemathesis/serializers.py +7 -2
  73. schemathesis/service/ci.py +1 -0
  74. schemathesis/service/client.py +39 -6
  75. schemathesis/service/events.py +5 -1
  76. schemathesis/service/extensions.py +224 -0
  77. schemathesis/service/hosts.py +6 -2
  78. schemathesis/service/metadata.py +25 -0
  79. schemathesis/service/models.py +211 -2
  80. schemathesis/service/report.py +6 -6
  81. schemathesis/service/serialization.py +45 -71
  82. schemathesis/service/usage.py +1 -0
  83. schemathesis/specs/graphql/_cache.py +26 -0
  84. schemathesis/specs/graphql/loaders.py +25 -5
  85. schemathesis/specs/graphql/nodes.py +1 -0
  86. schemathesis/specs/graphql/scalars.py +2 -2
  87. schemathesis/specs/graphql/schemas.py +130 -100
  88. schemathesis/specs/graphql/validation.py +1 -2
  89. schemathesis/specs/openapi/__init__.py +1 -0
  90. schemathesis/specs/openapi/_cache.py +123 -0
  91. schemathesis/specs/openapi/_hypothesis.py +78 -60
  92. schemathesis/specs/openapi/checks.py +504 -25
  93. schemathesis/specs/openapi/converter.py +31 -4
  94. schemathesis/specs/openapi/definitions.py +10 -17
  95. schemathesis/specs/openapi/examples.py +126 -12
  96. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  97. schemathesis/specs/openapi/expressions/context.py +1 -1
  98. schemathesis/specs/openapi/expressions/extractors.py +26 -0
  99. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  100. schemathesis/specs/openapi/expressions/nodes.py +29 -6
  101. schemathesis/specs/openapi/expressions/parser.py +26 -5
  102. schemathesis/specs/openapi/formats.py +44 -0
  103. schemathesis/specs/openapi/links.py +125 -42
  104. schemathesis/specs/openapi/loaders.py +77 -36
  105. schemathesis/specs/openapi/media_types.py +34 -0
  106. schemathesis/specs/openapi/negative/__init__.py +6 -3
  107. schemathesis/specs/openapi/negative/mutations.py +21 -6
  108. schemathesis/specs/openapi/parameters.py +39 -25
  109. schemathesis/specs/openapi/patterns.py +137 -0
  110. schemathesis/specs/openapi/references.py +37 -7
  111. schemathesis/specs/openapi/schemas.py +360 -241
  112. schemathesis/specs/openapi/security.py +25 -7
  113. schemathesis/specs/openapi/serialization.py +1 -0
  114. schemathesis/specs/openapi/stateful/__init__.py +198 -70
  115. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  116. schemathesis/specs/openapi/stateful/types.py +14 -0
  117. schemathesis/specs/openapi/utils.py +6 -1
  118. schemathesis/specs/openapi/validation.py +1 -0
  119. schemathesis/stateful/__init__.py +35 -21
  120. schemathesis/stateful/config.py +97 -0
  121. schemathesis/stateful/context.py +135 -0
  122. schemathesis/stateful/events.py +274 -0
  123. schemathesis/stateful/runner.py +309 -0
  124. schemathesis/stateful/sink.py +68 -0
  125. schemathesis/stateful/state_machine.py +67 -38
  126. schemathesis/stateful/statistic.py +22 -0
  127. schemathesis/stateful/validation.py +100 -0
  128. schemathesis/targets.py +33 -1
  129. schemathesis/throttling.py +25 -5
  130. schemathesis/transports/__init__.py +354 -0
  131. schemathesis/transports/asgi.py +7 -0
  132. schemathesis/transports/auth.py +25 -2
  133. schemathesis/transports/content_types.py +3 -1
  134. schemathesis/transports/headers.py +2 -1
  135. schemathesis/transports/responses.py +9 -4
  136. schemathesis/types.py +9 -0
  137. schemathesis/utils.py +11 -16
  138. schemathesis-3.39.7.dist-info/METADATA +293 -0
  139. schemathesis-3.39.7.dist-info/RECORD +160 -0
  140. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
  141. schemathesis/specs/openapi/filters.py +0 -49
  142. schemathesis/specs/openapi/stateful/links.py +0 -92
  143. schemathesis-3.25.6.dist-info/METADATA +0 -356
  144. schemathesis-3.25.6.dist-info/RECORD +0 -134
  145. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
  146. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import string
4
+ from base64 import b64encode
5
+ from functools import lru_cache
3
6
  from typing import TYPE_CHECKING
4
7
 
5
8
  if TYPE_CHECKING:
@@ -33,5 +36,46 @@ def unregister_string_format(name: str) -> None:
33
36
  raise ValueError(f"Unknown Open API format: {name}") from exc
34
37
 
35
38
 
39
+ def header_values(blacklist_characters: str = "\n\r") -> st.SearchStrategy[str]:
40
+ from hypothesis import strategies as st
41
+
42
+ return st.text(
43
+ alphabet=st.characters(min_codepoint=0, max_codepoint=255, blacklist_characters=blacklist_characters)
44
+ # Header values with leading non-visible chars can't be sent with `requests`
45
+ ).map(str.lstrip)
46
+
47
+
48
+ HEADER_FORMAT = "_header_value"
49
+
50
+
51
+ @lru_cache
52
+ def get_default_format_strategies() -> dict[str, st.SearchStrategy]:
53
+ """Get all default "format" strategies."""
54
+ from hypothesis import strategies as st
55
+ from requests.auth import _basic_auth_str
56
+
57
+ from ...serializers import Binary
58
+
59
+ def make_basic_auth_str(item: tuple[str, str]) -> str:
60
+ return _basic_auth_str(*item)
61
+
62
+ latin1_text = st.text(alphabet=st.characters(min_codepoint=0, max_codepoint=255))
63
+
64
+ # Define valid characters here to avoid filtering them out in `is_valid_header` later
65
+ header_value = header_values()
66
+
67
+ return {
68
+ "binary": st.binary().map(Binary),
69
+ "byte": st.binary().map(lambda x: b64encode(x).decode()),
70
+ # RFC 7230, Section 3.2.6
71
+ "_header_name": st.text(
72
+ min_size=1, alphabet=st.sampled_from("!#$%&'*+-.^_`|~" + string.digits + string.ascii_letters)
73
+ ),
74
+ HEADER_FORMAT: header_value,
75
+ "_basic_auth": st.tuples(latin1_text, latin1_text).map(make_basic_auth_str),
76
+ "_bearer_auth": header_value.map("Bearer {}".format),
77
+ }
78
+
79
+
36
80
  register = register_string_format
37
81
  unregister = unregister_string_format
@@ -2,26 +2,31 @@
2
2
 
3
3
  Based on https://swagger.io/docs/specification/links/
4
4
  """
5
+
5
6
  from __future__ import annotations
7
+
6
8
  from dataclasses import dataclass, field
7
9
  from difflib import get_close_matches
8
- from typing import Any, Generator, NoReturn, Sequence, Union, TYPE_CHECKING
9
-
10
- from ...models import APIOperation, Case
11
- from ...parameters import ParameterSet
12
- from ...stateful import ParsedData, StatefulTest
13
- from ...stateful.state_machine import Direction
14
- from ...types import NotSet
10
+ from types import SimpleNamespace
11
+ from typing import TYPE_CHECKING, Any, Generator, Literal, NoReturn, Sequence, TypedDict, Union, cast
15
12
 
16
13
  from ...constants import NOT_SET
17
14
  from ...internal.copy import fast_deepcopy
15
+ from ...models import APIOperation, Case, TransitionId
16
+ from ...stateful import ParsedData, StatefulTest, UnresolvableLink
17
+ from ...stateful.state_machine import Direction
18
18
  from . import expressions
19
19
  from .constants import LOCATION_TO_CONTAINER
20
20
  from .parameters import OpenAPI20Body, OpenAPI30Body, OpenAPIParameter
21
-
21
+ from .references import RECURSION_DEPTH_LIMIT, Unresolvable
22
22
 
23
23
  if TYPE_CHECKING:
24
+ from hypothesis.vendor.pretty import RepresentationPrinter
25
+ from jsonschema import RefResolver
26
+
27
+ from ...parameters import ParameterSet
24
28
  from ...transports.responses import GenericResponse
29
+ from ...types import NotSet
25
30
 
26
31
 
27
32
  @dataclass(repr=False)
@@ -29,6 +34,7 @@ class Link(StatefulTest):
29
34
  operation: APIOperation
30
35
  parameters: dict[str, Any]
31
36
  request_body: Any = NOT_SET
37
+ merge_body: bool = True
32
38
 
33
39
  def __post_init__(self) -> None:
34
40
  if self.request_body is not NOT_SET and not self.operation.body:
@@ -48,6 +54,7 @@ class Link(StatefulTest):
48
54
  operation = source_operation.schema.get_operation_by_id(definition["operationId"]) # type: ignore
49
55
  else:
50
56
  operation = source_operation.schema.get_operation_by_reference(definition["operationRef"]) # type: ignore
57
+ extension = definition.get(SCHEMATHESIS_LINK_EXTENSION)
51
58
  return cls(
52
59
  # Pylint can't detect that the API operation is always defined at this point
53
60
  # E.g. if there is no matching operation or no operations at all, then a ValueError will be risen
@@ -55,21 +62,25 @@ class Link(StatefulTest):
55
62
  operation=operation,
56
63
  parameters=definition.get("parameters", {}),
57
64
  request_body=definition.get("requestBody", NOT_SET), # `None` might be a valid value - `null`
65
+ merge_body=extension.get("merge_body", True) if extension is not None else True,
58
66
  )
59
67
 
60
68
  def parse(self, case: Case, response: GenericResponse) -> ParsedData:
61
69
  """Parse data into a structure expected by links definition."""
62
70
  context = expressions.ExpressionContext(case=case, response=response)
63
- parameters = {
64
- parameter: expressions.evaluate(expression, context) for parameter, expression in self.parameters.items()
65
- }
66
- return ParsedData(
67
- parameters=parameters,
68
- # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#link-object
69
- # > A literal value or {expression} to use as a request body when calling the target operation.
70
- # In this case all literals will be passed as is, and expressions will be evaluated
71
- body=expressions.evaluate(self.request_body, context),
72
- )
71
+ parameters = {}
72
+ for parameter, expression in self.parameters.items():
73
+ evaluated = expressions.evaluate(expression, context)
74
+ if isinstance(evaluated, Unresolvable):
75
+ raise UnresolvableLink(f"Unresolvable reference in the link: {expression}")
76
+ parameters[parameter] = evaluated
77
+ body = expressions.evaluate(self.request_body, context, evaluate_nested=True)
78
+ if self.merge_body:
79
+ body = merge_body(case.body, body)
80
+ return ParsedData(parameters=parameters, body=body)
81
+
82
+ def is_match(self) -> bool:
83
+ return self.operation.schema.filter_set.match(SimpleNamespace(operation=self.operation))
73
84
 
74
85
  def make_operation(self, collected: list[ParsedData]) -> APIOperation:
75
86
  """Create a modified version of the original API operation with additional data merged in."""
@@ -152,17 +163,27 @@ class Link(StatefulTest):
152
163
 
153
164
  def get_links(response: GenericResponse, operation: APIOperation, field: str) -> Sequence[Link]:
154
165
  """Get `x-links` / `links` definitions from the schema."""
155
- responses = operation.definition.resolved["responses"]
166
+ responses = operation.definition.raw["responses"]
156
167
  if str(response.status_code) in responses:
157
- response_definition = responses[str(response.status_code)]
168
+ definition = responses[str(response.status_code)]
158
169
  elif response.status_code in responses:
159
- response_definition = responses[response.status_code]
170
+ definition = responses[response.status_code]
160
171
  else:
161
- response_definition = responses.get("default", {})
162
- links = response_definition.get(field, {})
172
+ definition = responses.get("default", {})
173
+ if not definition:
174
+ return []
175
+ _, definition = operation.schema.resolver.resolve_in_scope(definition, operation.definition.scope) # type: ignore[attr-defined]
176
+ links = definition.get(field, {})
163
177
  return [Link.from_definition(name, definition, operation) for name, definition in links.items()]
164
178
 
165
179
 
180
+ SCHEMATHESIS_LINK_EXTENSION = "x-schemathesis"
181
+
182
+
183
+ class SchemathesisLink(TypedDict):
184
+ merge_body: bool
185
+
186
+
166
187
  @dataclass(repr=False)
167
188
  class OpenAPILink(Direction):
168
189
  """Alternative approach to link processing.
@@ -174,41 +195,87 @@ class OpenAPILink(Direction):
174
195
  status_code: str
175
196
  definition: dict[str, Any]
176
197
  operation: APIOperation
177
- parameters: list[tuple[str | None, str, str]] = field(init=False)
198
+ parameters: list[tuple[Literal["path", "query", "header", "cookie", "body"] | None, str, str]] = field(init=False)
178
199
  body: dict[str, Any] | NotSet = field(init=False)
200
+ merge_body: bool = True
201
+
202
+ def __repr__(self) -> str:
203
+ path = self.operation.path
204
+ method = self.operation.method
205
+ return f"state.schema['{path}']['{method}'].links['{self.status_code}']['{self.name}']"
206
+
207
+ def _repr_pretty_(self, printer: RepresentationPrinter, cycle: bool) -> None:
208
+ return printer.text(repr(self))
179
209
 
180
210
  def __post_init__(self) -> None:
211
+ extension = self.definition.get(SCHEMATHESIS_LINK_EXTENSION)
181
212
  self.parameters = [
182
213
  normalize_parameter(parameter, expression)
183
214
  for parameter, expression in self.definition.get("parameters", {}).items()
184
215
  ]
185
216
  self.body = self.definition.get("requestBody", NOT_SET)
217
+ if extension is not None:
218
+ self.merge_body = extension.get("merge_body", True)
186
219
 
187
220
  def set_data(self, case: Case, elapsed: float, **kwargs: Any) -> None:
188
221
  """Assign all linked definitions to the new case instance."""
189
222
  context = kwargs["context"]
190
- self.set_parameters(case, context)
191
- self.set_body(case, context)
192
- case.set_source(context.response, context.case, elapsed)
223
+ overrides = self.set_parameters(case, context)
224
+ self.set_body(case, context, overrides)
225
+ overrides_all_parameters = True
226
+ if case.operation.body and "body" not in overrides.get("body", []):
227
+ overrides_all_parameters = False
228
+ if overrides_all_parameters:
229
+ for parameter in case.operation.iter_parameters():
230
+ if parameter.name not in overrides.get(parameter.location, []):
231
+ overrides_all_parameters = False
232
+ break
233
+ case.set_source(
234
+ context.response,
235
+ context.case,
236
+ elapsed,
237
+ overrides_all_parameters,
238
+ transition_id=TransitionId(
239
+ name=self.name,
240
+ status_code=self.status_code,
241
+ ),
242
+ )
193
243
 
194
- def set_parameters(self, case: Case, context: expressions.ExpressionContext) -> None:
244
+ def set_parameters(
245
+ self, case: Case, context: expressions.ExpressionContext
246
+ ) -> dict[Literal["path", "query", "header", "cookie", "body"], list[str]]:
247
+ overrides: dict[Literal["path", "query", "header", "cookie", "body"], list[str]] = {}
195
248
  for location, name, expression in self.parameters:
196
- container = get_container(case, location, name)
249
+ location, container = get_container(case, location, name)
197
250
  # Might happen if there is directly specified container,
198
251
  # but the schema has no parameters of such type at all.
199
252
  # Therefore the container is empty, otherwise it will be at least an empty object
200
253
  if container is None:
201
254
  message = f"No such parameter in `{case.operation.method.upper()} {case.operation.path}`: `{name}`."
202
- possibilities = [param.name for param in case.operation.definition.parameters]
255
+ possibilities = [param.name for param in case.operation.iter_parameters()]
203
256
  matches = get_close_matches(name, possibilities)
204
257
  if matches:
205
258
  message += f" Did you mean `{matches[0]}`?"
206
259
  raise ValueError(message)
207
- container[name] = expressions.evaluate(expression, context)
208
-
209
- def set_body(self, case: Case, context: expressions.ExpressionContext) -> None:
260
+ value = expressions.evaluate(expression, context)
261
+ if value is not None:
262
+ container[name] = value
263
+ overrides.setdefault(location, []).append(name)
264
+ return overrides
265
+
266
+ def set_body(
267
+ self,
268
+ case: Case,
269
+ context: expressions.ExpressionContext,
270
+ overrides: dict[Literal["path", "query", "header", "cookie", "body"], list[str]],
271
+ ) -> None:
210
272
  if self.body is not NOT_SET:
211
- case.body = expressions.evaluate(self.body, context)
273
+ evaluated = expressions.evaluate(self.body, context, evaluate_nested=True)
274
+ overrides["body"] = ["body"]
275
+ if self.merge_body:
276
+ case.body = merge_body(case.body, evaluated)
277
+ else:
278
+ case.body = evaluated
212
279
 
213
280
  def get_target_operation(self) -> APIOperation:
214
281
  if "operationId" in self.definition:
@@ -216,21 +283,32 @@ class OpenAPILink(Direction):
216
283
  return self.operation.schema.get_operation_by_reference(self.definition["operationRef"]) # type: ignore
217
284
 
218
285
 
219
- def get_container(case: Case, location: str | None, name: str) -> dict[str, Any] | None:
286
+ def merge_body(old: Any, new: Any) -> Any:
287
+ if isinstance(old, dict) and isinstance(new, dict):
288
+ return {**old, **new}
289
+ return new
290
+
291
+
292
+ def get_container(
293
+ case: Case, location: Literal["path", "query", "header", "cookie", "body"] | None, name: str
294
+ ) -> tuple[Literal["path", "query", "header", "cookie", "body"], dict[str, Any] | None]:
220
295
  """Get a container that suppose to store the given parameter."""
221
296
  if location:
222
297
  container_name = LOCATION_TO_CONTAINER[location]
223
298
  else:
224
- for param in case.operation.definition.parameters:
299
+ for param in case.operation.iter_parameters():
225
300
  if param.name == name:
301
+ location = param.location
226
302
  container_name = LOCATION_TO_CONTAINER[param.location]
227
303
  break
228
304
  else:
229
305
  raise ValueError(f"Parameter `{name}` is not defined in API operation `{case.operation.verbose_name}`")
230
- return getattr(case, container_name)
306
+ return location, getattr(case, container_name)
231
307
 
232
308
 
233
- def normalize_parameter(parameter: str, expression: str) -> tuple[str | None, str, str]:
309
+ def normalize_parameter(
310
+ parameter: str, expression: str
311
+ ) -> tuple[Literal["path", "query", "header", "cookie", "body"] | None, str, str]:
234
312
  """Normalize runtime expressions.
235
313
 
236
314
  Runtime expressions may have parameter names prefixed with their location - `path.id`.
@@ -240,13 +318,15 @@ def normalize_parameter(parameter: str, expression: str) -> tuple[str | None, st
240
318
  try:
241
319
  # The parameter name is prefixed with its location. Example: `path.id`
242
320
  location, name = tuple(parameter.split("."))
243
- return location, name, expression
321
+ _location = cast(Literal["path", "query", "header", "cookie", "body"], location)
322
+ return _location, name, expression
244
323
  except ValueError:
245
324
  return None, parameter, expression
246
325
 
247
326
 
248
327
  def get_all_links(operation: APIOperation) -> Generator[tuple[str, OpenAPILink], None, None]:
249
- for status_code, definition in operation.definition.resolved["responses"].items():
328
+ for status_code, definition in operation.definition.raw["responses"].items():
329
+ definition = operation.schema.resolver.resolve_all(definition, RECURSION_DEPTH_LIMIT - 8) # type: ignore[attr-defined]
250
330
  for name, link_definition in definition.get(operation.schema.links_field, {}).items(): # type: ignore
251
331
  yield status_code, OpenAPILink(name, status_code, link_definition, operation)
252
332
 
@@ -273,6 +353,7 @@ def _get_response_by_status_code(responses: dict[StatusCode, dict[str, Any]], st
273
353
 
274
354
 
275
355
  def add_link(
356
+ resolver: RefResolver,
276
357
  responses: dict[StatusCode, dict[str, Any]],
277
358
  links_field: str,
278
359
  parameters: dict[str, str] | None,
@@ -282,6 +363,8 @@ def add_link(
282
363
  name: str | None = None,
283
364
  ) -> None:
284
365
  response = _get_response_by_status_code(responses, status_code)
366
+ if "$ref" in response:
367
+ _, response = resolver.resolve(response["$ref"])
285
368
  links_definition = response.setdefault(links_field, {})
286
369
  new_link: dict[str, str | dict[str, str]] = {}
287
370
  if parameters is not None:
@@ -295,8 +378,8 @@ def add_link(
295
378
  name = name or f"{target.method.upper()} {target.path}"
296
379
  # operationId is a dict lookup which is more efficient than using `operationRef`, since it
297
380
  # doesn't involve reference resolving when we will look up for this target during testing.
298
- if "operationId" in target.definition.resolved:
299
- new_link["operationId"] = target.definition.resolved["operationId"]
381
+ if "operationId" in target.definition.raw:
382
+ new_link["operationId"] = target.definition.raw["operationId"]
300
383
  else:
301
384
  new_link["operationRef"] = target.operation_reference
302
385
  # The name is arbitrary, so we don't really case what it is,