Spaces:
Running
Running
| """ | |
| 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" | |
| ) | |
| 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}") | |
| 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'" | |