summaryrefslogtreecommitdiff
path: root/tests/test_server.py
blob: aaff61e970b60d7928f45ad920899376928e4090 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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()