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, err := repository.Open(t.TempDir()) if err != nil { t.Fatal(err) } defer db.Close() if err := repository.Migrate(db); err != nil { t.Fatal(err) } 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(), ">修改密码") { 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) } }