2026-08-09 16:49:28 +08:00
|
|
|
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) {
|
2026-08-10 02:21:31 +08:00
|
|
|
db := newMySQLTestDB(t)
|
2026-08-09 16:49:28 +08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|