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.
230 lines
7.9 KiB
TypeScript
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',
|
|
);
|
|
});
|
|
});
|