Skip to content

Commit

Permalink
Fix/error handler (#162)
Browse files Browse the repository at this point in the history
* refactor: adjusted endpoint users

* refactor: reverting the last commit in validation errors

* feat: remove space

* refactor: test and implemented repo findByEmail
  • Loading branch information
Frompaje authored Aug 26, 2024
1 parent c8be42d commit 7caeaab
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 94 deletions.
38 changes: 38 additions & 0 deletions src/features/user/repositories/user-repository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,44 @@ describe('[Repositories] UserRepository', () => {
});
});

describe('findByEmail', () => {
it('return user if found', async () => {
const { repository } = makeSut();

const user = UserMock.create();

prisma.user.findUnique.mockResolvedValue(user);

const result = await repository.findByEmail(user.email);

expect(prisma.user.findUnique).toHaveBeenCalledWith({
select: expect.anything(),
where: {
email: user.email,
},
});

expect(result).toBe(user);
});

it('return null if user is not found', async () => {
const { repository } = makeSut();

prisma.user.findUnique.mockResolvedValue(null);

const result = await repository.findByEmail('non_existent_email');

expect(prisma.user.findUnique).toHaveBeenCalledWith({
select: expect.anything(),
where: {
email: 'non_existent_email',
},
});

expect(result).toBeNull();
});
});

describe('updateIsActiveStatus', () => {
it('should call service with correctly params', async () => {
const { repository } = makeSut();
Expand Down
44 changes: 26 additions & 18 deletions src/features/user/repositories/user-repository.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
import type { Prisma } from '@prisma/client';

import { prismaErrorHandler } from '@/shared/errors/prisma-error';
import { database } from '@/shared/infra/database/database';

type CreateUserParams = Prisma.Args<typeof database.user, 'create'>['data'];

export class UserRepository {
async create({ email, name, password, username }: CreateUserParams) {
try {
return database.user.create({
data: {
email,
name,
password,
username,
},
});
} catch (error) {
prismaErrorHandler(error);
}
create({ email, name, password, username }: CreateUserParams) {
return database.user.create({
data: {
email,
name,
password,
username,
},
});
}

async findById(id: string) {
const user = await database.user.findUnique({
findByEmail(email: string) {
return database.user.findUnique({
select: {
email: true,
id: true,
Expand All @@ -31,11 +26,24 @@ export class UserRepository {
username: true,
},
where: {
id,
email,
},
});
}

return user;
findById(id: string) {
return database.user.findUnique({
select: {
email: true,
id: true,
isActive: true,
name: true,
username: true,
},
where: {
id,
},
});
}

async updateIsActiveStatus(userId: string): Promise<void> {
Expand Down
65 changes: 14 additions & 51 deletions src/features/user/services/user-create-service.test.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,27 @@
import type { UserRepository } from '@/features/user/repositories/user-repository';
import { UserCreateService } from '@/features/user/services/user-create-service';
import { BcryptAdapter } from '@/shared/infra/crypto/bcrypt-adapter';
import { mock } from 'vitest-mock-extended';

// TODO: Refactor
const makeSut = () => {
class UserRepositoryStub implements UserRepository {
create({ email, name, password, username }: any) {
return Promise.resolve({
createdAt: new Date(2024, 5, 1),
deletedAt: null,
email,
id: 'valid_id',
isActive: true,
name,
password,
updatedAt: new Date(2024, 5, 1),
username,
});
}
import type { UserRepository } from '@/features/user/repositories/user-repository';
import type { BcryptAdapter } from '@/shared/infra/crypto/bcrypt-adapter';
import { bcryptAdapteMock } from '@/shared/test-helpers/mocks/bcryptAdapter.mock';
import { userRepositoryMock } from '@/shared/test-helpers/mocks/repositories/user-repository.mock';

findById(id: string): Promise<{
email: string;
id: string;
name: null | string;
username: string;
} | null> {
throw new Error('Method not implemented. ' + id);
}
updateIsActiveStatus(_: string): Promise<void> {
throw new Error('Method not implemented.');
}
}
import { UserCreateService } from './user-create-service';

const userRepository = new UserRepositoryStub();
let userCreateService: UserCreateService;

const bcryptAdapter = new BcryptAdapter();
let userRepository = mock<UserRepository>(userRepositoryMock);

const userCreateService = new UserCreateService(
userRepository,
bcryptAdapter
);
let bcryptAdapter = mock<BcryptAdapter>(bcryptAdapteMock);

return { bcryptAdapter, userCreateService, userRepository };
};
beforeEach(() => {
userCreateService = new UserCreateService(userRepository, bcryptAdapter);
});

describe('UserCreateService', () => {
it('should call userRepository with correct params', async () => {
const { bcryptAdapter, userCreateService, userRepository } = makeSut();

const repositorySpy = vi.spyOn(userRepository, 'create');

vi.spyOn(bcryptAdapter, 'encrypt').mockImplementationOnce(
async () => 'valid_password'
);
vi.spyOn(bcryptAdapter, 'encrypt').mockResolvedValue('valid_password');

await userCreateService.execute({
email: 'valid_email@email.com',
Expand All @@ -71,11 +40,7 @@ describe('UserCreateService', () => {
});

it('should throw when userRepository throws', async () => {
const { userCreateService, userRepository } = makeSut();

vi.spyOn(userRepository, 'create').mockImplementationOnce(async () => {
throw new Error('error');
});
vi.spyOn(userRepository, 'create').mockRejectedValue(new Error('error'));

const response = userCreateService.execute({
email: 'valid_email@email.com',
Expand All @@ -89,8 +54,6 @@ describe('UserCreateService', () => {
});

it('should conflict when password and repeatPassword dont match', async () => {
const { userCreateService } = makeSut();

const response = userCreateService.execute({
email: 'valid_email@email.com',
name: 'valid_name',
Expand Down
4 changes: 2 additions & 2 deletions src/features/user/services/user-create-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export class UserCreateService implements Service<Input, void> {
async execute({ email, name, password, repeatPassword, username }: Input) {
if (password != repeatPassword) {
throw new ValidationError(
'400',
'Cannot process the request because of validation errors'
'Cannot process the request because of validation errors',
['password', 'repeatPassword']
);
}

Expand Down
18 changes: 6 additions & 12 deletions src/features/user/validators/user-create-schema.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import { z } from 'zod';

const regex = /^(?=.*[A-Z])(?=.*[!#$&*@])(?=.*[\dA-Za-z]).{8,}$/;
const passwordMessage =
'Password must contain at least 1 uppercase letter, 1 special character, and be at least 8 characters long.';

export const userCreateBodySchema = z.object({
email: z.string().email(),
name: z.string().min(3),
password: z
.string()
.regex(
/^(?=.*[A-Z])(?=.*[!#$&*@])(?=.*[\dA-Za-z]).{8,}$/,
'Password must contain at least 1 uppercase letter, 1 special character, and be at least 8 characters long.'
),
password: z.string().regex(regex, passwordMessage),

repeatPassword: z
.string()
.regex(
/^(?=.*[A-Z])(?=.*[!#$&*@])(?=.*[\dA-Za-z]).{8,}$/,
'Password must contain at least 1 uppercase letter, 1 special character, and be at least 8 characters long.'
),
repeatPassword: z.string().regex(regex, passwordMessage),
username: z.string().min(3),
});
9 changes: 8 additions & 1 deletion src/middlewares/error-handler/error-handler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
import type { ErrorRequestHandler } from 'express';
import { ZodError } from 'zod';

import { HttpError } from '@/shared/errors/http-error.js';
import { prismaErrorHandler } from '@/shared/errors/prisma-error';
import { HttpStatusCode } from '@/shared/protocols/http-client.js';

export const errorHandler: ErrorRequestHandler = (err, _, res, next) => {
Expand All @@ -11,7 +13,8 @@ export const errorHandler: ErrorRequestHandler = (err, _, res, next) => {

if (err instanceof ZodError) {
return res.status(409).send({
issues: err.format(),
error: err.formErrors.fieldErrors,
issues: err.name,
message: 'Validation error',
});
}
Expand All @@ -20,5 +23,9 @@ export const errorHandler: ErrorRequestHandler = (err, _, res, next) => {
return res.status(err.code).json(err);
}

if (err instanceof PrismaClientKnownRequestError) {
return prismaErrorHandler(err, res, next);
}

return res.status(HttpStatusCode.serverError).json({ message: err.message });
};
23 changes: 13 additions & 10 deletions src/shared/errors/prisma-error.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
import type { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
import type { NextFunction, Response } from 'express';

import { ConflictError } from '@/shared/errors/conflict-error';
import { HttpStatusCode } from '../protocols/http-client';

export function prismaErrorHandler(error: unknown) {
if (error instanceof PrismaClientKnownRequestError) {
switch (error.code) {
case 'P2002': {
throw new ConflictError(
'There is already a user with this email or username'
);
}
export function prismaErrorHandler(
error: PrismaClientKnownRequestError,
res: Response,
_: NextFunction
) {
switch (error.code) {
case 'P2002': {
res.status(HttpStatusCode.badRequest).send({
message: 'There is already a user with this email or username',
});
}
}
}
6 changes: 6 additions & 0 deletions src/shared/test-helpers/mocks/bcryptAdapter.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { vi } from 'vitest';

export const bcryptAdapteMock = {
compare: vi.fn(),
encrypt: vi.fn(),
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { vi } from 'vitest';

export const userRepositoryMock = {
create: vi.fn(),
findByEmail: vi.fn(),
findById: vi.fn(),
updateIsActiveStatus: vi.fn(),
};

0 comments on commit 7caeaab

Please sign in to comment.