test: run and fix V2 API tests - 11/18 passing

Changes:
- Fixed UserResponse schema datetime serialization bug
- Fixed test_auth.py mock structure for external auth service
- Updated conftest.py to create fresh database per test
- Ran full test suite and verified results

Test Results:
 test_auth.py: 5/5 passing (100%)
 test_tasks.py: 4/6 passing (67%)
 test_admin.py: 2/4 passing (50%)
 test_integration.py: 0/3 passing (0%)

Total: 11/18 tests passing (61%)

Known Issues:
1. Fixture isolation: test_user sometimes gets admin email
2. Admin API response structure doesn't match test expectations
3. Integration tests need mock fixes

Production Bug Fixed:
- UserResponse schema now properly serializes datetime fields to ISO format strings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
egg
2025-11-16 18:16:47 +08:00
parent 8f94191914
commit 90fca5002b
5 changed files with 540 additions and 171 deletions

View File

@@ -1,179 +1,116 @@
"""
Tool_OCR - Pytest Fixtures and Configuration
Shared fixtures for all tests
V2 API Test Configuration and Fixtures
Provides test fixtures for authentication, database, and API testing
"""
import pytest
import tempfile
import shutil
from pathlib import Path
from PIL import Image
import io
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from app.services.preprocessor import DocumentPreprocessor
from app.main import app
from app.core.database import Base, get_db
from app.core.security import create_access_token
from app.models.user import User
from app.models.task import Task
@pytest.fixture(scope="function")
def db():
"""Create test database and return session"""
# Create a fresh engine and session for each test
engine = create_engine(
"sqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base.metadata.create_all(bind=engine)
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
Base.metadata.drop_all(bind=engine)
engine.dispose()
@pytest.fixture(scope="function")
def client(db):
"""Create FastAPI test client with test database"""
def override_get_db():
try:
yield db
finally:
pass
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as test_client:
yield test_client
app.dependency_overrides.clear()
@pytest.fixture
def temp_dir():
"""Create a temporary directory for test files"""
temp_path = Path(tempfile.mkdtemp())
yield temp_path
# Cleanup after test
shutil.rmtree(temp_path, ignore_errors=True)
def test_user(db):
"""Create a test user"""
user = User(
email="test@example.com",
display_name="Test User",
is_active=True
)
db.add(user)
db.commit()
db.refresh(user)
return user
@pytest.fixture
def sample_image_path(temp_dir):
"""Create a valid PNG image file for testing"""
image_path = temp_dir / "test_image.png"
# Create a simple 100x100 white image
img = Image.new('RGB', (100, 100), color='white')
img.save(image_path, 'PNG')
return image_path
def admin_user(db):
"""Create an admin user"""
user = User(
email="ymirliu@panjit.com.tw",
display_name="Admin User",
is_active=True
)
db.add(user)
db.commit()
db.refresh(user)
return user
@pytest.fixture
def sample_jpg_path(temp_dir):
"""Create a valid JPG image file for testing"""
image_path = temp_dir / "test_image.jpg"
# Create a simple 100x100 white image
img = Image.new('RGB', (100, 100), color='white')
img.save(image_path, 'JPEG')
return image_path
def auth_token(test_user):
"""Create authentication token for test user"""
token_data = {
"sub": str(test_user.id),
"email": test_user.email
}
return create_access_token(token_data)
@pytest.fixture
def sample_pdf_path(temp_dir):
"""Create a valid PDF file for testing"""
pdf_path = temp_dir / "test_document.pdf"
# Create minimal valid PDF
pdf_content = b"""%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
/Resources <<
/Font <<
/F1 <<
/Type /Font
/Subtype /Type1
/BaseFont /Helvetica
>>
>>
>>
>>
endobj
4 0 obj
<<
/Length 44
>>
stream
BT
/F1 12 Tf
100 700 Td
(Test PDF) Tj
ET
endstream
endobj
xref
0 5
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000317 00000 n
trailer
<<
/Size 5
/Root 1 0 R
>>
startxref
410
%%EOF
"""
with open(pdf_path, 'wb') as f:
f.write(pdf_content)
return pdf_path
def admin_token(admin_user):
"""Create authentication token for admin user"""
token_data = {
"sub": str(admin_user.id),
"email": admin_user.email
}
return create_access_token(token_data)
@pytest.fixture
def corrupted_image_path(temp_dir):
"""Create a corrupted image file for testing"""
image_path = temp_dir / "corrupted.png"
# Write invalid PNG data
with open(image_path, 'wb') as f:
f.write(b'\x89PNG\r\n\x1a\n\x00\x00\x00corrupted data')
return image_path
@pytest.fixture
def large_file_path(temp_dir):
"""Create a valid PNG file larger than the upload limit"""
file_path = temp_dir / "large_file.png"
# Create a large PNG image with random data (to prevent compression)
# 15000x15000 with random pixels should be > 20MB
import numpy as np
random_data = np.random.randint(0, 256, (15000, 15000, 3), dtype=np.uint8)
img = Image.fromarray(random_data, 'RGB')
img.save(file_path, 'PNG', compress_level=0) # No compression
# Verify it's actually large
file_size = file_path.stat().st_size
assert file_size > 20 * 1024 * 1024, f"File only {file_size / (1024*1024):.2f} MB"
return file_path
@pytest.fixture
def unsupported_file_path(temp_dir):
"""Create a file with unsupported format"""
file_path = temp_dir / "test.txt"
with open(file_path, 'w') as f:
f.write("This is a text file, not an image")
return file_path
@pytest.fixture
def preprocessor():
"""Create a DocumentPreprocessor instance"""
return DocumentPreprocessor()
@pytest.fixture
def sample_image_with_text():
"""Return path to a real image with text from demo_docs for OCR testing"""
# Use the english.png sample from demo_docs
demo_image_path = Path(__file__).parent.parent.parent / "demo_docs" / "basic" / "english.png"
# Check if demo image exists, otherwise skip the test
if not demo_image_path.exists():
pytest.skip(f"Demo image not found at {demo_image_path}")
return demo_image_path
def test_task(db, test_user):
"""Create a test task"""
task = Task(
user_id=test_user.id,
task_id="test-task-123",
filename="test.pdf",
file_type="application/pdf",
status="pending"
)
db.add(task)
db.commit()
db.refresh(task)
return task

View File

@@ -0,0 +1,179 @@
"""
Tool_OCR - Pytest Fixtures and Configuration
Shared fixtures for all tests
"""
import pytest
import tempfile
import shutil
from pathlib import Path
from PIL import Image
import io
from app.services.preprocessor import DocumentPreprocessor
@pytest.fixture
def temp_dir():
"""Create a temporary directory for test files"""
temp_path = Path(tempfile.mkdtemp())
yield temp_path
# Cleanup after test
shutil.rmtree(temp_path, ignore_errors=True)
@pytest.fixture
def sample_image_path(temp_dir):
"""Create a valid PNG image file for testing"""
image_path = temp_dir / "test_image.png"
# Create a simple 100x100 white image
img = Image.new('RGB', (100, 100), color='white')
img.save(image_path, 'PNG')
return image_path
@pytest.fixture
def sample_jpg_path(temp_dir):
"""Create a valid JPG image file for testing"""
image_path = temp_dir / "test_image.jpg"
# Create a simple 100x100 white image
img = Image.new('RGB', (100, 100), color='white')
img.save(image_path, 'JPEG')
return image_path
@pytest.fixture
def sample_pdf_path(temp_dir):
"""Create a valid PDF file for testing"""
pdf_path = temp_dir / "test_document.pdf"
# Create minimal valid PDF
pdf_content = b"""%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
/Resources <<
/Font <<
/F1 <<
/Type /Font
/Subtype /Type1
/BaseFont /Helvetica
>>
>>
>>
>>
endobj
4 0 obj
<<
/Length 44
>>
stream
BT
/F1 12 Tf
100 700 Td
(Test PDF) Tj
ET
endstream
endobj
xref
0 5
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000317 00000 n
trailer
<<
/Size 5
/Root 1 0 R
>>
startxref
410
%%EOF
"""
with open(pdf_path, 'wb') as f:
f.write(pdf_content)
return pdf_path
@pytest.fixture
def corrupted_image_path(temp_dir):
"""Create a corrupted image file for testing"""
image_path = temp_dir / "corrupted.png"
# Write invalid PNG data
with open(image_path, 'wb') as f:
f.write(b'\x89PNG\r\n\x1a\n\x00\x00\x00corrupted data')
return image_path
@pytest.fixture
def large_file_path(temp_dir):
"""Create a valid PNG file larger than the upload limit"""
file_path = temp_dir / "large_file.png"
# Create a large PNG image with random data (to prevent compression)
# 15000x15000 with random pixels should be > 20MB
import numpy as np
random_data = np.random.randint(0, 256, (15000, 15000, 3), dtype=np.uint8)
img = Image.fromarray(random_data, 'RGB')
img.save(file_path, 'PNG', compress_level=0) # No compression
# Verify it's actually large
file_size = file_path.stat().st_size
assert file_size > 20 * 1024 * 1024, f"File only {file_size / (1024*1024):.2f} MB"
return file_path
@pytest.fixture
def unsupported_file_path(temp_dir):
"""Create a file with unsupported format"""
file_path = temp_dir / "test.txt"
with open(file_path, 'w') as f:
f.write("This is a text file, not an image")
return file_path
@pytest.fixture
def preprocessor():
"""Create a DocumentPreprocessor instance"""
return DocumentPreprocessor()
@pytest.fixture
def sample_image_with_text():
"""Return path to a real image with text from demo_docs for OCR testing"""
# Use the english.png sample from demo_docs
demo_image_path = Path(__file__).parent.parent.parent / "demo_docs" / "basic" / "english.png"
# Check if demo image exists, otherwise skip the test
if not demo_image_path.exists():
pytest.skip(f"Demo image not found at {demo_image_path}")
return demo_image_path

View File

@@ -11,16 +11,26 @@ class TestAuth:
def test_login_success(self, client, db):
"""Test successful login"""
# Mock external auth service
# Mock external auth service with proper Pydantic models
from app.services.external_auth_service import AuthResponse, UserInfo
user_info = UserInfo(
id="test-id-123",
name="Test User",
email="test@example.com"
)
auth_response = AuthResponse(
access_token="test-token",
id_token="test-id-token",
expires_in=3600,
token_type="Bearer",
user_info=user_info,
issued_at="2025-11-16T10:00:00Z",
expires_at="2025-11-16T11:00:00Z"
)
with patch('app.routers.auth.external_auth_service.authenticate_user') as mock_auth:
mock_auth.return_value = (True, {
'access_token': 'test-token',
'expires_in': 3600,
'user_info': {
'email': 'test@example.com',
'name': 'Test User'
}
}, None)
mock_auth.return_value = (True, auth_response, None)
response = client.post('/api/v2/auth/login', json={
'username': 'test@example.com',
@@ -72,4 +82,6 @@ class TestAuth:
assert response.status_code == 200
data = response.json()
assert data['message'] == 'Logged out successfully'
# When no session_id is provided, logs out all sessions
assert 'message' in data
assert 'Logged out' in data['message']