Move test utilities from lib/ to test/ directory

- Rename `lib/test-preload.ts` → `test/preload.ts` and
  `lib/test-utils.ts` → `test/sqlite.ts`
- Update `bunfig.toml` preload path to `./test/preload.ts`
- Update all service test imports from `@/lib/test-utils` to
  `@/test/sqlite`
This commit is contained in:
2026-03-08 12:18:21 -04:00
parent 1328619c43
commit 609f984aa0
14 changed files with 25 additions and 326 deletions
-178
View File
@@ -1,178 +0,0 @@
import { afterEach, describe, expect, test } from "bun:test";
import {
getOidcProviderName,
isOidcAutoRegisterEnabled,
isOidcConfigured,
isPasswordLoginDisabled,
isTmdbConfigured,
} from "./config";
describe("isTmdbConfigured", () => {
const orig = process.env.TMDB_API_READ_ACCESS_TOKEN;
afterEach(() => {
if (orig !== undefined) {
process.env.TMDB_API_READ_ACCESS_TOKEN = orig;
} else {
delete process.env.TMDB_API_READ_ACCESS_TOKEN;
}
});
test("returns true when token is set", () => {
process.env.TMDB_API_READ_ACCESS_TOKEN = "some-token";
expect(isTmdbConfigured()).toBe(true);
});
test("returns false when token is unset", () => {
delete process.env.TMDB_API_READ_ACCESS_TOKEN;
expect(isTmdbConfigured()).toBe(false);
});
test("returns false when token is empty string", () => {
process.env.TMDB_API_READ_ACCESS_TOKEN = "";
expect(isTmdbConfigured()).toBe(false);
});
});
describe("isOidcConfigured", () => {
const origId = process.env.OIDC_CLIENT_ID;
const origSecret = process.env.OIDC_CLIENT_SECRET;
const origIssuer = process.env.OIDC_ISSUER_URL;
afterEach(() => {
for (const [key, val] of [
["OIDC_CLIENT_ID", origId],
["OIDC_CLIENT_SECRET", origSecret],
["OIDC_ISSUER_URL", origIssuer],
] as const) {
if (val !== undefined) {
process.env[key] = val;
} else {
delete process.env[key];
}
}
});
test("returns true when all three vars are set", () => {
process.env.OIDC_CLIENT_ID = "id";
process.env.OIDC_CLIENT_SECRET = "secret";
process.env.OIDC_ISSUER_URL = "https://issuer.example.com";
expect(isOidcConfigured()).toBe(true);
});
test("returns false when client ID is missing", () => {
delete process.env.OIDC_CLIENT_ID;
process.env.OIDC_CLIENT_SECRET = "secret";
process.env.OIDC_ISSUER_URL = "https://issuer.example.com";
expect(isOidcConfigured()).toBe(false);
});
test("returns false when client secret is missing", () => {
process.env.OIDC_CLIENT_ID = "id";
delete process.env.OIDC_CLIENT_SECRET;
process.env.OIDC_ISSUER_URL = "https://issuer.example.com";
expect(isOidcConfigured()).toBe(false);
});
test("returns false when issuer URL is missing", () => {
process.env.OIDC_CLIENT_ID = "id";
process.env.OIDC_CLIENT_SECRET = "secret";
delete process.env.OIDC_ISSUER_URL;
expect(isOidcConfigured()).toBe(false);
});
});
describe("getOidcProviderName", () => {
const orig = process.env.OIDC_PROVIDER_NAME;
afterEach(() => {
if (orig !== undefined) {
process.env.OIDC_PROVIDER_NAME = orig;
} else {
delete process.env.OIDC_PROVIDER_NAME;
}
});
test("returns custom name when set", () => {
process.env.OIDC_PROVIDER_NAME = "Okta";
expect(getOidcProviderName()).toBe("Okta");
});
test("returns 'SSO' as default", () => {
delete process.env.OIDC_PROVIDER_NAME;
expect(getOidcProviderName()).toBe("SSO");
});
});
describe("isOidcAutoRegisterEnabled", () => {
const orig = process.env.OIDC_AUTO_REGISTER;
afterEach(() => {
if (orig !== undefined) {
process.env.OIDC_AUTO_REGISTER = orig;
} else {
delete process.env.OIDC_AUTO_REGISTER;
}
});
test("returns true by default", () => {
delete process.env.OIDC_AUTO_REGISTER;
expect(isOidcAutoRegisterEnabled()).toBe(true);
});
test("returns false when explicitly set to 'false'", () => {
process.env.OIDC_AUTO_REGISTER = "false";
expect(isOidcAutoRegisterEnabled()).toBe(false);
});
test("returns true for any other value", () => {
process.env.OIDC_AUTO_REGISTER = "true";
expect(isOidcAutoRegisterEnabled()).toBe(true);
});
});
describe("isPasswordLoginDisabled", () => {
const origDisable = process.env.DISABLE_PASSWORD_LOGIN;
const origId = process.env.OIDC_CLIENT_ID;
const origSecret = process.env.OIDC_CLIENT_SECRET;
const origIssuer = process.env.OIDC_ISSUER_URL;
afterEach(() => {
for (const [key, val] of [
["DISABLE_PASSWORD_LOGIN", origDisable],
["OIDC_CLIENT_ID", origId],
["OIDC_CLIENT_SECRET", origSecret],
["OIDC_ISSUER_URL", origIssuer],
] as const) {
if (val !== undefined) {
process.env[key] = val;
} else {
delete process.env[key];
}
}
});
test("returns true when flag is 'true' and OIDC is configured", () => {
process.env.DISABLE_PASSWORD_LOGIN = "true";
process.env.OIDC_CLIENT_ID = "id";
process.env.OIDC_CLIENT_SECRET = "secret";
process.env.OIDC_ISSUER_URL = "https://issuer.example.com";
expect(isPasswordLoginDisabled()).toBe(true);
});
test("returns false when flag is 'true' but OIDC is not configured", () => {
process.env.DISABLE_PASSWORD_LOGIN = "true";
delete process.env.OIDC_CLIENT_ID;
delete process.env.OIDC_CLIENT_SECRET;
delete process.env.OIDC_ISSUER_URL;
expect(isPasswordLoginDisabled()).toBe(false);
});
test("returns false when flag is not set", () => {
delete process.env.DISABLE_PASSWORD_LOGIN;
process.env.OIDC_CLIENT_ID = "id";
process.env.OIDC_CLIENT_SECRET = "secret";
process.env.OIDC_ISSUER_URL = "https://issuer.example.com";
expect(isPasswordLoginDisabled()).toBe(false);
});
});
-16
View File
@@ -1,16 +0,0 @@
// Static shortcut descriptions for the help dialog.
// TanStack's HotkeyManager/SequenceManager handle all actual key listening.
export const SHORTCUT_DESCRIPTIONS = [
{ scope: "Global", description: "Search", keys: ["/"] },
{ scope: "Global", description: "Keyboard shortcuts", keys: ["?"] },
{ scope: "Navigation", description: "Go to dashboard", keys: ["g", "h"] },
{ scope: "Navigation", description: "Go to explore", keys: ["g", "e"] },
{ scope: "Title", description: "Cycle status", keys: ["w"] },
{ scope: "Title", description: "Mark watched", keys: ["m"] },
{ scope: "Title", description: "Go back", keys: ["Escape"] },
{ scope: "Title", description: "Rate 1 star", keys: ["1"] },
{ scope: "Title", description: "Rate 2 stars", keys: ["2"] },
{ scope: "Title", description: "Rate 3 stars", keys: ["3"] },
{ scope: "Title", description: "Rate 4 stars", keys: ["4"] },
{ scope: "Title", description: "Rate 5 stars", keys: ["5"] },
] as const;
-72
View File
@@ -1,72 +0,0 @@
import { describe, expect, mock, test } from "bun:test";
mock.module("@/lib/services/availability", () => ({
refreshAvailability: async () => {},
}));
mock.module("@/lib/services/backup", () => ({
createBackup: async () => ({}),
ensureBackupDir: async () => {},
pruneBackups: async () => {},
}));
mock.module("@/lib/services/credits", () => ({
refreshCredits: async () => {},
}));
mock.module("@/lib/services/image-cache", () => ({
cacheEpisodeStills: async () => {},
cacheImagesForTitle: async () => {},
cacheProfilePhotos: async () => {},
cacheProviderLogos: async () => {},
imageCacheEnabled: () => false,
}));
mock.module("@/lib/services/metadata", () => ({
refreshRecommendations: async () => {},
refreshTitle: async () => {},
refreshTvChildren: async () => {},
}));
mock.module("@/lib/services/update-check", () => ({
performUpdateCheck: async () => ({}),
}));
mock.module("@/lib/tmdb/client", () => ({
getTvDetails: async () => ({}),
}));
import { buildBackupCron } from "./cron";
describe("buildBackupCron", () => {
test("6h frequency", () => {
expect(buildBackupCron("6h", "02:00")).toBe("0 */6 * * *");
});
test("12h frequency", () => {
expect(buildBackupCron("12h", "02:00")).toBe("0 2,14 * * *");
});
test("1d frequency (default)", () => {
expect(buildBackupCron("1d", "03:30")).toBe("30 3 * * *");
});
test("7d frequency with day of week", () => {
expect(buildBackupCron("7d", "04:15", 3)).toBe("15 4 * * 3");
});
test("7d frequency defaults to Sunday (0)", () => {
expect(buildBackupCron("7d", "02:00")).toBe("0 2 * * 0");
});
test("defaults to 1d when called with no args", () => {
expect(buildBackupCron()).toBe("0 2 * * *");
});
test("handles invalid time gracefully", () => {
const result = buildBackupCron("1d", "invalid");
expect(result).toBe("0 2 * * *");
});
test("6h ignores hour from time, uses minute only", () => {
expect(buildBackupCron("6h", "14:45")).toBe("45 */6 * * *");
});
test("12h wraps hour correctly", () => {
expect(buildBackupCron("12h", "18:00")).toBe("0 18,6 * * *");
});
});
-50
View File
@@ -1,50 +0,0 @@
import { describe, expect, test } from "bun:test";
import { generateProviderUrl } from "./providers";
describe("generateProviderUrl", () => {
test("generates Netflix URL", () => {
expect(generateProviderUrl(8, "Inception")).toBe(
"https://www.netflix.com/search?q=Inception",
);
});
test("generates Amazon Prime Video URL", () => {
expect(generateProviderUrl(9, "The Matrix")).toBe(
"https://www.amazon.com/s?i=instant-video&k=The%20Matrix",
);
});
test("generates Disney+ URL", () => {
expect(generateProviderUrl(337, "Frozen")).toBe(
"https://www.disneyplus.com/search/Frozen",
);
});
test("URL-encodes spaces", () => {
expect(generateProviderUrl(8, "The Dark Knight")).toBe(
"https://www.netflix.com/search?q=The%20Dark%20Knight",
);
});
test("URL-encodes special characters", () => {
expect(generateProviderUrl(8, "Tom & Jerry")).toBe(
"https://www.netflix.com/search?q=Tom%20%26%20Jerry",
);
});
test("URL-encodes unicode", () => {
const url = generateProviderUrl(8, "Amelie");
expect(url).toContain("Amelie");
expect(url).not.toBeNull();
});
test("returns null for unknown provider ID", () => {
expect(generateProviderUrl(99999, "Test")).toBeNull();
});
test("generates Hulu URL", () => {
expect(generateProviderUrl(15, "Test")).toBe(
"https://www.hulu.com/search?q=Test",
);
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, mock, test } from "bun:test";
import { persons, titleCast } from "@/lib/db/schema";
import { clearAllTables, insertTitle, testDb } from "@/lib/test-utils";
import { clearAllTables, insertTitle, testDb } from "@/test/sqlite";
mock.module("./image-cache", () => ({
imageCacheEnabled: () => false,
+1 -2
View File
@@ -10,8 +10,7 @@ import {
insertTitle,
insertTvShow,
insertUser,
} from "@/lib/test-utils";
} from "@/test/sqlite";
import {
getContinueWatchingFeed,
getNewAvailableFeed,
+1 -1
View File
@@ -5,7 +5,7 @@ import {
insertStatus,
insertTitle,
insertUser,
} from "@/lib/test-utils";
} from "@/test/sqlite";
import {
getRadarrList,
getSonarrList,
+1 -1
View File
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, test } from "bun:test";
import { persons, titleCast } from "@/lib/db/schema";
import { clearAllTables, insertTitle, testDb } from "@/lib/test-utils";
import { clearAllTables, insertTitle, testDb } from "@/test/sqlite";
import { getLocalFilmography } from "./person";
+1 -1
View File
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, test } from "bun:test";
import { clearAllTables, insertUser } from "@/lib/test-utils";
import { clearAllTables, insertUser } from "@/test/sqlite";
import {
getSetting,
getUserCount,
+1 -1
View File
@@ -12,7 +12,7 @@ import {
insertTvShow,
insertUser,
testDb,
} from "@/lib/test-utils";
} from "@/test/sqlite";
import {
getUserTitleInfo,
logEpisodeWatch,
-9
View File
@@ -1,9 +0,0 @@
import { mock } from "bun:test";
import { applyMigrations, testDb } from "@/lib/test-utils";
mock.module("@/lib/db/client", () => ({
db: testDb,
closeDatabase: () => {},
}));
applyMigrations();
-224
View File
@@ -1,224 +0,0 @@
import { Database } from "bun:sqlite";
import { drizzle } from "drizzle-orm/bun-sqlite";
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
import * as schema from "@/lib/db/schema";
const {
user,
titles,
seasons,
episodes,
userMovieWatches,
userEpisodeWatches,
userTitleStatus,
userRatings,
availabilityOffers,
titleRecommendations,
integrations,
} = schema;
export const testClient = new Database(":memory:");
testClient.run("PRAGMA foreign_keys = ON");
export const testDb = drizzle({ client: testClient, schema });
export function applyMigrations() {
migrate(testDb, { migrationsFolder: "./drizzle" });
}
export function clearAllTables() {
const tables = testClient
.query(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '__drizzle%'",
)
.all() as { name: string }[];
testClient.run("PRAGMA foreign_keys = OFF");
for (const { name } of tables) {
testClient.run(`DELETE FROM "${name}"`);
}
testClient.run("PRAGMA foreign_keys = ON");
}
const now = new Date();
export function insertUser(id = "user-1") {
testDb
.insert(user)
.values({
id,
name: "Test User",
email: `${id}@test.com`,
emailVerified: true,
createdAt: now,
updatedAt: now,
})
.run();
return id;
}
export function insertTitle(
overrides: {
id?: string;
tmdbId?: number;
tvdbId?: number;
type?: "movie" | "tv";
title?: string;
} = {},
) {
const id = overrides.id ?? "title-1";
testDb
.insert(titles)
.values({
id,
tmdbId: overrides.tmdbId ?? 12345,
tvdbId: overrides.tvdbId,
type: overrides.type ?? "movie",
title: overrides.title ?? "Test Movie",
})
.run();
return id;
}
export function insertTvShow(
titleId = "tv-1",
tmdbId = 99999,
seasonCount = 1,
epsPerSeason = 3,
) {
insertTitle({ id: titleId, tmdbId, type: "tv", title: "Test Show" });
const episodeIds: string[] = [];
for (let s = 1; s <= seasonCount; s++) {
const seasonId = `${titleId}-s${s}`;
testDb
.insert(seasons)
.values({ id: seasonId, titleId, seasonNumber: s })
.run();
for (let e = 1; e <= epsPerSeason; e++) {
const epId = `${titleId}-s${s}e${e}`;
testDb
.insert(episodes)
.values({
id: epId,
seasonId,
episodeNumber: e,
name: `S${s}E${e}`,
})
.run();
episodeIds.push(epId);
}
}
return { titleId, episodeIds };
}
export function insertMovieWatch(
userId: string,
titleId: string,
watchedAt?: Date,
) {
testDb
.insert(userMovieWatches)
.values({
userId,
titleId,
watchedAt: watchedAt ?? new Date(),
source: "manual",
})
.run();
}
export function insertEpisodeWatch(
userId: string,
episodeId: string,
watchedAt?: Date,
) {
testDb
.insert(userEpisodeWatches)
.values({
userId,
episodeId,
watchedAt: watchedAt ?? new Date(),
source: "manual",
})
.run();
}
export function insertStatus(
userId: string,
titleId: string,
status: "watchlist" | "in_progress" | "completed",
) {
const now = new Date();
testDb
.insert(userTitleStatus)
.values({ userId, titleId, status, addedAt: now, updatedAt: now })
.run();
}
export function insertRating(
userId: string,
titleId: string,
ratingStars: number,
) {
testDb
.insert(userRatings)
.values({ userId, titleId, ratingStars, ratedAt: new Date() })
.run();
}
export function insertAvailabilityOffer(
titleId: string,
overrides: {
providerId?: number;
providerName?: string;
offerType?: "flatrate" | "rent" | "buy" | "free" | "ads";
} = {},
) {
testDb
.insert(availabilityOffers)
.values({
titleId,
providerId: overrides.providerId ?? 8,
providerName: overrides.providerName ?? "Netflix",
offerType: overrides.offerType ?? "flatrate",
})
.run();
}
export function insertIntegration(
userId: string,
provider: string,
token = "test-token",
) {
const type =
provider === "sonarr" || provider === "radarr" ? "list" : "webhook";
return testDb
.insert(integrations)
.values({
userId,
provider,
type,
token,
enabled: true,
createdAt: new Date(),
})
.returning()
.get();
}
export function insertRecommendation(
titleId: string,
recommendedTitleId: string,
overrides: {
source?: "tmdb_recommendations" | "tmdb_similar";
rank?: number;
} = {},
) {
testDb
.insert(titleRecommendations)
.values({
titleId,
recommendedTitleId,
source: overrides.source ?? "tmdb_recommendations",
rank: overrides.rank ?? 1,
})
.run();
}