review fixes: seeded-auth hard gate, ci.yml build serialization, assets 404
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
- M1: E2E_REQUIRE_SEEDED_AUTH=1 in the CI e2e step makes login failures hard failures (loginAs throws, guards disabled, globalSetup refuses a pre-populated DB); auth.spec redirect test asserts outright under the flag; non-admin /admin test is now a real authorization assertion - M2: ci.yml build step depends_on test — never two concurrent turbo builds on the shared workspace - M3: unknown /assets/* paths 404 from the SPA catch-all instead of serving index.html with an immutable cache header; spec arm added - minors: e2e step gets when: *image_build_when, health poll uses GATEWAY_PORT + AbortSignal.timeout, BETTER_AUTH_SECRET generated per run (no literal in tree), failure echoes artifact path, dev-guide documents the gate, stale verify-release comment fixed
This commit is contained in:
+16
-20
@@ -1,11 +1,14 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { loginAs, ADMIN_USER, TEST_USER } from './helpers/auth.js';
|
||||
import { loginAs, ADMIN_USER, REQUIRE_SEEDED_AUTH, TEST_USER } from './helpers/auth.js';
|
||||
|
||||
test.describe('Admin page — admin user', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAs(page, ADMIN_USER.email, ADMIN_USER.password);
|
||||
const url = page.url();
|
||||
test.skip(!url.includes('/chat'), 'No seeded admin user — skipping admin tests');
|
||||
test.skip(
|
||||
!REQUIRE_SEEDED_AUTH && !url.includes('/chat'),
|
||||
'No seeded admin user — skipping admin tests',
|
||||
);
|
||||
});
|
||||
|
||||
test('admin page loads with the Admin Panel heading', async ({ page }) => {
|
||||
@@ -43,26 +46,19 @@ test.describe('Admin page — non-admin user', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAs(page, TEST_USER.email, TEST_USER.password);
|
||||
const url = page.url();
|
||||
test.skip(!url.includes('/chat'), 'No seeded test user — skipping non-admin tests');
|
||||
test.skip(
|
||||
!REQUIRE_SEEDED_AUTH && !url.includes('/chat'),
|
||||
'No seeded test user — skipping non-admin tests',
|
||||
);
|
||||
});
|
||||
|
||||
test('non-admin visiting /admin sees access denied or is redirected', async ({ page }) => {
|
||||
test('non-admin visiting /admin never sees the admin panel', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
// Either redirected away or shown an access-denied message
|
||||
const onAdmin = page.url().includes('/admin');
|
||||
if (onAdmin) {
|
||||
// Should show some access-denied content rather than the full admin panel
|
||||
const hasPanel = await page
|
||||
.getByRole('heading', { name: /admin panel/i })
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
// If heading is visible, the guard allowed access (user may have admin role in this env)
|
||||
// — not a failure, just informational
|
||||
if (!hasPanel) {
|
||||
// access denied message, redirect, or guard placeholder
|
||||
const url = page.url();
|
||||
expect(url).toBeTruthy(); // environment-dependent — no hard assertion
|
||||
}
|
||||
}
|
||||
// Wait for the app shell to render (redirect and access-denied views both
|
||||
// keep the sidebar), then assert the panel itself is absent. globalSetup
|
||||
// seeds TEST_USER with role 'member', so this is a real authorization
|
||||
// assertion, not environment-dependent.
|
||||
await expect(page.getByRole('img', { name: /mosaic logo/i })).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByRole('heading', { name: /admin panel/i })).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { TEST_USER } from './helpers/auth.js';
|
||||
import { REQUIRE_SEEDED_AUTH, TEST_USER } from './helpers/auth.js';
|
||||
|
||||
// ── Login page ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -49,18 +49,14 @@ test.describe('Login page', () => {
|
||||
});
|
||||
|
||||
test('redirects to /chat after successful login', async ({ page }) => {
|
||||
// Only meaningful with known-good credentials; against a live environment
|
||||
// this would just probe someone else's user table.
|
||||
test.skip(!REQUIRE_SEEDED_AUTH, 'needs seeded credentials (E2E_REQUIRE_SEEDED_AUTH=1)');
|
||||
await page.goto('/login');
|
||||
await page.getByLabel('Email').fill(TEST_USER.email);
|
||||
await page.getByLabel('Password').fill(TEST_USER.password);
|
||||
await page.getByRole('button', { name: /sign in/i }).click();
|
||||
// Either reaches /chat or shows an error (if credentials are wrong in this env).
|
||||
// We assert a navigation away from /login, or the alert is shown.
|
||||
await Promise.race([
|
||||
expect(page).toHaveURL(/\/chat/, { timeout: 10_000 }),
|
||||
expect(page.getByRole('alert')).toBeVisible({ timeout: 10_000 }),
|
||||
]).catch(() => {
|
||||
// Acceptable — environment may not have seeded credentials
|
||||
});
|
||||
await expect(page).toHaveURL(/\/chat/, { timeout: 10_000 });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { loginAs, TEST_USER } from './helpers/auth.js';
|
||||
import { loginAs, REQUIRE_SEEDED_AUTH, TEST_USER } from './helpers/auth.js';
|
||||
|
||||
test.describe('Chat page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAs(page, TEST_USER.email, TEST_USER.password);
|
||||
// If login failed (no seeded user in env) we may be on /login — skip
|
||||
const url = page.url();
|
||||
test.skip(!url.includes('/chat'), 'No seeded test user — skipping authenticated tests');
|
||||
test.skip(
|
||||
!REQUIRE_SEEDED_AUTH && !url.includes('/chat'),
|
||||
'No seeded test user — skipping authenticated tests',
|
||||
);
|
||||
});
|
||||
|
||||
test('chat page loads and shows the conversation area', async ({ page }) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FullConfig } from '@playwright/test';
|
||||
import { ADMIN_USER, TEST_USER } from './helpers/auth.js';
|
||||
import { ADMIN_USER, REQUIRE_SEEDED_AUTH, TEST_USER } from './helpers/auth.js';
|
||||
|
||||
/**
|
||||
* Seed the E2E users through the gateway's real APIs (#1445, P6).
|
||||
@@ -10,7 +10,9 @@ import { ADMIN_USER, TEST_USER } from './helpers/auth.js';
|
||||
*
|
||||
* Against an environment that already has users (needsSetup=false), seeding is
|
||||
* skipped entirely: the specs keep their own skip-when-login-fails guards, so
|
||||
* a live environment stays usable as a test target without mutation.
|
||||
* a live environment stays usable as a test target without mutation. Under
|
||||
* E2E_REQUIRE_SEEDED_AUTH=1 (CI) that state is instead a hard failure and the
|
||||
* guards are disabled — see helpers/auth.ts.
|
||||
*
|
||||
* On a fresh database, any seeding failure throws and fails the whole run: an
|
||||
* E2E gate whose authenticated suites silently skip would pass while proving
|
||||
@@ -25,6 +27,14 @@ export default async function globalSetup(config: FullConfig): Promise<void> {
|
||||
}
|
||||
const status = (await statusRes.json()) as { needsSetup: boolean };
|
||||
if (!status.needsSetup) {
|
||||
if (REQUIRE_SEEDED_AUTH) {
|
||||
// CI boots the gateway on a fresh HOME-isolated database, so an
|
||||
// already-populated one means the isolation regressed — refuse to run
|
||||
// against unknown data rather than skip-and-pass.
|
||||
throw new Error(
|
||||
'E2E_REQUIRE_SEEDED_AUTH=1 but the database already has users — gateway HOME isolation regressed?',
|
||||
);
|
||||
}
|
||||
console.info('[e2e setup] users already exist; skipping seed');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -12,17 +12,29 @@ export const ADMIN_USER = {
|
||||
name: 'E2E Admin User',
|
||||
};
|
||||
|
||||
/**
|
||||
* Set when the database was seeded by global-setup (CI sets it in the
|
||||
* publish.yml e2e step). Seeded credentials MUST work, so login failures are
|
||||
* hard failures and the skip-when-login-fails guards are disabled — otherwise
|
||||
* a login regression would skip every authenticated suite and the gate would
|
||||
* pass while proving nothing. Unset (a live environment used as a test
|
||||
* target), the guards stay on and unseeded credentials skip their suites.
|
||||
*/
|
||||
export const REQUIRE_SEEDED_AUTH = process.env['E2E_REQUIRE_SEEDED_AUTH'] === '1';
|
||||
|
||||
/**
|
||||
* Fill the login form and submit, then wait for the post-login redirect to
|
||||
* /chat. On failed login the wait times out and is swallowed: the page stays
|
||||
* on /login, and the callers' `test.skip(!url.includes('/chat'))` guards see
|
||||
* that. Without this wait, every guard read page.url() before the redirect
|
||||
* happened and skipped its suite even when login succeeded (#1445).
|
||||
* /chat. Under REQUIRE_SEEDED_AUTH a missed redirect throws (failing the
|
||||
* test). Otherwise the timeout is swallowed: the page stays on /login and the
|
||||
* callers' `test.skip(...)` guards see that. Without this wait, every guard
|
||||
* read page.url() before the redirect happened and skipped its suite even
|
||||
* when login succeeded (#1445).
|
||||
*/
|
||||
export async function loginAs(page: Page, email: string, password: string): Promise<void> {
|
||||
await page.goto('/login');
|
||||
await page.getByLabel('Email').fill(email);
|
||||
await page.getByLabel('Password').fill(password);
|
||||
await page.getByRole('button', { name: /sign in/i }).click();
|
||||
await page.waitForURL(/\/chat/, { timeout: 10_000 }).catch(() => {});
|
||||
const redirect = page.waitForURL(/\/chat/, { timeout: 10_000 });
|
||||
await (REQUIRE_SEEDED_AUTH ? redirect : redirect.catch(() => {}));
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { loginAs, TEST_USER } from './helpers/auth.js';
|
||||
import { loginAs, REQUIRE_SEEDED_AUTH, TEST_USER } from './helpers/auth.js';
|
||||
|
||||
test.describe('Sidebar navigation', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAs(page, TEST_USER.email, TEST_USER.password);
|
||||
const url = page.url();
|
||||
test.skip(!url.includes('/chat'), 'No seeded test user — skipping authenticated tests');
|
||||
test.skip(
|
||||
!REQUIRE_SEEDED_AUTH && !url.includes('/chat'),
|
||||
'No seeded test user — skipping authenticated tests',
|
||||
);
|
||||
});
|
||||
|
||||
test('sidebar shows the Mosaic brand', async ({ page }) => {
|
||||
@@ -64,7 +67,10 @@ test.describe('Route transitions', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAs(page, TEST_USER.email, TEST_USER.password);
|
||||
const url = page.url();
|
||||
test.skip(!url.includes('/chat'), 'No seeded test user — skipping authenticated tests');
|
||||
test.skip(
|
||||
!REQUIRE_SEEDED_AUTH && !url.includes('/chat'),
|
||||
'No seeded test user — skipping authenticated tests',
|
||||
);
|
||||
});
|
||||
|
||||
test('navigating chat → projects → settings → chat works without errors', async ({ page }) => {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { loginAs, TEST_USER } from './helpers/auth.js';
|
||||
import { loginAs, REQUIRE_SEEDED_AUTH, TEST_USER } from './helpers/auth.js';
|
||||
|
||||
test.describe('Projects page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAs(page, TEST_USER.email, TEST_USER.password);
|
||||
const url = page.url();
|
||||
test.skip(!url.includes('/chat'), 'No seeded test user — skipping authenticated tests');
|
||||
test.skip(
|
||||
!REQUIRE_SEEDED_AUTH && !url.includes('/chat'),
|
||||
'No seeded test user — skipping authenticated tests',
|
||||
);
|
||||
});
|
||||
|
||||
test('projects page loads with heading', async ({ page }) => {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { loginAs, TEST_USER } from './helpers/auth.js';
|
||||
import { loginAs, REQUIRE_SEEDED_AUTH, TEST_USER } from './helpers/auth.js';
|
||||
|
||||
test.describe('Settings page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAs(page, TEST_USER.email, TEST_USER.password);
|
||||
const url = page.url();
|
||||
test.skip(!url.includes('/chat'), 'No seeded test user — skipping authenticated tests');
|
||||
test.skip(
|
||||
!REQUIRE_SEEDED_AUTH && !url.includes('/chat'),
|
||||
'No seeded test user — skipping authenticated tests',
|
||||
);
|
||||
});
|
||||
|
||||
test('settings page loads with heading', async ({ page }) => {
|
||||
|
||||
Reference in New Issue
Block a user