Skip to content

Commit

Permalink
feat: only 1 coach is allowed per 1-player-tournament team
Browse files Browse the repository at this point in the history
* The check for the number of coach is now done on request accept, and not on request creation
* fixed a bug in PATCH /admin/tournaments/:tournamentId
  • Loading branch information
Teddy Roncin committed Oct 16, 2023
1 parent 2d9474b commit 1bd9966
Show file tree
Hide file tree
Showing 9 changed files with 108 additions and 76 deletions.
15 changes: 13 additions & 2 deletions src/controllers/admin/tournaments/updateTournament.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Joi from 'joi';
import { Request, Response, NextFunction } from 'express';
import { hasPermission } from '../../../middlewares/authentication';
import { validateBody } from '../../../middlewares/validation';
import { notFound, success } from '../../../utils/responses';
import { conflict, notFound, success } from '../../../utils/responses';
import { Error, Permission } from '../../../types';
import { fetchTournaments, updateTournament } from '../../../operations/tournament';
import { addCasterToTournament, removeAllCastersFromTournament } from '../../../operations/caster';
Expand Down Expand Up @@ -30,16 +30,27 @@ export default [
// Controller
async (request: Request, response: Response, next: NextFunction) => {
try {
if (!(await fetchTournaments()).some((tournament) => tournament.id === request.params.tournamentId)) {
const allTournaments = await fetchTournaments();
if (!allTournaments.some((tournament) => tournament.id === request.params.tournamentId)) {
return notFound(response, Error.NotFound);
}

if (
request.body.name &&
allTournaments.some(
(tournament) => tournament.name === request.body.name && tournament.id !== request.params.tournamentId,
)
) {
return conflict(response, Error.TournamentNameAlreadyExists);
}

if (request.body.casters && request.body.casters.length > 0) {
await removeAllCastersFromTournament(request.params.tournamentId as string);

for (const casterName of request.body.casters) {
await addCasterToTournament(request.params.tournamentId as string, casterName);
}
request.body.casters = undefined;
}

const result = await updateTournament(request.params.tournamentId as string, request.body);
Expand Down
4 changes: 4 additions & 0 deletions src/controllers/teams/acceptRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export default [
return forbidden(response, Error.TeamFull);
}

if (team.coaches.length >= Math.min(tournament.playersPerTeam, 2) && askingUser.type === UserType.coach) {
return forbidden(response, Error.TeamMaxCoachReached);
}

await joinTeam(team.id, askingUser, askingUser.type);

const updatedTeam = await fetchTeam(team.id);
Expand Down
3 changes: 0 additions & 3 deletions src/controllers/teams/createTeamRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,6 @@ export default [

return success(response, filterUser(updatedUser));
} catch (error) {
// This may happen when max coach amount is reached already
if (error.code === 'API_COACH_MAX_TEAM') return forbidden(response, ResponseError.TeamMaxCoachReached);

return next(error);
}
},
Expand Down
11 changes: 1 addition & 10 deletions src/operations/team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@ import {
PrimitiveTeamWithPartialTournament,
} from '../types';
import nanoid from '../utils/nanoid';
import { countCoaches, formatUser, userInclusions } from './user';
import { formatUser, userInclusions } from './user';
import { setupDiscordTeam } from '../utils/discord';
import { fetchTournament } from './tournament';

const teamMaxCoachCount = 2;

const teamInclusions = {
users: {
include: userInclusions,
Expand Down Expand Up @@ -205,13 +203,6 @@ export const updateTeam = async (teamId: string, name: string): Promise<Team> =>
};

export const askJoinTeam = async (teamId: string, userId: string, userType: UserType) => {
// We check the amount of coaches at that point
const teamCoachCount = await countCoaches(teamId);
if (userType === UserType.coach && teamCoachCount >= teamMaxCoachCount)
throw Object.assign(new Error('Query cannot be executed: max count of coach reached already'), {
code: 'API_COACH_MAX_TEAM',
});

// Then we create the join request when it is alright
const updatedUser = await database.user.update({
data: {
Expand Down
2 changes: 1 addition & 1 deletion src/operations/tournament.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const updateTournament = (
): PrismaPromise<PrimitiveTournament> =>
database.tournament.update({
where: { id },
data: { ...data, casters: undefined },
data: { ...data },
});

export const updateTournamentsPosition = (tournaments: { id: string; position: number }[]) =>
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ export const enum Error {
TeamAlreadyExists = "Le nom de l'équipe existe déjà",
PlaceAlreadyAttributed = 'Cette place est déjà attribuée',
DiscordAccountAlreadyUsed = 'Ce compte discord est déjà lié à un compte',
TournamentNameAlreadyExists = 'Un tournoi a déjà ce nom',

// 410
// indicates that access to the target resource is no longer available at the server.
Expand Down
54 changes: 31 additions & 23 deletions tests/admin/tournaments/updateTournament.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@ import { sandbox } from '../../setup';
import * as tournamentOperations from '../../../src/operations/tournament';
import database from '../../../src/services/database';
import { Error, Permission, Tournament, User, UserType } from '../../../src/types';
import { createFakeUser } from '../../utils';
import { createFakeTournament, createFakeUser } from '../../utils';
import { generateToken } from '../../../src/utils/users';
import { fetchTournaments } from '../../../src/operations/tournament';

describe('PATCH /admin/tournaments/{tournamentId}', () => {
let nonAdminUser: User;
let admin: User;
let adminToken: string;
let tournaments: Tournament[];
let tournament: Tournament;

const validBody = {
name: 'test',
name: 'anothertestname',
maxPlayers: 100,
playersPerTeam: 5,
cashprize: 100,
Expand All @@ -32,26 +31,27 @@ describe('PATCH /admin/tournaments/{tournamentId}', () => {

after(async () => {
await database.user.deleteMany();
await database.tournament.delete({where: {id: tournament.id}});

Check warning on line 34 in tests/admin/tournaments/updateTournament.test.ts

View workflow job for this annotation

GitHub Actions / lint (18)

Replace `where:·{id:·tournament.id}` with `·where:·{·id:·tournament.id·}·`
});

before(async () => {
admin = await createFakeUser({ type: UserType.orga, permissions: [Permission.admin] });
nonAdminUser = await createFakeUser();
adminToken = generateToken(admin);

tournaments = await fetchTournaments();
tournament = await createFakeTournament({ id: 'test', name: 'test', playersPerTeam: 1, maxTeams: 1 });
});

it('should error as the user is not authenticated', () =>
request(app)
.patch(`/admin/tournaments/${tournaments[0].id}`)
.patch(`/admin/tournaments/${tournament.id}`)
.send(validBody)
.expect(401, { error: Error.Unauthenticated }));

it('should error as the user is not an administrator', () => {
const userToken = generateToken(nonAdminUser);
return request(app)
.patch(`/admin/tournaments/${tournaments[0].id}`)
.patch(`/admin/tournaments/${tournament.id}`)
.send(validBody)
.set('Authorization', `Bearer ${userToken}`)
.expect(403, { error: Error.NoPermission });
Expand All @@ -61,7 +61,7 @@ describe('PATCH /admin/tournaments/{tournamentId}', () => {
sandbox.stub(tournamentOperations, 'updateTournament').throws('Unexpected error');

await request(app)
.patch(`/admin/tournaments/${tournaments[0].id}`)
.patch(`/admin/tournaments/${tournament.id}`)
.send(validBody)
.set('Authorization', `Bearer ${adminToken}`)
.expect(500, { error: Error.InternalServerError });
Expand All @@ -75,27 +75,35 @@ describe('PATCH /admin/tournaments/{tournamentId}', () => {
.expect(404, { error: Error.NotFound });
});

it('should fail as a tournament already has this name', async () => {
await request(app)
.patch(`/admin/tournaments/${tournament.id}`)
.send({ name: 'Pokémon' })
.set('Authorization', `Bearer ${adminToken}`)
.expect(409, { error: Error.TournamentNameAlreadyExists });
});

it('should successfully update the tournament', async () => {
await request(app)
.patch(`/admin/tournaments/${tournaments[0].id}`)
.patch(`/admin/tournaments/${tournament.id}`)
.send(validBody)
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);

const tournament = await tournamentOperations.fetchTournament(tournaments[0].id);
const tournamentDatabase = await tournamentOperations.fetchTournament(tournament.id);

expect(tournament.name).to.equal(validBody.name);
expect(tournament.maxPlayers).to.equal(validBody.maxPlayers);
expect(tournament.playersPerTeam).to.equal(validBody.playersPerTeam);
expect(tournament.cashprize).to.equal(validBody.cashprize);
expect(tournament.cashprizeDetails).to.equal(validBody.cashprizeDetails);
expect(tournament.displayCashprize).to.equal(validBody.displayCashprize);
expect(tournament.format).to.equal(validBody.format);
expect(tournament.infos).to.equal(validBody.infos);
expect(tournament.casters).to.be.an('array');
expect(tournament.casters[0].name).to.be.equal(validBody.casters[0]);
expect(tournament.displayCasters).to.equal(validBody.displayCasters);
expect(tournament.display).to.equal(validBody.display);
expect(tournament.position).to.equal(150);
expect(tournamentDatabase.name).to.equal(validBody.name);
expect(tournamentDatabase.maxPlayers).to.equal(validBody.maxPlayers);
expect(tournamentDatabase.playersPerTeam).to.equal(validBody.playersPerTeam);
expect(tournamentDatabase.cashprize).to.equal(validBody.cashprize);
expect(tournamentDatabase.cashprizeDetails).to.equal(validBody.cashprizeDetails);
expect(tournamentDatabase.displayCashprize).to.equal(validBody.displayCashprize);
expect(tournamentDatabase.format).to.equal(validBody.format);
expect(tournamentDatabase.infos).to.equal(validBody.infos);
expect(tournamentDatabase.casters).to.be.an('array');
expect(tournamentDatabase.casters[0].name).to.be.equal(validBody.casters[0]);
expect(tournamentDatabase.displayCasters).to.equal(validBody.displayCasters);
expect(tournamentDatabase.display).to.equal(validBody.display);
expect(tournamentDatabase.position).to.equal(150);
});
});
61 changes: 57 additions & 4 deletions tests/teams/acceptRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Error, Team, User, UserType } from '../../src/types';
import { createFakeUser, createFakeTeam } from '../utils';
import { generateToken } from '../../src/utils/users';
import { getCaptain } from '../../src/utils/teams';
import { fetchUser } from '../../src/operations/user';

// eslint-disable-next-line func-names
describe('POST /teams/current/join-requests/:userId', function () {
Expand All @@ -22,6 +23,12 @@ describe('POST /teams/current/join-requests/:userId', function () {
let team: Team;
let captain: User;
let token: string;
let fullTeam: Team;
let fullCaptain: User;
let fullToken: string;
let onePlayerTeam: Team;
let onePlayerTeamCaptain: User;
let onePlayerTeamToken: string;

before(async () => {
const tournament = await tournamentOperations.fetchTournament('lol');
Expand All @@ -30,10 +37,13 @@ describe('POST /teams/current/join-requests/:userId', function () {
user2 = await createFakeUser({ paid: true, type: UserType.player });
await teamOperations.askJoinTeam(team.id, user.id, UserType.player);
await teamOperations.askJoinTeam(team.id, user2.id, UserType.player);
fullTeam = await createFakeTeam({ members: tournament.playersPerTeam, paid: true, locked: true });
fullCaptain = getCaptain(fullTeam);
fullToken = generateToken(fullCaptain);
// Fill the tournament
// Store the promises
const promises = [];
for (let index = 0; index < tournament.placesLeft; index++) {
for (let index = 1; index < tournament.placesLeft; index++) {
promises.push(createFakeTeam({ members: tournament.playersPerTeam, paid: true, locked: true }));
}
await Promise.all(promises);
Expand All @@ -42,6 +52,10 @@ describe('POST /teams/current/join-requests/:userId', function () {

captain = getCaptain(team);
token = generateToken(captain);

onePlayerTeam = await createFakeTeam({ tournament: 'pokemon' });
onePlayerTeamCaptain = getCaptain(onePlayerTeam);
onePlayerTeamToken = generateToken(onePlayerTeamCaptain);
});

after(async () => {
Expand Down Expand Up @@ -115,17 +129,56 @@ describe('POST /teams/current/join-requests/:userId', function () {
});

it('should succeed to join a full team as a coach', async () => {
const fullTeam = await createFakeTeam({ members: 5 });
const otherUser = await createFakeUser();

const fullCaptain = getCaptain(fullTeam);
const fullToken = generateToken(fullCaptain);
await teamOperations.askJoinTeam(fullTeam.id, otherUser.id, UserType.coach);

await request(app)
.post(`/teams/current/join-requests/${otherUser.id}`)
.set('Authorization', `Bearer ${fullToken}`)
.expect(200);

const databaseUser = await fetchUser(otherUser.id);
expect(databaseUser.teamId).to.be.equal(fullTeam.id);
});

it('should fail to join the team as a coach because there are already 2 coaches', async () => {
// There is only one coach for the moment
const coach = await createFakeUser();
await teamOperations.joinTeam(fullTeam.id, coach, UserType.coach);
const willNotJoinCoach = await createFakeUser();
await teamOperations.askJoinTeam(fullTeam.id, willNotJoinCoach.id, UserType.coach);
await request(app)
.post(`/teams/current/join-requests/${willNotJoinCoach.id}`)
.set('Authorization', `Bearer ${fullToken}`)
.expect(403, { error: Error.TeamMaxCoachReached });

const databaseCoach = await fetchUser(willNotJoinCoach.id);
expect(databaseCoach.teamId).to.be.null;
});

it('should successfully join the 1-player team as a coach', async () => {
const coach = await createFakeUser();
await teamOperations.askJoinTeam(onePlayerTeam.id, coach.id, UserType.coach);
await request(app)
.post(`/teams/current/join-requests/${coach.id}`)
.set('Authorization', `Bearer ${onePlayerTeamToken}`)
.expect(200);

const databaseCoach = await fetchUser(coach.id);
expect(databaseCoach.teamId).to.be.equal(onePlayerTeam.id);
});

it('should fail to join the 1-player team as a coach because there is already a coach', async () => {
const coach = await createFakeUser();
await teamOperations.askJoinTeam(onePlayerTeam.id, coach.id, UserType.coach);
await request(app)
.post(`/teams/current/join-requests/${coach.id}`)
.set('Authorization', `Bearer ${onePlayerTeamToken}`)
.expect(403, { error: Error.TeamMaxCoachReached });

const databaseCoach = await fetchUser(coach.id);
expect(databaseCoach.teamId).to.be.null;
});

it('should successfully join the team and not lock the team as it is not full', async () => {
Expand Down
33 changes: 0 additions & 33 deletions tests/teams/createTeamRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,39 +54,6 @@ describe('POST /teams/:teamId/join-requests', () => {
.expect(403, { error: Error.AlreadyInTeam });
});

it('should fail because coach limit is already reached (coach team members)', async () => {
const otherTeam = await createFakeTeam({ members: 2 });
const [user1, user2] = otherTeam.players;
await updateAdminUser(user1.id, { type: UserType.coach });
await updateAdminUser(user2.id, { type: UserType.coach });

const otherCoach = await createFakeUser();
const otherToken = generateToken(otherCoach);

return request(app)
.post(`/teams/${otherTeam.id}/join-requests`)
.send({ userType: UserType.coach })
.set('Authorization', `Bearer ${otherToken}`)
.expect(403, { error: Error.TeamMaxCoachReached });
});

it('should fail because coach limit is already reached (coach team member requests)', async () => {
const otherTeam = await createFakeTeam();
const [user1] = otherTeam.players;
const user2 = await createFakeUser();
await updateAdminUser(user1.id, { type: UserType.coach });
await teamOperations.askJoinTeam(otherTeam.id, user2.id, UserType.coach);

const otherCoach = await createFakeUser();
const otherToken = generateToken(otherCoach);

return request(app)
.post(`/teams/${otherTeam.id}/join-requests`)
.send({ userType: UserType.coach })
.set('Authorization', `Bearer ${otherToken}`)
.expect(403, { error: Error.TeamMaxCoachReached });
});

it('should fail because the user is a spectator', async () => {
const spectator = await createFakeUser({ type: UserType.spectator });
const spectatorToken = generateToken(spectator);
Expand Down

0 comments on commit 1bd9966

Please sign in to comment.