gitdataai/admin/tests/02-admin-api.spec.ts
ZhenYi fb91f5a6c5 feat(admin): add admin panel with billing alerts and model sync
- Add libs/api/admin with admin API endpoints:
  sync models, workspace credit, billing alert check
- Add workspace_alert_config model and alert service
- Add Session::no_op() for background tasks without user context
- Add admin/ Next.js admin panel (AI models, billing, workspaces, audit)
- Start billing alert background task every 30 minutes
2026-04-19 20:48:59 +08:00

357 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { test, expect, request } from "@playwright/test";
const BASE_URL = "http://localhost:3001";
const ADMIN_USER = process.env.ADMIN_TEST_USERNAME || "admin";
const ADMIN_PASS = process.env.ADMIN_TEST_PASSWORD || "admin123";
/**
* 创建已认证的 API context登录获取 session cookie
*/
async function createAuthContext() {
const ctx = await request.newContext({ baseURL: BASE_URL });
const loginRes = await ctx.post("/api/auth/login", {
data: { username: ADMIN_USER, password: ADMIN_PASS },
});
if (!loginRes.ok()) {
throw new Error(`Login failed: ${loginRes.status()} ${await loginRes.text()}`);
}
return ctx;
}
test.describe("Admin 用户管理 API", () => {
let ctx: Awaited<ReturnType<typeof request.newContext>>;
test.beforeAll(async () => {
ctx = await createAuthContext();
});
test.afterAll(async () => {
await ctx.dispose();
});
test("GET /api/users 返回分页用户列表", async () => {
const res = await ctx.get("/api/users");
expect(res.status()).toBe(200);
const data = await res.json();
expect(Array.isArray(data.users)).toBe(true);
expect(typeof data.total).toBe("number");
});
test("GET /api/users 支持分页参数", async () => {
const res = await ctx.get("/api/users?page=1&pageSize=5");
expect(res.status()).toBe(200);
const data = await res.json();
expect(data.users.length).toBeLessThanOrEqual(5);
});
test("GET /api/users 支持搜索", async () => {
const res = await ctx.get("/api/users?search=admin");
expect(res.status()).toBe(200);
const data = await res.json();
expect(Array.isArray(data.users)).toBe(true);
if (data.users.length > 0) {
const names = data.users.map((u: { username: string }) => u.username);
names.forEach((name: string) => {
expect(name.toLowerCase()).toContain("admin");
});
}
});
test("POST /api/users 创建并删除用户", async () => {
const randomUser = `testuser_${Date.now()}`;
const createRes = await ctx.post("/api/users", {
data: { username: randomUser, password: "TestPass123!", roleIds: [] },
});
expect(createRes.status(), await createRes.text()).toBeLessThanOrEqual(201);
const created = await createRes.json();
if (created.user?.id) {
await ctx.delete(`/api/users/${created.user.id}`);
}
});
test("POST /api/users 缺少密码返回错误", async () => {
const randomUser = `testuser_${Date.now()}`;
const res = await ctx.post("/api/users", {
data: { username: randomUser },
});
expect(res.status()).toBeGreaterThanOrEqual(400);
});
test("DELETE /api/users/[id] 删除用户", async () => {
// 先创建用户
const randomUser = `deluser_${Date.now()}`;
const createRes = await ctx.post("/api/users", {
data: { username: randomUser, password: "TestPass123!", roleIds: [] },
});
const created = await createRes.json();
if (!created.user?.id) {
// 无权限,跳过
return;
}
const deleteRes = await ctx.delete(`/api/users/${created.user.id}`);
expect([200, 204]).toContain(deleteRes.status());
});
test("角色列表 API", async () => {
const res = await ctx.get("/api/roles");
expect(res.status()).toBe(200);
const data = await res.json();
expect(Array.isArray(data.roles)).toBe(true);
});
test("权限列表 API", async () => {
const res = await ctx.get("/api/permissions");
expect(res.status()).toBe(200);
const data = await res.json();
expect(Array.isArray(data.permissions)).toBe(true);
});
test("审计日志 API", async () => {
const res = await ctx.get("/api/logs");
expect(res.status()).toBe(200);
const data = await res.json();
expect(Array.isArray(data.logs)).toBe(true);
});
test("GET /api/sessions 返回 Admin 在线会话", async () => {
const res = await ctx.get("/api/sessions");
expect(res.status()).toBe(200);
const data = await res.json();
expect(Array.isArray(data.sessions)).toBe(true);
});
test("GET /api/roles/[id] 返回角色详情(含权限)", async () => {
// 先获取角色列表
const listRes = await ctx.get("/api/roles");
const listData = await listRes.json();
if (!listData.roles?.length) return;
const roleId = listData.roles[0].id;
const res = await ctx.get(`/api/roles/${roleId}`);
expect(res.status()).toBe(200);
const data = await res.json();
expect(data).toHaveProperty("id");
expect(data).toHaveProperty("name");
});
test("POST /api/roles 创建并删除角色", async () => {
const randomRole = `testrole_${Date.now()}`;
const createRes = await ctx.post("/api/roles", {
data: { name: randomRole, description: "Test role" },
});
expect(createRes.status(), await createRes.text()).toBeLessThanOrEqual(201);
const created = await createRes.json();
if (created.id) {
const deleteRes = await ctx.delete(`/api/roles/${created.id}`);
expect([200, 204]).toContain(deleteRes.status());
}
});
});
test.describe("平台数据 API", () => {
let ctx: Awaited<ReturnType<typeof request.newContext>>;
test.beforeAll(async () => {
ctx = await createAuthContext();
});
test.afterAll(async () => {
await ctx.dispose();
});
test("GET /api/platform/stats 返回平台统计", async () => {
const res = await ctx.get("/api/platform/stats");
expect(res.status()).toBe(200);
const data = await res.json();
expect(data).toHaveProperty("stats");
expect(data.stats).toHaveProperty("userCount");
expect(data.stats).toHaveProperty("workspaceCount");
});
test("GET /api/platform/users 返回用户列表", async () => {
const res = await ctx.get("/api/platform/users");
expect(res.status()).toBe(200);
const data = await res.json();
expect(Array.isArray(data.users)).toBe(true);
});
test("GET /api/platform/workspaces 返回 workspace 列表", async () => {
const res = await ctx.get("/api/platform/workspaces");
expect(res.status()).toBe(200);
const data = await res.json();
expect(Array.isArray(data.workspaces) || Array.isArray(data)).toBe(true);
});
test("GET /api/platform/rooms 返回房间列表", async () => {
const res = await ctx.get("/api/platform/rooms");
expect(res.status()).toBe(200);
const data = await res.json();
expect(Array.isArray(data.rooms) || Array.isArray(data)).toBe(true);
});
test("GET /api/platform/repos 返回仓库列表", async () => {
const res = await ctx.get("/api/platform/repos");
expect(res.status()).toBe(200);
const data = await res.json();
expect(Array.isArray(data.repos) || Array.isArray(data)).toBe(true);
});
test("GET /api/platform/activity-stats 返回活动统计", async () => {
const res = await ctx.get("/api/platform/activity-stats");
expect(res.status()).toBe(200);
const data = await res.json();
expect(data).toHaveProperty("dau");
expect(data).toHaveProperty("mau");
});
test("GET /api/platform/audit-logs 返回审计日志", async () => {
const res = await ctx.get("/api/platform/audit-logs");
expect(res.status()).toBe(200);
const data = await res.json();
expect(Array.isArray(data.logs) || Array.isArray(data)).toBe(true);
});
test("GET /api/platform/sessions 返回平台会话", async () => {
const res = await ctx.get("/api/platform/sessions");
expect(res.status()).toBe(200);
const data = await res.json();
expect(Array.isArray(data.sessions) || Array.isArray(data)).toBe(true);
});
test("GET /api/admin/projects 返回项目列表", async () => {
const res = await ctx.get("/api/admin/projects");
expect(res.status()).toBe(200);
const data = await res.json();
expect(Array.isArray(data.projects) || Array.isArray(data)).toBe(true);
expect(typeof data.total).toBe("number");
});
test("GET /api/admin/projects 支持分页参数", async () => {
const res = await ctx.get("/api/admin/projects?page=1&pageSize=5");
expect(res.status()).toBe(200);
const data = await res.json();
expect(data.projects.length).toBeLessThanOrEqual(5);
});
test("GET /api/admin/projects 支持搜索", async () => {
const res = await ctx.get("/api/admin/projects?search=test");
expect(res.status()).toBe(200);
const data = await res.json();
expect(Array.isArray(data.projects)).toBe(true);
});
test("GET /api/platform/ai 返回 AI Provider/Model/定价", async () => {
const res = await ctx.get("/api/platform/ai");
expect(res.status()).toBe(200);
const data = await res.json();
expect(Array.isArray(data.providers)).toBe(true);
expect(Array.isArray(data.models)).toBe(true);
expect(Array.isArray(data.pricing)).toBe(true);
});
test("GET /api/platform/workspaces/[id] 返回 Workspace 详情", async () => {
// 先获取一个 workspace ID
const listRes = await ctx.get("/api/platform/workspaces?pageSize=1");
const listData = await listRes.json();
if (!listData.workspaces?.length) return;
const wsId = listData.workspaces[0].id;
const res = await ctx.get(`/api/platform/workspaces/${wsId}`);
expect(res.status()).toBe(200);
const data = await res.json();
expect(data).toHaveProperty("workspace");
expect(data).toHaveProperty("members");
expect(data).toHaveProperty("projects");
expect(data).toHaveProperty("billingHistory");
});
test("POST /api/platform/ai/sync 同步 AI 模型(需要 Rust 后端配置)", async () => {
const rustUrl = process.env.RUST_BACKEND_URL;
const apiKey = process.env.ADMIN_API_SHARED_KEY;
if (!rustUrl || !apiKey) return;
const res = await ctx.post("/api/platform/ai/sync");
expect([200, 500]).toContain(res.status());
});
test("POST /api/platform/alerts/check 检查告警(需要 Rust 后端配置)", async () => {
const rustUrl = process.env.RUST_BACKEND_URL;
const apiKey = process.env.ADMIN_API_SHARED_KEY;
if (!rustUrl || !apiKey) return;
const res = await ctx.post("/api/platform/alerts/check");
expect([200, 500]).toContain(res.status());
});
test("GET /api/platform/workspaces/[id]/alert-config 获取/保存告警配置", async () => {
const listRes = await ctx.get("/api/platform/workspaces?pageSize=1");
const listData = await listRes.json();
if (!listData.workspaces?.length) return;
const wsId = listData.workspaces[0].id;
// GET
const getRes = await ctx.get(`/api/platform/workspaces/${wsId}/alert-config`);
expect([200, 404]).toContain(getRes.status());
});
test("GET /api/platform/rooms/[id]/messages 返回房间消息", async () => {
const listRes = await ctx.get("/api/platform/rooms?pageSize=1");
const listData = await listRes.json();
if (!listData.rooms?.length) return;
const roomId = listData.rooms[0].id;
const res = await ctx.get(`/api/platform/rooms/${roomId}/messages`);
expect(res.status()).toBe(200);
const data = await res.json();
expect(data).toHaveProperty("messages");
expect(Array.isArray(data.messages)).toBe(true);
});
test("GET /api/admin/projects/[id] 返回项目详情", async () => {
const listRes = await ctx.get("/api/admin/projects?pageSize=1");
const listData = await listRes.json();
if (!listData.projects?.length) return;
const projId = listData.projects[0].id;
const res = await ctx.get(`/api/admin/projects/${projId}`);
expect(res.status()).toBe(200);
const data = await res.json();
expect(data).toHaveProperty("project");
expect(data).toHaveProperty("members");
});
test("GET /api/admin/projects/[id]/billing 返回项目账单信息", async () => {
const listRes = await ctx.get("/api/admin/projects?pageSize=1");
const listData = await listRes.json();
if (!listData.projects?.length) return;
const projId = String(listData.projects[0].id);
const res = await ctx.get(`/api/admin/projects/${projId}/billing`);
// 200 = project found, 404 = project not found (billing table has no entry)
expect([200, 404]).toContain(res.status());
});
test("POST /api/admin/projects/[id]/billing 充值(需要项目 ID", async () => {
const listRes = await ctx.get("/api/admin/projects?pageSize=1");
const listData = await listRes.json();
if (!listData.projects?.length) return;
const projId = String(listData.projects[0].id);
const res = await ctx.post(`/api/admin/projects/${projId}/billing`, {
data: { amount: 0.01, description: "Test credit" },
});
// 200 = ok, 404 = project not found, 500 = server error
expect([200, 404, 500]).toContain(res.status());
});
});
test.describe("中间件权限控制", () => {
test("未登录访问受保护 API 返回 401", async () => {
const freshCtx = await request.newContext({ baseURL: BASE_URL });
try {
const res = await freshCtx.get("/api/users");
expect(res.status()).toBe(401);
} finally {
await freshCtx.dispose();
}
});
test("无效 API Token 返回 401", async ({ request: req }) => {
const res = await req.get(`${BASE_URL}/api/users`, {
headers: { Authorization: "Bearer invalid_token_123" },
});
expect(res.status()).toBe(401);
});
});