test: Kern-Tracking absichern
14 Tests für Aufnehmen, Abhaken, Fortschritt ohne Specials, Rewatch, Papierkorb, Rückgängig und den Zugriffsschutz zwischen Nutzern. Die Testdateien laufen jetzt seriell: parallel räumten sie sich gegenseitig die gemeinsamen Katalog-Fixtures weg.
This commit is contained in:
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "flimmer",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"description": "Flimmer — selbst hostbarer Serien- und Film-Tracker",
|
||||
"license": "MIT",
|
||||
@@ -16,7 +16,7 @@
|
||||
"db:seed": "tsx prisma/seed.ts",
|
||||
"icons": "tsx scripts/generate-icons.ts",
|
||||
"screenshots": "tsx scripts/screenshots.ts",
|
||||
"test": "node --env-file-if-exists=.env --conditions=react-server --import tsx --test tests/*.test.ts"
|
||||
"test": "node --env-file-if-exists=.env --conditions=react-server --test-concurrency=1 --import tsx --test tests/*.test.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/adapter-pg": "^7.10.0",
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
import "./setup";
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import test, { after, before, beforeEach } from "node:test";
|
||||
|
||||
import { installTmdbStub, SERIES_ID, MOVIE_ID } from "./fixtures/tmdb";
|
||||
import { prisma } from "../src/lib/db";
|
||||
import { importMovie, importSeries } from "../src/lib/catalog";
|
||||
import {
|
||||
addToLibrary,
|
||||
getNextEpisode,
|
||||
getProgress,
|
||||
getWatchedEpisodeIds,
|
||||
markEpisodesWatched,
|
||||
markMovieWatched,
|
||||
markSeasonWatched,
|
||||
markUpTo,
|
||||
moveToTrash,
|
||||
restoreFromTrash,
|
||||
setStatus,
|
||||
startRewatch,
|
||||
undoWatches,
|
||||
unmarkEpisodes,
|
||||
updateEntryDetails,
|
||||
watchNextEpisode,
|
||||
} from "../src/lib/library";
|
||||
|
||||
/**
|
||||
* Prüft das Kern-Tracking: Aufnehmen, Abhaken, Fortschritt, Rewatch.
|
||||
* Läuft gegen die Entwicklungsdatenbank mit eigenem Testkonto und
|
||||
* nachgebildeten TMDB-Daten.
|
||||
*/
|
||||
|
||||
const stub = installTmdbStub();
|
||||
const TEST_EMAIL = "test-library@flimmer.invalid";
|
||||
const OTHER_EMAIL = "test-library-other@flimmer.invalid";
|
||||
|
||||
let userId: string;
|
||||
let seriesTitleId: string;
|
||||
let movieTitleId: string;
|
||||
|
||||
async function cleanup() {
|
||||
await prisma.user.deleteMany({ where: { email: { in: [TEST_EMAIL, OTHER_EMAIL] } } });
|
||||
await prisma.title.deleteMany({ where: { tmdbId: { gte: 999000, lt: 1000000 } } });
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
await cleanup();
|
||||
const user = await prisma.user.create({
|
||||
data: { email: TEST_EMAIL, name: "Testkonto", region: "DE" },
|
||||
select: { id: true },
|
||||
});
|
||||
userId = user.id;
|
||||
|
||||
seriesTitleId = (await importSeries(SERIES_ID, { region: "DE" })).id;
|
||||
movieTitleId = (await importMovie(MOVIE_ID, "DE")).id;
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Jeder Test startet mit einer leeren Bibliothek, aber demselben Katalog.
|
||||
await prisma.watch.deleteMany({ where: { userId } });
|
||||
await prisma.libraryEntry.deleteMany({ where: { userId } });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await cleanup();
|
||||
stub.restore();
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
||||
/** Alle regulären Episoden der Testserie, in Reihenfolge. */
|
||||
async function regularEpisodes() {
|
||||
return prisma.episode.findMany({
|
||||
where: { titleId: seriesTitleId, seasonNumber: { gt: 0 } },
|
||||
orderBy: [{ seasonNumber: "asc" }, { episodeNumber: "asc" }],
|
||||
select: { id: true, seasonNumber: true, episodeNumber: true, runtime: true },
|
||||
});
|
||||
}
|
||||
|
||||
test("Titel aufnehmen ist wiederholbar und schlägt den Streamingdienst vor", async () => {
|
||||
const first = await addToLibrary(userId, seriesTitleId, "WATCHLIST");
|
||||
const second = await addToLibrary(userId, seriesTitleId, "WATCHING");
|
||||
|
||||
assert.equal(first.id, second.id, "kein zweiter Eintrag für denselben Titel");
|
||||
assert.equal(first.status, "WATCHLIST");
|
||||
assert.equal(
|
||||
first.whereToWatch,
|
||||
"Netflix",
|
||||
"der erste Abo-Dienst der Region wird als Startwert übernommen",
|
||||
);
|
||||
});
|
||||
|
||||
test("Papierkorb behält den Fortschritt", async () => {
|
||||
const entry = await addToLibrary(userId, seriesTitleId);
|
||||
const episodes = await regularEpisodes();
|
||||
await markEpisodesWatched(userId, entry.id, [episodes[0]!.id]);
|
||||
|
||||
await moveToTrash(userId, entry.id);
|
||||
const trashed = await prisma.libraryEntry.findUniqueOrThrow({ where: { id: entry.id } });
|
||||
assert.ok(trashed.deletedAt, "liegt im Papierkorb statt gelöscht zu sein");
|
||||
|
||||
// Erneutes Aufnehmen belebt denselben Eintrag wieder.
|
||||
const revived = await addToLibrary(userId, seriesTitleId);
|
||||
assert.equal(revived.id, entry.id);
|
||||
assert.equal(revived.deletedAt, null);
|
||||
|
||||
const progress = await getProgress(userId, entry.id);
|
||||
assert.equal(progress.watched, 1, "die Sichtung hat den Umweg überlebt");
|
||||
|
||||
await moveToTrash(userId, entry.id);
|
||||
await restoreFromTrash(userId, entry.id);
|
||||
const restored = await prisma.libraryEntry.findUniqueOrThrow({ where: { id: entry.id } });
|
||||
assert.equal(restored.deletedAt, null);
|
||||
});
|
||||
|
||||
test("Fortschritt zählt Specials nicht mit", async () => {
|
||||
const entry = await addToLibrary(userId, seriesTitleId);
|
||||
const progress = await getProgress(userId, entry.id);
|
||||
|
||||
assert.equal(progress.total, 5, "5 reguläre Episoden, die Weihnachtsfolge zählt nicht");
|
||||
assert.equal(progress.watched, 0);
|
||||
assert.equal(progress.percent, 0);
|
||||
|
||||
const special = await prisma.episode.findFirstOrThrow({
|
||||
where: { titleId: seriesTitleId, seasonNumber: 0 },
|
||||
});
|
||||
await markEpisodesWatched(userId, entry.id, [special.id]);
|
||||
|
||||
const after = await getProgress(userId, entry.id);
|
||||
assert.equal(after.watched, 0, "ein Special bewegt den Fortschrittsbalken nicht");
|
||||
assert.ok(after.minutesWatched > 0, "die Schauzeit wird trotzdem gezählt");
|
||||
});
|
||||
|
||||
test("nächste Episode folgt der Reihenfolge und überspringt Specials", async () => {
|
||||
const entry = await addToLibrary(userId, seriesTitleId);
|
||||
|
||||
const first = await getNextEpisode(userId, entry.id);
|
||||
assert.equal(first?.seasonNumber, 1);
|
||||
assert.equal(first?.episodeNumber, 1);
|
||||
|
||||
await watchNextEpisode(userId, entry.id);
|
||||
const second = await getNextEpisode(userId, entry.id);
|
||||
assert.equal(second?.episodeNumber, 2);
|
||||
|
||||
// Über die Staffelgrenze hinweg
|
||||
await markSeasonWatched(
|
||||
userId,
|
||||
entry.id,
|
||||
(await prisma.season.findFirstOrThrow({ where: { titleId: seriesTitleId, seasonNumber: 1 } }))
|
||||
.id,
|
||||
);
|
||||
const third = await getNextEpisode(userId, entry.id);
|
||||
assert.equal(third?.seasonNumber, 2, "nach Staffel 1 kommt Staffel 2");
|
||||
assert.equal(third?.episodeNumber, 1);
|
||||
});
|
||||
|
||||
test("Status ergibt sich aus den Sichtungen", async () => {
|
||||
const entry = await addToLibrary(userId, seriesTitleId, "WATCHLIST");
|
||||
const episodes = await regularEpisodes();
|
||||
|
||||
const afterFirst = await markEpisodesWatched(userId, entry.id, [episodes[0]!.id]);
|
||||
assert.equal(afterFirst.status, "WATCHING", "die erste Episode startet die Serie");
|
||||
|
||||
const afterAll = await markEpisodesWatched(
|
||||
userId,
|
||||
entry.id,
|
||||
episodes.map((episode) => episode.id),
|
||||
);
|
||||
assert.equal(afterAll.status, "COMPLETED", "die letzte Episode schließt sie ab");
|
||||
|
||||
const completed = await prisma.libraryEntry.findUniqueOrThrow({ where: { id: entry.id } });
|
||||
assert.ok(completed.startedAt && completed.finishedAt);
|
||||
|
||||
await unmarkEpisodes(userId, entry.id, [episodes[0]!.id]);
|
||||
const reopened = await prisma.libraryEntry.findUniqueOrThrow({ where: { id: entry.id } });
|
||||
assert.equal(reopened.status, "WATCHING", "eine zurückgenommene Episode öffnet sie wieder");
|
||||
assert.equal(reopened.finishedAt, null);
|
||||
});
|
||||
|
||||
test("Pausiert und Abgebrochen bleiben stehen", async () => {
|
||||
const entry = await addToLibrary(userId, seriesTitleId);
|
||||
const episodes = await regularEpisodes();
|
||||
|
||||
await setStatus(userId, entry.id, "PAUSED");
|
||||
const result = await markEpisodesWatched(userId, entry.id, [episodes[0]!.id]);
|
||||
assert.equal(result.status, "PAUSED", "eine Sichtung hebt die Pause nicht selbsttätig auf");
|
||||
|
||||
await setStatus(userId, entry.id, "DROPPED");
|
||||
const dropped = await markEpisodesWatched(userId, entry.id, [episodes[1]!.id]);
|
||||
assert.equal(dropped.status, "DROPPED");
|
||||
});
|
||||
|
||||
test("„bis hierhin gesehen\" hakt alles davor ab", async () => {
|
||||
const entry = await addToLibrary(userId, seriesTitleId);
|
||||
const episodes = await regularEpisodes();
|
||||
|
||||
// Staffel 2, Episode 1 → alles aus Staffel 1 plus diese eine
|
||||
const target = episodes.find((e) => e.seasonNumber === 2 && e.episodeNumber === 1)!;
|
||||
const result = await markUpTo(userId, entry.id, target.id);
|
||||
|
||||
assert.equal(result.watchIds.length, 4, "3 aus Staffel 1 und die Zielepisode");
|
||||
const progress = await getProgress(userId, entry.id);
|
||||
assert.equal(progress.watched, 4);
|
||||
assert.equal(progress.percent, 80);
|
||||
});
|
||||
|
||||
test("doppeltes Abhaken legt keine zweite Sichtung an", async () => {
|
||||
const entry = await addToLibrary(userId, seriesTitleId);
|
||||
const episodes = await regularEpisodes();
|
||||
|
||||
await markEpisodesWatched(userId, entry.id, [episodes[0]!.id]);
|
||||
const again = await markEpisodesWatched(userId, entry.id, [episodes[0]!.id]);
|
||||
|
||||
assert.equal(again.watchIds.length, 0);
|
||||
const count = await prisma.watch.count({ where: { userId, episodeId: episodes[0]!.id } });
|
||||
assert.equal(count, 1);
|
||||
});
|
||||
|
||||
test("Rückgängig entfernt genau die eben angelegten Sichtungen", async () => {
|
||||
const entry = await addToLibrary(userId, seriesTitleId);
|
||||
const episodes = await regularEpisodes();
|
||||
|
||||
await markEpisodesWatched(userId, entry.id, [episodes[0]!.id]);
|
||||
const season = await markSeasonWatched(
|
||||
userId,
|
||||
entry.id,
|
||||
(await prisma.season.findFirstOrThrow({ where: { titleId: seriesTitleId, seasonNumber: 1 } }))
|
||||
.id,
|
||||
);
|
||||
|
||||
assert.equal(season.watchIds.length, 2, "die bereits gesehene Episode ist nicht dabei");
|
||||
|
||||
await undoWatches(userId, entry.id, season.watchIds);
|
||||
const progress = await getProgress(userId, entry.id);
|
||||
assert.equal(progress.watched, 1, "die erste Episode bleibt gesehen");
|
||||
});
|
||||
|
||||
test("Rewatch setzt den Fortschritt zurück, behält aber die Schauzeit", async () => {
|
||||
const entry = await addToLibrary(userId, seriesTitleId);
|
||||
const episodes = await regularEpisodes();
|
||||
await markEpisodesWatched(
|
||||
userId,
|
||||
entry.id,
|
||||
episodes.map((episode) => episode.id),
|
||||
);
|
||||
|
||||
const firstRun = await getProgress(userId, entry.id);
|
||||
assert.equal(firstRun.percent, 100);
|
||||
|
||||
await startRewatch(userId, entry.id);
|
||||
const secondRun = await getProgress(userId, entry.id);
|
||||
assert.equal(secondRun.watched, 0, "der neue Durchlauf beginnt bei null");
|
||||
assert.equal(secondRun.minutesWatched, 0);
|
||||
|
||||
const total = await prisma.watch.aggregate({ where: { userId }, _sum: { minutes: true } });
|
||||
assert.equal(
|
||||
total._sum.minutes,
|
||||
firstRun.minutesWatched,
|
||||
"die Sichtungen des ersten Durchlaufs bleiben für die Statistik erhalten",
|
||||
);
|
||||
|
||||
// Dieselbe Episode lässt sich im neuen Durchlauf erneut abhaken.
|
||||
const result = await markEpisodesWatched(userId, entry.id, [episodes[0]!.id]);
|
||||
assert.equal(result.watchIds.length, 1);
|
||||
const watched = await getWatchedEpisodeIds(userId, entry.id);
|
||||
assert.deepEqual([...watched], [episodes[0]!.id], "die Häkchen zeigen nur den aktuellen Durchlauf");
|
||||
});
|
||||
|
||||
test("verbleibende Laufzeit schrumpft mit dem Fortschritt", async () => {
|
||||
const entry = await addToLibrary(userId, seriesTitleId);
|
||||
const episodes = await regularEpisodes();
|
||||
|
||||
const before = await getProgress(userId, entry.id);
|
||||
const sum = episodes.reduce((total, episode) => total + (episode.runtime ?? 0), 0);
|
||||
assert.equal(before.minutesLeft, sum);
|
||||
|
||||
await markEpisodesWatched(userId, entry.id, [episodes[0]!.id]);
|
||||
const after = await getProgress(userId, entry.id);
|
||||
assert.equal(after.minutesLeft, sum - (episodes[0]!.runtime ?? 0));
|
||||
assert.equal(after.minutesWatched, episodes[0]!.runtime);
|
||||
});
|
||||
|
||||
test("Filme: gesehen, Rewatch und Laufzeit", async () => {
|
||||
const entry = await addToLibrary(userId, movieTitleId, "WATCHLIST");
|
||||
|
||||
const result = await markMovieWatched(userId, entry.id);
|
||||
assert.equal(result.status, "COMPLETED");
|
||||
assert.equal(result.watchIds.length, 1);
|
||||
|
||||
const progress = await getProgress(userId, entry.id);
|
||||
assert.equal(progress.percent, 100);
|
||||
assert.equal(progress.minutesWatched, 118);
|
||||
assert.equal(progress.minutesLeft, 0);
|
||||
|
||||
const again = await markMovieWatched(userId, entry.id);
|
||||
assert.equal(again.watchIds.length, 0, "zweimal im selben Durchlauf zählt nicht doppelt");
|
||||
|
||||
await startRewatch(userId, entry.id);
|
||||
await markMovieWatched(userId, entry.id);
|
||||
const runs = await prisma.watch.count({ where: { userId, entryId: entry.id } });
|
||||
assert.equal(runs, 2, "der zweite Durchlauf wird eigenständig gezählt");
|
||||
});
|
||||
|
||||
test("Bewertung, Notiz und Fundort lassen sich pflegen", async () => {
|
||||
const entry = await addToLibrary(userId, movieTitleId);
|
||||
|
||||
const updated = await updateEntryDetails(userId, entry.id, {
|
||||
rating: 9,
|
||||
notes: " Zweimal gesehen, beim zweiten Mal besser. ",
|
||||
whereToWatch: "Blu-ray",
|
||||
favorite: true,
|
||||
});
|
||||
|
||||
assert.equal(updated.rating, 9);
|
||||
assert.equal(updated.notes, "Zweimal gesehen, beim zweiten Mal besser.", "Rand wird beschnitten");
|
||||
assert.equal(updated.whereToWatch, "Blu-ray");
|
||||
assert.equal(updated.favorite, true);
|
||||
|
||||
await assert.rejects(
|
||||
() => updateEntryDetails(userId, entry.id, { rating: 11 }),
|
||||
/zwischen 1 und 10/,
|
||||
);
|
||||
});
|
||||
|
||||
test("fremde Einträge sind nicht erreichbar", async () => {
|
||||
const other = await prisma.user.create({
|
||||
data: { email: OTHER_EMAIL, name: "Andere" },
|
||||
select: { id: true },
|
||||
});
|
||||
const entry = await addToLibrary(userId, movieTitleId);
|
||||
|
||||
await assert.rejects(
|
||||
() => updateEntryDetails(other.id, entry.id, { rating: 1 }),
|
||||
/gehört nicht zu deiner Bibliothek/,
|
||||
);
|
||||
|
||||
await prisma.user.delete({ where: { id: other.id } });
|
||||
});
|
||||
Reference in New Issue
Block a user