Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
14 KiB
Testing Patterns
Analysis Date: 2026-06-02
Test Framework
Backend Runner:
- pytest 8.2+ with pytest-asyncio
- Config:
backend/pytest.ini—asyncio_mode = auto,testpaths = tests asyncio_mode = automeans allasync def test_*functions run as coroutines automatically
Backend Assertion Library:
- pytest built-in
assert unittest.mockforAsyncMock,MagicMock,patch
Frontend Runner:
- Vitest 4.1.7
- Config:
frontend/vitest.config.js—environment: 'happy-dom',globals: true @vue/test-utils2.4.10 for component mounting
Run Commands:
# Backend — from backend/ directory
pytest -v # Run all tests
pytest tests/test_auth_api.py # Single file
INTEGRATION=1 pytest -v # Run with live Docker services (PostgreSQL + MinIO + Redis)
# Frontend — from frontend/ directory
npm test # vitest run (one-shot)
npx vitest # watch mode
Test File Organization
Backend location: All tests in backend/tests/; flat structure, one file per concern.
Naming:
test_<area>.py—test_auth_api.py,test_documents.py,test_shares.pytest_<layer>_<area>.pyfor unit tests:test_task2_auth_service.py,test_cloud_backends.py
Frontend location: Co-located in __tests__/ subdirectories next to the code they test:
frontend/src/stores/__tests__/auth.test.jsfrontend/src/components/folders/__tests__/FolderTreeItem.test.jsfrontend/src/views/__tests__/FileManagerView.test.jsfrontend/src/router/__tests__/router.guard.test.js
Backend Test Structure
Standard async test (most common pattern):
@pytest.mark.asyncio
async def test_register_success(authed_client):
"""POST /api/auth/register with valid data returns 201 with id and handle."""
resp = await _register(authed_client)
assert resp.status_code == 201, resp.text
data = resp.json()
assert "id" in data
assert data["handle"] == "testuser"
Module-level async mark (newer pattern, avoids per-function decorator):
pytestmark = pytest.mark.asyncio # at module top — used in test_shares.py, test_audit.py
Shared helper functions: Each test file defines async helper functions (not fixtures) for setup operations:
async def _register(async_client, handle="testuser", email="t@example.com", password="ValidPass12!"):
return await async_client.post("/api/auth/register", json={...})
ORM-direct test data creation: Tests often insert data via ORM rather than API to test specific states:
doc = Document(id=doc_id, user_id=auth_user["user"].id, ...)
db_session.add(doc)
await db_session.commit()
Backend Fixtures (conftest.py)
All fixtures are async (@pytest_asyncio.fixture) unless purely synchronous.
Session fixture:
@pytest_asyncio.fixture
async def db_session():
# In-memory SQLite with PostgreSQL type shims (INET, JSONB patched to TEXT)
# Used for all unit/integration tests without live services
HTTP client fixtures:
@pytest_asyncio.fixture
async def async_client(db_session):
# httpx.AsyncClient + ASGITransport wrapping the real FastAPI app
# DB dependency overridden via app.dependency_overrides[get_db]
Auth fixtures (shared across all API tests):
@pytest_asyncio.fixture
async def auth_user(db_session):
# Creates User + Quota, issues JWT, returns:
# { "user": User, "token": str, "headers": {"Authorization": "Bearer ..."} }
@pytest_asyncio.fixture
async def second_auth_user(db_session):
# Same shape as auth_user — used for sharing tests (owner + recipient)
@pytest_asyncio.fixture
async def admin_user(db_session):
# Same shape, role="admin"
Infrastructure mocks:
@pytest.fixture
def mock_minio_presigned(monkeypatch):
# Patches MinIOBackend.generate_presigned_put_url with AsyncMock
@pytest.fixture
def mock_minio_stat(monkeypatch):
# Patches MinIOBackend.stat_object with AsyncMock returning 1024 bytes
# Override per-test: mock_minio_stat.return_value = 50_000_000
Cloud fixtures:
@pytest.fixture
def mock_google_drive_creds(): # Fake OAuth credential dict
@pytest.fixture
def mock_onedrive_creds(): # Fake MSAL credential dict
@pytest.fixture
async def cloud_connection_factory(db_session):
# Factory: creates CloudConnection ORM rows
# Usage: conn = await cloud_connection_factory(session, user_id, provider="google_drive")
File fixtures:
@pytest.fixture
def sample_txt(tmp_path): # Creates "sample.txt" in tmp_path
@pytest.fixture
def sample_pdf(tmp_path): # Creates minimal PDF via PyMuPDF
Service Availability and Integration Mode
Tests default to in-memory SQLite (no live services required):
- PostgreSQL-specific types (UUID, INET, JSONB) are patched via
SQLiteTypeCompilermonkey-patching - Tests that require PostgreSQL row-level locking semantics are marked
@pytest.mark.xfail(strict=False)
For live service testing, set INTEGRATION=1 or have Docker services running on their default ports (PostgreSQL:5432, MinIO:9000, Redis:6379). The live_services_available() fixture detects this.
Mocking
Backend mocking:
unittest.mock.patchfor external service calls:patch("services.auth.check_hibp", return_value=True)AsyncMockfor async methods:monkeypatch.setattr(MinIOBackend, "stat_object", mock, raising=False)FakeRedisclass defined inline in test files that need it (test_auth_api.py, test_security_headers.py, test_totp_replay.py) — in-memory dict with TTL support, mirrors Redis get/set/incr/expire interface- Celery tasks mocked with
MagicMock:monkeypatch.setattr("api.documents.extract_and_classify.delay", MagicMock()) app.dependency_overrides[get_db] = lambda: db_sessionfor DB substitution
Frontend mocking:
vi.mock('../../api/client.js', () => ({ login: vi.fn(), ... }))— mock entire API module- Individual function mocks:
const mockListFolders = vi.fn()thenvi.mock(...)referencing the mock - Store mocks for component tests:
vi.mock('../../stores/auth.js', () => ({ useAuthStore: () => ({ user: {...} }) })) - Heavy child component stubs:
vi.mock('../../components/X.vue', () => ({ default: { template: '<div/>' } })) - Browser storage stubs:
Object.defineProperty(globalThis, 'localStorage', { value: fakeLocalStorage })
Frontend Test Structure
Store tests (primary coverage):
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
beforeEach(() => {
setActivePinia(createPinia()) // fresh Pinia before each test
vi.clearAllMocks()
})
describe('useAuthStore — behavior group', () => {
it('describes exactly one assertion', async () => {
api.login.mockResolvedValue({ access_token: 'tok', user: {...} })
const store = useAuthStore()
await store.login('u@x.com', 'pass')
expect(store.accessToken).toBe('tok')
})
})
Component tests (mount-based):
import { mount, flushPromises } from '@vue/test-utils'
// ...
const wrapper = mount(ComponentName, {
props: { item: makeItem() },
global: { plugins: [router] }
})
await flushPromises()
expect(wrapper.find('button').exists()).toBe(false)
Coverage by Area
Backend Coverage (329 test functions across 26 test files)
| Area | Test file(s) | Coverage |
|---|---|---|
| Auth API (register, login, TOTP, backup codes, refresh, logout, change-password) | test_auth_api.py (498 lines) |
High |
| Auth service unit tests (JWT, password, TOTP, backup codes) | test_task2_auth_service.py |
High |
| Auth dependencies (get_current_user, get_current_admin) | test_auth_deps.py |
High |
| TOTP replay prevention (AUTH-08) | test_totp_replay.py (239 lines) |
High |
| Per-account rate limiting (SEC-02) | test_auth_api.py |
High |
| Documents API (list, filter, confirm, delete, PATCH, content) | test_documents.py (925 lines) |
High |
| Quota enforcement (atomic increment, concurrent race, delete decrement) | test_quota.py (239 lines) |
Medium — concurrent race xfail on SQLite |
| Folder API (CRUD, breadcrumb, IDOR) | test_folders.py (494 lines) |
High |
| Sharing API (SHARE-01 through SHARE-05) | test_shares.py (454 lines) |
High |
| Admin API (users, quotas, AI config, ADMIN-07 no-impersonation) | test_admin_api.py (431 lines) |
High |
| Audit log (SHARE events, AUTH events, CSV export) | test_audit.py (355 lines) |
High |
| Security headers (CSP, X-Frame-Options, nosniff) | test_security_headers.py |
High |
| Security invariants (credentials_enc not exposed, IDOR) | test_security.py |
High |
| Constant-time comparisons (SEC-03, hmac.compare_digest) | test_constant_time_auth.py |
High |
| Cloud storage (CLOUD-01 through CLOUD-07, SSRF, IDOR) | test_cloud.py (855 lines) |
High |
| Cloud backends (Google Drive, OneDrive, WebDAV, Nextcloud) | test_cloud_backends.py, test_webdav_backend.py |
Medium |
| Cloud credential encryption/decryption | test_cloud_utils.py (273 lines) |
High |
| AI classifier JSON parsing | test_classifier.py (266 lines) |
High |
| Text extraction | test_extractor.py |
High |
| MinIO object key schema | test_storage.py (277 lines) |
Medium |
| Settings API | test_settings.py |
Medium |
| Topics API | test_topics.py (204 lines) |
High |
| Health endpoint | test_health.py |
Low (smoke test) |
| Alembic migrations | test_alembic.py (246 lines) |
Medium |
| LM Studio provider | test_lmstudio.py |
Conditional — @pytest.mark.skipif unless reachable |
Frontend Coverage (14 test files, ~163 test cases)
| Area | Test file | Coverage |
|---|---|---|
| Auth store (login, logout, TOTP, no-browser-storage invariant) | stores/__tests__/auth.test.js |
High |
| Folders store (fetchFolders, createFolder, rename, delete) | stores/__tests__/folders.test.js |
High |
| Cloud connections store | stores/__tests__/cloudConnections.test.js |
Medium |
| Router guards (meta.public, meta.layout, redirect on unauthenticated) | router/__tests__/router.guard.test.js |
High |
| FileManagerView (folder navigation, search, sort, move, delete) | views/__tests__/FileManagerView.test.js |
Medium |
| FolderTreeItem (expand arrow, active state) | components/folders/__tests__/FolderTreeItem.test.js |
Medium |
| FolderBreadcrumb | components/folders/__tests__/FolderBreadcrumb.test.js |
Medium |
| TotpEnrollment component | components/auth/__tests__/TotpEnrollment.test.js |
Medium |
| PasswordStrengthBar component | components/auth/__tests__/PasswordStrengthBar.test.js |
Medium |
| AdminUsersTab component | components/admin/__tests__/AdminUsersTab.test.js |
Medium |
| AdminQuotasTab component | components/admin/__tests__/AdminQuotasTab.test.js |
Medium |
| AdminAiConfigTab component | components/admin/__tests__/AdminAiConfigTab.test.js |
Medium |
| SettingsAccountTab component | components/settings/__tests__/SettingsAccountTab.test.js |
Medium |
| SettingsCloudTab component | components/settings/__tests__/SettingsCloudTab.test.js |
Medium |
Test Gaps
Backend gaps:
test_storage.py— MinIO object key tests are largelyxfail(strict=False)waiting for module implementation- Concurrent quota race (
test_concurrent_quota_race) isxfail(strict=False)— requires PostgreSQL row-level locking - Delete quota decrement (
test_delete_decrements_quota) isxfail(strict=False)on SQLite - No
pytest-cov— no coverage measurement enforced - No CI configuration (no GitHub Actions yaml)
Frontend gaps:
src/components/documents/—DocumentCard.vue,DocumentPreviewModal.vue,SearchBar.vue,SortControls.vuehave no testssrc/components/cloud/—CloudFolderTreeItem.vue,CloudProviderTreeItem.vue,CloudCredentialModal.vuehave no testssrc/components/sharing/—ShareModal.vuehas no testssrc/components/upload/—DropZone.vue,UploadProgress.vuehave no testssrc/components/layout/—AppSidebar.vue,QuotaBar.vuehave no testssrc/stores/documents.js— documents store has no tests- No E2E tests (no Playwright or Cypress)
Security-Specific Tests
These test files exist specifically to enforce security invariants:
test_constant_time_auth.py— assertshmac.compare_digestused (source inspection + behavioral)test_security.py— assertscredentials_encnever appears in API responses (SEC-08); asserts admin DELETE callsstorage.delete_object(SEC-09)test_security_headers.py— asserts CSP, X-Frame-Options, X-Content-Type-Options on every response (SEC-05)test_totp_replay.py— asserts same TOTP code rejected on second use (AUTH-08)test_auth_api.py— includestest_origin_rejected(CSRF),test_per_account_rate_limit(SEC-02)test_auth_deps.py— includes wrong-owner 403, deactivated user 401, admin-blocked 403
Common Patterns
Async testing:
# Option 1 — per-test decorator
@pytest.mark.asyncio
async def test_something(async_client, auth_user):
resp = await async_client.get("/api/documents", headers=auth_user["headers"])
assert resp.status_code == 200
# Option 2 — module-level mark
pytestmark = pytest.mark.asyncio
async def test_something(async_client, auth_user):
...
Security negative tests (wrong owner → 403/404):
async def test_cannot_access_other_users_document(async_client, auth_user, second_auth_user, db_session):
doc_id = await _make_doc(db_session, auth_user)
resp = await async_client.get(f"/api/documents/{doc_id}", headers=second_auth_user["headers"])
assert resp.status_code in (403, 404)
Patching external calls:
with patch("services.auth.check_hibp", return_value=True) as mock_hibp:
resp = await authed_client.post("/api/auth/change-password", ...)
assert resp.status_code == 422
Frontend security invariant testing:
it('login() never writes accessToken to localStorage', async () => {
api.login.mockResolvedValue({ access_token: 'tok', user: {...} })
const store = useAuthStore()
await store.login('alice@example.com', 'password')
expect(fakeLocalStorage.setItem).not.toHaveBeenCalled()
})
Testing analysis: 2026-06-02