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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/functions/delete/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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<typeof schema> = async (event) => {
try {
// 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);
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<UserDoc>('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);
20 changes: 20 additions & 0 deletions src/functions/delete/index.ts
Original file line number Diff line number Diff line change
@@ -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,
},
},
},
},
],
};
9 changes: 9 additions & 0 deletions src/functions/delete/schema.ts
Original file line number Diff line number Diff line change
@@ -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;
105 changes: 105 additions & 0 deletions tests/delete.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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`);
});
});