Files
2026-06-02 15:32:06 +02:00

14 KiB

Testing Patterns

Analysis Date: 2026-06-02

Test Framework

Backend Runner:

  • pytest 8.2+ with pytest-asyncio
  • Config: backend/pytest.iniasyncio_mode = auto, testpaths = tests
  • asyncio_mode = auto means all async def test_* functions run as coroutines automatically

Backend Assertion Library:

  • pytest built-in assert
  • unittest.mock for AsyncMock, MagicMock, patch

Frontend Runner:

  • Vitest 4.1.7
  • Config: frontend/vitest.config.jsenvironment: 'happy-dom', globals: true
  • @vue/test-utils 2.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>.pytest_auth_api.py, test_documents.py, test_shares.py
  • test_<layer>_<area>.py for 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.js
  • frontend/src/components/folders/__tests__/FolderTreeItem.test.js
  • frontend/src/views/__tests__/FileManagerView.test.js
  • frontend/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 SQLiteTypeCompiler monkey-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.patch for external service calls: patch("services.auth.check_hibp", return_value=True)
  • AsyncMock for async methods: monkeypatch.setattr(MinIOBackend, "stat_object", mock, raising=False)
  • FakeRedis class 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_session for DB substitution

Frontend mocking:

  • vi.mock('../../api/client.js', () => ({ login: vi.fn(), ... })) — mock entire API module
  • Individual function mocks: const mockListFolders = vi.fn() then vi.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 largely xfail(strict=False) waiting for module implementation
  • Concurrent quota race (test_concurrent_quota_race) is xfail(strict=False) — requires PostgreSQL row-level locking
  • Delete quota decrement (test_delete_decrements_quota) is xfail(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.vue have no tests
  • src/components/cloud/CloudFolderTreeItem.vue, CloudProviderTreeItem.vue, CloudCredentialModal.vue have no tests
  • src/components/sharing/ShareModal.vue has no tests
  • src/components/upload/DropZone.vue, UploadProgress.vue have no tests
  • src/components/layout/AppSidebar.vue, QuotaBar.vue have no tests
  • src/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 — asserts hmac.compare_digest used (source inspection + behavioral)
  • test_security.py — asserts credentials_enc never appears in API responses (SEC-08); asserts admin DELETE calls storage.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 — includes test_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