Adds a second, mutually exclusive target column (loot_bag_definition_id) and a bypass_conditions column to shop_offers, so a later slice's quest referral can open one offer that reputation alone would not. Keeps shop.service.ts compiling against the now-nullable itemDefinition with temporary non-null assertions; Task 4 replaces them with a real branch on offer kind.
76 lines
3.0 KiB
TypeScript
76 lines
3.0 KiB
TypeScript
import 'reflect-metadata';
|
|
import { QueryRunner } from 'typeorm';
|
|
import { SellableLootBags1796000000000 } from './1796000000000-SellableLootBags';
|
|
|
|
async function runUp(): Promise<string[]> {
|
|
const query = jest.fn().mockResolvedValue(undefined);
|
|
const queryRunner = { query } as unknown as QueryRunner;
|
|
await new SellableLootBags1796000000000().up(queryRunner);
|
|
return query.mock.calls.map(([sql]) => sql as string);
|
|
}
|
|
|
|
async function runDown(): Promise<string[]> {
|
|
const query = jest.fn().mockResolvedValue(undefined);
|
|
const queryRunner = { query } as unknown as QueryRunner;
|
|
const migration = new SellableLootBags1796000000000();
|
|
await migration.up(queryRunner);
|
|
const upCount = query.mock.calls.length;
|
|
await migration.down(queryRunner);
|
|
return query.mock.calls.slice(upCount).map(([sql]) => sql as string);
|
|
}
|
|
|
|
describe('SellableLootBags1796000000000', () => {
|
|
it('adds the bag target and bypass columns', async () => {
|
|
const joined = (await runUp()).join('\n');
|
|
|
|
expect(joined).toContain('"loot_bag_definition_id" uuid');
|
|
expect(joined).toContain('"bypass_conditions" jsonb');
|
|
expect(joined).toContain('FK_shop_offers_loot_bag');
|
|
});
|
|
|
|
it('makes the item target nullable so a bag offer can exist', async () => {
|
|
const joined = (await runUp()).join('\n');
|
|
|
|
expect(joined).toContain('ALTER COLUMN "item_definition_id" DROP NOT NULL');
|
|
});
|
|
|
|
it('requires exactly one target per offer', async () => {
|
|
const joined = (await runUp()).join('\n');
|
|
|
|
// An offer that sells nothing, or sells both an item and a bag, is a
|
|
// content bug the database must not store.
|
|
expect(joined).toContain('CHK_shop_offers_single_target');
|
|
expect(joined).toContain('num_nonnulls');
|
|
});
|
|
|
|
it('keeps both target kinds unique per shop via partial indexes', async () => {
|
|
const joined = (await runUp()).join('\n');
|
|
|
|
// A plain unique index over a nullable column would let a shop hold
|
|
// unlimited bag offers, because Postgres treats NULLs as distinct.
|
|
expect(joined).toContain(
|
|
'CREATE UNIQUE INDEX "IDX_shop_offers_shop_item" ON "shop_offers" ("shop_id", "item_definition_id") WHERE "item_definition_id" IS NOT NULL',
|
|
);
|
|
expect(joined).toContain(
|
|
'CREATE UNIQUE INDEX "IDX_shop_offers_shop_bag" ON "shop_offers" ("shop_id", "loot_bag_definition_id") WHERE "loot_bag_definition_id" IS NOT NULL',
|
|
);
|
|
});
|
|
|
|
it('clears content offers so the seed can own stable ids', async () => {
|
|
const joined = (await runUp()).join('\n');
|
|
|
|
expect(joined).toContain('DELETE FROM "shop_offers"');
|
|
});
|
|
|
|
it('drops what it added and restores the original index on rollback', async () => {
|
|
const joined = (await runDown()).join('\n');
|
|
|
|
expect(joined).toContain('DROP COLUMN "loot_bag_definition_id"');
|
|
expect(joined).toContain('DROP COLUMN "bypass_conditions"');
|
|
expect(joined).toContain('ALTER COLUMN "item_definition_id" SET NOT NULL');
|
|
expect(joined).toContain(
|
|
'CREATE UNIQUE INDEX "IDX_shop_offers_shop_item" ON "shop_offers" ("shop_id", "item_definition_id")',
|
|
);
|
|
});
|
|
});
|