44import os
55import tempfile
66
7- import pytest
87import pytest_asyncio
98from httpx import ASGITransport , AsyncClient
109
@@ -40,7 +39,6 @@ def _fake_paste(**kwargs):
4039 }
4140
4241
43- @pytest .mark .anyio
4442async def test_healthz (client ):
4543 from app import __version__
4644
@@ -53,7 +51,6 @@ async def test_healthz(client):
5351 assert body ["version" ] == __version__
5452
5553
56- @pytest .mark .anyio
5754async def test_healthz_stays_200_when_storage_is_down (client , monkeypatch ):
5855 """Liveness MUST NOT depend on the storage backend — a Redis blip should
5956 not cascade into a container restart loop."""
@@ -66,14 +63,12 @@ async def boom():
6663 assert r .status_code == 200
6764
6865
69- @pytest .mark .anyio
7066async def test_readyz_200_when_storage_up (client ):
7167 r = await client .get ("/readyz" )
7268 assert r .status_code == 200
7369 assert r .json ()["status" ] == "ok"
7470
7571
76- @pytest .mark .anyio
7772async def test_readyz_503_when_storage_down (client , monkeypatch ):
7873 """Readiness must flip to 503 so the ingress drains traffic until the
7974 backend recovers."""
@@ -87,7 +82,6 @@ async def boom():
8782 assert r .json ()["status" ] == "error"
8883
8984
90- @pytest .mark .anyio
9185async def test_csp_uses_nonce_not_unsafe_inline (client ):
9286 """script-src must be nonce-based, not 'unsafe-inline'. A weakening
9387 of this header would silently re-enable XSS via any injection point."""
@@ -100,7 +94,6 @@ async def test_csp_uses_nonce_not_unsafe_inline(client):
10094 assert "'unsafe-inline'" not in script_src
10195
10296
103- @pytest .mark .anyio
10497async def test_csp_nonce_is_per_request (client ):
10598 """Nonces must be unique per response — reuse would let an attacker
10699 who saw one nonce reuse it on later injections."""
@@ -116,7 +109,6 @@ async def test_csp_nonce_is_per_request(client):
116109 assert f'nonce="{ n1 } "' in r1 .text
117110
118111
119- @pytest .mark .anyio
120112async def test_homepage_og_image_is_absolute_banner (client ):
121113 """Link unfurls need an absolute og:image. A relative URL (or the portrait
122114 logo) is what made iMessage render a huge blown-up icon."""
@@ -125,7 +117,6 @@ async def test_homepage_og_image_is_absolute_banner(client):
125117 assert 'name="twitter:card" content="summary_large_image"' in html
126118
127119
128- @pytest .mark .anyio
129120async def test_paste_page_omits_og_image (client ):
130121 """Paste pages carry no og:image on purpose, so unfurls fall back to the
131122 compact card instead of stretching an image into the hero slot."""
@@ -135,7 +126,6 @@ async def test_paste_page_omits_og_image(client):
135126 assert 'name="twitter:card" content="summary"' in html
136127
137128
138- @pytest .mark .anyio
139129async def test_paste_page_emits_sri_for_third_party_libs (client ):
140130 """Third-party scripts on the paste page carry SRI hashes so a tampered
141131 file in /static/ (or a compromised CDN if we ever front it) fails to load
@@ -146,7 +136,6 @@ async def test_paste_page_emits_sri_for_third_party_libs(client):
146136 assert 'crossorigin="anonymous"' in html
147137
148138
149- @pytest .mark .anyio
150139async def test_compressed_flag_round_trips (client ):
151140 """compressed is a server-transparent metadata flag — it must persist
152141 through save/load exactly as the client sent it, with no coercion."""
@@ -157,7 +146,6 @@ async def test_compressed_flag_round_trips(client):
157146 assert g .json ()["compressed" ] is True
158147
159148
160- @pytest .mark .anyio
161149async def test_kdf_field_round_trips (client ):
162150 """The kdf hint must persist exactly as sent so the viewer can run the
163151 matching KDF — derive PBKDF2 from a password meant for Argon2id and
@@ -173,22 +161,19 @@ async def test_kdf_field_round_trips(client):
173161 assert g ["has_password" ] is True
174162
175163
176- @pytest .mark .anyio
177164async def test_kdf_defaults_to_pbkdf2 (client ):
178165 r = await client .post ("/api/v1/pastes" , json = _fake_paste ())
179166 g = (await client .get (f"/api/v1/pastes/{ r .json ()['id' ]} " )).json ()
180167 assert g ["kdf" ] == "pbkdf2-sha256"
181168
182169
183- @pytest .mark .anyio
184170async def test_kdf_rejects_unknown_value (client ):
185171 """An unknown KDF must be rejected at the validator — silently accepting
186172 it would let a malformed paste reach the storage layer."""
187173 r = await client .post ("/api/v1/pastes" , json = _fake_paste (kdf = "scrypt" ))
188174 assert r .status_code == 422
189175
190176
191- @pytest .mark .anyio
192177async def test_compressed_flag_defaults_to_false (client ):
193178 """Pastes created without the flag must come back with compressed=False —
194179 backwards compatibility for clients and existing pastes."""
@@ -198,7 +183,6 @@ async def test_compressed_flag_defaults_to_false(client):
198183 assert g .json ()["compressed" ] is False
199184
200185
201- @pytest .mark .anyio
202186async def test_homepage_ships_argon2_lib_with_sri (client ):
203187 """The homepage must serve the Argon2id WASM bundle so the KDF picker
204188 can switch to argon2id without a second round-trip. SRI is mandatory
@@ -213,7 +197,6 @@ async def test_homepage_ships_argon2_lib_with_sri(client):
213197 assert m , "Argon2 WASM lib should be loaded on the homepage with SRI"
214198
215199
216- @pytest .mark .anyio
217200async def test_password_paste_includes_argon2_lib (client ):
218201 """A password-protected paste page must ship the Argon2 lib unconditionally
219202 — the viewer doesn't know yet which KDF the paste used."""
@@ -223,7 +206,6 @@ async def test_password_paste_includes_argon2_lib(client):
223206 assert "hash-wasm-argon2" in html
224207
225208
226- @pytest .mark .anyio
227209async def test_non_password_paste_omits_argon2_lib (client ):
228210 """Non-password pastes never run a KDF in the browser, so the 29 KB lib
229211 must not be shipped to them."""
@@ -232,7 +214,6 @@ async def test_non_password_paste_omits_argon2_lib(client):
232214 assert "hash-wasm-argon2" not in html
233215
234216
235- @pytest .mark .anyio
236217async def test_csp_allows_wasm_unsafe_eval (client ):
237218 """script-src must include 'wasm-unsafe-eval' so hash-wasm can instantiate
238219 its Argon2id WebAssembly module. Without it the lib silently fails to load
@@ -243,7 +224,6 @@ async def test_csp_allows_wasm_unsafe_eval(client):
243224 assert "'unsafe-eval'" not in csp .replace ("'wasm-unsafe-eval'" , "" )
244225
245226
246- @pytest .mark .anyio
247227async def test_paste_page_ships_qr_button_modal_and_lib (client ):
248228 """QR code is rendered client-side so the URL fragment (encryption key)
249229 never reaches the server. The button + modal + lib must be in the shell;
@@ -278,7 +258,6 @@ class _Req:
278258 )
279259
280260
281- @pytest .mark .anyio
282261async def test_create_and_get_paste (client ):
283262 r = await client .post ("/api/v1/pastes" , json = _fake_paste ())
284263 assert r .status_code == 201
@@ -291,7 +270,6 @@ async def test_create_and_get_paste(client):
291270 assert r2 .json ()["id" ] == data ["id" ]
292271
293272
294- @pytest .mark .anyio
295273async def test_burn_after_read (client ):
296274 r = await client .post ("/api/v1/pastes" , json = _fake_paste (burn = True ))
297275 paste_id = r .json ()["id" ]
@@ -300,7 +278,6 @@ async def test_burn_after_read(client):
300278 assert (await client .get (f"/api/v1/pastes/{ paste_id } " )).status_code == 404
301279
302280
303- @pytest .mark .anyio
304281async def test_max_views (client ):
305282 r = await client .post ("/api/v1/pastes" , json = _fake_paste (max_views = 2 ))
306283 paste_id = r .json ()["id" ]
@@ -310,7 +287,6 @@ async def test_max_views(client):
310287 assert (await client .get (f"/api/v1/pastes/{ paste_id } " )).status_code == 404
311288
312289
313- @pytest .mark .anyio
314290async def test_delete_with_valid_token (client ):
315291 r = await client .post ("/api/v1/pastes" , json = _fake_paste ())
316292 data = r .json ()
@@ -322,7 +298,6 @@ async def test_delete_with_valid_token(client):
322298 assert (await client .get (f"/api/v1/pastes/{ data ['id' ]} " )).status_code == 404
323299
324300
325- @pytest .mark .anyio
326301async def test_delete_with_invalid_token (client ):
327302 r = await client .post ("/api/v1/pastes" , json = _fake_paste ())
328303 r2 = await client .delete (
@@ -332,7 +307,6 @@ async def test_delete_with_invalid_token(client):
332307 assert r2 .status_code == 403
333308
334309
335- @pytest .mark .anyio
336310async def test_update_paste_replaces_ciphertext (client ):
337311 """The PUT endpoint lets the owner change the ciphertext while keeping
338312 the same id and metadata — used by the in-place edit flow."""
@@ -356,7 +330,6 @@ async def test_update_paste_replaces_ciphertext(client):
356330 assert j ["has_password" ] == original .json ()["has_password" ]
357331
358332
359- @pytest .mark .anyio
360333async def test_update_paste_invalid_token_returns_403 (client ):
361334 r = await client .post ("/api/v1/pastes" , json = _fake_paste ())
362335 pid = r .json ()["id" ]
@@ -369,7 +342,6 @@ async def test_update_paste_invalid_token_returns_403(client):
369342 assert u .status_code == 403
370343
371344
372- @pytest .mark .anyio
373345async def test_update_unknown_paste_returns_403 (client ):
374346 """Same enumeration-resistance policy as DELETE: missing-vs-wrong-token
375347 must be indistinguishable to a caller."""
@@ -382,7 +354,6 @@ async def test_update_unknown_paste_returns_403(client):
382354 assert u .status_code == 403
383355
384356
385- @pytest .mark .anyio
386357async def test_update_can_flip_compressed_flag (client ):
387358 """A re-encrypt path that turns compression on (or off) must be able to
388359 persist the new flag — otherwise the viewer would gunzip wrong bytes."""
@@ -401,7 +372,6 @@ async def test_update_can_flip_compressed_flag(client):
401372 assert (await client .get (f"/api/v1/pastes/{ data ['id' ]} " )).json ()["compressed" ] is True
402373
403374
404- @pytest .mark .anyio
405375async def test_delete_of_unknown_paste_also_returns_403 (client ):
406376 """The API must not distinguish 'paste exists with wrong token' from
407377 'paste does not exist' — otherwise an attacker can enumerate IDs by
@@ -413,7 +383,6 @@ async def test_delete_of_unknown_paste_also_returns_403(client):
413383 assert r .status_code == 403
414384
415385
416- @pytest .mark .anyio
417386async def test_form_delete_of_unknown_paste_also_returns_403 (client ):
418387 """Same policy for the HTML form endpoint — the form is the legitimate
419388 owner path, but we still avoid leaking existence to anyone who can POST."""
@@ -424,13 +393,11 @@ async def test_form_delete_of_unknown_paste_also_returns_403(client):
424393 assert r .status_code == 403
425394
426395
427- @pytest .mark .anyio
428396async def test_ssrf_webhook_blocked (client ):
429397 r = await client .post ("/api/v1/pastes" , json = _fake_paste (webhook_url = "http://192.168.1.1/hook" ))
430398 assert r .status_code == 400
431399
432400
433- @pytest .mark .anyio
434401async def test_content_too_large (client ):
435402 payload = {
436403 "content" : "A" * (1024 * 1024 ),
@@ -442,14 +409,12 @@ async def test_content_too_large(client):
442409 assert r .status_code in (400 , 413 )
443410
444411
445- @pytest .mark .anyio
446412async def test_security_txt (client ):
447413 r = await client .get ("/.well-known/security.txt" )
448414 assert r .status_code == 200
449415 assert "Contact:" in r .text
450416
451417
452- @pytest .mark .anyio
453418async def test_robots_txt_points_at_sitemap (client ):
454419 r = await client .get ("/robots.txt" )
455420 assert r .status_code == 200
@@ -458,7 +423,6 @@ async def test_robots_txt_points_at_sitemap(client):
458423 assert "/sitemap.xml" in r .text
459424
460425
461- @pytest .mark .anyio
462426async def test_sitemap_lists_only_the_landing_page (client ):
463427 r = await client .get ("/sitemap.xml" )
464428 assert r .status_code == 200
@@ -467,7 +431,6 @@ async def test_sitemap_lists_only_the_landing_page(client):
467431 assert r .text .count ("<loc>" ) == 1
468432
469433
470- @pytest .mark .anyio
471434async def test_paste_page_is_noindex (client ):
472435 created = await client .post ("/api/v1/pastes" , json = _fake_paste ())
473436 assert created .status_code == 201
@@ -478,7 +441,6 @@ async def test_paste_page_is_noindex(client):
478441 assert 'rel="canonical"' not in r .text
479442
480443
481- @pytest .mark .anyio
482444async def test_landing_page_has_description_and_canonical (client ):
483445 r = await client .get ("/" )
484446 assert r .status_code == 200
@@ -487,7 +449,6 @@ async def test_landing_page_has_description_and_canonical(client):
487449 assert "noindex" not in r .text
488450
489451
490- @pytest .mark .anyio
491452async def test_id_collision_retry (client , monkeypatch ):
492453 """When the random ID generator collides, create_paste should retry
493454 up to 8 times before giving up — it must never overwrite an existing paste."""
@@ -518,7 +479,6 @@ def fake_token_urlsafe(nbytes):
518479 assert calls ["n" ] == 2 # proves we actually retried past the collisions
519480
520481
521- @pytest .mark .anyio
522482async def test_security_headers_present (client ):
523483 r = await client .get ("/" )
524484 assert r .headers .get ("x-content-type-options" ) == "nosniff"
0 commit comments