package main import ( "net/http" "net/http/httptest" "net/url" "strings" "testing" ) func TestAdmin首次初始化登录退出完整流程(t *testing.T) { db := newMySQLTestDB(t) router, err := newRouter(db) if err != nil { t.Fatal(err) } setupPage := httptest.NewRecorder() router.ServeHTTP(setupPage, httptest.NewRequest(http.MethodGet, "/setup", nil)) if setupPage.Code != http.StatusOK || !strings.Contains(setupPage.Body.String(), "初始化管理员") { t.Fatalf("GET /setup = %d,响应:%s", setupPage.Code, setupPage.Body.String()) } if strings.Count(setupPage.Body.String(), `minlength="6"`) != 2 || !strings.Contains(setupPage.Body.String(), "至少 6 个字符") { t.Fatalf("初始化表单密码规则未同步为 6 个字符:%s", setupPage.Body.String()) } csrfCookie := findResponseCookie(t, setupPage, "cmautobuy_csrf") if csrfCookie.HttpOnly || csrfCookie.Value == "" { t.Fatalf("CSRF Cookie 应可供双提交表单使用: %#v", csrfCookie) } // 初始化和登录都是公开入口,但 POST 同样必须通过 CSRF。 missingCSRF := postFormRequest("/setup", url.Values{ "username": {"admin"}, "password": {"safe-password"}, "password_confirm": {"safe-password"}, }, nil) missingCSRFResponse := httptest.NewRecorder() router.ServeHTTP(missingCSRFResponse, missingCSRF) if missingCSRFResponse.Code != http.StatusForbidden { t.Fatalf("无 CSRF 初始化 = %d,期望 403", missingCSRFResponse.Code) } setup := postFormRequest("/setup", url.Values{ "csrf_token": {csrfCookie.Value}, "username": {"admin"}, "password": {"safe-password"}, "password_confirm": {"safe-password"}, }, csrfCookie) setupResponse := httptest.NewRecorder() router.ServeHTTP(setupResponse, setup) if setupResponse.Code != http.StatusSeeOther || !strings.HasPrefix(setupResponse.Header().Get("Location"), "/login?") { t.Fatalf("POST /setup = %d %s", setupResponse.Code, setupResponse.Header().Get("Location")) } // 初始化完成后入口永久关闭。 setupAgain := httptest.NewRecorder() router.ServeHTTP(setupAgain, httptest.NewRequest(http.MethodGet, "/setup", nil)) if setupAgain.Code != http.StatusSeeOther || !strings.HasPrefix(setupAgain.Header().Get("Location"), "/login?") { t.Fatalf("再次 GET /setup = %d %s", setupAgain.Code, setupAgain.Header().Get("Location")) } wrongLogin := postFormRequest("/login", url.Values{ "csrf_token": {csrfCookie.Value}, "username": {"admin"}, "password": {"wrong-password"}, "next": {"/pdd"}, }, csrfCookie) wrongResponse := httptest.NewRecorder() router.ServeHTTP(wrongResponse, wrongLogin) if wrongResponse.Code != http.StatusUnauthorized || !strings.Contains(wrongResponse.Body.String(), "用户名或密码错误") { t.Fatalf("错误密码响应 = %d %s", wrongResponse.Code, wrongResponse.Body.String()) } login := postFormRequest("/login", url.Values{ "csrf_token": {csrfCookie.Value}, "username": {"admin"}, "password": {"safe-password"}, "next": {"/pdd"}, }, csrfCookie) loginResponse := httptest.NewRecorder() router.ServeHTTP(loginResponse, login) if loginResponse.Code != http.StatusSeeOther || loginResponse.Header().Get("Location") != "/pdd" { t.Fatalf("POST /login = %d %s", loginResponse.Code, loginResponse.Header().Get("Location")) } authCookie := findResponseCookie(t, loginResponse, "cmautobuy_session") if !authCookie.HttpOnly || authCookie.Secure || authCookie.SameSite != http.SameSiteLaxMode || authCookie.Path != "/" || authCookie.MaxAge != 12*60*60 { t.Fatalf("认证 Cookie 属性不正确: %#v", authCookie) } protected := httptest.NewRequest(http.MethodGet, "/shopee", nil) protected.AddCookie(authCookie) protectedResponse := httptest.NewRecorder() router.ServeHTTP(protectedResponse, protected) if protectedResponse.Code != http.StatusOK || !strings.Contains(protectedResponse.Body.String(), "admin") || !strings.Contains(protectedResponse.Body.String(), "退出登录") { t.Fatalf("登录后业务页 = %d,响应:%s", protectedResponse.Code, protectedResponse.Body.String()) } logout := postFormRequest("/logout", url.Values{"csrf_token": {csrfCookie.Value}}, csrfCookie) logout.AddCookie(authCookie) logoutResponse := httptest.NewRecorder() router.ServeHTTP(logoutResponse, logout) if logoutResponse.Code != http.StatusSeeOther || !strings.HasPrefix(logoutResponse.Header().Get("Location"), "/login?") { t.Fatalf("POST /logout = %d %s", logoutResponse.Code, logoutResponse.Header().Get("Location")) } oldSession := httptest.NewRequest(http.MethodGet, "/shopee", nil) oldSession.AddCookie(authCookie) oldSessionResponse := httptest.NewRecorder() router.ServeHTTP(oldSessionResponse, oldSession) if oldSessionResponse.Code != http.StatusSeeOther || !strings.HasPrefix(oldSessionResponse.Header().Get("Location"), "/login?next=") { t.Fatalf("退出后旧 Session 仍可访问: %d %s", oldSessionResponse.Code, oldSessionResponse.Header().Get("Location")) } } func TestClientAPI不经过网页登录和CSRF(t *testing.T) { db := newMySQLTestDB(t) router, err := newRouter(db) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodPut, "/api/v1/client/registration", strings.NewReader(`{}`)) request.Header.Set("Content-Type", "application/json") response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusBadRequest || !strings.Contains(response.Body.String(), "MISSING_CLIENT_ID") { t.Fatalf("Client API 不应被网页登录或 CSRF 拦截: %d %s", response.Code, response.Body.String()) } } func postFormRequest(path string, form url.Values, cookies ...*http.Cookie) *http.Request { request := httptest.NewRequest(http.MethodPost, path, strings.NewReader(form.Encode())) request.Header.Set("Content-Type", "application/x-www-form-urlencoded") for _, cookie := range cookies { if cookie != nil { request.AddCookie(cookie) } } return request } func findResponseCookie(t *testing.T, response *httptest.ResponseRecorder, name string) *http.Cookie { t.Helper() for _, cookie := range response.Result().Cookies() { if cookie.Name == name { return cookie } } t.Fatalf("响应缺少 Cookie %s", name) return nil }