Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion apps/backend/lambdas/users/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,54 @@ export const handler = async (event: any): Promise<APIGatewayProxyResult> => {
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<string, unknown>)
: {};

// 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) {
Expand Down
22 changes: 22 additions & 0 deletions apps/backend/lambdas/users/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
185 changes: 185 additions & 0 deletions apps/backend/lambdas/users/test/user.unit.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>) {
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');
});
});
});
44 changes: 44 additions & 0 deletions apps/backend/lambdas/users/test/users.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});