feat(ECR-006): 落地运营后台 Phase A(admin API + admin-h5)
ci / h5 (push) Canceled after 0s
ci / api (push) Canceled after 0s
ci / ess-docs (push) Canceled after 0s

新增独立鉴权的 /api/v1/admin 与 Vue 控制台;会员授予与审计同事务,并补集成/单测。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-06 18:35:53 +08:00
co-authored by Cursor
parent 4a583c9480
commit 879bf70cb7
59 changed files with 2462 additions and 20 deletions
@@ -0,0 +1,126 @@
package integration_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func doAdminJSON(t *testing.T, r http.Handler, method, path string, body any, token string) (envelope, int) {
t.Helper()
auth := ""
if token != "" {
auth = "Bearer " + token
}
return doAdminAuth(t, r, method, path, body, auth)
}
func doAdminAuth(t *testing.T, r http.Handler, method, path string, body any, authorization string) (envelope, int) {
t.Helper()
var buf bytes.Buffer
if body != nil {
if err := json.NewEncoder(&buf).Encode(body); err != nil {
t.Fatalf("encode: %v", err)
}
}
req := httptest.NewRequest(method, path, &buf)
req.Header.Set("Content-Type", "application/json")
if authorization != "" {
req.Header.Set("Authorization", authorization)
}
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
var env envelope
_ = json.Unmarshal(w.Body.Bytes(), &env)
return env, w.Code
}
func TestAdminOpsPhaseA(t *testing.T) {
r, _ := setupAPI(t)
env, code := doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/users", nil, "")
if code != http.StatusUnauthorized || env.Code == 0 {
t.Fatalf("expected 401 without token, got http=%d code=%d", code, env.Code)
}
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("login failed http=%d code=%d msg=%s body=%s", code, env.Code, env.Message, string(env.Data))
}
var login struct {
Token string `json:"token"`
}
if err := json.Unmarshal(env.Data, &login); err != nil || login.Token == "" {
t.Fatalf("login token missing: %v %s", err, env.Data)
}
_, _ = doJSON(t, r, http.MethodGet, "/api/v1/profiles", nil, "")
env, code = doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/users", nil, login.Token)
if code != 200 || env.Code != 0 {
t.Fatalf("list users failed http=%d code=%d msg=%s", code, env.Code, env.Message)
}
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("expected users, got %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",
}, login.Token)
if code != 200 || env.Code != 0 {
t.Fatalf("grant failed http=%d code=%d msg=%s", code, env.Code, env.Message)
}
// Atomicity: membership active AND audit row for same grant.
env, code = doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/users/"+userID, nil, login.Token)
if code != 200 || env.Code != 0 {
t.Fatalf("get user failed http=%d code=%d msg=%s", code, env.Code, env.Message)
}
var detail struct {
Membership struct {
Active bool `json:"active"`
Status string `json:"status"`
} `json:"membership"`
}
if err := json.Unmarshal(env.Data, &detail); err != nil || !detail.Membership.Active {
t.Fatalf("expected active membership after grant: %v %s", err, env.Data)
}
env, code = doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/audit-logs", nil, login.Token)
if code != 200 || env.Code != 0 {
t.Fatalf("audit failed http=%d code=%d msg=%s", code, env.Code, env.Message)
}
var audit struct {
Items []struct {
Action string `json:"action"`
TargetID string `json:"target_id"`
} `json:"items"`
}
if err := json.Unmarshal(env.Data, &audit); err != nil || len(audit.Items) == 0 {
t.Fatalf("expected audit rows: %v %s", err, env.Data)
}
if audit.Items[0].Action != "membership.grant" || audit.Items[0].TargetID != userID {
t.Fatalf("unexpected audit %#v", audit.Items[0])
}
// Logout with lowercase bearer must invalidate session.
env, code = doAdminAuth(t, r, http.MethodPost, "/api/v1/admin/auth/logout", nil, "bearer "+login.Token)
if code != 200 || env.Code != 0 {
t.Fatalf("logout failed http=%d code=%d msg=%s", code, env.Code, env.Message)
}
env, code = doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/me", nil, login.Token)
if code != http.StatusUnauthorized || env.Code == 0 {
t.Fatalf("expected 401 after logout, got http=%d code=%d", code, env.Code)
}
}