diff --git a/serverless.ts b/serverless.ts index 5a9fa8f..f1b3d89 100644 --- a/serverless.ts +++ b/serverless.ts @@ -11,6 +11,7 @@ import waiver from '@functions/waiver'; import resume from '@functions/resume'; import resetPassword from '@functions/reset-password'; import forgotPassword from '@functions/forgot-password'; +import points from '@functions/points'; import * as path from 'path'; import * as dotenv from 'dotenv'; @@ -47,6 +48,7 @@ const serverlessConfiguration: AWS = { discord, forgotPassword, resetPassword, + points, }, package: { individually: true, patterns: ['!.env*', '.env.vault'] }, custom: { diff --git a/src/functions/index.ts b/src/functions/index.ts index 69c326f..b784a7d 100644 --- a/src/functions/index.ts +++ b/src/functions/index.ts @@ -9,3 +9,4 @@ export { default as waiver } from './waiver'; export { default as resume } from './resume'; export { default as resetPassword } from './reset-password'; export { default as forgotPassword } from './forgot-password'; +export { default as points } from './points'; diff --git a/src/functions/points/handler.ts b/src/functions/points/handler.ts new file mode 100644 index 0000000..f18571c --- /dev/null +++ b/src/functions/points/handler.ts @@ -0,0 +1,75 @@ +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 points: ValidatedEventAPIGatewayProxyEvent = async (event) => { + const email = event.body.email.toLowerCase(); + + try { + // check token + const isValidToken = validateToken(event.body.auth_token, process.env.JWT_SECRET, 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 pointsCollection = db.getCollection('f24-points-syst'); + + // Make sure user exists + const user = await users.findOne({ email: email }); + if (!user) { + return { + statusCode: 404, + body: JSON.stringify({ + statusCode: 404, + message: 'User not found.', + }), + }; + } + + // get users points + const pointUser = await pointsCollection.findOne({ email: email }); + if (!pointUser) { + return { + statusCode: 404, + body: JSON.stringify({ + statusCode: 404, + message: 'Points not found for this user.', + }), + }; + } + + return { + statusCode: 200, + body: JSON.stringify({ + statusCode: 200, + balance: pointUser.balance, + total_points: pointUser.total_points, + }), + }; + } catch (error) { + return { + statusCode: 500, + body: JSON.stringify({ + statusCode: 500, + message: 'Internal server error.', + error, + }), + }; + } +}; + +export const main = middyfy(points); diff --git a/src/functions/points/index.ts b/src/functions/points/index.ts new file mode 100644 index 0000000..69b82c1 --- /dev/null +++ b/src/functions/points/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: 'points', + cors: true, + request: { + schemas: { + 'application/json': schema, + }, + }, + }, + }, + ], +}; diff --git a/src/functions/points/schema.ts b/src/functions/points/schema.ts new file mode 100644 index 0000000..50207d1 --- /dev/null +++ b/src/functions/points/schema.ts @@ -0,0 +1,8 @@ +export default { + type: 'object', + properties: { + auth_token: { type: 'string' }, + email: { type: 'string', format: 'email' }, + }, + required: ['auth_token', 'email'], +} as const; diff --git a/tests/points.test.ts b/tests/points.test.ts new file mode 100644 index 0000000..b058710 --- /dev/null +++ b/tests/points.test.ts @@ -0,0 +1,111 @@ +import { main } from '../src/functions/points/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(), + }), + }), + }, + validateToken: jest.fn(), +})); + +describe('Points endpoint', () => { + beforeEach(() => { + jest.clearAllMocks(); + process.env.JWT_SECRET = 'test-secret'; + }); + + const path = '/points'; + const httpMethod = 'POST'; + + it('should return 401 for invalid auth token', async () => { + const userData = { + email: 'testab@test.org', + auth_token: 'invalidToken', + }; + const mockEvent = createEvent(userData, path, httpMethod); + (util.validateToken as jest.Mock).mockReturnValue(false); + + const result = await main(mockEvent, mockContext, jest.fn()); + + expect(result.statusCode).toBe(401); + expect(JSON.parse(result.body).message).toBe('Unauthorized'); + }); + + it('should return 404 if user is not found', async () => { + const userData = { + email: 'nonexistent@email.com', + auth_token: 'validToken', + }; + const mockEvent = createEvent(userData, path, httpMethod); + (util.validateToken as jest.Mock).mockReturnValue(true); + const findOneMock = util.MongoDB.getInstance('uri').getCollection('users').findOne as jest.Mock; + findOneMock.mockResolvedValue(null); + + const result = await main(mockEvent, mockContext, jest.fn()); + + expect(result.statusCode).toBe(404); + expect(JSON.parse(result.body).message).toBe('User not found.'); + }); + + it('should return 404 if points not found for user', async () => { + const userData = { + email: 'test@example.com', + auth_token: 'validToken', + }; + const mockEvent = createEvent(userData, path, httpMethod); + (util.validateToken as jest.Mock).mockReturnValue(true); + const findOneMock = util.MongoDB.getInstance('uri').getCollection('').findOne as jest.Mock; + findOneMock.mockResolvedValueOnce({ email: 'test@example.com' }); // user found + findOneMock.mockResolvedValueOnce(null); // points not found + + const result = await main(mockEvent, mockContext, jest.fn()); + + expect(result.statusCode).toBe(404); + expect(JSON.parse(result.body).message).toBe('Points not found for this user.'); + }); + + it('should return 200 with balance and total_points for valid user', async () => { + const userData = { + email: 'valid@email.com', + auth_token: 'validToken', + }; + + const mockEvent = createEvent(userData, path, httpMethod); + (util.validateToken as jest.Mock).mockReturnValue(true); + const findOneMock = util.MongoDB.getInstance('uri').getCollection('users').findOne as jest.Mock; + + findOneMock.mockResolvedValueOnce({ email: 'valid@email.com' }); // User exists + findOneMock.mockResolvedValueOnce({ user_email: 'valid@email.com', balance: 100, total_points: 150 }); // Points found + + const result = await main(mockEvent, mockContext, jest.fn()); + + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.balance).toBe(100); + expect(body.total_points).toBe(150); + }); + + it('should return 500 for internal server error', async () => { + const userData = { + email: 'valid@email.com', + auth_token: 'validToken', + }; + const mockEvent = createEvent(userData, path, httpMethod); + (util.validateToken as jest.Mock).mockReturnValue(true); + const findOneMock = util.MongoDB.getInstance('uri').getCollection('users').findOne as jest.Mock; + findOneMock.mockRejectedValue(new Error('Database error')); + + const result = await main(mockEvent, mockContext, jest.fn()); + + expect(result.statusCode).toBe(500); + expect(JSON.parse(result.body).message).toBe('Internal server error.'); + }); +});