Loading
feat(managementapi): repository create handler (S17 Phase 1 Step 9)
Implements Step 9 of the S17 Phase 1 plan: the POST /api/v1/{slug}/repositories create handler, replacing the Step 2 placeholder.
- Strict body decode (
DisallowUnknownFields, trailing-data rejection per error case E-2) and full validation before any database access: name rule, format/visibility enums, description length,visibilitydefaultsprivate,kinddefaultshosted. 201with the serialized resource and root-relativeLocation;400 bad_requeston structural violations;422 unprocessable_entityon any non-hostedkind(per the contract) and on the ADR-004 per-format cap;409 conflicton a name collision; detail-free500with the cause logged, cancellations classified out (no log, no write), matching the Step 8 read handlers.Depsgains theCreatorseam next to Step 8's reader; wiring shares oneRepositoryStoreand keeps the fail-fast nil checks and compile-time interface assertions.- Every response-asserting test also validates against
api/openapi/v1.yamlthrough the shared contract helpers; a DB-backed composition test drives POST through the production stores (201, then 409 on the same name).
Size
1,188 changed lines: 317 production (create.go 220, request.go 59, handler.go 22, wiring 16), 871 tests. The reviewable production surface is within the guideline; the overage is test code, per the documented test-first authorship flow.
Spec coverage
Spec: docs/specs/S17-rest-management-api.md
Acceptance criteria
| # | Criterion | Tests |
|---|---|---|
| AC-1 | OpenAPI document defines endpoints and validates in CI | Step 1 (merged); CI-owned. Not this MR. |
| AC-2 | POST creates each format: 201, resource body, Location; three-row transaction with full rollback |
TestCreateHandler_CreatesEachFormat (handler half); transactional insert and rollback owned by Step 6 (repositories_integration_test.go) |
| AC-3 | POST against an existing name returns 409 | TestCreateHandler_NameConflict_Returns409 |
| AC-4 | Non-hosted kind 422; invalid name/format/visibility, over-long description, malformed body 400; omitted visibility defaults to private |
TestCreateHandler_NonHostedKind_Returns422, TestCreateHandler_Validation_Returns400, TestCreateHandler_ValidationBoundaries_Accepted, TestCreateHandler_Defaults |
| AC-5 | Per-format cap rejects with 422 | TestCreateHandler_CapReached_Returns422; the handler-supplied ADR-004 value asserted in TestCreateHandler_CreatesEachFormat; cap counting owned by Step 6 |
| AC-6 | format=maven creates; container and npm likewise |
TestCreateHandler_CreatesEachFormat |
| AC-7 | GET detail 200/404, no settings key |
Sibling Step 8 MR. Not this MR. |
| AC-8 | GET list filters, sorts, 400 on invalid params | Sibling Step 8 MR. Not this MR. |
| AC-9 | GET list keyset pagination | Sibling Step 8 MR. Not this MR. |
| AC-10 | PATCH updates, 422 on immutable field | Sibling Step 10 MR. Not this MR. |
| AC-11 | DELETE hard-deletes, 409 with artifacts, 404 on miss | Sibling Step 10 MR. Not this MR. |
| AC-12 | Cross-slug isolation | Create side: TestCreateHandler_ScopesToResolvedNamespace, TestCreateHandler_UnknownSlug_404AndNoStoreCall; read/update/delete arms owned by Steps 8/10 |
| AC-13 | Every error path returns the S01 envelope with request_id |
assertErrorEnvelope asserted in every create-handler error test (400, 404, 409, 422, 500) |
Error cases
| # | Condition | Tests |
|---|---|---|
| E-1 | Slug resolves to no namespace: 404 not_found |
TestCreateHandler_UnknownSlug_404AndNoStoreCall; TestHandler_UnknownSlug_Returns404WithEnvelope (POST row, pre-existing) |
| E-2 | Malformed JSON body: 400 | TestCreateHandler_Validation_Returns400 (malformed, empty, non-object rows) |
| E-3 | Missing/invalid authentication: 401 (S08) | Central auth middleware (S08 stub). Not tested in this MR. |
| E-4 | Not permitted: 403 (S09) | Central authz middleware (S09 stub). Not tested in this MR. |
| E-5 | List invalid format/kind/sort/order/limit: 400 |
Sibling Step 8 MR. Not this MR. |
| E-6 | Create name conflict: 409 conflict |
TestCreateHandler_NameConflict_Returns409 |
| E-7 | Create invalid name, format, visibility, or over-long description: 400 | TestCreateHandler_Validation_Returns400; accepting edges in TestCreateHandler_ValidationBoundaries_Accepted |
| E-8 | Create non-hosted kind: 422 |
TestCreateHandler_NonHostedKind_Returns422 |
| E-9 | Create per-format cap reached: 422 | TestCreateHandler_CapReached_Returns422 |
| E-10 | Detail/Update/Delete repository missing: 404 | Sibling Steps 8/10 MRs. Not this MR. |
| E-11 | Delete with artifacts: 409 | Sibling Step 10 MR. Not this MR. |
| E-12 | Update immutable field: 422 | Sibling Step 10 MR. Not this MR. |
| E-13 | Update invalid field value: 400 | Sibling Step 10 MR. Not this MR. |
| E-14 | Unexpected server failure: 500 | TestCreateHandler_StoreFailure_Returns500, TestCreateHandler_StoreFailure_LogsCause, TestCreateHandler_StoreContractViolations_Return500 |
Security considerations
| # | Concern | Tests |
|---|---|---|
| S-1 | Auth (S08) and authz (S09) on every endpoint | Central middleware stubs, wired outside this handler. Not tested in this MR. |
| S-2 | Existence hiding: unresolved slug is 404 | TestCreateHandler_UnknownSlug_404AndNoStoreCall |
| S-3 | Input validation before any database access; URL-path-safe names | TestCreateHandler_Validation_Returns400 and TestCreateHandler_NonHostedKind_Returns422 (empty creator.inputs() after rejection); path-smuggling row name with path separator |
| S-4 | Injection: Jet builder with bound parameters | Datastore-owned (Step 6). Not tested at this seam. |
| S-5 | Name immutability | Update-side concern (sibling Step 10). Create-side name rules covered under S-3. |
| S-6 | Tenant isolation: queries scoped by resolved namespace_id |
TestCreateHandler_ScopesToResolvedNamespace; query-side scoping owned by Step 6 integration tests |
Related to #172 (closed)