import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { startTestDatabase, type TestDatabase } from './harness.js'; type Backup = typeof import('../src/services/backup.js'); type Players = typeof import('../src/services/players.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 backup: Backup; let players: Players; let characters: Characters; let credits: Credits; let db: Db; let ctx: { actorId: number; actorName: string }; before(async () => { database = await startTestDatabase(54339); 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'); backup = await import('../src/services/backup.js'); players = await import('../src/services/players.js'); characters = await import('../src/services/characters.js'); credits = await import('../src/services/credits.js'); const user = await db.prisma.user.create({ data: { username: 'umzug', passwordHash: 'x', role: 'ADMIN' }, }); ctx = { actorId: user.id, actorName: user.username }; }); after(async () => { await db.prisma.$disconnect(); await database.stop(); }); /** Räumt den Bestand ab, als wäre es eine frische Instanz. */ async function leereInstanz() { await db.prisma.characterLink.deleteMany(); await db.prisma.characterMember.deleteMany(); await db.prisma.creditLedger.deleteMany(); await db.prisma.character.deleteMany(); await db.prisma.playerAlias.deleteMany(); await db.prisma.player.deleteMany(); } /** Ein Bestand mit allem, was der Export kennen muss. */ async function bestandAnlegen() { const chef = await players.createPlayer( { displayName: 'Umzugs-Chef', steamId: '76561198000030001', forumName: 'UmzugsChef', notes: 'Führt die Gruppe', }, ctx, ); const gast = await players.createPlayer( { displayName: 'Umzugs-Gast', steamId: '76561198000030002' }, ctx, ); const problemfall = await players.createPlayer( { displayName: 'Umzugs-Problem', isProblem: true, problemNote: 'Kein Paket auffindbar' }, ctx, ); await players.addAlias(chef.id, 'STEAM_ID', '76561198000030099', 'Erstes Konto', ctx); await credits.bookCcPackage(chef.id, ctx); await credits.bookCcPackage(gast.id, ctx); await credits.bookRedesignPackage(chef.id, ctx); const erster = await characters.createCharacter( chef.id, { name: 'Erster Wurf', loreText: 'Ein Anfang' }, ctx, ); const zweiter = await characters.redesignCharacter( erster.character.id, { name: 'Zweiter Wurf', useKind: 'REDESIGN_PAID' }, ctx, ); await characters.addMember(zweiter.character.id, gast.id, { useKind: 'CC' }, ctx); await characters.addLink( zweiter.character.id, { label: 'Ersteller-Addon', url: 'https://steamcommunity.com/sharedfiles/?id=1' }, ctx, ); return { chef, gast, problemfall, erster, zweiter }; } describe('Umzug auf eine andere Instanz', () => { let csv = ''; it('schreibt jede Satzart in die Tabelle', async () => { await bestandAnlegen(); csv = await backup.exportAll(); for (const satzart of ['SPIELER', 'KONTO', 'GUTHABEN', 'CC', 'MITGLIED', 'VERWEIS']) { assert.ok(csv.includes(`${satzart};`), `${satzart} fehlt im Export`); } assert.ok(csv.startsWith('satzart;'), 'die Kopfzeile nennt die Satzart zuerst'); assert.ok(csv.includes('Ersteller-Addon'), 'Verweise gehören dazu'); assert.ok(csv.includes('Kein Paket auffindbar'), 'Problemfälle auch'); }); it('stellt den Bestand auf einer leeren Instanz wieder her', async () => { await leereInstanz(); assert.equal(await db.prisma.player.count(), 0); const bericht = await backup.importAll(csv, ctx); assert.deepEqual(bericht.fehler, [], 'keine Zeile darf hängen bleiben'); assert.equal(await db.prisma.player.count(), 3); assert.equal(await db.prisma.character.count(), 2); assert.equal(await db.prisma.characterLink.count(), 1); const chef = await db.prisma.player.findFirstOrThrow({ where: { displayName: 'Umzugs-Chef' }, }); assert.equal(chef.steamId, '76561198000030001'); assert.equal(chef.forumName, 'UmzugsChef'); // Guthaben und Verbräuche stehen wieder wie zuvor. const stand = await credits.getBalances(chef.id); assert.equal(stand.CC, 0, 'ein Paket gekauft, einen CC daraus gebaut'); assert.equal(stand.REDESIGN_PAID, 1, 'zwei Redesigns erhalten, eines verbraucht'); }); it('hält die Redesign-Kette zusammen', async () => { const chef = await db.prisma.player.findFirstOrThrow({ where: { displayName: 'Umzugs-Chef' }, }); const kette = await db.prisma.character.findMany({ where: { playerId: chef.id }, orderBy: { chainPosition: 'asc' }, }); assert.deepEqual(kette.map((c) => c.name), ['Erster Wurf', 'Zweiter Wurf']); assert.equal( kette[0]?.lineageId, kette[1]?.lineageId, 'beide gehören zur selben Kette', ); assert.equal(kette[0]?.status, 'INACTIVE'); assert.equal(kette[1]?.status, 'ACTIVE'); }); it('nimmt den Gruppen-CC samt seinem zweiten Nutzer mit', async () => { const gast = await db.prisma.player.findFirstOrThrow({ where: { displayName: 'Umzugs-Gast' }, }); const seine = await characters.listCharacters(gast.id); assert.deepEqual( seine.map((c) => c.name), ['Zweiter Wurf'], 'er ist Mitglied, nicht Besitzer', ); }); it('legt beim zweiten Einspielen nichts doppelt an', async () => { const vorher = { spieler: await db.prisma.player.count(), ccs: await db.prisma.character.count(), buchungen: await db.prisma.creditLedger.count(), mitglieder: await db.prisma.characterMember.count(), links: await db.prisma.characterLink.count(), }; const bericht = await backup.importAll(csv, ctx); assert.deepEqual(bericht.fehler, []); assert.equal( Object.values(bericht.angelegt).reduce((a, b) => a + b, 0), 0, 'alles war schon da', ); assert.deepEqual( { spieler: await db.prisma.player.count(), ccs: await db.prisma.character.count(), buchungen: await db.prisma.creditLedger.count(), mitglieder: await db.prisma.characterMember.count(), links: await db.prisma.characterLink.count(), }, vorher, 'ein zweiter Lauf darf den Bestand nicht verdoppeln', ); }); it('ergänzt einen Spieler, der auf der Zielinstanz fehlt', async () => { await db.prisma.player.deleteMany({ where: { displayName: 'Umzugs-Problem' } }); assert.equal(await db.prisma.player.count(), 2); const bericht = await backup.importAll(csv, ctx); assert.equal(bericht.angelegt.SPIELER, 1, 'nur der fehlende kommt dazu'); assert.equal(await db.prisma.player.count(), 3); const wieder = await db.prisma.player.findFirstOrThrow({ where: { displayName: 'Umzugs-Problem' }, }); assert.equal(wieder.isProblem, true, 'auch die Markierung kommt mit'); }); it('meldet krumme Zeilen, statt den Lauf abzubrechen', async () => { const kaputt = [ 'satzart;spieler;steam_id;cc_name;bezeichnung;wert', 'VERWEIS;Gibt-Es-Nicht;;Kein CC;Irgendwas;https://example.org/', 'SPIELER;Kommt-Trotzdem-Durch;76561198000030777;;;', ].join('\n'); const bericht = await backup.importAll(kaputt, ctx); assert.equal(bericht.fehler.length, 1); assert.match(bericht.fehler[0]?.grund ?? '', /nicht gefunden/); assert.equal(bericht.angelegt.SPIELER, 1, 'die gute Zeile läuft trotzdem durch'); }); it('weist eine Datei ohne Satzart ab', async () => { await assert.rejects( () => backup.importAll('name;wert\nirgendwas;1', ctx), (error: Error & { code?: string }) => error.code === 'NO_SATZART', ); }); });