From 90497ddc3b7c06c0c7420c34913e901a8261e74f Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Tue, 1 Sep 2026 19:39:32 +0200 Subject: [PATCH] test: Universen absichern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 12 Tests für Reihenfolgen, Fortschritt über Filme und Serien hinweg, Freigaben mit Lese- und Schreibrecht sowie die Rechteprüfung gegenüber Fremden. --- tests/universes.test.ts | 282 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 tests/universes.test.ts diff --git a/tests/universes.test.ts b/tests/universes.test.ts new file mode 100644 index 0000000..a3a5fcb --- /dev/null +++ b/tests/universes.test.ts @@ -0,0 +1,282 @@ +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, + getAccess, + getUniverse, + listUniverses, + 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); +});