sendou.ink/app/routines/syncLiveStreams.test.ts
2026-04-06 15:21:15 +03:00

292 lines
6.8 KiB
TypeScript

import { add } from "date-fns";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { db } from "~/db/sql";
import { RunningTournaments } from "~/features/tournament-bracket/core/RunningTournaments.server";
import {
testTournament,
tournamentCtxTeam,
} from "~/features/tournament-bracket/core/tests/test-utils";
import { dbReset } from "~/utils/Test";
import { SyncLiveStreamsRoutine } from "./syncLiveStreams";
const { mockGetStreams } = vi.hoisted(() => ({
mockGetStreams: vi.fn(),
}));
vi.mock("~/modules/twitch", () => ({
getStreams: mockGetStreams,
}));
vi.mock("~/modules/twitch/utils", () => ({
hasTwitchEnvVars: () => true,
}));
vi.mock("~/features/user-page/UserRepository.server", () => ({
findIdsByTwitchUsernames: () => [],
}));
function findAllTournamentStreamers() {
return db.selectFrom("TournamentStreamer").selectAll().execute();
}
function findAllLiveStreams() {
return db.selectFrom("LiveStream").selectAll().execute();
}
function addRunningTournament(
ctx?: Partial<Parameters<typeof testTournament>[0]["ctx"]>,
) {
const tournament = testTournament({ ctx });
RunningTournaments.add(tournament);
return tournament;
}
let timeOffset = 0;
describe("syncLiveStreams tournament streamers", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(
add(new Date("2025-01-15T12:00:00Z"), { minutes: timeOffset }),
);
timeOffset += 31;
dbReset();
RunningTournaments.clear();
mockGetStreams.mockReset();
});
afterEach(() => {
vi.useRealTimers();
});
test("populates LiveStream table with streams", async () => {
mockGetStreams.mockResolvedValue([
{
twitchUserName: "streamer_one",
viewerCount: 100,
thumbnailUrl: "https://thumb.jpg",
},
{
twitchUserName: "streamer_two",
viewerCount: 50,
thumbnailUrl: "https://thumb2.jpg",
},
]);
await SyncLiveStreamsRoutine.run();
const rows = await findAllLiveStreams();
expect(rows).toHaveLength(2);
expect(rows.map((r) => r.twitch).sort()).toEqual([
"streamer_one",
"streamer_two",
]);
expect(rows[0].viewerCount).toBe(100);
});
test("inserts streamer rows for players who are live", async () => {
mockGetStreams.mockResolvedValue([
{ twitchUserName: "player_one", viewerCount: 100, thumbnailUrl: "" },
]);
addRunningTournament({
teams: [
tournamentCtxTeam(1, {
members: [
{
userId: 10,
username: "Player One",
discordId: "10",
discordAvatar: null,
customUrl: null,
inGameName: null,
country: null,
twitch: "player_one",
plusTier: null,
role: "OWNER",
createdAt: 0,
streamTwitch: null,
streamViewerCount: null,
streamThumbnailUrl: null,
},
],
}),
],
});
await SyncLiveStreamsRoutine.run();
const rows = await findAllTournamentStreamers();
expect(rows).toHaveLength(1);
expect(rows[0].userId).toBe(10);
expect(rows[0].twitchAccount).toBe("player_one");
expect(rows[0].tournamentId).toBe(1);
});
test("skips dropped-out teams", async () => {
mockGetStreams.mockResolvedValue([
{ twitchUserName: "dropped_player", viewerCount: 50, thumbnailUrl: "" },
]);
addRunningTournament({
teams: [
tournamentCtxTeam(1, {
droppedOut: 1,
members: [
{
userId: 20,
username: "Dropped",
discordId: "20",
discordAvatar: null,
customUrl: null,
inGameName: null,
country: null,
twitch: "dropped_player",
plusTier: null,
role: "OWNER",
createdAt: 0,
streamTwitch: null,
streamViewerCount: null,
streamThumbnailUrl: null,
},
],
}),
],
});
await SyncLiveStreamsRoutine.run();
const rows = await findAllTournamentStreamers();
expect(rows).toHaveLength(0);
});
test("inserts cast accounts with null userId", async () => {
mockGetStreams.mockResolvedValue([
{ twitchUserName: "caster_account", viewerCount: 200, thumbnailUrl: "" },
]);
addRunningTournament({
castTwitchAccounts: ["caster_account"],
});
await SyncLiveStreamsRoutine.run();
const rows = await findAllTournamentStreamers();
expect(rows).toHaveLength(1);
expect(rows[0].userId).toBeNull();
expect(rows[0].twitchAccount).toBe("caster_account");
});
test("no inserts when no streams match", async () => {
mockGetStreams.mockResolvedValue([
{ twitchUserName: "unrelated_stream", viewerCount: 10, thumbnailUrl: "" },
]);
addRunningTournament({
teams: [
tournamentCtxTeam(1, {
members: [
{
userId: 30,
username: "No Match",
discordId: "30",
discordAvatar: null,
customUrl: null,
inGameName: null,
country: null,
twitch: "different_account",
plusTier: null,
role: "OWNER",
createdAt: 0,
streamTwitch: null,
streamViewerCount: null,
streamThumbnailUrl: null,
},
],
}),
],
});
await SyncLiveStreamsRoutine.run();
const rows = await findAllTournamentStreamers();
expect(rows).toHaveLength(0);
});
test("throttles to run only every 30 minutes", async () => {
mockGetStreams.mockResolvedValue([
{ twitchUserName: "streamer_a", viewerCount: 100, thumbnailUrl: "" },
]);
addRunningTournament({
teams: [
tournamentCtxTeam(1, {
members: [
{
userId: 40,
username: "Streamer A",
discordId: "40",
discordAvatar: null,
customUrl: null,
inGameName: null,
country: null,
twitch: "streamer_a",
plusTier: null,
role: "OWNER",
createdAt: 0,
streamTwitch: null,
streamViewerCount: null,
streamThumbnailUrl: null,
},
],
}),
],
});
await SyncLiveStreamsRoutine.run();
const rowsAfterFirst = await findAllTournamentStreamers();
expect(rowsAfterFirst).toHaveLength(1);
// clear DB and add a different tournament — if throttle works, nothing new is inserted
await db.deleteFrom("TournamentStreamer").execute();
RunningTournaments.clear();
mockGetStreams.mockResolvedValue([
{ twitchUserName: "streamer_b", viewerCount: 50, thumbnailUrl: "" },
]);
addRunningTournament({
id: 2,
teams: [
tournamentCtxTeam(2, {
members: [
{
userId: 50,
username: "Streamer B",
discordId: "50",
discordAvatar: null,
customUrl: null,
inGameName: null,
country: null,
twitch: "streamer_b",
plusTier: null,
role: "OWNER",
createdAt: 0,
streamTwitch: null,
streamViewerCount: null,
streamThumbnailUrl: null,
},
],
}),
],
});
// call again without advancing time — should be throttled
await SyncLiveStreamsRoutine.run();
const rowsAfterSecond = await findAllTournamentStreamers();
expect(rowsAfterSecond).toHaveLength(0);
});
});