package main import ( "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", "admin-password", "admin-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) } adminToken, _, _, err := service.Login(db, "admin", "admin-password", now) if err != nil { t.Fatal(err) } adminPageRequest := httptest.NewRequest(http.MethodGet, "/users", nil) adminPageRequest.AddCookie(&http.Cookie{Name: "cmautobuy_session", Value: adminToken}) adminPage := httptest.NewRecorder() router.ServeHTTP(adminPage, adminPageRequest) if adminPage.Code != http.StatusOK { t.Fatalf("管理员 GET /users = %d,响应:%s", adminPage.Code, adminPage.Body.String()) } for _, want := range []string{"用户管理", "新增采购员", "buyer", "重置密码", "data-confirm-submit"} { if !strings.Contains(adminPage.Body.String(), want) { t.Errorf("用户管理页缺少 %q", want) } } if strings.Count(adminPage.Body.String(), `minlength="6"`) != 4 || strings.Count(adminPage.Body.String(), "至少 6 个字符") != 2 { t.Fatalf("创建和重置密码表单没有统一使用 6 个字符规则") } for _, secret := range []string{"admin-password", "buyer-password"} { if strings.Contains(adminPage.Body.String(), secret) { t.Fatalf("用户管理页泄露密码 %q", secret) } } csrfCookie := findResponseCookie(t, adminPage, "cmautobuy_csrf") duplicate := postFormRequest("/users/create", url.Values{ "csrf_token": {csrfCookie.Value}, "username": {"BUYER"}, "password": {"another-password"}, "password_confirm": {"another-password"}, }, csrfCookie) duplicate.AddCookie(&http.Cookie{Name: "cmautobuy_session", Value: adminToken}) duplicateResponse := httptest.NewRecorder() router.ServeHTTP(duplicateResponse, duplicate) if duplicateResponse.Code != http.StatusBadRequest || !strings.Contains(duplicateResponse.Body.String(), "用户名已经存在") || !strings.Contains(duplicateResponse.Body.String(), `value="BUYER"`) { t.Fatalf("重复用户名应保留用户名并重开表单: %d %s", duplicateResponse.Code, duplicateResponse.Body.String()) } if strings.Contains(duplicateResponse.Body.String(), "another-password") { t.Fatal("校验失败页面不能回显密码") } // 即使已登录管理员,没有 CSRF 也不能创建账号。 noCSRF := postFormRequest("/users/create", url.Values{ "username": {"staff"}, "password": {"staff-password"}, "password_confirm": {"staff-password"}, }) noCSRF.AddCookie(&http.Cookie{Name: "cmautobuy_session", Value: adminToken}) noCSRFResponse := httptest.NewRecorder() router.ServeHTTP(noCSRFResponse, noCSRF) if noCSRFResponse.Code != http.StatusForbidden { t.Fatalf("无 CSRF 创建采购员 = %d,期望 403", noCSRFResponse.Code) } create := postFormRequest("/users/create", url.Values{ "csrf_token": {csrfCookie.Value}, "username": {"staff"}, "password": {"staff-password"}, "password_confirm": {"staff-password"}, }, csrfCookie) create.AddCookie(&http.Cookie{Name: "cmautobuy_session", Value: adminToken}) createResponse := httptest.NewRecorder() router.ServeHTTP(createResponse, create) if createResponse.Code != http.StatusSeeOther || !strings.HasPrefix(createResponse.Header().Get("Location"), "/users?") { t.Fatalf("创建采购员 = %d %s", createResponse.Code, createResponse.Header().Get("Location")) } if _, err := repository.FindUserByUsername(db, "STAFF"); err != nil { t.Fatalf("HTTP 创建的采购员不存在: %v", err) } buyerToken, _, _, err := service.Login(db, "buyer", "buyer-password", now) if err != nil { t.Fatal(err) } buyerBusiness := httptest.NewRequest(http.MethodGet, "/shopee", nil) buyerBusiness.AddCookie(&http.Cookie{Name: "cmautobuy_session", Value: buyerToken}) buyerBusinessResponse := httptest.NewRecorder() router.ServeHTTP(buyerBusinessResponse, buyerBusiness) if buyerBusinessResponse.Code != http.StatusOK { t.Fatalf("采购员应可访问业务页,实际 %d", buyerBusinessResponse.Code) } if strings.Contains(buyerBusinessResponse.Body.String(), `href="/users"`) { t.Fatal("采购员导航不应显示用户管理入口") } buyerUsers := httptest.NewRequest(http.MethodGet, "/users", nil) buyerUsers.AddCookie(&http.Cookie{Name: "cmautobuy_session", Value: buyerToken}) buyerUsersResponse := httptest.NewRecorder() router.ServeHTTP(buyerUsersResponse, buyerUsers) if buyerUsersResponse.Code != http.StatusForbidden || !strings.Contains(buyerUsersResponse.Body.String(), "只有管理员") { t.Fatalf("采购员访问 /users = %d,响应:%s", buyerUsersResponse.Code, buyerUsersResponse.Body.String()) } }