Add serverside checks to team member being added

This commit is contained in:
Kalle
2026-08-29 16:00:55 +03:00
parent 7ecf963a3b
commit 9e4b77dad8
2 changed files with 127 additions and 1 deletions

View File

@@ -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<ReturnType<typeof registerSchema>>({
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<number>().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);
});
});

View File

@@ -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({