diff --git a/apps/backend/lambdas/users/handler.ts b/apps/backend/lambdas/users/handler.ts index 582ea72..4bcedff 100644 --- a/apps/backend/lambdas/users/handler.ts +++ b/apps/backend/lambdas/users/handler.ts @@ -115,7 +115,54 @@ export const handler = async (event: any): Promise => { return json(200, { ok: true, route: 'PATCH /users/{userId}', pathParams: { userId }, body: { email: updatedUser!.email, name: updatedUser!.name, isAdmin: updatedUser!.is_admin } }); } - // <<< ROUTES-END + // POST /users + if ((normalizedPath === '/users' || normalizedPath === '') && method === 'POST') { + const body = event.body + ? (JSON.parse(event.body) as Record) + : {}; + + // extract fields to create user + let email = body.email as string; + let name = body.name as string; + let isAdmin = body.isAdmin as boolean; + if (!email || !name || typeof isAdmin !== 'boolean') { + return json(400, { message: 'email, name, and isAdmin are required' }); + } + + // Check if user with this email already exists + const existingUser = await db + .selectFrom('branch.users') + .where('email', '=', email) + .selectAll() + .executeTakeFirst(); + + if (existingUser) { + return json(409, { message: 'User with this email already exists' }); + } + + // insert new user (user_id auto-increments) + try { + await db + .insertInto('branch.users') + .values({ email, name, is_admin: isAdmin }) + .execute(); + } catch (err) { + console.error('Database insert error:', err); + return json(500, { message: 'Failed to create user' }); + } + + return json(201, { + ok: true, + route: 'POST /users', + pathParams: {}, + body: { + email, + name, + isAdmin, + }, + }); + } + // <<< ROUTES-END return json(404, { message: 'Not Found', path: normalizedPath, method }); } catch (err) { diff --git a/apps/backend/lambdas/users/openapi.yaml b/apps/backend/lambdas/users/openapi.yaml index 961f810..c639172 100644 --- a/apps/backend/lambdas/users/openapi.yaml +++ b/apps/backend/lambdas/users/openapi.yaml @@ -34,6 +34,28 @@ paths: responses: '200': description: OK + post: + summary: POST /users + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + email: + type: string + isAdmin: + type: boolean + responses: + '201': + description: Success + '400': + description: Bad Request + '500': + description: Internal Server Error /{userId}: patch: summary: PATCH /{userId} diff --git a/apps/backend/lambdas/users/test/user.unit.test.ts b/apps/backend/lambdas/users/test/user.unit.test.ts new file mode 100644 index 0000000..dad172d --- /dev/null +++ b/apps/backend/lambdas/users/test/user.unit.test.ts @@ -0,0 +1,185 @@ +import { describe, test, expect, beforeEach, jest } from '@jest/globals'; + +// Mock the database module BEFORE importing handler +jest.mock('../db'); + +import { handler } from '../handler'; +import db from '../db'; + +const mockDb = db as any; + +// Helper function to create a POST event +function postEvent(body: Record) { + return { + rawPath: '/users', + requestContext: { + http: { + method: 'POST', + }, + }, + body: JSON.stringify(body), + }; +} + +describe('POST /users unit tests', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Input Validation', () => { + test('400: missing email field', async () => { + const res = await handler( + postEvent({ + name: 'John Doe', + isAdmin: false, + }) + ); + + expect(res.statusCode).toBe(400); + const json = JSON.parse(res.body); + expect(json.message).toBeDefined(); + expect(json.message).toContain('required'); + }); + + test('400: missing name field', async () => { + const res = await handler( + postEvent({ + email: 'john@example.com', + isAdmin: false, + }) + ); + + expect(res.statusCode).toBe(400); + const json = JSON.parse(res.body); + expect(json.message).toBeDefined(); + expect(json.message).toContain('required'); + }); + + test('400: missing isAdmin field', async () => { + const res = await handler( + postEvent({ + name: 'John Doe', + email: 'john@example.com', + }) + ); + + expect(res.statusCode).toBe(400); + const json = JSON.parse(res.body); + expect(json.message).toBeDefined(); + expect(json.message).toContain('required'); + }); + + test('400: isAdmin is not a boolean', async () => { + const res = await handler( + postEvent({ + name: 'John Doe', + email: 'john@example.com', + isAdmin: 'yes', + }) + ); + + expect(res.statusCode).toBe(400); + const json = JSON.parse(res.body); + expect(json.message).toBeDefined(); + expect(json.message).toContain('required'); + }); + + test('400: empty email field', async () => { + const res = await handler( + postEvent({ + name: 'John Doe', + email: '', + isAdmin: false, + }) + ); + + expect(res.statusCode).toBe(400); + const json = JSON.parse(res.body); + expect(json.message).toContain('required'); + }); + + test('400: empty name field', async () => { + const res = await handler( + postEvent({ + name: '', + email: 'john@example.com', + isAdmin: false, + }) + ); + + expect(res.statusCode).toBe(400); + const json = JSON.parse(res.body); + expect(json.message).toContain('required'); + }); + }); + + describe('Response Format', () => { + }); + + describe('Success Cases', () => { + test('201: successful POST returns 201 status and correct response shape', async () => { + // Setup mocks for successful user creation + // Mock the email check to return null (user doesn't exist) + const whereChain = { + selectAll: jest.fn().mockReturnValue({ + executeTakeFirst: (jest.fn() as any).mockResolvedValue(null), + }), + }; + mockDb.selectFrom.mockReturnValue({ + where: jest.fn().mockReturnValue(whereChain), + }); + + // Mock the insert + mockDb.insertInto.mockReturnValue({ + values: jest.fn().mockReturnValue({ + execute: (jest.fn() as any).mockResolvedValue(undefined), + }), + }); + + const res = await handler( + postEvent({ + name: 'John Doe', + email: 'john@example.com', + isAdmin: false, + }) + ); + + expect(res.statusCode).toBe(201); + const json = JSON.parse(res.body); + expect(json).toHaveProperty('ok'); + expect(json).toHaveProperty('route'); + expect(json).toHaveProperty('pathParams'); + expect(json).toHaveProperty('body'); + }); + + test('409: returns 409 when user already exists', async () => { + // Mock: user already exists + const whereChain = { + selectAll: jest.fn().mockReturnValue({ + executeTakeFirst: (jest.fn() as any).mockResolvedValue({ + user_id: 1, + name: 'Existing User', + email: 'existing@example.com', + is_admin: false, + created_at: new Date(), + }), + }), + }; + mockDb.selectFrom.mockReturnValue({ + where: jest.fn().mockReturnValue(whereChain), + }); + + const res = await handler( + postEvent({ + name: 'New User', + email: 'existing@example.com', + isAdmin: true, + }) + ); + + expect(res.statusCode).toBe(409); + const json = JSON.parse(res.body); + expect(json).toHaveProperty('message'); + }); + }); +}); diff --git a/apps/backend/lambdas/users/test/users.test.ts b/apps/backend/lambdas/users/test/users.test.ts index 0257301..2d9b897 100644 --- a/apps/backend/lambdas/users/test/users.test.ts +++ b/apps/backend/lambdas/users/test/users.test.ts @@ -167,3 +167,47 @@ test("get users error", async () => { let res = await fetch("http://localhost:3000/user") expect(res.status).toBe(404); }); + +test("POST user success case", async () => { + let res = await fetch("http://localhost:3000/users", { + method: "POST", + body: JSON.stringify({ + name: "Jane Branch", + email: "jane@branch.com", + isAdmin: true + }) + }); + + expect(res.status).toBe(201); + + let body = await res.json(); + + expect(body.ok).toBe(true); + expect(body.body.name).toBe("Jane Branch"); + expect(body.body.email).toBe("jane@branch.com"); + expect(body.body.isAdmin).toBe(true); +}); + +test("POST user 400 case when invalid email is sent", async () => { + let res = await fetch("http://localhost:3000/users", { + method: "POST", + body: JSON.stringify({ + name: "Invalid User", + email: "", + isAdmin: false + }) + }); + + expect(res.status).toBe(400); +}); + +test("POST user 400 case when request sent with missing fields", async () => { + let res = await fetch("http://localhost:3000/users", { + method: "POST", + body: JSON.stringify({ + name: "Invalid User", + }) // missing email and admin fields + }); + + expect(res.status).toBe(400); +}); \ No newline at end of file