Timed jobs move from in-process timers to a job registry run by pg-boss: once per schedule across processes, retries, run history, cron schedules in the church timezone, and an admin Background jobs page with Run now. Playwright end-to-end tests run against their own seeded database. A Gitea Actions workflow runs backend tests, frontend checks and the e2e tests, and deploys v* tags with scripts/deploy.sh once secrets are set. Also fixes the at-the-door check-in count for multi-day tickets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
138 lines
6.6 KiB
JavaScript
138 lines
6.6 KiB
JavaScript
/**
|
|
* Prepares the end-to-end test database, then exits. Run before the e2e backend starts
|
|
* (see playwright.config.ts): drops and recreates the database, applies the migrations,
|
|
* and seeds settings, users and events. Writes e2e/.data/seed.json with the ids and
|
|
* login tokens the tests use.
|
|
*
|
|
* Database: E2E_DATABASE_URL, or backend/.env's DATABASE_URL with "_e2e" added to the
|
|
* database name. It is always a separate database — never the one you develop against.
|
|
*/
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const { execSync } = require('child_process');
|
|
|
|
const root = path.join(__dirname, '..', '..');
|
|
const backend = path.join(root, 'backend');
|
|
const req = (m) => require(require.resolve(m, { paths: [backend] }));
|
|
req('dotenv').config({ path: path.join(backend, '.env') });
|
|
|
|
const { e2eDatabaseUrl } = require('./db-url');
|
|
|
|
async function recreateDatabase(url) {
|
|
const { Client } = req('pg');
|
|
const target = new URL(url);
|
|
const dbName = decodeURIComponent(target.pathname.replace(/^\//, ''));
|
|
if (!/_e2e$|^e2e|test/i.test(dbName)) throw new Error(`Refusing to reset "${dbName}" — the e2e database name must contain "e2e" or "test".`);
|
|
const admin = new URL(url);
|
|
admin.pathname = '/postgres';
|
|
admin.search = '';
|
|
const client = new Client({ connectionString: admin.toString() });
|
|
await client.connect();
|
|
await client.query(`DROP DATABASE IF EXISTS "${dbName}" WITH (FORCE)`);
|
|
await client.query(`CREATE DATABASE "${dbName}"`);
|
|
await client.end();
|
|
}
|
|
|
|
async function seed(url) {
|
|
process.env.DATABASE_URL = url;
|
|
const prisma = require(path.join(backend, 'src/config/db'));
|
|
const { generateToken, hashPassword } = require(path.join(backend, 'src/config/auth'));
|
|
const { generateTicketsForRegistration } = require(path.join(backend, 'src/utils/ticketUtils'));
|
|
|
|
await prisma.appSetting.createMany({
|
|
data: [
|
|
{ key: 'setup_complete', value: 'true' },
|
|
{ key: 'org_name', value: 'E2E Church' },
|
|
{ key: 'org_address', value: '1 Test Street, Pretoria' },
|
|
],
|
|
});
|
|
|
|
const password = await hashPassword('E2e-password-123');
|
|
const mk = (role, name, email, phoneNumber) => prisma.user.create({
|
|
data: { name, email, phoneNumber, role, password, isActive: true, updatedAt: new Date() },
|
|
});
|
|
const users = {
|
|
admin: await mk('admin', 'Ada Admin', 'admin@e2e.test', '0820000001'),
|
|
supervisor: await mk('supervisor', 'Sam Supervisor', 'supervisor@e2e.test', '0820000002'),
|
|
staff: await mk('staff', 'Stan Staff', 'staff@e2e.test', '0820000003'),
|
|
user: await mk('user', 'Thandi Test', 'user@e2e.test', '0820000004'),
|
|
};
|
|
|
|
const D = 86400000;
|
|
const now = Date.now();
|
|
const at = (days, hourUtc = 16, minute = 30) => {
|
|
const d = new Date(now + days * D);
|
|
d.setUTCHours(hourUtc, minute, 0, 0);
|
|
return d;
|
|
};
|
|
|
|
const free = await prisma.event.create({
|
|
data: {
|
|
title: 'E2E Free Evening', description: 'A free evening for the tests.', startDate: at(1), endDate: at(1, 19),
|
|
price: 0, location: '1 Test Street, Pretoria', createdById: users.supervisor.id,
|
|
eventOptions: { create: [{ name: 'Entry', price: 0, isMainTicket: true }] },
|
|
},
|
|
});
|
|
const paid = await prisma.event.create({
|
|
data: {
|
|
title: 'E2E Paid Conference', description: 'A paid one-day conference.', startDate: at(3, 7), endDate: at(3, 15),
|
|
price: 150, location: '1 Test Street, Pretoria', createdById: users.supervisor.id,
|
|
eventOptions: { create: [{ name: 'Conference ticket', price: 150, isMainTicket: true }] },
|
|
},
|
|
});
|
|
|
|
// Multi-day: week 1 was a week ago, week 2 is in progress now, then three more weeks.
|
|
const todayStart = new Date(now - 30 * 60 * 1000);
|
|
const sessions = [-7, 0, 7, 14, 21].map((days, i) => {
|
|
const startsAt = new Date(todayStart.getTime() + days * D);
|
|
return { label: `Week ${i + 1}`, startsAt, endsAt: new Date(startsAt.getTime() + 90 * 60 * 1000), order: i };
|
|
});
|
|
const course = await prisma.event.create({
|
|
data: {
|
|
title: 'E2E Alpha Course', description: 'Five weekly evenings.', startDate: sessions[0].startsAt, endDate: sessions[4].endsAt,
|
|
price: 300, location: '1 Test Street, Pretoria', createdById: users.supervisor.id,
|
|
scheduleType: 'sessions', lateJoinEnabled: true, latePricing: 'prorata', checkInWindow: 'whole_day',
|
|
certificatesEnabled: true,
|
|
certificateConfig: { template: 'classic', title: 'Certificate of Completion', body: '{name} completed {event}.', rule: { type: 'count', value: 2 } },
|
|
sessions: { create: sessions },
|
|
eventOptions: { create: [{ name: 'Course ticket', price: 300, isMainTicket: true }] },
|
|
},
|
|
include: { sessions: { orderBy: { startsAt: 'asc' } }, eventOptions: true },
|
|
});
|
|
await prisma.event.update({ where: { id: course.id }, data: { lateJoinUntilSessionId: course.sessions[2].id } });
|
|
|
|
// Thandi is booked on the course and was checked in for week 1.
|
|
const reg = await prisma.registration.create({
|
|
data: {
|
|
userId: users.user.id, eventId: course.id, status: 'paid',
|
|
registrationOptions: { create: [{ eventOptionId: course.eventOptions[0].id, quantity: 1, priceSnapshot: 300, tranches: { create: [{ quantity: 1, priceSnapshot: 300 }] } }] },
|
|
},
|
|
});
|
|
await prisma.payment.create({ data: { amount: 300, method: 'cash', userId: users.user.id, registrationId: reg.id, eventId: course.id, recordedById: users.supervisor.id } });
|
|
await generateTicketsForRegistration(reg.id);
|
|
const ticket = await prisma.ticket.findFirst({ where: { eventId: course.id } });
|
|
await prisma.ticketUsage.create({ data: { ticketId: ticket.id, scannedById: users.staff.id, sessionId: course.sessions[0].id, scannedAt: course.sessions[0].startsAt } });
|
|
|
|
const out = {
|
|
users: Object.fromEntries(Object.entries(users).map(([k, u]) => [k, { id: u.id, name: u.name, email: u.email, token: generateToken(u.id, u.role, u.tokenVersion || 0) }])),
|
|
events: { free: free.id, paid: paid.id, course: course.id },
|
|
course: { sessions: course.sessions.map(s => ({ id: s.id, label: s.label })), ticketId: ticket.id, qrCode: ticket.qrCode },
|
|
};
|
|
fs.mkdirSync(path.join(root, 'e2e', '.data'), { recursive: true });
|
|
fs.writeFileSync(path.join(root, 'e2e', '.data', 'seed.json'), JSON.stringify(out, null, 2));
|
|
await prisma.$disconnect?.();
|
|
}
|
|
|
|
(async () => {
|
|
const url = e2eDatabaseUrl();
|
|
await recreateDatabase(url);
|
|
execSync('npx prisma migrate deploy', { cwd: backend, env: { ...process.env, DATABASE_URL: url }, stdio: 'ignore' });
|
|
await seed(url);
|
|
console.log('[e2e] Database ready:', new URL(url).pathname.slice(1));
|
|
process.exit(0);
|
|
})().catch((e) => {
|
|
console.error('[e2e] Preparing the database failed:', e);
|
|
process.exit(1);
|
|
});
|
|
|