import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { startTestDatabase, type TestDatabase } from './harness.js'; type Forum = typeof import('../src/services/forum.js'); type Players = typeof import('../src/services/players.js'); type Characters = typeof import('../src/services/characters.js'); type Db = typeof import('../src/db.js'); let database: TestDatabase; let forum: Forum; let players: Players; let characters: Characters; let db: Db; let ctx: { actorId: number; actorName: string }; before(async () => { database = await startTestDatabase(54337); 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'); forum = await import('../src/services/forum.js'); players = await import('../src/services/players.js'); characters = await import('../src/services/characters.js'); const user = await db.prisma.user.create({ data: { username: 'pfleger', passwordHash: 'x', role: 'ADMIN' }, }); ctx = { actorId: user.id, actorName: user.username }; }); after(async () => { await db.prisma.$disconnect(); await database.stop(); }); /** Legt einen Thread an, wie ihn der Abgleich ohne Spieler vorfinden würde. */ async function offenerThread(threadId: number, titel: string, autor: string) { return db.prisma.forumThread.create({ data: { threadId, title: titel, url: `https://forum.example.org/index.php?thread/${threadId}-antrag/`, authorName: autor, currentBoard: 'CC_ARCHIV', }, }); } describe('Anträge ohne zugeordneten Spieler', () => { it('hält sie aus der Arbeitsliste heraus, aber nicht aus der Datenbank', async () => { await offenerThread(9001, 'CC Antrag von einem Fremden', 'Unbekannter'); const arbeitsliste = await forum.listThreads({ onlyOpen: true }); assert.equal( arbeitsliste.some((t) => t.threadId === 9001), false, 'ohne Spieler ist nichts zu entscheiden', ); const wartend = await forum.listUnassignedThreads({}); assert.equal( wartend.items.some((t) => t.threadId === 9001), true, 'auffindbar bleibt er trotzdem', ); }); it('findet sie über Titel und Ersteller', async () => { await offenerThread(9002, 'Feldagent Ersthelfer', 'Darkbrine44'); const nachTitel = await forum.listUnassignedThreads({ query: 'ersthelfer' }); assert.deepEqual( nachTitel.items.map((t) => t.threadId), [9002], 'Grossschreibung darf keine Rolle spielen', ); const nachAutor = await forum.listUnassignedThreads({ query: 'darkbrine' }); assert.deepEqual( nachAutor.items.map((t) => t.threadId), [9002], ); }); it('begrenzt die Ausgabe, nennt aber die volle Zahl', async () => { for (const id of [9010, 9011, 9012]) await offenerThread(id, `Antrag ${id}`, 'Serienbeantrager'); const seite = await forum.listUnassignedThreads({ query: 'Serienbeantrager', take: 2 }); assert.equal(seite.items.length, 2); assert.equal(seite.total, 3, 'sonst wüsste niemand, dass etwas fehlt'); }); }); describe('Nachträgliche Zuordnung', () => { it('zieht wartende Anträge an einen neu angelegten Spieler', async () => { await offenerThread(9101, 'CC Antrag von Monox', 'Monox'); await offenerThread(9102, 'Redesign von Monox', 'monox'); const spieler = await players.createPlayer( { displayName: 'Monox', steamId: '76561198000002222', forumName: 'Monox' }, ctx, ); const zugeordnet = await forum.listThreads({ playerId: spieler.id }); assert.deepEqual( zugeordnet.map((t) => t.threadId).sort(), [9101, 9102], 'auch die abweichende Schreibweise gehört dazu', ); }); it('zieht sie auch nach, wenn der Forum-Name erst später eingetragen wird', async () => { await offenerThread(9201, 'Antrag eines Umbenannten', 'NeuerName'); const spieler = await players.createPlayer( { displayName: 'Ohne Forum', steamId: '76561198000003333' }, ctx, ); assert.equal((await forum.listThreads({ playerId: spieler.id })).length, 0); await players.updatePlayer(spieler.id, { forumName: 'NeuerName' }, ctx); const zugeordnet = await forum.listThreads({ playerId: spieler.id }); assert.deepEqual( zugeordnet.map((t) => t.threadId), [9201], ); }); it('holt über ein früheres Konto auch die alten Anträge', async () => { await offenerThread(9301, 'Antrag unter dem alten Namen', 'AltesKonto'); const spieler = await players.createPlayer( { displayName: 'Umzügler', steamId: '76561198000004444', forumName: 'NeuesKonto' }, ctx, ); await players.addAlias(spieler.id, 'FORUM_NAME', 'AltesKonto', 'Vor dem Wechsel', ctx); const zugeordnet = await forum.listThreads({ playerId: spieler.id }); assert.deepEqual( zugeordnet.map((t) => t.threadId), [9301], ); }); it('nimmt einem Spieler nie einen bereits zugeordneten Antrag weg', async () => { const erster = await players.createPlayer( { displayName: 'Erster', steamId: '76561198000005555', forumName: 'Doppelgänger' }, ctx, ); await offenerThread(9401, 'Strittiger Antrag', 'Doppelgänger'); await forum.assignPlayer(9401, erster.id, ctx); // Ein zweiter Spieler trägt denselben Namen als früheres Konto nach. const zweiter = await players.createPlayer( { displayName: 'Zweiter', steamId: '76561198000006666' }, ctx, ); await players.updatePlayer(zweiter.id, { forumName: 'Doppelgänger' }, ctx); const thread = await db.prisma.forumThread.findUniqueOrThrow({ where: { threadId: 9401 } }); assert.equal(thread.playerId, erster.id, 'eine getroffene Entscheidung bleibt stehen'); }); }); describe('Antrag am CC', () => { it('hängt den Thread an den CC, der aus ihm hervorgeht', async () => { const spieler = await players.createPlayer( { displayName: 'Antragsteller', steamId: '76561198000007777', forumName: 'Antragsteller' }, ctx, ); await db.prisma.creditLedger.create({ data: { playerId: spieler.id, kind: 'CC', amount: 1, source: 'CC_PACKAGE' }, }); await offenerThread(9501, 'The Duke', 'Antragsteller'); await forum.claimThreadsForPlayer(spieler.id, ['Antragsteller']); const { characterId } = await forum.classifyThread( 9501, { classification: 'NEW_CC', characterName: 'The Duke' }, ctx, ); assert.ok(characterId); const liste = await characters.listCharacters(spieler.id); const cc = liste.find((c) => c.id === characterId); assert.deepEqual( cc?.forumThreads.map((t) => t.threadId), [9501], 'ohne das bliebe der Name auf der Spielerseite ein toter Text', ); assert.match(cc?.forumThreads[0]?.url ?? '', /thread\/9501/); }); });