diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b0d6c7f..1e573f5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest coverage codecov + pip install pytest coverage codecov httpretty pip install . type python type pip diff --git a/tests/tests_client.py b/tests/tests_client.py index df476a4..fa17afc 100644 --- a/tests/tests_client.py +++ b/tests/tests_client.py @@ -1,13 +1,13 @@ +import json import unittest import warnings from datetime import datetime, timedelta -from http.client import HTTPMessage -from unittest.mock import ANY, Mock, call, patch +from unittest.mock import Mock, patch from uuid import UUID, uuid4 -from sypht.client import SyphtClient +import httpretty -from .util.mock_http_server import MockRequestHandler, MockServerSession +from sypht.client import SyphtClient def validate_uuid4(uuid_string): @@ -45,8 +45,6 @@ def test_data_extraction_1(self): fid = self.sypht_client.upload(f, ["invoices:2"]) self.assertTrue(validate_uuid4(fid)) - import json - print("<< fid", fid) results = self.sypht_client.fetch_results(fid) print("<< results", json.dumps(results, indent=2)) @@ -105,65 +103,70 @@ class RetryTest(unittest.TestCase): @patch.object(SyphtClient, "_authenticate_v2", return_value=("access_token", 100)) @patch.object(SyphtClient, "_authenticate_v1", return_value=("access_token2", 100)) - def test_it_should_eventually_fail_for_50x(self, auth_v1: Mock, auth_v2: Mock): + @httpretty.activate(verbose=True, allow_net_connect=False) + def test_it_should_retry_n_times(self, auth_v1: Mock, auth_v2: Mock): # arrange - requests = [] - - def create_request_handler(*args, **kwargs): - response_sequences = { - "/app/annotations?offset=0&fromDate=2021-01-01&toDate=2021-01-01": [ - (502, {}), - # Retries start from here... - # There should be n for where Retry(status=n). - (503, {}), - (504, {}), - (502, {}), - ], - } - return MockRequestHandler( - *args, **kwargs, requests=requests, responses=response_sequences - ) - - with MockServerSession(create_request_handler) as address: - sypht_client = SyphtClient(base_endpoint=address) - - # act / assert - with self.assertRaisesRegex(Exception, ".") as e: - sypht_client.get_annotations( - from_date=datetime( - year=2021, month=1, day=1, hour=0, minute=0, second=0 - ).strftime("%Y-%m-%d"), - to_date=datetime( - year=2021, month=1, day=1, hour=0, minute=0, second=0 - ).strftime("%Y-%m-%d"), - ) + self.count = 0 + + def get_annotations(request, uri, response_headers): + self.count += 1 + # 1 req + 3 retries = 4 + if self.count == 4: + return [200, response_headers, json.dumps({"annotations": []})] + return [502, response_headers, json.dumps({})] + + httpretty.register_uri( + httpretty.GET, + "https://api.sypht.com/app/annotations?offset=0&fromDate=2021-01-01&toDate=2021-01-01", + body=get_annotations, + ) + + sypht_client = SyphtClient(base_endpoint="https://api.sypht.com") + + # act / assert + response = sypht_client.get_annotations( + from_date=datetime( + year=2021, month=1, day=1, hour=0, minute=0, second=0 + ).strftime("%Y-%m-%d"), + to_date=datetime( + year=2021, month=1, day=1, hour=0, minute=0, second=0 + ).strftime("%Y-%m-%d"), + ) + + assert response == {"annotations": []} - self.assertEqual( - [ - ( - "GET", - "/app/annotations?offset=0&fromDate=2021-01-01&toDate=2021-01-01", - {}, - ), - ( - "GET", - "/app/annotations?offset=0&fromDate=2021-01-01&toDate=2021-01-01", - {}, - ), - ( - "GET", - "/app/annotations?offset=0&fromDate=2021-01-01&toDate=2021-01-01", - {}, - ), - ( - "GET", - "/app/annotations?offset=0&fromDate=2021-01-01&toDate=2021-01-01", - {}, - ), - ], - requests, + @patch.object(SyphtClient, "_authenticate_v2", return_value=("access_token", 100)) + @patch.object(SyphtClient, "_authenticate_v1", return_value=("access_token2", 100)) + @httpretty.activate(verbose=True, allow_net_connect=False) + def test_retry_should_eventually_fail_for_50x(self, auth_v1: Mock, auth_v2: Mock): + # arrange + self.count = 0 + + def get_annotations(request, uri, response_headers): + self.count += 1 + return [502, response_headers, json.dumps({})] + + httpretty.register_uri( + httpretty.GET, + "https://api.sypht.com/app/annotations?offset=0&fromDate=2021-01-01&toDate=2021-01-01", + body=get_annotations, + ) + + sypht_client = SyphtClient(base_endpoint="https://api.sypht.com") + + # act / assert + with self.assertRaisesRegex(Exception, ".") as e: + sypht_client.get_annotations( + from_date=datetime( + year=2021, month=1, day=1, hour=0, minute=0, second=0 + ).strftime("%Y-%m-%d"), + to_date=datetime( + year=2021, month=1, day=1, hour=0, minute=0, second=0 + ).strftime("%Y-%m-%d"), ) + assert self.count == 4, "should be 1 req + 3 retries" + if __name__ == "__main__": unittest.main() diff --git a/tests/util/__init__.py b/tests/util/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/util/mock_http_server.py b/tests/util/mock_http_server.py deleted file mode 100644 index 3a3499a..0000000 --- a/tests/util/mock_http_server.py +++ /dev/null @@ -1,91 +0,0 @@ -import http.server -import json -import socketserver -import threading -from typing import Callable - - -class MockRequestHandler(http.server.SimpleHTTPRequestHandler): - # class MockRequestHandler(http.server.CGIHTTPRequestHandler): - def __init__(self, *args, responses=None, requests=[], **kwargs): - self.response_sequences = responses - self.requests = requests - super().__init__(*args, **kwargs) - - def log_message(self, format, *args): - """Suppress logging to stdout.""" - pass - - def do_GET(self): - status = 404 - response = {} - if self.path in self.response_sequences: - responses = self.response_sequences[self.path] - if responses: - print(f"<< pop {self.path}") - status, response = responses.pop(0) - self.requests.append((self.command, self.path, response)) - else: - raise Exception(f"Unexpected path: {self.path}") - - self.send_response(status) - self.send_header("Content-type", "application/json") - self.end_headers() - if response: - self.wfile.write(json.dumps(response).encode()) - - -class MockServer(socketserver.TCPServer): - allow_reuse_address = True - """I think this implements socket.SO_REUSEADDR, which allows the server to restart without waiting for a TIME_WAIT to expire from a previous run of the code that left a socket dangling (in a separate process). Otherwise back-to-back server starts can fail with "socket already in use" error.""" - - -def start_test_server( - create_request_handler: Callable[..., http.server.BaseHTTPRequestHandler] -): - host = "localhost" - port = 4444 - address = f"http://{host}:{port}" - httpd = MockServer((host, port), create_request_handler) - httpd_thread = threading.Thread(target=httpd.serve_forever) - httpd_thread.daemon = True - httpd_thread.start() - return address, httpd, httpd_thread - - -class MockServerSession: - """Use this in tests to start a test server and shut it down when the test is done. - - Example: - - def create_request_handler(*args, **kwargs): - ... - return MockRequestHandler(*args, **kwargs, responses=response_sequences) - - with TestServerSession(create_request_handler): - ... - """ - - __test__ = False - """Stop pytest trying to "collect" this class as a test.""" - - def __init__( - self, create_request_handler: Callable[..., http.server.BaseHTTPRequestHandler] - ): - self.create_request_handler = create_request_handler - - def __enter__(self): - self.address, self.httpd, self.httpd_thread = start_test_server( - self.create_request_handler - ) - return self.address - - def __exit__(self, exc_type, exc_val, exc_tb): - self.httpd.shutdown() - self.httpd_thread.join() - - -if __name__ == "__main__": - # To test this server in the terminal, run: - httpd, httpd_thread = start_test_server() - httpd_thread.join()