Files
Harlekin 6094e4eaef Zeitplan in der Oberflaeche einstellbar
Takt und Schalter der beiden Abgleiche standen in der Umgebung und liessen
sich nur mit einem Neustart aendern. Jetzt stehen sie unter Verwaltung ->
Zeitplan, je Lauf einzeln: ob er automatisch laeuft, in welchem Abstand, beim
Kaeufe-Abgleich zusaetzlich der seltenere Takt fuer ruhende Konten und ab
wann ein Konto als ruhend gilt, beim Forumslauf die Stunde, vor der nicht
gelaufen wird.

Die Abstaende stehen in Minuten - damit laesst sich alles ausdruecken -, und
daneben steht, was das in Stunden bedeutet, damit niemand im Kopf rechnet.
Unter einer Viertelstunde geht nicht: beide Abgleiche greifen auf Server zu,
die der Community gehoeren.

Aenderungen greifen sofort, ohne Neustart. Die Vorgaben sind die bisherigen
Werte, sie stehen in der Migration.

SCHEDULE_ENABLED und FORUM_SCAN_HOUR entfallen damit.
2026-09-21 13:25:56 +02:00

230 lines
7.9 KiB
TypeScript

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',
);
});
});