import 'reflect-metadata'; import { DataSource, DataSourceOptions } from 'typeorm'; import { Player } from '../src/players/entities/player.entity'; import { Team } from '../src/teams/entities/team.entity'; import { Transaction } from '../src/transactions/entitites/transaction.entity'; import { TransactionTypeEnum } from '../src/transactions/transaction-type.enum'; import { DEACTIVATION_ADJUSTMENT_NOTE_PREFIX } from '../src/teams/team-members.service'; // Eigene DataSource statt der geteilten `AppDataSource`: deren `migrations`-Glob // (`src/database/migrations/**/*`) matcht auch `.spec.ts`-Dateien in diesem Ordner // (z.B. `AddPlayerLookupIndexes.spec.ts`) und TypeORM requiret sie beim // Metadaten-Aufbau -- das registriert `describe`/`it` NACH Testlauf-Start und lässt // Jest mit "Cannot add a test after tests have started running" abbrechen. Diese // Ausgleichsbuchung braucht keine Migrationen, nur Entities -- migrations: [] umgeht das. describe('TeamMembers rollback atomicity (real MySQL)', () => { let dataSource: DataSource; let team: Team; let player: Player; const marker = `rollback-test-${Date.now()}`; beforeAll(async () => { dataSource = new DataSource({ type: process.env.DATABASE_TYPE, url: process.env.DATABASE_URL, host: process.env.DATABASE_HOST, port: parseInt(process.env.DATABASE_PORT, 10) || 5432, username: process.env.DATABASE_USERNAME, password: process.env.DATABASE_PASSWORD, database: process.env.DATABASE_NAME, synchronize: false, entities: [__dirname + '/../src/**/*.entity{.ts,.js}'], } as DataSourceOptions); await dataSource.initialize(); team = await dataSource.getRepository(Team).save({ name: marker, alias: marker, } as Team); player = await dataSource.getRepository(Player).save({ firstName: 'Rollback', lastName: 'Test', team, teamRole: { id: 4 }, // treasurer balance: 42, active: true, } as unknown as Player); }); afterAll(async () => { await dataSource .getRepository(Transaction) .delete({ player: { id: player.id } }); await dataSource.getRepository(Player).delete({ id: player.id }); await dataSource.getRepository(Team).delete({ id: team.id }); await dataSource.destroy(); }); it('rolls back BOTH the inserted adjustment transaction AND the explicit balance write when a later error is thrown in the same manager transaction', async () => { await expect( dataSource.transaction(async (manager) => { await manager.getRepository(Transaction).insert({ amount: -42, date: new Date().toISOString(), note: `${DEACTIVATION_ADJUSTMENT_NOTE_PREFIX} #${player.id}`, player: { id: player.id } as Player, type: { id: TransactionTypeEnum.credit }, }); await manager .getRepository(Player) .save({ ...player, balance: 0, active: false }); throw new Error('forced failure after adjustment transaction would have been created'); }), ).rejects.toThrow('forced failure'); const persistedTransaction = await dataSource.getRepository(Transaction).findOne({ where: { player: { id: player.id } }, }); expect(persistedTransaction).toBeNull(); const reloadedPlayer = await dataSource.getRepository(Player).findOne({ where: { id: player.id }, }); expect(reloadedPlayer?.active).toBe(true); expect(Number(reloadedPlayer?.balance)).toBe(42); }); });