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:
Tronax 2026-08-22 09:40:13 +02:00
parent 85c790ed23
commit 67033e561c
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
7 changed files with 692 additions and 33 deletions

View file

@ -56,3 +56,24 @@ All state modifications are represented as structured operations:
Each operation carries a `(client_id, client_seq)` tuple enforced by a `UNIQUE` constraint in Postgres (`op_log`). Re-sent requests return previous `(seq, hlc_ts)` assignments without duplicating side effects.
- **Cursor Pull (`GET /api/lists/{id}/ops?since={seq}`)**:
Clients track `max(server_seq)` locally. Incremental sync fetches ops where `seq > cursor`, sorted by monotonic server sequence `seq ASC`.
---
## 6. Shared Lists
Since shared lists were introduced, ops flow between **all members** of a list,
not just its owner:
- **Membership**: A list has an owner (`lists.owner_id`, role `owner`) and any
number of members (`list_members`, role `member`). Members join via an
8-character invite code (`POST /api/lists/join`); owner and members can read
the code (`POST /api/lists/{id}/invite`) to share it.
- **Sync scope**: Push and Pull verify on every request that the caller is owner
or member of the list — non-members receive `404` (not `403`, so membership
cannot be probed). Once joined, a member pulls the full op history
(`?since=0`) and applies the same LWW projection locally.
- **Conflict semantics are unchanged**: With multiple writers the LWW register
`(hlc_ts, client_id)` resolves concurrent edits; the HLC guarantees that ops
from different clients that observed each other are still causally ordered.
Concurrent edits to the same item converge to the highest `hlc_ts` on every
device — last write wins, no data merge.