import os import tempfile import pytest from pathlib import Path from fastapi.testclient import TestClient # Use temporary database for tests temp_db = tempfile.NamedTemporaryFile(suffix=".db", delete=False) os.environ["DB_PATH"] = temp_db.name os.environ["ADMIN_USERNAME"] = "testadmin" os.environ["ADMIN_PASSWORD"] = "testpass12345" from app.main import app from app import config from app.database import init_db, get_db from app.auth import hash_password, verify_password, create_user, authenticate_user, ensure_admin_user, change_user_password config.ADMIN_USERNAME = "testadmin" config.ADMIN_PASSWORD = "testpass12345" client = TestClient(app) def setup_module(): init_db() ensure_admin_user() def teardown_module(): try: Path(temp_db.name).unlink(missing_ok=True) except Exception: pass def test_password_hashing(): pwd = "superSecretPassword123" h, salt = hash_password(pwd) assert verify_password(pwd, h, salt) is True assert verify_password("wrongPassword", h, salt) is False def test_user_creation_and_auth(): uid = create_user("demo_user", "demo_pass_999") assert uid > 0 auth = authenticate_user("demo_user", "demo_pass_999") assert auth is not None assert auth["username"] == "demo_user" wrong = authenticate_user("demo_user", "wrong_pass") assert wrong is None def test_unauthenticated_access_blocked(): # Attempting to access protected API without session resp = client.get("/api/me") assert resp.status_code == 401 resp = client.get("/api/cards") assert resp.status_code == 401 resp = client.post("/api/cards", json={"term": "apple", "translation": "яблоко"}) assert resp.status_code == 401 resp = client.get("/api/collections") assert resp.status_code == 401 resp = client.post("/api/cards/batch", json={"card_ids": [1], "action": "delete"}) assert resp.status_code == 401 resp = client.get("/api/export/csv") assert resp.status_code == 401 resp = client.get("/api/srs/queue") assert resp.status_code == 401 resp = client.get("/api/srs/stats") assert resp.status_code == 401 resp = client.post("/api/srs/review", json={"card_id": 1, "rating": 3}) assert resp.status_code == 401 def test_login_and_card_crud_workflow(): # 1. Login with testadmin resp = client.post("/api/login", json={"username": "testadmin", "password": "testpass12345"}) assert resp.status_code == 200 data = resp.json() assert data["status"] == "ok" assert "user" in data # 2. Check /api/me resp = client.get("/api/me") assert resp.status_code == 200 assert resp.json()["user"]["username"] == "testadmin" # 3. Create a card card_payload = { "term": "reluctant", "translation": "неохотный, нежелающий", "context_sentence": "He was reluctant to leave the room.", "phonetic": "/rɪˈlʌktənt/", "part_of_speech": "Adjective", "definition_en": "Not wanting to take some action", "tags": "general, daily", "notes": "Remember: reluctant to do sth" } resp = client.post("/api/cards", json=card_payload) assert resp.status_code == 200 created = resp.json() card_id = created["id"] assert created["term"] == "reluctant" # 4. Get cards list resp = client.get("/api/cards") assert resp.status_code == 200 res_data = resp.json() assert res_data["total"] >= 1 assert any(c["id"] == card_id for c in res_data["cards"]) # 5. Filter cards by search resp = client.get("/api/cards?search=reluctant") assert resp.status_code == 200 assert resp.json()["total"] >= 1 # 6. Filter cards by tag resp = client.get("/api/cards?tag=general") assert resp.status_code == 200 assert resp.json()["total"] >= 1 # 7. Check tags list resp = client.get("/api/tags") assert resp.status_code == 200 tags = resp.json() tag_names = [t["tag"] for t in tags] assert "general" in tag_names assert "daily" in tag_names # 8. Update card update_payload = { "translation": "неохотный, сопротивляющийся", "notes": "Updated note" } resp = client.put(f"/api/cards/{card_id}", json=update_payload) assert resp.status_code == 200 updated = resp.json() assert updated["translation"] == "неохотный, сопротивляющийся" assert updated["notes"] == "Updated note" # 9. Check stats resp = client.get("/api/stats") assert resp.status_code == 200 stats = resp.json() assert stats["total_cards"] >= 1 assert stats["today_added"] >= 1 # 10. Delete card resp = client.delete(f"/api/cards/{card_id}") assert resp.status_code == 200 assert resp.json()["status"] == "ok" # Verify deleted resp = client.get("/api/cards") assert not any(c["id"] == card_id for c in resp.json()["cards"]) # 11. Logout resp = client.post("/api/logout") assert resp.status_code == 200 # Verify session ended resp = client.get("/api/me") assert resp.status_code == 401 def test_login_rate_limiting(): # Attempt multiple failed logins from the same client for _ in range(5): resp = client.post("/api/login", json={"username": "testadmin", "password": "wrong_password_999"}) assert resp.status_code == 401 # 6th attempt should be rate limited (429) resp = client.post("/api/login", json={"username": "testadmin", "password": "wrong_password_999"}) assert resp.status_code == 429 def test_lookup_endpoint_authenticated(): from app.auth import _login_failures _login_failures.clear() # Login first resp = client.post("/api/login", json={"username": "testadmin", "password": "testpass12345"}) assert resp.status_code == 200 resp = client.get("/api/lookup?q=reluctant") assert resp.status_code == 200 data = resp.json() assert data["query"] == "reluctant" assert "translation" in data assert "phonetic" in data assert len(data["translation"]) > 0 def test_collections_and_batch_operations(): # Login client.post("/api/login", json={"username": "testadmin", "password": "testpass12345"}) # Create 3 cards in different collections c1 = client.post("/api/cards", json={ "term": "ubiquitous", "translation": "вездесущий", "collection": "Academic", "tags": "vocab, tech" }).json() c2 = client.post("/api/cards", json={ "term": "perseverance", "translation": "настойчивость", "collection": "Academic", "tags": "character" }).json() c3 = client.post("/api/cards", json={ "term": "bite the bullet", "translation": "стиснуть зубы", "collection": "Idioms", "tags": "idioms, daily" }).json() # Test GET /api/collections resp = client.get("/api/collections") assert resp.status_code == 200 cols = resp.json() col_names = {c["name"]: c["count"] for c in cols} assert "Academic" in col_names and col_names["Academic"] >= 2 assert "Idioms" in col_names and col_names["Idioms"] >= 1 # Test filter by collection in /api/cards resp = client.get("/api/cards?collection=Academic") assert resp.status_code == 200 academic_cards = resp.json() assert all(c["collection"] == "Academic" for c in academic_cards["cards"]) assert any(c["id"] == c1["id"] for c in academic_cards["cards"]) assert any(c["id"] == c2["id"] for c in academic_cards["cards"]) assert not any(c["id"] == c3["id"] for c in academic_cards["cards"]) # Test Batch set_collection batch_resp = client.post("/api/cards/batch", json={ "card_ids": [c1["id"], c2["id"]], "action": "set_collection", "value": "Advanced English" }) assert batch_resp.status_code == 200 assert batch_resp.json()["affected"] == 2 # Verify collection updated card1_check = client.get("/api/cards?search=ubiquitous").json()["cards"][0] assert card1_check["collection"] == "Advanced English" # Test Batch add_tags batch_resp = client.post("/api/cards/batch", json={ "card_ids": [c1["id"], c2["id"]], "action": "add_tags", "value": "stage2, vocabulary" }) assert batch_resp.status_code == 200 card1_tags = client.get("/api/cards?search=ubiquitous").json()["cards"][0]["tags"] assert "stage2" in card1_tags assert "vocabulary" in card1_tags # Test Batch delete batch_resp = client.post("/api/cards/batch", json={ "card_ids": [c1["id"], c2["id"], c3["id"]], "action": "delete" }) assert batch_resp.status_code == 200 assert batch_resp.json()["affected"] == 3 # Verify deleted all_cards = client.get("/api/cards").json()["cards"] deleted_ids = {c1["id"], c2["id"], c3["id"]} assert not any(c["id"] in deleted_ids for c in all_cards) def test_export_endpoints(): client.post("/api/login", json={"username": "testadmin", "password": "testpass12345"}) # Create a test card with context sentence for cloze card_resp = client.post("/api/cards", json={ "term": "serendipity", "translation": "счастливая случайность", "context_sentence": "Finding this book was pure serendipity.", "phonetic": "ˌser.ənˈdɪp.ə.ti", "part_of_speech": "Noun", "definition_en": "Good luck in making unexpected discoveries", "collection": "Vocabulary", "tags": "literature, rare", "notes": "Beautiful English word" }) card = card_resp.json() # 1. CSV Export resp = client.get("/api/export/csv") assert resp.status_code == 200 assert "text/csv" in resp.headers["Content-Type"] assert resp.content.startswith(b"\xef\xbb\xbf") # UTF-8 BOM csv_text = resp.content.decode("utf-8-sig") assert "serendipity" in csv_text assert "счастливая случайность" in csv_text assert "term,translation,context,phonetic" in csv_text # 2. JSON Export resp = client.get("/api/export/json") assert resp.status_code == 200 assert "application/json" in resp.headers["Content-Type"] json_data = resp.json() assert "cards" in json_data assert any(c["term"] == "serendipity" for c in json_data["cards"]) # 3. Anki Export resp = client.get("/api/export/anki") assert resp.status_code == 200 anki_text = resp.text assert "#separator:tab" in anki_text assert "#html:true" in anki_text assert "{{c1::serendipity}}" in anki_text # Cloze verified assert "collection_Vocabulary" in anki_text # 4. Markdown Export resp = client.get("/api/export/markdown") assert resp.status_code == 200 md_text = resp.text assert "| **serendipity** |" in md_text assert "### serendipity" in md_text # Filter export by collection resp_filtered = client.get("/api/export/json?collection=NonExistentCollection") assert resp_filtered.status_code == 200 assert len(resp_filtered.json()["cards"]) == 0 # Clean up client.delete(f"/api/cards/{card['id']}") def test_sm2_algorithm_unit(): from app.srs import calculate_sm2, calculate_next_review from datetime import datetime, timezone base = datetime(2026, 9, 22, 12, 0, 0, tzinfo=timezone.utc) # Rating 1 (Again) res1 = calculate_sm2(1, reps=2, interval=6, ease=2.5, base_time=base) assert res1["srs_reps"] == 0 assert res1["srs_interval"] == 1 assert res1["srs_ease"] == 2.3 assert res1["srs_state"] == "learning" # Rating 2 (Hard) res2 = calculate_sm2(2, reps=1, interval=1, ease=2.5, base_time=base) assert res2["srs_reps"] == 2 assert res2["srs_interval"] == 1 assert res2["srs_ease"] == 2.35 assert res2["srs_state"] == "learning" res2_later = calculate_sm2(2, reps=2, interval=10, ease=2.5, base_time=base) assert res2_later["srs_reps"] == 3 assert res2_later["srs_interval"] == 12 # round(10 * 1.2) = 12 assert res2_later["srs_state"] == "review" # reps >= 3 # Rating 3 (Good) res3_first = calculate_sm2(3, reps=0, interval=0, ease=2.5, base_time=base) assert res3_first["srs_reps"] == 1 assert res3_first["srs_interval"] == 1 assert res3_first["srs_state"] == "review" res3_second = calculate_sm2(3, reps=1, interval=1, ease=2.5, base_time=base) assert res3_second["srs_reps"] == 2 assert res3_second["srs_interval"] == 6 assert res3_second["srs_state"] == "review" res3_third = calculate_sm2(3, reps=2, interval=6, ease=2.5, base_time=base) assert res3_third["srs_reps"] == 3 assert res3_third["srs_interval"] == 15 # round(6 * 2.5) = 15 res3_mastered = calculate_sm2(3, reps=3, interval=15, ease=2.5, base_time=base) assert res3_mastered["srs_reps"] == 4 assert res3_mastered["srs_interval"] == 38 # round(15 * 2.5) = 38 >= 21 assert res3_mastered["srs_state"] == "mastered" # Rating 4 (Easy) res4_first = calculate_sm2(4, reps=0, interval=0, ease=2.5, base_time=base) assert res4_first["srs_reps"] == 1 assert res4_first["srs_interval"] == 4 assert res4_first["srs_ease"] == 2.65 # Boundary checks: Ease minimum 1.3 res_min_ease = calculate_sm2(1, reps=0, interval=1, ease=1.35, base_time=base) assert res_min_ease["srs_ease"] == 1.3 # Boundary checks: Ease maximum 3.0 res_max_ease = calculate_sm2(4, reps=1, interval=4, ease=2.95, base_time=base) assert res_max_ease["srs_ease"] == 3.0 # Invalid rating with pytest.raises(ValueError): calculate_sm2(0) with pytest.raises(ValueError): calculate_sm2(5) def test_srs_endpoints_flow(): client.post("/api/login", json={"username": "testadmin", "password": "testpass12345"}) # Create a new card card_resp = client.post("/api/cards", json={ "term": "meticulous", "translation": "дотошный, педантичный", "collection": "General" }) card = card_resp.json() card_id = card["id"] assert card["srs_state"] == "new" # 1. Check /api/srs/queue resp = client.get("/api/srs/queue") assert resp.status_code == 200 queue_data = resp.json() assert any(c["id"] == card_id for c in queue_data["cards"]) # 2. Check /api/srs/stats before review resp = client.get("/api/srs/stats") assert resp.status_code == 200 stats_before = resp.json() assert "new_cards" in stats_before assert "learning" in stats_before assert "mastered" in stats_before assert "streak_days" in stats_before # 3. Submit Review: Rating 3 (Good) rev_resp = client.post("/api/srs/review", json={"card_id": card_id, "rating": 3}) assert rev_resp.status_code == 200 rev_data = rev_resp.json() assert rev_data["status"] == "ok" updated_card = rev_data["card"] assert updated_card["srs_reps"] == 1 assert updated_card["srs_interval"] == 1 assert updated_card["srs_state"] == "review" # 4. Check stats after review: total_reviews incremented, streak >= 1 stats_after = client.get("/api/srs/stats").json() assert stats_after["total_reviews"] >= stats_before["total_reviews"] + 1 assert stats_after["streak_days"] >= 1 # 5. Review invalid rating bad_rev = client.post("/api/srs/review", json={"card_id": card_id, "rating": 99}) assert bad_rev.status_code in (400, 422) # 6. Review non-existent card missing_rev = client.post("/api/srs/review", json={"card_id": 999999, "rating": 3}) assert missing_rev.status_code == 404 # Clean up client.delete(f"/api/cards/{card_id}") def test_pwa_routes_response(): resp_m = client.get("/manifest.json") assert resp_m.status_code in (200, 404) if resp_m.status_code == 200: assert resp_m.headers["Content-Type"].startswith("application/manifest+json") resp_sw = client.get("/sw.js") assert resp_sw.status_code in (200, 404) if resp_sw.status_code == 200: assert resp_sw.headers["Service-Worker-Allowed"] == "/" assert "no-cache" in resp_sw.headers["Cache-Control"] def test_change_password_flow(): # 1. Create a dedicated user for password change test test_uid = create_user("pwd_user", "old_secret_pass_123") assert test_uid > 0 # 2. Login login_resp = client.post("/api/login", json={"username": "pwd_user", "password": "old_secret_pass_123"}) assert login_resp.status_code == 200 # 3. Try to change with wrong old password fail_resp = client.post("/api/change-password", json={ "old_password": "wrong_old_password", "new_password": "brand_new_pass_456" }) assert fail_resp.status_code == 400 # 4. Try to change with short new password (<6 chars) short_resp = client.post("/api/change-password", json={ "old_password": "old_secret_pass_123", "new_password": "123" }) assert short_resp.status_code == 422 # 5. Successfully change password ok_resp = client.post("/api/change-password", json={ "old_password": "old_secret_pass_123", "new_password": "brand_new_pass_456" }) assert ok_resp.status_code == 200 assert ok_resp.json()["status"] == "ok" # 6. Verify old password no longer authenticates old_auth = authenticate_user("pwd_user", "old_secret_pass_123") assert old_auth is None # 7. Verify new password authenticates new_auth = authenticate_user("pwd_user", "brand_new_pass_456") assert new_auth is not None assert new_auth["username"] == "pwd_user" # --------------------------------------------------------------------------- # Stage 5: Bookshelf & Interactive Reader Tests # --------------------------------------------------------------------------- def make_mock_epub(title="The Time Machine", author="H. G. Wells", chapters=None): import io, zipfile if chapters is None: chapters = [ ("Chapter 1: The Machine", "

The Time Traveller was expounding a recondite matter to us.

"), ("Chapter 2: The Journey", "

I seemed to see the clock hands spin around.

") ] buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as z: z.writestr("META-INF/container.xml", """ """) manifest_items = [] spine_items = [] for i, (ch_title, ch_body) in enumerate(chapters, 1): manifest_items.append(f'') spine_items.append(f'') z.writestr(f"OEBPS/ch{i}.xhtml", f""" {ch_title}

{ch_title}

{ch_body} """) opf = f""" {title} {author} {''.join(manifest_items)} {''.join(spine_items)} """ z.writestr("OEBPS/content.opf", opf) return buf.getvalue() def test_reader_unit(): from app.reader import count_words, parse_text, parse_epub # 1. count_words assert count_words("") == 0 assert count_words(" \n\t ") == 0 assert count_words("Hello world!") == 2 assert count_words("Привет, мир! Это тест.") == 4 assert count_words("Don't stop believing") == 3 # 2. parse_text with Markdown headers md_content = """# My Book Author: Arthur Dent ## Chapter 1: Hitchhiker Far out in the uncharted backwaters. ## Chapter 2: The Towel A towel is about the most useful thing. """ t, a, chs = parse_text(md_content) assert t == "My Book" assert a == "Arthur Dent" assert len(chs) == 2 assert "Chapter 1: Hitchhiker" in chs[0]["title"] assert "Far out" in chs[0]["content"] # 3. parse_text with Chapter X headings txt_content = """Chapter 1. The Boy Who Lived Mr. and Mrs. Dursley of number four Privet Drive. Chapter 2 - The Vanishing Glass Nearly ten years had passed. """ t, a, chs = parse_text(txt_content, title="Harry Potter", author="J.K. Rowling") assert t == "Harry Potter" assert a == "J.K. Rowling" assert len(chs) == 2 assert "Chapter 1" in chs[0]["title"] assert "Chapter 2" in chs[1]["title"] # 4. parse_text chunking on long text long_words = ["word"] * 7000 long_text = "\n\n".join([" ".join(long_words[i:i + 400]) for i in range(0, len(long_words), 400)]) t, a, chs = parse_text(long_text, title="Long Essay") assert t == "Long Essay" assert len(chs) >= 2 # 5. parse_epub epub_bytes = make_mock_epub("Brave New World", "Aldous Huxley") t, a, chs = parse_epub(epub_bytes) assert t == "Brave New World" assert a == "Aldous Huxley" assert len(chs) == 2 assert "Chapter 1: The Machine" in chs[0]["title"] assert "Time Traveller" in chs[0]["content"] # 6. parse_epub invalid archive error with pytest.raises(ValueError): parse_epub(b"not a zip file") def test_books_unauthenticated_blocked(): unauth = TestClient(app) resp = unauth.get("/api/books") assert resp.status_code == 401 resp = unauth.post("/api/books", json={"title": "Test", "content": "Text"}) assert resp.status_code == 401 resp = unauth.get("/api/books/1") assert resp.status_code == 401 resp = unauth.put("/api/books/1/progress", json={"current_chapter": 1, "current_position": 0.5}) assert resp.status_code == 401 resp = unauth.delete("/api/books/1") assert resp.status_code == 401 def test_books_crud_workflow(): # Login as admin client.post("/api/login", json={"username": "testadmin", "password": "testpass12345"}) # 1. Create book via JSON payload = { "title": "Fight Club", "author": "Chuck Palahniuk", "content": "Chapter 1\nTyler gets me a job.\n\nChapter 2\nA guy had come to the club.", "source_type": "text" } resp = client.post("/api/books", json=payload) assert resp.status_code == 200 book = resp.json() assert book["id"] > 0 book_id = book["id"] assert book["title"] == "Fight Club" assert book["author"] == "Chuck Palahniuk" assert book["source_type"] == "text" assert book["chapters_count"] == 2 assert book["total_words"] > 0 assert book["current_chapter"] == 0 assert book["current_position"] == 0.0 # 2. List books (ensure chapters_json is excluded from list) resp = client.get("/api/books") assert resp.status_code == 200 books = resp.json() assert isinstance(books, list) target = next((b for b in books if b["id"] == book_id), None) assert target is not None assert target["title"] == "Fight Club" assert target["chapters_count"] == 2 assert "chapters_json" not in target # 3. Get single book (includes chapters_json and chapters) resp = client.get(f"/api/books/{book_id}") assert resp.status_code == 200 detail = resp.json() assert detail["id"] == book_id assert "chapters_json" in detail assert "chapters" in detail assert len(detail["chapters"]) == 2 assert "Chapter 1" in detail["chapters"][0]["title"] # 4. Update reading progress resp = client.put(f"/api/books/{book_id}/progress", json={ "current_chapter": 1, "current_position": 0.75 }) assert resp.status_code == 200 updated = resp.json() assert updated["current_chapter"] == 1 assert updated["current_position"] == 0.75 # 5. Delete book resp = client.delete(f"/api/books/{book_id}") assert resp.status_code == 200 assert resp.json()["status"] == "ok" assert resp.json()["deleted_id"] == book_id # 6. Verify deleted (404) resp = client.get(f"/api/books/{book_id}") assert resp.status_code == 404 def test_books_multipart_file_uploads(): # Login client.post("/api/login", json={"username": "testadmin", "password": "testpass12345"}) # 1. Upload TXT file txt_data = b"Chapter 1: The Start\nOnce upon a time in a distant land.\n\nChapter 2: The End\nThey lived happily ever after." resp = client.post( "/api/books", files={"file": ("fairytale.txt", txt_data, "text/plain")}, data={"title": "Fairy Tale", "author": "Brothers Grimm"} ) assert resp.status_code == 200 book_txt = resp.json() assert book_txt["title"] == "Fairy Tale" assert book_txt["author"] == "Brothers Grimm" assert book_txt["source_type"] == "txt" assert book_txt["chapters_count"] == 2 # 2. Upload Markdown file md_data = b"# The Cyberpunk Guide\nBy: Anonymous\n\n## Section 1\nWelcome to the matrix.\n\n## Section 2\nJack into cyberspace." resp = client.post( "/api/books", files={"file": ("cyberpunk.md", md_data, "text/markdown")}, ) assert resp.status_code == 200 book_md = resp.json() assert book_md["title"] == "The Cyberpunk Guide" assert book_md["author"] == "Anonymous" assert book_md["source_type"] == "md" assert book_md["chapters_count"] == 2 # 3. Upload EPUB file epub_bytes = make_mock_epub("The Invisible Man", "H. G. Wells") resp = client.post( "/api/books", files={"file": ("invisible_man.epub", epub_bytes, "application/epub+zip")}, ) assert resp.status_code == 200 book_epub = resp.json() assert book_epub["title"] == "The Invisible Man" assert book_epub["author"] == "H. G. Wells" assert book_epub["source_type"] == "epub" assert book_epub["chapters_count"] == 2 def test_srs_queue_and_quiz_tag_filtering(): # Login client.post("/api/login", json={"username": "testadmin", "password": "testpass12345"}) # Create distinct tagged cards for book-scoped testing c1 = client.post("/api/cards", json={ "term": "recondite", "translation": "малопонятный, глубокий", "tags": "book:time-machine, scifi", "collection": "Literature" }).json() c2 = client.post("/api/cards", json={ "term": "invisible", "translation": "невидимый", "tags": "book:invisible-man, scifi", "collection": "Literature" }).json() c3 = client.post("/api/cards", json={ "term": "soap", "translation": "мыло", "tags": "book:fight-club", "collection": "Fiction" }).json() # 1. Filter queue by tag resp = client.get("/api/srs/queue?tag=book:time-machine") assert resp.status_code == 200 queue_cards = resp.json()["cards"] card_ids = [c["id"] for c in queue_cards] assert c1["id"] in card_ids assert c2["id"] not in card_ids assert c3["id"] not in card_ids # 2. Filter quiz by tag resp = client.get("/api/srs/quiz?tag=book:fight-club") assert resp.status_code == 200 quiz_items = resp.json()["quiz"] assert any(q["card"]["id"] == c3["id"] for q in quiz_items) assert not any(q["card"]["id"] == c1["id"] for q in quiz_items) assert not any(q["card"]["id"] == c2["id"] for q in quiz_items) def test_gzip_compression(): # 1. Assets >= 1000 bytes receive GZip encoding when requested resp_js = client.get("/static/js/app.js", headers={"Accept-Encoding": "gzip"}) assert resp_js.status_code == 200 assert resp_js.headers.get("content-encoding") == "gzip" resp_css = client.get("/static/css/style.css", headers={"Accept-Encoding": "gzip"}) assert resp_css.status_code == 200 assert resp_css.headers.get("content-encoding") == "gzip" resp_html = client.get("/", headers={"Accept-Encoding": "gzip"}) assert resp_html.status_code == 200 assert resp_html.headers.get("content-encoding") == "gzip" # 2. Assets < 1000 bytes are not compressed (respecting minimum_size=1000) resp_manifest = client.get("/manifest.json", headers={"Accept-Encoding": "gzip"}) assert resp_manifest.status_code == 200 assert resp_manifest.headers.get("content-encoding") is None # 3. Requests without gzip Accept-Encoding do not receive gzip resp_raw = client.get("/static/js/app.js", headers={"Accept-Encoding": "identity"}) assert resp_raw.status_code == 200 assert resp_raw.headers.get("content-encoding") is None