d0e6912017
Update project name from teaboy-os to tx-os and remove obsolete services. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 3154f23a-748a-4118-aa41-fc01b7b1f04d Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PVuelRZ Replit-Helium-Checkpoint-Created: true
185 lines
6.7 KiB
JavaScript
185 lines
6.7 KiB
JavaScript
// 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);
|
|
});
|
|
});
|