From 176690a2bafd11c3db1d638fabd7a7f6520112f6 Mon Sep 17 00:00:00 2001 From: Ajeyakrishna <98796547+Ajeyakrishna-k@users.noreply.github.com> Date: Fri, 15 Dec 2023 20:46:30 +0530 Subject: [PATCH] Adds cron job handler functions (#26) * fix: add npm test * feat: mocks jwt sign function * fix: lint issue * feat: add pull request template * feat: adds config and util functions * feat: adds cron tab and handler functions * chore: remove old code * fix: update cursor flow in get missed updates api * feat: update rds backend api response type --- src/handlers/scheduledEventHandler.ts | 39 +++++++++-- src/services/discordBotServices.ts | 27 ++++++++ src/services/rdsBackendService.ts | 32 +++++++++ src/tests/fixtures/missedRoleHandler.ts | 38 +++++++++++ src/tests/handlers/missedRoleHandler.test.ts | 67 +++++++++++++++++++ src/tests/services/discordBotService.test.ts | 49 ++++++++++++++ src/tests/services/rdsBackendService.test.ts | 68 ++++++++++++++++++++ src/types/global.types.ts | 5 +- src/worker.ts | 6 +- 9 files changed, 325 insertions(+), 6 deletions(-) create mode 100644 src/services/discordBotServices.ts create mode 100644 src/services/rdsBackendService.ts create mode 100644 src/tests/fixtures/missedRoleHandler.ts create mode 100644 src/tests/handlers/missedRoleHandler.test.ts create mode 100644 src/tests/services/discordBotService.test.ts create mode 100644 src/tests/services/rdsBackendService.test.ts diff --git a/src/handlers/scheduledEventHandler.ts b/src/handlers/scheduledEventHandler.ts index 45dcc2e..e5d81c8 100644 --- a/src/handlers/scheduledEventHandler.ts +++ b/src/handlers/scheduledEventHandler.ts @@ -2,7 +2,10 @@ import { KVNamespace } from '@cloudflare/workers-types'; import config from '../config/config'; import { NAMESPACE_NAME } from '../constants'; -import { env, NicknameUpdateResponseType } from '../types/global.types'; +import { updateUserRoles } from '../services/discordBotServices'; +import { getMissedUpdatesUsers } from '../services/rdsBackendService'; +import { DiscordUserRole, env, NicknameUpdateResponseType } from '../types/global.types'; +import { chunks } from '../utils/arrayUtils'; import { generateJwt } from '../utils/generateJwt'; export async function ping(env: env) { @@ -57,8 +60,36 @@ export async function callDiscordNicknameBatchUpdate(env: env) { console.error('Error while trying to update the last nickname change timestamp'); } } - - console.log(data); - return data; } + +export const addMissedUpdatesRole = async (env: env) => { + const MAX_ROLE_UPDATE = 25; + try { + let cursor: string | undefined = undefined; + for (let index = MAX_ROLE_UPDATE; index > 0; index--) { + if (index < MAX_ROLE_UPDATE && !cursor) break; + + const missedUpdatesUsers = await getMissedUpdatesUsers(env, cursor); + + if (!!missedUpdatesUsers && missedUpdatesUsers.usersToAddRole?.length > 1) { + const discordUserIdRoleIdList: DiscordUserRole[] = missedUpdatesUsers.usersToAddRole.map((userId) => ({ + userid: userId, + roleid: config(env).MISSED_UPDATES_ROLE_ID, + })); + + const discordUserRoleChunks = chunks(discordUserIdRoleIdList, MAX_ROLE_UPDATE); + for (const discordUserRoleList of discordUserRoleChunks) { + try { + await updateUserRoles(env, discordUserRoleList); + } catch (error) { + console.error('Error occurred while updating discord users', error); + } + } + } + cursor = missedUpdatesUsers?.cursor; + } + } catch (err) { + console.error('Error while adding missed updates roles'); + } +}; diff --git a/src/services/discordBotServices.ts b/src/services/discordBotServices.ts new file mode 100644 index 0000000..5550262 --- /dev/null +++ b/src/services/discordBotServices.ts @@ -0,0 +1,27 @@ +import config from '../config/config'; +import { DiscordRoleUpdatedList, DiscordUserRole, env } from '../types/global.types'; +import { generateDiscordBotJwt } from '../utils/generateJwt'; + +export const updateUserRoles = async (env: env, payload: DiscordUserRole[]): Promise => { + try { + const url = config(env).DISCORD_BOT_API_URL; + const token = await generateDiscordBotJwt(env); + + const response = await env.DISCORD_BOT.fetch(`${url}/roles?action=add-role`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + if (!response.ok) { + throw Error(`Role Update failed with status: ${response.status}`); + } + const data: DiscordRoleUpdatedList = await response.json(); + return data; + } catch (error) { + console.error('Error while updating discord user roles'); + throw error; + } +}; diff --git a/src/services/rdsBackendService.ts b/src/services/rdsBackendService.ts new file mode 100644 index 0000000..695b0f8 --- /dev/null +++ b/src/services/rdsBackendService.ts @@ -0,0 +1,32 @@ +import config from '../config/config'; +import { DiscordUsersResponse, env } from '../types/global.types'; +import { generateJwt } from '../utils/generateJwt'; + +export const getMissedUpdatesUsers = async (env: env, cursor: string | undefined) => { + try { + const baseUrl = config(env).RDS_BASE_API_URL; + + const url = new URL(`${baseUrl}/tasks/users/discord`); + url.searchParams.append('q', 'status:missed-updates'); + if (cursor) { + url.searchParams.append('cursor', cursor); + } + const token = await generateJwt(env); + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + if (!response.ok) { + throw new Error(`Fetch call to get user discord details failed with status: ${response.status}`); + } + + const responseData: DiscordUsersResponse = await response.json(); + return responseData?.data; + } catch (error) { + console.error('Error occurrent while fetching discord user details'); + throw error; + } +}; diff --git a/src/tests/fixtures/missedRoleHandler.ts b/src/tests/fixtures/missedRoleHandler.ts new file mode 100644 index 0000000..aba2b53 --- /dev/null +++ b/src/tests/fixtures/missedRoleHandler.ts @@ -0,0 +1,38 @@ +export const missedUpdatesUsersResponse = { + message: 'Discord details of users with status missed updates fetched successfully', + data: {}, +}; +export const missedUpdatesUsersMock = { + usersToAddRole: ['user1', 'user2'], + tasks: 10, + missedUpdatesTasks: 5, + cursor: 'some-cursor', +}; +export const missedUpdatesUsersMockWithNoUsers = { + usersToAddRole: [], + tasks: 10, + missedUpdatesTasks: 5, +}; +export const missedUpdatesUsersMockWithoutCursor = { + usersToAddRole: ['user1', 'user2'], + tasks: 10, + missedUpdatesTasks: 5, +}; + +export const updateRolesResponseMock = { + userid: 'user1', + roleid: '1', + success: true, +}; +export const discordUserRoleMock = [ + { userid: 'user1', roleid: '1' }, + { userid: 'user2', roleid: '2' }, +]; + +export const discordRoleUpdateResult = [ + { + userid: '1', + roleid: '2', + success: true, + }, +]; diff --git a/src/tests/handlers/missedRoleHandler.test.ts b/src/tests/handlers/missedRoleHandler.test.ts new file mode 100644 index 0000000..db8f233 --- /dev/null +++ b/src/tests/handlers/missedRoleHandler.test.ts @@ -0,0 +1,67 @@ +import { addMissedUpdatesRole } from '../../handlers/scheduledEventHandler'; +import { updateUserRoles } from '../../services/discordBotServices'; +import { getMissedUpdatesUsers } from '../../services/rdsBackendService'; +import { + missedUpdatesUsersMock, + missedUpdatesUsersMockWithNoUsers, + missedUpdatesUsersMockWithoutCursor, +} from '../fixtures/missedRoleHandler'; + +jest.mock('.../../../../services/rdsBackendService', () => ({ + getMissedUpdatesUsers: jest.fn(), +})); +jest.mock('.../../../../services/discordBotServices', () => ({ + updateUserRoles: jest.fn(), +})); +describe('addMissedUpdatesRole', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should call getMissedUpdatesUsers and updateUserRoles when there are users to add role', async () => { + (getMissedUpdatesUsers as jest.Mock) + .mockResolvedValueOnce(missedUpdatesUsersMock) + .mockResolvedValueOnce(missedUpdatesUsersMockWithoutCursor); + await addMissedUpdatesRole({}); + expect(getMissedUpdatesUsers).toHaveBeenCalledTimes(2); + expect(updateUserRoles).toHaveBeenCalledTimes(2); + }); + + it('should not call updateUserRoles when there are no users to add role', async () => { + (getMissedUpdatesUsers as jest.Mock).mockResolvedValueOnce(missedUpdatesUsersMockWithNoUsers); + + await addMissedUpdatesRole({}); + expect(getMissedUpdatesUsers).toHaveBeenCalledTimes(1); + expect(updateUserRoles).toHaveBeenCalledTimes(0); + }); + + it('should create chunks of userId and update roles multiple times when count is greater than 25', async () => { + const mockValue: any = { ...missedUpdatesUsersMockWithoutCursor, usersToAddRole: new Array(75).fill('id') }; + (getMissedUpdatesUsers as jest.Mock).mockResolvedValueOnce(mockValue); + + await addMissedUpdatesRole({}); + expect(getMissedUpdatesUsers).toHaveBeenCalledTimes(1); + expect(updateUserRoles).toHaveBeenCalledTimes(3); + }); + + it('should handle errors', async () => { + (getMissedUpdatesUsers as jest.Mock).mockRejectedValueOnce(new Error('Error fetching missed updates users')); + const consoleSpy = jest.spyOn(console, 'error'); + await addMissedUpdatesRole({}); + expect(consoleSpy).toHaveBeenCalledWith('Error while adding missed updates roles'); + }); + + it('should continue updating user roles even when a call fails', async () => { + (updateUserRoles as jest.Mock).mockRejectedValueOnce(new Error('Error occurred')); + const consoleSpy = jest.spyOn(console, 'error'); + const mockValue: any = { ...missedUpdatesUsersMockWithoutCursor, usersToAddRole: new Array(75).fill('id') }; + (getMissedUpdatesUsers as jest.Mock).mockResolvedValueOnce(mockValue); + await addMissedUpdatesRole({}); + expect(getMissedUpdatesUsers).toHaveBeenCalledTimes(1); + expect(consoleSpy).toHaveBeenCalledTimes(1); + expect(updateUserRoles).toHaveBeenCalledTimes(3); + }); +}); diff --git a/src/tests/services/discordBotService.test.ts b/src/tests/services/discordBotService.test.ts new file mode 100644 index 0000000..27251b7 --- /dev/null +++ b/src/tests/services/discordBotService.test.ts @@ -0,0 +1,49 @@ +import config from '../../config/config'; +import { updateUserRoles } from '../../services/discordBotServices'; +import { discordRoleUpdateResult, discordUserRoleMock } from '../fixtures/missedRoleHandler'; + +jest.mock('../../utils/generateJwt', () => ({ + generateDiscordBotJwt: jest.fn().mockResolvedValueOnce('mocked-jwt-token'), +})); +describe('discordBotService', () => { + describe('updateUserRoles', () => { + let fetchSpy: jest.Mock; + beforeEach(() => { + fetchSpy = jest.fn(); + jest.clearAllMocks(); + }); + + it('should make a successful API call and return the expected data', async () => { + fetchSpy.mockResolvedValueOnce({ + ok: true, + status: 200, + json: jest.fn().mockResolvedValueOnce(discordRoleUpdateResult), + } as unknown as Response); + const result = await updateUserRoles({ DISCORD_BOT: { fetch: fetchSpy } }, discordUserRoleMock); + + expect(fetchSpy).toHaveBeenCalledWith(`${config({}).DISCORD_BOT_API_URL}/roles?action=add-role`, { + method: 'POST', + headers: { + Authorization: 'Bearer mocked-jwt-token', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(discordUserRoleMock), + }); + expect(result).toEqual([...discordRoleUpdateResult]); + }); + + it('should throw error when api call fails', async () => { + fetchSpy.mockResolvedValueOnce({ + ok: false, + status: 400, + } as unknown as Response); + await expect(updateUserRoles({ DISCORD_BOT: { fetch: fetchSpy } }, [])).rejects.toThrow('Role Update failed with status: 400'); + }); + it('should handle unknown errors', async () => { + const consoleSpy = jest.spyOn(console, 'error'); + fetchSpy.mockRejectedValueOnce(new Error('Error occurred')); + await expect(updateUserRoles({ DISCORD_BOT: { fetch: fetchSpy } }, [])).rejects.toThrow('Error occurred'); + expect(consoleSpy).toHaveBeenCalledWith('Error while updating discord user roles'); + }); + }); +}); diff --git a/src/tests/services/rdsBackendService.test.ts b/src/tests/services/rdsBackendService.test.ts new file mode 100644 index 0000000..cbf1a28 --- /dev/null +++ b/src/tests/services/rdsBackendService.test.ts @@ -0,0 +1,68 @@ +import config from '../../config/config'; +import { getMissedUpdatesUsers } from '../../services/rdsBackendService'; +import { missedUpdatesUsersMock, missedUpdatesUsersResponse } from '../fixtures/missedRoleHandler'; + +jest.mock('../../utils/generateJwt', () => ({ + generateJwt: jest.fn().mockResolvedValue('mocked-jwt-token'), +})); + +describe('rdsBackendService', () => { + describe('updateUserRoles', () => { + let cursor: undefined | string; + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should make a successful API call and return the expected data', async () => { + jest.spyOn(global, 'fetch').mockResolvedValueOnce({ + ok: true, + status: 200, + json: jest.fn().mockResolvedValueOnce({ ...missedUpdatesUsersResponse, data: missedUpdatesUsersMock }), + } as unknown as Response); + const result = await getMissedUpdatesUsers({}, cursor); + const url = new URL(`${config({}).RDS_BASE_API_URL}/tasks/users/discord`); + url.searchParams.append('q', 'status:missed-updates'); + expect(fetch).toHaveBeenCalledWith(url, { + method: 'GET', + headers: { + Authorization: 'Bearer mocked-jwt-token', + 'Content-Type': 'application/json', + }, + }); + expect(result).toEqual({ ...missedUpdatesUsersMock }); + }); + it('should make a successful API call with cursor', async () => { + jest.spyOn(global, 'fetch').mockResolvedValueOnce({ + ok: true, + status: 200, + json: jest.fn().mockResolvedValueOnce({ ...missedUpdatesUsersResponse, data: missedUpdatesUsersMock }), + } as unknown as Response); + const result = await getMissedUpdatesUsers({}, 'cursorValue'); + const url = new URL(`${config({}).RDS_BASE_API_URL}/tasks/users/discord`); + url.searchParams.append('q', 'status:missed-updates'); + url.searchParams.append('cursor', 'cursorValue'); + expect(fetch).toHaveBeenCalledWith(url, { + method: 'GET', + headers: { + Authorization: 'Bearer mocked-jwt-token', + 'Content-Type': 'application/json', + }, + }); + expect(result).toEqual({ ...missedUpdatesUsersMock }); + }); + it('should throw error when api call fails', async () => { + jest.spyOn(global, 'fetch').mockResolvedValueOnce({ + ok: false, + status: 400, + } as unknown as Response); + await expect(getMissedUpdatesUsers({}, cursor)).rejects.toThrow('Fetch call to get user discord details failed with status: 400'); + }); + + it('should handle unknown errors', async () => { + const consoleSpy = jest.spyOn(console, 'error'); + jest.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Error occurred')); + await expect(getMissedUpdatesUsers({}, cursor)).rejects.toThrow('Error occurred'); + expect(consoleSpy).toHaveBeenCalledWith('Error occurrent while fetching discord user details'); + }); + }); +}); diff --git a/src/types/global.types.ts b/src/types/global.types.ts index 6652346..ac46191 100644 --- a/src/types/global.types.ts +++ b/src/types/global.types.ts @@ -19,7 +19,10 @@ export type NicknameUpdateResponseType = { unsuccessfulNicknameUpdates: number; }; }; - +export type DiscordUsersResponse = { + message: string; + data: DiscordUserIdList; +}; export type DiscordUserIdList = { usersToAddRole: string[]; tasks: number; diff --git a/src/worker.ts b/src/worker.ts index 219fd47..c1daf9a 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -1,8 +1,9 @@ -import { callDiscordNicknameBatchUpdate, ping } from './handlers/scheduledEventHandler'; +import { addMissedUpdatesRole, callDiscordNicknameBatchUpdate, ping } from './handlers/scheduledEventHandler'; import { env } from './types/global.types'; const EVERY_4_HOURS = '0 */4 * * *'; const EVERY_6_HOURS = '0 */6 * * *'; +const EVERY_12_HOURS = '0 */12 * * *'; export default { async scheduled(req: ScheduledController, env: env, ctx: ExecutionContext) { @@ -12,6 +13,9 @@ export default { break; case EVERY_6_HOURS: return await callDiscordNicknameBatchUpdate(env); + case EVERY_12_HOURS: { + return await addMissedUpdatesRole(env); + } default: console.error('Unknown Trigger Value!'); }