// UI smoke: admin creates a group, assigns a user + a restricted app to it, // then a regular user (granted access via that group) sees the restricted // app in /api/apps. Cleans up after itself. import { test, expect } from "@playwright/test"; import pg from "pg"; const DATABASE_URL = process.env.DATABASE_URL; if (!DATABASE_URL) { throw new Error("DATABASE_URL must be set to run this test"); } const API_BASE = process.env.TEST_API_BASE ?? "http://localhost:8080"; const TEST_PASSWORD = "TestPass123!"; const TEST_PASSWORD_HASH = "$2b$10$Bs636ukPMyz01nKrsi.5m.JlDXSN22AVCvn8cgPWWDbo5yJRQX2vu"; const pool = new pg.Pool({ connectionString: DATABASE_URL }); const createdUserIds = []; const createdAppIds = []; const createdGroupNames = []; function uniqueSuffix() { return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`; } async function createUser(prefix, opts = {}) { const username = `${prefix}_${uniqueSuffix()}`; const { rows } = await pool.query( `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active) VALUES ($1, $2, $3, $4, 'en', true) RETURNING id`, [username, `${username}@example.com`, TEST_PASSWORD_HASH, prefix], ); const id = rows[0].id; createdUserIds.push(id); const roleName = opts.admin ? "admin" : "user"; await pool.query( `INSERT INTO user_roles (user_id, role_id) SELECT $1, id FROM roles WHERE name = $2`, [id, roleName], ); return { id, username }; } async function createRestrictedApp(prefix) { const slug = `${prefix}_${uniqueSuffix()}`; const { rows } = await pool.query( `INSERT INTO apps (name_ar, name_en, slug, icon_name, route, color, sort_order, description_en) VALUES ($1, $2, $3, 'Grid2X2', '/test', '#000000', 999, 'visibility test app') RETURNING id`, [`تطبيق ${prefix}`, `App ${prefix}`, slug], ); const id = rows[0].id; createdAppIds.push(id); // Mark the app as restricted by attaching a permission that the regular // 'user' role does NOT have. Pick any permission held by 'admin' but not // by 'user'; if none exists, fall back to the first permission. const permRows = await pool.query( `SELECT id FROM permissions WHERE id IN (SELECT permission_id FROM role_permissions WHERE role_id = (SELECT id FROM roles WHERE name = 'admin')) AND id NOT IN (SELECT permission_id FROM role_permissions WHERE role_id = (SELECT id FROM roles WHERE name = 'user')) LIMIT 1`, ); const fallbackRows = permRows.rows.length ? permRows.rows : (await pool.query(`SELECT id FROM permissions LIMIT 1`)).rows; if (fallbackRows.length === 0) { throw new Error("No permissions exist in DB; cannot mark app as restricted"); } await pool.query( `INSERT INTO app_permissions (app_id, permission_id) VALUES ($1, $2)`, [id, fallbackRows[0].id], ); return { id, slug }; } test.afterAll(async () => { if (createdGroupNames.length > 0) { await pool.query( `DELETE FROM groups WHERE name = ANY($1::text[])`, [createdGroupNames], ); } if (createdAppIds.length > 0) { await pool.query( `DELETE FROM app_permissions WHERE app_id = ANY($1::int[])`, [createdAppIds], ); await pool.query( `DELETE FROM apps WHERE id = ANY($1::int[])`, [createdAppIds], ); } if (createdUserIds.length > 0) { await pool.query( `DELETE FROM user_roles WHERE user_id = ANY($1::int[])`, [createdUserIds], ); await pool.query( `DELETE FROM user_groups WHERE user_id = ANY($1::int[])`, [createdUserIds], ); await pool.query(`DELETE FROM users WHERE id = ANY($1::int[])`, [ createdUserIds, ]); } await pool.end(); }); async function login(page, username, password) { await page.goto("/login"); await page.locator("#username").fill(username); await page.locator("#password").fill(password); await Promise.all([ page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15_000, }), page.locator('form button[type="submit"]').click(), ]); } test.describe("Admin: create group + assign user/app → user sees restricted app", () => { test("end-to-end UI flow", async ({ page }) => { const admin = await createUser("vis_admin", { admin: true }); const member = await createUser("vis_member"); const app = await createRestrictedApp("vis"); const groupName = `Vis Smoke ${uniqueSuffix()}`; createdGroupNames.push(groupName); // 1) Admin logs in via UI and navigates to the Groups admin section. await login(page, admin.username, TEST_PASSWORD); await page.goto("/admin#section=groups"); // The "User Management" nav group should auto-expand because a child is active. await expect(page.locator('[data-testid="create-group-btn"]')).toBeVisible(); // 2) Create the group. await page.locator('[data-testid="create-group-btn"]').click(); await page.locator('[data-testid="group-name-input"]').fill(groupName); await page.locator('[data-testid="create-group-submit"]').click(); // The new group card should appear. const newCard = page .locator('[data-testid^="group-card-"]') .filter({ hasText: groupName }); await expect(newCard).toBeVisible({ timeout: 10_000 }); // 3) Open the edit dialog for the new group. await newCard.locator('[data-testid^="edit-group-"]').click(); // 4) Apps tab → check the restricted app. await page.locator('[data-testid="group-tab-apps"]').click(); const appLabel = page.locator("label").filter({ hasText: app.slug }); await expect(appLabel).toBeVisible(); await appLabel.locator('input[type="checkbox"]').check(); // 5) Users tab → check the member user. await page.locator('[data-testid="group-tab-users"]').click(); await page.locator('[data-testid="group-user-search"]').fill(member.username); const memberLabel = page.locator("label").filter({ hasText: member.username }); await expect(memberLabel).toBeVisible(); await memberLabel.locator('input[type="checkbox"]').check(); // 6) Save. await page.locator('[data-testid="save-group"]').click(); // Wait for the save to settle by re-opening the card and checking counts, // or just give the network a moment. await page.waitForTimeout(1500); // 7) Logout (clear cookies) and login as the member. await page.context().clearCookies(); await login(page, member.username, TEST_PASSWORD); // 8) Verify visibility through the API as the member's session. const apiRes = await page.request.get(`${API_BASE}/api/apps`); expect(apiRes.status()).toBe(200); const apps = await apiRes.json(); const ids = apps.map((a) => a.id); expect(ids).toContain(app.id); }); });