sendou.ink/app/features/tournament/TournamentAuditLogRepository.server.test.ts
Kalle 6e987d506f
Some checks are pending
E2E Tests / e2e (push) Waiting to run
Tests and checks on push / run-checks-and-tests (push) Waiting to run
Updates translation progress / update-translation-progress-issue (push) Waiting to run
Tournament layout refresh, improve admin experience (#3152)
2026-06-11 18:31:10 +03:00

317 lines
8.2 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { db } from "~/db/sql";
import type { Tables, TournamentAuditLogMetadata } from "~/db/tables";
import { dbInsertUsers, dbReset, withUserId } from "~/utils/Test";
import * as TournamentAuditLogRepository from "./TournamentAuditLogRepository.server";
const createTournament = () =>
db
.insertInto("Tournament")
.values({
mapPickingStyle: "TO",
settings: JSON.stringify({ bracketProgression: [] }),
})
.returning("id")
.executeTakeFirstOrThrow();
const createTeam = (tournamentId: number, name: string) =>
db
.insertInto("TournamentTeam")
.values({
tournamentId,
name,
inviteCode: `inv-${tournamentId}-${name}`,
})
.returning("id")
.executeTakeFirstOrThrow();
const insertEvent = ({
actorUserId,
...args
}: {
type: Tables["TournamentAuditLog"]["type"];
actorUserId: number;
tournamentTeamId: number;
subjectUserId?: number;
metadata?: TournamentAuditLogMetadata;
}) =>
withUserId(actorUserId, () =>
db
.transaction()
.execute((trx) => TournamentAuditLogRepository.insert(trx, args)),
);
describe("TournamentAuditLogRepository", () => {
beforeEach(async () => {
await dbInsertUsers(3);
});
afterEach(() => {
dbReset();
});
test("insert creates a stable history row from the live team", async () => {
const tournament = await createTournament();
const team = await createTeam(tournament.id, "Team Olive");
await insertEvent({
type: "TEAM_REGISTERED",
actorUserId: 1,
tournamentTeamId: team.id,
});
const teams = await TournamentAuditLogRepository.findTeamsByTournamentId(
tournament.id,
);
expect(teams).toHaveLength(1);
expect(teams[0].tournamentTeamId).toBe(team.id);
expect(teams[0].name).toBe("Team Olive");
});
test("findByTournamentId returns events newest first with resolved relations", async () => {
const tournament = await createTournament();
const team = await createTeam(tournament.id, "Team Olive");
await insertEvent({
type: "TEAM_REGISTERED",
actorUserId: 1,
tournamentTeamId: team.id,
subjectUserId: 1,
});
await insertEvent({
type: "MEMBER_ADDED",
actorUserId: 1,
tournamentTeamId: team.id,
subjectUserId: 2,
});
const events = await TournamentAuditLogRepository.findByTournamentId({
tournamentId: tournament.id,
limit: 30,
offset: 0,
});
expect(events).toHaveLength(2);
// newest first
expect(events[0].type).toBe("MEMBER_ADDED");
expect(events[0].actor?.id).toBe(1);
expect(events[0].subject?.id).toBe(2);
expect(events[0].team?.name).toBe("Team Olive");
expect(events[1].type).toBe("TEAM_REGISTERED");
});
test("team name survives the team being deleted", async () => {
const tournament = await createTournament();
const team = await createTeam(tournament.id, "Team Olive");
await insertEvent({
type: "TEAM_UNREGISTERED",
actorUserId: 1,
tournamentTeamId: team.id,
});
await db
.deleteFrom("TournamentTeam")
.where("TournamentTeam.id", "=", team.id)
.execute();
const events = await TournamentAuditLogRepository.findByTournamentId({
tournamentId: tournament.id,
limit: 30,
offset: 0,
});
expect(events).toHaveLength(1);
expect(events[0].team?.name).toBe("Team Olive");
});
test("a reused team id does not collapse two teams into one history", async () => {
const tournament = await createTournament();
const teamA = await createTeam(tournament.id, "Team A");
await insertEvent({
type: "TEAM_UNREGISTERED",
actorUserId: 1,
tournamentTeamId: teamA.id,
});
await db
.deleteFrom("TournamentTeam")
.where("TournamentTeam.id", "=", teamA.id)
.execute();
const teamB = await createTeam(tournament.id, "Team B");
// SQLite reuses the highest deleted rowid for the next insert
expect(teamB.id).toBe(teamA.id);
await insertEvent({
type: "TEAM_REGISTERED",
actorUserId: 1,
tournamentTeamId: teamB.id,
});
const teams = await TournamentAuditLogRepository.findTeamsByTournamentId(
tournament.id,
);
expect(teams).toHaveLength(2);
expect(teams.map((team) => team.name).sort()).toEqual(["Team A", "Team B"]);
const events = await TournamentAuditLogRepository.findByTournamentId({
tournamentId: tournament.id,
limit: 30,
offset: 0,
});
const eventByType = new Map(events.map((event) => [event.type, event]));
expect(eventByType.get("TEAM_UNREGISTERED")?.team?.name).toBe("Team A");
expect(eventByType.get("TEAM_REGISTERED")?.team?.name).toBe("Team B");
});
test("filters by event type and by team", async () => {
const tournament = await createTournament();
const teamA = await createTeam(tournament.id, "Team A");
const teamB = await createTeam(tournament.id, "Team B");
await insertEvent({
type: "TEAM_REGISTERED",
actorUserId: 1,
tournamentTeamId: teamA.id,
});
await insertEvent({
type: "TEAM_CHECKED_IN",
actorUserId: 1,
tournamentTeamId: teamA.id,
});
await insertEvent({
type: "TEAM_REGISTERED",
actorUserId: 1,
tournamentTeamId: teamB.id,
});
const byType = await TournamentAuditLogRepository.findByTournamentId({
tournamentId: tournament.id,
type: "TEAM_REGISTERED",
limit: 30,
offset: 0,
});
expect(byType).toHaveLength(2);
const teams = await TournamentAuditLogRepository.findTeamsByTournamentId(
tournament.id,
);
const teamAHistoryId = teams.find(
(team) => team.tournamentTeamId === teamA.id,
)?.id;
const byTeam = await TournamentAuditLogRepository.findByTournamentId({
tournamentId: tournament.id,
tournamentTeamHistoryId: teamAHistoryId,
limit: 30,
offset: 0,
});
expect(byTeam).toHaveLength(2);
const count = await TournamentAuditLogRepository.countByTournamentId({
tournamentId: tournament.id,
type: "TEAM_REGISTERED",
});
expect(count).toBe(2);
});
test("paginates via limit and offset", async () => {
const tournament = await createTournament();
const team = await createTeam(tournament.id, "Team Olive");
for (let i = 0; i < 3; i++) {
await insertEvent({
type: "TEAM_CHECKED_IN",
actorUserId: 1,
tournamentTeamId: team.id,
});
}
const firstPage = await TournamentAuditLogRepository.findByTournamentId({
tournamentId: tournament.id,
limit: 2,
offset: 0,
});
const secondPage = await TournamentAuditLogRepository.findByTournamentId({
tournamentId: tournament.id,
limit: 2,
offset: 2,
});
expect(firstPage).toHaveLength(2);
expect(secondPage).toHaveLength(1);
});
test("stores and reads back metadata", async () => {
const tournament = await createTournament();
const team = await createTeam(tournament.id, "Team Olive");
await insertEvent({
type: "TEAM_CHECKED_IN",
actorUserId: 1,
tournamentTeamId: team.id,
metadata: { bracketIdx: 2 },
});
const events = await TournamentAuditLogRepository.findByTournamentId({
tournamentId: tournament.id,
limit: 30,
offset: 0,
});
expect(events[0].metadata?.bracketIdx).toBe(2);
});
test("stores and reads back the in-game name for name change events", async () => {
const tournament = await createTournament();
const team = await createTeam(tournament.id, "Team Olive");
await insertEvent({
type: "UPDATE_IN_GAME_NAME",
actorUserId: 1,
tournamentTeamId: team.id,
subjectUserId: 2,
metadata: { inGameName: "New IGN#1234" },
});
const events = await TournamentAuditLogRepository.findByTournamentId({
tournamentId: tournament.id,
limit: 30,
offset: 0,
});
expect(events[0].type).toBe("UPDATE_IN_GAME_NAME");
expect(events[0].subject?.id).toBe(2);
expect(events[0].metadata?.inGameName).toBe("New IGN#1234");
});
test("updateTeamHistoryName keeps the preserved name current", async () => {
const tournament = await createTournament();
const team = await createTeam(tournament.id, "Old Name");
await insertEvent({
type: "TEAM_REGISTERED",
actorUserId: 1,
tournamentTeamId: team.id,
});
await db.transaction().execute((trx) =>
TournamentAuditLogRepository.updateTeamHistoryName(trx, {
tournamentTeamId: team.id,
name: "New Name",
}),
);
const events = await TournamentAuditLogRepository.findByTournamentId({
tournamentId: tournament.id,
limit: 30,
offset: 0,
});
expect(events[0].team?.name).toBe("New Name");
});
});