Files
digital-psychology/apps/api/internal/integration/ops_system_test.go
T
jackyu66gitandCursor 5ceb3ce749
ci / h5 (push) Canceled after 0s
ci / api (push) Canceled after 0s
ci / ess-docs (push) Canceled after 0s
feat(ECR-010): Ops-E 系统运营;修复登出解绑;P2 Complete
落地管理员 RBAC/封禁/推送任务 stub,logout 解绑 device 并统一各页 ensureAccount,同时收口 P2 生日生成与状态文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 18:54:59 +08:00

198 lines
6.2 KiB
Go

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)
}
}