* Fix upsert standings no-op: use EXCLUDED pseudo-table for conflict update Drizzle's onConflictDoUpdate set block was referencing the existing table columns instead of the incoming values, causing every sync to silently overwrite records with their own current data (no-op). Fixes #211 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add unit tests for regular-season-standings model Covers upsertRegularSeasonStandings (empty-array short-circuit, value mapping, syncedAt behaviour), upsertManualStanding, getRegularSeasonStandings sorting, getLastSyncedAt, deleteRegularSeasonStandings, and getParticipantStanding — 19 tests total. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
333 lines
10 KiB
TypeScript
333 lines
10 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
vi.mock("~/database/context", () => ({
|
|
database: vi.fn(),
|
|
}));
|
|
|
|
import {
|
|
upsertRegularSeasonStandings,
|
|
upsertManualStanding,
|
|
getRegularSeasonStandings,
|
|
getLastSyncedAt,
|
|
deleteRegularSeasonStandings,
|
|
getParticipantStanding,
|
|
} from "../regular-season-standings";
|
|
import { database } from "~/database/context";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function makeUpsertDb(returnedRows: object[] = []) {
|
|
const returning = vi.fn().mockResolvedValue(returnedRows);
|
|
const onConflictDoUpdate = vi.fn().mockReturnValue({ returning });
|
|
const values = vi.fn().mockReturnValue({ onConflictDoUpdate });
|
|
const insert = vi.fn().mockReturnValue({ values });
|
|
const db: Record<string, unknown> = {
|
|
insert,
|
|
transaction: vi.fn().mockImplementation(async (fn: (tx: unknown) => unknown) => fn(db)),
|
|
};
|
|
return { db, insert, values, onConflictDoUpdate, returning };
|
|
}
|
|
|
|
function makeRecord(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
participantId: "p-1",
|
|
sportsSeasonId: "ss-1",
|
|
wins: 10,
|
|
losses: 5,
|
|
gamesPlayed: 15,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// upsertRegularSeasonStandings
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("upsertRegularSeasonStandings", () => {
|
|
it("returns empty array immediately when given no records", async () => {
|
|
const { db } = makeUpsertDb();
|
|
vi.mocked(database).mockReturnValue(db as never);
|
|
|
|
const result = await upsertRegularSeasonStandings([]);
|
|
expect(result).toEqual([]);
|
|
expect(db.insert).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("maps winPct and gamesBack to strings", async () => {
|
|
const { db, values } = makeUpsertDb([]);
|
|
vi.mocked(database).mockReturnValue(db as never);
|
|
|
|
await upsertRegularSeasonStandings([
|
|
makeRecord({ winPct: 0.667, gamesBack: 2.5 }),
|
|
]);
|
|
|
|
const inserted = values.mock.calls[0][0];
|
|
expect(inserted[0].winPct).toBe("0.667");
|
|
expect(inserted[0].gamesBack).toBe("2.5");
|
|
});
|
|
|
|
it("maps null winPct and gamesBack to null", async () => {
|
|
const { db, values } = makeUpsertDb([]);
|
|
vi.mocked(database).mockReturnValue(db as never);
|
|
|
|
await upsertRegularSeasonStandings([
|
|
makeRecord({ winPct: null, gamesBack: null }),
|
|
]);
|
|
|
|
const inserted = values.mock.calls[0][0];
|
|
expect(inserted[0].winPct).toBeNull();
|
|
expect(inserted[0].gamesBack).toBeNull();
|
|
});
|
|
|
|
it("uses the provided db instead of calling database()", async () => {
|
|
const { db } = makeUpsertDb([]);
|
|
|
|
await upsertRegularSeasonStandings([makeRecord()], db as never);
|
|
|
|
expect(database).not.toHaveBeenCalled();
|
|
expect(db.transaction).toHaveBeenCalled();
|
|
});
|
|
|
|
it("returns the rows from the DB", async () => {
|
|
const row = { id: "row-1", wins: 10 };
|
|
const { db } = makeUpsertDb([row]);
|
|
vi.mocked(database).mockReturnValue(db as never);
|
|
|
|
const result = await upsertRegularSeasonStandings([makeRecord()]);
|
|
expect(result).toEqual([row]);
|
|
});
|
|
|
|
it("inserts multiple records in a single call", async () => {
|
|
const { db, values } = makeUpsertDb([]);
|
|
vi.mocked(database).mockReturnValue(db as never);
|
|
|
|
await upsertRegularSeasonStandings([
|
|
makeRecord({ participantId: "p-1" }),
|
|
makeRecord({ participantId: "p-2" }),
|
|
]);
|
|
|
|
expect(values.mock.calls[0][0]).toHaveLength(2);
|
|
});
|
|
|
|
it("sets syncedAt to now when not provided", async () => {
|
|
const before = new Date();
|
|
const { db, values } = makeUpsertDb([]);
|
|
vi.mocked(database).mockReturnValue(db as never);
|
|
|
|
await upsertRegularSeasonStandings([makeRecord()]);
|
|
|
|
const inserted = values.mock.calls[0][0];
|
|
expect(inserted[0].syncedAt).toBeInstanceOf(Date);
|
|
expect(inserted[0].syncedAt.getTime()).toBeGreaterThanOrEqual(before.getTime());
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// upsertManualStanding
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("upsertManualStanding", () => {
|
|
it("sets syncedAt to null", async () => {
|
|
const { db, values } = makeUpsertDb([{ id: "row-1" }]);
|
|
vi.mocked(database).mockReturnValue(db as never);
|
|
|
|
await upsertManualStanding("p-1", "ss-1", {
|
|
wins: 10,
|
|
losses: 5,
|
|
gamesPlayed: 15,
|
|
});
|
|
|
|
const inserted = values.mock.calls[0][0];
|
|
expect(inserted[0].syncedAt).toBeNull();
|
|
});
|
|
|
|
it("returns the first row", async () => {
|
|
const row = { id: "row-1" };
|
|
const { db } = makeUpsertDb([row]);
|
|
vi.mocked(database).mockReturnValue(db as never);
|
|
|
|
const result = await upsertManualStanding("p-1", "ss-1", {
|
|
wins: 10,
|
|
losses: 5,
|
|
gamesPlayed: 15,
|
|
});
|
|
|
|
expect(result).toEqual(row);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// getRegularSeasonStandings — sorting logic
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function makeRow(overrides: Record<string, unknown>) {
|
|
return {
|
|
conference: null,
|
|
division: null,
|
|
divisionRank: null,
|
|
leagueRank: null,
|
|
participant: {},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeQueryDb(rows: object[]) {
|
|
return {
|
|
query: {
|
|
regularSeasonStandings: {
|
|
findMany: vi.fn().mockResolvedValue(rows),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("getRegularSeasonStandings", () => {
|
|
it("sorts by conference alphabetically", async () => {
|
|
const rows = [
|
|
makeRow({ conference: "Western", leagueRank: 1 }),
|
|
makeRow({ conference: "Eastern", leagueRank: 2 }),
|
|
];
|
|
vi.mocked(database).mockReturnValue(makeQueryDb(rows) as never);
|
|
|
|
const result = await getRegularSeasonStandings("ss-1");
|
|
expect(result[0].conference).toBe("Eastern");
|
|
expect(result[1].conference).toBe("Western");
|
|
});
|
|
|
|
it("sorts by division within conference", async () => {
|
|
const rows = [
|
|
makeRow({ conference: "East", division: "Southeast", leagueRank: 1 }),
|
|
makeRow({ conference: "East", division: "Atlantic", leagueRank: 2 }),
|
|
];
|
|
vi.mocked(database).mockReturnValue(makeQueryDb(rows) as never);
|
|
|
|
const result = await getRegularSeasonStandings("ss-1");
|
|
expect(result[0].division).toBe("Atlantic");
|
|
expect(result[1].division).toBe("Southeast");
|
|
});
|
|
|
|
it("sorts by divisionRank within division", async () => {
|
|
const rows = [
|
|
makeRow({ conference: "East", division: "Atlantic", divisionRank: 3 }),
|
|
makeRow({ conference: "East", division: "Atlantic", divisionRank: 1 }),
|
|
makeRow({ conference: "East", division: "Atlantic", divisionRank: 2 }),
|
|
];
|
|
vi.mocked(database).mockReturnValue(makeQueryDb(rows) as never);
|
|
|
|
const result = await getRegularSeasonStandings("ss-1");
|
|
expect(result.map((r) => r.divisionRank)).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
it("falls back to leagueRank when divisionRank is null", async () => {
|
|
const rows = [
|
|
makeRow({ leagueRank: 3 }),
|
|
makeRow({ leagueRank: 1 }),
|
|
makeRow({ leagueRank: 2 }),
|
|
];
|
|
vi.mocked(database).mockReturnValue(makeQueryDb(rows) as never);
|
|
|
|
const result = await getRegularSeasonStandings("ss-1");
|
|
expect(result.map((r) => r.leagueRank)).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
it("places null conference/division rows last", async () => {
|
|
const rows = [
|
|
makeRow({ conference: null, leagueRank: 1 }),
|
|
makeRow({ conference: "Eastern", leagueRank: 2 }),
|
|
];
|
|
vi.mocked(database).mockReturnValue(makeQueryDb(rows) as never);
|
|
|
|
const result = await getRegularSeasonStandings("ss-1");
|
|
expect(result[0].conference).toBe("Eastern");
|
|
expect(result[1].conference).toBeNull();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// getLastSyncedAt
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("getLastSyncedAt", () => {
|
|
function makeSelectDb(lastSync: Date | null) {
|
|
return {
|
|
select: vi.fn().mockReturnValue({
|
|
from: vi.fn().mockReturnValue({
|
|
where: vi.fn().mockResolvedValue([{ lastSync }]),
|
|
}),
|
|
}),
|
|
};
|
|
}
|
|
|
|
it("returns the max syncedAt date", async () => {
|
|
const date = new Date("2025-03-01T00:00:00Z");
|
|
vi.mocked(database).mockReturnValue(makeSelectDb(date) as never);
|
|
|
|
const result = await getLastSyncedAt("ss-1");
|
|
expect(result).toEqual(date);
|
|
});
|
|
|
|
it("returns null when no synced records exist", async () => {
|
|
vi.mocked(database).mockReturnValue(makeSelectDb(null) as never);
|
|
|
|
const result = await getLastSyncedAt("ss-1");
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// deleteRegularSeasonStandings
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("deleteRegularSeasonStandings", () => {
|
|
it("calls delete on the DB", async () => {
|
|
const where = vi.fn().mockResolvedValue(undefined);
|
|
const del = vi.fn().mockReturnValue({ where });
|
|
const db = { delete: del };
|
|
vi.mocked(database).mockReturnValue(db as never);
|
|
|
|
await deleteRegularSeasonStandings("ss-1");
|
|
expect(del).toHaveBeenCalled();
|
|
expect(where).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// getParticipantStanding
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("getParticipantStanding", () => {
|
|
it("returns the standing row when found", async () => {
|
|
const row = { id: "row-1", participantId: "p-1" };
|
|
const db = {
|
|
query: {
|
|
regularSeasonStandings: {
|
|
findFirst: vi.fn().mockResolvedValue(row),
|
|
},
|
|
},
|
|
};
|
|
vi.mocked(database).mockReturnValue(db as never);
|
|
|
|
const result = await getParticipantStanding("p-1", "ss-1");
|
|
expect(result).toEqual(row);
|
|
});
|
|
|
|
it("returns undefined when not found", async () => {
|
|
const db = {
|
|
query: {
|
|
regularSeasonStandings: {
|
|
findFirst: vi.fn().mockResolvedValue(undefined),
|
|
},
|
|
},
|
|
};
|
|
vi.mocked(database).mockReturnValue(db as never);
|
|
|
|
const result = await getParticipantStanding("p-99", "ss-1");
|
|
expect(result).toBeUndefined();
|
|
});
|
|
});
|