add e2e tests for employee restrictions

This commit is contained in:
Gregor Vostrak
2026-02-10 14:41:04 +01:00
parent e45662c715
commit 8be55359ce
21 changed files with 1211 additions and 37 deletions

View File

@@ -155,6 +155,21 @@ function randomColor(): string {
// Entity creation
// ──────────────────────────────────────────────────
export async function createPublicProjectViaApi(
ctx: TestContext,
data: {
name: string;
is_billable?: boolean;
billable_rate?: number | null;
client_id?: string | null;
}
) {
return createProjectViaApi(ctx, {
...data,
is_public: true,
});
}
export async function createProjectViaApi(
ctx: TestContext,
data: {
@@ -164,6 +179,7 @@ export async function createProjectViaApi(
billable_rate?: number | null;
client_id?: string | null;
estimated_time?: number | null;
is_public?: boolean;
}
) {
const response = await ctx.request.post(
@@ -176,6 +192,7 @@ export async function createProjectViaApi(
billable_rate: data.billable_rate ?? null,
client_id: data.client_id ?? null,
estimated_time: data.estimated_time ?? null,
is_public: data.is_public ?? false,
},
}
);

View File

@@ -2,6 +2,7 @@ import { expect } from '@playwright/test';
import type { Browser, Page } from '@playwright/test';
import { PLAYWRIGHT_BASE_URL } from '../../playwright/config';
import { getInvitationAcceptUrl } from './mailpit';
import type { TestContext } from './api';
/**
* Register a new user in a fresh browser context and return the page + context.
@@ -66,3 +67,98 @@ export async function inviteAndAcceptMember(
// 4. Clean up
await secondUser.close();
}
/**
* Set up an employee member in the owner's organization.
* Returns the employee's page, their member ID, and a cleanup function.
*
* The owner page (from the fixture) is used to invite the employee.
* Test data should be created via the owner's ctx.
*
* IMPORTANT: Projects must be created with is_public: true for the employee to see them,
* or the employee must be added as a project member via createProjectMemberViaApi.
* Clients are only visible to employees if they have at least one visible project.
* Tags are visible to all org members with tags:view permission.
*/
export async function setupEmployeeUser(
ownerPage: Page,
ownerCtx: TestContext,
browser: Browser
): Promise<{
employeePage: Page;
employeeMemberId: string;
closeEmployee: () => Promise<void>;
}> {
const memberId = Math.floor(Math.random() * 100000);
const memberEmail = `employee+${memberId}@emp-perms.test`;
const memberName = 'Emp ' + memberId;
// Register the employee user first
const employee = await registerUser(browser, memberName, memberEmail);
// Send invitation from the owner
await ownerPage.goto(PLAYWRIGHT_BASE_URL + '/members');
await ownerPage.getByRole('button', { name: 'Invite Member' }).click();
await expect(ownerPage.getByPlaceholder('Member Email')).toBeVisible();
await ownerPage.getByPlaceholder('Member Email').fill(memberEmail);
await ownerPage.getByRole('button', { name: 'Employee' }).click();
await Promise.all([
ownerPage.waitForResponse(
(response) =>
response.url().includes('/invitations') &&
response.request().method() === 'POST' &&
response.status() === 204
),
ownerPage.getByRole('button', { name: 'Invite Member', exact: true }).click(),
]);
// Accept the invitation
const acceptUrl = await getInvitationAcceptUrl(employee.page.request, memberEmail);
await employee.page.goto(acceptUrl);
await employee.page.waitForURL(/dashboard/);
// Navigate to dashboard explicitly and wait for it to load to ensure the correct org context.
await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard');
await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({ timeout: 15000 });
// Verify we're on the correct organization (John's Organization).
const orgSwitcherText = await employee.page
.getByTestId('organization_switcher')
.first()
.textContent();
if (!orgSwitcherText?.includes("John's Organization")) {
// Switch to the owner's org using the PUT /current-team endpoint
const cookies = await employee.page.context().cookies();
const xsrfCookie = cookies.find((c) => c.name === 'XSRF-TOKEN');
const xsrfToken = xsrfCookie ? decodeURIComponent(xsrfCookie.value) : '';
await employee.page.request.put(`${PLAYWRIGHT_BASE_URL}/current-team`, {
headers: {
'X-XSRF-TOKEN': xsrfToken,
Accept: 'text/html',
},
data: { team_id: ownerCtx.orgId },
});
// Reload to pick up the new org
await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard');
await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({ timeout: 15000 });
}
// Find the employee's member ID in the owner's organization
const membersResponse = await ownerCtx.request.get(
`${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ownerCtx.orgId}/members`
);
expect(membersResponse.status()).toBe(200);
const membersBody = await membersResponse.json();
const employeeMember = membersBody.data.find(
(m: { role: string; name: string }) => m.role === 'employee' && m.name === memberName
);
expect(employeeMember).toBeTruthy();
return {
employeePage: employee.page,
employeeMemberId: employeeMember.id,
closeEmployee: employee.close,
};
}