package openapi import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "gitea.perlbach24.de/scripte/nexarch/internal/apiserver" "gitea.perlbach24.de/scripte/nexarch/internal/auth" ) func newTestServerWithRoutes() (*apiserver.Server, *auth.TokenIssuer) { issuer := auth.NewTokenIssuer("test-secret-nur-fuer-tests") srv := apiserver.NewServer(issuer) srv.HandleV1("/things", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) srv.HandleV1("/other", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) return srv, issuer } func testEntries() []Entry { return []Entry{ {Path: "/api/v1/things", Method: "GET", Summary: "Dinge auflisten", ExpectStatus: http.StatusOK}, {Path: "/api/v1/other", Method: "GET", Summary: "Anderes abrufen", ExpectStatus: http.StatusOK}, } } // Akzeptanzkriterium 1: das Dokument deckt alle tatsaechlich registrierten // Endpunkte ab. func TestCheckNoDrift_PassesWhenDocumentMatchesRoutes(t *testing.T) { srv, _ := newTestServerWithRoutes() if err := CheckNoDrift(srv, testEntries()); err != nil { t.Fatalf("erwartet keinen drift, habe: %v", err) } } // Akzeptanzkriterium 2 + Pruefung 1: automatisierter Abgleich schlaegt bei // Drift fehl — hier absichtlich eine Route aus dem Dokument entfernt, waehrend // der Server sie weiterhin registriert hat. func TestCheckNoDrift_FailsWhenRouteMissingFromDocument(t *testing.T) { srv, _ := newTestServerWithRoutes() entries := []Entry{testEntries()[0]} // "/api/v1/other" absichtlich entfernt err := CheckNoDrift(srv, entries) if err == nil { t.Fatal("erwartet drift-fehler, da eine registrierte route nicht dokumentiert ist") } driftErr, ok := err.(*ErrDrift) if !ok { t.Fatalf("erwartet *ErrDrift, habe %T", err) } if len(driftErr.MissingInDocument) != 1 || driftErr.MissingInDocument[0] != "/api/v1/other" { t.Fatalf("erwartet '/api/v1/other' als nicht dokumentiert, habe: %v", driftErr.MissingInDocument) } } // Symmetrischer Fall: Dokument nennt eine Route, die es beim Server gar // nicht (mehr) gibt (z.B. nach Entfernen eines Endpunkts im Code). func TestCheckNoDrift_FailsWhenDocumentedRouteNoLongerExists(t *testing.T) { srv, _ := newTestServerWithRoutes() entries := append(testEntries(), Entry{Path: "/api/v1/entfernt", Method: "GET", ExpectStatus: http.StatusOK}) err := CheckNoDrift(srv, entries) if err == nil { t.Fatal("erwartet drift-fehler fuer dokumentierte, aber nicht registrierte route") } driftErr := err.(*ErrDrift) if len(driftErr.MissingAsRoute) != 1 || driftErr.MissingAsRoute[0] != "/api/v1/entfernt" { t.Fatalf("erwartet '/api/v1/entfernt' als nicht (mehr) registriert, habe: %v", driftErr.MissingAsRoute) } } // Akzeptanzkriterium 2 + Pruefung 2: Beispielaufrufe aus dem Dokument // werden GEGEN DIE ECHTE API (httptest, echte Middleware-Kette) ausgefuehrt. func TestExamples_ExecuteSuccessfullyAgainstRealServer(t *testing.T) { srv, issuer := newTestServerWithRoutes() token, err := issuer.Issue("user-1", "acme") if err != nil { t.Fatalf("issue: %v", err) } for _, e := range testEntries() { req := httptest.NewRequest(e.Method, e.Path, nil) req.AddCookie(&http.Cookie{Name: auth.CookieName, Value: token}) rec := httptest.NewRecorder() srv.Handler().ServeHTTP(rec, req) if rec.Code != e.ExpectStatus { t.Fatalf("beispielaufruf %s %s: status = %d, want %d", e.Method, e.Path, rec.Code, e.ExpectStatus) } } } // Akzeptanzkriterium 3: Dokument ist ueber einen Endpunkt abrufbar und // valides JSON, das Tools wie Swagger UI importieren koennen. func TestDocumentHandler_ServesValidJSONWithAllPaths(t *testing.T) { doc := BuildDocument("NEXARCH Core API", "v1", testEntries()) req := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.json", nil) rec := httptest.NewRecorder() DocumentHandler(doc)(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200", rec.Code) } var decoded Document if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil { t.Fatalf("dokument nicht als json lesbar: %v", err) } if decoded.OpenAPI == "" { t.Fatal("erwartet gesetztes openapi-versionsfeld") } for _, e := range testEntries() { if _, ok := decoded.Paths[e.Path]; !ok { t.Fatalf("pfad %s fehlt im dekodierten dokument", e.Path) } } } func TestSwaggerUIHandler_ServesHTMLReferencingDocumentURL(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/docs", nil) rec := httptest.NewRecorder() SwaggerUIHandler("/api/v1/openapi.json")(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200", rec.Code) } ct := rec.Header().Get("Content-Type") if ct == "" { t.Fatal("erwartet gesetzten Content-Type-Header") } body := rec.Body.String() if !strings.Contains(body, "/api/v1/openapi.json") { t.Fatal("erwartet referenz auf das openapi-dokument in der swagger-ui-seite") } }