fix: logout servi « hors ligne » par le SW + drag&drop upload en 403 CSRF (v7.45.1)
- 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.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user