Skip to content

Commit

Permalink
fix: Internal error on unresolvable Open API links
Browse files Browse the repository at this point in the history
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
  • Loading branch information
Stranger6667 committed May 11, 2024
1 parent 53bdd2f commit 2d2dc79
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 21 deletions.
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Changelog
:version:`Unreleased <v3.28.0...HEAD>` - TBD
--------------------------------------------

**Fixed**

- Internal error on unresolvable Open API links during stateful testing.

**Performance**

- Improve performance of ``add_link`` by avoiding unnecessary reference resolving.
Expand Down
33 changes: 17 additions & 16 deletions src/schemathesis/specs/openapi/links.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@
"""

from __future__ import annotations

from dataclasses import dataclass, field
from difflib import get_close_matches
from typing import Any, Generator, NoReturn, Sequence, Union, TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Generator, NoReturn, Sequence, Union

from ...constants import NOT_SET
from ...internal.copy import fast_deepcopy
from ...models import APIOperation, Case
from ...parameters import ParameterSet
from ...stateful import ParsedData, StatefulTest
from ...stateful import ParsedData, StatefulTest, UnresolvableLink
from ...stateful.state_machine import Direction
from ...types import NotSet

from ...constants import NOT_SET
from ...internal.copy import fast_deepcopy
from . import expressions
from .constants import LOCATION_TO_CONTAINER
from .parameters import OpenAPI20Body, OpenAPI30Body, OpenAPIParameter

from .references import Unresolvable

if TYPE_CHECKING:
from ...transports.responses import GenericResponse
Expand Down Expand Up @@ -61,16 +61,17 @@ def from_definition(cls, name: str, definition: dict[str, dict[str, Any]], sourc
def parse(self, case: Case, response: GenericResponse) -> ParsedData:
"""Parse data into a structure expected by links definition."""
context = expressions.ExpressionContext(case=case, response=response)
parameters = {
parameter: expressions.evaluate(expression, context) for parameter, expression in self.parameters.items()
}
return ParsedData(
parameters=parameters,
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#link-object
# > A literal value or {expression} to use as a request body when calling the target operation.
# In this case all literals will be passed as is, and expressions will be evaluated
body=expressions.evaluate(self.request_body, context),
)
parameters = {}
for parameter, expression in self.parameters.items():
evaluated = expressions.evaluate(expression, context)
if isinstance(evaluated, Unresolvable):
raise UnresolvableLink(f"Unresolvable reference in the link: {expression}")
parameters[parameter] = evaluated
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#link-object
# > A literal value or {expression} to use as a request body when calling the target operation.
# In this case all literals will be passed as is, and expressions will be evaluated
body = expressions.evaluate(self.request_body, context)
return ParsedData(parameters=parameters, body=body)

def make_operation(self, collected: list[ParsedData]) -> APIOperation:
"""Create a modified version of the original API operation with additional data merged in."""
Expand Down
3 changes: 1 addition & 2 deletions src/schemathesis/specs/openapi/stateful/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
from ....stateful.state_machine import APIStateMachine, Direction, StepResult
from ....utils import combine_strategies
from .. import expressions
from . import links
from .links import APIOperationConnections, Connection, _convert_strategy, apply, make_response_filter
from .links import APIOperationConnections, Connection, apply

if TYPE_CHECKING:
from ....models import APIOperation, Case
Expand Down
12 changes: 10 additions & 2 deletions src/schemathesis/stateful/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
from .state_machine import APIStateMachine


class UnresolvableLink(Exception):
"""Raised when a link cannot be resolved."""


@enum.unique
class Stateful(enum.Enum):
none = 1
Expand Down Expand Up @@ -70,8 +74,12 @@ def make_operation(self) -> APIOperation:

def store(self, case: Case, response: GenericResponse) -> None:
"""Parse and store data for a stateful test."""
parsed = self.stateful_test.parse(case, response)
self.container.append(parsed)
try:
parsed = self.stateful_test.parse(case, response)
self.container.append(parsed)
except UnresolvableLink:
# For now, ignore if a link cannot be resolved
pass


@dataclass
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Exit code: 0
---
Stdout:
======================= Schemathesis test session starts =======================
Schema location: file:///tmp/schema.json
Base URL: http://127.0.0.1/api
Specification version: Open API 3.0.2
Random seed: 42
Workers: 1
Collected API operations: 3
Collected API links: 2
API probing: SUCCESS
Schema analysis: SKIP

POST /api/users/ . [ 33%]
-> GET /api/users/{user_id} . [ 50%]
-> PATCH /api/users/{user_id} . [ 60%]
GET /api/users/{user_id} . [ 80%]
PATCH /api/users/{user_id} . [100%]

=================================== SUMMARY ====================================

Performed checks:
not_a_server_error 5 / 5 passed PASSED

Tip: Use the `--report` CLI option to visualize test results via Schemathesis.io.
We run additional conformance checks on reports from public repos.

============================== 5 passed in 1.00s ===============================
85 changes: 85 additions & 0 deletions test/cli/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1507,6 +1507,91 @@ def test_openapi_links_disabled(cli, schema_url, hypothesis_max_examples, snapsh
)


@pytest.mark.openapi_version("3.0")
@pytest.mark.operations("create_user", "get_user")
def test_unresolvable_links(cli, empty_open_api_3_schema, testdir, snapshot_cli, base_url):
empty_open_api_3_schema["paths"] = {
"/users/": {
"post": {
"requestBody": {
"required": True,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"first_name": {"type": "string", "minLength": 3},
"last_name": {"type": "string", "minLength": 3},
},
"required": ["first_name", "last_name"],
"additionalProperties": False,
}
}
},
},
"responses": {
"201": {
"description": "OK",
"links": {
"next": {
"operationId": "get_user",
"parameters": {"user_id": "$response.body#/invalid_value"},
},
"update": {
"operationId": "update_user",
"parameters": {"user_id": "$response.body#/id"},
"requestBody": {"first_name": "foo", "last_name": "bar"},
},
},
}
},
},
},
"/users/{user_id}": {
"parameters": [{"in": "path", "name": "user_id", "required": True, "schema": {"type": "string"}}],
"get": {
"operationId": "get_user",
"responses": {
"200": {
"description": "OK",
}
},
},
"patch": {
"operationId": "update_user",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"first_name": {"type": "string", "minLength": 3},
"last_name": {"type": "string", "minLength": 3},
},
"required": ["first_name", "last_name"],
"additionalProperties": False,
}
}
},
"required": True,
},
"responses": {"200": {"description": "OK"}, "404": {"description": "Not found"}},
},
},
}
schema_file = testdir.make_openapi_schema_file(empty_open_api_3_schema)
assert (
cli.run(
str(schema_file),
f"--base-url={base_url}",
"--hypothesis-max-examples=1",
"--show-trace",
"--validate-schema=true",
)
== snapshot_cli
)


@pytest.mark.parametrize("recursion_limit", (1, 5))
@pytest.mark.operations("create_user", "get_user", "update_user")
@flaky(max_runs=5, min_passes=1)
Expand Down
2 changes: 1 addition & 1 deletion test/specs/openapi/test_stateful.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ def test_trimmed_output(testdir, app_schema, base_url):
@pytest.mark.parametrize("method", ("requests", "werkzeug"))
@pytest.mark.openapi_version("3.0")
@pytest.mark.operations("create_user", "get_user", "update_user")
def test_history(testdir, app_schema, base_url, response_factory, method):
def test_history(app_schema, response_factory, method):
# When cases are serialized
schema = schemathesis.from_dict(app_schema)
first = schema["/users/"]["POST"].make_case(body={"first_name": "Foo", "last_name": "bar"})
Expand Down

0 comments on commit 2d2dc79

Please sign in to comment.