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, visibility defaults private, kind defaults hosted.
  • 201 with the serialized resource and root-relative Location; 400 bad_request on structural violations; 422 unprocessable_entity on any non-hosted kind (per the contract) and on the ADR-004 per-format cap; 409 conflict on a name collision; detail-free 500 with the cause logged, cancellations classified out (no log, no write), matching the Step 8 read handlers.
  • Deps gains the Creator seam next to Step 8's reader; wiring shares one RepositoryStore and keeps the fail-fast nil checks and compile-time interface assertions.
  • Every response-asserting test also validates against api/openapi/v1.yaml through 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)

Merge request reports

Loading
Loading