Skip to content

Commit

Permalink
Implemented a worker to trigger api calls for sync buttons of dashboa…
Browse files Browse the repository at this point in the history
…rd site (#62)

* impleted worker to trigger api calls

* starting using default env

* fixed Cannot find name global

* added missing coverage

* covered token creation failure case

* fixed redundent PATCH

* divided sync actions into two wrokers

* commented syncNickNames API
  • Loading branch information
Pavangbhat authored Mar 17, 2024
1 parent 478bce1 commit f968f34
Show file tree
Hide file tree
Showing 8 changed files with 343 additions and 8 deletions.
49 changes: 48 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,49 @@ 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', {
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');
};
132 changes: 132 additions & 0 deletions src/tests/handlers/scheduledEventHandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {
syncExternalAccounts,
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: 'default',
},
};

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', {
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 external accounts', async () => {
await testSyncFunction(syncExternalAccounts, 'external-accounts/users?action=discord-users-sync', 'POST');
});

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');
});
});
8 changes: 4 additions & 4 deletions src/tests/services/rdsBackendService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('rdsBackendService', () => {
});

it('should make a successful API call and return the expected data', async () => {
jest.spyOn(global, 'fetch').mockResolvedValueOnce({
jest.spyOn(globalThis as any, 'fetch').mockResolvedValueOnce({
ok: true,
status: 200,
json: jest.fn().mockResolvedValueOnce({ ...missedUpdatesUsersResponse, data: missedUpdatesUsersMock }),
Expand All @@ -32,7 +32,7 @@ describe('rdsBackendService', () => {
expect(result).toEqual({ ...missedUpdatesUsersMock });
});
it('should make a successful API call with cursor', async () => {
jest.spyOn(global, 'fetch').mockResolvedValueOnce({
jest.spyOn(globalThis as any, 'fetch').mockResolvedValueOnce({
ok: true,
status: 200,
json: jest.fn().mockResolvedValueOnce({ ...missedUpdatesUsersResponse, data: missedUpdatesUsersMock }),
Expand All @@ -51,7 +51,7 @@ describe('rdsBackendService', () => {
expect(result).toEqual({ ...missedUpdatesUsersMock });
});
it('should throw error when api call fails', async () => {
jest.spyOn(global, 'fetch').mockResolvedValueOnce({
jest.spyOn(globalThis as any, 'fetch').mockResolvedValueOnce({
ok: false,
status: 400,
} as unknown as Response);
Expand All @@ -60,7 +60,7 @@ describe('rdsBackendService', () => {

it('should handle unknown errors', async () => {
const consoleSpy = jest.spyOn(console, 'error');
jest.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Error occurred'));
jest.spyOn(globalThis as any, 'fetch').mockRejectedValueOnce(new Error('Error occurred'));
await expect(getMissedUpdatesUsers({}, cursor)).rejects.toThrow('Error occurred');
expect(consoleSpy).toHaveBeenCalledWith('Error occurred while fetching discord user details');
});
Expand Down
78 changes: 78 additions & 0 deletions src/tests/utils/apiCaller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { RDS_BASE_DEVELOPMENT_API_URL } from '../../constants/urls';
import { env } from '../../types/global.types';
import { apiCaller } from '../../utils/apiCaller';
import { generateJwt } from '../../utils/generateJwt';
import * as generateJwtModule from '../../utils/generateJwt';

jest.mock('../../utils/generateJwt', () => ({
generateJwt: jest.fn().mockResolvedValue('mocked-token'),
}));

describe('apiCaller', () => {
const mockEnv: env = {
CURRENT_ENVIRONMENT: {
RDS_BASE_API_URL: 'default',
},
};

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_DEVELOPMENT_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_DEVELOPMENT_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();
});

it('should handle the case where generateJwt returns undefined and throw an error', async () => {
const generateJwtMock = jest.spyOn(generateJwtModule, 'generateJwt');
generateJwtMock.mockImplementationOnce(() => Promise.reject(new Error('Generate JWT error')));

await expect(apiCaller(mockEnv, 'someEndpoint', 'GET')).rejects.toThrow('Generate JWT error');
});
});
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;
}
};
Loading

0 comments on commit f968f34

Please sign in to comment.