CI / pruefen (pull_request) Canceled after 0s
Ein Universum kann mehrere Reihenfolgen haben, aber welche beim Oeffnen
erschien, liess sich nicht bestimmen — es war immer die
Erscheinungsreihenfolge. Das Feld isDefault gab es schon und wurde beim
Oeffnen auch ausgewertet; es fehlte nur der Weg, es zu setzen.
Der Standard gehoert dem Universum, nicht der einzelnen Person: wer
Schreibrecht hat, legt ihn fuer alle fest — so wie die Reihenfolgen selbst
auch geteilt sind. Die Rangfolge bleibt dadurch die schlichte: die Adresse
schlaegt den Standard schlaegt die erste.
setDefaultOrder() setzt zurueck und neu in einer Transaktion, damit nie zwei
Standards nebeneinander stehen. deleteOrder() faengt jetzt den Fall ab, dass
die Standardreihenfolge geloescht wird — dann uebernimmt die
Erscheinungsreihenfolge, statt dass die Auswahl auf "die erste" faellt.
Ueber die Schnittstelle geht es per PATCH /api/v1/universen/{id}. Der
Endpunkt prueft, dass die Reihenfolge zu genau diesem Universum gehoert:
ohne das setzte ein PATCH auf A den Standard in B, weil die Domaenenschicht
das Universum aus der Reihenfolge ableitet.
Vier Tests dazu: Umstellen samt Eindeutigkeit, Vorrang der Adresse,
Rueckfall beim Loeschen, Rechtepruefung. Klickweg und Endpunkt gegen einen
Produktionsbau geprueft.
Refs #4
354 lines
14 KiB
TypeScript
354 lines
14 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,
|
|
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);
|
|
});
|