├── .gitignore
├── .gitlab-ci.yml
├── LICENSE
├── README.md
├── api.yaml
├── img
├── scan_result.png
└── slack_notification.png
├── openapi_security_scanner.py
├── requirements.txt
└── schemathesis
├── __init__.py
├── __init__.pyc
├── __pycache__
├── __init__.cpython-38.pyc
├── _compat.cpython-38.pyc
├── _hypothesis.cpython-38.pyc
├── checks.cpython-38.pyc
├── constants.cpython-38.pyc
├── exceptions.cpython-38.pyc
├── hooks.cpython-38.pyc
├── lazy.cpython-38.pyc
├── loaders.cpython-38.pyc
├── models.cpython-38.pyc
├── parameters.cpython-38.pyc
├── schemas.cpython-38.pyc
├── serializers.cpython-38.pyc
├── stateful.cpython-38.pyc
├── targets.cpython-38.pyc
├── types.cpython-38.pyc
└── utils.cpython-38.pyc
├── _compat.py
├── _hypothesis.py
├── checks.py
├── cli
├── __init__.py
├── __pycache__
│ ├── __init__.cpython-38.pyc
│ ├── callbacks.cpython-38.pyc
│ ├── cassettes.cpython-38.pyc
│ ├── constants.cpython-38.pyc
│ ├── context.cpython-38.pyc
│ ├── handlers.cpython-38.pyc
│ ├── junitxml.cpython-38.pyc
│ └── options.cpython-38.pyc
├── callbacks.py
├── cassettes.py
├── constants.py
├── context.py
├── handlers.py
├── junitxml.py
├── options.py
└── output
│ ├── __init__.py
│ ├── __pycache__
│ ├── __init__.cpython-38.pyc
│ ├── default.cpython-38.pyc
│ └── short.cpython-38.pyc
│ ├── default.py
│ └── short.py
├── constants.py
├── exceptions.py
├── extra
├── __init__.py
├── _aiohttp.py
├── _flask.py
├── _server.py
└── pytest_plugin.py
├── fixups
├── __init__.py
├── __pycache__
│ ├── __init__.cpython-38.pyc
│ └── fast_api.cpython-38.pyc
└── fast_api.py
├── hooks.py
├── lazy.py
├── loaders.py
├── models.py
├── parameters.py
├── py.typed
├── runner
├── __init__.py
├── __pycache__
│ ├── __init__.cpython-38.pyc
│ ├── events.cpython-38.pyc
│ └── serialization.cpython-38.pyc
├── events.py
├── impl
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-38.pyc
│ │ ├── core.cpython-38.pyc
│ │ ├── solo.cpython-38.pyc
│ │ └── threadpool.cpython-38.pyc
│ ├── core.py
│ ├── solo.py
│ └── threadpool.py
└── serialization.py
├── schemas.py
├── serializers.py
├── specs
├── __init__.py
├── __pycache__
│ └── __init__.cpython-38.pyc
├── graphql
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-38.pyc
│ │ ├── loaders.cpython-38.pyc
│ │ └── schemas.cpython-38.pyc
│ ├── loaders.py
│ └── schemas.py
└── openapi
│ ├── __init__.py
│ ├── __pycache__
│ ├── __init__.cpython-38.pyc
│ ├── _hypothesis.cpython-38.pyc
│ ├── checks.cpython-38.pyc
│ ├── constants.cpython-38.pyc
│ ├── converter.cpython-38.pyc
│ ├── definitions.cpython-38.pyc
│ ├── examples.cpython-38.pyc
│ ├── filters.cpython-38.pyc
│ ├── links.cpython-38.pyc
│ ├── parameters.cpython-38.pyc
│ ├── references.cpython-38.pyc
│ ├── schemas.cpython-38.pyc
│ ├── security.cpython-38.pyc
│ ├── serialization.cpython-38.pyc
│ └── utils.cpython-38.pyc
│ ├── _hypothesis.py
│ ├── checks.py
│ ├── constants.py
│ ├── converter.py
│ ├── definitions.py
│ ├── examples.py
│ ├── expressions
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-38.pyc
│ │ ├── context.cpython-38.pyc
│ │ ├── errors.cpython-38.pyc
│ │ ├── lexer.cpython-38.pyc
│ │ ├── nodes.cpython-38.pyc
│ │ ├── parser.cpython-38.pyc
│ │ └── pointers.cpython-38.pyc
│ ├── context.py
│ ├── errors.py
│ ├── lexer.py
│ ├── nodes.py
│ ├── parser.py
│ └── pointers.py
│ ├── filters.py
│ ├── links.py
│ ├── parameters.py
│ ├── references.py
│ ├── schemas.py
│ ├── security.py
│ ├── serialization.py
│ ├── stateful
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-38.pyc
│ │ └── links.cpython-38.pyc
│ └── links.py
│ └── utils.py
├── stateful.py
├── targets.py
├── types.py
└── utils.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | image: python:latest
2 |
3 | stages:
4 | - first
5 | - second
6 | - third
7 |
8 | download-old-result:
9 | except:
10 | - pushes
11 | stage: first
12 | allow_failure: true
13 | script:
14 | - 'curl --location --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" --output artifacts.zip "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/jobs/artifacts/master/download?job=get-new-result"'
15 | - file artifacts.zip
16 | - unzip artifacts.zip
17 | - mv sample.html old_sample.html
18 | artifacts:
19 | paths:
20 | - old_sample.html
21 |
22 | get-new-result:
23 | except:
24 | - pushes
25 | stage: second
26 | allow_failure: true
27 | script:
28 | - pip install -r requirements.txt
29 | - python openapi_security_scanner.py
30 | artifacts:
31 | paths:
32 | - sample.html
33 |
34 | compare:
35 | except:
36 | - pushes
37 | stage: third
38 | allow_failure: true
39 | script:
40 | - ls -asl
41 | - diff sample.html old_sample.html > changes.diff
42 | - if [[ -s changes.diff ]]; then echo "file has something, integrate a way to notify yourself"; else echo "file is empty"; fi
43 | artifacts:
44 | paths:
45 | - changes.diff
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
OpenAPI Security Scanner
2 |
3 | ## About The Project
4 |
5 | Authorization security issues in modern web applications could not be easily picked up by security scanners. A few examples from [Shopify](https://hackerone.com/reports/980511), [LINE](https://hackerone.com/reports/698579), [NordVPN](https://hackerone.com/reports/751577).
6 | One of the reasons is that the security scanners mainly looking for SQLi, XSS, RCE instead of looking for authorization security bugs, and the scanners usually do not have knowledge of how the web application is supposed to behave. That's why the majority of the authorization bugs now are mainly discovered manually.
7 | OpenAPI Security Scanner is created to help to discover authorization security issues with both automation and manual review. It behaves like an external unit test tool to make sure everything is behaving the way it should be.
8 |
9 | ### Built With
10 |
11 | * [Schemathesis](https://github.com/schemathesis/schemathesis)
12 |
13 |
14 | ## Getting Started
15 |
16 | Watch this [video](https://youtu.be/K65e5QRQ1tc) to follow along or keep reading.
17 |
18 | [Fork](https://gitlab.com/ngalog-gitlab/openapi_security_scanner/-/forks/new) or clone this project and follow the instructions below to get started.
19 |
20 | The workflow to use this scanner would look something like this:
21 |
22 | - Prepare the OpenAPI yaml file of the targeted API
23 | - Define the targeted endpoints and variables to be used for the API testing
24 | - Generate multiple sets of credentials from different users to make sure a variety of edge cases are covered while performing the API tests
25 | - After the configuration, start the runner and the scanning result could be found in the artifacts
26 | - Schedule the runner to run periodically and customize the way you want to receive the notifications whenever there's a change in the scanning result
27 |
28 | ### Advantages
29 | For developers, using this tool would add an extra layer of assurance that the API is behaving as expected from both internally and externally. And the scanner could be triggered upon every code deployment by triggering the pipeline https://docs.gitlab.com/ee/ci/triggers/.
30 |
31 | For bug bounty hunters, using this tool can ensure you can always stay on top of the targeted API, because we all know, even a specific endpoint wasn't vulnerable before, it could be vulnerable in the future because of various reasons. So it's easier to have the manual part of bug hunting to be automated using this tool.
32 |
33 | ## Warning Before Use
34 | Make sure you have the permission to scan the targeted API.
35 |
36 | ### Prerequisites
37 |
38 | You'll need an OpenAPI yaml file of the web application and replace it with the content of `api.yaml` in the repository.
39 |
40 |
41 | ## Usage
42 |
43 | Replace the content of `api.yaml` by target's OpenAPI yaml file, and go to GitLab project's `Settings` -> `CI/CD` -> Expand `Variables` and enter the variables in below formats.
44 |
45 |
46 | #### OPENAPI_CREDS
47 |
48 | The credentials provided here will be passed to the API calls in the form of `Authorization: Bearer` header format.
49 | It is recommended to prepare access tokens for at least two user accounts with different permissions, and you can also create more than one access token per user account to test for correct behaviours for different scoped access tokens.
50 |
51 | ```json
52 | {
53 | "":[
54 | {
55 | "scope":"",
56 | "access_token":"",
57 | "name":""
58 | },
59 | {
60 | "scope":"",
61 | "access_token":"",
62 | "name":""
63 | }
64 | ],
65 | "":[
66 | {
67 | "scope":"",
68 | "access_token":"",
69 | "name":""
70 | }
71 | ]
72 | }
73 | ```
74 |
75 | #### OPENAPI_ENDPOINTS
76 |
77 | The endpoints provided here should be `GET` based API calls, and you can also include the query in the JSON body. In below example, the endpoint is `/org/{orgs}/repos` and the query for it is `type=all`. For the endpoint `/repos/{owner}/{repo}`, there'll be no query.
78 |
79 | ```json
80 | {
81 | "/orgs/{org}/repos":{
82 | "type":"all"
83 | },
84 | "/repos/{owner}/{repo}":{
85 |
86 | }
87 | }
88 | ```
89 |
90 | #### OPENAPI_PATHS
91 |
92 | The path variables can be provided here, and the scanner will generate all combinations from values in the path variables.
93 |
94 | ```json
95 | {
96 | "org":[
97 | "test-org"
98 | ],
99 | "owner":[
100 | "ngalongc",
101 | "reconless"
102 | ],
103 | "repo":[
104 | "public-repo",
105 | "private-repo"
106 | ]
107 | }
108 | ```
109 |
110 | In this case, the following combination API calls are generated, and they will all be called by different set of credentials provided in OPENAPI_CREDS.
111 |
112 | ```http
113 | /orgs/test-org/repos?type=all
114 | /repos/ngalongc/public-repo
115 | /repos/ngalongc/private-repo
116 | /repos/reconless/public-repo
117 | /repos/reconless/private-repo
118 | ```
119 |
120 | #### PRIVATE_TOKEN
121 |
122 | Go to https://gitlab.com/-/profile/personal_access_tokens and create an `api` access token and replace the value in here. This GitLab access token is used for downloading the scan results from GitLab runner.
123 |
124 | ```
125 |
126 | ```
127 |
128 | #### OPENAPI_BASE_URL
129 |
130 | This should be the value of the base url of your API server
131 |
132 | ```
133 | https://example.com/api/v3
134 | ```
135 |
136 | ### Demo Running simple permission tests on GitHub API
137 |
138 | *Screenshot of the slack notification when API changes are detected*
139 |
140 |
141 |
142 | *Screenshot of the scanning result in artifacts*
143 |
144 |
145 |
146 | Sample report: https://ngalog-gitlab.gitlab.io/-/openapi_security_scanner/-/jobs/966841401/artifacts/sample.html
147 |
148 | ## Limitations
149 | - It can only test for `GET` based endpoints
150 | - For some API endpoints such as `/users/{user_id}/activities`, the responses are different everytime they are called, so detecting the changes of this kind of API is not meaningful
151 |
152 |
153 |
154 | ## Contributing
155 |
156 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.
157 |
158 | 1. Fork the Project
159 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
160 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
161 | 4. Push to the Branch (`git push origin feature/AmazingFeature`)
162 | 5. Open a Merge Request
163 |
164 |
165 | ## License
166 |
167 | Distributed under the MIT License. See `LICENSE` for more information.
168 |
169 |
170 | ## Contact
171 |
172 | Ron Chan - [@ngalongc](https://twitter.com/ngalongc)
173 |
174 | Project Link: [https://gitlab.com/ngalog-gitlab/openapi_security_scanner](https://gitlab.com/ngalog-gitlab/openapi_security_scanner)
175 |
176 |
177 |
178 |
--------------------------------------------------------------------------------
/img/scan_result.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/img/scan_result.png
--------------------------------------------------------------------------------
/img/slack_notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/img/slack_notification.png
--------------------------------------------------------------------------------
/openapi_security_scanner.py:
--------------------------------------------------------------------------------
1 | import schemathesis
2 | import json
3 | import os
4 | import requests
5 | import itertools
6 |
7 | def format_md_result(path,status_code,resp_len,scope,username,method):
8 | return path + divider + status_code + divider + resp_len + divider + scope + divider + username + divider + method + divider+"\n"
9 |
10 |
11 | def expand_paths_to_list(path_examples):
12 | keys = path_examples.keys()
13 | values = (path_examples[key] for key in keys)
14 | combinations = [dict(zip(keys, combination)) for combination in itertools.product(*values)]
15 | return combinations
16 |
17 |
18 | def main(path_examples):
19 | result_md = ""
20 | for targeted_endpoint in targeted_endpoints:
21 | for credential in credentials: # credentials =
22 | for cred_set in credentials[credential]:
23 | access_token = cred_set['access_token']
24 | case = schema[targeted_endpoint]["GET"].make_case(path_parameters=path_examples,query=targeted_endpoints[targeted_endpoint])
25 | response = case.call(headers={"Authorization":"Bearer " + access_token})
26 | single_result = format_md_result(
27 | status_code=str(response.status_code),
28 | path=case.formatted_path,
29 | resp_len=str(len(response.text)),
30 | scope=cred_set['scope'],
31 | username=credential, # snapmortgage
32 | method="GET"
33 | )
34 | result_md += single_result
35 | #targeted_endpoint = {"/orgs/{org}/repos":{"type":"all"}}
36 | return result_md
37 |
38 |
39 | ## Initiate the variables
40 | base_url = os.getenv('OPENAPI_BASE_URL')
41 | targeted_endpoints = json.loads(os.getenv('OPENAPI_ENDPOINTS'))
42 | path_examples = json.loads(os.getenv('OPENAPI_PATHS'))
43 | credentials = json.loads(os.getenv('OPENAPI_CREDS'))
44 |
45 | schema = schemathesis.from_path("api.yaml",base_url=base_url)
46 |
47 | divider = "|"
48 | md_header = "| - | Status | Length | Scope | Username | Method |\n" + "| - | - | - | - | - | -|\n"
49 |
50 |
51 | ## Make path_examples to become a list with all possible combinations
52 | path_examples = expand_paths_to_list(path_examples)
53 |
54 |
55 | for path_param in path_examples:
56 | md_header += main(path_param)
57 |
58 | result_response = requests.post(json={"text":md_header},url="https://gitlab.com/api/v4/markdown")
59 | result_json = json.loads(result_response.text)
60 | html = result_json['html']
61 |
62 | with open('sample.html',mode='w') as f:
63 | f.write(html)
64 |
65 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | attr==0.3.1
2 | attrs==20.3.0
3 | certifi==2020.12.5
4 | chardet==4.0.0
5 | click==7.1.2
6 | curlify==2.2.1
7 | graphql-core==3.1.2
8 | hypothesis==5.43.3
9 | hypothesis-graphql==0.3.2
10 | hypothesis-jsonschema==0.18.2
11 | idna==2.10
12 | iniconfig==1.1.1
13 | jsonschema==3.2.0
14 | junit-xml==1.9
15 | multidict==5.1.0
16 | packaging==20.8
17 | pluggy==0.13.1
18 | py==1.10.0
19 | pyparsing==2.4.7
20 | pyrsistent==0.17.3
21 | pytest==6.2.1
22 | pytest-subtests==0.4.0
23 | PyYAML==5.3.1
24 | requests==2.25.1
25 | six==1.15.0
26 | sortedcontainers==2.3.0
27 | starlette==0.14.1
28 | toml==0.10.2
29 | typing-extensions==3.7.4.3
30 | urllib3==1.26.2
31 | Werkzeug==1.0.1
32 | yarl==1.6.3
33 |
--------------------------------------------------------------------------------
/schemathesis/__init__.py:
--------------------------------------------------------------------------------
1 | from . import fixups, hooks, serializers, targets
2 | from .cli import register_check, register_target
3 | from .constants import DataGenerationMethod, __version__
4 | from .loaders import from_asgi, from_dict, from_file, from_path, from_pytest_fixture, from_uri, from_wsgi
5 | from .models import Case
6 | from .specs import graphql
7 | from .specs.openapi._hypothesis import init_default_strategies, register_string_format
8 | from .stateful import Stateful
9 | from .utils import GenericResponse
10 |
11 | init_default_strategies()
12 |
--------------------------------------------------------------------------------
/schemathesis/__init__.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/__init__.pyc
--------------------------------------------------------------------------------
/schemathesis/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/__pycache__/_compat.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/__pycache__/_compat.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/__pycache__/_hypothesis.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/__pycache__/_hypothesis.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/__pycache__/checks.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/__pycache__/checks.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/__pycache__/constants.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/__pycache__/constants.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/__pycache__/exceptions.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/__pycache__/exceptions.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/__pycache__/hooks.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/__pycache__/hooks.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/__pycache__/lazy.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/__pycache__/lazy.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/__pycache__/loaders.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/__pycache__/loaders.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/__pycache__/models.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/__pycache__/models.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/__pycache__/parameters.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/__pycache__/parameters.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/__pycache__/schemas.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/__pycache__/schemas.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/__pycache__/serializers.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/__pycache__/serializers.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/__pycache__/stateful.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/__pycache__/stateful.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/__pycache__/targets.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/__pycache__/targets.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/__pycache__/types.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/__pycache__/types.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/__pycache__/utils.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/__pycache__/utils.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/_compat.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=unused-import
2 | try:
3 | from importlib import metadata
4 | except ImportError:
5 | import importlib_metadata as metadata # type: ignore
6 |
--------------------------------------------------------------------------------
/schemathesis/_hypothesis.py:
--------------------------------------------------------------------------------
1 | """Provide strategies for given endpoint(s) definition."""
2 | import asyncio
3 | from typing import Any, Callable, Dict, List, Optional, Tuple, Union
4 |
5 | import hypothesis
6 | from hypothesis import strategies as st
7 | from hypothesis.strategies import SearchStrategy
8 | from hypothesis.utils.conventions import InferType
9 |
10 | from .constants import DEFAULT_DEADLINE, DataGenerationMethod
11 | from .exceptions import InvalidSchema
12 | from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher
13 | from .models import Case, Endpoint
14 | from .stateful import Feedback, Stateful
15 |
16 | GivenInput = Union[SearchStrategy, InferType]
17 |
18 |
19 | def create_test(
20 | *,
21 | endpoint: Endpoint,
22 | test: Callable,
23 | settings: Optional[hypothesis.settings] = None,
24 | seed: Optional[int] = None,
25 | data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
26 | _given_args: Tuple[GivenInput, ...] = (),
27 | _given_kwargs: Optional[Dict[str, GivenInput]] = None,
28 | ) -> Callable:
29 | """Create a Hypothesis test."""
30 | hook_dispatcher = getattr(test, "_schemathesis_hooks", None)
31 | feedback: Optional[Feedback]
32 | if endpoint.schema.stateful == Stateful.links:
33 | feedback = Feedback(endpoint.schema.stateful, endpoint)
34 | else:
35 | feedback = None
36 | strategy = endpoint.as_strategy(
37 | hooks=hook_dispatcher, feedback=feedback, data_generation_method=data_generation_method
38 | )
39 | _given_kwargs = (_given_kwargs or {}).copy()
40 | _given_kwargs.setdefault("case", strategy)
41 | wrapped_test = hypothesis.given(*_given_args, **_given_kwargs)(test)
42 | if seed is not None:
43 | wrapped_test = hypothesis.seed(seed)(wrapped_test)
44 | if asyncio.iscoroutinefunction(test):
45 | wrapped_test.hypothesis.inner_test = make_async_test(test) # type: ignore
46 | setup_default_deadline(wrapped_test)
47 | if settings is not None:
48 | wrapped_test = settings(wrapped_test)
49 | wrapped_test._schemathesis_feedback = feedback # type: ignore
50 | return add_examples(wrapped_test, endpoint, hook_dispatcher=hook_dispatcher)
51 |
52 |
53 | def setup_default_deadline(wrapped_test: Callable) -> None:
54 | # Quite hacky, but it is the simplest way to set up the default deadline value without affecting non-Schemathesis
55 | # tests globally
56 | existing_settings = getattr(wrapped_test, "_hypothesis_internal_use_settings", None)
57 | if existing_settings is not None and existing_settings.deadline == hypothesis.settings.default.deadline:
58 | new_settings = hypothesis.settings(existing_settings, deadline=DEFAULT_DEADLINE)
59 | wrapped_test._hypothesis_internal_use_settings = new_settings # type: ignore
60 |
61 |
62 | def make_async_test(test: Callable) -> Callable:
63 | def async_run(*args: Any, **kwargs: Any) -> None:
64 | loop = asyncio.get_event_loop()
65 | coro = test(*args, **kwargs)
66 | future = asyncio.ensure_future(coro, loop=loop)
67 | loop.run_until_complete(future)
68 |
69 | return async_run
70 |
71 |
72 | def add_examples(test: Callable, endpoint: Endpoint, hook_dispatcher: Optional[HookDispatcher] = None) -> Callable:
73 | """Add examples to the Hypothesis test, if they are specified in the schema."""
74 | try:
75 | examples: List[Case] = [get_single_example(strategy) for strategy in endpoint.get_strategies_from_examples()]
76 | except InvalidSchema:
77 | # In this case, the user didn't pass `--validate-schema=false` and see an error in the output anyway,
78 | # and no tests will be executed. For this reason, examples can be skipped
79 | return test
80 | context = HookContext(endpoint) # context should be passed here instead
81 | GLOBAL_HOOK_DISPATCHER.dispatch("before_add_examples", context, examples)
82 | endpoint.schema.hooks.dispatch("before_add_examples", context, examples)
83 | if hook_dispatcher:
84 | hook_dispatcher.dispatch("before_add_examples", context, examples)
85 | for example in examples:
86 | test = hypothesis.example(case=example)(test)
87 | return test
88 |
89 |
90 | def get_single_example(strategy: st.SearchStrategy[Case]) -> Case:
91 | @hypothesis.given(strategy) # type: ignore
92 | @hypothesis.settings( # type: ignore
93 | database=None,
94 | max_examples=1,
95 | deadline=None,
96 | verbosity=hypothesis.Verbosity.quiet,
97 | phases=(hypothesis.Phase.generate,),
98 | suppress_health_check=hypothesis.HealthCheck.all(),
99 | )
100 | def example_generating_inner_function(ex: Case) -> None:
101 | examples.append(ex)
102 |
103 | examples: List[Case] = []
104 | example_generating_inner_function()
105 | return examples[0]
106 |
--------------------------------------------------------------------------------
/schemathesis/checks.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING, Optional, Tuple
2 |
3 | from .exceptions import get_status_code_error
4 | from .specs.openapi.checks import (
5 | content_type_conformance,
6 | response_headers_conformance,
7 | response_schema_conformance,
8 | status_code_conformance,
9 | )
10 | from .utils import GenericResponse
11 |
12 | if TYPE_CHECKING:
13 | from .models import Case, CheckFunction
14 |
15 |
16 | def not_a_server_error(response: GenericResponse, case: "Case") -> Optional[bool]: # pylint: disable=useless-return
17 | """A check to verify that the response is not a server-side error."""
18 | if response.status_code >= 500:
19 | exc_class = get_status_code_error(response.status_code)
20 | raise exc_class(f"Received a response with 5xx status code: {response.status_code}")
21 | return None
22 |
23 |
24 | DEFAULT_CHECKS: Tuple["CheckFunction", ...] = (not_a_server_error,)
25 | OPTIONAL_CHECKS = (
26 | status_code_conformance,
27 | content_type_conformance,
28 | response_headers_conformance,
29 | response_schema_conformance,
30 | )
31 | ALL_CHECKS: Tuple["CheckFunction", ...] = DEFAULT_CHECKS + OPTIONAL_CHECKS
32 |
--------------------------------------------------------------------------------
/schemathesis/cli/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/cli/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/cli/__pycache__/callbacks.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/cli/__pycache__/callbacks.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/cli/__pycache__/cassettes.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/cli/__pycache__/cassettes.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/cli/__pycache__/constants.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/cli/__pycache__/constants.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/cli/__pycache__/context.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/cli/__pycache__/context.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/cli/__pycache__/handlers.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/cli/__pycache__/handlers.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/cli/__pycache__/junitxml.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/cli/__pycache__/junitxml.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/cli/__pycache__/options.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/cli/__pycache__/options.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/cli/callbacks.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | from contextlib import contextmanager
4 | from typing import Dict, Generator, Optional, Tuple, Union
5 | from urllib.parse import urlparse
6 |
7 | import click
8 | import hypothesis
9 | from requests import PreparedRequest, RequestException
10 |
11 | from .. import utils
12 | from ..stateful import Stateful
13 | from .constants import DEFAULT_WORKERS
14 |
15 |
16 | def validate_schema(ctx: click.core.Context, param: click.core.Parameter, raw_value: str) -> str:
17 | if "app" not in ctx.params:
18 | try:
19 | netloc = urlparse(raw_value).netloc
20 | except ValueError as exc:
21 | raise click.UsageError("Invalid SCHEMA, must be a valid URL or file path.") from exc
22 | if not netloc:
23 | if "\x00" in raw_value or not utils.file_exists(raw_value):
24 | raise click.UsageError("Invalid SCHEMA, must be a valid URL or file path.")
25 | if "base_url" not in ctx.params:
26 | raise click.UsageError('Missing argument, "--base-url" is required for SCHEMA specified by file.')
27 | else:
28 | _validate_url(raw_value)
29 | return raw_value
30 |
31 |
32 | def _validate_url(value: str) -> None:
33 | try:
34 | PreparedRequest().prepare_url(value, {}) # type: ignore
35 | except RequestException as exc:
36 | raise click.UsageError("Invalid SCHEMA, must be a valid URL or file path.") from exc
37 |
38 |
39 | def validate_base_url(ctx: click.core.Context, param: click.core.Parameter, raw_value: str) -> str:
40 | try:
41 | netloc = urlparse(raw_value).netloc
42 | except ValueError as exc:
43 | raise click.UsageError("Invalid base URL") from exc
44 | if raw_value and not netloc:
45 | raise click.UsageError("Invalid base URL")
46 | return raw_value
47 |
48 |
49 | APPLICATION_FORMAT_MESSAGE = (
50 | "Can not import application from the given module!\n"
51 | "The `--app` option value should be in format:\n\n path:variable\n\n"
52 | "where `path` is an importable path to a Python module,\n"
53 | "and `variable` is a variable name inside that module."
54 | )
55 |
56 |
57 | def validate_app(ctx: click.core.Context, param: click.core.Parameter, raw_value: Optional[str]) -> Optional[str]:
58 | if raw_value is None:
59 | return raw_value
60 | try:
61 | utils.import_app(raw_value)
62 | # String is returned instead of an app because it might be passed to a subprocess
63 | # Since most app instances are not-transferable to another process, they are passed as strings and
64 | # imported in a subprocess
65 | return raw_value
66 | except Exception as exc:
67 | show_errors_tracebacks = ctx.params["show_errors_tracebacks"]
68 | message = utils.format_exception(exc, show_errors_tracebacks).strip()
69 | click.secho(f"{APPLICATION_FORMAT_MESSAGE}\n\nException:\n\n{message}", fg="red")
70 | if not show_errors_tracebacks:
71 | click.secho(
72 | "\nAdd this option to your command line parameters to see full tracebacks: --show-errors-tracebacks",
73 | fg="red",
74 | )
75 | raise click.exceptions.Exit(1)
76 |
77 |
78 | def validate_auth(
79 | ctx: click.core.Context, param: click.core.Parameter, raw_value: Optional[str]
80 | ) -> Optional[Tuple[str, str]]:
81 | if raw_value is not None:
82 | with reraise_format_error(raw_value):
83 | user, password = tuple(raw_value.split(":"))
84 | if not user:
85 | raise click.BadParameter("Username should not be empty")
86 | if not utils.is_latin_1_encodable(user):
87 | raise click.BadParameter("Username should be latin-1 encodable")
88 | if not utils.is_latin_1_encodable(password):
89 | raise click.BadParameter("Password should be latin-1 encodable")
90 | return user, password
91 | return None
92 |
93 |
94 | def validate_headers(
95 | ctx: click.core.Context, param: click.core.Parameter, raw_value: Tuple[str, ...]
96 | ) -> Dict[str, str]:
97 | headers = {}
98 | for header in raw_value:
99 | with reraise_format_error(header):
100 | key, value = header.split(":", maxsplit=1)
101 | value = value.lstrip()
102 | key = key.strip()
103 | if not key:
104 | raise click.BadParameter("Header name should not be empty")
105 | if not utils.is_latin_1_encodable(key):
106 | raise click.BadParameter("Header name should be latin-1 encodable")
107 | if not utils.is_latin_1_encodable(value):
108 | raise click.BadParameter("Header value should be latin-1 encodable")
109 | if utils.has_invalid_characters(key, value):
110 | raise click.BadParameter("Invalid return character or leading space in header")
111 | headers[key] = value
112 | return headers
113 |
114 |
115 | def validate_regex(ctx: click.core.Context, param: click.core.Parameter, raw_value: Tuple[str, ...]) -> Tuple[str, ...]:
116 | for value in raw_value:
117 | try:
118 | re.compile(value)
119 | except (re.error, OverflowError, RuntimeError) as exc:
120 | raise click.BadParameter(f"Invalid regex: {exc.args[0]}")
121 | return raw_value
122 |
123 |
124 | def convert_verbosity(
125 | ctx: click.core.Context, param: click.core.Parameter, value: Optional[str]
126 | ) -> Optional[hypothesis.Verbosity]:
127 | if value is None:
128 | return value
129 | return hypothesis.Verbosity[value]
130 |
131 |
132 | def convert_stateful(ctx: click.core.Context, param: click.core.Parameter, value: Optional[str]) -> Optional[Stateful]:
133 | if value is None:
134 | return value
135 | return Stateful[value]
136 |
137 |
138 | def convert_request_tls_verify(ctx: click.core.Context, param: click.core.Parameter, value: str) -> Union[str, bool]:
139 | if value.lower() in ("y", "yes", "t", "true", "on", "1"):
140 | return True
141 | if value.lower() in ("n", "no", "f", "false", "off", "0"):
142 | return False
143 | return value
144 |
145 |
146 | @contextmanager
147 | def reraise_format_error(raw_value: str) -> Generator[None, None, None]:
148 | try:
149 | yield
150 | except ValueError as exc:
151 | raise click.BadParameter(f"Should be in KEY:VALUE format. Got: {raw_value}") from exc
152 |
153 |
154 | def get_workers_count() -> int:
155 | """Detect the number of available CPUs for the current process, if possible.
156 |
157 | Use ``DEFAULT_WORKERS`` if not possible to detect.
158 | """
159 | if hasattr(os, "sched_getaffinity"):
160 | # In contrast with `os.cpu_count` this call respects limits on CPU resources on some Unix systems
161 | return len(os.sched_getaffinity(0))
162 | # Number of CPUs in the system, or 1 if undetermined
163 | return os.cpu_count() or DEFAULT_WORKERS
164 |
165 |
166 | def convert_workers(ctx: click.core.Context, param: click.core.Parameter, value: str) -> int:
167 | if value == "auto":
168 | return get_workers_count()
169 | return int(value)
170 |
--------------------------------------------------------------------------------
/schemathesis/cli/cassettes.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import json
3 | import re
4 | import sys
5 | import threading
6 | from queue import Queue
7 | from typing import Any, Dict, Generator, Iterator, List, Optional, cast
8 |
9 | import attr
10 | import click
11 | import requests
12 | from requests.cookies import RequestsCookieJar
13 | from requests.structures import CaseInsensitiveDict
14 |
15 | from .. import constants
16 | from ..runner import events
17 | from ..runner.serialization import SerializedCheck, SerializedInteraction
18 | from .context import ExecutionContext
19 | from .handlers import EventHandler
20 |
21 | # Wait until the worker terminates
22 | WRITER_WORKER_JOIN_TIMEOUT = 1
23 |
24 |
25 | @attr.s(slots=True) # pragma: no mutate
26 | class CassetteWriter(EventHandler):
27 | """Write interactions in a YAML cassette.
28 |
29 | A low-level interface is used to write data to YAML file during the test run and reduce the delay at
30 | the end of the test run.
31 | """
32 |
33 | file_handle: click.utils.LazyFile = attr.ib() # pragma: no mutate
34 | queue: Queue = attr.ib(factory=Queue) # pragma: no mutate
35 | worker: threading.Thread = attr.ib(init=False) # pragma: no mutate
36 |
37 | def __attrs_post_init__(self) -> None:
38 | self.worker = threading.Thread(target=worker, kwargs={"file_handle": self.file_handle, "queue": self.queue})
39 | self.worker.start()
40 |
41 | def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
42 | if isinstance(event, events.Initialized):
43 | # In the beginning we write metadata and start `http_interactions` list
44 | self.queue.put(Initialize())
45 | if isinstance(event, events.AfterExecution):
46 | # Seed is always present at this point, the original Optional[int] type is there because `TestResult`
47 | # instance is created before `seed` is generated on the hypothesis side
48 | seed = cast(int, event.result.seed)
49 | self.queue.put(
50 | Process(
51 | seed=seed,
52 | interactions=event.result.interactions,
53 | )
54 | )
55 | if isinstance(event, events.Finished):
56 | self.shutdown()
57 |
58 | def shutdown(self) -> None:
59 | self.queue.put(Finalize())
60 | self._stop_worker()
61 |
62 | def _stop_worker(self) -> None:
63 | self.worker.join(WRITER_WORKER_JOIN_TIMEOUT)
64 |
65 |
66 | @attr.s(slots=True) # pragma: no mutate
67 | class Initialize:
68 | """Start up, the first message to make preparations before proceeding the input data."""
69 |
70 |
71 | @attr.s(slots=True) # pragma: no mutate
72 | class Process:
73 | """A new chunk of data should be processed."""
74 |
75 | seed: int = attr.ib() # pragma: no mutate
76 | interactions: List[SerializedInteraction] = attr.ib() # pragma: no mutate
77 |
78 |
79 | @attr.s(slots=True) # pragma: no mutate
80 | class Finalize:
81 | """The work is done and there will be no more messages to process."""
82 |
83 |
84 | def get_command_representation() -> str:
85 | """Get how Schemathesis was run."""
86 | # It is supposed to be executed from Schemathesis CLI, not via Click's `command.invoke`
87 | if not sys.argv[0].endswith("schemathesis"):
88 | return ""
89 | args = " ".join(sys.argv[1:])
90 | return f"schemathesis {args}"
91 |
92 |
93 | def worker(file_handle: click.utils.LazyFile, queue: Queue) -> None:
94 | """Write YAML to a file in an incremental manner.
95 |
96 | This implementation doesn't use `pyyaml` package and composes YAML manually as string due to the following reasons:
97 | - It is much faster. The string-based approach gives only ~2.5% time overhead when `yaml.CDumper` has ~11.2%;
98 | - Implementation complexity. We have a quite simple format where all values are strings, and it is much simpler to
99 | implement it with string composition rather than with adjusting `yaml.Serializer` to emit explicit types.
100 | Another point is that with `pyyaml` we need to emit events and handle some low-level details like providing
101 | tags, anchors to have incremental writing, with strings it is much simpler.
102 | """
103 | current_id = 1
104 | stream = file_handle.open()
105 |
106 | def format_header_values(values: List[str]) -> str:
107 | return "\n".join(f" - {json.dumps(v)}" for v in values)
108 |
109 | def format_headers(headers: Dict[str, List[str]]) -> str:
110 | return "\n".join(f" {name}:\n{format_header_values(values)}" for name, values in headers.items())
111 |
112 | def format_check_message(message: Optional[str]) -> str:
113 | return "~" if message is None else f"{repr(message)}"
114 |
115 | def format_checks(checks: List[SerializedCheck]) -> str:
116 | return "\n".join(
117 | f" - name: '{check.name}'\n status: '{check.value.name.upper()}'\n message: {format_check_message(check.message)}"
118 | for check in checks
119 | )
120 |
121 | while True:
122 | item = queue.get()
123 | if isinstance(item, Initialize):
124 | stream.write(
125 | f"""command: '{get_command_representation()}'
126 | recorded_with: 'Schemathesis {constants.__version__}'
127 | http_interactions:"""
128 | )
129 | elif isinstance(item, Process):
130 | for interaction in item.interactions:
131 | status = interaction.status.name.upper()
132 | stream.write(
133 | f"""\n- id: '{current_id}'
134 | status: '{status}'
135 | seed: '{item.seed}'
136 | elapsed: '{interaction.response.elapsed}'
137 | recorded_at: '{interaction.recorded_at}'
138 | checks:
139 | {format_checks(interaction.checks)}
140 | request:
141 | uri: '{interaction.request.uri}'
142 | method: '{interaction.request.method}'
143 | headers:
144 | {format_headers(interaction.request.headers)}
145 | body:
146 | encoding: 'utf-8'
147 | base64_string: '{interaction.request.body}'
148 | response:
149 | status:
150 | code: '{interaction.response.status_code}'
151 | message: {json.dumps(interaction.response.message)}
152 | headers:
153 | {format_headers(interaction.response.headers)}
154 | body:
155 | encoding: '{interaction.response.encoding}'
156 | base64_string: '{interaction.response.body}'
157 | http_version: '{interaction.response.http_version}'"""
158 | )
159 | current_id += 1
160 | else:
161 | break
162 | file_handle.close()
163 |
164 |
165 | @attr.s(slots=True) # pragma: no mutate
166 | class Replayed:
167 | interaction: Dict[str, Any] = attr.ib() # pragma: no mutate
168 | response: requests.Response = attr.ib() # pragma: no mutate
169 |
170 |
171 | def replay(
172 | cassette: Dict[str, Any],
173 | id_: Optional[str] = None,
174 | status: Optional[str] = None,
175 | uri: Optional[str] = None,
176 | method: Optional[str] = None,
177 | ) -> Generator[Replayed, None, None]:
178 | """Replay saved interactions."""
179 | session = requests.Session()
180 | for interaction in filter_cassette(cassette["http_interactions"], id_, status, uri, method):
181 | request = get_prepared_request(interaction["request"])
182 | response = session.send(request) # type: ignore
183 | yield Replayed(interaction, response)
184 |
185 |
186 | def filter_cassette(
187 | interactions: List[Dict[str, Any]],
188 | id_: Optional[str] = None,
189 | status: Optional[str] = None,
190 | uri: Optional[str] = None,
191 | method: Optional[str] = None,
192 | ) -> Iterator[Dict[str, Any]]:
193 |
194 | filters = []
195 |
196 | def id_filter(item: Dict[str, Any]) -> bool:
197 | return item["id"] == id_
198 |
199 | def status_filter(item: Dict[str, Any]) -> bool:
200 | status_ = cast(str, status)
201 | return item["status"].upper() == status_.upper()
202 |
203 | def uri_filter(item: Dict[str, Any]) -> bool:
204 | uri_ = cast(str, uri)
205 | return bool(re.search(uri_, item["request"]["uri"]))
206 |
207 | def method_filter(item: Dict[str, Any]) -> bool:
208 | method_ = cast(str, method)
209 | return bool(re.search(method_, item["request"]["method"]))
210 |
211 | if id_ is not None:
212 | filters.append(id_filter)
213 |
214 | if status is not None:
215 | filters.append(status_filter)
216 |
217 | if uri is not None:
218 | filters.append(uri_filter)
219 |
220 | if method is not None:
221 | filters.append(method_filter)
222 |
223 | def is_match(interaction: Dict[str, Any]) -> bool:
224 | return all(filter_(interaction) for filter_ in filters)
225 |
226 | return filter(is_match, interactions)
227 |
228 |
229 | def get_prepared_request(data: Dict[str, Any]) -> requests.PreparedRequest:
230 | """Create a `requests.PreparedRequest` from a serialized one."""
231 | prepared = requests.PreparedRequest()
232 | prepared.method = data["method"]
233 | prepared.url = data["uri"]
234 | prepared._cookies = RequestsCookieJar() # type: ignore
235 | encoded = data["body"]["base64_string"]
236 | if encoded:
237 | prepared.body = base64.b64decode(encoded)
238 | # There is always 1 value in a request
239 | headers = [(key, value[0]) for key, value in data["headers"].items()]
240 | prepared.headers = CaseInsensitiveDict(headers)
241 | return prepared
242 |
--------------------------------------------------------------------------------
/schemathesis/cli/constants.py:
--------------------------------------------------------------------------------
1 | MIN_WORKERS = 1
2 | DEFAULT_WORKERS = MIN_WORKERS
3 | MAX_WORKERS = 64
4 |
--------------------------------------------------------------------------------
/schemathesis/cli/context.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | from typing import List, Optional
4 |
5 | import attr
6 |
7 | from ..runner.serialization import SerializedTestResult
8 |
9 |
10 | @attr.s(slots=True) # pragma: no mutate
11 | class ExecutionContext:
12 | """Storage for the current context of the execution."""
13 |
14 | hypothesis_output: List[str] = attr.ib(factory=list) # pragma: no mutate
15 | workers_num: int = attr.ib(default=1) # pragma: no mutate
16 | show_errors_tracebacks: bool = attr.ib(default=False) # pragma: no mutate
17 | endpoints_processed: int = attr.ib(default=0) # pragma: no mutate
18 | # It is set in runtime, from a `Initialized` event
19 | endpoints_count: Optional[int] = attr.ib(default=None) # pragma: no mutate
20 | current_line_length: int = attr.ib(default=0) # pragma: no mutate
21 | terminal_size: os.terminal_size = attr.ib(factory=shutil.get_terminal_size) # pragma: no mutate
22 | results: List[SerializedTestResult] = attr.ib(factory=list) # pragma: no mutate
23 | cassette_file_name: Optional[str] = attr.ib(default=None) # pragma: no mutate
24 | junit_xml_file: Optional[str] = attr.ib(default=None) # pragma: no mutate
25 | verbosity: int = attr.ib(default=0) # pragma: no mutate
26 |
--------------------------------------------------------------------------------
/schemathesis/cli/handlers.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional, Set, Tuple
2 |
3 | from ..models import Status
4 | from ..runner import events
5 | from ..runner.serialization import SerializedCheck
6 | from .context import ExecutionContext
7 |
8 |
9 | class EventHandler:
10 | def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
11 | raise NotImplementedError
12 |
13 | def shutdown(self) -> None:
14 | # Do nothing by default
15 | pass
16 |
17 |
18 | def get_unique_failures(checks: List[SerializedCheck]) -> List[SerializedCheck]:
19 | """Return only unique checks that should be displayed in the output."""
20 | seen: Set[Tuple[str, Optional[str]]] = set()
21 | unique_checks = []
22 | for check in reversed(checks):
23 | # There are also could be checks that didn't fail
24 | if check.example is not None and check.value == Status.failure and (check.name, check.message) not in seen:
25 | unique_checks.append(check)
26 | seen.add((check.name, check.message))
27 | return unique_checks
28 |
--------------------------------------------------------------------------------
/schemathesis/cli/junitxml.py:
--------------------------------------------------------------------------------
1 | import platform
2 | from typing import List, Optional
3 |
4 | import attr
5 | from click.utils import LazyFile
6 | from junit_xml import TestCase, TestSuite, to_xml_report_file
7 |
8 | from ..models import Status
9 | from ..runner import events
10 | from .handlers import EventHandler, ExecutionContext, get_unique_failures
11 |
12 |
13 | @attr.s(slots=True) # pragma: no mutate
14 | class JunitXMLHandler(EventHandler):
15 | file_handle: LazyFile = attr.ib() # pragma: no mutate
16 | test_cases: List = attr.ib(factory=list) # pragma: no mutate
17 | start_time: Optional[float] = attr.ib(default=None) # pragma: no mutate
18 |
19 | def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
20 | if isinstance(event, events.Initialized):
21 | self.start_time = event.start_time
22 | if isinstance(event, events.AfterExecution):
23 | test_case = TestCase(
24 | f"{event.result.method} {event.result.path}",
25 | elapsed_sec=event.elapsed_time,
26 | allow_multiple_subelements=True,
27 | )
28 | if event.status == Status.failure:
29 | checks = get_unique_failures(event.result.checks)
30 | for idx, check in enumerate(checks, 1):
31 | # `check.message` is always not empty for events with `failure` status
32 | test_case.add_failure_info(message=f"{idx}. {check.message}")
33 | if event.status == Status.error:
34 | test_case.add_error_info(
35 | message=event.result.errors[-1].exception, output=event.result.errors[-1].exception_with_traceback
36 | )
37 | self.test_cases.append(test_case)
38 | if isinstance(event, events.Finished):
39 | test_suites = [TestSuite("schemathesis", test_cases=self.test_cases, hostname=platform.node())]
40 | to_xml_report_file(file_descriptor=self.file_handle, test_suites=test_suites, prettyprint=True)
41 |
--------------------------------------------------------------------------------
/schemathesis/cli/options.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from typing import Any, List, Optional, Type, Union
3 |
4 | import click
5 |
6 | from ..types import NotSet
7 |
8 |
9 | class CustomHelpMessageChoice(click.Choice):
10 | """Allows you to customize how choices are displayed in the help message."""
11 |
12 | def __init__(self, *args: Any, choices_repr: str, **kwargs: Any):
13 | super().__init__(*args, **kwargs)
14 | self.choices_repr = choices_repr
15 |
16 | def get_metavar(self, param: click.Parameter) -> str:
17 | return self.choices_repr
18 |
19 |
20 | class CSVOption(click.Choice):
21 | def __init__(self, choices: Type[Enum]):
22 | self.enum = choices
23 | super().__init__(tuple(choices.__members__))
24 |
25 | def convert(
26 | self, value: str, param: Optional[click.core.Parameter], ctx: Optional[click.core.Context]
27 | ) -> List[Enum]:
28 | items = [item for item in value.split(",") if item]
29 | invalid_options = set(items) - set(self.choices)
30 | if not invalid_options and items:
31 | return [self.enum[item] for item in items]
32 | # Sort to keep the error output consistent with the passed values
33 | sorted_options = ", ".join(sorted(invalid_options, key=items.index))
34 | available_options = ", ".join(self.choices)
35 | self.fail(f"invalid choice(s): {sorted_options}. Choose from {available_options}")
36 |
37 |
38 | not_set = NotSet()
39 |
40 |
41 | class OptionalInt(click.types.IntRange):
42 | def convert( # type: ignore
43 | self, value: str, param: Optional[click.core.Parameter], ctx: Optional[click.core.Context]
44 | ) -> Union[int, NotSet]:
45 | if value == "None":
46 | return not_set
47 | try:
48 | int(value)
49 | return super().convert(value, param, ctx)
50 | except ValueError:
51 | self.fail("%s is not a valid integer or None" % value, param, ctx)
52 |
--------------------------------------------------------------------------------
/schemathesis/cli/output/__init__.py:
--------------------------------------------------------------------------------
1 | from . import default, short
2 |
--------------------------------------------------------------------------------
/schemathesis/cli/output/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/cli/output/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/cli/output/__pycache__/default.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/cli/output/__pycache__/default.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/cli/output/__pycache__/short.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/cli/output/__pycache__/short.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/cli/output/short.py:
--------------------------------------------------------------------------------
1 | import click
2 |
3 | from ...runner import events
4 | from ..context import ExecutionContext
5 | from ..handlers import EventHandler
6 | from . import default
7 |
8 |
9 | def handle_before_execution(context: ExecutionContext, event: events.BeforeExecution) -> None:
10 | if event.recursion_level > 0:
11 | context.endpoints_count += 1 # type: ignore
12 |
13 |
14 | def handle_after_execution(context: ExecutionContext, event: events.AfterExecution) -> None:
15 | context.endpoints_processed += 1
16 | context.results.append(event.result)
17 | context.hypothesis_output.extend(event.hypothesis_output)
18 | default.display_execution_result(context, event)
19 |
20 |
21 | class ShortOutputStyleHandler(EventHandler):
22 | def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
23 | """Short output style shows single symbols in the progress bar.
24 |
25 | Otherwise, identical to the default output style.
26 | """
27 | if isinstance(event, events.Initialized):
28 | default.handle_initialized(context, event)
29 | if isinstance(event, events.BeforeExecution):
30 | handle_before_execution(context, event)
31 | if isinstance(event, events.AfterExecution):
32 | handle_after_execution(context, event)
33 | if isinstance(event, events.Finished):
34 | if context.endpoints_count == context.endpoints_processed:
35 | click.echo()
36 | default.handle_finished(context, event)
37 | if isinstance(event, events.Interrupted):
38 | default.handle_interrupted(context, event)
39 | if isinstance(event, events.InternalError):
40 | default.handle_internal_error(context, event)
41 |
--------------------------------------------------------------------------------
/schemathesis/constants.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 | from ._compat import metadata
4 |
5 | try:
6 | __version__ = metadata.version(__package__)
7 | except metadata.PackageNotFoundError:
8 | # Local run without installation
9 | __version__ = "dev"
10 |
11 |
12 | USER_AGENT = f"schemathesis/{__version__}"
13 | DEFAULT_DEADLINE = 500 # pragma: no mutate
14 | DEFAULT_STATEFUL_RECURSION_LIMIT = 5 # pragma: no mutate
15 |
16 |
17 | class DataGenerationMethod(str, Enum):
18 | """Defines what data Schemathesis generates for tests."""
19 |
20 | # Generate data, that fits the API schema
21 | positive = "positive"
22 |
23 | @classmethod
24 | def default(cls) -> "DataGenerationMethod":
25 | return cls.positive
26 |
27 | def as_short_name(self) -> str:
28 | return {
29 | DataGenerationMethod.positive: "P",
30 | }[self]
31 |
32 |
33 | DEFAULT_DATA_GENERATION_METHODS = (DataGenerationMethod.default(),)
34 |
--------------------------------------------------------------------------------
/schemathesis/exceptions.py:
--------------------------------------------------------------------------------
1 | from hashlib import sha1
2 | from json import JSONDecodeError
3 | from typing import Dict, Type, Union
4 |
5 | import attr
6 | from jsonschema import ValidationError
7 |
8 | from .utils import GenericResponse
9 |
10 |
11 | class CheckFailed(AssertionError):
12 | """Custom error type to distinguish from arbitrary AssertionError that may happen in the dependent libraries."""
13 |
14 |
15 | CACHE: Dict[Union[str, int], Type[CheckFailed]] = {}
16 |
17 |
18 | def get_exception(name: str) -> Type[CheckFailed]:
19 | """Create a new exception class with provided name or fetch one from the cache."""
20 | if name in CACHE:
21 | exception_class = CACHE[name]
22 | else:
23 | exception_class = type(name, (CheckFailed,), {})
24 | exception_class.__qualname__ = CheckFailed.__name__
25 | exception_class.__name__ = CheckFailed.__name__
26 | CACHE[name] = exception_class
27 | return exception_class
28 |
29 |
30 | def _get_hashed_exception(prefix: str, message: str) -> Type[CheckFailed]:
31 | """Give different exceptions for different error messages."""
32 | messages_digest = sha1(message.encode("utf-8")).hexdigest()
33 | name = f"{prefix}{messages_digest}"
34 | return get_exception(name)
35 |
36 |
37 | def get_grouped_exception(prefix: str, *exceptions: AssertionError) -> Type[CheckFailed]:
38 | # The prefix is needed to distinguish multiple endpoints with the same error messages
39 | # that are coming from different endpoints
40 | messages = [exception.args[0] for exception in exceptions]
41 | message = "".join(messages)
42 | return _get_hashed_exception("GroupedException", f"{prefix}{message}")
43 |
44 |
45 | def get_status_code_error(status_code: int) -> Type[CheckFailed]:
46 | """Return new exception for an unexpected status code."""
47 | name = f"StatusCodeError{status_code}"
48 | return get_exception(name)
49 |
50 |
51 | def get_response_type_error(expected: str, received: str) -> Type[CheckFailed]:
52 | """Return new exception for an unexpected response type."""
53 | name = f"SchemaValidationError{expected}_{received}"
54 | return get_exception(name)
55 |
56 |
57 | def get_malformed_media_type_error(media_type: str) -> Type[CheckFailed]:
58 | name = f"MalformedMediaType{media_type}"
59 | return get_exception(name)
60 |
61 |
62 | def get_missing_content_type_error() -> Type[CheckFailed]:
63 | """Return new exception for a missing Content-Type header."""
64 | return get_exception("MissingContentTypeError")
65 |
66 |
67 | def get_schema_validation_error(exception: ValidationError) -> Type[CheckFailed]:
68 | """Return new exception for schema validation error."""
69 | return _get_hashed_exception("SchemaValidationError", str(exception))
70 |
71 |
72 | def get_response_parsing_error(exception: JSONDecodeError) -> Type[CheckFailed]:
73 | """Return new exception for response parsing error."""
74 | return _get_hashed_exception("ResponseParsingError", str(exception))
75 |
76 |
77 | def get_headers_error(message: str) -> Type[CheckFailed]:
78 | """Return new exception for missing headers."""
79 | return _get_hashed_exception("MissingHeadersError", message)
80 |
81 |
82 | class InvalidSchema(Exception):
83 | """Schema associated with an endpoint contains an error."""
84 |
85 |
86 | @attr.s # pragma: no mutate
87 | class HTTPError(Exception):
88 | response: GenericResponse = attr.ib() # pragma: no mutate
89 | url: str = attr.ib() # pragma: no mutate
90 |
--------------------------------------------------------------------------------
/schemathesis/extra/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/extra/__init__.py
--------------------------------------------------------------------------------
/schemathesis/extra/_aiohttp.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from typing import Optional
3 |
4 | from aiohttp import web
5 |
6 | from . import _server
7 |
8 |
9 | def _run_server(app: web.Application, port: int) -> None:
10 | """Run the given app on the given port.
11 |
12 | Intended to be called as a target for a separate thread.
13 | NOTE. `aiohttp.web.run_app` works only in the main thread and can't be used here (or maybe can we some tuning)
14 | """
15 | # Set a loop for a new thread (there is no by default for non-main threads)
16 | loop = asyncio.new_event_loop()
17 | asyncio.set_event_loop(loop)
18 | runner = web.AppRunner(app)
19 | loop.run_until_complete(runner.setup())
20 | site = web.TCPSite(runner, "127.0.0.1", port)
21 | loop.run_until_complete(site.start())
22 | loop.run_forever()
23 |
24 |
25 | def run_server(app: web.Application, port: Optional[int] = None, timeout: float = 0.05) -> int:
26 | """Start a thread with the given aiohttp application."""
27 | return _server.run(_run_server, app=app, port=port, timeout=timeout)
28 |
--------------------------------------------------------------------------------
/schemathesis/extra/_flask.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from flask import Flask
4 |
5 | from . import _server
6 |
7 |
8 | def run_server(app: Flask, port: Optional[int] = None, timeout: float = 0.05) -> int:
9 | """Start a thread with the given aiohttp application."""
10 | return _server.run(app.run, port=port, timeout=timeout)
11 |
--------------------------------------------------------------------------------
/schemathesis/extra/_server.py:
--------------------------------------------------------------------------------
1 | import threading
2 | from time import sleep
3 | from typing import Any, Callable, Optional
4 |
5 | from aiohttp.test_utils import unused_port
6 |
7 |
8 | def run(target: Callable, port: Optional[int] = None, timeout: float = 0.05, **kwargs: Any) -> int:
9 | """Start a thread with the given aiohttp application."""
10 | if port is None:
11 | port = unused_port()
12 | server_thread = threading.Thread(target=target, kwargs={"port": port, **kwargs})
13 | server_thread.daemon = True
14 | server_thread.start()
15 | sleep(timeout)
16 | return port
17 |
--------------------------------------------------------------------------------
/schemathesis/extra/pytest_plugin.py:
--------------------------------------------------------------------------------
1 | from functools import partial
2 | from typing import Any, Callable, Generator, List, Optional, Type, TypeVar, cast
3 |
4 | import pytest
5 | from _pytest import fixtures, nodes
6 | from _pytest.config import hookimpl
7 | from _pytest.fixtures import FuncFixtureInfo
8 | from _pytest.nodes import Node
9 | from _pytest.python import Class, Function, FunctionDefinition, Metafunc, Module, PyCollector
10 | from _pytest.runner import runtestprotocol
11 | from _pytest.warning_types import PytestWarning
12 | from hypothesis.errors import InvalidArgument
13 | from packaging import version
14 |
15 | from .. import DataGenerationMethod
16 | from .._hypothesis import create_test
17 | from ..models import Endpoint
18 | from ..stateful import Feedback
19 | from ..utils import is_schemathesis_test
20 |
21 | USE_FROM_PARENT = version.parse(pytest.__version__) >= version.parse("5.4.0")
22 |
23 | T = TypeVar("T", bound=Node)
24 |
25 |
26 | def create(cls: Type[T], *args: Any, **kwargs: Any) -> T:
27 | if USE_FROM_PARENT:
28 | return cls.from_parent(*args, **kwargs) # type: ignore
29 | return cls(*args, **kwargs)
30 |
31 |
32 | class SchemathesisFunction(Function): # pylint: disable=too-many-ancestors
33 | def __init__(
34 | self,
35 | *args: Any,
36 | test_func: Callable,
37 | test_name: Optional[str] = None,
38 | recursion_level: int = 0,
39 | data_generation_method: DataGenerationMethod,
40 | **kwargs: Any,
41 | ) -> None:
42 | super().__init__(*args, **kwargs)
43 | self.test_function = test_func
44 | self.test_name = test_name
45 | self.recursion_level = recursion_level
46 | self.data_generation_method = data_generation_method
47 |
48 | def _getobj(self) -> partial:
49 | """Tests defined as methods require `self` as the first argument.
50 |
51 | This method is called only for this case.
52 | """
53 | return partial(self.obj, self.parent.obj) # type: ignore
54 |
55 | @property
56 | def feedback(self) -> Optional[Feedback]:
57 | return getattr(self.obj, "_schemathesis_feedback", None)
58 |
59 | def warn_if_stateful_responses_not_stored(self) -> None:
60 | feedback = self.feedback
61 | if feedback is not None and not feedback.stateful_tests:
62 | self.warn(PytestWarning(NOT_USED_STATEFUL_TESTING_MESSAGE))
63 |
64 | def _get_stateful_tests(self) -> List["SchemathesisFunction"]:
65 | feedback = self.feedback
66 | recursion_level = self.recursion_level
67 | if feedback is None or recursion_level >= feedback.endpoint.schema.stateful_recursion_limit:
68 | return []
69 | previous_test_name = self.test_name or f"{feedback.endpoint.method.upper()}:{feedback.endpoint.full_path}"
70 |
71 | def make_test(
72 | endpoint: Endpoint,
73 | test: Callable,
74 | data_generation_method: DataGenerationMethod,
75 | previous_tests: str,
76 | ) -> "SchemathesisFunction":
77 | test_name = f"{previous_tests} -> {endpoint.method.upper()}:{endpoint.full_path}"
78 | return create(
79 | self.__class__,
80 | name=f"{self.originalname}[{test_name}][{self.data_generation_method.as_short_name()}]",
81 | parent=self.parent,
82 | callspec=getattr(self, "callspec", None),
83 | callobj=test,
84 | fixtureinfo=self._fixtureinfo,
85 | keywords=self.keywords,
86 | originalname=self.originalname,
87 | test_func=self.test_function,
88 | test_name=test_name,
89 | recursion_level=recursion_level + 1,
90 | data_generation_method=data_generation_method,
91 | )
92 |
93 | return [
94 | make_test(endpoint, test, data_generation_method, previous_test_name)
95 | for (endpoint, data_generation_method, test) in feedback.get_stateful_tests(self.test_function, None, None)
96 | ]
97 |
98 | def add_stateful_tests(self) -> None:
99 | idx = self.session.items.index(self) + 1
100 | tests = self._get_stateful_tests()
101 | self.session.items[idx:idx] = tests
102 | self.session.testscollected += len(tests)
103 |
104 |
105 | class SchemathesisCase(PyCollector):
106 | def __init__(self, test_function: Callable, *args: Any, **kwargs: Any) -> None:
107 | self.test_function = test_function
108 | self.schemathesis_case = test_function._schemathesis_test # type: ignore
109 | self.given_args = getattr(test_function, "_schemathesis_given_args", ())
110 | self.given_kwargs = getattr(test_function, "_schemathesis_given_kwargs", {})
111 | super().__init__(*args, **kwargs)
112 |
113 | def _get_test_name(self, endpoint: Endpoint, data_generation_method: DataGenerationMethod) -> str:
114 | return f"{self.name}[{endpoint.method.upper()}:{endpoint.full_path}][{data_generation_method.as_short_name()}]"
115 |
116 | def _gen_items(
117 | self, endpoint: Endpoint, data_generation_method: DataGenerationMethod
118 | ) -> Generator[SchemathesisFunction, None, None]:
119 | """Generate all items for the given endpoint.
120 |
121 | Could produce more than one test item if
122 | parametrization is applied via ``pytest.mark.parametrize`` or ``pytest_generate_tests``.
123 |
124 | This implementation is based on the original one in pytest, but with slight adjustments
125 | to produce tests out of hypothesis ones.
126 | """
127 | name = self._get_test_name(endpoint, data_generation_method)
128 | funcobj = create_test(
129 | endpoint=endpoint,
130 | test=self.test_function,
131 | _given_args=self.given_args,
132 | _given_kwargs=self.given_kwargs,
133 | data_generation_method=data_generation_method,
134 | )
135 |
136 | cls = self._get_class_parent()
137 | definition: FunctionDefinition = create(FunctionDefinition, name=self.name, parent=self.parent, callobj=funcobj)
138 | fixturemanager = self.session._fixturemanager
139 | fixtureinfo = fixturemanager.getfixtureinfo(definition, funcobj, cls)
140 |
141 | metafunc = self._parametrize(cls, definition, fixtureinfo)
142 |
143 | if not metafunc._calls:
144 | yield create(
145 | SchemathesisFunction,
146 | name=name,
147 | parent=self.parent,
148 | callobj=funcobj,
149 | fixtureinfo=fixtureinfo,
150 | test_func=self.test_function,
151 | originalname=self.name,
152 | data_generation_method=data_generation_method,
153 | )
154 | else:
155 | fixtures.add_funcarg_pseudo_fixture_def(self.parent, metafunc, fixturemanager)
156 | fixtureinfo.prune_dependency_tree()
157 | for callspec in metafunc._calls:
158 | subname = f"{name}[{callspec.id}]"
159 | yield create(
160 | SchemathesisFunction,
161 | name=subname,
162 | parent=self.parent,
163 | callspec=callspec,
164 | callobj=funcobj,
165 | fixtureinfo=fixtureinfo,
166 | keywords={callspec.id: True},
167 | originalname=name,
168 | test_func=self.test_function,
169 | data_generation_method=data_generation_method,
170 | )
171 |
172 | def _get_class_parent(self) -> Optional[Type]:
173 | clscol = self.getparent(Class)
174 | return clscol.obj if clscol else None
175 |
176 | def _parametrize(
177 | self, cls: Optional[Type], definition: FunctionDefinition, fixtureinfo: FuncFixtureInfo
178 | ) -> Metafunc:
179 | parent = self.getparent(Module)
180 | module = parent.obj if parent is not None else parent
181 | metafunc = Metafunc(definition, fixtureinfo, self.config, cls=cls, module=module)
182 | methods = []
183 | if hasattr(module, "pytest_generate_tests"):
184 | methods.append(module.pytest_generate_tests)
185 | if hasattr(cls, "pytest_generate_tests"):
186 | cls = cast(Type, cls)
187 | methods.append(cls().pytest_generate_tests)
188 | self.ihook.pytest_generate_tests.call_extra(methods, {"metafunc": metafunc})
189 | return metafunc
190 |
191 | def collect(self) -> List[Function]: # type: ignore
192 | """Generate different test items for all endpoints available in the given schema."""
193 | try:
194 | return [
195 | item
196 | for data_generation_method in self.schemathesis_case.data_generation_methods
197 | for endpoint in self.schemathesis_case.get_all_endpoints()
198 | for item in self._gen_items(endpoint, data_generation_method)
199 | ]
200 | except Exception:
201 | pytest.fail("Error during collection")
202 |
203 |
204 | NOT_USED_STATEFUL_TESTING_MESSAGE = (
205 | "You are using stateful testing, but no responses were stored during the test! "
206 | "Please, use `case.call` or `case.store_response` in your test to enable stateful tests."
207 | )
208 |
209 |
210 | @hookimpl(hookwrapper=True) # type:ignore # pragma: no mutate
211 | def pytest_pycollect_makeitem(collector: nodes.Collector, name: str, obj: Any) -> Generator[None, Any, None]:
212 | """Switch to a different collector if the test is parametrized marked by schemathesis."""
213 | outcome = yield
214 | if is_schemathesis_test(obj):
215 | outcome.force_result(create(SchemathesisCase, parent=collector, test_function=obj, name=name))
216 | else:
217 | outcome.get_result()
218 |
219 |
220 | @hookimpl(hookwrapper=True) # pragma: no mutate
221 | def pytest_pyfunc_call(pyfuncitem): # type:ignore
222 | """It is possible to have a Hypothesis exception in runtime.
223 |
224 | For example - kwargs validation is failed for some strategy.
225 | """
226 | outcome = yield
227 | try:
228 | outcome.get_result()
229 | except InvalidArgument as exc:
230 | pytest.fail(exc.args[0])
231 |
232 |
233 | def pytest_runtest_protocol(item: Function, nextitem: Optional[Function]) -> bool:
234 | item.ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
235 | reports = runtestprotocol(item, nextitem=nextitem)
236 | item.ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location)
237 | if isinstance(item, SchemathesisFunction):
238 | for report in reports:
239 | if report.when == "call" and report.outcome == "passed":
240 | item.warn_if_stateful_responses_not_stored()
241 | item.add_stateful_tests()
242 | return True
243 |
--------------------------------------------------------------------------------
/schemathesis/fixups/__init__.py:
--------------------------------------------------------------------------------
1 | from typing import Iterable, Optional
2 |
3 | from . import fast_api
4 |
5 | ALL_FIXUPS = {"fast_api": fast_api}
6 |
7 |
8 | def install(fixups: Optional[Iterable[str]] = None) -> None:
9 | """Install fixups.
10 |
11 | Without the first argument installs all available fixups.
12 | """
13 | fixups = fixups or list(ALL_FIXUPS.keys())
14 | for name in fixups:
15 | ALL_FIXUPS[name].install() # type: ignore
16 |
17 |
18 | def uninstall(fixups: Optional[Iterable[str]] = None) -> None:
19 | """Uninstall fixups.
20 |
21 | Without the first argument uninstalls all available fixups.
22 | """
23 | fixups = fixups or list(ALL_FIXUPS.keys())
24 | for name in fixups:
25 | ALL_FIXUPS[name].uninstall() # type: ignore
26 |
--------------------------------------------------------------------------------
/schemathesis/fixups/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/fixups/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/fixups/__pycache__/fast_api.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/fixups/__pycache__/fast_api.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/fixups/fast_api.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict
2 |
3 | from ..hooks import HookContext, register, unregister
4 | from ..utils import traverse_schema
5 |
6 |
7 | def install() -> None:
8 | register(before_load_schema)
9 |
10 |
11 | def uninstall() -> None:
12 | unregister(before_load_schema)
13 |
14 |
15 | def before_load_schema(context: HookContext, schema: Dict[str, Any]) -> None:
16 | traverse_schema(schema, _handle_boundaries)
17 |
18 |
19 | def _handle_boundaries(schema: Dict[str, Any]) -> Dict[str, Any]:
20 | """Convert Draft 7 keywords to Draft 4 compatible versions.
21 |
22 | FastAPI uses ``pydantic``, which generates Draft 7 compatible schemas.
23 | """
24 | for boundary_name, boundary_exclusive_name in (("maximum", "exclusiveMaximum"), ("minimum", "exclusiveMinimum")):
25 | value = schema.get(boundary_exclusive_name)
26 | # `bool` check is needed, since in Python `True` is an instance of `int`
27 | if isinstance(value, (int, float)) and not isinstance(value, bool):
28 | schema[boundary_exclusive_name] = True
29 | schema[boundary_name] = value
30 | return schema
31 |
--------------------------------------------------------------------------------
/schemathesis/hooks.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | from collections import defaultdict
3 | from enum import Enum, unique
4 | from typing import TYPE_CHECKING, Any, Callable, DefaultDict, Dict, List, Optional, Union, cast
5 |
6 | import attr
7 | from hypothesis import strategies as st
8 |
9 | from .types import GenericTest
10 | from .utils import GenericResponse
11 |
12 | if TYPE_CHECKING:
13 | from .models import Case, Endpoint
14 |
15 |
16 | class HookLocation(Enum):
17 | path_parameters = 1
18 | headers = 2
19 | cookies = 3
20 | query = 4
21 | body = 5
22 |
23 |
24 | @unique
25 | class HookScope(Enum):
26 | GLOBAL = 1
27 | SCHEMA = 2
28 | TEST = 3
29 |
30 |
31 | @attr.s(slots=True) # pragma: no mutate
32 | class RegisteredHook:
33 | signature: inspect.Signature = attr.ib() # pragma: no mutate
34 | scopes: List[HookScope] = attr.ib() # pragma: no mutate
35 |
36 |
37 | @attr.s(slots=True) # pragma: no mutate
38 | class HookContext:
39 | """A context that is passed to some hook functions."""
40 |
41 | endpoint: Optional["Endpoint"] = attr.ib(default=None) # pragma: no mutate
42 |
43 |
44 | @attr.s(slots=True) # pragma: no mutate
45 | class HookDispatcher:
46 | """Generic hook dispatcher.
47 |
48 | Provides a mechanism to extend Schemathesis in registered hook points.
49 | """
50 |
51 | scope: HookScope = attr.ib() # pragma: no mutate
52 | _hooks: DefaultDict[str, List[Callable]] = attr.ib(factory=lambda: defaultdict(list)) # pragma: no mutate
53 | _specs: Dict[str, RegisteredHook] = {} # pragma: no mutate
54 |
55 | def register(self, hook: Union[str, Callable]) -> Callable:
56 | """Register a new hook.
57 |
58 | Can be used as a decorator in two forms.
59 | Without arguments for registering hooks and autodetecting their names:
60 |
61 | @schema.hooks.register
62 | def before_generate_query(strategy, context):
63 | ...
64 |
65 | With a hook name as the first argument:
66 |
67 | @schema.hooks.register("before_generate_query")
68 | def hook(strategy, context):
69 | ...
70 | """
71 | if isinstance(hook, str):
72 |
73 | def decorator(func: Callable) -> Callable:
74 | hook_name = cast(str, hook)
75 | return self.register_hook_with_name(func, hook_name)
76 |
77 | return decorator
78 | return self.register_hook_with_name(hook, hook.__name__)
79 |
80 | def apply(self, hook: Callable, *, name: Optional[str] = None) -> Callable[[Callable], Callable]:
81 | """Register hook to run only on one test function.
82 |
83 | Example:
84 | def before_generate_query(strategy, context):
85 | ...
86 |
87 | @schema.hooks.apply(before_generate_query)
88 | @schema.parametrize()
89 | def test_api(case):
90 | ...
91 |
92 | """
93 |
94 | if name is None:
95 | hook_name = hook.__name__
96 | else:
97 | hook_name = name
98 |
99 | def decorator(func: GenericTest) -> GenericTest:
100 | dispatcher = self.add_dispatcher(func)
101 | dispatcher.register_hook_with_name(hook, hook_name)
102 | return func
103 |
104 | return decorator
105 |
106 | @classmethod
107 | def add_dispatcher(cls, func: GenericTest) -> "HookDispatcher":
108 | """Attach a new dispatcher instance to the test if it is not already present."""
109 | if not hasattr(func, "_schemathesis_hooks"):
110 | func._schemathesis_hooks = cls(scope=HookScope.TEST) # type: ignore
111 | return func._schemathesis_hooks # type: ignore
112 |
113 | def register_hook_with_name(self, hook: Callable, name: str) -> Callable:
114 | """A helper for hooks registration."""
115 | self._validate_hook(name, hook)
116 | self._hooks[name].append(hook)
117 | return hook
118 |
119 | @classmethod
120 | def register_spec(cls, scopes: List[HookScope]) -> Callable:
121 | """Register hook specification.
122 |
123 | All hooks, registered with `register` should comply with corresponding registered specs.
124 | """
125 |
126 | def _register_spec(spec: Callable) -> Callable:
127 | cls._specs[spec.__name__] = RegisteredHook(inspect.signature(spec), scopes)
128 | return spec
129 |
130 | return _register_spec
131 |
132 | def _validate_hook(self, name: str, hook: Callable) -> None:
133 | """Basic validation for hooks being registered."""
134 | spec = self._specs.get(name)
135 | if spec is None:
136 | raise TypeError(f"There is no hook with name '{name}'")
137 | # Some hooks are not present on all levels. We need to avoid registering hooks on wrong levels.
138 | if self.scope not in spec.scopes:
139 | scopes = ", ".join(scope.name for scope in spec.scopes)
140 | raise ValueError(
141 | f"Cannot register hook '{name}' on {self.scope.name} scope dispatcher. "
142 | f"Use a dispatcher with {scopes} scope(s) instead"
143 | )
144 | signature = inspect.signature(hook)
145 | if len(signature.parameters) != len(spec.signature.parameters):
146 | raise TypeError(
147 | f"Hook '{name}' takes {len(spec.signature.parameters)} arguments but {len(signature.parameters)} is defined"
148 | )
149 |
150 | def get_all_by_name(self, name: str) -> List[Callable]:
151 | """Get a list of hooks registered for a name."""
152 | return self._hooks.get(name, [])
153 |
154 | def dispatch(self, name: str, context: HookContext, *args: Any, **kwargs: Any) -> None:
155 | """Run all hooks for the given name."""
156 | for hook in self.get_all_by_name(name):
157 | hook(context, *args, **kwargs)
158 |
159 | def unregister(self, hook: Callable) -> None:
160 | """Unregister a specific hook."""
161 | # It removes this function from all places
162 | for hooks in self._hooks.values():
163 | hooks[:] = [item for item in hooks if item is not hook]
164 |
165 | def unregister_all(self) -> None:
166 | """Remove all registered hooks.
167 |
168 | Useful in tests.
169 | """
170 | self._hooks = defaultdict(list)
171 |
172 |
173 | all_scopes = HookDispatcher.register_spec(list(HookScope))
174 |
175 |
176 | @all_scopes
177 | def before_generate_path_parameters(context: HookContext, strategy: st.SearchStrategy) -> st.SearchStrategy:
178 | """Called on a strategy that generates values for ``path_parameters``."""
179 |
180 |
181 | @all_scopes
182 | def before_generate_headers(context: HookContext, strategy: st.SearchStrategy) -> st.SearchStrategy:
183 | """Called on a strategy that generates values for ``headers``."""
184 |
185 |
186 | @all_scopes
187 | def before_generate_cookies(context: HookContext, strategy: st.SearchStrategy) -> st.SearchStrategy:
188 | """Called on a strategy that generates values for ``cookies``."""
189 |
190 |
191 | @all_scopes
192 | def before_generate_query(context: HookContext, strategy: st.SearchStrategy) -> st.SearchStrategy:
193 | """Called on a strategy that generates values for ``query``."""
194 |
195 |
196 | @all_scopes
197 | def before_generate_body(context: HookContext, strategy: st.SearchStrategy) -> st.SearchStrategy:
198 | """Called on a strategy that generates values for ``body``."""
199 |
200 |
201 | @all_scopes
202 | def before_process_path(context: HookContext, path: str, methods: Dict[str, Any]) -> None:
203 | """Called before API path is processed."""
204 |
205 |
206 | @HookDispatcher.register_spec([HookScope.GLOBAL])
207 | def before_load_schema(context: HookContext, raw_schema: Dict[str, Any]) -> None:
208 | """Called before schema instance is created."""
209 |
210 |
211 | @all_scopes
212 | def before_add_examples(context: HookContext, examples: List["Case"]) -> None:
213 | """Called before explicit examples are added to a test via `@example` decorator.
214 |
215 | `examples` is a list that could be extended with examples provided by the user.
216 | """
217 |
218 |
219 | @HookDispatcher.register_spec([HookScope.GLOBAL])
220 | def add_case(context: HookContext, case: "Case", response: GenericResponse) -> Optional["Case"]:
221 | """Creates an additional test per endpoint. If this hook returns None, no additional test created.
222 |
223 | Called with a copy of the original case object and the server's response to the original case.
224 | """
225 |
226 |
227 | GLOBAL_HOOK_DISPATCHER = HookDispatcher(scope=HookScope.GLOBAL)
228 | dispatch = GLOBAL_HOOK_DISPATCHER.dispatch
229 | get_all_by_name = GLOBAL_HOOK_DISPATCHER.get_all_by_name
230 | register = GLOBAL_HOOK_DISPATCHER.register
231 | unregister = GLOBAL_HOOK_DISPATCHER.unregister
232 | unregister_all = GLOBAL_HOOK_DISPATCHER.unregister_all
233 |
--------------------------------------------------------------------------------
/schemathesis/lazy.py:
--------------------------------------------------------------------------------
1 | from inspect import signature
2 | from typing import Any, Callable, Dict, Iterable, Optional, Union
3 |
4 | import attr
5 | from _pytest.fixtures import FixtureRequest
6 | from pytest_subtests import SubTests
7 |
8 | from .constants import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod
9 | from .hooks import HookDispatcher, HookScope
10 | from .models import Endpoint
11 | from .schemas import BaseSchema
12 | from .types import Filter, GenericTest, NotSet
13 | from .utils import NOT_SET
14 |
15 |
16 | @attr.s(slots=True) # pragma: no mutate
17 | class LazySchema:
18 | fixture_name: str = attr.ib() # pragma: no mutate
19 | method: Optional[Filter] = attr.ib(default=NOT_SET) # pragma: no mutate
20 | endpoint: Optional[Filter] = attr.ib(default=NOT_SET) # pragma: no mutate
21 | tag: Optional[Filter] = attr.ib(default=NOT_SET) # pragma: no mutate
22 | operation_id: Optional[Filter] = attr.ib(default=NOT_SET) # pragma: no mutate
23 | hooks: HookDispatcher = attr.ib(factory=lambda: HookDispatcher(scope=HookScope.SCHEMA)) # pragma: no mutate
24 | validate_schema: bool = attr.ib(default=True) # pragma: no mutate
25 | skip_deprecated_endpoints: bool = attr.ib(default=False) # pragma: no mutate
26 | data_generation_methods: Iterable[DataGenerationMethod] = attr.ib(default=DEFAULT_DATA_GENERATION_METHODS)
27 |
28 | def parametrize(
29 | self,
30 | method: Optional[Filter] = NOT_SET,
31 | endpoint: Optional[Filter] = NOT_SET,
32 | tag: Optional[Filter] = NOT_SET,
33 | operation_id: Optional[Filter] = NOT_SET,
34 | validate_schema: Union[bool, NotSet] = NOT_SET,
35 | skip_deprecated_endpoints: Union[bool, NotSet] = NOT_SET,
36 | data_generation_methods: Union[Iterable[DataGenerationMethod], NotSet] = NOT_SET,
37 | ) -> Callable:
38 | if method is NOT_SET:
39 | method = self.method
40 | if endpoint is NOT_SET:
41 | endpoint = self.endpoint
42 | if tag is NOT_SET:
43 | tag = self.tag
44 | if operation_id is NOT_SET:
45 | operation_id = self.operation_id
46 | if data_generation_methods is NOT_SET:
47 | data_generation_methods = self.data_generation_methods
48 |
49 | def wrapper(func: Callable) -> Callable:
50 | def test(request: FixtureRequest, subtests: SubTests) -> None:
51 | """The actual test, which is executed by pytest."""
52 | if hasattr(test, "_schemathesis_hooks"):
53 | func._schemathesis_hooks = test._schemathesis_hooks # type: ignore
54 | schema = get_schema(
55 | request=request,
56 | name=self.fixture_name,
57 | method=method,
58 | endpoint=endpoint,
59 | tag=tag,
60 | operation_id=operation_id,
61 | hooks=self.hooks,
62 | test_function=func,
63 | validate_schema=validate_schema,
64 | skip_deprecated_endpoints=skip_deprecated_endpoints,
65 | data_generation_methods=data_generation_methods,
66 | )
67 | fixtures = get_fixtures(func, request)
68 | # Changing the node id is required for better reporting - the method and endpoint will appear there
69 | node_id = subtests.item._nodeid
70 | settings = getattr(test, "_hypothesis_internal_use_settings", None)
71 | tests = list(schema.get_all_tests(func, settings))
72 | request.session.testscollected += len(tests)
73 | for _endpoint, data_generation_method, sub_test in tests:
74 | subtests.item._nodeid = _get_node_name(node_id, _endpoint, data_generation_method)
75 | run_subtest(_endpoint, fixtures, sub_test, subtests)
76 | subtests.item._nodeid = node_id
77 |
78 | # Needed to prevent a failure when settings are applied to the test function
79 | test.is_hypothesis_test = True # type: ignore
80 |
81 | return test
82 |
83 | return wrapper
84 |
85 |
86 | def _get_node_name(node_id: str, endpoint: Endpoint, data_generation_method: DataGenerationMethod) -> str:
87 | """Make a test node name. For example: test_api[GET:/users]."""
88 | return f"{node_id}[{endpoint.method.upper()}:{endpoint.full_path}][{data_generation_method.as_short_name()}]"
89 |
90 |
91 | def run_subtest(endpoint: Endpoint, fixtures: Dict[str, Any], sub_test: Callable, subtests: SubTests) -> None:
92 | """Run the given subtest with pytest fixtures."""
93 | with subtests.test(method=endpoint.method.upper(), path=endpoint.path):
94 | sub_test(**fixtures)
95 |
96 |
97 | def get_schema(
98 | *,
99 | request: FixtureRequest,
100 | name: str,
101 | method: Optional[Filter] = None,
102 | endpoint: Optional[Filter] = None,
103 | tag: Optional[Filter] = None,
104 | operation_id: Optional[Filter] = None,
105 | test_function: GenericTest,
106 | hooks: HookDispatcher,
107 | validate_schema: Union[bool, NotSet] = NOT_SET,
108 | skip_deprecated_endpoints: Union[bool, NotSet] = NOT_SET,
109 | data_generation_methods: Union[Iterable[DataGenerationMethod], NotSet] = NOT_SET,
110 | ) -> BaseSchema:
111 | """Loads a schema from the fixture."""
112 | schema = request.getfixturevalue(name)
113 | if not isinstance(schema, BaseSchema):
114 | raise ValueError(f"The given schema must be an instance of BaseSchema, got: {type(schema)}")
115 | return schema.clone(
116 | method=method,
117 | endpoint=endpoint,
118 | tag=tag,
119 | operation_id=operation_id,
120 | test_function=test_function,
121 | hooks=hooks,
122 | validate_schema=validate_schema,
123 | skip_deprecated_endpoints=skip_deprecated_endpoints,
124 | data_generation_methods=data_generation_methods,
125 | )
126 |
127 |
128 | def get_fixtures(func: Callable, request: FixtureRequest) -> Dict[str, Any]:
129 | """Load fixtures, needed for the test function."""
130 | sig = signature(func)
131 | return {name: request.getfixturevalue(name) for name in sig.parameters if name != "case"}
132 |
--------------------------------------------------------------------------------
/schemathesis/parameters.py:
--------------------------------------------------------------------------------
1 | """API operation parameters.
2 |
3 | These are basic entities that describe what data could be sent to the API.
4 | """
5 | from copy import deepcopy
6 | from typing import Any, Dict, Generator, Generic, List, TypeVar
7 |
8 | import attr
9 |
10 |
11 | @attr.s(slots=True) # pragma: no mutate
12 | class Parameter:
13 | """A logically separate parameter bound to a location (e.g., to "query string").
14 |
15 | For example, if the API requires multiple headers to be present, each header is presented as a separate
16 | `Parameter` instance.
17 | """
18 |
19 | # The parameter definition in the language acceptable by the API
20 | definition: Any = attr.ib() # pragma: no mutate
21 |
22 | def __attrs_post_init__(self) -> None:
23 | # Do not use `converter=deepcopy` on the field due to mypy not detecting type annotations
24 | self.definition = deepcopy(self.definition)
25 |
26 | @property
27 | def location(self) -> str:
28 | """Where this parameter is located.
29 |
30 | E.g. "query" or "body"
31 | """
32 | raise NotImplementedError
33 |
34 | @property
35 | def name(self) -> str:
36 | """Parameter name."""
37 | raise NotImplementedError
38 |
39 | @property
40 | def is_required(self) -> bool:
41 | """Whether the parameter is required for a successful API call."""
42 | raise NotImplementedError
43 |
44 | @property
45 | def example(self) -> Any:
46 | """Parameter example."""
47 | raise NotImplementedError
48 |
49 |
50 | P = TypeVar("P", bound=Parameter)
51 |
52 |
53 | @attr.s # pragma: no mutate
54 | class ParameterSet(Generic[P]):
55 | """A set of parameters for the same location."""
56 |
57 | items: List[P] = attr.ib(factory=list) # pragma: no mutate
58 |
59 | def add(self, parameter: P) -> None:
60 | """Add a new parameter."""
61 | self.items.append(parameter)
62 |
63 | @property
64 | def example(self) -> Dict[str, Any]:
65 | """Composite example gathered from individual parameters."""
66 | return {item.name: item.example for item in self.items if item.example}
67 |
68 | def __bool__(self) -> bool:
69 | return bool(self.items)
70 |
71 | def __iter__(self) -> Generator[P, None, None]:
72 | yield from iter(self.items)
73 |
74 | def __len__(self) -> int:
75 | return len(self.items)
76 |
77 | def __getitem__(self, item: int) -> P:
78 | return self.items[item]
79 |
80 |
81 | class PayloadAlternatives(ParameterSet[P]):
82 | """A set of alternative payloads."""
83 |
84 | @property
85 | def example(self) -> Any:
86 | """We take only the first example."""
87 | # May be extended in the future
88 | if self.items:
89 | return self.items[0].example
90 |
--------------------------------------------------------------------------------
/schemathesis/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/py.typed
--------------------------------------------------------------------------------
/schemathesis/runner/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/runner/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/runner/__pycache__/events.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/runner/__pycache__/events.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/runner/__pycache__/serialization.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/runner/__pycache__/serialization.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/runner/events.py:
--------------------------------------------------------------------------------
1 | import time
2 | from typing import Dict, List, Optional, Union
3 |
4 | import attr
5 | from requests import exceptions
6 |
7 | from ..exceptions import HTTPError
8 | from ..models import Endpoint, Status, TestResult, TestResultSet
9 | from ..schemas import BaseSchema
10 | from ..utils import format_exception
11 | from .serialization import SerializedTestResult
12 |
13 |
14 | @attr.s() # pragma: no mutate
15 | class ExecutionEvent:
16 | """Generic execution event."""
17 |
18 |
19 | @attr.s(slots=True) # pragma: no mutate
20 | class Initialized(ExecutionEvent):
21 | """Runner is initialized, settings are prepared, requests session is ready."""
22 |
23 | # Total number of endpoints in the schema
24 | endpoints_count: int = attr.ib() # pragma: no mutate
25 | location: Optional[str] = attr.ib() # pragma: no mutate
26 | base_url: str = attr.ib() # pragma: no mutate
27 | specification_name: str = attr.ib() # pragma: no mutate
28 | # Timestamp of test run start
29 | start_time: float = attr.ib(factory=time.monotonic) # pragma: no mutate
30 |
31 | @classmethod
32 | def from_schema(cls, *, schema: BaseSchema) -> "Initialized":
33 | """Computes all needed data from a schema instance."""
34 | return cls(
35 | endpoints_count=schema.endpoints_count,
36 | location=schema.location,
37 | base_url=schema.get_base_url(),
38 | specification_name=schema.verbose_name,
39 | )
40 |
41 |
42 | class CurrentPathMixin:
43 | method: str
44 | path: str
45 |
46 | @property
47 | def current_endpoint(self) -> str:
48 | return f"{self.method} {self.path}"
49 |
50 |
51 | @attr.s(slots=True) # pragma: no mutate
52 | class BeforeExecution(CurrentPathMixin, ExecutionEvent):
53 | """Happens before each examined endpoint.
54 |
55 | It happens before a single hypothesis test, that may contain many examples inside.
56 | """
57 |
58 | method: str = attr.ib() # pragma: no mutate
59 | path: str = attr.ib() # pragma: no mutate
60 | relative_path: str = attr.ib() # pragma: no mutate
61 | recursion_level: int = attr.ib() # pragma: no mutate
62 |
63 | @classmethod
64 | def from_endpoint(cls, endpoint: Endpoint, recursion_level: int) -> "BeforeExecution":
65 | return cls(
66 | method=endpoint.method.upper(),
67 | path=endpoint.full_path,
68 | relative_path=endpoint.path,
69 | recursion_level=recursion_level,
70 | )
71 |
72 |
73 | @attr.s(slots=True) # pragma: no mutate
74 | class AfterExecution(CurrentPathMixin, ExecutionEvent):
75 | """Happens after each examined endpoint."""
76 |
77 | method: str = attr.ib() # pragma: no mutate
78 | path: str = attr.ib() # pragma: no mutate
79 | relative_path: str = attr.ib() # pragma: no mutate
80 |
81 | # Endpoint test status - success / failure / error
82 | status: Status = attr.ib() # pragma: no mutate
83 | result: SerializedTestResult = attr.ib() # pragma: no mutate
84 | # Test running time
85 | elapsed_time: float = attr.ib() # pragma: no mutate
86 | # Captured hypothesis stdout
87 | hypothesis_output: List[str] = attr.ib(factory=list) # pragma: no mutate
88 |
89 | @classmethod
90 | def from_result(
91 | cls, result: TestResult, status: Status, elapsed_time: float, hypothesis_output: List[str], endpoint: Endpoint
92 | ) -> "AfterExecution":
93 | return cls(
94 | method=endpoint.method.upper(),
95 | path=endpoint.full_path,
96 | relative_path=endpoint.path,
97 | result=SerializedTestResult.from_test_result(result),
98 | status=status,
99 | elapsed_time=elapsed_time,
100 | hypothesis_output=hypothesis_output,
101 | )
102 |
103 |
104 | @attr.s(slots=True) # pragma: no mutate
105 | class Interrupted(ExecutionEvent):
106 | """If execution was interrupted by Ctrl-C, or a received SIGTERM."""
107 |
108 |
109 | @attr.s(slots=True) # pragma: no mutate
110 | class InternalError(ExecutionEvent):
111 | """An error that happened inside the runner."""
112 |
113 | message: str = attr.ib() # pragma: no mutate
114 | exception_type: str = attr.ib() # pragma: no mutate
115 | exception: Optional[str] = attr.ib(default=None) # pragma: no mutate
116 | exception_with_traceback: Optional[str] = attr.ib(default=None) # pragma: no mutate
117 |
118 | @classmethod
119 | def from_exc(cls, exc: Exception) -> "InternalError":
120 | exception_type = f"{exc.__class__.__module__}.{exc.__class__.__qualname__}"
121 | if isinstance(exc, HTTPError):
122 | if exc.response.status_code == 404:
123 | message = f"Schema was not found at {exc.url}"
124 | else:
125 | message = f"Failed to load schema, code {exc.response.status_code} was returned from {exc.url}"
126 | return cls(message=message, exception_type=exception_type)
127 | exception = format_exception(exc)
128 | exception_with_traceback = format_exception(exc, include_traceback=True)
129 | if isinstance(exc, exceptions.ConnectionError):
130 | message = f"Failed to load schema from {exc.request.url}"
131 | else:
132 | message = "An internal error happened during a test run"
133 | return cls(
134 | message=message,
135 | exception_type=exception_type,
136 | exception=exception,
137 | exception_with_traceback=exception_with_traceback,
138 | )
139 |
140 |
141 | @attr.s(slots=True) # pragma: no mutate
142 | class Finished(ExecutionEvent):
143 | """The final event of the run.
144 |
145 | No more events after this point.
146 | """
147 |
148 | passed_count: int = attr.ib() # pragma: no mutate
149 | failed_count: int = attr.ib() # pragma: no mutate
150 | errored_count: int = attr.ib() # pragma: no mutate
151 |
152 | has_failures: bool = attr.ib() # pragma: no mutate
153 | has_errors: bool = attr.ib() # pragma: no mutate
154 | has_logs: bool = attr.ib() # pragma: no mutate
155 | is_empty: bool = attr.ib() # pragma: no mutate
156 |
157 | total: Dict[str, Dict[Union[str, Status], int]] = attr.ib() # pragma: no mutate
158 |
159 | # Total test run execution time
160 | running_time: float = attr.ib() # pragma: no mutate
161 |
162 | @classmethod
163 | def from_results(cls, results: TestResultSet, running_time: float) -> "Finished":
164 | return cls(
165 | passed_count=results.passed_count,
166 | failed_count=results.failed_count,
167 | errored_count=results.errored_count,
168 | has_failures=results.has_failures,
169 | has_errors=results.has_errors,
170 | has_logs=results.has_logs,
171 | is_empty=results.is_empty,
172 | total=results.total,
173 | running_time=running_time,
174 | )
175 |
--------------------------------------------------------------------------------
/schemathesis/runner/impl/__init__.py:
--------------------------------------------------------------------------------
1 | from .core import BaseRunner
2 | from .solo import SingleThreadASGIRunner, SingleThreadRunner, SingleThreadWSGIRunner
3 | from .threadpool import ThreadPoolASGIRunner, ThreadPoolRunner, ThreadPoolWSGIRunner
4 |
--------------------------------------------------------------------------------
/schemathesis/runner/impl/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/runner/impl/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/runner/impl/__pycache__/core.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/runner/impl/__pycache__/core.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/runner/impl/__pycache__/solo.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/runner/impl/__pycache__/solo.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/runner/impl/__pycache__/threadpool.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/runner/impl/__pycache__/threadpool.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/runner/impl/solo.py:
--------------------------------------------------------------------------------
1 | # weird mypy bug with imports
2 | from typing import Any, Dict, Generator, Union # pylint: disable=unused-import
3 |
4 | import attr
5 |
6 | from ...models import TestResultSet
7 | from ...utils import get_requests_auth
8 | from .. import events
9 | from .core import BaseRunner, asgi_test, get_session, network_test, wsgi_test
10 |
11 |
12 | @attr.s(slots=True) # pragma: no mutate
13 | class SingleThreadRunner(BaseRunner):
14 | """Fast runner that runs tests sequentially in the main thread."""
15 |
16 | request_tls_verify: Union[bool, str] = attr.ib(default=True) # pragma: no mutate
17 |
18 | def _execute(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
19 | auth = get_requests_auth(self.auth, self.auth_type)
20 | with get_session(auth) as session:
21 | yield from self._run_tests(
22 | self.schema.get_all_tests,
23 | network_test,
24 | self.hypothesis_settings,
25 | self.seed,
26 | checks=self.checks,
27 | max_response_time=self.max_response_time,
28 | targets=self.targets,
29 | results=results,
30 | session=session,
31 | headers=self.headers,
32 | request_timeout=self.request_timeout,
33 | request_tls_verify=self.request_tls_verify,
34 | store_interactions=self.store_interactions,
35 | )
36 |
37 |
38 | @attr.s(slots=True) # pragma: no mutate
39 | class SingleThreadWSGIRunner(SingleThreadRunner):
40 | def _execute(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
41 | yield from self._run_tests(
42 | self.schema.get_all_tests,
43 | wsgi_test,
44 | self.hypothesis_settings,
45 | self.seed,
46 | checks=self.checks,
47 | max_response_time=self.max_response_time,
48 | targets=self.targets,
49 | results=results,
50 | auth=self.auth,
51 | auth_type=self.auth_type,
52 | headers=self.headers,
53 | store_interactions=self.store_interactions,
54 | )
55 |
56 |
57 | @attr.s(slots=True) # pragma: no mutate
58 | class SingleThreadASGIRunner(SingleThreadRunner):
59 | def _execute(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
60 | yield from self._run_tests(
61 | self.schema.get_all_tests,
62 | asgi_test,
63 | self.hypothesis_settings,
64 | self.seed,
65 | checks=self.checks,
66 | max_response_time=self.max_response_time,
67 | targets=self.targets,
68 | results=results,
69 | headers=self.headers,
70 | store_interactions=self.store_interactions,
71 | )
72 |
--------------------------------------------------------------------------------
/schemathesis/runner/serialization.py:
--------------------------------------------------------------------------------
1 | """Transformation from Schemathesis-specific data structures to ones that can be serialized and sent over network.
2 |
3 | They all consist of primitive types and don't have references to schemas, app, etc.
4 | """
5 | import logging
6 | from typing import Any, Dict, List, Optional
7 |
8 | import attr
9 |
10 | from ..models import Case, Check, Interaction, Request, Response, Status, TestResult
11 | from ..utils import format_exception
12 |
13 |
14 | @attr.s(slots=True) # pragma: no mutate
15 | class SerializedCase:
16 | text_lines: List[str] = attr.ib() # pragma: no mutate
17 | requests_code: str = attr.ib()
18 |
19 | @classmethod
20 | def from_case(cls, case: Case, headers: Optional[Dict[str, Any]]) -> "SerializedCase":
21 | return cls(
22 | text_lines=case.as_text_lines(),
23 | requests_code=case.get_code_to_reproduce(headers),
24 | )
25 |
26 |
27 | @attr.s(slots=True) # pragma: no mutate
28 | class SerializedCheck:
29 | name: str = attr.ib() # pragma: no mutate
30 | value: Status = attr.ib() # pragma: no mutate
31 | example: Optional[SerializedCase] = attr.ib(default=None) # pragma: no mutate
32 | message: Optional[str] = attr.ib(default=None) # pragma: no mutate
33 |
34 | @classmethod
35 | def from_check(cls, check: Check, headers: Optional[Dict[str, Any]]) -> "SerializedCheck":
36 | return SerializedCheck(
37 | name=check.name,
38 | value=check.value,
39 | example=SerializedCase.from_case(check.example, headers) if check.example else None,
40 | message=check.message,
41 | )
42 |
43 |
44 | @attr.s(slots=True) # pragma: no mutate
45 | class SerializedError:
46 | exception: str = attr.ib() # pragma: no mutate
47 | exception_with_traceback: str = attr.ib() # pragma: no mutate
48 | example: Optional[SerializedCase] = attr.ib() # pragma: no mutate
49 |
50 | @classmethod
51 | def from_error(
52 | cls, exception: Exception, case: Optional[Case], headers: Optional[Dict[str, Any]]
53 | ) -> "SerializedError":
54 | return cls(
55 | exception=format_exception(exception),
56 | exception_with_traceback=format_exception(exception, True),
57 | example=SerializedCase.from_case(case, headers) if case else None,
58 | )
59 |
60 |
61 | @attr.s(slots=True) # pragma: no mutate
62 | class SerializedInteraction:
63 | request: Request = attr.ib() # pragma: no mutate
64 | response: Response = attr.ib() # pragma: no mutate
65 | checks: List[SerializedCheck] = attr.ib() # pragma: no mutate
66 | status: Status = attr.ib() # pragma: no mutate
67 | recorded_at: str = attr.ib() # pragma: no mutate
68 |
69 | @classmethod
70 | def from_interaction(cls, interaction: Interaction, headers: Optional[Dict[str, Any]]) -> "SerializedInteraction":
71 | return cls(
72 | request=interaction.request,
73 | response=interaction.response,
74 | checks=[SerializedCheck.from_check(check, headers) for check in interaction.checks],
75 | status=interaction.status,
76 | recorded_at=interaction.recorded_at,
77 | )
78 |
79 |
80 | @attr.s(slots=True) # pragma: no mutate
81 | class SerializedTestResult:
82 | method: str = attr.ib() # pragma: no mutate
83 | path: str = attr.ib() # pragma: no mutate
84 | has_failures: bool = attr.ib() # pragma: no mutate
85 | has_errors: bool = attr.ib() # pragma: no mutate
86 | has_logs: bool = attr.ib() # pragma: no mutate
87 | is_errored: bool = attr.ib() # pragma: no mutate
88 | seed: Optional[int] = attr.ib() # pragma: no mutate
89 | data_generation_method: str = attr.ib() # pragma: no mutate
90 | checks: List[SerializedCheck] = attr.ib() # pragma: no mutate
91 | logs: List[str] = attr.ib() # pragma: no mutate
92 | errors: List[SerializedError] = attr.ib() # pragma: no mutate
93 | interactions: List[SerializedInteraction] = attr.ib() # pragma: no mutate
94 |
95 | @classmethod
96 | def from_test_result(cls, result: TestResult) -> "SerializedTestResult":
97 | formatter = logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")
98 | return SerializedTestResult(
99 | method=result.endpoint.method.upper(),
100 | path=result.endpoint.full_path,
101 | has_failures=result.has_failures,
102 | has_errors=result.has_errors,
103 | has_logs=result.has_logs,
104 | is_errored=result.is_errored,
105 | seed=result.seed,
106 | data_generation_method=result.data_generation_method.as_short_name(),
107 | checks=[SerializedCheck.from_check(check, headers=result.overridden_headers) for check in result.checks],
108 | logs=[formatter.format(record) for record in result.logs],
109 | errors=[SerializedError.from_error(*error, headers=result.overridden_headers) for error in result.errors],
110 | interactions=[
111 | SerializedInteraction.from_interaction(interaction, headers=result.overridden_headers)
112 | for interaction in result.interactions
113 | ],
114 | )
115 |
--------------------------------------------------------------------------------
/schemathesis/serializers.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING, Any, Callable, Collection, Dict, Optional, Type
2 |
3 | import attr
4 | from typing_extensions import Protocol, runtime_checkable
5 |
6 | from .utils import is_json_media_type
7 |
8 | if TYPE_CHECKING:
9 | from .models import Case
10 |
11 |
12 | SERIALIZERS = {}
13 |
14 |
15 | @attr.s(slots=True) # pragma: no mutate
16 | class SerializerContext:
17 | """The context for serialization process."""
18 |
19 | case: "Case" = attr.ib() # pragma: no mutate
20 |
21 |
22 | @runtime_checkable
23 | class Serializer(Protocol):
24 | """Transform generated data to a form supported by the transport layer.
25 |
26 | For example, to handle multipart payloads, we need to serialize them differently for
27 | `requests` and `werkzeug` transports.
28 | """
29 |
30 | def as_requests(self, context: SerializerContext, payload: Any) -> Any:
31 | raise NotImplementedError
32 |
33 | def as_werkzeug(self, context: SerializerContext, payload: Any) -> Any:
34 | raise NotImplementedError
35 |
36 |
37 | def register(media_type: str, *, aliases: Collection[str] = ()) -> Callable[[Type[Serializer]], Type[Serializer]]:
38 | """Register a serializer for the given media type.
39 |
40 | Schemathesis uses ``requests`` for regular network calls and ``werkzeug`` for WSGI applications. Your serializer
41 | should have two methods, ``as_requests`` and ``as_werkzeug``, providing keyword arguments that Schemathesis will
42 | pass to ``requests.request`` and ``werkzeug.Client.open`` respectively.
43 |
44 | Example:
45 | @register("text/csv")
46 | class CSVSerializer:
47 |
48 | def as_requests(self, context, value):
49 | return {"data": to_csv(value)}
50 |
51 | def as_werkzeug(self, context, value):
52 | return {"data": to_csv(value)}
53 |
54 | The primary purpose of serializers is to transform data from its Python representation to the format suitable
55 | for making an API call. The generated data structure depends on your schema, but its type matches
56 | Python equivalents to the JSON Schema types.
57 |
58 | """
59 |
60 | def wrapper(serializer: Type[Serializer]) -> Type[Serializer]:
61 | if not issubclass(serializer, Serializer):
62 | raise TypeError(
63 | f"`{serializer.__name__}` is not a valid serializer. "
64 | f"Check `schemathesis.serializers.Serializer` documentation for examples."
65 | )
66 | SERIALIZERS[media_type] = serializer
67 | for alias in aliases:
68 | SERIALIZERS[alias] = serializer
69 | return serializer
70 |
71 | return wrapper
72 |
73 |
74 | def unregister(media_type: str) -> None:
75 | """Remove registered serializer for the given media type."""
76 | del SERIALIZERS[media_type]
77 |
78 |
79 | def _to_json(value: Any) -> Dict[str, Any]:
80 | if value is None:
81 | # If the body is `None`, then the app expects `null`, but `None` is also the default value for the `json`
82 | # argument in `requests.request` and `werkzeug.Client.open` which makes these cases indistinguishable.
83 | # Therefore we explicitly create such payload
84 | return {"data": b"null"}
85 | return {"json": value}
86 |
87 |
88 | @register("application/json")
89 | class JSONSerializer:
90 | def as_requests(self, context: SerializerContext, value: Any) -> Any:
91 | return _to_json(value)
92 |
93 | def as_werkzeug(self, context: SerializerContext, value: Any) -> Any:
94 | return _to_json(value)
95 |
96 |
97 | def _should_coerce_to_bytes(item: Any) -> bool:
98 | """Whether the item should be converted to bytes."""
99 | # These types are OK in forms, others should be coerced to bytes
100 | return not isinstance(item, (bytes, str, int))
101 |
102 |
103 | def prepare_form_data(data: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
104 | """Make the generated data suitable for sending as multipart.
105 |
106 | If the schema is loose, Schemathesis can generate data that can't be sent as multipart. In these cases,
107 | we convert it to bytes and send it as-is, ignoring any conversion errors.
108 |
109 | NOTE. This behavior might change in the future.
110 | """
111 | # Form data can be optional
112 | if data is not None:
113 | for name, value in data.items():
114 | if isinstance(value, list):
115 | data[name] = [to_bytes(item) if _should_coerce_to_bytes(item) else item for item in value]
116 | elif _should_coerce_to_bytes(value):
117 | data[name] = to_bytes(value)
118 | return data
119 |
120 |
121 | def to_bytes(value: Any) -> bytes:
122 | """Convert the input value to bytes and ignore any conversion errors."""
123 | return str(value).encode(errors="ignore")
124 |
125 |
126 | @register("multipart/form-data")
127 | class MultipartSerializer:
128 | def as_requests(self, context: SerializerContext, value: Optional[Dict[str, Any]]) -> Any:
129 | # Form data always is generated as a dictionary
130 | multipart = prepare_form_data(value)
131 | files, data = context.case.endpoint.prepare_multipart(multipart)
132 | return {"files": files, "data": data}
133 |
134 | def as_werkzeug(self, context: SerializerContext, value: Any) -> Any:
135 | return {"data": value}
136 |
137 |
138 | @register("application/x-www-form-urlencoded")
139 | class URLEncodedFormSerializer:
140 | def as_requests(self, context: SerializerContext, value: Any) -> Any:
141 | return {"data": value}
142 |
143 | def as_werkzeug(self, context: SerializerContext, value: Any) -> Any:
144 | return {"data": value}
145 |
146 |
147 | @register("text/plain")
148 | class TextSerializer:
149 | def as_requests(self, context: SerializerContext, value: Any) -> Any:
150 | return {"data": str(value).encode("utf8")}
151 |
152 | def as_werkzeug(self, context: SerializerContext, value: Any) -> Any:
153 | return {"data": str(value)}
154 |
155 |
156 | @register("application/octet-stream")
157 | class OctetStreamSerializer:
158 | def as_requests(self, context: SerializerContext, value: Any) -> Any:
159 | return {"data": value}
160 |
161 | def as_werkzeug(self, context: SerializerContext, value: Any) -> Any:
162 | return {"data": value}
163 |
164 |
165 | def get(media_type: str) -> Optional[Type[Serializer]]:
166 | """Get an appropriate serializer for the given media type."""
167 | if is_json_media_type(media_type):
168 | media_type = "application/json"
169 | return SERIALIZERS.get(media_type)
170 |
--------------------------------------------------------------------------------
/schemathesis/specs/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/__init__.py
--------------------------------------------------------------------------------
/schemathesis/specs/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/graphql/__init__.py:
--------------------------------------------------------------------------------
1 | from .loaders import from_dict, from_url
2 |
--------------------------------------------------------------------------------
/schemathesis/specs/graphql/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/graphql/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/graphql/__pycache__/loaders.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/graphql/__pycache__/loaders.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/graphql/__pycache__/schemas.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/graphql/__pycache__/schemas.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/graphql/loaders.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict, Optional
2 |
3 | import requests
4 |
5 | from ...hooks import HookContext, dispatch
6 | from .schemas import GraphQLSchema
7 |
8 | INTROSPECTION_QUERY = """
9 | query IntrospectionQuery {
10 | __schema {
11 | queryType { name }
12 | mutationType { name }
13 | subscriptionType { name }
14 | types {
15 | ...FullType
16 | }
17 | directives {
18 | name
19 | locations
20 | args {
21 | ...InputValue
22 | }
23 | }
24 | }
25 | }
26 | fragment FullType on __Type {
27 | kind
28 | name
29 | fields(includeDeprecated: true) {
30 | name
31 | args {
32 | ...InputValue
33 | }
34 | type {
35 | ...TypeRef
36 | }
37 | isDeprecated
38 | deprecationReason
39 | }
40 | inputFields {
41 | ...InputValue
42 | }
43 | interfaces {
44 | ...TypeRef
45 | }
46 | enumValues(includeDeprecated: true) {
47 | name
48 | isDeprecated
49 | deprecationReason
50 | }
51 | possibleTypes {
52 | ...TypeRef
53 | }
54 | }
55 | fragment InputValue on __InputValue {
56 | name
57 | type { ...TypeRef }
58 | defaultValue
59 | }
60 | fragment TypeRef on __Type {
61 | kind
62 | name
63 | ofType {
64 | kind
65 | name
66 | ofType {
67 | kind
68 | name
69 | ofType {
70 | kind
71 | name
72 | ofType {
73 | kind
74 | name
75 | ofType {
76 | kind
77 | name
78 | ofType {
79 | kind
80 | name
81 | ofType {
82 | kind
83 | name
84 | }
85 | }
86 | }
87 | }
88 | }
89 | }
90 | }
91 | }"""
92 |
93 |
94 | def from_url(url: str) -> GraphQLSchema:
95 | response = requests.post(url, json={"query": INTROSPECTION_QUERY})
96 | decoded = response.json()
97 | return from_dict(raw_schema=decoded["data"], location=url)
98 |
99 |
100 | def from_dict(raw_schema: Dict[str, Any], location: Optional[str] = None) -> GraphQLSchema:
101 | dispatch("before_load_schema", HookContext(), raw_schema)
102 | return GraphQLSchema(raw_schema, location=location) # type: ignore
103 |
--------------------------------------------------------------------------------
/schemathesis/specs/graphql/schemas.py:
--------------------------------------------------------------------------------
1 | from functools import partial
2 | from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, cast
3 | from urllib.parse import urlsplit
4 |
5 | import attr
6 | import graphql
7 | from hypothesis import strategies as st
8 | from hypothesis.strategies import SearchStrategy
9 | from hypothesis_graphql import strategies as gql_st
10 |
11 | from ... import DataGenerationMethod
12 | from ...checks import not_a_server_error
13 | from ...hooks import HookDispatcher
14 | from ...models import Case, CheckFunction, Endpoint
15 | from ...schemas import BaseSchema
16 | from ...stateful import Feedback
17 | from ...utils import GenericResponse
18 |
19 |
20 | @attr.s() # pragma: no mutate
21 | class GraphQLCase(Case):
22 | def as_requests_kwargs(
23 | self, base_url: Optional[str] = None, headers: Optional[Dict[str, str]] = None
24 | ) -> Dict[str, Any]:
25 | final_headers = self._get_headers(headers)
26 | base_url = self._get_base_url(base_url)
27 | return {"method": self.method, "url": base_url, "json": {"query": self.body}, "headers": final_headers}
28 |
29 | def as_werkzeug_kwargs(self, headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
30 | final_headers = self._get_headers(headers)
31 | return {
32 | "method": self.method,
33 | "path": self.endpoint.schema.get_full_path(self.formatted_path),
34 | "headers": final_headers,
35 | "query_string": self.query,
36 | "json": {"query": self.body},
37 | }
38 |
39 | def validate_response(
40 | self,
41 | response: GenericResponse,
42 | checks: Tuple[CheckFunction, ...] = (),
43 | additional_checks: Tuple[CheckFunction, ...] = (),
44 | ) -> None:
45 | checks = checks or (not_a_server_error,)
46 | checks += additional_checks
47 | return super().validate_response(response, checks)
48 |
49 |
50 | @attr.s() # pragma: no mutate
51 | class GraphQLSchema(BaseSchema):
52 | def get_full_path(self, path: str) -> str:
53 | return self.base_path
54 |
55 | @property # pragma: no mutate
56 | def verbose_name(self) -> str:
57 | return "GraphQL"
58 |
59 | @property
60 | def base_path(self) -> str:
61 | if self.base_url:
62 | return urlsplit(self.base_url).path
63 | return self._get_base_path()
64 |
65 | def _get_base_path(self) -> str:
66 | return cast(str, urlsplit(self.location).path)
67 |
68 | def get_all_endpoints(self) -> Generator[Endpoint, None, None]:
69 | yield Endpoint(
70 | base_url=self.location, path=self.base_path, method="POST", schema=self, definition=None # type: ignore
71 | )
72 |
73 | def get_case_strategy(
74 | self,
75 | endpoint: Endpoint,
76 | hooks: Optional[HookDispatcher] = None,
77 | feedback: Optional[Feedback] = None,
78 | data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
79 | ) -> SearchStrategy:
80 | constructor = partial(GraphQLCase, endpoint=endpoint)
81 | schema = graphql.build_client_schema(self.raw_schema)
82 | return st.builds(constructor, body=gql_st.query(schema))
83 |
84 | def get_strategies_from_examples(self, endpoint: Endpoint) -> List[SearchStrategy[Case]]:
85 | return []
86 |
87 | def get_parameter_serializer(self, endpoint: Endpoint, location: str) -> Optional[Callable]:
88 | return None
89 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/__init__.py
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/__pycache__/_hypothesis.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/__pycache__/_hypothesis.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/__pycache__/checks.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/__pycache__/checks.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/__pycache__/constants.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/__pycache__/constants.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/__pycache__/converter.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/__pycache__/converter.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/__pycache__/definitions.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/__pycache__/definitions.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/__pycache__/examples.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/__pycache__/examples.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/__pycache__/filters.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/__pycache__/filters.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/__pycache__/links.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/__pycache__/links.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/__pycache__/parameters.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/__pycache__/parameters.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/__pycache__/references.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/__pycache__/references.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/__pycache__/schemas.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/__pycache__/schemas.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/__pycache__/security.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/__pycache__/security.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/__pycache__/serialization.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/__pycache__/serialization.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/__pycache__/utils.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/__pycache__/utils.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/_hypothesis.py:
--------------------------------------------------------------------------------
1 | import re
2 | from base64 import b64encode
3 | from typing import Any, Callable, Dict, Optional, Tuple
4 | from urllib.parse import quote_plus
5 |
6 | from hypothesis import strategies as st
7 | from hypothesis_jsonschema import from_schema
8 | from requests.auth import _basic_auth_str
9 |
10 | from ... import utils
11 | from ...constants import DataGenerationMethod
12 | from ...exceptions import InvalidSchema
13 | from ...hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher
14 | from ...models import Case, Endpoint
15 | from ...stateful import Feedback
16 | from ...utils import NOT_SET
17 | from .constants import LOCATION_TO_CONTAINER
18 | from .parameters import OpenAPIParameter, parameters_to_json_schema
19 |
20 | PARAMETERS = frozenset(("path_parameters", "headers", "cookies", "query", "body"))
21 | SLASH = "/"
22 | STRING_FORMATS = {}
23 |
24 |
25 | def register_string_format(name: str, strategy: st.SearchStrategy) -> None:
26 | """Register a new strategy for generating data for specific string "format"."""
27 | if not isinstance(name, str):
28 | raise TypeError(f"name must be of type {str}, not {type(name)}")
29 | if not isinstance(strategy, st.SearchStrategy):
30 | raise TypeError(f"strategy must be of type {st.SearchStrategy}, not {type(strategy)}")
31 |
32 | STRING_FORMATS[name] = strategy
33 |
34 |
35 | def init_default_strategies() -> None:
36 | """Register all default "format" strategies."""
37 | register_string_format("binary", st.binary())
38 | register_string_format("byte", st.binary().map(lambda x: b64encode(x).decode()))
39 |
40 | def make_basic_auth_str(item: Tuple[str, str]) -> str:
41 | return _basic_auth_str(*item)
42 |
43 | latin1_text = st.text(alphabet=st.characters(min_codepoint=0, max_codepoint=255))
44 |
45 | register_string_format("_basic_auth", st.tuples(latin1_text, latin1_text).map(make_basic_auth_str)) # type: ignore
46 | register_string_format("_bearer_auth", st.text().map("Bearer {}".format))
47 |
48 |
49 | def is_valid_header(headers: Dict[str, Any]) -> bool:
50 | """Verify if the generated headers are valid."""
51 | for name, value in headers.items():
52 | if not utils.is_latin_1_encodable(value):
53 | return False
54 | if utils.has_invalid_characters(name, value):
55 | return False
56 | return True
57 |
58 |
59 | def is_illegal_surrogate(item: Any) -> bool:
60 | return isinstance(item, str) and bool(re.search(r"[\ud800-\udfff]", item))
61 |
62 |
63 | def is_valid_query(query: Dict[str, Any]) -> bool:
64 | """Surrogates are not allowed in a query string.
65 |
66 | `requests` and `werkzeug` will fail to send it to the application.
67 | """
68 | for name, value in query.items():
69 | if is_illegal_surrogate(name) or is_illegal_surrogate(value):
70 | return False
71 | return True
72 |
73 |
74 | @st.composite # type: ignore
75 | def get_case_strategy( # pylint: disable=too-many-locals
76 | draw: Callable,
77 | endpoint: Endpoint,
78 | hooks: Optional[HookDispatcher] = None,
79 | feedback: Optional[Feedback] = None,
80 | data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
81 | path_parameters: Any = NOT_SET,
82 | headers: Any = NOT_SET,
83 | cookies: Any = NOT_SET,
84 | query: Any = NOT_SET,
85 | body: Any = NOT_SET,
86 | ) -> Any:
87 | """A strategy that creates `Case` instances.
88 |
89 | Explicit `path_parameters`, `headers`, `cookies`, `query`, `body` arguments will be used in the resulting `Case`
90 | object.
91 | """
92 | to_strategy = {DataGenerationMethod.positive: make_positive_strategy}[data_generation_method]
93 |
94 | context = HookContext(endpoint)
95 |
96 | if path_parameters is NOT_SET:
97 | strategy = get_parameters_strategy(endpoint, to_strategy, "path")
98 | strategy = apply_hooks(endpoint, context, hooks, strategy, "path")
99 | path_parameters = draw(strategy)
100 | if headers is NOT_SET:
101 | strategy = get_parameters_strategy(endpoint, to_strategy, "header")
102 | strategy = apply_hooks(endpoint, context, hooks, strategy, "header")
103 | headers = draw(strategy)
104 | if cookies is NOT_SET:
105 | strategy = get_parameters_strategy(endpoint, to_strategy, "cookie")
106 | strategy = apply_hooks(endpoint, context, hooks, strategy, "cookie")
107 | cookies = draw(strategy)
108 | if query is NOT_SET:
109 | strategy = get_parameters_strategy(endpoint, to_strategy, "query")
110 | strategy = apply_hooks(endpoint, context, hooks, strategy, "query")
111 | query = draw(strategy)
112 |
113 | media_type = None
114 | if body is NOT_SET:
115 | if endpoint.body:
116 | parameter = draw(st.sampled_from(endpoint.body.items))
117 | strategy = _get_body_strategy(parameter, to_strategy)
118 | media_type = parameter.media_type
119 | body = draw(strategy)
120 | else:
121 | media_types = endpoint.get_request_payload_content_types() or ["application/json"]
122 | # Take the first available media type.
123 | # POSSIBLE IMPROVEMENT:
124 | # - Test examples for each available media type on Open API 2.0;
125 | # - On Open API 3.0, media types are explicit, and each example has it. We can pass `OpenAPIBody.media_type`
126 | # here from the examples handling code.
127 | media_type = media_types[0]
128 | if endpoint.schema.validate_schema and endpoint.method.upper() == "GET" and endpoint.body:
129 | raise InvalidSchema("Body parameters are defined for GET request.")
130 | return Case(
131 | endpoint=endpoint,
132 | feedback=feedback,
133 | media_type=media_type,
134 | path_parameters=path_parameters,
135 | headers=headers,
136 | cookies=cookies,
137 | query=query,
138 | body=body,
139 | )
140 |
141 |
142 | def _get_body_strategy(
143 | parameter: OpenAPIParameter, to_strategy: Callable[[Dict[str, Any]], st.SearchStrategy]
144 | ) -> st.SearchStrategy:
145 | schema = parameter.as_json_schema()
146 | strategy = to_strategy(schema)
147 | if not parameter.is_required:
148 | strategy |= st.just(NOT_SET)
149 | return strategy
150 |
151 |
152 | def get_parameters_strategy(
153 | endpoint: Endpoint, to_strategy: Callable[[Dict[str, Any]], st.SearchStrategy], location: str
154 | ) -> st.SearchStrategy:
155 | """Create a new strategy for the case's component from the endpoint parameters."""
156 | parameters = getattr(endpoint, LOCATION_TO_CONTAINER[location])
157 | if parameters:
158 | schema = parameters_to_json_schema(parameters)
159 | strategy = to_strategy(schema)
160 | serialize = endpoint.get_parameter_serializer(location)
161 | if serialize is not None:
162 | strategy = strategy.map(serialize)
163 | filter_func = {
164 | "path": is_valid_path,
165 | "header": is_valid_header,
166 | "cookie": is_valid_header,
167 | "query": is_valid_query,
168 | }[location]
169 | strategy = strategy.filter(filter_func)
170 | map_func = {"path": quote_all}.get(location)
171 | if map_func:
172 | strategy = strategy.map(map_func)
173 | return strategy
174 | # No parameters defined for this location
175 | return st.none()
176 |
177 |
178 | def make_positive_strategy(schema: Dict[str, Any]) -> st.SearchStrategy:
179 | return from_schema(schema, custom_formats=STRING_FORMATS)
180 |
181 |
182 | def is_valid_path(parameters: Dict[str, Any]) -> bool:
183 | """Single "." chars and empty strings "" are excluded from path by urllib3.
184 |
185 | A path containing to "/" or "%2F" will lead to ambiguous path resolution in
186 | many frameworks and libraries, such behaviour have been observed in both
187 | WSGI and ASGI applications.
188 |
189 | In this case one variable in the path template will be empty, which will lead to 404 in most of the cases.
190 | Because of it this case doesn't bring much value and might lead to false positives results of Schemathesis runs.
191 | """
192 |
193 | path_parameter_blacklist = (".", SLASH, "")
194 |
195 | return not any(
196 | (value in path_parameter_blacklist or is_illegal_surrogate(value) or isinstance(value, str) and SLASH in value)
197 | for value in parameters.values()
198 | )
199 |
200 |
201 | def quote_all(parameters: Dict[str, Any]) -> Dict[str, Any]:
202 | """Apply URL quotation for all values in a dictionary."""
203 | return {key: quote_plus(value) if isinstance(value, str) else value for key, value in parameters.items()}
204 |
205 |
206 | def apply_hooks(
207 | endpoint: Endpoint,
208 | context: HookContext,
209 | hooks: Optional[HookDispatcher],
210 | strategy: st.SearchStrategy[Case],
211 | location: str,
212 | ) -> st.SearchStrategy[Case]:
213 | """Apply all `before_generate_` hooks related to the given location."""
214 | strategy = _apply_hooks(context, GLOBAL_HOOK_DISPATCHER, strategy, location)
215 | strategy = _apply_hooks(context, endpoint.schema.hooks, strategy, location)
216 | if hooks is not None:
217 | strategy = _apply_hooks(context, hooks, strategy, location)
218 | return strategy
219 |
220 |
221 | def _apply_hooks(
222 | context: HookContext, hooks: HookDispatcher, strategy: st.SearchStrategy[Case], location: str
223 | ) -> st.SearchStrategy[Case]:
224 | """Apply all `before_generate_` hooks related to the given location & dispatcher."""
225 | container = LOCATION_TO_CONTAINER[location]
226 | for hook in hooks.get_all_by_name(f"before_generate_{container}"):
227 | strategy = hook(context, strategy)
228 | return strategy
229 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/checks.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING, Any, Dict, Generator, Optional, Union
2 |
3 | from ...exceptions import (
4 | get_headers_error,
5 | get_malformed_media_type_error,
6 | get_missing_content_type_error,
7 | get_response_type_error,
8 | get_status_code_error,
9 | )
10 | from ...utils import GenericResponse, are_content_types_equal, parse_content_type
11 | from .schemas import BaseOpenAPISchema
12 | from .utils import expand_status_code
13 |
14 | if TYPE_CHECKING:
15 | from ...models import Case
16 |
17 |
18 | def status_code_conformance(response: GenericResponse, case: "Case") -> Optional[bool]:
19 | if not isinstance(case.endpoint.schema, BaseOpenAPISchema):
20 | raise TypeError("This check can be used only with Open API schemas")
21 | responses = case.endpoint.definition.raw.get("responses", {})
22 | # "default" can be used as the default response object for all HTTP codes that are not covered individually
23 | if "default" in responses:
24 | return None
25 | allowed_response_statuses = list(_expand_responses(responses))
26 | if response.status_code not in allowed_response_statuses:
27 | responses_list = ", ".join(map(str, responses))
28 | message = (
29 | f"Received a response with a status code, which is not defined in the schema: "
30 | f"{response.status_code}\n\nDeclared status codes: {responses_list}"
31 | )
32 | exc_class = get_status_code_error(response.status_code)
33 | raise exc_class(message)
34 | return None # explicitly return None for mypy
35 |
36 |
37 | def _expand_responses(responses: Dict[Union[str, int], Any]) -> Generator[int, None, None]:
38 | for code in responses:
39 | yield from expand_status_code(code)
40 |
41 |
42 | def content_type_conformance(response: GenericResponse, case: "Case") -> Optional[bool]:
43 | if not isinstance(case.endpoint.schema, BaseOpenAPISchema):
44 | raise TypeError("This check can be used only with Open API schemas")
45 | content_types = case.endpoint.schema.get_content_types(case.endpoint, response)
46 | if not content_types:
47 | return None
48 | content_type = response.headers.get("Content-Type")
49 | if not content_type:
50 | raise get_missing_content_type_error()("Response is missing the `Content-Type` header")
51 | for option in content_types:
52 | try:
53 | if are_content_types_equal(option, content_type):
54 | return None
55 | except ValueError as exc:
56 | raise get_malformed_media_type_error(str(exc))(str(exc)) from exc
57 | expected_main, expected_sub = parse_content_type(option)
58 | received_main, received_sub = parse_content_type(content_type)
59 | exc_class = get_response_type_error(f"{expected_main}_{expected_sub}", f"{received_main}_{received_sub}")
60 | raise exc_class(
61 | f"Received a response with '{content_type}' Content-Type, "
62 | f"but it is not declared in the schema.\n\n"
63 | f"Defined content types: {', '.join(content_types)}"
64 | )
65 |
66 |
67 | def response_headers_conformance(response: GenericResponse, case: "Case") -> Optional[bool]:
68 | if not isinstance(case.endpoint.schema, BaseOpenAPISchema):
69 | raise TypeError("This check can be used only with Open API schemas")
70 | defined_headers = case.endpoint.schema.get_headers(case.endpoint, response)
71 | if not defined_headers:
72 | return None
73 |
74 | missing_headers = [header for header in defined_headers if header not in response.headers]
75 | if not missing_headers:
76 | return None
77 | message = ",".join(missing_headers)
78 | exc_class = get_headers_error(message)
79 | raise exc_class(f"Received a response with missing headers: {message}")
80 |
81 |
82 | def response_schema_conformance(response: GenericResponse, case: "Case") -> None:
83 | if not isinstance(case.endpoint.schema, BaseOpenAPISchema):
84 | raise TypeError("This check can be used only with Open API schemas")
85 | return case.endpoint.validate_response(response)
86 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/constants.py:
--------------------------------------------------------------------------------
1 | LOCATION_TO_CONTAINER = {
2 | "path": "path_parameters",
3 | "query": "query",
4 | "header": "headers",
5 | "cookie": "cookies",
6 | "body": "body",
7 | }
8 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/converter.py:
--------------------------------------------------------------------------------
1 | from copy import deepcopy
2 | from typing import Any, Dict
3 |
4 | from ...utils import traverse_schema
5 |
6 |
7 | def to_json_schema(schema: Dict[str, Any], nullable_name: str) -> Dict[str, Any]:
8 | """Convert Open API parameters to JSON Schema.
9 |
10 | NOTE. This function is applied to all keywords (including nested) during a schema resolving, thus it is not recursive.
11 | See a recursive version below.
12 | """
13 | schema = deepcopy(schema)
14 | if schema.get(nullable_name) is True:
15 | del schema[nullable_name]
16 | schema = {"anyOf": [schema, {"type": "null"}]}
17 | if schema.get("type") == "file":
18 | schema["type"] = "string"
19 | schema["format"] = "binary"
20 | return schema
21 |
22 |
23 | def to_json_schema_recursive(schema: Dict[str, Any], nullable_name: str) -> Dict[str, Any]:
24 | return traverse_schema(schema, to_json_schema, nullable_name)
25 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/examples.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict, List
2 |
3 | from hypothesis.strategies import SearchStrategy
4 |
5 | from ...models import Case, Endpoint
6 | from ._hypothesis import PARAMETERS, get_case_strategy
7 | from .constants import LOCATION_TO_CONTAINER
8 |
9 |
10 | def get_object_example_from_properties(object_schema: Dict[str, Any]) -> Dict[str, Any]:
11 | return {
12 | prop_name: prop["example"]
13 | for prop_name, prop in object_schema.get("properties", {}).items()
14 | if "example" in prop
15 | }
16 |
17 |
18 | def get_parameter_examples(endpoint_def: Dict[str, Any], examples_field: str) -> List[Dict[str, Any]]:
19 | """Gets parameter examples from OAS3 `examples` keyword or `x-examples` for Swagger 2."""
20 | return [
21 | {
22 | "type": LOCATION_TO_CONTAINER.get(parameter["in"]),
23 | "name": parameter["name"],
24 | "examples": [example["value"] for example in parameter[examples_field].values() if "value" in example],
25 | }
26 | for parameter in endpoint_def.get("parameters", [])
27 | if examples_field in parameter
28 | ]
29 |
30 |
31 | def get_parameter_example_from_properties(endpoint_def: Dict[str, Any]) -> Dict[str, Any]:
32 | static_parameters: Dict[str, Any] = {}
33 | for parameter in endpoint_def.get("parameters", []):
34 | parameter_schema = parameter["schema"] if "schema" in parameter else parameter
35 | example = get_object_example_from_properties(parameter_schema)
36 | if example:
37 | parameter_type = LOCATION_TO_CONTAINER[parameter["in"]]
38 | if parameter_type not in ("body", "form_data"):
39 | if parameter_type not in static_parameters:
40 | static_parameters[parameter_type] = {}
41 | static_parameters[parameter_type][parameter["name"]] = example
42 | else:
43 | # swagger 2 body and formData parameters should not include parameter names
44 | static_parameters[parameter_type] = example
45 | return static_parameters
46 |
47 |
48 | def get_request_body_examples(endpoint_def: Dict[str, Any], examples_field: str) -> Dict[str, Any]:
49 | """Gets request body examples from OAS3 `examples` keyword or `x-examples` for Swagger 2."""
50 | request_bodies_items = endpoint_def.get("requestBody", {}).get("content", {}).items()
51 | if not request_bodies_items:
52 | return {}
53 | # first element in tuple in media type, second element is dict
54 | media_type, schema = next(iter(request_bodies_items))
55 | parameter_type = "body" if media_type != "multipart/form-data" else "form_data"
56 | return {
57 | "type": parameter_type,
58 | "examples": [example["value"] for example in schema.get(examples_field, {}).values() if "value" in example],
59 | }
60 |
61 |
62 | def get_request_body_example_from_properties(endpoint_def: Dict[str, Any]) -> Dict[str, Any]:
63 | static_parameters: Dict[str, Any] = {}
64 | request_bodies_items = endpoint_def.get("requestBody", {}).get("content", {}).items()
65 | if request_bodies_items:
66 | media_type, request_body_schema = next(iter(request_bodies_items))
67 | example = get_object_example_from_properties(request_body_schema.get("schema", {}))
68 | if example:
69 | request_body_type = "body" if media_type != "multipart/form-data" else "form_data"
70 | static_parameters[request_body_type] = example
71 |
72 | return static_parameters
73 |
74 |
75 | def get_static_parameters_from_example(endpoint: Endpoint) -> Dict[str, Any]:
76 | static_parameters = {}
77 | for name in PARAMETERS:
78 | parameters = getattr(endpoint, name)
79 | example = parameters.example
80 | if example:
81 | static_parameters[name] = example
82 | return static_parameters
83 |
84 |
85 | def get_static_parameters_from_examples(endpoint: Endpoint, examples_field: str) -> List[Dict[str, Any]]:
86 | """Get static parameters from OpenAPI examples keyword."""
87 | endpoint_def = endpoint.definition.resolved
88 | return merge_examples(
89 | get_parameter_examples(endpoint_def, examples_field), get_request_body_examples(endpoint_def, examples_field)
90 | )
91 |
92 |
93 | def get_static_parameters_from_properties(endpoint: Endpoint) -> Dict[str, Any]:
94 | endpoint_def = endpoint.definition.resolved
95 | return {
96 | **get_parameter_example_from_properties(endpoint_def),
97 | **get_request_body_example_from_properties(endpoint_def),
98 | }
99 |
100 |
101 | def get_strategies_from_examples(endpoint: Endpoint, examples_field: str = "examples") -> List[SearchStrategy[Case]]:
102 | strategies = [
103 | get_case_strategy(endpoint=endpoint, **static_parameters)
104 | for static_parameters in get_static_parameters_from_examples(endpoint, examples_field)
105 | if static_parameters
106 | ]
107 | for static_parameters in static_parameters_union(
108 | get_static_parameters_from_example(endpoint), get_static_parameters_from_properties(endpoint)
109 | ):
110 | strategies.append(get_case_strategy(endpoint=endpoint, **static_parameters))
111 | return strategies
112 |
113 |
114 | def merge_examples(
115 | parameter_examples: List[Dict[str, Any]], request_body_examples: Dict[str, Any]
116 | ) -> List[Dict[str, Any]]:
117 | """Create list of static parameter objects from the parameter and request body examples."""
118 | static_parameter_list = []
119 | for idx in range(num_examples(parameter_examples, request_body_examples)):
120 | static_parameters: Dict[str, Any] = {}
121 | for parameter in parameter_examples:
122 | if parameter["type"] not in static_parameters:
123 | static_parameters[parameter["type"]] = {}
124 | static_parameters[parameter["type"]][parameter["name"]] = parameter["examples"][
125 | min(idx, len(parameter["examples"]) - 1)
126 | ]
127 | if "examples" in request_body_examples and request_body_examples["examples"]:
128 | static_parameters[request_body_examples["type"]] = request_body_examples["examples"][
129 | min(idx, len(request_body_examples["examples"]) - 1)
130 | ]
131 | static_parameter_list.append(static_parameters)
132 | return static_parameter_list
133 |
134 |
135 | def static_parameters_union(sp_1: Dict[str, Any], sp_2: Dict[str, Any]) -> List[Dict[str, Any]]:
136 | """Fill missing parameters in each static parameter dict with parameters provided in the other dict."""
137 | full_static_parameters = (_static_parameters_union(sp_1, sp_2), _static_parameters_union(sp_2, sp_1))
138 | return [static_parameter for static_parameter in full_static_parameters if static_parameter]
139 |
140 |
141 | def _static_parameters_union(base_obj: Dict[str, Any], fill_obj: Dict[str, Any]) -> Dict[str, Any]:
142 | """Fill base_obj with parameter examples in fill_obj that were not in base_obj."""
143 | if not base_obj:
144 | return {}
145 |
146 | full_static_parameters: Dict[str, Any] = {**base_obj}
147 |
148 | for parameter_type, examples in fill_obj.items():
149 | if parameter_type not in full_static_parameters:
150 | full_static_parameters[parameter_type] = examples
151 | elif parameter_type not in ("body", "form_data"):
152 | # copy individual parameter names.
153 | # body and form_data are unnamed, single examples, so we only do this for named parameters.
154 | for parameter_name, example in examples.items():
155 | if parameter_name not in full_static_parameters[parameter_type]:
156 | full_static_parameters[parameter_type][parameter_name] = example
157 | return full_static_parameters
158 |
159 |
160 | def num_examples(parameter_examples: List[Dict[str, Any]], request_body_examples: Dict[str, Any]) -> int:
161 | max_parameter_examples = (
162 | max(len(parameter["examples"]) for parameter in parameter_examples) if parameter_examples else 0
163 | )
164 | num_request_body_examples = len(request_body_examples["examples"]) if "examples" in request_body_examples else 0
165 | return max(max_parameter_examples, num_request_body_examples)
166 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/expressions/__init__.py:
--------------------------------------------------------------------------------
1 | """Runtime expressions support.
2 |
3 | https://swagger.io/docs/specification/links/#runtime-expressions
4 | """
5 | from typing import Any
6 |
7 | from . import lexer, nodes, parser
8 | from .context import ExpressionContext
9 |
10 |
11 | def evaluate(expr: Any, context: ExpressionContext) -> str:
12 | """Evaluate runtime expression in context."""
13 | if not isinstance(expr, str):
14 | # Can be a non-string constant
15 | return expr
16 | parts = [node.evaluate(context) for node in parser.parse(expr)]
17 | if len(parts) == 1:
18 | return parts[0] # keep the return type the same as the internal value type
19 | # otherwise, concatenate into a string
20 | return "".join(map(str, parts))
21 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/expressions/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/expressions/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/expressions/__pycache__/context.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/expressions/__pycache__/context.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/expressions/__pycache__/errors.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/expressions/__pycache__/errors.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/expressions/__pycache__/lexer.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/expressions/__pycache__/lexer.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/expressions/__pycache__/nodes.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/expressions/__pycache__/nodes.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/expressions/__pycache__/parser.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/expressions/__pycache__/parser.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/expressions/__pycache__/pointers.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/expressions/__pycache__/pointers.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/expressions/context.py:
--------------------------------------------------------------------------------
1 | import attr
2 |
3 | from ....models import Case
4 | from ....utils import GenericResponse
5 |
6 |
7 | @attr.s(slots=True) # pragma: no mutate
8 | class ExpressionContext:
9 | """Context in what an expression are evaluated."""
10 |
11 | response: GenericResponse = attr.ib() # pragma: no mutate
12 | case: Case = attr.ib() # pragma: no mutate
13 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/expressions/errors.py:
--------------------------------------------------------------------------------
1 | class RuntimeExpressionError(ValueError):
2 | """Generic error that happened during evaluation of a runtime expression."""
3 |
4 |
5 | class UnknownToken(RuntimeExpressionError):
6 | """Don't know how to handle a token value."""
7 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/expressions/lexer.py:
--------------------------------------------------------------------------------
1 | """Lexical analysis of runtime expressions."""
2 | from enum import Enum, unique
3 | from typing import Callable, Generator
4 |
5 | import attr
6 |
7 |
8 | @unique # pragma: no mutate
9 | class TokenType(Enum):
10 | VARIABLE = 1 # pragma: no mutate
11 | STRING = 2 # pragma: no mutate
12 | POINTER = 3 # pragma: no mutate
13 | DOT = 4 # pragma: no mutate
14 | LBRACKET = 5 # pragma: no mutate
15 | RBRACKET = 6 # pragma: no mutate
16 |
17 |
18 | @attr.s(slots=True) # pragma: no mutate
19 | class Token:
20 | """Lexical token that may occur in a runtime expression."""
21 |
22 | value: str = attr.ib() # pragma: no mutate
23 | type_: TokenType = attr.ib() # pragma: no mutate
24 |
25 | # Helpers for cleaner instantiation
26 |
27 | @classmethod
28 | def variable(cls, value: str) -> "Token":
29 | return cls(value, TokenType.VARIABLE)
30 |
31 | @classmethod
32 | def string(cls, value: str) -> "Token":
33 | return cls(value, TokenType.STRING)
34 |
35 | @classmethod
36 | def pointer(cls, value: str) -> "Token":
37 | return cls(value, TokenType.POINTER)
38 |
39 | @classmethod
40 | def lbracket(cls) -> "Token":
41 | return cls("{", TokenType.LBRACKET)
42 |
43 | @classmethod
44 | def rbracket(cls) -> "Token":
45 | return cls("}", TokenType.RBRACKET)
46 |
47 | @classmethod
48 | def dot(cls) -> "Token":
49 | return cls(".", TokenType.DOT)
50 |
51 | # Helpers for simpler type comparison
52 |
53 | @property
54 | def is_string(self) -> bool:
55 | return self.type_ == TokenType.STRING
56 |
57 | @property
58 | def is_variable(self) -> bool:
59 | return self.type_ == TokenType.VARIABLE
60 |
61 | @property
62 | def is_dot(self) -> bool:
63 | return self.type_ == TokenType.DOT
64 |
65 | @property
66 | def is_pointer(self) -> bool:
67 | return self.type_ == TokenType.POINTER
68 |
69 | @property
70 | def is_left_bracket(self) -> bool:
71 | return self.type_ == TokenType.LBRACKET
72 |
73 | @property
74 | def is_right_bracket(self) -> bool:
75 | return self.type_ == TokenType.RBRACKET
76 |
77 |
78 | TokenGenerator = Generator[Token, None, None]
79 |
80 |
81 | def tokenize(expression: str) -> TokenGenerator:
82 | """Do lexical analysis of the expression and return a list of tokens."""
83 | cursor = 0
84 |
85 | def is_eol() -> bool:
86 | return cursor == len(expression)
87 |
88 | def current_symbol() -> str:
89 | return expression[cursor]
90 |
91 | def move() -> None:
92 | nonlocal cursor
93 | cursor += 1
94 |
95 | def move_until(predicate: Callable[[], bool]) -> None:
96 | move()
97 | while not predicate():
98 | move()
99 |
100 | stop_symbols = {"$", ".", "{", "}", "#"}
101 |
102 | while not is_eol():
103 | if current_symbol() == "$":
104 | start = cursor
105 | move_until(lambda: is_eol() or current_symbol() in stop_symbols)
106 | yield Token.variable(expression[start:cursor])
107 | elif current_symbol() == ".":
108 | yield Token.dot()
109 | move()
110 | elif current_symbol() == "{":
111 | yield Token.lbracket()
112 | move()
113 | elif current_symbol() == "}":
114 | yield Token.rbracket()
115 | move()
116 | elif current_symbol() == "#":
117 | start = cursor
118 | # Symbol '}' is valid inside a JSON pointer, but also denotes closing of an embedded runtime expression
119 | # This is an ambiguous situation, for example:
120 | # Expression: `ID_{$request.body#/foo}}`
121 | # Body: `{"foo}": 1, "foo": 2}`
122 | # It could be evaluated differently:
123 | # - `ID_1` if we take the last bracket as the closing one
124 | # - `ID_2}` if we take the first bracket as the closing one
125 | # In this situation we take the second approach, to support cases like this:
126 | # `ID_{$response.body#/foo}_{$response.body#/bar}`
127 | # Which is much easier if we treat `}` as a closing bracket of an embedded runtime expression
128 | move_until(lambda: is_eol() or current_symbol() == "}")
129 | yield Token.pointer(expression[start:cursor])
130 | else:
131 | start = cursor
132 | move_until(lambda: is_eol() or current_symbol() in stop_symbols)
133 | yield Token.string(expression[start:cursor])
134 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/expressions/nodes.py:
--------------------------------------------------------------------------------
1 | """Expression nodes description and evaluation logic."""
2 | import json
3 | from enum import Enum, unique
4 | from typing import Any, Dict, Optional, Union
5 |
6 | import attr
7 | from requests.structures import CaseInsensitiveDict
8 |
9 | from ....utils import WSGIResponse
10 | from . import pointers
11 | from .context import ExpressionContext
12 | from .errors import RuntimeExpressionError
13 |
14 |
15 | @attr.s(slots=True) # pragma: no mutate
16 | class Node:
17 | """Generic expression node."""
18 |
19 | def evaluate(self, context: ExpressionContext) -> str:
20 | raise NotImplementedError
21 |
22 |
23 | @unique
24 | class NodeType(Enum):
25 | URL = "$url"
26 | METHOD = "$method"
27 | STATUS_CODE = "$statusCode"
28 | REQUEST = "$request"
29 | RESPONSE = "$response"
30 |
31 |
32 | @attr.s(slots=True) # pragma: no mutate
33 | class String(Node):
34 | """A simple string that is not evaluated somehow specifically."""
35 |
36 | value: str = attr.ib() # pragma: no mutate
37 |
38 | def evaluate(self, context: ExpressionContext) -> str:
39 | """String tokens are passed as they are.
40 |
41 | ``foo{$request.path.id}``
42 |
43 | "foo" is String token there.
44 | """
45 | return self.value
46 |
47 |
48 | @attr.s(slots=True) # pragma: no mutate
49 | class URL(Node):
50 | """A node for `$url` expression."""
51 |
52 | def evaluate(self, context: ExpressionContext) -> str:
53 | return context.case.get_full_url()
54 |
55 |
56 | @attr.s(slots=True) # pragma: no mutate
57 | class Method(Node):
58 | """A node for `$method` expression."""
59 |
60 | def evaluate(self, context: ExpressionContext) -> str:
61 | return context.case.endpoint.method.upper()
62 |
63 |
64 | @attr.s(slots=True) # pragma: no mutate
65 | class StatusCode(Node):
66 | """A node for `$statusCode` expression."""
67 |
68 | def evaluate(self, context: ExpressionContext) -> str:
69 | return str(context.response.status_code)
70 |
71 |
72 | @attr.s(slots=True) # pragma: no mutate
73 | class NonBodyRequest(Node):
74 | """A node for `$request` expressions where location is not `body`."""
75 |
76 | location: str = attr.ib() # pragma: no mutate
77 | parameter: str = attr.ib() # pragma: no mutate
78 |
79 | def evaluate(self, context: ExpressionContext) -> str:
80 | container: Union[Dict, CaseInsensitiveDict] = {
81 | "query": context.case.query,
82 | "path": context.case.path_parameters,
83 | "header": context.case.headers,
84 | }[self.location] or {}
85 | if self.location == "header":
86 | container = CaseInsensitiveDict(container)
87 | return str(container[self.parameter])
88 |
89 |
90 | @attr.s(slots=True) # pragma: no mutate
91 | class BodyRequest(Node):
92 | """A node for `$request` expressions where location is `body`."""
93 |
94 | pointer: Optional[str] = attr.ib(default=None) # pragma: no mutate
95 |
96 | def evaluate(self, context: ExpressionContext) -> Any:
97 | if self.pointer is None:
98 | try:
99 | return json.dumps(context.case.body)
100 | except TypeError as exc:
101 | raise RuntimeExpressionError("The request body is not JSON-serializable") from exc
102 | document = context.case.body
103 | return pointers.resolve(document, self.pointer[1:])
104 |
105 |
106 | @attr.s(slots=True) # pragma: no mutate
107 | class HeaderResponse(Node):
108 | """A node for `$response.header` expressions."""
109 |
110 | parameter: str = attr.ib() # pragma: no mutate
111 |
112 | def evaluate(self, context: ExpressionContext) -> str:
113 | return context.response.headers[self.parameter]
114 |
115 |
116 | @attr.s(slots=True) # pragma: no mutate
117 | class BodyResponse(Node):
118 | """A node for `$response.body` expressions."""
119 |
120 | pointer: Optional[str] = attr.ib(default=None) # pragma: no mutate
121 |
122 | def evaluate(self, context: ExpressionContext) -> Any:
123 | if self.pointer is None:
124 | return context.response.text
125 | if isinstance(context.response, WSGIResponse):
126 | document = context.response.json
127 | else:
128 | document = context.response.json()
129 | return pointers.resolve(document, self.pointer[1:])
130 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/expressions/parser.py:
--------------------------------------------------------------------------------
1 | from functools import lru_cache
2 | from typing import Generator, List, Union
3 |
4 | from . import lexer, nodes
5 | from .errors import RuntimeExpressionError, UnknownToken
6 |
7 |
8 | @lru_cache() # pragma: no mutate
9 | def parse(expr: str) -> List[nodes.Node]:
10 | """Parse lexical tokens into concrete expression nodes."""
11 | return list(_parse(expr))
12 |
13 |
14 | def _parse(expr: str) -> Generator[nodes.Node, None, None]:
15 | tokens = lexer.tokenize(expr)
16 | brackets_stack: List[str] = []
17 | for token in tokens:
18 | if token.is_string:
19 | yield nodes.String(token.value)
20 | elif token.is_variable:
21 | yield from _parse_variable(tokens, token, expr)
22 | elif token.is_left_bracket:
23 | if brackets_stack:
24 | raise RuntimeExpressionError("Nested embedded expressions are not allowed")
25 | brackets_stack.append("{")
26 | elif token.is_right_bracket:
27 | if not brackets_stack:
28 | raise RuntimeExpressionError("Unmatched bracket")
29 | brackets_stack.pop()
30 | if brackets_stack:
31 | raise RuntimeExpressionError("Unmatched bracket")
32 |
33 |
34 | def _parse_variable(tokens: lexer.TokenGenerator, token: lexer.Token, expr: str) -> Generator[nodes.Node, None, None]:
35 | if token.value == nodes.NodeType.URL.value:
36 | yield nodes.URL()
37 | elif token.value == nodes.NodeType.METHOD.value:
38 | yield nodes.Method()
39 | elif token.value == nodes.NodeType.STATUS_CODE.value:
40 | yield nodes.StatusCode()
41 | elif token.value == nodes.NodeType.REQUEST.value:
42 | yield _parse_request(tokens, expr)
43 | elif token.value == nodes.NodeType.RESPONSE.value:
44 | yield _parse_response(tokens, expr)
45 | else:
46 | raise UnknownToken(token.value)
47 |
48 |
49 | def _parse_request(tokens: lexer.TokenGenerator, expr: str) -> Union[nodes.BodyRequest, nodes.NonBodyRequest]:
50 | skip_dot(tokens, "$request")
51 | location = next(tokens)
52 | if location.value in ("query", "path", "header"):
53 | skip_dot(tokens, f"$request.{location.value}")
54 | parameter = take_string(tokens, expr)
55 | return nodes.NonBodyRequest(location.value, parameter)
56 | if location.value == "body":
57 | try:
58 | token = next(tokens)
59 | if token.is_pointer:
60 | return nodes.BodyRequest(token.value)
61 | except StopIteration:
62 | return nodes.BodyRequest()
63 | raise RuntimeExpressionError(f"Invalid expression: {expr}")
64 |
65 |
66 | def _parse_response(tokens: lexer.TokenGenerator, expr: str) -> Union[nodes.HeaderResponse, nodes.BodyResponse]:
67 | skip_dot(tokens, "$response")
68 | location = next(tokens)
69 | if location.value == "header":
70 | skip_dot(tokens, f"$response.{location.value}")
71 | parameter = take_string(tokens, expr)
72 | return nodes.HeaderResponse(parameter)
73 | if location.value == "body":
74 | try:
75 | token = next(tokens)
76 | if token.is_pointer:
77 | return nodes.BodyResponse(token.value)
78 | except StopIteration:
79 | return nodes.BodyResponse()
80 | raise RuntimeExpressionError(f"Invalid expression: {expr}")
81 |
82 |
83 | def skip_dot(tokens: lexer.TokenGenerator, name: str) -> None:
84 | token = next(tokens)
85 | if not token.is_dot:
86 | raise RuntimeExpressionError(f"`{name}` expression should be followed by a dot (`.`). Got: {token.value}")
87 |
88 |
89 | def take_string(tokens: lexer.TokenGenerator, expr: str) -> str:
90 | parameter = next(tokens)
91 | if not parameter.is_string:
92 | raise RuntimeExpressionError(f"Invalid expression: {expr}")
93 | return parameter.value
94 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/expressions/pointers.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict, List, Optional, Union
2 |
3 |
4 | def resolve(document: Any, pointer: str) -> Optional[Union[Dict, List, str, int, float]]:
5 | """Implementation is adapted from Rust's `serde-json` crate.
6 |
7 | Ref: https://github.com/serde-rs/json/blob/master/src/value/mod.rs#L751
8 | """
9 | if not pointer:
10 | return document
11 | if not pointer.startswith("/"):
12 | return None
13 |
14 | def replace(value: str) -> str:
15 | return value.replace("~1", "/").replace("~0", "~")
16 |
17 | tokens = map(replace, pointer.split("/")[1:])
18 | target = document
19 | for token in tokens:
20 | if isinstance(target, dict):
21 | target = target.get(token)
22 | elif isinstance(target, list):
23 | try:
24 | target = target[int(token)]
25 | except IndexError:
26 | return None
27 | else:
28 | return None
29 | return target
30 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/filters.py:
--------------------------------------------------------------------------------
1 | import re
2 | from typing import List, Optional
3 |
4 | from ...types import Filter
5 | from ...utils import force_tuple
6 |
7 |
8 | def should_skip_method(method: str, pattern: Optional[Filter]) -> bool:
9 | if pattern is None:
10 | return False
11 | patterns = force_tuple(pattern)
12 | return method.upper() not in map(str.upper, patterns)
13 |
14 |
15 | def should_skip_endpoint(endpoint: str, pattern: Optional[Filter]) -> bool:
16 | if pattern is None:
17 | return False
18 | return not _match_any_pattern(endpoint, pattern)
19 |
20 |
21 | def should_skip_by_tag(tags: Optional[List[str]], pattern: Optional[Filter]) -> bool:
22 | if pattern is None:
23 | return False
24 | if not tags:
25 | return True
26 | patterns = force_tuple(pattern)
27 | return not any(re.search(item, tag) for item in patterns for tag in tags)
28 |
29 |
30 | def should_skip_by_operation_id(operation_id: Optional[str], pattern: Optional[Filter]) -> bool:
31 | if pattern is None:
32 | return False
33 | if not operation_id:
34 | return True
35 | return not _match_any_pattern(operation_id, pattern)
36 |
37 |
38 | def should_skip_deprecated(is_deprecated: bool, skip_deprecated_endpoints: bool) -> bool:
39 | return skip_deprecated_endpoints and is_deprecated
40 |
41 |
42 | def _match_any_pattern(target: str, pattern: Filter) -> bool:
43 | patterns = force_tuple(pattern)
44 | return any(re.search(item, target) for item in patterns)
45 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/references.py:
--------------------------------------------------------------------------------
1 | from copy import deepcopy
2 | from functools import lru_cache
3 | from typing import Any, Callable, Dict, List, Tuple, Union, overload
4 | from urllib.request import urlopen
5 |
6 | import jsonschema
7 | import requests
8 | import yaml
9 |
10 | from ...utils import StringDatesYAMLLoader, traverse_schema
11 | from .converter import to_json_schema
12 |
13 | # Reference resolving will stop after this depth
14 | RECURSION_DEPTH_LIMIT = 100
15 |
16 |
17 | def load_file_impl(location: str, opener: Callable) -> Dict[str, Any]:
18 | """Load a schema from the given file."""
19 | with opener(location) as fd:
20 | return yaml.load(fd, StringDatesYAMLLoader)
21 |
22 |
23 | @lru_cache()
24 | def load_file(location: str) -> Dict[str, Any]:
25 | """Load a schema from the given file."""
26 | return load_file_impl(location, open)
27 |
28 |
29 | @lru_cache()
30 | def load_file_uri(location: str) -> Dict[str, Any]:
31 | """Load a schema from the given file uri."""
32 | return load_file_impl(location, urlopen)
33 |
34 |
35 | def load_remote_uri(uri: str) -> Any:
36 | """Load the resource and parse it as YAML / JSON."""
37 | response = requests.get(uri)
38 | return yaml.load(response.content, StringDatesYAMLLoader)
39 |
40 |
41 | class ConvertingResolver(jsonschema.RefResolver):
42 | """A custom resolver converts resolved OpenAPI schemas to JSON Schema.
43 |
44 | When recursive schemas are validated we need to have resolved documents properly converted.
45 | This approach is the simplest one, since this logic isolated in a single place.
46 | """
47 |
48 | def __init__(self, *args: Any, nullable_name: Any, **kwargs: Any) -> None:
49 | kwargs.setdefault(
50 | "handlers", {"file": load_file_uri, "": load_file, "http": load_remote_uri, "https": load_remote_uri}
51 | )
52 | super().__init__(*args, **kwargs)
53 | self.nullable_name = nullable_name
54 |
55 | def resolve(self, ref: str) -> Tuple[str, Any]:
56 | url, document = super().resolve(ref)
57 | document = traverse_schema(document, to_json_schema, nullable_name=self.nullable_name)
58 | return url, document
59 |
60 | @overload # pragma: no mutate
61 | def resolve_all(
62 | self, item: Dict[str, Any], recursion_level: int = 0
63 | ) -> Dict[str, Any]: # pylint: disable=function-redefined
64 | pass
65 |
66 | @overload # pragma: no mutate
67 | def resolve_all(self, item: List, recursion_level: int = 0) -> List: # pylint: disable=function-redefined
68 | pass
69 |
70 | # pylint: disable=function-redefined
71 | def resolve_all(self, item: Union[Dict[str, Any], List], recursion_level: int = 0) -> Union[Dict[str, Any], List]:
72 | """Recursively resolve all references in the given object."""
73 | if recursion_level > RECURSION_DEPTH_LIMIT:
74 | return item
75 | item = deepcopy(item)
76 | if isinstance(item, dict):
77 | if "$ref" in item and isinstance(item["$ref"], str):
78 | with self.resolving(item["$ref"]) as resolved:
79 | return self.resolve_all(resolved, recursion_level + 1)
80 | for key, sub_item in item.items():
81 | item[key] = self.resolve_all(sub_item, recursion_level)
82 | elif isinstance(item, list):
83 | for idx, sub_item in enumerate(item):
84 | item[idx] = self.resolve_all(sub_item, recursion_level)
85 | return item
86 |
87 | def resolve_in_scope(self, definition: Dict[str, Any], scope: str) -> Tuple[List[str], Dict[str, Any]]:
88 | scopes = [scope]
89 | # if there is `$ref` then we have a scope change that should be used during validation later to
90 | # resolve nested references correctly
91 | if "$ref" in definition:
92 | with self.in_scope(scope):
93 | new_scope, definition = deepcopy(self.resolve(definition["$ref"]))
94 | scopes.append(new_scope)
95 | return scopes, definition
96 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/security.py:
--------------------------------------------------------------------------------
1 | """Processing of ``securityDefinitions`` or ``securitySchemes`` keywords."""
2 | from typing import Any, ClassVar, Dict, Generator, List, Tuple, Type
3 |
4 | import attr
5 | from jsonschema import RefResolver
6 |
7 | from ...models import Endpoint
8 | from .parameters import OpenAPI20Parameter, OpenAPI30Parameter, OpenAPIParameter
9 |
10 |
11 | @attr.s(slots=True) # pragma: no mutate
12 | class BaseSecurityProcessor:
13 | api_key_locations: Tuple[str, ...] = ("header", "query")
14 | http_security_name = "basic"
15 | parameter_cls: ClassVar[Type[OpenAPIParameter]] = OpenAPI20Parameter
16 |
17 | def process_definitions(self, schema: Dict[str, Any], endpoint: Endpoint, resolver: RefResolver) -> None:
18 | """Add relevant security parameters to data generation."""
19 | for definition in self._get_active_definitions(schema, endpoint, resolver):
20 | if definition["type"] == "apiKey":
21 | self.process_api_key_security_definition(definition, endpoint)
22 | self.process_http_security_definition(definition, endpoint)
23 |
24 | def _get_active_definitions(
25 | self, schema: Dict[str, Any], endpoint: Endpoint, resolver: RefResolver
26 | ) -> Generator[Dict[str, Any], None, None]:
27 | """Get only security definitions active for the given endpoint."""
28 | definitions = self.get_security_definitions(schema, resolver)
29 | requirements = get_security_requirements(schema, endpoint)
30 | for name, definition in definitions.items():
31 | if name in requirements:
32 | yield definition
33 |
34 | def get_security_definitions(self, schema: Dict[str, Any], resolver: RefResolver) -> Dict[str, Any]:
35 | return schema.get("securityDefinitions", {})
36 |
37 | def get_security_definitions_as_parameters(
38 | self, schema: Dict[str, Any], endpoint: Endpoint, resolver: RefResolver, location: str
39 | ) -> List[Dict[str, Any]]:
40 | """Security definitions converted to OAS parameters.
41 |
42 | We need it to get proper serialization that will be applied on generated values. For this case it is only
43 | coercing to a string.
44 | """
45 | return [
46 | self._to_parameter(definition)
47 | for definition in self._get_active_definitions(schema, endpoint, resolver)
48 | if self._is_match(definition, location)
49 | ]
50 |
51 | def process_api_key_security_definition(self, definition: Dict[str, Any], endpoint: Endpoint) -> None:
52 | parameter = self.parameter_cls(self._make_api_key_parameter(definition))
53 | endpoint.add_parameter(parameter)
54 |
55 | def process_http_security_definition(self, definition: Dict[str, Any], endpoint: Endpoint) -> None:
56 | if definition["type"] == self.http_security_name:
57 | parameter = self.parameter_cls(self._make_http_auth_parameter(definition))
58 | endpoint.add_parameter(parameter)
59 |
60 | def _is_match(self, definition: Dict[str, Any], location: str) -> bool:
61 | return (definition["type"] == "apiKey" and location in self.api_key_locations) or (
62 | definition["type"] == self.http_security_name and location == "header"
63 | )
64 |
65 | def _to_parameter(self, definition: Dict[str, Any]) -> Dict[str, Any]:
66 | func = {
67 | "apiKey": self._make_api_key_parameter,
68 | self.http_security_name: self._make_http_auth_parameter,
69 | }[definition["type"]]
70 | return func(definition)
71 |
72 | def _make_http_auth_parameter(self, definition: Dict[str, Any]) -> Dict[str, Any]:
73 | schema = make_auth_header_schema(definition)
74 | return make_auth_header(**schema)
75 |
76 | def _make_api_key_parameter(self, definition: Dict[str, Any]) -> Dict[str, Any]:
77 | return make_api_key_schema(definition, type="string")
78 |
79 |
80 | def make_auth_header_schema(definition: Dict[str, Any]) -> Dict[str, str]:
81 | schema = definition.get("scheme", "basic").lower()
82 | return {"type": "string", "format": f"_{schema}_auth"}
83 |
84 |
85 | def make_auth_header(**kwargs: Any) -> Dict[str, Any]:
86 | return {"name": "Authorization", "in": "header", "required": True, **kwargs}
87 |
88 |
89 | def make_api_key_schema(definition: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]:
90 | return {"name": definition["name"], "required": True, "in": definition["in"], **kwargs}
91 |
92 |
93 | SwaggerSecurityProcessor = BaseSecurityProcessor
94 |
95 |
96 | @attr.s(slots=True) # pragma: no mutate
97 | class OpenAPISecurityProcessor(BaseSecurityProcessor):
98 | api_key_locations = ("header", "cookie", "query")
99 | http_security_name = "http"
100 | parameter_cls: ClassVar[Type[OpenAPIParameter]] = OpenAPI30Parameter
101 |
102 | def get_security_definitions(self, schema: Dict[str, Any], resolver: RefResolver) -> Dict[str, Any]:
103 | """In Open API 3 security definitions are located in ``components`` and may have references inside."""
104 | components = schema.get("components", {})
105 | security_schemes = components.get("securitySchemes", {})
106 | if "$ref" in security_schemes:
107 | return resolver.resolve(security_schemes["$ref"])[1]
108 | return security_schemes
109 |
110 | def _make_http_auth_parameter(self, definition: Dict[str, Any]) -> Dict[str, Any]:
111 | schema = make_auth_header_schema(definition)
112 | return make_auth_header(schema=schema)
113 |
114 | def _make_api_key_parameter(self, definition: Dict[str, Any]) -> Dict[str, Any]:
115 | return make_api_key_schema(definition, schema={"type": "string"})
116 |
117 |
118 | def get_security_requirements(schema: Dict[str, Any], endpoint: Endpoint) -> List[str]:
119 | """Get applied security requirements for the given endpoint."""
120 | # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object
121 | # > This definition overrides any declared top-level security.
122 | # > To remove a top-level security declaration, an empty array can be used.
123 | global_requirements = schema.get("security", [])
124 | local_requirements = endpoint.definition.raw.get("security", None)
125 | if local_requirements is not None:
126 | requirements = local_requirements
127 | else:
128 | requirements = global_requirements
129 | return [key for requirement in requirements for key in requirement]
130 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/serialization.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import json
3 | from typing import Any, Callable, Dict, Generator, List, Optional
4 |
5 | Generated = Dict[str, Any]
6 | Definition = Dict[str, Any]
7 | DefinitionList = List[Definition]
8 | MapFunction = Callable[[Generated], Generated]
9 |
10 |
11 | def compose(*functions: Callable) -> Callable:
12 | """Compose multiple functions into a single one."""
13 |
14 | def noop(x: Any) -> Any:
15 | return x
16 |
17 | return functools.reduce(lambda f, g: lambda x: f(g(x)), functions, noop)
18 |
19 |
20 | def make_serializer(
21 | func: Callable[[DefinitionList], Generator[Optional[Callable], None, None]]
22 | ) -> Callable[[DefinitionList], Optional[Callable]]:
23 | """A maker function to avoid code duplication."""
24 |
25 | def _wrapper(definitions: DefinitionList) -> Optional[Callable]:
26 | conversions = list(func(definitions))
27 | if conversions:
28 | return compose(*[conv for conv in conversions if conv is not None])
29 | return None
30 |
31 | return _wrapper
32 |
33 |
34 | def _serialize_openapi3(definitions: DefinitionList) -> Generator[Optional[Callable], None, None]:
35 | """Different collection styles for Open API 3.0."""
36 | for definition in definitions:
37 | name = definition["name"]
38 | if "content" in definition:
39 | # https://swagger.io/docs/specification/describing-parameters/#schema-vs-content
40 | options = iter(definition["content"].keys())
41 | media_type = next(options, None)
42 | if media_type == "application/json":
43 | yield to_json(name)
44 | else:
45 | # Simple serialization
46 | style = definition.get("style")
47 | explode = definition.get("explode")
48 | type_ = definition.get("schema", {}).get("type")
49 | if definition["in"] == "path":
50 | yield from _serialize_path_openapi3(name, type_, style, explode)
51 | elif definition["in"] == "query":
52 | yield from _serialize_query_openapi3(name, type_, style, explode)
53 | elif definition["in"] == "header":
54 | yield from _serialize_header_openapi3(name, type_, explode)
55 | elif definition["in"] == "cookie":
56 | yield from _serialize_cookie_openapi3(name, type_, explode)
57 |
58 |
59 | def _serialize_path_openapi3(
60 | name: str, type_: str, style: Optional[str], explode: Optional[bool]
61 | ) -> Generator[Optional[Callable], None, None]:
62 | # pylint: disable=too-many-branches
63 | if style == "simple":
64 | if type_ == "object":
65 | if explode is False:
66 | yield comma_delimited_object(name)
67 | if explode is True:
68 | yield delimited_object(name)
69 | if type_ == "array":
70 | yield delimited(name, delimiter=",")
71 | if style == "label":
72 | if type_ == "object":
73 | yield label_object(name, explode=explode)
74 | elif type_ == "array":
75 | yield label_array(name, explode=explode)
76 | else:
77 | yield label_primitive(name)
78 | if style == "matrix":
79 | if type_ == "object":
80 | yield matrix_object(name, explode=explode)
81 | elif type_ == "array":
82 | yield matrix_array(name, explode=explode)
83 | else:
84 | yield matrix_primitive(name)
85 |
86 |
87 | def _serialize_query_openapi3(
88 | name: str, type_: str, style: Optional[str], explode: Optional[bool]
89 | ) -> Generator[Optional[Callable], None, None]:
90 | if type_ == "object":
91 | if style == "deepObject":
92 | yield deep_object(name)
93 | if style is None or style == "form":
94 | if explode is False:
95 | yield comma_delimited_object(name)
96 | if explode is True:
97 | yield extracted_object(name)
98 | elif type_ == "array" and explode is False:
99 | if style == "pipeDelimited":
100 | yield delimited(name, delimiter="|")
101 | if style == "spaceDelimited":
102 | yield delimited(name, delimiter=" ")
103 | if style is None or style == "form": # "form" is the default style
104 | yield delimited(name, delimiter=",")
105 |
106 |
107 | def _serialize_header_openapi3(
108 | name: str, type_: str, explode: Optional[bool]
109 | ) -> Generator[Optional[Callable], None, None]:
110 | # Headers should be coerced to a string so we can check it for validity later
111 | yield to_string(name)
112 | # Header parameters always use the "simple" style, that is, comma-separated values
113 | if type_ == "array":
114 | yield delimited(name, delimiter=",")
115 | if type_ == "object":
116 | if explode is False:
117 | yield comma_delimited_object(name)
118 | if explode is True:
119 | yield delimited_object(name)
120 |
121 |
122 | def _serialize_cookie_openapi3(
123 | name: str, type_: str, explode: Optional[bool]
124 | ) -> Generator[Optional[Callable], None, None]:
125 | # Cookies should be coerced to a string so we can check it for validity later
126 | yield to_string(name)
127 | # Cookie parameters always use the "form" style
128 | if explode and type_ in ("array", "object"):
129 | # `explode=true` doesn't make sense
130 | # I.e. we can't create multiple values for the same cookie
131 | # We use the same behavior as in the examples - https://swagger.io/docs/specification/serialization/
132 | # The item is removed
133 | yield nothing(name)
134 | if explode is False:
135 | if type_ == "array":
136 | yield delimited(name, delimiter=",")
137 | if type_ == "object":
138 | yield comma_delimited_object(name)
139 |
140 |
141 | def _serialize_swagger2(definitions: DefinitionList) -> Generator[Optional[Callable], None, None]:
142 | """Different collection formats for Open API 2.0."""
143 | for definition in definitions:
144 | name = definition["name"]
145 | collection_format = definition.get("collectionFormat", "csv")
146 | type_ = definition.get("type")
147 | if definition["in"] == "header":
148 | # Headers should be coerced to a string so we can check it for validity later
149 | yield to_string(name)
150 | if definition["in"] != "body":
151 | if type_ in ("array", "object"):
152 | if collection_format == "csv":
153 | yield delimited(name, delimiter=",")
154 | if collection_format == "ssv":
155 | yield delimited(name, delimiter=" ")
156 | if collection_format == "tsv":
157 | yield delimited(name, delimiter="\t")
158 | if collection_format == "pipes":
159 | yield delimited(name, delimiter="|")
160 |
161 |
162 | serialize_openapi3_parameters = make_serializer(_serialize_openapi3)
163 | serialize_swagger2_parameters = make_serializer(_serialize_swagger2)
164 |
165 |
166 | def conversion(func: Callable[..., None]) -> Callable:
167 | def _wrapper(name: str, **kwargs: Any) -> MapFunction:
168 | def _map(item: Generated) -> Generated:
169 | if name in item:
170 | func(item, name, **kwargs)
171 | return item
172 |
173 | return _map
174 |
175 | return _wrapper
176 |
177 |
178 | def make_delimited(data: Dict[str, Any], delimiter: str = ",") -> str:
179 | return delimiter.join(f"{key}={value}" for key, value in data.items())
180 |
181 |
182 | @conversion
183 | def to_json(item: Generated, name: str) -> None:
184 | """Serialize an item to JSON."""
185 | item[name] = json.dumps(item[name])
186 |
187 |
188 | @conversion
189 | def delimited(item: Generated, name: str, delimiter: str) -> None:
190 | item[name] = delimiter.join(map(str, item[name]))
191 |
192 |
193 | @conversion
194 | def deep_object(item: Generated, name: str) -> None:
195 | """Serialize an object with `deepObject` style.
196 |
197 | id={"role": "admin", "firstName": "Alex"} => id[role]=admin&id[firstName]=Alex
198 | """
199 | generated = item.pop(name)
200 | item.update({f"{name}[{key}]": value for key, value in generated.items()})
201 |
202 |
203 | @conversion
204 | def comma_delimited_object(item: Generated, name: str) -> None:
205 | item[name] = ",".join(map(str, sum(item[name].items(), ())))
206 |
207 |
208 | @conversion
209 | def delimited_object(item: Generated, name: str) -> None:
210 | item[name] = make_delimited(item[name])
211 |
212 |
213 | @conversion
214 | def extracted_object(item: Generated, name: str) -> None:
215 | """Merge a child node to the parent one."""
216 | generated = item.pop(name)
217 | item.update(generated)
218 |
219 |
220 | @conversion
221 | def label_primitive(item: Generated, name: str) -> None:
222 | """Serialize a primitive value with the `label` style.
223 |
224 | 5 => ".5"
225 | """
226 | item[name] = f".{item[name]}"
227 |
228 |
229 | @conversion
230 | def label_array(item: Generated, name: str, explode: Optional[bool]) -> None:
231 | """Serialize an array with the `label` style.
232 |
233 | Explode=True
234 |
235 | id=[3, 4, 5] => ".3.4.5"
236 |
237 | Explode=False
238 |
239 | id=[3, 4, 5] => ".3,4,5"
240 | """
241 | if explode:
242 | delimiter = "."
243 | else:
244 | delimiter = ","
245 | item[name] = f".{delimiter.join(map(str, item[name]))}"
246 |
247 |
248 | @conversion
249 | def label_object(item: Generated, name: str, explode: Optional[bool]) -> None:
250 | """Serialize an object with the `label` style.
251 |
252 | Explode=True
253 |
254 | id={"role": "admin", "firstName": "Alex"} => ".role=admin.firstName=Alex"
255 |
256 | Explode=False
257 |
258 | id={"role": "admin", "firstName": "Alex"} => ".role=admin,firstName,Alex"
259 | """
260 | if explode:
261 | new = make_delimited(item[name], ".")
262 | else:
263 | object_items = map(str, sum(item[name].items(), ()))
264 | new = ",".join(object_items)
265 | item[name] = f".{new}"
266 |
267 |
268 | @conversion
269 | def matrix_primitive(item: Generated, name: str) -> None:
270 | """Serialize a primitive value with the `matrix` style.
271 |
272 | 5 => ";id=5"
273 | """
274 | item[name] = f";{name}={item[name]}"
275 |
276 |
277 | @conversion
278 | def matrix_array(item: Generated, name: str, explode: Optional[bool]) -> None:
279 | """Serialize an array with the `matrix` style.
280 |
281 | Explode=True
282 |
283 | id=[3, 4, 5] => ";id=3;id=4;id=5"
284 |
285 | Explode=False
286 |
287 | id=[3, 4, 5] => ";id=3,4,5"
288 | """
289 | if explode:
290 | new = ";".join(f"{name}={value}" for value in item[name])
291 | else:
292 | new = ",".join(map(str, item[name]))
293 | item[name] = f";{new}"
294 |
295 |
296 | @conversion
297 | def matrix_object(item: Generated, name: str, explode: Optional[bool]) -> None:
298 | """Serialize an object with the `matrix` style.
299 |
300 | Explode=True
301 |
302 | id={"role": "admin", "firstName": "Alex"} => ";role=admin;firstName=Alex"
303 |
304 | Explode=False
305 |
306 | id={"role": "admin", "firstName": "Alex"} => ";role=admin,firstName,Alex"
307 | """
308 | if explode:
309 | new = make_delimited(item[name], ";")
310 | else:
311 | object_items = map(str, sum(item[name].items(), ()))
312 | new = ",".join(object_items)
313 | item[name] = f";{new}"
314 |
315 |
316 | @conversion
317 | def nothing(item: Generated, name: str) -> None:
318 | """Remove a key from an item."""
319 | item.pop(name, None)
320 |
321 |
322 | @conversion
323 | def to_string(item: Generated, name: str) -> None:
324 | """Convert the value to a string."""
325 | item[name] = str(item[name])
326 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/stateful/__init__.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import operator
3 | from collections import defaultdict
4 | from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Type
5 |
6 | from hypothesis.stateful import Bundle, Rule, rule
7 | from hypothesis.strategies import SearchStrategy, none
8 | from requests.structures import CaseInsensitiveDict
9 |
10 | from ....stateful import APIStateMachine, Direction, StepResult
11 | from .. import expressions
12 | from ..links import OpenAPILink
13 | from . import links
14 |
15 | if TYPE_CHECKING:
16 | from ....models import Case, Endpoint
17 | from ..schemas import BaseOpenAPISchema
18 |
19 |
20 | EndpointConnections = Dict[str, List[SearchStrategy[Tuple[StepResult, OpenAPILink]]]]
21 |
22 |
23 | class OpenAPIStateMachine(APIStateMachine):
24 | def transform(self, result: StepResult, direction: Direction, case: "Case") -> "Case":
25 | context = expressions.ExpressionContext(case=result.case, response=result.response)
26 | direction.set_data(case, context=context)
27 | return case
28 |
29 |
30 | def create_state_machine(schema: "BaseOpenAPISchema") -> Type[APIStateMachine]:
31 | """Create a state machine class.
32 |
33 | This state machine will contain transitions that connect some endpoints' outputs with other endpoints' inputs.
34 | """
35 | bundles = init_bundles(schema)
36 | connections: EndpointConnections = defaultdict(list)
37 | for endpoint in schema.get_all_endpoints():
38 | links.apply(endpoint, bundles, connections)
39 |
40 | rules = make_all_rules(schema, bundles, connections)
41 |
42 | kwargs: Dict[str, Any] = {"bundles": bundles, "schema": schema}
43 | return type("APIWorkflow", (OpenAPIStateMachine,), {**kwargs, **rules})
44 |
45 |
46 | def init_bundles(schema: "BaseOpenAPISchema") -> Dict[str, CaseInsensitiveDict]:
47 | """Create bundles for all endpoints in the given schema.
48 |
49 | Each endpoint has a bundle that stores all responses from that endpoint.
50 | We need to create bundles first, so they can be referred when building connections between endpoints.
51 | """
52 | output: Dict[str, CaseInsensitiveDict] = {}
53 | for endpoint in schema.get_all_endpoints():
54 | output.setdefault(endpoint.path, CaseInsensitiveDict())
55 | output[endpoint.path][endpoint.method.upper()] = Bundle(endpoint.verbose_name) # type: ignore
56 | return output
57 |
58 |
59 | def make_all_rules(
60 | schema: "BaseOpenAPISchema", bundles: Dict[str, CaseInsensitiveDict], connections: EndpointConnections
61 | ) -> Dict[str, Rule]:
62 | """Create rules for all endpoints, based on the provided connections."""
63 | return {
64 | f"rule {endpoint.verbose_name}": make_rule(
65 | endpoint, bundles[endpoint.path][endpoint.method.upper()], connections
66 | )
67 | for endpoint in schema.get_all_endpoints()
68 | }
69 |
70 |
71 | def make_rule(endpoint: "Endpoint", bundle: Bundle, connections: EndpointConnections) -> Rule:
72 | """Create a rule for an endpoint."""
73 | previous_strategies = connections.get(endpoint.verbose_name)
74 | if previous_strategies is not None:
75 | previous = _combine_strategies(previous_strategies)
76 | else:
77 | previous = none()
78 | return rule(target=bundle, previous=previous, case=endpoint.as_strategy())(APIStateMachine.step) # type: ignore
79 |
80 |
81 | def _combine_strategies(strategies: List[SearchStrategy]) -> SearchStrategy:
82 | """Combine a list of strategies into a single one.
83 |
84 | If the input is `[a, b, c]`, then the result is equivalent to `a | b | c`.
85 | """
86 | return functools.reduce(operator.or_, strategies[1:], strategies[0])
87 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/stateful/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/stateful/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/stateful/__pycache__/links.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngalongc/openapi_security_scanner/9ba2244bf0e52db6f149243de403c8c7c157216f/schemathesis/specs/openapi/stateful/__pycache__/links.cpython-38.pyc
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/stateful/links.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING, Callable, Dict, List, Tuple
2 |
3 | import hypothesis.strategies as st
4 | from requests.structures import CaseInsensitiveDict
5 |
6 | from ....stateful import StepResult
7 | from ..links import OpenAPILink, get_all_links
8 | from ..utils import expand_status_code
9 |
10 | if TYPE_CHECKING:
11 | from ....models import Endpoint
12 |
13 | FilterFunction = Callable[[StepResult], bool]
14 |
15 |
16 | def apply(
17 | endpoint: "Endpoint",
18 | bundles: Dict[str, CaseInsensitiveDict],
19 | connections: Dict[str, List[st.SearchStrategy[Tuple[StepResult, OpenAPILink]]]],
20 | ) -> None:
21 | """Gather all connections based on Open API links definitions."""
22 | all_status_codes = list(endpoint.definition.resolved["responses"])
23 | for status_code, link in get_all_links(endpoint):
24 | target_endpoint = link.get_target_endpoint()
25 | strategy = bundles[endpoint.path][endpoint.method.upper()].filter(
26 | make_response_filter(status_code, all_status_codes)
27 | )
28 | connections[target_endpoint.verbose_name].append(_convert_strategy(strategy, link))
29 |
30 |
31 | def _convert_strategy(
32 | strategy: st.SearchStrategy[StepResult], link: OpenAPILink
33 | ) -> st.SearchStrategy[Tuple[StepResult, OpenAPILink]]:
34 | # This function is required to capture values properly (it won't work properly when lambda is defined in a loop)
35 | return strategy.map(lambda out: (out, link))
36 |
37 |
38 | def make_response_filter(status_code: str, all_status_codes: List[str]) -> FilterFunction:
39 | """Create a filter for stored responses.
40 |
41 | This filter will decide whether some response is suitable to use as a source for requesting some endpoint.
42 | """
43 | if status_code == "default":
44 | return default_status_code(all_status_codes)
45 | return match_status_code(status_code)
46 |
47 |
48 | def match_status_code(status_code: str) -> FilterFunction:
49 | """Create a filter function that matches all responses with the given status code.
50 |
51 | Note that the status code can contain "X", which means any digit.
52 | For example, 50X will match all status codes from 500 to 509.
53 | """
54 | status_codes = set(expand_status_code(status_code))
55 |
56 | def compare(result: StepResult) -> bool:
57 | return result.response.status_code in status_codes
58 |
59 | # This name is displayed in the resulting strategy representation. For example, if you run your tests with
60 | # `--hypothesis-show-statistics`, then you can see `Bundle(name='GET /users/{user_id}').filter(match_200_response)`
61 | # which gives you information about the particularly used filter.
62 | compare.__name__ = f"match_{status_code}_response"
63 |
64 | return compare
65 |
66 |
67 | def default_status_code(status_codes: List[str]) -> FilterFunction:
68 | """Create a filter that matches all "default" responses.
69 |
70 | In Open API, the "default" response is the one that is used if no other options were matched.
71 | Therefore we need to match only responses that were not matched by other listed status codes.
72 | """
73 | expanded_status_codes = {
74 | status_code for value in status_codes if value != "default" for status_code in expand_status_code(value)
75 | }
76 |
77 | def match_default_response(result: StepResult) -> bool:
78 | return result.response.status_code not in expanded_status_codes
79 |
80 | return match_default_response
81 |
--------------------------------------------------------------------------------
/schemathesis/specs/openapi/utils.py:
--------------------------------------------------------------------------------
1 | import string
2 | from itertools import product
3 | from typing import Generator, Union
4 |
5 |
6 | def expand_status_code(status_code: Union[str, int]) -> Generator[int, None, None]:
7 | chars = [list(string.digits) if digit == "X" else [digit] for digit in str(status_code).upper()]
8 | for expanded in product(*chars):
9 | yield int("".join(expanded))
10 |
--------------------------------------------------------------------------------
/schemathesis/targets.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING, Callable, Tuple
2 |
3 | import attr
4 |
5 | from .utils import GenericResponse
6 |
7 | if TYPE_CHECKING:
8 | from .models import Case
9 |
10 |
11 | @attr.s(slots=True) # pragma: no mutate
12 | class TargetContext:
13 | case: "Case" = attr.ib() # pragma: no mutate
14 | response: GenericResponse = attr.ib() # pragma: no mutate
15 | response_time: float = attr.ib() # pragma: no mutate
16 |
17 |
18 | def response_time(context: TargetContext) -> float:
19 | return context.response_time
20 |
21 |
22 | Target = Callable[[TargetContext], float]
23 | DEFAULT_TARGETS = ()
24 | OPTIONAL_TARGETS = (response_time,)
25 | ALL_TARGETS: Tuple[Target, ...] = DEFAULT_TARGETS + OPTIONAL_TARGETS
26 |
--------------------------------------------------------------------------------
/schemathesis/types.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from typing import TYPE_CHECKING, Any, Callable, Dict, List, Set, Tuple, Union
3 |
4 | from hypothesis.strategies import SearchStrategy
5 |
6 | if TYPE_CHECKING:
7 | from .hooks import HookContext
8 |
9 | PathLike = Union[Path, str] # pragma: no mutate
10 |
11 | Query = Dict[str, Any] # pragma: no mutate
12 | # Body can be of any Python type that corresponds to JSON Schema types + `bytes`
13 | Body = Union[List, Dict[str, Any], str, int, float, bool, bytes] # pragma: no mutate
14 | PathParameters = Dict[str, Any] # pragma: no mutate
15 | Headers = Dict[str, Any] # pragma: no mutate
16 | Cookies = Dict[str, Any] # pragma: no mutate
17 | FormData = Dict[str, Any] # pragma: no mutate
18 |
19 |
20 | class NotSet:
21 | pass
22 |
23 |
24 | # A filter for endpoint / method
25 | Filter = Union[str, List[str], Tuple[str], Set[str], NotSet] # pragma: no mutate
26 |
27 | Hook = Union[
28 | Callable[[SearchStrategy], SearchStrategy], Callable[[SearchStrategy, "HookContext"], SearchStrategy]
29 | ] # pragma: no mutate
30 |
31 | RawAuth = Tuple[str, str] # pragma: no mutate
32 | # Generic test with any arguments and no return
33 | GenericTest = Callable[..., None] # pragma: no mutate
34 |
--------------------------------------------------------------------------------
/schemathesis/utils.py:
--------------------------------------------------------------------------------
1 | import cgi
2 | import pathlib
3 | import re
4 | import sys
5 | import traceback
6 | from contextlib import contextmanager
7 | from json import JSONDecodeError
8 | from typing import Any, Callable, Dict, Generator, List, NoReturn, Optional, Set, Tuple, Type, Union, overload
9 |
10 | import requests
11 | import yaml
12 | from hypothesis.reporting import with_reporter
13 | from requests.auth import HTTPDigestAuth
14 | from requests.exceptions import InvalidHeader # type: ignore
15 | from requests.utils import check_header_validity # type: ignore
16 | from werkzeug.wrappers import Response as BaseResponse
17 | from werkzeug.wrappers.json import JSONMixin
18 |
19 | from .types import Filter, NotSet, RawAuth
20 |
21 | try:
22 | from yaml import CSafeLoader as SafeLoader
23 | except ImportError:
24 | # pylint: disable=unused-import
25 | from yaml import SafeLoader # type: ignore
26 |
27 |
28 | NOT_SET = NotSet()
29 |
30 |
31 | def file_exists(path: str) -> bool:
32 | try:
33 | return pathlib.Path(path).is_file()
34 | except OSError:
35 | # For example, path could be too long
36 | return False
37 |
38 |
39 | def is_latin_1_encodable(value: str) -> bool:
40 | """Header values are encoded to latin-1 before sending."""
41 | try:
42 | value.encode("latin-1")
43 | return True
44 | except UnicodeEncodeError:
45 | return False
46 |
47 |
48 | # Adapted from http.client._is_illegal_header_value
49 | INVALID_HEADER_RE = re.compile(r"\n(?![ \t])|\r(?![ \t\n])") # pragma: no mutate
50 |
51 |
52 | def has_invalid_characters(name: str, value: str) -> bool:
53 | try:
54 | check_header_validity((name, value))
55 | return bool(INVALID_HEADER_RE.search(value))
56 | except InvalidHeader:
57 | return True
58 |
59 |
60 | def is_schemathesis_test(func: Callable) -> bool:
61 | """Check whether test is parametrized with schemathesis."""
62 | try:
63 | from .schemas import BaseSchema # pylint: disable=import-outside-toplevel
64 |
65 | item = getattr(func, "_schemathesis_test", None)
66 | # Comparison is needed to avoid false-positives when mocks are collected by pytest
67 | return isinstance(item, BaseSchema)
68 | except Exception:
69 | return False
70 |
71 |
72 | def force_tuple(item: Filter) -> Union[List, Set, Tuple]:
73 | if not isinstance(item, (list, set, tuple)):
74 | return (item,)
75 | return item
76 |
77 |
78 | def dict_true_values(**kwargs: Any) -> Dict[str, Any]:
79 | """Create a dict with given kwargs while skipping items where bool(value) evaluates to False."""
80 | return {key: value for key, value in kwargs.items() if bool(value)}
81 |
82 |
83 | def dict_not_none_values(**kwargs: Any) -> Dict[str, Any]:
84 | return {key: value for key, value in kwargs.items() if value is not None}
85 |
86 |
87 | IGNORED_PATTERNS = (
88 | "Falsifying example: ",
89 | "You can add @seed",
90 | "Failed to reproduce exception. Expected:",
91 | "Flaky example!",
92 | "Traceback (most recent call last):",
93 | )
94 |
95 |
96 | @contextmanager
97 | def capture_hypothesis_output() -> Generator[List[str], None, None]:
98 | """Capture all output of Hypothesis into a list of strings.
99 |
100 | It allows us to have more granular control over Schemathesis output.
101 |
102 | Usage::
103 |
104 | @given(i=st.integers())
105 | def test(i):
106 | assert 0
107 |
108 | with capture_hypothesis_output() as output:
109 | test() # hypothesis test
110 | # output == ["Falsifying example: test(i=0)"]
111 | """
112 | output = []
113 |
114 | def get_output(value: str) -> None:
115 | # Drop messages that could be confusing in the Schemathesis context
116 | if value.startswith(IGNORED_PATTERNS):
117 | return
118 | output.append(value)
119 |
120 | # the following context manager is untyped
121 | with with_reporter(get_output): # type: ignore
122 | yield output
123 |
124 |
125 | def format_exception(error: Exception, include_traceback: bool = False) -> str:
126 | if include_traceback:
127 | return "".join(traceback.format_exception(type(error), error, error.__traceback__))
128 | return "".join(traceback.format_exception_only(type(error), error))
129 |
130 |
131 | def parse_content_type(content_type: str) -> Tuple[str, str]:
132 | """Parse Content Type and return main type and subtype."""
133 | try:
134 | content_type, _ = cgi.parse_header(content_type)
135 | main_type, sub_type = content_type.split("/", 1)
136 | except ValueError as exc:
137 | raise ValueError(f"Malformed media type: `{content_type}`") from exc
138 | return main_type.lower(), sub_type.lower()
139 |
140 |
141 | def is_json_media_type(value: str) -> bool:
142 | """Detect whether the content type is JSON-compatible.
143 |
144 | For example - `application/problem+json` matches.
145 | """
146 | main, sub = parse_content_type(value)
147 | return main == "application" and (sub == "json" or sub.endswith("+json"))
148 |
149 |
150 | def are_content_types_equal(source: str, target: str) -> bool:
151 | """Check if two content types are the same excluding options."""
152 | return parse_content_type(source) == parse_content_type(target)
153 |
154 |
155 | def make_loader(*tags_to_remove: str) -> Type[yaml.SafeLoader]:
156 | """Create a YAML loader, that doesn't parse specific tokens into Python objects."""
157 | cls: Type[yaml.SafeLoader] = type("YAMLLoader", (SafeLoader,), {})
158 | cls.yaml_implicit_resolvers = {
159 | key: [(tag, regexp) for tag, regexp in mapping if tag not in tags_to_remove]
160 | for key, mapping in cls.yaml_implicit_resolvers.copy().items()
161 | }
162 |
163 | # Fix pyyaml scientific notation parse bug
164 | # See PR: https://github.com/yaml/pyyaml/pull/174 for upstream fix
165 | cls.add_implicit_resolver( # type: ignore
166 | "tag:yaml.org,2002:float",
167 | re.compile(
168 | r"""^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+]?[0-9]+)?
169 | |[-+]?(?:[0-9][0-9_]*)(?:[eE][-+]?[0-9]+)
170 | |\.[0-9_]+(?:[eE][-+]?[0-9]+)?
171 | |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*
172 | |[-+]?\.(?:inf|Inf|INF)
173 | |\.(?:nan|NaN|NAN))$""",
174 | re.X,
175 | ),
176 | list("-+0123456789."),
177 | )
178 |
179 | return cls
180 |
181 |
182 | StringDatesYAMLLoader = make_loader("tag:yaml.org,2002:timestamp")
183 |
184 |
185 | class WSGIResponse(BaseResponse, JSONMixin): # pylint: disable=too-many-ancestors
186 | # We store "requests" request to build a reproduction code
187 | request: requests.PreparedRequest
188 |
189 | def on_json_loading_failed(self, e: JSONDecodeError) -> NoReturn:
190 | # We don't need a werkzeug-specific exception when JSON parsing error happens
191 | raise e
192 |
193 |
194 | def get_requests_auth(auth: Optional[RawAuth], auth_type: Optional[str]) -> Optional[Union[HTTPDigestAuth, RawAuth]]:
195 | if auth and auth_type == "digest":
196 | return HTTPDigestAuth(*auth)
197 | return auth
198 |
199 |
200 | GenericResponse = Union[requests.Response, WSGIResponse] # pragma: no mutate
201 |
202 |
203 | def get_response_payload(response: GenericResponse) -> str:
204 | if isinstance(response, requests.Response):
205 | return response.text
206 | return response.get_data(as_text=True)
207 |
208 |
209 | def import_app(path: str) -> Any:
210 | """Import an application from a string."""
211 | path, name = (re.split(r":(?![\\/])", path, 1) + [""])[:2]
212 | __import__(path)
213 | # accessing the module from sys.modules returns a proper module, while `__import__`
214 | # may return a parent module (system dependent)
215 | module = sys.modules[path]
216 | return getattr(module, name)
217 |
218 |
219 | Schema = Union[Dict[str, Any], List, str, float, int]
220 |
221 |
222 | @overload
223 | def traverse_schema(schema: Dict[str, Any], callback: Callable, *args: Any, **kwargs: Any) -> Dict[str, Any]:
224 | pass
225 |
226 |
227 | @overload
228 | def traverse_schema(schema: List, callback: Callable, *args: Any, **kwargs: Any) -> List:
229 | pass
230 |
231 |
232 | @overload
233 | def traverse_schema(schema: str, callback: Callable, *args: Any, **kwargs: Any) -> str:
234 | pass
235 |
236 |
237 | @overload
238 | def traverse_schema(schema: float, callback: Callable, *args: Any, **kwargs: Any) -> float:
239 | pass
240 |
241 |
242 | def traverse_schema(schema: Schema, callback: Callable[..., Dict[str, Any]], *args: Any, **kwargs: Any) -> Schema:
243 | """Apply callback recursively to the given schema."""
244 | if isinstance(schema, dict):
245 | schema = callback(schema, *args, **kwargs)
246 | for key, sub_item in schema.items():
247 | schema[key] = traverse_schema(sub_item, callback, *args, **kwargs)
248 | elif isinstance(schema, list):
249 | for idx, sub_item in enumerate(schema):
250 | schema[idx] = traverse_schema(sub_item, callback, *args, **kwargs)
251 | return schema
252 |
--------------------------------------------------------------------------------