diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2c1a234d..e6d2f685 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -74,4 +74,3 @@ repos: - drf-spectacular - pylint - faker - - httpx diff --git a/openapi_tester/loaders.py b/openapi_tester/loaders.py index 3081726d..622a4636 100644 --- a/openapi_tester/loaders.py +++ b/openapi_tester/loaders.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, cast from urllib.parse import urlparse -import httpx +import requests import yaml from django.urls import Resolver404, resolve from django.utils.functional import cached_property @@ -277,7 +277,7 @@ def load_schema(self) -> dict[str, Any]: :return: Schema contents as a dict :raises: ImproperlyConfigured """ - response = httpx.get(self.url) + response = requests.get(self.url, timeout=20) return cast( "dict", json.loads(response.content) diff --git a/openapi_tester/schema_tester.py b/openapi_tester/schema_tester.py index 48041cfe..331df48e 100644 --- a/openapi_tester/schema_tester.py +++ b/openapi_tester/schema_tester.py @@ -5,7 +5,8 @@ from typing import TYPE_CHECKING, Any, Callable, cast from django.conf import settings -from django.core.exceptions import ImproperlyConfigured +from django.core.exceptions import ImproperlyConfigured, ValidationError +from django.core.validators import URLValidator from openapi_tester.constants import ( INIT_ERROR, @@ -24,7 +25,7 @@ StaticSchemaLoader, UrlStaticSchemaLoader, ) -from openapi_tester.utils import is_path_an_url, lazy_combinations, normalize_schema_section +from openapi_tester.utils import lazy_combinations, normalize_schema_section from openapi_tester.validators import ( validate_enum, validate_format, @@ -75,11 +76,11 @@ def __init__( self.validators = validators or [] if schema_file_path is not None: - self.loader = ( - UrlStaticSchemaLoader(schema_file_path, field_key_map=field_key_map) - if is_path_an_url(schema_file_path) - else StaticSchemaLoader(schema_file_path, field_key_map=field_key_map) - ) + try: + URLValidator()(schema_file_path) + self.loader = UrlStaticSchemaLoader(schema_file_path, field_key_map=field_key_map) + except ValidationError: + self.loader = StaticSchemaLoader(schema_file_path, field_key_map=field_key_map) elif "drf_spectacular" in settings.INSTALLED_APPS: self.loader = DrfSpectacularSchemaLoader(field_key_map=field_key_map) elif "drf_yasg" in settings.INSTALLED_APPS: diff --git a/openapi_tester/utils.py b/openapi_tester/utils.py index 294ff7da..e07c7517 100644 --- a/openapi_tester/utils.py +++ b/openapi_tester/utils.py @@ -3,7 +3,6 @@ """ from __future__ import annotations -import re from copy import deepcopy from itertools import chain, combinations from typing import TYPE_CHECKING @@ -59,17 +58,3 @@ def lazy_combinations(options_list: Sequence[dict[str, Any]]) -> Iterator[dict]: for i in range(2, len(options_list) + 1): for combination in combinations(options_list, i): yield merge_objects(combination) - - -def is_path_an_url(path: str) -> bool: - regex = re.compile( - r"^(?:http|ftp)s?://" # http:// or https:// - r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain - r"(?:[A-Za-z._-]+)|" # any hostname - r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # or ip - r"(?::\d+)?" # optional port - r"(?:/?|[/?]\S+)$", - re.IGNORECASE, - ) - - return re.match(regex, path) is not None diff --git a/poetry.lock b/poetry.lock index 448f3a58..9e5fa445 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,21 +1,3 @@ -[[package]] -name = "anyio" -version = "3.6.2" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "main" -optional = false -python-versions = ">=3.6.2" - -[package.dependencies] -idna = ">=2.8" -sniffio = ">=1.1" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16,<0.22)"] - [[package]] name = "asgiref" version = "3.5.2" @@ -57,7 +39,7 @@ python-versions = ">=3.5" dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "backports.zoneinfo" @@ -103,13 +85,13 @@ optional = false python-versions = ">=3.6.0" [package.extras] -unicode_backport = ["unicodedata2"] +unicode-backport = ["unicodedata2"] [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" @@ -265,7 +247,7 @@ validation = ["swagger-spec-validator (>=2.1.0)"] name = "exceptiongroup" version = "1.0.1" description = "Backport of PEP 654 (exception groups)" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" @@ -296,52 +278,6 @@ python-versions = ">=3.7" docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] -[[package]] -name = "h11" -version = "0.12.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "httpcore" -version = "0.15.0" -description = "A minimal low-level HTTP client." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -anyio = ">=3.0.0,<4.0.0" -certifi = "*" -h11 = ">=0.11,<0.13" -sniffio = ">=1.0.0,<2.0.0" - -[package.extras] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] - -[[package]] -name = "httpx" -version = "0.23.0" -description = "The next generation HTTP client." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -certifi = "*" -httpcore = ">=0.15.0,<0.16.0" -rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} -sniffio = "*" - -[package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] - [[package]] name = "identify" version = "2.5.8" @@ -405,7 +341,7 @@ python-versions = ">=3.5" name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "main" +category = "dev" optional = false python-versions = "*" @@ -419,9 +355,9 @@ python-versions = ">=3.6.1,<4.0" [package.extras] colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +pipfile-deprecated-finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] -requirements_deprecated_finder = ["pip-api", "pipreqs"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] [[package]] name = "itypes" @@ -593,7 +529,7 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -688,7 +624,7 @@ python-versions = ">=3.7" name = "pytest" version = "7.2.0" description = "pytest: simple powerful testing with Python" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" @@ -720,21 +656,6 @@ pytest = ">=5.4.0" docs = ["sphinx", "sphinx-rtd-theme"] testing = ["Django", "django-configurations (>=2.0)"] -[[package]] -name = "pytest-httpx" -version = "0.21.2" -description = "Send responses to httpx." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -httpx = ">=0.23.0,<0.24.0" -pytest = ">=6,<8" - -[package.extras] -testing = ["pytest-asyncio (>=0.20.0,<0.21.0)", "pytest-cov (>=4.0.0,<5.0.0)"] - [[package]] name = "python-dateutil" version = "2.8.2" @@ -778,21 +699,7 @@ urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "rfc3986" -version = "1.5.0" -description = "Validating URI References per RFC 3986" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} - -[package.extras] -idna2008 = ["idna"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruamel.yaml" @@ -846,14 +753,6 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -[[package]] -name = "sniffio" -version = "1.3.0" -description = "Sniff out which async library your code is running under" -category = "main" -optional = false -python-versions = ">=3.7" - [[package]] name = "sqlparse" version = "0.4.3" @@ -874,7 +773,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" @@ -968,13 +867,9 @@ drf-yasg = ["drf-yasg"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "f764972b14bcacbc42b01be64f7ff49123963d08cc080f5911cf1f42c470ecf7" +content-hash = "d7dd4650d62970ec2522137937cbd498795c2a2c4afc49625535600c994acf34" [metadata.files] -anyio = [ - {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, - {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, -] asgiref = [ {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, @@ -1123,18 +1018,6 @@ filelock = [ {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, ] -h11 = [ - {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, - {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, -] -httpcore = [ - {file = "httpcore-0.15.0-py3-none-any.whl", hash = "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6"}, - {file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"}, -] -httpx = [ - {file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"}, - {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"}, -] identify = [ {file = "identify-2.5.8-py2.py3-none-any.whl", hash = "sha256:48b7925fe122720088aeb7a6c34f17b27e706b72c61070f27fe3789094233440"}, {file = "identify-2.5.8.tar.gz", hash = "sha256:7a214a10313b9489a0d61467db2856ae8d0b8306fc923e03a9effa53d8aedc58"}, @@ -1326,10 +1209,6 @@ pytest-django = [ {file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"}, {file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"}, ] -pytest-httpx = [ - {file = "pytest_httpx-0.21.2-py3-none-any.whl", hash = "sha256:a6ea51ab4e603507b4e91ac868e62c8a40dde85ded9f5dd71d6e29b0e8d5a217"}, - {file = "pytest_httpx-0.21.2.tar.gz", hash = "sha256:39596a2caa7aeb384de7f1cf134bcc4595177a449af7d0fc9bd5916cad515464"}, -] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, @@ -1384,10 +1263,6 @@ requests = [ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, ] -rfc3986 = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] "ruamel.yaml" = [ {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"}, {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"}, @@ -1439,10 +1314,6 @@ six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -sniffio = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, -] sqlparse = [ {file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"}, {file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"}, diff --git a/pyproject.toml b/pyproject.toml index a087c612..e4badfd1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,8 +57,6 @@ prance = "*" pyYAML = "*" drf-spectacular = { version = "*", optional = true } drf-yasg = { version = "*", optional = true } -httpx = "^0.23.0" -pytest-httpx = "^0.21.2" [tool.poetry.extras] drf-yasg = ["drf-yasg"] diff --git a/tests/test_loaders.py b/tests/test_loaders.py index 9a55c548..17eda4c9 100644 --- a/tests/test_loaders.py +++ b/tests/test_loaders.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from unittest.mock import Mock, patch import pytest @@ -12,9 +12,6 @@ ) from tests.utils import TEST_ROOT, get_schema_content -if TYPE_CHECKING: - from pytest_httpx import HTTPXMock - yaml_schema_path = str(TEST_ROOT) + "/schemas/manual_reference_schema.yaml" json_schema_path = str(TEST_ROOT) + "/schemas/manual_reference_schema.json" @@ -35,19 +32,14 @@ def test_loader_get_schema(loader): loader.get_schema() # runs internal validation -def test_url_schema_loader(httpx_mock: HTTPXMock): +def test_url_schema_loader(): test_schema_url = "http://schemas:8080/test/schema.yaml" schema_loader = UrlStaticSchemaLoader(test_schema_url) schema_content = get_schema_content(TEST_ROOT / "schemas" / "any_of_one_of_test_schema.yaml") - httpx_mock.add_response( - method="GET", - url="http://schemas:8080/test/schema.yaml", - content=schema_content, - status_code=200, - ) - - loaded_schema = schema_loader.load_schema() + with patch("openapi_tester.loaders.requests.get") as mocked_get_request: + mocked_get_request.return_value = Mock(content=schema_content) + loaded_schema = schema_loader.load_schema() assert type(loaded_schema) == dict assert loaded_schema["openapi"] == "3.0.0" diff --git a/tests/test_schema_tester.py b/tests/test_schema_tester.py index 686e732f..df8ab74f 100644 --- a/tests/test_schema_tester.py +++ b/tests/test_schema_tester.py @@ -27,6 +27,7 @@ VALIDATE_WRITE_ONLY_RESPONSE_KEY_ERROR, ) from openapi_tester.exceptions import CaseError, DocumentationError, UndocumentedSchemaSectionError +from openapi_tester.loaders import UrlStaticSchemaLoader from test_project.models import Names from tests import example_object, example_schema_types from tests.utils import TEST_ROOT, iterate_schema, mock_schema, response_factory @@ -100,6 +101,9 @@ def test_loader_inference(settings): # Test static loader assert isinstance(SchemaTester(schema_file_path="test").loader, StaticSchemaLoader) + # Test url static loader + assert isinstance(SchemaTester(schema_file_path="http://test.url:8080/schema.yaml").loader, UrlStaticSchemaLoader) + # Test no loader settings.INSTALLED_APPS = [] with pytest.raises(ImproperlyConfigured, match=INIT_ERROR): diff --git a/tests/test_utils.py b/tests/test_utils.py index 5ddba1bb..602beb04 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,4 @@ -import pytest - -from openapi_tester.utils import is_path_an_url, merge_objects +from openapi_tester.utils import merge_objects from tests.utils import sort_object object_1 = {"type": "object", "required": ["key1"], "properties": {"key1": {"type": "string"}}} @@ -32,17 +30,3 @@ def test_merge_objects(): "properties": {"key1": {"type": "string"}, "key2": {"type": "string"}}, } assert sort_object(merge_objects(test_schemas)) == sort_object(expected) - - -@pytest.mark.parametrize( - ("path", "is_url"), - [ - ("/path/to/schema", False), - ("http://www.schema.com", True), - ("https://www.schema.com", True), - ("http://schemas:8000", True), - ("http://schemas:8000/path/to/schema.yaml", True), - ], -) -def test_is_path_an_url(path: str, is_url: bool): - assert is_path_an_url(path) == is_url