Der Bauplan verspricht, dass ein zweiter Lauf nichts aendert. Zwei Schritte
haben das gebrochen, weil sie ohne Vergleich geschrieben haben: die
Servereinstellungen und die Rangfolge der Rollen.
Aufgefallen beim Einrichten des Technik-Webhooks - der Lauf meldete "GESETZT
abdulive_" und "GESETZT Rangfolge", obwohl die Vorschau zuvor "steht" gesagt
hatte. Vorschau und echter Lauf sagten also Verschiedenes.
Folgenlos war es nicht: jeder Lauf kostete zwei Aufrufe und hinterliess zwei
Eintraege im Audit-Log des Servers, einen davon eine Rollen-Umsortierung.
Jetzt wird beides vorher verglichen. ApiGuild traegt dafuer die vier Felder
aus GUILD_SETTINGS. Ein Test haelt es fest: nach einem Lauf darf der zweite
weder PATCH /guilds/{id} noch PATCH /guilds/{id}/roles schicken.
Am echten Server nachgeprueft - der dritte Lauf meldet keine Aenderung mehr.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
507 lines
21 KiB
TypeScript
507 lines
21 KiB
TypeScript
// Der ganze Durchlauf gegen einen nachgebauten Server. Kein Netz, kein Token.
|
|
//
|
|
// Die Frage, die hier beantwortet wird, ist nicht "kompiliert es", sondern:
|
|
// Was passiert beim zweiten Lauf? Ein Einrichtungsskript, das beim zweiten Mal
|
|
// alles doppelt anlegt, richtet mehr Schaden an, als es Arbeit spart.
|
|
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { CH, type ApiChannel, type ApiGuild, type ApiOverwrite, type ApiRole, type ApiWebhook, type Rest } from './api.ts';
|
|
import { applyPlan, type Step } from './apply.ts';
|
|
import { CATEGORIES, CHANNELS, EVERYONE, ROLES } from './plan.ts';
|
|
import { str } from './permissions.ts';
|
|
|
|
const GUILD = '100';
|
|
|
|
/** Ein Discord-Server im Arbeitsspeicher. Kennt nur die Routen, die wir nutzen. */
|
|
class FakeDiscord implements Rest {
|
|
guild: ApiGuild;
|
|
channels: ApiChannel[] = [];
|
|
webhooks: ApiWebhook[] = [];
|
|
automod: Array<{ id: string; name: string; actions?: unknown[] }> = [];
|
|
messages = new Map<string, Array<{ id: string; content: string; author: { id: string }; components?: unknown[] }>>();
|
|
calls: string[] = [];
|
|
private next = 1000;
|
|
|
|
/** Platz der Bot-Rolle. Discord haengt sie beim Einladen auf 1. */
|
|
botRolePosition: number;
|
|
readonly botUserId = '5000';
|
|
readonly botRoleId = '5001';
|
|
|
|
constructor(opts: { botRolePosition?: number } = {}) {
|
|
this.botRolePosition = opts.botRolePosition ?? 8;
|
|
this.guild = {
|
|
id: GUILD, name: 'abdulive', owner_id: '1', features: [],
|
|
roles: [
|
|
{
|
|
id: GUILD, name: '@everyone', permissions: '0', position: 0,
|
|
color: 0, hoist: false, mentionable: false, managed: false,
|
|
},
|
|
{
|
|
id: this.botRoleId, name: 'abdulive_ Bot', permissions: '8',
|
|
position: this.botRolePosition, color: 0, hoist: false,
|
|
mentionable: false, managed: true,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
private id(): string { return String(this.next++); }
|
|
|
|
async get<T>(path: string): Promise<T> {
|
|
this.calls.push(`GET ${path}`);
|
|
if (path === '/users/@me') return { id: this.botUserId, username: 'abdulive_ Bot', bot: true } as T;
|
|
if (path === `/guilds/${GUILD}/members/${this.botUserId}`) return { roles: [this.botRoleId] } as T;
|
|
if (path === `/guilds/${GUILD}/roles`) return structuredClone(this.guild.roles) as T;
|
|
if (path === `/guilds/${GUILD}`) return structuredClone(this.guild) as T;
|
|
if (path === `/guilds/${GUILD}/channels`) return structuredClone(this.channels) as T;
|
|
if (path === `/guilds/${GUILD}/auto-moderation/rules`) return structuredClone(this.automod) as T;
|
|
const wh = /^\/channels\/(\d+)\/webhooks$/.exec(path);
|
|
if (wh) return this.webhooks.filter((w) => w.channel_id === wh[1]) as T;
|
|
const msg = /^\/channels\/(\d+)\/messages\?limit=(\d+)$/.exec(path);
|
|
if (msg) return structuredClone(this.messages.get(msg[1]!) ?? []).slice(0, Number(msg[2])) as T;
|
|
throw new Error(`GET ${path} nicht nachgebaut`);
|
|
}
|
|
|
|
async post<T>(path: string, body: unknown): Promise<T> {
|
|
this.calls.push(`POST ${path}`);
|
|
const b = body as Record<string, never>;
|
|
if (path === `/guilds/${GUILD}/roles`) {
|
|
const role = { id: this.id(), position: 1, managed: false, ...b } as unknown as ApiRole;
|
|
this.guild.roles.push(role);
|
|
return structuredClone(role) as T;
|
|
}
|
|
if (path === `/guilds/${GUILD}/channels`) {
|
|
const ch = {
|
|
id: this.id(), position: this.channels.length, parent_id: null,
|
|
permission_overwrites: [], ...b,
|
|
} as unknown as ApiChannel;
|
|
this.channels.push(ch);
|
|
return structuredClone(ch) as T;
|
|
}
|
|
if (path === `/guilds/${GUILD}/auto-moderation/rules`) {
|
|
const src = body as Record<string, unknown>;
|
|
const rule = { id: this.id(), name: String(src['name']), actions: src['actions'] as unknown[] };
|
|
this.automod.push(rule);
|
|
return structuredClone(rule) as T;
|
|
}
|
|
const wh = /^\/channels\/(\d+)\/webhooks$/.exec(path);
|
|
if (wh) {
|
|
const made: ApiWebhook = { id: this.id(), name: String(b['name']), channel_id: wh[1]!, token: 'tok-' + this.id() };
|
|
this.webhooks.push(made);
|
|
return structuredClone(made) as T;
|
|
}
|
|
const msg = /^\/channels\/(\d+)\/messages$/.exec(path);
|
|
if (msg) {
|
|
const made = {
|
|
id: this.id(),
|
|
content: String((b as Record<string, unknown>)['content'] ?? ''),
|
|
author: { id: this.botUserId },
|
|
components: ((b as Record<string, unknown>)['components'] ?? []) as unknown[],
|
|
};
|
|
const list = this.messages.get(msg[1]!) ?? [];
|
|
list.push(made);
|
|
this.messages.set(msg[1]!, list);
|
|
return structuredClone(made) as T;
|
|
}
|
|
throw new Error(`POST ${path} nicht nachgebaut`);
|
|
}
|
|
|
|
async patch<T>(path: string, body: unknown): Promise<T> {
|
|
this.calls.push(`PATCH ${path}`);
|
|
if (path === `/guilds/${GUILD}`) {
|
|
// Wie der echte Server: gesetzte Einstellungen stehen danach auch drin.
|
|
// Ohne das liesse sich nicht pruefen, ob ein zweiter Lauf sie in Ruhe
|
|
// laesst.
|
|
Object.assign(this.guild, body as Record<string, unknown>);
|
|
return structuredClone(this.guild) as T;
|
|
}
|
|
if (path === `/guilds/${GUILD}/roles`) { // Rangfolge
|
|
for (const e of body as Array<{ id: string; position: number }>) {
|
|
const r = this.guild.roles.find((x) => x.id === e.id);
|
|
if (!r) continue;
|
|
if (e.position >= this.botRolePosition) throw new Error('Missing Permissions');
|
|
r.position = e.position;
|
|
}
|
|
return structuredClone(this.guild.roles) as T;
|
|
}
|
|
const role = /^\/guilds\/\d+\/roles\/(\d+)$/.exec(path);
|
|
if (role) {
|
|
const r = this.guild.roles.find((x) => x.id === role[1]);
|
|
if (r) Object.assign(r, body);
|
|
return structuredClone(r) as T;
|
|
}
|
|
const rule = /^\/guilds\/\d+\/auto-moderation\/rules\/(\d+)$/.exec(path);
|
|
if (rule) {
|
|
const r = this.automod.find((x) => x.id === rule[1]);
|
|
if (r) Object.assign(r, body);
|
|
return structuredClone(r) as T;
|
|
}
|
|
const msg = /^\/channels\/(\d+)\/messages\/(\d+)$/.exec(path);
|
|
if (msg) {
|
|
const m = (this.messages.get(msg[1]!) ?? []).find((x) => x.id === msg[2]);
|
|
if (m) {
|
|
const src = body as Record<string, unknown>;
|
|
m.content = String(src['content'] ?? '');
|
|
m.components = (src['components'] ?? []) as unknown[];
|
|
}
|
|
return structuredClone(m) as T;
|
|
}
|
|
const ch = /^\/channels\/(\d+)$/.exec(path);
|
|
if (ch) {
|
|
const c = this.channels.find((x) => x.id === ch[1]);
|
|
if (c) Object.assign(c, body);
|
|
return structuredClone(c) as T;
|
|
}
|
|
throw new Error(`PATCH ${path} nicht nachgebaut`);
|
|
}
|
|
}
|
|
|
|
const run = (api: Rest, dryRun = false) =>
|
|
applyPlan(api, GUILD, { dryRun, saveSecrets: false });
|
|
|
|
/** Der Kanal aus dem Bauplan, so wie er im nachgebauten Server gelandet ist. */
|
|
const live = (api: FakeDiscord, key: string): ApiChannel | undefined => {
|
|
const plan = CHANNELS.find((c) => c.key === key);
|
|
assert.ok(plan, `key ${key} gibt es im Bauplan nicht`);
|
|
return api.channels.find((c) => c.name === plan.name && c.type === plan.type);
|
|
};
|
|
|
|
const katName = (key: string): string => {
|
|
const k = CATEGORIES.find((c) => c.key === key);
|
|
assert.ok(k);
|
|
return k.name;
|
|
};
|
|
|
|
const counts = (steps: Step[]): Record<string, number> => {
|
|
const out: Record<string, number> = {};
|
|
for (const s of steps) out[s.change] = (out[s.change] ?? 0) + 1;
|
|
return out;
|
|
};
|
|
|
|
// ---------------------------------------------------------------- Erstlauf
|
|
|
|
test('der erste Lauf legt Rollen, Kategorien, Kanaele und Webhooks an', async () => {
|
|
const api = new FakeDiscord();
|
|
const res = await run(api);
|
|
|
|
assert.equal(res.guild, 'abdulive');
|
|
for (const r of ROLES) {
|
|
assert.ok(api.guild.roles.find((x) => x.name === r.name), `Rolle ${r.name} fehlt`);
|
|
}
|
|
assert.equal(api.channels.filter((c) => c.type === CH.CATEGORY).length, 3);
|
|
for (const c of CHANNELS) {
|
|
assert.ok(api.channels.find((x) => x.name === c.name), `Kanal ${c.name} fehlt`);
|
|
}
|
|
assert.equal(res.webhooks, 4);
|
|
assert.equal(api.automod.length, 2);
|
|
assert.equal(counts(res.steps)['uebersprungen'] ?? 0, 0);
|
|
});
|
|
|
|
test('@everyone bekommt die Grundrechte aus dem Bauplan', async () => {
|
|
const api = new FakeDiscord();
|
|
await run(api);
|
|
const everyone = api.guild.roles.find((r) => r.id === GUILD);
|
|
assert.equal(everyone?.permissions, str(EVERYONE));
|
|
});
|
|
|
|
test('Kanaele haengen unter der richtigen Kategorie', async () => {
|
|
const api = new FakeDiscord();
|
|
await run(api);
|
|
const kat = (key: string) =>
|
|
api.channels.find((c) => c.name === katName(key) && c.type === CH.CATEGORY)?.id;
|
|
for (const plan of CHANNELS) {
|
|
const c = live(api, plan.key);
|
|
assert.equal(c?.parent_id, kat(plan.category!), `${plan.name} haengt falsch`);
|
|
}
|
|
});
|
|
|
|
test('der interne Bereich ist fuer @everyone gesperrt und fuer Mod offen', async () => {
|
|
const api = new FakeDiscord();
|
|
await run(api);
|
|
const intern = api.channels.find((c) => c.name === katName('intern'));
|
|
const mod = api.guild.roles.find((r) => r.name === 'Mod');
|
|
const ow = intern?.permission_overwrites ?? [];
|
|
const jeder = ow.find((o) => o.id === GUILD);
|
|
assert.ok(jeder && (BigInt(jeder.deny) & 1024n) === 1024n, 'VIEW_CHANNEL muss fuer @everyone gesperrt sein');
|
|
const fuerMod = ow.find((o) => o.id === mod?.id);
|
|
assert.ok(fuerMod && (BigInt(fuerMod.allow) & 1024n) === 1024n, 'Mod muss hineinsehen koennen');
|
|
});
|
|
|
|
test('die Begruessung wird gepostet und die Kanalnamen sind aufgeloest', async () => {
|
|
const api = new FakeDiscord();
|
|
await run(api);
|
|
const willkommen = live(api, 'willkommen');
|
|
const posts = api.messages.get(willkommen!.id) ?? [];
|
|
assert.equal(posts.length, 1);
|
|
assert.match(posts[0]!.content, /<#\d+> - alles andere/, 'die Kanalnamen muessen aufgeloest sein');
|
|
});
|
|
|
|
// ---------------------------------------------------------------- Zweitlauf
|
|
|
|
test('der zweite Lauf aendert nichts', async () => {
|
|
const api = new FakeDiscord();
|
|
await run(api);
|
|
const nachErstlauf = {
|
|
rollen: api.guild.roles.length,
|
|
kanaele: api.channels.length,
|
|
webhooks: api.webhooks.length,
|
|
automod: api.automod.length,
|
|
};
|
|
|
|
api.calls = [];
|
|
const res = await run(api);
|
|
|
|
assert.deepEqual({
|
|
rollen: api.guild.roles.length,
|
|
kanaele: api.channels.length,
|
|
webhooks: api.webhooks.length,
|
|
automod: api.automod.length,
|
|
}, nachErstlauf, 'beim zweiten Lauf wurde etwas doppelt angelegt');
|
|
|
|
// Kein POST ausser den Lesevorgaengen - nichts Neues, nichts Geaendertes.
|
|
const schreibend = api.calls.filter((c) => c.startsWith('POST') || (c.startsWith('PATCH') && !c.endsWith('/roles')));
|
|
assert.deepEqual(schreibend.filter((c) => c !== `PATCH /guilds/${GUILD}`), [],
|
|
`unerwartete Schreibzugriffe: ${schreibend.join(', ')}`);
|
|
|
|
assert.equal(counts(res.steps)['erstellt'] ?? 0, 0);
|
|
assert.equal(res.webhooks, 4, 'die bestehenden Webhook-URLs muessen wiedergefunden werden');
|
|
});
|
|
|
|
test('die Begruessung wird nicht ein zweites Mal gepostet', async () => {
|
|
const api = new FakeDiscord();
|
|
await run(api);
|
|
await run(api);
|
|
const willkommen = live(api, 'willkommen');
|
|
assert.equal((api.messages.get(willkommen!.id) ?? []).length, 1);
|
|
});
|
|
|
|
// ---------------------------------------------------------------- Vorschau
|
|
|
|
test('die Vorschau schreibt nichts', async () => {
|
|
const api = new FakeDiscord();
|
|
const res = await run(api, true);
|
|
|
|
assert.equal(api.guild.roles.length, 2, 'nur @everyone und die Bot-Rolle, die es schon gab');
|
|
assert.equal(api.channels.length, 0);
|
|
assert.equal(api.webhooks.length, 0);
|
|
assert.equal(api.automod.length, 0);
|
|
assert.deepEqual(api.calls.filter((c) => !c.startsWith('GET')), []);
|
|
assert.ok((counts(res.steps)['erstellt'] ?? 0) > 10, 'die Vorschau muss zeigen, was entstehen wuerde');
|
|
});
|
|
|
|
// ---------------------------------------------------------------- Bestand
|
|
|
|
test('von Hand angelegte Kanaele und Rechte bleiben stehen', async () => {
|
|
const api = new FakeDiscord();
|
|
api.channels.push({
|
|
id: '900', name: 'memes', type: CH.TEXT, position: 0, parent_id: null,
|
|
permission_overwrites: [{ id: '777', type: 1, allow: '2048', deny: '0' }],
|
|
});
|
|
api.channels.push({
|
|
id: '901', name: 'allgemein', type: CH.TEXT, position: 1, parent_id: null,
|
|
permission_overwrites: [{ id: '777', type: 1, allow: '0', deny: '2048' }],
|
|
});
|
|
|
|
await run(api);
|
|
|
|
assert.ok(api.channels.find((c) => c.name === 'memes'), 'ein fremder Kanal darf nicht verschwinden');
|
|
const allgemein = api.channels.find((c) => c.id === '901');
|
|
assert.equal(live(api, 'allgemein')?.id, '901', 'der bestehende Kanal muss umbenannt werden');
|
|
assert.equal(api.channels.filter((c) => c.name.includes('allgemein')).length, 1, 'nicht doppelt anlegen');
|
|
const fremd = (allgemein?.permission_overwrites ?? []).find((o: ApiOverwrite) => o.id === '777');
|
|
assert.deepEqual(fremd, { id: '777', type: 1, allow: '0', deny: '2048' },
|
|
'eine von Hand gesetzte Ausnahme darf nicht wegfallen');
|
|
});
|
|
|
|
test('ein von Hand angelegter Webhook wird gemeldet, nicht ueberschrieben', async () => {
|
|
const api = new FakeDiscord();
|
|
await run(api); // legt alles an
|
|
// Jemand loescht den Go-Live-Webhook und baut ihn in der Oberflaeche nach -
|
|
// dann fehlt uns der Token, und das muss auffallen statt still zu scheitern.
|
|
const i = api.webhooks.findIndex((w) => w.name === 'Go-Live');
|
|
delete api.webhooks[i]!.token;
|
|
|
|
const res = await run(api);
|
|
const step = res.steps.find((s) => s.kind === 'Webhook' && s.name.startsWith('Go-Live'));
|
|
assert.equal(step?.change, 'uebersprungen');
|
|
assert.match(step?.note ?? '', /nicht auslesbar/);
|
|
assert.equal(res.webhooks, 3, 'die drei uebrigen muessen trotzdem durchkommen');
|
|
});
|
|
|
|
test('eine von Discord verwaltete Rolle wird in Ruhe gelassen', async () => {
|
|
const api = new FakeDiscord();
|
|
// Die Twitch-Anbindung legt eigene Rollen an. Die gehoeren Discord.
|
|
api.guild.roles.push({
|
|
id: '800', name: 'Stammgast', permissions: '0', position: 2,
|
|
color: 0, hoist: false, mentionable: false, managed: true,
|
|
});
|
|
|
|
const res = await run(api);
|
|
const step = res.steps.find((s) => s.kind === 'Rolle' && s.name === 'Stammgast');
|
|
assert.equal(step?.change, 'uebersprungen');
|
|
assert.equal(api.guild.roles.find((r) => r.id === '800')?.permissions, '0', 'unveraendert geblieben');
|
|
});
|
|
|
|
test('scheitert die Rangfolge, laeuft der Rest trotzdem durch', async () => {
|
|
const api = new FakeDiscord();
|
|
const echt = api.patch.bind(api);
|
|
api.patch = async <T,>(path: string, body: unknown): Promise<T> => {
|
|
if (path === `/guilds/${GUILD}/roles`) throw new Error('Missing Permissions');
|
|
return echt<T>(path, body);
|
|
};
|
|
|
|
const res = await run(api);
|
|
const step = res.steps.find((s) => s.name === 'Rangfolge');
|
|
assert.equal(step?.change, 'uebersprungen');
|
|
assert.equal(api.channels.length, 3 + CHANNELS.length, 'die Kanaele muessen trotzdem entstehen');
|
|
});
|
|
|
|
// ---------------------------------------------------------------- Rangfolge
|
|
|
|
test('die Rollen landen unter der Bot-Rolle, in der Reihenfolge des Bauplans', async () => {
|
|
const api = new FakeDiscord({ botRolePosition: 8 });
|
|
await run(api);
|
|
|
|
const pos = (name: string) => api.guild.roles.find((r) => r.name === name)?.position;
|
|
assert.deepEqual(ROLES.map((r) => pos(r.name)), [7, 6, 5, 4]);
|
|
for (const r of ROLES) {
|
|
assert.ok(pos(r.name)! < api.botRolePosition, `${r.name} steht ueber der Bot-Rolle`);
|
|
}
|
|
});
|
|
|
|
test('Bot-Rolle ganz unten: Rangfolge wird gemeldet statt zu scheitern', async () => {
|
|
// Der reale Fall direkt nach dem Einladen. Discord haengt die Bot-Rolle auf
|
|
// Platz 1, darunter ist kein Platz - frueher gab das ein Missing Permissions.
|
|
const api = new FakeDiscord({ botRolePosition: 1 });
|
|
const res = await run(api);
|
|
|
|
const step = res.steps.find((s) => s.name === 'Rangfolge');
|
|
assert.equal(step?.change, 'uebersprungen');
|
|
assert.match(step?.note ?? '', /nach oben ziehen/);
|
|
// Alles andere muss trotzdem stehen.
|
|
assert.equal(api.channels.length, 3 + CHANNELS.length);
|
|
assert.equal(res.webhooks, 4);
|
|
});
|
|
|
|
// ---------------------------------------------------------------- Umbenennen
|
|
|
|
test('ein Kanal unter altem Namen wird umbenannt, nicht verdoppelt', async () => {
|
|
const api = new FakeDiscord();
|
|
// So sah der Server nach dem ersten Lauf ohne Emoji aus.
|
|
const alt = new Map<string, string>();
|
|
CHANNELS.forEach((plan, i) => {
|
|
const id = String(900 + i); // der Fake erwartet Ziffern-IDs
|
|
alt.set(plan.key, id);
|
|
api.channels.push({
|
|
id, name: plan.aliases![0]!, type: plan.type,
|
|
position: 0, parent_id: null, permission_overwrites: [],
|
|
});
|
|
});
|
|
|
|
const res = await run(api);
|
|
|
|
for (const plan of CHANNELS) {
|
|
assert.equal(live(api, plan.key)?.id, alt.get(plan.key), `${plan.key} wurde neu angelegt statt umbenannt`);
|
|
}
|
|
assert.equal(api.channels.filter((c) => c.type !== CH.CATEGORY).length, CHANNELS.length,
|
|
'es darf kein Kanal dazugekommen sein');
|
|
assert.ok(res.steps.some((s) => s.name.includes('->')), 'die Umbenennung muss im Bericht stehen');
|
|
});
|
|
|
|
test('haengt Discord beim Umbenennen im Rate Limit, laeuft der Rest weiter', async () => {
|
|
const api = new FakeDiscord();
|
|
api.channels.push({
|
|
id: '901', name: 'allgemein', type: CH.TEXT, position: 0, parent_id: null,
|
|
permission_overwrites: [],
|
|
});
|
|
const echt = api.patch.bind(api);
|
|
api.patch = async <T,>(path: string, body: unknown): Promise<T> => {
|
|
if (path === '/channels/901') throw new Error('Discord verlangt 530 s Pause');
|
|
return echt<T>(path, body);
|
|
};
|
|
|
|
const res = await run(api);
|
|
|
|
const step = res.steps.find((s) => s.name.includes('allgemein'));
|
|
assert.equal(step?.change, 'uebersprungen');
|
|
assert.match(step?.note ?? '', /530 s/);
|
|
// Die uebrigen Kanaele und die Webhooks muessen trotzdem entstehen.
|
|
assert.equal(res.webhooks, 4);
|
|
assert.equal(api.automod.length, 2);
|
|
});
|
|
|
|
test('der zweite Lauf schreibt nichts mehr - auch nicht Einstellungen und Rangfolge', async () => {
|
|
// Der Bauplan verspricht, dass ein zweiter Lauf nichts aendert. Zwei
|
|
// Schritte haben das lange gebrochen, weil sie ohne Vergleich geschrieben
|
|
// haben: die Servereinstellungen und die Rangfolge der Rollen. Folgenlos
|
|
// war das nicht - jeder Lauf hinterliess zwei Eintraege im Audit-Log und
|
|
// liess den Bericht so aussehen, als sei etwas passiert.
|
|
const api = new FakeDiscord();
|
|
await run(api);
|
|
|
|
api.calls.length = 0;
|
|
const res = await run(api);
|
|
|
|
assert.equal(api.calls.some((c) => c === `PATCH /guilds/${GUILD}`), false,
|
|
'Servereinstellungen standen schon');
|
|
assert.equal(api.calls.some((c) => c === `PATCH /guilds/${GUILD}/roles`), false,
|
|
'die Rangfolge stand schon');
|
|
assert.equal(res.steps.find((s) => s.kind === 'Server')?.change, 'unveraendert');
|
|
assert.equal(res.steps.find((s) => s.name === 'Rangfolge')?.change, 'unveraendert');
|
|
assert.equal(res.steps.some((s) => s.change === 'erstellt'), false, 'nichts neu angelegt');
|
|
});
|
|
|
|
test('ein geaenderter Blocktext wird auf eine bestehende Regel nachgezogen', async () => {
|
|
const api = new FakeDiscord();
|
|
await run(api);
|
|
const regel = api.automod[0]!;
|
|
(regel.actions as Array<{ type: number; metadata: { custom_message: string } }>)[0]!
|
|
.metadata.custom_message = 'alter Text';
|
|
|
|
const res = await run(api);
|
|
|
|
const step = res.steps.find((s) => s.kind === 'AutoMod' && s.name === regel.name);
|
|
assert.equal(step?.change, 'angepasst');
|
|
assert.match(step?.note ?? '', /actions/);
|
|
assert.equal(api.automod.length, 2, 'die Regel darf nicht neu angelegt werden');
|
|
});
|
|
|
|
test('der Meldekanal einer bestehenden Regel bleibt, wie er gesetzt wurde', async () => {
|
|
const api = new FakeDiscord();
|
|
await run(api);
|
|
const regel = api.automod[0]!;
|
|
const actions = regel.actions as Array<{ type: number; metadata: Record<string, string> }>;
|
|
actions[0]!.metadata['custom_message'] = 'alter Text';
|
|
actions[1]!.metadata['channel_id'] = '424242'; // jemand hat umgestellt
|
|
|
|
await run(api);
|
|
|
|
const danach = (api.automod[0]!.actions as typeof actions).find((a) => a.type === 2);
|
|
assert.equal(danach?.metadata['channel_id'], '424242', 'die eigene Wahl darf nicht ueberschrieben werden');
|
|
});
|
|
|
|
test('der Willkommenstext bekommt die Knopfleiste', async () => {
|
|
const api = new FakeDiscord();
|
|
await run(api);
|
|
const post = api.messages.get(live(api, 'willkommen')!.id)![0]!;
|
|
const reihen = post.components as Array<{ type: number; components: Array<{ custom_id: string }> }>;
|
|
assert.equal(reihen.length, 1);
|
|
assert.equal(reihen[0]!.components[0]!.custom_id, 'rolle:live-ping');
|
|
});
|
|
|
|
test('eine Nachricht ohne Knopfleiste bekommt sie nachgereicht', async () => {
|
|
// So sieht der Willkommenstext aus, der vor den Knoepfen gepostet wurde.
|
|
const api = new FakeDiscord();
|
|
await run(api);
|
|
const kanal = live(api, 'willkommen')!.id;
|
|
api.messages.get(kanal)![0]!.components = [];
|
|
|
|
const res = await run(api);
|
|
|
|
const step = res.steps.find((s) => s.name === 'Willkommenstext');
|
|
assert.equal(step?.change, 'angepasst');
|
|
assert.equal((api.messages.get(kanal)![0]!.components as unknown[]).length, 1);
|
|
assert.equal(api.messages.get(kanal)!.length, 1, 'nicht neu posten');
|
|
});
|