Files
cmautobuy/admin/password_change_integration_test.go
T

149 lines
6.5 KiB
Go

package main
import (
"errors"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"cmautobuy/admin/repository"
"cmautobuy/admin/service"
)
func Test管理员自助修改密码完整流程和权限边界(t *testing.T) {
db := newMySQLTestDB(t)
now := time.Now()
if err := service.SetupInitialAdmin(db, "admin", "old-password", "old-password", now); err != nil {
t.Fatal(err)
}
admin, _ := repository.FindUserByUsername(db, "admin")
if err := service.CreatePurchaser(db, admin, "buyer", "buyer-password", "buyer-password", now); err != nil {
t.Fatal(err)
}
router, err := newRouter(db)
if err != nil {
t.Fatal(err)
}
adminTokenA, _, _, _ := service.Login(db, "admin", "old-password", now)
adminTokenB, _, _, _ := service.Login(db, "admin", "old-password", now)
buyerToken, _, _, _ := service.Login(db, "buyer", "buyer-password", now)
adminPageRequest := httptest.NewRequest(http.MethodGet, "/pdd?q=shoe", nil)
adminPageRequest.AddCookie(&http.Cookie{Name: "cmautobuy_session", Value: adminTokenA})
adminPage := httptest.NewRecorder()
router.ServeHTTP(adminPage, adminPageRequest)
if adminPage.Code != http.StatusOK {
t.Fatalf("管理员页面 = %d", adminPage.Code)
}
adminHTML := adminPage.Body.String()
for _, want := range []string{"修改密码", `action="/account/change-password"`, `autocomplete="current-password"`, `autocomplete="new-password"`, `value="/pdd?q=shoe"`} {
if !strings.Contains(adminHTML, want) {
t.Errorf("管理员页面缺少 %q", want)
}
}
for _, secret := range []string{"old-password", "buyer-password"} {
if strings.Contains(adminHTML, secret) {
t.Fatalf("页面泄露密码 %q", secret)
}
}
csrf := findResponseCookie(t, adminPage, "cmautobuy_csrf")
buyerPageRequest := httptest.NewRequest(http.MethodGet, "/pdd", nil)
buyerPageRequest.AddCookie(&http.Cookie{Name: "cmautobuy_session", Value: buyerToken})
buyerPage := httptest.NewRecorder()
router.ServeHTTP(buyerPage, buyerPageRequest)
if strings.Contains(buyerPage.Body.String(), `action="/account/change-password"`) || strings.Contains(buyerPage.Body.String(), ">修改密码</button>") {
t.Fatal("采购员页面不应显示修改密码入口或表单")
}
wrong := postFormRequest("/account/change-password", url.Values{
"csrf_token": {csrf.Value}, "current_password": {"wrong-password"},
"new_password": {"fresh-secret-789"}, "password_confirm": {"fresh-secret-789"},
"next": {"/pdd?q=shoe"},
}, csrf)
wrong.AddCookie(&http.Cookie{Name: "cmautobuy_session", Value: adminTokenA})
wrongResponse := httptest.NewRecorder()
router.ServeHTTP(wrongResponse, wrong)
location := wrongResponse.Header().Get("Location")
if wrongResponse.Code != http.StatusSeeOther || !strings.HasPrefix(location, "/pdd?") ||
!strings.Contains(location, "change_password=1") || strings.Contains(location, "wrong-password") || strings.Contains(location, "fresh-secret-789") {
t.Fatalf("错误当前密码重定向 = %d %q", wrongResponse.Code, location)
}
if _, err := service.Authenticate(db, adminTokenA, now.Add(time.Minute)); err != nil {
t.Fatalf("失败改密后 Session 应保持有效: %v", err)
}
errorPageRequest := httptest.NewRequest(http.MethodGet, location, nil)
errorPageRequest.AddCookie(&http.Cookie{Name: "cmautobuy_session", Value: adminTokenA})
errorPage := httptest.NewRecorder()
router.ServeHTTP(errorPage, errorPageRequest)
errorHTML := errorPage.Body.String()
if !strings.Contains(errorHTML, "当前密码错误,请重新输入") ||
!strings.Contains(errorHTML, `id="change-password-modal" >`) ||
!strings.Contains(errorHTML, `name="current_password" required`) ||
!strings.Contains(errorHTML, "autofocus") {
t.Fatalf("校验失败后弹窗、错误或焦点不正确: %s", errorHTML)
}
for _, secret := range []string{"wrong-password", "fresh-secret-789"} {
if strings.Contains(errorHTML, secret) {
t.Fatalf("失败页面泄露提交密码 %q", secret)
}
}
noCSRF := postFormRequest("/account/change-password", url.Values{
"current_password": {"old-password"}, "new_password": {"fresh-secret-789"},
"password_confirm": {"fresh-secret-789"},
})
noCSRF.AddCookie(&http.Cookie{Name: "cmautobuy_session", Value: adminTokenA})
noCSRFResponse := httptest.NewRecorder()
router.ServeHTTP(noCSRFResponse, noCSRF)
if noCSRFResponse.Code != http.StatusForbidden {
t.Fatalf("无 CSRF 改密 = %d,期望 403", noCSRFResponse.Code)
}
buyerPost := postFormRequest("/account/change-password", url.Values{
"csrf_token": {csrf.Value}, "current_password": {"buyer-password"},
"new_password": {"fresh-secret-789"}, "password_confirm": {"fresh-secret-789"},
}, csrf)
buyerPost.AddCookie(&http.Cookie{Name: "cmautobuy_session", Value: buyerToken})
buyerResponse := httptest.NewRecorder()
router.ServeHTTP(buyerResponse, buyerPost)
if buyerResponse.Code != http.StatusForbidden || !strings.Contains(buyerResponse.Body.String(), "只有管理员") {
t.Fatalf("采购员直接改密 = %d %s", buyerResponse.Code, buyerResponse.Body.String())
}
success := postFormRequest("/account/change-password", url.Values{
"csrf_token": {csrf.Value}, "current_password": {"old-password"},
"new_password": {"fresh-secret-789"}, "password_confirm": {"fresh-secret-789"},
"next": {"/pdd?q=shoe"}, "user_id": {"buyer"},
}, csrf)
success.AddCookie(&http.Cookie{Name: "cmautobuy_session", Value: adminTokenA})
successResponse := httptest.NewRecorder()
router.ServeHTTP(successResponse, success)
if successResponse.Code != http.StatusSeeOther || !strings.HasPrefix(successResponse.Header().Get("Location"), "/login?msg=") {
t.Fatalf("成功改密 = %d %q", successResponse.Code, successResponse.Header().Get("Location"))
}
cleared := findResponseCookie(t, successResponse, "cmautobuy_session")
if cleared.Value != "" || cleared.MaxAge != -1 {
t.Fatalf("成功后未清除认证 Cookie: %#v", cleared)
}
for _, token := range []string{adminTokenA, adminTokenB} {
if _, err := service.Authenticate(db, token, now.Add(time.Minute)); !errors.Is(err, service.ErrUnauthenticated) {
t.Fatalf("管理员全部旧 Session 应失效,实际 %v", err)
}
}
if _, _, _, err := service.Login(db, "admin", "old-password", now.Add(time.Minute)); !errors.Is(err, service.ErrInvalidCredentials) {
t.Fatalf("旧密码仍可登录: %v", err)
}
if _, _, _, err := service.Login(db, "admin", "fresh-secret-789", now.Add(time.Minute)); err != nil {
t.Fatalf("新密码不能登录: %v", err)
}
if _, _, _, err := service.Login(db, "buyer", "buyer-password", now.Add(time.Minute)); err != nil {
t.Fatalf("伪造 user_id 不应修改其他用户密码: %v", err)
}
}