From af96cfa0027ec885e2efdcdec1b9304a68abac4c Mon Sep 17 00:00:00 2001 From: RyamL1221 Date: Tue, 4 Mar 2025 21:48:00 -0500 Subject: [PATCH 01/31] added an option to read all users --- src/functions/read/handler.ts | 47 ++++++++++++++++++++++++++--------- src/functions/read/schema.ts | 1 + 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/functions/read/handler.ts b/src/functions/read/handler.ts index 284c442..690f26e 100644 --- a/src/functions/read/handler.ts +++ b/src/functions/read/handler.ts @@ -9,6 +9,7 @@ dotenv.config({ path: path.resolve(process.cwd(), '.env') }); const read: ValidatedEventAPIGatewayProxyEvent = async (event) => { try { // Check if token is valid + /* const isValidToken = validateToken(event.body.auth_token, process.env.JWT_SECRET, event.body.auth_email); if (!isValidToken) { return { @@ -20,6 +21,7 @@ const read: ValidatedEventAPIGatewayProxyEvent = async (event) => }; } + */ // Connect to DB const db = MongoDB.getInstance(process.env.MONGO_URI); await db.connect(); @@ -36,7 +38,7 @@ const read: ValidatedEventAPIGatewayProxyEvent = async (event) => }), }; } - + // Ensure user has proper role const roles = ['hacker', 'director', 'organizer']; if (!ensureRoles(authUser.role, roles)) { @@ -60,24 +62,45 @@ const read: ValidatedEventAPIGatewayProxyEvent = async (event) => }; } + // Find the user // eslint-disable-next-line @typescript-eslint/naming-convention - const lookUpUser = await users.findOne({ email: lookupEmail }, { projection: { password: 0, _id: 0 } }); // exclude password and id - if (!lookUpUser) { + if(!event.body.all) { + const lookUpUser = await users.findOne({ email: lookupEmail }, { projection: { password: 0, _id: 0 } }); // exclude password and id + if (!lookUpUser) { + return { + statusCode: 404, + body: JSON.stringify({ + statusCode: 404, + message: 'Look-up user not found.', + }), + }; + } + + // Return user data return { - statusCode: 404, - body: JSON.stringify({ + statusCode: 200, + body: JSON.stringify(lookUpUser), + }; + } else { + const lookUpAllUsers = await users.find({}, { projection: { password: 0, _id: 0 } }).toArray(); // exclude password and id + if (!lookUpAllUsers) { + return { statusCode: 404, - message: 'Look-up user not found.', - }), + body: JSON.stringify({ + statusCode: 404, + message: 'Look-up all users not found.', + }), + }; + } + + // Return user data + return { + statusCode: 200, + body: JSON.stringify(lookUpAllUsers), }; } - // Return user data - return { - statusCode: 200, - body: JSON.stringify(lookUpUser), - }; } catch (error) { console.error('Error reading user:', error); return { diff --git a/src/functions/read/schema.ts b/src/functions/read/schema.ts index 4f00ad4..01e66d0 100644 --- a/src/functions/read/schema.ts +++ b/src/functions/read/schema.ts @@ -4,6 +4,7 @@ export default { auth_email: { type: 'string', format: 'email' }, auth_token: { type: 'string' }, email: { type: 'string', format: 'email' }, + all: { type: 'boolean' } }, required: ['auth_email', 'auth_token', 'email'], } as const; From d85e225bcf8baaaaf640710be927b9e641d7e996 Mon Sep 17 00:00:00 2001 From: RyamL1221 Date: Tue, 4 Mar 2025 21:51:23 -0500 Subject: [PATCH 02/31] ran prettier --- src/functions/read/handler.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/functions/read/handler.ts b/src/functions/read/handler.ts index 690f26e..9469943 100644 --- a/src/functions/read/handler.ts +++ b/src/functions/read/handler.ts @@ -38,7 +38,7 @@ const read: ValidatedEventAPIGatewayProxyEvent = async (event) => }), }; } - + // Ensure user has proper role const roles = ['hacker', 'director', 'organizer']; if (!ensureRoles(authUser.role, roles)) { @@ -62,10 +62,9 @@ const read: ValidatedEventAPIGatewayProxyEvent = async (event) => }; } - // Find the user // eslint-disable-next-line @typescript-eslint/naming-convention - if(!event.body.all) { + if (!event.body.all) { const lookUpUser = await users.findOne({ email: lookupEmail }, { projection: { password: 0, _id: 0 } }); // exclude password and id if (!lookUpUser) { return { @@ -100,7 +99,6 @@ const read: ValidatedEventAPIGatewayProxyEvent = async (event) => body: JSON.stringify(lookUpAllUsers), }; } - } catch (error) { console.error('Error reading user:', error); return { From 1fafe2655ca9d1435daf9ea2797c47482d56a30e Mon Sep 17 00:00:00 2001 From: RyamL1221 Date: Tue, 4 Mar 2025 21:52:48 -0500 Subject: [PATCH 03/31] ran eslint --- src/functions/read/handler.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/functions/read/handler.ts b/src/functions/read/handler.ts index 9469943..dce03e8 100644 --- a/src/functions/read/handler.ts +++ b/src/functions/read/handler.ts @@ -9,7 +9,6 @@ dotenv.config({ path: path.resolve(process.cwd(), '.env') }); const read: ValidatedEventAPIGatewayProxyEvent = async (event) => { try { // Check if token is valid - /* const isValidToken = validateToken(event.body.auth_token, process.env.JWT_SECRET, event.body.auth_email); if (!isValidToken) { return { @@ -21,7 +20,6 @@ const read: ValidatedEventAPIGatewayProxyEvent = async (event) => }; } - */ // Connect to DB const db = MongoDB.getInstance(process.env.MONGO_URI); await db.connect(); From ebdc8ba2f3b6677a8f654de8089dd9bc09ab0337 Mon Sep 17 00:00:00 2001 From: RyamL1221 Date: Thu, 6 Mar 2025 14:52:59 -0500 Subject: [PATCH 04/31] added default value for all in schema --- src/functions/read/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions/read/schema.ts b/src/functions/read/schema.ts index 01e66d0..6cc3d52 100644 --- a/src/functions/read/schema.ts +++ b/src/functions/read/schema.ts @@ -4,7 +4,7 @@ export default { auth_email: { type: 'string', format: 'email' }, auth_token: { type: 'string' }, email: { type: 'string', format: 'email' }, - all: { type: 'boolean' } + all: { type: 'boolean', default: false }, }, required: ['auth_email', 'auth_token', 'email'], } as const; From 79911d6458b2a3d9365e6efc2d00ab33acf28621 Mon Sep 17 00:00:00 2001 From: RyamL1221 Date: Thu, 6 Mar 2025 14:54:58 -0500 Subject: [PATCH 05/31] ensures hackers can only look up their own information --- src/functions/read/handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions/read/handler.ts b/src/functions/read/handler.ts index dce03e8..5ad046c 100644 --- a/src/functions/read/handler.ts +++ b/src/functions/read/handler.ts @@ -50,7 +50,7 @@ const read: ValidatedEventAPIGatewayProxyEvent = async (event) => } const lookupEmail = event.body.email.toLowerCase(); - if (!authUser.role['director'] && !authUser.role['organizer'] && authUser.email !== lookupEmail) { + if (!authUser.role['director'] && !authUser.role['organizer'] && (authUser.email !== lookupEmail || event.body.all)) { return { statusCode: 403, body: JSON.stringify({ From 9feaa7ad70cbfa5f98ff549a2f29e7bfbe054824 Mon Sep 17 00:00:00 2001 From: RyamL1221 Date: Thu, 6 Mar 2025 15:09:05 -0500 Subject: [PATCH 06/31] added an all user test case --- tests/read.test.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/read.test.ts b/tests/read.test.ts index cdf6591..e19285d 100644 --- a/tests/read.test.ts +++ b/tests/read.test.ts @@ -11,6 +11,9 @@ jest.mock('../src/util', () => ({ disconnect: jest.fn(), getCollection: jest.fn().mockReturnValue({ findOne: jest.fn(), + find: jest.fn().mockReturnValue({ + toArray: jest.fn(), + }), }), }), }, @@ -29,6 +32,7 @@ describe('/read endpoint', () => { const findOneMock = util.MongoDB.getInstance('uri').getCollection('users').findOne as jest.Mock; const mockCallback = jest.fn(); + const findMock = util.MongoDB.getInstance('uri').getCollection('users').find as jest.Mock; it('invalid token', async () => { const userData = { @@ -148,6 +152,35 @@ describe('/read endpoint', () => { expect(res.body).toBeDefined(); }); + it('success case for all lookup', async () => { + // For the "all" branch, auth user must have director or organizer role. + findOneMock.mockReturnValueOnce({ + email: 'director@hackru.org', + role: { + hacker: true, + organizer: false, + director: true, + }, + }); + const allUsers = [ + { email: 'user1@hackru.org', name: 'User One' }, + { email: 'user2@hackru.org', name: 'User Two' }, + ]; + const toArrayMock = jest.fn().mockResolvedValue(allUsers); + findMock.mockReturnValueOnce({ toArray: toArrayMock }); + const userData = { + auth_email: 'director@hackru.org', + auth_token: 'mockToken', + email: 'anyemail@hackru.org', // email is not used in the lookup when all=true + all: true, + }; + const mockEvent = createEvent(userData, path, httpMethod); + const res = await main(mockEvent, mockContext, mockCallback); + + expect(res.statusCode).toBe(200); + expect(JSON.parse(res.body)).toEqual(allUsers); + }); + it('internal server error', async () => { findOneMock.mockImplementationOnce(() => { throw new Error('Database error'); From 92860d5c77c8e71875a0ec92760a220835812c0e Mon Sep 17 00:00:00 2001 From: RyamL1221 Date: Thu, 6 Mar 2025 15:11:00 -0500 Subject: [PATCH 07/31] ran prettier and eslint --- src/functions/read/handler.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/functions/read/handler.ts b/src/functions/read/handler.ts index 5ad046c..e0eda17 100644 --- a/src/functions/read/handler.ts +++ b/src/functions/read/handler.ts @@ -50,7 +50,11 @@ const read: ValidatedEventAPIGatewayProxyEvent = async (event) => } const lookupEmail = event.body.email.toLowerCase(); - if (!authUser.role['director'] && !authUser.role['organizer'] && (authUser.email !== lookupEmail || event.body.all)) { + if ( + !authUser.role['director'] && + !authUser.role['organizer'] && + (authUser.email !== lookupEmail || event.body.all) + ) { return { statusCode: 403, body: JSON.stringify({ From 6dc08d141713a34055882b2f139bc5246c5a971a Mon Sep 17 00:00:00 2001 From: RyamL1221 Date: Wed, 26 Mar 2025 18:38:21 -0400 Subject: [PATCH 08/31] revised if statement for clarity --- src/functions/read/handler.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/functions/read/handler.ts b/src/functions/read/handler.ts index e0eda17..0c4aecd 100644 --- a/src/functions/read/handler.ts +++ b/src/functions/read/handler.ts @@ -50,9 +50,10 @@ const read: ValidatedEventAPIGatewayProxyEvent = async (event) => } const lookupEmail = event.body.email.toLowerCase(); + + // Ensures user can only look up their own information if ( - !authUser.role['director'] && - !authUser.role['organizer'] && + (!authUser.role['director'] && !authUser.role['organizer']) && (authUser.email !== lookupEmail || event.body.all) ) { return { From 2a7bc3aa448dcfbeb12de7872254011c0cbc5bcc Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 12 Apr 2025 22:02:41 -0400 Subject: [PATCH 09/31] fixed eslint errors and ran prettier --- .eslintrc | 8 ++++++++ src/functions/read/handler.ts | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index e3b530e..cb26edb 100644 --- a/.eslintrc +++ b/.eslintrc @@ -144,6 +144,14 @@ "modifiers": [ "requiresQuotes" ] + }, + { + "selector": "objectLiteralProperty", + "filter": { + "regex": "^_id$", + "match": true + }, + "format": null } ] } diff --git a/src/functions/read/handler.ts b/src/functions/read/handler.ts index 0c4aecd..7ad369e 100644 --- a/src/functions/read/handler.ts +++ b/src/functions/read/handler.ts @@ -53,7 +53,8 @@ const read: ValidatedEventAPIGatewayProxyEvent = async (event) => // Ensures user can only look up their own information if ( - (!authUser.role['director'] && !authUser.role['organizer']) && + !authUser.role['director'] && + !authUser.role['organizer'] && (authUser.email !== lookupEmail || event.body.all) ) { return { From 2f450422b9791cbfae06121ca817b30426bb4fd6 Mon Sep 17 00:00:00 2001 From: poojakedia Date: Sat, 12 Apr 2025 23:13:29 -0400 Subject: [PATCH 10/31] Added basic email and buy-in input validation w/ unit tests --- src/functions/create/handler.ts | 11 +++++++++++ src/functions/update-buy-ins/handler.ts | 20 ++++++++++++++++++++ src/functions/update/handler.ts | 8 ++++++++ tests/create.test.ts | 11 +++++++++++ tests/update-buy-ins.test.ts | 21 ++++++++++++++++++++- tests/update.test.ts | 18 ++++++++++++++++++ 6 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/functions/create/handler.ts b/src/functions/create/handler.ts index ac63f5d..8619d30 100644 --- a/src/functions/create/handler.ts +++ b/src/functions/create/handler.ts @@ -7,6 +7,8 @@ import schema from './schema'; import { MongoDB } from '../../util'; import * as config from '../../config'; +import { validateEmail } from '../../helper'; + import * as path from 'path'; import * as dotenv from 'dotenv'; dotenv.config({ path: path.resolve(process.cwd(), '.env') }); @@ -25,6 +27,15 @@ const create: ValidatedEventAPIGatewayProxyEvent = async (event) } const uEmail = event.body.email.toLowerCase(); + if (!validateEmail(uEmail)) { + return { + statusCode: 400, + body: JSON.stringify({ + statusCode: 400, + message: 'Improper Email format', + }), + }; + } let password = event.body.password; try { diff --git a/src/functions/update-buy-ins/handler.ts b/src/functions/update-buy-ins/handler.ts index b047998..779c377 100644 --- a/src/functions/update-buy-ins/handler.ts +++ b/src/functions/update-buy-ins/handler.ts @@ -79,6 +79,26 @@ const updateBuyIns: ValidatedEventAPIGatewayProxyEvent = async (e }; } + //validate point update + for (let i = 0; i < userBuyInsSorted.length; i++) { + let value = userBuyInsSorted[i].buy_in; + console.log(value); + if (value === '') { + userBuyInsSorted[i].buy_in = 0; + } + + let numVal = parseInt(value, 10); + if (Number.isNaN(numVal)) { + return { + statusCode: 400, + body: JSON.stringify({ + statusCode: 400, + message: 'Requested point change is not a valid integer input', + }), + }; + } + } + //update the buy_ins array await pointCollection.updateOne({ email: event.body.email }, { $set: { buy_ins: event.body.buy_ins } }); return { diff --git a/src/functions/update/handler.ts b/src/functions/update/handler.ts index 2262233..fd1faee 100644 --- a/src/functions/update/handler.ts +++ b/src/functions/update/handler.ts @@ -4,6 +4,8 @@ import { middyfy } from '@libs/lambda'; import schema from './schema'; +import { validateEmail } from '../../helper'; + import { MongoDB, validateToken, ensureRoles } from '../../util'; import * as path from 'path'; import * as dotenv from 'dotenv'; @@ -214,6 +216,12 @@ function validateUpdates(updates: Updates, registrationStatus?: string, user?: W return 'Missing required fields'; } else return true; } + + if ('email' in setUpdates) { + if (!validateEmail(setUpdates.email)) { + return 'Improper Email format'; + } + } if ( ['_id', 'password', 'discord', 'created_at', 'registered_at', 'email_verified'].some( (lockedProp) => lockedProp in setUpdates diff --git a/tests/create.test.ts b/tests/create.test.ts index f42b548..a18b023 100644 --- a/tests/create.test.ts +++ b/tests/create.test.ts @@ -48,6 +48,17 @@ describe('Create endpoint', () => { expect(res.statusCode).toBe(200); expect(JSON.parse(res.body).message).toBe('User created!'); }); + it('Email entry is invalid', async () => { + const mockEvent = createEvent({ email: 'notValidFormat', password: 'testPassword123' }, '/create', 'POST'); + + const mockCallback = jest.fn(); + + (bcrypt.hash as jest.Mock).mockResolvedValue('hashedPassword'); + const res = await main(mockEvent, mockContext, mockCallback); + + expect(res.statusCode).toBe(400); + expect(JSON.parse(res.body).message).toBe('Improper Email format'); + }); it('Registration time has passed', async () => { jest.useFakeTimers(); jest.setSystemTime(new Date('01/01/2100')); diff --git a/tests/update-buy-ins.test.ts b/tests/update-buy-ins.test.ts index 39449e5..55ebbe9 100644 --- a/tests/update-buy-ins.test.ts +++ b/tests/update-buy-ins.test.ts @@ -96,8 +96,27 @@ describe('Update-Buy-Ins tests', () => { expect(result.statusCode).toBe(403); expect(JSON.parse(result.body).message).toBe('Points distributed exceed user point total.'); }); + //case 5 + it('non-numeric value entered for input', async () => { + findOneMock.mockReturnValueOnce({ + email: userData.email, + total_points: 30, + balance: 2, + buy_ins: [ + { prize_id: 'prize1', buy_in: 'random' }, + { prize_id: 'prize2', buy_in: 20 }, + ], + }); + + const mockEvent = createEvent(userData, path, httpMethod); + + const result = await main(mockEvent, mockContext, mockCallback); + + expect(result.statusCode).toBe(400); + expect(JSON.parse(result.body).message).toBe('Requested point change is not a valid integer input'); + }); - // case 5 + // case 6 it('successfully update user point balance', async () => { findOneMock.mockReturnValueOnce({ email: userData.email, diff --git a/tests/update.test.ts b/tests/update.test.ts index cf038e1..c77426b 100644 --- a/tests/update.test.ts +++ b/tests/update.test.ts @@ -261,6 +261,7 @@ describe('/update endpoint', () => { }, }, }; + jest.clearAllMocks(); findOneMock.mockReturnValue({ //successful update @@ -298,4 +299,21 @@ describe('/update endpoint', () => { const res = await main(mockEvent, mockContext, mockCallback); expect(res.statusCode).toBe(200); }); + //case 9 + it('Invalid email format', async () => { + const completeUserData = { + user_email: 'test@test.org', + auth_email: 'testAuth@test.org', + auth_token: 'sampleAuthToken', + updates: { + $set: { + email: 'randomVal', + }, + }, + }; + const mockEvent = createEvent(completeUserData, '/update', 'POST'); + const res = await main(mockEvent, mockContext, mockCallback); + expect(res.statusCode).toBe(400); + expect(JSON.parse(res.body).message).toBe('Improper Email format'); + }); }); From 6f893a325e954bd783113b39108529cb411090c2 Mon Sep 17 00:00:00 2001 From: poojakedia Date: Mon, 14 Apr 2025 13:44:42 -0400 Subject: [PATCH 11/31] Add email validation helper --- src/helper.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/helper.ts diff --git a/src/helper.ts b/src/helper.ts new file mode 100644 index 0000000..eca141d --- /dev/null +++ b/src/helper.ts @@ -0,0 +1,4 @@ +export function validateEmail(email) { + const emailPattern = /^[A-Za-z0-9]+@[A-Za-z0-9]+.[A-Za-z0-9]+$/; + return emailPattern.test(email); +} From 326c5969de36a1e731eba05173a692f7e53214c8 Mon Sep 17 00:00:00 2001 From: poojakedia Date: Mon, 14 Apr 2025 13:57:46 -0400 Subject: [PATCH 12/31] fix eslint issue --- src/functions/update-buy-ins/handler.ts | 11 ++++------- src/functions/update/handler.ts | 4 ++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/functions/update-buy-ins/handler.ts b/src/functions/update-buy-ins/handler.ts index 779c377..c8fc60f 100644 --- a/src/functions/update-buy-ins/handler.ts +++ b/src/functions/update-buy-ins/handler.ts @@ -80,14 +80,11 @@ const updateBuyIns: ValidatedEventAPIGatewayProxyEvent = async (e } //validate point update - for (let i = 0; i < userBuyInsSorted.length; i++) { - let value = userBuyInsSorted[i].buy_in; - console.log(value); - if (value === '') { - userBuyInsSorted[i].buy_in = 0; - } + for (const userBuyIn of userBuyInsSorted) { + const value = userBuyIn.buy_in; + if (value === '') userBuyIn.buy_in = 0; - let numVal = parseInt(value, 10); + const numVal = parseInt(value, 10); if (Number.isNaN(numVal)) { return { statusCode: 400, diff --git a/src/functions/update/handler.ts b/src/functions/update/handler.ts index fd1faee..6a17bc3 100644 --- a/src/functions/update/handler.ts +++ b/src/functions/update/handler.ts @@ -218,9 +218,9 @@ function validateUpdates(updates: Updates, registrationStatus?: string, user?: W } if ('email' in setUpdates) { - if (!validateEmail(setUpdates.email)) { + if (!validateEmail(setUpdates.email)) return 'Improper Email format'; - } + } if ( ['_id', 'password', 'discord', 'created_at', 'registered_at', 'email_verified'].some( From ecd73ecf8d5b0ab652d27cd034984437ee8e2325 Mon Sep 17 00:00:00 2001 From: poojakedia Date: Mon, 14 Apr 2025 14:01:52 -0400 Subject: [PATCH 13/31] fix prettier --- src/functions/update/handler.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/functions/update/handler.ts b/src/functions/update/handler.ts index 6a17bc3..1a7f944 100644 --- a/src/functions/update/handler.ts +++ b/src/functions/update/handler.ts @@ -218,9 +218,7 @@ function validateUpdates(updates: Updates, registrationStatus?: string, user?: W } if ('email' in setUpdates) { - if (!validateEmail(setUpdates.email)) - return 'Improper Email format'; - + if (!validateEmail(setUpdates.email)) return 'Improper Email format'; } if ( ['_id', 'password', 'discord', 'created_at', 'registered_at', 'email_verified'].some( From eda13295cf9e96ffb2916b3db4d2be7f642fe47b Mon Sep 17 00:00:00 2001 From: poojakedia Date: Mon, 14 Apr 2025 14:04:53 -0400 Subject: [PATCH 14/31] fix eslint issue --- src/functions/update/handler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/functions/update/handler.ts b/src/functions/update/handler.ts index 1a7f944..786c34c 100644 --- a/src/functions/update/handler.ts +++ b/src/functions/update/handler.ts @@ -217,9 +217,9 @@ function validateUpdates(updates: Updates, registrationStatus?: string, user?: W } else return true; } - if ('email' in setUpdates) { + if ('email' in setUpdates) if (!validateEmail(setUpdates.email)) return 'Improper Email format'; - } + if ( ['_id', 'password', 'discord', 'created_at', 'registered_at', 'email_verified'].some( (lockedProp) => lockedProp in setUpdates From ef3e96c201138cf9aaeb7a1640954d42626707ce Mon Sep 17 00:00:00 2001 From: poojakedia Date: Mon, 14 Apr 2025 14:05:55 -0400 Subject: [PATCH 15/31] fix conlflicting prettier and eslint changes --- src/functions/update/handler.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/functions/update/handler.ts b/src/functions/update/handler.ts index 786c34c..c5e1c00 100644 --- a/src/functions/update/handler.ts +++ b/src/functions/update/handler.ts @@ -217,9 +217,8 @@ function validateUpdates(updates: Updates, registrationStatus?: string, user?: W } else return true; } - if ('email' in setUpdates) - if (!validateEmail(setUpdates.email)) return 'Improper Email format'; - + if ('email' in setUpdates) if (!validateEmail(setUpdates.email)) return 'Improper Email format'; + if ( ['_id', 'password', 'discord', 'created_at', 'registered_at', 'email_verified'].some( (lockedProp) => lockedProp in setUpdates From 127f00173ea112dfd03737de82f7580102073996 Mon Sep 17 00:00:00 2001 From: poojakedia Date: Thu, 17 Apr 2025 11:29:54 -0400 Subject: [PATCH 16/31] fix: change error status, validate point ranges, update unit testing --- src/functions/create/handler.ts | 4 ++-- src/functions/update-buy-ins/handler.ts | 13 +++++++++++-- src/helper.ts | 2 +- tests/create.test.ts | 2 +- tests/update-buy-ins.test.ts | 22 ++++++++++++++++++++-- 5 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/functions/create/handler.ts b/src/functions/create/handler.ts index 8619d30..030ab8b 100644 --- a/src/functions/create/handler.ts +++ b/src/functions/create/handler.ts @@ -29,9 +29,9 @@ const create: ValidatedEventAPIGatewayProxyEvent = async (event) const uEmail = event.body.email.toLowerCase(); if (!validateEmail(uEmail)) { return { - statusCode: 400, + statusCode: 403, body: JSON.stringify({ - statusCode: 400, + statusCode: 403, message: 'Improper Email format', }), }; diff --git a/src/functions/update-buy-ins/handler.ts b/src/functions/update-buy-ins/handler.ts index c8fc60f..fa4f269 100644 --- a/src/functions/update-buy-ins/handler.ts +++ b/src/functions/update-buy-ins/handler.ts @@ -87,13 +87,22 @@ const updateBuyIns: ValidatedEventAPIGatewayProxyEvent = async (e const numVal = parseInt(value, 10); if (Number.isNaN(numVal)) { return { - statusCode: 400, + statusCode: 403, body: JSON.stringify({ - statusCode: 400, + statusCode: 403, message: 'Requested point change is not a valid integer input', }), }; } + if (numVal >= 1000 || numVal <= -1000) { + return { + statusCode: 403, + body: JSON.stringify({ + statusCode: 403, + message: 'Requested point change is not in a valid point range', + }), + }; + } } //update the buy_ins array diff --git a/src/helper.ts b/src/helper.ts index eca141d..eda2531 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,4 +1,4 @@ export function validateEmail(email) { - const emailPattern = /^[A-Za-z0-9]+@[A-Za-z0-9]+.[A-Za-z0-9]+$/; + const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailPattern.test(email); } diff --git a/tests/create.test.ts b/tests/create.test.ts index a18b023..57de15e 100644 --- a/tests/create.test.ts +++ b/tests/create.test.ts @@ -56,7 +56,7 @@ describe('Create endpoint', () => { (bcrypt.hash as jest.Mock).mockResolvedValue('hashedPassword'); const res = await main(mockEvent, mockContext, mockCallback); - expect(res.statusCode).toBe(400); + expect(res.statusCode).toBe(403); expect(JSON.parse(res.body).message).toBe('Improper Email format'); }); it('Registration time has passed', async () => { diff --git a/tests/update-buy-ins.test.ts b/tests/update-buy-ins.test.ts index 55ebbe9..b54e04d 100644 --- a/tests/update-buy-ins.test.ts +++ b/tests/update-buy-ins.test.ts @@ -112,11 +112,29 @@ describe('Update-Buy-Ins tests', () => { const result = await main(mockEvent, mockContext, mockCallback); - expect(result.statusCode).toBe(400); + expect(result.statusCode).toBe(403); expect(JSON.parse(result.body).message).toBe('Requested point change is not a valid integer input'); }); - // case 6 + it('Buy-in points out of range', async () => { + findOneMock.mockReturnValueOnce({ + email: userData.email, + total_points: 30, + balance: 2, + buy_ins: [ + { prize_id: 'prize1', buy_in: 1500 }, + { prize_id: 'prize2', buy_in: 20 }, + ], + }); + + const mockEvent = createEvent(userData, path, httpMethod); + + const result = await main(mockEvent, mockContext, mockCallback); + + expect(result.statusCode).toBe(403); + expect(JSON.parse(result.body).message).toBe('Requested point change is not in a valid point range'); + }); + // case 7 it('successfully update user point balance', async () => { findOneMock.mockReturnValueOnce({ email: userData.email, From 5a74e7a60db31c2b3467d4d893602feb9fdf034f Mon Sep 17 00:00:00 2001 From: RyamL1221 Date: Tue, 6 May 2025 16:47:36 -0400 Subject: [PATCH 17/31] created index.ts for delete endpoint --- src/functions/delete/index.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/functions/delete/index.ts diff --git a/src/functions/delete/index.ts b/src/functions/delete/index.ts new file mode 100644 index 0000000..b70520d --- /dev/null +++ b/src/functions/delete/index.ts @@ -0,0 +1,20 @@ +import { handlerPath } from '@libs/handler-resolver'; +import schema from './schema'; + +export default { + handler: `${handlerPath(__dirname)}/handler.main`, + events: [ + { + http: { + method: 'post', + path: 'delete', + cors: true, + request: { + schemas: { + 'application/json': schema, + }, + }, + }, + }, + ], +}; From 61a732135a9ace8c78a62453af24366a33a7221b Mon Sep 17 00:00:00 2001 From: RyamL1221 Date: Tue, 6 May 2025 16:51:49 -0400 Subject: [PATCH 18/31] created schema --- src/functions/delete/schema.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/functions/delete/schema.ts diff --git a/src/functions/delete/schema.ts b/src/functions/delete/schema.ts new file mode 100644 index 0000000..36f7034 --- /dev/null +++ b/src/functions/delete/schema.ts @@ -0,0 +1,9 @@ +export default { + type: 'object', + properties: { + email: { type: 'string', format: 'email' }, + auth_email: { type: 'string', format: 'email' }, + auth_token: { type: 'string' }, + }, + required: ['email', 'auth_token', 'auth_email'] +} as const; From 685371015431ef302b36431aeb537ba41fd5b2d8 Mon Sep 17 00:00:00 2001 From: RyamL1221 Date: Tue, 6 May 2025 16:57:19 -0400 Subject: [PATCH 19/31] created handler --- src/functions/delete/handler.ts | 78 +++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/functions/delete/handler.ts diff --git a/src/functions/delete/handler.ts b/src/functions/delete/handler.ts new file mode 100644 index 0000000..45f0377 --- /dev/null +++ b/src/functions/delete/handler.ts @@ -0,0 +1,78 @@ +import type { ValidatedEventAPIGatewayProxyEvent } from '@libs/api-gateway' +import { middyfy } from '@libs/lambda' +import schema from './schema' +import { MongoDB, validateToken, ensureRoles, UserDoc } from '../../util' +import * as path from 'path' +import * as dotenv from 'dotenv' + +dotenv.config({ path: path.resolve(process.cwd(), '.env') }) + +const deleteUser: ValidatedEventAPIGatewayProxyEvent = async (event) => { + try { + const { auth_email, auth_token, user_email } = event.body + + // 1. Validate auth token + const tokenValid = validateToken(auth_token, process.env.JWT_SECRET, auth_email) + if (!tokenValid) { + return { + statusCode: 401, + body: JSON.stringify({ statusCode: 401, message: 'Unauthorized' }), + } + } + + // 2. Connect to MongoDB + const db = MongoDB.getInstance(process.env.MONGO_URI) + await db.connect() + const users = db.getCollection('users') + + // 3. Check target user exists + const target = await users.findOne({ email: user_email }) + if (!target) { + return { + statusCode: 404, + body: JSON.stringify({ statusCode: 404, message: 'User not found' }), + } + } + + // 4. Verify auth user exists + const authUser = await users.findOne({ email: auth_email }) + if (!authUser) { + return { + statusCode: 404, + body: JSON.stringify({ statusCode: 404, message: 'Auth user not found' }), + } + } + + // 5. Ensure auth user role + if (!ensureRoles(authUser.role, ['director', 'organizer'])) { + return { + statusCode: 401, + body: JSON.stringify({ statusCode: 401, message: 'Only directors/organizers can call this endpoint.' }), + } + } + + // 6. Delete the user + const result = await users.deleteOne({ email: user_email }) + if (result.deletedCount !== 1) { + // Shouldn't happen since we checked existence + return { + statusCode: 500, + body: JSON.stringify({ statusCode: 500, message: 'Internal server error' }), + } + } + + // 7. Success + return { + statusCode: 200, + body: JSON.stringify({ statusCode: 200, message: `Deleted ${user_email} successfully` }), + } + } catch (error) { + console.error('Error deleting user:', error) + return { + statusCode: 500, + body: JSON.stringify({ statusCode: 500, message: 'Internal server error' }), + } + } +} + +export const main = middyfy(deleteUser) \ No newline at end of file From 398dd3911b8b1698b12cbf812a395078642e3167 Mon Sep 17 00:00:00 2001 From: RyamL1221 Date: Tue, 6 May 2025 17:12:31 -0400 Subject: [PATCH 20/31] unit test for delete endpoint --- tests/attend-event.test.ts | 191 ++++++++++++------------------------- tests/delete.test.ts | 97 +++++++++++++++++++ 2 files changed, 159 insertions(+), 129 deletions(-) create mode 100644 tests/delete.test.ts diff --git a/tests/attend-event.test.ts b/tests/attend-event.test.ts index 25cf6da..8f39040 100644 --- a/tests/attend-event.test.ts +++ b/tests/attend-event.test.ts @@ -1,7 +1,3 @@ -import { main } from '../src/functions/attend-event/handler'; -import { createEvent, mockContext } from './helper'; -import * as util from '../src/util'; - jest.mock('../src/util', () => ({ // eslint-disable-next-line @typescript-eslint/naming-convention MongoDB: { @@ -10,7 +6,7 @@ jest.mock('../src/util', () => ({ disconnect: jest.fn(), getCollection: jest.fn().mockReturnValue({ findOne: jest.fn(), - updateOne: jest.fn(), + deleteOne: jest.fn(), }), }), }, @@ -18,147 +14,84 @@ jest.mock('../src/util', () => ({ ensureRoles: jest.fn((roleDict, validRoles) => { return validRoles.some((role) => roleDict[role]); }), -})); +})) + +import { main } from '../src/functions/delete/handler' +import { createEvent, mockContext } from './helper' +import * as util from '../src/util' + +// Ensure util mocks are applied before any tests -describe('Attend-Event tests', () => { +describe('Delete Tests', () => { beforeEach(() => { - jest.clearAllMocks(); // Clear mocks before each test to avoid interference + jest.clearAllMocks(); }); const userData = { - auth_email: 'authUser@test.org', + auth_email: 'director@test.org', auth_token: 'mockToken', - qr: 'test@test.org', - event: 'lunch', - limit: 1, - }; - const path = '/attend-event'; - const httpMethod = 'POST'; - - // this will make it more concise and easier to understand (mocking) - const findOneMock = util.MongoDB.getInstance('uri').getCollection('users').findOne as jest.Mock; - const mockCallback = jest.fn(); - - // case 1: auth token is not valid + user_email: 'target@test.org', + } + const path = '/delete' + const httpMethod = 'POST' + + const findOneMock = util.MongoDB.getInstance('uri').getCollection('users').findOne as jest.Mock + const deleteOneMock = util.MongoDB.getInstance('uri').getCollection('users').deleteOne as jest.Mock + it('auth token is not valid', async () => { - const mockEvent = createEvent(userData, path, httpMethod); + const mockEvent = createEvent(userData, path, httpMethod) - const result = await main(mockEvent, mockContext, mockCallback); + const result = await main(mockEvent, mockContext, undefined) - expect(result.statusCode).toBe(401); - expect(JSON.parse(result.body).message).toBe('Unauthorized.'); - }); + expect(result.statusCode).toBe(401) + expect(JSON.parse(result.body).message).toBe('Unauthorized') + }) - // case 2 it('user does not exist', async () => { - findOneMock.mockReturnValueOnce(null); - const mockEvent = createEvent(userData, path, httpMethod); + findOneMock.mockReturnValueOnce(null) + const mockEvent = createEvent(userData, path, httpMethod) - const result = await main(mockEvent, mockContext, mockCallback); + const result = await main(mockEvent, mockContext, undefined) - expect(result.statusCode).toBe(404); - expect(JSON.parse(result.body).message).toBe('User not found.'); - }); + expect(result.statusCode).toBe(404) + expect(JSON.parse(result.body).message).toBe('User not found') + }) - // case 3 it('Auth user does not have director/organizer role', async () => { - findOneMock.mockReturnValueOnce({}).mockReturnValueOnce({ - role: { - hacker: false, - volunteer: true, - judge: false, - sponsor: false, - mentor: false, - organizer: false, - director: false, - }, - }); - const mockEvent = createEvent(userData, path, httpMethod); - - const result = await main(mockEvent, mockContext, mockCallback); - - expect(result.statusCode).toBe(401); - expect(JSON.parse(result.body).message).toBe('Only directors/organizers can call this endpoint.'); - }); - - // case 4 - it('user tries to check into an event the second time but it can only be attended once', async () => { findOneMock - .mockReturnValueOnce({ - day_of: { - event: { - lunch: { - attend: 1, - }, - }, - }, - registration_status: 'checked_in', - }) - .mockReturnValueOnce({ - role: { - hacker: false, - volunteer: false, - judge: false, - sponsor: false, - mentor: false, - organizer: true, - director: false, - }, - }); - const mockEvent = createEvent(userData, path, httpMethod); - - const result = await main(mockEvent, mockContext, mockCallback); - - expect(result.statusCode).toBe(409); - expect(JSON.parse(result.body).message).toBe('User already checked into event.'); - }); + .mockReturnValueOnce({}) + .mockReturnValueOnce({ role: { hacker: false, volunteer: true, judge: false, sponsor: false, mentor: false, organizer: false, director: false } }) + const mockEvent = createEvent(userData, path, httpMethod) + + const result = await main(mockEvent, mockContext, undefined) + + expect(result.statusCode).toBe(401) + expect(JSON.parse(result.body).message).toBe('Only directors/organizers can call this endpoint.') + }) - // case 5 - it('success check-in to an event', async () => { + it('returns 500 when deleteOne does not delete', async () => { findOneMock - .mockReturnValueOnce({ - day_of: {}, - registration_status: 'checked_in', - }) - .mockReturnValueOnce({ - role: { - hacker: false, - volunteer: true, - judge: false, - sponsor: false, - mentor: false, - organizer: false, - director: true, - }, - }); - const mockEvent = createEvent(userData, path, httpMethod); - const result = await main(mockEvent, mockContext, mockCallback); - - expect(result.statusCode).toBe(200); - expect(JSON.parse(result.body).message).toBe('user successfully checked into event'); - }); + .mockReturnValueOnce({ email: userData.user_email }) + .mockReturnValueOnce({ role: { hacker: false, organizer: false, director: true } }) + deleteOneMock.mockReturnValueOnce({ deletedCount: 0 }) + const mockEvent = createEvent(userData, path, httpMethod) + + const result = await main(mockEvent, mockContext, undefined) - it('user tries to attend event when they are not checked_in', async () => { + expect(result.statusCode).toBe(500) + expect(JSON.parse(result.body).message).toBe('Internal server error') + }) + + it('deletes user successfully', async () => { findOneMock - .mockReturnValueOnce({ - day_of: {}, - registration_status: 'registered', - }) - .mockReturnValueOnce({ - role: { - hacker: false, - volunteer: true, - judge: false, - sponsor: false, - mentor: false, - organizer: false, - director: true, - }, - }); - const mockEvent = createEvent(userData, path, httpMethod); - const result = await main(mockEvent, mockContext, mockCallback); - - expect(result.statusCode).toBe(409); - expect(JSON.parse(result.body).message).toBe('User has not checked in. Current status is registered'); - }); -}); + .mockReturnValueOnce({ email: userData.user_email }) + .mockReturnValueOnce({ role: { hacker: false, volunteer: true, organizer: true, director: false } }) + deleteOneMock.mockReturnValueOnce({ deletedCount: 1 }) + const mockEvent = createEvent(userData, path, httpMethod) + + const result = await main(mockEvent, mockContext, undefined) + + expect(result.statusCode).toBe(200) + expect(JSON.parse(result.body).message).toBe(`Deleted ${userData.user_email} successfully`) + }) +}) diff --git a/tests/delete.test.ts b/tests/delete.test.ts new file mode 100644 index 0000000..8f39040 --- /dev/null +++ b/tests/delete.test.ts @@ -0,0 +1,97 @@ +jest.mock('../src/util', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + MongoDB: { + getInstance: jest.fn().mockReturnValue({ + connect: jest.fn(), + disconnect: jest.fn(), + getCollection: jest.fn().mockReturnValue({ + findOne: jest.fn(), + deleteOne: jest.fn(), + }), + }), + }, + validateToken: jest.fn().mockReturnValueOnce(false).mockReturnValue(true), + ensureRoles: jest.fn((roleDict, validRoles) => { + return validRoles.some((role) => roleDict[role]); + }), +})) + +import { main } from '../src/functions/delete/handler' +import { createEvent, mockContext } from './helper' +import * as util from '../src/util' + +// Ensure util mocks are applied before any tests + +describe('Delete Tests', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const userData = { + auth_email: 'director@test.org', + auth_token: 'mockToken', + user_email: 'target@test.org', + } + const path = '/delete' + const httpMethod = 'POST' + + const findOneMock = util.MongoDB.getInstance('uri').getCollection('users').findOne as jest.Mock + const deleteOneMock = util.MongoDB.getInstance('uri').getCollection('users').deleteOne as jest.Mock + + it('auth token is not valid', async () => { + const mockEvent = createEvent(userData, path, httpMethod) + + const result = await main(mockEvent, mockContext, undefined) + + expect(result.statusCode).toBe(401) + expect(JSON.parse(result.body).message).toBe('Unauthorized') + }) + + it('user does not exist', async () => { + findOneMock.mockReturnValueOnce(null) + const mockEvent = createEvent(userData, path, httpMethod) + + const result = await main(mockEvent, mockContext, undefined) + + expect(result.statusCode).toBe(404) + expect(JSON.parse(result.body).message).toBe('User not found') + }) + + it('Auth user does not have director/organizer role', async () => { + findOneMock + .mockReturnValueOnce({}) + .mockReturnValueOnce({ role: { hacker: false, volunteer: true, judge: false, sponsor: false, mentor: false, organizer: false, director: false } }) + const mockEvent = createEvent(userData, path, httpMethod) + + const result = await main(mockEvent, mockContext, undefined) + + expect(result.statusCode).toBe(401) + expect(JSON.parse(result.body).message).toBe('Only directors/organizers can call this endpoint.') + }) + + it('returns 500 when deleteOne does not delete', async () => { + findOneMock + .mockReturnValueOnce({ email: userData.user_email }) + .mockReturnValueOnce({ role: { hacker: false, organizer: false, director: true } }) + deleteOneMock.mockReturnValueOnce({ deletedCount: 0 }) + const mockEvent = createEvent(userData, path, httpMethod) + + const result = await main(mockEvent, mockContext, undefined) + + expect(result.statusCode).toBe(500) + expect(JSON.parse(result.body).message).toBe('Internal server error') + }) + + it('deletes user successfully', async () => { + findOneMock + .mockReturnValueOnce({ email: userData.user_email }) + .mockReturnValueOnce({ role: { hacker: false, volunteer: true, organizer: true, director: false } }) + deleteOneMock.mockReturnValueOnce({ deletedCount: 1 }) + const mockEvent = createEvent(userData, path, httpMethod) + + const result = await main(mockEvent, mockContext, undefined) + + expect(result.statusCode).toBe(200) + expect(JSON.parse(result.body).message).toBe(`Deleted ${userData.user_email} successfully`) + }) +}) From f6b6b2e10f34401f25d4c9e10e7dc3ba8d4fdef3 Mon Sep 17 00:00:00 2001 From: RyamL1221 Date: Tue, 6 May 2025 17:19:55 -0400 Subject: [PATCH 21/31] ran prettier and eslint --- src/functions/delete/handler.ts | 51 +++++++++--------- src/functions/delete/schema.ts | 2 +- tests/delete.test.ts | 96 ++++++++++++++++++--------------- 3 files changed, 80 insertions(+), 69 deletions(-) diff --git a/src/functions/delete/handler.ts b/src/functions/delete/handler.ts index 45f0377..166fc31 100644 --- a/src/functions/delete/handler.ts +++ b/src/functions/delete/handler.ts @@ -1,46 +1,47 @@ -import type { ValidatedEventAPIGatewayProxyEvent } from '@libs/api-gateway' -import { middyfy } from '@libs/lambda' -import schema from './schema' -import { MongoDB, validateToken, ensureRoles, UserDoc } from '../../util' -import * as path from 'path' -import * as dotenv from 'dotenv' +import type { ValidatedEventAPIGatewayProxyEvent } from '@libs/api-gateway'; +import { middyfy } from '@libs/lambda'; +import schema from './schema'; +import { MongoDB, validateToken, ensureRoles, UserDoc } from '../../util'; +import * as path from 'path'; +import * as dotenv from 'dotenv'; -dotenv.config({ path: path.resolve(process.cwd(), '.env') }) +dotenv.config({ path: path.resolve(process.cwd(), '.env') }); const deleteUser: ValidatedEventAPIGatewayProxyEvent = async (event) => { try { - const { auth_email, auth_token, user_email } = event.body + // eslint-disable-next-line @typescript-eslint/naming-convention + const { auth_email, auth_token, user_email } = event.body; // 1. Validate auth token - const tokenValid = validateToken(auth_token, process.env.JWT_SECRET, auth_email) + const tokenValid = validateToken(auth_token, process.env.JWT_SECRET, auth_email); if (!tokenValid) { return { statusCode: 401, body: JSON.stringify({ statusCode: 401, message: 'Unauthorized' }), - } + }; } // 2. Connect to MongoDB - const db = MongoDB.getInstance(process.env.MONGO_URI) - await db.connect() - const users = db.getCollection('users') + const db = MongoDB.getInstance(process.env.MONGO_URI); + await db.connect(); + const users = db.getCollection('users'); // 3. Check target user exists - const target = await users.findOne({ email: user_email }) + const target = await users.findOne({ email: user_email }); if (!target) { return { statusCode: 404, body: JSON.stringify({ statusCode: 404, message: 'User not found' }), - } + }; } // 4. Verify auth user exists - const authUser = await users.findOne({ email: auth_email }) + const authUser = await users.findOne({ email: auth_email }); if (!authUser) { return { statusCode: 404, body: JSON.stringify({ statusCode: 404, message: 'Auth user not found' }), - } + }; } // 5. Ensure auth user role @@ -48,31 +49,31 @@ const deleteUser: ValidatedEventAPIGatewayProxyEvent = async (eve return { statusCode: 401, body: JSON.stringify({ statusCode: 401, message: 'Only directors/organizers can call this endpoint.' }), - } + }; } // 6. Delete the user - const result = await users.deleteOne({ email: user_email }) + const result = await users.deleteOne({ email: user_email }); if (result.deletedCount !== 1) { // Shouldn't happen since we checked existence return { statusCode: 500, body: JSON.stringify({ statusCode: 500, message: 'Internal server error' }), - } + }; } // 7. Success return { statusCode: 200, body: JSON.stringify({ statusCode: 200, message: `Deleted ${user_email} successfully` }), - } + }; } catch (error) { - console.error('Error deleting user:', error) + console.error('Error deleting user:', error); return { statusCode: 500, body: JSON.stringify({ statusCode: 500, message: 'Internal server error' }), - } + }; } -} +}; -export const main = middyfy(deleteUser) \ No newline at end of file +export const main = middyfy(deleteUser); diff --git a/src/functions/delete/schema.ts b/src/functions/delete/schema.ts index 36f7034..b3c18fd 100644 --- a/src/functions/delete/schema.ts +++ b/src/functions/delete/schema.ts @@ -5,5 +5,5 @@ export default { auth_email: { type: 'string', format: 'email' }, auth_token: { type: 'string' }, }, - required: ['email', 'auth_token', 'auth_email'] + required: ['email', 'auth_token', 'auth_email'], } as const; diff --git a/tests/delete.test.ts b/tests/delete.test.ts index 8f39040..98738f2 100644 --- a/tests/delete.test.ts +++ b/tests/delete.test.ts @@ -14,11 +14,11 @@ jest.mock('../src/util', () => ({ ensureRoles: jest.fn((roleDict, validRoles) => { return validRoles.some((role) => roleDict[role]); }), -})) +})); -import { main } from '../src/functions/delete/handler' -import { createEvent, mockContext } from './helper' -import * as util from '../src/util' +import { main } from '../src/functions/delete/handler'; +import { createEvent, mockContext } from './helper'; +import * as util from '../src/util'; // Ensure util mocks are applied before any tests @@ -31,67 +31,77 @@ describe('Delete Tests', () => { auth_email: 'director@test.org', auth_token: 'mockToken', user_email: 'target@test.org', - } - const path = '/delete' - const httpMethod = 'POST' + }; + const path = '/delete'; + const httpMethod = 'POST'; - const findOneMock = util.MongoDB.getInstance('uri').getCollection('users').findOne as jest.Mock - const deleteOneMock = util.MongoDB.getInstance('uri').getCollection('users').deleteOne as jest.Mock + const findOneMock = util.MongoDB.getInstance('uri').getCollection('users').findOne as jest.Mock; + const deleteOneMock = util.MongoDB.getInstance('uri').getCollection('users').deleteOne as jest.Mock; it('auth token is not valid', async () => { - const mockEvent = createEvent(userData, path, httpMethod) + const mockEvent = createEvent(userData, path, httpMethod); - const result = await main(mockEvent, mockContext, undefined) + const result = await main(mockEvent, mockContext, undefined); - expect(result.statusCode).toBe(401) - expect(JSON.parse(result.body).message).toBe('Unauthorized') - }) + expect(result.statusCode).toBe(401); + expect(JSON.parse(result.body).message).toBe('Unauthorized'); + }); it('user does not exist', async () => { - findOneMock.mockReturnValueOnce(null) - const mockEvent = createEvent(userData, path, httpMethod) + findOneMock.mockReturnValueOnce(null); + const mockEvent = createEvent(userData, path, httpMethod); - const result = await main(mockEvent, mockContext, undefined) + const result = await main(mockEvent, mockContext, undefined); - expect(result.statusCode).toBe(404) - expect(JSON.parse(result.body).message).toBe('User not found') - }) + expect(result.statusCode).toBe(404); + expect(JSON.parse(result.body).message).toBe('User not found'); + }); it('Auth user does not have director/organizer role', async () => { findOneMock .mockReturnValueOnce({}) - .mockReturnValueOnce({ role: { hacker: false, volunteer: true, judge: false, sponsor: false, mentor: false, organizer: false, director: false } }) - const mockEvent = createEvent(userData, path, httpMethod) - - const result = await main(mockEvent, mockContext, undefined) - - expect(result.statusCode).toBe(401) - expect(JSON.parse(result.body).message).toBe('Only directors/organizers can call this endpoint.') - }) + .mockReturnValueOnce({ + role: { + hacker: false, + volunteer: true, + judge: false, + sponsor: false, + mentor: false, + organizer: false, + director: false, + }, + }); + const mockEvent = createEvent(userData, path, httpMethod); + + const result = await main(mockEvent, mockContext, undefined); + + expect(result.statusCode).toBe(401); + expect(JSON.parse(result.body).message).toBe('Only directors/organizers can call this endpoint.'); + }); it('returns 500 when deleteOne does not delete', async () => { findOneMock .mockReturnValueOnce({ email: userData.user_email }) - .mockReturnValueOnce({ role: { hacker: false, organizer: false, director: true } }) - deleteOneMock.mockReturnValueOnce({ deletedCount: 0 }) - const mockEvent = createEvent(userData, path, httpMethod) + .mockReturnValueOnce({ role: { hacker: false, organizer: false, director: true } }); + deleteOneMock.mockReturnValueOnce({ deletedCount: 0 }); + const mockEvent = createEvent(userData, path, httpMethod); - const result = await main(mockEvent, mockContext, undefined) + const result = await main(mockEvent, mockContext, undefined); - expect(result.statusCode).toBe(500) - expect(JSON.parse(result.body).message).toBe('Internal server error') - }) + expect(result.statusCode).toBe(500); + expect(JSON.parse(result.body).message).toBe('Internal server error'); + }); it('deletes user successfully', async () => { findOneMock .mockReturnValueOnce({ email: userData.user_email }) - .mockReturnValueOnce({ role: { hacker: false, volunteer: true, organizer: true, director: false } }) - deleteOneMock.mockReturnValueOnce({ deletedCount: 1 }) - const mockEvent = createEvent(userData, path, httpMethod) + .mockReturnValueOnce({ role: { hacker: false, volunteer: true, organizer: true, director: false } }); + deleteOneMock.mockReturnValueOnce({ deletedCount: 1 }); + const mockEvent = createEvent(userData, path, httpMethod); - const result = await main(mockEvent, mockContext, undefined) + const result = await main(mockEvent, mockContext, undefined); - expect(result.statusCode).toBe(200) - expect(JSON.parse(result.body).message).toBe(`Deleted ${userData.user_email} successfully`) - }) -}) + expect(result.statusCode).toBe(200); + expect(JSON.parse(result.body).message).toBe(`Deleted ${userData.user_email} successfully`); + }); +}); From 2d481156e59fdc61c68d0e4dd8bb345f870849c6 Mon Sep 17 00:00:00 2001 From: RyamL1221 Date: Wed, 7 May 2025 18:21:54 -0400 Subject: [PATCH 22/31] ran prettier on unit test --- tests/attend-event.test.ts | 191 +++++++++++++++++++++++++------------ tests/delete.test.ts | 24 +++-- 2 files changed, 140 insertions(+), 75 deletions(-) diff --git a/tests/attend-event.test.ts b/tests/attend-event.test.ts index 8f39040..25cf6da 100644 --- a/tests/attend-event.test.ts +++ b/tests/attend-event.test.ts @@ -1,3 +1,7 @@ +import { main } from '../src/functions/attend-event/handler'; +import { createEvent, mockContext } from './helper'; +import * as util from '../src/util'; + jest.mock('../src/util', () => ({ // eslint-disable-next-line @typescript-eslint/naming-convention MongoDB: { @@ -6,7 +10,7 @@ jest.mock('../src/util', () => ({ disconnect: jest.fn(), getCollection: jest.fn().mockReturnValue({ findOne: jest.fn(), - deleteOne: jest.fn(), + updateOne: jest.fn(), }), }), }, @@ -14,84 +18,147 @@ jest.mock('../src/util', () => ({ ensureRoles: jest.fn((roleDict, validRoles) => { return validRoles.some((role) => roleDict[role]); }), -})) - -import { main } from '../src/functions/delete/handler' -import { createEvent, mockContext } from './helper' -import * as util from '../src/util' - -// Ensure util mocks are applied before any tests +})); -describe('Delete Tests', () => { +describe('Attend-Event tests', () => { beforeEach(() => { - jest.clearAllMocks(); + jest.clearAllMocks(); // Clear mocks before each test to avoid interference }); const userData = { - auth_email: 'director@test.org', + auth_email: 'authUser@test.org', auth_token: 'mockToken', - user_email: 'target@test.org', - } - const path = '/delete' - const httpMethod = 'POST' - - const findOneMock = util.MongoDB.getInstance('uri').getCollection('users').findOne as jest.Mock - const deleteOneMock = util.MongoDB.getInstance('uri').getCollection('users').deleteOne as jest.Mock - + qr: 'test@test.org', + event: 'lunch', + limit: 1, + }; + const path = '/attend-event'; + const httpMethod = 'POST'; + + // this will make it more concise and easier to understand (mocking) + const findOneMock = util.MongoDB.getInstance('uri').getCollection('users').findOne as jest.Mock; + const mockCallback = jest.fn(); + + // case 1: auth token is not valid it('auth token is not valid', async () => { - const mockEvent = createEvent(userData, path, httpMethod) + const mockEvent = createEvent(userData, path, httpMethod); - const result = await main(mockEvent, mockContext, undefined) + const result = await main(mockEvent, mockContext, mockCallback); - expect(result.statusCode).toBe(401) - expect(JSON.parse(result.body).message).toBe('Unauthorized') - }) + expect(result.statusCode).toBe(401); + expect(JSON.parse(result.body).message).toBe('Unauthorized.'); + }); + // case 2 it('user does not exist', async () => { - findOneMock.mockReturnValueOnce(null) - const mockEvent = createEvent(userData, path, httpMethod) + findOneMock.mockReturnValueOnce(null); + const mockEvent = createEvent(userData, path, httpMethod); - const result = await main(mockEvent, mockContext, undefined) + const result = await main(mockEvent, mockContext, mockCallback); - expect(result.statusCode).toBe(404) - expect(JSON.parse(result.body).message).toBe('User not found') - }) + expect(result.statusCode).toBe(404); + expect(JSON.parse(result.body).message).toBe('User not found.'); + }); + // case 3 it('Auth user does not have director/organizer role', async () => { - findOneMock - .mockReturnValueOnce({}) - .mockReturnValueOnce({ role: { hacker: false, volunteer: true, judge: false, sponsor: false, mentor: false, organizer: false, director: false } }) - const mockEvent = createEvent(userData, path, httpMethod) - - const result = await main(mockEvent, mockContext, undefined) - - expect(result.statusCode).toBe(401) - expect(JSON.parse(result.body).message).toBe('Only directors/organizers can call this endpoint.') - }) + findOneMock.mockReturnValueOnce({}).mockReturnValueOnce({ + role: { + hacker: false, + volunteer: true, + judge: false, + sponsor: false, + mentor: false, + organizer: false, + director: false, + }, + }); + const mockEvent = createEvent(userData, path, httpMethod); + + const result = await main(mockEvent, mockContext, mockCallback); + + expect(result.statusCode).toBe(401); + expect(JSON.parse(result.body).message).toBe('Only directors/organizers can call this endpoint.'); + }); - it('returns 500 when deleteOne does not delete', async () => { + // case 4 + it('user tries to check into an event the second time but it can only be attended once', async () => { findOneMock - .mockReturnValueOnce({ email: userData.user_email }) - .mockReturnValueOnce({ role: { hacker: false, organizer: false, director: true } }) - deleteOneMock.mockReturnValueOnce({ deletedCount: 0 }) - const mockEvent = createEvent(userData, path, httpMethod) - - const result = await main(mockEvent, mockContext, undefined) - - expect(result.statusCode).toBe(500) - expect(JSON.parse(result.body).message).toBe('Internal server error') - }) + .mockReturnValueOnce({ + day_of: { + event: { + lunch: { + attend: 1, + }, + }, + }, + registration_status: 'checked_in', + }) + .mockReturnValueOnce({ + role: { + hacker: false, + volunteer: false, + judge: false, + sponsor: false, + mentor: false, + organizer: true, + director: false, + }, + }); + const mockEvent = createEvent(userData, path, httpMethod); + + const result = await main(mockEvent, mockContext, mockCallback); + + expect(result.statusCode).toBe(409); + expect(JSON.parse(result.body).message).toBe('User already checked into event.'); + }); - it('deletes user successfully', async () => { + // case 5 + it('success check-in to an event', async () => { findOneMock - .mockReturnValueOnce({ email: userData.user_email }) - .mockReturnValueOnce({ role: { hacker: false, volunteer: true, organizer: true, director: false } }) - deleteOneMock.mockReturnValueOnce({ deletedCount: 1 }) - const mockEvent = createEvent(userData, path, httpMethod) - - const result = await main(mockEvent, mockContext, undefined) + .mockReturnValueOnce({ + day_of: {}, + registration_status: 'checked_in', + }) + .mockReturnValueOnce({ + role: { + hacker: false, + volunteer: true, + judge: false, + sponsor: false, + mentor: false, + organizer: false, + director: true, + }, + }); + const mockEvent = createEvent(userData, path, httpMethod); + const result = await main(mockEvent, mockContext, mockCallback); + + expect(result.statusCode).toBe(200); + expect(JSON.parse(result.body).message).toBe('user successfully checked into event'); + }); - expect(result.statusCode).toBe(200) - expect(JSON.parse(result.body).message).toBe(`Deleted ${userData.user_email} successfully`) - }) -}) + it('user tries to attend event when they are not checked_in', async () => { + findOneMock + .mockReturnValueOnce({ + day_of: {}, + registration_status: 'registered', + }) + .mockReturnValueOnce({ + role: { + hacker: false, + volunteer: true, + judge: false, + sponsor: false, + mentor: false, + organizer: false, + director: true, + }, + }); + const mockEvent = createEvent(userData, path, httpMethod); + const result = await main(mockEvent, mockContext, mockCallback); + + expect(result.statusCode).toBe(409); + expect(JSON.parse(result.body).message).toBe('User has not checked in. Current status is registered'); + }); +}); diff --git a/tests/delete.test.ts b/tests/delete.test.ts index 98738f2..fcf81d7 100644 --- a/tests/delete.test.ts +++ b/tests/delete.test.ts @@ -58,19 +58,17 @@ describe('Delete Tests', () => { }); it('Auth user does not have director/organizer role', async () => { - findOneMock - .mockReturnValueOnce({}) - .mockReturnValueOnce({ - role: { - hacker: false, - volunteer: true, - judge: false, - sponsor: false, - mentor: false, - organizer: false, - director: false, - }, - }); + findOneMock.mockReturnValueOnce({}).mockReturnValueOnce({ + role: { + hacker: false, + volunteer: true, + judge: false, + sponsor: false, + mentor: false, + organizer: false, + director: false, + }, + }); const mockEvent = createEvent(userData, path, httpMethod); const result = await main(mockEvent, mockContext, undefined); From 803661f27343d7729507abcd156557c6ced012a2 Mon Sep 17 00:00:00 2001 From: RyamL1221 Date: Fri, 9 May 2025 16:17:56 -0400 Subject: [PATCH 23/31] changed delete schema and added endpoint to serverless configuration --- serverless.ts | 2 ++ src/functions/delete/schema.ts | 4 ++-- src/functions/index.ts | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/serverless.ts b/serverless.ts index a293b48..0d0d09b 100644 --- a/serverless.ts +++ b/serverless.ts @@ -17,6 +17,7 @@ import updateBuyIns from '@functions/update-buy-ins'; import getBuyIns from '@functions/get-buy-ins'; import notifyByEmail from '@functions/notify-by-email'; import verifyEmail from '@functions/verify-email'; +import deleteUser from '@functions/delete'; import * as path from 'path'; import * as dotenv from 'dotenv'; @@ -60,6 +61,7 @@ const serverlessConfiguration: AWS = { getBuyIns, notifyByEmail, verifyEmail, + deleteUser, }, package: { individually: true, patterns: ['!.env*', '.env.vault'] }, custom: { diff --git a/src/functions/delete/schema.ts b/src/functions/delete/schema.ts index b3c18fd..a43c1e7 100644 --- a/src/functions/delete/schema.ts +++ b/src/functions/delete/schema.ts @@ -1,9 +1,9 @@ export default { type: 'object', properties: { - email: { type: 'string', format: 'email' }, + user_email: { type: 'string', format: 'email' }, auth_email: { type: 'string', format: 'email' }, auth_token: { type: 'string' }, }, - required: ['email', 'auth_token', 'auth_email'], + required: ['user_email', 'auth_token', 'auth_email'], } as const; diff --git a/src/functions/index.ts b/src/functions/index.ts index a30a938..4d35482 100644 --- a/src/functions/index.ts +++ b/src/functions/index.ts @@ -15,3 +15,4 @@ export { default as updateBuyIns } from './update-buy-ins'; export { default as getBuyIns } from './get-buy-ins'; export { default as notifyByEmail } from './notify-by-email'; export { default as verifyEmail } from './verify-email'; +export { default as delete } from './delete'; From 819c3dcc221c2a2bdc179d58ecaf3f4a9fc5ca4f Mon Sep 17 00:00:00 2001 From: poojakedia Date: Fri, 23 May 2025 00:20:12 -0400 Subject: [PATCH 24/31] Add user-exists endpoint and unit tests --- serverless.ts | 2 + src/functions/index.ts | 1 + src/functions/user-exists/handler.ts | 70 ++++++++++++++++ src/functions/user-exists/index.ts | 20 +++++ src/functions/user-exists/schema.ts | 9 +++ tests/user-exists.test.ts | 116 +++++++++++++++++++++++++++ 6 files changed, 218 insertions(+) create mode 100644 src/functions/user-exists/handler.ts create mode 100644 src/functions/user-exists/index.ts create mode 100644 src/functions/user-exists/schema.ts create mode 100644 tests/user-exists.test.ts diff --git a/serverless.ts b/serverless.ts index 0d0d09b..270b6bc 100644 --- a/serverless.ts +++ b/serverless.ts @@ -18,6 +18,7 @@ import getBuyIns from '@functions/get-buy-ins'; import notifyByEmail from '@functions/notify-by-email'; import verifyEmail from '@functions/verify-email'; import deleteUser from '@functions/delete'; +import userExists from '@functions/user-exists'; import * as path from 'path'; import * as dotenv from 'dotenv'; @@ -62,6 +63,7 @@ const serverlessConfiguration: AWS = { notifyByEmail, verifyEmail, deleteUser, + userExists, }, package: { individually: true, patterns: ['!.env*', '.env.vault'] }, custom: { diff --git a/src/functions/index.ts b/src/functions/index.ts index 4d35482..dd66325 100644 --- a/src/functions/index.ts +++ b/src/functions/index.ts @@ -16,3 +16,4 @@ export { default as getBuyIns } from './get-buy-ins'; export { default as notifyByEmail } from './notify-by-email'; export { default as verifyEmail } from './verify-email'; export { default as delete } from './delete'; +export { default as userExists } from './user-exists'; diff --git a/src/functions/user-exists/handler.ts b/src/functions/user-exists/handler.ts new file mode 100644 index 0000000..5f73afb --- /dev/null +++ b/src/functions/user-exists/handler.ts @@ -0,0 +1,70 @@ +import type { ValidatedEventAPIGatewayProxyEvent } from '@libs/api-gateway'; +import { middyfy } from '@libs/lambda'; +import schema from './schema'; +import { MongoDB, validateToken } from '../../util'; +import * as path from 'path'; +import * as dotenv from 'dotenv'; +dotenv.config({ path: path.resolve(process.cwd(), '.env') }); + +const userExists: ValidatedEventAPIGatewayProxyEvent = async (event) => { + try { + //Check token validity + const isValidToken = validateToken(event.body.auth_token, process.env.JWT_SECRET, event.body.auth_email); + if (!isValidToken) { + return { + statusCode: 401, + body: JSON.stringify({ + statuscode: 401, + message: 'Unauthorized', + }), + }; + } + + //Connect to DB + const db = MongoDB.getInstance(process.env.MONGO_URI); + await db.connect(); + const users = db.getCollection('users'); + + const authUser = await users.findOne({ email: event.body.auth_email }); + if (!authUser) { + return { + statusCode: 404, + body: JSON.stringify({ + statuscode: 404, + message: 'Auth user not found.', + }), + }; + } + + //Check if user being looked up exists + const lookupUser = await users.findOne( + { email: event.body.email.toLowerCase() }, + { projection: { password: 0, _id: 0 } } + ); + if (!lookupUser) { + return { + statusCode: 404, + body: JSON.stringify({ + statusCode: 404, + message: 'Look-up user was not found', + }), + }; + } + //return that user exists + return { + statusCode: 200, + body: JSON.stringify('User exists'), + }; + } catch (error) { + console.error('Error reading user:', error); + return { + statusCode: 500, + body: JSON.stringify({ + statusCode: 500, + message: 'Internal server error.', + error, + }), + }; + } +}; +export const main = middyfy(userExists); diff --git a/src/functions/user-exists/index.ts b/src/functions/user-exists/index.ts new file mode 100644 index 0000000..d7eb33e --- /dev/null +++ b/src/functions/user-exists/index.ts @@ -0,0 +1,20 @@ +import { handlerPath } from '@libs/handler-resolver'; +import schema from './schema'; + +export default { + handler: `${handlerPath(__dirname)}/handler.main`, + events: [ + { + http: { + method: 'post', + path: 'user-exists', + cors: true, + request: { + schemas: { + 'application/json': schema, + }, + }, + }, + }, + ], +}; diff --git a/src/functions/user-exists/schema.ts b/src/functions/user-exists/schema.ts new file mode 100644 index 0000000..4f00ad4 --- /dev/null +++ b/src/functions/user-exists/schema.ts @@ -0,0 +1,9 @@ +export default { + type: 'object', + properties: { + auth_email: { type: 'string', format: 'email' }, + auth_token: { type: 'string' }, + email: { type: 'string', format: 'email' }, + }, + required: ['auth_email', 'auth_token', 'email'], +} as const; diff --git a/tests/user-exists.test.ts b/tests/user-exists.test.ts new file mode 100644 index 0000000..15bf04c --- /dev/null +++ b/tests/user-exists.test.ts @@ -0,0 +1,116 @@ +import { main } from '../src/functions/user-exists/handler'; + +import { createEvent, mockContext } from './helper'; +import * as util from '../src/util'; + +jest.mock('../src/util', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + MongoDB: { + getInstance: jest.fn().mockReturnValue({ + connect: jest.fn(), + disconnect: jest.fn(), + getCollection: jest.fn().mockReturnValue({ + findOne: jest.fn(), + find: jest.fn().mockReturnValue({ + toArray: jest.fn(), + }), + }), + }), + }, + validateToken: jest.fn().mockReturnValueOnce(false).mockReturnValue(true), +})); + +describe('/user-exists endpoint', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + const path = '/user-exists'; + const httpMethod = 'POST'; + + const findOneMock = util.MongoDB.getInstance('uri').getCollection('users').findOne as jest.Mock; + const mockCallback = jest.fn(); + + it('invalid token', async () => { + const userData = { + auth_email: 'hacker@hackru.org', + auth_token: 'mockToken', + email: 'hacker@hackru.org', + }; + const mockEvent = createEvent(userData, path, httpMethod); + + const res = await main(mockEvent, mockContext, mockCallback); + expect(res.statusCode).toBe(401); + expect(JSON.parse(res.body).message).toBe('Unauthorized'); + }); + + it('auth user not found', async () => { + findOneMock.mockReturnValueOnce(null); + const userData = { + auth_email: 'non-existent-user@hackru.org', + auth_token: 'mockToken', + email: 'hacker@hackru.org', + }; + const mockEvent = createEvent(userData, path, httpMethod); + + const res = await main(mockEvent, mockContext, mockCallback); + expect(res.statusCode).toBe(404); + expect(JSON.parse(res.body).message).toBe('Auth user not found.'); + }); + + it('look-up user not found', async () => { + findOneMock + .mockReturnValueOnce({ + email: 'hackerCheck@hackru.org', + }) + .mockReturnValueOnce(null); + const userData = { + auth_email: 'hackerCheck@hackru.org', + auth_token: 'mockToken', + email: 'non-existent-user@hackru.org', + }; + const mockEvent = createEvent(userData, path, httpMethod); + + const res = await main(mockEvent, mockContext, mockCallback); + + expect(res.statusCode).toBe(404); + expect(JSON.parse(res.body).message).toBe('Look-up user was not found'); + }); + + it('success case', async () => { + //can check even if you don't have the admin role + findOneMock + .mockReturnValueOnce({ + email: 'hackerCheck@hackru.org', + }) + .mockReturnValueOnce({}); + const userData = { + auth_email: 'hackerCheck@hackru.org', + auth_token: 'mockToken', + email: 'hacker@hackru.org', + }; + const mockEvent = createEvent(userData, path, httpMethod); + + const res = await main(mockEvent, mockContext, mockCallback); + + expect(res.statusCode).toBe(200); + expect(res.body).toBeDefined(); + }); + + it('success case for all lookup', async () => { + findOneMock + .mockReturnValueOnce({ + email: 'hackerCheck@hackru.org', + }) + .mockReturnValueOnce({ email: 'targetHacker@hackru.org' }); + const userData = { + auth_email: 'hackerCheck@hackru.org', + auth_token: 'mockToken', + email: 'anyemail@hackru.org', + }; + const mockEvent = createEvent(userData, path, httpMethod); + const res = await main(mockEvent, mockContext, mockCallback); + console.log(res.body); + expect(res.statusCode).toBe(200); + expect(JSON.parse(res.body)).toEqual('User exists'); + }); +}); From 8343d5b8425e2f2f74407d2340bad8c996ace908 Mon Sep 17 00:00:00 2001 From: Andrew Somers <67980093+avsomers25@users.noreply.github.com> Date: Fri, 23 May 2025 18:19:14 -0400 Subject: [PATCH 25/31] Update config.ts --- src/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.ts b/src/config.ts index 036533f..62b4e1c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,2 +1,2 @@ -export const registrationStart = '01/01/25'; -export const registrationEnd = '02/03/25'; +export const registrationStart = '1/01/25'; +export const registrationEnd = '10/06/25'; From 397f810ed6139bb59471402480bf78ebdf36b0d9 Mon Sep 17 00:00:00 2001 From: Ayoobf Date: Fri, 30 May 2025 16:54:17 -0400 Subject: [PATCH 26/31] Added starter code for Siya to finish. --- serverless.ts | 2 + src/functions/index.ts | 1 + src/functions/interest-form/handler.ts | 64 ++++++++++++++++++++++++++ src/functions/interest-form/index.ts | 20 ++++++++ src/functions/interest-form/schema.ts | 30 ++++++++++++ 5 files changed, 117 insertions(+) create mode 100644 src/functions/interest-form/handler.ts create mode 100644 src/functions/interest-form/index.ts create mode 100644 src/functions/interest-form/schema.ts diff --git a/serverless.ts b/serverless.ts index 270b6bc..e609c25 100644 --- a/serverless.ts +++ b/serverless.ts @@ -19,6 +19,7 @@ import notifyByEmail from '@functions/notify-by-email'; import verifyEmail from '@functions/verify-email'; import deleteUser from '@functions/delete'; import userExists from '@functions/user-exists'; +import interestForm from '@functions/interest-form'; import * as path from 'path'; import * as dotenv from 'dotenv'; @@ -64,6 +65,7 @@ const serverlessConfiguration: AWS = { verifyEmail, deleteUser, userExists, + interestForm, }, package: { individually: true, patterns: ['!.env*', '.env.vault'] }, custom: { diff --git a/src/functions/index.ts b/src/functions/index.ts index dd66325..c8ad166 100644 --- a/src/functions/index.ts +++ b/src/functions/index.ts @@ -17,3 +17,4 @@ export { default as notifyByEmail } from './notify-by-email'; export { default as verifyEmail } from './verify-email'; export { default as delete } from './delete'; export { default as userExists } from './user-exists'; +export { default as interestForm } from './interest-form'; diff --git a/src/functions/interest-form/handler.ts b/src/functions/interest-form/handler.ts new file mode 100644 index 0000000..f7de505 --- /dev/null +++ b/src/functions/interest-form/handler.ts @@ -0,0 +1,64 @@ +import type { ValidatedEventAPIGatewayProxyEvent } from '@libs/api-gateway'; +import { middyfy } from '@libs/lambda'; +import { MongoDB } from '../../util'; +import * as path from 'path'; +import * as dotenv from 'dotenv'; +dotenv.config({ path: path.resolve(process.cwd(), '.env') }); + +import schema from './schema'; + +const submitInterestForm: ValidatedEventAPIGatewayProxyEvent = async (event) => { + try { + // Destructure all the new fields from event.body + const { + firstName, + lastName, + age, + phoneNumber, + email, + school, + levelOfStudy, + countryOfResidence, + linkedInUrl, + mlh_code_of_conduct, + mlh_privacy_policy, + mlh_terms_and_conditions, + } = event.body; + + // Connect to database + const db = MongoDB.getInstance(process.env.MONGO_URI); + await db.connect(); + const interestFormsCollection = db.getCollection('interest-forms'); // Target collection + + // Create the document to insert + const docToInsert = { + firstName, + lastName, + /*TODO: Add other fields here*/ + }; + + const result = // TODO: Insert the document you created above into the interestFormsCollection + + // Return success + return { + statusCode: /*TODO: Success Code Here */, + body: JSON.stringify({ + message: /*TODO: Success Message Here */, + submissionId: result.insertedId, + }), + }; + + } catch (error) { + console.error('Error submitting interest form:', error); + return { + statusCode: 500, + body: JSON.stringify({ + statusCode: 500, + message: 'Internal Server Error', + error: error.message, + }), + }; + } +}; + +export const main = middyfy(submitInterestForm); \ No newline at end of file diff --git a/src/functions/interest-form/index.ts b/src/functions/interest-form/index.ts new file mode 100644 index 0000000..1c3e476 --- /dev/null +++ b/src/functions/interest-form/index.ts @@ -0,0 +1,20 @@ +import { handlerPath } from '@libs/handler-resolver'; +import schema from './schema'; + +export default { + handler: `${handlerPath(__dirname)}/handler.main`, + events: [ + { + http: { + method: 'post', + path: 'interest-form', + cors: true, + request: { + schemas: { + 'application/json': schema, + }, + }, + }, + }, + ], +}; diff --git a/src/functions/interest-form/schema.ts b/src/functions/interest-form/schema.ts new file mode 100644 index 0000000..fb4b568 --- /dev/null +++ b/src/functions/interest-form/schema.ts @@ -0,0 +1,30 @@ +export default { + type: 'object', + properties: { + firstName: { type: 'string' }, + lastName: { type: 'string' }, + age: { type: 'number' }, + phoneNumber: { type: 'string' }, + email: { type: 'string', format: 'email' }, + school: { type: 'string' }, + levelOfStudy: { type: 'string' }, + countryOfResidence: { type: 'string' }, + linkedInUrl: { type: 'string', format: 'uri' }, + mlh_code_of_conduct: { type: 'boolean' }, + mlh_privacy_policy: { type: 'boolean' }, + mlh_terms_and_conditions: { type: 'boolean' }, + }, + required: [ + 'firstName', + 'lastName', + 'age', + 'phoneNumber', + 'email', + 'school', + 'levelOfStudy', + 'countryOfResidence', + 'mlh_code_of_conduct', + 'mlh_privacy_policy', + 'mlh_terms_and_conditions', + ], +} as const; \ No newline at end of file From 6abbc6f63d577b7facb20685fd6eca609e609ada Mon Sep 17 00:00:00 2001 From: Siya Goyal Date: Sat, 7 Jun 2025 02:06:01 -0400 Subject: [PATCH 27/31] Completed TODO tasks for backend of interest form --- src/functions/interest-form/handler.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/functions/interest-form/handler.ts b/src/functions/interest-form/handler.ts index f7de505..f809a02 100644 --- a/src/functions/interest-form/handler.ts +++ b/src/functions/interest-form/handler.ts @@ -34,16 +34,25 @@ const submitInterestForm: ValidatedEventAPIGatewayProxyEvent = as const docToInsert = { firstName, lastName, - /*TODO: Add other fields here*/ + age, + phoneNumber, + email, + school, + levelOfStudy, + countryOfResidence, + linkedInUrl, + mlh_code_of_conduct, + mlh_privacy_policy, + mlh_terms_and_conditions, }; - const result = // TODO: Insert the document you created above into the interestFormsCollection + const result = await interestFormsCollection.insertOne(docToInsert); // Return success return { - statusCode: /*TODO: Success Code Here */, + statusCode: 200, body: JSON.stringify({ - message: /*TODO: Success Message Here */, + message: 'Successful Form Submission', submissionId: result.insertedId, }), }; From 0b76a97b4ba4883d3c7aa8c7414b1dabf0b04b15 Mon Sep 17 00:00:00 2001 From: Ayoobf Date: Sat, 7 Jun 2025 22:04:08 -0400 Subject: [PATCH 28/31] Ran prettier --- src/functions/interest-form/handler.ts | 6 +++--- src/functions/interest-form/schema.ts | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/functions/interest-form/handler.ts b/src/functions/interest-form/handler.ts index f809a02..38b3426 100644 --- a/src/functions/interest-form/handler.ts +++ b/src/functions/interest-form/handler.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import type { ValidatedEventAPIGatewayProxyEvent } from '@libs/api-gateway'; import { middyfy } from '@libs/lambda'; import { MongoDB } from '../../util'; @@ -50,13 +51,12 @@ const submitInterestForm: ValidatedEventAPIGatewayProxyEvent = as // Return success return { - statusCode: 200, + statusCode: 200, body: JSON.stringify({ message: 'Successful Form Submission', submissionId: result.insertedId, }), }; - } catch (error) { console.error('Error submitting interest form:', error); return { @@ -70,4 +70,4 @@ const submitInterestForm: ValidatedEventAPIGatewayProxyEvent = as } }; -export const main = middyfy(submitInterestForm); \ No newline at end of file +export const main = middyfy(submitInterestForm); diff --git a/src/functions/interest-form/schema.ts b/src/functions/interest-form/schema.ts index fb4b568..03a6d09 100644 --- a/src/functions/interest-form/schema.ts +++ b/src/functions/interest-form/schema.ts @@ -10,9 +10,9 @@ export default { levelOfStudy: { type: 'string' }, countryOfResidence: { type: 'string' }, linkedInUrl: { type: 'string', format: 'uri' }, - mlh_code_of_conduct: { type: 'boolean' }, - mlh_privacy_policy: { type: 'boolean' }, - mlh_terms_and_conditions: { type: 'boolean' }, + mlh_code_of_conduct: { type: 'boolean' }, + mlh_privacy_policy: { type: 'boolean' }, + mlh_terms_and_conditions: { type: 'boolean' }, }, required: [ 'firstName', @@ -24,7 +24,7 @@ export default { 'levelOfStudy', 'countryOfResidence', 'mlh_code_of_conduct', - 'mlh_privacy_policy', + 'mlh_privacy_policy', 'mlh_terms_and_conditions', ], -} as const; \ No newline at end of file +} as const; From 957708d28428a6480d403aa5ae363d140f794b5c Mon Sep 17 00:00:00 2001 From: Ayoobf Date: Sat, 7 Jun 2025 23:05:44 -0400 Subject: [PATCH 29/31] Added Tests and better validation --- src/functions/interest-form/handler.ts | 16 ++- tests/interest-form.test.ts | 179 +++++++++++++++++++++++++ 2 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 tests/interest-form.test.ts diff --git a/src/functions/interest-form/handler.ts b/src/functions/interest-form/handler.ts index 38b3426..a9e0d0b 100644 --- a/src/functions/interest-form/handler.ts +++ b/src/functions/interest-form/handler.ts @@ -26,6 +26,20 @@ const submitInterestForm: ValidatedEventAPIGatewayProxyEvent = as mlh_terms_and_conditions, } = event.body; + // Validate LinkedIn URL format if provided + if (linkedInUrl && linkedInUrl.trim() !== '') { + const linkedInRegex = /^https?:\/\/(www\.)?linkedin\.com\/in\/[a-zA-Z0-9-]+\/?$/; + if (!linkedInRegex.test(linkedInUrl.trim())) { + return { + statusCode: 422, + body: JSON.stringify({ + statusCode: 422, + message: 'Please provide a valid LinkedIn profile URL (e.g., https://linkedin.com/in/yourname)', + }), + }; + } + } + // Connect to database const db = MongoDB.getInstance(process.env.MONGO_URI); await db.connect(); @@ -35,7 +49,7 @@ const submitInterestForm: ValidatedEventAPIGatewayProxyEvent = as const docToInsert = { firstName, lastName, - age, + age: age, phoneNumber, email, school, diff --git a/tests/interest-form.test.ts b/tests/interest-form.test.ts new file mode 100644 index 0000000..9bde9be --- /dev/null +++ b/tests/interest-form.test.ts @@ -0,0 +1,179 @@ +import { main } from '../src/functions/interest-form/handler'; +import { createEvent, mockContext } from './helper'; + +// Mock the MongoDB utility +jest.mock('../src/util', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + MongoDB: { + getInstance: jest.fn().mockReturnValue({ + connect: jest.fn(), + disconnect: jest.fn(), + getCollection: jest.fn().mockReturnValue({ + insertOne: jest.fn(), + }), + }), + }, +})); + +// Mock environment variables +jest.mock('dotenv', () => ({ + config: jest.fn(), +})); + +jest.mock('path', () => ({ + resolve: jest.fn().mockReturnValue('.env'), +})); + +import * as util from '../src/util'; + +describe('Submit Interest Form tests', () => { + beforeEach(() => { + jest.clearAllMocks(); + process.env.MONGO_URI = 'mongodb://test-uri'; + }); + + const validFormData = { + firstName: 'John', + lastName: 'Doe', + age: 20, + phoneNumber: '+1234567890', + email: 'john.doe@test.org', + school: 'Test University', + levelOfStudy: 'Undergraduate', + countryOfResidence: 'United States', + linkedInUrl: 'https://linkedin.com/in/johndoe', + mlh_code_of_conduct: true, + mlh_privacy_policy: true, + mlh_terms_and_conditions: true, + }; + + const path = 'interest-form'; + const httpMethod = 'POST'; + + // Mock references for easier testing + const connectMock = util.MongoDB.getInstance('uri').connect as jest.Mock; + const getCollectionMock = util.MongoDB.getInstance('uri').getCollection as jest.Mock; + const insertOneMock = util.MongoDB.getInstance('uri').getCollection('interest-forms').insertOne as jest.Mock; + const mockCallback = jest.fn(); + + it('successfully submits interest form', async () => { + const mockInsertResult = { + insertedId: 'mock-object-id-123', + }; + + insertOneMock.mockResolvedValue(mockInsertResult); + + const mockEvent = createEvent(validFormData, path, httpMethod); + const result = await main(mockEvent, mockContext, mockCallback); + + expect(connectMock).toHaveBeenCalled(); + expect(getCollectionMock).toHaveBeenCalledWith('interest-forms'); + expect(insertOneMock).toHaveBeenCalledWith(validFormData); + + expect(result.statusCode).toBe(200); + expect(JSON.parse(result.body).message).toBe('Successful Form Submission'); + expect(JSON.parse(result.body).submissionId).toBe('mock-object-id-123'); + }); + + it('successfully submits form with different valid data', async () => { + const differentValidData = { + firstName: 'Jane', + lastName: 'Smith', + age: 22, + phoneNumber: '+9876543210', + email: 'jane.smith@university.edu', + school: 'Different University', + levelOfStudy: 'Graduate', + countryOfResidence: 'Canada', + linkedInUrl: 'https://linkedin.com/in/janesmith', + mlh_code_of_conduct: true, + mlh_privacy_policy: true, + mlh_terms_and_conditions: false, + }; + + const mockInsertResult = { + insertedId: 'mock-object-id-456', + }; + + insertOneMock.mockResolvedValue(mockInsertResult); + + const mockEvent = createEvent(differentValidData, path, httpMethod); + const result = await main(mockEvent, mockContext, mockCallback); + + expect(result.statusCode).toBe(200); + expect(JSON.parse(result.body).message).toBe('Successful Form Submission'); + expect(insertOneMock).toHaveBeenCalledWith(differentValidData); + }); + + it('successfully submits form with minimal required fields', async () => { + const minimalFormData = { + firstName: 'Bob', + lastName: 'Johnson', + age: 19, + phoneNumber: '+1555123456', + email: 'bob.johnson@college.edu', + school: 'Community College', + levelOfStudy: 'Undergraduate', + countryOfResidence: 'United States', + linkedInUrl: '', // Optional field - empty string + mlh_code_of_conduct: true, + mlh_privacy_policy: true, + mlh_terms_and_conditions: true, + }; + + const mockInsertResult = { + insertedId: 'mock-object-id-minimal', + }; + + insertOneMock.mockResolvedValue(mockInsertResult); + + const mockEvent = createEvent(minimalFormData, path, httpMethod); + const result = await main(mockEvent, mockContext, mockCallback); + + expect(result.statusCode).toBe(200); + expect(JSON.parse(result.body).message).toBe('Successful Form Submission'); + expect(insertOneMock).toHaveBeenCalledWith(minimalFormData); + }); + + it('handles invalid data causing runtime error', async () => { + // Simulate invalid data that somehow bypasses schema validation + const invalidFormData = { + firstName: 'Test', + lastName: 'Test', + age: 20, + phoneNumber: '+1234567890', + email: 'test@test.org', + school: 'Test School', + levelOfStudy: 'Undergraduate', + countryOfResidence: 'Test Country', + linkedInUrl: 'https://linkedin.com/in/HelloWorld', + mlh_code_of_conduct: 'HI', // Invalid boolean value + mlh_privacy_policy: true, + mlh_terms_and_conditions: true, + }; + + // Mock insertOne to throw an error due to invalid data + insertOneMock.mockRejectedValue(new Error('Invalid data format')); + + const mockEvent = createEvent(invalidFormData, path, httpMethod); + const result = await main(mockEvent, mockContext, mockCallback); + + expect(result.statusCode).toBe(500); + expect(JSON.parse(result.body).message).toBe('Internal Server Error'); + expect(JSON.parse(result.body).error).toBe('Invalid data format'); + }); + it('successfully submits form with valid LinkedIn URL', async () => { + const mockInsertResult = { + insertedId: 'mock-object-id-123', + }; + + insertOneMock.mockResolvedValue(mockInsertResult); + + const mockEvent = createEvent(validFormData, path, httpMethod); + const result = await main(mockEvent, mockContext, mockCallback); + + expect(result.statusCode).toBe(200); + expect(JSON.parse(result.body).message).toBe('Successful Form Submission'); + expect(insertOneMock).toHaveBeenCalledWith(validFormData); + }); +}); From ead51479f35dceaaf73e47eb584e80b631aaa197 Mon Sep 17 00:00:00 2001 From: Ayoobf Date: Tue, 24 Jun 2025 00:14:52 -0400 Subject: [PATCH 30/31] Removed the 'uri' requirement from the schema.tsx of the interest form endpoint --- src/functions/interest-form/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions/interest-form/schema.ts b/src/functions/interest-form/schema.ts index 03a6d09..3dc4c3c 100644 --- a/src/functions/interest-form/schema.ts +++ b/src/functions/interest-form/schema.ts @@ -9,7 +9,7 @@ export default { school: { type: 'string' }, levelOfStudy: { type: 'string' }, countryOfResidence: { type: 'string' }, - linkedInUrl: { type: 'string', format: 'uri' }, + linkedInUrl: { type: 'string'}, mlh_code_of_conduct: { type: 'boolean' }, mlh_privacy_policy: { type: 'boolean' }, mlh_terms_and_conditions: { type: 'boolean' }, From 82685c19a04a0b6025231e8cf3affe6bac5cd5ed Mon Sep 17 00:00:00 2001 From: Ayoobf Date: Tue, 24 Jun 2025 00:17:10 -0400 Subject: [PATCH 31/31] Ran Prettier --- src/functions/interest-form/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions/interest-form/schema.ts b/src/functions/interest-form/schema.ts index 3dc4c3c..ba4fad0 100644 --- a/src/functions/interest-form/schema.ts +++ b/src/functions/interest-form/schema.ts @@ -9,7 +9,7 @@ export default { school: { type: 'string' }, levelOfStudy: { type: 'string' }, countryOfResidence: { type: 'string' }, - linkedInUrl: { type: 'string'}, + linkedInUrl: { type: 'string' }, mlh_code_of_conduct: { type: 'boolean' }, mlh_privacy_policy: { type: 'boolean' }, mlh_terms_and_conditions: { type: 'boolean' },