v1.1.0: session guard on all game routes + register + sign-in screen (ISS-107)
CI / build + unit + e2e (pull_request) Failing after 26m20s
CI / build + unit + e2e (pull_request) Failing after 26m20s
This commit is contained in:
@@ -6,6 +6,11 @@ import { initialBearingDeg } from '../lib/geo';
|
||||
import { weatherFor } from '../lib/weather';
|
||||
import { windBurnFactor } from '../lib/sim/movement';
|
||||
|
||||
import { authenticate } from './auth';
|
||||
|
||||
// v1.1.0 (ISS-107): game routes are session-guarded — mint + apply a player session before every test.
|
||||
test.beforeEach(async ({ page }) => { await authenticate(page); });
|
||||
|
||||
// v0.12.0 package model against the live origin (https://flight.dev.kkhost.pl):
|
||||
// the full core loop through the UI — hook creates a short package at EDDF → Load
|
||||
// via the right pane → click its destination in the on-board list (left pane since
|
||||
|
||||
+24
-22
@@ -1,12 +1,13 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
// v0.71.0 — Admin panel e2e: top-bar button → modal with login form; wrong credentials show an
|
||||
// error; a session minted by the /api/test/admin-session hook (FLIGHT_TEST_HOOKS=1) authenticates
|
||||
// WITHOUT the real admin password ever appearing in test code; then the generic DB grid is
|
||||
// exercised end-to-end: table list → edit + save a cell (verified by an independent re-fetch) →
|
||||
// add a row → delete it → logout back to the login form.
|
||||
// v0.71.0 — Admin panel e2e: top-bar button → modal; wrong credentials show an error; a session
|
||||
// minted by the /api/test/admin-session hook (FLIGHT_TEST_HOOKS=1) authenticates WITHOUT the real
|
||||
// admin password ever appearing in test code; then the generic DB grid is exercised end-to-end:
|
||||
// table list → edit + save a cell (verified by an independent re-fetch) → add a row → delete it →
|
||||
// logout. v1.1.0 (ISS-107): the whole app now sits behind the sign-in screen — unauthenticated
|
||||
// visitors see IT instead of the shell, and logout drops back to it.
|
||||
|
||||
const SESSION_KEY = 'flight.session'; // must match components/AdminPanel.tsx
|
||||
const SESSION_KEY = 'flight.session'; // must match lib/authClient.ts
|
||||
|
||||
async function mintAdminSession(page: import('@playwright/test').Page): Promise<string> {
|
||||
const res = await page.request.post('/api/test/admin-session');
|
||||
@@ -14,31 +15,32 @@ async function mintAdminSession(page: import('@playwright/test').Page): Promise<
|
||||
return ((await res.json()) as { sessionId: string }).sessionId;
|
||||
}
|
||||
|
||||
test('admin panel: login form, wrong-creds error, session auth, DB grid CRUD, logout', async ({ page }) => {
|
||||
test('admin panel: sign-in gate, wrong-creds error, session auth, DB grid CRUD, logout', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const adminBtn = page.getByTestId('admin-btn');
|
||||
await expect(adminBtn).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
// --- Open the modal: no stored session → login form.
|
||||
await adminBtn.click();
|
||||
const dialog = page.locator('[role="dialog"]');
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(page.getByTestId('admin-username')).toBeVisible();
|
||||
await expect(page.getByTestId('admin-password')).toBeVisible();
|
||||
// --- Unauthenticated → the sign-in screen replaces the whole shell (v1.1.0, ISS-107).
|
||||
const signIn = page.getByTestId('sign-in-form');
|
||||
await expect(signIn).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByTestId('app-header')).toHaveCount(0);
|
||||
|
||||
// --- Wrong credentials → error line (the seeded admin's plaintext is never used here).
|
||||
await page.getByTestId('admin-username').fill('kamilkrzywda');
|
||||
await page.getByTestId('admin-password').fill('wrong-password-on-purpose');
|
||||
await page.getByTestId('admin-login-submit').click();
|
||||
await expect(page.getByTestId('admin-error')).toBeVisible({ timeout: 10_000 });
|
||||
await page.getByTestId('sign-in-username').fill('kamilkrzywda');
|
||||
await page.getByTestId('sign-in-password').fill('wrong-password-on-purpose');
|
||||
await page.getByTestId('sign-in-submit').click();
|
||||
await expect(page.getByTestId('auth-error')).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// --- Authenticate via the test hook + localStorage, then reopen the panel.
|
||||
// --- Authenticate via the test hook + localStorage, then reload into the shell.
|
||||
const sessionId = await mintAdminSession(page);
|
||||
// Both values must be passed as arguments — the function runs in the browser, where Node-side
|
||||
// variables are not in scope.
|
||||
await page.evaluate(([key, sid]) => window.localStorage.setItem(key, sid), [SESSION_KEY, sessionId]);
|
||||
// The independent re-fetch below goes through page.request → needs the header at context level.
|
||||
await page.context().setExtraHTTPHeaders({ 'X-Flight-Session': sessionId });
|
||||
await page.reload();
|
||||
const adminBtn = page.getByTestId('admin-btn');
|
||||
await expect(adminBtn).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
// --- Open the modal: stored session is valid → straight to the table list.
|
||||
await adminBtn.click();
|
||||
|
||||
// --- Authenticated admin → table list including packages and users.
|
||||
@@ -104,8 +106,8 @@ test('admin panel: login form, wrong-creds error, session auth, DB grid CRUD, lo
|
||||
await page.getByTestId(`admin-delete-${addedIdx}`).click();
|
||||
await expect.poll(findUserRow, { timeout: 15_000 }).toBe(-1);
|
||||
|
||||
// --- Logout returns to the login form.
|
||||
// --- Logout drops back to the sign-in screen (v1.1.0, ISS-107 — onLogout unmounts the shell).
|
||||
await page.getByTestId('admin-logout').click();
|
||||
await expect(page.getByTestId('admin-username')).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByTestId('sign-in-form')).toBeVisible({ timeout: 10_000 });
|
||||
await expect(tables).toBeHidden();
|
||||
});
|
||||
|
||||
+2
-1
@@ -13,7 +13,7 @@ test('GET /api/version returns the app version from package.json', async ({ page
|
||||
expect(body.version).toBe(pkg.version);
|
||||
});
|
||||
|
||||
test('GET /api/openapi serves the spec with all seventeen endpoints and the package.json version', async ({ page }) => {
|
||||
test('GET /api/openapi serves the spec with all eighteen endpoints and the package.json version', async ({ page }) => {
|
||||
const res = await page.request.get('/api/openapi');
|
||||
expect(res.status()).toBe(200);
|
||||
expect(res.headers()['content-type']).toContain('application/json');
|
||||
@@ -38,6 +38,7 @@ test('GET /api/openapi serves the spec with all seventeen endpoints and the pack
|
||||
'/api/planes/{planeId}/queue',
|
||||
'/api/planes/{planeId}/queue/cancel',
|
||||
'/api/planes/{planeId}/repair', // v0.39.0 (ISS-069)
|
||||
'/api/register', // v1.1.0 (ISS-107) — self-service account creation
|
||||
'/api/state',
|
||||
'/api/version',
|
||||
]);
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
// v1.1.0 (ISS-107) — session helpers for the e2e suite. Every game route is now session-guarded, so
|
||||
// each spec authenticates in a beforeEach: mint a player session server-side via the test hook
|
||||
// (FLIGHT_TEST_HOOKS=1; no real credentials in test code), then apply it BOTH ways the client uses —
|
||||
// localStorage for the app's own fetches (lib/authClient.ts reads it) and context extraHTTPHeaders
|
||||
// so page.request.* calls in specs carry X-Flight-Session too.
|
||||
|
||||
export const SESSION_KEY = 'flight.session'; // must match lib/authClient.ts
|
||||
|
||||
export async function mintPlayerSession(page: Page): Promise<string> {
|
||||
const res = await page.request.post('/api/test/mint-player-session');
|
||||
expect(res.status(), 'mint-player-session hook should be enabled in the CI stack (FLIGHT_TEST_HOOKS=1)').toBe(200);
|
||||
return ((await res.json()) as { sessionId: string }).sessionId;
|
||||
}
|
||||
|
||||
export async function applySession(page: Page, sessionId: string): Promise<void> {
|
||||
await page.context().setExtraHTTPHeaders({ 'X-Flight-Session': sessionId });
|
||||
// Runs before every navigation's scripts — the app reads the session from localStorage on load.
|
||||
await page.addInitScript(
|
||||
([key, sid]) => window.localStorage.setItem(key, sid),
|
||||
[SESSION_KEY, sessionId],
|
||||
);
|
||||
}
|
||||
|
||||
export async function authenticate(page: Page): Promise<string> {
|
||||
const sessionId = await mintPlayerSession(page);
|
||||
await applySession(page, sessionId);
|
||||
return sessionId;
|
||||
}
|
||||
+8
-2
@@ -15,8 +15,14 @@ interface StateBody {
|
||||
// (delivered/abandoned) is unbounded in the DB and must never leak into a pushed frame.
|
||||
const ALLOWED_PACKAGE_STATUSES = new Set(['open', 'loaded', 'expired']);
|
||||
|
||||
test('GET /api/events streams state snapshots over SSE', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/events`, { headers: { Accept: 'text/event-stream' } });
|
||||
test('GET /api/events streams state snapshots over SSE', async ({ request }) => {
|
||||
// v1.1.0 (ISS-107): the stream is session-guarded — EventSource cannot set headers, so the client
|
||||
// passes ?session= (requireAuthSse). Mint one via the test hook and use exactly that path.
|
||||
const mint = await request.post('/api/test/mint-player-session');
|
||||
expect(mint.status(), 'mint-player-session hook should be enabled in the CI stack (FLIGHT_TEST_HOOKS=1)').toBe(200);
|
||||
const { sessionId } = (await mint.json()) as { sessionId: string };
|
||||
|
||||
const res = await fetch(`${baseUrl}/api/events?session=${sessionId}`, { headers: { Accept: 'text/event-stream' } });
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('content-type')).toContain('text/event-stream');
|
||||
expect(res.body).not.toBeNull();
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
import { authenticate } from './auth';
|
||||
|
||||
// v1.1.0 (ISS-107): game routes are session-guarded — mint + apply a player session before every test.
|
||||
test.beforeEach(async ({ page }) => { await authenticate(page); });
|
||||
|
||||
// v0.15.0 — ISS-002: hovering a plane or package in either side panel highlights it on the map.
|
||||
// Fleet rows + on-board blocks highlight the plane marker (and its route leg while flying);
|
||||
// open-package rows highlight both endpoint airports. The map exposes three top-most
|
||||
|
||||
@@ -3,6 +3,11 @@ import { expect, test } from '@playwright/test';
|
||||
// v0.39.0 (ISS-069) — maintenance: expectations use the real balance constants.
|
||||
import { REPAIR_COST } from '../lib/game/catalog';
|
||||
|
||||
import { authenticate } from './auth';
|
||||
|
||||
// v1.1.0 (ISS-107): game routes are session-guarded — mint + apply a player session before every test.
|
||||
test.beforeEach(async ({ page }) => { await authenticate(page); });
|
||||
|
||||
// v0.39.0 (ISS-069), reworked in v0.45.0 (ISS-089) — maintenance visible flow against the live
|
||||
// origin: health is shown per plane in the fleet list; forcing it to 0 via the set-health hook shows
|
||||
// the "broken" pill and blocks dispatch with a 409 that mutates nothing; clicking Repair deducts
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
import { authenticate } from './auth';
|
||||
|
||||
// v1.1.0 (ISS-107): game routes are session-guarded — mint + apply a player session before every test.
|
||||
test.beforeEach(async ({ page }) => { await authenticate(page); });
|
||||
|
||||
// v0.12.0 — map interactions against the live origin (PLAN.md "Middle — Map"):
|
||||
// expanded airport popup (name + ICAO kept, plus approximate local time and a
|
||||
// deterministic generated weather), plane markers whose positions come from the
|
||||
|
||||
@@ -2,6 +2,11 @@ import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
import { authenticate } from './auth';
|
||||
|
||||
// v1.1.0 (ISS-107): game routes are session-guarded — mint + apply a player session before every test.
|
||||
test.beforeEach(async ({ page }) => { await authenticate(page); });
|
||||
|
||||
const APP_URL = '/';
|
||||
|
||||
// Read at test time so the assertion tracks package.json automatically.
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
import { authenticate } from './auth';
|
||||
|
||||
// v1.1.0 (ISS-107): game routes are session-guarded — mint + apply a player session before every test.
|
||||
test.beforeEach(async ({ page }) => { await authenticate(page); });
|
||||
|
||||
// v0.9.0 — single-screen 3-pane shell + left Assets pane against the live origin.
|
||||
// The panes render from the shell's single /api/state poll (lib/useGameState.ts).
|
||||
|
||||
|
||||
@@ -7,6 +7,11 @@ import { haversineKm, initialBearingDeg } from '../lib/geo';
|
||||
import { weatherFor } from '../lib/weather';
|
||||
import { fuelAtWeighted, requiredFuelFor, windBurnFactor } from '../lib/sim/movement';
|
||||
|
||||
import { authenticate } from './auth';
|
||||
|
||||
// v1.1.0 (ISS-107): game routes are session-guarded — mint + apply a player session before every test.
|
||||
test.beforeEach(async ({ page }) => { await authenticate(page); });
|
||||
|
||||
// v0.26.0 — ISS-043 part 2 (UI): the flight queue. A plane's not-yet-started legs show up in a
|
||||
// Queue sub-section of its on-board block (cancel per leg; adding happens via fly-to / Relocate
|
||||
// while flying, ISS-053), and
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { authenticate } from './auth';
|
||||
|
||||
// v1.1.0 (ISS-107): game routes are session-guarded — mint + apply a player session before every test.
|
||||
test.beforeEach(async ({ page }) => { await authenticate(page); });
|
||||
|
||||
// v0.18.0 — ISS-010: relocating a GROUNDED (empty) plane to any airport against the live origin.
|
||||
// Two UI surfaces drive the EXISTING POST /api/planes/{id}/fly endpoint (pure repositioning has
|
||||
// been allowed since v0.12.0 — no API change): the left-pane Relocate section (ISS-023, v0.21.0)
|
||||
|
||||
@@ -9,6 +9,11 @@ import { fuelAtWeighted, guaranteedRangeKm, windBurnFactor } from '../lib/sim/mo
|
||||
import { FUEL_DISCOUNT_PER_REP, MAX_FUEL_DISCOUNT, MAX_PLANE_DISCOUNT, PLANE_DISCOUNT_PER_REP, repRate } from '../lib/game/catalog';
|
||||
import { STARTING_REPUTATION } from '../lib/game/seed';
|
||||
|
||||
import { authenticate } from './auth';
|
||||
|
||||
// v1.1.0 (ISS-107): game routes are session-guarded — mint + apply a player session before every test.
|
||||
test.beforeEach(async ({ page }) => { await authenticate(page); });
|
||||
|
||||
// v0.12.0 — right Jobs & Market pane against the live origin: open packages render with
|
||||
// names/reward/deadline and Load (UI click) binds one to the starter; the on-board list
|
||||
// (moved to the left pane in v0.20.0, ISS-021) shows loaded packages with fly-to-destination +
|
||||
|
||||
@@ -4,6 +4,11 @@ import { test, expect, type Page } from '@playwright/test';
|
||||
// pool, at EDDF.
|
||||
import { POOL_TARGET_PER_AIRPORT as POOL_TARGET } from '../lib/game/seed';
|
||||
|
||||
import { authenticate } from './auth';
|
||||
|
||||
// v1.1.0 (ISS-107): game routes are session-guarded — mint + apply a player session before every test.
|
||||
test.beforeEach(async ({ page }) => { await authenticate(page); });
|
||||
|
||||
// v0.52.0 (ISS-117), finalized in #135: fixed world clock against the live origin — a fully
|
||||
// grounded fleet freezes the clock, background-tick advance at a raised rate while something is
|
||||
// active, package expiry via the FLIGHT_TEST_HOOKS hooks (enabled in docker-compose.yml), and
|
||||
|
||||
@@ -2,6 +2,11 @@ import { execSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
import { authenticate } from './auth';
|
||||
|
||||
// v1.1.0 (ISS-107): game routes are session-guarded — mint + apply a player session before every test.
|
||||
test.beforeEach(async ({ page }) => { await authenticate(page); });
|
||||
|
||||
// Repo root — docker-compose.yml lives here; `docker compose restart` runs on
|
||||
// this host (the test runner), browsers stay remote. CI points E2E_COMPOSE_ARGS at the
|
||||
// isolated stack file + project (-f docker-compose.ci.yml -p flight-ci) instead of the default one.
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { authenticate } from './auth';
|
||||
|
||||
// v1.1.0 (ISS-107): game routes are session-guarded — mint + apply a player session before every test.
|
||||
test.beforeEach(async ({ page }) => { await authenticate(page); });
|
||||
|
||||
// v0.17.0 — ISS-008: action errors surface as toasts at the map's right edge (clear of the
|
||||
// right pane, in addition to the inline pane error) and game events appear as info toasts via
|
||||
// snapshot diffing; the left-pane Time section offers an explicit "Enable notifications" opt-in
|
||||
|
||||
@@ -6,6 +6,11 @@ import { initialBearingDeg } from '../lib/geo';
|
||||
import { weatherFor } from '../lib/weather';
|
||||
import { windBurnFactor } from '../lib/sim/movement';
|
||||
|
||||
import { authenticate } from './auth';
|
||||
|
||||
// v1.1.0 (ISS-107): game routes are session-guarded — mint + apply a player session before every test.
|
||||
test.beforeEach(async ({ page }) => { await authenticate(page); });
|
||||
|
||||
// v0.31.0 — ISS-055: gameplay with TWO planes in the fleet. The reported bug ("can't load cargo
|
||||
// if both planes are on the same airport") was UX-only: LoadControls' multi-plane path rendered a
|
||||
// greyed-out Load button until a plane was picked from the dropdown, while the single-plane path
|
||||
|
||||
Reference in New Issue
Block a user