import "./setup"; import assert from "node:assert/strict"; import test, { after, before, beforeEach } from "node:test"; import { installTmdbStub, SERIES_ID, MOVIE_ID, MOVIE_2_ID } from "./fixtures/tmdb"; import { prisma } from "../src/lib/db"; import { importMovie, importSeries } from "../src/lib/catalog"; import { addToLibrary, markMovieWatched, markEpisodesWatched } from "../src/lib/library"; import { achievementName, evaluateAchievements, listAchievements, listUnlockedAchievements, } from "../src/lib/achievements"; import { kuratierteReihe } from "../src/lib/achievement-names"; /** * Prüft die Errungenschaften: Vergabe, Entdopplung, die Stummschaltung beim * ersten Lauf und die Absicherung gegen falsch abgeschriebene Reihen. */ const tmdb = installTmdbStub(); const TEST_EMAIL = "test-achievements@flimmer.invalid"; let userId: string; let seriesTitleId: string; let movieTitleId: string; async function aufraeumen() { await prisma.user.deleteMany({ where: { email: TEST_EMAIL } }); await prisma.title.deleteMany({ where: { tmdbId: { gte: 999000, lt: 1000000 } } }); } before(async () => { await aufraeumen(); userId = ( await prisma.user.create({ data: { email: TEST_EMAIL, name: "Sammlerin" }, select: { id: true }, }) ).id; seriesTitleId = (await importSeries(SERIES_ID, { region: "DE" })).id; movieTitleId = (await importMovie(MOVIE_ID, "DE")).id; await importMovie(MOVIE_2_ID, "DE"); }); beforeEach(async () => { await prisma.achievement.deleteMany({ where: { userId } }); await prisma.watch.deleteMany({ where: { userId } }); await prisma.libraryEntry.deleteMany({ where: { userId } }); }); after(async () => { await aufraeumen(); tmdb.restore(); await prisma.$disconnect(); }); // --------------------------------------------------------------------------- test("der erste Lauf eines Kontos ist stumm", async () => { // Bestandskonten bringen Jahre an Daten mit. Zwei Dutzend Meldungen auf // einmal waeren Laerm, kein Erfolgserlebnis. await addToLibrary(userId, movieTitleId, "WATCHLIST"); const erster = await evaluateAchievements(userId); assert.equal(erster.stumm, true, "der erste Lauf meldet nichts"); assert.deepEqual(erster.neu, [], "auch wenn er Abzeichen vergibt"); // Vergeben wurde trotzdem. const vergeben = await prisma.achievement.count({ where: { userId } }); assert.ok(vergeben > 0, "Erstkontakt ist erreicht"); }); test("ab dem zweiten Lauf wird gemeldet", async () => { await addToLibrary(userId, movieTitleId, "WATCHLIST"); await evaluateAchievements(userId); // stummer Erstlauf const eintrag = await prisma.libraryEntry.findFirstOrThrow({ where: { userId, titleId: movieTitleId }, select: { id: true }, }); await markMovieWatched(userId, eintrag.id); await prisma.libraryEntry.update({ where: { id: eintrag.id }, data: { currentRun: 2 } }); const zweiter = await evaluateAchievements(userId); assert.equal(zweiter.stumm, false); assert.ok( zweiter.neu.some((abzeichen) => abzeichen.key === "wiederholungstaeter"), "der dritte Durchlauf wird gemeldet", ); }); test("ein zweiter Lauf vergibt nichts doppelt", async () => { await addToLibrary(userId, movieTitleId, "WATCHLIST"); await evaluateAchievements(userId); const nachErstem = await prisma.achievement.count({ where: { userId } }); const zweiter = await evaluateAchievements(userId); assert.deepEqual(zweiter.neu, []); assert.equal(await prisma.achievement.count({ where: { userId } }), nachErstem); }); test("einmal erreicht bleibt erreicht — auch wenn die Bedingung wegfällt", async () => { const eintrag = await addToLibrary(userId, movieTitleId, "WATCHLIST"); await evaluateAchievements(userId); assert.ok( (await prisma.achievement.findFirst({ where: { userId, key: "erstkontakt" } })) != null, ); // Titel wieder entfernen: die Bedingung ist nicht mehr erfüllt. await prisma.libraryEntry.delete({ where: { id: eintrag.id } }); await evaluateAchievements(userId); assert.ok( (await prisma.achievement.findFirst({ where: { userId, key: "erstkontakt" } })) != null, "das Abzeichen bleibt stehen", ); }); test("Durchgemacht rechnet in der Zeitzone der Instanz", async () => { const eintrag = await addToLibrary(userId, seriesTitleId, "WATCHING"); const episoden = await prisma.episode.findMany({ where: { titleId: seriesTitleId }, take: 1, select: { id: true }, }); await markEpisodesWatched(userId, eintrag.id, [episoden[0]!.id]); // 03:30 Uhr in Europe/Berlin ist im Sommer 01:30 UTC. Wer in UTC rechnet, // verpasst das Abzeichen — genau der Fehler, der schon einmal das // Monatsdiagramm leergefegt hat. await prisma.watch.updateMany({ where: { userId }, data: { watchedAt: new Date("2026-07-15T01:30:00.000Z") }, }); await evaluateAchievements(userId); const nachts = await prisma.achievement.findFirst({ where: { userId, key: "durchgemacht" } }); assert.ok(nachts, "03:30 Ortszeit zählt als Nacht"); }); test("Tagsueber gesehen loest Durchgemacht nicht aus", async () => { const eintrag = await addToLibrary(userId, seriesTitleId, "WATCHING"); const episoden = await prisma.episode.findMany({ where: { titleId: seriesTitleId }, take: 1, select: { id: true }, }); await markEpisodesWatched(userId, eintrag.id, [episoden[0]!.id]); await prisma.watch.updateMany({ where: { userId }, data: { watchedAt: new Date("2026-07-15T12:00:00.000Z") }, }); await evaluateAchievements(userId); assert.equal(await prisma.achievement.count({ where: { userId, key: "durchgemacht" } }), 0); }); test("die Übersicht zeigt Erreichtes und Offenes", async () => { await addToLibrary(userId, movieTitleId, "WATCHLIST"); await evaluateAchievements(userId); const alle = await listAchievements(userId); assert.ok(alle.length > 15, "der Katalog ist vollständig sichtbar"); const erreicht = alle.filter((abzeichen) => abzeichen.unlockedAt !== null); const offen = alle.filter((abzeichen) => abzeichen.unlockedAt === null); assert.ok(erreicht.length >= 1); assert.ok(offen.length >= 1, "auch das noch nicht Erreichte steht da"); assert.deepEqual( await listUnlockedAchievements(userId), erreicht, "das öffentliche Profil zeigt nur Erreichtes", ); // Jedes Abzeichen trägt Name und Beschreibung — Farbe allein reicht nicht. assert.ok(alle.every((abzeichen) => abzeichen.name && abzeichen.beschreibung)); }); test("ein kuratierter Name gilt nur, wenn er zur Reihe passt", async () => { // Richtige Kennung mit passendem Namen: greift. assert.equal(kuratierteReihe(556, "Spider-Man Filmreihe")?.name, "Spider Bro"); // Dieselbe Kennung, aber TMDB führt etwas ganz anderes darunter — dann darf // der Eintrag nicht greifen. Genau so bleibt eine falsch abgeschriebene // Kennung folgenlos. assert.equal(kuratierteReihe(556, "Ocean's Filmreihe"), undefined); // Unbekannte Kennung: nichts. assert.equal(kuratierteReihe(999999, "Irgendwas"), undefined); // Ohne Namen lässt sich nichts prüfen — im Zweifel generisch. assert.equal(kuratierteReihe(556, null), undefined); }); test("Anzeigenamen entstehen aus Schlüssel und eingefrorenem Namen", () => { assert.equal(achievementName("marathon", null), "Marathon"); assert.equal(achievementName("reihe:556", "Spider-Man Filmreihe"), "Spider Bro"); assert.equal( achievementName("reihe:12345", "Nordlicht Filmreihe"), "Reihe komplett — Nordlicht Filmreihe", "ohne kuratierten Eintrag der generische Name", ); assert.equal( achievementName("universum:abc", "Sternenpfad"), "Universum vollendet — Sternenpfad", ); }); test("ein abgeschlossenes Universum wird zum Abzeichen", async () => { const eintrag = await addToLibrary(userId, movieTitleId, "WATCHLIST"); await markMovieWatched(userId, eintrag.id); const universum = await prisma.universe.create({ data: { ownerId: userId, name: "Testuniversum", slug: `test-achievements-${Date.now()}`, items: { create: [{ titleId: movieTitleId }] }, orders: { create: [{ name: "Erscheinungsreihenfolge", kind: "RELEASE", isDefault: true }] }, }, select: { id: true }, }); try { await evaluateAchievements(userId); const abzeichen = await prisma.achievement.findFirst({ where: { userId, key: `universum:${universum.id}` }, select: { label: true }, }); assert.ok(abzeichen, "das vollendete Universum zählt"); assert.equal(abzeichen.label, "Testuniversum", "der Name wird eingefroren"); // Umbenennen ändert das Abzeichen nicht mehr. await prisma.universe.update({ where: { id: universum.id }, data: { name: "Anders benannt" }, }); await evaluateAchievements(userId); const danach = await prisma.achievement.findFirstOrThrow({ where: { userId, key: `universum:${universum.id}` }, select: { label: true }, }); assert.equal(danach.label, "Testuniversum"); } finally { await prisma.universe.delete({ where: { id: universum.id } }); } });