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,163 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
|
||||||
|
// Exercises the Kiosk-facing HTTP API exactly as the Kiosk client does.
|
||||||
|
// Scenarios: b (validate + name), c (duplicate name conflict), k (ETag/304),
|
||||||
|
// old-client backward compatibility.
|
||||||
|
const BASE = process.env.P5_BASE ?? 'http://localhost:5173';
|
||||||
|
|
||||||
|
const KA1 = 'ALCE-AAAA-AAAA-0001';
|
||||||
|
const KA2 = 'ALCE-AAAA-AAAA-0002';
|
||||||
|
const KB1 = 'BOBB-BBBB-BBBB-0001';
|
||||||
|
const EMAIL = 'p5-alice@example.com';
|
||||||
|
|
||||||
|
const results: { id: string; pass: boolean; note: string }[] = [];
|
||||||
|
function rec(id: string, pass: boolean, note: string) {
|
||||||
|
results.push({ id, pass, note });
|
||||||
|
console.log(`${pass ? 'PASS' : 'FAIL'} ${id} ${note}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function post(path: string, body: unknown) {
|
||||||
|
const res = await fetch(BASE + path, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
return { status: res.status, body: await res.json().catch(() => null), headers: res.headers };
|
||||||
|
}
|
||||||
|
async function put(path: string, body: unknown) {
|
||||||
|
const res = await fetch(BASE + path, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
return { status: res.status, body: await res.json().catch(() => null) };
|
||||||
|
}
|
||||||
|
async function get(path: string, headers: Record<string, string> = {}) {
|
||||||
|
const res = await fetch(BASE + path, { headers });
|
||||||
|
return { status: res.status, body: await res.json().catch(() => null), headers: res.headers };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// (b) validate without a name first (old-client behavior), then set a name.
|
||||||
|
const validateNoName = await post('/api/v2/license', {
|
||||||
|
license_key: KA1,
|
||||||
|
email_address: EMAIL
|
||||||
|
});
|
||||||
|
rec(
|
||||||
|
'b1',
|
||||||
|
validateNoName.status === 200 && validateNoName.body?.systemNameStatus === 'unchanged',
|
||||||
|
`validation without systemName -> ${validateNoName.status}, status=${validateNoName.body?.systemNameStatus}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const setName = await post('/api/v2/license', {
|
||||||
|
license_key: KA1,
|
||||||
|
email_address: EMAIL,
|
||||||
|
systemName: 'Living Room'
|
||||||
|
});
|
||||||
|
rec(
|
||||||
|
'b2',
|
||||||
|
setName.status === 200 && setName.body?.systemNameStatus === 'ok',
|
||||||
|
`name a Kiosk -> ${setName.status}, status=${setName.body?.systemNameStatus}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// (c) duplicate name, different casing, same account -> conflict + not cleared.
|
||||||
|
const dup = await post('/api/v2/license', {
|
||||||
|
license_key: KA2,
|
||||||
|
email_address: EMAIL,
|
||||||
|
systemName: 'living room'
|
||||||
|
});
|
||||||
|
rec(
|
||||||
|
'c1',
|
||||||
|
dup.status === 200 && dup.body?.systemNameStatus === 'conflict',
|
||||||
|
`case-insensitive duplicate -> status=${dup.body?.systemNameStatus} (validation still ${dup.status})`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Confirm the original name survived and KA2 has no name.
|
||||||
|
const after = await get('/api/v2/licenses/' + KA1 + '/schedule');
|
||||||
|
rec('c2', after.status === 200, `KA1 schedule GET after conflict -> ${after.status}`);
|
||||||
|
|
||||||
|
// (c) PUT system-name endpoint: 409 for a taken name.
|
||||||
|
const putDup = await put('/api/v2/licenses/' + KA2 + '/system-name', {
|
||||||
|
systemName: 'LIVING ROOM'
|
||||||
|
});
|
||||||
|
rec(
|
||||||
|
'c3',
|
||||||
|
putDup.status === 409 && putDup.body?.message === 'name_taken',
|
||||||
|
`PUT duplicate -> ${putDup.status} ${putDup.body?.message}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const putOk = await put('/api/v2/licenses/' + KA2 + '/system-name', { systemName: 'Kitchen' });
|
||||||
|
rec('c4', putOk.status === 200, `PUT unique name -> ${putOk.status}`);
|
||||||
|
|
||||||
|
const putInvalid = await put('/api/v2/licenses/' + KA2 + '/system-name', { systemName: ' ' });
|
||||||
|
rec(
|
||||||
|
'c5',
|
||||||
|
putInvalid.status === 400 && putInvalid.body?.message === 'invalid_name',
|
||||||
|
`PUT blank -> ${putInvalid.status} ${putInvalid.body?.message}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const putUnknown = await put('/api/v2/licenses/ZZZZ-ZZZZ-ZZZZ-ZZZZ/system-name', {
|
||||||
|
systemName: 'X'
|
||||||
|
});
|
||||||
|
rec(
|
||||||
|
'c6',
|
||||||
|
putUnknown.status === 404 && putUnknown.body?.message === 'unknown_license',
|
||||||
|
`PUT unknown license -> ${putUnknown.status} ${putUnknown.body?.message}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// (k) schedule GET parity between the legacy endpoint and the new key-only endpoint.
|
||||||
|
const modern = await get('/api/v2/licenses/' + KA1 + '/schedule');
|
||||||
|
const legacy = await get(
|
||||||
|
'/api/v2/schedule?email=' + encodeURIComponent(EMAIL) + '&licenseKey=' + KA1
|
||||||
|
);
|
||||||
|
rec(
|
||||||
|
'k1',
|
||||||
|
modern.status === 200 && legacy.status === 200 && modern.body?.version === legacy.body?.version,
|
||||||
|
`modern vs legacy version equal=${modern.body?.version === legacy.body?.version}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// (k) conditional GET: matching If-None-Match -> 304.
|
||||||
|
const etag = modern.headers.get('etag') ?? '';
|
||||||
|
const notModified = await get('/api/v2/licenses/' + KA1 + '/schedule', { 'if-none-match': etag });
|
||||||
|
rec('k2', notModified.status === 304, `conditional GET with ETag -> ${notModified.status}`);
|
||||||
|
|
||||||
|
const stale = await get('/api/v2/licenses/' + KA1 + '/schedule', { 'if-none-match': '"stale"' });
|
||||||
|
rec('k3', stale.status === 200, `conditional GET with stale ETag -> ${stale.status}`);
|
||||||
|
|
||||||
|
// Unknown license -> 404.
|
||||||
|
const missing = await get('/api/v2/licenses/ZZZZ-ZZZZ-ZZZZ-ZZZZ/schedule');
|
||||||
|
rec('k4', missing.status === 404, `unknown license schedule GET -> ${missing.status}`);
|
||||||
|
|
||||||
|
// Old-client validation (no systemName field at all) never clears the name.
|
||||||
|
const validateAgain = await post('/api/v2/license', { license_key: KA1, email_address: EMAIL });
|
||||||
|
const checkName = await put('/api/v2/licenses/' + KA1 + '/system-name', {
|
||||||
|
systemName: 'Living Room'
|
||||||
|
});
|
||||||
|
rec(
|
||||||
|
'old1',
|
||||||
|
validateAgain.status === 200 &&
|
||||||
|
validateAgain.body?.systemNameStatus === 'unchanged' &&
|
||||||
|
checkName.status === 200,
|
||||||
|
`old-client validate stays unchanged; name still present`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Old-client list endpoint shape: blocks array still present.
|
||||||
|
rec(
|
||||||
|
'old2',
|
||||||
|
legacy.status === 200 && Array.isArray(legacy.body?.blocks),
|
||||||
|
`legacy endpoint still returns email+blocks (${legacy.body?.blocks?.length} blocks)`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cross-account: Bob's key with Alice's email must fail.
|
||||||
|
const cross = await get(
|
||||||
|
'/api/v2/schedule?email=' + encodeURIComponent(EMAIL) + '&licenseKey=' + KB1
|
||||||
|
);
|
||||||
|
rec('auth1', cross.status === 400, `cross-account legacy schedule -> ${cross.status}`);
|
||||||
|
|
||||||
|
console.log('\nP5_HTTP_RESULTS ' + JSON.stringify(results));
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error('RUNNER ERROR', e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import pg from 'pg';
|
||||||
|
import { PrismaPg } from '@prisma/adapter-pg';
|
||||||
|
import { PrismaClient } from '../src/lib/generated/prisma/client';
|
||||||
|
|
||||||
|
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
|
||||||
|
const prisma = new PrismaClient({ adapter: new PrismaPg(pool) });
|
||||||
|
|
||||||
|
// A handful of real Proclamations so schedule Blocks have valid references.
|
||||||
|
async function main() {
|
||||||
|
const category = await prisma.proclamationCategory.findFirst({ orderBy: { id: 'asc' } });
|
||||||
|
const group = await prisma.proclamationGroup.findFirst({ orderBy: { id: 'asc' } });
|
||||||
|
const freq = await prisma.solfeggioFrequency.findFirst({ orderBy: { id: 'asc' } });
|
||||||
|
if (!category || !group || !freq) throw new Error('seed missing catalog');
|
||||||
|
|
||||||
|
const names = ['P5 Dawn', 'P5 Dusk', 'P5 Noon', 'P5 Night'];
|
||||||
|
const ids: string[] = [];
|
||||||
|
for (const name of names) {
|
||||||
|
const existing = await prisma.proclamation.findUnique({ where: { name } });
|
||||||
|
if (existing) {
|
||||||
|
ids.push(existing.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const created = await prisma.proclamation.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
content: `<p>${name} content</p>`,
|
||||||
|
categoryId: category.id,
|
||||||
|
groupId: group.id,
|
||||||
|
solfeggioFrequencyId: freq.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ids.push(created.id);
|
||||||
|
}
|
||||||
|
console.log(JSON.stringify(ids));
|
||||||
|
await prisma.$disconnect();
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(async (e) => {
|
||||||
|
console.error(e);
|
||||||
|
await prisma.$disconnect();
|
||||||
|
await pool.end();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import pg from 'pg';
|
||||||
|
import { PrismaPg } from '@prisma/adapter-pg';
|
||||||
|
import { PrismaClient } from '../src/lib/generated/prisma/client';
|
||||||
|
|
||||||
|
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
|
||||||
|
const adapter = new PrismaPg(pool);
|
||||||
|
const prisma = new PrismaClient({ adapter });
|
||||||
|
|
||||||
|
// Two accounts, each with two License Keys, for scenario coverage:
|
||||||
|
// - acct A: keys KA1, KA2 (same account -> name uniqueness + copy)
|
||||||
|
// - acct B: key KB1 (cross-account isolation)
|
||||||
|
// Names are left NULL; the Kiosk sets them via the API.
|
||||||
|
const accounts = [
|
||||||
|
{ email: 'p5-alice@example.com', key: 'ALCE-AAAA-AAAA-0001' },
|
||||||
|
{ email: 'p5-alice@example.com', key: 'ALCE-AAAA-AAAA-0002' },
|
||||||
|
{ email: 'p5-bob@example.com', key: 'BOBB-BBBB-BBBB-0001' }
|
||||||
|
];
|
||||||
|
|
||||||
|
async function ensureUser(email) {
|
||||||
|
let user = await prisma.user.findUnique({ where: { email } });
|
||||||
|
if (!user) {
|
||||||
|
user = await prisma.user.create({
|
||||||
|
data: { id: crypto.randomUUID(), name: email.split('@')[0], email, emailVerified: true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
for (const { email, key } of accounts) {
|
||||||
|
const user = await ensureUser(email);
|
||||||
|
const existing = await prisma.licenseKey.findUnique({ where: { key } });
|
||||||
|
if (!existing) {
|
||||||
|
await prisma.licenseKey.create({ data: { key, active: true, userId: user.id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('P5 test data ready:');
|
||||||
|
for (const { email, key } of accounts) console.log(` ${email} -> ${key}`);
|
||||||
|
await prisma.$disconnect();
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(async (e) => {
|
||||||
|
console.error(e);
|
||||||
|
await prisma.$disconnect();
|
||||||
|
await pool.end();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import pg from 'pg';
|
||||||
|
import { PrismaPg } from '@prisma/adapter-pg';
|
||||||
|
import { PrismaClient } from '../src/lib/generated/prisma/client';
|
||||||
|
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
|
||||||
|
const prisma = new PrismaClient({ adapter: new PrismaPg(pool) });
|
||||||
|
const schedules = await prisma.schedule.findMany({ include: { blocks: true } });
|
||||||
|
const keys = await prisma.licenseKey.findMany({ select: { key: true, systemName: true } });
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
scheduleCount: schedules.length,
|
||||||
|
blockCount: schedules.reduce((n, s) => n + s.blocks.length, 0),
|
||||||
|
types: schedules.map((s) => s.type),
|
||||||
|
names: keys
|
||||||
|
.filter((k) => k.systemName)
|
||||||
|
.map((k) => k.systemName)
|
||||||
|
.sort()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await prisma.$disconnect();
|
||||||
|
await pool.end();
|
||||||
@@ -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