99 lines
3.5 KiB
TypeScript
99 lines
3.5 KiB
TypeScript
import { expect, type Page, test } from '@playwright/test';
|
|
import {
|
|
BACKEND_API_URL,
|
|
deleteOrganizations,
|
|
deleteTestUsers,
|
|
type CreatedTestUser,
|
|
} from './helpers/seeded-users';
|
|
|
|
/**
|
|
* Scoped provisioning e2e (Workstream 8 / §3.4, §3.7). Proves the onboarding
|
|
* contract: when a system admin creates an `owner` user with no organization,
|
|
* the backend auto-creates the company and links the owner to it.
|
|
*
|
|
* Requires the backend running with the database migrated + seeded, and the
|
|
* seed passwords in the environment.
|
|
*/
|
|
const ADMIN_PASSWORD = 'flatlogicAdmin123!';
|
|
const ADMIN_EMAIL = 'admin@flatlogic.com';
|
|
|
|
interface RoleRow {
|
|
readonly id: string;
|
|
readonly name: string | null;
|
|
}
|
|
interface UserRow {
|
|
readonly id: string;
|
|
readonly email: string | null;
|
|
readonly organizationId: string | null;
|
|
}
|
|
interface CreateUserResponse {
|
|
readonly id?: string;
|
|
readonly organizationId?: string | null;
|
|
readonly temporaryPassword?: string;
|
|
}
|
|
|
|
async function login(page: Page, email: string, password: string): Promise<void> {
|
|
await page.goto('/login');
|
|
await page.getByPlaceholder('you@school.edu').fill(email);
|
|
await page.getByPlaceholder('Enter your password').fill(password);
|
|
await page.getByRole('button', { name: 'Sign In', exact: true }).click();
|
|
await page.waitForURL((url) => !url.pathname.startsWith('/login'), {
|
|
timeout: 10_000,
|
|
});
|
|
}
|
|
|
|
test.describe('Scoped provisioning', () => {
|
|
test('creating an owner auto-creates and links the company', async ({
|
|
page,
|
|
}) => {
|
|
await login(page, ADMIN_EMAIL, ADMIN_PASSWORD);
|
|
let createdUser: CreatedTestUser | null = null;
|
|
|
|
try {
|
|
// Resolve the seeded `owner` role id.
|
|
const rolesRes = await page.request.get(`${BACKEND_API_URL}/roles`);
|
|
expect(rolesRes.status()).toBe(200);
|
|
const rolesBody = (await rolesRes.json()) as { rows?: RoleRow[] };
|
|
const ownerRole = (rolesBody.rows ?? []).find((r) => r.name === 'owner');
|
|
expect(ownerRole, 'seeded owner role must exist').toBeTruthy();
|
|
|
|
// Create a brand-new owner with no organization.
|
|
const email = `provisioned-owner-${Date.now()}@example.com`;
|
|
const createRes = await page.request.post(`${BACKEND_API_URL}/users`, {
|
|
data: { data: { email, app_role: ownerRole!.id } },
|
|
});
|
|
expect(createRes.ok()).toBe(true);
|
|
const createBody = (await createRes.json()) as CreateUserResponse;
|
|
expect(createBody.id, 'created owner id').toBeTruthy();
|
|
createdUser = {
|
|
id: createBody.id!,
|
|
email,
|
|
password: createBody.temporaryPassword ?? '',
|
|
organizationId: createBody.organizationId ?? null,
|
|
};
|
|
|
|
// The owner now belongs to an auto-created company.
|
|
const lookupRes = await page.request.get(
|
|
`${BACKEND_API_URL}/users?email=${encodeURIComponent(email)}`,
|
|
);
|
|
expect(lookupRes.status()).toBe(200);
|
|
const lookupBody = (await lookupRes.json()) as { rows?: UserRow[] };
|
|
const created = (lookupBody.rows ?? []).find((u) => u.email === email);
|
|
expect(created, 'the provisioned owner must exist').toBeTruthy();
|
|
expect(
|
|
created!.organizationId,
|
|
'owner-create must auto-create and link a company',
|
|
).toBeTruthy();
|
|
createdUser = {
|
|
...createdUser,
|
|
organizationId: created!.organizationId,
|
|
};
|
|
} finally {
|
|
if (createdUser) {
|
|
await deleteTestUsers(page.request, [createdUser]);
|
|
await deleteOrganizations(page.request, [createdUser.organizationId]);
|
|
}
|
|
}
|
|
});
|
|
});
|