129 lines
4.3 KiB
TypeScript
129 lines
4.3 KiB
TypeScript
import { expect, type Page, test } from '@playwright/test';
|
|
|
|
/**
|
|
* Audio library e2e (Workstream 13). Proves the RBAC + persistence contract:
|
|
* director/office_manager/teacher upload (manage), all four campus roles
|
|
* read/play, support_staff is read-only, external roles are locked out.
|
|
*
|
|
* Requires the backend running with the database migrated + seeded.
|
|
*/
|
|
const USER_PASSWORD = 'flatlogicUser123!';
|
|
|
|
const BACKEND_API_URL =
|
|
process.env.VITE_BACKEND_API_URL || 'http://localhost:8080/api';
|
|
|
|
const AUDIO = `${BACKEND_API_URL}/audio_files`;
|
|
|
|
const TEACHER = 'teacher@flatlogic.com';
|
|
const SUPPORT_STAFF = 'support_staff@flatlogic.com';
|
|
const DIRECTOR = 'director@flatlogic.com';
|
|
const GUARDIAN = 'guardian@flatlogic.com';
|
|
|
|
const DENIED = [400, 401, 403];
|
|
|
|
interface AudioRow {
|
|
readonly id: string;
|
|
readonly title: string;
|
|
readonly kind: string;
|
|
readonly url: string | null;
|
|
readonly recipe: unknown;
|
|
}
|
|
|
|
const SAMPLE_RECIPE = {
|
|
voices: [
|
|
{ waveform: 'sine', notes: [{ freq: 523.25, startAt: 0, duration: 0.6, gain: 0.3 }] },
|
|
],
|
|
};
|
|
|
|
async function logout(page: Page): Promise<void> {
|
|
await page.request.post(`${BACKEND_API_URL}/auth/signout`);
|
|
await page.context().clearCookies();
|
|
}
|
|
|
|
async function login(page: Page, email: string): Promise<void> {
|
|
await logout(page);
|
|
await page.goto('/login');
|
|
await page.getByPlaceholder('you@school.edu').fill(email);
|
|
await page.getByPlaceholder('Enter your password').fill(USER_PASSWORD);
|
|
await page.getByRole('button', { name: 'Sign In', exact: true }).click();
|
|
await page.waitForURL((url) => !url.pathname.startsWith('/login'), {
|
|
timeout: 10_000,
|
|
});
|
|
}
|
|
|
|
async function findAudio(page: Page, title: string): Promise<AudioRow | undefined> {
|
|
const res = await page.request.get(AUDIO);
|
|
expect(res.status()).toBe(200);
|
|
const body = (await res.json()) as { rows?: AudioRow[] };
|
|
return (body.rows ?? []).find((row) => row.title === title);
|
|
}
|
|
|
|
test.describe('Audio library', () => {
|
|
test('a teacher uploads an audio file that persists and peers can read', async ({
|
|
page,
|
|
}) => {
|
|
await login(page, TEACHER);
|
|
const title = `Calm Bell ${Date.now()}`;
|
|
const res = await page.request.post(AUDIO, {
|
|
data: { data: { title, url: '/files/calm-bell.mp3' } },
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
expect(await findAudio(page, title), 'uploader sees it').toBeTruthy();
|
|
|
|
// A same-campus director can read/play it.
|
|
await login(page, DIRECTOR);
|
|
expect(await findAudio(page, title), 'director on the campus sees it').toBeTruthy();
|
|
});
|
|
|
|
test('a teacher generates a recipe sound that persists with kind=recipe', async ({
|
|
page,
|
|
}) => {
|
|
await login(page, TEACHER);
|
|
const title = `Generated ${Date.now()}`;
|
|
const res = await page.request.post(AUDIO, {
|
|
data: { data: { kind: 'recipe', title, recipe: SAMPLE_RECIPE } },
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
|
|
const row = await findAudio(page, title);
|
|
expect(row, 'creator sees the recipe row').toBeTruthy();
|
|
expect(row?.kind).toBe('recipe');
|
|
expect(row?.url).toBeNull();
|
|
expect(row?.recipe).toBeTruthy();
|
|
});
|
|
|
|
test('the service rejects content that does not match the kind', async ({ page }) => {
|
|
await login(page, TEACHER);
|
|
|
|
// recipe kind without a recipe payload.
|
|
const missingRecipe = await page.request.post(AUDIO, {
|
|
data: { data: { kind: 'recipe', title: 'No recipe' } },
|
|
});
|
|
expect(DENIED).toContain(missingRecipe.status());
|
|
|
|
// file kind without a url.
|
|
const missingUrl = await page.request.post(AUDIO, {
|
|
data: { data: { kind: 'file', title: 'No url' } },
|
|
});
|
|
expect(DENIED).toContain(missingUrl.status());
|
|
});
|
|
|
|
test('support_staff can read but cannot upload', async ({ page }) => {
|
|
await login(page, SUPPORT_STAFF);
|
|
expect((await page.request.get(AUDIO)).status()).toBe(200);
|
|
const create = await page.request.post(AUDIO, {
|
|
data: { data: { title: 'Support should not upload', url: '/files/x.mp3' } },
|
|
});
|
|
expect(DENIED).toContain(create.status());
|
|
});
|
|
|
|
test('external roles cannot access the audio library', async ({ page }) => {
|
|
await login(page, GUARDIAN);
|
|
expect(DENIED).toContain((await page.request.get(AUDIO)).status());
|
|
const create = await page.request.post(AUDIO, {
|
|
data: { data: { title: 'Guardian should not upload', url: '/files/x.mp3' } },
|
|
});
|
|
expect(DENIED).toContain(create.status());
|
|
});
|
|
});
|