"""Tests for API endpoints.""" import pytest from io import BytesIO class TestHealthEndpoint: """Test health check endpoint.""" def test_health_check(self, client): """Test health endpoint returns correct status.""" response = client.get("/health") assert response.status_code == 200 data = response.json() assert data["status"] == "ok" assert data["service"] == "mve-incrementables-parser" assert data["version"] == "1.0.0" class TestAuthEndpoint: """Test authentication endpoints.""" def test_login_success(self, client): """Test successful login.""" response = client.post( "/auth/login", json={"username": "testuser", "password": "test_password_123"} ) assert response.status_code == 200 data = response.json() assert "access_token" in data assert data["token_type"] == "bearer" assert data["expires_in"] == 3600 def test_login_invalid_username(self, client): """Test login with invalid username.""" response = client.post( "/auth/login", json={"username": "wronguser", "password": "test_password_123"} ) assert response.status_code == 401 data = response.json() assert data["detail"] == "Invalid credentials" def test_login_invalid_password(self, client): """Test login with invalid password.""" response = client.post( "/auth/login", json={"username": "testuser", "password": "wrongpassword"} ) assert response.status_code == 401 data = response.json() assert data["detail"] == "Invalid credentials" def test_login_missing_fields(self, client): """Test login with missing fields.""" response = client.post( "/auth/login", json={"username": "testuser"} ) assert response.status_code == 422 # Validation error class TestParseEndpoint: """Test incrementables parse endpoint.""" def create_mock_pdf(self, content: str) -> BytesIO: """Create a simple mock PDF file for testing.""" # This is a minimal PDF structure pdf_content = f"""%PDF-1.4 1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj 2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >> endobj 4 0 obj << /Length {len(content)} >> stream {content} endstream endobj xref 0 5 0000000000 65535 f 0000000009 00000 n 0000000058 00000 n 0000000115 00000 n 0000000214 00000 n trailer << /Size 5 /Root 1 0 R >> startxref {300 + len(content)} %%EOF""" return BytesIO(pdf_content.encode()) def test_parse_requires_auth(self, client): """Test parse endpoint requires authentication.""" files = {"file": ("test.pdf", BytesIO(b"fake pdf"), "application/pdf")} response = client.post("/v1/incrementables/parse", files=files) assert response.status_code == 403 # Forbidden without auth def test_parse_invalid_file_type(self, client, auth_headers): """Test parse endpoint rejects non-PDF files.""" files = {"file": ("test.txt", BytesIO(b"not a pdf"), "text/plain")} response = client.post( "/v1/incrementables/parse", files=files, headers=auth_headers ) assert response.status_code == 400 assert "PDF" in response.json()["detail"] def test_parse_invalid_file_extension(self, client, auth_headers): """Test parse endpoint rejects files without .pdf extension.""" files = {"file": ("test.txt", BytesIO(b"content"), "application/pdf")} response = client.post( "/v1/incrementables/parse", files=files, headers=auth_headers ) assert response.status_code == 400 assert "PDF" in response.json()["detail"] def test_parse_with_correlation_id(self, client, auth_headers): """Test parse endpoint accepts correlation ID.""" pdf_content = self.create_mock_pdf("test content") files = {"file": ("test.pdf", pdf_content, "application/pdf")} headers = {**auth_headers, "X-Correlation-Id": "test-correlation-123"} response = client.post( "/v1/incrementables/parse", files=files, headers=headers ) # Will fail at extraction/parsing stage, but should accept the correlation ID # Either 422 (parsing failed) or 500 (extraction failed) assert response.status_code in [422, 500] def test_parse_with_document_ref(self, client, auth_headers): """Test parse endpoint accepts document reference.""" pdf_content = self.create_mock_pdf("test content") files = {"file": ("test.pdf", pdf_content, "application/pdf")} data = {"document_ref": "REF-12345"} response = client.post( "/v1/incrementables/parse", files=files, data=data, headers=auth_headers ) # Will fail at extraction/parsing stage assert response.status_code in [422, 500] def test_parse_generates_correlation_id(self, client, auth_headers): """Test parse endpoint generates correlation ID if not provided.""" pdf_content = self.create_mock_pdf("test content") files = {"file": ("test.pdf", pdf_content, "application/pdf")} response = client.post( "/v1/incrementables/parse", files=files, headers=auth_headers ) # Even on error, should have correlation_id in some responses # This test verifies the endpoint is reachable assert response.status_code in [422, 500] class TestAuthIntegration: """Test authentication integration.""" def test_expired_token_rejected(self, client, test_settings): """Test that expired tokens are rejected.""" from app.core.security import create_access_token from datetime import timedelta # Create an already-expired token expired_token = create_access_token( data={"sub": "testuser"}, expires_delta=timedelta(seconds=-1) ) headers = {"Authorization": f"Bearer {expired_token}"} pdf_content = BytesIO(b"fake pdf") files = {"file": ("test.pdf", pdf_content, "application/pdf")} response = client.post( "/v1/incrementables/parse", files=files, headers=headers ) assert response.status_code == 401 def test_invalid_token_format(self, client): """Test that invalid token format is rejected.""" headers = {"Authorization": "Bearer invalid_token_xyz"} pdf_content = BytesIO(b"fake pdf") files = {"file": ("test.pdf", pdf_content, "application/pdf")} response = client.post( "/v1/incrementables/parse", files=files, headers=headers ) assert response.status_code == 401 def test_missing_bearer_prefix(self, client, auth_token): """Test that token without Bearer prefix is rejected.""" headers = {"Authorization": auth_token} pdf_content = BytesIO(b"fake pdf") files = {"file": ("test.pdf", pdf_content, "application/pdf")} response = client.post( "/v1/incrementables/parse", files=files, headers=headers ) assert response.status_code == 403