Der Zugang aus der Erstinstallation ist ab Werk unantastbar: weder Rolle noch Zustand lassen sich aendern, sein Passwort nicht von aussen zuruecksetzen, und loeschen laesst er sich nicht - auch nicht durch sich selbst. So bleibt ein Weg zurueck, wenn sich jemand aus Versehen die Rechte entzieht. Sein eigenes Passwort aendert er weiterhin unter "Konto". BOOTSTRAP_ADMIN_EDITABLE=true hebt den Schutz auf. Welcher Zugang gemeint ist, traegt der Start nach: den Namen kennt nur er, die Migration nicht. Ein CC behielt nach dem Verschieben seines Redesigns den Vermerk "Redesignt", obwohl ueber ihm nichts mehr stand. Der Vermerk stammte von einem Nachfolger, der inzwischen woanders lag. Jetzt folgt er der Kette: wer einen Nachfolger hat, wurde ersetzt - wer ans Ende rueckt, nicht mehr. Ein Platz entsteht dabei nicht aus dem Nichts, die Deckung wird geprueft wie sonst auch. Neu bearbeitbar: Name, Beschreibung und der verknuepfte Forums-Antrag eines CCs. Ein Antrag, den der Abgleich noch nicht kennt, wird dabei vorgemerkt - sonst staende er beim naechsten Lauf als offener Antrag da, obwohl er laengst eingetragen ist. Ebenso laesst sich nachtraeglich umstellen, ob ein Redesign bezahlt oder Kulanz war; umgebucht wird ueber eine Gegenbuchung, ueberschrieben wird nichts. Zuletzt: Eintraege eines anderen Editors kamen nicht an, solange die Seite offen stand. Jetzt wird der Stand geholt, sobald der Tab wieder in den Vordergrund kommt - das trifft den Moment, in dem jemand wieder hinsieht, ohne im Hintergrund dauernd zu fragen.
497 lines
19 KiB
TypeScript
497 lines
19 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { after, before, describe, it } from 'node:test';
|
|
import { startTestDatabase, type TestDatabase } from './harness.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 characters: Characters;
|
|
let credits: Credits;
|
|
let db: Db;
|
|
let ctx: { actorId: number; actorName: string };
|
|
|
|
before(async () => {
|
|
database = await startTestDatabase(54340);
|
|
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');
|
|
characters = await import('../src/services/characters.js');
|
|
credits = await import('../src/services/credits.js');
|
|
|
|
const user = await db.prisma.user.create({
|
|
data: { username: 'ketten', passwordHash: 'x', role: 'ADMIN' },
|
|
});
|
|
ctx = { actorId: user.id, actorName: user.username };
|
|
});
|
|
|
|
after(async () => {
|
|
await db.prisma.$disconnect();
|
|
await database.stop();
|
|
});
|
|
|
|
async function spielerMitPaketen(name: string, pakete = 1) {
|
|
const player = await db.prisma.player.create({ data: { displayName: name } });
|
|
for (let i = 0; i < pakete; i += 1) await credits.bookCcPackage(player.id, ctx);
|
|
return player.id;
|
|
}
|
|
|
|
/** Die Kette eines Spielers, wie sie angezeigt würde. */
|
|
async function kette(playerId: number, lineageId: string) {
|
|
const glieder = await db.prisma.character.findMany({
|
|
where: { playerId, lineageId },
|
|
orderBy: [{ chainPosition: 'asc' }, { id: 'asc' }],
|
|
select: { id: true, name: true, predecessorId: true, chainPosition: true },
|
|
});
|
|
return glieder;
|
|
}
|
|
|
|
/** Prüft, dass Positionen und Vorgänger-Verweise dasselbe sagen. */
|
|
function verkettungStimmt(glieder: { id: number; predecessorId: number | null }[]): void {
|
|
let erwartet: number | null = null;
|
|
for (const glied of glieder) {
|
|
assert.equal(
|
|
glied.predecessorId,
|
|
erwartet,
|
|
'die Verkettung muss der Reihenfolge folgen, sonst laufen beide Sichten auseinander',
|
|
);
|
|
erwartet = glied.id;
|
|
}
|
|
}
|
|
|
|
describe('Lücken in einer Redesign-Kette schliessen', () => {
|
|
it('legt Redesigns hintereinander ab', async () => {
|
|
const spieler = await spielerMitPaketen('Kettenbauer', 1);
|
|
const eins = await characters.createCharacter(spieler, { name: 'CC 1' }, ctx);
|
|
await credits.bookRedesignPackage(spieler, ctx);
|
|
const zwei = await characters.redesignCharacter(
|
|
eins.character.id,
|
|
{ name: 'Redesign 2', useKind: 'REDESIGN_PAID' },
|
|
ctx,
|
|
);
|
|
|
|
const glieder = await kette(spieler, zwei.character.lineageId);
|
|
assert.deepEqual(glieder.map((g) => g.name), ['CC 1', 'Redesign 2']);
|
|
verkettungStimmt(glieder);
|
|
});
|
|
|
|
it('schiebt ein später gefundenes Redesign mitten hinein', async () => {
|
|
const spieler = await spielerMitPaketen('Nachtraeger', 2);
|
|
const eins = await characters.createCharacter(spieler, { name: 'CC 1' }, ctx);
|
|
await credits.bookRedesignPackage(spieler, ctx);
|
|
const drei = await characters.redesignCharacter(
|
|
eins.character.id,
|
|
{ name: 'Redesign 3', useKind: 'REDESIGN_PAID' },
|
|
ctx,
|
|
);
|
|
|
|
// Redesign 2 taucht erst jetzt auf - als eigener CC, noch ohne Kette.
|
|
const zwei = await characters.createCharacter(spieler, { name: 'Redesign 2' }, ctx);
|
|
assert.notEqual(zwei.character.lineageId, drei.character.lineageId);
|
|
|
|
await characters.moveInChain(
|
|
zwei.character.id,
|
|
{ lineageId: drei.character.lineageId, afterId: eins.character.id },
|
|
ctx,
|
|
);
|
|
|
|
const glieder = await kette(spieler, drei.character.lineageId);
|
|
assert.deepEqual(
|
|
glieder.map((g) => g.name),
|
|
['CC 1', 'Redesign 2', 'Redesign 3'],
|
|
'die Lücke ist geschlossen, ohne die übrigen anzufassen',
|
|
);
|
|
verkettungStimmt(glieder);
|
|
});
|
|
|
|
it('setzt einen Eintrag auch an den Anfang', async () => {
|
|
const spieler = await spielerMitPaketen('Vorspann', 2);
|
|
const b = await characters.createCharacter(spieler, { name: 'B' }, ctx);
|
|
const a = await characters.createCharacter(spieler, { name: 'A' }, ctx);
|
|
|
|
await characters.moveInChain(
|
|
a.character.id,
|
|
{ lineageId: b.character.lineageId, afterId: null },
|
|
ctx,
|
|
);
|
|
|
|
const glieder = await kette(spieler, b.character.lineageId);
|
|
assert.deepEqual(glieder.map((g) => g.name), ['A', 'B']);
|
|
verkettungStimmt(glieder);
|
|
assert.equal(glieder[0]?.predecessorId, null, 'der Erste hat keinen Vorgänger');
|
|
});
|
|
|
|
it('holt einen Eintrag aus einer Kette in eine andere', async () => {
|
|
const spieler = await spielerMitPaketen('Umsortierer', 3);
|
|
const ersteKette = await characters.createCharacter(spieler, { name: 'Strang A' }, ctx);
|
|
const zweiteKette = await characters.createCharacter(spieler, { name: 'Strang B' }, ctx);
|
|
const wanderer = await characters.createCharacter(spieler, { name: 'Wanderer' }, ctx);
|
|
|
|
await characters.moveInChain(
|
|
wanderer.character.id,
|
|
{ lineageId: ersteKette.character.lineageId, afterId: ersteKette.character.id },
|
|
ctx,
|
|
);
|
|
assert.equal((await kette(spieler, ersteKette.character.lineageId)).length, 2);
|
|
|
|
// Doch die andere Kette.
|
|
await characters.moveInChain(
|
|
wanderer.character.id,
|
|
{ lineageId: zweiteKette.character.lineageId, afterId: zweiteKette.character.id },
|
|
ctx,
|
|
);
|
|
|
|
const erste = await kette(spieler, ersteKette.character.lineageId);
|
|
const zweite = await kette(spieler, zweiteKette.character.lineageId);
|
|
assert.deepEqual(erste.map((g) => g.name), ['Strang A'], 'die alte Kette schliesst sich');
|
|
assert.deepEqual(zweite.map((g) => g.name), ['Strang B', 'Wanderer']);
|
|
verkettungStimmt(erste);
|
|
verkettungStimmt(zweite);
|
|
});
|
|
|
|
it('holt einen falsch einsortierten Eintrag wieder heraus', async () => {
|
|
// Zwei eigenständige CCs, einer davon versehentlich in die Kette des
|
|
// anderen gezogen. Beide sind aus einem eigenen Paket bezahlt.
|
|
const spieler = await spielerMitPaketen('Loeser', 2);
|
|
const eins = await characters.createCharacter(spieler, { name: 'Bleibt' }, ctx);
|
|
const zwei = await characters.createCharacter(spieler, { name: 'Gehoert woanders hin' }, ctx);
|
|
|
|
await characters.moveInChain(
|
|
zwei.character.id,
|
|
{ lineageId: eins.character.lineageId, afterId: eins.character.id },
|
|
ctx,
|
|
);
|
|
// In der Kette gilt: wer einen Nachfolger hat, wurde ersetzt.
|
|
const eingereiht = await db.prisma.character.findUniqueOrThrow({
|
|
where: { id: eins.character.id },
|
|
});
|
|
assert.equal(eingereiht.inactiveReason, 'REWORKED');
|
|
|
|
const { lineageId } = await characters.detachFromChain(zwei.character.id, ctx);
|
|
assert.notEqual(lineageId, eins.character.lineageId);
|
|
|
|
const alte = await kette(spieler, eins.character.lineageId);
|
|
assert.deepEqual(alte.map((g) => g.name), ['Bleibt']);
|
|
|
|
// Und ohne Nachfolger ist "redesignt" hinfällig.
|
|
const zurueck = await db.prisma.character.findUniqueOrThrow({
|
|
where: { id: eins.character.id },
|
|
});
|
|
assert.equal(zurueck.status, 'ACTIVE');
|
|
assert.equal(zurueck.inactiveReason, null);
|
|
|
|
const allein = await db.prisma.character.findUniqueOrThrow({
|
|
where: { id: zwei.character.id },
|
|
});
|
|
assert.equal(allein.predecessorId, null, 'allein heisst ohne Vorgänger');
|
|
});
|
|
|
|
it('lässt ein Redesign nicht ohne eigenen Platz allein stehen', async () => {
|
|
// Ein Redesign hat keine eigene CC-Buchung - es tauscht einen Platz aus.
|
|
// Allein gestellt braucht es einen, und der muss gedeckt sein.
|
|
const spieler = await spielerMitPaketen('Knapp', 1);
|
|
const eins = await characters.createCharacter(spieler, { name: 'Original' }, ctx);
|
|
await credits.bookRedesignPackage(spieler, ctx);
|
|
const zwei = await characters.redesignCharacter(
|
|
eins.character.id,
|
|
{ name: 'Neue Fassung', useKind: 'REDESIGN_PAID' },
|
|
ctx,
|
|
);
|
|
|
|
await assert.rejects(
|
|
() => characters.detachFromChain(zwei.character.id, ctx),
|
|
(error: Error & { code?: string }) => error.code === 'NO_FREE_SLOT',
|
|
'sonst stünden zwei aktive CCs auf einem Paket',
|
|
);
|
|
});
|
|
|
|
it('räumt die Redesign-Markierung ab, wenn der Nachfolger wegzieht', async () => {
|
|
// Genau der Fall aus dem Betrieb: das Redesign gehörte in eine andere
|
|
// Kette. Was zurückbleibt, ist von niemandem mehr ersetzt worden.
|
|
const spieler = await spielerMitPaketen('Umsortiert', 2);
|
|
const platzA = await characters.createCharacter(spieler, { name: 'Platz A' }, ctx);
|
|
const platzB = await characters.createCharacter(spieler, { name: 'Platz B' }, ctx);
|
|
|
|
await credits.bookRedesignPackage(spieler, ctx);
|
|
const redesign = await characters.redesignCharacter(
|
|
platzB.character.id,
|
|
{ name: 'Redesign von B', useKind: 'REDESIGN_PAID' },
|
|
ctx,
|
|
);
|
|
|
|
// B ist jetzt redesignt. Nun stellt sich heraus: das Redesign gehört zu A.
|
|
await characters.moveInChain(
|
|
redesign.character.id,
|
|
{ lineageId: platzA.character.lineageId, afterId: platzA.character.id },
|
|
ctx,
|
|
);
|
|
|
|
const b = await db.prisma.character.findUniqueOrThrow({ where: { id: platzB.character.id } });
|
|
assert.equal(b.status, 'ACTIVE', 'ohne Nachfolger ist B wieder der aktuelle CC');
|
|
assert.equal(b.inactiveReason, null);
|
|
|
|
const a = await db.prisma.character.findUniqueOrThrow({ where: { id: platzA.character.id } });
|
|
assert.equal(a.inactiveReason, 'REWORKED', 'A hat jetzt einen Nachfolger');
|
|
});
|
|
|
|
it('lässt keinen CC in die Kette eines anderen Spielers', async () => {
|
|
const einer = await spielerMitPaketen('Einer', 1);
|
|
const anderer = await spielerMitPaketen('Anderer', 1);
|
|
const seiner = await characters.createCharacter(einer, { name: 'Seiner' }, ctx);
|
|
const fremder = await characters.createCharacter(anderer, { name: 'Fremder' }, ctx);
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
characters.moveInChain(
|
|
fremder.character.id,
|
|
{ lineageId: seiner.character.lineageId, afterId: seiner.character.id },
|
|
ctx,
|
|
),
|
|
(error: Error & { code?: string }) => error.code === 'FOREIGN_CHAIN',
|
|
);
|
|
});
|
|
|
|
it('lässt einen CC nicht hinter sich selbst stellen', async () => {
|
|
const spieler = await spielerMitPaketen('Schlange', 1);
|
|
const cc = await characters.createCharacter(spieler, { name: 'Beisst sich' }, ctx);
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
characters.moveInChain(
|
|
cc.character.id,
|
|
{ lineageId: cc.character.lineageId, afterId: cc.character.id },
|
|
ctx,
|
|
),
|
|
(error: Error & { code?: string }) => error.code === 'SELF_REFERENCE',
|
|
);
|
|
});
|
|
|
|
it('macht aus einem eingereihten CC einen redesignten', async () => {
|
|
const spieler = await spielerMitPaketen('Unberuehrt', 2);
|
|
const aktiv = await characters.createCharacter(spieler, { name: 'Aktiv' }, ctx);
|
|
const ziel = await characters.createCharacter(spieler, { name: 'Ziel' }, ctx);
|
|
|
|
await characters.moveInChain(
|
|
aktiv.character.id,
|
|
{ lineageId: ziel.character.lineageId, afterId: ziel.character.id },
|
|
ctx,
|
|
);
|
|
|
|
const danach = await db.prisma.character.findUniqueOrThrow({
|
|
where: { id: aktiv.character.id },
|
|
});
|
|
assert.equal(danach.status, 'ACTIVE', 'am Ende der Kette bleibt er der aktuelle');
|
|
|
|
const davor = await db.prisma.character.findUniqueOrThrow({ where: { id: ziel.character.id } });
|
|
assert.equal(davor.inactiveReason, 'REWORKED', 'wer einen Nachfolger bekommt, ist ersetzt');
|
|
});
|
|
});
|
|
|
|
describe('Verweise an einem CC', () => {
|
|
it('hängt einen Verweis an und gibt ihn mit aus', async () => {
|
|
const spieler = await spielerMitPaketen('Verweiser', 1);
|
|
const cc = await characters.createCharacter(spieler, { name: 'Mit Addon' }, ctx);
|
|
|
|
await characters.addLink(
|
|
cc.character.id,
|
|
{ label: 'Ersteller-Addon', url: 'https://steamcommunity.com/sharedfiles/?id=42' },
|
|
ctx,
|
|
);
|
|
|
|
const liste = await characters.listCharacters(spieler);
|
|
const gefunden = liste.find((c) => c.id === cc.character.id);
|
|
assert.deepEqual(
|
|
gefunden?.links.map((l) => l.label),
|
|
['Ersteller-Addon'],
|
|
);
|
|
});
|
|
|
|
it('weist eine Adresse ab, die keine ist', async () => {
|
|
const spieler = await spielerMitPaketen('Vorsichtig', 1);
|
|
const cc = await characters.createCharacter(spieler, { name: 'Sauber' }, ctx);
|
|
|
|
for (const url of ['javascript:alert(1)', 'nur-text', 'file:///etc/passwd']) {
|
|
await assert.rejects(
|
|
() => characters.addLink(cc.character.id, { label: 'Test', url }, ctx),
|
|
(error: Error & { code?: string }) => error.code === 'INVALID_URL',
|
|
`"${url}" hätte abgewiesen werden müssen`,
|
|
);
|
|
}
|
|
});
|
|
|
|
it('lässt sich ändern und entfernen', async () => {
|
|
const spieler = await spielerMitPaketen('Pfleger', 1);
|
|
const cc = await characters.createCharacter(spieler, { name: 'Gepflegt' }, ctx);
|
|
const link = await characters.addLink(
|
|
cc.character.id,
|
|
{ label: 'Alt', url: 'https://example.org/alt' },
|
|
ctx,
|
|
);
|
|
|
|
const geaendert = await characters.updateLink(
|
|
link.id,
|
|
{ label: 'Neu', url: 'https://example.org/neu' },
|
|
ctx,
|
|
);
|
|
assert.equal(geaendert.label, 'Neu');
|
|
assert.equal(geaendert.url, 'https://example.org/neu');
|
|
|
|
await characters.removeLink(link.id, ctx);
|
|
assert.equal(await db.prisma.characterLink.count({ where: { characterId: cc.character.id } }), 0);
|
|
});
|
|
});
|
|
|
|
describe('Redesign nachträglich umbuchen', () => {
|
|
it('stellt von bezahlt auf Kulanz um, ohne die Geschichte zu verbiegen', async () => {
|
|
const spieler = await spielerMitPaketen('Umbucher', 1);
|
|
const eins = await characters.createCharacter(spieler, { name: 'Erste Fassung' }, ctx);
|
|
const zwei = await characters.redesignCharacter(
|
|
eins.character.id,
|
|
{ name: 'Zweite Fassung', useKind: 'REDESIGN_PAID' },
|
|
ctx,
|
|
);
|
|
assert.equal(zwei.balances.REDESIGN_PAID, 0, 'das bezahlte ist verbraucht');
|
|
|
|
await credits.bookKulanz(spieler, 1, 'Absprache aufgetaucht', ctx);
|
|
const danach = await characters.changeRedesignKind(zwei.character.id, 'REDESIGN_KULANZ', ctx);
|
|
|
|
assert.equal(danach.balances.REDESIGN_PAID, 1, 'das bezahlte ist wieder frei');
|
|
assert.equal(danach.balances.REDESIGN_KULANZ, 0, 'dafür ist die Kulanz verbraucht');
|
|
|
|
// Korrigiert wird durch Gegenbuchung, nicht durch Überschreiben.
|
|
const zeilen = await db.prisma.creditLedger.findMany({
|
|
where: { characterId: zwei.character.id },
|
|
orderBy: { id: 'asc' },
|
|
});
|
|
assert.equal(zeilen.length, 3, 'Verbrauch, Storno, neuer Verbrauch');
|
|
assert.equal(zeilen[1]?.source, 'REVERSAL');
|
|
assert.equal(zeilen[2]?.kind, 'REDESIGN_KULANZ');
|
|
});
|
|
|
|
it('und wieder zurück', async () => {
|
|
const spieler = await spielerMitPaketen('Hin und her', 1);
|
|
const eins = await characters.createCharacter(spieler, { name: 'A' }, ctx);
|
|
await credits.bookKulanz(spieler, 1, 'Erstmal Kulanz', ctx);
|
|
const zwei = await characters.redesignCharacter(
|
|
eins.character.id,
|
|
{ name: 'B', useKind: 'REDESIGN_KULANZ' },
|
|
ctx,
|
|
);
|
|
|
|
const danach = await characters.changeRedesignKind(zwei.character.id, 'REDESIGN_PAID', ctx);
|
|
assert.equal(danach.balances.REDESIGN_KULANZ, 1);
|
|
assert.equal(danach.balances.REDESIGN_PAID, 0, 'das aus dem CC-Paket ist jetzt dran');
|
|
});
|
|
|
|
it('weist dieselbe Art ab', async () => {
|
|
const spieler = await spielerMitPaketen('Unveraendert', 1);
|
|
const eins = await characters.createCharacter(spieler, { name: 'X' }, ctx);
|
|
const zwei = await characters.redesignCharacter(
|
|
eins.character.id,
|
|
{ name: 'Y', useKind: 'REDESIGN_PAID' },
|
|
ctx,
|
|
);
|
|
|
|
await assert.rejects(
|
|
() => characters.changeRedesignKind(zwei.character.id, 'REDESIGN_PAID', ctx),
|
|
(error: Error & { code?: string }) => error.code === 'SAME_KIND',
|
|
);
|
|
});
|
|
|
|
it('weist einen CC ohne Redesign-Buchung ab', async () => {
|
|
const spieler = await spielerMitPaketen('Ohne Redesign', 1);
|
|
const eins = await characters.createCharacter(spieler, { name: 'Direkt aus dem Paket' }, ctx);
|
|
|
|
await assert.rejects(
|
|
() => characters.changeRedesignKind(eins.character.id, 'REDESIGN_KULANZ', ctx),
|
|
(error: Error & { code?: string }) => error.code === 'NO_REDESIGN_BOOKING',
|
|
'ein CC aus dem Paket ist kein Redesign',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Angaben eines CCs nachtragen', () => {
|
|
it('ändert Namen und Beschreibung', async () => {
|
|
const spieler = await spielerMitPaketen('Korrigierer', 1);
|
|
const cc = await characters.createCharacter(spieler, { name: 'Tipfehler' }, ctx);
|
|
|
|
await characters.updateCharacter(
|
|
cc.character.id,
|
|
{ name: 'Tippfehler behoben', loreText: 'Jetzt mit Beschreibung' },
|
|
ctx,
|
|
);
|
|
|
|
const danach = await db.prisma.character.findUniqueOrThrow({ where: { id: cc.character.id } });
|
|
assert.equal(danach.name, 'Tippfehler behoben');
|
|
assert.equal(danach.loreText, 'Jetzt mit Beschreibung');
|
|
});
|
|
|
|
it('hängt einen Antrag an, den der Abgleich schon kennt', async () => {
|
|
const spieler = await spielerMitPaketen('Verknuepfer', 1);
|
|
const cc = await characters.createCharacter(spieler, { name: 'Mit Antrag' }, ctx);
|
|
|
|
await db.prisma.forumThread.create({
|
|
data: {
|
|
threadId: 7771,
|
|
title: 'Sein Antrag',
|
|
url: 'https://forum.example.org/index.php?thread/7771-sein-antrag/',
|
|
currentBoard: 'CC_ARCHIV',
|
|
},
|
|
});
|
|
|
|
await characters.updateCharacter(
|
|
cc.character.id,
|
|
{ threadRef: 'https://forum.example.org/index.php?thread/7771-sein-antrag/' },
|
|
ctx,
|
|
);
|
|
|
|
const thread = await db.prisma.forumThread.findUniqueOrThrow({ where: { threadId: 7771 } });
|
|
assert.equal(thread.linkedCharacterId, cc.character.id);
|
|
assert.equal(thread.needsClassification, false, 'er ist damit erledigt');
|
|
});
|
|
|
|
it('merkt einen unbekannten Antrag vor', async () => {
|
|
const spieler = await spielerMitPaketen('Vormerker', 1);
|
|
const cc = await characters.createCharacter(spieler, { name: 'Noch ungelesen' }, ctx);
|
|
|
|
// Nur die Nummer, und der Abgleich kennt den Thread nicht.
|
|
await characters.updateCharacter(cc.character.id, { threadRef: '8888' }, ctx);
|
|
|
|
const thread = await db.prisma.forumThread.findUniqueOrThrow({ where: { threadId: 8888 } });
|
|
assert.equal(thread.linkedCharacterId, cc.character.id);
|
|
assert.equal(
|
|
thread.needsClassification,
|
|
false,
|
|
'sonst stünde er beim nächsten Lauf als offener Antrag da',
|
|
);
|
|
});
|
|
|
|
it('löst eine Verknüpfung wieder', async () => {
|
|
const spieler = await spielerMitPaketen('Loeser2', 1);
|
|
const cc = await characters.createCharacter(spieler, { name: 'Doch nicht' }, ctx);
|
|
await characters.updateCharacter(cc.character.id, { threadRef: '9999' }, ctx);
|
|
|
|
await characters.updateCharacter(cc.character.id, { threadRef: null }, ctx);
|
|
|
|
const thread = await db.prisma.forumThread.findUniqueOrThrow({ where: { threadId: 9999 } });
|
|
assert.equal(thread.linkedCharacterId, null);
|
|
|
|
const liste = await characters.listCharacters(spieler);
|
|
const eintrag = liste.find((c) => c.id === cc.character.id);
|
|
assert.deepEqual(eintrag?.forumThreads, [], 'am CC hängt kein Antrag mehr');
|
|
});
|
|
|
|
it('weist eine Eingabe ab, die keine Thread-Angabe ist', async () => {
|
|
const spieler = await spielerMitPaketen('Vertipper', 1);
|
|
const cc = await characters.createCharacter(spieler, { name: 'Sauber' }, ctx);
|
|
|
|
await assert.rejects(
|
|
() => characters.updateCharacter(cc.character.id, { threadRef: 'keine ahnung' }, ctx),
|
|
(error: Error & { code?: string }) => error.code === 'INVALID_THREAD',
|
|
);
|
|
});
|
|
});
|