veza/tests/e2e/playlists-edit-audit.spec.ts
senke 9a4c0d2af4 feat(web): update all features, stories, e2e tests, and auth interceptor
Update auth, playlists, tracks, search, profile, dashboard, player,
settings, and social features. Add e2e audit specs for all major pages.
Update ESLint config, vitest config, and route configuration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 19:16:36 +02:00

252 lines
9.3 KiB
TypeScript

/**
* E2E tests — Playlist Edit (/playlists/:id/edit + edit dialog)
* Covers: redirect, edit dialog, security, a11y, i18n, regression
*/
import { test, expect } from '@playwright/test';
import { loginViaAPI, CONFIG, navigateTo } from './helpers';
test.describe('Edition playlist (/playlists/:id/edit)', () => {
test.beforeEach(async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
});
test.describe('Chargement & Rendu', () => {
test('redirect /edit preserves the playlist ID @critical', async ({ page }) => {
// First find a valid playlist
await navigateTo(page, '/playlists');
await page.waitForLoadState('networkidle').catch(() => {});
const playlistLink = page.locator('a[href^="/playlists/"]').filter({
hasNot: page.locator('[href="/playlists/favoris"]'),
}).first();
const href = await playlistLink.getAttribute('href').catch(() => null);
if (!href) return;
const playlistId = href.replace('/playlists/', '');
// Navigate to /edit URL
await page.goto(`${CONFIG.baseURL}${href}/edit`, { waitUntil: 'domcontentloaded' });
await page.waitForLoadState('networkidle').catch(() => {});
// Wait for redirect
await page.waitForTimeout(2_000);
// URL should be the detail page, NOT literal /:id
const url = page.url();
expect(url).toContain(playlistId);
expect(url).not.toContain(':id');
// Page should show the playlist, not "Not Found"
const notFound = page.getByText('Playlist Not Found');
const isNotFound = await notFound.isVisible().catch(() => false);
expect(isNotFound).toBe(false);
});
test('redirect /edit does not crash on invalid ID', async ({ page }) => {
await page.goto(`${CONFIG.baseURL}/playlists/00000000-0000-0000-0000-000000000000/edit`, {
waitUntil: 'domcontentloaded',
});
await page.waitForLoadState('networkidle').catch(() => {});
await page.waitForTimeout(2_000);
// Should redirect to the detail page (which shows Not Found)
const url = page.url();
expect(url).toContain('00000000-0000-0000-0000-000000000000');
expect(url).not.toContain('/edit');
});
});
test.describe('Fonctionnalites', () => {
test('edit button not visible for non-owner', async ({ page }) => {
await navigateTo(page, '/playlists');
await page.waitForLoadState('networkidle').catch(() => {});
// Find a playlist not owned by this user
const playlistLink = page.locator('a[href^="/playlists/"]').filter({
has: page.locator('text=Curated by'),
}).first();
const href = await playlistLink.getAttribute('href').catch(() => null);
if (!href) return;
await navigateTo(page, href);
// Edit button should NOT be visible (non-owner)
const editButton = page.getByRole('button', { name: /edit playlist/i });
const isVisible = await editButton.isVisible({ timeout: 3_000 }).catch(() => false);
expect(isVisible).toBe(false);
});
test('owner can see and open edit dialog', async ({ page }) => {
// Navigate to playlists list to find user's own playlist
await navigateTo(page, '/playlists');
await page.waitForLoadState('networkidle').catch(() => {});
// Look for a playlist without "Curated by" (owned by current user)
const ownPlaylist = page.locator('a[href^="/playlists/"]').filter({
hasNot: page.locator('text=Curated by'),
}).filter({
hasNot: page.locator('[href="/playlists/favoris"]'),
}).first();
const href = await ownPlaylist.getAttribute('href').catch(() => null);
if (!href) {
// No own playlist found, skip test
test.skip();
return;
}
await navigateTo(page, href);
// Edit button should be visible (owner)
const editButton = page.getByRole('button', { name: /edit playlist/i });
const isVisible = await editButton.isVisible({ timeout: 5_000 }).catch(() => false);
if (!isVisible) {
// Permissions may not show edit for this playlist type
return;
}
// Click edit button to open dialog
await editButton.click();
// Dialog should open with title input pre-filled
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible({ timeout: 3_000 });
});
});
test.describe('Securite', () => {
test('XSS in edit redirect ID is handled safely', async ({ page }) => {
const jsErrors: string[] = [];
page.on('pageerror', (error) => {
jsErrors.push(error.message);
});
await page.goto(
`${CONFIG.baseURL}/playlists/%3Cscript%3Ealert(1)%3C%2Fscript%3E/edit`,
{ waitUntil: 'domcontentloaded' },
);
await page.waitForLoadState('networkidle').catch(() => {});
await page.waitForTimeout(2_000);
// Should not execute XSS
const xssErrors = jsErrors.filter((e) => e.includes('alert'));
expect(xssErrors).toHaveLength(0);
});
test('no sensitive data in redirect URL', async ({ page }) => {
await navigateTo(page, '/playlists');
await page.waitForLoadState('networkidle').catch(() => {});
const playlistLink = page.locator('a[href^="/playlists/"]').filter({
hasNot: page.locator('[href="/playlists/favoris"]'),
}).first();
const href = await playlistLink.getAttribute('href').catch(() => null);
if (!href) return;
await page.goto(`${CONFIG.baseURL}${href}/edit`, { waitUntil: 'domcontentloaded' });
await page.waitForLoadState('networkidle').catch(() => {});
await page.waitForTimeout(2_000);
const url = page.url();
expect(url).not.toMatch(/token=/i);
expect(url).not.toMatch(/@.*\./);
expect(url).not.toMatch(/api[_-]?key/i);
});
});
test.describe('Accessibilite', () => {
test('edit dialog has accessible title when opened', async ({ page }) => {
await navigateTo(page, '/playlists');
await page.waitForLoadState('networkidle').catch(() => {});
const ownPlaylist = page.locator('a[href^="/playlists/"]').filter({
hasNot: page.locator('text=Curated by'),
}).filter({
hasNot: page.locator('[href="/playlists/favoris"]'),
}).first();
const href = await ownPlaylist.getAttribute('href').catch(() => null);
if (!href) {
test.skip();
return;
}
await navigateTo(page, href);
const editButton = page.getByRole('button', { name: /edit playlist/i });
const isVisible = await editButton.isVisible({ timeout: 5_000 }).catch(() => false);
if (!isVisible) return;
await editButton.click();
const dialog = page.getByRole('dialog');
const isDialogVisible = await dialog.isVisible({ timeout: 3_000 }).catch(() => false);
if (!isDialogVisible) return;
// Dialog should have a heading/title
const dialogTitle = dialog.locator('h2, [role="heading"]');
const titleCount = await dialogTitle.count();
expect(titleCount).toBeGreaterThan(0);
});
});
test.describe('i18n', () => {
test('no raw i18n keys in edit dialog', async ({ page }) => {
await navigateTo(page, '/playlists');
await page.waitForLoadState('networkidle').catch(() => {});
const ownPlaylist = page.locator('a[href^="/playlists/"]').filter({
hasNot: page.locator('text=Curated by'),
}).filter({
hasNot: page.locator('[href="/playlists/favoris"]'),
}).first();
const href = await ownPlaylist.getAttribute('href').catch(() => null);
if (!href) {
test.skip();
return;
}
await navigateTo(page, href);
const editButton = page.getByRole('button', { name: /edit playlist/i });
const isVisible = await editButton.isVisible({ timeout: 5_000 }).catch(() => false);
if (!isVisible) return;
await editButton.click();
const dialog = page.getByRole('dialog');
const isDialogVisible = await dialog.isVisible({ timeout: 3_000 }).catch(() => false);
if (!isDialogVisible) return;
const dialogText = await dialog.textContent() || '';
// No raw i18n keys should be visible
expect(dialogText).not.toMatch(/playlists\.editDialog\./);
expect(dialogText).not.toMatch(/playlists\.actions\./);
expect(dialogText).not.toMatch(/playlists\.form\./);
});
});
test.describe('Regression', () => {
test('BUG #1: /edit redirect uses actual playlist ID, not literal :id', async ({ page }) => {
await navigateTo(page, '/playlists');
await page.waitForLoadState('networkidle').catch(() => {});
const playlistLink = page.locator('a[href^="/playlists/"]').filter({
hasNot: page.locator('[href="/playlists/favoris"]'),
}).first();
const href = await playlistLink.getAttribute('href').catch(() => null);
if (!href) return;
await page.goto(`${CONFIG.baseURL}${href}/edit`, { waitUntil: 'domcontentloaded' });
await page.waitForLoadState('networkidle').catch(() => {});
await page.waitForTimeout(2_000);
// CRITICAL: URL must NOT be "/playlists/:id"
const url = page.url();
expect(url).not.toBe(`${CONFIG.baseURL}/playlists/:id`);
expect(url).not.toContain('/:id');
// Should be the actual playlist detail URL
expect(url).toContain('/playlists/');
expect(url).not.toContain('/edit');
});
});
});