diff --git a/src/base/presence/PresenceManager.spec.ts b/src/base/presence/PresenceManager.spec.ts index 06fee13..1913e05 100644 --- a/src/base/presence/PresenceManager.spec.ts +++ b/src/base/presence/PresenceManager.spec.ts @@ -95,6 +95,19 @@ describe('Base > Presence > PresenceManager', () => { expect(clearIntervalSpy).toHaveBeenCalledWith(oldHandle); expect((manager as unknown as { intervalHandle: number }).intervalHandle).not.toBe(oldHandle); }); + + it('should call manager.update on an interval.', async () => { + jest.useFakeTimers(); + const updateSpy = jest.spyOn(manager, 'update'); + updateSpy.mockImplementation(() => Promise.resolve()); + + await manager.setRefreshInterval(1000); + jest.advanceTimersByTime(3000); + + expect(manager.update).toHaveBeenCalled(); + jest.clearAllTimers(); + jest.useRealTimers(); + }); }); describe('setPresence()', () => { @@ -117,6 +130,16 @@ describe('Base > Presence > PresenceManager', () => { expect(logger.info).toHaveBeenCalledWith('Presence changed to: something'); }); + it('should log presence change even if no user exists.', () => { + const oldUser = client.user; + client.user = null; + + manager.setPresence('something'); + expect(logger.info).toHaveBeenCalledWith('Presence changed to: something'); + + client.user = oldUser; + }); + it('should log error if presence change fails.', () => { const expectedError = new Error('Oops'); (client.user!.setPresence as jest.Mock).mockImplementationOnce(() => { diff --git a/src/base/presence/PresenceResolver.ts b/src/base/presence/PresenceResolver.ts index e8d2226..487adcd 100644 --- a/src/base/presence/PresenceResolver.ts +++ b/src/base/presence/PresenceResolver.ts @@ -69,7 +69,7 @@ export class PresenceResolver { case 'uptime': return this.getUptime(); default: - throw new Error('Invalid presence name provided.'); + return Promise.resolve(''); } } diff --git a/src/commands/ConfigureCommand.spec.ts b/src/commands/ConfigureCommand.spec.ts index 845e71a..73c24e1 100644 --- a/src/commands/ConfigureCommand.spec.ts +++ b/src/commands/ConfigureCommand.spec.ts @@ -54,8 +54,7 @@ describe('Commands > ConfigureCommand', () => { describe('runChannel()', () => { const interaction = { - deferReply: jest.fn(), - editReply: jest.fn(), + reply: jest.fn(), locale: 'en-US', guildId: '1267881983548063785', options: { @@ -64,16 +63,11 @@ describe('Commands > ConfigureCommand', () => { } } as unknown as GuildChatInputCommandInteraction; - it('should defer the reply.', async () => { - await command.run(interaction); - expect(interaction.deferReply).toHaveBeenCalled(); - }); - it('should reply with pre check message if no channel is provided.', async () => { (interaction.options.getChannel as jest.Mock).mockReturnValueOnce(null); await command.run(interaction); - expect(interaction.editReply).toHaveBeenCalledWith({ content: 'No channel provided.' }); + expect(interaction.reply).toHaveBeenCalledWith({ content: 'No channel provided.' }); }); it('should update guild channel.', async () => { @@ -83,7 +77,7 @@ describe('Commands > ConfigureCommand', () => { it('should reply with channel update message.', async () => { await command.run(interaction); - expect(interaction.editReply).toHaveBeenCalledWith({ content: 'Successfully updated notifications channel to Channel.' }); + expect(interaction.reply).toHaveBeenCalledWith({ content: 'Successfully updated notifications channel to Channel.' }); }); }); @@ -93,34 +87,34 @@ describe('Commands > ConfigureCommand', () => { update: jest.fn().mockImplementation(() => Promise.resolve()) }; const followUpResponseMock = { - awaitMessageComponent: jest.fn().mockResolvedValue(userResponseMock) + awaitMessageComponent: jest.fn().mockImplementation(({ filter }) => { + filter({ user: { id: 1 }, customId: 'configure-storefronts-enable' }); + return userResponseMock; + }) }; const interaction = { - deferReply: jest.fn(), - editReply: jest.fn(), + reply: jest.fn(), followUp: jest.fn().mockResolvedValue(followUpResponseMock), locale: 'en-US', guildId: '1267881983548063785', options: { getSubcommand: jest.fn().mockReturnValue('storefronts') + }, + user: { + id: 1 } } as unknown as GuildChatInputCommandInteraction; - it('should defer the reply.', async () => { - await command.run(interaction); - expect(interaction.deferReply).toHaveBeenCalled(); - }); - it('should reply with empty storefronts message if no storefronts exist.', async () => { (getStorefronts as jest.Mock).mockResolvedValueOnce([]); await command.run(interaction); - expect(interaction.editReply).toHaveBeenCalledWith({ content: 'No storefronts are available right now.' }); + expect(interaction.reply).toHaveBeenCalledWith({ content: 'No storefronts are available right now.' }); }); it('should reply with start message.', async () => { await command.run(interaction); - expect(interaction.editReply).toHaveBeenCalledWith({ content: 'You will receive a follow up for each storefront available. Please, click on the buttons as they appear to enable or disable notifications for each storefront.' }); + expect(interaction.reply).toHaveBeenCalledWith({ content: 'You will receive a follow up for each storefront available. Please, click on the buttons as they appear to enable or disable notifications for each storefront.' }); }); it('should send follow up with correct components for each storefront.', async () => { @@ -181,8 +175,7 @@ describe('Commands > ConfigureCommand', () => { describe('runLanguage()', () => { const interaction = { - deferReply: jest.fn(), - editReply: jest.fn(), + reply: jest.fn(), locale: 'en-US', guildId: '1267881983548063785', options: { @@ -191,16 +184,11 @@ describe('Commands > ConfigureCommand', () => { } } as unknown as GuildChatInputCommandInteraction; - it('should defer the reply.', async () => { - await command.run(interaction); - expect(interaction.deferReply).toHaveBeenCalled(); - }); - it('should reply with pre check message if no locale is provided.', async () => { (interaction.options.getString as jest.Mock).mockReturnValueOnce(null); await command.run(interaction); - expect(interaction.editReply).toHaveBeenCalledWith({ content: 'No language provided.' }); + expect(interaction.reply).toHaveBeenCalledWith({ content: 'No language provided.' }); }); it('should update guild locale.', async () => { @@ -210,14 +198,13 @@ describe('Commands > ConfigureCommand', () => { it('should reply with language update message.', async () => { await command.run(interaction); - expect(interaction.editReply).toHaveBeenCalledWith({ content: 'Successfully updated notifications language to **English**.' }); + expect(interaction.reply).toHaveBeenCalledWith({ content: 'Successfully updated notifications language to **English**.' }); }); }); describe('runDefault()', () => { const interaction = { - deferReply: jest.fn(), - editReply: jest.fn(), + reply: jest.fn(), locale: 'en-US', guildId: '1267881983548063785', options: { @@ -225,14 +212,9 @@ describe('Commands > ConfigureCommand', () => { } } as unknown as GuildChatInputCommandInteraction; - it('should defer the reply.', async () => { - await command.run(interaction); - expect(interaction.deferReply).toHaveBeenCalled(); - }); - it('should reply with unknown subcommand message.', async () => { await command.run(interaction); - expect(interaction.editReply).toHaveBeenCalledWith({ content: 'Unknown subcommand received.' }); + expect(interaction.reply).toHaveBeenCalledWith({ content: 'Unknown subcommand received.' }); }); }); }); diff --git a/src/commands/ConfigureCommand.ts b/src/commands/ConfigureCommand.ts index be5a5f2..65e0aa8 100644 --- a/src/commands/ConfigureCommand.ts +++ b/src/commands/ConfigureCommand.ts @@ -68,8 +68,6 @@ export default class ConfigureCommand extends Command { } public override async run(interaction: GuildChatInputCommandInteraction): Promise { - await interaction.deferReply(); - const subCommand = interaction.options.getSubcommand(); switch (subCommand) { @@ -89,12 +87,12 @@ export default class ConfigureCommand extends Command { const channel = interaction.options.getChannel('channel'); if (!channel) { - await interaction.editReply({ content: t('commands.configure.run.channel.pre_check.text') }); + await interaction.reply({ content: t('commands.configure.run.channel.pre_check.text') }); return; } await updateOrCreateGuildChannel(interaction.guildId, channel.id); - await interaction.editReply({ content: t('commands.configure.run.channel.success.text', { channel: channel.toString() }) }); + await interaction.reply({ content: t('commands.configure.run.channel.success.text', { channel: channel.toString() }) }); } private async runStorefronts(interaction: GuildChatInputCommandInteraction): Promise { @@ -102,11 +100,11 @@ export default class ConfigureCommand extends Command { const storefronts = await getStorefronts(); if (!storefronts.length) { - await interaction.editReply({ content: t('commands.configure.run.storefronts.empty.text') }); + await interaction.reply({ content: t('commands.configure.run.storefronts.empty.text') }); return; } - await interaction.editReply({ content: t('commands.configure.run.storefronts.start.text') }); + await interaction.reply({ content: t('commands.configure.run.storefronts.start.text') }); const buttonIds = { enable: 'configure-storefronts-enable', @@ -153,19 +151,19 @@ export default class ConfigureCommand extends Command { const locale = interaction.options.getString('language') as Locale | null; if (!locale) { - await interaction.editReply({ content: t('commands.configure.run.language.pre_check.text') }); + await interaction.reply({ content: t('commands.configure.run.language.pre_check.text') }); return; } const language = t(AVAILABLE_LOCALES[locale]); await updateOrCreateGuildLocale(interaction.guildId, locale); - await interaction.editReply({ content: t('commands.configure.run.language.success.text', { language }) }); + await interaction.reply({ content: t('commands.configure.run.language.success.text', { language }) }); } private async runDefault(interaction: ChatInputCommandInteraction): Promise { const t = getInteractionTranslator(interaction); - await interaction.editReply({ content: t('commands.configure.run.default.response.text') }); + await interaction.reply({ content: t('commands.configure.run.default.response.text') }); } } diff --git a/src/commands/HelpCommand.spec.ts b/src/commands/HelpCommand.spec.ts index 588179e..911384a 100644 --- a/src/commands/HelpCommand.spec.ts +++ b/src/commands/HelpCommand.spec.ts @@ -24,16 +24,10 @@ describe('Commands > HelpCommand', () => { describe('run()', () => { const command = new HelpCommand(client); const interaction = { - deferReply: jest.fn(), - editReply: jest.fn(), + reply: jest.fn(), locale: 'en-US' } as unknown as ChatInputCommandInteraction; - it('should defer the reply.', async () => { - await command.run(interaction); - expect(interaction.deferReply).toHaveBeenCalled(); - }); - it('should reply with the embed.', async () => { await command.run(interaction); @@ -60,7 +54,7 @@ describe('Commands > HelpCommand', () => { ) ]; - expect(interaction.editReply).toHaveBeenCalledWith({ embeds: expectedEmbeds, components: expectedComponents }); + expect(interaction.reply).toHaveBeenCalledWith({ embeds: expectedEmbeds, components: expectedComponents }); }); }); }); diff --git a/src/commands/HelpCommand.ts b/src/commands/HelpCommand.ts index 75a7dd1..64e57b8 100644 --- a/src/commands/HelpCommand.ts +++ b/src/commands/HelpCommand.ts @@ -17,7 +17,6 @@ export default class HelpCommand extends Command { } public override async run(interaction: ChatInputCommandInteraction): Promise { - await interaction.deferReply(); const t = getInteractionTranslator(interaction); const embed = new EmbedBuilder() @@ -40,6 +39,6 @@ export default class HelpCommand extends Command { new ButtonBuilder().setEmoji('🌎').setStyle(ButtonStyle.Link).setURL(BOT_WEBSITE_URL).setLabel(t('commands.help.run.buttons.bot_website.label')), ); - await interaction.editReply({ embeds: [embed], components: [row1, row2] }); + await interaction.reply({ embeds: [embed], components: [row1, row2] }); } } diff --git a/src/commands/InfoCommand.spec.ts b/src/commands/InfoCommand.spec.ts index ec02601..a6d6768 100644 --- a/src/commands/InfoCommand.spec.ts +++ b/src/commands/InfoCommand.spec.ts @@ -53,22 +53,16 @@ describe('Commands > InfoCommand', () => { describe('run()', () => { const command = new InfoCommand(client); const interaction = { - deferReply: jest.fn(), - editReply: jest.fn(), + reply: jest.fn(), locale: 'en-US', guildId: '1267881983548063785' } as unknown as GuildChatInputCommandInteraction; - it('should defer the reply.', async () => { - await command.run(interaction); - expect(interaction.deferReply).toHaveBeenCalled(); - }); - it('should reply with configure message if no settings are found.', async () => { (getGuild as jest.Mock).mockResolvedValueOnce(null); await command.run(interaction); - expect(interaction.editReply).toHaveBeenCalledWith({ content: 'No settings have been found for this server. Please use **/configure channel** command to set up the subscription channel.' }); + expect(interaction.reply).toHaveBeenCalledWith({ content: 'No settings have been found for this server. Please use **/configure channel** command to set up the subscription channel.' }); }); it('should reply with the correct embed if channel exists.', async () => { @@ -91,7 +85,7 @@ describe('Commands > InfoCommand', () => { }) ]; - expect(interaction.editReply).toHaveBeenCalledWith({ embeds: expectedEmbeds }); + expect(interaction.reply).toHaveBeenCalledWith({ embeds: expectedEmbeds }); }); it('should reply with the correct embed if no channel is set.', async () => { @@ -128,7 +122,7 @@ describe('Commands > InfoCommand', () => { }) ]; - expect(interaction.editReply).toHaveBeenCalledWith({ embeds: expectedEmbeds }); + expect(interaction.reply).toHaveBeenCalledWith({ embeds: expectedEmbeds }); }); it('should reply with the correct embed if channel set does not exist.', async () => { @@ -152,7 +146,7 @@ describe('Commands > InfoCommand', () => { }) ]; - expect(interaction.editReply).toHaveBeenCalledWith({ embeds: expectedEmbeds }); + expect(interaction.reply).toHaveBeenCalledWith({ embeds: expectedEmbeds }); }); }); }); diff --git a/src/commands/InfoCommand.ts b/src/commands/InfoCommand.ts index 20185a9..40f2c7d 100644 --- a/src/commands/InfoCommand.ts +++ b/src/commands/InfoCommand.ts @@ -20,12 +20,11 @@ export default class InfoCommand extends Command { } public override async run(interaction: GuildChatInputCommandInteraction): Promise { - await interaction.deferReply(); const t = getInteractionTranslator(interaction); const guildInfo = await getGuild(interaction.guildId); if (!guildInfo) { - await interaction.editReply({ content: t('commands.info.run.pre_check.text') }); + await interaction.reply({ content: t('commands.info.run.pre_check.text') }); return; } @@ -52,7 +51,7 @@ export default class InfoCommand extends Command { text: t('commands.info.run.embed.footer', { createdAt, updatedAt }) }); - await interaction.editReply({ embeds: [embed] }); + await interaction.reply({ embeds: [embed] }); } private async getChannel(channelId: string | null): Promise { diff --git a/src/commands/OffersCommand.spec.ts b/src/commands/OffersCommand.spec.ts index 65524fa..f61db9b 100644 --- a/src/commands/OffersCommand.spec.ts +++ b/src/commands/OffersCommand.spec.ts @@ -44,27 +44,21 @@ describe('Commands > OffersCommand', () => { describe('run()', () => { const command = new OffersCommand(client); const interaction = { - deferReply: jest.fn(), - editReply: jest.fn(), + reply: jest.fn(), followUp: jest.fn(), locale: 'en-US' } as unknown as ChatInputCommandInteraction; - it('should defer the reply.', async () => { - await command.run(interaction); - expect(interaction.deferReply).toHaveBeenCalled(); - }); - it('should reply with the empty message if no offers are available.', async () => { (getCurrentGameOffers as jest.Mock).mockResolvedValueOnce([]); await command.run(interaction); - expect(interaction.editReply).toHaveBeenCalledWith({ content: 'There are currently no offers in any of the following storefronts: Steam, EpicGames.' }); + expect(interaction.reply).toHaveBeenCalledWith({ content: 'There are currently no offers in any of the following storefronts: Steam, EpicGames.' }); }); it('should reply with start message.', async () => { await command.run(interaction); - expect(interaction.editReply).toHaveBeenCalledWith({ content: "Here's a list of the currently available offers." }); + expect(interaction.reply).toHaveBeenCalledWith({ content: "Here's a list of the currently available offers." }); }); it('should send an embed and component for each game.', async () => { diff --git a/src/commands/OffersCommand.ts b/src/commands/OffersCommand.ts index 12def7e..b6e1b53 100644 --- a/src/commands/OffersCommand.ts +++ b/src/commands/OffersCommand.ts @@ -19,17 +19,16 @@ export default class OffersCommand extends Command { } public override async run(interaction: ChatInputCommandInteraction): Promise { - await interaction.deferReply(); const t = getInteractionTranslator(interaction); const offers = await getCurrentGameOffers(); if (!offers.length) { const storefronts = await getStorefronts(); - await interaction.editReply({ content: t('commands.offers.run.empty.text', { list: storefronts.join(', ') }) }); + await interaction.reply({ content: t('commands.offers.run.empty.text', { list: storefronts.join(', ') }) }); return; } - await interaction.editReply({ content: t('commands.offers.run.start.text') }); + await interaction.reply({ content: t('commands.offers.run.start.text') }); for (const offer of offers) { const { embed, component } = offerToMessage(offer, t); diff --git a/src/config/app.spec.ts b/src/config/app.spec.ts index ffa3016..c22923e 100644 --- a/src/config/app.spec.ts +++ b/src/config/app.spec.ts @@ -63,6 +63,13 @@ describe('Config > App', () => { expect(config.DISCORD_SHARDING_COUNT).toBe('auto'); }); + it('should export valid DISCORD_SHARDING_COUNT if set to auto.', () => { + process.env = { ...mockedEnv, DISCORD_SHARDING_COUNT: 'auto' }; + resetModule(); + + expect(config.DISCORD_SHARDING_COUNT).toBe('auto'); + }); + it('should export valid REDIS_URI.', () => { expect(config.REDIS_URI).toBe('redis://localhost:6379'); }); diff --git a/src/features/gameOffers/classes/OffersNotifier.spec.ts b/src/features/gameOffers/classes/OffersNotifier.spec.ts index 9a5811e..6161838 100644 --- a/src/features/gameOffers/classes/OffersNotifier.spec.ts +++ b/src/features/gameOffers/classes/OffersNotifier.spec.ts @@ -166,6 +166,17 @@ describe('Features > GameOffers > Classes > OffersNotifier', () => { guild.shardId = 0; }); + it('should send the message if client is not sharded.', async () => { + const oldShard = client.shard; + client.shard = null; + + await triggerRef.trigger!(); + + expect(channel.send).toHaveBeenCalled(); + + client.shard = oldShard; + }); + it('should not send the message if channel is null.', async () => { (guild.channels.fetch as jest.Mock).mockResolvedValueOnce(null); await triggerRef.trigger!(); diff --git a/src/features/gameOffers/functions/getCurrentGameOffers.spec.ts b/src/features/gameOffers/functions/getCurrentGameOffers.spec.ts index 6f4961a..309d82d 100644 --- a/src/features/gameOffers/functions/getCurrentGameOffers.spec.ts +++ b/src/features/gameOffers/functions/getCurrentGameOffers.spec.ts @@ -42,5 +42,12 @@ describe('Features > GameOffers > Functions > GetCurrentGameOffers', () => { const result = await getCurrentGameOffers(); expect(result).toStrictEqual([gameOffer]); }); + + it('should return empty array if no offers exist.', async () => { + (client.keys as jest.Mock).mockResolvedValueOnce([]); + + const result = await getCurrentGameOffers(); + expect(result).toStrictEqual([]); + }); }); }); diff --git a/src/features/gameOffers/functions/getCurrentGameOffers.ts b/src/features/gameOffers/functions/getCurrentGameOffers.ts index 0388e4a..f919d3a 100644 --- a/src/features/gameOffers/functions/getCurrentGameOffers.ts +++ b/src/features/gameOffers/functions/getCurrentGameOffers.ts @@ -4,8 +4,11 @@ import { GameOffer } from '../../../models/gameOffer'; export const getCurrentGameOffers = (): Promise => { return withRedis(async (client) => { const keys = await client.keys('offer:*'); - const items = await client.mGet(keys); + if (!keys.length) { + return []; + } + const items = await client.mGet(keys); return items .filter((item) => item !== null) .map((item: string) => JSON.parse(item)); diff --git a/src/i18n/translate.ts b/src/i18n/translate.ts index 66b7a34..33310b2 100644 --- a/src/i18n/translate.ts +++ b/src/i18n/translate.ts @@ -28,14 +28,14 @@ export const MESSAGE_KEY_TO_LOCALE: Partial> = { }; const castLocaleStrings = localeStrings as LocaleMessageMap; -const getMessage = (locale: Locale, key: MessageKey, useDefault: boolean = true): IntlMessageFormat => { +const getMessage = (locale: Locale, key: MessageKey, useDefault: boolean): IntlMessageFormat => { const messagesForLocale = castLocaleStrings[locale]; if (!messagesForLocale) { throw new TranslatorError(`No messages for locale ${locale} exist.`); } const message = messagesForLocale[key]; - const defaultMessage = castLocaleStrings[DEFAULT_LOCALE]?.[key]; + const defaultMessage = castLocaleStrings[DEFAULT_LOCALE]![key]; // Note: We can safely null-assert here because DEFAULT_LOCALE is a key of localStrings. const messageToReturn = useDefault ? message ?? defaultMessage : message; if (!messageToReturn) { diff --git a/src/services/database/migration.ts b/src/services/database/migration.ts index 3044c27..3cb1817 100644 --- a/src/services/database/migration.ts +++ b/src/services/database/migration.ts @@ -58,10 +58,6 @@ export const runMigrations = async (migrationsDirectory: string): Promise