Skip to content

Commit

Permalink
Fix up the tests and user creation api behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
d1str0 committed Nov 29, 2024
1 parent 66f19f8 commit 06b1533
Show file tree
Hide file tree
Showing 14 changed files with 770 additions and 30 deletions.
60 changes: 60 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
.env

# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules
jspm_packages

# Optional npm cache directory
.npm

# Optional REPL history
.node_repl_history

# 0x
profile-*

# mac files
.DS_Store

# vim swap files
*.swp

# webstorm
.idea

# vscode
.vscode
*code-workspace

# clinic
profile*
*clinic*
*flamegraph*
6 changes: 5 additions & 1 deletion api/jest.config.js → api/jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
module.exports = {
import type { Config } from '@jest/types';

const config: Config.InitialOptions = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/test/**/*.test.ts'],
Expand All @@ -7,3 +9,5 @@ module.exports = {
'^.+\\.ts$': 'ts-jest',
},
};

export default config;
31 changes: 31 additions & 0 deletions api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@types/jest": "^29.5.14",
"@types/node": "^22.9.3",
"jest": "^29.7.0",
"jest-mock-extended": "^4.0.0-beta1",
"prisma": "^5.22.0",
"ts-jest": "^29.2.5",
"ts-node-dev": "^2.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[name]` on the table `User` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "User_name_key" ON "User"("name");
2 changes: 1 addition & 1 deletion api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ datasource db {
model User {
id Int @id @default(autoincrement())
email String @unique
name String
name String @unique
password String
}
12 changes: 11 additions & 1 deletion api/src/handlers/user.handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { FastifyReply, FastifyRequest } from 'fastify';
import { getAllUserNames, createUser } from '../services/user.service';
import {
getAllUserNames,
createUser,
UserExistsError,
} from '../services/user.service';
import { CreateUserBody } from '../types/user.types';
import { log } from 'console';

export async function getUsersHandler(
request: FastifyRequest,
Expand All @@ -22,6 +27,11 @@ export async function createUserHandler(
const user = await createUser(name, email, password);
reply.status(201).send(user);
} catch (error) {
if (error instanceof UserExistsError) {
reply.conflict(error.message);
return;
}
log(error);
reply.status(500).send({ error: 'An error occurred' });
}
}
6 changes: 4 additions & 2 deletions api/src/plugins/errorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ function errorHandler(fastify: FastifyInstance, opts: any, done: () => void) {
return reply.status(400).send({
error: 'Validation Error',
details: error.validation.map((err) => ({
field: err.params.missingProperty || err.params.propertyName,
message: customErrorMessage(err),
})),
});
}

reply.status(500).send({ error: error.message });
if (error.statusCode) {
return reply.status(error.statusCode).send({ error: error.message });
}
return reply.status(500).send({ error: error.message });
});

done();
Expand Down
19 changes: 19 additions & 0 deletions api/src/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import bcrypt from 'bcrypt';

const SALT_ROUNDS = 10; // Industry standard

export class UserExistsError extends Error {
constructor(message: string) {
super(message);
this.name = 'UserExistsError';
}
}

export async function getAllUserNames() {
const users = await prisma.user.findMany({
select: {
Expand All @@ -18,6 +25,18 @@ export async function createUser(
email: string,
password: string,
) {
// Check if user exists by name or email
const existingUser = await prisma.user.findFirst({
where: {
OR: [{ name }, { email }],
},
});

if (existingUser) {
const field = existingUser.name === name ? 'name' : 'email';
throw new UserExistsError(`User with this ${field} already exists`);
}

// Hash password before storing
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);

Expand Down
104 changes: 104 additions & 0 deletions api/test/routes/api/user.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// test/api/user.test.ts
import Fastify, { FastifyInstance } from 'fastify';
import sensible from '@fastify/sensible';
import rootRoutes from '../../../src/routes';
import errorHandler from '../../../src/plugins/errorHandler';

describe('User API Routes', () => {
let app: FastifyInstance;

beforeAll(async () => {
app = Fastify();
await app.register(sensible);

// Register routes with API prefix and error handler
await app.register(
async (fastify) => {
await fastify.register(errorHandler);
await fastify.register(rootRoutes);
},
{ prefix: '/api' },
);

await app.ready();
});

afterAll(async () => {
await app.close();
});

describe('POST /api/user', () => {
const validUser = {
name: 'john_doe',
email: '[email protected]',
password: 'password123',
};

// TODO: Add test for valid user creation

test('should reject user with conflicting name', async () => {
// Try to create another user with the same name
const response = await app.inject({
method: 'POST',
url: '/api/user',
payload: validUser,
});

expect(response.statusCode).toBe(409);
expect(response.json()).toMatchObject({
error: 'User with this name already exists',
});
});

test('should reject invalid name format', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/user',
payload: {
...validUser,
name: 'invalid@name',
},
});

expect(response.statusCode).toBe(400);
expect(response.json()).toMatchObject({
error: 'Validation Error',
details: expect.arrayContaining([
expect.objectContaining({
message: 'name must contain only letters, numbers, and underscores',
}),
]),
});
});

test('should reject missing required fields', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/user',
payload: {},
});

expect(response.statusCode).toBe(400);
expect(response.json()).toMatchObject({
error: 'Validation Error',
details: expect.arrayContaining([
expect.objectContaining({
message: 'name is required',
}),
]),
});
});
});

describe('GET /api/user', () => {
test('should return list of user names', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/user',
});

expect(response.statusCode).toBe(200);
expect(Array.isArray(response.json())).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// test/root.test.ts
import Fastify, { FastifyInstance } from 'fastify';
import sensible from '@fastify/sensible';
import rootRoutes from '../../src/routes/root';
import rootRoutes from '../../src/routes/index';

describe('Root Routes', () => {
let app: FastifyInstance;
Expand Down
23 changes: 0 additions & 23 deletions node_modules/.package-lock.json

This file was deleted.

Loading

0 comments on commit 06b1533

Please sign in to comment.