gitdataai/admin/tests/03-platform-workspaces.spec.ts
ZhenYi a7e31d5649 feat(tests): add comprehensive Playwright integration tests for all API endpoints
- tests/01: add graceful login timeout handling with try/catch
- tests/02: migrate from isolated request.newContext to uiLogin + page.request pattern
- tests/03: workspace members, platform users CRUD, billing, alert-config
- tests/04: room messages, room list, repo list/detail APIs
- tests/05: project members CRUD, project detail, project billing
- tests/06: API token CRUD, logout, health check
- tests/07: AI provider/model/version/pricing CRUD
- tests/08: admin user CRUD, role CRUD

All tests use consistent checkBackendAvailable() + uiLogin() pattern with
graceful degradation (test.skip) when backend is unreachable.
2026-04-20 22:37:05 +08:00

348 lines
15 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 } from "@playwright/test";
const ADMIN_USER = process.env.ADMIN_TEST_USERNAME || "admin";
const ADMIN_PASS = process.env.ADMIN_TEST_PASSWORD || "admin123";
/**
* 快速检测后端是否可达2秒超时
*/
async function checkBackendAvailable(): Promise<boolean> {
try {
const ctrl = new AbortController();
const id = setTimeout(() => ctrl.abort(), 2000);
const res = await fetch("http://localhost:3001/api/health", { signal: ctrl.signal });
clearTimeout(id);
return res.ok;
} catch {
return false;
}
}
async function uiLogin(page: Parameters<typeof test>[0]): Promise<boolean> {
try {
await page.goto("/login");
await page.fill("input#username", ADMIN_USER);
await page.fill("input#password", ADMIN_PASS);
await page.click('button[type="submit"]');
await page.waitForURL((url) => !url.toString().includes("/login"), { timeout: 8000 });
return true;
} catch {
return false;
}
}
test.describe("Workspace 成员管理 API", () => {
test("GET /api/platform/workspaces/[id]/members 返回成员列表", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const wsRes = await page.request.get("/api/platform/workspaces?pageSize=1");
const wsData = await wsRes.json();
const workspaceId = wsData.workspaces?.[0]?.id;
if (!workspaceId) { test.skip(); }
const res = await page.request.get(`/api/platform/workspaces/${workspaceId}/members`);
expect(res.status()).toBe(200);
const data = await res.json();
expect(Array.isArray(data.members)).toBe(true);
});
test("POST /api/platform/workspaces/[id]/members 添加成员", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const wsRes = await page.request.get("/api/platform/workspaces?pageSize=1");
const wsData = await wsRes.json();
const workspaceId = wsData.workspaces?.[0]?.id;
if (!workspaceId) { test.skip(); }
const usersRes = await page.request.get("/api/platform/users?pageSize=5");
const usersData = await usersRes.json();
const membersRes = await page.request.get(`/api/platform/workspaces/${workspaceId}/members`);
const members = (await membersRes.json()).members || [];
const memberIds = members.map((m: { userId: string }) => m.userId);
const testUser = usersData.users?.find((u: { uid: string }) => !memberIds.includes(u.uid));
if (!testUser) { test.skip(); }
const res = await page.request.post(`/api/platform/workspaces/${workspaceId}/members`, {
data: { userId: testUser.uid, role: "member" },
});
expect([201, 409]).toContain(res.status());
});
test("POST /api/platform/workspaces/[id]/members 缺少 userId 返回 400", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const wsRes = await page.request.get("/api/platform/workspaces?pageSize=1");
const wsData = await wsRes.json();
const workspaceId = wsData.workspaces?.[0]?.id;
if (!workspaceId) { test.skip(); }
const res = await page.request.post(`/api/platform/workspaces/${workspaceId}/members`, {
data: { role: "member" },
});
expect(res.status()).toBe(400);
});
test("POST /api/platform/workspaces/[id]/members 无效角色返回 400", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const wsRes = await page.request.get("/api/platform/workspaces?pageSize=1");
const wsData = await wsRes.json();
const workspaceId = wsData.workspaces?.[0]?.id;
if (!workspaceId) { test.skip(); }
const usersRes = await page.request.get("/api/platform/users?pageSize=5");
const usersData = await usersRes.json();
const testUser = usersData.users?.[0];
if (!testUser) { test.skip(); }
const res = await page.request.post(`/api/platform/workspaces/${workspaceId}/members`, {
data: { userId: testUser.uid, role: "invalid_role" },
});
expect(res.status()).toBe(400);
});
test("POST /api/platform/workspaces/[id]/add-credit 充值成功", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const wsRes = await page.request.get("/api/platform/workspaces?pageSize=1");
const wsData = await wsRes.json();
const workspaceId = wsData.workspaces?.[0]?.id;
if (!workspaceId) { test.skip(); }
const res = await page.request.post(`/api/platform/workspaces/${workspaceId}/add-credit`, {
data: { amount: 0.01, description: "Test credit" },
});
expect(res.status()).toBe(200);
expect((await res.json()).success).toBe(true);
});
test("POST /api/platform/workspaces/[id]/add-credit 金额为0返回400", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const wsRes = await page.request.get("/api/platform/workspaces?pageSize=1");
const wsData = await wsRes.json();
const workspaceId = wsData.workspaces?.[0]?.id;
if (!workspaceId) { test.skip(); }
const res = await page.request.post(`/api/platform/workspaces/${workspaceId}/add-credit`, {
data: { amount: 0 },
});
expect(res.status()).toBe(400);
});
test("POST /api/platform/workspaces/[id]/add-credit 负金额返回400", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const wsRes = await page.request.get("/api/platform/workspaces?pageSize=1");
const wsData = await wsRes.json();
const workspaceId = wsData.workspaces?.[0]?.id;
if (!workspaceId) { test.skip(); }
const res = await page.request.post(`/api/platform/workspaces/${workspaceId}/add-credit`, {
data: { amount: -10 },
});
expect(res.status()).toBe(400);
});
test("GET /api/platform/workspaces/[id]/alert-config 获取告警配置", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const wsRes = await page.request.get("/api/platform/workspaces?pageSize=1");
const wsData = await wsRes.json();
const workspaceId = wsData.workspaces?.[0]?.id;
if (!workspaceId) { test.skip(); }
const res = await page.request.get(`/api/platform/workspaces/${workspaceId}/alert-config`);
expect([200, 404]).toContain(res.status());
});
});
test.describe("Workspace 成员更新/删除 API", () => {
test("PATCH /api/platform/workspaces/[id]/members/[memberId] 更新成员角色", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const wsRes = await page.request.get("/api/platform/workspaces?pageSize=1");
const wsData = await wsRes.json();
const workspaceId = wsData.workspaces?.[0]?.id;
if (!workspaceId) { test.skip(); }
const membersRes = await page.request.get(`/api/platform/workspaces/${workspaceId}/members`);
const members = (await membersRes.json()).members || [];
const nonOwner = members.find((m: { role: string }) => m.role !== "owner");
if (!nonOwner) { test.skip(); }
const res = await page.request.patch(
`/api/platform/workspaces/${workspaceId}/members/${nonOwner.id}`,
{ data: { role: "admin" } }
);
expect([200, 403]).toContain(res.status());
});
test("PATCH /api/platform/workspaces/[id]/members/[memberId] 无效角色返回400", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const wsRes = await page.request.get("/api/platform/workspaces?pageSize=1");
const wsData = await wsRes.json();
const workspaceId = wsData.workspaces?.[0]?.id;
if (!workspaceId) { test.skip(); }
const membersRes = await page.request.get(`/api/platform/workspaces/${workspaceId}/members`);
const members = (await membersRes.json()).members || [];
const nonOwner = members.find((m: { role: string }) => m.role !== "owner");
if (!nonOwner) { test.skip(); }
const res = await page.request.patch(
`/api/platform/workspaces/${workspaceId}/members/${nonOwner.id}`,
{ data: { role: "superadmin" } }
);
expect(res.status()).toBe(400);
});
test("PATCH /api/platform/workspaces/[id]/members/[memberId] 不存在的成员返回404", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const wsRes = await page.request.get("/api/platform/workspaces?pageSize=1");
const wsData = await wsRes.json();
const workspaceId = wsData.workspaces?.[0]?.id;
if (!workspaceId) { test.skip(); }
const res = await page.request.patch(
`/api/platform/workspaces/${workspaceId}/members/nonexistent`,
{ data: { role: "admin" } }
);
expect(res.status()).toBe(404);
});
test("DELETE /api/platform/workspaces/[id]/members/[memberId] 删除非owner成员", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const wsRes = await page.request.get("/api/platform/workspaces?pageSize=1");
const wsData = await wsRes.json();
const workspaceId = wsData.workspaces?.[0]?.id;
if (!workspaceId) { test.skip(); }
const usersRes = await page.request.get("/api/platform/users?pageSize=5");
const usersData = await usersRes.json();
const membersRes = await page.request.get(`/api/platform/workspaces/${workspaceId}/members`);
const members = (await membersRes.json()).members || [];
const memberIds = members.map((m: { userId: string }) => m.userId);
const testUser = usersData.users?.find((u: { uid: string }) => !memberIds.includes(u.uid));
if (!testUser) { test.skip(); }
const addRes = await page.request.post(`/api/platform/workspaces/${workspaceId}/members`, {
data: { userId: testUser.uid, role: "member" },
});
if (addRes.status() !== 201) { test.skip(); }
const updatedMembers = (
await (await page.request.get(`/api/platform/workspaces/${workspaceId}/members`)).json()
).members || [];
const added = updatedMembers.find((m: { userId: string }) => m.userId === testUser.uid);
if (!added) { test.skip(); }
const res = await page.request.delete(
`/api/platform/workspaces/${workspaceId}/members/${added.id}`
);
expect([200, 403]).toContain(res.status());
});
test("DELETE /api/platform/workspaces/[id]/members/[memberId] 不存在的成员返回404", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const wsRes = await page.request.get("/api/platform/workspaces?pageSize=1");
const wsData = await wsRes.json();
const workspaceId = wsData.workspaces?.[0]?.id;
if (!workspaceId) { test.skip(); }
const res = await page.request.delete(
`/api/platform/workspaces/${workspaceId}/members/nonexistent`
);
expect(res.status()).toBe(404);
});
});
test.describe("平台用户管理 API", () => {
test("GET /api/platform/users/[uid] 返回用户详情", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const usersRes = await page.request.get("/api/platform/users?pageSize=1");
const usersData = await usersRes.json();
const uid = usersData.users?.[0]?.uid;
if (!uid) { test.skip(); }
const res = await page.request.get(`/api/platform/users/${uid}`);
expect(res.status()).toBe(200);
expect((await res.json()).user).toBeDefined();
});
test("GET /api/platform/users/[uid] 不存在的用户返回404", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const res = await page.request.get("/api/platform/users/nonexistent-uid-12345");
expect(res.status()).toBe(404);
});
test("PATCH /api/platform/users/[uid] 更新用户信息", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const usersRes = await page.request.get("/api/platform/users?pageSize=1");
const usersData = await usersRes.json();
const uid = usersData.users?.[0]?.uid;
if (!uid) { test.skip(); }
const res = await page.request.patch(`/api/platform/users/${uid}`, {
data: { displayName: "Updated Name" },
});
expect(res.status()).toBe(200);
});
test("PATCH /api/platform/users/[uid] 更新邮箱", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const usersRes = await page.request.get("/api/platform/users?pageSize=1");
const usersData = await usersRes.json();
const uid = usersData.users?.[0]?.uid;
if (!uid) { test.skip(); }
const res = await page.request.patch(`/api/platform/users/${uid}`, {
data: { email: "test@example.com" },
});
expect([200, 500]).toContain(res.status());
});
test("PATCH /api/platform/users/[uid] 密码太短返回400", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const usersRes = await page.request.get("/api/platform/users?pageSize=1");
const usersData = await usersRes.json();
const uid = usersData.users?.[0]?.uid;
if (!uid) { test.skip(); }
const res = await page.request.patch(`/api/platform/users/${uid}`, {
data: { password: "123" },
});
expect(res.status()).toBe(400);
});
test("PATCH /api/platform/users/[uid] 不存在的用户返回404", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const res = await page.request.patch("/api/platform/users/nonexistent-uid-12345", {
data: { displayName: "Test" },
});
expect(res.status()).toBe(404);
});
test("PATCH /api/platform/users 批量启用/禁用用户", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const usersRes = await page.request.get("/api/platform/users?pageSize=1");
const usersData = await usersRes.json();
const uid = usersData.users?.[0]?.uid;
if (!uid) { test.skip(); }
const res = await page.request.patch("/api/platform/users", {
data: { ids: [uid], action: "disable" },
});
expect(res.status()).toBe(200);
expect((await res.json()).success).toBe(true);
});
test("PATCH /api/platform/users 空ids返回400", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const res = await page.request.patch("/api/platform/users", {
data: { ids: [], action: "enable" },
});
expect(res.status()).toBe(400);
});
test("PATCH /api/platform/users 无效action返回400", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const usersRes = await page.request.get("/api/platform/users?pageSize=1");
const usersData = await usersRes.json();
const uid = usersData.users?.[0]?.uid;
if (!uid) { test.skip(); }
const res = await page.request.patch("/api/platform/users", {
data: { ids: [uid], action: "toggle" },
});
expect(res.status()).toBe(400);
});
});