One sign-in drive for every e2e, the public surface and visibility in the rights matrix, and the stack gates in validate
e2e-login-flow and e2e-revocation now share the matrix's driveLogin, which counts a sign-in only once the app is really reached and the app install is asserted; their own loops could report "shell reached" while still on the picker page. The rights matrix gains a public-surface group (allow-listed endpoints answer 200, non-public paths 404, status codes only) and a visibility group (an event with a meeting point and a participant switched public and private by its owner: event, meeting point, attendees collection and catalog listing follow). validate.ts probes the stack and runs test:e2e-rights and test:e2e-revocation as layers, reported SKIPPED when the stack is down and never counted as passed.
This commit is contained in:
+24
-50
@@ -13,12 +13,22 @@
|
||||
* only (the app's FRONT_URL), so the app MUST be served at APP_URL — do not
|
||||
* serve a private instance on an ephemeral port: the pod rejects the redirect.
|
||||
*
|
||||
* ONE LOGIN DRIVE. The sign-in is `driveLogin` from `e2e-rights-matrix-support.ts`,
|
||||
* shared with the rights matrix: a state machine over OBSERVED hops (it must
|
||||
* leave the app for the pod, the consent Accept is clicked once stable and the
|
||||
* page must then leave the consent screen, and the shell counts only with the
|
||||
* bottom navigation shown and no picker). The previous local loop could report
|
||||
* "shell reached" while still on `:4000/` — the user never consented, the app
|
||||
* was never installed. `authenticated` also requires the install to be visible
|
||||
* on the pod (the user's registered Event container answers 200).
|
||||
*
|
||||
* The two benign ≥400 classes (the pod notification WebSocket channel, and an
|
||||
* anonymous WAC profile read) are classified, never fatal. Anything else fails.
|
||||
*/
|
||||
|
||||
import { chromium, type Page } from 'playwright';
|
||||
import { podLogin, readResource, type ApodsStackConfig } from '../src/shared/support/activitypodsStack';
|
||||
import type { ApodsStackConfig } from '../src/shared/support/activitypodsStack';
|
||||
import { driveLogin, installedContainer, loginWithRetry, type DriveContext } from './e2e-rights-matrix-support';
|
||||
|
||||
const strip = (u: string): string => u.replace(/\/+$/, '');
|
||||
const BACKEND: string = strip(process.env.BACKEND_URL || 'http://localhost:3003');
|
||||
@@ -27,6 +37,7 @@ const config: ApodsStackConfig = {
|
||||
podProviderUrl: strip(process.env.POD_PROVIDER_URL || 'http://localhost:3000'),
|
||||
backendUrl: BACKEND,
|
||||
};
|
||||
const POD_FRONT: string = strip(process.env.POD_FRONTEND_URL || 'http://localhost:5000');
|
||||
|
||||
const USER = `flow${Date.now()}`;
|
||||
const PASS = 'Test1test';
|
||||
@@ -37,9 +48,9 @@ interface HttpError { url: string; status: number; method: string; body: string
|
||||
const redirects: string[] = [];
|
||||
const errors4xx: HttpError[] = [];
|
||||
const consoleErrors: string[] = [];
|
||||
const hops: string[] = [];
|
||||
/** The shared login drive's context: it writes the observed hops; HTTP and page errors are collected by `wire` below. */
|
||||
const drive: DriveContext = { app: APP, podFront: POD_FRONT, hops: [], errors: [], consoleErrors: [], pageErrors: [], deadline: DEADLINE };
|
||||
|
||||
const clean = (s: string): string => s.replace('http://localhost:', ':').replace('http://127.0.0.1:', ':127.');
|
||||
const bodyText = (page: Page): Promise<string> =>
|
||||
page.locator('body').innerText().catch(() => '').then(t => t.replace(/\s+/g, ' ').trim());
|
||||
const snap = (page: Page, name: string): Promise<void> =>
|
||||
@@ -65,46 +76,6 @@ function wire(page: Page): void {
|
||||
page.on('pageerror', e => consoleErrors.push('pageerror: ' + String(e.message).slice(0, 300)));
|
||||
}
|
||||
|
||||
/** Drive picker → pod login → consent until the app shell is reached. */
|
||||
async function driveLogin(page: Page): Promise<boolean> {
|
||||
let authorizeSeen = 0;
|
||||
for (let i = 0; i < 20 && Date.now() < DEADLINE; i++) {
|
||||
const url = page.url();
|
||||
if (hops[hops.length - 1] !== clean(url)) hops.push(clean(url));
|
||||
|
||||
if (url.includes(':5000') && (await page.locator('input[name="username"]').count().catch(() => 0))) {
|
||||
hops.push(' [login] submitting credentials');
|
||||
await page.locator('input[name="username"]').fill(USER).catch(() => {});
|
||||
await page.locator('input[name="password"]').fill(PASS).catch(() => {});
|
||||
const btn = page.getByRole('button', { name: /SE CONNECTER|Se connecter|Sign in|Connexion/i });
|
||||
if (await btn.count()) await btn.first().click().catch(() => {});
|
||||
else await page.locator('button[type=submit]').first().click().catch(() => {});
|
||||
await page.waitForTimeout(3500);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (url.includes(':5000/authorize')) {
|
||||
authorizeSeen++;
|
||||
const acc = page.getByRole('button', { name: /^ACCEPTER$|^Accepter$|^Accept$/i });
|
||||
let visible = await acc.first().isVisible({ timeout: 5000 }).catch(() => false);
|
||||
hops.push(` [authorize #${authorizeSeen}] accept visible=${visible}`);
|
||||
await snap(page, 'login-flow-consent');
|
||||
if (visible) { await acc.first().click().catch(() => {}); hops.push(' [authorize] clicked accept'); }
|
||||
if (authorizeSeen >= 3) { hops.push(' [LOOP] authorize revisited 3x — consent loop'); return false; }
|
||||
await page.waitForTimeout(5000);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (url.startsWith(APP)) {
|
||||
if (url.includes('/login')) { await page.waitForTimeout(5000); continue; }
|
||||
const picker = await page.locator('text=Connectez-vous avec votre Pod').count().catch(() => 0);
|
||||
if (!picker) { hops.push(' [app] shell reached'); return true; }
|
||||
}
|
||||
await page.waitForTimeout(1500);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function createEvent(page: Page): Promise<boolean> {
|
||||
await page.goto(`${APP}/events/new`, { waitUntil: 'domcontentloaded' }).catch(() => {});
|
||||
await page.waitForTimeout(1500);
|
||||
@@ -142,19 +113,20 @@ function classify(e: HttpError): string | null {
|
||||
}
|
||||
|
||||
(async () => {
|
||||
// Sign in (signup fallback) via the shared helper — the UI login then uses the same creds.
|
||||
await podLogin(config, USER, PASS);
|
||||
// Sign in (signup fallback, retried: a fresh signup can outlive the helper's
|
||||
// timeout while succeeding server-side) — the UI login then uses the same creds.
|
||||
const session = await loginWithRetry(config, USER, PASS);
|
||||
|
||||
const browser = await chromium.launch({ headless: true, args: ['--allow-insecure-localhost', '--disable-web-security'] });
|
||||
const ctx = await browser.newContext();
|
||||
const page = await ctx.newPage();
|
||||
wire(page);
|
||||
|
||||
await page.goto(`${APP}/`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1500);
|
||||
await page.getByRole('button', { name: /Se connecter avec localhost:3000/i }).click();
|
||||
const authenticated = await driveLogin(page);
|
||||
// The shared drive opens the app, clicks the pod's sign-in button and follows the observed hops.
|
||||
const login = await driveLogin(page, 'flow', USER, PASS, drive);
|
||||
await snap(page, 'login-flow-after-login');
|
||||
const install = login.ok ? await installedContainer(session) : { ok: false, actual: 'not checked: the sign-in drive failed' };
|
||||
const authenticated = login.ok && install.ok;
|
||||
const eventCreated = authenticated ? await createEvent(page) : false;
|
||||
await snap(page, 'login-flow-after-event');
|
||||
const meetingPointCreated = eventCreated ? await createMeetingPoint(page) : false;
|
||||
@@ -169,10 +141,12 @@ function classify(e: HttpError): string | null {
|
||||
const report = {
|
||||
user: USER,
|
||||
authenticated,
|
||||
login: login.detail,
|
||||
installed: install.actual,
|
||||
eventCreated,
|
||||
meetingPointCreated,
|
||||
redirects,
|
||||
hops,
|
||||
hops: drive.hops,
|
||||
benign4xx: benign,
|
||||
fatal4xx: fatal,
|
||||
consoleErrors,
|
||||
|
||||
Reference in New Issue
Block a user