Skip to content

Commit 06b1533

Browse files
committed
Fix up the tests and user creation api behavior
1 parent 66f19f8 commit 06b1533

File tree

14 files changed

+770
-30
lines changed

14 files changed

+770
-30
lines changed

Diff for: .gitignore

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
.env
2+
3+
# Logs
4+
logs
5+
*.log
6+
npm-debug.log*
7+
8+
# Runtime data
9+
pids
10+
*.pid
11+
*.seed
12+
13+
# Directory for instrumented libs generated by jscoverage/JSCover
14+
lib-cov
15+
16+
# Coverage directory used by tools like istanbul
17+
coverage
18+
19+
# nyc test coverage
20+
.nyc_output
21+
22+
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
23+
.grunt
24+
25+
# node-waf configuration
26+
.lock-wscript
27+
28+
# Compiled binary addons (http://nodejs.org/api/addons.html)
29+
build/Release
30+
31+
# Dependency directories
32+
node_modules
33+
jspm_packages
34+
35+
# Optional npm cache directory
36+
.npm
37+
38+
# Optional REPL history
39+
.node_repl_history
40+
41+
# 0x
42+
profile-*
43+
44+
# mac files
45+
.DS_Store
46+
47+
# vim swap files
48+
*.swp
49+
50+
# webstorm
51+
.idea
52+
53+
# vscode
54+
.vscode
55+
*code-workspace
56+
57+
# clinic
58+
profile*
59+
*clinic*
60+
*flamegraph*

Diff for: api/jest.config.js renamed to api/jest.config.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
module.exports = {
1+
import type { Config } from '@jest/types';
2+
3+
const config: Config.InitialOptions = {
24
preset: 'ts-jest',
35
testEnvironment: 'node',
46
testMatch: ['**/test/**/*.test.ts'],
@@ -7,3 +9,5 @@ module.exports = {
79
'^.+\\.ts$': 'ts-jest',
810
},
911
};
12+
13+
export default config;

Diff for: api/package-lock.json

+31
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: api/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"@types/jest": "^29.5.14",
4040
"@types/node": "^22.9.3",
4141
"jest": "^29.7.0",
42+
"jest-mock-extended": "^4.0.0-beta1",
4243
"prisma": "^5.22.0",
4344
"ts-jest": "^29.2.5",
4445
"ts-node-dev": "^2.0.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
Warnings:
3+
4+
- A unique constraint covering the columns `[name]` on the table `User` will be added. If there are existing duplicate values, this will fail.
5+
6+
*/
7+
-- CreateIndex
8+
CREATE UNIQUE INDEX "User_name_key" ON "User"("name");

Diff for: api/prisma/schema.prisma

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ datasource db {
1616
model User {
1717
id Int @id @default(autoincrement())
1818
email String @unique
19-
name String
19+
name String @unique
2020
password String
2121
}

Diff for: api/src/handlers/user.handler.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { FastifyReply, FastifyRequest } from 'fastify';
2-
import { getAllUserNames, createUser } from '../services/user.service';
2+
import {
3+
getAllUserNames,
4+
createUser,
5+
UserExistsError,
6+
} from '../services/user.service';
37
import { CreateUserBody } from '../types/user.types';
8+
import { log } from 'console';
49

510
export async function getUsersHandler(
611
request: FastifyRequest,
@@ -22,6 +27,11 @@ export async function createUserHandler(
2227
const user = await createUser(name, email, password);
2328
reply.status(201).send(user);
2429
} catch (error) {
30+
if (error instanceof UserExistsError) {
31+
reply.conflict(error.message);
32+
return;
33+
}
34+
log(error);
2535
reply.status(500).send({ error: 'An error occurred' });
2636
}
2737
}

Diff for: api/src/plugins/errorHandler.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,15 @@ function errorHandler(fastify: FastifyInstance, opts: any, done: () => void) {
3737
return reply.status(400).send({
3838
error: 'Validation Error',
3939
details: error.validation.map((err) => ({
40-
field: err.params.missingProperty || err.params.propertyName,
4140
message: customErrorMessage(err),
4241
})),
4342
});
4443
}
4544

46-
reply.status(500).send({ error: error.message });
45+
if (error.statusCode) {
46+
return reply.status(error.statusCode).send({ error: error.message });
47+
}
48+
return reply.status(500).send({ error: error.message });
4749
});
4850

4951
done();

Diff for: api/src/services/user.service.ts

+19
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ import bcrypt from 'bcrypt';
33

44
const SALT_ROUNDS = 10; // Industry standard
55

6+
export class UserExistsError extends Error {
7+
constructor(message: string) {
8+
super(message);
9+
this.name = 'UserExistsError';
10+
}
11+
}
12+
613
export async function getAllUserNames() {
714
const users = await prisma.user.findMany({
815
select: {
@@ -18,6 +25,18 @@ export async function createUser(
1825
email: string,
1926
password: string,
2027
) {
28+
// Check if user exists by name or email
29+
const existingUser = await prisma.user.findFirst({
30+
where: {
31+
OR: [{ name }, { email }],
32+
},
33+
});
34+
35+
if (existingUser) {
36+
const field = existingUser.name === name ? 'name' : 'email';
37+
throw new UserExistsError(`User with this ${field} already exists`);
38+
}
39+
2140
// Hash password before storing
2241
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
2342

Diff for: api/test/routes/api/user.test.ts

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// test/api/user.test.ts
2+
import Fastify, { FastifyInstance } from 'fastify';
3+
import sensible from '@fastify/sensible';
4+
import rootRoutes from '../../../src/routes';
5+
import errorHandler from '../../../src/plugins/errorHandler';
6+
7+
describe('User API Routes', () => {
8+
let app: FastifyInstance;
9+
10+
beforeAll(async () => {
11+
app = Fastify();
12+
await app.register(sensible);
13+
14+
// Register routes with API prefix and error handler
15+
await app.register(
16+
async (fastify) => {
17+
await fastify.register(errorHandler);
18+
await fastify.register(rootRoutes);
19+
},
20+
{ prefix: '/api' },
21+
);
22+
23+
await app.ready();
24+
});
25+
26+
afterAll(async () => {
27+
await app.close();
28+
});
29+
30+
describe('POST /api/user', () => {
31+
const validUser = {
32+
name: 'john_doe',
33+
34+
password: 'password123',
35+
};
36+
37+
// TODO: Add test for valid user creation
38+
39+
test('should reject user with conflicting name', async () => {
40+
// Try to create another user with the same name
41+
const response = await app.inject({
42+
method: 'POST',
43+
url: '/api/user',
44+
payload: validUser,
45+
});
46+
47+
expect(response.statusCode).toBe(409);
48+
expect(response.json()).toMatchObject({
49+
error: 'User with this name already exists',
50+
});
51+
});
52+
53+
test('should reject invalid name format', async () => {
54+
const response = await app.inject({
55+
method: 'POST',
56+
url: '/api/user',
57+
payload: {
58+
...validUser,
59+
name: 'invalid@name',
60+
},
61+
});
62+
63+
expect(response.statusCode).toBe(400);
64+
expect(response.json()).toMatchObject({
65+
error: 'Validation Error',
66+
details: expect.arrayContaining([
67+
expect.objectContaining({
68+
message: 'name must contain only letters, numbers, and underscores',
69+
}),
70+
]),
71+
});
72+
});
73+
74+
test('should reject missing required fields', async () => {
75+
const response = await app.inject({
76+
method: 'POST',
77+
url: '/api/user',
78+
payload: {},
79+
});
80+
81+
expect(response.statusCode).toBe(400);
82+
expect(response.json()).toMatchObject({
83+
error: 'Validation Error',
84+
details: expect.arrayContaining([
85+
expect.objectContaining({
86+
message: 'name is required',
87+
}),
88+
]),
89+
});
90+
});
91+
});
92+
93+
describe('GET /api/user', () => {
94+
test('should return list of user names', async () => {
95+
const response = await app.inject({
96+
method: 'GET',
97+
url: '/api/user',
98+
});
99+
100+
expect(response.statusCode).toBe(200);
101+
expect(Array.isArray(response.json())).toBe(true);
102+
});
103+
});
104+
});

Diff for: api/test/routes/root.test.ts renamed to api/test/routes/index.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// test/root.test.ts
22
import Fastify, { FastifyInstance } from 'fastify';
33
import sensible from '@fastify/sensible';
4-
import rootRoutes from '../../src/routes/root';
4+
import rootRoutes from '../../src/routes/index';
55

66
describe('Root Routes', () => {
77
let app: FastifyInstance;

Diff for: node_modules/.package-lock.json

-23
This file was deleted.

0 commit comments

Comments
 (0)