Files
sofa/packages/core/test/import-resolve.test.ts
T
jakeandClaude Opus 4.6 5f3e14b415 feat: migrate test runner from Bun to Vitest
- Replace bun:test with Vitest across all 22 test files
- Create @sofa/test package with shared setup and DB test helpers
  - setup.ts: vi.mock for @sofa/db/client, Bun.randomUUIDv7 polyfill
  - db.ts: in-memory SQLite via better-sqlite3, seed helpers
- Add per-project vitest configs (packages/core, apps/web)
- Add root vitest.config.ts with projects and v8 coverage
- Set up @vitest/browser + Playwright for web component tests
- Move validateBackupDatabase from core/backup.ts to @sofa/db/client
- Derive required backup tables from schema instead of hard-coded list
- Update CI workflow with Playwright install and Codecov upload
- Update CLAUDE.md documentation for Vitest

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:03:06 -04:00

302 lines
9.7 KiB
TypeScript

import { afterEach, describe, expect, test, vi } from "vitest";
import * as tmdbClient from "@sofa/tmdb/client";
import { resolveMovieTmdbId, resolveShowTmdbId } from "../src/imports/resolve";
// The TMDB client functions (findByExternalId, searchMovies, searchTv) are
// called by the resolve functions. We spy on them directly rather than on
// globalThis.fetch because openapi-fetch captures the fetch reference at
// module-init time, making globalThis.fetch mocking ineffective.
let findSpy: ReturnType<typeof vi.spyOn>;
let searchMoviesSpy: ReturnType<typeof vi.spyOn>;
let searchTvSpy: ReturnType<typeof vi.spyOn>;
afterEach(() => {
findSpy?.mockRestore();
searchMoviesSpy?.mockRestore();
searchTvSpy?.mockRestore();
});
// ── resolveMovieTmdbId ──────────────────────────────────────────────
describe("resolveMovieTmdbId", () => {
test("returns tmdbId directly when provided", async () => {
findSpy = vi.spyOn(tmdbClient, "findByExternalId");
searchMoviesSpy = vi.spyOn(tmdbClient, "searchMovies");
const result = await resolveMovieTmdbId({ tmdbId: 123 });
expect(result).toBe(123);
expect(findSpy).not.toHaveBeenCalled();
expect(searchMoviesSpy).not.toHaveBeenCalled();
});
test("resolves via IMDB ID lookup", async () => {
findSpy = vi.spyOn(tmdbClient, "findByExternalId").mockResolvedValue({
movie_results: [{ id: 456 }],
tv_results: [],
tv_episode_results: [],
} as never);
const result = await resolveMovieTmdbId({ imdbId: "tt1234567" });
expect(result).toBe(456);
expect(findSpy).toHaveBeenCalledWith("tt1234567", "imdb_id");
});
test("falls back to TVDB lookup when no IMDB ID", async () => {
findSpy = vi.spyOn(tmdbClient, "findByExternalId").mockResolvedValue({
movie_results: [{ id: 789 }],
tv_results: [],
tv_episode_results: [],
} as never);
const result = await resolveMovieTmdbId({ tvdbId: 99887 });
expect(result).toBe(789);
expect(findSpy).toHaveBeenCalledWith("99887", "tvdb_id");
});
test("IMDB returns no movie, falls back to TVDB", async () => {
let callIndex = 0;
findSpy = vi.spyOn(tmdbClient, "findByExternalId").mockImplementation(async () => {
callIndex++;
if (callIndex === 1) {
// IMDB lookup — no results
return {
movie_results: [],
tv_results: [],
tv_episode_results: [],
} as never;
}
// TVDB lookup — has result
return {
movie_results: [{ id: 789 }],
tv_results: [],
tv_episode_results: [],
} as never;
});
const result = await resolveMovieTmdbId({
imdbId: "tt0000001",
tvdbId: 99887,
});
expect(result).toBe(789);
expect(findSpy).toHaveBeenCalledTimes(2);
});
test("falls back to title search when no IDs available", async () => {
searchMoviesSpy = vi.spyOn(tmdbClient, "searchMovies").mockResolvedValue({
results: [{ id: 321, title: "Inception", release_date: "2010-07-16" }],
} as never);
const result = await resolveMovieTmdbId({ title: "Inception" });
expect(result).toBe(321);
expect(searchMoviesSpy).toHaveBeenCalledWith("Inception");
});
test("title search with year prefers matching year", async () => {
searchMoviesSpy = vi.spyOn(tmdbClient, "searchMovies").mockResolvedValue({
results: [
{ id: 100, title: "Dune", release_date: "1984-12-14" },
{ id: 200, title: "Dune", release_date: "2021-10-22" },
],
} as never);
const result = await resolveMovieTmdbId({ title: "Dune", year: 2021 });
expect(result).toBe(200);
});
test("title search without year match returns null", async () => {
searchMoviesSpy = vi.spyOn(tmdbClient, "searchMovies").mockResolvedValue({
results: [
{ id: 100, title: "Dune", release_date: "1984-12-14" },
{ id: 200, title: "Dune", release_date: "2021-10-22" },
],
} as never);
// year=1999 doesn't match any result — don't fall back to an arbitrary result
const result = await resolveMovieTmdbId({ title: "Dune", year: 1999 });
expect(result).toBeNull();
});
test("returns null when all methods fail", async () => {
findSpy = vi.spyOn(tmdbClient, "findByExternalId").mockResolvedValue({
movie_results: [],
tv_results: [],
tv_episode_results: [],
} as never);
searchMoviesSpy = vi.spyOn(tmdbClient, "searchMovies").mockResolvedValue({
results: [],
} as never);
const result = await resolveMovieTmdbId({
imdbId: "tt0000000",
title: "NonexistentFilm",
});
expect(result).toBeNull();
});
test("returns null when no identifiers at all", async () => {
findSpy = vi.spyOn(tmdbClient, "findByExternalId");
searchMoviesSpy = vi.spyOn(tmdbClient, "searchMovies");
const result = await resolveMovieTmdbId({});
expect(result).toBeNull();
expect(findSpy).not.toHaveBeenCalled();
expect(searchMoviesSpy).not.toHaveBeenCalled();
});
test("cache prevents duplicate lookups", async () => {
findSpy = vi.spyOn(tmdbClient, "findByExternalId").mockResolvedValue({
movie_results: [{ id: 555 }],
tv_results: [],
tv_episode_results: [],
} as never);
const cache = new Map<string, number | null>();
const first = await resolveMovieTmdbId({ imdbId: "tt9999999" }, cache);
expect(first).toBe(555);
expect(findSpy).toHaveBeenCalledTimes(1);
const second = await resolveMovieTmdbId({ imdbId: "tt9999999" }, cache);
expect(second).toBe(555);
// No additional calls — cache hit
expect(findSpy).toHaveBeenCalledTimes(1);
});
test("cache stores null for unresolvable items", async () => {
searchMoviesSpy = vi.spyOn(tmdbClient, "searchMovies").mockResolvedValue({
results: [],
} as never);
const cache = new Map<string, number | null>();
const first = await resolveMovieTmdbId({ title: "Nothing" }, cache);
expect(first).toBeNull();
// Cache should contain a null entry
expect(cache.size).toBe(1);
const cachedValue = [...cache.values()][0];
expect(cachedValue).toBeNull();
});
});
// ── resolveShowTmdbId ───────────────────────────────────────────────
describe("resolveShowTmdbId", () => {
test("returns tmdbId directly when provided", async () => {
findSpy = vi.spyOn(tmdbClient, "findByExternalId");
const result = await resolveShowTmdbId({ tmdbId: 42 });
expect(result).toBe(42);
expect(findSpy).not.toHaveBeenCalled();
});
test("resolves via IMDB ID — show-level result", async () => {
findSpy = vi.spyOn(tmdbClient, "findByExternalId").mockResolvedValue({
movie_results: [],
tv_results: [{ id: 600, name: "Breaking Bad" }],
tv_episode_results: [],
} as never);
const result = await resolveShowTmdbId({ imdbId: "tt5555555" });
expect(result).toBe(600);
});
test("resolves via IMDB ID — episode-level result extracts show_id", async () => {
findSpy = vi.spyOn(tmdbClient, "findByExternalId").mockResolvedValue({
movie_results: [],
tv_results: [],
tv_episode_results: [
{
id: 9001,
episode_number: 3,
name: "Fly",
season_number: 3,
show_id: 700,
},
],
} as never);
const result = await resolveShowTmdbId({ imdbId: "tt7777777" });
expect(result).toBe(700);
});
test("falls back to TVDB lookup", async () => {
findSpy = vi.spyOn(tmdbClient, "findByExternalId").mockResolvedValue({
movie_results: [],
tv_results: [{ id: 800 }],
tv_episode_results: [],
} as never);
const result = await resolveShowTmdbId({ tvdbId: 12345 });
expect(result).toBe(800);
expect(findSpy).toHaveBeenCalledWith("12345", "tvdb_id");
});
test("TVDB lookup extracts show_id from episode result", async () => {
findSpy = vi.spyOn(tmdbClient, "findByExternalId").mockResolvedValue({
movie_results: [],
tv_results: [],
tv_episode_results: [
{
id: 1,
episode_number: 1,
name: "Pilot",
season_number: 1,
show_id: 850,
},
],
} as never);
const result = await resolveShowTmdbId({ tvdbId: 54321 });
expect(result).toBe(850);
});
test("falls back to title search", async () => {
searchTvSpy = vi.spyOn(tmdbClient, "searchTv").mockResolvedValue({
results: [{ id: 900, name: "The Office" }],
} as never);
const result = await resolveShowTmdbId({ title: "The Office" });
expect(result).toBe(900);
expect(searchTvSpy).toHaveBeenCalledWith("The Office");
});
test("returns null when all methods fail", async () => {
findSpy = vi.spyOn(tmdbClient, "findByExternalId").mockResolvedValue({
movie_results: [],
tv_results: [],
tv_episode_results: [],
} as never);
searchTvSpy = vi.spyOn(tmdbClient, "searchTv").mockResolvedValue({
results: [],
} as never);
const result = await resolveShowTmdbId({
imdbId: "tt0000000",
title: "Nothing",
});
expect(result).toBeNull();
});
test("cache prevents duplicate show lookups", async () => {
findSpy = vi.spyOn(tmdbClient, "findByExternalId").mockResolvedValue({
movie_results: [],
tv_results: [{ id: 950 }],
tv_episode_results: [],
} as never);
const cache = new Map<string, number | null>();
const first = await resolveShowTmdbId({ imdbId: "tt8888888" }, cache);
expect(first).toBe(950);
expect(findSpy).toHaveBeenCalledTimes(1);
const second = await resolveShowTmdbId({ imdbId: "tt8888888" }, cache);
expect(second).toBe(950);
expect(findSpy).toHaveBeenCalledTimes(1);
});
});