mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-16 01:05:42 -04:00
736 lines
22 KiB
TypeScript
736 lines
22 KiB
TypeScript
import { readdirSync, readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
|
|
import {
|
|
importJobs,
|
|
titles,
|
|
userEpisodeWatches,
|
|
userMovieWatches,
|
|
userRatings,
|
|
userTitleStatus,
|
|
} from "@sofa/db/schema";
|
|
import {
|
|
clearAllTables,
|
|
eq,
|
|
insertMovieWatch,
|
|
insertStatus,
|
|
insertTitle,
|
|
insertTvShow,
|
|
insertUser,
|
|
testClient,
|
|
testDb,
|
|
} from "@sofa/test/db";
|
|
import * as tmdbClient from "@sofa/tmdb/client";
|
|
|
|
import type { NormalizedImport } from "../src/imports/parsers";
|
|
import { processImportJob, readImportJob } from "../src/imports/processor";
|
|
|
|
// ── Helpers ─────────────────────────────────────────────────────────
|
|
|
|
/** Insert a fully-fetched movie title (lastFetchedAt set so metadata won't re-fetch). */
|
|
function insertMovieTitle(id: string, tmdbId: number, movieTitle = "Test Movie") {
|
|
testDb
|
|
.insert(titles)
|
|
.values({
|
|
id,
|
|
tmdbId,
|
|
type: "movie",
|
|
title: movieTitle,
|
|
lastFetchedAt: new Date(),
|
|
})
|
|
.run();
|
|
return id;
|
|
}
|
|
|
|
/** Insert a fully-fetched TV title with seasons/episodes (lastFetchedAt set). */
|
|
function insertTvShowWithFetchedAt(
|
|
titleId: string,
|
|
tmdbId: number,
|
|
seasonCount = 1,
|
|
epsPerSeason = 3,
|
|
) {
|
|
const result = insertTvShow(titleId, tmdbId, seasonCount, epsPerSeason);
|
|
// Mark as fully fetched so getOrFetchTitleByTmdbId skips TMDB API calls
|
|
testDb.update(titles).set({ lastFetchedAt: new Date() }).where(eq(titles.id, titleId)).run();
|
|
return result;
|
|
}
|
|
|
|
/** Create an import job row in the DB and return its ID. */
|
|
function createJob(
|
|
userId: string,
|
|
payload: NormalizedImport,
|
|
options: {
|
|
importWatches?: boolean;
|
|
importWatchlist?: boolean;
|
|
importRatings?: boolean;
|
|
} = {},
|
|
): string {
|
|
const id = `job-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
testDb
|
|
.insert(importJobs)
|
|
.values({
|
|
id,
|
|
userId,
|
|
source: payload.source,
|
|
status: "pending",
|
|
payload: JSON.stringify(payload),
|
|
importWatches: options.importWatches ?? true,
|
|
importWatchlist: options.importWatchlist ?? true,
|
|
importRatings: options.importRatings ?? true,
|
|
createdAt: new Date(),
|
|
})
|
|
.run();
|
|
return id;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
clearAllTables();
|
|
});
|
|
|
|
// ── Movie Import ────────────────────────────────────────────────────
|
|
|
|
describe("processImportJob — movies", () => {
|
|
test("imports a movie with direct tmdbId", async () => {
|
|
const userId = insertUser();
|
|
insertMovieTitle("movie-1", 550, "Fight Club");
|
|
|
|
const payload: NormalizedImport = {
|
|
source: "trakt",
|
|
movies: [
|
|
{
|
|
tmdbId: 550,
|
|
title: "Fight Club",
|
|
year: 1999,
|
|
watchedAt: "2024-06-15T20:00:00Z",
|
|
},
|
|
],
|
|
episodes: [],
|
|
watchlist: [],
|
|
ratings: [],
|
|
};
|
|
|
|
const jobId = createJob(userId, payload);
|
|
await processImportJob(jobId);
|
|
|
|
const job = readImportJob(jobId);
|
|
expect(job.status).toBe("success");
|
|
expect(job.importedCount).toBe(1);
|
|
expect(job.skippedCount).toBe(0);
|
|
expect(job.failedCount).toBe(0);
|
|
|
|
// Verify watch record created
|
|
const watches = testDb
|
|
.select()
|
|
.from(userMovieWatches)
|
|
.where(eq(userMovieWatches.userId, userId))
|
|
.all();
|
|
expect(watches).toHaveLength(1);
|
|
expect(watches[0].titleId).toBe("movie-1");
|
|
});
|
|
|
|
test("deduplicates existing movie watches", async () => {
|
|
const userId = insertUser();
|
|
insertMovieTitle("movie-1", 550, "Fight Club");
|
|
insertMovieWatch(userId, "movie-1");
|
|
|
|
const payload: NormalizedImport = {
|
|
source: "trakt",
|
|
movies: [{ tmdbId: 550, title: "Fight Club" }],
|
|
episodes: [],
|
|
watchlist: [],
|
|
ratings: [],
|
|
};
|
|
|
|
const jobId = createJob(userId, payload);
|
|
await processImportJob(jobId);
|
|
|
|
const job = readImportJob(jobId);
|
|
expect(job.status).toBe("success");
|
|
expect(job.importedCount).toBe(0);
|
|
expect(job.skippedCount).toBe(1);
|
|
|
|
// Should still be just the original watch
|
|
const watches = testDb
|
|
.select()
|
|
.from(userMovieWatches)
|
|
.where(eq(userMovieWatches.userId, userId))
|
|
.all();
|
|
expect(watches).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
// ── Episode Import ──────────────────────────────────────────────────
|
|
|
|
describe("processImportJob — episodes", () => {
|
|
test("imports episodes for a pre-seeded TV show", async () => {
|
|
const userId = insertUser();
|
|
insertTvShowWithFetchedAt("tv-1", 1399, 1, 3);
|
|
|
|
const payload: NormalizedImport = {
|
|
source: "trakt",
|
|
movies: [],
|
|
episodes: [
|
|
{
|
|
showTmdbId: 1399,
|
|
showTitle: "Test Show",
|
|
seasonNumber: 1,
|
|
episodeNumber: 1,
|
|
watchedAt: "2024-01-10T20:00:00Z",
|
|
},
|
|
{
|
|
showTmdbId: 1399,
|
|
showTitle: "Test Show",
|
|
seasonNumber: 1,
|
|
episodeNumber: 2,
|
|
watchedAt: "2024-01-11T20:00:00Z",
|
|
},
|
|
],
|
|
watchlist: [],
|
|
ratings: [],
|
|
};
|
|
|
|
const jobId = createJob(userId, payload);
|
|
await processImportJob(jobId);
|
|
|
|
const job = readImportJob(jobId);
|
|
expect(job.status).toBe("success");
|
|
expect(job.importedCount).toBe(2);
|
|
expect(job.failedCount).toBe(0);
|
|
|
|
const watches = testDb
|
|
.select()
|
|
.from(userEpisodeWatches)
|
|
.where(eq(userEpisodeWatches.userId, userId))
|
|
.all();
|
|
expect(watches).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
// ── Watchlist Import ────────────────────────────────────────────────
|
|
|
|
describe("processImportJob — watchlist", () => {
|
|
test("sets title status to watchlist", async () => {
|
|
const userId = insertUser();
|
|
insertMovieTitle("movie-wl", 999, "Watchlist Movie");
|
|
|
|
const payload: NormalizedImport = {
|
|
source: "simkl",
|
|
movies: [],
|
|
episodes: [],
|
|
watchlist: [{ tmdbId: 999, title: "Watchlist Movie", type: "movie" }],
|
|
ratings: [],
|
|
};
|
|
|
|
const jobId = createJob(userId, payload, {
|
|
importWatches: false,
|
|
importWatchlist: true,
|
|
importRatings: false,
|
|
});
|
|
await processImportJob(jobId);
|
|
|
|
const job = readImportJob(jobId);
|
|
expect(job.status).toBe("success");
|
|
expect(job.importedCount).toBe(1);
|
|
|
|
const statusRow = testDb
|
|
.select()
|
|
.from(userTitleStatus)
|
|
.where(eq(userTitleStatus.userId, userId))
|
|
.all();
|
|
expect(statusRow).toHaveLength(1);
|
|
expect(statusRow[0].status).toBe("watchlist");
|
|
expect(statusRow[0].titleId).toBe("movie-wl");
|
|
});
|
|
});
|
|
|
|
// ── Status Normalization ────────────────────────────────────────────
|
|
|
|
describe("processImportJob — status normalization", () => {
|
|
const watchlistOnly = {
|
|
importWatches: false,
|
|
importWatchlist: true,
|
|
importRatings: false,
|
|
};
|
|
|
|
test("stores TV 'completed' as 'in_progress'", async () => {
|
|
const userId = insertUser();
|
|
insertTvShowWithFetchedAt("tv-c", 500);
|
|
|
|
const payload: NormalizedImport = {
|
|
source: "simkl",
|
|
movies: [],
|
|
episodes: [],
|
|
watchlist: [{ tmdbId: 500, title: "Test Show", type: "tv", status: "completed" }],
|
|
ratings: [],
|
|
};
|
|
|
|
const jobId = createJob(userId, payload, watchlistOnly);
|
|
await processImportJob(jobId);
|
|
|
|
const rows = testDb
|
|
.select()
|
|
.from(userTitleStatus)
|
|
.where(eq(userTitleStatus.userId, userId))
|
|
.all();
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].titleId).toBe("tv-c");
|
|
expect(rows[0].status).toBe("in_progress");
|
|
});
|
|
|
|
test("stores movie 'in_progress' as 'watchlist'", async () => {
|
|
const userId = insertUser();
|
|
insertMovieTitle("m-ip", 501);
|
|
|
|
const payload: NormalizedImport = {
|
|
source: "simkl",
|
|
movies: [],
|
|
episodes: [],
|
|
watchlist: [{ tmdbId: 501, title: "Test Movie", type: "movie", status: "in_progress" }],
|
|
ratings: [],
|
|
};
|
|
|
|
const jobId = createJob(userId, payload, watchlistOnly);
|
|
await processImportJob(jobId);
|
|
|
|
const rows = testDb
|
|
.select()
|
|
.from(userTitleStatus)
|
|
.where(eq(userTitleStatus.userId, userId))
|
|
.all();
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].titleId).toBe("m-ip");
|
|
expect(rows[0].status).toBe("watchlist");
|
|
});
|
|
});
|
|
|
|
// ── Migration: normalize_imported_statuses ──────────────────────────
|
|
|
|
describe("normalize_imported_statuses migration", () => {
|
|
test("repairs TV 'completed' and movie 'in_progress' rows", () => {
|
|
const dir = fileURLToPath(new URL("../../db/drizzle", import.meta.url));
|
|
const folder = readdirSync(dir).find((d) => d.endsWith("_normalize_imported_statuses"));
|
|
expect(folder).toBeDefined();
|
|
|
|
const userId = insertUser();
|
|
insertTitle({ id: "mig-tv", tmdbId: 7001, type: "tv", title: "Mig Show" });
|
|
insertTitle({ id: "mig-watched", tmdbId: 7002, type: "movie", title: "Mig Watched" });
|
|
insertTitle({ id: "mig-unwatched", tmdbId: 7003, type: "movie", title: "Mig Unwatched" });
|
|
insertStatus(userId, "mig-tv", "completed");
|
|
insertStatus(userId, "mig-watched", "in_progress");
|
|
insertStatus(userId, "mig-unwatched", "in_progress");
|
|
insertMovieWatch(userId, "mig-watched");
|
|
|
|
testClient.exec(readFileSync(join(dir, folder!, "migration.sql"), "utf8"));
|
|
|
|
const statusOf = (titleId: string) =>
|
|
testDb.select().from(userTitleStatus).where(eq(userTitleStatus.titleId, titleId)).get()
|
|
?.status;
|
|
expect(statusOf("mig-tv")).toBe("in_progress");
|
|
expect(statusOf("mig-watched")).toBe("completed");
|
|
expect(statusOf("mig-unwatched")).toBe("watchlist");
|
|
});
|
|
});
|
|
|
|
// ── Rating Import ───────────────────────────────────────────────────
|
|
|
|
describe("processImportJob — ratings", () => {
|
|
test("stores rating correctly", async () => {
|
|
const userId = insertUser();
|
|
insertMovieTitle("movie-r", 888, "Rated Movie");
|
|
|
|
const payload: NormalizedImport = {
|
|
source: "trakt",
|
|
movies: [],
|
|
episodes: [],
|
|
watchlist: [],
|
|
ratings: [
|
|
{
|
|
tmdbId: 888,
|
|
title: "Rated Movie",
|
|
type: "movie",
|
|
rating: 4,
|
|
ratedAt: "2024-03-01T12:00:00Z",
|
|
},
|
|
],
|
|
};
|
|
|
|
const jobId = createJob(userId, payload, {
|
|
importWatches: false,
|
|
importWatchlist: false,
|
|
importRatings: true,
|
|
});
|
|
await processImportJob(jobId);
|
|
|
|
const job = readImportJob(jobId);
|
|
expect(job.status).toBe("success");
|
|
expect(job.importedCount).toBe(1);
|
|
|
|
const ratingRows = testDb
|
|
.select()
|
|
.from(userRatings)
|
|
.where(eq(userRatings.userId, userId))
|
|
.all();
|
|
expect(ratingRows).toHaveLength(1);
|
|
expect(ratingRows[0].ratingStars).toBe(4);
|
|
expect(ratingRows[0].titleId).toBe("movie-r");
|
|
});
|
|
});
|
|
|
|
// ── Job State Transitions ───────────────────────────────────────────
|
|
|
|
describe("processImportJob — state transitions", () => {
|
|
test("job starts as pending, ends as success", async () => {
|
|
const userId = insertUser();
|
|
insertMovieTitle("movie-st", 111, "State Test");
|
|
|
|
const payload: NormalizedImport = {
|
|
source: "letterboxd",
|
|
movies: [{ tmdbId: 111, title: "State Test" }],
|
|
episodes: [],
|
|
watchlist: [],
|
|
ratings: [],
|
|
};
|
|
|
|
const jobId = createJob(userId, payload);
|
|
|
|
// Before processing
|
|
const before = readImportJob(jobId);
|
|
expect(before.status).toBe("pending");
|
|
expect(before.startedAt).toBeNull();
|
|
expect(before.finishedAt).toBeNull();
|
|
|
|
await processImportJob(jobId);
|
|
|
|
// After processing
|
|
const after = readImportJob(jobId);
|
|
expect(after.status).toBe("success");
|
|
expect(after.startedAt).not.toBeNull();
|
|
expect(after.finishedAt).not.toBeNull();
|
|
});
|
|
|
|
test("empty import with no matching options succeeds with warning", async () => {
|
|
const userId = insertUser();
|
|
|
|
const payload: NormalizedImport = {
|
|
source: "trakt",
|
|
movies: [{ tmdbId: 111, title: "A Movie" }],
|
|
episodes: [],
|
|
watchlist: [],
|
|
ratings: [],
|
|
};
|
|
|
|
// Disable all import options — movies exist but importWatches is false
|
|
const jobId = createJob(userId, payload, {
|
|
importWatches: false,
|
|
importWatchlist: false,
|
|
importRatings: false,
|
|
});
|
|
await processImportJob(jobId);
|
|
|
|
const job = readImportJob(jobId);
|
|
expect(job.status).toBe("success");
|
|
expect(job.warnings.length).toBeGreaterThan(0);
|
|
expect(job.warnings[0]).toContain("No items to import");
|
|
});
|
|
|
|
test("readImportJob throws for non-existent job", () => {
|
|
expect(() => readImportJob("non-existent-id")).toThrow("Import job non-existent-id not found");
|
|
});
|
|
|
|
test("schema allows only one active import job per user", () => {
|
|
const userId = insertUser();
|
|
const createdAt = new Date();
|
|
|
|
testDb
|
|
.insert(importJobs)
|
|
.values({
|
|
id: "job-1",
|
|
userId,
|
|
source: "trakt",
|
|
status: "pending",
|
|
payload: JSON.stringify({
|
|
source: "trakt",
|
|
movies: [],
|
|
episodes: [],
|
|
watchlist: [],
|
|
ratings: [],
|
|
}),
|
|
importWatches: true,
|
|
importWatchlist: true,
|
|
importRatings: true,
|
|
createdAt,
|
|
})
|
|
.run();
|
|
|
|
let duplicateActiveJobRejected = false;
|
|
try {
|
|
testDb
|
|
.insert(importJobs)
|
|
.values({
|
|
id: "job-2",
|
|
userId,
|
|
source: "simkl",
|
|
status: "running",
|
|
payload: JSON.stringify({
|
|
source: "simkl",
|
|
movies: [],
|
|
episodes: [],
|
|
watchlist: [],
|
|
ratings: [],
|
|
}),
|
|
importWatches: true,
|
|
importWatchlist: true,
|
|
importRatings: true,
|
|
createdAt,
|
|
})
|
|
.run();
|
|
} catch {
|
|
duplicateActiveJobRejected = true;
|
|
}
|
|
expect(duplicateActiveJobRejected).toBe(true);
|
|
|
|
testDb.update(importJobs).set({ status: "success" }).where(eq(importJobs.id, "job-1")).run();
|
|
|
|
expect(() =>
|
|
testDb
|
|
.insert(importJobs)
|
|
.values({
|
|
id: "job-3",
|
|
userId,
|
|
source: "letterboxd",
|
|
status: "pending",
|
|
payload: JSON.stringify({
|
|
source: "letterboxd",
|
|
movies: [],
|
|
episodes: [],
|
|
watchlist: [],
|
|
ratings: [],
|
|
}),
|
|
importWatches: true,
|
|
importWatchlist: true,
|
|
importRatings: true,
|
|
createdAt,
|
|
})
|
|
.run(),
|
|
).not.toThrow();
|
|
});
|
|
});
|
|
|
|
// ── Failed Resolution ───────────────────────────────────────────────
|
|
|
|
describe("processImportJob — failed resolution", () => {
|
|
test("records failure when movie cannot be resolved", async () => {
|
|
const userId = insertUser();
|
|
|
|
const payload: NormalizedImport = {
|
|
source: "letterboxd",
|
|
movies: [{ title: "Completely Unknown Film ZZZZZ" }],
|
|
episodes: [],
|
|
watchlist: [],
|
|
ratings: [],
|
|
};
|
|
|
|
// Mock TMDB search to return empty results (no network call)
|
|
const searchSpy = vi.spyOn(tmdbClient, "searchMovies").mockResolvedValue({
|
|
results: [],
|
|
} as never);
|
|
|
|
try {
|
|
const jobId = createJob(userId, payload);
|
|
await processImportJob(jobId);
|
|
|
|
const job = readImportJob(jobId);
|
|
expect(job.status).toBe("success");
|
|
expect(job.failedCount).toBe(1);
|
|
expect(job.importedCount).toBe(0);
|
|
expect(job.errors.length).toBeGreaterThan(0);
|
|
expect(job.errors[0]).toContain("Could not resolve movie");
|
|
} finally {
|
|
searchSpy.mockRestore();
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── Rewatches, added dates, late cancellation ───────────────────────
|
|
|
|
describe("processImportJob — rewatches and added dates", () => {
|
|
const movieWatches = (userId: string) =>
|
|
testDb.select().from(userMovieWatches).where(eq(userMovieWatches.userId, userId)).all();
|
|
|
|
const emptyPayload = (source: NormalizedImport["source"]): NormalizedImport => ({
|
|
source,
|
|
movies: [],
|
|
episodes: [],
|
|
watchlist: [],
|
|
ratings: [],
|
|
});
|
|
|
|
const twoPlays: NormalizedImport = {
|
|
...emptyPayload("trakt"),
|
|
movies: [
|
|
{ tmdbId: 550, title: "Fight Club", watchedAt: "2024-01-01T20:00:00Z" },
|
|
{ tmdbId: 550, title: "Fight Club", watchedAt: "2024-06-01T20:00:00Z" },
|
|
],
|
|
};
|
|
|
|
test("keeps rewatches with distinct timestamps", async () => {
|
|
const userId = insertUser();
|
|
insertMovieTitle("movie-1", 550, "Fight Club");
|
|
|
|
const jobId = createJob(userId, twoPlays);
|
|
await processImportJob(jobId);
|
|
|
|
const job = readImportJob(jobId);
|
|
expect(job.importedCount).toBe(2);
|
|
expect(movieWatches(userId)).toHaveLength(2);
|
|
});
|
|
|
|
test("skips a play already recorded at the same time", async () => {
|
|
const userId = insertUser();
|
|
insertMovieTitle("movie-1", 550, "Fight Club");
|
|
insertMovieWatch(userId, "movie-1", new Date("2024-01-01T20:30:00Z"));
|
|
|
|
const payload: NormalizedImport = {
|
|
...emptyPayload("trakt"),
|
|
movies: [{ tmdbId: 550, title: "Fight Club", watchedAt: "2024-01-01T20:00:00Z" }],
|
|
};
|
|
const jobId = createJob(userId, payload);
|
|
await processImportJob(jobId);
|
|
|
|
const job = readImportJob(jobId);
|
|
expect(job.skippedCount).toBe(1);
|
|
expect(job.importedCount).toBe(0);
|
|
expect(movieWatches(userId)).toHaveLength(1);
|
|
});
|
|
|
|
test("re-running the same import is idempotent", async () => {
|
|
const userId = insertUser();
|
|
insertMovieTitle("movie-1", 550, "Fight Club");
|
|
|
|
const firstJobId = createJob(userId, twoPlays);
|
|
await processImportJob(firstJobId);
|
|
expect(readImportJob(firstJobId).importedCount).toBe(2);
|
|
|
|
const secondJobId = createJob(userId, twoPlays);
|
|
await processImportJob(secondJobId);
|
|
|
|
const second = readImportJob(secondJobId);
|
|
expect(second.skippedCount).toBe(2);
|
|
expect(second.importedCount).toBe(0);
|
|
expect(movieWatches(userId)).toHaveLength(2);
|
|
});
|
|
|
|
test("keeps episode rewatches", async () => {
|
|
const userId = insertUser();
|
|
insertTvShowWithFetchedAt("tv-1", 1396);
|
|
|
|
const payload: NormalizedImport = {
|
|
...emptyPayload("trakt"),
|
|
episodes: [
|
|
{
|
|
showTmdbId: 1396,
|
|
showTitle: "Test Show",
|
|
seasonNumber: 1,
|
|
episodeNumber: 1,
|
|
watchedAt: "2024-01-01T20:00:00Z",
|
|
},
|
|
{
|
|
showTmdbId: 1396,
|
|
showTitle: "Test Show",
|
|
seasonNumber: 1,
|
|
episodeNumber: 1,
|
|
watchedAt: "2024-02-01T20:00:00Z",
|
|
},
|
|
],
|
|
};
|
|
const jobId = createJob(userId, payload);
|
|
await processImportJob(jobId);
|
|
|
|
expect(readImportJob(jobId).importedCount).toBe(2);
|
|
const watches = testDb
|
|
.select()
|
|
.from(userEpisodeWatches)
|
|
.where(eq(userEpisodeWatches.userId, userId))
|
|
.all();
|
|
expect(watches).toHaveLength(2);
|
|
});
|
|
|
|
test("date-only diary entries on different dates are both kept", async () => {
|
|
const userId = insertUser();
|
|
insertMovieTitle("movie-1", 550, "Fight Club");
|
|
|
|
const payload: NormalizedImport = {
|
|
...emptyPayload("letterboxd"),
|
|
movies: [
|
|
{ tmdbId: 550, title: "Fight Club", watchedOn: "2024-01-01" },
|
|
{ tmdbId: 550, title: "Fight Club", watchedOn: "2024-01-10" },
|
|
],
|
|
};
|
|
const jobId = createJob(userId, payload);
|
|
await processImportJob(jobId);
|
|
|
|
expect(readImportJob(jobId).importedCount).toBe(2);
|
|
expect(movieWatches(userId)).toHaveLength(2);
|
|
});
|
|
|
|
test("library item backdates addedAt after a watch created the row", async () => {
|
|
const userId = insertUser();
|
|
insertMovieTitle("movie-1", 550, "Fight Club");
|
|
|
|
const payload: NormalizedImport = {
|
|
...emptyPayload("trakt"),
|
|
movies: [{ tmdbId: 550, title: "Fight Club", watchedAt: "2024-06-01T20:00:00Z" }],
|
|
watchlist: [
|
|
{
|
|
tmdbId: 550,
|
|
title: "Fight Club",
|
|
type: "movie",
|
|
status: "completed",
|
|
addedAt: "2023-01-01T00:00:00Z",
|
|
},
|
|
],
|
|
};
|
|
const jobId = createJob(userId, payload);
|
|
await processImportJob(jobId);
|
|
|
|
const row = testDb
|
|
.select()
|
|
.from(userTitleStatus)
|
|
.where(eq(userTitleStatus.userId, userId))
|
|
.get();
|
|
expect(row).toBeDefined();
|
|
expect(row?.addedAt).toEqual(new Date("2023-01-01T00:00:00Z"));
|
|
});
|
|
|
|
test("cancel during the last item is not overwritten by success", async () => {
|
|
const userId = insertUser();
|
|
insertMovieTitle("movie-1", 550, "Fight Club");
|
|
|
|
const payload: NormalizedImport = {
|
|
...emptyPayload("letterboxd"),
|
|
movies: [{ imdbId: "tt0137523", title: "Fight Club" }],
|
|
};
|
|
|
|
let jobId = "";
|
|
const findSpy = vi.spyOn(tmdbClient, "findByExternalId").mockImplementation(async () => {
|
|
testDb.update(importJobs).set({ status: "cancelled" }).where(eq(importJobs.id, jobId)).run();
|
|
return { movie_results: [{ id: 550 }] } as never;
|
|
});
|
|
|
|
try {
|
|
jobId = createJob(userId, payload);
|
|
await processImportJob(jobId);
|
|
|
|
expect(findSpy).toHaveBeenCalled();
|
|
const job = testDb.select().from(importJobs).where(eq(importJobs.id, jobId)).get();
|
|
expect(job?.status).toBe("cancelled");
|
|
expect(job?.finishedAt).not.toBeNull();
|
|
} finally {
|
|
findSpy.mockRestore();
|
|
}
|
|
});
|
|
});
|