Check tournament ids valid before persisting seeds

This commit is contained in:
Kalle
2026-08-26 17:09:30 +03:00
parent 2116bcce95
commit f238db8cd2
2 changed files with 105 additions and 4 deletions

View File

@@ -0,0 +1,84 @@
import { 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 { wrappedAction } from "~/utils/Test";
import type { adminSeedsActionSchema } from "../tournament-admin-schemas";
import { action } from "./to.$id.admin.seeds.server";
const seedsAction = wrappedAction<typeof adminSeedsActionSchema>({ action });
const teamRow = (tournamentTeamId: number) =>
db
.selectFrom("TournamentTeam")
.select(["id", "tournamentId", "seed", "startingBracketIdx"])
.where("id", "=", tournamentTeamId)
.executeTakeFirstOrThrow();
describe("tournament seeds admin action", () => {
test("does not touch the seeds of a team of another tournament", async () => {
await UserFactory.createAdmin();
const attacker = await UserFactory.createRegular();
const victimTo = await UserFactory.create();
const [player] = await UserFactory.createMany(1);
// the attacker organizes a tournament of their own
const ownTournament = await TournamentFactory.create({
authorId: attacker.id,
});
// somebody else's tournament, with a team they seeded themselves
const otherTournament = await TournamentFactory.create({
authorId: victimTo.id,
});
const otherTeam = await TournamentTeamFactory.create({
tournamentId: otherTournament.id,
memberUserIds: [player.id],
});
await seedsAction(
{ _action: "UPDATE_SEEDS", seeds: JSON.stringify([otherTeam.id]) as any },
{ user: "regular", params: { id: String(ownTournament.id) } },
);
const after = await teamRow(otherTeam.id);
expect(after.tournamentId).toBe(otherTournament.id);
expect(after.seed, "another tournament's team got seeded").toBeNull();
});
test("does not touch the starting bracket of a team of another tournament", async () => {
await UserFactory.createAdmin();
const attacker = await UserFactory.createRegular();
const victimTo = await UserFactory.create();
const [player] = await UserFactory.createMany(1);
const ownTournament = await TournamentFactory.create({
authorId: attacker.id,
});
const otherTournament = await TournamentFactory.create({
authorId: victimTo.id,
});
const otherTeam = await TournamentTeamFactory.create({
tournamentId: otherTournament.id,
memberUserIds: [player.id],
});
await seedsAction(
{
_action: "UPDATE_STARTING_BRACKETS",
startingBrackets: JSON.stringify([
{ tournamentTeamId: otherTeam.id, startingBracketIdx: 0 },
]) as any,
},
{ user: "regular", params: { id: String(ownTournament.id) } },
);
const after = await teamRow(otherTeam.id);
expect(
after.startingBracketIdx,
"another tournament's team got moved to a starting bracket",
).toBeNull();
});
});

View File

@@ -1,6 +1,7 @@
import type { ActionFunction } from "react-router";
import * as TournamentRepository from "~/features/tournament/TournamentRepository.server";
import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server";
import type { Tournament } from "~/features/tournament-bracket/core/Tournament";
import {
clearTournamentDataCache,
requireTournamentOrganizer,
@@ -30,6 +31,7 @@ export const action: ActionFunction = async ({ request, params }) => {
case "UPDATE_SEEDS": {
requireTournamentOrganizer(tournament, user);
errorToastIfFalsy(!tournament.hasStarted, "Tournament has started");
validateTeamsOfTournament(tournament, data.seeds);
await TournamentRepository.updateTeamSeeds({
tournamentId,
@@ -54,6 +56,10 @@ export const action: ActionFunction = async ({ request, params }) => {
),
"Invalid starting bracket idx",
);
validateTeamsOfTournament(
tournament,
data.startingBrackets.map((t) => t.tournamentTeamId),
);
await TournamentTeamRepository.updateStartingBrackets(
data.startingBrackets,
@@ -73,10 +79,9 @@ export const action: ActionFunction = async ({ request, params }) => {
"No starting bracket has A/B divisions enabled",
);
const validTeamIds = new Set(tournament.ctx.teams.map((t) => t.id));
errorToastIfFalsy(
data.abDivisions.every((t) => validTeamIds.has(t.tournamentTeamId)),
"Invalid tournament team id",
validateTeamsOfTournament(
tournament,
data.abDivisions.map((t) => t.tournamentTeamId),
);
await TournamentTeamRepository.updateAbDivisions(data.abDivisions);
@@ -93,3 +98,15 @@ export const action: ActionFunction = async ({ request, params }) => {
return successToast(message);
};
function validateTeamsOfTournament(
tournament: Tournament,
tournamentTeamIds: number[],
) {
const validTeamIds = new Set(tournament.ctx.teams.map((t) => t.id));
errorToastIfFalsy(
tournamentTeamIds.every((id) => validTeamIds.has(id)),
"Invalid tournament team id",
);
}