diff --git a/app/features/tournament/actions/to.$id.register.server.test.ts b/app/features/tournament/actions/to.$id.register.server.test.ts new file mode 100644 index 000000000..5e0ebca3b --- /dev/null +++ b/app/features/tournament/actions/to.$id.register.server.test.ts @@ -0,0 +1,117 @@ +import { addDays } from "date-fns"; +import { describe, expect, test, vi } from "vitest"; +import * as FriendshipFactory from "~/db/seed/factories/FriendshipFactory"; +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 { dateToDatabaseTimestamp } from "~/utils/dates"; +import { assertResponseErrored, wrappedAction } from "~/utils/Test"; +import type { registerSchema } from "../tournament-schemas.server"; +import { action as registerAction } from "./to.$id.register.server"; + +// adding a player notifies them; not under test here +vi.mock("~/features/notifications/core/notify.server", () => ({ + notify: () => Promise.resolve(), +})); + +/** `maxMembersPerTeam` of a regular 4v4 tournament that sets no limit of its own. */ +const MAX_ROSTER_SIZE = 6; + +const register = wrappedAction>({ + action: registerAction, + isJsonSubmission: true, +}); + +const memberCountOfOnlyTeam = async (tournamentId: number) => { + const { count } = await db + .selectFrom("TournamentTeamMember") + .innerJoin( + "TournamentTeam", + "TournamentTeam.id", + "TournamentTeamMember.tournamentTeamId", + ) + .select((eb) => eb.fn.countAll().as("count")) + .where("TournamentTeam.tournamentId", "=", tournamentId) + .executeTakeFirstOrThrow(); + + return count; +}; + +/** + * A tournament open for registration with one team of `rosterSize` captained by the + * user the action submits as, and a friend of theirs available to be added to it. + */ +const scenario = async ({ + rosterSize, + requireInGameNames = false, +}: { + rosterSize: number; + requireInGameNames?: boolean; +}) => { + // the captain, who submits the request + const captain = await UserFactory.createRegular({ + profile: requireInGameNames ? { inGameName: "Captain#1234" } : null, + }); + // the player they try to add, without an in-game name of their own + const friend = await UserFactory.create({ profile: null }); + const teammates = await UserFactory.createMany(rosterSize - 1); + + await FriendshipFactory.create({ + userOneId: captain.id, + userTwoId: friend.id, + }); + + const tournament = await TournamentFactory.create({ + authorId: captain.id, + startTimes: [dateToDatabaseTimestamp(addDays(new Date(), 7))], + requireInGameNames, + }); + + await TournamentTeamFactory.create({ + tournamentId: tournament.id, + memberUserIds: [captain.id, ...teammates.map((user) => user.id)], + }); + + return { + tournamentId: tournament.id, + addFriend: () => + register( + { _action: "ADD_PLAYER", userId: friend.id }, + { user: "regular", params: { id: String(tournament.id) } }, + ), + }; +}; + +describe("Tournament registration ADD_PLAYER", () => { + test("adds a player to a team with room left", async () => { + const { tournamentId, addFriend } = await scenario({ + rosterSize: MAX_ROSTER_SIZE - 1, + }); + + await addFriend(); + + expect(await memberCountOfOnlyTeam(tournamentId)).toBe(MAX_ROSTER_SIZE); + }); + + test("does not add a player to a team already at max capacity", async () => { + const { tournamentId, addFriend } = await scenario({ + rosterSize: MAX_ROSTER_SIZE, + }); + + assertResponseErrored(await addFriend(), "Team is already at max capacity"); + + expect(await memberCountOfOnlyTeam(tournamentId)).toBe(MAX_ROSTER_SIZE); + }); + + test("does not add a player without an in-game name when the tournament requires one", async () => { + const { tournamentId, addFriend } = await scenario({ + rosterSize: 1, + requireInGameNames: true, + }); + + assertResponseErrored(await addFriend(), "no in-game name"); + + expect(await memberCountOfOnlyTeam(tournamentId)).toBe(1); + }); +}); diff --git a/app/features/tournament/actions/to.$id.register.server.ts b/app/features/tournament/actions/to.$id.register.server.ts index 9cbc308ec..a20c9037b 100644 --- a/app/features/tournament/actions/to.$id.register.server.ts +++ b/app/features/tournament/actions/to.$id.register.server.ts @@ -269,16 +269,25 @@ export const action: ActionFunction = async ({ request, params }) => { "User is already in a team", ); errorToastIfFalsy(ownTeam, "You are not registered to this tournament"); + errorToastIfFalsy( + ownTeam.memberUserIds.length < tournament.maxMembersPerTeam, + "Team is already at max capacity", + ); errorToastIfFalsy( (await SQGroupRepository.findFriendsAndTeammates(user.id)).friends.some( (friendPlayer) => friendPlayer.id === data.userId, ), "Not a friend", ); + const userToAdd = await UserRepository.findLeanById(data.userId); errorToastIfFalsy( - (await UserRepository.findLeanById(data.userId))?.friendCode, + userToAdd?.friendCode, "User you are trying to add has no friend code set", ); + errorToastIfFalsy( + !tournament.ctx.settings.requireInGameNames || userToAdd.inGameName, + "User you are trying to add has no in-game name set", + ); errorToastIfFalsy(tournament.registrationOpen, "Registration is closed"); await requireNotBannedByOrganization({