first commit - MVE Incrementables Parser microservice with FastAPI, JWT, Celery, Redis
This commit is contained in:
235
tests/test_api.py
Normal file
235
tests/test_api.py
Normal file
@@ -0,0 +1,235 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user