Die Spielerseite zeigt CCs nicht mehr als zwei Listen "aktiv" und "inaktiv", sondern als das, was sie sind: je Platz eine Kette, von unten nach oben gelesen. Unten der erste CC, darueber seine Redesigns. Gefunden wird selten alles auf einmal. Ein Redesign taucht Jahre spaeter auf, weil sein Antrag in einem anderen Board lag oder weil der Spieler es fuer den Beitritt zu einem fremden Gruppen-CC benutzt hat. Dann steht in der Kette schon "3 auf 5", und 4 fehlt. Deshalb laesst sich jeder Eintrag mit der Maus an seinen Platz ziehen - auch mitten in eine Kette und in jede andere Kette desselben Spielers. Das Verschieben ordnet nur ein: es bucht nichts und aendert keinen Status. Dafuer traegt jeder CC jetzt eine Position in seiner Kette. Sie laesst sich verschieben, ohne die ganze Kette neu zu haengen; der Vorgaenger-Verweis, an dem der Rest der Anwendung haengt, wird danach aus den Positionen neu abgeleitet. So koennen beide Sichten nicht auseinanderlaufen. Der Bestand bekommt seine Positionen in der Migration aus der vorhandenen Verkettung. An jedem CC haengen jetzt beliebig viele Verweise: ein Name und eine Adresse, der Name wird zum Link. Nur http und https - ein "javascript:"-Link in einer Oberflaeche, die mehrere Leute bedienen, waere ein offenes Scheunentor. Die SteamID fuehrt auf der Spielerseite zum GExtension-Profil, der Forum-Name zum Forumsprofil. Dessen Adresse kommt aus dem Abgleich: die Profile liegen nicht unter dem Forums-Pfad, sondern eine Ebene darueber, und das laesst sich nicht verlaesslich erraten - also wird uebernommen, worauf das Forum selbst verweist. Zuletzt der Umzug: unter Import gibt es fuer Administratoren den gesamten Bestand als eine Tabelle - Spieler, Guthaben, CCs samt Ketten, frueherer Konten, Mitgliedschaften und Verweise. Eingespielt wird ergaenzend: was da ist, bleibt unangetastet, zweimal dieselbe Datei aendert nichts, und krumme Zeilen werden mit Zeilennummer gemeldet statt den Lauf abzubrechen. Ein Test spielt den vollen Weg durch - exportieren, Bestand loeschen, zurueckholen.
231 lines
7.9 KiB
TypeScript
231 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';
|
|
process.env.SCHEDULE_ENABLED = 'false';
|
|
|
|
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',
|
|
);
|
|
});
|
|
});
|