package integration_test import ( "context" "encoding/json" "net/http" "strings" "testing" "time" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" "github.com/yuxingu/digital-psychology/apps/api/internal/config" "github.com/yuxingu/digital-psychology/apps/api/internal/db" ) func TestOpsESystemRBACBanPush(t *testing.T) { r, _ := setupAPI(t) env, code := doAdminJSON(t, r, http.MethodPost, "/api/v1/admin/auth/login", map[string]string{ "username": "admin", "password": "change-me", }, "") if code != 200 || env.Code != 0 { t.Fatalf("super login failed http=%d code=%d msg=%s", code, env.Code, env.Message) } var login struct { Token string `json:"token"` Admin struct { ID string `json:"id"` Role string `json:"role"` } `json:"admin"` } if err := json.Unmarshal(env.Data, &login); err != nil || login.Token == "" { t.Fatalf("login parse: %v %s", err, env.Data) } if login.Admin.Role != "super" { t.Fatalf("bootstrap admin role want super got %q", login.Admin.Role) } superTok := login.Token opsUser := "ops_" + uuid.NewString()[:8] opsPass := "ops-pass-1" insertOpsAdmin(t, opsUser, opsPass) env, code = doAdminJSON(t, r, http.MethodPost, "/api/v1/admin/auth/login", map[string]string{ "username": opsUser, "password": opsPass, }, "") if code != 200 || env.Code != 0 { t.Fatalf("ops login failed http=%d code=%d msg=%s", code, env.Code, env.Message) } _ = json.Unmarshal(env.Data, &login) opsTok := login.Token if login.Admin.Role != "ops" { t.Fatalf("ops role want ops got %q", login.Admin.Role) } env, code = doAdminJSON(t, r, http.MethodPut, "/api/v1/admin/membership/plan-prices", map[string]any{ "items": []map[string]any{{"plan": "month", "display_cents": 990}}, }, opsTok) if code != http.StatusForbidden || env.Code != 40301 { t.Fatalf("ops put prices want 40301, got http=%d code=%d", code, env.Code) } env, code = doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/admins", nil, opsTok) if code != http.StatusForbidden || env.Code != 40301 { t.Fatalf("ops list admins want 40301, got http=%d code=%d", code, env.Code) } key := mustRegister(t, r) env, code = doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/users", nil, opsTok) if code != 200 || env.Code != 0 { t.Fatalf("ops list users failed http=%d code=%d", code, env.Code) } var list struct { Items []struct { ID string `json:"id"` } `json:"items"` } if err := json.Unmarshal(env.Data, &list); err != nil || len(list.Items) == 0 { t.Fatalf("users: %v %s", err, env.Data) } userID := list.Items[0].ID env, code = doAdminJSON(t, r, http.MethodPost, "/api/v1/admin/users/"+userID+"/membership/grant", map[string]string{ "plan": "month", }, opsTok) if code != http.StatusForbidden || env.Code != 40301 { t.Fatalf("ops grant want 40301, got http=%d code=%d", code, env.Code) } env, code = doAdminJSON(t, r, http.MethodPost, "/api/v1/admin/users/"+userID+"/ban", nil, opsTok) if code != 200 || env.Code != 0 { t.Fatalf("ban failed http=%d code=%d msg=%s", code, env.Code, env.Message) } env, _, httpStatus := doJSONExpect(t, r, http.MethodGet, "/api/v1/profiles", nil, key, 40310) if httpStatus != http.StatusForbidden { t.Fatalf("banned device want HTTP 403, got %d code=%d", httpStatus, env.Code) } env, code = doAdminJSON(t, r, http.MethodPost, "/api/v1/admin/users/"+userID+"/unban", nil, opsTok) if code != 200 || env.Code != 0 { t.Fatalf("unban failed http=%d code=%d", code, env.Code) } _, key = doJSON(t, r, http.MethodGet, "/api/v1/profiles", nil, key) env, code = doAdminJSON(t, r, http.MethodPost, "/api/v1/admin/push-jobs", map[string]string{ "title": "测试推送", "body": "不下发", }, opsTok) if code != 200 || env.Code != 0 { t.Fatalf("create push failed http=%d code=%d msg=%s", code, env.Code, env.Message) } var job struct { ID string `json:"id"` Status string `json:"status"` } if err := json.Unmarshal(env.Data, &job); err != nil || job.ID == "" || job.Status != "draft" { t.Fatalf("push job: %v %s", err, env.Data) } env, code = doAdminJSON(t, r, http.MethodPatch, "/api/v1/admin/push-jobs/"+job.ID, map[string]string{ "status": "cancelled", }, opsTok) if code != 200 || env.Code != 0 { t.Fatalf("cancel push failed http=%d code=%d", code, env.Code) } env, code = doAdminJSON(t, r, http.MethodPost, "/api/v1/admin/push-jobs", map[string]string{ "title": strings.Repeat("超", 129), "body": "too long", }, opsTok) if code != http.StatusBadRequest || env.Code != 40055 { t.Fatalf("long title want 40055, got http=%d code=%d", code, env.Code) } env, code = doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/admins", nil, superTok) if code != 200 || env.Code != 0 { t.Fatalf("list admins as super failed") } var admins struct { Items []struct { ID string `json:"id"` Role string `json:"role"` } `json:"items"` } if err := json.Unmarshal(env.Data, &admins); err != nil { t.Fatalf("admins parse: %v", err) } var soleSuper string superCount := 0 for _, a := range admins.Items { if a.Role == "super" { superCount++ soleSuper = a.ID } } if superCount != 1 || soleSuper == "" { t.Fatalf("want exactly 1 super, got %d", superCount) } env, code = doAdminJSON(t, r, http.MethodPatch, "/api/v1/admin/admins/"+soleSuper, map[string]string{ "role": "ops", }, superTok) if code != http.StatusConflict || env.Code != 40901 { t.Fatalf("demote last super want 40901, got http=%d code=%d msg=%s", code, env.Code, env.Message) } env, code = doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/me", nil, superTok) if code != 200 || env.Code != 0 { t.Fatalf("super me failed") } _ = env } func insertOpsAdmin(t *testing.T, username, password string) { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() cfg := config.Load() pool, err := db.Connect(ctx, cfg.DatabaseURL) if err != nil { t.Fatalf("connect: %v", err) } defer pool.Close() hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { t.Fatalf("hash: %v", err) } _, err = pool.Exec(ctx, ` INSERT INTO admin_accounts(username, password_hash, role, status) VALUES ($1,$2,'ops','active')`, username, string(hash)) if err != nil { t.Fatalf("insert ops admin: %v", err) } }