- 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.
348 lines
15 KiB
TypeScript
348 lines
15 KiB
TypeScript
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);
|
||
});
|
||
});
|