479108779f
All API calls now go through a thin request() wrapper around native fetch. Removes the axios dependency entirely. The wrapper injects the JWT on every request and — the key fix — clears localStorage and redirects to /login on any 401 response, so expired sessions no longer leave users on broken pages. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
22 KiB
22 KiB
Merge Checklist — Pre-merge Test Suite
Run all tests relevant to the changed area before merging any feature branch into main.
Every test describes the exact UI action or API call to perform and the expected outcome.
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 |
Mark each row before opening the PR.
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 |
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": "<id>", "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 |
10. Document Upload & Processing
| # | Test | Steps | Expected |
|---|---|---|---|
| 10.1 | Upload valid PDF | Drag-and-drop or file picker → select a PDF under the size limit | 202; document row appears with status=pending; transitions to done |
| 10.2 | Upload oversized PDF | Upload a PDF exceeding max_pdf_bytes |
413; error shown; no row created |
| 10.3 | Upload non-PDF | Upload a .docx or .jpg |
415; error shown |
| 10.4 | Multi-file upload | Select 3 PDFs at once | All 3 appear in upload queue panel; each processes independently |
| 10.5 | Upload queue panel | During upload → check bottom-right panel | Per-file status indicator; "Review →" link after each completes |
| 10.6 | Drag-and-drop overlay | Drag file over the documents page | Full-page overlay appears; drop uploads file |
| 10.7 | Processing status poll | Upload a large PDF | Table row auto-updates every 3s until status = done or failed |
| 10.8 | AI extraction result | Open slide-over for a done document |
title, document_type, tags, extracted_data fields populated |
| 10.9 | Failed extraction | AI service down → upload PDF | Status = failed; error_message shown in slide-over |
| 10.10 | Re-analyse | Click "Re-analyse" in slide-over | 202; status resets to pending; re-processes through AI |
11. Document List & Filtering
| # | Test | Steps | Expected |
|---|---|---|---|
| 11.1 | Default list | Navigate to /apps/documents |
Own documents shown, newest first, 20 per page |
| 11.2 | Search | Type in search box (debounced 400ms) | Results filtered by title / filename / tags / type |
| 11.3 | Filter by status | Add filter chip → Status → "done" | Only completed docs shown |
| 11.4 | Filter by type | Add filter chip → Document type → "invoice" | Only invoices shown |
| 11.5 | Filter by category | Add filter chip → Category → pick one | Only docs in that category shown |
| 11.6 | Remove filter chip | Click × on a chip | Filter removed; full list restored |
| 11.7 | Sort by column | Click "Date" column header | List re-ordered; chevron indicates direction; click again reverses |
| 11.8 | Pagination | Upload > 20 docs → scroll to bottom | Page controls appear; page 2 loads next 20 |
| 11.9 | "Mine" view | Click "Mine" in SourcePanel | Only own (uploaded) documents shown |
| 11.10 | "Shared with me" view | Click "Shared with me" | Docs shared by others via groups; own docs excluded |
| 11.11 | Category filter via SourcePanel | Click a category in the left tree | Table filtered to that category's documents |
| 11.12 | URL state preserved | Apply filters → copy URL → open in new tab | Same filters applied |
12. Document Detail — Slide-over
| # | Test | Steps | Expected |
|---|---|---|---|
| 12.1 | Open slide-over | Click any document row | 480px right panel slides in; metadata loaded |
| 12.2 | Inline title edit | Click pencil icon next to title → type new title → confirm | Title saved; updated in table row |
| 12.3 | Change document type | Click a type chip (Invoice, Receipt, etc.) | Type updated immediately |
| 12.4 | Edit tags | Click into tag area → type a tag → press Enter → remove a tag with × | Tags saved correctly |
| 12.5 | Assign category | Categories combobox → search → select | Category badge appears on document; table row updates |
| 12.6 | Remove category | Click × on an assigned category badge | Category removed from document |
| 12.7 | AI category suggestions | Slide-over shows "Suggested categories" | "Assign" and "Create & Assign" buttons present; clicking assigns |
| 12.8 | Confirm folder suggestion | "Confirm" button next to suggested_folder | Category created (if needed) and assigned; suggested_folder cleared |
| 12.9 | Reject folder suggestion | "Reject" button next to suggested_folder | suggested_folder cleared; no category created |
| 12.10 | Confirm filename suggestion | "Confirm" button next to suggested_filename | title updated to suggested value; suggested_filename cleared |
| 12.11 | Reject filename suggestion | "Reject" button next to suggested_filename | suggested_filename cleared; title unchanged |
| 12.12 | Extracted data section | Open slide-over on done doc |
Key-value table of AI-extracted fields (vendor, amounts, dates, etc.) |
| 12.13 | Raw text section | Expand raw text collapse | First ~500k chars of extracted PDF text shown |
| 12.14 | Download | Click "Download" | Browser downloads the original PDF file |
| 12.15 | View in new tab | Click "View" | PDF opens in new browser tab; URL auto-revokes after 60s |
| 12.16 | Delete | Click "Delete" → confirm dialog | Document and file removed; table row gone |
| 12.17 | Non-owner cannot edit | Recipient of shared doc opens slide-over | Edit controls (type, tags, title, delete) absent; download available |
13. Document Sharing
| # | Test | Steps | Expected |
|---|---|---|---|
| 13.1 | Share from slide-over | Owner opens sharing section → selects a group from combobox → shares | Group appears in shares list; share_count in table row increments |
| 13.2 | Only user's own groups shown | Open group picker in share section | Only groups the current user belongs to are listed |
| 13.3 | Recipient sees shared doc | Log in as group member → "Shared with me" view | Shared document appears with primary accent border |
| 13.4 | Recipient download | Recipient clicks Download on shared doc | PDF downloaded successfully |
| 13.4b | Non-owner calls GET /documents/{id}/shares |
Regular user on a doc they don't own | 404 (doc-service hides existence, consistent with admin 404 semantics — not 403) |
| 13.5 | Recipient cannot delete | Recipient opens slide-over | Delete button absent |
| 13.6 | Recipient cannot re-share | Recipient opens sharing section | Share controls absent |
| 13.7 | Remove share | Owner clicks remove on a group share | Group removed; share_count decrements; recipient no longer sees doc |
| 13.8 | Bulk share | Select multiple rows → bulk share → pick group | All selected docs shared with that group |
| 13.9 | Share count indicator | Document shared with 2 groups | Users icon in table row shows "2" |
| 13.10 | Share with non-member group | POST /api/documents/{id}/shares with group not in X-User-Groups |
403 / validation error |
14. Categories
| # | Test | Steps | Expected |
|---|---|---|---|
| 14.1 | Create category | SourcePanel → "New category" form → submit | Category appears in tree |
| 14.2 | Rename category | Manage categories dialog → edit → save | New name reflected everywhere |
| 14.3 | Delete category | Delete category with documents assigned | 204; documents remain; category assignment removed |
| 14.4 | Category search | More than 4 categories → type in search field | Tree filtered in real time |
| 14.5 | Manage categories dialog | Click "Manage categories" | Modal shows all categories with rename/delete actions |
| 14.6 | New category triggers re-analysis | Create category with name similar to AI suggestion | Background re-analysis triggered (check backend logs) |
15. Bulk Actions
| # | Test | Steps | Expected |
|---|---|---|---|
| 15.1 | Select rows | Tick checkboxes on multiple rows | Floating bulk actions bar appears at bottom |
| 15.2 | Bulk share | Select docs → Share with group → confirm | All selected docs shared; confirmation |
| 15.3 | Bulk delete | Select docs → Delete → confirm dialog | All selected docs deleted; bar disappears |
| 15.4 | Clear selection | Click "Clear" in bulk bar | All checkboxes deselected; bar hides |
| 15.5 | Bulk bar — "Mine" view only | Switch to "Shared with me" view | Bulk actions bar not shown (no edit rights for shared docs) |
16. Watch Directory
| # | Test | Steps | Expected |
|---|---|---|---|
| 16.1 | Enable watch | Doc settings page → toggle watch_enabled on → save |
File watcher starts; backend logs confirm |
| 16.2 | Ingest new file | Drop a PDF into the bind-mounted watch directory | Document appears in "All Documents" view with source=watch |
| 16.3 | Sub-folder to category | Place PDF in watch/invoices/ |
Document auto-assigned to "invoices" category |
| 16.4 | Startup scan | Restart doc-service with PDFs already in watch dir | Pre-existing PDFs ingested (idempotent — no duplicates) |
| 16.5 | AI folder suggestion | ai_folder_suggestion enabled → ingest file |
suggested_folder populated; confirm/reject buttons visible in slide-over |
| 16.6 | AI rename suggestion | ai_rename_suggestion enabled → ingest file |
suggested_filename populated; confirm/reject buttons visible |
| 16.7 | No-remove policy | Delete PDF from watch dir | Document record remains in DB |
| 16.8 | Disable watch | Toggle watch_enabled off → save |
Watcher stops; new files dropped are not ingested |
| 16.9 | Watch docs visible to all users | Log in as any authenticated user | Watch-ingested docs (user_id = "watch") appear in "All Documents" |
17. AI Service — Queue & Providers
| # | Test | Steps | Expected |
|---|---|---|---|
| 17.1 | Health check | GET /health on ai-service (via backend services endpoint) |
{"status": "ok"} |
| 17.2 | Provider health | GET /health/provider |
Active provider name, model, configured=true |
| 17.3 | Unconfigured provider | Set provider to "anthropic" with empty API key → test connection | 503 or 502 with clear error |
| 17.4 | Sync chat | POST /chat with valid messages array |
Response returned synchronously |
| 17.5 | Queue — async job | POST /queue/jobs |
job_id returned immediately |
| 17.6 | Queue — poll job | GET /queue/jobs/{id} after enqueue |
Returns status (pending → done) and result |
| 17.7 | Queue — cancel job | DELETE /queue/jobs/{id} before processing |
Job removed; status = cancelled |
| 17.8 | Queue pause | POST /queue/pause |
204; current job finishes; no new jobs picked up |
| 17.9 | Queue resume | POST /queue/resume after pause |
204; worker resumes; pending jobs process |
| 17.10 | Priority ordering | Enqueue LOW then HIGH job | HIGH job processed first |
| 17.11 | Provider timeout | POST /chat when provider is unreachable |
504 returned after timeout |
18. Infrastructure & Security
| # | Test | Steps | Expected |
|---|---|---|---|
| 18.1 | Non-root containers | docker inspect <container> --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 <script>alert(1)</script> 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 |
19. Frontend — UI & Routing
| # | Test | Steps | Expected |
|---|---|---|---|
| 19.1 | PrivateRoute redirect | Open any protected route without token | Redirected to /login |
| 19.2 | AdminRoute redirect | Log in as non-admin → navigate to /admin |
Redirected to /login |
| 19.3 | ServiceAdminRoute | Non-admin, non-group-member → navigate to /apps/documents/settings |
Redirected (access denied) |
| 19.4 | Sidebar collapse | Click collapse button | Sidebar shrinks to icon-only; expand restores labels |
| 19.5 | Apps accordion | Click "Apps" in sidebar | Expands to show "Documents" NavLink |
| 19.6 | SourcePanel visibility | Navigate to /apps then /apps/documents |
SourcePanel only visible on /apps/documents route |
| 19.7 | Theme toggle | Click sun/moon button | Mode switches; persists on reload |
| 19.8 | Unknown route | Navigate to /does-not-exist |
Redirected to / |
| 19.9 | TanStack Query cache | Navigate away from docs → back | List loads from cache instantly; background refetch runs |
| 19.10 | 30s service poll | Leave /apps open for 30s |
GET /api/services fires again in network tab |