File size: 4,491 Bytes
7c6ffa6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3bcdb36
 
 
 
 
 
 
 
 
 
 
 
 
7c6ffa6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
from __future__ import annotations


def test_document_upload_extracts_and_chunks_text_file(client) -> None:
    response = client.post(
        "/documents/upload",
        data={
            "title": "Upload smoke test",
            "subject": "Biology",
            "chapter": "Photosynthesis",
        },
        files={
            "file": (
                "photosynthesis.txt",
                b"Photosynthesis uses light to make glucose and oxygen.",
                "text/plain",
            ),
        },
    )

    assert response.status_code == 201
    payload = response.json()
    assert payload["status"] == "ready"
    assert payload["chunk_count"] >= 1
    # extracted_text was removed from API response (security hardening)
    assert "extracted_text" not in payload


def test_document_upload_returns_failed_document_when_indexing_fails(client, monkeypatch) -> None:
    from app.routes import documents

    def fail_indexing(*_args, **_kwargs) -> None:
        raise RuntimeError("embedding backend unavailable")

    monkeypatch.setattr(documents, "replace_document_chunks", fail_indexing)

    response = client.post(
        "/documents/upload",
        data={"title": "Indexing failure"},
        files={
            "file": (
                "notes.txt",
                b"This text extracts but indexing fails.",
                "text/plain",
            ),
        },
    )

    assert response.status_code == 201
    payload = response.json()
    assert payload["status"] == "failed"
    assert payload["chunk_count"] == 0
    # extraction_error is stored server-side but must NOT be in the API response
    assert "extraction_error" not in payload


def test_document_upload_strips_lone_surrogates_before_database_write(client, monkeypatch) -> None:
    from app.routes import documents

    monkeypatch.setattr(
        documents,
        "extract_text_from_file",
        lambda *_args, **_kwargs: "Clean biology text \ud835 with invalid surrogate.",
    )

    response = client.post(
        "/documents/upload",
        data={"title": "Unicode repair", "subject": "Biology"},
        files={
            "file": (
                "unicode.txt",
                b"placeholder",
                "text/plain",
            ),
        },
    )

    assert response.status_code == 201
    payload = response.json()
    assert payload["status"] == "ready"
    # extracted_text was removed from API response (security hardening);
    # verify it is NOT leaked to the client and the upload still succeeds
    assert "extracted_text" not in payload


def test_upload_rejects_unsupported_mime_type(client) -> None:
    response = client.post(
        "/documents/upload",
        data={"title": "Bad type"},
        files={"file": ("notes.exe", b"MZ\x90\x00", "application/x-msdownload")},
    )
    assert response.status_code == 415


def test_upload_rejects_images_before_creating_a_failed_document(client) -> None:
    response = client.post(
        "/documents/upload",
        data={"title": "Scanned notes"},
        files={"file": ("notes.png", b"\x89PNG\r\n\x1a\n", "image/png")},
    )

    assert response.status_code == 415
    detail = response.json()["detail"]
    assert "Image OCR" in detail
    assert client.get("/documents").json() == []


def test_upload_rejects_file_over_the_size_limit(client) -> None:
    # 20 MB + 1 byte of allowed-type content must be rejected as 413, not stored.
    oversized = b"a" * (20 * 1024 * 1024 + 1)
    response = client.post(
        "/documents/upload",
        data={"title": "Too big"},
        files={"file": ("huge.txt", oversized, "text/plain")},
    )
    assert response.status_code == 413


def test_upload_accepts_a_malayalam_filename_and_sanitises_it(client) -> None:
    response = client.post(
        "/documents/upload",
        data={"title": "Malayalam name"},
        files={
            "file": (
                "ശബ്ദം physics notes.txt",
                b"Sound travels as a longitudinal wave.",
                "text/plain",
            ),
        },
    )
    assert response.status_code == 201
    assert response.json()["status"] == "ready"


def test_upload_requires_authentication(auth_client) -> None:
    # No Authorization header -> the upload endpoint must refuse.
    response = auth_client.post(
        "/documents/upload",
        data={"title": "Anon"},
        files={"file": ("x.txt", b"hello", "text/plain")},
    )
    assert response.status_code in (401, 403)