Files
NianAIGC/backend/internal/httpapi/auth_logout_test.go
2026-08-18 16:58:58 +08:00

100 lines
4.0 KiB
Go

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 TestAuthLogoutUsesPublicBaseURLBehindDevelopmentProxy(t *testing.T) {
handler := httpapi.NewAuthLogoutHandler(httpapi.LogoutConfig{PublicBaseURL: "http://127.0.0.1:3000"})
request := httptest.NewRequest(http.MethodPost, "http://127.0.0.1:8080/api/auth/logout", nil)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusTemporaryRedirect {
t.Fatalf("status = %d, want %d", response.Code, http.StatusTemporaryRedirect)
}
if got := response.Header().Get("Location"); got != "http://127.0.0.1:3000/auth/login?loggedOut=1" {
t.Fatalf("Location = %q", got)
}
}
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())
}
}
}