From c295bae63a13d379f06fab9dad27df1513a8bde9 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Sat, 22 Aug 2026 17:59:37 +0200 Subject: [PATCH] test(shops): add entity-schema cross-check for sellable loot bags The migration spec only asserted SQL substrings against a mocked QueryRunner and had no getMetadataArgsStorage() check that ShopOffer's column options and partial unique indexes actually match the new schema. Since ts-jest does not type-check in this package (isolatedModules: true), this is the only automated guard against entity/migration drift -- matches the house convention in npc-system.migration.spec.ts and loot-bags.migration.spec.ts. --- .../sellable-loot-bags.migration.spec.ts | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/apps/api/src/database/migrations/sellable-loot-bags.migration.spec.ts b/apps/api/src/database/migrations/sellable-loot-bags.migration.spec.ts index 4943255..4059387 100644 --- a/apps/api/src/database/migrations/sellable-loot-bags.migration.spec.ts +++ b/apps/api/src/database/migrations/sellable-loot-bags.migration.spec.ts @@ -1,6 +1,7 @@ import 'reflect-metadata'; -import { QueryRunner } from 'typeorm'; +import { getMetadataArgsStorage, QueryRunner } from 'typeorm'; import { SellableLootBags1796000000000 } from './1796000000000-SellableLootBags'; +import { ShopOffer } from '../../shops/entities/shop-offer.entity'; async function runUp(): Promise { const query = jest.fn().mockResolvedValue(undefined); @@ -73,3 +74,47 @@ describe('SellableLootBags1796000000000', () => { ); }); }); + +describe('slice 0.8.5 entity schema', () => { + function column(target: unknown, propertyName: string) { + return getMetadataArgsStorage().columns.find( + (candidate) => + candidate.target === target && candidate.propertyName === propertyName, + ); + } + + it('lets the item target sit empty when the offer sells a bag instead', () => { + expect(column(ShopOffer, 'itemDefinitionId')?.options.nullable).toBe(true); + }); + + it('gives a bag offer its own nullable uuid target', () => { + const lootBagDefinitionId = column(ShopOffer, 'lootBagDefinitionId'); + expect(lootBagDefinitionId?.options.type).toBe('uuid'); + expect(lootBagDefinitionId?.options.nullable).toBe(true); + }); + + it('requires every offer to carry its bypass conditions, empty or not', () => { + const bypassConditions = column(ShopOffer, 'bypassConditions'); + expect(bypassConditions?.options.type).toBe('jsonb'); + expect(bypassConditions?.options.nullable).toBeFalsy(); + }); + + it('declares both partial unique indexes with their WHERE clauses', () => { + const indices = getMetadataArgsStorage().indices.filter( + (candidate) => candidate.target === ShopOffer, + ); + + const itemIndex = indices.find( + (candidate) => candidate.name === 'IDX_shop_offers_shop_item', + ); + const bagIndex = indices.find( + (candidate) => candidate.name === 'IDX_shop_offers_shop_bag', + ); + + expect(itemIndex?.unique).toBe(true); + expect(itemIndex?.where).toBe('"item_definition_id" IS NOT NULL'); + + expect(bagIndex?.unique).toBe(true); + expect(bagIndex?.where).toBe('"loot_bag_definition_id" IS NOT NULL'); + }); +});