File size: 7,881 Bytes
afd56bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
"""
Test E2E: Upload PDF → Indeksacja RAG → Limity planu

Sprint 9 — test weryfikujący:
  1. Upload PDF zwraca 202 + doc_id
  2. GET /documents pokazuje dokument w statusie >= uploaded
  3. GET /documents zwraca pole quota z limitami
  4. Gdy projekt osiągnął limit — POST zwraca 429
  5. DELETE usuwa dokument
"""

import pytest
from unittest.mock import MagicMock
from fastapi.testclient import TestClient

# ── Stałe konfiguracyjne ──────────────────────────────────────────────────────
MINIMAL_PDF = (
    b"%PDF-1.4\n"
    b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"
    b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"
    b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"
    b"xref\n0 4\n"
    b"0000000000 65535 f \n"
    b"0000000009 00000 n \n"
    b"0000000058 00000 n \n"
    b"0000000115 00000 n \n"
    b"trailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n190\n%%EOF"
)


@pytest.fixture
def client():
    """FastAPI TestClient z wyłączonym pipelinem RAG (mock)."""
    import sys
    import os

    # Dodaje backend do PYTHONPATH
    backend_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    if backend_dir not in sys.path:
        sys.path.insert(0, backend_dir)

    os.environ.setdefault("DATABASE_URL", "sqlite:///./test_upload.db")
    os.environ.setdefault("GOOGLE_API_KEY", "test-key-not-real")
    os.environ.setdefault("BIELIK_MODE", "disabled")

    try:
        from server import app

        return TestClient(app)
    except Exception as e:
        pytest.skip(f"Nie można zaimportować serwera: {e}")


@pytest.fixture
def mock_project_id(client, tmp_path):
    """Tworzy testowy projekt i zwraca jego ID (lub skip jeśli DB niedostępna)."""
    try:
        resp = client.post(
            "/api/projects",
            json={
                "title": "Test Upload E2E",
                "program_name": "SMART 2.0",
                "program_type": "SMART",
                "project_description": "Testowy projekt do weryfikacji upload flow",
            },
            headers={"Authorization": "Bearer dev_test_token"},
        )
        if resp.status_code not in (200, 201):
            pytest.skip(f"Nie można stworzyć projektu: {resp.status_code} {resp.text}")
        return resp.json().get("id") or resp.json().get("project_id")
    except Exception as e:
        pytest.skip(f"DB niedostępna: {e}")


# ── Testy ─────────────────────────────────────────────────────────────────────


class TestUploadEndpoint:
    """Testy endpointu upload bez pipelines RAG (unit-level)."""

    def test_upload_rejects_non_pdf(self, client):
        """Pliki inne niż PDF są odrzucane z HTTP 400."""
        resp = client.post(
            "/api/projects/test-proj-123/documents",
            files={
                "file": (
                    "document.docx",
                    b"fake content",
                    "application/vnd.openxmlformats",
                )
            },
        )
        assert (
            resp.status_code in (400, 404)
        ), f"Oczekiwano 400 (zły typ pliku) lub 404 (brak projektu). Otrzymano: {resp.status_code}"

    def test_upload_rejects_oversized_file(self, client):
        """Pliki powyżej 20MB są odrzucane z HTTP 413."""
        big_pdf = MINIMAL_PDF + b"X" * (21 * 1024 * 1024)
        resp = client.post(
            "/api/projects/test-proj-123/documents",
            files={"file": ("big.pdf", big_pdf, "application/pdf")},
        )
        # 413 jeśli projekt istnieje, 404 jeśli nie — oba akceptujemy
        assert resp.status_code in (
            413,
            404,
        ), f"Oczekiwano 413 lub 404. Otrzymano: {resp.status_code}"

    def test_list_documents_returns_quota_field(self, client):
        """GET /documents zwraca pole quota z informacjami o limicie."""
        resp = client.get("/api/projects/some-random-project-id/documents")
        # 200 lub 404 — ważne że gdy 200, to ma pole quota
        if resp.status_code == 200:
            data = resp.json()
            assert "quota" in data, "Brak pola 'quota' w odpowiedzi GET /documents"
            quota = data["quota"]
            assert "current" in quota
            assert "limit" in quota
            assert "can_upload" in quota
            assert "plan" in quota


class TestUploadLimits:
    """Testy limitów planowych."""

    def test_upload_limit_constants(self):
        """Limity są skonfigurowane poprawnie."""
        import sys
        import os

        backend_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        if backend_dir not in sys.path:
            sys.path.insert(0, backend_dir)

        from endpoints.documents import (
            UPLOAD_LIMIT_HARD,
            UPLOAD_LIMIT_FREE,
            UPLOAD_LIMIT_PRO,
        )

        assert UPLOAD_LIMIT_HARD == 10, "Hard limit powinien wynosić 10"
        assert UPLOAD_LIMIT_FREE == 3, "Limit Free powinien wynosić 3"
        assert UPLOAD_LIMIT_PRO == 50, "Limit Pro powinien wynosić 50"
        assert (
            UPLOAD_LIMIT_FREE < UPLOAD_LIMIT_PRO < UPLOAD_LIMIT_HARD
            or UPLOAD_LIMIT_PRO >= UPLOAD_LIMIT_HARD
        )

    def test_check_upload_limits_structure(self):
        """_check_upload_limits zwraca poprawną strukturę."""
        import sys
        import os

        backend_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        if backend_dir not in sys.path:
            sys.path.insert(0, backend_dir)

        from endpoints.documents import _check_upload_limits

        # Mock DB z 0 dokumentami
        mock_db = MagicMock()
        mock_query = MagicMock()
        mock_db.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.count.return_value = 0
        mock_query.first.return_value = None  # brak projektu → fallback free

        result = _check_upload_limits(mock_db, "test-proj")

        assert isinstance(result, dict)
        assert "allowed" in result
        assert "current" in result
        assert "limit" in result
        assert "plan" in result
        assert "reason" in result
        assert result["allowed"] is True  # 0 plików — powinno być dozwolone

    def test_check_upload_limits_blocks_when_at_free_limit(self):
        """_check_upload_limits blokuje upload gdy osiągnięto limit Free."""
        import sys
        import os

        backend_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        if backend_dir not in sys.path:
            sys.path.insert(0, backend_dir)

        from endpoints.documents import _check_upload_limits, UPLOAD_LIMIT_FREE

        mock_db = MagicMock()
        mock_query = MagicMock()
        mock_db.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.count.return_value = UPLOAD_LIMIT_FREE  # dokładnie na limicie
        mock_query.first.return_value = None  # brak projektu → fallback free

        result = _check_upload_limits(mock_db, "test-proj")

        assert result["allowed"] is False
        assert result["current"] == UPLOAD_LIMIT_FREE
        assert "reason" in result and len(result["reason"]) > 0


class TestPDFContent:
    """Sprawdza że testowy plik PDF jest poprawny."""

    def test_minimal_pdf_starts_with_magic(self):
        assert MINIMAL_PDF[:4] == b"%PDF", "Testowy PDF musi zaczynać się od '%PDF'"

    def test_minimal_pdf_ends_with_eof(self):
        assert MINIMAL_PDF.strip().endswith(
            b"%%EOF"
        ), "Testowy PDF musi kończyć się '%%EOF'"