Files
cmautobuy/admin/auth_integration_test.go
T

160 lines
6.1 KiB
Go

package main
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"cmautobuy/admin/repository"
)
func TestAdmin首次初始化登录退出完整流程(t *testing.T) {
db, err := repository.Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer db.Close()
if err := repository.Migrate(db); err != nil {
t.Fatal(err)
}
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())
}
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, err := repository.Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer db.Close()
if err := repository.Migrate(db); err != nil {
t.Fatal(err)
}
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
}