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, COLLECTION_ID } from "./fixtures/tmdb"; import { prisma } from "../src/lib/db"; import { importCollection, importSeries } from "../src/lib/catalog"; import { addToLibrary, markEpisodesWatched, markMovieWatched } from "../src/lib/library"; import { addCollection, addTitle, createOrder, createUniverse, deleteOrder, getAccess, getUniverse, listUniverses, setDefaultOrder, setOrderPositions, shareUniverse, trashUniverse, } from "../src/lib/universes"; /** * Prüft Universen: Reihenfolgen, Fortschritt aus Sicht des Betrachters, * Teilen und die Rechteprüfung. */ const stub = installTmdbStub(); const OWNER_EMAIL = "test-uni-owner@flimmer.invalid"; const GUEST_EMAIL = "test-uni-guest@flimmer.invalid"; let ownerId: string; let guestId: string; let seriesTitleId: string; let movieTitleId: string; let movie2TitleId: string; async function cleanup() { await prisma.user.deleteMany({ where: { email: { in: [OWNER_EMAIL, GUEST_EMAIL] } } }); await prisma.title.deleteMany({ where: { tmdbId: { gte: 999000, lt: 1000000 } } }); } before(async () => { await cleanup(); ownerId = ( await prisma.user.create({ data: { email: OWNER_EMAIL, name: "Besitzerin" }, select: { id: true } }) ).id; guestId = ( await prisma.user.create({ data: { email: GUEST_EMAIL, name: "Gast" }, select: { id: true } }) ).id; seriesTitleId = (await importSeries(SERIES_ID, { region: "DE" })).id; const { titles } = await importCollection(COLLECTION_ID, "DE"); movieTitleId = titles.find((title) => title.tmdbId === MOVIE_ID)!.id; movie2TitleId = titles.find((title) => title.tmdbId === MOVIE_2_ID)!.id; }); beforeEach(async () => { await prisma.universe.deleteMany({ where: { ownerId } }); await prisma.watch.deleteMany({ where: { userId: { in: [ownerId, guestId] } } }); await prisma.libraryEntry.deleteMany({ where: { userId: { in: [ownerId, guestId] } } }); }); after(async () => { await cleanup(); stub.restore(); await prisma.$disconnect(); }); test("ein neues Universum bringt die Erscheinungsreihenfolge mit", async () => { const universe = await createUniverse(ownerId, "Vorführer-Universum", "Zwei Filme, eine Rolle."); const view = await getUniverse(ownerId, universe.id); assert.ok(view); assert.equal(view.orders.length, 1); assert.equal(view.orders[0]?.kind, "RELEASE"); assert.equal(view.orders[0]?.isDefault, true); assert.equal(view.entries.length, 0); }); test("Slugs bleiben je Besitzer eindeutig", async () => { const first = await createUniverse(ownerId, "Sternenpfad"); const second = await createUniverse(ownerId, "Sternenpfad"); assert.equal(first.slug, "sternenpfad"); assert.equal(second.slug, "sternenpfad-2", "die Dublette wird durchgezählt"); }); test("Filme und Serien landen gemeinsam in einem Universum", async () => { const universe = await createUniverse(ownerId, "Gemischt"); await addTitle(ownerId, universe.id, seriesTitleId); await addTitle(ownerId, universe.id, movieTitleId); const view = await getUniverse(ownerId, universe.id); assert.equal(view?.entries.length, 2); assert.deepEqual( view?.entries.map((entry) => entry.type).sort(), ["MOVIE", "SERIES"], "Serien und Filme stehen gleichberechtigt nebeneinander", ); // Zweimal derselbe Titel legt keinen zweiten Eintrag an. await addTitle(ownerId, universe.id, movieTitleId); const again = await getUniverse(ownerId, universe.id); assert.equal(again?.entries.length, 2); }); test("TMDB-Collection lässt sich als Startpunkt übernehmen", async () => { const universe = await createUniverse(ownerId, "Aus Collection"); const added = await addCollection(ownerId, universe.id, COLLECTION_ID); assert.equal(added, 2); const view = await getUniverse(ownerId, universe.id); assert.deepEqual( view?.entries.map((entry) => entry.title), [ "Die Stille zwischen den Bildern", "Die Stille zwischen den Bildern II", ], "Erscheinungsreihenfolge ergibt sich aus den Daten", ); // Erneutes Übernehmen fügt nichts doppelt hinzu. assert.equal(await addCollection(ownerId, universe.id, COLLECTION_ID), 0); }); test("eigene Reihenfolge lässt sich frei sortieren", async () => { const universe = await createUniverse(ownerId, "Sortierbar"); await addCollection(ownerId, universe.id, COLLECTION_ID); await addTitle(ownerId, universe.id, seriesTitleId); const order = await createOrder(ownerId, universe.id, "Chronologisch"); const before = await getUniverse(ownerId, universe.id, order.id); assert.equal(before?.entries.length, 3); // Umdrehen const reversed = [...before!.entries].reverse().map((entry) => entry.itemId); await setOrderPositions(ownerId, order.id, reversed); const after = await getUniverse(ownerId, universe.id, order.id); assert.deepEqual( after?.entries.map((entry) => entry.itemId), reversed, "die eigene Reihenfolge steht so, wie sie gesetzt wurde", ); // Die Erscheinungsreihenfolge bleibt davon unberührt. const release = await getUniverse(ownerId, universe.id, before!.orders.find((o) => o.kind === "RELEASE")!.id); assert.notDeepEqual(release?.entries.map((entry) => entry.itemId), reversed); }); test("die Erscheinungsreihenfolge lässt sich nicht umsortieren", async () => { const universe = await createUniverse(ownerId, "Fest"); await addTitle(ownerId, universe.id, movieTitleId); const view = await getUniverse(ownerId, universe.id); const releaseOrder = view!.orders.find((order) => order.kind === "RELEASE")!; await assert.rejects( () => setOrderPositions(ownerId, releaseOrder.id, [view!.entries[0]!.itemId]), /ergibt sich aus den Daten/, ); }); test("Fortschritt und Restlaufzeit rechnen über Filme und Serien", async () => { const universe = await createUniverse(ownerId, "Fortschritt"); await addTitle(ownerId, universe.id, movieTitleId); await addTitle(ownerId, universe.id, seriesTitleId); const fresh = await getUniverse(ownerId, universe.id); // Film 118 min + Serie 5 Episoden (52+47+48+50+50 = 247 min) assert.equal(fresh?.progress.total, 2); assert.equal(fresh?.progress.completed, 0); assert.equal(fresh?.progress.totalMinutes, 118 + 247); assert.equal(fresh?.progress.remainingMinutes, 118 + 247); // Erscheinungsreihenfolge: die Serie (2023) läuft vor dem Film (2024). assert.equal(fresh?.nextUp?.title, "Nordlicht"); // Film sehen const movieEntry = await addToLibrary(ownerId, movieTitleId); await markMovieWatched(ownerId, movieEntry.id); const afterMovie = await getUniverse(ownerId, universe.id); assert.equal(afterMovie?.progress.completed, 1); assert.equal(afterMovie?.progress.percent, 50); assert.equal(afterMovie?.progress.watchedMinutes, 118); assert.equal(afterMovie?.progress.remainingMinutes, 247); assert.equal( afterMovie?.nextUp?.type, "SERIES", "der Film ist durch, offen bleibt die Serie", ); // Zwei Episoden der Serie const seriesEntry = await addToLibrary(ownerId, seriesTitleId); const episodes = await prisma.episode.findMany({ where: { titleId: seriesTitleId, seasonNumber: { gt: 0 } }, orderBy: [{ seasonNumber: "asc" }, { episodeNumber: "asc" }], select: { id: true }, }); await markEpisodesWatched(ownerId, seriesEntry.id, [episodes[0]!.id, episodes[1]!.id]); const afterEpisodes = await getUniverse(ownerId, universe.id); const series = afterEpisodes!.entries.find((entry) => entry.type === "SERIES")!; assert.equal(series.watchedEpisodes, 2); assert.equal(series.totalEpisodes, 5); assert.equal(series.completed, false); assert.ok( series.remainingMinutes < 247 && series.remainingMinutes > 0, "die Restlaufzeit schrumpft anteilig", ); }); test("geteilte Universen zeigen jedem den eigenen Fortschritt", async () => { const universe = await createUniverse(ownerId, "Geteilt"); await addTitle(ownerId, universe.id, movieTitleId); const entry = await addToLibrary(ownerId, movieTitleId); await markMovieWatched(ownerId, entry.id); await shareUniverse(ownerId, universe.id, GUEST_EMAIL, "READ"); const ownerView = await getUniverse(ownerId, universe.id); const guestView = await getUniverse(guestId, universe.id); assert.equal(ownerView?.progress.completed, 1); assert.equal(guestView?.progress.completed, 0, "der Gast hat den Film noch nicht gesehen"); assert.equal(guestView?.canWrite, false, "Leserecht bleibt Leserecht"); assert.equal(guestView?.isOwner, false); }); test("Schreibrecht lässt sich vergeben, Löschen bleibt beim Besitzer", async () => { const universe = await createUniverse(ownerId, "Gemeinsam"); await shareUniverse(ownerId, universe.id, GUEST_EMAIL, "WRITE"); const access = await getAccess(guestId, universe.id); assert.equal(access.canWrite, true); assert.equal(access.isOwner, false); // Mit Schreibrecht darf der Gast Titel aufnehmen … await addTitle(guestId, universe.id, movieTitleId); const view = await getUniverse(guestId, universe.id); assert.equal(view?.entries.length, 1); // … aber nicht löschen. await assert.rejects(() => trashUniverse(guestId, universe.id), /Nur wer das Universum angelegt hat/); }); test("Fremde sehen ein Universum gar nicht", async () => { const universe = await createUniverse(ownerId, "Privat"); const access = await getAccess(guestId, universe.id); assert.equal(access.canRead, false); assert.equal(await getUniverse(guestId, universe.id), null); await assert.rejects(() => addTitle(guestId, universe.id, movieTitleId), /gibt es nicht/); }); test("die Übersicht listet eigene und geteilte Universen", async () => { const own = await createUniverse(ownerId, "Meins"); await addTitle(ownerId, own.id, movieTitleId); await addTitle(ownerId, own.id, movie2TitleId); await shareUniverse(ownerId, own.id, GUEST_EMAIL, "READ"); const entry = await addToLibrary(guestId, movieTitleId); await markMovieWatched(guestId, entry.id); const guestList = await listUniverses(guestId); assert.equal(guestList.length, 1); assert.equal(guestList[0]?.isOwner, false); assert.equal(guestList[0]?.ownerName, "Besitzerin"); assert.equal(guestList[0]?.itemCount, 2); assert.equal(guestList[0]?.completed, 1, "der Gast hat einen der beiden Filme gesehen"); assert.equal(guestList[0]?.percent, 50); }); test("gelöschte Universen verschwinden aus der Übersicht", async () => { const universe = await createUniverse(ownerId, "Weg damit"); await trashUniverse(ownerId, universe.id); assert.equal(await getUniverse(ownerId, universe.id), null); const list = await listUniverses(ownerId); assert.equal(list.find((item) => item.id === universe.id), undefined); }); // --- Standardreihenfolge ------------------------------------------------------ test("die Standardreihenfolge lässt sich umstellen und bleibt eindeutig", async () => { const universe = await createUniverse(ownerId, "Standardwechsel"); await addCollection(ownerId, universe.id, COLLECTION_ID); const order = await createOrder(ownerId, universe.id, "Chronologisch"); // Frisch angelegt führt die Erscheinungsreihenfolge. const vorher = await getUniverse(ownerId, universe.id); assert.equal(vorher?.orders.find((o) => o.isDefault)?.kind, "RELEASE"); await setDefaultOrder(ownerId, order.id); // Ohne Angabe in der Adresse erscheint jetzt die eigene Reihenfolge. const nachher = await getUniverse(ownerId, universe.id); assert.equal(nachher?.activeOrderId, order.id); const standards = nachher!.orders.filter((o) => o.isDefault); assert.equal(standards.length, 1, "es steht immer genau ein Standard"); assert.equal(standards[0]!.id, order.id, "und zwar der zuletzt gesetzte"); }); test("die Adresse schlägt die Standardreihenfolge", async () => { const universe = await createUniverse(ownerId, "Adressvorrang"); await addCollection(ownerId, universe.id, COLLECTION_ID); const order = await createOrder(ownerId, universe.id, "Chronologisch"); await setDefaultOrder(ownerId, order.id); const release = (await getUniverse(ownerId, universe.id))!.orders.find( (o) => o.kind === "RELEASE", )!; const view = await getUniverse(ownerId, universe.id, release.id); assert.equal(view?.activeOrderId, release.id, "die angeforderte gewinnt gegen den Standard"); }); test("wird die Standardreihenfolge gelöscht, übernimmt die Erscheinungsreihenfolge", async () => { const universe = await createUniverse(ownerId, "Rueckfall"); await addCollection(ownerId, universe.id, COLLECTION_ID); const order = await createOrder(ownerId, universe.id, "Chronologisch"); await setDefaultOrder(ownerId, order.id); await deleteOrder(ownerId, order.id); const view = await getUniverse(ownerId, universe.id); const standards = view!.orders.filter((o) => o.isDefault); assert.equal(standards.length, 1, "das Universum bleibt nicht ohne Standard zurück"); assert.equal(standards[0]!.kind, "RELEASE"); assert.equal(view?.activeOrderId, standards[0]!.id); }); test("den Standard setzen darf nur, wer schreiben darf", async () => { const universe = await createUniverse(ownerId, "Rechteprobe"); await addCollection(ownerId, universe.id, COLLECTION_ID); const order = await createOrder(ownerId, universe.id, "Chronologisch"); await shareUniverse(ownerId, universe.id, GUEST_EMAIL, "READ"); await assert.rejects(() => setDefaultOrder(guestId, order.id), /nur ansehen/); // Der Standard steht danach unverändert auf der Erscheinungsreihenfolge. const unveraendert = await getUniverse(ownerId, universe.id); assert.equal(unveraendert?.orders.find((o) => o.isDefault)?.kind, "RELEASE"); // Mit Schreibrecht geht es dann. await shareUniverse(ownerId, universe.id, GUEST_EMAIL, "WRITE"); await setDefaultOrder(guestId, order.id); assert.equal((await getUniverse(guestId, universe.id))?.activeOrderId, order.id); });