mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
* 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).
263 lines
9.4 KiB
TypeScript
263 lines
9.4 KiB
TypeScript
import { test as base, expect } from "@playwright/test";
|
|
import { authFile } from "../../playwright.config";
|
|
import { login, openSettings } from "./helpers";
|
|
|
|
const API = process.env.API_URL || "http://localhost:13490";
|
|
|
|
const TEST_USER = "rbactest";
|
|
const TEST_PASSWORD = "RbacTest1";
|
|
|
|
/** 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 the test user with role "user" and clear the mustChangePassword flag
|
|
* so the browser login redirects to "/" instead of "/change-password".
|
|
*/
|
|
async function ensureTestUser(adminToken: string): Promise<void> {
|
|
// Create - ignore 409 if already exists
|
|
const createRes = await fetch(`${API}/api/auth/register`, {
|
|
method: "POST",
|
|
headers: authJson(adminToken),
|
|
body: JSON.stringify({
|
|
username: TEST_USER,
|
|
password: TEST_PASSWORD,
|
|
role: "user",
|
|
}),
|
|
});
|
|
if (createRes.status !== 201 && createRes.status !== 409) {
|
|
throw new Error(`Failed to create test user: ${createRes.status}`);
|
|
}
|
|
|
|
// Login as the test user to get a token
|
|
const loginRes = await fetch(`${API}/api/auth/login`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ username: TEST_USER, password: TEST_PASSWORD }),
|
|
});
|
|
if (!loginRes.ok) {
|
|
throw new Error(`Failed to login as test user: ${loginRes.status}`);
|
|
}
|
|
const loginData = await loginRes.json();
|
|
|
|
// Change password (same value) to clear mustChangePassword flag
|
|
const changeRes = await fetch(`${API}/api/auth/change-password`, {
|
|
method: "POST",
|
|
headers: authJson(loginData.token),
|
|
body: JSON.stringify({
|
|
currentPassword: TEST_PASSWORD,
|
|
newPassword: TEST_PASSWORD,
|
|
}),
|
|
});
|
|
if (!changeRes.ok) {
|
|
throw new Error(`Failed to clear mustChangePassword: ${changeRes.status}`);
|
|
}
|
|
}
|
|
|
|
/** Delete the test user if it exists. */
|
|
async function cleanupTestUser(adminToken: string): Promise<void> {
|
|
const listRes = await fetch(`${API}/api/auth/users`, {
|
|
headers: authOnly(adminToken),
|
|
});
|
|
if (!listRes.ok) return;
|
|
const { users } = await listRes.json();
|
|
const testUser = users.find((u: { username: string }) => u.username === TEST_USER);
|
|
if (testUser) {
|
|
await fetch(`${API}/api/auth/users/${testUser.id}`, {
|
|
method: "DELETE",
|
|
headers: authOnly(adminToken),
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Admin sees all tabs ─────────────────────────────────────────────
|
|
|
|
base.describe("RBAC - Admin sees all tabs", () => {
|
|
base.use({
|
|
storageState: authFile,
|
|
});
|
|
|
|
base.test("admin sees all settings tabs", async ({ page }) => {
|
|
await page.goto("/");
|
|
await openSettings(page);
|
|
|
|
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: /system settings/i })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: /security/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: /api keys/i })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: /tools/i })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: /about/i })).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ── User sees restricted tabs ───────────────────────────────────────
|
|
|
|
base.describe("RBAC - User sees restricted tabs", () => {
|
|
let adminToken: string;
|
|
|
|
base.beforeAll(async () => {
|
|
adminToken = await getAdminToken();
|
|
await ensureTestUser(adminToken);
|
|
});
|
|
|
|
base.afterAll(async () => {
|
|
await cleanupTestUser(adminToken);
|
|
});
|
|
|
|
base.test("user role only sees permitted settings tabs", async ({ page }) => {
|
|
await login(page, TEST_USER, TEST_PASSWORD);
|
|
|
|
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: /about/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();
|
|
|
|
// Should NOT see editor-only tabs (requires settings:write)
|
|
await expect(page.getByRole("button", { name: /ai features/i })).not.toBeVisible();
|
|
});
|
|
|
|
base.test("user role gets 403 on admin API endpoints", async ({ page }) => {
|
|
await login(page, TEST_USER, TEST_PASSWORD);
|
|
|
|
// Extract token from localStorage
|
|
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: authOnly(bearerToken),
|
|
});
|
|
expect(usersRes.status).toBe(403);
|
|
|
|
// PUT /api/v1/settings requires settings:write
|
|
const settingsRes = await fetch(`${API}/api/v1/settings`, {
|
|
method: "PUT",
|
|
headers: authJson(bearerToken),
|
|
body: JSON.stringify({ testSetting: "hacked" }),
|
|
});
|
|
expect(settingsRes.status).toBe(403);
|
|
});
|
|
});
|
|
|
|
// ── Editor sees collaborative tabs ─────────────────────────────────
|
|
|
|
base.describe("RBAC - Editor sees collaborative tabs", () => {
|
|
let adminToken: string;
|
|
|
|
base.beforeAll(async () => {
|
|
adminToken = await getAdminToken();
|
|
// Create editor user
|
|
const createRes = await fetch(`${API}/api/auth/register`, {
|
|
method: "POST",
|
|
headers: authJson(adminToken),
|
|
body: JSON.stringify({
|
|
username: "editortest",
|
|
password: "EditorTest1",
|
|
role: "editor",
|
|
}),
|
|
});
|
|
if (createRes.status !== 201 && createRes.status !== 409) {
|
|
throw new Error(`Failed to create editor user: ${createRes.status}`);
|
|
}
|
|
|
|
// Clear mustChangePassword
|
|
const loginRes = await fetch(`${API}/api/auth/login`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ username: "editortest", password: "EditorTest1" }),
|
|
});
|
|
if (!loginRes.ok) throw new Error(`Editor login failed: ${loginRes.status}`);
|
|
const loginData = await loginRes.json();
|
|
await fetch(`${API}/api/auth/change-password`, {
|
|
method: "POST",
|
|
headers: authJson(loginData.token),
|
|
body: JSON.stringify({
|
|
currentPassword: "EditorTest1",
|
|
newPassword: "EditorTest1",
|
|
}),
|
|
});
|
|
});
|
|
|
|
base.afterAll(async () => {
|
|
const listRes = await fetch(`${API}/api/auth/users`, {
|
|
headers: authOnly(adminToken),
|
|
});
|
|
if (!listRes.ok) return;
|
|
const { users } = await listRes.json();
|
|
const editor = users.find((u: { username: string }) => u.username === "editortest");
|
|
if (editor) {
|
|
await fetch(`${API}/api/auth/users/${editor.id}`, {
|
|
method: "DELETE",
|
|
headers: authOnly(adminToken),
|
|
});
|
|
}
|
|
});
|
|
|
|
base.test(
|
|
"editor sees general, security, api-keys, tools, about but not admin tabs",
|
|
async ({ page }) => {
|
|
await login(page, "editortest", "EditorTest1");
|
|
await openSettings(page);
|
|
|
|
// Should see these
|
|
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: /about/i })).toBeVisible();
|
|
|
|
// Should NOT see admin 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();
|
|
},
|
|
);
|
|
|
|
base.test("editor gets 403 on admin API endpoints", async ({ page }) => {
|
|
await login(page, "editortest", "EditorTest1");
|
|
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
|
|
expect(token).toBeTruthy();
|
|
|
|
const usersRes = await fetch(`${API}/api/auth/users`, {
|
|
headers: authOnly(token as string),
|
|
});
|
|
expect(usersRes.status).toBe(403);
|
|
|
|
const settingsRes = await fetch(`${API}/api/v1/settings`, {
|
|
method: "PUT",
|
|
headers: authJson(token as string),
|
|
body: JSON.stringify({ testSetting: "hacked" }),
|
|
});
|
|
expect(settingsRes.status).toBe(403);
|
|
});
|
|
});
|