diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..e528c02 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,9 @@ +version: 2 + +exclude_patterns: + - '**/*.spec.ts' + +checks: + similar-code: + config: + threshold: 50 diff --git a/README.md b/README.md index d7bbb30..e6778a0 100644 --- a/README.md +++ b/README.md @@ -92,8 +92,8 @@ The server can also be built and run locally without Docker — see [Development Once the server is running, it will post messages to a single Discord channel (`DISCORD_CHANNEL_ID` defined in [Installation](#installation)) when: * The status of a realm (for each in `REALM_SLUGS` defined in [Installation](#installation)) changes - * The server will poll the _World of Warcraft_ API every 5 minutes - * The server will only post a message when the realm status changes from its previous state (i.e. it won't post a message for the same status multiple times) + * The server will poll the _World of Warcraft_ API every 60 seconds + * The server will only post a message when the status of a realm changes from its previous state (i.e. it won't post messages for the same status multiple times) * The realm status is provided in the API from the [connected realm](https://us.battle.net/support/en/article/000014296) data instead of the realm data itself – the server will automatically retrieve the connected realm data from the realm slug * Daily quests and weekly raids reset * The server will post a message every day when daily quests reset and a message on Tuesday when weekly raids reset, both at a fixed time of 1500 UTC (based on the US reset time) diff --git a/src/index.spec.ts b/src/index.spec.ts index 3a7dc58..edcd3ce 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -30,9 +30,6 @@ describe('index', () => { expect(cron.schedule).toHaveBeenNthCalledWith(2, '* * * * *', expect.any(Function)); expect(postResetMessage).toHaveBeenCalledTimes(1); - - expect(postRealmStatus).toHaveBeenCalledTimes(2); - expect(postRealmStatus).toHaveBeenNthCalledWith(1, 'test_realm_slug_1'); - expect(postRealmStatus).toHaveBeenNthCalledWith(2, 'test_realm_slug_2'); + expect(postRealmStatus).toHaveBeenCalledTimes(1); }); }); diff --git a/src/index.ts b/src/index.ts index a9ed66c..ff5e975 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,12 @@ import cron from 'node-cron'; import postResetMessage from './reset'; -import postRealmStatus from './realm'; +import postRealmStatuses from './realm'; global.fetch = require('node-fetch'); process.env.TZ = 'UTC'; -const { - REALM_SLUGS, - REALM_SLUG, // Backward compatibility for pre-v1.0.2 -} = process.env; - // Every day at 15:00 UTC cron.schedule('0 15 * * *', () => { try { @@ -25,8 +20,7 @@ cron.schedule('0 15 * * *', () => { // Every 60 seconds cron.schedule('* * * * *', () => { try { - String(REALM_SLUGS || /* istanbul ignore next */ REALM_SLUG) - .split(',').forEach((slug) => postRealmStatus(slug)); + postRealmStatuses(); } catch (error) { /* istanbul ignore next */ console.error('Unable to post realm status message', error); diff --git a/src/realm.spec.ts b/src/realm.spec.ts index 53bbba3..8cb3f33 100644 --- a/src/realm.spec.ts +++ b/src/realm.spec.ts @@ -1,7 +1,7 @@ import fetchMock from 'fetch-mock'; import postDiscordChannelMessage from './discord'; -import postRealmStatus from './realm'; +import postRealmStatuses from './realm'; jest.mock('./blizzard', () => ({ __esModule: true, @@ -15,11 +15,23 @@ jest.mock('./discord', () => ({ beforeEach(() => { fetchMock.get( - 'begin:https://us.api.blizzard.com/data/wow/realm/', + 'begin:https://us.api.blizzard.com/data/wow/realm/test_realm_slug_1', { body: { + slug: 'test_realm_slug_1', name: { en_US: 'test_realm_name' }, - connected_realm: { href: 'https://test-connected-realm-api.com/' }, + connected_realm: { href: 'https://test-connected-realm-1-api.com/' }, + }, + }, + ); + + fetchMock.get( + 'begin:https://us.api.blizzard.com/data/wow/realm/test_realm_slug_2', + { + body: { + slug: 'test_realm_slug_2', + name: { en_US: 'test_realm_name' }, + connected_realm: { href: 'https://test-connected-realm-2-api.com/' }, }, }, ); @@ -30,91 +42,89 @@ afterEach(() => { fetchMock.restore(); }); -describe('postRealmStatus', () => { +describe('postRealmStatuses', () => { it('Makes first request without posting a message to Discord', async () => { fetchMock.get( - 'https://test-connected-realm-api.com/', - { - body: { - status: { - type: 'UP', - name: { en_US: 'Up' }, - }, - }, - }, + 'https://test-connected-realm-1-api.com/', + { body: { status: { type: 'UP', name: { en_US: 'Up' } } } }, + ); + + fetchMock.get( + 'https://test-connected-realm-2-api.com/', + { body: { status: { type: 'UP', name: { en_US: 'Up' } } } }, ); - await postRealmStatus('test_realm_slug'); + await postRealmStatuses(); + + const [realm1, realm2, connectedRealm1, connectedRealm2] = fetchMock.calls(); + + expect(realm1[0]).toBe('https://us.api.blizzard.com/data/wow/realm/test_realm_slug_1?namespace=dynamic-us'); + expect(realm1[1]).toStrictEqual({ headers: { Authorization: 'Bearer test_blizzard_api_token' } }); + + expect(realm2[0]).toBe('https://us.api.blizzard.com/data/wow/realm/test_realm_slug_2?namespace=dynamic-us'); + expect(realm2[1]).toStrictEqual({ headers: { Authorization: 'Bearer test_blizzard_api_token' } }); + + expect(connectedRealm1[0]).toBe('https://test-connected-realm-1-api.com/'); + expect(connectedRealm1[1]).toStrictEqual({ headers: { Authorization: 'Bearer test_blizzard_api_token' } }); - const [realm, connectedRealm] = fetchMock.calls(); + expect(connectedRealm2[0]).toBe('https://test-connected-realm-2-api.com/'); + expect(connectedRealm2[1]).toStrictEqual({ headers: { Authorization: 'Bearer test_blizzard_api_token' } }); - expect(realm[0]).toBe('https://us.api.blizzard.com/data/wow/realm/test_realm_slug?namespace=dynamic-us'); - expect(realm[1]).toStrictEqual({ headers: { Authorization: 'Bearer test_blizzard_api_token' } }); - expect(connectedRealm[0]).toBe('https://test-connected-realm-api.com/'); - expect(connectedRealm[1]).toStrictEqual({ headers: { Authorization: 'Bearer test_blizzard_api_token' } }); expect(postDiscordChannelMessage).toHaveBeenCalledTimes(0); }); - it('Posts to Discord when realm status changes after the initial request', async () => { + it('Posts to Discord for multiple realm status changes', async () => { fetchMock.get( - 'https://test-connected-realm-api.com/', - { - body: { - status: { - type: 'DOWN', - name: { en_US: 'Down' }, - }, - }, - }, + 'https://test-connected-realm-1-api.com/', + { body: { status: { type: 'DOWN', name: { en_US: 'Down' } } } }, ); - await postRealmStatus('test_realm_slug'); - - const [realm, connectedRealm] = fetchMock.calls(); - - expect(realm[0]).toBe('https://us.api.blizzard.com/data/wow/realm/test_realm_slug?namespace=dynamic-us'); - expect(realm[1]).toStrictEqual({ headers: { Authorization: 'Bearer test_blizzard_api_token' } }); + fetchMock.get( + 'https://test-connected-realm-2-api.com/', + { body: { status: { type: 'DOWN', name: { en_US: 'Down' } } } }, + ); - expect(connectedRealm[0]).toBe('https://test-connected-realm-api.com/'); - expect(connectedRealm[1]).toStrictEqual({ headers: { Authorization: 'Bearer test_blizzard_api_token' } }); + await postRealmStatuses(); expect(postDiscordChannelMessage).toHaveBeenCalledTimes(1); - expect(postDiscordChannelMessage).toHaveBeenNthCalledWith(1, ':red_circle: test_realm_name is **down**'); + expect(postDiscordChannelMessage).toHaveBeenNthCalledWith( + 1, + ':red_circle: test_realm_name is **down**\n:red_circle: test_realm_name is **down**', + ); }); - it('Posts to Discord when realm status changes after posting for the first time', async () => { + it('Posts to Discord for a single realm status change', async () => { fetchMock.get( - 'https://test-connected-realm-api.com/', - { - body: { - status: { - type: 'UP', - name: { en_US: 'Up' }, - }, - }, - }, + 'https://test-connected-realm-1-api.com/', + { body: { status: { type: 'UP', name: { en_US: 'Up' } } } }, ); - await postRealmStatus('test_realm_slug'); + fetchMock.get( + 'https://test-connected-realm-2-api.com/', + { body: { status: { type: 'DOWN', name: { en_US: 'Down' } } } }, + ); + + await postRealmStatuses(); expect(postDiscordChannelMessage).toHaveBeenCalledTimes(1); - expect(postDiscordChannelMessage).toHaveBeenNthCalledWith(1, ':green_circle: test_realm_name is **up**'); + expect(postDiscordChannelMessage).toHaveBeenNthCalledWith( + 1, + ':green_circle: test_realm_name is **up**', + ); }); - it('Does not post to Discord when realm status is the same', async () => { + it('Does not post to Discord when no realm status changes', async () => { fetchMock.get( - 'https://test-connected-realm-api.com/', - { - body: { - status: { - type: 'UP', - name: { en_US: 'Up' }, - }, - }, - }, + 'https://test-connected-realm-1-api.com/', + { body: { status: { type: 'UP', name: { en_US: 'Up' } } } }, + ); + + fetchMock.get( + 'https://test-connected-realm-2-api.com/', + { body: { status: { type: 'DOWN', name: { en_US: 'Down' } } } }, ); - await postRealmStatus('test_realm_slug'); + await postRealmStatuses(); expect(postDiscordChannelMessage).toHaveBeenCalledTimes(0); }); diff --git a/src/realm.ts b/src/realm.ts index faeb585..41ac22b 100644 --- a/src/realm.ts +++ b/src/realm.ts @@ -9,6 +9,8 @@ import { const { NODE_ENV, + REALM_SLUGS, + REALM_SLUG, // Backwards compatibility for pre-v1.0.2 } = process.env; const realmStatuses: { @@ -16,14 +18,9 @@ const realmStatuses: { } = {}; /** - * Handler for posting a realm status message to Discord + * Helper for getting realm and connected realm data */ -const postRealmStatus = async (slug: string) => { - /* istanbul ignore if */ - if (NODE_ENV !== 'test') { - console.log('Posting realm status message to Discord for:', slug); - } - +const getRealmData = async (slug: string) => { const headers = { Authorization: `Bearer ${await getBlizzardAPIToken()}` }; // Get the realm data for the realm name and connected realm ID @@ -38,22 +35,46 @@ const postRealmStatus = async (slug: string) => { { headers }, ).then((response) => response.json()); - if ( - realmStatuses[slug] // Do nothing on the first request - && realmStatuses[slug] !== connectedRealm.status.type - ) { - let message = REALM_STATUS_MESSAGES.UP; - if (connectedRealm.status.type !== REALM_STATUS_UP) { - message = REALM_STATUS_MESSAGES.DOWN; - } + return { realm, connectedRealm }; +}; - message = message.replace('{REALM_NAME}', realm.name.en_US) - .replace('{REALM_STATUS}', connectedRealm.status.name.en_US.toLowerCase()); +/** + * Handler for posting realm statuses message to Discord + */ +const postRealmStatuses = async () => { + const slugs = String(REALM_SLUGS || /* istanbul ignore next */ REALM_SLUG).split(','); - postDiscordChannelMessage(message); + /* istanbul ignore if */ + if (NODE_ENV !== 'test') { + console.log('Posting realm status message to Discord for:', ...slugs); } - realmStatuses[slug] = connectedRealm.status.type; + const messages: Array = []; + + const results = await Promise.all(slugs.map(getRealmData)); + + results.forEach(({ realm, connectedRealm }) => { + if ( + realmStatuses[realm.slug] // Do nothing on the first request + && realmStatuses[realm.slug] !== connectedRealm.status.type + ) { + let message = REALM_STATUS_MESSAGES.UP; + if (connectedRealm.status.type !== REALM_STATUS_UP) { + message = REALM_STATUS_MESSAGES.DOWN; + } + + message = message.replace('{REALM_NAME}', realm.name.en_US) + .replace('{REALM_STATUS}', connectedRealm.status.name.en_US.toLowerCase()); + + messages.push(message); + } + + realmStatuses[realm.slug] = connectedRealm.status.type; + }); + + if (messages.length > 0) { + postDiscordChannelMessage(messages.join('\n')); + } }; -export default postRealmStatus; +export default postRealmStatuses;