All checks were successful
Filters are evaluated once into an id set used by both search branches. Adds tests for age/genre/mechanic filters with a search term, rank 2 vs 3 and null p_limit. approxNormalize uses NFKD. Perf script checks confirm_local value and unix socket and re-analyzes after rollback. Adds docs/specs/3-katalog-suche-umsetzung.md (measurements, log check incl. GET finding), release checklist section 2a and the CSV-only note for alt titles. Refs #3 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
413 lines
22 KiB
TypeScript
413 lines
22 KiB
TypeScript
// Tests für build_startbestand.ts. Aufruf (Repo-Root):
|
||
// deno test --allow-read=supabase supabase/catalog/
|
||
// Fixtures sind offensichtlich fiktiv ("Testspiel", "Testverlag"). Es werden keine echten Spieldaten verwendet.
|
||
import assert from "node:assert/strict";
|
||
import {
|
||
build,
|
||
BuildError,
|
||
generateStartbestandSql,
|
||
NAMESPACE,
|
||
approxNormalize,
|
||
parseAltTitles,
|
||
parseCsv,
|
||
parseKeys,
|
||
parseStartbestand,
|
||
parseVocabulary,
|
||
renderKeys,
|
||
uuidV5,
|
||
} from "./build_startbestand.ts";
|
||
|
||
const VOCAB = `kind,vocab_key,name_de,name_en
|
||
genre,test-genre-a,Testgenre A,Test genre A
|
||
genre,test-genre-b,Testgenre B,Test genre B
|
||
mechanic,test-mechanic-a,Testmechanik A,Test mechanic A
|
||
`;
|
||
const HEADER =
|
||
"key,title,publishers,year,min_players,max_players,min_minutes,max_minutes,min_age,genres,mechanics,description,description_locale,hidden,facts_source,alt_titles,base_game_key";
|
||
|
||
// Zeile mit Standardwerten; einzelne Felder lassen sich überschreiben.
|
||
function row(over: Record<string, string> = {}): string {
|
||
const base: Record<string, string> = {
|
||
key: "testspiel-a", title: "Testspiel A", publishers: "Testverlag", year: "2020", min_players: "2",
|
||
max_players: "4", min_minutes: "30", max_minutes: "60", min_age: "8", genres: "Testgenre A",
|
||
mechanics: "Testmechanik A", description: "Fiktiver Text.", description_locale: "de", hidden: "",
|
||
facts_source: "Test", alt_titles: "", base_game_key: "",
|
||
};
|
||
const r = { ...base, ...over };
|
||
return HEADER.split(",").map((c) => {
|
||
const v = r[c];
|
||
return /[",\n]/.test(v) ? `"${v.replaceAll('"', '""')}"` : v;
|
||
}).join(",");
|
||
}
|
||
const csv = (...rows: string[]) => [HEADER, ...rows].join("\n") + "\n";
|
||
|
||
async function vocab() {
|
||
return await parseVocabulary(VOCAB);
|
||
}
|
||
async function rejects(text: string, includes: RegExp, keys = new Map<string, string>()) {
|
||
const v = await vocab();
|
||
await assert.rejects(
|
||
() => parseStartbestand(text, v, keys),
|
||
(e: unknown) => {
|
||
assert.ok(e instanceof BuildError, "BuildError erwartet");
|
||
assert.match(e.problems.join("\n"), includes);
|
||
return true;
|
||
},
|
||
);
|
||
}
|
||
|
||
// ---- UUID v5 ------------------------------------------------------------------------------
|
||
Deno.test("uuidV5: Referenzwert aus RFC 4122 / Python uuid5(NAMESPACE_DNS, ...)", async () => {
|
||
assert.equal(
|
||
await uuidV5("www.example.com", "6ba7b810-9dad-11d1-80b4-00c04fd430c8"),
|
||
"2ed6657d-e927-568b-95e1-2665a8aea6a2",
|
||
);
|
||
});
|
||
|
||
Deno.test("uuidV5: gleicher key gibt gleiche UUID wie uuid_generate_v5 in PostgreSQL (siehe pgTAP 06)", async () => {
|
||
assert.equal(NAMESPACE, "6f1d9a52-3c4e-4b8a-9f27-5d0e8c71a3b4");
|
||
assert.equal(await uuidV5("game:testspiel-a"), "5189ad0e-ab5b-5500-a3e0-a0b3671cdf14");
|
||
assert.equal(await uuidV5("game:testspiel-a"), await uuidV5("game:testspiel-a"));
|
||
assert.notEqual(await uuidV5("game:testspiel-a"), await uuidV5("game:testspiel-b"));
|
||
});
|
||
|
||
// ---- CSV ----------------------------------------------------------------------------------
|
||
Deno.test("parseCsv: Anführungszeichen, doppelte Anführungszeichen, Zeilenumbruch im Feld, CRLF, BOM", () => {
|
||
const parsed = parseCsv('a,b\r\n"x, y","sagt ""hallo"""\r\n"zwei\nZeilen",z\r\n', "t.csv");
|
||
assert.deepEqual(parsed.header, ["a", "b"]);
|
||
assert.equal(parsed.rows[0].cells.a, "x, y");
|
||
assert.equal(parsed.rows[0].cells.b, 'sagt "hallo"');
|
||
assert.equal(parsed.rows[1].cells.a, "zwei\nZeilen");
|
||
assert.equal(parsed.rows[1].line, 3);
|
||
});
|
||
|
||
Deno.test("parseCsv: falsche Spaltenzahl und offenes Anführungszeichen nennen die Zeile", () => {
|
||
assert.throws(() => parseCsv("a,b\n1,2,3\n", "t.csv"), /t\.csv:2: 3 Spalten statt 2/);
|
||
assert.throws(() => parseCsv('a,b\n"offen,2\n', "t.csv"), /t\.csv:2: Anführungszeichen nicht geschlossen/);
|
||
});
|
||
|
||
Deno.test("parseCsv: nur Kopfzeile ergibt keine Zeilen", () => {
|
||
assert.equal(parseCsv("a,b\n", "t.csv").rows.length, 0);
|
||
});
|
||
|
||
// ---- Vokabular ----------------------------------------------------------------------------
|
||
Deno.test("parseVocabulary: gültig, IDs nach UUID v5", async () => {
|
||
const v = await vocab();
|
||
assert.equal(v.length, 3);
|
||
assert.equal(v[0].id, await uuidV5("genre:test-genre-a"));
|
||
assert.equal(v[2].id, await uuidV5("mechanic:test-mechanic-a"));
|
||
});
|
||
|
||
Deno.test("parseVocabulary: doppelter vocab_key, doppelter Name, falsche kind, falscher Schlüssel", async () => {
|
||
const head = "kind,vocab_key,name_de,name_en\n";
|
||
await assert.rejects(() => parseVocabulary(head + "genre,a-b,A,A\ngenre,a-b,B,B\n"), /vocabulary\.csv:3: vocab_key "a-b" doppelt/);
|
||
await assert.rejects(() => parseVocabulary(head + "genre,a-b,A,A\ngenre,c-d,a,C\n"), /vocabulary\.csv:3: name_de "a" doppelt/);
|
||
await assert.rejects(() => parseVocabulary(head + "genre,a-b,A,A\ngenre,c-d,C,a\n"), /vocabulary\.csv:3: name_en "a" doppelt/);
|
||
await assert.rejects(() => parseVocabulary(head + "kategorie,a-b,A,A\n"), /kind muss/);
|
||
await assert.rejects(() => parseVocabulary(head + "genre,A_B,A,A\n"), /vocab_key "A_B" ungültig/);
|
||
await assert.rejects(() => parseVocabulary("kind,vocab_key,name_de\ngenre,a-b,A\n"), /Spalte "name_en" fehlt/);
|
||
});
|
||
|
||
Deno.test("parseVocabulary: gleiche Namen in verschiedenen Arten sind erlaubt", async () => {
|
||
const v = await parseVocabulary("kind,vocab_key,name_de,name_en\ngenre,a-b,A,A\nmechanic,a-b,A,A\n");
|
||
assert.equal(v.length, 2);
|
||
assert.notEqual(v[0].id, v[1].id);
|
||
});
|
||
|
||
// ---- Startbestand: gültig -----------------------------------------------------------------
|
||
Deno.test("parseStartbestand: gültige Zeile (Positivfall)", async () => {
|
||
const games = await parseStartbestand(csv(row()), await vocab(), new Map());
|
||
assert.equal(games.length, 1);
|
||
assert.equal(games[0].id, await uuidV5("game:testspiel-a"));
|
||
assert.deepEqual(games[0].publishers, ["Testverlag"]);
|
||
assert.equal(games[0].hidden, false);
|
||
});
|
||
|
||
Deno.test("parseStartbestand: nur Kopfzeile ist gültig (heutiger Stand ohne Spiele)", async () => {
|
||
assert.deepEqual(await parseStartbestand(HEADER + "\n", await vocab(), new Map()), []);
|
||
});
|
||
|
||
// ---- Startbestand: eine Negativprobe je Prüfregel -----------------------------------------
|
||
Deno.test("SB2: fehlende Pflicht-Fakten", async () => {
|
||
for (const col of ["year", "min_players", "max_players", "min_minutes", "max_minutes", "min_age"]) {
|
||
await rejects(csv(row({ [col]: "" })), new RegExp(`startbestand\\.csv:2: ${col} fehlt`));
|
||
}
|
||
await rejects(csv(row({ publishers: "" })), /mindestens ein Verlag/);
|
||
await rejects(csv(row({ publishers: " ; " })), /mindestens ein Verlag/);
|
||
await rejects(csv(row({ genres: "" })), /mindestens ein Genre/);
|
||
await rejects(csv(row({ mechanics: "" })), /mindestens eine Mechanik/);
|
||
await rejects(csv(row({ title: "" })), /title muss 1-200 Zeichen/);
|
||
});
|
||
|
||
Deno.test("DB-Grenzen: Jahr, max < min, keine Zahl, negativ", async () => {
|
||
await rejects(csv(row({ year: "1799" })), /year 1799 außerhalb/);
|
||
await rejects(csv(row({ year: "2101" })), /year 2101 außerhalb/);
|
||
await rejects(csv(row({ year: "20x0" })), /year "20x0" ist keine ganze Zahl/);
|
||
await rejects(csv(row({ min_players: "0" })), /min_players 0 außerhalb/);
|
||
await rejects(csv(row({ min_players: "5", max_players: "3" })), /max_players \(3\) kleiner als min_players \(5\)/);
|
||
await rejects(csv(row({ min_minutes: "90", max_minutes: "30" })), /max_minutes \(30\) kleiner als min_minutes \(90\)/);
|
||
await rejects(csv(row({ min_age: "-1" })), /min_age "-1" ist keine ganze Zahl/);
|
||
await rejects(csv(row({ title: "x".repeat(201) })), /title muss 1-200 Zeichen/);
|
||
});
|
||
|
||
Deno.test("SB4: Kurztext Pflicht, Sprache de, Länge", async () => {
|
||
await rejects(csv(row({ description: "" })), /Kurztext fehlt/);
|
||
await rejects(csv(row({ description_locale: "en" })), /description_locale muss "de" sein/);
|
||
await rejects(csv(row({ description_locale: "" })), /description_locale muss "de" sein/);
|
||
await rejects(csv(row({ description: "x".repeat(5001) })), /länger als 5000/);
|
||
});
|
||
|
||
Deno.test("SB5: unbekanntes Genre oder unbekannte Mechanik, Doppelte", async () => {
|
||
await rejects(csv(row({ genres: "Erfundenes" })), /unbekannt in genres: "Erfundenes"/);
|
||
await rejects(csv(row({ mechanics: "Testgenre A" })), /unbekannt in mechanics/); // Genre ist keine Mechanik
|
||
await rejects(csv(row({ genres: "Testgenre A;Testgenre A" })), /genres: "Testgenre A" doppelt/);
|
||
});
|
||
|
||
Deno.test("SB6: keine Spalte für Cover, Quelle oder BGG-Kennung", async () => {
|
||
for (const col of ["image_path", "bgg_id", "catalog_source_id", "source_ref", "cover"]) {
|
||
await rejects(`${HEADER},${col}\n${row()},x\n`, new RegExp(`unbekannte Spalte "${col}"`));
|
||
}
|
||
});
|
||
|
||
Deno.test("SB7: base_game_key muss auf einen vorhandenen key zeigen", async () => {
|
||
await rejects(csv(row({ base_game_key: "gibt-es-nicht" })), /base_game_key "gibt-es-nicht" hat keinen passenden key/);
|
||
await rejects(csv(row({ base_game_key: "testspiel-a" })), /verweist auf das Spiel selbst/);
|
||
const ok = await parseStartbestand(
|
||
csv(row(), row({ key: "testspiel-b", title: "Testspiel B", base_game_key: "testspiel-a" })),
|
||
await vocab(),
|
||
new Map(),
|
||
);
|
||
assert.equal(ok.length, 2);
|
||
});
|
||
|
||
Deno.test("key: Format und Eindeutigkeit", async () => {
|
||
await rejects(csv(row({ key: "Ungültig_Key" })), /key "Ungültig_Key" ungültig/);
|
||
await rejects(csv(row({ key: "a" })), /key "a" ungültig/);
|
||
await rejects(csv(row(), row({ title: "Noch eins" })), /startbestand\.csv:3: key "testspiel-a" doppelt/);
|
||
});
|
||
|
||
Deno.test("hidden: nur leer oder ja; Verlage nicht doppelt", async () => {
|
||
await rejects(csv(row({ hidden: "nein" })), /hidden muss leer oder "ja" sein/);
|
||
await rejects(csv(row({ publishers: "Testverlag;TESTVERLAG" })), /Verlag "TESTVERLAG" doppelt/);
|
||
});
|
||
|
||
Deno.test("Pflichtspalte fehlt in der Kopfzeile", async () => {
|
||
await assert.rejects(
|
||
async () => await parseStartbestand("key,title\nx,y\n", await vocab(), new Map()),
|
||
/Spalte "publishers" fehlt/,
|
||
);
|
||
});
|
||
|
||
Deno.test("mehrere Fehler werden gesammelt, mit Zeilennummern", async () => {
|
||
const v = await vocab();
|
||
await assert.rejects(
|
||
() => parseStartbestand(csv(row({ year: "" }), row({ key: "testspiel-b", genres: "X" })), v, new Map()),
|
||
(e: unknown) => {
|
||
assert.ok(e instanceof BuildError);
|
||
assert.match(e.problems.join("\n"), /startbestand\.csv:2: year fehlt/);
|
||
assert.match(e.problems.join("\n"), /startbestand\.csv:3: unbekannt in genres/);
|
||
return true;
|
||
},
|
||
);
|
||
});
|
||
|
||
// ---- Schlüsselliste -----------------------------------------------------------------------
|
||
Deno.test("Schlüssel sind unveränderlich: ein entfernter key bricht ab", async () => {
|
||
const previous = parseKeys(`key,uuid\ntestspiel-a,${await uuidV5("game:testspiel-a")}\ntestspiel-alt,${await uuidV5("game:testspiel-alt")}\n`);
|
||
await rejects(csv(row()), /Schlüssel entfernt oder umbenannt: "testspiel-alt"/, previous);
|
||
// Umbenennen ist dasselbe wie Entfernen
|
||
await rejects(csv(row({ key: "testspiel-neu" })), /Schlüssel entfernt oder umbenannt: "testspiel-a"/, previous);
|
||
});
|
||
|
||
Deno.test("Schlüssel: falsche UUID in der Liste (Namespace geändert?)", async () => {
|
||
const previous = parseKeys("key,uuid\ntestspiel-a,00000000-0000-5000-8000-000000000000\n");
|
||
await rejects(csv(row()), /uuid zu "testspiel-a" ist 00000000-0000-5000-8000-000000000000, erwartet/, previous);
|
||
});
|
||
|
||
Deno.test("Schlüsselliste wächst nur und ist sortiert", async () => {
|
||
const first = await build({ vocabularyCsv: VOCAB, startbestandCsv: csv(row({ key: "zzz-spiel" })), previousKeysCsv: "key,uuid\n" });
|
||
const second = await build({
|
||
vocabularyCsv: VOCAB,
|
||
startbestandCsv: csv(row({ key: "zzz-spiel" }), row({ key: "aaa-spiel", title: "Neu" })),
|
||
previousKeysCsv: first["startbestand-keys.csv"],
|
||
});
|
||
const before = first["startbestand-keys.csv"].trim().split("\n");
|
||
const after = second["startbestand-keys.csv"].trim().split("\n");
|
||
for (const line of before) assert.ok(after.includes(line), `Zeile fehlt: ${line}`);
|
||
assert.deepEqual(after.slice(1).map((l) => l.split(",")[0]), ["aaa-spiel", "zzz-spiel"]);
|
||
assert.equal(renderKeys(parseKeys(second["startbestand-keys.csv"])), second["startbestand-keys.csv"]);
|
||
});
|
||
|
||
// ---- SQL-Erzeugung ------------------------------------------------------------------------
|
||
async function sqlFor(...rows: string[]): Promise<string> {
|
||
return generateStartbestandSql(await parseStartbestand(csv(...rows), await vocab(), new Map()));
|
||
}
|
||
|
||
Deno.test("hidden = ja erzeugt hidden_at, leer fasst hidden_at nicht an", async () => {
|
||
const hiddenSql = await sqlFor(row({ hidden: "ja" }));
|
||
assert.match(hiddenSql, /update public\.games set hidden_at = now\(\)/);
|
||
assert.match(hiddenSql, /where hidden_at is null and id = any/);
|
||
const plain = await sqlFor(row());
|
||
assert.doesNotMatch(plain, /hidden_at/);
|
||
assert.doesNotMatch(plain, /update public\.games set/);
|
||
});
|
||
|
||
Deno.test("facts_source und base_game_key landen nicht in der SQL", async () => {
|
||
const sql = await sqlFor(row({ facts_source: "GEHEIMER-NACHWEIS", base_game_key: "" }));
|
||
assert.doesNotMatch(sql, /GEHEIMER-NACHWEIS|base_game_key|facts_source/);
|
||
});
|
||
|
||
// ---- alt_titles (KAT-01, Spec #3 4.3) --------------------------------------------------------
|
||
function altProblems(raw: string, main = "Testspiel A"): string[] {
|
||
const problems: string[] = [];
|
||
parseAltTitles(raw, main, (m) => problems.push(m));
|
||
return problems;
|
||
}
|
||
|
||
Deno.test("alt_titles: Format de:Titel;en:Title, Leerraum am Rand wird entfernt, ':' im Titel ist erlaubt", () => {
|
||
const problems: string[] = [];
|
||
const parsed = parseAltTitles(" de: Catan: Das Duell ; en:Test Duel ", "Testspiel A", (m) => problems.push(m));
|
||
assert.deepEqual(problems, []);
|
||
assert.deepEqual(parsed, [{ locale: "de", title: "Catan: Das Duell" }, { locale: "en", title: "Test Duel" }]);
|
||
assert.deepEqual(parseAltTitles("", "Testspiel A", () => assert.fail("kein Fehler erwartet")), []);
|
||
});
|
||
|
||
Deno.test("alt_titles: Sprache, fehlender Doppelpunkt, leerer und zu langer Titel werden abgelehnt", () => {
|
||
assert.match(altProblems("fr:Titre").join("\n"), /Sprache "fr".*"de" oder "en"/);
|
||
assert.match(altProblems("Nur ein Titel").join("\n"), /kein ":"/);
|
||
assert.match(altProblems("de: ").join("\n"), /leerer Titel/);
|
||
assert.match(altProblems("de:" + "x".repeat(201)).join("\n"), /länger als 200 Zeichen/);
|
||
assert.equal(altProblems("de:" + "x".repeat(200)).length, 0);
|
||
assert.match(altProblems("de:!!!").join("\n"), /nur aus Sonderzeichen/);
|
||
});
|
||
|
||
Deno.test("alt_titles: Duplikat in derselben Sprache und Titel gleich dem Haupttitel werden abgelehnt", () => {
|
||
assert.match(altProblems("de:Café;de:CAFE").join("\n"), /doppelt \(Sprache de\)/);
|
||
assert.equal(altProblems("de:Café;en:Café").length, 0, "gleicher Titel in anderer Sprache ist zulässig");
|
||
assert.match(altProblems("de:testspiel-a").join("\n"), /gleich dem Haupttitel/);
|
||
assert.match(altProblems("en:CATAN®", "Catan").join("\n"), /gleich dem Haupttitel/);
|
||
});
|
||
|
||
Deno.test("alt_titles: ';' im Titel ergibt einen Fehler mit Zeilennummer und Hinweis", async () => {
|
||
await rejects(
|
||
csv(row(), row({ key: "testspiel-b", title: "Testspiel B", alt_titles: "de:Catan;Das Duell" })),
|
||
/startbestand\.csv:3: alt_titles: Eintrag "Das Duell" hat kein ":".*";" ist in alternativen Titeln nicht erlaubt/,
|
||
);
|
||
await rejects(
|
||
csv(row({ alt_titles: "de:Eins;Zwei: Drei" })),
|
||
/startbestand\.csv:2: alt_titles: Sprache "Zwei" .*";" ist in alternativen Titeln nicht erlaubt/,
|
||
);
|
||
});
|
||
|
||
Deno.test("alt_titles: Fehler anderer Art nennen die Zeilennummer", async () => {
|
||
await rejects(csv(row(), row({ key: "testspiel-b", alt_titles: "fr:X" })), /startbestand\.csv:3: alt_titles: Sprache "fr"/);
|
||
});
|
||
|
||
Deno.test("approxNormalize: Marken- und Schutzzeichen, Akzente, ß, Æ, Sonderzeichen", () => {
|
||
assert.equal(approxNormalize("CATAN®"), "catan");
|
||
assert.equal(approxNormalize("Spiel™Ding"), "spielding"); // Schutzzeichen werden entfernt, nicht zum Trenner (wie search_normalize)
|
||
assert.equal(approxNormalize("Café Größe"), "cafe grosse");
|
||
assert.equal(approxNormalize("Æon’s End"), "aeon s end");
|
||
assert.equal(approxNormalize("7 Wonders: Duel"), "7 wonders duel");
|
||
assert.equal(approxNormalize("Ticket to Ride – Europe"), "ticket to ride europe");
|
||
assert.equal(approxNormalize("!!!"), "");
|
||
assert.equal(approxNormalize("Spiel™ ½"), "spiel 1 2"); // Bruch wie unaccent: NFKD
|
||
});
|
||
|
||
Deno.test("SQL alt_titles: Abgleich über normalisierten Titel, Update nur bei geänderter Schreibweise", async () => {
|
||
const sql = await sqlFor(row({ alt_titles: "de:Alter Titel;en:Old 'Title'" }));
|
||
assert.match(sql, /with want\(game_id, locale, title\) as \(values/);
|
||
assert.match(sql, /'de', 'Alter Titel'\)/);
|
||
assert.match(sql, /'en', 'Old ''Title'''\)/); // Apostroph verdoppelt
|
||
assert.match(sql, /delete from public\.game_alt_titles a/);
|
||
assert.match(sql, /public\.search_normalize\(w\.title\) = a\.title_search/);
|
||
assert.match(sql, /on conflict \(game_id, locale, title_search\) do update set title = excluded\.title/);
|
||
assert.match(sql, /where public\.game_alt_titles\.title is distinct from excluded\.title;/);
|
||
assert.doesNotMatch(sql, /game_alt_titles \(id/); // keine abgeleitete ID
|
||
});
|
||
|
||
Deno.test("SQL alt_titles: ohne alternative Titel nur das Löschen, nie ein leeres values", async () => {
|
||
const sql = await sqlFor(row());
|
||
assert.match(sql, /delete from public\.game_alt_titles a where a\.game_id = any\(array\['/);
|
||
assert.doesNotMatch(sql, /with want\(game_id, locale, title\)/);
|
||
assert.doesNotMatch(sql, /insert into public\.game_alt_titles/);
|
||
});
|
||
|
||
Deno.test("checks.sql: Informationszeile zu Startbestandsspielen mit alternativen Titeln", async () => {
|
||
const out = await build({ vocabularyCsv: VOCAB, startbestandCsv: csv(row()), previousKeysCsv: "key,uuid\n" });
|
||
assert.match(out["checks.sql"], /'Info-Alt', 'Startbestandsspiele mit alternativen Titeln'/);
|
||
assert.match(out["checks.sql"], /count\(distinct a\.game_id\) as n from public\.game_alt_titles a join sb on sb\.id = a\.game_id/);
|
||
});
|
||
|
||
Deno.test("SQL: Apostroph im Text wird verdoppelt, Upsert nutzt is distinct from, Zuordnungen werden abgeglichen", async () => {
|
||
const sql = await sqlFor(row({ title: "Wo's lang", description: "Es ist 'nett'." }));
|
||
assert.match(sql, /'Wo''s lang'/);
|
||
assert.match(sql, /'Es ist ''nett''\.'/);
|
||
assert.match(sql, /on conflict \(id\) do update set/);
|
||
assert.match(sql, /is distinct from \(excluded\.title/);
|
||
assert.match(sql, /on conflict \(\(lower\(name\)\)\) do nothing/); // Verlage
|
||
assert.match(sql, /delete from public\.game_genres/);
|
||
assert.match(sql, /delete from public\.game_mechanics/);
|
||
assert.match(sql, /delete from public\.game_publishers/);
|
||
assert.match(sql, /on conflict \(game_id, genre_id\) do nothing/);
|
||
assert.match(sql, /Genre fehlt in der Datenbank/); // Wächter gegen still verlorene Zuordnungen
|
||
assert.doesNotMatch(sql, /image_path|bgg_id|catalog_source_id|source_ref/); // SB6
|
||
});
|
||
|
||
Deno.test("SQL: leerer Startbestand ergibt eine gültige, leere Datei", async () => {
|
||
const sql = generateStartbestandSql([]);
|
||
assert.match(sql, /keine Spiele/);
|
||
assert.doesNotMatch(sql, /insert into/);
|
||
});
|
||
|
||
Deno.test("build: deterministisch, ohne Zeitstempel", async () => {
|
||
const input = { vocabularyCsv: VOCAB, startbestandCsv: csv(row(), row({ key: "testspiel-b", title: "B" })), previousKeysCsv: "key,uuid\n" };
|
||
assert.deepEqual(await build(input), await build(input));
|
||
});
|
||
|
||
Deno.test("checks.sql: bettet die Startbestands-IDs ein, Sollwert 180, leerer Bestand ist gültiges SQL", async () => {
|
||
const out = await build({ vocabularyCsv: VOCAB, startbestandCsv: csv(row()), previousKeysCsv: "key,uuid\n" });
|
||
assert.match(out["checks.sql"], new RegExp(await uuidV5("game:testspiel-a")));
|
||
assert.match(out["checks.sql"], />= 180/);
|
||
const empty = await build({ vocabularyCsv: VOCAB, startbestandCsv: HEADER + "\n", previousKeysCsv: "key,uuid\n" });
|
||
assert.match(empty["checks.sql"], /jsonb_array_elements_text\('\[\]'::jsonb\)/);
|
||
assert.doesNotMatch(empty["checks.sql"], /values \(\s*\)/);
|
||
});
|
||
|
||
Deno.test("vocabulary.sql: on conflict do nothing ohne Konfliktziel", async () => {
|
||
const out = await build({ vocabularyCsv: VOCAB, startbestandCsv: HEADER + "\n", previousKeysCsv: "key,uuid\n" });
|
||
assert.match(out["vocabulary.sql"], /insert into public\.genres \(id, name_de, name_en\) values/);
|
||
assert.match(out["vocabulary.sql"], /insert into public\.mechanics/);
|
||
assert.equal((out["vocabulary.sql"].match(/on conflict do nothing;/g) ?? []).length, 2);
|
||
});
|
||
|
||
// ---- Eingecheckte, erzeugte Dateien sind aktuell ------------------------------------------
|
||
Deno.test("die eingecheckten erzeugten Dateien entsprechen den CSV-Quellen", async () => {
|
||
const dir = new URL(".", import.meta.url);
|
||
const read = (name: string) => Deno.readTextFile(new URL(name, dir));
|
||
const real = await build({
|
||
vocabularyCsv: await read("vocabulary.csv"),
|
||
startbestandCsv: await read("startbestand.csv"),
|
||
previousKeysCsv: await read("startbestand-keys.csv"),
|
||
});
|
||
for (const name of ["vocabulary.sql", "startbestand.sql", "startbestand-keys.csv", "checks.sql"] as const) {
|
||
assert.equal(await read(name), real[name], `${name} ist nicht aktuell: build_startbestand.ts ausführen`);
|
||
}
|
||
const fixture = await build({
|
||
vocabularyCsv: await read("testdata/vocabulary.csv"),
|
||
startbestandCsv: await read("testdata/startbestand.csv"),
|
||
previousKeysCsv: "key,uuid\n",
|
||
});
|
||
const support = (name: string) => Deno.readTextFile(new URL(`../tests/support/${name}`, dir));
|
||
assert.equal(await support("vocabulary_fixture.inc"), fixture["vocabulary.sql"]);
|
||
assert.equal(await support("startbestand_fixture.inc"), fixture["startbestand.sql"]);
|
||
assert.equal(await support("checks_fixture.inc"), fixture.checksFixtureInclude);
|
||
});
|
||
|
||
Deno.test("Vokabular-Vorschlag: 12-15 Genres, 20-30 Mechaniken (Aufgabe B3a)", async () => {
|
||
const v = await parseVocabulary(await Deno.readTextFile(new URL("vocabulary.csv", import.meta.url)));
|
||
const genres = v.filter((e) => e.kind === "genre").length;
|
||
const mechanics = v.filter((e) => e.kind === "mechanic").length;
|
||
assert.ok(genres >= 12 && genres <= 15, `Genres: ${genres}`);
|
||
assert.ok(mechanics >= 20 && mechanics <= 30, `Mechaniken: ${mechanics}`);
|
||
});
|