diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7dd0410..1486fdb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,8 +11,7 @@ jobs: strategy: matrix: - # We don't test on Windows currently as it appears mocket may not - # work there. + # TODO: add windows-latest platform: [ubuntu-latest, macos-latest] python-version: [3.8, 3.9, "3.10", 3.11, 3.12] diff --git a/pyproject.toml b/pyproject.toml index 157fd73..196f4c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ classifiers = [ [project.optional-dependencies] test = [ - "mocket>=3.12.8", + "pytest-httpserver>=1.0.10", ] [tool.setuptools.package-data] diff --git a/setup.cfg b/setup.cfg index 80fea57..6c53b8c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,7 @@ python = [testenv:{py38,py39,py310,py311,py312}-test] deps = - mocket + pytest-httpserver pytest commands = pytest tests @@ -35,6 +35,8 @@ commands = flake8 minfraud [testenv:py312-mypy] deps = mypy + pytest_httpserver + pytest types-requests voluptuous-stubs commands = mypy minfraud tests diff --git a/tests/test_webservice.py b/tests/test_webservice.py index 5d28529..f3d6c34 100644 --- a/tests/test_webservice.py +++ b/tests/test_webservice.py @@ -3,10 +3,8 @@ import os from io import open from typing import Type, Union - -# httpretty currently doesn't work, but mocket with the compat interface -# does. See, e.g., https://github.com/gabrielfalcao/HTTPretty/issues/220 -from mocket.plugins.httpretty import httpretty, httprettified # type: ignore +import pytest_httpserver +import pytest from minfraud.errors import ( HTTPError, @@ -25,8 +23,24 @@ class BaseTest(unittest.TestCase): client_class: Union[Type[AsyncClient], Type[Client]] = Client + @pytest.fixture(autouse=True) + def setup_httpserver(self, httpserver: pytest_httpserver.HTTPServer): + self.httpserver = httpserver + def setUp(self): self.client = self.client_class(42, "abcdef123456") + self.client._base_uri = self.httpserver.url_for("/") + "minfraud/v2.0" + self.client._factors_uri = ( + self.httpserver.url_for("/") + "minfraud/v2.0/factors" + ) + self.client._insights_uri = ( + self.httpserver.url_for("/") + "minfraud/v2.0/insights" + ) + self.client._score_uri = self.httpserver.url_for("/") + "minfraud/v2.0/score" + self.client._report_uri = ( + self.httpserver.url_for("/") + "minfraud/v2.0/transactions/report" + ) + self.base_uri = self.client._base_uri test_dir = os.path.join(os.path.dirname(__file__), "data") with open(os.path.join(test_dir, self.request_file), encoding="utf-8") as file: @@ -36,9 +50,6 @@ def setUp(self): with open(os.path.join(test_dir, self.response_file), encoding="utf-8") as file: self.response = file.read() - base_uri = "https://minfraud.maxmind.com/minfraud/v2.0" - - @httprettified def test_invalid_auth(self): for error in ( "ACCOUNT_ID_REQUIRED", @@ -52,19 +63,16 @@ def test_invalid_auth(self): status_code=401, ) - @httprettified def test_invalid_request(self): with self.assertRaisesRegex(InvalidRequestError, "IP invalid"): self.create_error(text='{"code":"IP_ADDRESS_INVALID","error":"IP invalid"}') - @httprettified def test_300_error(self): with self.assertRaisesRegex( HTTPError, r"Received an unexpected HTTP status \(300\) for" ): self.create_error(status_code=300) - @httprettified def test_permission_required(self): with self.assertRaisesRegex(PermissionRequiredError, "permission"): self.create_error( @@ -72,7 +80,6 @@ def test_permission_required(self): status_code=403, ) - @httprettified def test_400_with_invalid_json(self): with self.assertRaisesRegex( HTTPError, @@ -81,19 +88,16 @@ def test_400_with_invalid_json(self): ): self.create_error(text="{blah}") - @httprettified def test_400_with_no_body(self): with self.assertRaisesRegex(HTTPError, "Received a 400 error with no body"): self.create_error() - @httprettified def test_400_with_unexpected_content_type(self): with self.assertRaisesRegex( HTTPError, "Received a 400 with the following body: b?'?plain'?" ): self.create_error(content_type="text/plain", text="plain") - @httprettified def test_400_without_json_body(self): with self.assertRaisesRegex( HTTPError, @@ -102,7 +106,6 @@ def test_400_without_json_body(self): ): self.create_error(text="plain") - @httprettified def test_400_with_unexpected_json(self): with self.assertRaisesRegex( HTTPError, @@ -111,16 +114,15 @@ def test_400_with_unexpected_json(self): ): self.create_error(text='{"not":"expected"}') - @httprettified def test_500_error(self): with self.assertRaisesRegex(HTTPError, r"Received a server error \(500\) for"): self.create_error(status_code=500) def create_error(self, status_code=400, text="", content_type=None): uri = "/".join( - [self.base_uri, "transactions", "report"] + ["/minfraud/v2.0", "transactions", "report"] if self.type == "report" - else [self.base_uri, self.type] + else ["/minfraud/v2.0", self.type] ) if content_type is None: content_type = ( @@ -128,38 +130,37 @@ def create_error(self, status_code=400, text="", content_type=None): if self.type == "report" else "application/vnd.maxmind.com-error+json; charset=UTF-8; version=2.0" ) - httpretty.register_uri( - httpretty.POST, - uri=uri, - status=status_code, - body=text, + self.httpserver.expect_request(uri, method="POST").respond_with_data( + text, content_type=content_type, + status=status_code, ) return self.run_client(getattr(self.client, self.type)(self.full_request)) def create_success(self, text=None, client=None, request=None): uri = "/".join( - [self.base_uri, "transactions", "report"] + ["/minfraud/v2.0", "transactions", "report"] if self.type == "report" - else [self.base_uri, self.type] + else ["/minfraud/v2.0", self.type] ) - httpretty.register_uri( - httpretty.POST, - uri=uri, - status=204 if self.type == "report" else 200, - body=self.response if text is None else text, + if request is None: + request = self.full_request + + response = self.response if text is None else text + status = 204 if self.type == "report" else 200 + self.httpserver.expect_request(uri, method="POST").respond_with_data( + response, content_type=f"application/vnd.maxmind.com-minfraud-{self.type}+json; charset=UTF-8; version=2.0", + status=status, ) if client is None: client = self.client - if request is None: - request = self.full_request + return self.run_client(getattr(client, self.type)(request)) def run_client(self, v): return v - @httprettified def test_named_constructor_args(self): id = "47" key = "1234567890ab" @@ -170,7 +171,6 @@ def test_named_constructor_args(self): self.assertEqual(client._account_id, id) self.assertEqual(client._license_key, key) - @httprettified def test_missing_constructor_args(self): with self.assertRaises(TypeError): self.client_class(license_key="1234567890ab") @@ -180,10 +180,10 @@ def test_missing_constructor_args(self): class BaseTransactionTest(BaseTest): + def has_ip_location(self): return self.type in ["factors", "insights"] - @httprettified def test_200(self): model = self.create_success() response = json.loads(self.response) @@ -197,7 +197,6 @@ def test_200(self): self.assertEqual("004", model.ip_address.traits.mobile_network_code) self.assertEqual("ANONYMOUS_IP", model.ip_address.risk_reasons[0].code) - @httprettified def test_200_on_request_with_nones(self): model = self.create_success( request={ @@ -215,20 +214,25 @@ def test_200_on_request_with_nones(self): response = self.response self.assertEqual(0.01, model.risk_score) - @httprettified def test_200_with_email_hashing(self): - uri = "/".join([self.base_uri, self.type]) + uri = "/".join(["/minfraud/v2.0", self.type]) - httpretty.register_uri( - httpretty.POST, - uri=uri, - status=200, - body=self.response, - content_type=f"application/vnd.maxmind.com-minfraud-{self.type}+json; charset=UTF-8; version=2.0", + last = None + + def custom_handler(r): + nonlocal last + last = r + return "hello world" + + self.httpserver.expect_request(uri, method="POST").respond_with_handler( + custom_handler ) request = {"email": {"address": "Test+ignore@maxmind.com"}} - self.run_client(getattr(self.client, self.type)(request, hash_email=True)) + try: + self.run_client(getattr(self.client, self.type)(request, hash_email=True)) + except Exception as e: + pass self.assertEqual( { @@ -237,14 +241,21 @@ def test_200_with_email_hashing(self): "domain": "maxmind.com", } }, - json.loads(httpretty.last_request.body), + json.loads(last.data.decode("utf-8")), ) # This was fixed in https://github.com/maxmind/minfraud-api-python/pull/78 - @httprettified + def test_200_with_locales(self): locales = ("fr",) client = self.client_class(42, "abcdef123456", locales=locales) + client._base_uri = self.httpserver.url_for("/") + "minfraud/v2.0" + client._factors_uri = self.httpserver.url_for("/") + "minfraud/v2.0/factors" + client._insights_uri = self.httpserver.url_for("/") + "minfraud/v2.0/insights" + client._score_uri = self.httpserver.url_for("/") + "minfraud/v2.0/score" + client._report_uri = ( + self.httpserver.url_for("/") + "minfraud/v2.0/transactions/report" + ) model = self.create_success(client=client) response = json.loads(self.response) if self.has_ip_location(): @@ -254,7 +265,6 @@ def test_200_with_locales(self): self.assertEqual("Royaume-Uni", model.ip_address.country.name) self.assertEqual("Londres", model.ip_address.city.name) - @httprettified def test_200_with_reserved_ip_warning(self): model = self.create_success( """ @@ -275,7 +285,6 @@ def test_200_with_reserved_ip_warning(self): self.assertEqual(12, model.risk_score) - @httprettified def test_200_with_no_body(self): with self.assertRaisesRegex( MinFraudError, @@ -284,7 +293,6 @@ def test_200_with_no_body(self): ): self.create_success(text="") - @httprettified def test_200_with_invalid_json(self): with self.assertRaisesRegex( MinFraudError, @@ -293,7 +301,6 @@ def test_200_with_invalid_json(self): ): self.create_success(text="{") - @httprettified def test_insufficient_funds(self): with self.assertRaisesRegex(InsufficientFundsError, "out of funds"): self.create_error( @@ -328,11 +335,9 @@ class TestReportTransaction(BaseTest): request_file = "full-report-request.json" response_file = "report-response.json" - @httprettified def test_204(self): self.create_success() - @httprettified def test_204_on_request_with_nones(self): self.create_success( request={ @@ -347,6 +352,10 @@ def test_204_on_request_with_nones(self): class AsyncBase: + @pytest.fixture(autouse=True) + def setup_httpserver(self, httpserver: pytest_httpserver.HTTPServer): + self.httpserver = httpserver + def setUp(self): self._loop = asyncio.new_event_loop() super().setUp()