package main import ( "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" "cmautobuy/admin/model" "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") for _, name := range []string{"buyer-a", "buyer-b"} { if err := service.CreatePurchaser(db, admin, name, "buyer-password", "buyer-password", now); err != nil { t.Fatal(err) } } buyerA, _ := repository.FindUserByUsername(db, "buyer-a") for _, client := range []model.Client{ {ClientID: "client-a", Name: "采购一号机"}, {ClientID: "client-free", Name: "未绑定机器"}, } { if err := service.RegisterClient(db, client, true); err != nil { t.Fatal(err) } } router, err := newRouter(db) if err != nil { t.Fatal(err) } adminToken, _, _, _ := service.Login(db, "admin", "admin-password", now) adminPageRequest := httptest.NewRequest(http.MethodGet, "/clients", nil) adminPageRequest.AddCookie(&http.Cookie{Name: "cmautobuy_session", Value: adminToken}) adminPage := httptest.NewRecorder() router.ServeHTTP(adminPage, adminPageRequest) if adminPage.Code != http.StatusOK { t.Fatalf("管理员客户端页 = %d: %s", adminPage.Code, adminPage.Body.String()) } for _, want := range []string{"当前负责人", "绑定", "buyer-a", "buyer-b", "/clients/delete"} { if !strings.Contains(adminPage.Body.String(), want) { t.Errorf("管理员客户端页缺少 %q", want) } } csrf := findResponseCookie(t, adminPage, "cmautobuy_csrf") assign := postFormRequest("/clients/assign", url.Values{ "csrf_token": {csrf.Value}, "client_id": {"client-a"}, "user_id": {buyerA.UserID}, }, csrf) assign.AddCookie(&http.Cookie{Name: "cmautobuy_session", Value: adminToken}) assignResponse := httptest.NewRecorder() router.ServeHTTP(assignResponse, assign) if assignResponse.Code != http.StatusSeeOther || !strings.HasPrefix(assignResponse.Header().Get("Location"), "/clients?") { t.Fatalf("管理员绑定响应 = %d %s", assignResponse.Code, assignResponse.Header().Get("Location")) } buyerToken, _, _, _ := service.Login(db, "buyer-a", "buyer-password", now) buyerPageRequest := httptest.NewRequest(http.MethodGet, "/clients", nil) buyerPageRequest.AddCookie(&http.Cookie{Name: "cmautobuy_session", Value: buyerToken}) buyerPage := httptest.NewRecorder() router.ServeHTTP(buyerPage, buyerPageRequest) body := buyerPage.Body.String() if buyerPage.Code != http.StatusOK || !strings.Contains(body, "采购一号机") || strings.Contains(body, "未绑定机器") { t.Fatalf("采购员客户端可见范围不正确: %d %s", buyerPage.Code, body) } for _, forbidden := range []string{"/clients/assign", "/clients/unassign", "/clients/delete", "转交"} { if strings.Contains(body, forbidden) { t.Errorf("采购员只读页面不应包含 %q", forbidden) } } buyerCSRF := findResponseCookie(t, buyerPage, "cmautobuy_csrf") forbiddenAssign := postFormRequest("/clients/assign", url.Values{ "csrf_token": {buyerCSRF.Value}, "client_id": {"client-free"}, "user_id": {buyerA.UserID}, }, buyerCSRF) forbiddenAssign.AddCookie(&http.Cookie{Name: "cmautobuy_session", Value: buyerToken}) forbiddenResponse := httptest.NewRecorder() router.ServeHTTP(forbiddenResponse, forbiddenAssign) if forbiddenResponse.Code != http.StatusForbidden || !strings.Contains(forbiddenResponse.Body.String(), "只有管理员") { t.Fatalf("采购员绑定应返回 403,实际 %d %s", forbiddenResponse.Code, forbiddenResponse.Body.String()) } rows, err := service.ListClientViewsForUser(db, buyerA, "", time.Minute) if err != nil || len(rows) != 1 || rows[0].ClientID != "client-a" { t.Fatalf("越权请求后归属不应变化,rows=%+v err=%v", rows, err) } }