Files
SnapOtter/tests/e2e/gui-settings-rbac.spec.ts
T
SnapOtterandGitHub 6917a8b0c7 fix(test): repair integration suite after analytics column/endpoint removal (#340)
* fix(test): repair integration suite after analytics column/endpoint removal

#336 moved analytics to a build-time bake: migration 0005 dropped the
users.analytics_enabled and analytics_consent_* columns and removed the
PUT /api/v1/user/analytics endpoint. Two integration tests were left
referencing the old shape and went red on main (13 failures):

- migrate-from-sqlite.test.ts built 1.x SQLite fixtures whose users table
  declared the analytics columns. The generic SELECT *-based importer then
  tried to INSERT them into the 2.0 target, which no longer has those
  columns, failing with Postgres 42703 and rolling back the whole import
  (cascading to all 12 assertions). 1.x never had analytics columns, so the
  fixtures are corrected to drop them. Also removed the now-dead analytics
  entries from the importer's TS/BOOL conversion sets.

- analytics.test.ts asserted the removed PUT endpoint returns 404 but sent
  the request unauthenticated, so the global auth preHandler answered 401
  first. It now authenticates, reaching Fastify's not-found handler (404).

Also removed the stale /api/v1/user/analytics path from openapi.yaml.

Verified locally: full platform integration bucket 1029 passed / 0 failed;
monorepo typecheck clean.

* test(e2e): drop orphaned analytics-consent dismissal calls

#336 deleted the entire analytics consent system (consent page, consent
module, and PUT /api/v1/user/analytics), but six tests/e2e files still
PUT to that removed endpoint to 'dismiss analytics consent.' The calls
were silent no-ops (Playwright request.put / fetch don't throw on 4xx),
so they passed while hitting a dead route.

There is no consent prompt to dismiss anymore, so remove the calls:
- auth.setup.ts / qa-auth.setup.ts: keep the waitForFunction that syncs on
  login completion, drop the now-unused token capture, the dead PUT, and
  the stale 'consent guard' comments.
- rbac / rbac-full / gui-settings-rbac / gui-settings-expanded specs: the
  re-login blocks existed solely to obtain a token for the PUT (reLoginData
  was used nowhere else and the block was the tail of each helper), so
  remove the whole block. The meaningful create-user/login/change-password
  work is untouched.

Verified: no /api/v1/user/analytics refs remain in tests/e2e; biome clean
(no unused vars).
2026-06-24 13:30:18 +08:00

748 lines
28 KiB
TypeScript

import { test as base, expect } from "@playwright/test";
import { getTestImagePath, login, openSettings } from "./helpers";
const API = process.env.API_URL || "http://localhost:13490";
const UID = Date.now().toString(36);
const EDITOR_USER = `guieditor-${UID}`;
const EDITOR_PASS = "EditorPass1";
const USER_USER = `guiuser-${UID}`;
const USER_PASS = "UserPass1";
/** Auth header only (GET, DELETE). */
function authOnly(token: string): Record<string, string> {
return { Authorization: `Bearer ${token}` };
}
/** Auth + JSON content-type (POST, PUT). */
function authJson(token: string): Record<string, string> {
return { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
}
async function getAdminToken(): Promise<string> {
const res = await fetch(`${API}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "admin", password: "admin" }),
});
const data = await res.json();
return data.token;
}
/**
* Create a user with a given role and clear mustChangePassword
* so the browser login redirects to "/" instead of "/change-password".
*/
async function createReadyUser(
adminToken: string,
username: string,
password: string,
role: string,
): Promise<void> {
const createRes = await fetch(`${API}/api/auth/register`, {
method: "POST",
headers: authJson(adminToken),
body: JSON.stringify({ username, password, role }),
});
if (createRes.status !== 201 && createRes.status !== 409) {
throw new Error(`Failed to create user ${username}: ${createRes.status}`);
}
// Login to get token, then change password to clear mustChangePassword
const loginRes = await fetch(`${API}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
if (!loginRes.ok) throw new Error(`Login failed for ${username}: ${loginRes.status}`);
const loginData = await loginRes.json();
await fetch(`${API}/api/auth/change-password`, {
method: "POST",
headers: authJson(loginData.token),
body: JSON.stringify({ currentPassword: password, newPassword: password }),
});
}
/** Delete a user by username if it exists. */
async function deleteUser(adminToken: string, username: string): Promise<void> {
const listRes = await fetch(`${API}/api/auth/users`, {
headers: authOnly(adminToken),
});
if (!listRes.ok) return;
const { users } = await listRes.json();
const found = users.find((u: { username: string }) => u.username === username);
if (found) {
await fetch(`${API}/api/auth/users/${found.id}`, {
method: "DELETE",
headers: authOnly(adminToken),
});
}
}
// ---------------------------------------------------------------------------
// RBAC role visibility verification for settings dialog tabs
// ---------------------------------------------------------------------------
// The NAV_ITEMS and their required permissions from the source:
// general - none
// system - settings:write
// security - none
// people - users:manage
// teams - teams:manage
// roles - users:manage
// audit-log - audit:read
// api-keys - none
// ai-features - settings:write
// tools - none
// analytics - none (Product Analytics)
// about - none
base.describe("RBAC Settings Visibility - Admin", () => {
base.use({ storageState: ".playwright/.auth/user.json" });
base.test("admin sees all settings tabs including admin-only ones", async ({ page }) => {
await page.goto("/");
await openSettings(page);
// Tabs visible to all roles
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
await expect(page.getByRole("button", { name: /security/i })).toBeVisible();
await expect(page.getByRole("button", { name: /api keys/i })).toBeVisible();
await expect(page.getByRole("button", { name: /tools/i })).toBeVisible();
await expect(page.getByRole("button", { name: /product analytics/i })).toBeVisible();
await expect(page.getByRole("button", { name: /about/i })).toBeVisible();
// Admin-only tabs (require settings:write, users:manage, teams:manage, audit:read)
await expect(page.getByRole("button", { name: /system settings/i })).toBeVisible();
await expect(page.getByRole("button", { name: /people/i })).toBeVisible();
await expect(page.getByRole("button", { name: /teams/i })).toBeVisible();
await expect(page.getByRole("button", { name: /^roles$/i })).toBeVisible();
await expect(page.getByRole("button", { name: /audit log/i })).toBeVisible();
await expect(page.getByRole("button", { name: /ai features/i })).toBeVisible();
});
base.test("admin can navigate to People tab and see user table", async ({ page }) => {
await page.goto("/");
await openSettings(page);
await page.getByRole("button", { name: /people/i }).click();
await expect(page.getByText(/\d+ users?/)).toBeVisible({ timeout: 5_000 });
await expect(page.getByText("admin").first()).toBeVisible();
});
base.test("admin can navigate to Audit Log tab and see entries", async ({ page }) => {
await page.goto("/");
await openSettings(page);
await page.getByRole("button", { name: /audit log/i }).click();
await expect(page.locator("h3").filter({ hasText: "Audit Log" })).toBeVisible();
// Filter dropdown should be present
await expect(
page.locator("select").filter({ has: page.locator("option[value='']") }),
).toBeVisible();
});
base.test("admin sees all 12 nav items", async ({ page }) => {
await page.goto("/");
await openSettings(page);
// Count the navigation buttons in the settings dialog sidebar
const navButtons = page.locator(".w-48 button");
const count = await navButtons.count();
expect(count).toBe(12);
});
base.test("admin can navigate to System Settings and see configuration", async ({ page }) => {
await page.goto("/");
await openSettings(page);
await page.getByRole("button", { name: /system settings/i }).click();
await expect(page.locator("h3").filter({ hasText: "System Settings" })).toBeVisible();
await expect(page.getByText("File Upload Limit (MB)")).toBeVisible();
await expect(page.getByText("Default Theme")).toBeVisible();
});
base.test("admin can navigate to Teams tab and see team list", async ({ page }) => {
await page.goto("/");
await openSettings(page);
await page.getByRole("button", { name: /teams/i }).click();
await expect(page.locator("h3").filter({ hasText: "Teams" })).toBeVisible();
await expect(page.getByText("Default").first()).toBeVisible();
});
base.test("admin can navigate to Roles tab and see built-in roles", async ({ page }) => {
await page.goto("/");
await openSettings(page);
await page.getByRole("button", { name: /^roles$/i }).click();
await expect(page.locator("h3").filter({ hasText: "Roles" })).toBeVisible();
await expect(page.getByText("Built-in").first()).toBeVisible();
});
base.test("admin can navigate to AI Features tab", async ({ page }) => {
await page.goto("/");
await openSettings(page);
await page.getByRole("button", { name: /ai features/i }).click();
await expect(page.locator("h3").filter({ hasText: "AI Features" })).toBeVisible();
});
base.test("admin has full API access to admin endpoints", async ({ page }) => {
await page.goto("/");
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
const bearerToken = token as string;
// GET /api/auth/users requires users:manage
const usersRes = await fetch(`${API}/api/auth/users`, {
headers: { Authorization: `Bearer ${bearerToken}` },
});
expect(usersRes.status).toBe(200);
// GET /api/v1/settings requires settings:read
const settingsRes = await fetch(`${API}/api/v1/settings`, {
headers: { Authorization: `Bearer ${bearerToken}` },
});
expect(settingsRes.status).toBe(200);
// GET /api/v1/audit-log requires audit:read
const auditRes = await fetch(`${API}/api/v1/audit-log`, {
headers: { Authorization: `Bearer ${bearerToken}` },
});
expect(auditRes.status).toBe(200);
});
});
base.describe("RBAC Settings Visibility - Editor", () => {
let adminToken: string;
base.beforeAll(async () => {
adminToken = await getAdminToken();
await createReadyUser(adminToken, EDITOR_USER, EDITOR_PASS, "editor");
});
base.afterAll(async () => {
await deleteUser(adminToken, EDITOR_USER);
});
base.test(
"editor sees general, security, api-keys, tools, analytics, about",
async ({ page }) => {
await login(page, EDITOR_USER, EDITOR_PASS);
await openSettings(page);
// Should see these tabs
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
await expect(page.getByRole("button", { name: /security/i })).toBeVisible();
await expect(page.getByRole("button", { name: /api keys/i })).toBeVisible();
await expect(page.getByRole("button", { name: /tools/i })).toBeVisible();
await expect(page.getByRole("button", { name: /product analytics/i })).toBeVisible();
await expect(page.getByRole("button", { name: /about/i })).toBeVisible();
},
);
base.test(
"editor does NOT see system settings, people, teams, roles, audit log, ai features",
async ({ page }) => {
await login(page, EDITOR_USER, EDITOR_PASS);
await openSettings(page);
// Wait for dialog to fully render
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
// Should NOT see admin-only tabs
await expect(page.getByRole("button", { name: /system settings/i })).not.toBeVisible();
await expect(page.getByRole("button", { name: /people/i })).not.toBeVisible();
await expect(page.getByRole("button", { name: /teams/i })).not.toBeVisible();
await expect(page.getByRole("button", { name: /^roles$/i })).not.toBeVisible();
await expect(page.getByRole("button", { name: /audit log/i })).not.toBeVisible();
await expect(page.getByRole("button", { name: /ai features/i })).not.toBeVisible();
},
);
base.test("editor sees exactly 6 nav items", async ({ page }) => {
await login(page, EDITOR_USER, EDITOR_PASS);
await openSettings(page);
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
const navButtons = page.locator(".w-48 button");
const count = await navButtons.count();
expect(count).toBe(6);
});
base.test("editor can access Security tab and see change password form", async ({ page }) => {
await login(page, EDITOR_USER, EDITOR_PASS);
await openSettings(page);
await page.getByRole("button", { name: /security/i }).click();
await expect(page.getByText("Change Password").first()).toBeVisible();
await expect(page.getByPlaceholder("Current Password")).toBeVisible();
});
base.test("editor can access API Keys tab and generate a key", async ({ page }) => {
await login(page, EDITOR_USER, EDITOR_PASS);
await openSettings(page);
await page.getByRole("button", { name: /api keys/i }).click();
await expect(page.getByRole("button", { name: /generate api key/i })).toBeVisible();
});
base.test("editor can access Tools tab and see tool toggles", async ({ page }) => {
await login(page, EDITOR_USER, EDITOR_PASS);
await openSettings(page);
await page.getByRole("button", { name: /tools/i }).click();
await expect(page.locator("h3").filter({ hasText: "Tools" }).first()).toBeVisible();
await expect(page.getByText(/\d+ tools? disabled/)).toBeVisible({ timeout: 5_000 });
});
base.test("editor can access Product Analytics tab", async ({ page }) => {
await login(page, EDITOR_USER, EDITOR_PASS);
await openSettings(page);
await page.getByRole("button", { name: /product analytics/i }).click();
await expect(page.getByText("Product Analytics").first()).toBeVisible();
await expect(page.getByText(/share anonymous usage data/i)).toBeVisible();
});
base.test("editor General tab shows correct username and role", async ({ page }) => {
await login(page, EDITOR_USER, EDITOR_PASS);
await openSettings(page);
// General is the default tab
await expect(page.getByText(EDITOR_USER)).toBeVisible({ timeout: 5_000 });
await expect(page.getByText("editor").first()).toBeVisible();
});
base.test("editor gets 403 on admin API endpoints", async ({ page }) => {
await login(page, EDITOR_USER, EDITOR_PASS);
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
const bearerToken = token as string;
// GET /api/auth/users requires users:manage -- editor does not have this
const usersRes = await fetch(`${API}/api/auth/users`, {
headers: { Authorization: `Bearer ${bearerToken}` },
});
expect(usersRes.status).toBe(403);
// PUT /api/v1/settings requires settings:write -- editor does not have this
const settingsRes = await fetch(`${API}/api/v1/settings`, {
method: "PUT",
headers: {
Authorization: `Bearer ${bearerToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ testSetting: "hacked" }),
});
expect(settingsRes.status).toBe(403);
// GET /api/v1/audit-log requires audit:read -- editor does not have this
const auditRes = await fetch(`${API}/api/v1/audit-log`, {
headers: { Authorization: `Bearer ${bearerToken}` },
});
expect(auditRes.status).toBe(403);
});
});
base.describe("RBAC Settings Visibility - User", () => {
let adminToken: string;
base.beforeAll(async () => {
adminToken = await getAdminToken();
await createReadyUser(adminToken, USER_USER, USER_PASS, "user");
});
base.afterAll(async () => {
await deleteUser(adminToken, USER_USER);
});
base.test("user sees general, security, api-keys, tools, analytics, about", async ({ page }) => {
await login(page, USER_USER, USER_PASS);
await openSettings(page);
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
await expect(page.getByRole("button", { name: /security/i })).toBeVisible();
await expect(page.getByRole("button", { name: /api keys/i })).toBeVisible();
await expect(page.getByRole("button", { name: /tools/i })).toBeVisible();
await expect(page.getByRole("button", { name: /product analytics/i })).toBeVisible();
await expect(page.getByRole("button", { name: /about/i })).toBeVisible();
});
base.test(
"user does NOT see system settings, people, teams, roles, audit log, ai features",
async ({ page }) => {
await login(page, USER_USER, USER_PASS);
await openSettings(page);
// Wait for dialog to fully render
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
await expect(page.getByRole("button", { name: /system settings/i })).not.toBeVisible();
await expect(page.getByRole("button", { name: /people/i })).not.toBeVisible();
await expect(page.getByRole("button", { name: /teams/i })).not.toBeVisible();
await expect(page.getByRole("button", { name: /^roles$/i })).not.toBeVisible();
await expect(page.getByRole("button", { name: /audit log/i })).not.toBeVisible();
await expect(page.getByRole("button", { name: /ai features/i })).not.toBeVisible();
},
);
base.test("user sees exactly 6 nav items", async ({ page }) => {
await login(page, USER_USER, USER_PASS);
await openSettings(page);
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
const navButtons = page.locator(".w-48 button");
const count = await navButtons.count();
expect(count).toBe(6);
});
base.test("user can access About tab and see version", async ({ page }) => {
await login(page, USER_USER, USER_PASS);
await openSettings(page);
await page.getByRole("button", { name: /about/i }).click();
await expect(page.locator("h3").filter({ hasText: "About" })).toBeVisible();
await expect(page.getByText("Version:")).toBeVisible();
});
base.test("user General tab shows correct username and role", async ({ page }) => {
await login(page, USER_USER, USER_PASS);
await openSettings(page);
// General is the default tab; should show the user's username and role
await expect(page.getByText(USER_USER)).toBeVisible({ timeout: 5_000 });
await expect(page.getByText("user").first()).toBeVisible();
});
base.test("user can access Tools tab and see tool toggles", async ({ page }) => {
await login(page, USER_USER, USER_PASS);
await openSettings(page);
await page.getByRole("button", { name: /tools/i }).click();
await expect(page.locator("h3").filter({ hasText: "Tools" }).first()).toBeVisible();
await expect(page.getByText(/\d+ tools? disabled/)).toBeVisible({ timeout: 5_000 });
});
base.test("user can access Security tab and change password form", async ({ page }) => {
await login(page, USER_USER, USER_PASS);
await openSettings(page);
await page.getByRole("button", { name: /security/i }).click();
await expect(page.getByText("Change Password").first()).toBeVisible();
await expect(page.getByPlaceholder("Current Password")).toBeVisible();
});
base.test("user can access Product Analytics tab", async ({ page }) => {
await login(page, USER_USER, USER_PASS);
await openSettings(page);
await page.getByRole("button", { name: /product analytics/i }).click();
await expect(page.getByText("Product Analytics").first()).toBeVisible();
});
base.test("user gets 403 on admin and editor API endpoints", async ({ page }) => {
await login(page, USER_USER, USER_PASS);
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
const bearerToken = token as string;
// GET /api/auth/users requires users:manage
const usersRes = await fetch(`${API}/api/auth/users`, {
headers: { Authorization: `Bearer ${bearerToken}` },
});
expect(usersRes.status).toBe(403);
// PUT /api/v1/settings requires settings:write
const settingsRes = await fetch(`${API}/api/v1/settings`, {
method: "PUT",
headers: {
Authorization: `Bearer ${bearerToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ testSetting: "hacked" }),
});
expect(settingsRes.status).toBe(403);
// GET /api/v1/audit-log requires audit:read
const auditRes = await fetch(`${API}/api/v1/audit-log`, {
headers: { Authorization: `Bearer ${bearerToken}` },
});
expect(auditRes.status).toBe(403);
// GET /api/v1/teams requires teams:manage
const teamsRes = await fetch(`${API}/api/v1/teams`, {
headers: { Authorization: `Bearer ${bearerToken}` },
});
expect(teamsRes.status).toBe(403);
});
base.test("user can still navigate to a tool page and use it", async ({ page }) => {
await login(page, USER_USER, USER_PASS);
// Navigate to the resize tool page -- user role should have tools:use permission
await page.goto("/image/resize");
await page.waitForLoadState("networkidle");
// The tool page should load (not redirect or show a 403)
// Look for the dropzone or tool heading
const dropzone = page.locator("[class*='border-dashed']");
const toolHeading = page.getByText("Resize").first();
const dropzoneVisible = await dropzone.isVisible().catch(() => false);
const headingVisible = await toolHeading.isVisible().catch(() => false);
expect(dropzoneVisible || headingVisible).toBe(true);
});
base.test("user can upload an image to a tool page", async ({ page }) => {
await login(page, USER_USER, USER_PASS);
// Navigate to the resize tool
await page.goto("/image/resize");
await page.waitForLoadState("networkidle");
// Upload a test image via the file chooser
const dropzone = page.locator("[class*='border-dashed']").first();
const dropzoneVisible = await dropzone.isVisible().catch(() => false);
if (dropzoneVisible) {
const fileChooserPromise = page.waitForEvent("filechooser");
await dropzone.click();
const fileChooser = await fileChooserPromise;
const testImagePath = getTestImagePath();
await fileChooser.setFiles(testImagePath);
// Wait for the upload to register (the image preview should appear)
await page.waitForTimeout(1_000);
// Verify the image was accepted (a download or process button should appear,
// or the filename should show in the UI)
const hasProcessButton = await page
.getByRole("button", { name: /process|download|resize/i })
.first()
.isVisible()
.catch(() => false);
const hasImagePreview = await page
.locator("img")
.first()
.isVisible()
.catch(() => false);
expect(hasProcessButton || hasImagePreview).toBe(true);
}
});
base.test("user can access API Keys tab", async ({ page }) => {
await login(page, USER_USER, USER_PASS);
await openSettings(page);
await page.getByRole("button", { name: /api keys/i }).click();
await expect(page.locator("h3").filter({ hasText: "API Keys" })).toBeVisible();
await expect(page.getByRole("button", { name: /generate api key/i })).toBeVisible();
});
base.test("user gets 403 on roles endpoint", async ({ page }) => {
await login(page, USER_USER, USER_PASS);
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
const bearerToken = token as string;
// GET /api/v1/roles requires users:manage
const rolesRes = await fetch(`${API}/api/v1/roles`, {
headers: { Authorization: `Bearer ${bearerToken}` },
});
expect(rolesRes.status).toBe(403);
});
base.test("user cannot register new users via API", async ({ page }) => {
await login(page, USER_USER, USER_PASS);
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
const bearerToken = token as string;
// POST /api/auth/register requires users:manage
const registerRes = await fetch(`${API}/api/auth/register`, {
method: "POST",
headers: {
Authorization: `Bearer ${bearerToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
username: "hacked-user",
password: "HackedPass1",
role: "admin",
}),
});
expect(registerRes.status).toBe(403);
});
});
// ---------------------------------------------------------------------------
// RBAC -- additional cross-role endpoint verification
// ---------------------------------------------------------------------------
base.describe("RBAC API Endpoints - Editor (extended)", () => {
let adminToken: string;
const EDITOR_EXT = `guieditorext-${UID}`;
const EDITOR_EXT_PASS = "EditorExtPass1";
base.beforeAll(async () => {
adminToken = await getAdminToken();
await createReadyUser(adminToken, EDITOR_EXT, EDITOR_EXT_PASS, "editor");
});
base.afterAll(async () => {
await deleteUser(adminToken, EDITOR_EXT);
});
base.test("editor gets 403 on teams endpoint", async ({ page }) => {
await login(page, EDITOR_EXT, EDITOR_EXT_PASS);
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
const bearerToken = token as string;
// GET /api/v1/teams requires teams:manage
const teamsRes = await fetch(`${API}/api/v1/teams`, {
headers: { Authorization: `Bearer ${bearerToken}` },
});
expect(teamsRes.status).toBe(403);
});
base.test("editor gets 403 on roles endpoint", async ({ page }) => {
await login(page, EDITOR_EXT, EDITOR_EXT_PASS);
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
const bearerToken = token as string;
// GET /api/v1/roles requires users:manage
const rolesRes = await fetch(`${API}/api/v1/roles`, {
headers: { Authorization: `Bearer ${bearerToken}` },
});
expect(rolesRes.status).toBe(403);
});
base.test("editor cannot register new users via API", async ({ page }) => {
await login(page, EDITOR_EXT, EDITOR_EXT_PASS);
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
const bearerToken = token as string;
const registerRes = await fetch(`${API}/api/auth/register`, {
method: "POST",
headers: {
Authorization: `Bearer ${bearerToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
username: "hacked-editor-user",
password: "HackedPass1",
role: "user",
}),
});
expect(registerRes.status).toBe(403);
});
base.test("editor can read own settings via API", async ({ page }) => {
await login(page, EDITOR_EXT, EDITOR_EXT_PASS);
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
const bearerToken = token as string;
// GET /api/v1/config/auth is public, but session should work
const sessionRes = await fetch(`${API}/api/auth/session`, {
headers: { Authorization: `Bearer ${bearerToken}` },
});
expect(sessionRes.status).toBe(200);
const session = await sessionRes.json();
expect(session.user.role).toBe("editor");
});
base.test("editor About tab shows correct role", async ({ page }) => {
await login(page, EDITOR_EXT, EDITOR_EXT_PASS);
await openSettings(page);
await page.getByRole("button", { name: /about/i }).click();
await expect(page.locator("h3").filter({ hasText: "About" })).toBeVisible();
await expect(page.getByText("Version:")).toBeVisible();
});
});
base.describe("RBAC -- Editor and User see identical tabs (intentional)", () => {
let adminToken: string;
const RBAC_EDITOR = `rbaceditor-${UID}`;
const RBAC_EDITOR_PASS = "RbacEditorPass1";
const RBAC_USER = `rbacuser-${UID}`;
const RBAC_USER_PASS = "RbacUserPass1";
base.beforeAll(async () => {
adminToken = await getAdminToken();
await createReadyUser(adminToken, RBAC_EDITOR, RBAC_EDITOR_PASS, "editor");
await createReadyUser(adminToken, RBAC_USER, RBAC_USER_PASS, "user");
});
base.afterAll(async () => {
await deleteUser(adminToken, RBAC_EDITOR);
await deleteUser(adminToken, RBAC_USER);
});
base.test(
"editor and user see the same 6 tabs (correct behavior, not a bug)",
async ({ page }) => {
// Verify editor tab count
await login(page, RBAC_EDITOR, RBAC_EDITOR_PASS);
await openSettings(page);
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
const editorNavButtons = page.locator(".w-48 button");
const editorCount = await editorNavButtons.count();
// Close and switch to user
await page.keyboard.press("Escape");
await page.goto("/login");
await login(page, RBAC_USER, RBAC_USER_PASS);
await openSettings(page);
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
const userNavButtons = page.locator(".w-48 button");
const userCount = await userNavButtons.count();
// Both should see exactly 6 tabs
expect(editorCount).toBe(6);
expect(userCount).toBe(6);
expect(editorCount).toBe(userCount);
},
);
base.test("editor and user both see the same set of tab labels", async ({ page }) => {
const expectedTabs = ["General", "Security", "API Keys", "Tools", "Product Analytics", "About"];
// Check editor
await login(page, RBAC_EDITOR, RBAC_EDITOR_PASS);
await openSettings(page);
for (const label of expectedTabs) {
await expect(page.getByRole("button", { name: new RegExp(label, "i") })).toBeVisible();
}
// Close and check user
await page.keyboard.press("Escape");
await page.goto("/login");
await login(page, RBAC_USER, RBAC_USER_PASS);
await openSettings(page);
for (const label of expectedTabs) {
await expect(page.getByRole("button", { name: new RegExp(label, "i") })).toBeVisible();
}
});
});