Files
flimmer/tests/universes.test.ts
T
Florian Wilde 90497ddc3b test: Universen absichern
12 Tests für Reihenfolgen, Fortschritt über Filme und Serien hinweg,
Freigaben mit Lese- und Schreibrecht sowie die Rechteprüfung gegenüber
Fremden.
2026-09-01 19:39:32 +02:00

283 lines
11 KiB
TypeScript

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);
});