181 lines
5.6 KiB
Go
181 lines
5.6 KiB
Go
package api
|
|
|
|
import (
|
|
"os"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The OpenAPI specification is written by hand, so it can drift from the code.
|
|
// This test compares it against the routes the router actually registers, which
|
|
// is the thing the specification claims to describe.
|
|
//
|
|
// The route list below is checked against router.go itself, so a route added
|
|
// there without a corresponding entry here also fails.
|
|
|
|
// specPath is the specification under test.
|
|
const specPath = "../../docs/openapi.yaml"
|
|
|
|
// routerPath is the file whose route registrations are the source of truth.
|
|
const routerPath = "router.go"
|
|
|
|
// apiRoutePattern matches the API routes registered in the router.
|
|
var apiRoutePattern = regexp.MustCompile(`mux\.Handle\("(GET|POST|PATCH|DELETE|PUT) (/api/v1[^"]*|/health/[^"]*)"`)
|
|
|
|
// specPathPattern matches top-level path keys in the specification.
|
|
var specPathPattern = regexp.MustCompile(`(?m)^ (/(?:api/v1|health)[^:]*):`)
|
|
|
|
// specMethodPattern matches an HTTP method under a path.
|
|
var specMethodPattern = regexp.MustCompile(`(?m)^ (get|post|patch|delete|put):`)
|
|
|
|
func readFile(t *testing.T, path string) string {
|
|
t.Helper()
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", path, err)
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
// routerRoutes returns "METHOD /path" for every API route the router registers.
|
|
func routerRoutes(t *testing.T) map[string]bool {
|
|
t.Helper()
|
|
|
|
out := map[string]bool{}
|
|
for _, m := range apiRoutePattern.FindAllStringSubmatch(readFile(t, routerPath), -1) {
|
|
out[m[1]+" "+m[2]] = true
|
|
}
|
|
if len(out) == 0 {
|
|
t.Fatal("no routes were found in router.go; the matcher needs updating")
|
|
}
|
|
return out
|
|
}
|
|
|
|
// specRoutes returns "METHOD /path" for every operation in the specification.
|
|
func specRoutes(t *testing.T) map[string]bool {
|
|
t.Helper()
|
|
spec := readFile(t, specPath)
|
|
|
|
// Split the document at each path key, so methods are attributed correctly.
|
|
locs := specPathPattern.FindAllStringSubmatchIndex(spec, -1)
|
|
out := map[string]bool{}
|
|
|
|
for i, loc := range locs {
|
|
path := spec[loc[2]:loc[3]]
|
|
end := len(spec)
|
|
if i+1 < len(locs) {
|
|
end = locs[i+1][0]
|
|
}
|
|
for _, m := range specMethodPattern.FindAllStringSubmatch(spec[loc[1]:end], -1) {
|
|
out[strings.ToUpper(m[1])+" "+path] = true
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
t.Fatal("no operations were found in the specification; the matcher needs updating")
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestOpenAPISpecMatchesRegisteredRoutes(t *testing.T) {
|
|
router := routerRoutes(t)
|
|
spec := specRoutes(t)
|
|
|
|
var missing, extra []string
|
|
for route := range router {
|
|
if !spec[route] {
|
|
missing = append(missing, route)
|
|
}
|
|
}
|
|
for route := range spec {
|
|
if !router[route] {
|
|
extra = append(extra, route)
|
|
}
|
|
}
|
|
sort.Strings(missing)
|
|
sort.Strings(extra)
|
|
|
|
if len(missing) > 0 {
|
|
t.Errorf("these routes exist but are not documented in %s:\n %s",
|
|
specPath, strings.Join(missing, "\n "))
|
|
}
|
|
if len(extra) > 0 {
|
|
t.Errorf("these routes are documented in %s but are not registered:\n %s",
|
|
specPath, strings.Join(extra, "\n "))
|
|
}
|
|
}
|
|
|
|
// The specification enumerates several closed sets that also exist as Go
|
|
// constants. A value added in one place and not the other would make the
|
|
// specification wrong in a way clients would act on.
|
|
func TestOpenAPIEnumsMatchTheDomain(t *testing.T) {
|
|
spec := readFile(t, specPath)
|
|
|
|
cases := []struct {
|
|
name string
|
|
values []string
|
|
}{
|
|
{"relationship types", []string{
|
|
"similar_to", "mentions", "involves", "supports", "contradicts", "updates",
|
|
"supersedes", "provides_context_for", "possibly_explains", "occurred_before",
|
|
"occurred_after", "derived_from", "same_entity_as",
|
|
}},
|
|
{"provenance", []string{
|
|
"explicit", "deterministic", "model_inferred", "user_confirmed", "user_rejected",
|
|
}},
|
|
{"entity types", []string{
|
|
"person", "organisation", "location", "product", "event", "date", "subject", "concept",
|
|
}},
|
|
{"source types", []string{
|
|
"note", "document", "json", "email", "transcript", "markdown",
|
|
}},
|
|
{"job types", []string{
|
|
"parse_item", "chunk_item", "generate_embeddings", "extract_entities",
|
|
"extract_claims", "discover_relationships", "reprocess_item",
|
|
"delete_item_data", "reembed_workspace",
|
|
}},
|
|
{"error codes", []string{
|
|
CodeInvalidRequest, CodeValidationFailed, CodeUnauthorised, CodeForbidden,
|
|
CodeNotFound, CodeDuplicate, CodeConflict, CodePayloadTooLarge,
|
|
CodeUnsupportedMedia, CodeRateLimited, CodeCSRFFailed, CodeDependencyDown,
|
|
CodeModelInvalid, CodeFeatureDisabled, CodeInternal,
|
|
}},
|
|
{"signal types", []string{
|
|
"semantic_similarity", "shared_entity", "alias_entity", "shared_concept",
|
|
"temporal_proximity", "shared_metadata", "claim_support", "claim_contradiction",
|
|
"model_classification", "user_confirmation", "user_rejection", "content_hash_match",
|
|
}},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
for _, v := range c.values {
|
|
if !strings.Contains(spec, v) {
|
|
t.Errorf("%s: %q is not documented in the specification", c.name, v)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The specification must not describe scores as probabilities, since the whole
|
|
// design rests on their not being probabilities.
|
|
func TestOpenAPIDescribesScoresHonestly(t *testing.T) {
|
|
spec := readFile(t, specPath)
|
|
|
|
for _, required := range []string{
|
|
"NOT a calibrated probability",
|
|
"Not a probability",
|
|
"not a fact",
|
|
} {
|
|
if !strings.Contains(spec, required) {
|
|
t.Errorf("the specification does not state %q", required)
|
|
}
|
|
}
|
|
// A claim that would be untrue.
|
|
for _, forbidden := range []string{"probability that", "confidence interval", "calibrated probability that"} {
|
|
if strings.Contains(strings.ToLower(spec), strings.ToLower(forbidden)) {
|
|
t.Errorf("the specification contains the misleading phrase %q", forbidden)
|
|
}
|
|
}
|
|
}
|