66 loop. Regular ``def`` tests run normally without one.
77
88 Use ``async def`` for tests that call ASGI apps (they are async callables).
9- Use plain ``def`` for tests that only exercise sync APIs like StaticFiles ()
9+ Use plain ``def`` for tests that only exercise sync APIs like HashedStatic ()
1010 construction, url(), and file_map lookups.
1111
1212 Do NOT write ``async def`` for a test that has no await in its body. It will
2020
2121import pytest
2222
23- from staticware import StaticFiles , StaticRewriteMiddleware
23+ from staticware import HashedStatic , StaticRewriteMiddleware
2424
2525
2626# ── Helpers ──────────────────────────────────────────────────────────────
@@ -58,49 +58,49 @@ def expected_hash(content: bytes, length: int = 8) -> str:
5858 return hashlib .sha256 (content ).hexdigest ()[:length ]
5959
6060
61- # ── StaticFiles : hashing and url() ──────────────────────────────────────
61+ # ── HashedStatic : hashing and url() ──────────────────────────────────────
6262
6363
6464
65- def test_file_map_contains_all_files (static : StaticFiles , static_dir : Path ) -> None :
65+ def test_file_map_contains_all_files (static : HashedStatic , static_dir : Path ) -> None :
6666 assert "styles.css" in static .file_map
6767 assert "images/logo.png" in static .file_map
6868
6969
70- def test_hash_is_correct (static : StaticFiles , static_dir : Path ) -> None :
70+ def test_hash_is_correct (static : HashedStatic , static_dir : Path ) -> None :
7171 css_content = (static_dir / "styles.css" ).read_bytes ()
7272 h = expected_hash (css_content )
7373 assert static .file_map ["styles.css" ] == f"styles.{ h } .css"
7474
7575
76- def test_hash_in_subdirectory (static : StaticFiles , static_dir : Path ) -> None :
76+ def test_hash_in_subdirectory (static : HashedStatic , static_dir : Path ) -> None :
7777 png_content = (static_dir / "images" / "logo.png" ).read_bytes ()
7878 h = expected_hash (png_content )
7979 assert static .file_map ["images/logo.png" ] == f"images/logo.{ h } .png"
8080
8181
82- def test_url_returns_hashed_path (static : StaticFiles ) -> None :
82+ def test_url_returns_hashed_path (static : HashedStatic ) -> None :
8383 url = static .url ("styles.css" )
8484 assert url .startswith ("/static/styles." )
8585 assert url .endswith (".css" )
8686 assert url != "/static/styles.css"
8787
8888
89- def test_url_unknown_file_returns_unchanged (static : StaticFiles ) -> None :
89+ def test_url_unknown_file_returns_unchanged (static : HashedStatic ) -> None :
9090 assert static .url ("nonexistent.js" ) == "/static/nonexistent.js"
9191
9292
93- def test_url_strips_leading_slash (static : StaticFiles ) -> None :
93+ def test_url_strips_leading_slash (static : HashedStatic ) -> None :
9494 assert static .url ("/styles.css" ) == static .url ("styles.css" )
9595
9696
9797def test_custom_prefix (static_dir : Path ) -> None :
98- s = StaticFiles (static_dir , prefix = "/assets" )
98+ s = HashedStatic (static_dir , prefix = "/assets" )
9999 assert s .url ("styles.css" ).startswith ("/assets/" )
100100
101101
102102def test_custom_hash_length (static_dir : Path ) -> None :
103- s = StaticFiles (static_dir , hash_length = 4 )
103+ s = HashedStatic (static_dir , hash_length = 4 )
104104 url = s .url ("styles.css" )
105105 # /static/styles.XXXX.css — 4-char hash
106106 stem = url .split ("/" )[- 1 ] # styles.XXXX.css
@@ -109,7 +109,7 @@ def test_custom_hash_length(static_dir: Path) -> None:
109109
110110
111111def test_nonexistent_directory (tmp_path : Path ) -> None :
112- s = StaticFiles (tmp_path / "nope" )
112+ s = HashedStatic (tmp_path / "nope" )
113113 assert s .file_map == {}
114114
115115
@@ -119,15 +119,15 @@ def test_symlinks_outside_directory_excluded(tmp_path: Path) -> None:
119119 outside = tmp_path / "secret.txt"
120120 outside .write_text ("secret data" )
121121 (static_dir / "link.txt" ).symlink_to (outside )
122- s = StaticFiles (static_dir )
122+ s = HashedStatic (static_dir )
123123 assert "link.txt" not in s .file_map
124124
125125
126126def test_extensionless_file (tmp_path : Path ) -> None :
127127 d = tmp_path / "static"
128128 d .mkdir ()
129129 (d / "Makefile" ).write_text ("all: build" )
130- s = StaticFiles (d )
130+ s = HashedStatic (d )
131131 h = expected_hash (b"all: build" )
132132 assert s .file_map ["Makefile" ] == f"Makefile.{ h } "
133133
@@ -136,7 +136,7 @@ def test_dotfile(tmp_path: Path) -> None:
136136 d = tmp_path / "static"
137137 d .mkdir ()
138138 (d / ".gitignore" ).write_text ("*.pyc" )
139- s = StaticFiles (d )
139+ s = HashedStatic (d )
140140 h = expected_hash (b"*.pyc" )
141141 assert s .file_map [".gitignore" ] == f".gitignore.{ h } "
142142
@@ -145,23 +145,23 @@ def test_multi_dot_filename(tmp_path: Path) -> None:
145145 d = tmp_path / "static"
146146 d .mkdir ()
147147 (d / "jquery.min.js" ).write_text ("js code" )
148- s = StaticFiles (d )
148+ s = HashedStatic (d )
149149 h = expected_hash (b"js code" )
150150 assert s .file_map ["jquery.min.js" ] == f"jquery.min.{ h } .js"
151151
152152
153- # ── StaticFiles : ASGI serving ───────────────────────────────────────────
153+ # ── HashedStatic : ASGI serving ───────────────────────────────────────────
154154
155155
156- async def test_serve_original_filename (static : StaticFiles , static_dir : Path ) -> None :
156+ async def test_serve_original_filename (static : HashedStatic , static_dir : Path ) -> None :
157157 resp = ResponseCollector ()
158158 await static (make_scope ("/static/styles.css" ), receive , resp )
159159 assert resp .status == 200
160160 assert resp .text == "body { color: red; }"
161161 assert b"cache-control" not in resp .headers
162162
163163
164- async def test_serve_hashed_filename_with_immutable_cache (static : StaticFiles ) -> None :
164+ async def test_serve_hashed_filename_with_immutable_cache (static : HashedStatic ) -> None :
165165 hashed_name = static .file_map ["styles.css" ]
166166 resp = ResponseCollector ()
167167 await static (make_scope (f"/static/{ hashed_name } " ), receive , resp )
@@ -170,39 +170,39 @@ async def test_serve_hashed_filename_with_immutable_cache(static: StaticFiles) -
170170 assert resp .headers [b"cache-control" ] == b"public, max-age=31536000, immutable"
171171
172172
173- async def test_serve_404_for_missing_file (static : StaticFiles ) -> None :
173+ async def test_serve_404_for_missing_file (static : HashedStatic ) -> None :
174174 resp = ResponseCollector ()
175175 await static (make_scope ("/static/nope.css" ), receive , resp )
176176 assert resp .status == 404
177177
178178
179- async def test_serve_404_outside_prefix (static : StaticFiles ) -> None :
179+ async def test_serve_404_outside_prefix (static : HashedStatic ) -> None :
180180 resp = ResponseCollector ()
181181 await static (make_scope ("/other/styles.css" ), receive , resp )
182182 assert resp .status == 404
183183
184184
185- async def test_path_traversal_rejected (static : StaticFiles ) -> None :
185+ async def test_path_traversal_rejected (static : HashedStatic ) -> None :
186186 resp = ResponseCollector ()
187187 await static (make_scope ("/static/../../etc/passwd" ), receive , resp )
188188 assert resp .status == 404
189189
190190
191- async def test_non_http_scope_ignored (static : StaticFiles ) -> None :
191+ async def test_non_http_scope_ignored (static : HashedStatic ) -> None :
192192 """WebSocket and lifespan scopes should be silently ignored."""
193193 resp = ResponseCollector ()
194194 await static ({"type" : "websocket" , "path" : "/static/styles.css" }, receive , resp )
195195 assert resp .status == 0 # send was never called
196196
197197
198- async def test_serve_subdirectory_file (static : StaticFiles ) -> None :
198+ async def test_serve_subdirectory_file (static : HashedStatic ) -> None :
199199 resp = ResponseCollector ()
200200 await static (make_scope ("/static/images/logo.png" ), receive , resp )
201201 assert resp .status == 200
202202 assert resp .body == b"\x89 PNG fake image data"
203203
204204
205- async def test_content_type_header (static : StaticFiles ) -> None :
205+ async def test_content_type_header (static : HashedStatic ) -> None :
206206 resp = ResponseCollector ()
207207 await static (make_scope ("/static/styles.css" ), receive , resp )
208208 assert b"text/css" in resp .headers [b"content-type" ]
@@ -246,7 +246,7 @@ async def app(scope: dict, receive: Any, send: Any) -> None:
246246 return app
247247
248248
249- async def test_rewrite_html_response (static : StaticFiles ) -> None :
249+ async def test_rewrite_html_response (static : HashedStatic ) -> None :
250250 html = '<link href="/static/styles.css">'
251251 app = StaticRewriteMiddleware (make_html_app (html ), static = static )
252252 resp = ResponseCollector ()
@@ -257,7 +257,7 @@ async def test_rewrite_html_response(static: StaticFiles) -> None:
257257 assert "/static/styles.css" not in resp .text
258258
259259
260- async def test_rewrite_updates_content_length (static : StaticFiles ) -> None :
260+ async def test_rewrite_updates_content_length (static : HashedStatic ) -> None :
261261 html = '<link href="/static/styles.css">'
262262 app = StaticRewriteMiddleware (make_html_app (html ), static = static )
263263 resp = ResponseCollector ()
@@ -267,23 +267,23 @@ async def test_rewrite_updates_content_length(static: StaticFiles) -> None:
267267 assert declared_length == len (resp .body )
268268
269269
270- async def test_rewrite_leaves_unknown_paths_alone (static : StaticFiles ) -> None :
270+ async def test_rewrite_leaves_unknown_paths_alone (static : HashedStatic ) -> None :
271271 html = '<script src="/static/app.js"></script>'
272272 app = StaticRewriteMiddleware (make_html_app (html ), static = static )
273273 resp = ResponseCollector ()
274274 await app (make_scope ("/" ), receive , resp )
275275 assert "/static/app.js" in resp .text
276276
277277
278- async def test_non_html_passes_through (static : StaticFiles ) -> None :
278+ async def test_non_html_passes_through (static : HashedStatic ) -> None :
279279 data = b'{"path": "/static/styles.css"}'
280280 app = StaticRewriteMiddleware (make_json_app (data ), static = static )
281281 resp = ResponseCollector ()
282282 await app (make_scope ("/api/data" ), receive , resp )
283283 assert resp .body == data
284284
285285
286- async def test_rewrite_multiple_paths (static : StaticFiles ) -> None :
286+ async def test_rewrite_multiple_paths (static : HashedStatic ) -> None :
287287 html = '<link href="/static/styles.css"><img src="/static/images/logo.png">'
288288 app = StaticRewriteMiddleware (make_html_app (html ), static = static )
289289 resp = ResponseCollector ()
@@ -293,7 +293,7 @@ async def test_rewrite_multiple_paths(static: StaticFiles) -> None:
293293 assert f"/static/{ static .file_map ['images/logo.png' ]} " in resp .text
294294
295295
296- async def test_rewrite_non_http_passes_through (static : StaticFiles ) -> None :
296+ async def test_rewrite_non_http_passes_through (static : HashedStatic ) -> None :
297297 """Non-HTTP scopes are forwarded to the wrapped app without rewriting."""
298298 calls : list [str ] = []
299299
@@ -306,7 +306,7 @@ async def ws_app(scope: dict, receive: Any, send: Any) -> None:
306306
307307
308308async def test_rewrite_raises_runtime_error_on_body_before_start (
309- static : StaticFiles ,
309+ static : HashedStatic ,
310310) -> None :
311311 """Middleware should raise RuntimeError if app sends body before start.
312312
@@ -328,7 +328,7 @@ async def broken_app(scope: dict, receive: Any, send: Any) -> None:
328328 await app (make_scope ("/" ), receive , ResponseCollector ())
329329
330330
331- async def test_rewrite_streaming_html_response (static : StaticFiles ) -> None :
331+ async def test_rewrite_streaming_html_response (static : HashedStatic ) -> None :
332332 """Middleware rewrites static paths even when the body arrives in multiple chunks."""
333333 chunk1 = b'<link href="/static/'
334334 chunk2 = b'styles.css">'
@@ -355,7 +355,7 @@ async def streaming_app(scope: dict, receive: Any, send: Any) -> None:
355355 assert "/static/styles.css" not in resp .text
356356
357357
358- async def test_serve_prefix_only_returns_404 (static : StaticFiles ) -> None :
358+ async def test_serve_prefix_only_returns_404 (static : HashedStatic ) -> None :
359359 """Requesting /static or /static/ with no filename returns 404."""
360360 # /static with no trailing slash
361361 resp_no_slash = ResponseCollector ()
@@ -368,7 +368,7 @@ async def test_serve_prefix_only_returns_404(static: StaticFiles) -> None:
368368 assert resp_slash .status == 404
369369
370370
371- async def test_rewrite_non_utf8_html_passes_through (static : StaticFiles ) -> None :
371+ async def test_rewrite_non_utf8_html_passes_through (static : HashedStatic ) -> None :
372372 """HTML response with non-UTF-8 bytes passes through unchanged."""
373373 raw_body = b"<html>\x80 \x81 \x82 not valid utf-8</html>"
374374
0 commit comments