import { test, expect } from "@playwright/test"; /** * Admin REST API integration tests (RCA-13) * * These tests call the relay server's /admin/* endpoints directly * via Playwright's APIRequestContext, without a browser. * * Run with: npx playwright test tests/api/ --project=chromium * * Requires: * - Relay server running at BASE_URL (default http://localhost:8100) * - TEST_ADMIN_USER and TEST_ADMIN_PASS env vars (or defaults admin/testpassword123) * * NOTE: These tests assume a clean server state. Run against a dedicated * test instance, not production. */ const API_BASE = process.env.RELAY_BASE_URL ?? "http://localhost:8100"; const ADMIN_USER = process.env.TEST_ADMIN_USER ?? "admin"; const ADMIN_PASS = process.env.TEST_ADMIN_PASS ?? "testpassword123"; let adminToken = ""; test.describe("Admin Auth API", () => { test("POST /admin/auth/login — valid credentials returns JWT", async ({ request }) => { const res = await request.post(`${API_BASE}/admin/auth/login`, { data: { username: ADMIN_USER, password: ADMIN_PASS }, }); expect(res.status()).toBe(200); const body = await res.json(); expect(body).toHaveProperty("token"); expect(typeof body.token).toBe("string"); expect(body.token.length).toBeGreaterThan(10); expect(body).toHaveProperty("expiresAt"); adminToken = body.token; }); test("POST /admin/auth/login — wrong password returns 401", async ({ request }) => { const res = await request.post(`${API_BASE}/admin/auth/login`, { data: { username: ADMIN_USER, password: "wrongpassword" }, }); expect(res.status()).toBe(401); const body = await res.json(); expect(body).toHaveProperty("error"); }); test("POST /admin/auth/login — missing fields returns 400", async ({ request }) => { const res = await request.post(`${API_BASE}/admin/auth/login`, { data: { username: ADMIN_USER }, }); expect(res.status()).toBe(400); }); test("GET /admin/auth/me — valid token returns user info", async ({ request }) => { // Ensure we have a token if (!adminToken) { const login = await request.post(`${API_BASE}/admin/auth/login`, { data: { username: ADMIN_USER, password: ADMIN_PASS }, }); adminToken = (await login.json()).token; } const res = await request.get(`${API_BASE}/admin/auth/me`, { headers: { Authorization: `Bearer ${adminToken}` }, }); expect(res.status()).toBe(200); const body = await res.json(); expect(body).toHaveProperty("username", ADMIN_USER); }); test("GET /admin/auth/me — no token returns 401", async ({ request }) => { const res = await request.get(`${API_BASE}/admin/auth/me`); expect(res.status()).toBe(401); }); test("POST /admin/auth/logout — clears session", async ({ request }) => { const login = await request.post(`${API_BASE}/admin/auth/login`, { data: { username: ADMIN_USER, password: ADMIN_PASS }, }); const token = (await login.json()).token; const res = await request.post(`${API_BASE}/admin/auth/logout`, { headers: { Authorization: `Bearer ${token}` }, }); expect([200, 204]).toContain(res.status()); // Token should now be invalid const me = await request.get(`${API_BASE}/admin/auth/me`, { headers: { Authorization: `Bearer ${token}` }, }); expect(me.status()).toBe(401); }); }); test.describe("Setup API", () => { test("GET /admin/setup/status returns initialised flag", async ({ request }) => { const res = await request.get(`${API_BASE}/admin/setup/status`); expect(res.status()).toBe(200); const body = await res.json(); expect(body).toHaveProperty("initialised"); expect(typeof body.initialised).toBe("boolean"); }); }); test.describe("Dashboard API", () => { test.beforeAll(async ({ request }) => { const login = await request.post(`${API_BASE}/admin/auth/login`, { data: { username: ADMIN_USER, password: ADMIN_PASS }, }); adminToken = (await login.json()).token; }); test("GET /admin/dashboard — returns stats shape", async ({ request }) => { const res = await request.get(`${API_BASE}/admin/dashboard`, { headers: { Authorization: `Bearer ${adminToken}` }, }); expect(res.status()).toBe(200); const body = await res.json(); expect(body).toHaveProperty("devices"); expect(body).toHaveProperty("cookies"); expect(body).toHaveProperty("syncCount"); expect(body).toHaveProperty("uptimeSeconds"); expect(typeof body.syncCount).toBe("number"); expect(typeof body.uptimeSeconds).toBe("number"); }); test("GET /admin/dashboard — unauthenticated returns 401", async ({ request }) => { const res = await request.get(`${API_BASE}/admin/dashboard`); expect(res.status()).toBe(401); }); }); test.describe("Cookies API", () => { test.beforeAll(async ({ request }) => { const login = await request.post(`${API_BASE}/admin/auth/login`, { data: { username: ADMIN_USER, password: ADMIN_PASS }, }); adminToken = (await login.json()).token; }); test("GET /admin/cookies — returns list with pagination fields", async ({ request }) => { const res = await request.get(`${API_BASE}/admin/cookies`, { headers: { Authorization: `Bearer ${adminToken}` }, }); expect(res.status()).toBe(200); const body = await res.json(); expect(body).toHaveProperty("cookies"); expect(Array.isArray(body.cookies)).toBe(true); expect(body).toHaveProperty("total"); expect(typeof body.total).toBe("number"); }); test("GET /admin/cookies?domain=xxx — filters by domain", async ({ request }) => { const res = await request.get(`${API_BASE}/admin/cookies?domain=nonexistent.example`, { headers: { Authorization: `Bearer ${adminToken}` }, }); expect(res.status()).toBe(200); const body = await res.json(); // All returned cookies should match the domain filter for (const cookie of body.cookies) { expect(cookie.domain).toBe("nonexistent.example"); } }); test("DELETE /admin/cookies/:id — removes specific cookie", async ({ request }) => { // First: push a cookie via the device API so we have something to delete // (Depends on RCA-13 admin API — if there's a test cookie fixture, use that) // This test is a placeholder that verifies the endpoint contract: const res = await request.delete(`${API_BASE}/admin/cookies/nonexistent-id`, { headers: { Authorization: `Bearer ${adminToken}` }, }); // 404 for nonexistent, or 200 if the implementation ignores missing IDs expect([200, 404]).toContain(res.status()); }); test("DELETE /admin/cookies — bulk delete requires body", async ({ request }) => { const res = await request.delete(`${API_BASE}/admin/cookies`, { headers: { Authorization: `Bearer ${adminToken}` }, data: { ids: [] }, }); expect([200, 400]).toContain(res.status()); }); test("GET /admin/cookies — unauthenticated returns 401", async ({ request }) => { const res = await request.get(`${API_BASE}/admin/cookies`); expect(res.status()).toBe(401); }); }); test.describe("Devices API", () => { test.beforeAll(async ({ request }) => { const login = await request.post(`${API_BASE}/admin/auth/login`, { data: { username: ADMIN_USER, password: ADMIN_PASS }, }); adminToken = (await login.json()).token; }); test("GET /admin/devices — returns list of devices", async ({ request }) => { const res = await request.get(`${API_BASE}/admin/devices`, { headers: { Authorization: `Bearer ${adminToken}` }, }); expect(res.status()).toBe(200); const body = await res.json(); expect(body).toHaveProperty("devices"); expect(Array.isArray(body.devices)).toBe(true); // Each device should have the expected shape for (const device of body.devices) { expect(device).toHaveProperty("id"); expect(device).toHaveProperty("name"); expect(device).toHaveProperty("platform"); expect(device).toHaveProperty("online"); expect(typeof device.online).toBe("boolean"); } }); test("POST /admin/devices/:id/revoke — returns 404 for unknown device", async ({ request, }) => { const res = await request.post(`${API_BASE}/admin/devices/nonexistent/revoke`, { headers: { Authorization: `Bearer ${adminToken}` }, }); expect([404, 400]).toContain(res.status()); }); test("GET /admin/devices — unauthenticated returns 401", async ({ request }) => { const res = await request.get(`${API_BASE}/admin/devices`); expect(res.status()).toBe(401); }); }); test.describe("Settings API", () => { test.beforeAll(async ({ request }) => { const login = await request.post(`${API_BASE}/admin/auth/login`, { data: { username: ADMIN_USER, password: ADMIN_PASS }, }); adminToken = (await login.json()).token; }); test("GET /admin/settings — returns settings object", async ({ request }) => { const res = await request.get(`${API_BASE}/admin/settings`, { headers: { Authorization: `Bearer ${adminToken}` }, }); expect(res.status()).toBe(200); const body = await res.json(); expect(body).toHaveProperty("sync"); expect(body).toHaveProperty("security"); expect(body).toHaveProperty("appearance"); }); test("PATCH /admin/settings — partial update is accepted", async ({ request }) => { const res = await request.patch(`${API_BASE}/admin/settings`, { headers: { Authorization: `Bearer ${adminToken}` }, data: { sync: { autoSync: true } }, }); expect([200, 204]).toContain(res.status()); }); test("PATCH /admin/settings — unknown fields are ignored or rejected gracefully", async ({ request, }) => { const res = await request.patch(`${API_BASE}/admin/settings`, { headers: { Authorization: `Bearer ${adminToken}` }, data: { unknownField: "value" }, }); expect([200, 204, 400]).toContain(res.status()); }); test("GET /admin/settings — unauthenticated returns 401", async ({ request }) => { const res = await request.get(`${API_BASE}/admin/settings`); expect(res.status()).toBe(401); }); });