# Backend Tests Tests covering the FastAPI gateway (`backend:8000`): auth, user/profile management, admin (users/groups/appearance), service health, plugin system, AI settings, document settings, and infrastructure/security. Full combined suite: `tests/ALL_TESTS.md` **Test environment:** Feature stack at `http://localhost:$PORT` (see CLAUDE.md §Feature branch workflow). **Admin credentials:** any superuser account created during stack setup. **Regular user credentials:** a second non-admin account for permission boundary tests. --- ## Legend | Symbol | Meaning | |--------|---------| | ✅ | Pass | | ❌ | Fail | | — | N/A for this change | --- ## 1. Authentication | # | Test | Steps | Expected | |---|------|-------|----------| | 1.1 | Register new account | `POST /api/auth/register` with valid email + strong password | 201; user row created; login works immediately | | 1.2 | Password policy — too short | Register with 7-char password | 422 with validation error | | 1.3 | Password policy — no uppercase | Register with all-lowercase password | 422 with validation error | | 1.4 | Password policy — no special char | Register without special character | 422 with validation error | | 1.5 | Password policy — common word | Register with password containing "password" | 422 with validation error | | 1.6 | Duplicate email | Register with an already-used email | 400 | | 1.7 | Login — valid credentials | `POST /api/auth/login` with correct email + password | 200; `access_token` returned | | 1.8 | Login — wrong password | `POST /api/auth/login` with wrong password | 401 | | 1.9 | Login — inactive account | Admin deactivates user; attempt login | 401 | | 1.10 | JWT expiry respected | Manually craft token with `exp` in the past; call any protected route | 401 | | 1.11 | Logout clears session | Click Logout in UI; try navigating to `/` | Redirected to `/login` | | 1.12 | Unauthenticated redirect | Open `/` without a token in `localStorage` | Redirected to `/login` | --- ## 2. User — Profile & Preferences | # | Test | Steps | Expected | |---|------|-------|----------| | 2.1 | Fetch own profile | `GET /api/profile/me` | 200; profile auto-created if first request | | 2.2 | Update profile fields | `PUT /api/profile/me` with full_name, phone, position, address, date_of_birth | 200; fields persisted; visible on `/profile` page | | 2.3 | Invalid phone format | `PUT /api/profile/me` with letters in phone field | 422 | | 2.4 | Future date of birth | `PUT /api/profile/me` with DOB = tomorrow | 422 | | 2.5 | DOB before 1900 | `PUT /api/profile/me` with DOB = 1899-12-31 | 422 | | 2.6 | Fetch dashboard preferences | `GET /api/users/me/preferences` | 200; `app_ids` list | | 2.7 | Pin an app | Dashboard → pencil button → press `+` on a card → save | Card appears in pinned grid on next load | | 2.8 | Unpin an app | Dashboard → pencil button → press `−` on a pinned card → save | Card removed from pinned grid | | 2.9 | Pin limit (50) | `PATCH /api/users/me/preferences` with 51 app IDs | 422 | | 2.10 | Color mode — user pref | User sets mode to "dark"; reload page | Dark theme applied; preference persists across sessions | | 2.11 | Color mode — system fallback | User has NULL color_mode; admin default_mode = "light" | Light theme applied | --- ## 3. Admin — Users | # | Test | Steps | Expected | |---|------|-------|----------| | 3.1 | List all users | Admin → `/admin/users` | All registered users shown in table | | 3.2 | Create user | Admin clicks "Create user"; fills form | 201; new user appears in list; can log in | | 3.3 | Toggle user active | Admin clicks toggle on active user | User deactivated; login returns 401 | | 3.4 | Delete user | Admin deletes a user | 204; user no longer in list; their documents remain (orphaned) | | 3.5 | Non-admin access | Regular user navigates to `/admin/users` | Redirected to `/login` | | 3.6 | Admin 404 semantics | Regular user calls `GET /api/admin/users` via curl | 404 (not 403) | --- ## 4. Admin — Groups | # | Test | Steps | Expected | |---|------|-------|----------| | 4.1 | List groups | Admin → `/admin/groups` | All groups shown with member count | | 4.2 | Create group | Fill name + description → submit | Group appears in list; `{group_name}-admin` bootstrap group also exists (auto-created on service start) | | 4.3 | Edit group | Click edit on group → change name → save | Name updated | | 4.4 | Delete group | Delete group | 204; group gone; memberships cascade-deleted | | 4.5 | Add member | Open group → search user → add | 204; user appears in member list | | 4.6 | Remove member | Click remove on a member | User removed from group | | 4.7 | Duplicate group name | Create group with name that already exists | 400 / validation error shown | | 4.8 | Non-admin access | Regular user calls `GET /api/admin/groups` | 404 | | 4.9 | Set group admin role | Admin → group detail → tick "Group admin" checkbox on a member → save | `PATCH /api/admin/groups/{id}/members/{user_id}/admin` with `{"is_group_admin": true}` returns 200; badge shown in member list | | 4.10 | Unset group admin role | Admin unticks "Group admin" on an existing group admin member | Returns 200; badge removed; user loses group-admin privileges | --- ## 5. Admin — Appearance | # | Test | Steps | Expected | |---|------|-------|----------| | 5.1 | List themes | Admin → `/admin/appearance` | Built-in themes + any custom themes shown | | 5.2 | Switch active theme | Select a different theme → save | All users see the new theme on next load | | 5.3 | Create custom theme | Admin → create theme. Required fields: `id` (slug), `label`, `light` (CSS vars object), `dark` (CSS vars object) | 201; theme appears in selector; can be activated | | 5.4 | Edit custom theme | Admin edits colour values on a custom theme | Colours update live after activation | | 5.5 | Delete custom theme | Admin deletes a custom theme | 204; theme gone from selector; active theme reverts to default | | 5.6 | Set default mode | `PATCH /api/settings/appearance` with `{"theme": "", "default_mode": "dark"}` (both fields required) | 200; new users without a personal preference see dark mode | --- ## 6. Service Health & Dashboard | # | Test | Steps | Expected | |---|------|-------|----------| | 6.1 | Services endpoint | `GET /api/services` (authenticated) | Returns health status for doc-service and ai-service | | 6.2 | Healthy service card | Both services running → `/apps` page. API response uses `healthy: true` (boolean), not `status: "healthy"` | Cards show "Available" badge; clicking navigates to the app | | 6.3 | Unhealthy service card | Stop doc-service container → wait 30s → `/apps` | Doc-service card dimmed, "Unavailable", not clickable | | 6.4 | Service recovery | Restart stopped container → wait 30s | Card returns to "Available" | | 6.5 | Dashboard pinned cards | Pin a service → go to `/` | Pinned card appears in home grid | | 6.6 | Customize mode | Click pencil on dashboard → toggle cards | Pinned list updates after save | --- ## 7. Plugin System | # | Test | Steps | Expected | |---|------|-------|----------| | 7.1 | List plugins | `GET /api/plugins` (authenticated) | Returns accessible plugins for current user | | 7.2 | Superuser sees all plugins | Log in as admin → `GET /api/plugins` | All registered service plugins returned | | 7.3 | Group member sees plugin | Add user to `doc-service-admin` group → `GET /api/plugins` | doc-service plugin returned | | 7.4 | Unpermitted user hidden | Regular user not in any admin group → `GET /api/plugins` | Empty list (plugins hidden, not 403) | | 7.5 | Manifest fetch | `GET /api/plugins/doc-service/manifest` as permitted user | JSON Schema + access rules returned | | 7.6 | Settings read | `GET /api/plugins/doc-service/settings` | Current doc-service plugin settings returned | | 7.7 | Settings write | `PATCH /api/plugins/doc-service/settings` with valid payload | 200; setting persisted to volume | | 7.8 | Unpermitted settings access | Regular user `GET /api/plugins/doc-service/settings` | 404 | --- ## 8. AI Service Settings | # | Test | Steps | Expected | |---|------|-------|----------| | 8.1 | Read AI config | Admin (or `ai-service-admin` member) → `GET /api/settings/ai` | Config returned; API keys masked | | 8.2 | Update provider | `PATCH /api/settings/ai` with provider = "anthropic" + valid key | 200; config persisted | | 8.3 | Test connection | `POST /api/settings/ai/test` with valid config | 200; success response from provider | | 8.4 | Test connection — bad key | `POST /api/settings/ai/test` with wrong API key | 502 or error detail | | 8.5 | Read system prompts | `GET /api/settings/system-prompts` | All registered service prompts returned | | 8.6 | Update system prompt | `PATCH /api/settings/system-prompts/doc-service` with new prompt text | 200; doc-service picks up new prompt within 30s | | 8.7 | Non-admin access | Regular user calls any `/api/settings/ai` endpoint | 404 | | 8.8 | `ai-service-admin` delegation | Non-superuser added to `ai-service-admin` group → accesses AI settings page | Page loads; can read and write settings | --- ## 9. Document Service Settings | # | Test | Steps | Expected | |---|------|-------|----------| | 9.1 | Read upload limits | `GET /api/settings/documents/limits` (admin or `doc-service-admin`) | `max_pdf_bytes` returned | | 9.2 | Update upload limit | `PATCH /api/settings/documents/limits` with new value | 200; upload of oversized PDF now rejected with 413 | | 9.3 | Non-admin access | Regular user calls `GET /api/settings/documents/limits` | 404 | | 9.4 | Settings page loads | Admin navigates to `/apps/documents/settings` | Upload limits section + watch directory config visible | | 9.5 | `doc-service-admin` delegation | Non-superuser added to `doc-service-admin` → navigates to settings page | Page loads; settings editable | --- ## 18. Infrastructure & Security | # | Test | Steps | Expected | |---|------|-------|----------| | 18.1 | Non-root containers | `docker inspect --format '{{.Config.User}}'` for each service | Returns `1001` (or `70` for db) | | 18.2 | No host ports in prod | `docker compose up --build -d` → `docker ps` | Only port 80 (frontend) exposed; no 8000/8001/8010/5432 | | 18.3 | backend-net isolation | `curl http://localhost:8000` from host in prod | Connection refused | | 18.4 | Pre-commit hook runs | Stage a file with `eval("x")` → `git commit` | Commit blocked; security_check.py output shown | | 18.5 | Pre-commit hook — clean code | Normal commit | Hook passes; commit succeeds | | 18.6 | JWT algorithm none rejected | Craft token with `"alg": "none"` → call protected route | 401 | | 18.7 | XSS — input sanitation | Enter `` in title/name fields | Value stored as plain text; not executed in UI | | 18.8 | SQL injection attempt | Pass `'; DROP TABLE documents; --` as search param | 200 with empty results; no DB error | | 18.9 | CORS | `curl -H "Origin: http://evil.com" http://localhost/api/users/me` | Request blocked or `access-control-allow-origin` not set for that origin | | 18.10 | Config volume persistence | Restart all containers | AI provider config + doc limits survive restart | | 18.11 | Migration auto-apply | Start fresh stack | Both `alembic upgrade head` chains run without error; all tables created |