grantforge-api / backend /tests /test_upload_rag_e2e.py
GrantForge Bot
Deploy to Hugging Face
afd56bc
"""
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'"