feat(managementapi): repository list and detail handlers (S17 Phase 1 Step 8)

Implements S17 Phase 1 Step 8 of the repository CRUD plan: the management API read handlers, replacing the Step 2 placeholders for GET /api/v1/{slug}/repositories (list) and GET .../{repository_name} (detail).

  • List: format/kind filters, the five indexed sorts in both directions, limit default 20 / clamp 100, keyset pagination via an opaque cursor and RFC 8288 Link headers (rel="next"/rel="prev", backward paging included), validation before any store access.
  • Detail: name lookup scoped to the resolved namespace, existence-hiding 404.
  • Responses validated against api/openapi/v1.yaml with kin-openapi contract tests; the cursor decoder is fuzzed.
  • docs/dev/api-style.md gains the pagination convention and the now-live contract-test gate, paired with the code that introduces them.

Files beyond the plan's Step 8 entry

  • cmd/artifact-registry/wire_management.go + test: consequence of widening NewHandler(finder) to NewHandler(deps Deps) so Steps 9/10 can add write seams without breaking construction sites; the wire site injects the production RepositoryStore.
  • internal/managementapi/resolve.go: unexported requestNamespaceID/logAndWriteInternalError helpers shared by both new handlers.
  • docs/specs/S17-rest-management-api.md: two companion-table rows aligned with the authoritative OpenAPI document (id is a UUID string, and the invalid-cursor 400 the plan pins is now in the Error Cases table).
  • go.mod/go.sum: github.com/getkin/kin-openapi, plan-sanctioned ("whichever of Step 1/8 lands first").

The pagination fake in fake_repository_reader_test.go mirrors the store's keyset contract; real-store keyset correctness (index backing, gap-free boundaries) is pinned by the Step 5 integration tests (!698 (merged)), and the handler-store composition (the walk through the real keyset SQL) is pinned by the stacked follow-up !791 (merged), which auto-retargets to main when this merges and closes the fake-drift caveat.

Spec coverage

Spec: docs/specs/S17-rest-management-api.md

Acceptance criteria

# Criterion Tests
AC-1 OpenAPI document defines endpoints/resource/enums/envelope, CI-validated Step 1 (merged, !637 (merged)); the CI job owns validation. TestReadHandlers_ResponsesMatchOpenAPIContract re-validates the document on load.
AC-2 POST creates each format atomically, 201 + Location Step 9 MR (create handler). Not this MR.
AC-3 POST duplicate name returns 409 Step 9 MR. Not this MR.
AC-4 POST validation 400 / non-hosted 422 / default visibility Step 9 MR. Not this MR.
AC-5 POST per-format cap 422 Step 9 MR. Not this MR.
AC-6 POST format=maven works Step 9 MR. Not this MR.
AC-7 GET detail 200 existing / 404 missing, no settings key TestDetailHandler_ExistingRepository_Returns200, TestDetailHandler_MissingRepository_Returns404, TestReadHandlers_ResponsesMatchOpenAPIContract
AC-8 GET list filters format/kind, sorts each column both directions, 400 on unrecognized params TestListHandler_ParamMapping (every wire enum to its store param), TestListHandler_InvalidParams_Return400; the executed ordering per column/direction is store-owned, pinned by Step 5's repositories_integration_test.go
AC-9 Keyset pagination: next link, no dups/gaps, final page omits next, limit capped TestListHandler_KeysetWalk, TestListHandler_KeysetWalk_PreservesFilter, TestListHandler_PrevLink, TestListHandler_BackwardWalk_Links (backward Link emission, asc and desc, keyset symmetry), TestListHandler_CursorPageAfterDeletion, TestListHandler_LastUpdatedAtSort_CursorCoalescesToCreatedAt, TestListHandler_LimitBounds, TestReadHandlers_PaginatedResponseMatchesContract
AC-10 PATCH updates, 422 on immutable field Step 10 MR. Not this MR.
AC-11 DELETE hard-deletes / 409 with artifacts / 404 missing Step 10 MR. Not this MR.
AC-12 Cross-slug: absent from other slug's list, 404 on detail/update/delete List: TestListHandler_TenantScoping; detail: TestDetailHandler_CrossSlug_Returns404. Update/delete halves: Step 10 MR.
AC-13 Every error path returns the S01 envelope with request_id assertErrorEnvelope in every error-path test (400/404/500), TestReadHandlers_ResponsesMatchOpenAPIContract error rows; slug-404/finder-500 rows pre-exist in handler_test.go

Error cases

# Condition Tests
E-1 Slug resolves to no namespace: 404 not_found TestHandler_UnknownSlug_Returns404WithEnvelope (pre-existing, Step 2), TestReadHandlers_ResponsesMatchOpenAPIContract/detail_404_unknown_slug
E-2 Malformed JSON body: 400 Steps 9/10 MRs (the read endpoints take no body). Not this MR.
E-3 Auth missing/invalid: 401 (S08) S08 stub middleware-owned, outside this handler. Not tested in this MR.
E-4 Authenticated but not permitted: 403 (S09) S09 stub middleware-owned, outside this handler. Not tested in this MR.
E-5 List: invalid format/kind/sort/order/limit: 400 TestListHandler_InvalidParams_Return400; cursor rejections additionally pinned by TestDecodeListCursor_Rejects and FuzzDecodeListCursor
E-6 Create: name conflict 409 Step 9 MR. Not this MR.
E-7 Create: invalid fields 400 Step 9 MR. Not this MR.
E-8 Create: non-hosted kind 422 Step 9 MR. Not this MR.
E-9 Create: cap reached 422 Step 9 MR. Not this MR.
E-10 Detail/Update/Delete: repository missing 404 Detail: TestDetailHandler_MissingRepository_Returns404, TestDetailHandler_CrossSlug_Returns404. Update/delete: Step 10 MR.
E-11 Delete: repository still has artifacts 409 Step 10 MR. Not this MR.
E-12 Update: immutable field 422 Step 10 MR. Not this MR.
E-13 Update: invalid field value 400 Step 10 MR. Not this MR.
E-14 Unexpected server failure 500 TestListHandler_StoreFailure_Returns500, TestDetailHandler_StoreFailure_Returns500, TestDetailHandler_UnknownEnumRow_Returns500; finder-500 rows pre-exist in handler_test.go

Security considerations

# Concern Tests
S-1 Authentication and authorization (S08/S09 stubs) Middleware-owned (wired outside this handler). Not tested in this MR.
S-2 Existence hiding: invisible resources return 404 TestDetailHandler_MissingRepository_Returns404, TestDetailHandler_CrossSlug_Returns404, TestHandler_UnknownSlug_Returns404WithEnvelope (pre-existing)
S-3 Input validation before any database access TestListHandler_InvalidParams_Return400 (asserts zero store calls on 400). Write-body validation: Steps 9/10 MRs.
S-4 Injection: Jet builder with bound parameters Datastore-owned; Step 5's repositories_integration_test.go exercises the built queries. Not a handler concern.
S-5 Name immutability Steps 9/10 MRs (write surface). Not this MR.
S-6 Tenant isolation: every query scoped by namespace_id TestListHandler_TenantScoping, TestDetailHandler_CrossSlug_Returns404, plus the recorded-params namespace assertions in TestListHandler_Defaults and TestDetailHandler_ExistingRepository_Returns200

Size

+2,628/−132 across 17 files, over the 500 LOC guideline, so per that rule here is the split and the justification.

Category Added Deleted Share of adds
Production code 678 47 26%
Tests 1,895 79 72%
Docs (dev guide, spec, plan row) 35 6 1%
Dependencies (go.mod/go.sum) 20 0 <1%

The reviewable production surface is smaller than the raw count suggests: the two new handlers hold ~325 executable lines (list.go: 285 code / 113 comment / 79 blank; detail.go: 40 code), with the remainder being doc comments per house style and the NewHandler(deps) wiring change. The bulk of the diff is tests: a store-shaped pagination fake (363), the one-time kin-openapi contract-test gate that Steps 9/10 extend rather than rebuild (229), and table-driven pagination walks.

Why not split: Step 8 is the plan's approved MR unit, and list + detail share the serializer, slug resolution, and error conventions. Splitting detail out would produce a ~100-line second MR that re-reviews the same seams. The irreducible review core is the keyset pagination logic in list.go; everything around it exists to pin that logic down.

Related to #171 (closed)

Edited by João Pereira

Merge request reports

Loading
Loading