Skip to content

Commit

Permalink
impleted worker to trigger api calls
Browse files Browse the repository at this point in the history
  • Loading branch information
Pavangbhat committed Mar 4, 2024
1 parent 095fd01 commit 75187b2
Show file tree
Hide file tree
Showing 6 changed files with 321 additions and 3 deletions.
50 changes: 49 additions & 1 deletion src/handlers/scheduledEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<any | null> => {
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');
};
128 changes: 128 additions & 0 deletions src/tests/handlers/scheduledEventHandler.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
70 changes: 70 additions & 0 deletions src/tests/utils/apiCaller.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
11 changes: 11 additions & 0 deletions src/types/global.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>;
users: Array<unknown>;
};
};
36 changes: 36 additions & 0 deletions src/utils/apiCaller.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>,
): Promise<Record<string, any>> => {
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;
}
};
29 changes: 27 additions & 2 deletions src/worker.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down

0 comments on commit 75187b2

Please sign in to comment.