Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev to main sync #63

Merged
merged 7 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading