Tests for shared lists & docs catch-up to post-MVP state
Integration tests for the invite/join/membership feature that shipped without any coverage: - internal/store/liststore_test.go: CreateList adds owner as member with invite code, GetLists returns owned+joined but not foreign lists, GetList access control (owner/member yes, stranger and soft-deleted no), JoinByInviteCode normalization/idempotency/role-keeping, lazy invite code generation. Runs against TEST_DATABASE_URL, skips otherwise. - internal/httpapi/api_test.go: full E2E over the real router — register, create list (code in response), invite endpoint, join (lowercase), cross-member op push/pull sync, stranger gets 404 on every list endpoint, invalid code 400, idempotent re-join, and 401 gating of all protected routes. - lists.go Invite handler: store errors now map through apiError, so non-members get 404 instead of 400 (consistent with Get/Push/Pull). Docs updated to the actual post-MVP state: AGENTS.md (post-MVP features, repo structure, roadmap with open points like join rate limiting), API.md (join/invite endpoints, invite_code fields, membership rules), SYNC.md (shared lists section), README (local-only default, sharing, integration test recipe).
This commit is contained in:
parent
85c790ed23
commit
67033e561c
7 changed files with 692 additions and 33 deletions
106
AGENTS.md
106
AGENTS.md
|
|
@ -34,8 +34,21 @@ Vollständiger Plan liegt als genehmigtem Plan zugrunde (siehe Abschnitt "Roadma
|
|||
Google via **Credential Manager**, Generic OIDC via **Custom Tabs + eigenes PKCE**.
|
||||
- **Vorschläge:** aggregiert aus Item-Namen aller User (`item_names`-Tabelle,
|
||||
pg_trgm fuzzy search). Endpoint `GET /api/suggestions?q=`.
|
||||
- **MVP-Scope:** Single-Owner-Listen (`list_members` existiert, wird in Phase 2 für
|
||||
geteilte Listen genutzt); keine Echtzeit-Push (nur Periodic-Pull 15 min + Pull-on-Online);
|
||||
- **Post-MVP (umgesetzt):**
|
||||
- **Geteilte Listen per Invite-Code:** `lists.invite_code` (8 Zeichen, UNIQUE,
|
||||
Migration 000002). `POST /api/lists/{id}/invite` (Owner/Member lesen Code,
|
||||
wird lazy generiert), `POST /api/lists/join` (beitreten als `member`).
|
||||
`GET /api/lists` + `GET /api/lists/{id}` + Push/Pull-Ops prüfen Membership
|
||||
(owner ODER `list_members`-Eintrag). Kein Leave/Revoke-Endpoint, kein
|
||||
Rate-Limiting auf join (bekannt, bewusst offengehalten).
|
||||
- **Account optional / Local-Only-Mode:** App startet ohne Login direkt in die
|
||||
Listen (`local_user`-Fallback im `SessionManager`); Sync/Account ist optionaler
|
||||
Einstieg. Teilen/Beitreten erfordert aktive Server-Verbindung (App-gated).
|
||||
- **Self-Hosted Server-URL:** konfigurierbar in der App (Auth-Screen), umgesetzt
|
||||
via `DynamicBaseUrlInterceptor` (schreibt scheme/host/port pro Request um).
|
||||
- **Authentik-OIDC:** läuft über den bestehenden Generic-OIDC-Provider
|
||||
(`provider: "generic"`); die App erlaubt manuelle id_token-Eingabe.
|
||||
- **MVP-Scope:** keine Echtzeit-Push (nur Periodic-Pull 15 min + Pull-on-Online);
|
||||
keine Web-UI.
|
||||
|
||||
---
|
||||
|
|
@ -103,22 +116,26 @@ mitbringsl/
|
|||
│ │ ├── store/ # PHASE C – Store-Schicht
|
||||
│ │ │ ├── db.go # pgxpool-Setup
|
||||
│ │ │ ├── opstore.go # AppendOps (idempotent, LWW-Projektion), PullOps (cursor)
|
||||
│ │ │ ├── liststore.go # CreateList/GetLists/GetList
|
||||
│ │ │ ├── liststore.go # CreateList/GetLists/GetList/GetInviteCode/JoinByInviteCode
|
||||
│ │ │ ├── liststore_test.go # Invite/Join/Membership-Integrationstests (brauchen TEST_DATABASE_URL)
|
||||
│ │ │ ├── itemstore.go # GetItems
|
||||
│ │ │ └── suggeststore.go # Search (pg_trgm fuzzy)
|
||||
│ │ └── httpapi/
|
||||
│ │ ├── api.go # API-Objekt + Router (alle Routen aktiv)
|
||||
│ │ ├── api_test.go # E2E-HTTP-Tests: Share/Join/Sync + 401-Gating (TEST_DATABASE_URL)
|
||||
│ │ ├── render.go # JSON-Render + Problem + Fehler-Sentinale + decodeJSON
|
||||
│ │ ├── middleware.go # requestID/logging/recover/cors + Chain
|
||||
│ │ ├── health.go # /healthz + /readyz
|
||||
│ │ ├── auth.go # Register/Login/Logout/OIDC-Handler + RequireAuth-Middleware
|
||||
│ │ ├── lists.go # GET/POST /api/lists, GET /api/lists/{id}
|
||||
│ │ ├── ops.go # POST/GET /api/lists/{id}/ops (Push/Pull)
|
||||
│ │ ├── lists.go # GET/POST /api/lists, GET /api/lists/{id}, POST /invite, POST /join
|
||||
│ │ ├── ops.go # POST/GET /api/lists/{id}/ops (Push/Pull, Membership-Check)
|
||||
│ │ └── suggest.go # GET /api/suggestions?q=
|
||||
│ ├── migrations/
|
||||
│ │ ├── embed.go # //go:embed *.sql
|
||||
│ │ ├── 000001_init_schema.up.sql # users/sessions/lists/list_members/items/op_log/item_names
|
||||
│ │ └── 000001_init_schema.down.sql
|
||||
│ │ ├── 000001_init_schema.down.sql
|
||||
│ │ ├── 000002_add_invite_code.up.sql # lists.invite_code UNIQUE + Backfill owner→list_members
|
||||
│ │ └── 000002_add_invite_code.down.sql
|
||||
│ ├── Dockerfile # Multi-Stage, baut server + migrate
|
||||
│ ├── .dockerignore
|
||||
│ ├── go.mod / go.sum
|
||||
|
|
@ -128,8 +145,22 @@ mitbringsl/
|
|||
│ ├── Caddyfile.behind-proxy # hinter externem Reverse Proxy: auto_https off, trusted_proxies
|
||||
│ ├── .env.example # alle env-Vars dokumentiert inkl. Caddy-Ports
|
||||
│ └── db/init/001_extensions.sql # CREATE EXTENSION pgcrypto, pg_trgm
|
||||
├── docs/ # NOCH LEER (folgt Phase F)
|
||||
└── android/ # NOCH LEER (folgt Phase D)
|
||||
├── docs/
|
||||
│ ├── ARCHITECTURE.md
|
||||
│ ├── SYNC.md # HLC/LWW/op_log
|
||||
│ └── API.md # REST Specs (inkl. join/invite)
|
||||
└── android/ # Single-Module :app (Compose M3, Room, Hilt)
|
||||
└── app/src/main/java/com/example/mitbringsl/
|
||||
├── data/auth/ # SessionManager (Local-User-Fallback, Server-URL)
|
||||
├── data/local/ # Room DB (lists, items, op_log), DAOs (LWW upsert)
|
||||
├── data/remote/ # Retrofit-API, DTOs, DynamicBaseUrlInterceptor, AuthInterceptor
|
||||
├── data/repository/ # ShoppingRepository, AuthRepository
|
||||
├── data/sync/ # HybridLogicalClock, SyncManager, SyncWorker
|
||||
├── di/ # Hilt Modules
|
||||
├── ui/auth/ # AuthScreen (Login/Register/OIDC/Server-URL)
|
||||
├── ui/lists/ # ListsScreen/ViewModel (Join-Dialog, Offline-Banner)
|
||||
├── ui/detail/ # ListDetailScreen/ViewModel (Share-Button, Autocomplete)
|
||||
└── util/ # NetworkMonitor
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -171,19 +202,36 @@ Legende: ✅ erledigt · 🚧 in Arbeit · ⬜ offen
|
|||
- ✅ **Phase E – SyncEngine:** `HybridLogicalClock` (client-seitig), `SyncWorker` (`CoroutineWorker` Outbox Drain + Server Cursor Pull), `SyncManager` (15 min periodisch + Sofort-Sync), `ShoppingRepository` (local-first mutations via Room + `op_log`).
|
||||
- ✅ **Phase E – Listen-Übersicht + Detail + AddItemBar:** `ListsScreen` & `ListsViewModel`, `ListDetailScreen` & `ListDetailViewModel` (Sectioning erledigt/offen, Autocomplete Suggestions dropdown), Compose Navigation 3.
|
||||
- ✅ **Phase F – Polish:** `NetworkMonitor` (ConnectivityManager StateFlow), Offline-Banner in `ListsScreen`, Material 3 Empty States.
|
||||
- ✅ **Phase F – README + docs:** `README.md` Quickstart, `docs/ARCHITECTURE.md`, `docs/SYNC.md` (HLC/LWW/op_log), `docs/API.md` (REST Specs).
|
||||
- ✅ **Phase F – README + docs:** `README.md` Quickstart, `docs/ARCHITECTURE.md`,
|
||||
`docs/SYNC.md` (HLC/LWW/op_log), `docs/API.md` (REST Specs).
|
||||
- ✅ **Post-MVP – Account optional:** App startet ohne Login direkt in die Listen
|
||||
(Local-Only-Default), Sync/Account optional.
|
||||
- ✅ **Post-MVP – Self-Hosted Server-URL + Authentik:** Server-URL in der App
|
||||
konfigurierbar (`DynamicBaseUrlInterceptor`), Authentik über Generic-OIDC.
|
||||
- ✅ **Post-MVP – Geteilte Listen:** Invite-Codes (Migration 000002), Join/Invite-
|
||||
Endpoints, Membership-Checks in `GetLists`/`GetList`/Push/Pull.
|
||||
- ✅ **Post-MVP – Tests für Share/Join:** `liststore_test.go` (Store-Level) +
|
||||
`api_test.go` (E2E über HTTP): Invite/Join/Idempotenz/Rollen/Access-Control
|
||||
(Fremde → 404 auf allen Listen-Endpoints), Cross-Member-Sync via op_log,
|
||||
401-Gating aller geschützten Routen. Beide Dateien skippen ohne
|
||||
`TEST_DATABASE_URL` (Docker-Rezept im Dateikopf).
|
||||
|
||||
### Wo genau weitermachen?
|
||||
### Wo genau weitermachen?
|
||||
**Alle Phasen (Phase A bis F) sind vollständig abgeschlossen und verifiziert ✅.**
|
||||
**MVP + Post-MVP-Features sind abgeschlossen.** Das Backend ist feature-complete
|
||||
für den aktuellen Scope. Offene Punkte, geordnet nach Nutzen:
|
||||
|
||||
Erledigt:
|
||||
- ✅ Phase A: Backend-Fundament, Migrationen, Docker, Caddy Setup.
|
||||
- ✅ Phase B: Argon2id Password Auth, Sessions, OIDC (Google/Generic) Verification.
|
||||
- ✅ Phase C: HLC Sync Engine, idempotent `op_log` append, LWW Projections, REST Endpoints & Suggestions.
|
||||
- ✅ Phase D: Android Architecture (Kotlin 2.x, Compose M3, Room LWW DAOs, Retrofit API, Hilt DI).
|
||||
- ✅ Phase E: Local-First `ShoppingRepository`, WorkManager `SyncWorker` & `SyncManager`, Full UI (Auth, Lists, Detail mit Autocomplete).
|
||||
- ✅ Phase F: `NetworkMonitor` Offline-Indicator, Dokumentation (`ARCHITECTURE.md`, `SYNC.md`, `API.md`, `README.md`).
|
||||
1. **Join-Rate-Limiting:** `POST /api/lists/join` hat kein Rate-Limit; 8-Zeichen-
|
||||
Code ist brute-force-bar (62⁸ ≈ 2×10¹⁴, aber trotzdem). Z.B. pro Session/IP
|
||||
drosseln oder fehlerhafte Joins verzögern.
|
||||
2. **Leave-List / Member-Removal / Code-Revocation:** Es gibt keinen Endpoint,
|
||||
eine Liste zu verlassen, Mitglieder zu entfernen oder einen Invite-Code zu
|
||||
rotieren. `list_members`-Rollen (`owner`/`member`) werden bisher kaum genutzt.
|
||||
3. **Invite-Code-Optimierung:** Codes sind UUID-Präfixe (`uuid[:8]`), nicht
|
||||
kollisionsresistent geprüft (UNIQUE-Constraint fängt es, aber CreateList kann
|
||||
dann fehlschlagen). Besser: kryptografisches Alphabet ohne Verwechslungsbuchstaben.
|
||||
4. **Echtzeit-Push (langfristig):** Periodic-Pull 15 min ist MVP; SSE/WebSocket
|
||||
für sofortige Updates wäre der nächste Schritt.
|
||||
5. **Android-Tests:** UI/ViewModel-Tests fehlen fast komplett (nur 2 Stock-Tests).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -213,19 +261,27 @@ Erledigt:
|
|||
# Backend lokal bauen
|
||||
cd backend && go build ./... && go vet ./...
|
||||
|
||||
# Unit-Tests (ohne DB)
|
||||
cd backend && go test ./...
|
||||
|
||||
# Integrationstests (Store + E2E-HTTP) gegen Docker-PostgreSQL:
|
||||
docker run -d --name mitbringsl-test-pg -e POSTGRES_USER=app \
|
||||
-e POSTGRES_PASSWORD=testpw -e POSTGRES_DB=appdb -p 55432:5432 postgres:16-alpine
|
||||
cd backend && TEST_DATABASE_URL="postgres://app:testpw@localhost:55432/appdb?sslmode=disable" \
|
||||
go test ./...
|
||||
docker rm -f mitbringsl-test-pg # danach aufräumen
|
||||
|
||||
# Backend-Image bauen
|
||||
cd backend && docker build -t mitbringsl-backend:test .
|
||||
|
||||
# Komplettes Stack starten (braucht deploy/.env)
|
||||
cd deploy && cp .env.example .env && docker compose up -d --build
|
||||
|
||||
# Migrationen manuell gegen bestehende DB anwenden
|
||||
docker run --rm --network <net> \
|
||||
-e DATABASE_URL="postgres://app:PW@<db-host>:5432/appdb?sslmode=disable" \
|
||||
mitbringsl-backend:test /app/migrate up
|
||||
# Android bauen
|
||||
cd android && ./gradlew assembleDebug && ./gradlew test
|
||||
```
|
||||
|
||||
## Git-Status
|
||||
- Repo initialisiert, Branch `main`. Remote ist konfiguriert (`origin`).
|
||||
- Phase A + Phase B + Phase C + Phase D + Phase E + Phase F committed und gepusht.
|
||||
- **Projekt Mitbringsl MVP ist vollständig abgeschlossen ✅.**
|
||||
- Branch `main`, Remote `origin` konfiguriert (git.dietzlabs.net).
|
||||
- MVP (Phase A–F) + Post-MVP-Features (optionale Accounts, Server-URL-Config,
|
||||
geteilte Listen) committed und gepusht.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue