Add the update group endpoint, owner-only #49

Merged
rob merged 1 commit from feat/update-group-endpoint into main 2026-08-05 02:46:47 +00:00
Owner

Closes task 76. PUT /api/groups/{groupId} renames a group, gated by the existing GroupOwner policy via RequireGroupCapability — Editor/Viewer get 403, non-member/nonexistent group get 404 (ADR-0041).

No new ADR: this follows ADR-0041/0043's existing shape with no new architectural decision.

Scope note: the ticket text says "edits a group's name and description", but groups has no description column (only places does — see docs/data-model.md), and UpdateGroupRequest/GroupResponse/the Group entity already existed with Name only, from earlier tickets. I've treated that as stale wording carried over from a template rather than a real requirement, and implemented name-only. Adding a description column is a schema change nobody has decided; flagging it here rather than inventing it.

personal_for_user_id: GroupRepository.UpdateAsync's SET clause only ever writes name and updated_at — there's no way to move personal_for_user_id through this path, over-posting or not. Covered by an integration test that over-posts the field on a personal group's rename and asserts it's unchanged in the database.

Concurrency: no optimistic concurrency check — last write wins, one UPDATE ... RETURNING statement. Acceptable at ADR-0007's hobby scale; documented on GroupRepository.UpdateAsync.

Tests: Owner success (200, updated_at advances), personal group rename with personal_for_user_id unchanged, Editor 403, Viewer 403, non-member 404, nonexistent group 404, blank name 400, unauthenticated 401 — all against a real Postgres via WebApplicationFactory + Testcontainers.

Closes task 76. `PUT /api/groups/{groupId}` renames a group, gated by the existing `GroupOwner` policy via `RequireGroupCapability` — Editor/Viewer get 403, non-member/nonexistent group get 404 (ADR-0041). No new ADR: this follows ADR-0041/0043's existing shape with no new architectural decision. **Scope note:** the ticket text says "edits a group's name and description", but `groups` has no `description` column (only `places` does — see `docs/data-model.md`), and `UpdateGroupRequest`/`GroupResponse`/the `Group` entity already existed with `Name` only, from earlier tickets. I've treated that as stale wording carried over from a template rather than a real requirement, and implemented name-only. Adding a description column is a schema change nobody has decided; flagging it here rather than inventing it. **personal_for_user_id**: `GroupRepository.UpdateAsync`'s `SET` clause only ever writes `name` and `updated_at` — there's no way to move `personal_for_user_id` through this path, over-posting or not. Covered by an integration test that over-posts the field on a personal group's rename and asserts it's unchanged in the database. **Concurrency**: no optimistic concurrency check — last write wins, one `UPDATE ... RETURNING` statement. Acceptable at ADR-0007's hobby scale; documented on `GroupRepository.UpdateAsync`. Tests: Owner success (200, `updated_at` advances), personal group rename with `personal_for_user_id` unchanged, Editor 403, Viewer 403, non-member 404, nonexistent group 404, blank name 400, unauthenticated 401 — all against a real Postgres via `WebApplicationFactory` + Testcontainers.
Add the update group endpoint, owner-only
All checks were successful
CI / build (pull_request) Successful in 2m6s
b046844843
rob left a comment

Verdict: mergeable

Verified: personal_for_user_id is structurally unwritable through this path — UpdateGroupName's SET clause and its parameter object carry no such column/property, independent of the unique index; the over-post test proves it end to end. Mutating the route policy to GroupCapability.Member reddens both the Editor and Viewer 403 tests. 404 vs 403 split matches ADR-0041. UPDATE ... RETURNING is a single atomic statement returning post-update values, confirmed by the advancing updated_at assertion. Ran two concurrent UpdateAsync calls against the same row directly: both complete, the persisted row matches whichever call's own return value, no lost update. Scope is rename-only, no schema script, no other endpoints touched. GroupResponse/UpdateGroupRequest are hand-shaped contracts, no entity on the wire.

Verdict: mergeable Verified: `personal_for_user_id` is structurally unwritable through this path — `UpdateGroupName`'s `SET` clause and its parameter object carry no such column/property, independent of the unique index; the over-post test proves it end to end. Mutating the route policy to `GroupCapability.Member` reddens both the Editor and Viewer 403 tests. 404 vs 403 split matches ADR-0041. `UPDATE ... RETURNING` is a single atomic statement returning post-update values, confirmed by the advancing `updated_at` assertion. Ran two concurrent `UpdateAsync` calls against the same row directly: both complete, the persisted row matches whichever call's own return value, no lost update. Scope is rename-only, no schema script, no other endpoints touched. `GroupResponse`/`UpdateGroupRequest` are hand-shaped contracts, no entity on the wire.
rob merged commit 6823117b29 into main 2026-08-05 02:46:47 +00:00
rob deleted branch feat/update-group-endpoint 2026-08-05 02:46:48 +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!49
No description provided.