internal/server/api/routes_test.go
Ref: Size: 2.8 KiB History
package api
import (
"net/http"
"reflect"
"testing"
)
// typesPkgPath is where every wire exemplar's element type must live — the
// contract package, and nothing else.
const typesPkgPath = "github.com/a73x/eitri/internal/server/api/types"
// exemplarElem unwraps a table exemplar (typed nil pointer-to-struct or typed
// nil slice) to its element struct type, failing the test on any other shape.
func exemplarElem(t *testing.T, route Route, role string, v any) reflect.Type {
t.Helper()
rt := reflect.TypeOf(v)
switch rt.Kind() {
case reflect.Pointer, reflect.Slice:
elem := rt.Elem()
if elem.Kind() != reflect.Struct {
t.Fatalf("%s %s: %s exemplar %v is not pointer-to-struct or slice-of-struct", route.Method, route.Path, role, rt)
}
return elem
default:
t.Fatalf("%s %s: %s exemplar has kind %v; want typed nil pointer or slice", route.Method, route.Path, role, rt.Kind())
return nil
}
}
// TestRouteTable pins the structural invariants the OpenAPI generator relies
// on: complete entries, unique method+path, exemplars drawn from the contract
// package, and disjoint request/response type sets (the generator emits no
// `required` array for request schemas, so a type serving both roles would
// get the wrong treatment on one of them).
func TestRouteTable(t *testing.T) {
const wantRoutes = 39
if len(routeTable) != wantRoutes {
t.Fatalf("route table has %d entries, want %d — new endpoint? update this pin and cmd/eitri-apispec coverage together", len(routeTable), wantRoutes)
}
seen := make(map[string]bool, len(routeTable))
requestTypes := map[reflect.Type]bool{}
responseTypes := map[reflect.Type]bool{}
for _, rt := range routeTable {
if rt.Method == "" || rt.Path == "" || rt.Doc == "" || rt.handler == nil {
t.Errorf("%s %s: incomplete entry (method/path/doc/handler must all be set)", rt.Method, rt.Path)
}
key := rt.Method + " " + rt.Path
if seen[key] {
t.Errorf("duplicate route %s", key)
}
seen[key] = true
if rt.Request != nil {
elem := exemplarElem(t, rt, "request", rt.Request)
if elem.PkgPath() != typesPkgPath {
t.Errorf("%s: request exemplar %v lives outside the contract package", key, elem)
}
requestTypes[elem] = true
}
if rt.Response != nil {
elem := exemplarElem(t, rt, "response", rt.Response)
if elem.PkgPath() != typesPkgPath {
t.Errorf("%s: response exemplar %v lives outside the contract package", key, elem)
}
responseTypes[elem] = true
}
if rt.Kind == KindJSON && (rt.Success == http.StatusOK || rt.Success == http.StatusCreated) && rt.Response == nil {
t.Errorf("%s: succeeds with %d but declares no response body", key, rt.Success)
}
}
for typ := range requestTypes {
if responseTypes[typ] {
t.Errorf("type %v is used as both request and response exemplar; the sets must stay disjoint", typ)
}
}
}