diff --git a/src/events/events.ts b/src/events/events.ts index b7037366f..841920dd3 100644 --- a/src/events/events.ts +++ b/src/events/events.ts @@ -127,6 +127,10 @@ export class Events { newValue: unknown; }>(); + readonly errorNoFreeGameServerAvailable = new Subject<{ + gameId: GameId; + }>(); + constructor() { for (const eventName in this) { const prop = this[eventName]; diff --git a/src/games/services/game-server-assigner.service.spec.ts b/src/games/services/game-server-assigner.service.spec.ts index ec70ec68b..2c5b2dc2e 100644 --- a/src/games/services/game-server-assigner.service.spec.ts +++ b/src/games/services/game-server-assigner.service.spec.ts @@ -15,6 +15,7 @@ import { PlayersService } from '@/players/services/players.service'; // eslint-disable-next-line jest/no-mocks-import import { PlayersService as MockedPlayersService } from '@/players/services/__mocks__/players.service'; import { Player, playerSchema } from '@/players/models/player'; +import { NoFreeGameServerAvailableError } from '@/game-servers/errors/no-free-game-server-available.error'; jest.mock('./games.service'); jest.mock('@/game-servers/services/game-servers.service'); @@ -174,6 +175,30 @@ describe('GameServerAssignerService', () => { ).rejects.toThrow(CannotAssignGameServerError); }); }); + + describe('when no free gameservers are available', () => { + let emitted: boolean; + + beforeEach(() => { + gameServersService.assignGameServer.mockRejectedValue( + new NoFreeGameServerAvailableError(), + ); + events.errorNoFreeGameServerAvailable.subscribe(() => { + emitted = true; + }); + emitted = false; + }); + + it('should emit event', async () => { + try { + await service.assignGameServer(game._id); + } catch (error) { + // empty + } + + expect(emitted).toBe(true); + }); + }); }); describe('#handleOrphanedGames()', () => { diff --git a/src/games/services/game-server-assigner.service.ts b/src/games/services/game-server-assigner.service.ts index a45074645..db5553a0d 100644 --- a/src/games/services/game-server-assigner.service.ts +++ b/src/games/services/game-server-assigner.service.ts @@ -12,6 +12,7 @@ import { Game } from '../models/game'; import { GamesService } from './games.service'; import { PlayerId } from '@/players/types/player-id'; import { PlayersService } from '@/players/services/players.service'; +import { NoFreeGameServerAvailableError } from '@/game-servers/errors/no-free-game-server-available.error'; @Injectable() export class GameServerAssignerService implements OnModuleInit { @@ -90,6 +91,11 @@ export class GameServerAssignerService implements OnModuleInit { return game; } catch (error) { assertIsError(error); + + if (error instanceof NoFreeGameServerAvailableError) { + this.events.errorNoFreeGameServerAvailable.next({ gameId }); + } + throw new CannotAssignGameServerError(game, error.message); } } diff --git a/src/plugins/discord/notifications/no-free-game-servers-available.ts b/src/plugins/discord/notifications/no-free-game-servers-available.ts new file mode 100644 index 000000000..87bd9220c --- /dev/null +++ b/src/plugins/discord/notifications/no-free-game-servers-available.ts @@ -0,0 +1,28 @@ +import { EmbedBuilder } from 'discord.js'; + +interface NoFreeGameServersAvailableOptions { + game: { + number: string; + url: string; + }; + client: { + name: string; + iconUrl: string; + }; +} + +export const noFreeGameServersAvailable = ( + options: NoFreeGameServersAvailableOptions, +): EmbedBuilder => { + return new EmbedBuilder() + .setTitle('No free game servers available') + .setColor('#ff0000') + .setDescription( + `Game number: **[${options.game.number}](${options.game.url})**`, + ) + .setFooter({ + text: options.client.name, + iconURL: options.client.iconUrl, + }) + .setTimestamp(); +}; diff --git a/src/plugins/discord/services/admin-notifications.service.spec.ts b/src/plugins/discord/services/admin-notifications.service.spec.ts index a9d5deadc..517cef830 100644 --- a/src/plugins/discord/services/admin-notifications.service.spec.ts +++ b/src/plugins/discord/services/admin-notifications.service.spec.ts @@ -440,4 +440,25 @@ describe('AdminNotificationsService', () => { }); })); }); + + describe('when no free game servers are available', () => { + let game: Game; + beforeEach(async () => { + game = await gamesService._createOne(); + }); + + it('should send a notification to admins channel', () => + new Promise((resolve) => { + sentMessages.subscribe((message) => { + expect(message.embeds[0].toJSON().title).toEqual( + 'No free game servers available', + ); + resolve(); + }); + + events.errorNoFreeGameServerAvailable.next({ + gameId: game._id, + }); + })); + }); }); diff --git a/src/plugins/discord/services/admin-notifications.service.ts b/src/plugins/discord/services/admin-notifications.service.ts index 3decdf12d..e275672b1 100644 --- a/src/plugins/discord/services/admin-notifications.service.ts +++ b/src/plugins/discord/services/admin-notifications.service.ts @@ -27,6 +27,7 @@ import { GamesService } from '@/games/services/games.service'; import { substituteRequested } from '../notifications/substitute-requested'; import { version } from '../../../../package.json'; import { DISCORD_CLIENT } from '../discord-client.token'; +import { noFreeGameServersAvailable } from '../notifications/no-free-game-servers-available'; @Injectable() export class AdminNotificationsService implements OnModuleInit { @@ -380,6 +381,24 @@ export class AdminNotificationsService implements OnModuleInit { ), ) .subscribe((embed) => this.notifications.next(embed)); + + this.events.errorNoFreeGameServerAvailable + .pipe( + concatMap(({ gameId }) => from(this.gamesService.getById(gameId))), + map((game) => + noFreeGameServersAvailable({ + game: { + number: `${game.number}`, + url: `${this.environment.clientUrl}/game/${game.id}`, + }, + client: { + name: new URL(this.environment.clientUrl).hostname, + iconUrl: `${this.environment.clientUrl}/${iconUrlPath}`, + }, + }), + ), + ) + .subscribe((embed) => this.notifications.next(embed)); } private async getAdminChannels() {