Skip to content

Commit

Permalink
refactor: DiscordModule refactor (#369)
Browse files Browse the repository at this point in the history
* add MessageEmbedFactoryService
* better role resolution
* fix tests
* exclude __mocks__ from build
  • Loading branch information
garrappachc authored May 13, 2020
1 parent 073c128 commit bfe35e7
Show file tree
Hide file tree
Showing 15 changed files with 480 additions and 153 deletions.
63 changes: 63 additions & 0 deletions __mocks__/discord.js.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { EventEmitter } from 'events';
import { Collection } from '../node_modules/discord.js/src';

export class Message { }

export class TextChannel {
constructor(public name: string) { }
send(message: string) { return Promise.resolve(new Message()); }
}

export const queueChannel = new TextChannel('queue');
export const adminChannel = new TextChannel('admins');

export class Role {
constructor(public name: string) { }

mentionable = true;

toString() {
return `&<${this.name}>`;
}
}

export const pickupsRole = new Role('pickups');

export class Guild {
constructor(public name: string) { }

available = true;

channels = {
cache: new Collection([
[ 'queue', queueChannel ],
[ 'admins', adminChannel ],
]),
};

roles = {
cache: new Collection([
[ 'pickups', pickupsRole ],
]),
};
}

export class Client extends EventEmitter {

static _instance: Client;

user = { tag: 'bot#1337' };
guilds = {
cache: new Collection([ [ 'guild1', new Guild('FAKE_GUILD') ] ]),
}

constructor() {
super();
Client._instance = this;
}

login(token: string) { return Promise.resolve('FAKE_TOKEN'); }

}

export { MessageEmbed } from '../node_modules/discord.js/src';
23 changes: 0 additions & 23 deletions configs/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,28 +55,5 @@ export default () => ({
* Default: 5 * 60 * 1000 (5 minutes).
*/
promptAnnouncementDelay: 5 * 60 * 1000,

/**
* When prompting players to join the pickup queue, this role will be mentioned in the announcement.
*/
promptJoinQueueMentionRole: '<@&610855230992678922>',

/**
* When enabled, a notification will be sent to the admin channel each time any player receives a ban.
* Default: true
*/
notifyBans: true,

/**
* When enabled, a notification will be sent to the admin channel each time a new player registers.
* Default: true
*/
notifyNewPlayers: true,

/**
* Enables the substitute request announcements.
* Default: true
*/
notifySubstituteRequests: true,
},
});
2 changes: 2 additions & 0 deletions sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,7 @@ LOG_RELAY_PORT=

# Discord (optional)
DISCORD_BOT_TOKEN=
DISCORD_GUILD=
DISCORD_QUEUE_NOTIFICATIONS_CHANNEL=pickup_queue
DISCORD_QUEUE_NOTIFICATIONS_MENTION_ROLE="TF2 gamers"
DISCORD_ADMIN_NOTIFICATIONS_CHANNEL=pickup_admin_notifications
3 changes: 3 additions & 0 deletions src/discord/discord.module.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { Module, forwardRef } from '@nestjs/common';
import { DiscordNotificationsService } from './services/discord-notifications.service';
import { PlayersModule } from '@/players/players.module';
import { MessageEmbedFactoryService } from './services/message-embed-factory.service';

@Module({
imports: [
forwardRef(() => PlayersModule),
],
providers: [
DiscordNotificationsService,
MessageEmbedFactoryService,
],
exports: [
DiscordNotificationsService,
MessageEmbedFactoryService,
],
})
export class DiscordModule { }
189 changes: 154 additions & 35 deletions src/discord/services/discord-notifications.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,80 +1,199 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DiscordNotificationsService } from './discord-notifications.service';
import { DiscordNotificationsService, TargetChannel } from './discord-notifications.service';
import { Environment } from '@/environment/environment';
import { PlayersService } from '@/players/services/players.service';
import { ConfigService } from '@nestjs/config';
import { Client, Channel, Collection, TextChannel } from 'discord.js';
import { MessageEmbedFactoryService } from './message-embed-factory.service';
import { PlayersService } from '@/players/services/players.service';
import { Client, queueChannel, pickupsRole, adminChannel } from '@mocks/discord.js';
import { MessageEmbed } from 'discord.js';
import { ObjectId } from 'mongodb';

class EnvironmentStub {
discordBotToken = 'FAKE_DISCORD_BOT_TOKEN';
discordQueueNotificationsChannel = 'queue';
}

class PlayersServiceStub { }
discordGuild = 'FAKE_GUILD';
discordQueueNotificationsMentionRole = pickupsRole.name;
discordQueueNotificationsChannel = queueChannel.name;
discordAdminNotificationsChannel = adminChannel.name;
};

class ConfigServiceStub {
get(key: string) {
switch (key) {
case 'discordNotifications.promptJoinQueueMentionRole':
return 'FAKE_ROLE';

default:
return null;
}
}
}

jest.mock('discord.js');
class PlayersServiceStub {
getById() { return Promise.resolve({ name: 'FAKE_PLAYER' }); }
}

describe('DiscordNotificationsService', () => {
let service: DiscordNotificationsService;
let environment: EnvironmentStub;
let client: Client;
let queueChannel: TextChannel;

beforeEach(() => {
(Client as any as jest.MockedClass<typeof Client>).mockClear();
});

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
DiscordNotificationsService,
{ provide: Environment, useClass: EnvironmentStub },
{ provide: PlayersService, useClass: PlayersServiceStub },
{ provide: ConfigService, useClass: ConfigServiceStub },
{ provide: PlayersService, useClass: PlayersServiceStub },
MessageEmbedFactoryService,
],
}).compile();

service = module.get<DiscordNotificationsService>(DiscordNotificationsService);
});

beforeEach(() => {
client = (Client as any as jest.MockedClass<typeof Client>).mock.instances[0];

client.user = { tag: 'bot#1337' } as any;

jest.spyOn(client, 'login').mockResolvedValue('FAKE_DISCORD_BOT_TOKEN');
jest.spyOn(client, 'on').mockImplementation((event, listener) => {
if (event === 'ready') {
listener();
}
return client;
});
environment = module.get(Environment);
client = Client._instance;
});

it('should be defined', () => {
expect(service).toBeDefined();
});

it('should create the Client instance', () => {
expect(Client).toHaveBeenCalledTimes(1);
});

describe('#onModuleInit()', () => {
it('should login', () => {
const spy = jest.spyOn(client, 'login');
service.onModuleInit();
client.emit('ready');
expect(spy).toHaveBeenCalledWith('FAKE_DISCORD_BOT_TOKEN');
});
});

describe('when logged in', () => {
beforeEach(() => {
service.onModuleInit();
client.emit('ready');
});

describe('#notifyQueue()', () => {
describe('when the role to mention exists and is mentionable', () => {
it('should send the message mentioning the role', () => {
const spy = jest.spyOn(queueChannel, 'send');
service.notifyQueue(6, 12);
expect(spy).toHaveBeenCalledWith(expect.stringMatching('&<pickups> 6/12'));
});
});

describe('when the role to mention exists but is not mentionable', () => {
beforeEach(() => {
pickupsRole.mentionable = false;
});

it('should send the message without mentioning the role', () => {
const spy = jest.spyOn(queueChannel, 'send');
service.notifyQueue(6, 12);
expect(spy).toHaveBeenCalledWith(expect.stringMatching('6/12'));
});
});

describe('when the role to mention does not exist', () => {
beforeEach(() => {
environment.discordQueueNotificationsMentionRole = 'foo';
});

it('should send the message without mentioning the role', () => {
const spy = jest.spyOn(queueChannel, 'send');
service.notifyQueue(6, 12);
expect(spy).toHaveBeenCalledWith(expect.stringMatching('6/12'));
});
});

describe('when the channel does not exist', () => {
beforeEach(() => {
environment.discordQueueNotificationsChannel = 'foo';
});

it('should not send any messages', () => {
const spy = jest.spyOn(queueChannel, 'send');
service.notifyQueue(6, 12);
expect(spy).not.toHaveBeenCalled();
});
});
});

describe('#sendNotification()', () => {
let notification: MessageEmbed;

beforeEach(() => {
notification = new MessageEmbed().setTitle('test');
});

describe('when the channel exists', () => {
it('should send a notification to the queue channel', async () => {
const spy = jest.spyOn(queueChannel, 'send');
await service.sendNotification(TargetChannel.Queue, notification);
expect(spy).toHaveBeenCalledWith(notification);
});

it('should send a notification to the admins channel', async () => {
const spy = jest.spyOn(adminChannel, 'send');
await service.sendNotification(TargetChannel.Admins, notification);
expect(spy).toHaveBeenCalledWith(notification);
});
});

describe('when the channel does not exist', () => {
beforeEach(() => {
environment.discordQueueNotificationsChannel = 'foo';
environment.discordAdminNotificationsChannel = 'bar';
});

it('should not send anything', async () => {
const spy = jest.spyOn(queueChannel, 'send');
await service.sendNotification(TargetChannel.Queue, notification);
expect(spy).not.toHaveBeenCalled();
});
});

describe('when the channel is not specified', () => {
beforeEach(() => {
delete environment.discordQueueNotificationsChannel;
});

it('should not send anything', async () => {
const spy = jest.spyOn(queueChannel, 'send');
await service.sendNotification(TargetChannel.Queue, notification);
expect(spy).not.toHaveBeenCalled();
});
});
});

describe('#notifySubstituteRequest()', () => {
it('should send a notification to the queue channel', async () => {
const spy = jest.spyOn(queueChannel, 'send');
await service.notifySubstituteRequest({ gameId: 'FAKE_GAME', gameNumber: 3, gameClass: 'soldier', team: 'RED' });
expect(spy).toHaveBeenCalledWith(expect.any(MessageEmbed));
});
});

describe('#notifyPlayerBanAdded()', () => {
it('should send a notification to the queue channel', async () => {
const spy = jest.spyOn(adminChannel, 'send');
await service.notifyPlayerBanAdded({ player: new ObjectId(), admin: new ObjectId(), start: new Date(),
end: new Date(), _id: new ObjectId().toString() });
expect(spy).toHaveBeenCalledWith(expect.any(MessageEmbed));
});
});

describe('#notifyPlayerBanRevoked()', () => {
it('should send a notification to the queue channel', async () => {
const spy = jest.spyOn(adminChannel, 'send');
await service.notifyPlayerBanRevoked({ player: new ObjectId(), admin: new ObjectId(), start: new Date(),
end: new Date(), _id: new ObjectId().toString() });
expect(spy).toHaveBeenCalledWith(expect.any(MessageEmbed));
});
});

describe('#notifyNewPlayer()', () => {
it('should send a notification to the queue channel', async () => {
const spy = jest.spyOn(adminChannel, 'send');
await service.notifyNewPlayer({ id: 'FAKE_PLAYER_ID', name: 'FAKE_PLAYER', steamId: 'FAKE_STEAM_ID', hasAcceptedRules: true });
expect(spy).toHaveBeenCalledWith(expect.any(MessageEmbed));
});
});
});
});
Loading

0 comments on commit bfe35e7

Please sign in to comment.