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 #76

Merged
merged 6 commits into from
Sep 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
958 changes: 232 additions & 726 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"ts-jest": "29.1.1",
"ts-node": "10.9.1",
"typescript": "5.1.6",
"wrangler": "3.7.0"
"wrangler": "3.19.0"
},
"dependencies": {
"@tsndr/cloudflare-worker-jwt": "2.2.1"
Expand Down
116 changes: 18 additions & 98 deletions src/handlers/scheduledEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +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,
OrphanTasksStatusUpdateResponseType,
UserStatusResponse,
} from '../types/global.types';
import { apiCaller } from '../utils/apiCaller';
import { DiscordUserRole, env, NicknameUpdateResponseType } from '../types/global.types';
import { fireAndForgetApiCall } from '../utils/apiCaller';
import { chunks } from '../utils/arrayUtils';
import { generateJwt } from '../utils/generateJwt';

Expand All @@ -21,7 +15,7 @@ export async function ping(env: env) {
return response;
}

export async function callDiscordNicknameBatchUpdate(env: env) {
export async function callDiscordNicknameBatchUpdateHandler(env: env) {
const namespace = env[NAMESPACE_NAME] as unknown as KVNamespace;
let lastNicknameUpdate: string | null = '0';
try {
Expand Down Expand Up @@ -70,7 +64,7 @@ export async function callDiscordNicknameBatchUpdate(env: env) {
return data;
}

export const addMissedUpdatesRole = async (env: env) => {
export const addMissedUpdatesRoleHandler = async (env: env) => {
const MAX_ROLE_UPDATE = 25;
try {
let cursor: string | undefined = undefined;
Expand Down Expand Up @@ -102,96 +96,22 @@ export const addMissedUpdatesRole = async (env: env) => {
}
};

export const syncUsersStatus = async (env: env): Promise<any | null> => {
await apiCaller(env, 'users/status/update', 'PATCH');
export const syncApiHandler = async (env: env) => {
const handlers = [
fireAndForgetApiCall(env, 'users/status/sync', 'PATCH'),
fireAndForgetApiCall(env, 'external-accounts/users?action=discord-users-sync', 'POST'),
fireAndForgetApiCall(env, 'users', 'POST'),
fireAndForgetApiCall(env, 'discord-actions/nicknames/sync?dev=true', 'POST'),
fireAndForgetApiCall(env, 'discord-actions/group-idle-7d', 'PUT'),
fireAndForgetApiCall(env, 'discord-actions/group-onboarding-31d-plus', 'PUT'),
];

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;
await Promise.all(handlers);
console.log(
`Worker for syncing idle users, nicknames, idle 7d users, and onboarding 31d+ users has completed. Worker for syncing user status, external accounts, and unverified users has completed.`,
);
} catch (error) {
console.error('Error during syncUsersStatus:', error);
return null;
console.error('Error occurred during Sync API calls:', error);
}
};

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;
}
16 changes: 8 additions & 8 deletions src/tests/handlers/missedRoleHandler.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { addMissedUpdatesRole } from '../../handlers/scheduledEventHandler';
import { addMissedUpdatesRoleHandler } from '../../handlers/scheduledEventHandler';
import { updateUserRoles } from '../../services/discordBotServices';
import { getMissedUpdatesUsers } from '../../services/rdsBackendService';
import {
Expand All @@ -13,7 +13,7 @@ jest.mock('.../../../../services/rdsBackendService', () => ({
jest.mock('.../../../../services/discordBotServices', () => ({
updateUserRoles: jest.fn(),
}));
describe('addMissedUpdatesRole', () => {
describe('addMissedUpdatesRoleHandler', () => {
beforeEach(() => {
jest.resetAllMocks();
});
Expand All @@ -25,7 +25,7 @@ describe('addMissedUpdatesRole', () => {
(getMissedUpdatesUsers as jest.Mock)
.mockResolvedValueOnce(missedUpdatesUsersMock)
.mockResolvedValueOnce(missedUpdatesUsersMockWithoutCursor);
await addMissedUpdatesRole({});
await addMissedUpdatesRoleHandler({});
expect(getMissedUpdatesUsers).toHaveBeenCalledTimes(2);
expect(updateUserRoles).toHaveBeenCalledTimes(2);
});
Expand All @@ -34,15 +34,15 @@ describe('addMissedUpdatesRole', () => {
const usersMockData = { ...missedUpdatesUsersMockWithoutCursor };
usersMockData.usersToAddRole = usersMockData.usersToAddRole.slice(0, 1);
(getMissedUpdatesUsers as jest.Mock).mockResolvedValueOnce(usersMockData);
await addMissedUpdatesRole({});
await addMissedUpdatesRoleHandler({});
expect(getMissedUpdatesUsers).toHaveBeenCalledTimes(1);
expect(updateUserRoles).toHaveBeenCalledTimes(1);
});

it('should not call updateUserRoles when there are no users to add role', async () => {
(getMissedUpdatesUsers as jest.Mock).mockResolvedValueOnce(missedUpdatesUsersMockWithNoUsers);

await addMissedUpdatesRole({});
await addMissedUpdatesRoleHandler({});
expect(getMissedUpdatesUsers).toHaveBeenCalledTimes(1);
expect(updateUserRoles).toHaveBeenCalledTimes(0);
});
Expand All @@ -51,15 +51,15 @@ describe('addMissedUpdatesRole', () => {
const mockValue: any = { ...missedUpdatesUsersMockWithoutCursor, usersToAddRole: new Array(75).fill('id') };
(getMissedUpdatesUsers as jest.Mock).mockResolvedValueOnce(mockValue);

await addMissedUpdatesRole({});
await addMissedUpdatesRoleHandler({});
expect(getMissedUpdatesUsers).toHaveBeenCalledTimes(1);
expect(updateUserRoles).toHaveBeenCalledTimes(3);
});

it('should handle errors', async () => {
(getMissedUpdatesUsers as jest.Mock).mockRejectedValueOnce(new Error('Error fetching missed updates users'));
const consoleSpy = jest.spyOn(console, 'error');
await addMissedUpdatesRole({});
await addMissedUpdatesRoleHandler({});
expect(consoleSpy).toHaveBeenCalledWith('Error while adding missed updates roles');
});

Expand All @@ -68,7 +68,7 @@ describe('addMissedUpdatesRole', () => {
const consoleSpy = jest.spyOn(console, 'error');
const mockValue: any = { ...missedUpdatesUsersMockWithoutCursor, usersToAddRole: new Array(75).fill('id') };
(getMissedUpdatesUsers as jest.Mock).mockResolvedValueOnce(mockValue);
await addMissedUpdatesRole({});
await addMissedUpdatesRoleHandler({});
expect(getMissedUpdatesUsers).toHaveBeenCalledTimes(1);
expect(consoleSpy).toHaveBeenCalledTimes(1);
expect(updateUserRoles).toHaveBeenCalledTimes(3);
Expand Down
124 changes: 19 additions & 105 deletions src/tests/handlers/scheduledEventHandler.test.ts
Original file line number Diff line number Diff line change
@@ -1,132 +1,46 @@
import {
syncExternalAccounts,
syncIdle7dUsers,
syncIdleUsers,
syncNickNames,
syncOnboarding31dPlusUsers,
syncUnverifiedUsers,
syncUsersStatus,
} from '../../handlers/scheduledEventHandler';
import { syncApiHandler } 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;
const fireAndForgetApiCallMock = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(apiCallerModule, 'fireAndForgetApiCall').mockImplementation(fireAndForgetApiCallMock);
});

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);
it('should call all sync functions', async () => {
await syncApiHandler(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');
expect(apiCallerModule.fireAndForgetApiCall).toHaveBeenCalledWith(mockEnv, 'users/status/sync', 'PATCH');
expect(apiCallerModule.fireAndForgetApiCall).toHaveBeenCalledWith(mockEnv, 'external-accounts/users?action=discord-users-sync', 'POST');
expect(apiCallerModule.fireAndForgetApiCall).toHaveBeenCalledWith(mockEnv, 'users', 'POST');
expect(apiCallerModule.fireAndForgetApiCall).toHaveBeenCalledWith(mockEnv, 'discord-actions/nicknames/sync?dev=true', 'POST');
expect(apiCallerModule.fireAndForgetApiCall).toHaveBeenCalledWith(mockEnv, 'discord-actions/group-idle-7d', 'PUT');
expect(apiCallerModule.fireAndForgetApiCall).toHaveBeenCalledWith(mockEnv, 'discord-actions/group-onboarding-31d-plus', 'PUT');
expect(apiCallerModule.fireAndForgetApiCall).toHaveBeenCalledTimes(6);
});

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 catch errors during API calls', async () => {
const mockError = new Error('API error');
(apiCallerModule.fireAndForgetApiCall as jest.MockedFunction<typeof apiCallerModule.fireAndForgetApiCall>).mockRejectedValueOnce(
mockError,
);

it('should sync idle 7d users', async () => {
await testSyncFunction(syncIdle7dUsers, 'discord-actions/group-idle-7d', 'PUT');
});
await syncApiHandler(mockEnv);

it('should sync onboarding 31d+ users', async () => {
await testSyncFunction(syncOnboarding31dPlusUsers, 'discord-actions/group-onboarding-31d-plus', 'PUT');
expect(console.error).toHaveBeenCalledWith('Error occurred during Sync API calls:', mockError);
});
});
Loading
Loading