mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-15 22:55:42 -04:00
test(db): cover platform seeding, session cleanup, and import recovery
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
6 files changed
+296
-1
No files matched your search
@@ -16,6 +16,7 @@
|
||||
"format": "oxfmt --config ../../.oxfmtrc.json",
|
||||
"format:check": "oxfmt --check --config ../../.oxfmtrc.json",
|
||||
"check-types": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"db:generate": "bun --bun drizzle-kit generate",
|
||||
"db:migrate": "bun --bun drizzle-kit migrate",
|
||||
"db:push": "bun --bun drizzle-kit push",
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { beforeEach, describe, expect, test } from "vitest";
|
||||
|
||||
import { clearAllTables, insertUser, testDb } from "@sofa/test/db";
|
||||
|
||||
import { deleteExpiredSessions, deleteExpiredVerifications } from "../src/queries/auth-cleanup";
|
||||
import { session, verification } from "../src/schema";
|
||||
|
||||
const HOUR = 60 * 60 * 1000;
|
||||
|
||||
beforeEach(() => {
|
||||
clearAllTables();
|
||||
});
|
||||
|
||||
describe("deleteExpiredSessions", () => {
|
||||
test("deletes only expired sessions", () => {
|
||||
insertUser("u1");
|
||||
const now = new Date();
|
||||
testDb
|
||||
.insert(session)
|
||||
.values([
|
||||
{
|
||||
id: "s-old",
|
||||
userId: "u1",
|
||||
token: "tok-old",
|
||||
expiresAt: new Date(Date.now() - HOUR),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
{
|
||||
id: "s-new",
|
||||
userId: "u1",
|
||||
token: "tok-new",
|
||||
expiresAt: new Date(Date.now() + HOUR),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
])
|
||||
.run();
|
||||
|
||||
expect(deleteExpiredSessions()).toBe(1);
|
||||
expect(
|
||||
testDb
|
||||
.select()
|
||||
.from(session)
|
||||
.all()
|
||||
.map((s) => s.id),
|
||||
).toEqual(["s-new"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteExpiredVerifications", () => {
|
||||
test("deletes only expired verifications", () => {
|
||||
const now = new Date();
|
||||
testDb
|
||||
.insert(verification)
|
||||
.values([
|
||||
{
|
||||
id: "v-old",
|
||||
identifier: "a@example.com",
|
||||
value: "x",
|
||||
expiresAt: new Date(Date.now() - HOUR),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
{
|
||||
id: "v-new",
|
||||
identifier: "b@example.com",
|
||||
value: "y",
|
||||
expiresAt: new Date(Date.now() + HOUR),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
])
|
||||
.run();
|
||||
|
||||
expect(deleteExpiredVerifications()).toBe(1);
|
||||
expect(
|
||||
testDb
|
||||
.select()
|
||||
.from(verification)
|
||||
.all()
|
||||
.map((v) => v.id),
|
||||
).toEqual(["v-new"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { beforeEach, describe, expect, test } from "vitest";
|
||||
|
||||
import { clearAllTables, eq, insertUser, testDb } from "@sofa/test/db";
|
||||
|
||||
import { recoverStaleImportJobs } from "../src/queries/imports";
|
||||
import { importJobs } from "../src/schema";
|
||||
|
||||
beforeEach(() => {
|
||||
clearAllTables();
|
||||
});
|
||||
|
||||
function insertJob(
|
||||
id: string,
|
||||
userId: string,
|
||||
status: "pending" | "running" | "success" | "cancelled",
|
||||
) {
|
||||
testDb
|
||||
.insert(importJobs)
|
||||
.values({
|
||||
id,
|
||||
userId,
|
||||
source: "trakt",
|
||||
status,
|
||||
payload: "{}",
|
||||
importWatches: true,
|
||||
importWatchlist: true,
|
||||
importRatings: true,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
function getJob(id: string) {
|
||||
return testDb.select().from(importJobs).where(eq(importJobs.id, id)).get()!;
|
||||
}
|
||||
|
||||
describe("recoverStaleImportJobs", () => {
|
||||
test("errors out pending and running jobs only", () => {
|
||||
insertUser("u1");
|
||||
// importJobs_active_user allows one pending/running job per user
|
||||
insertUser("u2");
|
||||
insertJob("j-pending", "u1", "pending");
|
||||
insertJob("j-running", "u2", "running");
|
||||
insertJob("j-success", "u1", "success");
|
||||
insertJob("j-cancelled", "u1", "cancelled");
|
||||
|
||||
expect(recoverStaleImportJobs()).toBe(2);
|
||||
|
||||
for (const id of ["j-pending", "j-running"]) {
|
||||
const job = getJob(id);
|
||||
expect(job.status).toBe("error");
|
||||
expect(job.finishedAt).toBeInstanceOf(Date);
|
||||
expect(job.errors).toContain("Import interrupted by server restart");
|
||||
}
|
||||
for (const [id, status] of [
|
||||
["j-success", "success"],
|
||||
["j-cancelled", "cancelled"],
|
||||
] as const) {
|
||||
const job = getJob(id);
|
||||
expect(job.status).toBe(status);
|
||||
expect(job.finishedAt).toBeNull();
|
||||
expect(job.errors).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
import { beforeEach, describe, expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
clearAllTables,
|
||||
eq,
|
||||
insertPlatform,
|
||||
insertTitle,
|
||||
insertTitleAvailability,
|
||||
insertUser,
|
||||
insertUserPlatform,
|
||||
testDb,
|
||||
} from "@sofa/test/db";
|
||||
|
||||
import { platformTmdbIds, platforms, titleAvailability, userPlatforms } from "../src/schema";
|
||||
import { SEED_DATA, seedPlatforms } from "../src/seed-platforms";
|
||||
|
||||
beforeEach(() => {
|
||||
clearAllTables();
|
||||
});
|
||||
|
||||
describe("seedPlatforms", () => {
|
||||
test("seeds a fresh database", () => {
|
||||
seedPlatforms();
|
||||
|
||||
expect(testDb.select().from(platforms).all()).toHaveLength(SEED_DATA.length);
|
||||
const totalIds = SEED_DATA.reduce((sum, s) => sum + s.tmdbProviderIds.length, 0);
|
||||
expect(testDb.select().from(platformTmdbIds).all()).toHaveLength(totalIds);
|
||||
});
|
||||
|
||||
test("is idempotent", () => {
|
||||
seedPlatforms();
|
||||
const firstIds = testDb
|
||||
.select({ id: platforms.id })
|
||||
.from(platforms)
|
||||
.all()
|
||||
.map((p) => p.id)
|
||||
.toSorted();
|
||||
const firstMappings = testDb.select().from(platformTmdbIds).all().length;
|
||||
|
||||
seedPlatforms();
|
||||
const secondIds = testDb
|
||||
.select({ id: platforms.id })
|
||||
.from(platforms)
|
||||
.all()
|
||||
.map((p) => p.id)
|
||||
.toSorted();
|
||||
|
||||
expect(secondIds).toEqual(firstIds);
|
||||
expect(testDb.select().from(platformTmdbIds).all()).toHaveLength(firstMappings);
|
||||
});
|
||||
|
||||
test("restores changed metadata without changing the id", () => {
|
||||
seedPlatforms();
|
||||
const seed = SEED_DATA[0]!;
|
||||
const mapping = testDb
|
||||
.select()
|
||||
.from(platformTmdbIds)
|
||||
.where(eq(platformTmdbIds.tmdbProviderId, seed.tmdbProviderIds[0]!))
|
||||
.get()!;
|
||||
testDb.update(platforms).set({ name: "Old" }).where(eq(platforms.id, mapping.platformId)).run();
|
||||
|
||||
seedPlatforms();
|
||||
|
||||
const row = testDb.select().from(platforms).where(eq(platforms.id, mapping.platformId)).get();
|
||||
expect(row?.id).toBe(mapping.platformId);
|
||||
expect(row?.name).toBe(seed.name);
|
||||
});
|
||||
|
||||
test("merging duplicates keeps user choices and avoids duplicate availability", () => {
|
||||
const netflix = SEED_DATA.find((s) => s.tmdbProviderIds.includes(8))!;
|
||||
expect(netflix.tmdbProviderIds).toContain(175);
|
||||
|
||||
insertPlatform({ id: "p-a", name: "Netflix", tmdbProviderIds: [8] });
|
||||
insertPlatform({ id: "p-b", name: "Netflix Ads", tmdbProviderIds: [175] });
|
||||
insertUser("u1");
|
||||
insertUser("u2");
|
||||
insertUserPlatform("u1", "p-b");
|
||||
insertUserPlatform("u2", "p-a");
|
||||
insertUserPlatform("u2", "p-b");
|
||||
insertTitle({ id: "t1", tmdbId: 1, type: "movie" });
|
||||
insertTitleAvailability("t1", "p-a");
|
||||
insertTitleAvailability("t1", "p-b");
|
||||
|
||||
seedPlatforms();
|
||||
|
||||
const mappedIds = testDb
|
||||
.select()
|
||||
.from(platformTmdbIds)
|
||||
.all()
|
||||
.filter((m) => netflix.tmdbProviderIds.includes(m.tmdbProviderId))
|
||||
.map((m) => m.platformId);
|
||||
const canonical = mappedIds[0]!;
|
||||
expect(mappedIds).toHaveLength(netflix.tmdbProviderIds.length);
|
||||
expect(new Set(mappedIds)).toEqual(new Set([canonical]));
|
||||
expect(canonical).toBe("p-a");
|
||||
|
||||
expect(testDb.select().from(platforms).where(eq(platforms.id, "p-b")).all()).toHaveLength(0);
|
||||
expect(testDb.select().from(platforms).where(eq(platforms.id, canonical)).all()).toHaveLength(
|
||||
1,
|
||||
);
|
||||
|
||||
const u1 = testDb.select().from(userPlatforms).where(eq(userPlatforms.userId, "u1")).all();
|
||||
expect(u1).toHaveLength(1);
|
||||
expect(u1[0]!.platformId).toBe(canonical);
|
||||
|
||||
const u2 = testDb.select().from(userPlatforms).where(eq(userPlatforms.userId, "u2")).all();
|
||||
expect(u2).toHaveLength(1);
|
||||
expect(u2[0]!.platformId).toBe(canonical);
|
||||
|
||||
const avail = testDb
|
||||
.select()
|
||||
.from(titleAvailability)
|
||||
.where(eq(titleAvailability.titleId, "t1"))
|
||||
.all();
|
||||
expect(avail).toHaveLength(1);
|
||||
expect(avail[0]!.platformId).toBe(canonical);
|
||||
expect(avail[0]!.offerType).toBe("flatrate");
|
||||
});
|
||||
|
||||
test("unrelated platforms survive seeding", () => {
|
||||
const tmdbId = 999999;
|
||||
expect(SEED_DATA.some((s) => s.tmdbProviderIds.includes(tmdbId))).toBe(false);
|
||||
insertPlatform({ id: "p-x", name: "Tiny", tmdbProviderIds: [tmdbId] });
|
||||
|
||||
seedPlatforms();
|
||||
|
||||
expect(testDb.select().from(platforms).where(eq(platforms.id, "p-x")).all()).toHaveLength(1);
|
||||
const mapping = testDb
|
||||
.select()
|
||||
.from(platformTmdbIds)
|
||||
.where(eq(platformTmdbIds.tmdbProviderId, tmdbId))
|
||||
.all();
|
||||
expect(mapping).toHaveLength(1);
|
||||
expect(mapping[0]!.platformId).toBe("p-x");
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"extends": "@sofa/tsconfig/base.json",
|
||||
"include": ["src"]
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineProject } from "vitest/config";
|
||||
|
||||
export default defineProject({
|
||||
test: {
|
||||
include: ["test/**/*.test.ts"],
|
||||
setupFiles: ["@sofa/test/setup"],
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user