Files
civitai__civitai/tests/preview-auth-guard.spec.ts
T
Briant Diehl 4b17fc88ad test(auth): tier-3 e2e specs for the cutover + unblock the hub harness
Runtime (Playwright) complements to the unit layer. Both target DEPLOYED
environments (PREVIEW_URL / HUB_URL); validated locally via discovery + types.

- tests/preview-auth-guard.spec.ts (main-app preview harness): the _app moderator
  guard bounces a gate-passing non-mod (tester/gold) off /moderator, admits a mod
  (control), and /login forwards to the hub threading returnUrl. Covers the
  tier-1 _app guard + /login cutover at runtime.
- apps/auth/e2e/hub-login.spec.ts (hub harness): GET /login/<provider> 302s to the
  upstream authorize URL with code flow + scope + PKCE (S256) + state + /callback
  redirect_uri. Runtime complement to the buildAuthorizeUrl unit test.

Fix: hub-auth.setup.ts imported sessionCookieName from @civitai/auth, whose
package main is raw TS with no "type":"module" — Playwright's ESM loader can't
extract a named export from it, and as a project dependency that import broke the
ENTIRE hub suite (hub-smoke included). Derive the cookie name locally instead
(mirrors preview-auth.setup.ts), so the suite can list + run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 15:44:57 -06:00

71 lines
3.8 KiB
TypeScript

import { expect, test } from '@playwright/test';
import { storageStatePath } from './preview-fixtures';
/**
* Auth-guard tests for a deployed PR preview — the behaviors the NextAuth -> hub cutover introduced that the
* existing smoke / moderation specs don't cover:
*
* 1. The _app moderator guard (server/auth/route-guard.ts, moved off the edge middleware) REDIRECTS a
* gate-passing NON-moderator away from /moderator. preview-smoke proves a mod can enter and
* preview-moderation proves the queues render — neither proves a non-mod is bounced, which is the whole
* point of the new guard.
* 2. /login is now a pure server-side redirect to the centralized hub (buildHubLoginRedirect), threading the
* returnUrl. We assert the redirect target WITHOUT following it (the hop lands on the external hub).
*
* Only runs under playwright.preview.config.ts (needs PREVIEW_URL + the minted storage states).
*/
const MODERATOR_PATH = '/moderator/reports';
// tester (Flipt allowlist) + gold (allowlist + tier) both CLEAR the preview gate but lack isModerator, so the
// _app guard must bounce them from /moderator specifically.
const NON_MOD_ROLES = ['tester', 'gold'] as const;
for (const role of NON_MOD_ROLES) {
test.describe(`_app moderator guard bounces ${role}`, () => {
test.use({ storageState: storageStatePath(role) });
test(`${role} cannot reach ${MODERATOR_PATH}`, async ({ page }) => {
await page.goto(MODERATOR_PATH, { waitUntil: 'domcontentloaded' });
// The guard sends an authed non-mod to '/' (login can't grant the missing permission, so it would loop).
expect(page.url(), `${role} should be bounced off the moderator surface`).not.toContain('/moderator');
// A bounce to /login would mean the session didn't resolve (a different failure) — assert it did.
expect(page.url(), `${role} session should resolve, not bounce to login`).not.toContain('/login');
// And it isn't the preview-access gate firing instead (these roles are allowlisted).
expect(page.url(), `${role} should not hit preview-restricted`).not.toContain('/preview-restricted');
});
});
}
test.describe('_app moderator guard admits a moderator (control)', () => {
test.use({ storageState: storageStatePath('mod') });
test('mod stays on /moderator/reports', async ({ page }) => {
const resp = await page.goto(MODERATOR_PATH, { waitUntil: 'domcontentloaded' });
expect(resp?.status(), 'HTTP status for /moderator/reports').toBeLessThan(400);
await expect(page).toHaveURL(/\/moderator\/reports/);
});
});
test.describe('/login forwards to the centralized hub', () => {
test('threads the returnUrl into the hub redirect', async ({ playwright, baseURL }) => {
// Inspect the 3xx Location WITHOUT following it — the target is the external hub. A fresh, cookieless
// request context is an anonymous /login hit (/login is exempt from the preview gate).
const ctx = await playwright.request.newContext({ baseURL });
try {
const res = await ctx.get('/login?returnUrl=%2Fmodels', { maxRedirects: 0 });
expect(res.status(), '/login should redirect').toBeGreaterThanOrEqual(300);
expect(res.status(), '/login should redirect').toBeLessThan(400);
const location = res.headers()['location'] ?? '';
expect(location, '/login should set a redirect Location').not.toBe('');
// Deploy-agnostic: don't pin the hub host (env-per-deploy). The hop targets a hub /login and the original
// dest survives it — only the '/' encodes (%2F / %252F …); the letters 'models' stay literal at any depth.
expect(location.toLowerCase(), 'should target the hub login').toContain('login');
expect(location, 'returnUrl dest should survive the hop').toContain('models');
} finally {
await ctx.dispose();
}
});
});