Team managers (captain and above) can now deactivate/reactivate a player and change their team-role from the player detail page. Deactivation zeroes the open balance via an auditable adjustment transaction instead of overwriting the balance field, and both actions are blocked if they would leave a team without an active treasurer. Also hardens the existing PUT teams/:id/players endpoint down to profile-only fields, fixing a typo bug and closing a gap where any authenticated user could mutate a player's active/role/balance in any team. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
86 lines
3.4 KiB
TypeScript
86 lines
3.4 KiB
TypeScript
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);
|
|
});
|
|
});
|