SCRUM-211 — Config: namespaces list and create¶
Plan ref: CFG-B1 (docs/11-admin-plane-plan.md). First real Config handler; also
lands the handler seam every later Config ticket plugs into.
What changed¶
| Change | Where |
|---|---|
Handler seam: api.Handlers{Store, Log}.For(route) returns the implemented handler for a route pattern, or the 501 NotImplemented stub. server.Deps.Handlers wired in main. A nil Handlers keeps every route at 501, so existing tests are unchanged |
internal/api/handlers.go, internal/server/server.go, config.go |
GET /api/admin/config/namespaces (viewer) → {"namespaces":[…]} sorted by name, never null |
internal/api/namespaces.go, internal/store/namespaces.go |
POST /api/admin/config/namespaces (admin) → 201 item. One transaction: namespace row + schema v1 {} + empty draft (revision 1) + audit namespace.create |
same |
Optional name claim on staff tokens; Claims.ActorName() = name, else sub (used for audit_log.actor_name) |
internal/auth/claims.go |
Item shape (matches doc 10 §9 row 6):
{"name":"balance.weapons","audience":"client","description":"weapon balance",
"latest_version":null,"created_at":"…",
"draft":{"revision":1,"updated_at":"…","has_unpublished_changes":false}}
has_unpublished_changes: no version yet → revision > 1; otherwise the draft body
differs (jsonb equality) from the latest version's body.
POST validation (400 validation_failed): name required, ≤64,
^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$; audience ∈ client|server;
description ≤500 chars. Malformed JSON, unknown field, trailing data, >64 KiB →
400 invalid_body. Duplicate name → 409 already_exists. DB errors → 500
internal_error (cause logged with request_id, never returned).
How to verify¶
cd services/config
go vet ./... && go test -race -count=1 ./... # DB tests skip without the env var
# DB-backed (local Postgres; see handoff/pg-env.sh on the dev box, or any PG 16+)
export CONFIG_TEST_DATABASE_URL='postgres://USER:PASS@127.0.0.1:5433/config_test?sslmode=disable'
go test -race -count=1 -v -run Namespace ./internal/store/ ./internal/server/
| Test | Proves |
|---|---|
api.TestValidateCreateNamespace |
every field rule, both sides of each boundary |
api.TestCreateNamespaceRejectsBadBodies |
unknown field, trailing data, >64 KiB, not JSON → 400 invalid_body |
api.TestNilHandlersAnswer501, TestForFallsBackToNotImplemented |
unimplemented routes stay 501 |
store.TestCreateNamespaceThenList |
create → list shows it with latest_version nil, revision 1, no unpublished changes, exactly one namespace.create audit row |
store.TestCreateNamespaceDuplicate |
second create → ErrNamespaceExists, no second audit row |
server.TestNamespacesRoutesThroughTheLiveServer |
real listener + real JWT guard: viewer GET 200, live_ops POST 403, admin POST 201, then visible in GET |
By hand against a running Config (staff token in $T, admin role):
curl -s -H "Authorization: Bearer $T" localhost:8080/api/admin/config/namespaces
curl -s -H "Authorization: Bearer $T" -d '{"name":"ui.motd","audience":"client"}' \
localhost:8080/api/admin/config/namespaces # 201
# again → 409 already_exists; {"name":"Bad Name","audience":"client"} → 400
Results at time of writing¶
go vet,go test -race ./...: pass (6 packages).- DB-backed namespace tests against local Postgres 16: 4/4 pass.
Follow-ups¶
- SCRUM-199 (admin-auth signing) should emit a
nameclaim so audit rows show a human name; until thenactor_namefalls back tosub.
How it was built¶
DeepSeek run scoped (Landlock) to services/config (161 s, ~29k output tokens).
Claude review: package-doc comments detached, a misleading comment on For
corrected, "PHP" → admin-auth; tests re-run with -race against Postgres.