---
applyTo: "tests/**/*.py"
---

# Testing Instructions

These instructions apply to all test files in the `tests/` directory.

## Test Organization
- Mirror the structure of `app/` directory in `tests/`
- Name test files with `test_` prefix (e.g., `test_api.py`)
- Group related tests in classes with `Test` prefix
- Use descriptive test function names: `test_<what>_<condition>_<expected>`

## Pytest Configuration
- Configuration in `pytest.ini`
- Run tests: `pytest -v`
- With coverage: `pytest --cov=app --cov-report=term-missing`
- Run specific markers: `pytest -m unit` or `pytest -m integration`

## Test Markers
Use pytest markers to categorize tests:
```python
import pytest

@pytest.mark.unit
def test_document_validation():
    """Test document validation logic."""
    pass

@pytest.mark.integration
def test_document_upload_api():
    """Test document upload endpoint."""
    pass

@pytest.mark.slow
def test_large_file_processing():
    """Test processing of large files."""
    pass

@pytest.mark.requires_external
def test_openai_integration():
    """Test OpenAI API integration."""
    pass
```

Available markers:
- `unit` - Unit tests for individual functions/methods
- `integration` - Integration tests for API endpoints and workflows
- `slow` - Tests that take significant time to run
- `security` - Security-related tests
- `requires_external` - Tests requiring external services (OpenAI, Azure, etc.)
- `requires_db` - Tests requiring database
- `requires_redis` - Tests requiring Redis

## Fixtures
Use pytest fixtures for test setup and teardown:
```python
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base

@pytest.fixture
def db_session():
    """Provide a database session for tests."""
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    Session = sessionmaker(bind=engine)
    session = Session()

    yield session

    session.close()
    Base.metadata.drop_all(engine)

@pytest.fixture
def sample_document():
    """Provide a sample document for tests."""
    return {
        "filename": "test.pdf",
        "content_type": "application/pdf",
        "size": 1024
    }
```

## API Testing with FastAPI
Use `TestClient` from FastAPI:
```python
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_upload_document():
    """Test document upload endpoint."""
    with open("tests/fixtures/sample.pdf", "rb") as f:
        response = client.post(
            "/api/documents/upload",
            files={"file": ("test.pdf", f, "application/pdf")}
        )

    assert response.status_code == 201
    assert "id" in response.json()
```

## Async Testing
For async code, use `pytest-asyncio`:
```python
import pytest
import httpx

@pytest.mark.asyncio
async def test_async_document_processing():
    """Test async document processing."""
    async with httpx.AsyncClient(app=app, base_url="http://test") as client:
        response = await client.get("/api/documents/1")
        assert response.status_code == 200
```

## Mocking External Services
Always mock external services in tests:
```python
from unittest.mock import Mock, patch

@pytest.mark.unit
def test_openai_metadata_extraction(mocker):
    """Test metadata extraction with mocked OpenAI."""
    mock_response = {
        "document_type": "invoice",
        "amount": 100.00,
        "date": "2024-01-01"
    }

    mocker.patch(
        "app.utils.openai_client.extract_metadata",
        return_value=mock_response
    )

    result = extract_document_metadata("test.pdf")
    assert result["document_type"] == "invoice"

@pytest.mark.unit
def test_azure_ocr_processing(mocker):
    """Test OCR with mocked Azure service."""
    mock_text = "Sample extracted text"

    mocker.patch(
        "app.utils.azure_client.extract_text",
        return_value=mock_text
    )

    result = perform_ocr("test.pdf")
    assert result == mock_text
```

## Database Testing
```python
@pytest.mark.requires_db
def test_create_document(db_session):
    """Test document creation in database."""
    from app.models import Document

    doc = Document(
        filename="test.pdf",
        user_id=1,
        file_path="/tmp/test.pdf"
    )
    db_session.add(doc)
    db_session.commit()

    assert doc.id is not None
    assert doc.filename == "test.pdf"
```

## Test Coverage Goals
- Achieve **100% test coverage** for all new code — use `# pragma: no cover` only for genuinely unreachable or platform-specific branches, with an inline comment explaining why
- Enforce the threshold: `pytest --cov=app --cov-fail-under=100`
- Focus on critical paths and error handling
- Test both success and failure scenarios
- Don't test third-party library code

## Test Structure
Follow the Arrange-Act-Assert pattern:
```python
def test_document_validation():
    """Test that invalid documents are rejected."""
    # Arrange
    invalid_document = {
        "filename": "",  # Empty filename
        "size": -1  # Invalid size
    }

    # Act
    result = validate_document(invalid_document)

    # Assert
    assert result.is_valid is False
    assert "filename" in result.errors
    assert "size" in result.errors
```

## Parameterized Tests
Use `pytest.mark.parametrize` for multiple test cases:
```python
@pytest.mark.parametrize("filename,expected", [
    ("document.pdf", True),
    ("image.jpg", True),
    ("script.exe", False),
    ("", False),
])
def test_allowed_file_types(filename, expected):
    """Test file type validation."""
    result = is_allowed_file(filename)
    assert result == expected
```

## Test Data
- Place test fixtures in `tests/fixtures/` directory
- Use small sample files for testing
- Don't commit large test files
- Clean up test files in teardown

## Error Testing
Always test error conditions:
```python
def test_missing_file_raises_error():
    """Test that missing files raise appropriate error."""
    with pytest.raises(FileNotFoundError):
        process_document("/nonexistent/file.pdf")

def test_invalid_api_request():
    """Test API error handling."""
    response = client.post("/api/documents/", json={})
    assert response.status_code == 422  # Validation error
```

## Best Practices
- Test one thing per test function
- Use descriptive test names
- Keep tests independent (no dependencies between tests)
- Use fixtures for common setup
- Mock external dependencies
- Test edge cases and error conditions
- Keep tests fast (use mocks for slow operations)
- Clean up resources after tests
