Generate the OpenAPI spec with Huma instead of swag
CI / integration (push) Successful in 49s
CI / docker (push) Successful in 1m0s
CI / test (push) Successful in 1m6s
CI / publish (push) Skipped

The spec is now built from the same calls that register the routes, so it
can't drift from the router like the swag comments did (all /api/v1
routes were missing).

- /api/v1 endpoints are typed Huma operations; the spec is OpenAPI 3.1 at
  /openapi.json (3.0 at /openapi-3.0.json, YAML too), docs at /docs.
  /openapi and /swagger/* redirect there
- Bearer token security is declared per operation; token creation and
  the new GET /api/v1/health are public
- Legacy routes use the same handlers, are hidden from the spec and keep
  the Deprecation header
- /sync/reset is hidden (debug only); the /sync/new, /sync/quick and
  /sync/new/full aliases are removed (use /sync and /sync/full)
- Invalid parameters return 422 (validation); every error is
  {"error": "..."}; empty lists are [] instead of null
- /sync/progress is documented as progress or result (oneOf)
- Tests validate the spec and check real responses of every documented
  operation against it (kin-openapi)
- Removed swag annotations, cmd/docs, echo-swagger and the swag tool
This commit is contained in:
Sansan committed 2026-10-06 16:31:06 +02:00
1 parent 0ec2425f78
commit 973ebf224b
37 files changed
+1245 -5967

No files matched your search

+24 -12
View File
@@ -28,7 +28,7 @@ func TestDestructiveSyncEndpointsIgnoreGet(t *testing.T) {
e := newTestEnv(t)
e.addSong("Game A", "Song", "a")
for _, prefix := range []string{"", "/api/v1"} {
for _, path := range []string{"/sync", "/sync/new", "/sync/quick", "/sync/full", "/sync/new/full", "/sync/reset"} {
for _, path := range []string{"/sync", "/sync/full", "/sync/reset"} {
// GET falls through to the frontend's static files, which answer 404.
assert.Equal(t, http.StatusNotFound, e.do(http.MethodGet, prefix+path).Code, prefix+path)
}
@@ -99,21 +99,33 @@ func TestHealthEndpoint(t *testing.T) {
assert.JSONEq(t, `{"status":"up"}`, rec.Body.String())
}
func TestOpenAPIAndSwagger(t *testing.T) {
func TestSyncAliasesAreRemoved(t *testing.T) {
e := newTestEnv(t)
for _, path := range []string{"/openapi", "/swagger/doc.json"} {
rec := e.do(http.MethodGet, path)
require.Equal(t, http.StatusOK, rec.Code, path)
var spec struct {
Paths map[string]map[string]any `json:"paths"`
for _, prefix := range []string{"", "/api/v1"} {
for _, path := range []string{"/sync/new", "/sync/quick", "/sync/new/full"} {
assert.NotEqual(t, http.StatusOK, e.do(http.MethodPost, prefix+path).Code, prefix+path)
}
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &spec), path)
assert.Contains(t, spec.Paths, "/api/v1/token", path)
assert.Contains(t, spec.Paths["/sync/reset"], "post", path)
}
assert.False(t, e.server.sync.IsSyncing())
}
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/swagger/index.html").Code)
func TestV1HealthEndpoint(t *testing.T) {
e := newTestEnv(t)
rec := e.do(http.MethodGet, "/api/v1/health")
assert.Equal(t, http.StatusOK, rec.Code)
assert.JSONEq(t, `{"status":"up"}`, rec.Body.String())
}
func TestDocsAndSpecLinks(t *testing.T) {
e := newTestEnv(t)
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/docs").Code)
for _, path := range []string{"/openapi.json", "/openapi.yaml", "/openapi-3.0.json"} {
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, path).Code, path)
}
rec := e.do(http.MethodGet, "/openapi")
assert.Equal(t, http.StatusMovedPermanently, rec.Code)
assert.Equal(t, "/openapi.json", rec.Header().Get("Location"))
assert.Equal(t, "/docs", e.do(http.MethodGet, "/swagger/index.html").Header().Get("Location"))
}
func TestSearchPageAndFrontend(t *testing.T) {