落地管理员 RBAC/封禁/推送任务 stub,logout 解绑 device 并统一各页 ensureAccount,同时收口 P2 生日生成与状态文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
198 lines
6.2 KiB
Go
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)
|
|
}
|
|
}
|