mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-13 14:46:10 -05:00
Fix tournament casts appearing for every round vod
This commit is contained in:
295
app/routines/syncTournamentVods.test.ts
Normal file
295
app/routines/syncTournamentVods.test.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { db, sql } from "~/db/sql";
|
||||
import type { CastedMatchesInfo } from "~/db/tables";
|
||||
import { dbInsertUsers, dbReset } from "~/utils/Test";
|
||||
|
||||
const { mockGetUsersByLogin, mockGetArchiveVideos } = vi.hoisted(() => ({
|
||||
mockGetUsersByLogin: vi.fn(),
|
||||
mockGetArchiveVideos: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("~/modules/twitch/vods", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("~/modules/twitch/vods")>();
|
||||
return {
|
||||
...actual,
|
||||
getUsersByLogin: mockGetUsersByLogin,
|
||||
getArchiveVideos: mockGetArchiveVideos,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("~/modules/twitch/utils", () => ({
|
||||
hasTwitchEnvVars: () => true,
|
||||
}));
|
||||
|
||||
const TOURNAMENT_ID = 1;
|
||||
const MATCH_START_SECONDS = 1700000000;
|
||||
|
||||
describe("syncTournamentVods", () => {
|
||||
beforeEach(async () => {
|
||||
dbReset();
|
||||
mockGetUsersByLogin.mockReset();
|
||||
mockGetArchiveVideos.mockReset();
|
||||
await dbInsertUsers(5);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
test("player streamer gets VODs only for matches they participated in", async () => {
|
||||
await seedTournamentWithMatches();
|
||||
await seedStreamer("player_stream", 1);
|
||||
await seedTournamentTeamAndGameResult(1, [1, 2]);
|
||||
await seedTournamentTeamAndGameResult(2, [3, 4]);
|
||||
|
||||
mockGetUsersByLogin.mockResolvedValue([
|
||||
{ id: "twitch-1", login: "player_stream" },
|
||||
]);
|
||||
mockGetArchiveVideos.mockResolvedValue([twitchVideo()]);
|
||||
|
||||
await runProcessOneTournament();
|
||||
|
||||
const vods = await findAllVods();
|
||||
expect(vods).toHaveLength(1);
|
||||
expect(vods[0].matchId).toBe(1);
|
||||
expect(vods[0].userId).toBe(1);
|
||||
expect(vods[0].account).toBe("player_stream");
|
||||
});
|
||||
|
||||
test("cast account in TournamentStreamer without castedMatchHistory does NOT produce VODs", async () => {
|
||||
await seedTournamentWithMatches();
|
||||
await seedStreamer("caster_stream", null);
|
||||
|
||||
mockGetUsersByLogin.mockResolvedValue([
|
||||
{ id: "twitch-c", login: "caster_stream" },
|
||||
]);
|
||||
mockGetArchiveVideos.mockResolvedValue([twitchVideo()]);
|
||||
|
||||
await runProcessOneTournament();
|
||||
|
||||
const vods = await findAllVods();
|
||||
expect(vods).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("cast account with castedMatchHistory produces VODs for casted matches only", async () => {
|
||||
await seedTournamentWithMatches({
|
||||
castedMatchesInfo: {
|
||||
lockedMatches: [],
|
||||
castedMatches: [],
|
||||
castedMatchHistory: [
|
||||
{
|
||||
twitchAccount: "caster_stream",
|
||||
matchId: 2,
|
||||
timestamp: MATCH_START_SECONDS + 1800,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
mockGetUsersByLogin.mockResolvedValue([
|
||||
{ id: "twitch-c", login: "caster_stream" },
|
||||
]);
|
||||
mockGetArchiveVideos.mockResolvedValue([twitchVideo()]);
|
||||
|
||||
await runProcessOneTournament();
|
||||
|
||||
const vods = await findAllVods();
|
||||
expect(vods).toHaveLength(1);
|
||||
expect(vods[0].matchId).toBe(2);
|
||||
expect(vods[0].userId).toBeNull();
|
||||
expect(vods[0].account).toBe("caster_stream");
|
||||
});
|
||||
|
||||
test("player VOD is preferred over cast VOD for same match and account", async () => {
|
||||
await seedTournamentWithMatches({
|
||||
castedMatchesInfo: {
|
||||
lockedMatches: [],
|
||||
castedMatches: [],
|
||||
castedMatchHistory: [
|
||||
{
|
||||
twitchAccount: "dual_stream",
|
||||
matchId: 1,
|
||||
timestamp: MATCH_START_SECONDS,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await seedStreamer("dual_stream", 1);
|
||||
await seedTournamentTeamAndGameResult(1, [1, 2]);
|
||||
|
||||
mockGetUsersByLogin.mockResolvedValue([
|
||||
{ id: "twitch-d", login: "dual_stream" },
|
||||
]);
|
||||
mockGetArchiveVideos.mockResolvedValue([twitchVideo()]);
|
||||
|
||||
await runProcessOneTournament();
|
||||
|
||||
const vods = await findAllVods();
|
||||
expect(vods).toHaveLength(1);
|
||||
expect(vods[0].userId).toBe(1);
|
||||
});
|
||||
|
||||
test("no VODs inserted when no Twitch videos match", async () => {
|
||||
await seedTournamentWithMatches({
|
||||
castedMatchesInfo: {
|
||||
lockedMatches: [],
|
||||
castedMatches: [],
|
||||
castedMatchHistory: [
|
||||
{
|
||||
twitchAccount: "caster_stream",
|
||||
matchId: 1,
|
||||
timestamp: MATCH_START_SECONDS,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
mockGetUsersByLogin.mockResolvedValue([
|
||||
{ id: "twitch-c", login: "caster_stream" },
|
||||
]);
|
||||
// video ends well before match started
|
||||
mockGetArchiveVideos.mockResolvedValue([
|
||||
twitchVideo({
|
||||
createdAt: new Date((MATCH_START_SECONDS - 50000) * 1000).toISOString(),
|
||||
duration: "1h0m0s",
|
||||
}),
|
||||
]);
|
||||
|
||||
await runProcessOneTournament();
|
||||
|
||||
const vods = await findAllVods();
|
||||
expect(vods).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("no matches with startedAt results in no processing", async () => {
|
||||
await seedTournamentWithMatches();
|
||||
|
||||
// clear startedAt on all matches
|
||||
await db.updateTable("TournamentMatch").set({ startedAt: null }).execute();
|
||||
|
||||
await runProcessOneTournament();
|
||||
|
||||
const vods = await findAllVods();
|
||||
expect(vods).toHaveLength(0);
|
||||
expect(mockGetUsersByLogin).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
function twitchVideo({
|
||||
id = "video1",
|
||||
createdAt = new Date((MATCH_START_SECONDS - 600) * 1000).toISOString(),
|
||||
duration = "2h0m0s",
|
||||
viewCount = 100,
|
||||
} = {}) {
|
||||
return {
|
||||
id,
|
||||
created_at: createdAt,
|
||||
duration,
|
||||
view_count: viewCount,
|
||||
};
|
||||
}
|
||||
|
||||
async function seedTournamentWithMatches({
|
||||
castedMatchesInfo,
|
||||
}: {
|
||||
castedMatchesInfo?: CastedMatchesInfo;
|
||||
} = {}) {
|
||||
sql
|
||||
.prepare(
|
||||
/*sql*/ `insert into "Tournament" ("id", "mapPickingStyle", "settings", "isFinalized", "castedMatchesInfo") values (?, 'AUTO_SZ', ?, 1, ?)`,
|
||||
)
|
||||
.run(
|
||||
TOURNAMENT_ID,
|
||||
JSON.stringify({
|
||||
bracketProgression: [
|
||||
{ type: "double_elimination", name: "Main Bracket" },
|
||||
],
|
||||
}),
|
||||
castedMatchesInfo ? JSON.stringify(castedMatchesInfo) : null,
|
||||
);
|
||||
|
||||
sql
|
||||
.prepare(
|
||||
/*sql*/ `insert into "TournamentStage" ("id", "tournamentId", "name", "type", "settings", "number") values (1, ?, 'Main Bracket', 'double_elimination', '{}', 0)`,
|
||||
)
|
||||
.run(TOURNAMENT_ID);
|
||||
|
||||
sql
|
||||
.prepare(
|
||||
/*sql*/ `insert into "TournamentGroup" ("id", "stageId", "number") values (1, 1, 1)`,
|
||||
)
|
||||
.run();
|
||||
|
||||
sql
|
||||
.prepare(
|
||||
/*sql*/ `insert into "TournamentRound" ("id", "stageId", "groupId", "number") values (1, 1, 1, 1)`,
|
||||
)
|
||||
.run();
|
||||
|
||||
const insertTournamentMatchStm = sql.prepare(
|
||||
/*sql*/ `insert into "TournamentMatch" ("id", "roundId", "stageId", "groupId", "number", "opponentOne", "opponentTwo", "status", "startedAt") values (?, 1, 1, 1, ?, ?, ?, 4, ?)`,
|
||||
);
|
||||
|
||||
insertTournamentMatchStm.run(
|
||||
1,
|
||||
1,
|
||||
JSON.stringify({ id: 1 }),
|
||||
JSON.stringify({ id: 2 }),
|
||||
MATCH_START_SECONDS,
|
||||
);
|
||||
|
||||
insertTournamentMatchStm.run(
|
||||
2,
|
||||
2,
|
||||
JSON.stringify({ id: 3 }),
|
||||
JSON.stringify({ id: 4 }),
|
||||
MATCH_START_SECONDS + 1800,
|
||||
);
|
||||
}
|
||||
|
||||
async function seedTournamentTeamAndGameResult(
|
||||
matchId: number,
|
||||
participantUserIds: number[],
|
||||
) {
|
||||
const teamId = matchId * 100;
|
||||
|
||||
sql
|
||||
.prepare(
|
||||
/*sql*/ `insert or ignore into "TournamentTeam" ("id", "name", "tournamentId", "inviteCode") values (?, ?, ?, ?)`,
|
||||
)
|
||||
.run(teamId, `Team ${teamId}`, TOURNAMENT_ID, `code-${teamId}`);
|
||||
|
||||
const { lastInsertRowid: gameResultId } = sql
|
||||
.prepare(
|
||||
/*sql*/ `insert into "TournamentMatchGameResult" ("matchId", "number", "stageId", "mode", "source", "winnerTeamId", "reporterId") values (?, 1, 1, 'SZ', '{}', ?, 1)`,
|
||||
)
|
||||
.run(matchId, teamId);
|
||||
|
||||
for (const userId of participantUserIds) {
|
||||
sql
|
||||
.prepare(
|
||||
/*sql*/ `insert into "TournamentMatchGameResultParticipant" ("matchGameResultId", "userId", "tournamentTeamId") values (?, ?, ?)`,
|
||||
)
|
||||
.run(Number(gameResultId), userId, teamId);
|
||||
}
|
||||
}
|
||||
|
||||
async function seedStreamer(
|
||||
twitchAccount: string,
|
||||
userId: number | null = null,
|
||||
) {
|
||||
await db
|
||||
.insertInto("TournamentStreamer")
|
||||
.values({ tournamentId: TOURNAMENT_ID, twitchAccount, userId })
|
||||
.execute();
|
||||
}
|
||||
|
||||
function findAllVods() {
|
||||
return db.selectFrom("TournamentMatchVod").selectAll().execute();
|
||||
}
|
||||
|
||||
// lazy-import so mocks are in place before the module loads
|
||||
async function runProcessOneTournament() {
|
||||
const { processOneTournament } = await import("./syncTournamentVods");
|
||||
return processOneTournament(TOURNAMENT_ID);
|
||||
}
|
||||
@@ -63,6 +63,12 @@ export async function processOneTournament(tournamentId: number) {
|
||||
);
|
||||
|
||||
for (const streamer of streamers) {
|
||||
const dbUserId =
|
||||
streamerDbUserIds.get(streamer.twitchAccount.toLowerCase()) ?? null;
|
||||
|
||||
// cast accounts (null userId) are handled via castedMatchHistory below
|
||||
if (dbUserId === null) continue;
|
||||
|
||||
const twitchUserId = loginToTwitchId.get(
|
||||
streamer.twitchAccount.toLowerCase(),
|
||||
);
|
||||
@@ -74,16 +80,11 @@ export async function processOneTournament(tournamentId: number) {
|
||||
);
|
||||
if (!videos) continue;
|
||||
|
||||
const dbUserId =
|
||||
streamerDbUserIds.get(streamer.twitchAccount.toLowerCase()) ?? null;
|
||||
|
||||
for (const match of matches) {
|
||||
if (!match.startedAt) continue;
|
||||
|
||||
if (dbUserId !== null) {
|
||||
const matchParticipants = participantsByMatch.get(match.id);
|
||||
if (!matchParticipants?.has(dbUserId)) continue;
|
||||
}
|
||||
const matchParticipants = participantsByMatch.get(match.id);
|
||||
if (!matchParticipants?.has(dbUserId)) continue;
|
||||
|
||||
const vodMatch = findMatchingVod(match.startedAt, match, videos);
|
||||
if (!vodMatch) continue;
|
||||
|
||||
BIN
db-test.sqlite3
BIN
db-test.sqlite3
Binary file not shown.
Reference in New Issue
Block a user