- SW : le fetch event de navigation arrive en redirect:'manual' → toute 302 du serveur (logout → /auth/login) se lisait opaqueredirect (status 0) → « bad status » → page hors ligne. networkFirst rejoue la requête en redirect:'follow' et sert une 302 synthétique vers l'URL finale (Chromium refuse une response 'redirected' servie à une navigation → ERR_FAILED). timeoutFetch annule désormais réellement (AbortController branchée). - Local workspace : _doUpload (upload/upload-folder) et toggleFavorite n'envoyaient pas X-CSRF-Token → 403 systématique depuis A19 (derniers appels mutants du front, balayage complet refait). - Porte : e2e/regression_logout_dnd.spec.js (2 tests verts, joués sur l'image rebuildée) · pytest 1094 passed · ruff OK · OpenAPI 7.45.1.
109 lines
4.4 KiB
JavaScript
109 lines
4.4 KiB
JavaScript
/**
|
|
* Régression — 2 bugs rapportés (post v7.45.0) :
|
|
* 1. Logout : le SW servait sa page « hors ligne » sur TOUTE navigation
|
|
* redirigée (fetch event en redirect:'manual' → opaqueredirect → rejet).
|
|
* 2. Drag & drop de fichiers : POST /api/local-workspace/upload sans
|
|
* X-CSRF-Token → 403 (A19 a retiré l'exemption sans équiper l'appel).
|
|
*
|
|
* Instance attendue sur FD_BASE_URL (défaut 8080), compte e2e documenté.
|
|
*/
|
|
const { test, expect } = require('@playwright/test');
|
|
|
|
const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8080';
|
|
const USER = process.env.FD_USER || '[email protected]';
|
|
const PASS = process.env.FD_PASS || 'e2e-secret-123';
|
|
|
|
async function login(page) {
|
|
await page.goto(`${FD_BASE}/auth/login?provider=local`, { waitUntil: 'domcontentloaded' });
|
|
await page.fill('#email', USER);
|
|
await page.fill('#password', PASS);
|
|
await page.click('.btn-primary');
|
|
const ok = await page
|
|
.waitForURL('**/workspaces', { timeout: 8000 })
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
if (!ok) {
|
|
const resp = await page.request.post(`${FD_BASE}/auth/register`, {
|
|
data: { email: USER, password: PASS, name: 'E2E' },
|
|
});
|
|
if (!resp.ok() && resp.status() !== 409) {
|
|
throw new Error(`register ${resp.status()}: ${await resp.text()}`);
|
|
}
|
|
await page.goto(`${FD_BASE}/workspaces`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForURL('**/workspaces', { timeout: 15000 });
|
|
}
|
|
// workspace actif requis par /api/local-workspace/upload (A22)
|
|
const ws = await page.evaluate(async () => (await fetch('/api/workspaces')).json());
|
|
if (!ws.workspaces || ws.workspaces.length === 0) {
|
|
await page.evaluate(async () => {
|
|
const csrf = (document.cookie.match(/csrf_token=([^;]+)/) || [])[1] || '';
|
|
const r = await fetch('/api/workspaces', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf },
|
|
body: JSON.stringify({ name: 'E2E workspace' }),
|
|
});
|
|
const w = await r.json();
|
|
await fetch(`/api/workspaces/${w.id}/select`, {
|
|
method: 'POST',
|
|
headers: { 'X-CSRF-Token': csrf },
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
test('logout avec SW : atterrit sur la page login, pas hors ligne', async ({ page }) => {
|
|
await login(page);
|
|
await page.evaluate(async () => {
|
|
if ('serviceWorker' in navigator) await navigator.serviceWorker.ready;
|
|
});
|
|
await page.waitForTimeout(1000);
|
|
expect(await page.evaluate(() => !!navigator.serviceWorker.controller)).toBe(true);
|
|
|
|
await page.goto(`${FD_BASE}/auth/logout`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForURL(/\/auth\/login/, { timeout: 10000 });
|
|
const body = await page.evaluate(() => document.body.innerText);
|
|
expect(body).not.toContain('hors ligne');
|
|
expect(page.url()).toContain('/auth/login');
|
|
});
|
|
|
|
test('drop de fichier : upload accepté (CSRF), 200 et item créé', async ({ page }) => {
|
|
test.setTimeout(60000);
|
|
await login(page);
|
|
await page.goto(`${FD_BASE}/local-workspace`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(1000);
|
|
|
|
const uploadResp = [];
|
|
page.on('response', (r) => {
|
|
if (r.url().includes('/api/local-workspace/upload')) {
|
|
uploadResp.push(
|
|
r.text().then((body) => ({ status: r.status(), body })).catch(() => ({ status: r.status(), body: '' }))
|
|
);
|
|
}
|
|
});
|
|
|
|
await page.evaluate(() => {
|
|
const zone = document.querySelector('[x-data*="wsInitData"]') || document.body;
|
|
const dt = new DataTransfer();
|
|
dt.items.add(new File(['hello world'], 'e2e-drop.txt', { type: 'text/plain' }));
|
|
zone.dispatchEvent(new DragEvent('dragover', { dataTransfer: dt, bubbles: true, cancelable: true }));
|
|
zone.dispatchEvent(new DragEvent('drop', { dataTransfer: dt, bubbles: true, cancelable: true }));
|
|
});
|
|
|
|
await expect.poll(() => uploadResp.length, { timeout: 15000 }).toBeGreaterThan(0);
|
|
const resp = await uploadResp[0];
|
|
expect(resp.status, resp.body).toBe(200);
|
|
|
|
// Nettoyage : l'upload crée une page fichier → on la supprime.
|
|
const parsed = JSON.parse(resp.body || '{}');
|
|
for (const item of parsed.items || []) {
|
|
if (!item.id) continue;
|
|
await page.evaluate(async (pid) => {
|
|
const csrf = (document.cookie.match(/csrf_token=([^;]+)/) || [])[1] || '';
|
|
await fetch('/api/local-workspace/items/' + pid, {
|
|
method: 'DELETE',
|
|
headers: { 'X-CSRF-Token': csrf },
|
|
});
|
|
}, item.id);
|
|
}
|
|
});
|