Teilen: der Beitrag stellt sich selbst in den Agentur-Kanal

Filipe: "ich will dass wenn ich es teile im discord der agentur, dass
ein banner und ein kompletter fertiger text fuer die creator entsteht
und nicht einfach der link kopiert wird." Den fertigen Text gibt es
seit fdadd5d5; was fehlte, war der letzte Schritt -- bisher musste er
ihn selbst einfuegen.

NEU AN DER KACHEL: ein Knopf "In den Agentur-Kanal stellen", sobald ein
Kanal eingerichtet ist. Der Browser schickt dabei KEINEN Text mit --
der Server stellt ihn aus denselben Daten noch einmal zusammen. Sonst
koennte jemand mit einem veraenderten Text unter unserem Namen posten.

EINGERICHTET WIRD IM WORKSPACE, NICHT AUF DER KOMMANDOZEILE. Die
Webhook-Adresse IST der Schluessel zum Kanal; ueber einen Befehl
eingetragen landet sie im Chatverlauf, im Bildschirmfoto und in
~/.bash_history -- am 03.09.2026 ist genau das mit einem Token
passiert. Gemessen am 08.10.2026 kann `claudian` die Datei ohnehin
nicht anlegen: /home/dogiweb/workspace-daten ist drwxr-xr-x
dogiweb:dogiweb, und `sudo -l` deckt `dogiweb` nicht ab. Der Dienst
kann es. Damit braucht der Weg keine Rechteaenderung, kein root und
keinen Neustart.

DAS GEHEIMNIS liegt in /home/dogiweb/workspace-daten/discord-webhook.txt
mit 600, wird bei JEDEM Aufruf gelesen (ein Austausch wirkt sofort) und
kommt nie wieder heraus: GET beantwortet nur "ist eingerichtet" und
"seit wann", das Protokoll nennt sie nicht, und die Teilen-Antwort
auch nicht.

DIE SCHRANKEN, jede einzeln gemessen:
  - nur discord.com/discordapp.com/ptb./canary., nur https, nur Wege
    unter /api/webhooks/ -- sonst waere das Feld ein Weg, den Server zu
    beliebigen Zielen sprechen zu lassen
  - nur die Leitung, nur die Agentur-Adresse; von crew.* aus 404 statt
    403, damit man nicht erahnen kann, was es woanders gibt
  - allowed_mentions leer: im Beitrag steht TikToks Text, und ein
    "@everyone" darin wuerde sonst den ganzen Server anpingen
  - keine flags: die Vorschau bleibt an, denn genau sie ist das Banner
  - ohne Teilen-Link keine Nachricht
  - zwoelf je Stunde und Person, gezaehlt im Protokoll (Erfolge UND
    Fehlschlaege -- sonst bremst sie den nicht, der in einer Schleife
    haengt; derselbe Fehler wie am 07.10. an der Kampagnen-Bremse)

SIEBEN NEUE KENNUNGEN, sieben Saetze in meldung.js -- verlangt hat sie
`pruef-meldungen`, nicht mein Gedaechtnis. Jeder Satz sagt auch, was
stattdessen geht: Der Beitrag liegt in jedem Fall zum Kopieren da.

ZWEI FEHLER FANDEN NUR DIE PRUEFUNGEN:
  - teilenRouter.use("/workspace/api/eintrag", angemeldet) deckte die
    neuen Kanal-Wege nicht ab. Ohne req.person ist istLeitung falsch,
    also antworteten sie JEDEM mit 403 "nicht_erlaubt", auch
    DogFather. Es ist ZU gescheitert, nicht auf -- aber die Auskunft
    schickte in die falsche Richtung.
  - Das Datum im Dateikopf kam aus UTC (pruef-struktur): zwischen 00:00
    und 02:00 stuende dort der Vortag, in einer Zeile, die spaeter
    jemand liest, um zu wissen, seit wann der Kanal gilt.

Geprueft: pruef-kampagne 190 -> 226 (Abschnitte 12c und 12d), mit einer
ehrlichen Luecke -- die Dateirechte lassen sich auf Windows nicht
messen, auf dem Server schon; die Schlusszeile sagt jetzt "IN ORDNUNG,
mit Luecke" statt "ALLES IN ORDNUNG". bild-kampagne 53 -> 83
(Kanalfeld vermessen und abgebildet, Eingabefeld 45/46 px auf beiden
Breiten). Dazu gruen: meldungen 10, struktur, css-klassen, zeichen,
deutsche-texte, alle-wege, schranke, ports, portnummern, tippziele,
lesbarkeit, agentur 67, eventkarte 190, workspace-seiten 37,
haus-trennung 107, formulare 23, fingermass.

SECHS GEGENPROBEN am Weg in den Kanal, alle sechs schlagen an:
Rechnerliste uebergangen, Erwaehnungen wieder erlaubt, Vorschau
unterdrueckt, Bremse aus, ohne Teilen-Link gesendet, Adresse in die
Antwort gelegt.

NEBENBEI, UND ES WAR MEIN EIGENER REST: pruef-kampagne-lesen war seit
698c23e2 rot, also seit heute Vormittag. Mit dem Bildleser kamen drei
Pruefdaten in den Ordner, und die Wache dort verlangt zu Recht, dass
nichts Unerklaertes liegenbleibt. Ich hatte sie VOR dem Commit
gemessen und danach nicht noch einmal -- ein Pruefstand von vorher ist
keiner. Die drei sind jetzt benannt, mit Grund, und eine Gegenwache
prueft, dass es jede noch gibt. 186 -> 187, zwei Gegenproben: eine
unbekannte Datei faellt auf, ein toter Eintrag in der Liste auch.
This commit is contained in:
2026-10-08 22:00:04 +02:00
parent e93c4067f9
commit 964590ecbb
55 changed files with 1753 additions and 715 deletions
+360 -3
View File
@@ -35,7 +35,7 @@
Aufrufen mit: node server/pruef-kampagne.mjs
===================================================================== */
import { mkdtempSync, rmSync, readFileSync, existsSync } from "node:fs";
import { mkdtempSync, rmSync, readFileSync, writeFileSync, existsSync, statSync } from "node:fs";
import { brotliDecompressSync } from "node:zlib";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
@@ -57,6 +57,13 @@ const TIKPORT = await eigenerPort(import.meta, "der nachgebaute TikTok-Dienst",
und `bildAdresseErlaubt` ebenfalls. Beides nur hier — im Betrieb
steht die Variable nicht, und dann gilt die echte Regel. */
process.env.TIKTOK_KAMPAGNE_BASIS = "http://127.0.0.1:" + TIKPORT;
/* DERSELBE SCHALTER FÜR DEN KANAL (08.10.2026). Er nennt die Basis,
mit der eine Webhook-Adresse anfangen muss, damit 127.0.0.1
erlaubt ist. Im Betrieb steht die Variable nicht — dann gilt die
echte Regel (https und ein Discord-Rechner). Ohne diesen Weg wäre
das Stellen in den Kanal nur am ECHTEN Discord prüfbar, und das
heißt: in Filipes Kanal. */
process.env.DISCORD_WEBHOOK_BASIS = "http://127.0.0.1:" + TIKPORT;
process.env.PORT = String(PORT);
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
@@ -76,6 +83,20 @@ let geprueft = 0;
const melde = (z) => console.log(z);
const ok = (b, t) => { geprueft++; melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
/* DER DRITTE AUSGANG JE FRAGE -- nicht in Ordnung, aber auch nicht
bestanden. `nichtNachsehbar` weiter unten bricht den GANZEN Lauf
ab; das ist für eine fehlende Voraussetzung richtig, nicht für
eine einzelne Frage, die auf diesem Betriebssystem keine Antwort
hat. Hier „ok" zu melden wäre eine Zusicherung über etwas, das
nicht gemessen wurde -- genau die Sorte grüner Haken, die nichts
bestätigt (01.09.2026). */
let offen = 0;
const unklar = (was, wie) => {
offen++;
melde(` -- NICHT NACHSEHBAR: ${was}`);
melde(` ${wie}`);
};
function nichtNachsehbar(grund, rat) {
melde("");
melde("KONNTE NICHT NACHSEHEN");
@@ -208,12 +229,35 @@ const BANNERBILD = pngBauen(750, 500, (x, y, b, h) => {
let abrufe = 0;
const gesehen = [];
/* ---- DER NACHGEBAUTE DISCORD-KANAL (08.10.2026) ----------------
Was er bekommt, landet in `kanalPost`. `kanalAntwort` lässt sich
umstellen, damit auch die beiden Absagen geprüft werden können:
4xx heißt „der Zugang ist hin", 5xx heißt „später nochmal" --
zwei verschiedene Sätze, also zwei Messungen. */
const kanalPost = [];
let kanalAntwort = 204;
const tikDienst = createServer((anfrage, antwort) => {
abrufe++;
const u = new URL(anfrage.url, "http://127.0.0.1");
gesehen.push({ pfad: u.pathname, kennung: anfrage.headers["user-agent"] || "" });
const leite = (nach) => { antwort.writeHead(302, { location: nach }); antwort.end(); };
if (u.pathname.startsWith("/api/webhooks/")) {
const stuecke = [];
anfrage.on("data", (s) => stuecke.push(s));
anfrage.on("end", () => {
let rumpf = null;
try { rumpf = JSON.parse(Buffer.concat(stuecke).toString("utf8")); }
catch { rumpf = null; }
kanalPost.push({ pfad: u.pathname, methode: anfrage.method, rumpf });
antwort.writeHead(kanalAntwort, { "content-type": "application/json" });
antwort.end(kanalAntwort < 300 ? "" : JSON.stringify({ message: "nein" }));
});
return undefined;
}
if (u.pathname === "/t/gipfel") return leite(`/falcon/home?activityId=${ID_GIPFEL}&campaign_traffic_id=1`);
if (u.pathname === "/t/gipfel-zweiter-versand") return leite(`/falcon/home?activityId=${ID_GIPFEL}&campaign_traffic_id=2`);
if (u.pathname === "/t/beendet") return leite(`/falcon/home?activityId=${ID_BEENDET}`);
@@ -395,6 +439,17 @@ const dbLesen = (sql, ...w) => {
try { return x.prepare(sql).all(...w); } finally { x.close(); }
};
/* ZUM SCHREIBEN DASSELBE MUSTER. `d` ist ab Zeile 375 geschlossen;
wer es spaeter benutzt, bekommt „database is not open" -- genau
das ist beim ersten Lauf von Abschnitt 12c passiert. Eine eigene
Verbindung je Aufruf ist hier richtig: Der Server haelt seine
eigene, und zwei offene auf derselben Datei sind in SQLite
genau dann ein Problem, wenn eine davon vergessen wird. */
const dbSchreiben = (sql, ...w) => {
const x = new DatabaseSync(process.env.WORKSPACE_DB);
try { return x.prepare(sql).run(...w); } finally { x.close(); }
};
/* =====================================================================
1. DEN LINK AUS DEM GANZEN TEXT HOLEN
===================================================================== */
@@ -1435,6 +1490,302 @@ melde("=== 12b. Ein Event nach draußen teilen ===");
13. DAS PROTOKOLL
===================================================================== */
melde("");
melde("=== 12c. Der Beitrag stellt sich selbst in den Kanal ===");
{
/* Filipe: „ich will dass wenn ich es teile im discord der agentur,
dass ein banner und ein kompletter fertiger text für die creator
entsteht und nicht einfach der link kopiert wird.“
DAS GEHEIMNIS LIEGT IN EINER DATEI, nicht im Quelltext und nicht
in der Datenbank. Hier wird sie in den Wegwerf-Datenordner
geschrieben — dieselbe Stelle, an der im Betrieb die echte
liegt (`DATEN_ORDNER` ist der Ordner der Datenbank). */
const WEBHOOK = join(ordner, "discord-webhook.txt");
const legeAn = (inhalt) => writeFileSync(WEBHOOK, inhalt, "utf8");
const nimmWeg = () => { try { rmSync(WEBHOOK); } catch { /* war nicht da */ } };
const stellen = (keks, id = kachelId) =>
roh(`/workspace/api/eintrag/${id}/discord`, { methode: "POST", keks });
/* DIESER ABSCHNITT STELLT SEINEN ZUSTAND SELBST HER. Abschnitt 12b
zieht den Teilen-Link am Ende zurück; wer sich darauf verlässt,
dass er noch da ist, misst etwas anderes als gedacht — beim
ersten Lauf kam `nicht_geteilt`, wo `discord_nicht_eingerichtet`
stehen sollte. Ein Prüfabschnitt, der auf dem Zustand des
vorigen aufbaut, ist von dessen Reihenfolge abhängig. */
const wiederGeteilt = await roh(`/workspace/api/eintrag/${kachelId}/teilen`,
{ methode: "POST", keks: keksDogi, rumpf: {} });
ok(wiederGeteilt.code === 200
&& /\/e\/[A-Za-z0-9_-]{20,}$/.test(JSON.parse(wiederGeteilt.text || "{}").weg || ""),
`das Event ist für diesen Abschnitt geteilt (${wiederGeteilt.code})`);
/* ---- 1. Ohne Datei gibt es keinen Knopf und keine Absage ohne Satz */
nimmWeg();
const ohne = await stellen(keksDogi);
const jOhne = JSON.parse(ohne.text || "{}");
ok(ohne.code === 409 && jOhne.fehler === "discord_nicht_eingerichtet",
`ohne eingerichteten Kanal: ${ohne.code} ${jOhne.fehler}`);
/* Und die Oberfläche erfährt das, BEVOR sie einen Knopf zeigt. */
const tOhne = await roh(`/workspace/api/eintrag/${kachelId}/teilen`,
{ methode: "POST", keks: keksDogi, rumpf: {} });
ok(JSON.parse(tOhne.text || "{}").discord === false,
" und die Teilen-Antwort sagt `discord: false`");
/* ---- 2. Eine FREMDE Adresse gilt nicht --------------------------
Ohne diese Messung wäre das Feld ein Weg, unseren Server zu
beliebigen Zielen sprechen zu lassen. Drei Formen werden
abgelehnt, und zwar jede aus einem eigenen Grund. */
const abgelehnt = [
["https://beispiel.test/api/webhooks/1/x", "fremder Rechner"],
["http://discord.com/api/webhooks/1/x", "ohne Verschlüsselung"],
["https://discord.com/irgendwas/1/x", "kein Webhook-Weg"],
[`http://127.0.0.1:${TIKPORT + 1}/api/webhooks/1/x`, "andere Basis als der Schalter"],
];
let fremdeAbgelehnt = 0;
for (const [adresse, warum] of abgelehnt) {
legeAn(adresse);
const r = await stellen(keksDogi);
const j = JSON.parse(r.text || "{}");
const gut = r.code === 409 && j.fehler === "discord_nicht_eingerichtet";
if (gut) fremdeAbgelehnt++;
else melde(` -- DURCHGELASSEN: ${warum} -> ${r.code} ${j.fehler}`);
}
ok(fremdeAbgelehnt === abgelehnt.length,
`alle ${abgelehnt.length} fremden Adressen werden abgelehnt (${fremdeAbgelehnt})`);
/* ---- 3. Mit dem nachgebauten Kanal ------------------------------ */
legeAn([
"# Der Kanal der Agentur. Diese Zeile ist ein Kommentar.",
`http://127.0.0.1:${TIKPORT}/api/webhooks/42/geheim`,
].join("\n"));
const tMit = await roh(`/workspace/api/eintrag/${kachelId}/teilen`,
{ methode: "POST", keks: keksDogi, rumpf: {} });
const jMit = JSON.parse(tMit.text || "{}");
ok(jMit.discord === true, "mit Datei sagt die Teilen-Antwort `discord: true`");
/* UND DIE ADRESSE SELBST STEHT NICHT DRIN. Eine Auskunft über das
Vorhandensein ist etwas anderes als der Schlüssel. */
ok(!/webhooks|geheim|127\.0\.0\.1/.test(tMit.text || ""),
" und die Adresse steht nicht in der Antwort");
const vorZahl = kanalPost.length;
const r1 = await stellen(keksDogi);
const j1d = JSON.parse(r1.text || "{}");
ok(r1.code === 200 && j1d.ok === true,
`DogFather stellt den Beitrag: ${r1.code} (${j1d.zeichen} Zeichen)`);
ok(kanalPost.length === vorZahl + 1,
` genau ein Beitrag kommt im Kanal an (${kanalPost.length - vorZahl})`);
const post = kanalPost[kanalPost.length - 1];
const inhalt = String(post?.rumpf?.content || "");
ok(post?.methode === "POST" && post?.pfad === "/api/webhooks/42/geheim",
` an den richtigen Weg (${post?.methode} ${post?.pfad})`);
ok(inhalt.length > 200 && inhalt.length <= 2000,
` mit dem ganzen Text (${inhalt.length} Zeichen, Grenze 2000)`);
ok(/^#\s/m.test(inhalt) && inhalt.includes("Gipfel"),
" Titel als Überschrift");
/* DER LINK STEHT GANZ AM ENDE UND NACKT. Discord setzt die Vorschau
UNTER die Nachricht — steht der Link oben, schiebt das Banner den
Text aus dem Blick. Und in spitzen Klammern wäre die Vorschau
unterdrückt, also genau das Banner weg. */
const letzte = inhalt.trimEnd().split("\n").pop().trim();
ok(/^https?:\/\/\S+$/.test(letzte) && letzte.includes("/e/"),
` und der Link nackt in der letzten Zeile (${letzte.replace(/^https?:\/\/[^/]+/, "…")})`);
/* ---- 4. Ein fremder Text darf niemanden anpingen ---------------
Im Beitrag steht, was TikTok geschrieben hat. Stünde darin
„@everyone", würde unser Beitrag den ganzen Server anpingen. */
ok(Array.isArray(post?.rumpf?.allowed_mentions?.parse)
&& post.rumpf.allowed_mentions.parse.length === 0,
" Erwähnungen sind abgeschaltet (allowed_mentions leer)");
ok(post?.rumpf?.flags === undefined,
" und die Vorschau ist NICHT unterdrückt (keine flags)");
/* ---- 5. Wer darf das? ------------------------------------------ */
const alsCreator = await stellen(keksLuna);
ok(alsCreator.code === 403 || alsCreator.code === 404,
`ein Creator darf nicht in den Kanal stellen (${alsCreator.code})`);
/* ---- 6. Ein ungeteiltes Event hat keinen Link ------------------- */
const frisch = dbSchreiben(
`INSERT INTO eintraege (bereich, haus, art, titel, text, datum, erstellt_von, erstellt)
VALUES ('agentur','agentur','kampagne','Noch nicht geteilt','x',?,?,?)`,
heuteLokal(), 1, new Date().toISOString());
const nurFrisch = await stellen(keksDogi, Number(frisch.lastInsertRowid));
ok(nurFrisch.code === 409
&& JSON.parse(nurFrisch.text || "{}").fehler === "nicht_geteilt",
`ohne Teilen-Link keine Nachricht (${nurFrisch.code})`);
/* ---- 7. Die beiden Absagen von Discord -------------------------
4xx und 5xx bekommen verschiedene Sätze, weil sie verschiedene
Dinge bedeuten: „der Zugang ist hin" gegen „später nochmal".
Ohne diese Messung wäre nicht belegt, dass die Unterscheidung
überhaupt ankommt. */
kanalAntwort = 401;
const vier = await stellen(keksDogi);
ok(vier.code === 502 && JSON.parse(vier.text || "{}").fehler === "discord_abgelehnt",
`Discord sagt 401 -> discord_abgelehnt (${vier.code})`);
kanalAntwort = 500;
const fuenf = await stellen(keksDogi);
ok(fuenf.code === 502
&& JSON.parse(fuenf.text || "{}").fehler === "discord_nicht_erreichbar",
`Discord sagt 500 -> discord_nicht_erreichbar (${fuenf.code})`);
kanalAntwort = 204;
/* ---- 8. Jeder Versuch steht im Protokoll ------------------------ */
const prot = dbLesen(
`SELECT aktion, COUNT(*) AS n FROM protokoll
WHERE aktion LIKE 'event_%gesendet' GROUP BY aktion`);
const karte = new Map(prot.map((z) => [z.aktion, z.n]));
ok((karte.get("event_gesendet") || 0) >= 1
&& (karte.get("event_nicht_gesendet") || 0) >= 2,
`Erfolg und Fehlschlag stehen beide im Protokoll `
+ `(gesendet ${karte.get("event_gesendet") || 0}, `
+ `nicht gesendet ${karte.get("event_nicht_gesendet") || 0})`);
/* ---- 9. Die Bremse --------------------------------------------- */
let gebremst = null;
for (let i = 0; i < 16 && !gebremst; i++) {
const r = await stellen(keksDogi);
if (r.code === 429) gebremst = i + 1;
}
ok(gebremst !== null && gebremst <= 16,
`nach ${gebremst} weiteren Versuchen bremst sie (Grenze 12 je Stunde)`);
const letzteAbsage = await stellen(keksDogi);
ok(JSON.parse(letzteAbsage.text || "{}").fehler === "senden_gebremst",
" und sagt es mit einer Kennung, zu der es einen Satz gibt");
nimmWeg();
}
melde("=== 12d. Den Kanal eintragen -- ohne Kommandozeile ===");
{
/* WARUM ES DIESEN WEG GIBT. Die Webhook-Adresse ist der Schlüssel
zum Kanal. Über einen Befehl eingetragen landet sie im
Chatverlauf, im Bildschirmfoto und in `~/.bash_history` -- am
03.09.2026 ist genau das mit einem Token passiert, und er musste
zurückgezogen werden. Gemessen am 08.10.2026 kann `claudian` die
Datei auch gar nicht anlegen (der Ordner gehört `dogiweb`, die
Gruppe hat kein Schreibrecht, und `sudo` deckt `dogiweb` nicht
ab) -- der Dienst aber schon.
GEPRÜFT WIRD DESHALB NICHT NUR „es geht", sondern vor allem:
Wer darf es, und kommt die Adresse irgendwo wieder heraus. */
const WEG = "/workspace/api/teilen/kanal";
const ADRESSE = `http://127.0.0.1:${TIKPORT}/api/webhooks/77/einGeheimnis`;
const kanalDatei = join(ordner, "discord-webhook.txt");
try { rmSync(kanalDatei); } catch { /* war nicht da */ }
/* ---- 1. Wer darf? ---------------------------------------------- */
const lunaSieht = await roh(WEG, { keks: keksLuna });
ok(lunaSieht.code === 403,
`ein Creator sieht den Kanal nicht (${lunaSieht.code})`);
const lunaSetzt = await roh(WEG,
{ methode: "PUT", keks: keksLuna, rumpf: { adresse: ADRESSE } });
ok(lunaSetzt.code === 403,
` und darf ihn nicht eintragen (${lunaSetzt.code})`);
/* ZWEI GETRENNTE HÄUSER (24.09.2026). Auf der Crew-Adresse gibt es
den Agentur-Kanal nicht -- auch nicht für DogFather. 404, nicht
403: Was es hier nicht gibt, soll man nicht erahnen können. */
const crewSieht = await roh(WEG, { keks: keksDogiCrew, wirt: CREW_HOST });
ok(crewSieht.code === 404
&& JSON.parse(crewSieht.text || "{}").fehler === "nicht_hier",
`auf der Crew-Adresse gibt es ihn nicht (${crewSieht.code})`);
const crewSetzt = await roh(WEG, { methode: "PUT", keks: keksDogiCrew,
wirt: CREW_HOST, rumpf: { adresse: ADRESSE } });
ok(crewSetzt.code === 404 && !existsSync(kanalDatei),
` und von dort wird keine Datei angelegt (${crewSetzt.code})`);
/* ---- 2. Untaugliche Adressen kommen nicht hinein ---------------- */
const schlecht = [
["", "leer"],
["kein-link", "gar keine Adresse"],
["https://beispiel.test/api/webhooks/1/x", "fremder Rechner"],
["https://discord.com/kanal/1/x", "kein Webhook-Weg"],
["javascript:alert(1)", "anderes Verfahren"],
];
let abgewiesen = 0;
for (const [adresse, warum] of schlecht) {
const r = await roh(WEG, { methode: "PUT", keks: keksDogi, rumpf: { adresse } });
const j = JSON.parse(r.text || "{}");
if (r.code === 400 && j.fehler === "adresse_untauglich") abgewiesen++;
else melde(` -- ANGENOMMEN: ${warum} -> ${r.code} ${j.fehler}`);
}
ok(abgewiesen === schlecht.length && !existsSync(kanalDatei),
`alle ${schlecht.length} untauglichen Adressen werden abgewiesen, `
+ `ohne eine Datei anzulegen (${abgewiesen})`);
/* ---- 3. DogFather trägt ihn ein -------------------------------- */
const setzt = await roh(WEG,
{ methode: "PUT", keks: keksDogi, rumpf: { adresse: ADRESSE } });
ok(setzt.code === 200 && JSON.parse(setzt.text || "{}").bereit === true,
`DogFather trägt den Kanal ein (${setzt.code})`);
ok(existsSync(kanalDatei), " die Datei liegt im Datenordner");
/* ERST NACHSEHEN, OB SIE DA IST. Beim ersten Lauf schlug der
Eintrag fehl, und dieses `readFileSync` riss mit ENOENT den
ganzen Lauf ab -- aus einem roten Haken wurde ein Absturz, und
die zehn Fragen danach liefen nie. Eine Prüfung darf am
Fehlschlag einer anderen nicht sterben. */
ok(existsSync(kanalDatei)
&& readFileSync(kanalDatei, "utf8").includes(ADRESSE),
" und enthält die Adresse");
/* ---- 4. UND SIE KOMMT NIE WIEDER HERAUS ------------------------
Das ist die Messung, auf die es ankommt. Ein Feld, das sein
Geheimnis zurückliest, ist eine Kopie mehr -- und jede Kopie ist
eine Stelle, an der es wegkommen kann. */
const liest = await roh(WEG, { keks: keksDogi });
const jLiest = JSON.parse(liest.text || "{}");
ok(jLiest.bereit === true && jLiest.steht === true && !!jLiest.seit,
`die Auskunft sagt bereit/seit (${jLiest.seit})`);
ok(!/webhooks|einGeheimnis|127\.0\.0\.1/.test(liest.text || ""),
" aber die Adresse steht NICHT in der Antwort");
/* ---- 5. Die Rechte der Datei ----------------------------------- */
if (process.platform === "win32") {
/* DER DRITTE AUSGANG. Windows kennt keine Unix-Rechte; `chmod`
schaltet dort nur das Schreibschutz-Bit. Hier „in Ordnung" zu
melden wäre eine Zusicherung über etwas, das nicht gemessen
wurde -- und genau diese Prüfung soll auf dem Server laufen,
wo sie etwas bedeutet. */
unklar("die Rechte der Kanaldatei",
"Windows kennt keine Unix-Rechte. Auf dem Server nachsehen: "
+ "ls -l /home/dogiweb/workspace-daten/discord-webhook.txt "
+ "muss -rw------- und dogiweb zeigen.");
} else {
const m = statSync(kanalDatei).mode & 0o777;
ok(m === 0o600, `nur der Dienst darf sie lesen (${m.toString(8)})`);
}
/* ---- 6. Und jetzt geht der Beitrag --------------------------- */
const tNun = await roh(`/workspace/api/eintrag/${kachelId}/teilen`,
{ methode: "POST", keks: keksDogi, rumpf: {} });
ok(JSON.parse(tNun.text || "{}").discord === true,
"nach dem Eintragen sagt die Teilen-Antwort `discord: true`");
/* ---- 7. Entfernen wirkt sofort ---------------------------------- */
const entfernt = await roh(WEG, { methode: "DELETE", keks: keksDogi });
ok(entfernt.code === 200
&& JSON.parse(entfernt.text || "{}").bereit === false
&& !existsSync(kanalDatei),
`Entfernen wirkt sofort und räumt die Datei weg (${entfernt.code})`);
const tWeg = await roh(`/workspace/api/eintrag/${kachelId}/teilen`,
{ methode: "POST", keks: keksDogi, rumpf: {} });
ok(JSON.parse(tWeg.text || "{}").discord === false,
" und der Knopf verschwindet wieder");
/* ---- 8. Im Protokoll steht die Adresse nicht ------------------- */
const prot = dbLesen(
"SELECT aktion, detail FROM protokoll WHERE aktion LIKE 'kanal_%'");
ok(prot.length >= 2,
`Eintragen und Entfernen stehen im Protokoll (${prot.length} Zeilen)`);
ok(!prot.some((z) => /webhooks|einGeheimnis|127\.0\.0\.1/.test(z.detail || "")),
" und die Adresse steht in keinem Protokolleintrag");
}
melde("=== 13. Jeder Versuch steht im Protokoll, nicht nur der Erfolg ===");
{
@@ -1502,8 +1853,14 @@ melde("=== 14. Zwanzig in einer Stunde, dann ist Schluss ===");
===================================================================== */
melde("");
melde(`${abrufe} Abrufe beim nachgebauten Dienst, keiner im echten Netz.`);
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`
+ (offen ? `, ${offen} nicht nachsehbar` : ""));
/* „ALLES IN ORDNUNG" DARF NICHT DASTEHEN, WENN EINE FRAGE OFFEN IST.
Sonst verschwindet die ehrliche Lücke hinter der Überschrift, und
niemand sieht mehr, dass eine Zusicherung auf diesem Rechner gar
nicht geprüft werden konnte (dieselbe Form wie in
pruef-kampagne-bild.mjs). */
melde(fehler ? "NICHT IN ORDNUNG" : (offen ? "IN ORDNUNG, mit Luecke" : "ALLES IN ORDNUNG"));
tikDienst.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Rest */ }