Files
CC-Manager/server/test/backup.test.ts
T
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',
);
});
});