f4b6b8941a
CI runners hit Jest's default 5s `beforeAll` timeout on slideshowPublic.test.js's bootCrmDb call (~5.4s observed vs ~2s local — runner-to-runner I/O variance, not a regression). Same hook shape on slideshowAdmin.test.js is one slow runner away from the same failure. Raise both to 30s so this stops blocking unrelated PRs branched off beta. Adjacent to #654 — not strictly part of that fix but the only blocker between #656 and a green CI right now.
204 lines
8.5 KiB
JavaScript
204 lines
8.5 KiB
JavaScript
/**
|
|
* HTTP route tests for the ADMIN Live Slideshow endpoints:
|
|
* POST /api/admin/events/:id/slideshow/generate
|
|
* POST /api/admin/events/:id/slideshow/disable
|
|
* PATCH /api/admin/events/:id/slideshow
|
|
* PUT /api/admin/settings/slideshow (global preset + watermark + fit)
|
|
*
|
|
* Pins the contracts + the two regressions hit during the build:
|
|
* - the events table has NO `updated_at` column, so these writes must NOT set
|
|
* it (else every call 500s — that was the original "Generate" failure);
|
|
* - the `slideshow` feature flag gates these endpoints (403 when off);
|
|
* - PUT /admin/settings/slideshow validates + clamps every key.
|
|
*/
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
|
|
process.env.NODE_ENV = 'test';
|
|
process.env.TEST_DATABASE_PATH = path.join(
|
|
fs.mkdtempSync(path.join(os.tmpdir(), 'picpeak-show-admin-')), 'db.sqlite'
|
|
);
|
|
process.env.JWT_SECRET = process.env.JWT_SECRET || 'slideshow-test-secret';
|
|
|
|
const express = require('express');
|
|
const cookieParser = require('cookie-parser');
|
|
const request = require('supertest');
|
|
const { bootCrmDb, seedMinimal, assignAdminRole, mintAdminToken } = require('../integration/helpers/crmDb');
|
|
const { invalidateFeatureFlagCache } = require('../../src/middleware/requireFeatureFlag');
|
|
|
|
async function setFlag(db, key, on) {
|
|
await db('feature_flags').where({ key }).del();
|
|
await db('feature_flags').insert({ key, value: on ? 1 : 0 });
|
|
invalidateFeatureFlagCache();
|
|
}
|
|
|
|
async function insertEvent(db, adminId, over = {}) {
|
|
const base = {
|
|
slug: `ev-${Math.random().toString(16).slice(2)}`,
|
|
event_type: 'wedding',
|
|
event_name: 'Test Wedding',
|
|
event_date: '2026-05-29',
|
|
host_email: 'host@example.com',
|
|
admin_email: 'admin@example.com',
|
|
password_hash: 'x',
|
|
share_link: `/gallery/share-${Math.random().toString(16).slice(2)}`,
|
|
share_token: `st-${Math.random().toString(16).slice(2)}`,
|
|
expires_at: new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString(),
|
|
is_active: 1, is_archived: 0, is_draft: 0,
|
|
created_by: adminId,
|
|
created_at: new Date().toISOString(),
|
|
...over,
|
|
};
|
|
const r = await db('events').insert(base).returning('id');
|
|
return r[0]?.id ?? r[0];
|
|
}
|
|
|
|
describe('admin Live Slideshow endpoints', () => {
|
|
let db; let cleanup; let app; let adminId; let token;
|
|
|
|
// Match slideshowPublic.test.js — bootCrmDb's full migration run intermittently
|
|
// exceeds Jest's default 5s `beforeAll` timeout on slower CI runners; raise
|
|
// it so this doesn't block PRs.
|
|
beforeAll(async () => {
|
|
({ db, cleanup } = await bootCrmDb());
|
|
({ adminId } = await seedMinimal(db));
|
|
await assignAdminRole(db, adminId, 'super_admin');
|
|
token = mintAdminToken(adminId);
|
|
|
|
app = express();
|
|
app.use(express.json());
|
|
app.use(cookieParser());
|
|
app.use('/api/admin/events', require('../../src/routes/adminEvents'));
|
|
app.use('/api/admin/settings', require('../../src/routes/adminSettings'));
|
|
// eslint-disable-next-line no-unused-vars
|
|
app.use((err, req, res, next) => {
|
|
res.status(err.statusCode || err.status || 500).json({ error: err.message, code: err.code });
|
|
});
|
|
}, 30000);
|
|
|
|
afterAll(async () => { await cleanup(); });
|
|
|
|
beforeEach(async () => {
|
|
await db('events').del();
|
|
await db('app_settings').del();
|
|
await setFlag(db, 'slideshow', true);
|
|
});
|
|
|
|
const auth = (req) => req.set('Authorization', `Bearer ${token}`);
|
|
|
|
describe('generate / disable', () => {
|
|
it('mints a share token (no updated_at column → must not 500)', async () => {
|
|
const id = await insertEvent(db, adminId);
|
|
const res = await auth(request(app).post(`/api/admin/events/${id}/slideshow/generate`));
|
|
expect(res.status).toBe(200);
|
|
expect(typeof res.body.show_share_token).toBe('string');
|
|
expect(res.body.show_share_token).toHaveLength(64);
|
|
expect(res.body.slideshow_url).toContain(`/show/${res.body.show_share_token}`);
|
|
const row = await db('events').where({ id }).first();
|
|
expect(row.show_share_token).toBe(res.body.show_share_token);
|
|
});
|
|
|
|
it('regenerate rotates the token', async () => {
|
|
const id = await insertEvent(db, adminId, { show_share_token: 'old-token' });
|
|
const res = await auth(request(app).post(`/api/admin/events/${id}/slideshow/generate`));
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.show_share_token).not.toBe('old-token');
|
|
});
|
|
|
|
it('disable nulls the token', async () => {
|
|
const id = await insertEvent(db, adminId, { show_share_token: 'live-token' });
|
|
const res = await auth(request(app).post(`/api/admin/events/${id}/slideshow/disable`));
|
|
expect(res.status).toBe(200);
|
|
const row = await db('events').where({ id }).first();
|
|
expect(row.show_share_token == null).toBe(true);
|
|
});
|
|
|
|
it('403 when the slideshow feature is off', async () => {
|
|
const id = await insertEvent(db, adminId);
|
|
await setFlag(db, 'slideshow', false);
|
|
const res = await auth(request(app).post(`/api/admin/events/${id}/slideshow/generate`));
|
|
expect(res.status).toBe(403);
|
|
});
|
|
|
|
it('401 without an admin token', async () => {
|
|
const id = await insertEvent(db, adminId);
|
|
const res = await request(app).post(`/api/admin/events/${id}/slideshow/generate`);
|
|
expect(res.status).toBe(401);
|
|
});
|
|
});
|
|
|
|
describe('PATCH /:id/slideshow', () => {
|
|
it('persists display + watermark mode (no updated_at column → must not 500)', async () => {
|
|
const id = await insertEvent(db, adminId);
|
|
const res = await auth(request(app).patch(`/api/admin/events/${id}/slideshow`)).send({
|
|
show_interval_ms: 9000,
|
|
show_transition: 'cut',
|
|
show_transition_ms: 300,
|
|
show_watermark: true,
|
|
show_colorfilter: 'bw',
|
|
});
|
|
expect(res.status).toBe(200);
|
|
const row = await db('events').where({ id }).first();
|
|
expect(row.show_interval_ms).toBe(9000);
|
|
expect(row.show_transition).toBe('cut');
|
|
expect(row.show_transition_ms).toBe(300);
|
|
expect(row.show_colorfilter).toBe('bw');
|
|
expect(row.show_watermark === 1 || row.show_watermark === true).toBe(true);
|
|
});
|
|
|
|
it('show_watermark=null sets the column to NULL (inherit global)', async () => {
|
|
const id = await insertEvent(db, adminId, { show_watermark: 1 });
|
|
const res = await auth(request(app).patch(`/api/admin/events/${id}/slideshow`)).send({ show_watermark: null });
|
|
expect(res.status).toBe(200);
|
|
const row = await db('events').where({ id }).first();
|
|
expect(row.show_watermark == null).toBe(true);
|
|
});
|
|
|
|
it('400 on an invalid transition', async () => {
|
|
const id = await insertEvent(db, adminId);
|
|
const res = await auth(request(app).patch(`/api/admin/events/${id}/slideshow`)).send({ show_transition: 'wormhole' });
|
|
expect(res.status).toBe(400);
|
|
});
|
|
});
|
|
|
|
describe('PUT /api/admin/settings/slideshow', () => {
|
|
const getSetting = async (key) => {
|
|
const row = await db('app_settings').where({ setting_key: key }).first();
|
|
return row ? JSON.parse(row.setting_value) : undefined;
|
|
};
|
|
|
|
it('persists the global preset + watermark + fit, clamping out-of-range values', async () => {
|
|
const res = await auth(request(app).put('/api/admin/settings/slideshow')).send({
|
|
slideshow_fit: 'contain',
|
|
slideshow_interval_ms: 9000,
|
|
slideshow_transition: 'slide',
|
|
slideshow_transition_ms: 250,
|
|
slideshow_colorfilter: 'sepia',
|
|
slideshow_watermark_enabled: true,
|
|
slideshow_watermark_opacity: 999, // clamp -> 100
|
|
slideshow_watermark_size: 99, // clamp -> 40
|
|
});
|
|
expect(res.status).toBe(200);
|
|
expect(await getSetting('slideshow_fit')).toBe('contain');
|
|
expect(await getSetting('slideshow_interval_ms')).toBe(9000);
|
|
expect(await getSetting('slideshow_transition')).toBe('slide');
|
|
expect(await getSetting('slideshow_transition_ms')).toBe(250);
|
|
expect(await getSetting('slideshow_colorfilter')).toBe('sepia');
|
|
expect(await getSetting('slideshow_watermark_enabled')).toBe(true);
|
|
expect(await getSetting('slideshow_watermark_opacity')).toBe(100);
|
|
expect(await getSetting('slideshow_watermark_size')).toBe(40);
|
|
});
|
|
|
|
it('coerces an invalid fit / transition to the safe default', async () => {
|
|
const res = await auth(request(app).put('/api/admin/settings/slideshow')).send({
|
|
slideshow_fit: 'banana',
|
|
slideshow_transition: 'wormhole',
|
|
});
|
|
expect(res.status).toBe(200);
|
|
expect(await getSetting('slideshow_fit')).toBe('cover');
|
|
expect(await getSetting('slideshow_transition')).toBe('crossfade');
|
|
});
|
|
});
|
|
});
|