Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ pythonpath = "."
addopts = [
"--import-mode=importlib",
]

[tool.mypy]
strict = true
2 changes: 1 addition & 1 deletion server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
4 changes: 2 additions & 2 deletions tests/acceptance/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
64 changes: 36 additions & 28 deletions tests/integration/test_client_server.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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

Expand All @@ -35,20 +39,20 @@ 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
"""
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)
Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -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
Expand All @@ -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

Expand All @@ -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
2 changes: 1 addition & 1 deletion tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down