mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-04-11 17:56:21 +00:00
## Summary
Auto-generated OpenAPI 3.0.3 spec endpoint (`/api/spec`) and Swagger UI
(`/api/docs`) for the CoreScope API.
## What
- **`cmd/server/openapi.go`** — Route metadata map
(`routeDescriptions()`) + spec builder that walks the mux router to
generate a complete OpenAPI 3.0.3 spec at runtime. Includes:
- All 47 API endpoints grouped by tag (admin, analytics, channels,
config, nodes, observers, packets)
- Query parameter documentation for key endpoints (packets, nodes,
search, resolve-hops)
- Path parameter extraction from mux `{name}` patterns
- `ApiKeyAuth` security scheme for API-key-protected endpoints
- Swagger UI served as a self-contained HTML page using unpkg CDN
- **`cmd/server/openapi_test.go`** — Tests for spec endpoint (validates
JSON structure, required fields, path count, security schemes,
self-exclusion of `/api/spec` and `/api/docs`), Swagger UI endpoint, and
`extractPathParams` helper.
- **`cmd/server/routes.go`** — Stores router reference on `Server`
struct for spec generation; registers `/api/spec` and `/api/docs`
routes.
## Design Decisions
- **Runtime spec generation** vs static YAML: The spec walks the actual
router, so it can never drift from registered routes. Route metadata
(summaries, descriptions, tags, auth flags) is maintained in a parallel
map — the test enforces minimum path count to catch drift.
- **No external dependencies**: Uses only stdlib + existing gorilla/mux.
Swagger UI loaded from unpkg CDN (no vendored assets).
- **Security tagging**: Auth-protected endpoints (those behind
`requireAPIKey` middleware) are tagged with `security: [{ApiKeyAuth:
[]}]` in the spec, matching the actual middleware configuration.
## Testing
- `go test -run TestOpenAPI` — validates spec structure, field presence,
path count ≥ 20, security schemes
- `go test -run TestSwagger` — validates HTML response with swagger-ui
references
- `go test -run TestExtractPathParams` — unit tests for path parameter
extraction
---------
Co-authored-by: you <you@example.com>
143 lines
3.3 KiB
Go
143 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestOpenAPISpecEndpoint(t *testing.T) {
|
|
_, r := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/spec", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
ct := w.Header().Get("Content-Type")
|
|
if ct != "application/json; charset=utf-8" {
|
|
t.Errorf("unexpected content-type: %s", ct)
|
|
}
|
|
|
|
var spec map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &spec); err != nil {
|
|
t.Fatalf("invalid JSON: %v", err)
|
|
}
|
|
|
|
// Check required OpenAPI fields
|
|
if spec["openapi"] != "3.0.3" {
|
|
t.Errorf("expected openapi 3.0.3, got %v", spec["openapi"])
|
|
}
|
|
|
|
info, ok := spec["info"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("missing info object")
|
|
}
|
|
if info["title"] != "CoreScope API" {
|
|
t.Errorf("unexpected title: %v", info["title"])
|
|
}
|
|
|
|
paths, ok := spec["paths"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("missing paths object")
|
|
}
|
|
|
|
// Should have at least 20 paths
|
|
if len(paths) < 20 {
|
|
t.Errorf("expected at least 20 paths, got %d", len(paths))
|
|
}
|
|
|
|
// Check a known path exists
|
|
if _, ok := paths["/api/nodes"]; !ok {
|
|
t.Error("missing /api/nodes path")
|
|
}
|
|
if _, ok := paths["/api/packets"]; !ok {
|
|
t.Error("missing /api/packets path")
|
|
}
|
|
|
|
// Check tags exist
|
|
tags, ok := spec["tags"].([]interface{})
|
|
if !ok || len(tags) == 0 {
|
|
t.Error("missing or empty tags")
|
|
}
|
|
|
|
// Check security schemes
|
|
components, ok := spec["components"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("missing components")
|
|
}
|
|
schemes, ok := components["securitySchemes"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("missing securitySchemes")
|
|
}
|
|
if _, ok := schemes["ApiKeyAuth"]; !ok {
|
|
t.Error("missing ApiKeyAuth security scheme")
|
|
}
|
|
|
|
// Spec should NOT contain /api/spec or /api/docs (self-referencing)
|
|
if _, ok := paths["/api/spec"]; ok {
|
|
t.Error("/api/spec should not appear in the spec")
|
|
}
|
|
if _, ok := paths["/api/docs"]; ok {
|
|
t.Error("/api/docs should not appear in the spec")
|
|
}
|
|
}
|
|
|
|
func TestSwaggerUIEndpoint(t *testing.T) {
|
|
_, r := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/docs", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
ct := w.Header().Get("Content-Type")
|
|
if ct != "text/html; charset=utf-8" {
|
|
t.Errorf("unexpected content-type: %s", ct)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if len(body) < 100 {
|
|
t.Error("response too short for Swagger UI HTML")
|
|
}
|
|
if !strings.Contains(body, "swagger-ui") {
|
|
t.Error("response doesn't contain swagger-ui reference")
|
|
}
|
|
if !strings.Contains(body, "/api/spec") {
|
|
t.Error("response doesn't point to /api/spec")
|
|
}
|
|
}
|
|
|
|
func TestExtractPathParams(t *testing.T) {
|
|
tests := []struct {
|
|
path string
|
|
expect []string
|
|
}{
|
|
{"/api/nodes", nil},
|
|
{"/api/nodes/{pubkey}", []string{"pubkey"}},
|
|
{"/api/channels/{hash}/messages", []string{"hash"}},
|
|
}
|
|
for _, tt := range tests {
|
|
got := extractPathParams(tt.path)
|
|
if len(got) != len(tt.expect) {
|
|
t.Errorf("extractPathParams(%q) = %v, want %v", tt.path, got, tt.expect)
|
|
continue
|
|
}
|
|
for i := range got {
|
|
if got[i] != tt.expect[i] {
|
|
t.Errorf("extractPathParams(%q)[%d] = %q, want %q", tt.path, i, got[i], tt.expect[i])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|