Files
flowdeck/e2e/regression_logout_dnd.spec.js
T
bruno c20aeaada1
FlowDeck CI / docker (push) Successful in 1m51s
FlowDeck CI / lint (push) Successful in 1m56s
FlowDeck CI / test (push) Successful in 15m11s
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.
2026-10-02 17:41:07 -04:00

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);
}
});