CI / test (push) Failing after 6m12s
/v1/openapi.json is the API clients use — OpenAI's endpoints, Anthropic's, and what llmgw adds — in OpenAPI 3.1, generated from internal/clientapi's route table by the generator the admin API's document comes from, now told each document's title, server, token and error. A test reads the router's mux.HandleFunc calls and refuses a route described and not mounted, or mounted and not described. Asked for before there is a key, so it needs none. /docs/api shows it in Swagger UI 5.33.1 (Apache 2.0, served from the gateway as the page's policy requires): Authorize takes a key, Try it out sends to this gateway. Staff are offered the admin API's document too, from /docs/api/admin.json, which nobody else is given. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
77 lines
2.2 KiB
Go
77 lines
2.2 KiB
Go
package clientapi
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// routerRoutes are the client API's routes as the router mounts them, read
|
|
// from its source: every mux.HandleFunc under /v1, /mcp and /me, with the
|
|
// path constants it builds some of them from put back.
|
|
func routerRoutes(t *testing.T) []string {
|
|
t.Helper()
|
|
_, file, _, _ := runtime.Caller(0)
|
|
dir := filepath.Join(filepath.Dir(file), "..", "router")
|
|
consts := map[string]string{"imagesPath": "/v1/images/generations", "meshesPath": "/v1/3d/generations"}
|
|
literal := regexp.MustCompile(`mux\.HandleFunc\("([A-Z]+) (/[^"]*)"`)
|
|
built := regexp.MustCompile(`mux\.HandleFunc\("([A-Z]+) "\+([A-Za-z]+Path)(\+"([^"]*)")?`)
|
|
files, _ := filepath.Glob(filepath.Join(dir, "*.go"))
|
|
var out []string
|
|
add := func(method, path string) {
|
|
if strings.HasPrefix(path, "/v1/") || path == "/mcp" || path == "/me" {
|
|
out = append(out, method+" "+path)
|
|
}
|
|
}
|
|
for _, f := range files {
|
|
if strings.HasSuffix(f, "_test.go") {
|
|
continue
|
|
}
|
|
b, _ := os.ReadFile(f)
|
|
for _, m := range literal.FindAllStringSubmatch(string(b), -1) {
|
|
add(m[1], m[2])
|
|
}
|
|
for _, m := range built.FindAllStringSubmatch(string(b), -1) {
|
|
c, ok := consts[m[2]]
|
|
if !ok {
|
|
t.Fatalf("a route built from %s, which this test does not know", m[2])
|
|
}
|
|
add(m[1], c+m[4])
|
|
}
|
|
}
|
|
slices.Sort(out)
|
|
return slices.Compact(out)
|
|
}
|
|
|
|
func TestEveryClientRouteIsDescribedAndNothingElse(t *testing.T) {
|
|
// An endpoint added to the router and not to this table would be missing
|
|
// from the document every SDK is generated from; one here and not in the
|
|
// router would be a path that answers 404.
|
|
mounted := routerRoutes(t)
|
|
if len(mounted) < 30 {
|
|
t.Fatalf("read only %d routes from the router: %v", len(mounted), mounted)
|
|
}
|
|
var described []string
|
|
for _, r := range Routes {
|
|
described = append(described, r.Method+" "+r.Path)
|
|
}
|
|
slices.Sort(described)
|
|
for _, m := range mounted {
|
|
if m == "GET /v1/openapi.json" {
|
|
continue // the document itself
|
|
}
|
|
if !slices.Contains(described, m) {
|
|
t.Errorf("%s is mounted and not described in clientapi.Routes", m)
|
|
}
|
|
}
|
|
for _, d := range described {
|
|
if !slices.Contains(mounted, d) {
|
|
t.Errorf("%s is described and not mounted", d)
|
|
}
|
|
}
|
|
}
|