""" 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'"