diff options
| author | Jason McCormick <jason@mfamily.org> | 2026-10-16 16:57:47 -0400 |
|---|---|---|
| committer | Jason McCormick <jason@mfamily.org> | 2026-10-16 16:57:47 -0400 |
| commit | 98119d8fb55e1633d7bdd79db30fd5caea9ee641 (patch) | |
| tree | 370d5aefedfc3eba4d69a6682f7d6139701a2539 /tests/test_server.py | |
Diffstat (limited to 'tests/test_server.py')
| -rw-r--r-- | tests/test_server.py | 118 |
1 files changed, 118 insertions, 0 deletions
diff --git a/tests/test_server.py b/tests/test_server.py new file mode 100644 index 0000000..aaff61e --- /dev/null +++ b/tests/test_server.py @@ -0,0 +1,118 @@ +import http.client +import tempfile +import threading +import unittest +from pathlib import Path + +from server import MAX_FILE_BYTES, detect_image_type, make_server + + +PNG = b"\x89PNG\r\n\x1a\n" + b"test-image" + + +def multipart(filename: str, payload: bytes, boundary: str = "test-boundary") -> tuple[bytes, str]: + body = ( + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="image"; filename="{filename}"\r\n' + "Content-Type: application/octet-stream\r\n\r\n" + ).encode() + payload + f"\r\n--{boundary}--\r\n".encode() + return body, f"multipart/form-data; boundary={boundary}" + + +class ServerTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.upload_dir = Path(self.temp.name) / "private" + self.server = make_server("127.0.0.1", 0, self.upload_dir) + self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) + self.thread.start() + self.port = self.server.server_address[1] + + def tearDown(self): + self.server.shutdown() + self.server.server_close() + self.thread.join() + self.temp.cleanup() + + def request(self, method, path, body=None, headers=None): + connection = http.client.HTTPConnection("127.0.0.1", self.port, timeout=3) + connection.request(method, path, body=body, headers=headers or {}) + response = connection.getresponse() + data = response.read() + connection.close() + return response, data + + def test_accepts_png_and_stores_it_under_target(self): + body, content_type = multipart("photo.png", PNG) + response, data = self.request( + "POST", "/logo-contest", body, {"Content-Type": content_type, "Content-Length": str(len(body))} + ) + self.assertEqual(response.status, 201, data) + target_dir = self.upload_dir / "logo-contest" + files = list(target_dir.iterdir()) + self.assertEqual(len(files), 1) + self.assertEqual(files[0].read_bytes(), PNG) + self.assertNotIn("photo", files[0].name) + self.assertEqual(files[0].stat().st_mode & 0o777, 0o600) + self.assertEqual(target_dir.stat().st_mode & 0o777, 0o700) + + hidden_response, _ = self.request("GET", f"/logo-contest/{files[0].name}") + self.assertEqual(hidden_response.status, 404) + + def test_root_is_rejected_but_target_page_is_available(self): + root_response, _ = self.request("GET", "/") + target_response, page = self.request("GET", "/911u19jasdl") + self.assertEqual(root_response.status, 418) + self.assertEqual(target_response.status, 200) + self.assertIn(b"Private Drop", page) + + def test_upload_to_root_is_rejected_as_teapot(self): + body, content_type = multipart("photo.png", PNG) + response, _ = self.request( + "POST", "/", body, {"Content-Type": content_type, "Content-Length": str(len(body))} + ) + self.assertEqual(response.status, 418) + + def test_targets_are_stored_in_separate_directories(self): + for target in ("logo-contest", "911u19jasdl"): + body, content_type = multipart("photo.png", PNG) + response, _ = self.request( + "POST", f"/{target}", body, {"Content-Type": content_type, "Content-Length": str(len(body))} + ) + self.assertEqual(response.status, 201) + self.assertEqual(len(list((self.upload_dir / target).iterdir())), 1) + + def test_invalid_or_nested_targets_are_rejected(self): + for path in ("/bad.target", "/nested/target", "/%2e%2e", "/healthz"): + body, content_type = multipart("photo.png", PNG) + response, _ = self.request( + "POST", path, body, {"Content-Type": content_type, "Content-Length": str(len(body))} + ) + self.assertEqual(response.status, 404) + + def test_rejects_non_image_content(self): + body, content_type = multipart("fake.jpg", b"not really an image") + response, _ = self.request( + "POST", "/logo-contest", body, {"Content-Type": content_type, "Content-Length": str(len(body))} + ) + self.assertEqual(response.status, 400) + self.assertFalse((self.upload_dir / "logo-contest").exists()) + + def test_rejects_oversized_request_before_reading_body(self): + response, _ = self.request( + "POST", "/logo-contest", b"", {"Content-Type": "multipart/form-data; boundary=x", "Content-Length": str(MAX_FILE_BYTES + 1024 * 1024 + 1)} + ) + self.assertEqual(response.status, 413) + + def test_unknown_paths_are_not_served(self): + response, _ = self.request("GET", "/server.py") + self.assertEqual(response.status, 404) + + def test_detects_supported_signatures(self): + self.assertEqual(detect_image_type(b"\xff\xd8\xffrest"), (".jpg", "image/jpeg")) + self.assertEqual(detect_image_type(PNG), (".png", "image/png")) + self.assertEqual(detect_image_type(b"\x00\x00\x00\x18ftypheic\x00\x00\x00\x00heic"), (".heic", "image/heic")) + + +if __name__ == "__main__": + unittest.main() |
