gitdataai/admin/tests/05-project-members-api.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

302 lines
11 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";
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("项目成员管理 API", () => {
test("GET /api/admin/projects/[id]/members 返回项目成员列表", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const projRes = await page.request.get("/api/admin/projects?pageSize=1");
const projData = await projRes.json();
const projectId = projData.projects?.[0]?.id;
if (!projectId) { test.skip(); }
const res = await page.request.get(`/api/admin/projects/${projectId}/members`);
expect(res.status()).toBe(200);
const data = await res.json();
expect(Array.isArray(data.members)).toBe(true);
});
test("POST /api/admin/projects/[id]/members 添加成员", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const projRes = await page.request.get("/api/admin/projects?pageSize=1");
const projData = await projRes.json();
const projectId = projData.projects?.[0]?.id;
if (!projectId) { 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/admin/projects/${projectId}/members`);
const members = (await membersRes.json()).members || [];
const memberUserIds = members.map((m: { userId: string }) => String(m.userId));
const testUser = usersData.users?.find(
(u: { uid: string }) => !memberUserIds.includes(String(u.uid))
);
if (!testUser) { test.skip(); }
const res = await page.request.post(`/api/admin/projects/${projectId}/members`, {
data: { userId: testUser.uid, scope: "member" },
});
expect([201, 409]).toContain(res.status());
});
test("POST /api/admin/projects/[id]/members 缺少 userId 返回 400", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const projRes = await page.request.get("/api/admin/projects?pageSize=1");
const projData = await projRes.json();
const projectId = projData.projects?.[0]?.id;
if (!projectId) { test.skip(); }
const res = await page.request.post(`/api/admin/projects/${projectId}/members`, {
data: { scope: "member" },
});
expect(res.status()).toBe(400);
});
test("POST /api/admin/projects/[id]/members 无效 scope 返回 400", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const projRes = await page.request.get("/api/admin/projects?pageSize=1");
const projData = await projRes.json();
const projectId = projData.projects?.[0]?.id;
if (!projectId) { 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/admin/projects/${projectId}/members`, {
data: { userId: testUser.uid, scope: "superadmin" },
});
expect(res.status()).toBe(400);
});
test("PATCH /api/admin/projects/[id]/members/[memberId] 更新成员角色", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const projRes = await page.request.get("/api/admin/projects?pageSize=1");
const projData = await projRes.json();
const projectId = projData.projects?.[0]?.id;
if (!projectId) { test.skip(); }
const membersRes = await page.request.get(`/api/admin/projects/${projectId}/members`);
const members = (await membersRes.json()).members || [];
const nonOwner = members.find((m: { scope: string }) => m.scope !== "owner");
if (!nonOwner) { test.skip(); }
const res = await page.request.patch(
`/api/admin/projects/${projectId}/members/${nonOwner.id}`,
{ data: { scope: "admin" } }
);
expect([200, 403]).toContain(res.status());
});
test("PATCH /api/admin/projects/[id]/members/[memberId] 无效角色返回400", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const projRes = await page.request.get("/api/admin/projects?pageSize=1");
const projData = await projRes.json();
const projectId = projData.projects?.[0]?.id;
if (!projectId) { test.skip(); }
const membersRes = await page.request.get(`/api/admin/projects/${projectId}/members`);
const members = (await membersRes.json()).members || [];
const nonOwner = members.find((m: { scope: string }) => m.scope !== "owner");
if (!nonOwner) { test.skip(); }
const res = await page.request.patch(
`/api/admin/projects/${projectId}/members/${nonOwner.id}`,
{ data: { scope: "invalid" } }
);
expect(res.status()).toBe(400);
});
test("PATCH /api/admin/projects/[id]/members/[memberId] 不存在的成员返回404", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const projRes = await page.request.get("/api/admin/projects?pageSize=1");
const projData = await projRes.json();
const projectId = projData.projects?.[0]?.id;
if (!projectId) { test.skip(); }
const res = await page.request.patch(
`/api/admin/projects/${projectId}/members/nonexistent`,
{ data: { scope: "admin" } }
);
expect(res.status()).toBe(404);
});
test("DELETE /api/admin/projects/[id]/members/[memberId] 删除非owner成员", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const projRes = await page.request.get("/api/admin/projects?pageSize=1");
const projData = await projRes.json();
const projectId = projData.projects?.[0]?.id;
if (!projectId) { 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/admin/projects/${projectId}/members`);
const members = (await membersRes.json()).members || [];
const memberUserIds = members.map((m: { userId: string }) => String(m.userId));
const testUser = usersData.users?.find(
(u: { uid: string }) => !memberUserIds.includes(String(u.uid))
);
if (!testUser) { test.skip(); }
const addRes = await page.request.post(`/api/admin/projects/${projectId}/members`, {
data: { userId: testUser.uid, scope: "member" },
});
if (addRes.status() !== 201) { test.skip(); }
const updatedMembers = (
await (await page.request.get(`/api/admin/projects/${projectId}/members`)).json()
).members || [];
const added = updatedMembers.find(
(m: { userId: string }) => String(m.userId) === String(testUser.uid)
);
if (!added) { test.skip(); }
const res = await page.request.delete(
`/api/admin/projects/${projectId}/members/${added.id}`
);
expect([200, 403]).toContain(res.status());
});
test("DELETE /api/admin/projects/[id]/members/[memberId] 不存在的成员返回404", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const projRes = await page.request.get("/api/admin/projects?pageSize=1");
const projData = await projRes.json();
const projectId = projData.projects?.[0]?.id;
if (!projectId) { test.skip(); }
const res = await page.request.delete(
`/api/admin/projects/${projectId}/members/nonexistent`
);
expect(res.status()).toBe(404);
});
});
test.describe("项目详情与计费 API", () => {
test("GET /api/admin/projects/[id] 返回项目详情", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const projRes = await page.request.get("/api/admin/projects?pageSize=1");
const projData = await projRes.json();
const projectId = projData.projects?.[0]?.id;
if (!projectId) { test.skip(); }
const res = await page.request.get(`/api/admin/projects/${projectId}`);
expect(res.status()).toBe(200);
const data = await res.json();
expect(data).toHaveProperty("project");
expect(data).toHaveProperty("members");
expect(Array.isArray(data.members)).toBe(true);
});
test("GET /api/admin/projects/[id] 不存在的项目返回404", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const res = await page.request.get("/api/admin/projects/nonexistent-proj-id");
expect(res.status()).toBe(404);
});
test("GET /api/admin/projects/[id]/billing 返回项目账单", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const projRes = await page.request.get("/api/admin/projects?pageSize=1");
const projData = await projRes.json();
const projectId = projData.projects?.[0]?.id;
if (!projectId) { test.skip(); }
const res = await page.request.get(`/api/admin/projects/${projectId}/billing`);
expect([200, 404]).toContain(res.status());
});
test("POST /api/admin/projects/[id]/billing 充值", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const projRes = await page.request.get("/api/admin/projects?pageSize=1");
const projData = await projRes.json();
const projectId = projData.projects?.[0]?.id;
if (!projectId) { test.skip(); }
const res = await page.request.post(`/api/admin/projects/${projectId}/billing`, {
data: { amount: 0.01, description: "Test credit" },
});
expect([200, 404, 500]).toContain(res.status());
});
test("POST /api/admin/projects/[id]/billing 缺少金额返回错误", async ({ page }) => {
if (!await checkBackendAvailable()) { test.skip(); }
if (!await uiLogin(page)) { test.skip(); }
const projRes = await page.request.get("/api/admin/projects?pageSize=1");
const projData = await projRes.json();
const projectId = projData.projects?.[0]?.id;
if (!projectId) { test.skip(); }
const res = await page.request.post(`/api/admin/projects/${projectId}/billing`, {
data: { description: "Test" },
});
expect(res.status()).toBeGreaterThanOrEqual(400);
});
});