/* ===================================================================== test-suche.mjs — die Suche über alles Der heikelste Teil sind die Sonderzeichen von LIKE. Ein Prozentzeichen im Suchbegriff wäre ohne Behandlung ein Platzhalter, ein Unterstrich ebenfalls -- und Unterstriche stehen regelmässig in E-Mail-Adressen. Der Fehler ist tückisch, weil die Suche trotzdem Treffer liefert, nur die falschen. Man merkt es nie. ===================================================================== */ import { mkdtempSync, rmSync, writeSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { randomBytes, createHmac } from "node:crypto"; const L = (...a) => writeSync(1, a.join(" ") + "\n"); const tmp = mkdtempSync(join(tmpdir(), "wd-suche-")); process.env.DB_PATH = join(tmp, "t.db"); process.env.ENCRYPTION_KEY = randomBytes(32).toString("base64"); process.env.WEBDESIGN_API_SECRET = randomBytes(24).toString("hex"); const { initDb, db } = await import("./db.js"); initDb(); const geheim = await import("./lib/webdesign-geheimnisse.js"); await geheim.geheimnisseLaden(); const { suche } = await import("./routes/webdesign-suche.js"); let ok = 0, fehler = 0; const pruefe = (n, b, z) => b ? (ok++, L(" ok " + n)) : (fehler++, L(" FEHL " + n + (z !== undefined ? " -> " + z : ""))); function ausweis(gueltigMs = 60_000) { const n = Buffer.from(JSON.stringify({ bereich: "wd-admin", rolle: "dogfather", exp: Date.now() + gueltigMs, })).toString("base64url"); return n + "." + createHmac("sha256", process.env.WEBDESIGN_API_SECRET).update(n).digest("base64url"); } async function such(q, kopf) { let out = null; await suche( { headers: kopf || { authorization: "Bearer " + ausweis() }, cookies: {}, query: { q }, params: {} }, { status() { return this; }, json(o) { out = o; return this; }, setHeader() {}, end() {} } ); return out; } const jetzt = new Date().toISOString(); db.prepare(`INSERT INTO wd_kunden (id,email,name,firma,erstellt_am) VALUES ('k1','anna@example.com','Anna Muster','Musterbau GmbH',?)`).run(jetzt); db.prepare(`INSERT INTO wd_kunden (id,email,name,erstellt_am) VALUES ('k2','bert_meier@example.com','Bert Meier',?)`).run(jetzt); db.prepare(`INSERT INTO wd_kunden (id,email,name,erstellt_am,gesperrt) VALUES ('k3','clara@example.com','Clara Sperr',?,1)`).run(jetzt); db.prepare(`INSERT INTO wd_anfragen (id,nummer,name,email,paket,status,erstellt_am) VALUES ('a1','A-2608-0001','Anna Muster','anna@example.com','website','neu',?)`).run(jetzt); db.prepare(`INSERT INTO wd_anfragen (id,nummer,name,email,paket,status,erstellt_am) VALUES ('a2','A-2608-0002','Dora 50% Rabatt','dora@example.com','shop','neu',?)`).run(jetzt); db.prepare(`INSERT INTO wd_projekte (id,nummer,kunde_id,titel,paket,status,erstellt_am,termin_am) VALUES ('p1','P-2608-0001','k1','Website für Musterbau','website','design',?, '2026-10-15')`).run(jetzt); db.prepare(`INSERT INTO wd_zahlungen (id,nummer,kunde_id,art,zweck_text,betrag_cent,status,erstellt_am) VALUES ('z1','Z-2608-0001','k1','anzahlung','Anzahlung Website',30000,'offen',?)`).run(jetzt); L("\n1. UEBER ALLE BEREICHE HINWEG"); let r = await such("Muster"); pruefe("die Suche antwortet", r && r.ok === true, JSON.stringify(r).slice(0, 100)); const arten = new Set(r.treffer.map((t) => t.art)); pruefe("sie findet die Anfrage", arten.has("anfrage")); pruefe("sie findet den Kunden", arten.has("kunde")); pruefe("sie findet das Projekt", arten.has("projekt")); pruefe("sie findet die Zahlung ueber den KUNDENNAMEN", arten.has("zahlung")); /* Vier Bereiche in einem Aufruf. Die erste Fassung erwartete drei und uebersah, dass die Zahlung ueber den Namen des Kunden mitgefunden wird -- was genau richtig ist: Wer "Muster" sucht, will auch dessen offene Rechnung sehen, ohne sie ueber die Zahlungsnummer zu kennen. */ pruefe("alle vier Bereiche in EINEM Aufruf", arten.size === 4, Array.from(arten).join(",")); L("\n2. JEDER TREFFER TRAEGT SEINEN ZUSAMMENHANG"); const projektTreffer = r.treffer.find((t) => t.art === "projekt"); pruefe("das Projekt nennt Nummer und Kunde", /P-2608-0001/.test(projektTreffer.unten) && /Anna Muster/.test(projektTreffer.unten), projektTreffer.unten); pruefe("und den Liefertermin", /2026-10-15/.test(projektTreffer.unten), projektTreffer.unten); pruefe("jeder Treffer weiss, wohin er fuehrt", r.treffer.every((t) => !!t.ziel), JSON.stringify(r.treffer.map((t) => t.ziel))); pruefe("jeder Treffer hat eine Marke fuer seinen Stand", r.treffer.every((t) => !!t.marke), JSON.stringify(r.treffer.map((t) => t.marke))); L("\n3. SUCHE UEBER DEN KUNDENNAMEN FINDET SEIN PROJEKT"); /* Man merkt sich den Menschen, nicht die Projektnummer. */ r = await such("Musterbau"); pruefe("die Firma des Kunden fuehrt zum Kunden", r.treffer.some((t) => t.art === "kunde" && t.titel === "Anna Muster")); L("\n4. ZAHLUNGEN"); r = await such("Anzahlung"); pruefe("die Zahlung wird gefunden", r.treffer.some((t) => t.art === "zahlung")); const z = r.treffer.find((t) => t.art === "zahlung"); pruefe("mit lesbarem Betrag", /300,00/.test(z.unten), z.unten); L("\n5. LIKE-SONDERZEICHEN — hier entstehen falsche Treffer"); /* Ohne Behandlung waere "%" ein Platzhalter und faende ALLES. */ r = await such("%"); pruefe("ein einzelnes Prozentzeichen ist zu kurz und sucht gar nicht", r.zuKurz === true, JSON.stringify(r).slice(0, 80)); r = await such("50%"); pruefe("nach '50%' wird woertlich gesucht", r.treffer.length === 1 && /Dora/.test(r.treffer[0].titel), r.treffer.length + " Treffer: " + r.treffer.map((t) => t.titel).join(", ")); /* Ohne Behandlung waere "_" ein beliebiges Zeichen: "bert_meier" faende dann auch "bertXmeier". Geprueft wird umgekehrt -- ein Muster mit Unterstrich darf NICHT auf etwas anderes passen. */ r = await such("bert_meier"); pruefe("ein Unterstrich wird woertlich genommen", r.treffer.length === 1 && r.treffer[0].art === "kunde", r.treffer.length + " Treffer"); r = await such("bertxmeier"); pruefe("und passt nicht auf ein beliebiges Zeichen", r.treffer.length === 0, r.treffer.length + " Treffer"); L("\n6. ZU KURZ WIRD GAR NICHT GESUCHT"); r = await such("a"); pruefe("ein einzelner Buchstabe sucht nicht", r.zuKurz === true && r.treffer.length === 0); r = await such(""); pruefe("leer ebenfalls nicht", r.zuKurz === true); r = await such(" "); pruefe("nur Leerzeichen ebenfalls nicht", r.zuKurz === true); L("\n7. NICHTS GEFUNDEN IST EIN EIGENER ZUSTAND"); r = await such("gibtesnichtxyz"); pruefe("die Antwort ist gueltig", r.ok === true); pruefe("mit null Treffern und NICHT zuKurz", r.anzahl === 0 && r.zuKurz === false, JSON.stringify(r).slice(0, 90)); L("\n8. GESPERRTE KUNDEN WERDEN GEZEIGT, ABER GEKENNZEICHNET"); /* Verstecken waere falsch: Man sucht sie ja gerade, WEIL etwas nicht stimmt. Ohne Kennzeichnung wuerde man sich aber wundern. */ r = await such("Sperr"); pruefe("der gesperrte Kunde erscheint", r.treffer.some((t) => t.art === "kunde")); pruefe("und ist als gesperrt gekennzeichnet", r.treffer.find((t) => t.art === "kunde").marke === "gesperrt", r.treffer.find((t) => t.art === "kunde").marke); L("\n9. ARCHIVIERTES TAUCHT NICHT AUF"); db.prepare(`UPDATE wd_anfragen SET archiviert = 1 WHERE id = 'a1'`).run(); r = await such("A-2608-0001"); pruefe("eine archivierte Anfrage wird nicht mehr gefunden", !r.treffer.some((t) => t.art === "anfrage"), JSON.stringify(r.treffer)); L("\n10. DIE RECHTEPRUEFUNG GREIFT"); for (const [name, kopf] of [ ["ohne Ausweis kein Ergebnis", {}], ["mit gefaelschter Unterschrift kein Ergebnis", { authorization: "Bearer " + ausweis().split(".")[0] + ".XXXX" }], ["mit abgelaufenem Ausweis kein Ergebnis", { authorization: "Bearer " + ausweis(-1000) }], ]) { const a = await such("Muster", kopf); pruefe(name, a && a.ok === false, JSON.stringify(a).slice(0, 70)); } L(`\n===== ${ok} bestanden, ${fehler} fehlgeschlagen =====`); process.exitCode = fehler ? 1 : 0; rmSync(tmp, { recursive: true, force: true });