From 75187b2feb56d8c84116be71efe11cf1c1718724 Mon Sep 17 00:00:00 2001 From: pavangbhat Date: Tue, 5 Mar 2024 03:25:53 +0530 Subject: [PATCH] impleted worker to trigger api calls --- src/handlers/scheduledEventHandler.ts | 50 ++++++- .../handlers/scheduledEventHandler.test.ts | 128 ++++++++++++++++++ src/tests/utils/apiCaller.test.ts | 70 ++++++++++ src/types/global.types.ts | 11 ++ src/utils/apiCaller.ts | 36 +++++ src/worker.ts | 29 +++- 6 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 src/tests/handlers/scheduledEventHandler.test.ts create mode 100644 src/tests/utils/apiCaller.test.ts create mode 100644 src/utils/apiCaller.ts diff --git a/src/handlers/scheduledEventHandler.ts b/src/handlers/scheduledEventHandler.ts index 256cd26..7c427ed 100644 --- a/src/handlers/scheduledEventHandler.ts +++ b/src/handlers/scheduledEventHandler.ts @@ -4,7 +4,8 @@ import config from '../config/config'; import { NAMESPACE_NAME } from '../constants'; import { updateUserRoles } from '../services/discordBotServices'; import { getMissedUpdatesUsers } from '../services/rdsBackendService'; -import { DiscordUserRole, env, NicknameUpdateResponseType } from '../types/global.types'; +import { DiscordUserRole, env, NicknameUpdateResponseType, UserStatusResponse } from '../types/global.types'; +import { apiCaller } from '../utils/apiCaller'; import { chunks } from '../utils/arrayUtils'; import { generateJwt } from '../utils/generateJwt'; @@ -94,3 +95,50 @@ export const addMissedUpdatesRole = async (env: env) => { console.error('Error while adding missed updates roles'); } }; + +export const syncUsersStatus = async (env: env): Promise => { + await apiCaller(env, 'users/status/update', 'PATCH'); + + try { + const idleUsersData = (await apiCaller(env, 'users/status?aggregate=true', 'GET')) as UserStatusResponse | undefined; + + if (!idleUsersData?.data?.users || idleUsersData.data.users.length === 0) { + console.error('Error: Users data is not in the expected format or no users found'); + return null; + } + + const response = await apiCaller(env, 'users/status/batch', 'PATCH', { + method: 'PATCH', + body: JSON.stringify({ users: idleUsersData.data.users }), + }); + + return response; + } catch (error) { + console.error('Error during syncUsersStatus:', error); + return null; + } +}; + +export const syncExternalAccounts = async (env: env) => { + return await apiCaller(env, 'external-accounts/users?action=discord-users-sync', 'POST'); +}; + +export const syncUnverifiedUsers = async (env: env) => { + return await apiCaller(env, 'users', 'POST'); +}; + +export const syncIdleUsers = async (env: env) => { + return await apiCaller(env, 'discord-actions/group-idle', 'PUT'); +}; + +export const syncNickNames = async (env: env) => { + return await apiCaller(env, 'discord-actions/nicknames/sync?dev=true', 'POST'); +}; + +export const syncIdle7dUsers = async (env: env) => { + return await apiCaller(env, 'discord-actions/group-idle-7d', 'PUT'); +}; + +export const syncOnboarding31dPlusUsers = async (env: env) => { + return await apiCaller(env, 'discord-actions/group-onboarding-31d-plus', 'PUT'); +}; diff --git a/src/tests/handlers/scheduledEventHandler.test.ts b/src/tests/handlers/scheduledEventHandler.test.ts new file mode 100644 index 0000000..82a50b5 --- /dev/null +++ b/src/tests/handlers/scheduledEventHandler.test.ts @@ -0,0 +1,128 @@ +import { + syncIdle7dUsers, + syncIdleUsers, + syncNickNames, + syncOnboarding31dPlusUsers, + syncUnverifiedUsers, + syncUsersStatus, +} from '../../handlers/scheduledEventHandler'; +import { env } from '../../types/global.types'; +import * as apiCallerModule from '../../utils/apiCaller'; + +jest.mock('../../utils/apiCaller', () => ({ + apiCaller: jest.fn(), +})); + +const consoleErrorMock: jest.SpyInstance = jest.spyOn(console, 'error').mockImplementation(); +const apiCallerFunction = apiCallerModule.apiCaller; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +afterAll(() => { + consoleErrorMock.mockRestore(); +}); + +describe('syncUsersStatus', () => { + const mockEnv: env = { + CURRENT_ENVIRONMENT: { + RDS_BASE_API_URL: 'staging', + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should successfully sync users status', async () => { + (apiCallerFunction as jest.Mock).mockResolvedValueOnce(undefined); + (apiCallerFunction as jest.Mock).mockResolvedValueOnce({ + data: { + users: [{ userId: 'asdoiuahow212' }], + }, + }); + (apiCallerFunction as jest.Mock).mockResolvedValueOnce({ success: true }); + + await syncUsersStatus(mockEnv); + + expect(apiCallerFunction).toHaveBeenCalledWith(mockEnv, 'users/status/update', 'PATCH'); + expect(apiCallerFunction).toHaveBeenCalledWith(mockEnv, 'users/status?aggregate=true', 'GET'); + expect(apiCallerFunction).toHaveBeenCalledWith(mockEnv, 'users/status/batch', 'PATCH', { + method: 'PATCH', + body: JSON.stringify({ users: [{ userId: 'asdoiuahow212' }] }), + }); + expect(apiCallerFunction).toHaveBeenCalledTimes(3); + }); + + it('should handle error during users data retrieval', async () => { + (apiCallerFunction as jest.Mock).mockResolvedValueOnce(undefined); + (apiCallerFunction as jest.Mock).mockRejectedValueOnce(new Error('Error fetching users data')); + + const result = await syncUsersStatus(mockEnv); + + expect(result).toBeNull(); + + expect(apiCallerFunction).toHaveBeenCalledWith(mockEnv, 'users/status/update', 'PATCH'); + expect(apiCallerFunction).toHaveBeenCalledWith(mockEnv, 'users/status?aggregate=true', 'GET'); + expect(apiCallerFunction).toHaveBeenCalledTimes(2); + + expect(console.error).toHaveBeenCalledWith('Error during syncUsersStatus:', new Error('Error fetching users data')); + expect(console.error).toHaveBeenCalledTimes(1); + }); + + it('should log an error when no users are found or data is not in the expected format', async () => { + (apiCallerFunction as jest.Mock).mockResolvedValueOnce(undefined); + (apiCallerFunction as jest.Mock).mockResolvedValueOnce({ + data: { + users: [], + }, + }); + + const result = await syncUsersStatus(mockEnv); + + expect(result).toBeNull(); + + expect(apiCallerFunction).toHaveBeenCalledWith(mockEnv, 'users/status/update', 'PATCH'); + expect(apiCallerFunction).toHaveBeenCalledWith(mockEnv, 'users/status?aggregate=true', 'GET'); + expect(apiCallerFunction).toHaveBeenCalledTimes(2); + + expect(console.error).toHaveBeenCalledWith('Error: Users data is not in the expected format or no users found'); + expect(console.error).toHaveBeenCalledTimes(1); + }); +}); + +describe('sync apis', () => { + const mockEnv: env = { + CURRENT_ENVIRONMENT: { + RDS_BASE_API_URL: 'staging', + }, + }; + + const testSyncFunction = async (syncFunction: Function, endpoint: string, method: string) => { + await syncFunction(mockEnv); + + expect(apiCallerFunction).toHaveBeenCalledWith(mockEnv, endpoint, method); + expect(apiCallerFunction).toHaveBeenCalledTimes(1); + }; + + it('should sync unverified users', async () => { + await testSyncFunction(syncUnverifiedUsers, 'users', 'POST'); + }); + + it('should sync idle users', async () => { + await testSyncFunction(syncIdleUsers, 'discord-actions/group-idle', 'PUT'); + }); + + it('should sync nicknames', async () => { + await testSyncFunction(syncNickNames, 'discord-actions/nicknames/sync?dev=true', 'POST'); + }); + + it('should sync idle 7d users', async () => { + await testSyncFunction(syncIdle7dUsers, 'discord-actions/group-idle-7d', 'PUT'); + }); + + it('should sync onboarding 31d+ users', async () => { + await testSyncFunction(syncOnboarding31dPlusUsers, 'discord-actions/group-onboarding-31d-plus', 'PUT'); + }); +}); diff --git a/src/tests/utils/apiCaller.test.ts b/src/tests/utils/apiCaller.test.ts new file mode 100644 index 0000000..6999da1 --- /dev/null +++ b/src/tests/utils/apiCaller.test.ts @@ -0,0 +1,70 @@ +import { RDS_BASE_STAGING_API_URL } from '../../constants/urls'; +import { env } from '../../types/global.types'; +import { apiCaller } from '../../utils/apiCaller'; +import { generateJwt } from '../../utils/generateJwt'; + +jest.mock('../../utils/generateJwt', () => ({ + generateJwt: jest.fn().mockResolvedValue('mocked-token'), +})); + +describe('apiCaller', () => { + const mockEnv: env = { + CURRENT_ENVIRONMENT: { + RDS_BASE_API_URL: 'staging', + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + (globalThis as any).fetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ success: true }), + }), + ); + }); + + it('should make a successful API call', async () => { + const result = await apiCaller(mockEnv, 'users', 'GET'); + expect(generateJwt).toHaveBeenCalledWith(mockEnv); + + expect(result).toEqual({ success: true }); + expect((globalThis as any).fetch).toHaveBeenCalledWith(`${RDS_BASE_STAGING_API_URL}/users`, { + method: 'GET', + headers: { + Authorization: 'Bearer mocked-token', + 'Content-Type': 'application/json', + }, + }); + }); + + it('should make a successful POST API call', async () => { + const result = await apiCaller(mockEnv, 'test', 'POST', { + body: JSON.stringify({ data: 'example' }), + }); + expect(generateJwt).toHaveBeenCalledWith(mockEnv); + + expect(result).toEqual({ success: true }); + expect((globalThis as any).fetch).toHaveBeenCalledWith(`${RDS_BASE_STAGING_API_URL}/test`, { + method: 'POST', + headers: { + Authorization: 'Bearer mocked-token', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ data: 'example' }), + }); + }); + + it('should log and rethrow error during fetch call failure', async () => { + const mockError = new Error('Network error'); + + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + + (globalThis as any).fetch = jest.fn().mockRejectedValue(mockError); + + await expect(apiCaller({}, 'someEndpoint', 'GET')).rejects.toThrowError(mockError); + expect(consoleErrorSpy).toHaveBeenCalledWith(`Error during fetch operation: ${mockError}`); + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + + consoleErrorSpy.mockRestore(); + }); +}); diff --git a/src/types/global.types.ts b/src/types/global.types.ts index ac46191..98a30fa 100644 --- a/src/types/global.types.ts +++ b/src/types/global.types.ts @@ -39,3 +39,14 @@ export type DiscordRoleUpdatedList = { roleid: string; success: boolean; }; +export type UserStatusResponse = { + message: string; + data: { + totalUsers: number; + totalIdleUsers: number; + totalActiveUsers: number; + totalUnprocessedUsers: number; + unprocessedUsers: Array; + users: Array; + }; +}; diff --git a/src/utils/apiCaller.ts b/src/utils/apiCaller.ts new file mode 100644 index 0000000..79f6474 --- /dev/null +++ b/src/utils/apiCaller.ts @@ -0,0 +1,36 @@ +import config from '../config/config'; +import { env } from '../types/global.types'; +import { generateJwt } from './generateJwt'; + +export const apiCaller = async ( + env: env, + endpoint: string, + method: string, + options?: Record, +): Promise> => { + const url = config(env).RDS_BASE_API_URL; + let token; + try { + token = await generateJwt(env); + } catch (err) { + console.error(`Error while generating JWT token: ${err}`); + throw err; + } + + const defaultOptions = { + method, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }; + + try { + const response = await fetch(`${url}/${endpoint}`, { ...defaultOptions, ...options }); + return await response.json(); + } catch (error) { + // TODO: Handle these errors: log to newRelic or any other better approach + console.error(`Error during fetch operation: ${error}`); + throw error; + } +}; diff --git a/src/worker.ts b/src/worker.ts index 60bd74d..7401a41 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -1,8 +1,19 @@ -import { addMissedUpdatesRole, callDiscordNicknameBatchUpdate } from './handlers/scheduledEventHandler'; +import { + addMissedUpdatesRole, + callDiscordNicknameBatchUpdate, + syncExternalAccounts, + syncIdle7dUsers, + syncIdleUsers, + syncNickNames, + syncOnboarding31dPlusUsers, + syncUnverifiedUsers, + syncUsersStatus, +} from './handlers/scheduledEventHandler'; import { env } from './types/global.types'; const EVERY_6_HOURS = '0 */6 * * *'; const EVERY_11_HOURS = '0 */11 * * *'; +const EVERY_1_HOUR = '0 */1 * * *'; export default { // eslint-disable-next-line no-unused-vars @@ -14,8 +25,22 @@ export default { case EVERY_11_HOURS: { return await addMissedUpdatesRole(env); } - default: + + case EVERY_1_HOUR: { + await syncUsersStatus(env); + await syncExternalAccounts(env); + await syncUnverifiedUsers(env); + await syncIdleUsers(env); + await syncNickNames(env); + await syncIdle7dUsers(env); + await syncOnboarding31dPlusUsers(env); + break; + } + + default: { console.error('Unknown Trigger Value!'); + break; + } } }, // We need to keep all 3 parameters in this format even if they are not used as as cloudflare workers need them to be present So we are disabling eslint rule of no-unused-vars