feat(M5): 前端完善 + 部署 — Vue3 全量页面、仪表盘统计与我的审计接口、systemd/sudoers/迁移脚本、部署文档
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestAPIStats 仪表盘统计:admin 登录后 /admin/stats 返回各状态数与待审批数。
|
||||
func TestAPIStats(t *testing.T) {
|
||||
app := setupTestApp(t)
|
||||
|
||||
w := app.doJSON(http.MethodGet, "/api/v1/admin/stats", nil)
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("unauth stats status = %d, want 401", w.Code)
|
||||
}
|
||||
|
||||
w = app.doJSON(http.MethodPost, "/api/v1/auth/admin/login", map[string]string{"username": "root", "password": "Passw0rd"})
|
||||
ck := sessionCookie(t, w)
|
||||
|
||||
// 建 1 个用户 + 1 个待审批申请,stats 应反映
|
||||
w = app.doJSON(http.MethodPost, "/api/v1/users", map[string]any{
|
||||
"username": "m5stats", "email": "m5stats@example.com",
|
||||
"supervisor": "profA", "purpose": "test",
|
||||
}, ck)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("create user status = %d, body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
w = app.doJSON(http.MethodPost, "/api/v1/approvals", map[string]any{
|
||||
"username": "m5apply", "email": "m5apply@example.com",
|
||||
"supervisor": "profB", "purpose": "apply test",
|
||||
})
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("submit approval status = %d, body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
w = app.doJSON(http.MethodGet, "/api/v1/admin/stats", nil, ck)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("stats status = %d, body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
m := decodeBody(t, w)
|
||||
data := m["data"].(map[string]any)
|
||||
if data["total"].(float64) != 1 {
|
||||
t.Fatalf("stats total = %v, want 1", data["total"])
|
||||
}
|
||||
if data["pending_approvals"].(float64) != 1 {
|
||||
t.Fatalf("pending_approvals = %v, want 1", data["pending_approvals"])
|
||||
}
|
||||
if _, ok := data["expiring_soon"]; !ok {
|
||||
t.Fatalf("stats missing expiring_soon: %v", data)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAPIMeAudit 外部用户"我的审计":登录后可查自己的审计痕迹,管理端接口拒绝 user 会话。
|
||||
func TestAPIMeAudit(t *testing.T) {
|
||||
app := setupTestApp(t)
|
||||
|
||||
// 建用户 + 通过 OTP 登录(复用 m3 流程)
|
||||
adminW := app.doJSON(http.MethodPost, "/api/v1/auth/admin/login", map[string]string{"username": "root", "password": "Passw0rd"})
|
||||
ack := sessionCookie(t, adminW)
|
||||
adminW = app.doJSON(http.MethodPost, "/api/v1/users", map[string]any{
|
||||
"username": "meaudit", "email": "meaudit@example.com",
|
||||
}, ack)
|
||||
if adminW.Code != http.StatusOK {
|
||||
t.Fatalf("create user status = %d, body=%s", adminW.Code, adminW.Body.String())
|
||||
}
|
||||
|
||||
// OTP 双通道:经 store 取验证码(模拟用户看到图形验证码)+ 登录
|
||||
|
||||
// OTP 双通道:经 store 取验证码(模拟用户看到图形验证码)+ 登录
|
||||
cap, err := app.captchas.New()
|
||||
if err != nil {
|
||||
t.Fatalf("captcha new: %v", err)
|
||||
}
|
||||
sendW := app.doJSON(http.MethodPost, "/api/v1/auth/otp/send", map[string]string{
|
||||
"username": "meaudit", "captcha_id": cap.ID, "captcha_code": cap.Text,
|
||||
})
|
||||
if sendW.Code != http.StatusOK {
|
||||
t.Fatalf("otp send status = %d, body=%s", sendW.Code, sendW.Body.String())
|
||||
}
|
||||
code, err := app.otps.Current(context.Background(), "ext_meaudit")
|
||||
if err != nil {
|
||||
t.Fatalf("get otp: %v", err)
|
||||
}
|
||||
loginW := app.doJSON(http.MethodPost, "/api/v1/auth/otp/login", map[string]string{"username": "meaudit", "code": code})
|
||||
if loginW.Code != http.StatusOK {
|
||||
t.Fatalf("otp login status = %d, body=%s", loginW.Code, loginW.Body.String())
|
||||
}
|
||||
uck := sessionCookie(t, loginW)
|
||||
|
||||
// 用户访问管理端审计 → 403
|
||||
w := app.doJSON(http.MethodGet, "/api/v1/audit", nil, uck)
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Fatalf("user access admin audit status = %d, want 403", w.Code)
|
||||
}
|
||||
|
||||
// 我的审计:登录成功等操作应可见
|
||||
w = app.doJSON(http.MethodGet, "/api/v1/me/audit", nil, uck)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("me/audit status = %d, body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
m := decodeBody(t, w)
|
||||
data := m["data"].(map[string]any)
|
||||
if data["total"].(float64) < 1 {
|
||||
t.Fatalf("me/audit total = %v, want >= 1", data["total"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user