236 lines
7.6 KiB
Python
236 lines
7.6 KiB
Python
"""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
|