package openapi import ( "encoding/json" "os" "path/filepath" "regexp" "sort" "strings" "testing" ) var routePattern = regexp.MustCompile(`HandleFunc\("([A-Z]+) (/[^"]*)"`) func registeredRoutes(t *testing.T) map[string]bool { t.Helper() files, err := filepath.Glob("../*api/*.go") if err != nil { t.Fatal(err) } extra, _ := filepath.Glob("../apiwiring/*.go") files = append(files, extra...) files = append(files, "../../cmd/app/main.go") routes := map[string]bool{} for _, f := range files { if strings.HasSuffix(f, "_test.go") { continue } raw, err := os.ReadFile(f) if err != nil { t.Fatal(err) } for _, m := range routePattern.FindAllStringSubmatch(string(raw), -1) { routes[m[1]+" "+m[2]] = true } } return routes } // Vertragstest: jede registrierte Route ist beschrieben und umgekehrt. func TestEveryRegisteredRouteIsDocumentedAndViceVersa(t *testing.T) { registered := registeredRoutes(t) if len(registered) < 40 { t.Fatalf("nur %d routen im quelltext gefunden, muster kaputt?", len(registered)) } documented := map[string]bool{} for _, op := range Operations() { documented[op.Method+" "+op.Path] = true } var missing, stale []string for r := range registered { if !documented[r] { missing = append(missing, r) } } for d := range documented { if !registered[d] { stale = append(stale, d) } } sort.Strings(missing) sort.Strings(stale) if len(missing) > 0 { t.Errorf("routen ohne beschreibung: %v", missing) } if len(stale) > 0 { t.Errorf("beschriebene routen ohne implementierung: %v", stale) } } func TestEveryOperationDeclaresPermissionAndErrorSchema(t *testing.T) { var doc struct { Paths map[string]map[string]struct { Permission string `json:"x-permission"` Security *[]any `json:"security"` Responses map[string]map[string]any `json:"responses"` } `json:"paths"` } if err := json.Unmarshal(Document(), &doc); err != nil { t.Fatal(err) } for path, methods := range doc.Paths { for method, o := range methods { id := method + " " + path if o.Permission == "" { t.Errorf("%s: x-permission fehlt", id) } public := o.Security != nil && len(*o.Security) == 0 if !public && !strings.HasPrefix(o.Permission, "dms.") { t.Errorf("%s: recht %q ausserhalb des dms-namensraums", id, o.Permission) } if !public { if _, ok := o.Responses["401"]; !ok { t.Errorf("%s: 401 nicht beschrieben", id) } } for status, resp := range o.Responses { if status[0] == '4' || status[0] == '5' { if !strings.Contains(toJSON(resp), "#/components/schemas/Error") { t.Errorf("%s: fehlerantwort %s ohne einheitliches Fehlerschema", id, status) } } } } } } func toJSON(v any) string { b, _ := json.Marshal(v) return string(b) } func TestMatchPrefersLiteralSegments(t *testing.T) { cases := []struct{ method, path, perm string }{ {"GET", "/folders/children", "dms.folder.read"}, {"GET", "/folders/abc/children", "dms.folder.read"}, {"GET", "/documents/1/revisions/2/content", "dms.revision.read"}, {"DELETE", "/documents/1/tags/2", "dms.tag.write"}, {"POST", "/trash", "dms.trash.write"}, {"POST", "/trash/purge", "dms.trash.purge"}, } for _, c := range cases { op, ok := Match(c.method, c.path) if !ok || op.Permission != c.perm { t.Errorf("%s %s: %+v ok=%v, erwartet %s", c.method, c.path, op, ok, c.perm) } } if _, ok := Match("GET", "/gibt-es-nicht"); ok { t.Error("unbekannte route darf nicht matchen") } if _, ok := Match("PATCH", "/tags"); ok { t.Error("falsche methode darf nicht matchen") } } func TestPublicPathsAreExactlySignatureAndServiceRoutes(t *testing.T) { got := PublicPaths() for _, p := range []string{"/download", "/openapi.json", "/retention/destroy-callback", "/share/{token}"} { if !got[p] { t.Errorf("%s fehlt in PublicPaths", p) } } if len(got) != 4 { t.Errorf("unerwartete oeffentliche pfade: %v", got) } }