Skip to content

Commit

Permalink
feat(password-reset): implementing execute method at the controller
Browse files Browse the repository at this point in the history
  • Loading branch information
leonardodimarchi committed Jan 12, 2024
1 parent 194c917 commit 9a09f17
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 12 deletions.
15 changes: 15 additions & 0 deletions src/generated/i18n.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ export type I18nTranslations = {
'student-already-enrolled': string;
};
};
'password-reset': {
validations: {
OLD_PASSWORD_IS_DEFINED: string;
OLD_PASSWORD_IS_STRING: string;
OLD_PASSWORD_IS_NOT_EMPTY: string;
NEW_PASSWORD_IS_DEFINED: string;
NEW_PASSWORD_IS_STRING: string;
NEW_PASSWORD_IS_NOT_EMPTY: string;
};
errors: {
'password-reset-not-found': string;
'incorrect-old-password': string;
'user-not-found': string;
};
};
user: {
validations: {
EMAIL_IS_EMAIL: string;
Expand Down
15 changes: 15 additions & 0 deletions src/i18n/en/password-reset.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"validations": {
"OLD_PASSWORD_IS_DEFINED": "Your old password must be defined.",
"OLD_PASSWORD_IS_STRING": "Your old password must be a valid text.",
"OLD_PASSWORD_IS_NOT_EMPTY": "Your old password must be valid.",
"NEW_PASSWORD_IS_DEFINED": "Your new password must be defined.",
"NEW_PASSWORD_IS_STRING": "Your new password must be a valid text.",
"NEW_PASSWORD_IS_NOT_EMPTY": "Your new password must be valid."
},
"errors": {
"password-reset-not-found": "Your password reset request was not found.",
"incorrect-old-password": "Your old password must match the current password.",
"user-not-found": "The user was not found."
}
}
15 changes: 15 additions & 0 deletions src/i18n/pt-br/password-reset.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"validations": {
"OLD_PASSWORD_IS_DEFINED": "A antiga senha do usuário deve ser definido.",
"OLD_PASSWORD_IS_STRING": "A antiga senha do usuário deve ser um texto válido.",
"OLD_PASSWORD_IS_NOT_EMPTY": "A antiga senha do usuário não pode estar vazia.",
"NEW_PASSWORD_IS_DEFINED": "A nova senha do usuário deve ser definido.",
"NEW_PASSWORD_IS_STRING": "A nova senha do usuário deve ser um texto válido.",
"NEW_PASSWORD_IS_NOT_EMPTY": "A nova senha do usuário não pode estar vazia."
},
"errors": {
"password-reset-not-found": "A solicitação de alteração de senha não foi encontrada.",
"incorrect-old-password": "A senha atual não coincidiu com o registro.",
"user-not-found": "O usuário não foi encontrado."
}
}
2 changes: 1 addition & 1 deletion src/i18n/pt-br/user.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"NAME_IS_NOT_EMPTY": "O nome de usuário não pode estar vazio.",
"PASSWORD_IS_DEFINED": "A senha do usuário deve ser definido.",
"PASSWORD_IS_STRING": "A senha do usuário deve ser um texto válido.",
"PASSWORD_IS_NOT_EMPTY": "A senha do usuário não pode estar vazio."
"PASSWORD_IS_NOT_EMPTY": "A senha do usuário não pode estar vazia."
},
"errors": {
"duplicated-email": "O e-mail enviado já existe.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { UserNotFoundError } from '@modules/user/domain/errors/user-not-found.error';
import { UserRepository } from '@modules/user/domain/repositories/user.repository';
import { PasswordEncryptionService } from '@modules/user/domain/services/password-encryption.service';
import { Injectable } from '@nestjs/common';
import { UseCase } from '@shared/domain/usecases/usecase';
import { Either, left, right } from '@shared/helpers/either';
import { IncorrectOldPasswordError } from '../../errors/incorrect-old-password.error';
import { PasswordResetNotFoundError } from '../../errors/password-reset-not-found.error';
import { PasswordResetRepository } from '../../repositories/password-reset.repository';
import { UserNotFoundError } from '@modules/user/domain/errors/user-not-found.error';
import { UseCase } from '@shared/domain/usecases/usecase';

export interface ExecutePasswordResetUseCaseInput {
code: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { RequestPasswordResetUseCase } from '@modules/password-reset/domain/usecases/request/request-password-reset.usecase';
import { PasswordResetController } from './password-reset.controller';
import { faker } from '@faker-js/faker';
import { IncorrectOldPasswordError } from '@modules/password-reset/domain/errors/incorrect-old-password.error';
import { PasswordResetNotFoundError } from '@modules/password-reset/domain/errors/password-reset-not-found.error';
import { ExecutePasswordResetUseCase } from '@modules/password-reset/domain/usecases/execute/execute-password-reset.usecase';
import { RequestPasswordResetUseCase } from '@modules/password-reset/domain/usecases/request/request-password-reset.usecase';
import { ValidatePasswordResetUseCase } from '@modules/password-reset/domain/usecases/validate/validate-password-reset.usecase';
import { right, left } from '@shared/helpers/either';
import { DeepMocked, createMock } from 'test/utils/create-mock';
import { MockPasswordReset } from 'test/factories/password-reset-mock';
import { UserNotFoundError } from '@modules/user/domain/errors/user-not-found.error';
import { InternalServerErrorException } from '@nestjs/common';
import { PasswordResetNotFoundError } from '@modules/password-reset/domain/errors/password-reset-not-found.error';
import {
BadRequestException,
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { left, right } from '@shared/helpers/either';
import { MockPasswordReset } from 'test/factories/password-reset-mock';
import { createI18nMock } from 'test/utils/create-i18n-mock';
import { DeepMocked, createMock } from 'test/utils/create-mock';
import { PasswordResetController } from './password-reset.controller';

describe('PasswordResetController', () => {
let controller: PasswordResetController;
Expand Down Expand Up @@ -92,4 +98,91 @@ describe('PasswordResetController', () => {
expect(call).rejects.toThrow(InternalServerErrorException);
});
});

describe('execute', () => {
it('should return nothing when sucessful', async () => {
executePasswordResetUseCase.exec.mockResolvedValueOnce(right({}));

const result = await controller.execute(
'A'.repeat(8),
{
oldPassword: '',
newPassword: '',
},
createI18nMock(),
);

expect(result).toBeUndefined();
});

it('should throw a not found exception if the password reset was not found', async () => {
executePasswordResetUseCase.exec.mockResolvedValueOnce(
left(new PasswordResetNotFoundError()),
);

const call = async () =>
await controller.execute(
'A'.repeat(8),
{
oldPassword: '',
newPassword: '',
},
createI18nMock(),
);

expect(call).rejects.toThrow(NotFoundException);
});

it('should throw a not found exception if the user was not found', async () => {
executePasswordResetUseCase.exec.mockResolvedValueOnce(
left(new UserNotFoundError()),
);

const call = async () =>
await controller.execute(
'A'.repeat(8),
{
oldPassword: '',
newPassword: '',
},
createI18nMock(),
);

expect(call).rejects.toThrow(NotFoundException);
});

it('should throw a bad request exception if the old password is incorrect', async () => {
executePasswordResetUseCase.exec.mockResolvedValueOnce(
left(new IncorrectOldPasswordError()),
);

const call = async () =>
await controller.execute(
'A'.repeat(8),
{
oldPassword: '',
newPassword: '',
},
createI18nMock(),
);

expect(call).rejects.toThrow(BadRequestException);
});

it('should throw an internal server exception when receiving an unknown error', async () => {
executePasswordResetUseCase.exec.mockResolvedValueOnce(left(new Error()));

const call = async () =>
await controller.execute(
'A'.repeat(8),
{
oldPassword: '',
newPassword: '',
},
createI18nMock(),
);

expect(call).rejects.toThrow(InternalServerErrorException);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import { IncorrectOldPasswordError } from '@modules/password-reset/domain/errors/incorrect-old-password.error';
import { PasswordResetNotFoundError } from '@modules/password-reset/domain/errors/password-reset-not-found.error';
import { ExecutePasswordResetUseCase } from '@modules/password-reset/domain/usecases/execute/execute-password-reset.usecase';
import { RequestPasswordResetUseCase } from '@modules/password-reset/domain/usecases/request/request-password-reset.usecase';
import { ValidatePasswordResetUseCase } from '@modules/password-reset/domain/usecases/validate/validate-password-reset.usecase';
import { UserNotFoundError } from '@modules/user/domain/errors/user-not-found.error';
import {
BadRequestException,
Body,
Controller,
Get,
InternalServerErrorException,
NotFoundException,
Param,
Patch,
Post,
} from '@nestjs/common';
import { ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger';
import {
ApiHeader,
ApiOkResponse,
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import { I18n, I18nContext } from 'nestjs-i18n';
import { I18nTranslations } from 'src/generated/i18n.generated';
import { ExecutePasswordResetPayload } from '../models/payloads/execute-password-reset.payload';
import { PasswordResetCodeValidationViewModel } from '../models/view-models/password-reset-code-validation.view-model';
import { PasswordResetNotFoundError } from '@modules/password-reset/domain/errors/password-reset-not-found.error';

@ApiTags('Password Resets')
@Controller('password-resets')
Expand Down Expand Up @@ -43,6 +56,10 @@ export class PasswordResetController {

@ApiOperation({ summary: 'Validates a password reset code' })
@ApiHeader({ name: 'Accept-Language', example: 'en', required: false })
@ApiOkResponse({
description: 'The code was validated',
type: PasswordResetCodeValidationViewModel,
})
@Get('validate/:code')
public async validateCode(
@Param('code') code: string,
Expand All @@ -63,4 +80,41 @@ export class PasswordResetController {

throw new InternalServerErrorException();
}

@ApiOperation({ summary: 'Reset user password' })
@ApiHeader({ name: 'Accept-Language', example: 'en', required: false })
@Patch('reset/:code')
public async execute(
@Param('code') code: string,
@Body() payload: ExecutePasswordResetPayload,
@I18n() i18n: I18nContext<I18nTranslations>,
): Promise<void> {
const result = await this.executePasswordResetUseCase.exec({
code,
newPassword: payload.newPassword,
oldPassword: payload.oldPassword,
});

if (result.isLeft()) {
if (result.value instanceof PasswordResetNotFoundError) {
throw new NotFoundException(
i18n.t('password-reset.errors.password-reset-not-found'),
);
}

if (result.value instanceof UserNotFoundError) {
throw new NotFoundException(
i18n.t('password-reset.errors.user-not-found'),
);
}

if (result.value instanceof IncorrectOldPasswordError) {
throw new BadRequestException(
i18n.t('password-reset.errors.incorrect-old-password'),
);
}

throw new InternalServerErrorException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsDefined, IsString, IsNotEmpty } from 'class-validator';
import { i18nValidationMessage } from 'nestjs-i18n';
import { I18nTranslations } from 'src/generated/i18n.generated';

export class ExecutePasswordResetPayload {
@ApiProperty({ example: 'J0hn.Doe@123' })
@IsDefined({
message: i18nValidationMessage<I18nTranslations>(
'user.validations.PASSWORD_IS_DEFINED',
),
})
@IsString({
message: i18nValidationMessage<I18nTranslations>(
'user.validations.PASSWORD_IS_STRING',
),
})
@IsNotEmpty({
message: i18nValidationMessage<I18nTranslations>(
'user.validations.PASSWORD_IS_NOT_EMPTY',
),
})
oldPassword: string;

@ApiProperty({ example: 'J0hn.Doe@123' })
@IsDefined({
message: i18nValidationMessage<I18nTranslations>(
'user.validations.PASSWORD_IS_DEFINED',
),
})
@IsString({
message: i18nValidationMessage<I18nTranslations>(
'user.validations.PASSWORD_IS_STRING',
),
})
@IsNotEmpty({
message: i18nValidationMessage<I18nTranslations>(
'user.validations.PASSWORD_IS_NOT_EMPTY',
),
})
newPassword: string;
}

0 comments on commit 9a09f17

Please sign in to comment.