mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-26 04:57:08 -05:00
Tournament factories
This commit is contained in:
@@ -1,23 +1,8 @@
|
||||
import { modesShort, rankedModesShort } from "~/modules/in-game-lists/modes";
|
||||
import { rankedModesShort } from "~/modules/in-game-lists/modes";
|
||||
import { stageIds } from "~/modules/in-game-lists/stage-ids";
|
||||
import type { ModeWithStage } from "~/modules/in-game-lists/types";
|
||||
import { faker } from "./faker";
|
||||
|
||||
/** A random mode, turf war included. */
|
||||
export function mode() {
|
||||
return faker.helpers.arrayElement(modesShort);
|
||||
}
|
||||
|
||||
/** A random ranked mode. */
|
||||
export function rankedMode() {
|
||||
return faker.helpers.arrayElement(rankedModesShort);
|
||||
}
|
||||
|
||||
/** A random stage. */
|
||||
export function stageId() {
|
||||
return faker.helpers.arrayElement(stageIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* A map list of `count` maps, rotating through the ranked modes and never repeating
|
||||
* a stage, the way a real one looks. Callers add whatever `source` their domain uses.
|
||||
|
||||
16
app/db/seed/core/actAs.ts
Normal file
16
app/db/seed/core/actAs.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
type AuthenticatedUser,
|
||||
userAsyncLocalStorage,
|
||||
} from "~/features/auth/core/user-context.server";
|
||||
|
||||
/**
|
||||
* Runs `fn` inside the acting-user store, so that repository functions resolving
|
||||
* the actor via `actorId()` see `userId` as the acting user. Needed because seeding
|
||||
* happens outside a request, where there is no acting user at all.
|
||||
*/
|
||||
export function actAs<T>(userId: number, fn: () => T): T {
|
||||
return userAsyncLocalStorage.run(
|
||||
{ user: { id: userId } as AuthenticatedUser },
|
||||
fn,
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ type Options = {
|
||||
* creates with the group; the members named by `additionalMemberUserIds` join it
|
||||
* the way they do in production. Invite and chat codes are the repository's own.
|
||||
*/
|
||||
export const { create, createMany } = defineFactory({
|
||||
export const { create } = defineFactory({
|
||||
defaults: () => ({
|
||||
status: "ACTIVE" as const,
|
||||
additionalMemberUserIds: [],
|
||||
|
||||
@@ -12,7 +12,7 @@ type Options = {
|
||||
|
||||
/** Creates SendouQ matches. Both groups have to be full, as they are when the
|
||||
* matchmaking UI creates a match. */
|
||||
export const { create, createMany } = defineFactory({
|
||||
export const { create } = defineFactory({
|
||||
defaults: () => ({
|
||||
mapList: SplatoonFaker.mapList(SENDOUQ_BEST_OF).map((map) => ({
|
||||
...map,
|
||||
|
||||
69
app/db/seed/factories/TournamentFactory.ts
Normal file
69
app/db/seed/factories/TournamentFactory.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { TournamentSettings } from "~/db/tables-json";
|
||||
import * as CalendarRepository from "~/features/calendar/CalendarRepository.server";
|
||||
import * as TournamentRepository from "~/features/tournament/TournamentRepository.server";
|
||||
import { databaseTimestampNow } from "~/utils/dates";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { defineFactory } from "../core/defineFactory";
|
||||
import { faker } from "../core/faker";
|
||||
|
||||
const SINGLE_ELIMINATION: TournamentSettings["bracketProgression"] = [
|
||||
{
|
||||
name: "Bracket",
|
||||
type: "single_elimination",
|
||||
requiresCheckIn: false,
|
||||
settings: {
|
||||
thirdPlaceMatch: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** The wrapping calendar event is not the caller's to choose, so it is not an argument. */
|
||||
type InsertArgs = Omit<
|
||||
Parameters<typeof CalendarRepository.insert>[0],
|
||||
"isFullTournament"
|
||||
>;
|
||||
|
||||
type Options = {
|
||||
/** Mark the tournament finished without recording any results. For cases that
|
||||
* only need the flag; a tournament with real results is finalized by
|
||||
* `TournamentRepository.finalize` with a summary. */
|
||||
isFinalized: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates tournaments. Aggregate factory: the `CalendarEvent` wrapping the
|
||||
* tournament and its start date are created with it, because there is no such thing
|
||||
* as a tournament without one. Returns both ids.
|
||||
*
|
||||
* The bracket is a single elimination one unless `bracketProgression` says otherwise.
|
||||
*/
|
||||
export const { create } = defineFactory({
|
||||
defaults: () => ({
|
||||
name: faker.company.name(),
|
||||
description: null,
|
||||
discordInviteCode: null,
|
||||
bracketUrl: faker.internet.url(),
|
||||
organizationId: null,
|
||||
tags: null,
|
||||
badges: [],
|
||||
rules: null,
|
||||
startTimes: [databaseTimestampNow()],
|
||||
mapPickingStyle: "TO" as const,
|
||||
bracketProgression: SINGLE_ELIMINATION,
|
||||
}),
|
||||
insert: async (args: InsertArgs) => {
|
||||
const { eventId, tournamentId } = await CalendarRepository.insert({
|
||||
...args,
|
||||
isFullTournament: true,
|
||||
});
|
||||
|
||||
invariant(tournamentId, "Expected the tournament to be created");
|
||||
|
||||
return { id: tournamentId, eventId };
|
||||
},
|
||||
applyOptions: async (tournament, { isFinalized }: Options) => {
|
||||
if (!isFinalized) return;
|
||||
|
||||
await TournamentRepository.finalizeWithoutSummary(tournament.id);
|
||||
},
|
||||
});
|
||||
42
app/db/seed/factories/TournamentTeamFactory.ts
Normal file
42
app/db/seed/factories/TournamentTeamFactory.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server";
|
||||
import { actAs } from "../core/actAs";
|
||||
import { defineFactory } from "../core/defineFactory";
|
||||
import { faker } from "../core/faker";
|
||||
|
||||
type InsertArgs = Parameters<typeof TournamentTeamRepository.insert>[0];
|
||||
|
||||
type Options = {
|
||||
/** Has the team checked in to the tournament? */
|
||||
isCheckedIn: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates tournament teams. `userId` is the owner, on whose behalf the team is
|
||||
* registered; the members named by `additionalMemberUserIds` are added to it the
|
||||
* way they are in production. Invite code and in-game names are the repository's own.
|
||||
*/
|
||||
export const { create } = defineFactory({
|
||||
defaults: () => ({
|
||||
team: {
|
||||
name: faker.company.name(),
|
||||
prefersNotToHost: 0 as const,
|
||||
teamId: null,
|
||||
},
|
||||
additionalMemberUserIds: [],
|
||||
avatarImgId: null,
|
||||
}),
|
||||
insert: async ({ userId, ...args }: InsertArgs) => {
|
||||
const team = await actAs(userId, () =>
|
||||
TournamentTeamRepository.insert({ ...args, userId }),
|
||||
);
|
||||
|
||||
return { id: team.id, ownerUserId: userId };
|
||||
},
|
||||
applyOptions: async (team, { isCheckedIn }: Options) => {
|
||||
if (!isCheckedIn) return;
|
||||
|
||||
await actAs(team.ownerUserId, () =>
|
||||
TournamentTeamRepository.checkIn(team.id),
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import * as TournamentFactory from "~/db/seed/factories/TournamentFactory";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
import { db } from "~/db/sql";
|
||||
import * as Seasons from "~/features/mmr/core/Seasons";
|
||||
@@ -43,19 +44,16 @@ const createGroupMatch = async (createdAt: number) => {
|
||||
};
|
||||
|
||||
const createTournamentMatch = async ({
|
||||
authorId,
|
||||
isFinalized,
|
||||
}: {
|
||||
authorId: number;
|
||||
isFinalized: boolean;
|
||||
}) => {
|
||||
const tournament = await db
|
||||
.insertInto("Tournament")
|
||||
.values({
|
||||
mapPickingStyle: "TO",
|
||||
settings: JSON.stringify({ bracketProgression: [] }),
|
||||
isFinalized: isFinalized ? 1 : 0,
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
const tournament = await TournamentFactory.create(
|
||||
{ authorId },
|
||||
{ isFinalized },
|
||||
);
|
||||
|
||||
const stage = await db
|
||||
.insertInto("TournamentStage")
|
||||
@@ -131,6 +129,7 @@ const reportTournamentWeapons = async (args: {
|
||||
createdAt?: number;
|
||||
}) => {
|
||||
const match = await createTournamentMatch({
|
||||
authorId: args.userId,
|
||||
isFinalized: args.isFinalized ?? true,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import * as TournamentFactory from "~/db/seed/factories/TournamentFactory";
|
||||
import * as TournamentTeamFactory from "~/db/seed/factories/TournamentTeamFactory";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
import { db } from "~/db/sql";
|
||||
import { dbReset, withUserId } from "~/utils/Test";
|
||||
@@ -7,18 +9,21 @@ import * as TournamentLFGRepository from "./TournamentLFGRepository.server";
|
||||
const users = UserFactory.pool();
|
||||
|
||||
const createTournament = () =>
|
||||
db
|
||||
.insertInto("Tournament")
|
||||
.values({
|
||||
mapPickingStyle: "TO",
|
||||
settings: JSON.stringify({ bracketProgression: [] }),
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
TournamentFactory.create({ authorId: users.id(1) });
|
||||
|
||||
const createPlaceholder = (tournamentId: number, userId: number) =>
|
||||
TournamentLFGRepository.insertPlaceholderTeam({ tournamentId, userId });
|
||||
|
||||
const createRegisteredTeam = (
|
||||
tournamentId: number,
|
||||
[owner, ...members]: number[],
|
||||
) =>
|
||||
TournamentTeamFactory.create({
|
||||
tournamentId,
|
||||
userId: owner,
|
||||
additionalMemberUserIds: members,
|
||||
});
|
||||
|
||||
describe("insertPlaceholderTeam", () => {
|
||||
beforeEach(async () => {
|
||||
await users.create(2);
|
||||
@@ -93,7 +98,7 @@ describe("findLookingTeamsByTournamentId", () => {
|
||||
);
|
||||
const members = groups[0].members;
|
||||
|
||||
expect(members[0].id).toBe(1);
|
||||
expect(members[0].id).toBe(users.id(1));
|
||||
expect(members[0].username).toBeDefined();
|
||||
expect(members[0].role).toBe("OWNER");
|
||||
});
|
||||
@@ -228,36 +233,6 @@ describe("startLooking", () => {
|
||||
await dbReset();
|
||||
});
|
||||
|
||||
const createRegisteredTeam = async (
|
||||
tournamentId: number,
|
||||
memberUserIds: number[],
|
||||
) => {
|
||||
const team = await db
|
||||
.insertInto("TournamentTeam")
|
||||
.values({
|
||||
tournamentId,
|
||||
name: "Real Team",
|
||||
inviteCode: `inv-${tournamentId}-${memberUserIds.join("-")}`,
|
||||
isLooking: 0,
|
||||
isPlaceholder: 0,
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
for (const [idx, userId] of memberUserIds.entries()) {
|
||||
await db
|
||||
.insertInto("TournamentTeamMember")
|
||||
.values({
|
||||
tournamentTeamId: team.id,
|
||||
userId,
|
||||
role: idx === 0 ? "OWNER" : "REGULAR",
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
return team;
|
||||
};
|
||||
|
||||
test("generates chatCode for a 2+ member team", async () => {
|
||||
const tournament = await createTournament();
|
||||
const team = await createRegisteredTeam(tournament.id, [
|
||||
@@ -546,16 +521,12 @@ describe("updateMemberRole", () => {
|
||||
|
||||
test("changes role from REGULAR to MANAGER", async () => {
|
||||
const tournament = await createTournament();
|
||||
const team = await createPlaceholder(tournament.id, users.id(1));
|
||||
|
||||
await db
|
||||
.insertInto("TournamentTeamMember")
|
||||
.values({
|
||||
tournamentTeamId: team.id,
|
||||
userId: users.id(2),
|
||||
role: "REGULAR",
|
||||
})
|
||||
.execute();
|
||||
const team = await TournamentTeamFactory.create({
|
||||
tournamentId: tournament.id,
|
||||
userId: users.id(1),
|
||||
additionalMemberUserIds: [users.id(2)],
|
||||
});
|
||||
await TournamentLFGRepository.startLooking(team.id);
|
||||
|
||||
await TournamentLFGRepository.updateMemberRole({
|
||||
userId: users.id(2),
|
||||
@@ -650,26 +621,8 @@ describe("leaveLfg", () => {
|
||||
test("sets isLooking=0 for non-placeholder team", async () => {
|
||||
const tournament = await createTournament();
|
||||
|
||||
const team = await db
|
||||
.insertInto("TournamentTeam")
|
||||
.values({
|
||||
tournamentId: tournament.id,
|
||||
name: "Real Team",
|
||||
inviteCode: "abc",
|
||||
isLooking: 1,
|
||||
isPlaceholder: 0,
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
await db
|
||||
.insertInto("TournamentTeamMember")
|
||||
.values({
|
||||
tournamentTeamId: team.id,
|
||||
userId: users.id(1),
|
||||
role: "OWNER",
|
||||
})
|
||||
.execute();
|
||||
const team = await createRegisteredTeam(tournament.id, [users.id(1)]);
|
||||
await TournamentLFGRepository.startLooking(team.id);
|
||||
|
||||
await TournamentLFGRepository.leaveLfg({
|
||||
userId: users.id(1),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import * as TournamentFactory from "~/db/seed/factories/TournamentFactory";
|
||||
import * as TournamentTeamFactory from "~/db/seed/factories/TournamentTeamFactory";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
import { db } from "~/db/sql";
|
||||
import { dbReset } from "~/utils/Test";
|
||||
@@ -8,25 +10,16 @@ let teamOneMember: { id: number };
|
||||
let teamTwoMember: { id: number };
|
||||
|
||||
const createTournament = () =>
|
||||
db
|
||||
.insertInto("Tournament")
|
||||
.values({
|
||||
mapPickingStyle: "TO",
|
||||
settings: JSON.stringify({ bracketProgression: [] }),
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
TournamentFactory.create({ authorId: teamOneMember.id });
|
||||
|
||||
/** Both users are on both teams, so that either team's results can be attributed. */
|
||||
const createTeam = (tournamentId: number, name: string) =>
|
||||
db
|
||||
.insertInto("TournamentTeam")
|
||||
.values({
|
||||
tournamentId,
|
||||
name,
|
||||
inviteCode: `inv-${tournamentId}-${name}`,
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
TournamentTeamFactory.create({
|
||||
tournamentId,
|
||||
userId: teamOneMember.id,
|
||||
additionalMemberUserIds: [teamTwoMember.id],
|
||||
team: { name, prefersNotToHost: 0, teamId: null },
|
||||
});
|
||||
|
||||
const createStage = (tournamentId: number, name: string, number: number) =>
|
||||
db
|
||||
@@ -140,18 +133,6 @@ describe("findByTournamentTeamId", () => {
|
||||
const teamA = await createTeam(tournament.id, "A");
|
||||
const teamB = await createTeam(tournament.id, "B");
|
||||
|
||||
// Insert team members so we have someone to attribute results to
|
||||
for (const id of [teamOneMember.id, teamTwoMember.id]) {
|
||||
await db
|
||||
.insertInto("TournamentTeamMember")
|
||||
.values({ tournamentTeamId: teamA.id, userId: id, role: "OWNER" })
|
||||
.execute();
|
||||
await db
|
||||
.insertInto("TournamentTeamMember")
|
||||
.values({ tournamentTeamId: teamB.id, userId: id, role: "OWNER" })
|
||||
.execute();
|
||||
}
|
||||
|
||||
const stage1 = await createStage(tournament.id, "Stage 1", 1);
|
||||
const stage1Group = await createGroup(stage1.id, 8);
|
||||
const stage1Round = await createRound(stage1.id, stage1Group.id, 1);
|
||||
|
||||
@@ -7,14 +7,12 @@ vi.mock("~/features/chat/ChatSystemMessage.server", () => ({
|
||||
}));
|
||||
|
||||
import type { z } from "zod";
|
||||
import * as TournamentFactory from "~/db/seed/factories/TournamentFactory";
|
||||
import * as TournamentTeamFactory from "~/db/seed/factories/TournamentTeamFactory";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
import { db } from "~/db/sql";
|
||||
import { action as removeMemberApiAction } from "~/features/api-public/routes/tournament.$id.teams.$teamId.remove-member";
|
||||
import {
|
||||
dbInsertTournament,
|
||||
dbInsertTournamentTeam,
|
||||
dbStartTournament,
|
||||
} from "~/features/tournament/tournament-test-utils";
|
||||
import { dbStartTournament } from "~/features/tournament/tournament-test-utils";
|
||||
import type { matchSchema } from "~/features/tournament-bracket/tournament-bracket-schemas.server";
|
||||
import type { SerializeFrom } from "~/utils/remix";
|
||||
import {
|
||||
@@ -85,19 +83,26 @@ const removeMemberAction = ({
|
||||
{ user: "admin", params: { id: "1", teamId: String(teamId) } },
|
||||
);
|
||||
|
||||
const createTeam = (
|
||||
tournamentId: number,
|
||||
[owner, ...members]: number[],
|
||||
): Promise<{ id: number }> =>
|
||||
TournamentTeamFactory.create(
|
||||
{
|
||||
tournamentId,
|
||||
userId: owner,
|
||||
additionalMemberUserIds: members,
|
||||
},
|
||||
{ isCheckedIn: true },
|
||||
);
|
||||
|
||||
describe("Tournament match page", () => {
|
||||
beforeEach(async () => {
|
||||
await UserFactory.createMany(10);
|
||||
await dbInsertTournament();
|
||||
await dbInsertTournamentTeam({
|
||||
membersCount: 6,
|
||||
ownerId: 1,
|
||||
});
|
||||
await dbInsertTournamentTeam({
|
||||
membersCount: 4,
|
||||
ownerId: 7,
|
||||
});
|
||||
await dbStartTournament([1, 2]);
|
||||
const tournament = await TournamentFactory.create({ authorId: 1 });
|
||||
const teamOne = await createTeam(tournament.id, [1, 2, 3, 4, 5, 6]);
|
||||
const teamTwo = await createTeam(tournament.id, [7, 8, 9, 10]);
|
||||
await dbStartTournament([teamOne.id, teamTwo.id], tournament.id);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -219,26 +224,18 @@ describe("Tournament match page", () => {
|
||||
});
|
||||
|
||||
it("should not require setting active roster if both teams have no subs", async () => {
|
||||
await dbInsertTournament();
|
||||
await dbInsertTournamentTeam({
|
||||
membersCount: 4,
|
||||
ownerId: 1,
|
||||
tournamentId: 2,
|
||||
});
|
||||
await dbInsertTournamentTeam({
|
||||
membersCount: 4,
|
||||
ownerId: 5,
|
||||
tournamentId: 2,
|
||||
});
|
||||
await dbStartTournament([3, 4], 2);
|
||||
const tournament = await TournamentFactory.create({ authorId: 1 });
|
||||
const teamOne = await createTeam(tournament.id, [1, 2, 3, 4]);
|
||||
const teamTwo = await createTeam(tournament.id, [5, 6, 7, 8]);
|
||||
await dbStartTournament([teamOne.id, teamTwo.id], tournament.id);
|
||||
|
||||
const res = await reportScoreAction({
|
||||
position: 0,
|
||||
params: {
|
||||
id: "2",
|
||||
id: String(tournament.id),
|
||||
mid: "2",
|
||||
},
|
||||
winnerTeamId: 3,
|
||||
winnerTeamId: teamOne.id,
|
||||
});
|
||||
|
||||
expect(res).toBe(null);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as TournamentFactory from "~/db/seed/factories/TournamentFactory";
|
||||
import * as TournamentTeamFactory from "~/db/seed/factories/TournamentTeamFactory";
|
||||
import { db } from "~/db/sql";
|
||||
import invariant from "../../utils/invariant";
|
||||
import { dbInsertTournament } from "../tournament/tournament-test-utils";
|
||||
import { withUserId } from "~/utils/Test";
|
||||
import * as TournamentTeamRepository from "../tournament/TournamentTeamRepository.server";
|
||||
|
||||
/**
|
||||
* Seeds a played tournament hosted by `organizationId`, starting at `startTime`
|
||||
@@ -25,55 +27,39 @@ export async function seedOrgEventWithParticipants({
|
||||
participantUserIds: number[];
|
||||
checkIn?: "in" | "out" | "none";
|
||||
}) {
|
||||
const { tournamentId } = await dbInsertTournament({
|
||||
const [ownerUserId, ...memberUserIds] = participantUserIds;
|
||||
|
||||
const tournament = await TournamentFactory.create({
|
||||
authorId: ownerUserId,
|
||||
organizationId,
|
||||
startTime,
|
||||
startTimes: [startTime],
|
||||
});
|
||||
|
||||
invariant(tournamentId, "Expected tournamentId to be defined");
|
||||
const team = await TournamentTeamFactory.create(
|
||||
{
|
||||
tournamentId: tournament.id,
|
||||
userId: ownerUserId,
|
||||
additionalMemberUserIds: memberUserIds,
|
||||
},
|
||||
{ isCheckedIn: checkIn === "in" },
|
||||
);
|
||||
|
||||
const event = await db
|
||||
.insertInto("CalendarEvent")
|
||||
.values({
|
||||
authorId: participantUserIds[0],
|
||||
name: `Event ${tournamentId}`,
|
||||
bracketUrl: "https://example.com/bracket",
|
||||
organizationId,
|
||||
tournamentId,
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
await db
|
||||
.insertInto("CalendarEventDate")
|
||||
.values({ eventId: event.id, startsAt: startTime })
|
||||
.execute();
|
||||
|
||||
const team = await db
|
||||
.insertInto("TournamentTeam")
|
||||
.values({
|
||||
tournamentId,
|
||||
name: `Team ${tournamentId}`,
|
||||
inviteCode: `inv-${tournamentId}`,
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
if (checkIn !== "none") {
|
||||
await db
|
||||
.insertInto("TournamentTeamCheckIn")
|
||||
.values({
|
||||
if (checkIn === "out") {
|
||||
// a check out leaves a row of its own only when it concerns one bracket,
|
||||
// otherwise checking out simply undoes the check in
|
||||
await withUserId(ownerUserId, async () => {
|
||||
await TournamentTeamRepository.checkIn(team.id, { bracketIdx: 0 });
|
||||
await TournamentTeamRepository.checkOut({
|
||||
tournamentTeamId: team.id,
|
||||
checkedInAt: startTime,
|
||||
isCheckOut: checkIn === "out" ? 1 : 0,
|
||||
})
|
||||
.execute();
|
||||
bracketIdx: 0,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const stage = await db
|
||||
.insertInto("TournamentStage")
|
||||
.values({
|
||||
tournamentId,
|
||||
tournamentId: tournament.id,
|
||||
name: "Stage",
|
||||
number: 1,
|
||||
type: "single_elimination",
|
||||
@@ -118,7 +104,7 @@ export async function seedOrgEventWithParticipants({
|
||||
matchId: match.id,
|
||||
mode: "SZ",
|
||||
number: 1,
|
||||
reporterId: participantUserIds[0],
|
||||
reporterId: ownerUserId,
|
||||
source: "TO",
|
||||
stageId: 1,
|
||||
winnerTeamId: team.id,
|
||||
@@ -137,5 +123,5 @@ export async function seedOrgEventWithParticipants({
|
||||
)
|
||||
.execute();
|
||||
|
||||
return { tournamentId, teamId: team.id };
|
||||
return { tournamentId: tournament.id, teamId: team.id };
|
||||
}
|
||||
|
||||
@@ -1,34 +1,38 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import * as TournamentFactory from "~/db/seed/factories/TournamentFactory";
|
||||
import * as TournamentTeamFactory from "~/db/seed/factories/TournamentTeamFactory";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
import { db } from "~/db/sql";
|
||||
import type { Tables } from "~/db/tables";
|
||||
import type { TournamentAuditLogMetadata } from "~/db/tables-json";
|
||||
import { dbReset, withUserId } from "~/utils/Test";
|
||||
import * as TournamentAuditLogRepository from "./TournamentAuditLogRepository.server";
|
||||
import * as TournamentTeamRepository from "./TournamentTeamRepository.server";
|
||||
|
||||
let actor: { id: number };
|
||||
let subject: { id: number };
|
||||
|
||||
const createTournament = () =>
|
||||
db
|
||||
.insertInto("Tournament")
|
||||
.values({
|
||||
mapPickingStyle: "TO",
|
||||
settings: JSON.stringify({ bracketProgression: [] }),
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
const createTournament = () => TournamentFactory.create({ authorId: actor.id });
|
||||
|
||||
const createTeam = (tournamentId: number, name: string) =>
|
||||
db
|
||||
.insertInto("TournamentTeam")
|
||||
.values({
|
||||
/** Registering a team is itself audited, so the team comes with a `TEAM_REGISTERED` event. */
|
||||
const createTeam = (
|
||||
tournamentId: number,
|
||||
name: string,
|
||||
options?: { isCheckedIn: boolean },
|
||||
) =>
|
||||
TournamentTeamFactory.create(
|
||||
{
|
||||
tournamentId,
|
||||
name,
|
||||
inviteCode: `inv-${tournamentId}-${name}`,
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
userId: actor.id,
|
||||
team: { name, prefersNotToHost: 0, teamId: null },
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
const deleteTeam = (tournamentTeamId: number) =>
|
||||
withUserId(actor.id, () =>
|
||||
TournamentTeamRepository.deleteById(tournamentTeamId),
|
||||
);
|
||||
|
||||
const insertEvent = ({
|
||||
actorUserId,
|
||||
@@ -59,12 +63,6 @@ describe("TournamentAuditLogRepository", () => {
|
||||
const tournament = await createTournament();
|
||||
const team = await createTeam(tournament.id, "Team Olive");
|
||||
|
||||
await insertEvent({
|
||||
type: "TEAM_REGISTERED",
|
||||
actorUserId: actor.id,
|
||||
tournamentTeamId: team.id,
|
||||
});
|
||||
|
||||
const teams = await TournamentAuditLogRepository.findTeamsByTournamentId(
|
||||
tournament.id,
|
||||
);
|
||||
@@ -76,19 +74,11 @@ describe("TournamentAuditLogRepository", () => {
|
||||
|
||||
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: actor.id,
|
||||
tournamentTeamId: team.id,
|
||||
subjectUserId: actor.id,
|
||||
});
|
||||
await insertEvent({
|
||||
type: "MEMBER_ADDED",
|
||||
actorUserId: actor.id,
|
||||
tournamentTeamId: team.id,
|
||||
subjectUserId: subject.id,
|
||||
await TournamentTeamFactory.create({
|
||||
tournamentId: tournament.id,
|
||||
userId: actor.id,
|
||||
additionalMemberUserIds: [subject.id],
|
||||
team: { name: "Team Olive", prefersNotToHost: 0, teamId: null },
|
||||
});
|
||||
|
||||
const events = await TournamentAuditLogRepository.findByTournamentId({
|
||||
@@ -110,16 +100,7 @@ describe("TournamentAuditLogRepository", () => {
|
||||
const tournament = await createTournament();
|
||||
const team = await createTeam(tournament.id, "Team Olive");
|
||||
|
||||
await insertEvent({
|
||||
type: "TEAM_UNREGISTERED",
|
||||
actorUserId: actor.id,
|
||||
tournamentTeamId: team.id,
|
||||
});
|
||||
|
||||
await db
|
||||
.deleteFrom("TournamentTeam")
|
||||
.where("TournamentTeam.id", "=", team.id)
|
||||
.execute();
|
||||
await deleteTeam(team.id);
|
||||
|
||||
const events = await TournamentAuditLogRepository.findByTournamentId({
|
||||
tournamentId: tournament.id,
|
||||
@@ -127,7 +108,7 @@ describe("TournamentAuditLogRepository", () => {
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].type).toBe("TEAM_UNREGISTERED");
|
||||
expect(events[0].team?.name).toBe("Team Olive");
|
||||
});
|
||||
|
||||
@@ -135,27 +116,12 @@ describe("TournamentAuditLogRepository", () => {
|
||||
const tournament = await createTournament();
|
||||
const teamA = await createTeam(tournament.id, "Team A");
|
||||
|
||||
await insertEvent({
|
||||
type: "TEAM_UNREGISTERED",
|
||||
actorUserId: actor.id,
|
||||
tournamentTeamId: teamA.id,
|
||||
});
|
||||
|
||||
await db
|
||||
.deleteFrom("TournamentTeam")
|
||||
.where("TournamentTeam.id", "=", teamA.id)
|
||||
.execute();
|
||||
await deleteTeam(teamA.id);
|
||||
|
||||
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: actor.id,
|
||||
tournamentTeamId: teamB.id,
|
||||
});
|
||||
|
||||
const teams = await TournamentAuditLogRepository.findTeamsByTournamentId(
|
||||
tournament.id,
|
||||
);
|
||||
@@ -167,31 +133,26 @@ describe("TournamentAuditLogRepository", () => {
|
||||
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");
|
||||
const unregistered = events.find(
|
||||
(event) => event.type === "TEAM_UNREGISTERED",
|
||||
);
|
||||
const registered = events.filter(
|
||||
(event) => event.type === "TEAM_REGISTERED",
|
||||
);
|
||||
expect(unregistered?.team?.name).toBe("Team A");
|
||||
// newest first
|
||||
expect(registered.map((event) => event.team?.name)).toEqual([
|
||||
"Team B",
|
||||
"Team A",
|
||||
]);
|
||||
});
|
||||
|
||||
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: actor.id,
|
||||
tournamentTeamId: teamA.id,
|
||||
});
|
||||
await insertEvent({
|
||||
type: "TEAM_CHECKED_IN",
|
||||
actorUserId: actor.id,
|
||||
tournamentTeamId: teamA.id,
|
||||
});
|
||||
await insertEvent({
|
||||
type: "TEAM_REGISTERED",
|
||||
actorUserId: actor.id,
|
||||
tournamentTeamId: teamB.id,
|
||||
const teamA = await createTeam(tournament.id, "Team A", {
|
||||
isCheckedIn: true,
|
||||
});
|
||||
await createTeam(tournament.id, "Team B");
|
||||
|
||||
const byType = await TournamentAuditLogRepository.findByTournamentId({
|
||||
tournamentId: tournament.id,
|
||||
@@ -227,7 +188,8 @@ describe("TournamentAuditLogRepository", () => {
|
||||
const tournament = await createTournament();
|
||||
const team = await createTeam(tournament.id, "Team Olive");
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
// two more on top of the team's own TEAM_REGISTERED
|
||||
for (let i = 0; i < 2; i++) {
|
||||
await insertEvent({
|
||||
type: "TEAM_CHECKED_IN",
|
||||
actorUserId: actor.id,
|
||||
@@ -297,12 +259,6 @@ describe("TournamentAuditLogRepository", () => {
|
||||
const tournament = await createTournament();
|
||||
const team = await createTeam(tournament.id, "Old Name");
|
||||
|
||||
await insertEvent({
|
||||
type: "TEAM_REGISTERED",
|
||||
actorUserId: actor.id,
|
||||
tournamentTeamId: team.id,
|
||||
});
|
||||
|
||||
await db.transaction().execute((trx) =>
|
||||
TournamentAuditLogRepository.updateTeamHistoryName(trx, {
|
||||
tournamentTeamId: team.id,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import * as TournamentFactory from "~/db/seed/factories/TournamentFactory";
|
||||
import * as TournamentTeamFactory from "~/db/seed/factories/TournamentTeamFactory";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
import { db } from "~/db/sql";
|
||||
import type { SkillTeamIdentifier } from "~/features/mmr/mmr-utils";
|
||||
@@ -9,25 +11,7 @@ import * as TournamentRepository from "./TournamentRepository.server";
|
||||
let player: { id: number };
|
||||
|
||||
const createTournament = () =>
|
||||
db
|
||||
.insertInto("Tournament")
|
||||
.values({
|
||||
mapPickingStyle: "TO",
|
||||
settings: JSON.stringify({ bracketProgression: [] }),
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
const createTeam = (tournamentId: number) =>
|
||||
db
|
||||
.insertInto("TournamentTeam")
|
||||
.values({
|
||||
tournamentId,
|
||||
name: "team",
|
||||
inviteCode: `inv-${tournamentId}`,
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
TournamentFactory.create({ authorId: player.id });
|
||||
|
||||
const insertPriorSkill = (args: {
|
||||
userId: number;
|
||||
@@ -150,7 +134,10 @@ describe("TournamentRepository.finalize", () => {
|
||||
await insertPriorSkill({ userId: player.id, season: 0, matchesCount: 100 });
|
||||
|
||||
const { id: tournamentId } = await createTournament();
|
||||
const { id: tournamentTeamId } = await createTeam(tournamentId);
|
||||
const { id: tournamentTeamId } = await TournamentTeamFactory.create({
|
||||
tournamentId,
|
||||
userId: player.id,
|
||||
});
|
||||
|
||||
await TournamentRepository.finalize({
|
||||
tournamentId,
|
||||
|
||||
@@ -1,98 +1,7 @@
|
||||
import * as CalendarRepository from "~/features/calendar/CalendarRepository.server";
|
||||
import { databaseTimestampNow } from "~/utils/dates";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { withUserId } from "~/utils/Test";
|
||||
import * as BracketRepository from "../tournament-bracket/BracketRepository.server";
|
||||
import * as Engine from "../tournament-bracket/core/engine";
|
||||
import { tournamentFromDB } from "../tournament-bracket/core/Tournament.server";
|
||||
import * as TournamentTeamRepository from "./TournamentTeamRepository.server";
|
||||
|
||||
/**
|
||||
* Creates a mock tournament with one single elimination bracket.
|
||||
*
|
||||
* @returns The created event and tournament ids.
|
||||
*/
|
||||
export async function dbInsertTournament({
|
||||
organizationId = null,
|
||||
startTime = null,
|
||||
}: {
|
||||
/** Organization hosting the tournament. Defaults to no organization. */
|
||||
organizationId?: number | null;
|
||||
/** Event start time as a database timestamp (seconds). Defaults to now. */
|
||||
startTime?: number | null;
|
||||
} = {}) {
|
||||
return CalendarRepository.insert({
|
||||
isFullTournament: true,
|
||||
authorId: 1,
|
||||
badges: [],
|
||||
bracketUrl: "https://example.com/bracket",
|
||||
description: null,
|
||||
discordInviteCode: "test-discord",
|
||||
name: "Test Tournament",
|
||||
organizationId,
|
||||
rules: null,
|
||||
startTimes: [startTime ?? databaseTimestampNow()],
|
||||
tags: null,
|
||||
bracketProgression: [
|
||||
{
|
||||
name: "Bracket",
|
||||
type: "single_elimination",
|
||||
requiresCheckIn: false,
|
||||
settings: {
|
||||
thirdPlaceMatch: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
mapPickingStyle: "TO",
|
||||
mapPoolMaps: ([1, 2, 3, 4, 5] as const).map((id) => ({
|
||||
mode: "SZ",
|
||||
stageId: id,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a tournament team into the database with the specified number of members. Also checks in the team to the tournament.
|
||||
*/
|
||||
export async function dbInsertTournamentTeam({
|
||||
membersCount,
|
||||
ownerId,
|
||||
tournamentId = 1,
|
||||
}: {
|
||||
/** Total number of members in the team, including the owner. */
|
||||
membersCount: number;
|
||||
/** Id of the user who owns the team. The other members are relative to this ID so e.g. if captain has ID of 5 then other members have 6,7,8 etc. */
|
||||
ownerId: number;
|
||||
/** Id of the tournament to associate the team with. Defaults to 1. */
|
||||
tournamentId?: number;
|
||||
}) {
|
||||
const tournamentTeam = await withUserId(ownerId, () =>
|
||||
TournamentTeamRepository.insert({
|
||||
team: {
|
||||
name: `Test Team ${ownerId}`,
|
||||
prefersNotToHost: 0,
|
||||
teamId: null,
|
||||
},
|
||||
userId: ownerId,
|
||||
tournamentId,
|
||||
}),
|
||||
);
|
||||
|
||||
for (let i = 1; i < membersCount; i++) {
|
||||
const memberId = ownerId + i;
|
||||
|
||||
await withUserId(memberId, () =>
|
||||
TournamentTeamRepository.join({
|
||||
userId: memberId,
|
||||
newTeamId: tournamentTeam.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await withUserId(ownerId, () =>
|
||||
TournamentTeamRepository.checkIn(tournamentTeam.id),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a tournament with the given seeding and tournament ID.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { add } from "date-fns";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import * as TournamentFactory from "~/db/seed/factories/TournamentFactory";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
import * as CalendarRepository from "~/features/calendar/CalendarRepository.server";
|
||||
import { clearAllTournamentDataCache } from "~/features/tournament-bracket/core/Tournament.server";
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import { dbReset } from "~/utils/Test";
|
||||
@@ -18,47 +18,6 @@ vi.mock("~/features/notifications/core/notify.server", () => ({
|
||||
notify: mockNotify,
|
||||
}));
|
||||
|
||||
async function createTestTournament({
|
||||
name,
|
||||
startTime,
|
||||
authorId = author.id,
|
||||
discordInviteCode = "test-discord",
|
||||
}: {
|
||||
name: string;
|
||||
startTime: Date;
|
||||
authorId?: number;
|
||||
discordInviteCode?: string;
|
||||
}) {
|
||||
return CalendarRepository.insert({
|
||||
isFullTournament: true,
|
||||
authorId,
|
||||
badges: [],
|
||||
bracketUrl: "https://example.com/bracket",
|
||||
description: null,
|
||||
discordInviteCode,
|
||||
name,
|
||||
organizationId: null,
|
||||
rules: null,
|
||||
startTimes: [dateToDatabaseTimestamp(startTime)],
|
||||
tags: null,
|
||||
bracketProgression: [
|
||||
{
|
||||
name: "Bracket",
|
||||
type: "single_elimination",
|
||||
requiresCheckIn: false,
|
||||
settings: {
|
||||
thirdPlaceMatch: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
mapPickingStyle: "TO",
|
||||
mapPoolMaps: ([1, 2, 3, 4, 5] as const).map((id) => ({
|
||||
mode: "SZ",
|
||||
stageId: id,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
describe("NotifyCheckInStartRoutine", () => {
|
||||
beforeEach(async () => {
|
||||
vi.useFakeTimers();
|
||||
@@ -77,9 +36,10 @@ describe("NotifyCheckInStartRoutine", () => {
|
||||
const now = new Date();
|
||||
const oneHourFromNow = add(now, { hours: 1 });
|
||||
|
||||
await createTestTournament({
|
||||
await TournamentFactory.create({
|
||||
name: "Tournament 1 Hour Away",
|
||||
startTime: oneHourFromNow,
|
||||
authorId: author.id,
|
||||
startTimes: [dateToDatabaseTimestamp(oneHourFromNow)],
|
||||
});
|
||||
|
||||
await NotifyCheckInStartRoutine.run();
|
||||
@@ -100,9 +60,10 @@ describe("NotifyCheckInStartRoutine", () => {
|
||||
test("does NOT send notification for tournament starting exactly now", async () => {
|
||||
const now = new Date();
|
||||
|
||||
await createTestTournament({
|
||||
await TournamentFactory.create({
|
||||
name: "Tournament Starting Now",
|
||||
startTime: now,
|
||||
authorId: author.id,
|
||||
startTimes: [dateToDatabaseTimestamp(now)],
|
||||
});
|
||||
|
||||
await NotifyCheckInStartRoutine.run();
|
||||
@@ -114,9 +75,10 @@ describe("NotifyCheckInStartRoutine", () => {
|
||||
const now = new Date();
|
||||
const thirtyMinutesFromNow = add(now, { minutes: 30 });
|
||||
|
||||
await createTestTournament({
|
||||
await TournamentFactory.create({
|
||||
name: "Tournament 30 Minutes Away",
|
||||
startTime: thirtyMinutesFromNow,
|
||||
authorId: author.id,
|
||||
startTimes: [dateToDatabaseTimestamp(thirtyMinutesFromNow)],
|
||||
});
|
||||
|
||||
await NotifyCheckInStartRoutine.run();
|
||||
@@ -138,9 +100,10 @@ describe("NotifyCheckInStartRoutine", () => {
|
||||
const now = new Date();
|
||||
const oneAndHalfHoursFromNow = add(now, { hours: 1, minutes: 30 });
|
||||
|
||||
await createTestTournament({
|
||||
await TournamentFactory.create({
|
||||
name: "Tournament 1.5 Hours Away",
|
||||
startTime: oneAndHalfHoursFromNow,
|
||||
authorId: author.id,
|
||||
startTimes: [dateToDatabaseTimestamp(oneAndHalfHoursFromNow)],
|
||||
});
|
||||
|
||||
await NotifyCheckInStartRoutine.run();
|
||||
@@ -153,17 +116,16 @@ describe("NotifyCheckInStartRoutine", () => {
|
||||
const thirtyMinutesFromNow = add(now, { minutes: 30 });
|
||||
const fortyFiveMinutesFromNow = add(now, { minutes: 45 });
|
||||
|
||||
await createTestTournament({
|
||||
await TournamentFactory.create({
|
||||
name: "Tournament A",
|
||||
startTime: thirtyMinutesFromNow,
|
||||
discordInviteCode: "test-discord-1",
|
||||
authorId: author.id,
|
||||
startTimes: [dateToDatabaseTimestamp(thirtyMinutesFromNow)],
|
||||
});
|
||||
|
||||
await createTestTournament({
|
||||
await TournamentFactory.create({
|
||||
name: "Tournament B",
|
||||
startTime: fortyFiveMinutesFromNow,
|
||||
authorId: otherAuthor.id,
|
||||
discordInviteCode: "test-discord-2",
|
||||
startTimes: [dateToDatabaseTimestamp(fortyFiveMinutesFromNow)],
|
||||
});
|
||||
|
||||
await NotifyCheckInStartRoutine.run();
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
import { expect } from "vitest";
|
||||
import type { z } from "zod";
|
||||
import { REGULAR_USER_TEST_ID } from "~/db/seed/constants";
|
||||
import { actAs } from "~/db/seed/core/actAs";
|
||||
import { resetFactories } from "~/db/seed/core/defineFactory";
|
||||
import { db } from "~/db/sql";
|
||||
import { ADMIN_ID } from "~/features/admin/admin-constants";
|
||||
@@ -40,10 +41,7 @@ export function withUser<T>(user: AuthenticatedUser, fn: () => T): T {
|
||||
* matters (repositories read the actor solely via `actorId()` / `actorIdOrNull()`).
|
||||
*/
|
||||
export function withUserId<T>(id: number, fn: () => T): T {
|
||||
return userAsyncLocalStorage.run(
|
||||
{ user: { id } as unknown as AuthenticatedUser },
|
||||
fn,
|
||||
);
|
||||
return actAs(id, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user