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).
4.4 KiB
Mitbringsl – REST API Specification
Base URL: /
Authentication: Bearer Token via Authorization: Bearer <session_token> header or session cookie.
Health & Diagnostics
GET /healthz
Liveness probe. Returns HTTP 200 OK.
GET /readyz
Readiness probe. Checks PostgreSQL connection pool health.
- 200 OK: Database reachable.
- 503 Service Unavailable: Database error.
Authentication (/auth)
POST /auth/register
Create a new email/password account.
- Request:
{ "email": "user@example.com", "password": "secretpassword", "display_name": "Max" } - Response (201 Created):
{ "token": "<opaque_session_token>", "expires_at": "2026-09-04T20:00:00Z", "user": { "id": "<uuid>", "email": "user@example.com", "display_name": "Max" } }
POST /auth/login
Authenticate with email/password.
- Request:
{ "email": "user@example.com", "password": "secretpassword" } - Response (200 OK): Same shape as register.
POST /auth/oidc
Authenticate via Google or custom OpenID Connect provider.
- Request:
{ "provider": "google", "id_token": "<jwt>" } - Response (200 OK): Same shape as login.
POST /auth/logout
Revoke active session token. Returns 204 No Content.
Lists (/api/lists)
All list endpoints return 404 Not Found when the authenticated user is neither
owner nor member of the list (no membership leaks), and 401 without a valid
session. List responses carry an invite_code (8 characters) for sharing.
GET /api/lists
Fetch all active (non-deleted) lists the authenticated user owns or joined as a member.
- Response (200 OK):
{ "lists": [ { "id": "<uuid>", "name": "Wocheneinkauf", "invite_code": "A1B2C3D4", "updated_at": "2026-08-05T19:00:00Z", "hlc_ts": 177000000000000 } ] }
POST /api/lists
Create a new list. The owner is automatically added to list_members (role
owner) and an invite code is generated.
- Request:
{ "name": "Supermarkt" } - Response (201 Created):
{ "id": "<uuid>", "name": "Supermarkt", "invite_code": "A1B2C3D4", ... }
GET /api/lists/{id}
Fetch list detail including items (owner or member only).
- Response (200 OK):
{ "id": "<uuid>", "name": "Wocheneinkauf", "invite_code": "A1B2C3D4", "updated_at": "...", "hlc_ts": 177000000000000, "items": [ { "id": "<uuid>", "name": "Milch", "quantity": "1L", "checked": false, "hlc_ts": 177000000000001 } ] }
POST /api/lists/{id}/invite
Return the invite code of a list (owner or member only). Generates a code on the fly for legacy lists without one.
- Request: empty JSON object
{} - Response (200 OK):
{ "invite_code": "A1B2C3D4" }
POST /api/lists/join
Join a shared list using its invite code (case-insensitive). Joining again is idempotent; the existing membership role is kept (an owner cannot be demoted).
- Request:
{ "invite_code": "a1b2c3d4" } - Response (200 OK): the joined list (same shape as
GET /api/listsitems). - 400 Bad Request: unknown or empty invite code.
Sync & Ops (/api/lists/{id}/ops)
Both endpoints require membership (owner or member); otherwise 404 Not Found.
POST /api/lists/{id}/ops
Push a batch of client operations (max 100).
- Request:
{ "client_id": "<client_uuid>", "ops": [ { "client_seq": 1, "op_type": "item_add", "target_id": "<item_uuid>", "hlc_ts": 177000000000000, "payload": { "name": "Brot", "quantity": "1 Stück" } } ] } - Response (200 OK):
{ "results": [ { "client_seq": 1, "seq": 42, "hlc_ts": 177000000000001 } ] }
GET /api/lists/{id}/ops?since={seq}
Pull operations since server sequence since.
- Response (200 OK):
{ "ops": [ { "seq": 42, "client_id": "<client_uuid>", "op_type": "item_add", "target_id": "<item_uuid>", "payload": { "name": "Brot" }, "client_seq": 1, "hlc_ts": 177000000000001, "created_at": "2026-08-05T20:00:00Z" } ], "has_more": false }
Suggestions (/api/suggestions)
GET /api/suggestions?q={query}
Fuzzy autocomplete search for item names (pg_trgm).
- Response (200 OK):
{ "suggestions": ["milch", "mineralwasser", "müsli"] }