Files
ApiCore/ApiCore-Backend/test/t6-migrations.e2e-spec.ts
T
Nick SzaunigandClaude Opus 5.5 8a31c42578
Backend Image / build-and-push (push) Successful in 6m39s
Frontend Image / build-and-push (push) Successful in 11m25s
feat: T6 wave 1 – compliance schema, permissions, lock order, backup
- Migrations (additive): veterinary medicines with lots and movement
  ledger, treatment medicine/lot/dose links, label particulars, authority
  notices, apiary restrictions, compliance settings and acknowledgements;
  notification category COMPLIANCE, added to non-empty Telegram lists.
- Permissions veterinary-medicines:* and compliance:* with role grants.
- LOCKABLE_TABLES: Apiary locks before Colony; T6 tables appended.
- Backup exports and restores the new tables in FK order; unsealed files
  skip them and report dropped medicine links; an open TSK draft in the
  target wins.
- describeNullContracts shared helper; Telegram backlog e2e no longer
  depends on the time of day.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-15 13:04:13 +02:00

94 lines
2.9 KiB
TypeScript

import {
ScratchDatabase,
createScratchDatabase,
} from './utils/migration-harness';
const BEFORE_COMPLIANCE_CATEGORY =
'20261016100300_t6_notification_category_compliance';
const COMPLIANCE_DEFAULT = '20261016100400_t6_notification_compliance_default';
const TIMEOUT_MS = 240_000;
async function seedUsers(db: ScratchDatabase): Promise<void> {
await db.query(
`INSERT INTO "Role" ("id", "key", "name", "updatedAt")
VALUES ('role-1', 'MIG_TEST', 'Migration test', now())`,
);
for (const id of ['user-a', 'user-b', 'user-c', 'user-d']) {
await db.query(
`INSERT INTO "User" ("id", "email", "passwordHash", "name", "roleId", "updatedAt")
VALUES ($1, $2, 'x', $1, 'role-1', now())`,
[id, `${id}@example.com`],
);
}
}
describe('T6 migrations (e2e)', () => {
let db: ScratchDatabase | undefined;
afterEach(async () => {
await db?.drop();
db = undefined;
});
it(
'adds COMPLIANCE to the Telegram categories of every existing preference and keeps the rest, an empty list (opt-out) stays empty',
async () => {
db = await createScratchDatabase(BEFORE_COMPLIANCE_CATEGORY);
await seedUsers(db);
await db.query(
`INSERT INTO "NotificationPreference" ("userId", "telegramCategories", "updatedAt") VALUES
('user-a', DEFAULT, now()),
('user-b', '{SYSTEM}', now()),
('user-c', '{}', now()),
('user-d', '{DIGEST,TASK_DUE,SEASON}', now())`,
);
await db.migrateThrough(COMPLIANCE_DEFAULT);
expect(
await db.query(
`SELECT "userId", "telegramCategories"::text AS "categories"
FROM "NotificationPreference" ORDER BY "userId"`,
),
).toEqual([
{ userId: 'user-a', categories: '{TASK_DUE,DIGEST,COMPLIANCE}' },
{ userId: 'user-b', categories: '{SYSTEM,COMPLIANCE}' },
{ userId: 'user-c', categories: '{}' },
{ userId: 'user-d', categories: '{DIGEST,TASK_DUE,SEASON,COMPLIANCE}' },
]);
},
TIMEOUT_MS,
);
it(
'gives a preference created afterwards the new default and lists COMPLIANCE once',
async () => {
db = await createScratchDatabase(BEFORE_COMPLIANCE_CATEGORY);
await seedUsers(db);
await db.migrateThrough(COMPLIANCE_DEFAULT);
await db.query(
`INSERT INTO "NotificationPreference" ("userId", "updatedAt") VALUES ('user-a', now())`,
);
expect(
await db.query(
`SELECT "telegramCategories"::text AS "categories" FROM "NotificationPreference"`,
),
).toEqual([{ categories: '{TASK_DUE,DIGEST,COMPLIANCE}' }]);
expect(
await db.query(
`SELECT enum_range(NULL::"NotificationCategory")::text AS "values"`,
),
).toEqual([
{
values:
'{TASK_DUE,DIGEST,SUGGESTION,WITHDRAWAL,SEASON,SYSTEM,COMPLIANCE}',
},
]);
},
TIMEOUT_MS,
);
});