import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { startTestDatabase, type TestDatabase } from './harness.js'; type Characters = typeof import('../src/services/characters.js'); type Credits = typeof import('../src/services/credits.js'); type Db = typeof import('../src/db.js'); let database: TestDatabase; let characters: Characters; let credits: Credits; let db: Db; let ctx: { actorId: number; actorName: string }; before(async () => { database = await startTestDatabase(54341); process.env.DATABASE_URL = database.url; process.env.SESSION_SECRET = 'test-secret-that-is-long-enough-000000'; process.env.NODE_ENV = 'test'; db = await import('../src/db.js'); characters = await import('../src/services/characters.js'); credits = await import('../src/services/credits.js'); const user = await db.prisma.user.create({ data: { username: 'nacherfasser', passwordHash: 'x', role: 'ADMIN' }, }); ctx = { actorId: user.id, actorName: user.username }; }); after(async () => { await db.prisma.$disconnect(); await database.stop(); }); async function spieler(name: string, pakete = 2) { const p = await db.prisma.player.create({ data: { displayName: name } }); for (let i = 0; i < pakete; i += 1) await credits.bookCcPackage(p.id, ctx); return p.id; } /** Die aktiven Mitglieder eines CCs, nach Namen. */ async function mitglieder(characterId: number): Promise { const liste = await db.prisma.characterMember.findMany({ where: { characterId, leftAt: null }, include: { player: { select: { displayName: true } } }, orderBy: { id: 'asc' }, }); return liste.map((m) => m.player.displayName).sort(); } /** * Der Fall aus dem Betrieb: Spieler B ist längst eingetragen, und erst beim * Durcharbeiten von Spieler A stellt sich heraus, dass die beiden einen * Gruppen-CC teilten - über mehrere Redesigns hinweg, mit wechselnder * Besetzung. */ describe('Nachträglich aufräumen, was längst passiert ist', () => { let b = 0; let a = 0; let c = 0; let d = 0; let alpha = 0; let beta = 0; let gamma = 0; before(async () => { b = await spieler('Spieler B'); a = await spieler('Spieler A'); c = await spieler('Spieler C'); d = await spieler('Spieler D'); // B wurde zuerst durchgearbeitet: ein CC, zweimal redesignt. const ersterCC = await characters.createCharacter(b, { name: 'Alpha' }, ctx); alpha = ersterCC.character.id; await credits.bookRedesignPackage(b, ctx); const zweiter = await characters.redesignCharacter( alpha, { name: 'Beta', useKind: 'REDESIGN_PAID' }, ctx, ); beta = zweiter.character.id; await credits.bookRedesignPackage(b, ctx); const dritter = await characters.redesignCharacter( beta, { name: 'Gamma', useKind: 'REDESIGN_PAID' }, ctx, ); gamma = dritter.character.id; }); it('trägt A rückwirkend in ein längst redesigntes Glied ein', async () => { // Alpha ist inaktiv - im laufenden Betrieb tritt dort niemand mehr bei. // Beim Nachtragen geht es aber nicht um jetzt, sondern um damals. const alt = await db.prisma.character.findUniqueOrThrow({ where: { id: alpha } }); assert.equal(alt.status, 'INACTIVE'); await characters.addMember( alpha, a, { useKind: 'CC', joinedAt: new Date('2024-03-01') }, ctx, ); assert.deepEqual(await mitglieder(alpha), ['Spieler A', 'Spieler B']); }); it('zieht ihn durch die Kette und bucht jedes Glied einzeln', async () => { // Für ihn ist jedes Glied ein eigener Eintrag in seiner Liste: der // unterste kostet den Platz, jedes weitere ein Redesign. const vorher = await credits.getBalances(a); const ergebnis = await characters.addMemberToChain(beta, a, {}, ctx); assert.equal(ergebnis.added, 2, 'Beta und Gamma kommen dazu'); const danach = await credits.getBalances(a); assert.equal(danach.CC, vorher.CC, 'sein Platz wurde einmal bezahlt'); assert.equal( danach.REDESIGN_PAID, vorher.REDESIGN_PAID - 2, 'für Beta und Gamma je ein Redesign', ); assert.deepEqual(await mitglieder(gamma), ['Spieler A', 'Spieler B']); }); it('belegt dabei genau einen Platz', async () => { const belegt = (await characters.listCharacters(a)).filter( (cc) => cc.status === 'ACTIVE' && cc.members.some((m) => m.playerId === a && !m.leftAt), ); assert.deepEqual( belegt.map((cc) => cc.name), ['Gamma'], 'die früheren Glieder sind inaktiv und belegen nichts', ); }); it('lässt C mittendrin ein- und wieder aussteigen', async () => { // C war nur bei Beta dabei - davor und danach nicht. await characters.addMember(beta, c, { useKind: 'CC', joinedAt: new Date('2024-06-01') }, ctx); assert.deepEqual(await mitglieder(beta), ['Spieler A', 'Spieler B', 'Spieler C']); await characters.removeMember(beta, c, 'Ausgestiegen vor dem nächsten Redesign', ctx); assert.deepEqual(await mitglieder(beta), ['Spieler A', 'Spieler B']); // Bei Gamma steht er gar nicht erst. assert.equal((await mitglieder(gamma)).includes('Spieler C'), false); }); it('nimmt D erst beim letzten Glied auf', async () => { await characters.addMember(gamma, d, { useKind: 'CC' }, ctx); assert.deepEqual(await mitglieder(gamma), ['Spieler A', 'Spieler B', 'Spieler D']); assert.equal((await mitglieder(alpha)).includes('Spieler D'), false, 'damals war er nicht da'); }); it('zeigt jedem Beteiligten seine eigene Geschichte', async () => { const seins = async (playerId: number) => (await characters.listCharacters(playerId)).map((cc) => cc.name).sort(); assert.deepEqual(await seins(a), ['Alpha', 'Beta', 'Gamma'], 'A war von Anfang an dabei'); assert.deepEqual(await seins(b), ['Alpha', 'Beta', 'Gamma']); assert.deepEqual(await seins(c), ['Beta'], 'C nur in der Mitte'); assert.deepEqual(await seins(d), ['Gamma'], 'D nur am Ende'); }); it('verlangt an einem laufenden CC weiterhin eine Guthaben-Angabe', async () => { const fremder = await spieler('Fremder', 1); await assert.rejects( () => characters.addMember(gamma, fremder, {}, ctx), (error: Error & { code?: string }) => error.code === 'NEED_KIND', 'wer neu dazukommt, zahlt - die Ausnahme gilt nur der eigenen Kette', ); }); it('weist einen verdrehten Zeitraum ab', async () => { const spaeter = await spieler('Verdreht', 1); await assert.rejects( () => characters.addMember( alpha, spaeter, { joinedAt: new Date('2025-01-01'), leftAt: new Date('2024-01-01') }, ctx, ), (error: Error & { code?: string }) => error.code === 'BAD_RANGE', ); }); });