test(web): add Playwright E2E and admin API test suite for RCA-19
Prepares the full QA test infrastructure for the admin frontend before all prerequisite feature tasks (RCA-12–18) are complete. - playwright.config.ts: 6 browser/device projects (Chromium, Firefox, WebKit, mobile Chrome, mobile Safari, tablet) - tests/e2e/01-login.spec.ts: login form, route guards, setup wizard - tests/e2e/02-dashboard.spec.ts: stats cards, device list, quick actions - tests/e2e/03-cookies.spec.ts: cookie list, search, detail panel, delete - tests/e2e/04-devices.spec.ts: device cards, revoke flow, status filter - tests/e2e/05-settings.spec.ts: three-tab layout, save/error toasts - tests/e2e/06-responsive.spec.ts: no horizontal scroll on mobile/tablet - tests/api/admin-api.spec.ts: REST API contract tests for all /admin/* endpoints - helpers/auth.ts: loginViaUI + loginViaAPI helpers - helpers/mock-api.ts: route intercept fixtures for all pages Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
273
web/tests/api/admin-api.spec.ts
Normal file
273
web/tests/api/admin-api.spec.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user