import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { startTestDatabase, type TestDatabase } from './harness.js'; type Schedule = typeof import('../src/services/schedule.js'); type Db = typeof import('../src/db.js'); let database: TestDatabase; let schedule: Schedule; let db: Db; const JETZT = new Date('2026-09-21T12:00:00Z'); const TAG = 24 * 60 * 60 * 1000; before(async () => { database = await startTestDatabase(54338); process.env.DATABASE_URL = database.url; process.env.SESSION_SECRET = 'test-secret-that-is-long-enough-000000'; process.env.NODE_ENV = 'test'; // Der Zeitplan selbst darf im Test nicht anlaufen, nur seine Logik zählt. process.env.SCHEDULE_ENABLED = 'false'; db = await import('../src/db.js'); schedule = await import('../src/services/schedule.js'); }); after(async () => { await db.prisma.$disconnect(); await database.stop(); }); /** * Ein Spieler mit Steam-Konto, letztem Kauf vor `kaufVorTagen` und letztem * Abgleich vor `abgleichVorStunden`. */ async function spieler( name: string, steamId: string, kaufVorTagen: number | null, abgleichVorStunden: number | null, ) { const angelegt = await db.prisma.player.create({ data: { displayName: name, steamId, gextensionSyncedAt: abgleichVorStunden === null ? null : new Date(JETZT.getTime() - abgleichVorStunden * 60 * 60 * 1000), }, }); if (kaufVorTagen !== null) { await db.prisma.creditLedger.create({ data: { playerId: angelegt.id, kind: 'CC', amount: 1, source: 'CC_PACKAGE', occurredAt: new Date(JETZT.getTime() - kaufVorTagen * TAG), }, }); } return angelegt.id; } describe('Wer wann auf neue Pakete geprüft wird', () => { let frisch = 0; let frischGeradeGeprueft = 0; let ruhend = 0; let ruhendLangeHer = 0; let nochNieGeprueft = 0; before(async () => { // Kauf vor einem Monat, Abgleich vor drei Stunden: fällig (Takt zwei Stunden). frisch = await spieler('Frisch', '76561198000020001', 30, 3); // Ebenso frisch, aber gerade eben geprüft: noch nicht wieder dran. frischGeradeGeprueft = await spieler('Eben geprüft', '76561198000020002', 30, 1); // Letzter Kauf vor vier Jahren, Abgleich vor drei Stunden: der Takt ist // hier sechs Stunden, also noch nicht dran. ruhend = await spieler('Ruhend', '76561198000020003', 4 * 365, 3); // Derselbe Fall, aber der Abgleich liegt sieben Stunden zurück: fällig. ruhendLangeHer = await spieler('Ruhend und überfällig', '76561198000020004', 4 * 365, 7); // Nie abgeglichen: immer fällig, egal wie lange der Kauf her ist. nochNieGeprueft = await spieler('Neu hier', '76561198000020005', null, null); }); it('nimmt einen Spieler mit frischem Kauf nach zwei Stunden wieder dran', async () => { const faellig = await schedule.faelligeSpieler(JETZT); assert.ok(faellig.includes(frisch)); }); it('lässt ihn in Ruhe, solange die zwei Stunden nicht um sind', async () => { const faellig = await schedule.faelligeSpieler(JETZT); assert.equal(faellig.includes(frischGeradeGeprueft), false); }); it('prüft ein ruhendes Konto nur alle sechs Stunden', async () => { const faellig = await schedule.faelligeSpieler(JETZT); assert.equal( faellig.includes(ruhend), false, 'drei Stunden reichen bei einem Konto ohne Kauf seit Jahren nicht', ); assert.ok(faellig.includes(ruhendLangeHer), 'nach sieben Stunden aber schon'); }); it('nimmt einen nie geprüften Spieler sofort dran', async () => { const faellig = await schedule.faelligeSpieler(JETZT); assert.ok(faellig.includes(nochNieGeprueft)); }); it('lässt Spieler ohne Steam-Konto ganz aus', async () => { const ohne = await db.prisma.player.create({ data: { displayName: 'Ohne Konto' } }); const faellig = await schedule.faelligeSpieler(JETZT); assert.equal( faellig.includes(ohne.id), false, 'ohne Kennung gibt es bei GExtension nichts abzurufen', ); }); it('zählt ein früheres Konto mit', async () => { const umzuegler = await db.prisma.player.create({ data: { displayName: 'Nur Alias', gextensionSyncedAt: new Date(JETZT.getTime() - TAG) }, }); await db.prisma.playerAlias.create({ data: { playerId: umzuegler.id, type: 'STEAM_ID', value: '76561198000020099' }, }); const faellig = await schedule.faelligeSpieler(JETZT); assert.ok(faellig.includes(umzuegler.id), 'seine Kaeufe liegen unter der alten Kennung'); }); }); describe('Der nächtliche Forumslauf', () => { const nacht = 4; it('läuft nicht, bevor die Stunde erreicht ist', () => { const frueh = new Date('2026-09-21T02:30:00'); assert.equal(schedule.forumLaufFaellig(null, frueh, nacht), false); }); it('läuft, sobald die Stunde erreicht ist und heute noch nichts lief', () => { const morgens = new Date('2026-09-21T04:10:00'); const gestern = new Date('2026-09-20T04:05:00'); assert.equal(schedule.forumLaufFaellig(gestern, morgens, nacht), true); }); it('läuft an einem Tag nur einmal', () => { const morgens = new Date('2026-09-21T04:10:00'); const abends = new Date('2026-09-21T22:00:00'); assert.equal(schedule.forumLaufFaellig(morgens, abends, nacht), false); }); it('holt einen ausgefallenen Lauf am selben Tag nach', () => { // Der Dienst stand nachts still und kommt erst mittags hoch. const mittags = new Date('2026-09-21T12:00:00'); const vorgestern = new Date('2026-09-19T04:00:00'); assert.equal(schedule.forumLaufFaellig(vorgestern, mittags, nacht), true); }); it('stößt nach einem Neustart nicht erneut an', () => { const nachStart = new Date('2026-09-21T06:00:00'); const heuteFrueh = new Date('2026-09-21T04:02:00'); assert.equal(schedule.forumLaufFaellig(heuteFrueh, nachStart, nacht), false); }); });