package httpapi_test import ( "net/http" "net/http/httptest" "testing" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/httpapi" ) type logoutContract struct { Version int `json:"version"` Path string `json:"path"` Methods []string `json:"methods"` Status int `json:"status"` Location string `json:"location"` RequiresAuthentication bool `json:"requiresAuthentication"` DuplicateBaseCookieWrite bool `json:"duplicateBaseCookieWrite"` } func TestAuthLogoutConsumesSharedContractAndClearsExactlyTwentyCookies(t *testing.T) { var contract logoutContract loadHTTPFixture(t, "logout-v1.json", &contract) cookie := loadPasswordSessionCookieContract(t) handler := httpapi.NewAuthLogoutHandler(httpapi.LogoutConfig{PublicBaseURL: "https://app.example.test"}) if contract.Version != 1 || contract.RequiresAuthentication || contract.DuplicateBaseCookieWrite { t.Fatalf("invalid shared logout contract: %+v", contract) } for _, method := range contract.Methods { t.Run(method, func(t *testing.T) { request := httptest.NewRequest(method, "https://app.example.test"+contract.Path, nil) response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != contract.Status || response.Header().Get("Location") != contract.Location || response.Body.Len() != 0 { t.Fatalf("response = %d location=%q body=%q", response.Code, response.Header().Get("Location"), response.Body.String()) } cookies := response.Result().Cookies() if len(cookies) != cookie.Cookie.MaxChunks { t.Fatalf("cookies = %d, want %d", len(cookies), cookie.Cookie.MaxChunks) } for index, got := range cookies { if got.Name != cookie.Cookie.ChunkNames[index] || got.Value != cookie.Cookie.Clear.Value || got.MaxAge != -1 || !got.HttpOnly || got.SameSite != http.SameSiteLaxMode || got.Path != "/" || !got.Secure { t.Errorf("cookie %d = %#v", index, got) } } }) } } func TestAuthLogoutUsesRequestOriginAndExplicitCookieSecurity(t *testing.T) { handler := httpapi.NewAuthLogoutHandler(httpapi.LogoutConfig{CookieSecure: "false"}) request := httptest.NewRequest(http.MethodPost, "https://request.example.test/api/auth/logout", nil) response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Header().Get("Location") != "https://request.example.test/auth/login?loggedOut=1" { t.Fatalf("Location = %q", response.Header().Get("Location")) } for _, cookie := range response.Result().Cookies() { if cookie.Secure { t.Fatalf("cookie unexpectedly secure: %#v", cookie) } } } func TestAuthLogoutMethodAndPathSemantics(t *testing.T) { handler := httpapi.NewAuthLogoutHandler(httpapi.LogoutConfig{}) tests := []struct { method, path string status int }{ {http.MethodOptions, "/api/auth/logout", http.StatusNoContent}, {http.MethodHead, "/api/auth/logout", http.StatusTemporaryRedirect}, {http.MethodPut, "/api/auth/logout", http.StatusMethodNotAllowed}, {http.MethodGet, "/api/auth/logout/", http.StatusNotFound}, } for _, test := range tests { response := httptest.NewRecorder() handler.ServeHTTP(response, httptest.NewRequest(test.method, "http://app.test"+test.path, nil)) if response.Code != test.status || response.Body.Len() != 0 { t.Errorf("%s %s = %d %q", test.method, test.path, response.Code, response.Body.String()) } } }