Files
picpeak/tests/e2e/auth-smoke.spec.ts
T
Paul Nothaft 44adabca9e test(e2e): read the admin JWT from the cookie, not the login body (#1071)
Three specs acquire an admin token with `const body = await res.json();
return body.token`. The admin login has not returned a token in its body
for some time — establishAdminSession() sets the JWT as the httpOnly
`admin_token` cookie and responds with `res.json({ user })` — so the
token was undefined and every one of them failed at the first assertion,
before exercising anything they were written to cover.

Server-side the cookie and an Authorization: Bearer header are
interchangeable (see middleware/gallery.js, which reads the cookie first
and accepts an admin-typed Bearer second), so the fix is to read the
value back out of the context cookie jar and keep threading it as a
Bearer. Every downstream call in these specs stays exactly as it was.

Measured against a real stack, running only these three files:

  before   0 passed, 6 failed   — all six at the token assertion
  after    3 passed, 3 failed

The three that still fail no longer fail on auth: they get deep into the
flow and then miss UI that has since changed (a settings label, a
locator that no longer resolves). That is a separate and much larger
staleness problem across this directory — a full run is 12 passed
against roughly two dozen failures of that kind — and it is not
addressed here.

Worth knowing: no CI workflow runs tests/e2e at all, which is why this
rotted silently while `npm run test:e2e` stayed documented in CLAUDE.md.
Wiring it up is the obvious follow-up, but it has to wait until the
suite is actually green, or it would just pin main red.

Claude-Session: https://claude.ai/code/session_01Ra4hcsYiKuQLbbRsg6EjAc

Co-authored-by: Paul Nothaft <paul@MacStudio-von-Paul.local>
2026-08-18 22:14:00 +02:00

215 lines
7.0 KiB
TypeScript

import { test, expect, Page } from '@playwright/test';
import fs from 'fs';
import path from 'path';
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@example.com';
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'Admin!234';
const GALLERY_PASSWORD = process.env.GALLERY_PASSWORD || 'PlaywrightGallery123!';
async function createEventWithPhotos(page: Page, adminToken?: string, attempt = 1) {
const api = page.request;
let token = adminToken;
if (!token) {
const loginResponse = await api.post('/api/auth/admin/login', {
data: {
username: ADMIN_EMAIL,
password: ADMIN_PASSWORD,
},
});
expect(loginResponse.ok()).toBeTruthy();
const cookies = await page.context().cookies();
token = cookies.find((c) => c.name === 'admin_token')?.value;
expect(token, 'admin_token cookie missing from the login response').toBeTruthy();
}
const eventName = `Playwright Smoke ${Date.now()}`;
const eventDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
.toISOString()
.slice(0, 10);
if (!token) {
throw new Error('Failed to acquire admin token');
}
const eventResponse = await api.post('/api/admin/events', {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: {
event_type: 'wedding',
event_name: eventName,
event_date: eventDate,
customer_name: 'Playwright Host',
customer_email: 'host@example.com',
host_name: 'Playwright Host',
host_email: 'host@example.com',
admin_email: ADMIN_EMAIL,
password: GALLERY_PASSWORD,
expiration_days: 30,
allow_user_uploads: false,
allow_downloads: true,
disable_right_click: false,
watermark_downloads: false,
},
});
if (!eventResponse.ok()) {
const message = await eventResponse.text();
if (
attempt < 3 &&
/UNIQUE constraint failed: events\.slug/i.test(message || '')
) {
await page.waitForTimeout(150);
return createEventWithPhotos(page, token, attempt + 1);
}
throw new Error(`Event creation failed: ${eventResponse.status()} ${message}`);
}
const event = await eventResponse.json();
const imagePath = path.join(process.cwd(), 'test-assets', 'img1.png');
const buffer = fs.readFileSync(imagePath);
const uploadResponse = await api.post(`/api/admin/events/${event.id}/upload`, {
headers: {
Authorization: `Bearer ${token}`,
},
multipart: {
photos: {
name: path.basename(imagePath),
mimeType: 'image/png',
buffer,
},
category_id: 'individual',
},
});
expect(uploadResponse.ok()).toBeTruthy();
return {
event,
shareLink: event.share_link,
slug: event.slug,
adminToken: token,
};
}
async function updateShortGallerySetting(page: Page, adminToken: string, enabled: boolean) {
const response = await page.request.put('/api/admin/settings/general', {
headers: {
Authorization: `Bearer ${adminToken}`,
'Content-Type': 'application/json',
},
data: {
general_short_gallery_urls: enabled,
},
});
expect(response.ok()).toBeTruthy();
}
async function openGalleryShareLink(page: Page, shareLink: string) {
await page.context().clearCookies();
await page.goto(shareLink);
await page.waitForLoadState('domcontentloaded');
try {
await page.getByText(/Enter Gallery Password/i).first().waitFor({ timeout: 5000 });
} catch {
// No password prompt shown (public gallery)
}
let passwordEntered = false;
const passwordTextbox = page.getByRole('textbox', { name: /password/i }).first();
if (await passwordTextbox.count()) {
await passwordTextbox.fill(GALLERY_PASSWORD);
passwordEntered = true;
}
const galleryPasswordField = page.getByPlaceholder(/gallery password/i);
if (!passwordEntered && await galleryPasswordField.count()) {
await galleryPasswordField.fill(GALLERY_PASSWORD);
passwordEntered = true;
} else if (!passwordEntered) {
const genericPasswordField = page.getByPlaceholder(/password/i).first();
if (await genericPasswordField.count()) {
await genericPasswordField.fill(GALLERY_PASSWORD);
passwordEntered = true;
} else {
const labelledPasswordField = page.getByLabel(/password/i).first();
if (await labelledPasswordField.count()) {
await labelledPasswordField.fill(GALLERY_PASSWORD);
passwordEntered = true;
}
}
}
if (!passwordEntered) {
const fallbackPasswordField = page.locator('input').first();
if (await fallbackPasswordField.count()) {
await fallbackPasswordField.fill(GALLERY_PASSWORD);
passwordEntered = true;
}
}
const viewButton = page.getByRole('button', { name: /View Gallery/i });
if (await viewButton.count()) {
try {
await viewButton.click({ noWaitAfter: true, timeout: 2000 });
} catch {
// Already navigated into gallery view.
}
}
const tiles = page.locator('.relative.group');
await expect(tiles.first()).toBeVisible({ timeout: 20000 });
return tiles;
}
test('admin login and gallery viewing smoke test', async ({ page }) => {
const { shareLink, adminToken } = await createEventWithPhotos(page);
// Admin UI login
await page.goto('/admin/login');
const emailField = page.getByLabel(/Email/i);
if (await emailField.count()) {
await emailField.fill(ADMIN_EMAIL);
await page.getByLabel(/Password/i).fill(ADMIN_PASSWORD);
await page.getByRole('button', { name: /Sign In|Log in/i }).click();
}
await expect(page.getByRole('heading', { name: /Dashboard/i })).toBeVisible({ timeout: 20000 });
let resetToken = adminToken;
try {
// Verify long-form share link works
const tiles = await openGalleryShareLink(page, shareLink);
await tiles.first().hover();
await tiles.first().getByRole('button', { name: /View full size/i }).click();
await expect(page.getByRole('button', { name: /Close/i })).toBeVisible();
await page.getByRole('button', { name: /Close/i }).click();
// Enable short gallery URLs
await updateShortGallerySetting(page, adminToken, true);
const settingsResponse = await page.request.get('/api/admin/settings', {
headers: {
Authorization: `Bearer ${adminToken}`,
},
});
expect(settingsResponse.ok()).toBeTruthy();
const adminSettings = await settingsResponse.json();
expect(adminSettings.general_short_gallery_urls === true || adminSettings.general_short_gallery_urls === 'true').toBeTruthy();
const { shareLink: shortShareLink, event: shortEvent } = await createEventWithPhotos(page, adminToken);
expect(shortShareLink).toMatch(/\/gallery\/[0-9a-fA-F]{32}$/);
expect(shortShareLink).not.toContain(shortEvent.slug);
// Verify short share link works
await openGalleryShareLink(page, shortShareLink);
// Legacy share link should still work after enabling short URLs
await openGalleryShareLink(page, shareLink);
} finally {
await updateShortGallerySetting(page, resetToken, false).catch(() => {
/* noop */
});
}
});