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 all 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
103 changes: 102 additions & 1 deletion src/handlers/scheduledEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ 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,
OrphanTasksStatusUpdateResponseType,
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 +101,97 @@ 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');
};

export async function filterOrphanTasks(env: env) {
const namespace = env[NAMESPACE_NAME] as unknown as KVNamespace;
let lastOrphanTasksFilterationTimestamp: string | null = '0'; // O means it will take the oldest unix timestamp
try {
lastOrphanTasksFilterationTimestamp = await namespace.get('ORPHAN_TASKS_UPDATED_TIME');

if (!lastOrphanTasksFilterationTimestamp) {
console.log(`Empty KV ORPHAN_TASKS_UPDATED_TIME: ${lastOrphanTasksFilterationTimestamp}`);
lastOrphanTasksFilterationTimestamp = '0'; // O means it will take the oldest unix timestamp
}
} catch (err) {
console.error(err, 'Error while fetching the timestamp of last orphan tasks filteration');
throw err;
}

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 response = await fetch(`${url}/tasks/orphanTasks`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
lastOrphanTasksFilterationTimestamp,
}),
});
if (!response.ok) {
throw new Error('Error while trying to update status of orphan tasks to backlog');
}

const data: OrphanTasksStatusUpdateResponseType = await response.json();

try {
await namespace.put('ORPHAN_TASKS_UPDATED_TIME', Date.now().toString());
} catch (err) {
console.error('Error while trying to update the last orphan tasks filteration timestamp');
}

return data;
}
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');
});
});
18 changes: 18 additions & 0 deletions src/types/global.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ export type NicknameUpdateResponseType = {
unsuccessfulNicknameUpdates: number;
};
};

export type OrphanTasksStatusUpdateResponseType = {
message: string;
data: {
orphanTasksUpdatedCount: number;
};
};
export type DiscordUsersResponse = {
message: string;
data: DiscordUserIdList;
Expand All @@ -39,3 +46,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>;
};
};
Loading
Loading