package apiserver import ( "encoding/json" "net/http" "net/http/httptest" "testing" "gitea.perlbach24.de/scripte/nexarch/internal/auth" ) func newTestServer() (*Server, *auth.TokenIssuer) { issuer := auth.NewTokenIssuer("test-secret-nur-fuer-tests") return NewServer(issuer), issuer } func withAuthCookie(req *http.Request, token string) *http.Request { req.AddCookie(&http.Cookie{Name: auth.CookieName, Value: token}) return req } // Akzeptanzkriterium 1: API unter versioniertem Pfad erreichbar. func TestHandleV1_RegistersUnderVersionedPath(t *testing.T) { srv, issuer := newTestServer() srv.HandleV1("/things", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) token, err := issuer.Issue("user-1", "acme") if err != nil { t.Fatalf("issue: %v", err) } req := withAuthCookie(httptest.NewRequest(http.MethodGet, "/api/v1/things", nil), token) rec := httptest.NewRecorder() srv.Handler().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200", rec.Code) } } // Akzeptanzkriterium 2 + Pruefung 1 (Stichprobe): mehrere Endpunkte liefern // bei fehlerhafter Anfrage dasselbe Fehlerschema. func TestErrorFormat_ConsistentAcrossEndpoints(t *testing.T) { srv, _ := newTestServer() srv.HandleV1("/endpunkt-a", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) srv.HandleV1("/endpunkt-b", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) for _, path := range []string{"/api/v1/endpunkt-a", "/api/v1/endpunkt-b"} { req := httptest.NewRequest(http.MethodGet, path, nil) // ohne cookie -> 401 rec := httptest.NewRecorder() srv.Handler().ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Fatalf("%s: status = %d, want 401", path, rec.Code) } var body errorBody if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("%s: fehlerantwort nicht im erwarteten json-schema: %v (body: %s)", path, err, rec.Body.String()) } if body.Error.Code == "" || body.Error.Message == "" { t.Fatalf("%s: erwartet nicht-leeren code/message, habe %+v", path, body) } } } // Akzeptanzkriterium 3 + Pruefung 2: Middleware-Kette setzt Tenant-/ // Benutzerkontext zuverlaessig, nachweislich fuer mehrere Endpunkte. func TestMiddleware_SetsRequestContextForEveryEndpoint(t *testing.T) { srv, issuer := newTestServer() var gotA, gotB RequestContext srv.HandleV1("/kontext-a", func(w http.ResponseWriter, r *http.Request) { gotA, _ = FromContext(r.Context()) w.WriteHeader(http.StatusOK) }) srv.HandleV1("/kontext-b", func(w http.ResponseWriter, r *http.Request) { gotB, _ = FromContext(r.Context()) w.WriteHeader(http.StatusOK) }) token, err := issuer.Issue("user-42", "tenant-x") if err != nil { t.Fatalf("issue: %v", err) } for path, got := range map[string]*RequestContext{"/api/v1/kontext-a": &gotA, "/api/v1/kontext-b": &gotB} { req := withAuthCookie(httptest.NewRequest(http.MethodGet, path, nil), token) rec := httptest.NewRecorder() srv.Handler().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("%s: status = %d, want 200", path, rec.Code) } if got.UserID != "user-42" || got.TenantSlug != "tenant-x" { t.Fatalf("%s: request-context unerwartet: %+v", path, *got) } } } // Akzeptanzkriterium 1 + Pruefung 3: eine neue v2-Route laesst sich anlegen, // ohne v1 zu beeintraechtigen. func TestVersioning_V2DoesNotAffectV1(t *testing.T) { srv, issuer := newTestServer() srv.HandleV1("/things", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("v1-antwort")) }) srv.Handle("v2", "/things", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("v2-antwort")) }) token, err := issuer.Issue("user-1", "acme") if err != nil { t.Fatalf("issue: %v", err) } reqV1 := withAuthCookie(httptest.NewRequest(http.MethodGet, "/api/v1/things", nil), token) recV1 := httptest.NewRecorder() srv.Handler().ServeHTTP(recV1, reqV1) if recV1.Body.String() != "v1-antwort" { t.Fatalf("v1 antwort = %q, want v1-antwort", recV1.Body.String()) } reqV2 := withAuthCookie(httptest.NewRequest(http.MethodGet, "/api/v2/things", nil), token) recV2 := httptest.NewRecorder() srv.Handler().ServeHTTP(recV2, reqV2) if recV2.Body.String() != "v2-antwort" { t.Fatalf("v2 antwort = %q, want v2-antwort", recV2.Body.String()) } // v1 nach dem Anlegen von v2 erneut pruefen — unveraendert. reqV1Again := withAuthCookie(httptest.NewRequest(http.MethodGet, "/api/v1/things", nil), token) recV1Again := httptest.NewRecorder() srv.Handler().ServeHTTP(recV1Again, reqV1Again) if recV1Again.Body.String() != "v1-antwort" { t.Fatalf("v1 antwort nach v2-anlage = %q, want weiterhin v1-antwort", recV1Again.Body.String()) } } func TestAuthAndTenantContext_RejectsInvalidToken(t *testing.T) { srv, _ := newTestServer() srv.HandleV1("/geschuetzt", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) req := withAuthCookie(httptest.NewRequest(http.MethodGet, "/api/v1/geschuetzt", nil), "kaputtes.token.hier") rec := httptest.NewRecorder() srv.Handler().ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Fatalf("status = %d, want 401", rec.Code) } }