From 7367bb027639273e1b188858ada166ec6b911035 Mon Sep 17 00:00:00 2001 From: Dusan Haustein Date: Thu, 6 Mar 2025 14:39:27 +0100 Subject: [PATCH] feat: strict type checking --- .github/workflows/ci.yml | 2 +- client.py | 2 +- pyproject.toml | 3 ++ server.py | 2 +- tests/acceptance/test_e2e.py | 4 +- tests/integration/test_client_server.py | 64 ++++++++++++++----------- tests/unit/test_client.py | 2 +- tests/unit/test_server.py | 2 +- 8 files changed, 46 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23e12e0..8316a15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: - name: "Type check with mypy" run: | - uv run mypy . -v + uv run mypy . --strict -v test: runs-on: ubuntu-latest diff --git a/client.py b/client.py index 922475d..459f6ec 100644 --- a/client.py +++ b/client.py @@ -29,7 +29,7 @@ def send_request( return request.decode("utf-8"), data.decode("utf-8") -def main(): +def main() -> None: parser = argparse.ArgumentParser( description="HTTP Client with configurable request" ) diff --git a/pyproject.toml b/pyproject.toml index 171c39d..8c373f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,3 +16,6 @@ pythonpath = "." addopts = [ "--import-mode=importlib", ] + +[tool.mypy] +strict = true diff --git a/server.py b/server.py index ad95655..d09cc95 100644 --- a/server.py +++ b/server.py @@ -47,7 +47,7 @@ def start_server( return response.decode("utf-8"), data.decode("utf-8") -def main(): +def main() -> None: parser = argparse.ArgumentParser( description="HTTP Server with configurable response" ) diff --git a/tests/acceptance/test_e2e.py b/tests/acceptance/test_e2e.py index f25c1f7..231600b 100644 --- a/tests/acceptance/test_e2e.py +++ b/tests/acceptance/test_e2e.py @@ -57,7 +57,7 @@ def client_container(podman_network: str) -> Generator[str, None, None]: # example test cases -def test_basic_http_communication(server_container: str, client_container: str): +def test_basic_http_communication(server_container: str, client_container: str) -> None: """Test basic HTTP request-response cycle""" client = podman.PodmanClient() container = client.containers.get(client_container) @@ -71,6 +71,6 @@ def test_basic_http_communication(server_container: str, client_container: str): assert "HTTP/1.1 200 OK" in output.decode() -def test_http_headers_compliance(server_container: str, client_container: str): +def test_http_headers_compliance(server_container: str, client_container: str) -> None: """Test HTTP headers format compliance""" pass diff --git a/tests/integration/test_client_server.py b/tests/integration/test_client_server.py index dd54796..6f2163b 100644 --- a/tests/integration/test_client_server.py +++ b/tests/integration/test_client_server.py @@ -1,13 +1,17 @@ import random import threading import time -from typing import Generator +from typing import Callable, Generator import pytest from client import send_request from server import contruct_response, start_server +# fun with types +ServerFactoryCallable = Callable[[bytes], tuple[threading.Thread, int]] +ServerFactoryFixture = Generator[ServerFactoryCallable, None, None] + # this is ugly, these asserts should probably run implicitly for almost every test case automatically def assert_server_response_compliant(response: str) -> None: @@ -24,7 +28,7 @@ def assert_client_request_compliant(request: str) -> None: @pytest.fixture(scope="function") -def server_factory() -> Generator: +def server_factory() -> ServerFactoryFixture: """Creates and manages test HTTP server instances with configurable responses Starts the server in a separate thread and stops it after the test completes @@ -35,7 +39,7 @@ def server_factory() -> Generator: - port: The randomly assigned port number (8081-9000) Example: - def test_custom_response(server_factory): + def test_custom_response(server_factory: ServerFactory) -> None: thread, port = server_factory(b"Custom Response") request, response = send_request(b"Test", port=port) assert "Custom Response" in response @@ -43,12 +47,12 @@ def test_custom_response(server_factory): threads = [] def create_server( - response_payload: bytes = b"Request received!", + response_payload: bytes, ) -> tuple[threading.Thread, int]: # ugly hack to randomly assign the port to get around port collision port = random.randint(8081, 9080) - def run_server(): + def run_server() -> None: start_server(contruct_response(response_payload), port=port) thread = threading.Thread(target=run_server, daemon=True) @@ -64,7 +68,7 @@ def run_server(): thread.join(timeout=1) -def test_custom_server_responses(server_factory): +def test_custom_server_responses(server_factory: ServerFactoryCallable) -> None: thread, port = server_factory(b"Custom Response") request, response = send_request(b"Test request", port=port) @@ -74,7 +78,9 @@ def test_custom_server_responses(server_factory): @pytest.mark.parametrize("response_payload", [b"Call 1", b"Call 2", b"Hello yet again"]) -def test_multiple_subsequent_calls(server_factory, response_payload): +def test_multiple_subsequent_calls( + server_factory: ServerFactoryCallable, response_payload: bytes +) -> None: thread, port = server_factory(response_payload) request, response = send_request(b"Test request", port=port) @@ -83,7 +89,7 @@ def test_multiple_subsequent_calls(server_factory, response_payload): assert_server_response_compliant(response) -def test_empty_request_response(server_factory): +def test_empty_request_response(server_factory: ServerFactoryCallable) -> None: thread, port = server_factory(b"") request, response = send_request(b"", port=port) @@ -94,19 +100,21 @@ def test_empty_request_response(server_factory): assert_server_response_compliant(response) -def test_extra_long_request_payload_rejected(server_factory): +def test_extra_long_request_payload_rejected( + server_factory: ServerFactoryCallable, +) -> None: """Test that a request larger than the default buffer size of 1024 the server will reject this with 'Connection reset by peer'""" - thread, port = server_factory() + thread, port = server_factory(b"Request received!") very_long_payload = b"bla" * 1000000000 with pytest.raises(ConnectionResetError): request, response = send_request(very_long_payload, port=port) -def test_special_characters_payload(server_factory): +def test_special_characters_payload(server_factory: ServerFactoryCallable) -> None: special_chars = b"!@#$%^&*()\n\t" - thread, port = server_factory() + thread, port = server_factory(b"Request received!") request, response = send_request(special_chars, port=port) @@ -116,10 +124,10 @@ def test_special_characters_payload(server_factory): assert_server_response_compliant(response) -def test_unicode_payload(server_factory): +def test_unicode_payload(server_factory: ServerFactoryCallable) -> None: unicode_payload = "Grzegorz Brzęczyszczykiewicz かわいい猫".encode("utf-8") - thread, port = server_factory() + thread, port = server_factory(b"Request received!") request, response = send_request(unicode_payload, port=port) assert unicode_payload.decode() in request @@ -128,13 +136,13 @@ def test_unicode_payload(server_factory): assert_server_response_compliant(response) -def test_max_num_connections(server_factory): +def test_max_num_connections(server_factory: ServerFactoryCallable) -> None: """Test server can handle up to 5 (by default) simultaneous connections""" - thread, port = server_factory() + thread, port = server_factory(b"Request received!") threads = [] responses = {} - def make_request(n): + def make_request(n: int) -> None: request, response = send_request(f"Connection {n}".encode(), port=port) responses[n] = response @@ -150,57 +158,57 @@ def make_request(n): assert "Request received!" in responses[i] -def test_over_max_num_connections(server_factory): +def test_over_max_num_connections(server_factory: ServerFactoryCallable) -> None: """Test server will refuse more than default amount of 5 simultaneous connections""" pass -def test_request_timeout(server_factory): +def test_request_timeout(server_factory: ServerFactoryCallable) -> None: """Test client handles server timeout""" pass -def test_content_encoding(server_factory): +def test_content_encoding(server_factory: ServerFactoryCallable) -> None: """Test server properly handles and responds with gzip/deflate content""" pass -def test_redirect_handling(server_factory): +def test_redirect_handling(server_factory: ServerFactoryCallable) -> None: """Test server sends proper 3xx redirects and client can follow them""" pass -def test_chunked_transfer_encoding(server_factory): +def test_chunked_transfer_encoding(server_factory: ServerFactoryCallable) -> None: """Test server can chunk large responses in chunks and client can properly reconstruct them""" pass -def test_connection_keep_alive(server_factory): +def test_connection_keep_alive(server_factory: ServerFactoryCallable) -> None: """Test server honors Connection: keep-alive header""" pass -def test_malformed_http_headers(server_factory): +def test_malformed_http_headers(server_factory: ServerFactoryCallable) -> None: """Test server properly handles malformed headers""" pass -def test_other_http_methods(server_factory): +def test_other_http_methods(server_factory: ServerFactoryCallable) -> None: """Test server correctly implements HEAD, GET, POST etc""" pass -def test_content_negotiation(server_factory): +def test_content_negotiation(server_factory: ServerFactoryCallable) -> None: """Test server respects Accept headers and returns content in correct format""" pass -def test_rate_limiting(server_factory): +def test_rate_limiting(server_factory: ServerFactoryCallable) -> None: """Test server/client can deal with 429 Too Many Requests""" pass -def test_partial_content(server_factory): +def test_partial_content(server_factory: ServerFactoryCallable) -> None: """Test server/client handles 206 Partial Content""" pass diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 11b9be4..81b0077 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -5,7 +5,7 @@ from client import send_request -def test_send_request(): +def test_send_request() -> None: """Test client with a mocked server response We pretend-check the client is 'HTTP complicant' by checking it includes the expected headers diff --git a/tests/unit/test_server.py b/tests/unit/test_server.py index 68b2b63..6bc8fc2 100644 --- a/tests/unit/test_server.py +++ b/tests/unit/test_server.py @@ -5,7 +5,7 @@ from server import contruct_response, start_server -def test_server_connection(): +def test_server_connection() -> None: """Test server with a mocked client connection We mock the socker and fake a client request and check the server response is 'HTTP compliant'