test: Phase 5 verification scripts and opt-in e2e integration spec
Adds scripts to seed scenario data, create Proclamations, snapshot schedules, drive the Kiosk-facing HTTP endpoints, and an opt-in (P5_INTEGRATION=1) Vitest spec exercising create/type/copy against a scratch database. The integration spec is skipped by default so the normal suite needs no DB.
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { PrismaClient } from '$lib/generated/prisma/client';
|
||||
import pg from 'pg';
|
||||
import { PrismaPg } from '@prisma/adapter-pg';
|
||||
import {
|
||||
getScheduleForLicense,
|
||||
setScheduleType,
|
||||
createBlock,
|
||||
updateBlock,
|
||||
copySchedule,
|
||||
listBlocksForLicense,
|
||||
scheduleBlocksToInput,
|
||||
type OwnedLicense
|
||||
} from '$lib/server/schedule';
|
||||
import { buildSchedulePayload } from '$lib/server/v2-serializers';
|
||||
|
||||
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
|
||||
const adapter = new PrismaPg(pool);
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
const K_A1 = 'ALCE-AAAA-AAAA-0001';
|
||||
const K_A2 = 'ALCE-AAAA-AAAA-0002';
|
||||
const K_B1 = 'BOBB-BBBB-BBBB-0001';
|
||||
|
||||
const results: { id: string; pass: boolean; note: string }[] = [];
|
||||
|
||||
let PROCS: string[] = [];
|
||||
|
||||
async function licenseByKey(key: string) {
|
||||
return prisma.licenseKey.findUnique({ where: { key } });
|
||||
}
|
||||
async function ownedFor(email: string): Promise<OwnedLicense[]> {
|
||||
const user = await prisma.user.findUnique({ where: { email } });
|
||||
if (!user) throw new Error('no user');
|
||||
return prisma.licenseKey.findMany({
|
||||
where: { userId: user.id },
|
||||
select: { id: true, systemName: true, key: true }
|
||||
});
|
||||
}
|
||||
function payloadFor(schedule: Awaited<ReturnType<typeof getScheduleForLicense>>) {
|
||||
const type = schedule?.type === 'daily' || schedule?.type === 'weekly' ? schedule.type : null;
|
||||
return buildSchedulePayload(type, scheduleBlocksToInput(schedule?.blocks ?? []));
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
const [ka1, ka2, kb1] = await Promise.all([
|
||||
licenseByKey(K_A1),
|
||||
licenseByKey(K_A2),
|
||||
licenseByKey(K_B1)
|
||||
]);
|
||||
await prisma.schedule.deleteMany({
|
||||
where: { licenseKeyId: { in: [ka1!.id, ka2!.id, kb1!.id] } }
|
||||
});
|
||||
const procs = await prisma.proclamation.findMany({
|
||||
where: { name: { startsWith: 'P5 ' } },
|
||||
select: { id: true },
|
||||
orderBy: { name: 'asc' }
|
||||
});
|
||||
PROCS = procs.map((p) => p.id);
|
||||
expect(PROCS.length).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
console.log('\nP5_RESULTS ' + JSON.stringify(results));
|
||||
await prisma.$disconnect();
|
||||
await pool.end();
|
||||
});
|
||||
|
||||
const enabled = process.env.P5_INTEGRATION === '1';
|
||||
|
||||
describe.skipIf(!enabled)('Phase 5 end-to-end service scenarios', () => {
|
||||
it('e: create a daily schedule via the Portal path', async () => {
|
||||
const ka1 = await licenseByKey(K_A1);
|
||||
await setScheduleType(ka1!.id, 'daily', { confirmed: true });
|
||||
await createBlock(ka1!.id, {
|
||||
startMinutes: 7 * 60,
|
||||
endMinutes: 9 * 60,
|
||||
proclamationIds: [PROCS[0]],
|
||||
dynamicAudio: true
|
||||
});
|
||||
await createBlock(ka1!.id, {
|
||||
startMinutes: 18 * 60,
|
||||
endMinutes: 20 * 60 + 30,
|
||||
proclamationIds: [PROCS[1]],
|
||||
dynamicAudio: false
|
||||
});
|
||||
const daily = await getScheduleForLicense(ka1!.id);
|
||||
const p = payloadFor(daily);
|
||||
results.push({
|
||||
id: 'e',
|
||||
pass: p.type === 'daily' && p.daily.length === 2,
|
||||
note: `type=${p.type} blocks=${p.daily.length}`
|
||||
});
|
||||
expect(p.type).toBe('daily');
|
||||
expect(p.daily).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('f: weekly adjacent + midnight-crossing + Sunday spillover rejection', async () => {
|
||||
const kb1 = await licenseByKey(K_B1);
|
||||
await setScheduleType(kb1!.id, 'weekly', { confirmed: true });
|
||||
await createBlock(kb1!.id, {
|
||||
day: 'sun',
|
||||
startMinutes: 23 * 60,
|
||||
endMinutes: 60,
|
||||
proclamationIds: [PROCS[2]]
|
||||
});
|
||||
await createBlock(kb1!.id, {
|
||||
day: 'mon',
|
||||
startMinutes: 6 * 60,
|
||||
endMinutes: 7 * 60,
|
||||
proclamationIds: [PROCS[0]]
|
||||
});
|
||||
await createBlock(kb1!.id, {
|
||||
day: 'mon',
|
||||
startMinutes: 7 * 60,
|
||||
endMinutes: 8 * 60 + 30,
|
||||
proclamationIds: [PROCS[1]]
|
||||
});
|
||||
|
||||
const p = payloadFor(await getScheduleForLicense(kb1!.id));
|
||||
expect(p.type).toBe('weekly');
|
||||
expect(p.weekly.sun).toHaveLength(1);
|
||||
expect(p.weekly.sun[0].overnight).toBe(true);
|
||||
expect(p.weekly.mon).toHaveLength(2);
|
||||
expect(p.weekly.mon[0].endMinutes).toBe(p.weekly.mon[1].startMinutes);
|
||||
|
||||
// Sunday's 23:00->01:00 spills into Monday [00:00,01:00); a Monday 00:00-00:30 block must be rejected.
|
||||
const rejected = await createBlock(kb1!.id, {
|
||||
day: 'mon',
|
||||
startMinutes: 0,
|
||||
endMinutes: 30,
|
||||
proclamationIds: [PROCS[3]]
|
||||
});
|
||||
const monCount = (await getScheduleForLicense(kb1!.id))!.blocks.filter(
|
||||
(b) => b.day === 'mon'
|
||||
).length;
|
||||
results.push({
|
||||
id: 'f',
|
||||
pass: !rejected.ok && monCount === 2,
|
||||
note: `sun overnight=${p.weekly.sun[0].overnight} mon adjacent=${p.weekly.mon[0].endMinutes === p.weekly.mon[1].startMinutes} spilloverRejected=${!rejected.ok}`
|
||||
});
|
||||
expect(rejected.ok).toBe(false);
|
||||
expect(monCount).toBe(2);
|
||||
});
|
||||
|
||||
it('i: schedules are per-License-Key (A does not affect B)', async () => {
|
||||
const [ka1, kb1] = await Promise.all([licenseByKey(K_A1), licenseByKey(K_B1)]);
|
||||
const a = payloadFor(await getScheduleForLicense(ka1!.id));
|
||||
const b = payloadFor(await getScheduleForLicense(kb1!.id));
|
||||
results.push({
|
||||
id: 'i',
|
||||
pass: a.type === 'daily' && b.type === 'weekly',
|
||||
note: `A=${a.type} B=${b.type}`
|
||||
});
|
||||
expect(a.type).toBe('daily');
|
||||
expect(b.type).toBe('weekly');
|
||||
});
|
||||
|
||||
it('j: copy A -> second system and verify independence', async () => {
|
||||
const [ka1, ka2] = await Promise.all([licenseByKey(K_A1), licenseByKey(K_A2)]);
|
||||
const owned = await ownedFor('p5-alice@example.com');
|
||||
const copy = await copySchedule(owned, ka1!.id, [ka2!.id]);
|
||||
expect(copy.ok).toBe(true);
|
||||
|
||||
const ka2Before = payloadFor(await getScheduleForLicense(ka2!.id));
|
||||
expect(ka2Before.type).toBe('daily');
|
||||
expect(ka2Before.daily).toHaveLength(2);
|
||||
|
||||
const src = payloadFor(await getScheduleForLicense(ka1!.id));
|
||||
expect(src.version).not.toBe(ka2Before.version);
|
||||
|
||||
// Edit source; copy must not change.
|
||||
const blocks = await listBlocksForLicense(ka1!.id);
|
||||
await updateBlock(ka1!.id, blocks[0].id, {
|
||||
startMinutes: 6 * 60,
|
||||
endMinutes: 7 * 60,
|
||||
proclamationIds: blocks[0].proclamations.map((p) => p.proclamation.id),
|
||||
dynamicAudio: true
|
||||
});
|
||||
const ka2After = payloadFor(await getScheduleForLicense(ka2!.id));
|
||||
results.push({
|
||||
id: 'j',
|
||||
pass: ka2After.version === ka2Before.version && src.version !== ka2Before.version,
|
||||
note: `copied daily blocks=${ka2Before.daily.length} independent=${ka2After.version === ka2Before.version}`
|
||||
});
|
||||
expect(ka2After.version).toBe(ka2Before.version);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user