sendou.ink/app/features/leaderboards/LeaderboardRepository.server.test.ts

282 lines
6.7 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { db } from "~/db/sql";
import * as Seasons from "~/features/mmr/core/Seasons";
import type { MainWeaponId } from "~/modules/in-game-lists/types";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import { dbInsertUsers, dbReset } from "~/utils/Test";
import * as LeaderboardRepository from "./LeaderboardRepository.server";
import { MATCHES_COUNT_NEEDED_FOR_LEADERBOARD } from "./leaderboards-constants";
const SEASON = Seasons.currentOrPrevious()!.nth;
const SEASON_RANGE = Seasons.nthToDateRange(SEASON);
const OVER_THRESHOLD = MATCHES_COUNT_NEEDED_FOR_LEADERBOARD + 1;
const IN_SEASON_TIMESTAMP = dateToDatabaseTimestamp(SEASON_RANGE.starts);
const OUT_OF_SEASON_TIMESTAMP = dateToDatabaseTimestamp(
new Date(SEASON_RANGE.starts.getTime() - 60 * 1000),
);
const createGroup = () =>
db
.insertInto("Group")
.values({
status: "INACTIVE",
inviteCode: "test-invite-code",
})
.returning("id")
.executeTakeFirstOrThrow();
const createGroupMatch = async (createdAt: number) => {
const alphaGroup = await createGroup();
const bravoGroup = await createGroup();
return db
.insertInto("GroupMatch")
.values({
alphaGroupId: alphaGroup.id,
bravoGroupId: bravoGroup.id,
chatCode: "test-chat-code",
createdAt,
})
.returning("id")
.executeTakeFirstOrThrow();
};
const createTournamentMatch = async ({
isFinalized,
}: {
isFinalized: boolean;
}) => {
const tournament = await db
.insertInto("Tournament")
.values({
mapPickingStyle: "TO",
settings: JSON.stringify({ bracketProgression: [] }),
isFinalized: isFinalized ? 1 : 0,
})
.returning("id")
.executeTakeFirstOrThrow();
const stage = await db
.insertInto("TournamentStage")
.values({
tournamentId: tournament.id,
name: "Main bracket",
number: 1,
type: "double_elimination",
settings: "{}",
})
.returning("id")
.executeTakeFirstOrThrow();
const group = await db
.insertInto("TournamentGroup")
.values({ stageId: stage.id, number: 1 })
.returning("id")
.executeTakeFirstOrThrow();
const round = await db
.insertInto("TournamentRound")
.values({
stageId: stage.id,
groupId: group.id,
number: 1,
maps: JSON.stringify({ count: 3, type: "BEST_OF" }),
})
.returning("id")
.executeTakeFirstOrThrow();
return db
.insertInto("TournamentMatch")
.values({
stageId: stage.id,
groupId: group.id,
roundId: round.id,
number: 1,
status: 4,
opponentOne: JSON.stringify({ id: null }),
opponentTwo: JSON.stringify({ id: null }),
})
.returning("id")
.executeTakeFirstOrThrow();
};
const reportSendouqWeapons = async (args: {
userId: number;
weaponSplId: MainWeaponId;
count: number;
matchCreatedAt?: number;
}) => {
const match = await createGroupMatch(
args.matchCreatedAt ?? IN_SEASON_TIMESTAMP,
);
await db
.insertInto("ReportedWeapon")
.values(
Array.from({ length: args.count }, (_, mapIndex) => ({
groupMatchId: match.id,
mapIndex,
userId: args.userId,
weaponSplId: args.weaponSplId,
})),
)
.execute();
};
const reportTournamentWeapons = async (args: {
userId: number;
weaponSplId: MainWeaponId;
count: number;
isFinalized?: boolean;
createdAt?: number;
}) => {
const match = await createTournamentMatch({
isFinalized: args.isFinalized ?? true,
});
await db
.insertInto("ReportedWeapon")
.values(
Array.from({ length: args.count }, (_, mapIndex) => ({
tournamentMatchId: match.id,
mapIndex,
userId: args.userId,
weaponSplId: args.weaponSplId,
createdAt: args.createdAt ?? IN_SEASON_TIMESTAMP,
})),
)
.execute();
};
describe("seasonPopularUsersWeapon", () => {
beforeEach(async () => {
await dbInsertUsers(2);
});
afterEach(() => {
dbReset();
});
test("returns user's most reported SendouQ weapon", async () => {
await reportSendouqWeapons({
userId: 1,
weaponSplId: 10,
count: OVER_THRESHOLD,
});
const result = await LeaderboardRepository.seasonPopularUsersWeapon(SEASON);
expect(result).toEqual({ 1: 10 });
});
test("requires more reports than the threshold", async () => {
await reportSendouqWeapons({
userId: 1,
weaponSplId: 10,
count: MATCHES_COUNT_NEEDED_FOR_LEADERBOARD,
});
const result = await LeaderboardRepository.seasonPopularUsersWeapon(SEASON);
expect(result).toEqual({});
});
test("counts weapons reported in finalized tournaments", async () => {
await reportTournamentWeapons({
userId: 1,
weaponSplId: 1000,
count: OVER_THRESHOLD,
});
const result = await LeaderboardRepository.seasonPopularUsersWeapon(SEASON);
expect(result).toEqual({ 1: 1000 });
});
test("ignores weapons reported in unfinalized tournaments", async () => {
await reportTournamentWeapons({
userId: 1,
weaponSplId: 1000,
count: OVER_THRESHOLD,
isFinalized: false,
});
const result = await LeaderboardRepository.seasonPopularUsersWeapon(SEASON);
expect(result).toEqual({});
});
test("combines SendouQ and tournament reports of the same weapon", async () => {
const half = Math.ceil(OVER_THRESHOLD / 2);
await reportSendouqWeapons({ userId: 1, weaponSplId: 10, count: half });
await reportTournamentWeapons({
userId: 1,
weaponSplId: 10,
count: OVER_THRESHOLD - half,
});
const result = await LeaderboardRepository.seasonPopularUsersWeapon(SEASON);
expect(result).toEqual({ 1: 10 });
});
test("picks the most reported weapon across both sources", async () => {
await reportSendouqWeapons({
userId: 1,
weaponSplId: 0,
count: OVER_THRESHOLD + 1,
});
await reportSendouqWeapons({
userId: 1,
weaponSplId: 10,
count: OVER_THRESHOLD - 3,
});
await reportTournamentWeapons({
userId: 1,
weaponSplId: 10,
count: OVER_THRESHOLD - 3,
});
const result = await LeaderboardRepository.seasonPopularUsersWeapon(SEASON);
expect(result).toEqual({ 1: 10 });
});
test("returns weapons of multiple users", async () => {
await reportSendouqWeapons({
userId: 1,
weaponSplId: 10,
count: OVER_THRESHOLD,
});
await reportTournamentWeapons({
userId: 2,
weaponSplId: 1000,
count: OVER_THRESHOLD,
});
const result = await LeaderboardRepository.seasonPopularUsersWeapon(SEASON);
expect(result).toEqual({ 1: 10, 2: 1000 });
});
test("ignores reports outside the season", async () => {
await reportSendouqWeapons({
userId: 1,
weaponSplId: 10,
count: OVER_THRESHOLD,
matchCreatedAt: OUT_OF_SEASON_TIMESTAMP,
});
await reportTournamentWeapons({
userId: 2,
weaponSplId: 1000,
count: OVER_THRESHOLD,
createdAt: OUT_OF_SEASON_TIMESTAMP,
});
const result = await LeaderboardRepository.seasonPopularUsersWeapon(SEASON);
expect(result).toEqual({});
});
});