Add list pending invitations endpoint #56

Merged
rob merged 1 commit from feat/list-invitations-endpoint into main 2026-08-05 05:04:03 +00:00
Owner

Implements task 83: GET /api/users/me/invitations, paginated, scoped to the caller's own pending invitations.

  • Scoped by caller id in SQL (GroupMembershipRepository.ListPendingForUserAsync), not filtered in memory.
  • Each invitation carries the group's name and the inviter's display name (never email, ADR-0046) — LEFT JOIN on the inviter, since invited_by_user_id is ON DELETE SET NULL.
  • Ordered by created_at, group_id for a stable total ordering, following GroupRepository.ListForUserAsync's own shape.
  • No ADR-0041 policy applies — the caller's own id is the scope, same as GET /api/groups. This route lives under /api/users/me, not /api/groups/{groupId}, so task 148's GroupScopedEndpointAuthorisationTests enumeration never matches it in the first place; no carve-out needed.

No new ADR: this follows the shape ADR-0041/0044/0046/0051 already settled rather than deciding anything new.

Implements task 83: `GET /api/users/me/invitations`, paginated, scoped to the caller's own pending invitations. - Scoped by caller id in SQL (`GroupMembershipRepository.ListPendingForUserAsync`), not filtered in memory. - Each invitation carries the group's name and the inviter's display name (never email, ADR-0046) — `LEFT JOIN` on the inviter, since `invited_by_user_id` is `ON DELETE SET NULL`. - Ordered by `created_at, group_id` for a stable total ordering, following `GroupRepository.ListForUserAsync`'s own shape. - No ADR-0041 policy applies — the caller's own id is the scope, same as `GET /api/groups`. This route lives under `/api/users/me`, not `/api/groups/{groupId}`, so task 148's `GroupScopedEndpointAuthorisationTests` enumeration never matches it in the first place; no carve-out needed. No new ADR: this follows the shape ADR-0041/0044/0046/0051 already settled rather than deciding anything new.
Add list pending invitations endpoint
All checks were successful
CI / build (pull_request) Successful in 2m7s
e98b56a118
rob left a comment

Verdict: mergeable

Verified against the pinned SDK (10.0.100), real PostgreSQL via Testcontainers, and mutation testing:

  • Caller scoping: removing gm.user_id = @UserId from SelectPendingInvitationsForUser's WHERE reddens 4 tests, including ListPendingForUserAsync_AnotherUsersInvitation_NeverAppears.
  • Pending-only filter: removing gm.status = 'pending' reddens exactly ListPendingForUserAsync_AnAcceptedMembership_IsNotAnInvitation.
  • Deleted-inviter case: swapping the inviter LEFT JOIN for INNER JOIN reddens exactly ListPendingForUserAsync_TheInvitersAccountHasBeenDeleted_StillListsItWithNoInviterDisplayName — an INNER JOIN would silently drop the invitation, confirming the LEFT JOIN is load-bearing, not decorative.
  • No email on the wire: asserted directly against the raw JSON body (not just the DTO), both for the literal address and the substring "email" — passes.
  • Stable ordering: ORDER BY gm.created_at, gm.group_id proven with three rows sharing one created_at, paged 2+1 with no row skipped or duplicated.
  • Task 148's safety net claim checked directly: GroupScopedEndpoints() matches only /api/groups/{groupId}... and /api/places/...; /api/users/me/invitations matches neither predicate, so the "no carve-out needed" claim is accurate.
  • OpenApiDocumentTests path list addition is a real assertion, not a silenced failure — the full path list is asserted, so this would fail without the new entry.
  • Pagination bounds match ADR-0044 (Page ≥ 1, PageSize 1–50, defaults 1/20, refused not clamped).
  • main hasn't moved (still 707201a, PR56's merge base); the sibling feat/change-member-role-endpoint hasn't merged, so no conflict to resolve here.

Nothing to change.

Verdict: mergeable Verified against the pinned SDK (10.0.100), real PostgreSQL via Testcontainers, and mutation testing: - Caller scoping: removing `gm.user_id = @UserId` from `SelectPendingInvitationsForUser`'s `WHERE` reddens 4 tests, including `ListPendingForUserAsync_AnotherUsersInvitation_NeverAppears`. - Pending-only filter: removing `gm.status = 'pending'` reddens exactly `ListPendingForUserAsync_AnAcceptedMembership_IsNotAnInvitation`. - Deleted-inviter case: swapping the inviter `LEFT JOIN` for `INNER JOIN` reddens exactly `ListPendingForUserAsync_TheInvitersAccountHasBeenDeleted_StillListsItWithNoInviterDisplayName` — an `INNER JOIN` would silently drop the invitation, confirming the `LEFT JOIN` is load-bearing, not decorative. - No email on the wire: asserted directly against the raw JSON body (not just the DTO), both for the literal address and the substring "email" — passes. - Stable ordering: `ORDER BY gm.created_at, gm.group_id` proven with three rows sharing one `created_at`, paged 2+1 with no row skipped or duplicated. - Task 148's safety net claim checked directly: `GroupScopedEndpoints()` matches only `/api/groups/{groupId}...` and `/api/places/...`; `/api/users/me/invitations` matches neither predicate, so the "no carve-out needed" claim is accurate. - `OpenApiDocumentTests` path list addition is a real assertion, not a silenced failure — the full path list is asserted, so this would fail without the new entry. - Pagination bounds match ADR-0044 (`Page` ≥ 1, `PageSize` 1–50, defaults 1/20, refused not clamped). - `main` hasn't moved (still `707201a`, PR56's merge base); the sibling `feat/change-member-role-endpoint` hasn't merged, so no conflict to resolve here. Nothing to change.
rob merged commit 44ce5586da into main 2026-08-05 05:04:03 +00:00
rob deleted branch feat/list-invitations-endpoint 2026-08-05 05:04:03 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rob/PlaceMark!56
No description provided.