Architecture And Tests Nodejs

📝 On

Notes I make throughout my studies on:

  • Backend architecture
  • DDD (Domain-Driven Design)
  • TDD (Test-Driven Development)
  • SOLID
  • Jest
  • MongoDB
  • Cache
  • Amazon SES

🏆 Challenge

  • Write down the way I solve problems, tracing paths and the like.
  • Put into practice the knowledge that is acquired daily in my studies.

👀 Projects in which I am applying these concepts

Available at: Backend GoBarber


NodeJS Architecture and Testing

  • There is NO perfect architecture / structure for all projects.
  • It is up to me, as a developer, to understand what makes sense to use in my project.
  • BIG part of scalability concepts will learn here.
  • Architecture will not matter if what I am going to build is just an MVC, where it will die shortly after construction.

Developing big applications IS NOT JUST CODAR, it’s thinking about:

  • Architecture
  • Tests
  • Documentation
  • Organization
  • Programming principles

So far, the backend is separating files by type, for example: services, which is handling all business rules. Thus, the structure is disorganized, in case the structure grows.

Backend folder structure

Backend folder structure

From now on, they will be separated by Domain .

What is Domain ?

It is the area of ​​knowledge of that module / file.

Domain Driven Design (DDD) based architecture

  • It is a methodology, just like SCRUM .
  • And like SCRUM , it has many techniques, but not all of them fit into any type of project.
  • In summary, DDD are concepts , principles and good practices that I must use in good parts of Backend projects .
  • Applies to Backend only .

Test Driven Development (TDD)

  • It is also a methodology, just like DDD and can be used in conjunction with it.
  • Applies to Backend, Frontent and Mobile .
  • Create tests, before creating the features themselves.

Modules / Domain Layer

  • Improve the application’s folder structure.
  • Further isolate responsibilities based on the domain , which area of ​​knowledge a file is part of.
  • Begin to divide the application into modules .
  • Modules are basically sectors that one or more files are part of. Responsible for the business rule, it is basically the heart of my application.
  • Has no knowledge

Example: User, all files related to this domain will be part of the same module.

Modules / Domain Layer

Domain / Module Layer folder structure

Note: Previously, the User entity was part of the models folder , which basically has the same concept of entities : How we managed to represent information in the application.

Shared

  • If more than one module makes use of a file, this file must be part of the Shared folder .

Example: Database, errors, middlewares, routes and etc.

Shared folder structure

Shared folder structure

Infra Layer

  • Responsible for communicating my application with external services.
  • Responsible for the technical decisions of the application.
  • In other words, they are the tools that will be chosen to relate to the module layer .

Example: Database, Automatic Email Service and so on …

  • In the Shared folder , I will add a folder below that will contain all the information of a specific package / lib.
  • Still inside the folder below , I will create a folder with files responsible for communicating with some protocol, in this case http . That is, inside the http they will have the express files or any lib that makes use of http protocols .

Folder structure of the Infra Layer

Folder structure of the Infra Layer

  • In the module layer, there is communication between an entity and the database, which in this case is TypeOrm (PostgreSQL). So, I must create the infra / typeorm folder that will contain the entity folder inside it.

Domain Layer folder structure with infra

Folder structure of the Domain Layer with the Infrastructure Layer

Configuring Imports

  • Go to tsconfig, in the baseUrl attribute put:
"./src"
  • And in the paths attribute and put:
{ 
  "@modules/*": ["modules/*"],
  "@config/*": ["config/*"],
  "@shared/*": ["shared/*]
}
  • Right after that, arrange all the necessary imports using @modules or @config or @shared
  • Also fix imports in the ormconfig.json file .
  • Put in package.json in scripts :
{
    "build": "tsc",
    "dev:server": "ts-node-dev -r tsconfig-paths/register --inspect --transpileOnly --ignore-watch node_modules src/shared/infra/http/server.ts",
    "typeorm": "ts-node-dev -r tsconfig-paths/register ./node_modules/typeorm/cli.js"
  },
  • Install to handle imports created in the TypeScript configuration file:
$ yarn add tsconfig-paths -D
  • Run the application to verify that it is working.

SOLID

Liskov Substitution Principle

  • This principle defines that an object inherited from superclass A , it must be possible to change it in such a way that it accepts a subclass that also inherits A without the application stop working.
  • In the layers (repositories) that we have that depend on other libraries (typeorm), it should be possible to change them as necessary, following a set of rules. And in the end, we will depend only on a set of rules (interface), but not necessarily on a specific library (typeorm) .
// AppointmentsRepository that inherits methods from a standard TypeORM repository. 
import  {  EntityRepository ,  Repository  }  from  'typeorm' ; 
import  Appointment  from  '@ modules / appointments / infra / typeorm / entities / Appointment' ;

@ EntityRepository ( Appointment ) 
class  AppointmentsRepository  extends  Repository < Appointment >  { 
	public  async  findByDate ( date : Date ) : Promise < Appointment | undefined >  { 
		const  findAppointment  =  await  this . findOne ( { 
			where : { date } , 
		} ) ; 
		return findAppointment ; 
	} 
}

export  default  AppointmentsRepository ;

Applying Liskov Substitution Principle

  • Create an interface to handle the appointments repository. That is, I am creating my rules.
import  Appointment  from  '../infra/typeorm/entities/Appointment' ;

export  default  interface  IAppointmentsRepository  { 
  findByDate ( date : Date ) : Promise < Appointment | undefined > ; 
}
  • Go to the Typeorm repository and add my new rules to the AppointmensRepository class:
import  {  EntityRepository ,  Repository  }  from  'typeorm' ;

import  IAppointmentsRepository  from  '@ modules / appointments / repositories / IAppointmentsRepository' ;

import  Appointment  from  '@ modules / appointments / infra / typeorm / entities / Appointment' ;

@ EntityRepository ( Appointment ) 
class  AppointmentsRepository  extends  Repository < Appointment >  implements  IAppointmentsRepository  { 
	public  async  findByDate ( date : Date ) : Promise < Appointment | undefined >  { 
		const  findAppointment  =  await  this . findOne ( { 
			where : { date } , 
		}) ; 
		return  findAppointment ; 
	} 
}

export  default  AppointmentsRepository ;

When one class extends another, it means that it will inherit the methods. The implements, on the other hand, serve as a shape (format or rule) to be followed by the class, but not that it inherits its methods.

That way, our services will depend only on repository rules, and not necessarily a TypeORM repository or whatever the other library is. In fact, the service should not be aware of the final format of the structure that persists our data.

Rewriting Repositories

  • I must have more control over the methods of the repository, since they are inherited from TypeORM, such as: create, findOne and etc.
  • Go to the TypeORM repository and add my own create method, which will use two TypeORM methods. And I must also create my ICreateAppointmentDTO.ts interface:
export  default  interface  ICreateAppointmentDTO  { 
  provider_id : string; 
  date: Date ; 
}
import  {  getRepository ,  Repository  }  from  'typeorm' ;

import  IAppointmentsRepository  from  '@ modules / appointments / repositories / IAppointmentsRepository' ;

import  ICreateAppointmentDTO  from  '@ modules / appointments / dtos / ICreateAppointmentDTO' ;

import  Appointment  from  '@ modules / appointments / infra / typeorm / entities / Appointment' ;

class  AppointmentsRepository  implements  IAppointmentsRepository  {

  private  ormRepository : Repository < Appointment > ;

  constructor ( ) { 
    this . ormRepository  =  getRepository ( Appointment ) ; 
  }

	public  async  findByDate ( date : Date ) : Promise < Appointment | undefined >  { 
		const  findAppointment  =  await  this . ormRepository . findOne ( { 
			where : { date } , 
		} ) ; 
		return  findAppointment ; 
	}

  public  async  create ( { provider_id , date } : ICraeteAppointmentDTO ) : Promise < Appointment > { 
    const  appointment  =  this . ormRepository . create ( { 
      provider_id ,
      gives you
    } ) ;

    await  this . ormRepository . save ( appointment ) ;

    return  appointment ; 
  } 
}

export  default  AppointmentsRepository ;

Dependency Inversion Principle

This principle defines that:

  • High-level modules should not depend on low-level modules. Both should depend only on abstractions (for example, interfaces).
  • Abstractions should not depend on details. Details (implementations) must depend on abstractions.

That is, instead of my service depending directly on the typeorm repository , it now depends only on the interface of the repository, which will be passed through the route .

The route is part of the infra layer, which is the same as the TypeORM, so it is not necessary to apply the same principle to it. The service is part of the domain layer, so it must be decoupled from the infrastructure layer in such a way that it is not aware of its operation.

import  {  startOfHour  }  from  'date-fns' ; 
import  {  getCustomRepository  }  from  'typeorm' ; 
import  Appointment  from  '@ modules / appointments / infra / typeorm / entities / Appointment' ; 
import  AppError  from  '@ shared / errors / AppError' ; 
import  AppointmentsRepository  from  '../repositories/AppointmentsRepository' ;

 Request  interface { 
	provider_id : string ; 
	date : Date ; 
}

class  CreateAppointmentService  { 
	public  async  execute ( { provider_id , date } : Request ) : Promise < Appointment >  { 
    // depending on the repository for the typeorm 
		const  appointmentsRepository  =  getCustomRepository ( AppointmentsRepository ) ;

		const  appointmentDate  =  startOfHour ( date ) ;

		const  findAppointmentInSameDate  =  await  appointmentsRepository . findByDate ( 
			appointmentDate , 
		) ;

		if  ( findAppointmentInSameDate )  { 
			throw  new  AppError ( 'This Appointment is already booked!' ) ; 
		}

		const  appointment  =  await  appointmentsRepository . create ( { 
			provider_id , 
			date : appointmentDate , 
		} ) ;

		return  appointment ; 
	} 
}

export  default  CreateAppointmentService ;

Applying the Dependency Inversion Principle

import  {  startOfHour  }  from  'date-fns' ; 
import  Appointment  from  '@ modules / appointments / infra / typeorm / entities / Appointment' ; 
import  AppError  from  '@ shared / errors / AppError' ; 
import  IAppointmentsRepository  from  '../repositories/IAppointmentsRepository' ;

interface  IRequest  { 
	provider_id : string ; 
	date : Date ; 
}

class  CreateAppointmentService  {

  // depending only on the interface (abstraction) of the repository, which will be passed to the constructor when a new repository is instantiated on the route. 
  constructor ( private  appointmentsRepository : IAppointmensRepository )  { }

	public  async  execute ( { provider_id , date } : Request ) : Promise < Appointment >  { 
		const  appointmentDate  =  startOfHour ( date ) ;

		const  findAppointmentInSameDate  =  await  this . appointmentsRepository . findByDate ( 
			appointmentDate , 
		) ;

		if  ( findAppointmentInSameDate )  { 
			throw  new  AppError ( 'This Appointment is already booked!' ) ; 
		}

		const  appointment  =  await  this . appointmentsRepository . create ( { 
			provider_id , 
			date : appointmentDate , 
		} ) ;

		return  appointment ; 
	} 
}

export  default  CreateAppointmentService ;

Note: The Appointment entity is still provided by TypeORM, but in this case the Dependecy Inversion Principle will not be applied in order not to ‘hinder’ the understanding of this principle.

Both Liskov Substitution Principle and Dependency Inversion Principle have similar concepts. In short, the Liskov Substitution Principle says that my domain layer must depend on abstractions (interfaces), and not directly from the infra layer . The Dependency Inversion Principle says that modules must depend on abstractions (interfaces) and not on other modules .

User Module Refactorings

  • Create the user repository at the domain layer, which will be my interface that will dictate the rules for the typeorm repository to follow.
// modules / users / repositories / IUsersRepository 
import  User  from  '../infra/typeorm/entities/User' ; 
import  ICreateUserDTO  from  '../dtos/ICreateUserDTO' ;

export  default  interface  IUsersRepository  {

	findByEmail ( email : string ) : Promise < User | undefined > ; 
	findById ( id : string ) : Promise < User | undefined > ; 
	create ( data : ICreateUserDTO ) : Promise < User > ; 
	save ( user : User ) : Promise < User > ;
}
// modules / users / dtos / ICreateUserDTO 
export  default  interface  ICreateUserDTO  { 
	name : string; 
	email: string ; 
	password: string ; 
}
  • Now create the typeorm repository.
// modules / users / infra / typeorm / repositories / UsersRepository 
import  {  getRepository ,  Repository  }  from  'typeorm' ;

import  User  from  '../entities/User' ; 
import  IUsersRepository  from  '@ modules / users / repositories / IUsersRepository' ;

class  UsersRepository  implements  IUsersRepository { 
	private  ormRepository : Repository < User > ;

	constructor ( ) { 
		this . ormRepository  =  getRepository ( User ) ; 
	}

	public  async  findByEmail ( email : string ) : Promise < User > { 
		const  user  =  this . ormRepository . findOne ( {  where : { email } } ) ;

		return  user ; 
	} 
	public  async  findById ( id : string ) : Promise < User > { 
		const  user  =  this . ormRepository . findOne ( id ) ;

		return  user ; 
	}

	public  async  create ( user : User ) : Promise < User > { 
		const  user  =  this . ormRepository . create ( user ) ;

		return  this . ormRepository . save ( user ) ; 
	}

	public  async  save ( user : User ) : Promise < User > { 
		return  this . ormRepository . save ( user ) ; 
	} 
}
  • Now I must use the interface of this repository in the service, because in each service a repository will be sent, and I must inform its interface.
import  {  hash  }  from  'bcryptjs' ; 
import  AppError  from  '@ shared / errors / AppError' ; 
import  User  from  '../infra/typeorm/entities/User' ;

import  IUsersRepository  from  '../repositories/IUsersRepository' ;

interface  IRequest  { 
	name : string ; 
	email : string ; 
	password : string ; 
}

class  CreateUserService  { 
	constructor ( private  usersRepository : IUsersRepository )  { }

	public  async  execute ( { name , email , password } : IRequest ) : Promise < User >  { 
		const  checkUserExist  =  await  this . usersRepository . findByEmail ( email ) ;

		if  ( checkUserExist )  { 
			throw  new  AppError ( 'Email already used!' ) ; 
		}

		const  hashedPassword  =  await  hash ( password ,  8 ) ;

		const  user  =  await  this . usersRepository . create ( { 
			name , 
			email , 
			password : hashedPassword , 
		} ) ;

		return  user ; 
	} 
}

export  default  CreateUserService ;

Make these changes to the other services.

  • Go to user domain routes and make the instance of the typeorm repository to be sent by the parameter of each route.

Dependency Injection

  • Reason for using:
  • Do not need to go through the repositories whenever a service is instantiated.
  • Go to the shared folder, create a container folder and an index.ts
import  {  container  }  from  'tsyringe' ;

import  IAppointmentsRepository  from  '@ modules / appointsments / repositories / IAppointmentsRepository' ; 
import  IUsersRepository  from  '@ modules / users / repositories / IUsersRepository' ;

import  AppointmentsRepository  from  '@ modules / appointments / infra / typeorm / repositories / AppointmentsRepository' ;

import  UsersRepository  from  '@ modules / users / infra / typeorm / repositories / UsersRepository' ;

container . registerSingleton < IAppointmentsRepository > ( 'AppointmentsRepository' ,  AppointmentsRepository ) ;

container . registerSingleton < IUsersRepository > ( 'UsersRepository' ,  UsersRepository ) ;
  • Go to the appointment services and make changes to the constructors of all appointments
import  {  startOfHour  }  from  'date-fns' ; 
import  {  injectable ,  inject  }  from  'tsyringe' ;

import  Appointment  from  '@ modules / appointments / infra / typeorm / entities / Appointment' ; 
import  AppError  from  '@ shared / errors / AppError' ; 
import  IAppointmentsRepository  from  '../repositories/IAppointmentsRepository' ;

interface  IRequest  { 
	provider_id : string ; 
	date : Date ; 
}

// add the tsyringe decorator 
@ injectable ( ) 
class  CreateAppointmentService  { 
	constructor ( 
		// and will inject this dependency whenever the service is instantiated. 
		@ inject ( 'AppointmentsRepository' ) 
		private  appointmentsRepository : IAppointmentsRepository , 
	)  { }

	public  async  execute ( { provider_id , date } : IRequest ) : Promise < Appointment >  { 
		const  appointmentDate  =  startOfHour ( date ) ;

		const  findAppointmentInSameDate  =  await  this . appointmentsRepository . findByDate ( 
			appointmentDate , 
		) ;

		if  ( findAppointmentInSameDate )  { 
			throw  new  AppError ( 'This Appointment is already booked!' ) ; 
		}

		const  appointment  =  await  this . appointmentsRepository . create ( { 
			provider_id , 
			date : appointmentDate , 
		} ) ;

		return  appointment ; 
	} 
}

export  default  CreateAppointmentService ;
  • Go on the appointments routes, remove the typeorm repository, import the tsyringe container and use the resolve method ('insert_o_service_aqui_onde_ele_seria_instanciado).
import  {  Router  }  from  'express' ; 
import  {  container  }  from  'tsyringe' ;

import  CreateAppointmentService  from  '@ modules / appointments / services / CreateAppointmentService' ;

const  appointmentsRouter  =  Router ( ) ;

appointmentsRouter . post ( '/' ,  async  ( request ,  response )  =>  { 
	const  { provider_id , date }  =  request . body ;

	// const appointmentsRepository = new AppointmentsRepository ();

	// const createAppointments = new CreateAppointmentService ();

	// put it this way 
	const  createAppointments  =  container . resolve ( CreateAppointmentService ) 
	. . .

} ) ;
  • Go to server.ts (main file) and import the dependency injection container.
...
 import  '@ shared / container' ; 
...

Using Controllers

  • Controllers in a large architecture have little responsibility, where they will take care to abstract the logic that is in the routes . There are already Services that deal with business rules and the Repository that deals with data manipulation / persistence .
  • The routes were only responsible for (now it will be the responsibility of the Controllers):
  • Receive the requisition data.
  • Call other files to handle the data.
  • Return the Answer.
  • Following Restful API standards, Controllers must have only 5 methods:
  • Index (list all)
  • Show (list only one)
  • Create
  • Update
  • Delete

If more methods are needed, I must create a new controller.

// users / infra / http / controllers / SessionsController.ts 
import  {  Request ,  Response  }  from  'express' ;

import  {  container  }  from  'tsyringe' ; 
import  AuthenticateUserService  from  '@ modules / users / services / AuthenticateUserService' ;

class  SessionsController  { 
	public  async  create ( request : Request ,  response : Response ) : Promise < Response >  { 
		const  { password , email }  =  request . body ;

		const  authenticateUser  =  container . resolve ( AuthenticateUserService ) ;

		const  { user , token }  =  await  authenticateUser . execute ( { password , email } ) ;

		delete  user . password ;

		return  response . json ( { user , token } ) ; 
	} 
}

export  default  SessionsController ;
// sessions.routes.ts 
import  {  Router  }  from  'express' ;

import  SessionsController  from  '../controllers/SessionsController' ;

const  sessionsRouter  =  Router ( ) ; 
const  sessionsController  =  new  SessionsController ( ) ;

sessionsRouter . post ( '/' ,  sessionsController . create ) ;

export  default  sessionsRouter ;

Tests and TDD

We have created tests to ensure that our application continues to function properly regardless of the number of functionality and the number of developers.

3 Main types of tests

  1. Unit Testing
  2. Integration Test
  3. E2E test (end-to-end)

Unit Testing

  • Tests application-specific features.
  • You need these features to be pure functions .
Pure Functions

It does not depend on another part of the application or external service.

You will never own:

  • API calls
  • Side effects

Side effect example: Trigger an email whenever a new user is created

Integration Test

It tests a complete functionality, passing through several layers of the application.

Rota -> Controller -> Service -> Repository -> Service -> …

Example: Creating a new user with sending email.

E2E test (end-to-end)

  • Simulates user action within the application.
  • Used in interfaces (React, React Native).

Example:

  1. Click on the email input
  2. Type danilobandeira29@gmail.com
  3. Click on the password input
  4. Type 123456
  5. Click the “sign in” button
  6. Expects the user to be redirected to the Dashboard

Test Driven Development (TDD)

  • “Test Driven Development”.
  • We created some tests even before the functionality itself (or some of them) was created.

Example: When the user signs up for the application, they should receive a welcome email.

Configuring Jest

$ yarn add jest -D
$ yarn jest --init
$ yarn add @ types / jest ts-jest -D
  • Configure the jest.config.js file: preset: ‘ts-jest’, …, testMatch: [‘** / *. Spec.ts’]
  • Create a test just to test the jest settings.
test ( 'sum two numbers' ,  ( )  =>  { 
	expect ( 1  +  2 ) . toBe ( 3 ) ; 
} ) ;

Thinking about Tests

  • I must create tests for new features and for those that already exist in the application.
  • I will create unit tests initially. These are minor tests and should not depend on external services .
  • Creating a new appointment:
  • I need to get provider_id and a date.
  • This provider_id must be one that already exists in the bank?
  • What if this provider_id is deleted?
  • Should I create a new user each time I run the tests?
  • Should I create a database for testing only?
  • It is difficult to create tests that depend on external things, such as databases, sending e-mail … etc.
  • Unit testing should not depend on anything other than itself. But in the case of services, they depend on a repository that connects with the bank. Therefore, I will create a fake repository , where it will not connect with the database. Database is passive to errors, so avoid connecting to it in that case.
  • Each service will generate at least one unit test.

Creating First Unit Test

  • Go to the appointment service folder and create CreateAppointmentService.spec.ts
describe ( 'Create Appointment' ,  ( )  =>  { 
	it ( 'should be able to create a new appointment' ,  ( )  =>  { 
	} ) ; 
} ) ;
  • I will create a unit test because it is simpler and should not depend on external libraries , such as typeorm, some database and the like.
  • But for that, I must create my fake repository , which will have my repository interface and so it will not depend on the typeorm , and will save the data in memory.

You can see the Liskov Substitution and Dependency Inversion Principle here. Where my service is depending only on an appointmentsRepository that has IAppointmentsRepository typing, regardless of whether it is a typeorm repository (which saves in the database) or a repository that has pure JavaScript methods (which saves locally).

// @modules / appointments / repositories / fakes / FakeAppointmentsRepository

import  {  uuid  }  from  'uuidv4' ; 
import  Appointment  from  '@ modules / appointments / infra / typeorm / entities / Appointments' ; 
import  ICreateAppointmentDTO  from  '../dtos/ICreateAppointmentsDTO' ; 
import  IAppointmentsRepository  from  '../IAppointmentsRepository' ;

class  FakeAppointmentsRepository  implements  IAppointmentsRepository  { 
	private  appointments : Appointments [ ]  =  [ ] ;

	public  async  findByDate ( date : Date ) : Promise < Appointment | undefined >  { 
		const  findAppointment  =  this . appointments . find ( 
			appointment  =>  appointment . date  ===  date , 
		) ;

		return  findAppointment ; 
	}

	public  async  create ( { provider_id , date } : ICreateAppointmentDTO ) : Promise < Appointment > { 
		const  appointment  =  new  Appointment ( ) ;

		Object . assign ( appointment ,  {  id : uuid ( ) , date , provider_id } ) ;

		this . appointments . push ( appointment ) ;

		return  appointment ; 
	} 
}

export  default  FakeAppointmentsRepository ;
import  FakeAppointmentsRepository  from  '@ modules / appoinetments / repositories / fakes / FakeAppointmentsRepository' ; 
import  CreateAppointmentService  from  './CreateAppointmentService' ;

describe ( 'Create Appointment' ,  ( )  =>  { 
	it ( 'should be able to create a new appointment' ,  ( )  =>  { 
		const  fakeAppointmentsRepository  =  new  FakeAppointmentsRepository ( ) ; 
		const  createAppointment  =  new  CreateAppointmentService ( fakeAppointmentsRepository ) ;

		const  appointment  =  await  createAppointment . execute ( { 
			date : new  Date ( ) , 
			provider_id : '4444' 
		} )

		expect ( appointment ) . toHaveProperty ( 'id' ) ; 
		expect ( appointment . provider_id ) . toBe ( '4444' ) ; 
	} ) ; 
} ) ;

When trying to execute, it will give error, because the test file does not understand the import @modules

  • Go to jest.config.js and import:
const  { pathsToModuleNameMapper }  =  require ( 'ts-jest / utils' ) ; 
const  { compilerOptions }  =  require ( './tsconfig.json' ) ;

module . exports  =  {  
	moduleNameMapper : pathsToModuleNameMapper ( compilerOptions . paths ,  {  prefix : '<rootDir> / src /'  } ) 
}
  • Go to tsconfig.json and remove all unnecessary comments and commas.
$ yarn test

Coverage Report

  • Checks files to see which files are already tested , which are not being tested , among other information .
  • For that, I must go to jest.config.js and enable:
collectCoverage : true , 
collectCoverageFrom : [ 
	'<rootDir> /src/modules/**/*.ts' 
] , 
coverageDir : ' coverage ' ,  // this folder will be created at the root that will hold the 
coverageReporters reports : [ 
	' text -summary ' , 
	' lcov ' , 
] ,
$ yarn test
  • Access the html ./coverage/lcov-report/index.html and view the report of files that were covered by the tests.

Scheduling Tests

import  AppError  from  '@ shared / errors / AppError' ; 
import  FakeAppointmentsRepository  from  '../repositories/fakes/FakeAppointmentsRepository' ; 
import  CreateAppointmentService  from  './CreateAppointmentService' ;

describe ( 'CreateAppointment' ,  ( )  =>  { 
	it ( 'should be able to create a new appointment' ,  async  ( )  =>  { 
		const  fakeAppointmentsRepository  =  new  FakeAppointmentsRepository ( ) ; 
		const  createAppointment  =  new  CreateAppointmentService ( 
			fakeAppointmentsRepository , 
		) ;

		const  appointment  =  await  createAppointment . execute ( { 
			date : new  Date ( ) , 
			provider_id : '4444' , 
		} ) ;

		expect ( appointment ) . toHaveProperty ( 'id' ) ; 
		expect ( appointment . provider_id ) . toBe ( '4444' ) ; 
	} ) ;

	it ( 'should not be able to create two appointments at the same time' ,  async  ( )  =>  { 
		const  fakeAppointmentsRepository  =  new  FakeAppointmentsRepository ( ) ; 
		const  createAppointment  =  new  CreateAppointmentService ( 
			fakeAppointmentsRepository , 
		) ; 
		const  date  =  new  Date ( 2020 ,  5 ,  10 ,  14 ) ;

		await  createAppointment . execute ( { 
			date , 
			provider_id : '4444' , 
		} ) ;

		expect ( 
			createAppointment . execute ( { 
				date , 
				provider_id : '4444' , 
			} ) , 
		) . rejects . toBeInstanceOf ( AppError ) ; 
	} ) ; 
} ) ;

User Creation Tests

  • Unit tests, as stated earlier, should not depend on apis or database, so I am going to create my repository with pure Javascript so as not to depend on the typeorm repository that communicates with the database.
  • Create the fake users repository.
// @modules /users/repositories/fakes/FakeUsersRepository.ts 
import  {  uuid  }  from  'uuidv4' ;

import  IUsersRepository  from  '../IUsersRepository' ; 
import  User  from  '../../infra/typeorm/entities/User' ; 
import  ICreateUserDTO  from  '../../dtos/ICreateUserDTO' ;

class  FakeUsersRepository  implements  IUsersRepository  {  
	private  users : User [ ]  =  [ ] ;

	public  async  findByEmail ( email : string ) : Promise < User | undefined >  { 
		const  findUser  =  this . users . find ( user  =>  user . email  ===  email ) ;

		return  findUser ; 
	}

	public  async  findById ( id : string ) : Promise < User | undefined >  { 
		const  findUser  =  this . users . find ( user  =>  user . id  ===  id ) ;

		return  findUser ; 
	}

	public  async  create ( userData : ICreateUserDTO ) : Promise < User > { 
		const  user  =  new  User ( ) ;

		Object . assign ( user ,  {  id : uuid ( )  } ,  userData ) ;

		this . users . push ( user ) ;

		return  user ; 
	}

	public  async  save ( user : User ) : Promise < User > { 
		const  findIndex  =  this . users . findIndex ( findUser  =>  findUser . id  ===  user . id ) ;

		this . users [ findIndex ]  =  user ;

		return  user ; 
	}

}
  • Now, create the tests
import  AppError  from  '@ shared / errors / AppError' ; 
import  CreateUserService  from  './CreateUserService' ; 
import  FakeUsersRepository  from  '../repositories/fakes/FakeUsersRepository' ;

describe ( 'CreateUser' ,  ( )  =>  { 
	it ( 'should be able to create a new user' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  createUser  =  new  CreateUserService ( fakeUserRepository ) ;

		const  user  =  await  createUser . execute ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' 
		} ) ;

		expect ( user ) . toHaveProperty ( 'id' ) ; 
	} ) ;

	it ( 'should not be able to create two user with the same email' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  createUser  =  new  CreateUserService ( fakeUserRepository ) ;

		await  createUser . execute ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' 
		} ) ;

		expect ( createUser . execute ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' 
		} ) ) . rejects . toBeInstanceOf ( AppError ) ; 
	} ) ; 
} ) ;
$ yarn test

If there is an error related to reflect-metadata, go to jest.config.js and put

setupFiles : [ 
		'reflect-metadata' 
	] ,

Authentication Tests

  • Create the AuthenticateUserService.spec.ts
  • For that, I must import another service besides the AuthenticateUserService, since to authenticate a user, I need to create a user.
  • One test should NEVER depend on another.
  • I can approach it in two ways:
  • Use the repository’s own create method
  • Using the CreateUserService service
import  CreateUserService  from  './CreateUserService' ; 
import  AuthenticateUserService  from  './AuthenticateUserService' ;

describe ( 'AuthenticateUser' ,  ( )  =>  { 
	it ( 'should be able to authenticate' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  createUser  =  new  CreateUserService ( fakeUsersRepository ) ; 
		const  authenticateUser  =  new  AuthenticateUserService ( fakeUsersRepository) ) ;

		const  user  =  await  createUser . execute ( { 
			name : 'Test unit' , 
			email : 'testunit@example.com' , 
			password : '123456' , 
		} )

		const  authUser  =  await  authenticateUser . execute ( { 
			email : 'testunit@example.com' , 
			password : '123456' , 
		} )

		expect ( authUser ) . toHaveProperty ( 'token' ) ; 
		expect ( authUser . user ) . toEqual ( user ) ;

	} ) ; 
} ) ;
  • Now, it is possible to notice that CreateUserService has more than one responsibility:
  • User creation
  • Hash the user’s password
  • In addition to hurting the Single Responsability Principle, it is also hurting the Dependency Inversion Principle , as it depends directly on bcryptjs (it should only depend on an interface) for the logic of hashing the user creation and user authentication service itself .
  • I will take advantage and isolate Hash so that it is possible to reuse it in another service besides CreateUser / AuthenticateUser and thus not hurt the DRY (Don’t Repeat Yourself) concept , that is, not repeat business rules .
// CreateUserService 
import  {  hash  }  from  'bcryptjs' ;
...

@ injectable ( ) 
class  CreateUserService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository , 
	)  { }

	public  async  execute ( { name , email , password } : IRequest ) : Promise < User >  { 
		const  checkUserExist  =  await  this . usersRepository . findByEmail ( email ) ;

		if  ( checkUserExist )  { 
			throw  new  AppError ( 'Email already used!' ) ; 
		}

		const  hashedPassword  =  await  hash ( password ,  8 ) ;

		...
	} 
}

export  default  CreateUserService ;
import  {  compare  }  from  'bcryptjs' ;

...

@ injectable ( ) 
class  AuthenticateUserService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository , 
	)  { }

	public  async  execute ( { password , email } : IRequest ) : Promise < IResponse >  { 
		const  user  =  await  this . usersRepository . findByEmail ( email ) ;

		if  ( ! user )  { 
			throw  new  AppError ( 'Incorrect email / password combination.' ,  401 ) ; 
		}

		const  passwordMatched  =  await  compare ( 
			password , 
			user . password , 
		) ;

	...
	} 
}

export  default  AuthenticateUserService ;
  • I will create a providers folder that will contain features that will be offered to services.
  • Within the providers, I will create HashProvider, which will contain models (interface), implementations (which libraries will I use for hashing), fakes (libraries made with JS Pure for hashing).
// @modules / users / providers / HashProvider / models 
export  default  interface  IHashProvider  { 
	generateHash ( payload : string ) : Promise < string > ; 
	compareHash ( payload : string ,  hashed : string ) : Promise < boolean > ; 
}
// @modules / users / provider / HashProvider / implementations 
import  {  hash ,  compare  }  from  'bcrypjs' ; 
import  IHashProvider  from  '../models/IHashProvider' ;

class  BCryptHashProvider  implements  IHashProvider  {

	public  async  generateHash ( payload : string ) : Promise < string > { 
		return  hash ( payload ,  8 ) ; 
	}

	public  async  compareHash ( payload : string ,  hashed : string ) : Promise < string > { 
		return  compare ( payload ,  hashed ) ; 
	}

}

export  default  BCryptHashProvider ;
  • Go to CreateUserService / AuthenticateUserService and make it depend only on the HashProvider interface .
// CreateUserService 
import  IHashProvider  from  '../providers/HashProvider/models/IHashProvider' ;

@ injectable ( ) 
class  CreateUserService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		private  hashProvider : IHashProvider , 
	)  { }

	public  async  execute ( { name , email , password } : IRequest ) : Promise < User >  { 
		const  checkUserExist  =  await  this . usersRepository . findByEmail ( email ) ;

		if  ( checkUserExist )  { 
			throw  new  AppError ( 'Email already used!' ) ; 
		}

		const  hashedPassword  =  await  this . hashProvider . generateHash ( password ) ;

		const  user  =  await  this . usersRepository . create ( { 
			name , 
			email , 
			password : hashedPassword , 
		} ) ;

		return  user ; 
	} 
}

export  default  CreateUserService ;
// AuthenticateUserService 
import  {  injectable ,  inject  }  from  'tsyringe' ; 
import  {  sign  }  from  'jsonwebtoken' ; 
import  authConfig  from  '@ config / auth' ; 
import  AppError  from  '@ shared / errors / AppError' ; 
import  User  from  '../infra/typeorm/entities/User' ;

import  IUsersRepository  from  '../repositories/IUsersRepository' ; 
import  IHashProvider  from  '../providers/HashProvider/models/IHashProvider' ;

interface  IRequest  { 
	email : string ; 
	password : string ; 
}

 IResponse  interface { 
	user : User ; 
	token : string ; 
} 
@ injectable ( ) 
class  AuthenticateUserService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		private  hashProvider : IHashProvider , 
	)  { }

	public  async  execute ( { password , email } : IRequest ) : Promise < IResponse >  { 
		const  user  =  await  this . usersRepository . findByEmail ( email ) ;

		if  ( ! user )  { 
			throw  new  AppError ( 'Incorrect email / password combination.' ,  401 ) ; 
		}

		const  passwordMatched  =  await  this . hashProvider . compareHash ( 
			password , 
			user . password , 
		) ;

		if  ( ! passwordMatched )  { 
			throw  new  AppError ( 'Incorrect email / password combination.' ,  401 ) ; 
		}

		const  { secret , expiresIn }  =  authConfig . jwt ;

		const  token  =  sign ( { } ,  secret ,  { 
			subject : user . id , 
			expiresIn , 
		} ) ;

		return  { user , token } ; 
	} 
}

export  default  AuthenticateUserService ;
  • Now I need to inject the IHashProvider dependency.
// @modules /users/provider/index.ts 
import  {  container  }  from  'tsyringe' ;

import  BCryptHashProvider  from  './HashProvider/implementations/BCryptHashProvider' ; 
import  IHashProvider  from  './HashProvider/models/IHashProvider' ;

container . registerSingleTon < IHashProvider > ( 'HashProvider' ,  BCryptHashProvider ) ;
  • I need to inject this dependency into the service and import this index.ts into @ shared / container / index.ts;
import  IHashProvider  from  '../providers/HashProvider/models/IHashProvider' ;

@ injectable ( ) 
class  CreateUserService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		@ inject ( 'HashProvider' ) 
		private  hashProvider : IHashProvider , 
	)  { }

	...

	} 
}

export  default  CreateUserService ;
// @shared /container/index.ts 
import  '@ modules / users / providers' ;

...
  • Create my FakeHashProvider, as my tests should not depend on strategies or other libraries like bcryptjs .
import  IHashProvider  from  '../models/IHashProvider' ;

class  FakeHashProvider  implements  IHashProvider  {

	public  async  generateHash ( payload : string ) : Promise < string > { 
		return  payload ; 
	}

	public  async  compareHash ( payload : string ,  hashed : string ) : Promise < boolean > { 
		return  payload  ===  hashed ; 
	}

} ;

export  default  FakeHashProvider ;
  • Go to AuthenticateUserService.spec.ts
import  CreateUserService  from  './CreateUserService' ; 
import  AuthenticateUserService  from  './AuthenticateUserService' ; 
import  FakeHashProvider  from  '../providers/HashProvider/fakes/FakeHashProviders' ;

describe ( 'AuthenticateUser' ,  ( )  =>  { 
	it ( 'should be able to authenticate' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  fakeHashProvider  =  new  FakeHashProvider ( ) ; 
		const  createUser  =  new  CreateUserService ( 
			fakeUsersRepository , 
			fakeHashProvider 
		) ; 
		const  authenticateUser =  new  AuthenticateUserService ( 
			fakeUsersRepository , 
			fakeHashProvider 
		) ;

		const  user  =  await  createUser . execute ( { 
			name : 'Test unit' , 
			email : 'testunit@example.com' , 
			password : '123456' , 
		} )

		const  authUser  =  await  authenticateUser . execute ( { 
			email : 'testunit@example.com' , 
			password : '123456' , 
		} )

		expect ( authUser ) . toHaveProperty ( 'token' ) ; 
		expect ( authUser . user ) . toEqual ( user ) ;

	} ) ; 
} ) ;

Provider Storage

  • Currently, UpdateUserAvatarService is using lib multer to upload an image. We cannot say that only the user module will upload files , so we will isolate this logic for the shared and create models , implementations , fakes , dependency injection as was done in HashProvider.
class  UpdateUserAvatarService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository , 
	) { }

	public  async  execute ( { user_id , avatarFilename } : IRequest ) : Promise < User >  { 
		const  user  =  await  this . usersRepository . findById ( user_id ) ; 
		if  ( ! user )  { 
			throw  new  AppError ( 'Only authenticated user can change avatar' ,  401 ) ; 
		}

		// this direct dependency on the multer must be removed, as it ends up hurting liskov substitution and dependency inversion principle 
		if  ( user . avatar )  { 
			const  userAvatarFilePath  =  path . join ( uploadConfig . directory ,  user . avatar ) ; 
			const  userAvatarFileExists  =  await  fs . promises . stat ( userAvatarFilePath ) ; 
			if  ( userAvatarFileExists )  {
				await  fs . promises . unlink ( userAvatarFilePath ) ; 
			} 
			user . avatar  =  avatarFilename ; 
			await  this . usersRepository . save ( user ) ;

			return  user ; 
		} 
	} 
}

export  default  UpdateUserAvatarService ;
  • Create folder in shared / container / providers / StorageProvider .
  • Within StorageProvider there will be models , implementations , fakes .
	//shared/container/providers/StorageProvider/models/IStorageProvider.ts

	export  default  interface  IStorageProvider  { 
		saveFile ( file : string ) : Promise < string > ; 
		deleteFile ( file : string ) : Promise < string > ; 
	}
  • Change the uploadConfig
...

const  tmpFolder  =  path . resolve ( __dirname ,  '..' ,  '..' ,  'tmp'  ) ;

export  default  { 
	tmpFolder ,

	uploadsFolder : path . join ( tmpFolder ,  'uploads' ) ;

	...
}
	//shared/container/providers/StorageProvider/implementations/DiskStorageProvider.ts 
	import  fs  from  'fs' ; 
	import  path  from  'path' ; 
	import  uploadConfig  from  '@ config / upload' ;

	import  IStorageProvider  from  '../models/IStorageProvider' ;

	class  DiskStorageProvider  implements  IStorageProvider  { 
		public  async  saveFile ( file : string ) : Promise < string > { 
			await  fs . promises . rename ( 
				path . resolve ( uploadConfig . tmpFolder ,  file ) , 
				path . resolve ( uploadConfig . uploadsFolder ,  file ) , 
			);

			return  file ; 
		}

		public  async  deleteFile ( file : string ) : Promise < void > { 
			const  filePath  =  path . resolves ( uploadConfig . uploadsFolder ,  file ) ;

			try { 
				await  fs . promises . stat ( filePath ) ; 
			}  catch  { 
				return ; 
			}

			await  fs . promises . unlink ( filePath ) ;

		} 
	}

	export  default  DiskStorageProvider ;
  • Inject the dependency that will be on UpdateUserAvatarService;
  • Create @ shared / container / providers / index.ts and import this container into @ shared / container / index.ts .
import  {  container  }  from  'tsyringe' ;

import  IStorageProvider  from  './StorageProvider/models/IStorageProvider' ; 
import  DiskStorageProvider  from  './StorageProvider/implementations/DiskStorageProvider' ;

container . registerSingleton < IStorageProvider > ( 
	'StorageProvider' , 
	DiskStorageProvider ) ;
  • Go to UpdateUserAvatarService.ts
import  path  from  'path' ; 
import  fs  from  'fs' ; 
import  uploadConfig  from  '@ config / upload' ; 
import  AppError  from  '@ shared / errors / AppError' ;

import  {  injectable ,  inject  }  from  'tsyringe' ; 
import  IStorageProvider  from  '@ shared / container / providers / StorageProvider / models / IStorageProvider' ; 
import  User  from  '../infra/typeorm/entities/User' ; 
import  IUsersRepository  from  '../repositories/IUsersRepository' ;

interface  IRequest  { 
	user_id : string ; 
	avatarFilename : string ; 
}

@ injectable ( ) 
class  UpdateUserAvatarService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		@ inject ( 'StorageProvider' ) 
		private  storageProvider : IStorageProvider , 
	)  { }

	public  async  execute ( { user_id , avatarFileName } : IRequest ) : Promise < User >  { 
		const  user  =  await  this . usersRepository . findById ( user_id ) ;

		if  ( ! user )  { 
			throw  new  AppError ( 'Only authenticated user can change avatar' ,  401 ) ; 
		}

		if  ( user . avatar )  { 
			await  this . storageProvider . deleteFile ( user . avatar ) ; 
		}

		const  filePath  =  await  this . storageProvider . saveFile ( avatarFileName ) ;

		user . avatar  =  filePath ;

		await  this . usersRepository . save ( user ) ;

		return  user ; 
	} 
} 
export  default  UpdateUserAvatarService ;
  • Create the fakes storage provider, which will be used in the tests since the unit tests should not depend on any library.
// @shared /container/providers/StorageProvider/fakes/FakeStorageProvider.ts 
	import  IStorageProvider  from  '../models/IStorageProvider' ;

	class  FakeStorageProvider  implements  IStorageProvider  {

		private  storage : string [ ]  =  [ ] ;

		public  async  saveFile ( file : string ) : Promise < string >  { 
			this . storage . push ( file ) ;

			return  file ; 
		}

		public  async  deleteFile ( file : string ) : Promise < void >  { 
			const  findIndex  =  this . storage . findIndex ( item  =>  item  ===  file ) ;

			this . storage . splice ( findIndex ,  1 ) ; 
		} 
	}

	export  default  FakeStorageProvider ;

Updating Avatar

  • Create UpdateUserAvatarService.spec.ts unit test
import  AppError  from  '@ shared / errors / AppError' ;

import  FakeUsersRepository  from  '../repositories/fakes/FakeUserRepository' ; 
import  FakeStorageProvider  from  '@ shared / container / providers / StorageProvider / fakes / FakeStorageProvider' ; 
import  UpdateUserAvatarService  from  './UpdateUserAvatarService' ;

describe ( 'UpdateUserAvatarService' ,  ( )  =>  { 
	it ( 'should be able to update user avatar' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  fakeStorageProvider  =  new  FakeStorageProvider ( ) ; 
		const  updateUserAvatarService  =  new  UpdateUserAvatarService ( 
			fakeUsersRepository , 
			fakeStorageProvider 
		) ;

		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test Unit' , 
			email : 'testunit@example.com' , 
			password : '123456' , 
		} ) ;

		await  updateUserAvatarService . execute ( { 
				user_id : user . id , 
				avatarFileName : 'avatar.jpg' , 
		} )

		expect ( user . avatar ) . toBe ( 'avatar.jpg' ) ;

	} ) ;

	it ( 'should not be able to update avatar from non existing user' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  fakeStorageProvider  =  new  FakeStorageProvider ( ) ; 
		const  updateUserAvatarService  =  new  UpdateUserAvatarService ( 
			fakeUsersRepository , 
			fakeStorageProvider 
		) ;

		expect ( updateUserAvatarService . execute ( { 
				user_id : 'non-existing-id' , 
				avatarFileName : 'avatar.jpg' , 
		} ) ) . rejects . toBeInstanceOf ( AppError ) ;

	} ) ;

	it ( 'should delete old avatar when updating a new one' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  fakeStorageProvider  =  new  FakeStorageProvider ( ) ; 
		const  updateUserAvatarService  =  new  UpdateUserAvatarService ( 
			fakeUsersRepository , 
			fakeStorageProvider 
		) ;

		const  deleteFile  =  jest . spyOn ( fakeStorageProvider ,  'deleteFile' ) ;

		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test Unit' , 
			email : 'testunit@example.com' , 
			password : '123456' , 
		} ) ;

		await  updateUserAvatarService . execute ( { 
				user_id : user . id , 
				avatarFileName : 'avatar.jpg' , 
		} ) 
		await  updateUserAvatarService . execute ( { 
				user_id : user . id , 
				avatarFileName : ' avatar2.jpg ' , 
		} )

		expect ( deleteFile ) . toHaveBeenCalledWithin ( 'avatar.jpg' ) ; 
		expect ( user . avatar ) . toBe ( ' avatar2.jpg ' ) ;

	} ) ;

} ) ;

const deleteFile = jest.spyOn (fakeStorageProvider, ‘deleteFile’) checks whether the function was called or not, and in the end I hope it was called with the avatar.jpg parameter .

Mapping Application Features

Made in Software Engineering.

I must :

- Mapear os Requisitos
- Conhecer as Regras de Negócio
  • DEV will not always have the necessary resources to map ALL the application’s features.

GoBarber will be mapped based on your Design. In this case, it will only be a guide for us, the mapping of the features does not need to describe ALL the features exactly, some of them will appear during the development.

  • In the real world, MANY MEETINGS must be held with the CUSTOMER so that MANY NOTES are made , to MAP FUNCTIONALITIES and BUSINESS RULES !

  • Is BASED on Feedback from the customer to be improved. I won’t always get it right the first time.

  • I must think of a project in a simple way . There is no point creating many features if the customer uses only 20% of them and wants them to be improved.

  • Create with Macro Functions in mind .

Macro Features

  • Inside they are well defined , but we can see them as an application screen .

  • And each macro functionality will be well described and documented, being divided into: Functional, Non-Functional Requirements and Business Rules.

Functional Requirements

  • Describes the functionality itself.

Non-Functional Requirements

  • The Business Rules are not directly linked.
  • Focused on the technical part.
  • It is linked to some lib that we choose to use.

Business rules

  • They are restrictions so that the functionality can be executed.
  • It is legal for a business rule to be associated with a functional requirement.

Macro Features - GoBarber

Password recovery

Functional Requirements

  • The user must be able to change the password informing his email.
  • The user should receive an email with instructions to reset their password.
  • The user must be able to change his password.

Non-Functional Requirements

  • Use Mailtrap for testing in a development environment.
  • Use Amazon SES to send email in production
  • The sending of email must happen in the background (background job).

Example: if 400 users try to recover their password at the same time, it would be trying to send 400 emails at the same time , which could cause a slow service . Therefore, the approach adopted will be of queue (background job) , where the queue will be processed little by little by the tool.

Business rules

  • The link that will be sent by email, should expire in 2h.
  • The user must confirm the password when resetting the password.
Profile Update

Functional Requirements

  • The user must be able to update his name, email and password.

Non-Functional Requirements

Business rules

  • The user cannot change the email to another email already used by another user.
  • When changing the password, the user must inform the old one.
  • When changing the password, the user must confirm the new password.
Provider Panel

Functional Requirements

  • The user must be able to view the schedules for a specific day.
  • The provider must receive a notification whenever there is a new appointment.
  • The provider must be able to view unread notifications.

Non-Functional Requirements

  • The provider’s schedules for the day must be cached.

It is possible that the provider is reloading the page many times throughout the day. Soon, the cache will be cleared and stored only when you have a new schedule.

  • The provider’s notifications must be stored in MongoDB.
  • Service provider notifications must be sent in real time using Socket.io.

Business rules

  • The notification must have a read or unread status for the provider to control.
Service Scheduling

Functional Requirements

  • The user must be able to list all registered service providers.
  • The user must be able to list days of a month with at least one time available by the provider.
  • The user must be able to list available times on a specific day for a provider.
  • The user must be able to make a new appointment with the provider.

Non-Functional Requirements

  • The list of providers must be cached.

This way, you will avoid processing costs of the machine.

Business rules

  • Each appointment must last exactly 1 hour.
  • Appointments must be available between 8 am and 6 pm (first time at 8 am, last time at 5 pm).
  • The user cannot schedule at an already busy time.
  • The user cannot schedule an appointment that has passed.
  • The user cannot schedule services with himself.
  • The user can only make an appointment at a time with a service provider.

Applying TDD in practice

  • I will take macro functionality and start writing your tests, taking one functional requirement at a time.

Problem

How am I going to write tests for a feature that doesn’t even exist yet?

Solution

For this, I will create a basic and minimal structure, so that it is possible to fully test a functionality even if it does not yet exist.

  • Create the MailProvider in the shared folder, since sending email is not necessarily just a password recovery request.

  • Create in @ shared / container / providers / MailProvider the models folders (rules that implementations must follow), implementations (which are the email service providers), fakes (for testing) .

// models / IMailProvider

export  default  interface  IMailProvider  { 
	sendMail ( to : string ,  body : string ) : Promise < void > ; 
}
// fakes / FakeMailProvider

import  IMailProvider  from  '../models/IMailProvider' ;

interface  IMessage  {  
	to : string ; 
	body : string ; 
}

class  FakeMailProvider  implements  IMailProvider  { 
	private  messages : IMessage [ ]  =  [ ] ;

	public  async  sendMail ( to : string ,  body : string ) : Promise < void >  { 
		this . messages . push ( { 
			to , 
			body , 
		} ) ; 
	} 
}

export  default  FakeMailProvider ;
  • Create SendForgotPasswordEmailService.ts and SendForgotPasswordEmailService.spec.ts
// SendForgotPasswordEmailService.ts 
import  {  injectable ,  inject  }  from  'tsyringe' ;

import  IUsersRepository  from  '../repositories/IUsersRepository' ; 
import  IMailProvider  from  '@ shared / container / providers / MailProvider / models / IMailProvider' ;

interface  IRequest  { 
	email : string ; 
}

@ injectable ( ) 
class  SendForgotPasswordEmailService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		@ inject ( 'MailProvider' ) 
		private  mailProvider : IMailProvider 
	)

	public  async  execute ( { email } : IRequest ) : Promise < void > { 
		this . mailProvider . sendMail ( email ,  'Password recovery request' ) ; 
	} ; 
}

export  default  SendForgotPasswordEmailService
// SendForgotPasswordEmailService.spec.ts

import  SendForgotPasswordEmailService  from  './SendForgotPasswordEmailService' ; 
import  FakeUsersRepository  from  '../repositories/fakes/FakeUsersRepository' ; 
import  FakeMailProvider  from  '@ shared / container / providers / MailProvider / fakes / FakeMailProvider' ;

describe ( 'SendForgotPasswordEmail' ,  ( )  =>  { 
	it ( 'should be able to recover the password using email' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  fakeMailProvider  =  new  FakeMailProvider ( ) ;

		const  sendForgotPasswordEmail  =  new  SendForgotPasswordEmailService ( 
			fakeUsersRepository , 
			fakeMailProvider , 
		) ;

		const  sendMail  =  jest . spyOn ( fakeMailProvider ,  'sendMail' ) ;

		await  fakeUsersRepository . create ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' , 
		} ) ;

		await  sendForgotPasswordEmail . execute ( { 
			email : 'test@example.com' , 
		} ) ;

		expect ( sendMail ) . toHaveBeenCalled ( ) ;

	} ) 
} ) ;
  • Now just run the test.

Recovering password

  • Build a test so that it is not possible to recover the password of a user that does not exist
describe ( 'SendForgotPasswordEmail' ,  ( )  =>  { 
	it ( 'should be able to recover the password using email' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  fakeMailProvider  =  new  FakeMailProvider ( ) ;

		const  sendForgotPasswordEmail  =  new  SendForgotPasswordEmailService ( 
			fakeUsersRepository , 
			fakeMailProvider , 
		) ;

		const  sendMail  =  jest . spyOn ( fakeMailProvider ,  'sendMail' ) ;

		await  fakeUsersRepository . create ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' , 
		} ) ;

		await  sendForgotPasswordEmail . execute ( { 
			email : 'test@example.com' , 
		} ) ;

		expect ( sendMail ) . toHaveBeenCalled ( ) ;

	} ) ;

	it ( 'should not be able to recover the password a non-existing user' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  fakeMailProvider  =  new  FakeMailProvider ( ) ;

		const  sendForgotPasswordEmail  =  new  SendForgotPasswordEmailService ( 
			fakeUsersRepository , 
			fakeMailProvider , 
		) ;

		await 	expect ( 
			sendForgotPasswordEmail . execute ( { 
			email : 'test@example.com' , 
		} ) ) . rejects . toBeInstanceOf ( AppError ) ;

	} ) 
} ) ;
  • If I run this new test ( should not be able to recover the password a non-existing user ), an ‘error’ will occur saying that: ‘an error was expected, but the promise has been resolved’. That is, instead of being reject , it was resolved .

  • If it is observed in the service SendForgotPasswordEmailService.ts , it is possible to see that there is no verification whether the user exists or not before sending the email, therefore, it must be added.

// SendForgotPasswordEmailService.ts 
import  {  injectable ,  inject  }  from  'tsyringe' ;

import  IUsersRepository  from  '../repositories/IUsersRepository' ; 
import  IMailProvider  from  '@ shared / container / providers / MailProvider / models / IMailProvider' ;

interface  IRequest  { 
	email : string ; 
}

@ injectable ( ) 
class  SendForgotPasswordEmailService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		@ inject ( 'MailProvider' ) 
		private  mailProvider : IMailProvider 
	)

	public  async  execute ( { email } : IRequest ) : Promise < void > { 
		const  checkUserExist   =  await  this . usersRepository . findByEmail ( email ) ;

		if  ( ! checkUserExist  )  { 
			throw  new  AppError ( 'User does not exists.' ) ; 
		}

		this . mailProvider . sendMail ( email ,  'Password recovery request' ) ; 
	} ; 
}

export  default  SendForgotPasswordEmailService

Thus, it was possible to notice that the service was being built based on the application tests.

Recovering password

Problem

It was observed that in the link that the user will receive by email, it must have an encryption (token) so that it is not possible to change the password of another user, and to ensure that the password recovery has been provided with a request.

Solution

  1. Create and map UserToken entity in @ modules / users / infra / typeorm / entities
// @modules /users/infra/typeorm/entities/UserToken.ts 
import  { 
	Entity , 
	PrimaryGeneratedColumn , 
	Column , 
	CreateDateColumn , 
	UpdateDateColumn , 
	Generated , 
}  from  'typeorm' ;

@ Entity ( 'user_tokens' ) 
class  UserToken  { 
	@ PrimaryGeneratedColumn ( 'uuid' ) 
	id : string ;

	@ Column ( ) 
	@ Generated ( 'uuid' ) 
	token : string ;

	@ Column ( ) 
	user_id : string ;

	@ CreateDateColumn ( ) 
	created_at : Date ;

	@ UpdateDateColumn ( ) 
	updated_at : Date ; 
}

export  default  UserToken ;
  1. Create the interface for the IUserToken repository
import  UserToken  from  '../infra/typeorm/entities/UserToken' ;

export  default  interface  IUserTokenRepository  { 
	generate ( user_id : string ) : Promise < UserToken > ; 
}
  1. Create the fake repository using the interface
import  {  uuid  }  from  'uuidv4' ;

import  UserToken  from  '../../infra/typeorm/entities/UserToken' ; 
import  IUserTokenRepository  from  '../IUserTokenRepository' ;

class  FakeUserTokensRepository  implements  IUserTokenRepository  {

	public  async  generate ( user_id : string ) : Promise < UserToken >  { 
		const  userToken  =  new  UserToken ( ) ;

		Object . assign ( userToken ,  { 
			id : uuid ( ) , 
			token : uuid ( ) , 
			user_id , 
		} )

		return  userToken ; 
	}

}

export  default  FakeUserTokensRepository ;
  1. Create the test to verify that the repository method for creating the token is being called.
  2. Change in SendForgotPasswordEmailService.ts to use the generate method of UserTokensRepository
import  {  injectable ,  inject  }  from  'tsyringe' ;

import  IUsersRepository  from  '../repositories/IUsersRepository' ; 
import  IUserTokensRepository  from  '../repositories/IUserTokensRepository' ; 
import  IMailProvider  from  '@ shared / container / providers / MailProvider / models / IMailProvider' ;

interface  IRequest  { 
	email : string ; 
}

@ injectable ( ) 
class  SendForgotPasswordEmailService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		@ inject ( 'MailProvider' ) 
		private  mailProvider : IMailProvider ,

		@ inject ( 'UserTokensRepository' ) 
		private  userTokensRepository : IUserTokensRepository , 
	)

	public  async  execute ( { email } : IRequest ) : Promise < void > { 
		const  user   =  await  this . usersRepository . findByEmail ( email ) ;

		if  ( ! user )  { 
			throw  new  AppError ( 'User does not exists.' ) ; 
		}

		await  this . userTokensRepository . generate ( user . id ) ;

		await  this . mailProvider . sendMail ( email ,  'Password recovery request' ) ; 
	} ; 
}
  1. Declare variables (let) in the file, and add the beforeEach method to instantiate the necessary classes before each test, to avoid repetition of creating instances.
import  AppError  from  '@ shared / errors / AppError' ;

import  FakeMailProvider  from  '@ shared / container / providers / MailProvider / fakes / FakeMailProvider' ; 
import  FakeUserTokenRepository  from  '@ modules / users / repositories / fakes / FakeUserTokensRepository' ; 
import  FakeUsersRepository  from  '../repositories/fakes/FakeUsersRepository' ; 
import  SendForgotPasswordEmailService  from  './SendForgotPasswordEmailService' ;

let  fakeUsersRepository : FakeUsersRepository ; 
let  fakeUserTokensRepository : FakeUserTokenRepository ; 
let  fakeMailProvider : FakeMailProvider ; 
let  sendForgotPasswordEmail : SendForgotPasswordEmailService ;

describe ( 'SendForgotPasswordEmail' ,  ( )  =>  { 
	beforeEach ( ( )  =>  { 
		fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		fakeUserTokensRepository  =  new  FakeUserTokenRepository ( ) ; 
		fakeMailProvider  =  new  FakeMailProvider ( ) ;

		sendForgotPasswordEmail  =  new  SendForgotPasswordEmailService ( 
			fakeUsersRepository , 
			fakeMailProvider , 
			fakeUserTokensRepository , 
		) ; 
	} ) ;

	it ( 'should be able to recover the password using the email' ,  async  ( )  =>  { 
		const  sendEmail  =  jest . spyOn ( fakeMailProvider ,  'sendMail' ) ;

		await  fakeUsersRepository . create ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' , 
		} ) ;

		await  sendForgotPasswordEmail . execute ( { 
			email : 'test@example.com' , 
		} ) ;

		expect ( sendEmail ) . toHaveBeenCalled ( ) ; 
	} ) ;

	it ( 'should not be able to recover the password of a non-existing user' ,  async  ( )  =>  { 
		await  expect ( 
			sendForgotPasswordEmail . execute ( { 
				email : 'test@example.com' , 
			} ) , 
		) . rejects . toBeInstanceOf ( AppError ) ; 
	} ) ;

	it ( 'should generate a forgot password token' ,  async  ( )  =>  { 
		const  generateToken  =  jest . spyOn ( fakeUserTokensRepository ,  'generate' ) ;

		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' , 
		} ) ;

		await  sendForgotPasswordEmail . execute ( {  email : 'test@example.com'  } ) ;

		expect ( generateToken ) . toHaveBeenCalledWith ( user . id ) ; 
	} ) ; 
} ) ;

Password reset

  1. Create the findByToken method in the UserTokens interface;
import  UserToken  from  '../infra/typeorm/entities/UserToken' ;

export  default  interface  IUserTokenRepository  { 
	generate ( user_id : string ) : Promise < UserToken > ; 
	findByToken ( token : string ) : Promise < UserToken | undefined > ; 
}
  1. Create _ResetPasswordService.spec.ts. _;
import  FakeUsersRepository  from  '../repositories/fakes/FakeUsersRepository' ; 
import  FakeUserTokensRepository  from  '../repositories/fakes/FakeUserTokensRepository' ; 
import  FakeHashProvider  from  '../provider/HashProvider/fakes/FakeHashProvider' ; 
import  ResetPasswordService  from  './ResetPasswordService' ;

let  fakeUsersRepository : FakeUsersRepository ; 
let  fakeUserTokensRepository : FakeUserTokensRepository ; 
let  resetPassword : ResetPasswordService ;

describe ( 'ResetPassword' ,  ( )  =>  { 
	beforeEach ( ( )  =>  { 
		fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		fakeUserTokensRepository  =  new  FakeUserTokensRepository ( ) ; 
		resetPassword  =  new  ResetPasswordService ( 
			fakeUsersRepository , 
			fakeUserTokensRepository , 
		) ; 
	} )

	it ( 'should be able to reset the password' ,  ( )  =>  { 
		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' , 
		} ) ;

		const  generateHash  =  jest . spyOn ( fakeHashProvider ,  'generateHash' ) ;

		const  { token }  =  await  fakeUserTokensRepository . generate ( user . id ) ;

		await  resetPassword . execute ( { 
			token , 
			password : '4444' , 
		} ) ;

		expect ( user . password ) . toBe ( '4444' ) ; 
		expect ( generateHash ) . toHaveBeenCalledWith ( '4444' ) ; 
	} ) ; 
} ) ;
  1. Create ResetPasswordService.ts ;
import  {  injectable ,  inject  }  from  'tsyringe' ; 
import  AppError  from  '@ shared / errors / AppError' ;

import  IUsersRepository  from  '../repositories/IUsersRepository' ; 
import  IUserTokensRepository  from  '../repositories/IUserTokensRepository' ; 
import  IHashProvider  from  '../providers/HashProvider/models/IHashProvider' ;

interface  IRequest  { 
	token : string ; 
	password : string ; 
}

@ injectable ( ) 
class  ResetPasswordService  {

	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		@ inject ( 'UserTokensRepository' ) 
		private  userTokensRepository : IUserTokensRepository ,

		@ inject ( 'HashProvider' ) 
		private  hashProvider : IHashProvider , 
	)

	public  async  execute ( { token , password } : IRequest ) : Promise < void > { 
		const  userToken  =  await  this . userTokensRepository . findByToken ( token ) ;

		if  ( ! userToken )  { 
			throw  new  AppError ( 'User Token does not exist.' ) ; 
		}

		const  user  =  await  this . usersRepository . findById ( userToken . user_id ) ;

		if  ( ! user )  { 
			throw  new  AppError ( 'User does not exists.' ) ; 
		}

		user . password  =  await  this . hashProvider . generateHash ( password ) ;

		await  this . usersRepository . save ( user ) ; 
	} 
}

export  default  ResetPasswordService ;

Finishing the tests

  1. Go to ResetPasswordService.spec.ts and add more unit tests for:
  • non-existent userToken case
  • nonexistent user case
  • the token expires in 2h
import  AppError  from  '@ shared / errors / AppError' ;

import  FakeUsersRepository  from  '../repositories/fakes/FakeUsersRepository' ; 
import  FakeUserTokensRepository  from  '../repositories/fakes/FakeUserTokensRepository' ; 
import  FakeHashProvider  from  '../provider/HashProvider/fakes/FakeHashProvider' ; 
import  ResetPasswordService  from  './ResetPasswordService' ;

let  fakeUsersRepository : FakeUsersRepository ; 
let  fakeUserTokensRepository : FakeUserTokensRepository ; 
let  resetPassword : ResetPasswordService ;

describe ( 'ResetPassword' ,  ( )  =>  { 
	beforeEach ( ( )  =>  { 
		fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		fakeUserTokensRepository  =  new  FakeUserTokensRepository ( ) ; 
		resetPassword  =  new  ResetPasswordService ( 
			fakeUsersRepository , 
			fakeUserTokensRepository , 
		) ; 
	} )

	it ( 'should be able to reset the password' ,  ( )  =>  { 
		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' , 
		} ) ;

		const  generateHash  =  jest . spyOn ( fakeHashProvider ,  'generateHash' ) ;

		const  { token }  =  await  fakeUserTokensRepository . generate ( user . id ) ;

		await  resetPassword . execute ( { 
			token , 
			password : '4444' , 
		} ) ;

		expect ( user . password ) . toBe ( '4444' ) ; 
		expect ( generateHash ) . toHaveBeenCalledWith ( '4444' ) ; 
	} ) ;

	it ( 'should not be able to reset the password with a non-existing user' ,  ( )  =>  { 
		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' , 
		} ) ;

		const  { token }  =  await  fakeUserTokensRepository . generate ( 'non-existing user' ) ;

		await  expect ( resetPassword . execute ( { 
			token , 
			password : '4444' , 
		} ) ) . rejects . toBeInstanceOf ( AppError ) ; 
	} ) ;

	it ( 'should not be able to reset the password with a non-existing token' ,  ( )  =>  { 
		await  expect ( resetPassword . execute ( { 
			token : 'non-existing-token' , 
			password : '4444' , 
		} ) ) . rejects . toBeInstanceOf ( AppError ) ; 
	} ) ;

	it ( 'should not be able to reset the password if passed more than 2 hours' ,  ( )  =>  { 
		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' , 
		} ) ;

		jest . spyOn ( Date ,  'now' ) . mockImplementationOnce ( ( )  =>  { 
			const  customDate  =  new  Date ( ) ;

			return  customDate . setHours ( customDate . getHours ( )  +  3 ) ; 
		} ) ;

		const  { token }  =  await  fakeUserTokensRepository . generate ( user . id ) ;

		await  expect ( resetPassword . execute ( { 
			token , 
			password : '4444' , 
		} ) ) . rejects . toBeInstanceOf ( AppError ) ; 
	} ) 
} ) ;
  1. Generate a date when a FakeUserToken is generated;
import  {  uuid  }  from  'uuidv4' ;

import  UserToken  from  '../../infra/typeorm/entities/UserToken' ; 
import  IUserTokenRepository  from  '../IUserTokenRepository' ;

class  FakeUserTokensRepository  implements  IUserTokenRepository  {

	public  async  generate ( user_id : string ) : Promise < UserToken >  { 
		const  userToken  =  new  UserToken ( ) ;

		Object . assign ( userToken ,  { 
			id : uuid ( ) , 
			token : uuid ( ) , 
			user_id , 
			created_at : new  Date ( ) , 
			updated_at : new  Date ( ) , 
		} )

		return  userToken ; 
	}

}

export  default  FakeUserTokensRepository ;
  1. Use some date-fns functions to check the token’s creation date;
import  {  injectable ,  inject  }  from  'tsyringe' ; 
import  {  isAfter ,  addHours  }  from  'date-fns' ;

import  AppError  from  '@ shared / errors / AppError' ;

import  IUsersRepository  from  '../repositories/IUsersRepository' ; 
import  IUserTokensRepository  from  '../repositories/IUserTokensRepository' ; 
import  IHashProvider  from  '../providers/HashProvider/models/IHashProvider' ;

interface  IRequest  { 
	token : string ; 
	password : string ; 
}

@ injectable ( ) 
class  ResetPasswordService  {

	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		@ inject ( 'UserTokensRepository' ) 
		private  userTokensRepository : IUserTokensRepository ,

		@ inject ( 'HashProvider' ) 
		private  hashProvider : IHashProvider , 
	)

	public  async  execute ( { token , password } : IRequest ) : Promise < void > { 
		const  userToken  =  await  this . userTokensRepository . findByToken ( token ) ;

		if  ( ! userToken )  { 
			throw  new  AppError ( 'User Token does not exist.' ) ; 
		}

		const  user  =  await  this . usersRepository . findById ( userToken . user_id ) ;

		if  ( ! user )  { 
			throw  new  AppError ( 'User does not exists.' ) ; 
		}

		const  tokenCreateAt  =  userToken . created_at 
		const  compareDate  =  addHours ( tokenCreateAt ,  2 ) ;

		if  ( isAfter ( Date . now ( ) ,  compareDate ) )  { 
			throw  new  AppError ( 'Token expired.' ) ; 
		}

		user . password  =  await  this . hashProvider . generateHash ( password ) ;

		await  this . usersRepository . save ( user ) ; 
	} 
}

export  default  ResetPasswordService ;

addHours adds hours on a specific date; isAfter will compare if the date the service was executed is greater than the date the token was created added to 2 hours. If this is true, it means that the tokenCreatedAt + 2 (hours) has passed the current time, that is, that the token has expired.

  • non-existent userToken case
  • nonexistent user case
  • the token expires in 2h

Saving Tokens to the Bank

  1. Create Routes and Controllers
  2. Create the Token Repository (TypeORM)
  3. Create Migration for Token Creation
  4. Inject Token Dependency
  5. Email sending provider (DEV)
  6. Test everything

Remembering that the controllers of a Restful API must have a maximum of 5 methods:

  • index (list all)
  • show (list only one)
  • create
  • update
  • delete

Start by creating routes ( password.routes.ts ) and controllers ( ForgotPasswordController.ts , ResetPasswordControler.ts )

// ForgotPasswordController.ts 
import  {  container  }  from  'tsyringe'

import  {  Request ,  Response  }  from  'express' ;

import  ForgotPasswordEmailService  from  '@ modules / users / services / ForgotPasswordEmailService' ;

class  ForgotPasswordController  { 
	public  async  create ( request : Request ,  response : Response ) : Promise < Response > { 
		const  { email }  =  request . body ;

		const  forgotPassword  =  container . resolve ( ForgotPasswordEmailService ) ;

		await  forgotPassword . execute ( { 
			email , 
		} ) ;

		return  response . status ( 204 ) ; 
	} 
}

export  default  ForgotPasswordController
// ResetPasswordController.ts 
import  {  container  }  from  'tsyringe'

import  {  Request ,  Response  }  from  'express' ;

import  ResetPasswordService  from  '@ modules / users / services / ResetPasswordService' ;

class  ResetPasswordController  { 
	public  async  create ( request : Request ,  response : Response ) : Promise < Response > { 
		const  { token , password }  =  request . body ;

		const  resetPassword  =  container . resolve ( ResetPasswordService ) ;

		await  resetPassword . execute ( { 
			token , 
			password , 
		} ) ;

		return  response . status ( 400 ) . json ( ) ; 
	} 
}

export  default  ResetPasswordController ;
// password.routes.ts 
import  {  Router  }  from  'express' ;

import  ResetPasswordController  from  '../controllers/ResetPasswordController' ; 
import  ForgotPasswordController  from  '../controllers/ForgotPasswordController' ;

const  passwordRouter  =  Router ( ) ; 
const  forgotPasswordController  =  new  ForgotPasswordController ( ) ; 
const  resetPasswordController  =  new  ResetPasswordController ( ) ;

passwordRouter . post ( '/ reset' ,  resetPasswordController . create ) ; 
passwordRouter . post ( '/ forgot' , forgotPasswordController . create ) ;

export  default  passwordRouter ;

Update the route index.ts

// @shared /infra/http/routes/index.ts 
import  {  Router  }  from  'express' ; 
import  appointmentsRouter  from  '@ modules / appointments / infra / http / routes / appointments.routes' ; 
import  usersRouter  from  '@ modules / users / infra / http / routes / user.routes' ; 
import  sessionsRouter  from  '@ modules / users / infra / http / routes / sessions.routes' ; 
import  passwordRouter  from  '@ modules / users / infra / http / routes / password.routes' ;

const  routes  =  Router ( ) ;

routes . use ( '/ appointments' ,  appointmentsRouter ) ; 
routes . use ( '/ users' ,  usersRouter ) ; 
routes . use ( '/ sessions' ,  sessionsRouter ) ; 
routes . use ( '/ password' ,  passwordRouter ) ;

export  default  routes ;

Create the UserTokensRepository typeorm repository

// @modules /users/infra/typeorm/repositories/UserTokensRepository.ts 
import  {  Repository ,  getRepository  }  from  'typeorm' ;

import  UserToken  from  '../entities/UserToken' ; 
import  IUserTokensRepository  from  '@ modules / users / repositories / IUserTokensRepository' ;

class  UserTokensRepository  implements  IUserTokensRepository  { 
	private  ormRepository : Repository < UserToken > ;

	constructor ( ) { 
		this . ormRepository  =  getRepository ( UserToken ) ; 
	}

	public  async  findByToken ( token : string ) : Promise  < UserToken | undefined > { 
		const  userToken  =  await  this . ormRepository . findOne ( {  where : { token }  } ) ;

		return  userToken ; 
	}

	public  async  generate ( user_id : string ) : Promise < UserToken > { 
		const  userToken  =  this . ormRepository . create ( { 
			user_id , 
		} ) ;

		await  this . ormRepository . save ( userToken ) ;

		return  userToken ; 
	} 
}

export  default  UserTokensRepository ;

Create the migration

$ yarn typeorm migration: create -n CreateUserTokens

Go to @ shared / infra / typeorm / migrations / CreateUserTokens.ts

import  {  MigrationInterface ,  QueryRunner ,  Table  }  from  'typeorm' ;

export  default  class  CreateUserTokens1597089880963 
	implements  MigrationInterface  { 
	public  async  up ( queryRunner : QueryRunner ) : Promise < void > { 
		await  queryRunner . createTable ( new  Table ( { 
			name : 'user_tokens' , 
			columns : [ 
				{ 
					name : 'id' , 
					type : 'uuid' , 
					isPrimary :true , 
					generatedStrategy : 'uuid' , 
					default : 'uuid_generate_v4 ()' , 
				} , 
				{ 
					name : 'token' , 
					type : 'uuid' , 
					generatedStrategy : 'uuid' , 
					default : 'uuid_generate_v4 ()' , 
				} , 
				{ 
					name : 'user_id' , 
					type : 'uuid' , 
				} , 
				{ 
					name : 'created_at' , 
					type :'timestamp' , 
					default : 'now ()' , 
				} , 
				{ 
					name : 'updated_at' , 
					type : 'timestamp' , 
					default : 'now ()' , 
				} , 
			] , 
			foreignKeys : [ 
				{ 
					name : 'TokenUser' , 
					referencedTableName : 'users' , 
					referencedColumnNames : [ 'id' ] , 
					columnsName : [ 'user_id' ] ,
					onDelete : 'CASCADE' , 
					onUpdate : 'CASCADE' , 
				} 
			] , 
		} ) ) 
	}

	public  async  down ( queryRunner : QueryRunner ) : Promise < void > { 
		await  queryRunner . dropTable ( 'user_tokens' ) ; 
	}

}

Go to @ shared / container / index.ts and add

import  IUserTokensRepository  from  '@ modules / users / repositories / IUserTokensRepository' ; 
import  UserTokensRepository  from  '@ modules / users / infra / typeorm / repositories / UserTokensRepository' ;

container . registerSingleton < IUserTokensRepository > ( 
	'UserTokensRepository' , 
	UserTokensRepository , 
) ;

Developing emails

  • Using the EthrealMail lib
  1. Install nodemailer
$ yarn add nodemailer

$ yarn add @ types / nodemailer -D
  1. Create the nodemailer implementation in the MailProvider folder
import  nodemailer ,  {  Transporter  }  from  'nodemailer' ;

import  IMailProvider  from  '../models/IMailProvider' ;

class  EtherealMailProvider  implements  { 
	private  client : Transporter ;

	constructor ( ){ 
		nodemailer . createTestAccount ( ) . then ( account  =>  { 
			 const transporter =  nodemailer . createTransport ( { 
					host : account . smtp . host , 
					port : account . smtp . port , 
					secure : account . smtp . secure , 
					auth : { 
							user : account .user , 
							pass : account . pass , 
					} , 
				} ) ;

			this . client  =  transporter ;

		} ) ; 
	}

	public async  sendMail ( to : string ,  body : string ) : Promise < void > { 
		const  message  =  this . client . sendMail ( { 
        from : 'Team GoBarber <team@gobarber.com>' , 
        to , 
        subject : 'Recovery password' , 
        text : body , 
    } ) ;

		console . log ( 'Message sent:% s' ,  message . messageId ) ; 
		console . log ( 'Preview URL:% s' ,  nodemailer . getTestMessageUrl ( message ) ) ;

	}

}

export  default  EtherealMailProvider ;
  1. Create the dependency injection
import  {  container  }  from  'tsyringe' ;

import  IMailProvider  from  './MailProvider/models/IMailProvider' ; 
import  EtherealMailProvider  from  './MailProvider/implementations/EtherealMailProvider' ;

container . registerInstance < IMailProvider > ( 
	'MailProvider' , 
	new  EtherealMailProvider ( ) , 
)

I must use registerInstance , because with registerSingleton it was not working, as it is not instantiating EtherealMailProvider as it should happen.

  1. Get the token that is generated in SendForgotPasswordEmail
...
		 const  { token }  =  await  this . userTokensRepository . generate ( user . id ) ;

		await  this . mailProvider . sendMail ( 
			email , 
			`Password recovery request: $ { token } ` , 
		) ;
  • Test the route for insomnia
  • Receive console.log with the token
  • Click on the link, get the token sent in the body of the email
  • Trigger ResetPasswordService also for insomnia, passing the token and the new password.

Email Template

  • I must use a template engine for this, there are several: Nunjuncks, Handlerbars …
  • This Email Template will also be a provider.
  • Create the MailTemplateProvider folder , and within it models , implementations , fakes , dtos ;
// dtos 
interface  ITemplateVariables  { 
	[ key : string ] : string | number ; 
}

export  default  interface  IParseMailTemplateDTO  { 
	template : string; 
	variables: ITemplateVariables ; 
}
// models 
import  IParseMailTemplateDTO  from  '../dtos/IParseMailTemplateDTO' ;

export  default  interface  IMailTemplateProvider  { 
	parse ( data : IParseMailTemplateDTO ) : Promise < string > ; 
} 
// fakes 
import  IMailTemplateProvider  from  '../models/IMailTemplateProvider' ;

class  FakeMailTemplateProvider  implements  IMailTemplateProvider  { 
	public  async  parse ( { template } : IMailTemplateProvider ) : Promise < string >  { 
		return  template ; 
	} 
}

export  default  FakeMailTemplateProvider ;
  • Install the handlerbars;
// implementations 
import  handlebars  from  handlebars

import  IMailTemplateProvider  from  '../models/IMailTemplateProvider' ;

class  HandlebarsMailTemplateProvider  implements  IMailTemplateProvider  { 
	public  async  parse ( { template , variables } : IMailTemplateProvider ) : Promise < string >  { 
		const  parseTemplate  =  handlebars . compile ( template ) ;

		return  parseTemplate ( variables ) ; 
	} 
}

export  default  HandlebarsMailTemplateProvider ;
  • Create the dependency injection
import  {  container  }  from  'tsyringe' ; 
import  IMailTemplateProvider  from  './MailTemplateProvider/models/IMailTemplateProvider' ; 
import  HandlebarsMailTemplateProvider  from  './MailTemplateProvider/implementations/HandlebarsMailTemplateProvider' ;

container . registerSingleton < IMailTemplateProvider > ( 
	'MailTemplateProvider' , 
	HandlebarsMailTemplateProvider 
) ;

MailTemplateProvider is directly associated with MailProvider , as MailTemplateProvider would not even exist if there were no MailProvider, so it will not be passed as a dependency injection of the SendForgotPasswordEmail service .

  • I must create a DTO for MailProvider since now its interface will receive more than just to and body
// @shared / container / providers / MailProvider / dtos / ISendMailDTO 
import  IParseMailTemplateDTO  from  '@ shared / container / providers / MailTemplateProvider / dtos / IParseMailTemplateDTO' ;

interface  IMailContact  { 
	name : string ; 
	email : string ; 
}

export  default  interface  ISendMailDTO  { 
	to : IMailContact; 
	from ?: IMailContact ; 
	subject: string ; 
	templateData: IParseMailTemplateDTO ; 
}
  • Now I must change both the fake and the implementations of MailProvider ;
// fakes 
import  IMailProvider  from  '../models/IMailProvider' ; 
import  ISendMailDTO  from  '../dtos/ISendMailDTO' ;

class  FakeMailProvider  implements  IMailProvider  { 
	private  messages : ISendMailDTO [ ] ;

	public  async  sendMail ( message : ISendMailDTO ) : Promise < void > { 
		this . messages . push ( message ) ; 
	} 
}

export  default  FakeMailProvider ;

Do the MailTemplateProvider dependency injection in MailProvider . One provider can depend on others, as MailTemplateProvider only exists if MailProvider also exists.

import  {  container  }  from  'tsyringe' ;

import  IStorageProvider  from  './StorageProvider/models/IStorageProvider' ; 
import  DiskStorageProvider  from  './StorageProvider/implementations/DiskStorageProvider' ;

import  IMailProvider  from  './MailProvider/models/IMailProvider' ; 
import  EtherealMailProvider  from  './MailProvider/implementations/EtherealMailProvider' ;

import  IMailTemplateProvider  from  './MailTemplateProvider/models/IMailTemplateProvider' ; 
import  HandlebarsMailTemplateProvider  from  './MailTemplateProvider/implementations/HandlebarsMailTemplateProvider' ;

container . registerSingleton < IStorageProvider > ( 
	'StorageProvider' , 
	DiskStorageProvider , 
) ;

container . registerSingleton < IMailTemplateProvider > ( 
	'MailTemplateProvider' , 
	HandlebarsMailTemplateProvider , 
) ;

container . registerInstance < IMailProvider > ( 
	'MailProvider' , 
	container . resolve ( EtherealMailProvider ) , 
) ;
  • Go to EtherealMailProvider and change the sendMail function, as was done in fake.
import  nodemailer ,  {  Transporter  }  from  'nodemailer' ; 
import  {  injectable ,  inject  }  from  'tsyringe' ;

import  IMailProvider  from  '../models/IMailProvider' ; 
import  ISendMailDTO  from  '../dtos/ISendMailDTO' ; 
import  IMailTemplateProvider  from  '@ shared / container / providers / MailTemplateProvider / models / IMailTemplateProvider' ;

@ injectable ( ) 
class  EtherealMailProvider  implements  IMailProvider  { 
	private  client : Transporter ;

	constructor ( 
		@ inject ( 'MailTemplateProvider' ) 
		private  mailTemplateProvider : IMailTemplateProvider , 
	)  { 
		nodemailer . createTestAccount ( ) . then ( account  =>  { 
			const  transporter  =  nodemailer . createTransport ( { 
				host : account . smtp . host , 
				port : account . smtp .port , 
				secure : account . smtp . secure , 
				auth : { 
					user : account . user , 
					pass : account . pass , 
				} , 
			} ) ;

			this . client  =  transporter ; 
		} ) ; 
	}

	public  async  sendMail ( { to , from , subject , templateData } : ISendMailDTO ) : Promise < void >  { 
		const  message  =  await  this . client . sendMail ( { 
			from : { 
				name : from ? . name  ||  'Team GoBarber' , 
				address : from ? . email ||  'team@gobarber.com.br' , 
			} , 
			to : { 
				name : to . name , 
				address : to . email , 
			} , 
			subject , 
			html : await  this . mailTemplateProvider . parse ( templateData ) , 
		} ) ;

		console . log ( 'Message sent:% s' ,  message . messageId ) ; 
		console . log ( 'Preview URL:% s' ,  nodemailer . getTestMessageUrl ( message ) ) ; 
	} 
}

export  default  EtherealMailProvider ;

Execute forgot password email and reset password on insomnia routes

Template Engine

  1. Go to users, create the views folder and a file that will serve as a template for user emails.
<! - @ modules / users / views / forgot_password.hbs ->
 < style > 
.message-content { font-family : Arial , Helvetica , sans-serif ; max-width : 600 px ; font-size : 18 px ; line-height : 21 px ; 	}	

</ style >

< div  class = " message-content " >
	< p > Hello, {{ name }} </ p >
	< p > It looks like a password change for your account has been requested. </ p >
	< p > If it was you, then click the link below to choose a new password </ p >
	< p >
		< A  href = " {{ link }} " > Password Recovery </ a >
	</ p >
	< p > If it wasn't you, then discard that email </ p >
	< p >
		Thank you </ br >
		< strong  style = " color: ## FF9000 " > Team GoBarber </ strong >
	</ p >
</ div >
  1. Go to the IParseMailTemplate interface and edit the template attribute for file.
  2. Go to FakeMailTemplateProvider .
import  IMailTemplateProvider  from  '../models/IMailTemplateProvider' ;

class  FakeTemplateMailProvider  implements  IMailTemplateProvider  { 
	public  async  parse ( ) : Promise < string >  { 
		return  'Mail content' ; 
	} 
}

export  default  FakeTemplateMailProvider ;
  1. Go to HandlebarsMailTemplateProvider.ts
import  handlebars  from  'handlebars' ; 
import  fs  from  'fs' ;

import  IParseMailTemplateDTO  from  '../dtos/IParseMailTemplateDTO' ; 
import  IMailTemplateProvider  from  '../models/IMailTemplateProvider' ;

class  HandlebarsMailTemplateProvider  implements  IMailTemplateProvider  { 
	public  async  parse ( { 
		file , 
		variables , 
	} : IParseMailTemplateDTO ) : Promise < string >  { 
		const  templateFileContent  =  await  fs . promises . readFile ( file ,  { 
			encoding : 'utf-8' , 
		} ) ;

		const  parseTemplate  =  handlebars . compile ( templateFileContent ) ;

		return  parseTemplate ( variables ) ; 
	} 
}
  1. Go to SendForgotPasswordMailService.ts
import  path  from  'path' ;

const  forgotPasswordTemplate  =  path . resolve ( 
	__dirname , 
	'..' , 
	'views' , 
	'forgot_password.hbs' , 
) ;

await  this . mailProvider . sendMail ( { 
	to : { 
		name : user . name , 
		email : user . email , 
	} , 
	subject : '[GoBarber] Password Recovery' , 
	templateData : { 
		file : forgotPasswordTemplate , 
		variables : { 
			name : user . name , 
			link :`http: // localhost: 3000 / reset_password? token = $ { token } ` , 
		} , 
	} ,

Tests refactoring

  • Make all tests use the beforeEach (() => {}) syntax to make them less verbose.

Profile Update

Create the unit test very simple, as well as the service and gradually add the business rules.

Profile Update

Functional Requirements

  • The user must be able to update his name, email and password.

Non-Functional Requirements

Business rules

  • The user cannot change the email to another email already used by another user.
  • When changing the password, the user must inform the old one.
  • When recovering the password, the user must confirm the new password. (will be done later)
  1. Create UpdateProfileService.ts and UpdateProfileService.spec.ts .

  2. Increase both the test and the Service so that the business rules are adequate.

📝 On

Notes I make throughout my studies on:

  • Backend architecture
  • DDD (Domain-Driven Design)
  • TDD (Test-Driven Development)
  • SOLID
  • Jest
  • MongoDB
  • Cache
  • Amazon SES

🏆 Challenge

  • Write down the way I solve problems, tracing paths and the like.
  • Put into practice the knowledge that is acquired daily in my studies.

👀 Projects in which I am applying these concepts

Available at: Backend GoBarber


NodeJS Architecture and Testing

  • There is NO perfect architecture / structure for all projects.
  • It is up to me, as a developer, to understand what makes sense to use in my project.
  • BIG part of scalability concepts will learn here.
  • Architecture will not matter if what I am going to build is just an MVC, where it will die shortly after construction.

Developing big applications IS NOT JUST CODAR, it’s thinking about:

  • Architecture
  • Tests
  • Documentation
  • Organization
  • Programming principles

So far, the backend is separating files by type, for example: services, which is handling all business rules. Thus, the structure is disorganized, in case the structure grows.

Backend folder structure

Backend folder structure

From now on, they will be separated by Domain .

What is Domain ?

It is the area of ​​knowledge of that module / file.

Domain Driven Design (DDD) based architecture

  • It is a methodology, just like SCRUM .
  • And like SCRUM , it has many techniques, but not all of them fit into any type of project.
  • In summary, DDD are concepts , principles and good practices that I must use in good parts of Backend projects .
  • Applies to Backend only .

Test Driven Development (TDD)

  • It is also a methodology, just like DDD and can be used in conjunction with it.
  • Applies to Backend, Frontent and Mobile .
  • Create tests, before creating the features themselves.

Modules / Domain Layer

  • Improve the application’s folder structure.
  • Further isolate responsibilities based on the domain , which area of ​​knowledge a file is part of.
  • Begin to divide the application into modules .
  • Modules are basically sectors that one or more files are part of. Responsible for the business rule, it is basically the heart of my application.
  • Has no knowledge

Example: User, all files related to this domain will be part of the same module.

Modules / Domain Layer

Domain / Module Layer folder structure

Note: Previously, the User entity was part of the models folder , which basically has the same concept of entities : How we managed to represent information in the application.

Shared

  • If more than one module makes use of a file, this file must be part of the Shared folder .

Example: Database, errors, middlewares, routes and etc.

Shared folder structure

Shared folder structure

Infra Layer

  • Responsible for communicating my application with external services.
  • Responsible for the technical decisions of the application.
  • In other words, they are the tools that will be chosen to relate to the module layer .

Example: Database, Automatic Email Service and so on …

  • In the Shared folder , I will add a folder below that will contain all the information of a specific package / lib.
  • Still inside the folder below , I will create a folder with files responsible for communicating with some protocol, in this case http . That is, inside the http they will have the express files or any lib that makes use of http protocols .

Folder structure of the Infra Layer

Folder structure of the Infra Layer

  • In the module layer, there is communication between an entity and the database, which in this case is TypeOrm (PostgreSQL). So, I must create the infra / typeorm folder that will contain the entity folder inside it.

Domain Layer folder structure with infra

Folder structure of the Domain Layer with the Infrastructure Layer

Configuring Imports

  • Go to tsconfig, in the baseUrl attribute put:
"./src"
  • And in the paths attribute and put:
{ 
  "@modules/*": ["modules/*"],
  "@config/*": ["config/*"],
  "@shared/*": ["shared/*]
}
  • Right after that, arrange all the necessary imports using @modules or @config or @shared
  • Also fix imports in the ormconfig.json file .
  • Put in package.json in scripts :
{
    "build": "tsc",
    "dev:server": "ts-node-dev -r tsconfig-paths/register --inspect --transpileOnly --ignore-watch node_modules src/shared/infra/http/server.ts",
    "typeorm": "ts-node-dev -r tsconfig-paths/register ./node_modules/typeorm/cli.js"
  },
  • Install to handle imports created in the TypeScript configuration file:
$ yarn add tsconfig-paths -D
  • Run the application to verify that it is working.

SOLID

Liskov Substitution Principle

  • This principle defines that an object inherited from superclass A , it must be possible to change it in such a way that it accepts a subclass that also inherits A without the application stop working.
  • In the layers (repositories) that we have that depend on other libraries (typeorm), it should be possible to change them as necessary, following a set of rules. And in the end, we will depend only on a set of rules (interface), but not necessarily on a specific library (typeorm) .
// AppointmentsRepository that inherits methods from a standard TypeORM repository. 
import  {  EntityRepository ,  Repository  }  from  'typeorm' ; 
import  Appointment  from  '@ modules / appointments / infra / typeorm / entities / Appointment' ;

@ EntityRepository ( Appointment ) 
class  AppointmentsRepository  extends  Repository < Appointment >  { 
	public  async  findByDate ( date : Date ) : Promise < Appointment | undefined >  { 
		const  findAppointment  =  await  this . findOne ( { 
			where : { date } , 
		} ) ; 
		return findAppointment ; 
	} 
}

export  default  AppointmentsRepository ;

Applying Liskov Substitution Principle

  • Create an interface to handle the appointments repository. That is, I am creating my rules.
import  Appointment  from  '../infra/typeorm/entities/Appointment' ;

export  default  interface  IAppointmentsRepository  { 
  findByDate ( date : Date ) : Promise < Appointment | undefined > ; 
}
  • Go to the Typeorm repository and add my new rules to the AppointmensRepository class:
import  {  EntityRepository ,  Repository  }  from  'typeorm' ;

import  IAppointmentsRepository  from  '@ modules / appointments / repositories / IAppointmentsRepository' ;

import  Appointment  from  '@ modules / appointments / infra / typeorm / entities / Appointment' ;

@ EntityRepository ( Appointment ) 
class  AppointmentsRepository  extends  Repository < Appointment >  implements  IAppointmentsRepository  { 
	public  async  findByDate ( date : Date ) : Promise < Appointment | undefined >  { 
		const  findAppointment  =  await  this . findOne ( { 
			where : { date } , 
		}) ; 
		return  findAppointment ; 
	} 
}

export  default  AppointmentsRepository ;

When one class extends another, it means that it will inherit the methods. The implements, on the other hand, serve as a shape (format or rule) to be followed by the class, but not that it inherits its methods.

That way, our services will depend only on repository rules, and not necessarily a TypeORM repository or whatever the other library is. In fact, the service should not be aware of the final format of the structure that persists our data.

Rewriting Repositories

  • I must have more control over the methods of the repository, since they are inherited from TypeORM, such as: create, findOne and etc.
  • Go to the TypeORM repository and add my own create method, which will use two TypeORM methods. And I must also create my ICreateAppointmentDTO.ts interface:
export  default  interface  ICreateAppointmentDTO  { 
  provider_id : string; 
  date: Date ; 
}
import  {  getRepository ,  Repository  }  from  'typeorm' ;

import  IAppointmentsRepository  from  '@ modules / appointments / repositories / IAppointmentsRepository' ;

import  ICreateAppointmentDTO  from  '@ modules / appointments / dtos / ICreateAppointmentDTO' ;

import  Appointment  from  '@ modules / appointments / infra / typeorm / entities / Appointment' ;

class  AppointmentsRepository  implements  IAppointmentsRepository  {

  private  ormRepository : Repository < Appointment > ;

  constructor ( ) { 
    this . ormRepository  =  getRepository ( Appointment ) ; 
  }

	public  async  findByDate ( date : Date ) : Promise < Appointment | undefined >  { 
		const  findAppointment  =  await  this . ormRepository . findOne ( { 
			where : { date } , 
		} ) ; 
		return  findAppointment ; 
	}

  public  async  create ( { provider_id , date } : ICraeteAppointmentDTO ) : Promise < Appointment > { 
    const  appointment  =  this . ormRepository . create ( { 
      provider_id ,
      gives you
    } ) ;

    await  this . ormRepository . save ( appointment ) ;

    return  appointment ; 
  } 
}

export  default  AppointmentsRepository ;

Dependency Inversion Principle

This principle defines that:

  • High-level modules should not depend on low-level modules. Both should depend only on abstractions (for example, interfaces).
  • Abstractions should not depend on details. Details (implementations) must depend on abstractions.

That is, instead of my service depending directly on the typeorm repository , it now depends only on the interface of the repository, which will be passed through the route .

The route is part of the infra layer, which is the same as the TypeORM, so it is not necessary to apply the same principle to it. The service is part of the domain layer, so it must be decoupled from the infrastructure layer in such a way that it is not aware of its operation.

import  {  startOfHour  }  from  'date-fns' ; 
import  {  getCustomRepository  }  from  'typeorm' ; 
import  Appointment  from  '@ modules / appointments / infra / typeorm / entities / Appointment' ; 
import  AppError  from  '@ shared / errors / AppError' ; 
import  AppointmentsRepository  from  '../repositories/AppointmentsRepository' ;

 Request  interface { 
	provider_id : string ; 
	date : Date ; 
}

class  CreateAppointmentService  { 
	public  async  execute ( { provider_id , date } : Request ) : Promise < Appointment >  { 
    // depending on the repository for the typeorm 
		const  appointmentsRepository  =  getCustomRepository ( AppointmentsRepository ) ;

		const  appointmentDate  =  startOfHour ( date ) ;

		const  findAppointmentInSameDate  =  await  appointmentsRepository . findByDate ( 
			appointmentDate , 
		) ;

		if  ( findAppointmentInSameDate )  { 
			throw  new  AppError ( 'This Appointment is already booked!' ) ; 
		}

		const  appointment  =  await  appointmentsRepository . create ( { 
			provider_id , 
			date : appointmentDate , 
		} ) ;

		return  appointment ; 
	} 
}

export  default  CreateAppointmentService ;

Applying the Dependency Inversion Principle

import  {  startOfHour  }  from  'date-fns' ; 
import  Appointment  from  '@ modules / appointments / infra / typeorm / entities / Appointment' ; 
import  AppError  from  '@ shared / errors / AppError' ; 
import  IAppointmentsRepository  from  '../repositories/IAppointmentsRepository' ;

interface  IRequest  { 
	provider_id : string ; 
	date : Date ; 
}

class  CreateAppointmentService  {

  // depending only on the interface (abstraction) of the repository, which will be passed to the constructor when a new repository is instantiated on the route. 
  constructor ( private  appointmentsRepository : IAppointmensRepository )  { }

	public  async  execute ( { provider_id , date } : Request ) : Promise < Appointment >  { 
		const  appointmentDate  =  startOfHour ( date ) ;

		const  findAppointmentInSameDate  =  await  this . appointmentsRepository . findByDate ( 
			appointmentDate , 
		) ;

		if  ( findAppointmentInSameDate )  { 
			throw  new  AppError ( 'This Appointment is already booked!' ) ; 
		}

		const  appointment  =  await  this . appointmentsRepository . create ( { 
			provider_id , 
			date : appointmentDate , 
		} ) ;

		return  appointment ; 
	} 
}

export  default  CreateAppointmentService ;

Note: The Appointment entity is still provided by TypeORM, but in this case the Dependecy Inversion Principle will not be applied in order not to ‘hinder’ the understanding of this principle.

Both Liskov Substitution Principle and Dependency Inversion Principle have similar concepts. In short, the Liskov Substitution Principle says that my domain layer must depend on abstractions (interfaces), and not directly from the infra layer . The Dependency Inversion Principle says that modules must depend on abstractions (interfaces) and not on other modules .

User Module Refactorings

  • Create the user repository at the domain layer, which will be my interface that will dictate the rules for the typeorm repository to follow.
// modules / users / repositories / IUsersRepository 
import  User  from  '../infra/typeorm/entities/User' ; 
import  ICreateUserDTO  from  '../dtos/ICreateUserDTO' ;

export  default  interface  IUsersRepository  {

	findByEmail ( email : string ) : Promise < User | undefined > ; 
	findById ( id : string ) : Promise < User | undefined > ; 
	create ( data : ICreateUserDTO ) : Promise < User > ; 
	save ( user : User ) : Promise < User > ;
}
// modules / users / dtos / ICreateUserDTO 
export  default  interface  ICreateUserDTO  { 
	name : string; 
	email: string ; 
	password: string ; 
}
  • Now create the typeorm repository.
// modules / users / infra / typeorm / repositories / UsersRepository 
import  {  getRepository ,  Repository  }  from  'typeorm' ;

import  User  from  '../entities/User' ; 
import  IUsersRepository  from  '@ modules / users / repositories / IUsersRepository' ;

class  UsersRepository  implements  IUsersRepository { 
	private  ormRepository : Repository < User > ;

	constructor ( ) { 
		this . ormRepository  =  getRepository ( User ) ; 
	}

	public  async  findByEmail ( email : string ) : Promise < User > { 
		const  user  =  this . ormRepository . findOne ( {  where : { email } } ) ;

		return  user ; 
	} 
	public  async  findById ( id : string ) : Promise < User > { 
		const  user  =  this . ormRepository . findOne ( id ) ;

		return  user ; 
	}

	public  async  create ( user : User ) : Promise < User > { 
		const  user  =  this . ormRepository . create ( user ) ;

		return  this . ormRepository . save ( user ) ; 
	}

	public  async  save ( user : User ) : Promise < User > { 
		return  this . ormRepository . save ( user ) ; 
	} 
}
  • Now I must use the interface of this repository in the service, because in each service a repository will be sent, and I must inform its interface.
import  {  hash  }  from  'bcryptjs' ; 
import  AppError  from  '@ shared / errors / AppError' ; 
import  User  from  '../infra/typeorm/entities/User' ;

import  IUsersRepository  from  '../repositories/IUsersRepository' ;

interface  IRequest  { 
	name : string ; 
	email : string ; 
	password : string ; 
}

class  CreateUserService  { 
	constructor ( private  usersRepository : IUsersRepository )  { }

	public  async  execute ( { name , email , password } : IRequest ) : Promise < User >  { 
		const  checkUserExist  =  await  this . usersRepository . findByEmail ( email ) ;

		if  ( checkUserExist )  { 
			throw  new  AppError ( 'Email already used!' ) ; 
		}

		const  hashedPassword  =  await  hash ( password ,  8 ) ;

		const  user  =  await  this . usersRepository . create ( { 
			name , 
			email , 
			password : hashedPassword , 
		} ) ;

		return  user ; 
	} 
}

export  default  CreateUserService ;

Make these changes to the other services.

  • Go to user domain routes and make the instance of the typeorm repository to be sent by the parameter of each route.

Dependency Injection

  • Reason for using:
  • Do not need to go through the repositories whenever a service is instantiated.
  • Go to the shared folder, create a container folder and an index.ts
import  {  container  }  from  'tsyringe' ;

import  IAppointmentsRepository  from  '@ modules / appointsments / repositories / IAppointmentsRepository' ; 
import  IUsersRepository  from  '@ modules / users / repositories / IUsersRepository' ;

import  AppointmentsRepository  from  '@ modules / appointments / infra / typeorm / repositories / AppointmentsRepository' ;

import  UsersRepository  from  '@ modules / users / infra / typeorm / repositories / UsersRepository' ;

container . registerSingleton < IAppointmentsRepository > ( 'AppointmentsRepository' ,  AppointmentsRepository ) ;

container . registerSingleton < IUsersRepository > ( 'UsersRepository' ,  UsersRepository ) ;
  • Go to the appointment services and make changes to the constructors of all appointments
import  {  startOfHour  }  from  'date-fns' ; 
import  {  injectable ,  inject  }  from  'tsyringe' ;

import  Appointment  from  '@ modules / appointments / infra / typeorm / entities / Appointment' ; 
import  AppError  from  '@ shared / errors / AppError' ; 
import  IAppointmentsRepository  from  '../repositories/IAppointmentsRepository' ;

interface  IRequest  { 
	provider_id : string ; 
	date : Date ; 
}

// add the tsyringe decorator 
@ injectable ( ) 
class  CreateAppointmentService  { 
	constructor ( 
		// and will inject this dependency whenever the service is instantiated. 
		@ inject ( 'AppointmentsRepository' ) 
		private  appointmentsRepository : IAppointmentsRepository , 
	)  { }

	public  async  execute ( { provider_id , date } : IRequest ) : Promise < Appointment >  { 
		const  appointmentDate  =  startOfHour ( date ) ;

		const  findAppointmentInSameDate  =  await  this . appointmentsRepository . findByDate ( 
			appointmentDate , 
		) ;

		if  ( findAppointmentInSameDate )  { 
			throw  new  AppError ( 'This Appointment is already booked!' ) ; 
		}

		const  appointment  =  await  this . appointmentsRepository . create ( { 
			provider_id , 
			date : appointmentDate , 
		} ) ;

		return  appointment ; 
	} 
}

export  default  CreateAppointmentService ;
  • Go on the appointments routes, remove the typeorm repository, import the tsyringe container and use the resolve method ('insert_o_service_aqui_onde_ele_seria_instanciado).
import  {  Router  }  from  'express' ; 
import  {  container  }  from  'tsyringe' ;

import  CreateAppointmentService  from  '@ modules / appointments / services / CreateAppointmentService' ;

const  appointmentsRouter  =  Router ( ) ;

appointmentsRouter . post ( '/' ,  async  ( request ,  response )  =>  { 
	const  { provider_id , date }  =  request . body ;

	// const appointmentsRepository = new AppointmentsRepository ();

	// const createAppointments = new CreateAppointmentService ();

	// put it this way 
	const  createAppointments  =  container . resolve ( CreateAppointmentService ) 
	. . .

} ) ;
  • Go to server.ts (main file) and import the dependency injection container.
...
 import  '@ shared / container' ; 
...

Using Controllers

  • Controllers in a large architecture have little responsibility, where they will take care to abstract the logic that is in the routes . There are already Services that deal with business rules and the Repository that deals with data manipulation / persistence .
  • The routes were only responsible for (now it will be the responsibility of the Controllers):
  • Receive the requisition data.
  • Call other files to handle the data.
  • Return the Answer.
  • Following Restful API standards, Controllers must have only 5 methods:
  • Index (list all)
  • Show (list only one)
  • Create
  • Update
  • Delete

If more methods are needed, I must create a new controller.

// users / infra / http / controllers / SessionsController.ts 
import  {  Request ,  Response  }  from  'express' ;

import  {  container  }  from  'tsyringe' ; 
import  AuthenticateUserService  from  '@ modules / users / services / AuthenticateUserService' ;

class  SessionsController  { 
	public  async  create ( request : Request ,  response : Response ) : Promise < Response >  { 
		const  { password , email }  =  request . body ;

		const  authenticateUser  =  container . resolve ( AuthenticateUserService ) ;

		const  { user , token }  =  await  authenticateUser . execute ( { password , email } ) ;

		delete  user . password ;

		return  response . json ( { user , token } ) ; 
	} 
}

export  default  SessionsController ;
// sessions.routes.ts 
import  {  Router  }  from  'express' ;

import  SessionsController  from  '../controllers/SessionsController' ;

const  sessionsRouter  =  Router ( ) ; 
const  sessionsController  =  new  SessionsController ( ) ;

sessionsRouter . post ( '/' ,  sessionsController . create ) ;

export  default  sessionsRouter ;

Tests and TDD

We have created tests to ensure that our application continues to function properly regardless of the number of functionality and the number of developers.

3 Main types of tests

  1. Unit Testing
  2. Integration Test
  3. E2E test (end-to-end)

Unit Testing

  • Tests application-specific features.
  • You need these features to be pure functions .
Pure Functions

It does not depend on another part of the application or external service.

You will never own:

  • API calls
  • Side effects

Side effect example: Trigger an email whenever a new user is created

Integration Test

It tests a complete functionality, passing through several layers of the application.

Rota -> Controller -> Service -> Repository -> Service -> …

Example: Creating a new user with sending email.

E2E test (end-to-end)

  • Simulates user action within the application.
  • Used in interfaces (React, React Native).

Example:

  1. Click on the email input
  2. Type danilobandeira29@gmail.com
  3. Click on the password input
  4. Type 123456
  5. Click the “sign in” button
  6. Expects the user to be redirected to the Dashboard

Test Driven Development (TDD)

  • “Test Driven Development”.
  • We created some tests even before the functionality itself (or some of them) was created.

Example: When the user signs up for the application, they should receive a welcome email.

Configuring Jest

$ yarn add jest -D
$ yarn jest --init
$ yarn add @ types / jest ts-jest -D
  • Configure the jest.config.js file: preset: ‘ts-jest’, …, testMatch: [‘** / *. Spec.ts’]
  • Create a test just to test the jest settings.
test ( 'sum two numbers' ,  ( )  =>  { 
	expect ( 1  +  2 ) . toBe ( 3 ) ; 
} ) ;

Thinking about Tests

  • I must create tests for new features and for those that already exist in the application.
  • I will create unit tests initially. These are minor tests and should not depend on external services .
  • Creating a new appointment:
  • I need to get provider_id and a date.
  • This provider_id must be one that already exists in the bank?
  • What if this provider_id is deleted?
  • Should I create a new user each time I run the tests?
  • Should I create a database for testing only?
  • It is difficult to create tests that depend on external things, such as databases, sending e-mail … etc.
  • Unit testing should not depend on anything other than itself. But in the case of services, they depend on a repository that connects with the bank. Therefore, I will create a fake repository , where it will not connect with the database. Database is passive to errors, so avoid connecting to it in that case.
  • Each service will generate at least one unit test.

Creating First Unit Test

  • Go to the appointment service folder and create CreateAppointmentService.spec.ts
describe ( 'Create Appointment' ,  ( )  =>  { 
	it ( 'should be able to create a new appointment' ,  ( )  =>  { 
	} ) ; 
} ) ;
  • I will create a unit test because it is simpler and should not depend on external libraries , such as typeorm, some database and the like.
  • But for that, I must create my fake repository , which will have my repository interface and so it will not depend on the typeorm , and will save the data in memory.

You can see the Liskov Substitution and Dependency Inversion Principle here. Where my service is depending only on an appointmentsRepository that has IAppointmentsRepository typing, regardless of whether it is a typeorm repository (which saves in the database) or a repository that has pure JavaScript methods (which saves locally).

// @modules / appointments / repositories / fakes / FakeAppointmentsRepository

import  {  uuid  }  from  'uuidv4' ; 
import  Appointment  from  '@ modules / appointments / infra / typeorm / entities / Appointments' ; 
import  ICreateAppointmentDTO  from  '../dtos/ICreateAppointmentsDTO' ; 
import  IAppointmentsRepository  from  '../IAppointmentsRepository' ;

class  FakeAppointmentsRepository  implements  IAppointmentsRepository  { 
	private  appointments : Appointments [ ]  =  [ ] ;

	public  async  findByDate ( date : Date ) : Promise < Appointment | undefined >  { 
		const  findAppointment  =  this . appointments . find ( 
			appointment  =>  appointment . date  ===  date , 
		) ;

		return  findAppointment ; 
	}

	public  async  create ( { provider_id , date } : ICreateAppointmentDTO ) : Promise < Appointment > { 
		const  appointment  =  new  Appointment ( ) ;

		Object . assign ( appointment ,  {  id : uuid ( ) , date , provider_id } ) ;

		this . appointments . push ( appointment ) ;

		return  appointment ; 
	} 
}

export  default  FakeAppointmentsRepository ;
import  FakeAppointmentsRepository  from  '@ modules / appoinetments / repositories / fakes / FakeAppointmentsRepository' ; 
import  CreateAppointmentService  from  './CreateAppointmentService' ;

describe ( 'Create Appointment' ,  ( )  =>  { 
	it ( 'should be able to create a new appointment' ,  ( )  =>  { 
		const  fakeAppointmentsRepository  =  new  FakeAppointmentsRepository ( ) ; 
		const  createAppointment  =  new  CreateAppointmentService ( fakeAppointmentsRepository ) ;

		const  appointment  =  await  createAppointment . execute ( { 
			date : new  Date ( ) , 
			provider_id : '4444' 
		} )

		expect ( appointment ) . toHaveProperty ( 'id' ) ; 
		expect ( appointment . provider_id ) . toBe ( '4444' ) ; 
	} ) ; 
} ) ;

When trying to execute, it will give error, because the test file does not understand the import @modules

  • Go to jest.config.js and import:
const  { pathsToModuleNameMapper }  =  require ( 'ts-jest / utils' ) ; 
const  { compilerOptions }  =  require ( './tsconfig.json' ) ;

module . exports  =  {  
	moduleNameMapper : pathsToModuleNameMapper ( compilerOptions . paths ,  {  prefix : '<rootDir> / src /'  } ) 
}
  • Go to tsconfig.json and remove all unnecessary comments and commas.
$ yarn test

Coverage Report

  • Checks files to see which files are already tested , which are not being tested , among other information .
  • For that, I must go to jest.config.js and enable:
collectCoverage : true , 
collectCoverageFrom : [ 
	'<rootDir> /src/modules/**/*.ts' 
] , 
coverageDir : ' coverage ' ,  // this folder will be created at the root that will hold the 
coverageReporters reports : [ 
	' text -summary ' , 
	' lcov ' , 
] ,
$ yarn test
  • Access the html ./coverage/lcov-report/index.html and view the report of files that were covered by the tests.

Scheduling Tests

import  AppError  from  '@ shared / errors / AppError' ; 
import  FakeAppointmentsRepository  from  '../repositories/fakes/FakeAppointmentsRepository' ; 
import  CreateAppointmentService  from  './CreateAppointmentService' ;

describe ( 'CreateAppointment' ,  ( )  =>  { 
	it ( 'should be able to create a new appointment' ,  async  ( )  =>  { 
		const  fakeAppointmentsRepository  =  new  FakeAppointmentsRepository ( ) ; 
		const  createAppointment  =  new  CreateAppointmentService ( 
			fakeAppointmentsRepository , 
		) ;

		const  appointment  =  await  createAppointment . execute ( { 
			date : new  Date ( ) , 
			provider_id : '4444' , 
		} ) ;

		expect ( appointment ) . toHaveProperty ( 'id' ) ; 
		expect ( appointment . provider_id ) . toBe ( '4444' ) ; 
	} ) ;

	it ( 'should not be able to create two appointments at the same time' ,  async  ( )  =>  { 
		const  fakeAppointmentsRepository  =  new  FakeAppointmentsRepository ( ) ; 
		const  createAppointment  =  new  CreateAppointmentService ( 
			fakeAppointmentsRepository , 
		) ; 
		const  date  =  new  Date ( 2020 ,  5 ,  10 ,  14 ) ;

		await  createAppointment . execute ( { 
			date , 
			provider_id : '4444' , 
		} ) ;

		expect ( 
			createAppointment . execute ( { 
				date , 
				provider_id : '4444' , 
			} ) , 
		) . rejects . toBeInstanceOf ( AppError ) ; 
	} ) ; 
} ) ;

User Creation Tests

  • Unit tests, as stated earlier, should not depend on apis or database, so I am going to create my repository with pure Javascript so as not to depend on the typeorm repository that communicates with the database.
  • Create the fake users repository.
// @modules /users/repositories/fakes/FakeUsersRepository.ts 
import  {  uuid  }  from  'uuidv4' ;

import  IUsersRepository  from  '../IUsersRepository' ; 
import  User  from  '../../infra/typeorm/entities/User' ; 
import  ICreateUserDTO  from  '../../dtos/ICreateUserDTO' ;

class  FakeUsersRepository  implements  IUsersRepository  {  
	private  users : User [ ]  =  [ ] ;

	public  async  findByEmail ( email : string ) : Promise < User | undefined >  { 
		const  findUser  =  this . users . find ( user  =>  user . email  ===  email ) ;

		return  findUser ; 
	}

	public  async  findById ( id : string ) : Promise < User | undefined >  { 
		const  findUser  =  this . users . find ( user  =>  user . id  ===  id ) ;

		return  findUser ; 
	}

	public  async  create ( userData : ICreateUserDTO ) : Promise < User > { 
		const  user  =  new  User ( ) ;

		Object . assign ( user ,  {  id : uuid ( )  } ,  userData ) ;

		this . users . push ( user ) ;

		return  user ; 
	}

	public  async  save ( user : User ) : Promise < User > { 
		const  findIndex  =  this . users . findIndex ( findUser  =>  findUser . id  ===  user . id ) ;

		this . users [ findIndex ]  =  user ;

		return  user ; 
	}

}
  • Now, create the tests
import  AppError  from  '@ shared / errors / AppError' ; 
import  CreateUserService  from  './CreateUserService' ; 
import  FakeUsersRepository  from  '../repositories/fakes/FakeUsersRepository' ;

describe ( 'CreateUser' ,  ( )  =>  { 
	it ( 'should be able to create a new user' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  createUser  =  new  CreateUserService ( fakeUserRepository ) ;

		const  user  =  await  createUser . execute ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' 
		} ) ;

		expect ( user ) . toHaveProperty ( 'id' ) ; 
	} ) ;

	it ( 'should not be able to create two user with the same email' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  createUser  =  new  CreateUserService ( fakeUserRepository ) ;

		await  createUser . execute ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' 
		} ) ;

		expect ( createUser . execute ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' 
		} ) ) . rejects . toBeInstanceOf ( AppError ) ; 
	} ) ; 
} ) ;
$ yarn test

If there is an error related to reflect-metadata, go to jest.config.js and put

setupFiles : [ 
		'reflect-metadata' 
	] ,

Authentication Tests

  • Create the AuthenticateUserService.spec.ts
  • For that, I must import another service besides the AuthenticateUserService, since to authenticate a user, I need to create a user.
  • One test should NEVER depend on another.
  • I can approach it in two ways:
  • Use the repository’s own create method
  • Using the CreateUserService service
import  CreateUserService  from  './CreateUserService' ; 
import  AuthenticateUserService  from  './AuthenticateUserService' ;

describe ( 'AuthenticateUser' ,  ( )  =>  { 
	it ( 'should be able to authenticate' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  createUser  =  new  CreateUserService ( fakeUsersRepository ) ; 
		const  authenticateUser  =  new  AuthenticateUserService ( fakeUsersRepository) ) ;

		const  user  =  await  createUser . execute ( { 
			name : 'Test unit' , 
			email : 'testunit@example.com' , 
			password : '123456' , 
		} )

		const  authUser  =  await  authenticateUser . execute ( { 
			email : 'testunit@example.com' , 
			password : '123456' , 
		} )

		expect ( authUser ) . toHaveProperty ( 'token' ) ; 
		expect ( authUser . user ) . toEqual ( user ) ;

	} ) ; 
} ) ;
  • Now, it is possible to notice that CreateUserService has more than one responsibility:
  • User creation
  • Hash the user’s password
  • In addition to hurting the Single Responsability Principle, it is also hurting the Dependency Inversion Principle , as it depends directly on bcryptjs (it should only depend on an interface) for the logic of hashing the user creation and user authentication service itself .
  • I will take advantage and isolate Hash so that it is possible to reuse it in another service besides CreateUser / AuthenticateUser and thus not hurt the DRY (Don’t Repeat Yourself) concept , that is, not repeat business rules .
// CreateUserService 
import  {  hash  }  from  'bcryptjs' ;
...

@ injectable ( ) 
class  CreateUserService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository , 
	)  { }

	public  async  execute ( { name , email , password } : IRequest ) : Promise < User >  { 
		const  checkUserExist  =  await  this . usersRepository . findByEmail ( email ) ;

		if  ( checkUserExist )  { 
			throw  new  AppError ( 'Email already used!' ) ; 
		}

		const  hashedPassword  =  await  hash ( password ,  8 ) ;

		...
	} 
}

export  default  CreateUserService ;
import  {  compare  }  from  'bcryptjs' ;

...

@ injectable ( ) 
class  AuthenticateUserService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository , 
	)  { }

	public  async  execute ( { password , email } : IRequest ) : Promise < IResponse >  { 
		const  user  =  await  this . usersRepository . findByEmail ( email ) ;

		if  ( ! user )  { 
			throw  new  AppError ( 'Incorrect email / password combination.' ,  401 ) ; 
		}

		const  passwordMatched  =  await  compare ( 
			password , 
			user . password , 
		) ;

	...
	} 
}

export  default  AuthenticateUserService ;
  • I will create a providers folder that will contain features that will be offered to services.
  • Within the providers, I will create HashProvider, which will contain models (interface), implementations (which libraries will I use for hashing), fakes (libraries made with JS Pure for hashing).
// @modules / users / providers / HashProvider / models 
export  default  interface  IHashProvider  { 
	generateHash ( payload : string ) : Promise < string > ; 
	compareHash ( payload : string ,  hashed : string ) : Promise < boolean > ; 
}
// @modules / users / provider / HashProvider / implementations 
import  {  hash ,  compare  }  from  'bcrypjs' ; 
import  IHashProvider  from  '../models/IHashProvider' ;

class  BCryptHashProvider  implements  IHashProvider  {

	public  async  generateHash ( payload : string ) : Promise < string > { 
		return  hash ( payload ,  8 ) ; 
	}

	public  async  compareHash ( payload : string ,  hashed : string ) : Promise < string > { 
		return  compare ( payload ,  hashed ) ; 
	}

}

export  default  BCryptHashProvider ;
  • Go to CreateUserService / AuthenticateUserService and make it depend only on the HashProvider interface .
// CreateUserService 
import  IHashProvider  from  '../providers/HashProvider/models/IHashProvider' ;

@ injectable ( ) 
class  CreateUserService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		private  hashProvider : IHashProvider , 
	)  { }

	public  async  execute ( { name , email , password } : IRequest ) : Promise < User >  { 
		const  checkUserExist  =  await  this . usersRepository . findByEmail ( email ) ;

		if  ( checkUserExist )  { 
			throw  new  AppError ( 'Email already used!' ) ; 
		}

		const  hashedPassword  =  await  this . hashProvider . generateHash ( password ) ;

		const  user  =  await  this . usersRepository . create ( { 
			name , 
			email , 
			password : hashedPassword , 
		} ) ;

		return  user ; 
	} 
}

export  default  CreateUserService ;
// AuthenticateUserService 
import  {  injectable ,  inject }  from  'tsyringe' ; 
import  {  sign  }  from  'jsonwebtoken' ; 
import  authConfig  from  '@ config / auth' ; 
import  AppError  from  '@ shared / errors / AppError' ; 
import  User  from  '../infra/typeorm/entities/User' ;

import  IUsersRepository  from  '../repositories/IUsersRepository' ; 
import  IHashProvider  from  '../providers/HashProvider/models/IHashProvider' ;

interface  IRequest  { 
	email : string ; 
	password : string ; 
}

 IResponse  interface { 
	user : User ; 
	token : string ; 
} 
@ injectable ( ) 
class  AuthenticateUserService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		private  hashProvider : IHashProvider , 
	)  { }

	public async  execute ( { password , email } : IRequest ) : Promise < IResponse >  { 
		const  user  =  await  this . usersRepository . findByEmail ( email ) ;

		if  ( ! user )  { 
			throw  new  AppError ( 'Incorrect email / password combination.' ,  401 ) ; 
		}

		const  passwordMatched  =  await  this . hashProvider . compareHash ( 
			password , 
			user . password , 
		) ;

		if  ( ! passwordMatched )  { 
			throw  new  AppError ( 'Incorrect email / password combination.' ,  401 ) ; 
		}

		const  { secret , expiresIn }  =  authConfig . jwt ;

		const  token  =  sign ( { } ,  secret ,  { 
			subject : user . id , 
			expiresIn , 
		} ) ;

		return  { user , token } ; 
	} 
}

export  default  AuthenticateUserService ;
  • Now I need to inject the IHashProvider dependency.
// @modules /users/provider/index.ts 
import  {  container  }  from  'tsyringe' ;

import  BCryptHashProvider  from  './HashProvider/implementations/BCryptHashProvider' ; 
import  IHashProvider  from  './HashProvider/models/IHashProvider' ;

container . registerSingleTon < IHashProvider > ( 'HashProvider' ,  BCryptHashProvider ) ;
  • I need to inject this dependency into the service and import this index.ts into @ shared / container / index.ts;
import  IHashProvider  from  '../providers/HashProvider/models/IHashProvider' ;

@ injectable ( ) 
class  CreateUserService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		@ inject ( 'HashProvider' ) 
		private  hashProvider : IHashProvider , 
	)  { }

	...

	} 
}

export  default  CreateUserService ;
// @shared /container/index.ts 
import  '@ modules / users / providers' ;

...
  • Create my FakeHashProvider, as my tests should not depend on strategies or other libraries like bcryptjs .
import  IHashProvider  from  '../models/IHashProvider' ;

class  FakeHashProvider  implements  IHashProvider  {

	public  async  generateHash( payload : string ) : Promise < string > { 
		return  payload ; 
	}

	public  async  compareHash ( payload : string ,  hashed :string ) : Promise < boolean > { 
		return  payload  ===  hashed ; 
	}

} ;

export  default  FakeHashProvider ;
  • Go to AuthenticateUserService.spec.ts
import  CreateUserService  from  './CreateUserService' ;
import  AuthenticateUserService  from  './AuthenticateUserService' ; 
import  FakeHashProvider  from  '../providers/HashProvider/fakes/FakeHashProviders' ;

describe ( 'AuthenticateUser' ,  ( )  =>  { 
	it ( 'should be able to authenticate' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  fakeHashProvider  =  new  FakeHashProvider ( ) ; 
		const  createUser  =  new  CreateUserService ( 
			fakeUsersRepository , 
			fakeHashProvider 
		) ; 
		const  authenticateUser =  new  AuthenticateUserService ( 
			fakeUsersRepository , 
			fakeHashProvider 
		) ;

		const  user  =  await  createUser . execute ( { 
			name : 'Test unit' , 
			email : 'testunit@example.com' , 
			password : '123456' , 
		} )

		const  authUser  =  await  authenticateUser . execute ( { 
			email : 'testunit@example.com' , 
			password : '123456' , 
		} )

		expect ( authUser ) . toHaveProperty ( 'token' ) ; 
		expect ( authUser . user ) . toEqual ( user ) ;

	} ) ; 
} ) ;

Provider Storage

  • Currently, UpdateUserAvatarService is using lib multer to upload an image. We cannot say that only the user module will upload files , so we will isolate this logic for the shared and create models , implementations , fakes , dependency injection as was done in HashProvider.
class  UpdateUserAvatarService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository , 
	) { }

	public  async  execute ( { user_id , avatarFilename } : IRequest ) : Promise < User >  { 
		const  user  =  await  this . usersRepository . findById ( user_id ) ; 
		if  ( ! user )  { 
			throw  new  AppError ( 'Only authenticated user can change avatar' ,  401 ) ; 
		}

		// this direct dependency on the multer must be removed, as it ends up hurting liskov substitution and dependency inversion principle 
		if  ( user . avatar )  { 
			const  userAvatarFilePath  =  path . join ( uploadConfig . directory ,  user . avatar ) ; 
			const  userAvatarFileExists  =  await  fs . promises . stat ( userAvatarFilePath ) ; 
			if  ( userAvatarFileExists )  {
				await  fs . promises . unlink ( userAvatarFilePath ) ; 
			} 
			user . avatar  =  avatarFilename ; 
			await  this . usersRepository . save ( user ) ;

			return  user ; 
		} 
	} 
}

export  default  UpdateUserAvatarService ;
  • Create folder in shared / container / providers / StorageProvider .
  • Within StorageProvider there will be models , implementations , fakes .
	//shared/container/providers/StorageProvider/models/IStorageProvider.ts

	export  default  interface  IStorageProvider  { 
		saveFile ( file : string ) : Promise < string > ; 
		deleteFile ( file : string ) : Promise < string > ; 
	}
  • Change the uploadConfig
...

const  tmpFolder  =  path . resolve ( __dirname ,  '..' ,  '..' ,  'tmp'  ) ;

export  default  { 
	tmpFolder ,

	uploadsFolder : path . join ( tmpFolder ,  'uploads' ) ;

	...
}
	//shared/container/providers/StorageProvider/implementations/DiskStorageProvider.ts 
	import  fs  from  'fs' ; 
	import  path  from  'path' ; 
	import  uploadConfig  from  '@ config / upload' ;

	import  IStorageProvider  from  '../models/IStorageProvider' ;

	class  DiskStorageProvider  implements  IStorageProvider  { 
		public  async  saveFile ( file : string ) : Promise < string > { 
			await  fs . promises . rename ( 
				path . resolve ( uploadConfig . tmpFolder ,  file ) , 
				path . resolve ( uploadConfig . uploadsFolder ,  file ) , 
			);

			return  file ; 
		}

		public  async  deleteFile ( file : string ) : Promise < void > { 
			const  filePath  =  path . resolves ( uploadConfig . uploadsFolder ,  file ) ;

			try { 
				await  fs . promises . stat ( filePath ) ; 
			}  catch  { 
				return ; 
			}

			await  fs . promises . unlink ( filePath ) ;

		} 
	}

	export  default  DiskStorageProvider ;
  • Inject the dependency that will be on UpdateUserAvatarService;
  • Create @ shared / container / providers / index.ts and import this container into @ shared / container / index.ts .
import  {  container  }  from  'tsyringe' ;

import  IStorageProvider  from  './StorageProvider/models/IStorageProvider' ; 
import  DiskStorageProvider  from  './StorageProvider/implementations/DiskStorageProvider' ;

container . registerSingleton < IStorageProvider > ( 
	'StorageProvider' , 
	DiskStorageProvider ) ;
  • Go to UpdateUserAvatarService.ts
import  path  from  'path' ; 
import  fs  from  'fs' ; 
import  uploadConfig  from  '@ config / upload' ; 
import  AppError  from  '@ shared / errors / AppError' ;

import  {  injectable ,  inject  }  from  'tsyringe' ; 
import  IStorageProvider  from  '@ shared / container / providers / StorageProvider / models / IStorageProvider' ; 
import  User  from  '../infra/typeorm/entities/User' ; 
import  IUsersRepository  from  '../repositories/IUsersRepository' ;

interface  IRequest  { 
	user_id : string ; 
	avatarFilename : string ; 
}

@ injectable ( ) 
class  UpdateUserAvatarService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		@ inject ( 'StorageProvider' ) 
		private  storageProvider : IStorageProvider , 
	)  { }

	public  async  execute ( { user_id , avatarFileName } : IRequest ) : Promise < User >  { 
		const  user  =  await  this . usersRepository . findById ( user_id ) ;

		if  ( ! user )  { 
			throw  new  AppError ( 'Only authenticated user can change avatar' ,  401 ) ; 
		}

		if  ( user . avatar )  { 
			await  this . storageProvider . deleteFile ( user . avatar ) ; 
		}

		const  filePath  =  await  this . storageProvider . saveFile ( avatarFileName ) ;

		user . avatar  =  filePath ;

		await  this . usersRepository . save ( user ) ;

		return  user ; 
	} 
} 
export  default  UpdateUserAvatarService ;
  • Create the fakes storage provider, which will be used in the tests since the unit tests should not depend on any library.
// @shared /container/providers/StorageProvider/fakes/FakeStorageProvider.ts 
	import  IStorageProvider  from  '../models/IStorageProvider' ;

	class  FakeStorageProvider  implements  IStorageProvider  {

		private  storage : string [ ]  =  [ ] ;

		public  async  saveFile ( file : string ) : Promise < string >  { 
			this . storage . push ( file ) ;

			return  file ; 
		}

		public  async  deleteFile ( file : string ) : Promise < void >  { 
			const  findIndex  =  this . storage . findIndex ( item  =>  item  ===  file ) ;

			this . storage . splice ( findIndex ,  1 ) ; 
		} 
	}

	export  default  FakeStorageProvider ;

Updating Avatar

  • Create UpdateUserAvatarService.spec.ts unit test
import  AppError  from  '@ shared / errors / AppError' ;

import  FakeUsersRepository  from  '../repositories/fakes/FakeUserRepository' ; 
import  FakeStorageProvider  from  '@ shared / container / providers / StorageProvider / fakes / FakeStorageProvider' ; 
import  UpdateUserAvatarService  from  './UpdateUserAvatarService' ;

describe ( 'UpdateUserAvatarService' ,  ( )  =>  { 
	it ( 'should be able to update user avatar' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  fakeStorageProvider  =  new  FakeStorageProvider ( ) ; 
		const  updateUserAvatarService  =  new  UpdateUserAvatarService ( 
			fakeUsersRepository , 
			fakeStorageProvider 
		) ;

		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test Unit' , 
			email : 'testunit@example.com' , 
			password : '123456' , 
		} ) ;

		await  updateUserAvatarService . execute ( { 
				user_id : user . id , 
				avatarFileName : 'avatar.jpg' , 
		} )

		expect ( user . avatar ) . toBe ( 'avatar.jpg' ) ;

	} ) ;

	it ( 'should not be able to update avatar from non existing user' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  fakeStorageProvider  =  new  FakeStorageProvider ( ) ; 
		const  updateUserAvatarService  =  new  UpdateUserAvatarService ( 
			fakeUsersRepository , 
			fakeStorageProvider 
		) ;

		expect ( updateUserAvatarService . execute ( { 
				user_id : 'non-existing-id' , 
				avatarFileName : 'avatar.jpg' , 
		} ) ) . rejects . toBeInstanceOf ( AppError ) ;

	} ) ;

	it ( 'should delete old avatar when updating a new one' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  fakeStorageProvider  =  new  FakeStorageProvider ( ) ; 
		const  updateUserAvatarService  =  new  UpdateUserAvatarService ( 
			fakeUsersRepository , 
			fakeStorageProvider 
		) ;

		const  deleteFile  =  jest . spyOn ( fakeStorageProvider ,  'deleteFile' ) ;

		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test Unit' , 
			email : 'testunit@example.com' , 
			password : '123456' , 
		} ) ;

		await  updateUserAvatarService . execute ( { 
				user_id : user . id , 
				avatarFileName : 'avatar.jpg' , 
		} ) 
		await  updateUserAvatarService . execute ( { 
				user_id : user . id , 
				avatarFileName : ' avatar2.jpg ' , 
		} )

		expect ( deleteFile ) . toHaveBeenCalledWithin ( 'avatar.jpg' ) ; 
		expect ( user . avatar ) . toBe ( ' avatar2.jpg ' ) ;

	} ) ;

} ) ;

const deleteFile = jest.spyOn (fakeStorageProvider, ‘deleteFile’) checks whether the function was called or not, and in the end I hope it was called with the avatar.jpg parameter .

Mapping Application Features

Made in Software Engineering.

I must :

- Mapear os Requisitos
- Conhecer as Regras de Negócio
  • DEV will not always have the necessary resources to map ALL the application’s features.

GoBarber will be mapped based on your Design. In this case, it will only be a guide for us, the mapping of the features does not need to describe ALL the features exactly, some of them will appear during the development.

  • In the real world, MANY MEETINGS must be held with the CUSTOMER so that MANY NOTES are made , to MAP FUNCTIONALITIES and BUSINESS RULES !

  • Is BASED on Feedback from the customer to be improved. I won’t always get it right the first time.

  • I must think of a project in a simple way . There is no point creating many features if the customer uses only 20% of them and wants them to be improved.

  • Create with Macro Functions in mind .

Macro Features

  • Inside they are well defined , but we can see them as an application screen .

  • And each macro functionality will be well described and documented, being divided into: Functional, Non-Functional Requirements and Business Rules.

Functional Requirements

  • Describes the functionality itself.

Non-Functional Requirements

  • The Business Rules are not directly linked.
  • Focused on the technical part.
  • It is linked to some lib that we choose to use.

Business rules

  • They are restrictions so that the functionality can be executed.
  • It is legal for a business rule to be associated with a functional requirement.

Macro Features - GoBarber

Password recovery

Functional Requirements

  • The user must be able to change the password informing his email.
  • The user should receive an email with instructions to reset their password.
  • The user must be able to change his password.

Non-Functional Requirements

  • Use Mailtrap for testing in a development environment.
  • Use Amazon SES to send email in production
  • The sending of email must happen in the background (background job).

Example: if 400 users try to recover their password at the same time, it would be trying to send 400 emails at the same time , which could cause a slow service . Therefore, the approach adopted will be of queue (background job) , where the queue will be processed little by little by the tool.

Business rules

  • The link that will be sent by email, should expire in 2h.
  • The user must confirm the password when resetting the password.
Profile Update

Functional Requirements

  • The user must be able to update his name, email and password.

Non-Functional Requirements

Business rules

  • The user cannot change the email to another email already used by another user.
  • When changing the password, the user must inform the old one.
  • When changing the password, the user must confirm the new password.
Provider Panel

Functional Requirements

  • The user must be able to view the schedules for a specific day.
  • The provider must receive a notification whenever there is a new appointment.
  • The provider must be able to view unread notifications.

Non-Functional Requirements

  • The provider’s schedules for the day must be cached.

It is possible that the provider is reloading the page many times throughout the day. Soon, the cache will be cleared and stored only when you have a new schedule.

  • The provider’s notifications must be stored in MongoDB.
  • Service provider notifications must be sent in real time using Socket.io.

Business rules

  • The notification must have a read or unread status for the provider to control.
Service Scheduling

Functional Requirements

  • The user must be able to list all registered service providers.
  • The user must be able to list days of a month with at least one time available by the provider.
  • The user must be able to list available times on a specific day for a provider.
  • The user must be able to make a new appointment with the provider.

Non-Functional Requirements

  • The list of providers must be cached.

This way, you will avoid processing costs of the machine.

Business rules

  • Each appointment must last exactly 1 hour.
  • Appointments must be available between 8 am and 6 pm (first time at 8 am, last time at 5 pm).
  • The user cannot schedule at an already busy time.
  • The user cannot schedule an appointment that has passed.
  • The user cannot schedule services with himself.
  • The user can only make an appointment at a time with a service provider.

Applying TDD in practice

  • I will take macro functionality and start writing your tests, taking one functional requirement at a time.

Problem

How am I going to write tests for a feature that doesn’t even exist yet?

Solution

For this, I will create a basic and minimal structure, so that it is possible to fully test a functionality even if it does not yet exist.

  • Create the MailProvider in the shared folder, since sending email is not necessarily just a password recovery request.

  • Create in @ shared / container / providers / MailProvider the models folders (rules that implementations must follow), implementations (which are the email service providers), fakes (for testing) .

// models / IMailProvider

export  default  interface  IMailProvider  { 
	sendMail ( to : string ,  body : string ) : Promise < void > ; 
}
// fakes / FakeMailProvider

import  IMailProvider  from  '../models/IMailProvider' ;

interface  IMessage  {  
	to : string ; 
	body : string ; 
}

class  FakeMailProvider  implements  IMailProvider  { 
	private  messages : IMessage [ ]  =  [ ] ;

	public  async  sendMail ( to : string ,  body : string ) : Promise < void >  { 
		this . messages . push ( { 
			to , 
			body , 
		} ) ; 
	} 
}

export  default  FakeMailProvider ;
  • Create SendForgotPasswordEmailService.ts and SendForgotPasswordEmailService.spec.ts
// SendForgotPasswordEmailService.ts 
import  {  injectable ,  inject  }  from  'tsyringe' ;

import  IUsersRepository  from  '../repositories/IUsersRepository' ; 
import  IMailProvider  from  '@ shared / container / providers / MailProvider / models / IMailProvider' ;

interface  IRequest  { 
	email : string ; 
}

@ injectable ( ) 
class  SendForgotPasswordEmailService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		@ inject ( 'MailProvider' ) 
		private  mailProvider : IMailProvider 
	)

	public  async  execute ( { email } : IRequest ) : Promise < void > { 
		this . mailProvider . sendMail ( email ,  'Password recovery request' ) ; 
	} ; 
}

export  default  SendForgotPasswordEmailService
// SendForgotPasswordEmailService.spec.ts

import  SendForgotPasswordEmailService  from  './SendForgotPasswordEmailService' ; 
import  FakeUsersRepository  from  '../repositories/fakes/FakeUsersRepository' ; 
import  FakeMailProvider  from  '@ shared / container / providers / MailProvider / fakes / FakeMailProvider' ;

describe ( 'SendForgotPasswordEmail' ,  ( )  =>  { 
	it ( 'should be able to recover the password using email' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  fakeMailProvider  =  new  FakeMailProvider ( ) ;

		const  sendForgotPasswordEmail  =  new  SendForgotPasswordEmailService ( 
			fakeUsersRepository , 
			fakeMailProvider , 
		) ;

		const  sendMail  =  jest . spyOn ( fakeMailProvider ,  'sendMail' ) ;

		await  fakeUsersRepository . create ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' , 
		} ) ;

		await  sendForgotPasswordEmail . execute ( { 
			email : 'test@example.com' , 
		} ) ;

		expect ( sendMail ) . toHaveBeenCalled ( ) ;

	} ) 
} ) ;
  • Now just run the test.

Recovering password

  • Build a test so that it is not possible to recover the password of a user that does not exist
describe ( 'SendForgotPasswordEmail' ,  ( )  =>  { 
	it ( 'should be able to recover the password using email' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  fakeMailProvider  =  new  FakeMailProvider ( ) ;

		const  sendForgotPasswordEmail  =  new  SendForgotPasswordEmailService ( 
			fakeUsersRepository , 
			fakeMailProvider , 
		) ;

		const  sendMail  =  jest . spyOn ( fakeMailProvider ,  'sendMail' ) ;

		await  fakeUsersRepository . create ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' , 
		} ) ;

		await  sendForgotPasswordEmail . execute ( { 
			email : 'test@example.com' , 
		} ) ;

		expect ( sendMail ) . toHaveBeenCalled ( ) ;

	} ) ;

	it ( 'should not be able to recover the password a non-existing user' ,  ( )  =>  { 
		const  fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		const  fakeMailProvider  =  new  FakeMailProvider ( ) ;

		const  sendForgotPasswordEmail  =  new  SendForgotPasswordEmailService ( 
			fakeUsersRepository , 
			fakeMailProvider , 
		) ;

		await 	expect ( 
			sendForgotPasswordEmail . execute ( { 
			email : 'test@example.com' , 
		} ) ) . rejects . toBeInstanceOf ( AppError ) ;

	} ) 
} ) ;
  • If I run this new test ( should not be able to recover the password a non-existing user ), an ‘error’ will occur saying that: ‘an error was expected, but the promise has been resolved’. That is, instead of being reject , it was resolved .

  • If it is observed in the service SendForgotPasswordEmailService.ts , it is possible to see that there is no verification whether the user exists or not before sending the email, therefore, it must be added.

// SendForgotPasswordEmailService.ts 
import  {  injectable ,  inject  }  from  'tsyringe' ;

import  IUsersRepository  from  '../repositories/IUsersRepository' ; 
import  IMailProvider  from  '@ shared / container / providers / MailProvider / models / IMailProvider' ;

interface  IRequest  { 
	email : string ; 
}

@ injectable ( ) 
class  SendForgotPasswordEmailService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		@ inject ( 'MailProvider' ) 
		private  mailProvider : IMailProvider 
	)

	public  async  execute ( { email } : IRequest ) : Promise < void > { 
		const  checkUserExist   =  await  this . usersRepository . findByEmail ( email ) ;

		if  ( ! checkUserExist  )  { 
			throw  new  AppError ( 'User does not exists.' ) ; 
		}

		this . mailProvider . sendMail ( email ,  'Password recovery request' ) ; 
	} ; 
}

export  default  SendForgotPasswordEmailService

Thus, it was possible to notice that the service was being built based on the application tests.

Recovering password

Problem

It was observed that in the link that the user will receive by email, it must have an encryption (token) so that it is not possible to change the password of another user, and to ensure that the password recovery has been provided with a request.

Solution

  1. Create and map UserToken entity in @ modules / users / infra / typeorm / entities
// @modules /users/infra/typeorm/entities/UserToken.ts 
import  { 
	Entity , 
	PrimaryGeneratedColumn , 
	Column , 
	CreateDateColumn , 
	UpdateDateColumn , 
	Generated , 
}  from  'typeorm' ;

@ Entity ( 'user_tokens' ) 
class  UserToken  { 
	@ PrimaryGeneratedColumn ( 'uuid' ) 
	id : string ;

	@ Column ( ) 
	@ Generated ( 'uuid' ) 
	token : string ;

	@ Column ( ) 
	user_id : string ;

	@ CreateDateColumn ( ) 
	created_at : Date ;

	@ UpdateDateColumn ( ) 
	updated_at : Date ; 
}

export  default  UserToken ;
  1. Create the interface for the IUserToken repository
import  UserToken  from  '../infra/typeorm/entities/UserToken' ;

export  default  interface  IUserTokenRepository  { 
	generate ( user_id : string ) : Promise < UserToken > ; 
}
  1. Create the fake repository using the interface
import  {  uuid  }  from  'uuidv4' ;

import  UserToken  from  '../../infra/typeorm/entities/UserToken' ; 
import  IUserTokenRepository  from  '../IUserTokenRepository' ;

class  FakeUserTokensRepository  implements  IUserTokenRepository  {

	public  async  generate ( user_id : string ) : Promise < UserToken >  { 
		const  userToken  =  new  UserToken ( ) ;

		Object . assign ( userToken ,  { 
			id : uuid ( ) , 
			token : uuid ( ) , 
			user_id , 
		} )

		return  userToken ; 
	}

}

export  default  FakeUserTokensRepository ;
  1. Create the test to verify that the repository method for creating the token is being called.
  2. Change in SendForgotPasswordEmailService.ts to use the generate method of UserTokensRepository
import  {  injectable ,  inject  }  from  'tsyringe' ;

import  IUsersRepository  from  '../repositories/IUsersRepository' ; 
import  IUserTokensRepository  from  '../repositories/IUserTokensRepository' ; 
import  IMailProvider  from  '@ shared / container / providers / MailProvider / models / IMailProvider' ;

interface  IRequest  { 
	email : string ; 
}

@ injectable ( ) 
class  SendForgotPasswordEmailService  { 
	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		@ inject ( 'MailProvider' ) 
		private  mailProvider : IMailProvider ,

		@ inject ( 'UserTokensRepository' ) 
		private  userTokensRepository : IUserTokensRepository , 
	)

	public  async  execute ( { email } : IRequest ) : Promise < void > { 
		const  user   =  await  this . usersRepository . findByEmail ( email ) ;

		if  ( ! user )  { 
			throw  new  AppError ( 'User does not exists.' ) ; 
		}

		await  this . userTokensRepository . generate ( user . id ) ;

		await  this . mailProvider . sendMail ( email ,  'Password recovery request' ) ; 
	} ; 
}
  1. Declare variables (let) in the file, and add the beforeEach method to instantiate the necessary classes before each test, to avoid repetition of creating instances.
import  AppError  from  '@ shared / errors / AppError' ;

import  FakeMailProvider  from  '@ shared / container / providers / MailProvider / fakes / FakeMailProvider' ; 
import  FakeUserTokenRepository  from  '@ modules / users / repositories / fakes / FakeUserTokensRepository' ; 
import  FakeUsersRepository  from  '../repositories/fakes/FakeUsersRepository' ; 
import  SendForgotPasswordEmailService  from  './SendForgotPasswordEmailService' ;

let  fakeUsersRepository : FakeUsersRepository ; 
let  fakeUserTokensRepository : FakeUserTokenRepository ; 
let  fakeMailProvider : FakeMailProvider ; 
let  sendForgotPasswordEmail : SendForgotPasswordEmailService ;

describe ( 'SendForgotPasswordEmail' ,  ( )  =>  { 
	beforeEach ( ( )  =>  { 
		fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		fakeUserTokensRepository  =  new  FakeUserTokenRepository ( ) ; 
		fakeMailProvider  =  new  FakeMailProvider ( ) ;

		sendForgotPasswordEmail  =  new  SendForgotPasswordEmailService ( 
			fakeUsersRepository , 
			fakeMailProvider , 
			fakeUserTokensRepository , 
		) ; 
	} ) ;

	it ( 'should be able to recover the password using the email' ,  async  ( )  =>  { 
		const  sendEmail  =  jest . spyOn ( fakeMailProvider ,  'sendMail' ) ;

		await  fakeUsersRepository . create ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' , 
		} ) ;

		await  sendForgotPasswordEmail . execute ( { 
			email : 'test@example.com' , 
		} ) ;

		expect ( sendEmail ) . toHaveBeenCalled ( ) ; 
	} ) ;

	it ( 'should not be able to recover the password of a non-existing user' ,  async  ( )  =>  { 
		await  expect ( 
			sendForgotPasswordEmail . execute ( { 
				email : 'test@example.com' , 
			} ) , 
		) . rejects . toBeInstanceOf ( AppError ) ; 
	} ) ;

	it ( 'should generate a forgot password token' ,  async  ( )  =>  { 
		const  generateToken  =  jest . spyOn ( fakeUserTokensRepository ,  'generate' ) ;

		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' , 
		} ) ;

		await  sendForgotPasswordEmail . execute ( {  email : 'test@example.com'  } ) ;

		expect ( generateToken ) . toHaveBeenCalledWith ( user . id ) ; 
	} ) ; 
} ) ;

Password reset

  1. Create the findByToken method in the UserTokens interface;
import  UserToken  from  '../infra/typeorm/entities/UserToken' ;

export  default  interface  IUserTokenRepository  { 
	generate ( user_id : string ) : Promise < UserToken > ; 
	findByToken ( token : string ) : Promise < UserToken | undefined > ; 
}
  1. Create _ResetPasswordService.spec.ts. _;
import  FakeUsersRepository  from  '../repositories/fakes/FakeUsersRepository' ; 
import  FakeUserTokensRepository  from  '../repositories/fakes/FakeUserTokensRepository' ; 
import  FakeHashProvider  from  '../provider/HashProvider/fakes/FakeHashProvider' ; 
import  ResetPasswordService  from  './ResetPasswordService' ;

let  fakeUsersRepository : FakeUsersRepository ; 
let  fakeUserTokensRepository : FakeUserTokensRepository ; 
let  resetPassword : ResetPasswordService ;

describe ( 'ResetPassword' ,  ( )  =>  { 
	beforeEach ( ( )  =>  { 
		fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		fakeUserTokensRepository  =  new  FakeUserTokensRepository ( ) ; 
		resetPassword  =  new  ResetPasswordService ( 
			fakeUsersRepository , 
			fakeUserTokensRepository , 
		) ; 
	} )

	it ( 'should be able to reset the password' ,  ( )  =>  { 
		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' , 
		} ) ;

		const  generateHash  =  jest . spyOn ( fakeHashProvider ,  'generateHash' ) ;

		const  { token }  =  await  fakeUserTokensRepository . generate ( user . id ) ;

		await  resetPassword . execute ( { 
			token , 
			password : '4444' , 
		} ) ;

		expect ( user . password ) . toBe ( '4444' ) ; 
		expect ( generateHash ) . toHaveBeenCalledWith ( '4444' ) ; 
	} ) ; 
} ) ;
  1. Create ResetPasswordService.ts ;
import  {  injectable ,  inject  }  from  'tsyringe' ; 
import  AppError  from  '@ shared / errors / AppError' ;

import  IUsersRepository  from  '../repositories/IUsersRepository' ; 
import  IUserTokensRepository  from  '../repositories/IUserTokensRepository' ; 
import  IHashProvider  from  '../providers/HashProvider/models/IHashProvider' ;

interface  IRequest  { 
	token : string ; 
	password : string ; 
}

@ injectable ( ) 
class  ResetPasswordService  {

	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		@ inject ( 'UserTokensRepository' ) 
		private  userTokensRepository : IUserTokensRepository ,

		@ inject ( 'HashProvider' ) 
		private  hashProvider : IHashProvider , 
	)

	public  async  execute ( { token , password } : IRequest ) : Promise < void > { 
		const  userToken  =  await  this . userTokensRepository . findByToken ( token ) ;

		if  ( ! userToken )  { 
			throw  new  AppError ( 'User Token does not exist.' ) ; 
		}

		const  user  =  await  this . usersRepository . findById ( userToken . user_id ) ;

		if  ( ! user )  { 
			throw  new  AppError ( 'User does not exists.' ) ; 
		}

		user . password  =  await  this . hashProvider . generateHash ( password ) ;

		await  this . usersRepository . save ( user ) ; 
	} 
}

export  default  ResetPasswordService ;

Finishing the tests

  1. Go to ResetPasswordService.spec.ts and add more unit tests for:
  • non-existent userToken case
  • nonexistent user case
  • the token expires in 2h
import  AppError  from  '@ shared / errors / AppError' ;

import  FakeUsersRepository  from  '../repositories/fakes/FakeUsersRepository' ; 
import  FakeUserTokensRepository  from  '../repositories/fakes/FakeUserTokensRepository' ; 
import  FakeHashProvider  from  '../provider/HashProvider/fakes/FakeHashProvider' ; 
import  ResetPasswordService  from  './ResetPasswordService' ;

let  fakeUsersRepository : FakeUsersRepository ; 
let  fakeUserTokensRepository : FakeUserTokensRepository ; 
let  resetPassword : ResetPasswordService ;

describe ( 'ResetPassword' ,  ( )  =>  { 
	beforeEach ( ( )  =>  { 
		fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		fakeUserTokensRepository  =  new  FakeUserTokensRepository ( ) ; 
		resetPassword  =  new  ResetPasswordService ( 
			fakeUsersRepository , 
			fakeUserTokensRepository , 
		) ; 
	} )

	it ( 'should be able to reset the password' ,  ( )  =>  { 
		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' , 
		} ) ;

		const  generateHash  =  jest . spyOn ( fakeHashProvider ,  'generateHash' ) ;

		const  { token }  =  await  fakeUserTokensRepository . generate ( user . id ) ;

		await  resetPassword . execute ( { 
			token , 
			password : '4444' , 
		} ) ;

		expect ( user . password ) . toBe ( '4444' ) ; 
		expect ( generateHash ) . toHaveBeenCalledWith ( '4444' ) ; 
	} ) ;

	it ( 'should not be able to reset the password with a non-existing user' ,  ( )  =>  { 
		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' , 
		} ) ;

		const  { token }  =  await  fakeUserTokensRepository . generate ( 'non-existing user' ) ;

		await  expect ( resetPassword . execute ( { 
			token , 
			password : '4444' , 
		} ) ) . rejects . toBeInstanceOf ( AppError ) ; 
	} ) ;

	it ( 'should not be able to reset the password with a non-existing token' ,  ( )  =>  { 
		await  expect ( resetPassword . execute ( { 
			token : 'non-existing-token' , 
			password : '4444' , 
		} ) ) . rejects . toBeInstanceOf ( AppError ) ; 
	} ) ;

	it ( 'should not be able to reset the password if passed more than 2 hours' ,  ( )  =>  { 
		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' , 
		} ) ;

		jest . spyOn ( Date ,  'now' ) . mockImplementationOnce ( ( )  =>  { 
			const  customDate  =  new  Date ( ) ;

			return  customDate . setHours ( customDate . getHours ( )  +  3 ) ; 
		} ) ;

		const  { token }  =  await  fakeUserTokensRepository . generate ( user . id ) ;

		await  expect ( resetPassword . execute ( { 
			token , 
			password : '4444' , 
		} ) ) . rejects . toBeInstanceOf ( AppError ) ; 
	} ) 
} ) ;
  1. Generate a date when a FakeUserToken is generated;
import  {  uuid  }  from  'uuidv4' ;

import  UserToken  from  '../../infra/typeorm/entities/UserToken' ; 
import  IUserTokenRepository  from  '../IUserTokenRepository' ;

class  FakeUserTokensRepository  implements  IUserTokenRepository  {

	public  async  generate ( user_id : string ) : Promise < UserToken >  { 
		const  userToken  =  new  UserToken ( ) ;

		Object . assign ( userToken ,  { 
			id : uuid ( ) , 
			token : uuid ( ) , 
			user_id , 
			created_at : new  Date ( ) , 
			updated_at : new  Date ( ) , 
		} )

		return  userToken ; 
	}

}

export  default  FakeUserTokensRepository ;
  1. Use some date-fns functions to check the token’s creation date;
import  {  injectable ,  inject  }  from  'tsyringe' ; 
import  {  isAfter ,  addHours  }  from  'date-fns' ;

import  AppError  from  '@ shared / errors / AppError' ;

import  IUsersRepository  from  '../repositories/IUsersRepository' ; 
import  IUserTokensRepository  from  '../repositories/IUserTokensRepository' ; 
import  IHashProvider  from  '../providers/HashProvider/models/IHashProvider' ;

interface  IRequest  { 
	token : string ; 
	password : string ; 
}

@ injectable ( ) 
class  ResetPasswordService  {

	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		@ inject ( 'UserTokensRepository' ) 
		private  userTokensRepository : IUserTokensRepository ,

		@ inject ( 'HashProvider' ) 
		private  hashProvider : IHashProvider , 
	)

	public  async  execute ( { token , password } : IRequest ) : Promise < void > { 
		const  userToken  =  await  this . userTokensRepository . findByToken ( token ) ;

		if  ( ! userToken )  { 
			throw  new  AppError ( 'User Token does not exist.' ) ; 
		}

		const  user  =  await  this . usersRepository . findById ( userToken . user_id ) ;

		if  ( ! user )  { 
			throw  new  AppError ( 'User does not exists.' ) ; 
		}

		const  tokenCreateAt  =  userToken . created_at 
		const  compareDate  =  addHours ( tokenCreateAt ,  2 ) ;

		if  ( isAfter ( Date . now ( ) ,  compareDate ) )  { 
			throw  new  AppError ( 'Token expired.' ) ; 
		}

		user . password  =  await  this . hashProvider . generateHash ( password ) ;

		await  this . usersRepository . save ( user ) ; 
	} 
}

export  default  ResetPasswordService ;

addHours adds hours on a specific date; isAfter will compare if the date the service was executed is greater than the date the token was created added to 2 hours. If this is true, it means that the tokenCreatedAt + 2 (hours) has passed the current time, that is, that the token has expired.

  • non-existent userToken case
  • nonexistent user case
  • the token expires in 2h

Saving Tokens to the Bank

  1. Create Routes and Controllers
  2. Create the Token Repository (TypeORM)
  3. Create Migration for Token Creation
  4. Inject Token Dependency
  5. Email sending provider (DEV)
  6. Test everything

Remembering that the controllers of a Restful API must have a maximum of 5 methods:

  • index (list all)
  • show (list only one)
  • create
  • update
  • delete

Start by creating routes ( password.routes.ts ) and controllers ( ForgotPasswordController.ts , ResetPasswordControler.ts )

// ForgotPasswordController.ts 
import  {  container  }  from  'tsyringe'

import  {  Request ,  Response  }  from  'express' ;

import  ForgotPasswordEmailService  from  '@ modules / users / services / ForgotPasswordEmailService' ;

class  ForgotPasswordController  { 
	public  async  create ( request : Request ,  response : Response ) : Promise < Response > { 
		const  { email }  =  request . body ;

		const  forgotPassword  =  container . resolve ( ForgotPasswordEmailService ) ;

		await  forgotPassword . execute ( { 
			email , 
		} ) ;

		return  response . status ( 204 ) ; 
	} 
}

export  default  ForgotPasswordController
// ResetPasswordController.ts 
import  {  container  }  from  'tsyringe'

import  {  Request ,  Response  }  from  'express' ;

import  ResetPasswordService  from  '@ modules / users / services / ResetPasswordService' ;

class  ResetPasswordController  { 
	public  async  create ( request : Request ,  response : Response ) : Promise < Response > { 
		const  { token , password }  =  request . body ;

		const  resetPassword  =  container . resolve ( ResetPasswordService ) ;

		await  resetPassword . execute ( { 
			token , 
			password , 
		} ) ;

		return  response . status ( 400 ) . json ( ) ; 
	} 
}

export  default  ResetPasswordController ;
// password.routes.ts 
import  {  Router  }  from  'express' ;

import  ResetPasswordController  from  '../controllers/ResetPasswordController' ; 
import  ForgotPasswordController  from  '../controllers/ForgotPasswordController' ;

const  passwordRouter  =  Router ( ) ; 
const  forgotPasswordController  =  new  ForgotPasswordController ( ) ; 
const  resetPasswordController  =  new  ResetPasswordController ( ) ;

passwordRouter . post ( '/ reset' ,  resetPasswordController . create ) ; 
passwordRouter . post ( '/ forgot' , forgotPasswordController . create ) ;

export  default  passwordRouter ;

Update the route index.ts

// @shared /infra/http/routes/index.ts 
import  {  Router  }  from  'express' ; 
import  appointmentsRouter  from  '@ modules / appointments / infra / http / routes / appointments.routes' ; 
import  usersRouter  from  '@ modules / users / infra / http / routes / user.routes' ; 
import  sessionsRouter  from  '@ modules / users / infra / http / routes / sessions.routes' ; 
import  passwordRouter  from  '@ modules / users / infra / http / routes / password.routes' ;

const  routes  =  Router ( ) ;

routes . use ( '/ appointments' ,  appointmentsRouter ) ; 
routes . use ( '/ users' ,  usersRouter ) ; 
routes . use ( '/ sessions' ,  sessionsRouter ) ; 
routes . use ( '/ password' ,  passwordRouter ) ;

export  default  routes ;

Create the UserTokensRepository typeorm repository

// @modules /users/infra/typeorm/repositories/UserTokensRepository.ts 
import  {  Repository ,  getRepository  }  from  'typeorm' ;

import  UserToken  from  '../entities/UserToken' ; 
import  IUserTokensRepository  from  '@ modules / users / repositories / IUserTokensRepository' ;

class  UserTokensRepository  implements  IUserTokensRepository  { 
	private  ormRepository : Repository < UserToken > ;

	constructor ( ) { 
		this . ormRepository  =  getRepository ( UserToken ) ; 
	}

	public  async  findByToken ( token : string ) : Promise  < UserToken | undefined > { 
		const  userToken  =  await  this . ormRepository . findOne ( {  where : { token }  } ) ;

		return  userToken ; 
	}

	public  async  generate ( user_id : string ) : Promise < UserToken > { 
		const  userToken  =  this . ormRepository . create ( { 
			user_id , 
		} ) ;

		await  this . ormRepository . save ( userToken ) ;

		return  userToken ; 
	} 
}

export  default  UserTokensRepository ;

Create the migration

$ yarn typeorm migration: create -n CreateUserTokens

Go to @ shared / infra / typeorm / migrations / CreateUserTokens.ts

import  {  MigrationInterface ,  QueryRunner ,  Table  }  from  'typeorm' ;

export  default  class  CreateUserTokens1597089880963 
	implements  MigrationInterface  { 
	public  async  up ( queryRunner : QueryRunner ) : Promise < void > { 
		await  queryRunner . createTable ( new  Table ( { 
			name : 'user_tokens' , 
			columns : [ 
				{ 
					name : 'id' , 
					type : 'uuid' , 
					isPrimary :true , 
					generatedStrategy : 'uuid' , 
					default : 'uuid_generate_v4 ()' , 
				} , 
				{ 
					name : 'token' , 
					type : 'uuid' , 
					generatedStrategy : 'uuid' , 
					default : 'uuid_generate_v4 ()' , 
				} , 
				{ 
					name : 'user_id' , 
					type : 'uuid' , 
				} , 
				{ 
					name : 'created_at' , 
					type :'timestamp' , 
					default : 'now ()' , 
				} , 
				{ 
					name : 'updated_at' , 
					type : 'timestamp' , 
					default : 'now ()' , 
				} , 
			] , 
			foreignKeys : [ 
				{ 
					name : 'TokenUser' , 
					referencedTableName : 'users' , 
					referencedColumnNames : [ 'id' ] , 
					columnsName : [ 'user_id' ] ,
					onDelete : 'CASCADE' , 
					onUpdate : 'CASCADE' , 
				} 
			] , 
		} ) ) 
	}

	public  async  down ( queryRunner : QueryRunner ) : Promise < void > { 
		await  queryRunner . dropTable ( 'user_tokens' ) ; 
	}

}

Go to @ shared / container / index.ts and add

import  IUserTokensRepository  from  '@ modules / users / repositories / IUserTokensRepository' ; 
import  UserTokensRepository  from  '@ modules / users / infra / typeorm / repositories / UserTokensRepository' ;

container . registerSingleton < IUserTokensRepository > ( 
	'UserTokensRepository' , 
	UserTokensRepository , 
) ;

Developing emails

  • Using the EthrealMail lib
  1. Install nodemailer
$ yarn add nodemailer

$ yarn add @ types / nodemailer -D
  1. Create the nodemailer implementation in the MailProvider folder
import  nodemailer ,  {  Transporter  }  from  'nodemailer' ;

import  IMailProvider  from  '../models/IMailProvider' ;

class  EtherealMailProvider  implements  { 
	private  client : Transporter ;

	constructor (){ 
		nodemailer . createTestAccount ( ) . then ( account  =>  { 
			 const transporter =  nodemailer . createTransport ( { 
					host : account . smtp . host , 
					port : account . smtp . port , 
					secure : account . smtp . secure , 
					auth : { 
							user : account.user , 
							pass : account . pass , 
					} , 
				} ) ;

			this . client  =  transporter ;

		} ) ; 
	}

	public async  sendMail ( to : string ,  body : string ) : Promise < void > { 
		const  message  =  this . client . sendMail ( { 
        from : 'Team GoBarber <team@gobarber.com>' , 
        to , 
        subject : 'Recovery password' , 
        text : body , 
    } ) ;

		console . log ( 'Message sent:% s' ,  message . messageId ) ; 
		console . log ( 'Preview URL:% s' ,  nodemailer . getTestMessageUrl ( message ) ) ;

	}

}

export  default  EtherealMailProvider ;
  1. Create the dependency injection
import  {  container  }  from  'tsyringe' ;

import  IMailProvider  from  './MailProvider/models/IMailProvider' ; 
import  EtherealMailProvider  from  './MailProvider/implementations/EtherealMailProvider' ;

container . registerInstance < IMailProvider > ( 
	'MailProvider' , 
	new  EtherealMailProvider ( ) , 
)

I must use registerInstance , because with registerSingleton it was not working, as it is not instantiating EtherealMailProvider as it should happen.

  1. Get the token that is generated in SendForgotPasswordEmail
...
		 const  { token }  =  await  this . userTokensRepository . generate ( user . id ) ;

		await  this . mailProvider . sendMail ( 
			email , 
			`Password recovery request: $ { token } ` , 
		) ;
  • Test the route for insomnia
  • Receive console.log with the token
  • Click on the link, get the token sent in the body of the email
  • Trigger ResetPasswordService also for insomnia, passing the token and the new password.

Email Template

  • I must use a template engine for this, there are several: Nunjuncks, Handlerbars …
  • This Email Template will also be a provider.
  • Create the MailTemplateProvider folder , and within it models , implementations , fakes , dtos ;
// dtos 
interface  ITemplateVariables  { 
	[ key : string ] : string | number ; 
}

export  default  interface  IParseMailTemplateDTO  { 
	template : string; 
	variables: ITemplateVariables ; 
}
// models 
import  IParseMailTemplateDTO  from  '../dtos/IParseMailTemplateDTO' ;

export  default  interface  IMailTemplateProvider  { 
	parse ( data : IParseMailTemplateDTO ) : Promise < string > ; 
} 
// fakes 
import  IMailTemplateProvider  from  '../models/IMailTemplateProvider' ;

class  FakeMailTemplateProvider  implements  IMailTemplateProvider  { 
	public  async  parse ( { template } : IMailTemplateProvider ) : Promise < string >  { 
		return  template ; 
	} 
}

export  default  FakeMailTemplateProvider ;
  • Install the handlerbars;
// implementations 
import  handlebars  from  handlebars

import  IMailTemplateProvider  from  '../models/IMailTemplateProvider' ;

class  HandlebarsMailTemplateProvider  implements  IMailTemplateProvider  { 
	public  async  parse ( { template , variables } : IMailTemplateProvider ) : Promise < string >  { 
		const  parseTemplate  =  handlebars . compile ( template ) ;

		return  parseTemplate ( variables ) ; 
	} 
}

export  default  HandlebarsMailTemplateProvider ;
  • Create the dependency injection
import  {  container  }  from  'tsyringe' ; 
import  IMailTemplateProvider  from  './MailTemplateProvider/models/IMailTemplateProvider' ; 
import  HandlebarsMailTemplateProvider  from  './MailTemplateProvider/implementations/HandlebarsMailTemplateProvider' ;

container . registerSingleton < IMailTemplateProvider > ( 
	'MailTemplateProvider' , 
	HandlebarsMailTemplateProvider 
) ;

MailTemplateProvider is directly associated with MailProvider , as MailTemplateProvider would not even exist if there were no MailProvider, so it will not be passed as a dependency injection of the SendForgotPasswordEmail service .

  • I must create a DTO for MailProvider since now its interface will receive more than just to and body
// @shared / container / providers / MailProvider / dtos / ISendMailDTO 
import  IParseMailTemplateDTO  from  '@ shared / container / providers / MailTemplateProvider / dtos / IParseMailTemplateDTO' ;

interface  IMailContact  { 
	name : string ; 
	email : string ; 
}

export  default  interface  ISendMailDTO  { 
	to :IMailContact;
	from ?: IMailContact ; 
	subject: string ; 
	templateData: IParseMailTemplateDTO ; 
}
  • Now I must change both the fake and the implementations of MailProvider ;
// fakes 
import  IMailProvider  from  '../models/IMailProvider' ; 
import  ISendMailDTO  from  '../dtos/ISendMailDTO' ;

class  FakeMailProvider  implements  IMailProvider  { 
	private  messages : ISendMailDTO [ ] ;

	public  async  sendMail ( message : ISendMailDTO ) : Promise < void > { 
		this . messages . push ( message ) ; 
	} 
}

export  default  FakeMailProvider ;

Do the MailTemplateProvider dependency injection in MailProvider . One provider can depend on others, as MailTemplateProvider only exists if MailProvider also exists.

import  {  container  }  from  'tsyringe' ;

import  IStorageProvider  from  './StorageProvider/models/IStorageProvider' ; 
import  DiskStorageProvider  from  './StorageProvider/implementations/DiskStorageProvider' ;

import  IMailProvider  from  './MailProvider/models/IMailProvider' ; 
import  EtherealMailProvider  from  './MailProvider/implementations/EtherealMailProvider' ;

import  IMailTemplateProvider  from  './MailTemplateProvider/models/IMailTemplateProvider' ; 
import  HandlebarsMailTemplateProvider  from  './MailTemplateProvider/implementations/HandlebarsMailTemplateProvider' ;

container . registerSingleton < IStorageProvider > ( 
	'StorageProvider' , 
	DiskStorageProvider , 
) ;

container . registerSingleton < IMailTemplateProvider > ( 
	'MailTemplateProvider' , 
	HandlebarsMailTemplateProvider , 
) ;

container . registerInstance < IMailProvider > ( 
	'MailProvider' , 
	container . resolve ( EtherealMailProvider ) , 
) ;
  • Go to EtherealMailProvider and change the sendMail function, as was done in fake.
import  nodemailer ,  {  Transporter  }  from  'nodemailer' ; 
import  {  injectable ,  inject  }  from  'tsyringe' ;

import  IMailProvider  from  '../models/IMailProvider' ; 
import  ISendMailDTO  from  '../dtos/ISendMailDTO' ; 
import  IMailTemplateProvider  from  '@ shared / container / providers / MailTemplateProvider / models / IMailTemplateProvider' ;

@ injectable ( ) 
class  EtherealMailProvider  implements  IMailProvider  { 
	private  client : Transporter ;

	constructor ( 
		@ inject ( 'MailTemplateProvider' ) 
		private  mailTemplateProvider : IMailTemplateProvider , 
	)  { 
		nodemailer . createTestAccount ( ) . then ( account  =>  { 
			const  transporter  =  nodemailer . createTransport ( { 
				host : account . smtp . host , 
				port : account . smtp .port , 
				secure : account . smtp . secure , 
				auth : { 
					user : account . user , 
					pass : account . pass , 
				} , 
			} ) ;

			this . client  =  transporter ; 
		} ) ; 
	}

	public  async  sendMail ( { to , from , subject , templateData } : ISendMailDTO ) : Promise < void >  { 
		const  message  =  await  this . client . sendMail ( { 
			from : { 
				name : from ? . name  ||  'Team GoBarber' , 
				address : from ? . email ||  'team@gobarber.com.br' , 
			} , 
			to : { 
				name : to . name , 
				address : to . email , 
			} , 
			subject , 
			html : await  this . mailTemplateProvider . parse ( templateData ) , 
		} ) ;

		console . log ( 'Message sent:% s' ,  message . messageId ) ; 
		console . log ( 'Preview URL:% s' ,  nodemailer . getTestMessageUrl ( message ) ) ; 
	} 
}

export default  EtherealMailProvider ;

Execute forgot password email and reset password on insomnia routes

Template Engine

  1. Go to users, create the views folder and a file that will serve as a template for user emails.
<! - @ modules / users / views / forgot_password.hbs ->
 < style > 
.message-content { font-family : Arial , Helvetica , sans-serif ; max-width : 600 px ; font-size : 18 px ; line-height : 21 px ; 	}	

</ style >

< div  class = " message-content " >
	< p > Hello, {{ name }} </ p >
	< p > It looks like a password change for your account has been requested. </ p >
	< p > If it was you, then click the link below to choose a new password </ p >
	< p >
		< A  href = " {{ link }} " > Password Recovery </ a >
	</ p >
	< p > If it wasn't you, then discard that email </ p >
	< p >
		Thank you </ br >
		< strong  style = " color: ## FF9000 " > Team GoBarber </ strong >
	</ p >
</ div >
  1. Go to the IParseMailTemplate interface and edit the template attribute for file.
  2. Go to FakeMailTemplateProvider .
import  IMailTemplateProvider  from  '../models/IMailTemplateProvider' ;

class  FakeTemplateMailProvider  implements  IMailTemplateProvider  { 
	public  async  parse ( ) : Promise < string >  { 
		return  'Mail content' ; 
	} 
}

export  default  FakeTemplateMailProvider ;
  1. Go to HandlebarsMailTemplateProvider.ts
import  handlebars  from  'handlebars' ; 
import  fs  from  'fs' ;

import  IParseMailTemplateDTO  from  '../dtos/IParseMailTemplateDTO' ; 
import  IMailTemplateProvider  from  '../models/IMailTemplateProvider' ;

class  HandlebarsMailTemplateProvider  implements  IMailTemplateProvider  { 
	public  async  parse ( { 
		file , 
		variables , 
	} : IParseMailTemplateDTO ) : Promise < string >  { 
		const  templateFileContent  =  await  fs . promises . readFile ( file ,  { 
			encoding : 'utf-8' , 
		} ) ;

		const  parseTemplate  =  handlebars . compile ( templateFileContent ) ;

		return  parseTemplate ( variables ) ; 
	} 
}
  1. Go to SendForgotPasswordMailService.ts
import  path  from  'path' ;

const  forgotPasswordTemplate  =  path . resolve ( 
	__dirname , 
	'..' , 
	'views' , 
	'forgot_password.hbs' , 
) ;

await  this . mailProvider . sendMail ( { 
	to : { 
		name : user . name , 
		email : user . email , 
	} , 
	subject : '[GoBarber] Password Recovery' , 
	templateData : { 
		file : forgotPasswordTemplate , 
		variables : { 
			name : user . name , 
			link :`http: // localhost: 3000 / reset_password? token = $ { token } ` , 
		} , 
	} ,

Tests refactoring

  • Make all tests use the beforeEach (() => {}) syntax to make them less verbose.

Profile Update

Create the unit test very simple, as well as the service and gradually add the business rules.

Profile Update

Functional Requirements

  • The user must be able to update his name, email and password.

Non-Functional Requirements

Business rules

  • The user cannot change the email to another email already used by another user.
  • When changing the password, the user must inform the old one.
  • When recovering the password, the user must confirm the new password. (will be done later)
  1. Create UpdateProfileService.ts and UpdateProfileService.spec.ts .
// @modules /users/services/UpdateProfileService.spec.ts; 
import  AppError  from  '@ shared / errors / AppError' ;

import  FakeUsersRepository  from  '@ modules / users / repositories / fakes / FakeUsersRepository' ; 
import  FakeHashProvider  from  '../providers/HashProvider/fakes/FakeHashProvider' ; 
import  UpdateProfileService  from  './UpdateProfileService' ;

let  fakeUsersRepository : FakeUsersRepository ; 
let  fakeHashProvider : FakeHashProvider ; 
let  updateProfile : UpdateProfileService ;

describe ( 'UpdateProfile' ,  ( )  =>  { 
	beforeEach ( ( )  =>  { 
		fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		fakeHashProvider  =  new  FakeHashProvider ( ) ;

		updateProfile  =  new  UpdateProfileService ( 
			fakeUsersRepository , 
			fakeHashProvider , 
		) ; 
	} ) ;

	it ( 'should be able to update the profile' ,  async  ( )  =>  { 
		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test unit' , 
			email : 'testunit@example.com' , 
			password : '123456' , 
		} ) ;

		const  updateUser  =  await  updateProfile . execute ( { 
			user_id : user . id , 
			email : 'new-email' , 
			name : 'new-name' , 
		} ) ;

		expect ( updateUser . name ) . toBe ( 'new-name' ) ; 
		expect ( updateUser . email ) . toBe ( 'new-email' ) ; 
	} ) ;

} ) ;
// @modules /users/services/UpdateProfileService.ts

interface  IRequest { 
	user_id : string ; 
	name : string ; 
	email : string ; 
	old_password ?: string ; 
	password ?: string ; 
}

class  UpdateProfileService  {

	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		@ inject ( 'HashProvider' ) 
		private  hashProvider : IHashProvider , 
	) { }

	public  async  execute ( { 
		user_id , 
		name , 
		email , 
		old_password , 
		password , 
	} : IRequest ) : Promise < void >  { 
		 const  user  =  await  this . usersRepository . findById ( user_id ) ;

		 if ( ! user )  { 
			 throw  new  AppError ( 'User not found.' ) ; 
		 }

		 user . email  =  email 
		 user . name  =  name ;

		 return  this . userRepository . save ( user ) ; 
	} 
}
  1. Increase both the test and the Service so that the business rules are adequate.
// @modules /users/services/UpdateProfileService.spec.ts; 
import  AppError  from  '@ shared / errors / AppError' ;

import  FakeUsersRepository  from  '@ modules / users / repositories / fakes / FakeUsersRepository' ; 
import  FakeHashProvider  from  '../providers/HashProvider/fakes/FakeHashProvider' ; 
import  UpdateProfileService  from  './UpdateProfileService' ;

let  fakeUsersRepository : FakeUsersRepository ; 
let  fakeHashProvider : FakeHashProvider ; 
let  updateProfile : UpdateProfileService ;

describe ( 'UpdateProfile' ,  ( )  =>  { 
	beforeEach ( ( )  =>  { 
		fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		fakeHashProvider  =  new  FakeHashProvider ( ) ;

		updateProfile  =  new  UpdateProfileService ( 
			fakeUsersRepository , 
			fakeHashProvider , 
		) ; 
	} ) ;

	it ( 'should be able to update the profile' ,  async  ( )  =>  { 
		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test unit' , 
			email : 'testunit@example.com' , 
			password : '123456' , 
		} ) ;

		const  updateUser  =  await  updateProfile . execute ( { 
			user_id : user . id , 
			email : 'new-email' , 
			name : 'new-name' , 
		} ) ;

		expect ( updateUser . name ) . toBe ( 'new-name' ) ; 
		expect ( updateUser . email ) . toBe ( 'new-email' ) ; 
	} ) ;

	it ( 'should not be able to change the email using another user email' ,  async  ( )  =>  { 
		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test unit' , 
			email : 'testunit@example.com' , 
			password : '123456' , 
		} ) ;

		await  fakeUsersRepository . create ( { 
			name : 'Test unit' , 
			email : 'testunit2@example.com' , 
			password : '123456' , 
		} ) ;

		await  expect ( await  updateProfile . execute ( { 
			user_id : user . id , 
			email : 'testunit2@example.com' , 
			name : 'new-name' , 
		} ) ) . rejects . toBeInstanceOf ( AppError ) ; 
	} ) ;

	it ( 'should be able to update the password using old password' ,  async  ( )  =>  { 
		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test unit' , 
			email : 'testunit@example.com' , 
			password : ' 123456 ' , 
		} ) ;

		const  updateUser  =  await  updateProfile . execute ( { 
			user_id : user . id , 
			email : 'testunit2@example.com' , 
			name : 'new-name' , 
			old_password : '123456' , 
			password : '4444' , 
		} ) ;

		expect ( updateUser . password ) . toBe ( '4444' ) ; 
	} ) ;

	it ( 'should not be able to update the password using wrong old password' ,  async  ( )  =>  { 
		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test unit' , 
			email : 'testunit@example.com' , 
			password : '123456' , 
		} ) ;

		await  expect ( await  updateProfile . execute ( { 
			user_id : user . id , 
			email : 'testunit2@example.com' , 
			name : 'new-name' , 
			old_password : 'wrong-old-password' , 
			password : '4444' , 
		} ) ) . rejects . toBeInstanceOf ( AppError ) ; 
	} ) ;

		it ( 'should not be able to update the password without old password' ,  async  ( )  =>  { 
		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test unit' , 
			email : 'testunit@example.com' , 
			password : '123456' , 
		} ) ;

		await  expect ( await  updateProfile . execute ( { 
			user_id : user . id , 
			email : 'testunit2@example.com' , 
			name : 'new-name' , 
			password : '4444' , 
		} ) ) . rejects . toBeInstanceOf ( AppError ) ; 
	} ) ;

} ) ;
// @modules /users/services/UpdateProfileService.ts

interface  IRequest { 
	user_id : string ; 
	name : string ; 
	email : string ; 
	old_password ?: string ; 
	password ?: string ; 
}

class  UpdateProfileService  {

	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository ,

		@ inject ( 'HashProvider' ) 
		private  hashProvider : IHashProvider , 
	) { }

	public  async  execute ( { 
		user_id , 
		name , 
		email , 
		old_password , 
		password , 
	} : IRequest ) : Promise < void >  { 
		 const  user  =  await  this . usersRepository . findById ( user_id ) ;

		 if ( ! user )  { 
			 throw  new  AppError ( 'User not found.' ) ; 
		 }

		 const  checkEmailIsAlreadyUsed  =  await  this . usersRepository . findByEmail ( 
			 email 
			) ;

			if ( checkEmailIsAlreadyUsed   &&  ckeckEmailIsAlreadyUsed . id ! == user . id ) { 
				throw  new  AppError ( 'E-mail already used.' ) ; 
			}

		 user . email  =  email 
		 user . name  =  name ;

		 if  ( password  && ! old_password )  { 
			 throw  new  AppError ( 
				 'You need to inform the old password to set a new password' 
				) ; 
		 }

		 if  ( password  &&  old_password )  { 
			 const  checkPassword  =  await  this . hashProvider . compare ( 
				 old_password , 
				 user . password 
				) ;

				if  ( ! checkPassword )  { 
					throw  new  AppError ( 'Old password does not match.' ) ; 
				}

				user . password  =  await  this . hashProvider . generateHash ( password ) ; 
		 }

		 return  this . userRepository . save ( user ) ; 
	} 
}

Profile Routes and Controller

  1. Create ShowProfileService.ts and ShowProfileService.spec.ts
// ShowProfileService.ts 
interface  IRequest  { 
	user_id : string ; 
}

class  ShowProfileService  {

	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository 
	) { }

	public  async  execute ( { user_id } : IRequest ) : Promise < User > { 
		const  user  =  await  this . usersRepository . findById ( user_id ) ;

		if ( ! user ) { 
			throw  new  AppError ( 'User not found.' ) ; 
		}

		return  user ; 
	} 
}

export  default  ShowProfileService ;
// ShowProfileService.spec.ts 
describe ( 'ShowProfile' ,  ( )  =>  { 
	beforeEach ( ( )  =>  { 
		fakeUsersRepository  =  new  FakeUsersRepository ( ) ; 
		showProfile  =  new  ShowProfileService ( fakeUsersRepository ) ; 
	} ) .

	it ( 'should be able to show the profile' ,  async  ( )  =>  { 
		const  user  =  await  fakeUsersRepository . create ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' , 
		} ) ;

		const  profile  =  await  showProfile . execute ( {  user_id : user . id  } ) ;

		expect ( profile . name ) . toBe ( 'Test example' ) ; 
		expect ( profile . email ) . toBe ( 'test@example.com' ) ; 
	} ) ;

	it ( 'should be able to show the profile from a non-existing user' ,  async  ( )  =>  { 
		await  expect ( showProfile . execute ( {  user_id : user . id  } ) ) 
		. rejects 
		. toBeInstanceOf ( AppError ) ; 
	} ) ;

} )
  1. Create ProfileController.ts
class  ProfileController  { 
	public  async  show ( request : Request ,  response : Response ) : Promise < Response > { 
		const  { user_id }  =  request . user . id ;

		const  showProfile  =  container . resolve ( ShowProfileService ) ;

		const  user  =  await  showProfile . execute ( { user_id } ) ;

		return  response . json ( user ) ; 
	}

	public  async  update ( request : Request ,  response : Response ) : Promise < Response > { 
		const  { user_id }  =  request . user . id ;

		const  { email , password , old_password , name }  =  request . body ;

		const  updateProfile  =  container . resolve ( UpdateProfileService ) ;

		const  user  =  await  updateProfileService . execute ( { 
			user_id , 
			email , 
			password , 
			old_password ,
			name
		} ) ;

		return  response . json ( user ) ; 
	} 
}

export  default  ProfileController ;
  1. Create profile.routes.ts .

Users.routes.ts could be used, but it deals only with UsersController, which in turn deals with the methods of all users, not just the ones that are authenticated.

import  {  Router  }  from  'express' ; 
import  ProfileController  from  '../controller/ProfileController' ; 
import  ensureAuthenticated  from  '../middlewares/ensureAuthenticated' ;

const  profileRouter  =  Router ( ) ; 
const  profileController  =  new  ProfileController ( ) ; 
profileRouter . use ( ensureAuthenticated ) ;

profileRouter . get ( '/' ,  profileController . show ) ; 
profileRouter . put ( '/' ,  profileController . update ) ;

export  default  profileRouter ;
  1. Import the new route on the server
...
 app . use ( '/ profile' ,  profileRouter ) ;

List of Providers

  1. I will create a service to list all providers
// listProvidersService.ts

interface  IRequest  { 
	user_id ?: string ; 
}

class  ListProvidersService  {

	constructor ( 
		@ inject ( 'UsersRepository' ) 
		private  usersRepository : IUsersRepository , 
	)

	public  async  execute ( { user_id } : IRequest ) : Promise < User [ ] > { 
		const  users  =  await  this . usersRepository . findAllProviders ( { 
			except_user_id : user_id , 
		} ) ;

		return  users ; 
	} 
}
  1. Create this new method in the repository interface and a DTO for the findAllProviders method.

It will be more descriptive when I use the service and pass the user_id I’m receiving from the route as a parameter to the expect_user_id attribute .

export  default  interface  IUsersRepository  { 
	findAllProviders ( data : IFindAllProviders ) : Promise < User [ ] > ; 
	findById ( id : string ) : Promise < User | undefined > ; 
	findByEmail ( email : string ) : Promise < User | undefined > ; 
	create ( data: ICreateUserDTO ) : Promise < User > ; 
	save ( user : User ) : Promise < User > ; 
}
export  default  interface  IFindAllProviders  { 
	except_user_id ?: string ; 
}
  1. Implement the new method in FakeUsersRepository
class  FakeUsersRepository  implements  IUsersRepository  {
	...

	public  async  findAllProviders ( except_user_id ?: string ) : Promise < User [ ] > { 
		let  { users }  =  this ;

		if  ( except_user_id )  { 
			users  =  this . users . filter ( user  =>  user . id ! == except_user_id ) ; 
		}

		return  users ; 
	} 
}
  1. Implement the new method in the TypeORM repository
class  UsersRepository  implements  IUsersRepository  {
	...

	public  async  findAllProviders ( except_user_id ?: string ) : Promise < User [ ] >  { 
		let  users : User [ ] ;

		if  ( except_user_id )  { 
			users  =  await  this . ormRepository . find ( {  
				where : { 
					id : Not ( except_user_id ) , 
				}  
			} ) ; 
		}  else  { 
			users  =  await  this . ormRepostiry . find ( ) ; 
		}

		return  users ; 
	} 
}
  1. Create the test for ListProvidersService.ts
describe ( 'ListProviders' ,  ( )  =>  { 
	beforeEach ( ( )  =>  { ... } ) ;

	it ( 'should be able to list all providers' ,  async  ( )  =>  { 
		const  user1  =  await  fakeUsersRepository . create ( { 
			name : 'Test example' , 
			email : 'test@example.com' , 
			password : '123456' , 
		} ) ;

		const  user2  =  await  fakeUsersRepository . create ( { 
			name : 'Test 2' , 
			email : 'test2@example.com' , 
			password : '123456' , 
		} ) ;

		const  loggedUser  =  await  fakeUsersRepository . create ( { 
			name : 'John Doe' , 
			email : 'john@doe.com' , 
			password : '123456' , 
		} ) ;

		const  providers  =  await  this . listProviders . execute ( { 
			user_id : loggedUser . id , 
		} ) ;

		expect ( providers ) . toBe ( [ 
			user1 , 
			user2 , 
		] ) ; 
	} ) ; 
} )

In simple cases like listing or something that does not involve a business rule, TDD does not necessarily have to be done before functionality !!

  1. Create ProvidersController.ts
class  ProvidersControllers  { 
	public  async  index ( request : Request ,  response : Response ) : Promise < Response > { 
		const  user_id  =  request . user . id ;

		const  listProviders  =  container . resolve ( ListProvidersService ) ;

		const  providers  =  await  listProviders . execute ( { user_id } ) ;

		return  response . json ( providers ) ; 
	} 
}
  1. Creates ProvidersRouter
const  providersRouter  =  Router ( ) ; 
const  providersController  =  new  ProvidersController ( ) ;

providersRouter . use ( ensureAuthenticated ) ; 
providersRouter . get ( '/' ,  providersControllers . index ) ;

export  default  providersRouter ;
  1. Go on index.ts the @ shared / infra / http / routes / index.ts and make the import of providersRouter

Filtering schedules by month

  1. I will create a new service for this, called ListProviderMonthAvailability.ts
interface  IRequest  {  
	provider_id : string ; 
	year : number ; 
	month : number ; 
}

class  ListProviderMonthAvailability  {

	constructor ( 
		@ inject ( 'AppointmentsRepository' ) 
		private  appointmentsRepository : IAppointmentsRepository 
	)

	public  async  execute ( { provider_id , year , month } : IRequest ) : Promise < void > { 
		const  appointments  =  await  this . appointmentsRepository . find ( ???? ) 
	} 
}

export  default  ListProviderMonthAvailability ;

There is still no method in the Repository interface to list all the schedules of a provider in a specific month

  1. Create the findAllInMonthFromProvider method and a data
import  {  getMonth ,  getYear  }  from  'date-fns' ; 

export  default  interface  IFindAllInMonthFromProviderDTO  { 
	provider_id : string; 
	year: number ; 
	month: number ; 
}
class  FakeAppointmentsRepository  implements  IAppointmentsRepository  {
	...
	public  async  findAllInMonthFromProvider ( {  
		provider_id , 
		year ,
		month
	} : IFindAllInMonthFromProviderDTO ) : Promise < Appointment [ ] > { 
		const  appointments  =  this . appointments . filter (  appointment  =>  
			appointment . provider_id  ===  provider_id  && 
			getYear ( appointment . date )  ===  year  && 
			getMonth ( appointment . date )  +  1  ===  , ) month

		return  appointments ; 
	} 
}

getMonth (appointment.date) + 1 because getMonth counts the months starting from 0. That is, 0 = January, 1 = February …

  1. Go create the new interface method in the TypeORM repository.
class  AppointmentsRepository  implements  IAppointmentsRepository  {
	...
	public  async  findAllInMonthFromProvider ( {  
		provider_id , 
		year ,
		month
	} : IFindAllInMonthFromProviderDTO ) : Promise < Appointments [ ] > {

		const  parsedMonth  =  month . toString ( ) . padStart ( 2 ,  '0' ) ;

		const  appointments  =  await  this . ormRepository . find ( { 
			where : { 
				provider_id , 
				date : Raw (  dateFieldName  =>  
					`to_char ( $ { dateFieldName } , 'MM-YYYY') = ' $ { parsedMonth } - $ { year } '` 
				) 
			} 
		} )

		return  appointments ; 
	} 
}

Raw is a way of writing query sql in typeorm. This will make the ORM not even try to convert / interpret that query. You can receive a function or just the query. When a function is passed and I want to receive the name of that real column (because TypeORM changes the name of the columns and puts a nickname / alias), I passed the parameter dateFielName to_char (valor_que_quero_converter, forma_que_quero) converts a value to string. Just look in the documentation. When to_char converts to MM, it adds 0 in front of the value if it has only one decimal place. Example: 01 = January, 10 = October. So, I should take the month, convert it to a string and if it is not 2 in size, I would add ‘0’ at the beginning of it, using the padStart (2, ‘0’)

  1. Create the test and update the service
describe ( 'ListProviderMonthAvailability' ,  ( )  =>  { 
	beforeEach ( ( )  =>  { 
		fakeAppointmentsRepository  =  new  FakeAppointmentsRepository ( ) ; 
		listProviderMonthAvailability  =  new  ListProviderMonthAvailability ( 
			fakeAppointmentsRepository , 
		) ; 
	} ) ;

	it ( 'should be able to list provider availability in month' ,  ( )  =>  { 
		await  fakeAppointmentsRepository . create ( { 
			provider_id : 'provider-id' , 
			date : new  Date ( 2020 ,  8 ,  15 ,  17 ,  0 ,  0 )) , 
		} ) ; 
		await  fakeAppointmentsRepository . create ( { 
			provider_id : 'provider-id',
			date : new  Date ( 2020 ,  8 ,  15 ,  10 ,  0 ,  0 ) , 
		} ) ; 
		await  fakeAppointmentsRepository . create ( { 
			provider_id : 'provider-id' , 
			date : new  Date ( 2020 ,  8 ,  16 ,  9 ,  0 ,  0 ) , 
		} ) ;

		const  availability  =  await  listProviderMonthAvailability . execute ( { 
			provider_id : 'provider-id' , 
			month : 9 , 
			year : 2020 , 
		} ) ;

		expect ( availability ) . toEqual ( expect . arrayContaining ( [ 
			{ 
				day : 14 , 
				available : true ,  
			} , 
			{ 
				day : 15 , 
				available : false ,  
			} , 
			{ 
				day : 16 , 
				available : false ,  
			} , 
			{ 
				day : 17 , 
				available : true ,  
			} , 
		] ) ) 
	} ) 
} )
  1. Refactor Service
interface  IRequest  {  
	provider_id : string ; 
	year : number ; 
	month : number ; 
}

class  ListProviderMonthAvailability  {

	constructor ( 
		@ inject ( 'AppointmentsRepository' ) 
		private  appointmentsRepository : IAppointmentsRepository 
	)

	public  async  execute ( { provider_id , year , month } : IRequest ) : Promise < void > { 
		const  appointments  =  await  this . appointmentsRepository . findAllInMonthFromProvider ( { 
			provider_id , 
			year , 
			month , 
		} ) ;

		console . log ( appointments ) ;

		return  [  {  day : 1 ,  available : false  } ] ; 
	} 
}

export  default  ListProviderMonthAvailability ;

Listing available days

  1. Go to ListProviderMonthAvailabilityService.ts
import  {  getDate ,  getDaysInMonth  }  from  'date-fns' ;

interface  IRequest  {  
	provider_id : string ; 
	year : number ; 
	month : number ; 
}

class  ListProviderMonthAvailability  {

	constructor ( 
		@ inject ( 'AppointmentsRepository' ) 
		private  appointmentsRepository : IAppointmentsRepository 
	)

	public  async  execute ( { provider_id , year , month } : IRequest ) : Promise < void > { 
		const  appointments  =  await  this . appointmentsRepository . findAllInMonthFromProvider ( { 
			provider_id , 
			year , 
			month , 
		} ) ;

		const  numberOfDaysInMonth  =  getDaysInMonth ( new  Date ( year ,  month  -  1 ) ) ;

		const  eachDayArray  =  Array . from ( 
			{  length : numberOfDaysInMonth  } , 
			( _ ,  index )  =>  index  +  1 ,   
		) ;

		const  availability  =  eachDayArray . map ( day  =>  { 
			const  appointmentsInDay  =  appointments . filter ( appointment  =>  
				getDay ( appointment . date )  ===  day 
			) ;

			return  { 
				day , 
				available : appointmentsInDay . length  <  10 , 
			} ; 			
		} )

		return  availability ; 
	} 
}

export  default  ListProviderMonthAvailability ;

Array.from () creates an array from some options. As a first parameter, it receives an object where the length is the attribute. The second parameter is a function that contains (value, index).

  1. Go to the tests and add appointments from 8 am to 5 pm on the same day.

Listing available times

  1. Create DTO for the new interface method
export  default  interface  IFindAllInDayFromProviderDTO  { 
	provider_id : string; 
	year: number ; 
	month: number ; 
	day: number ;

}
  1. Create the method in the findAllInDayFromProvider interface
export  default  interface  IAppointmentsRepository  {
	...
	findALlInDayFromProvider ( data : IFindAllInDayFromProviderDTO ) : Promise < Appointment [ ] > ; 
}
  1. Create these methods in the fake and TypeORM repository
class  FakeAppointmentsRepository { 
public  async  findAllInDayFromProvider ( {  
	provider_id , 
	year , 
	month ,
	day
 } : IFindAllInDayFromProviderDTO ) : Promise < Appointment [ ] >  { 
	 const  appointmentsInDay  =  this . appointments . filter ( appointment  => 
	 	appointment . provider_id  ===  provider_id  && 
	 	getDate ( appointment . date )  ===  day  && 
	 	getYear ( appointment . date )  ===  year  && 
	 	( getMonthappointment . date )  +  1  ===  month  && , 
	 )

	 return  appointmentsInDay ; 
 } 
} 
export  default  FakeAppointmentsRepository ;
class  AppointmentsRepository  { 
	public  async  findAllInDayFromProvider ( {  
	provider_id , 
	year , 
	month ,
	day
 } : IFindAllInDayFromProviderDTO ) : Promise < Appointment [ ] >  { 
	 const  parsedDay  =  day . toString ( ) . padStart ( 2 ,  '0' ) ; 
	 const  parsedMonth  =  month . toString ( ) . padStart ( 2 ,  '0' ) ;

	 const  appointments  =  await  this . ormRepository . find ( { 
		 where : { provider_id , 
		 date : Raw ( dateFieldName  => 
		 `to_char ( $ { dateFieldName } , 'DD-MM-YYYY') = ' $ { parsedDay } - $ { parsedMonth } - $ { year } ' ' 
		 ) , 
		} , 
	 } ) ;

	return  appointments ; 
 } 
}
  1. Create the ListProviderDayAvailabilityService.ts and ListProviderDayAvailabilityService.spec.ts service
class  ListProviderDayAvailabilityService  { 
	public  async  execute ( {  
		provider_id , 
		year , 
		month , 
		day , 
	} : IFindAllInDayFromProvider ) : Promise < Appointemnt [ ] > { 
		const  appointmentsInDay  =  await  this . appointmentsRepository . findAllInDayFromProvider ( { 
			provider_id , 
			year , 
			month ,
			day , 
		} ) ;

		const  hourStart  =  8 ;

		const  eachHour  =  Array . from ( 
			{  length : 10  } , 
			( _ ,  index )  =>  index  +  8 ) , 
		) ;

		const  availability  =  eachHour ( hour  =>  { 
			const  hasAppointmentInHour  =  appointments . find ( appointment  => 
				getHours ( new  Date ( appointment . date ) )  ===  hour ; 
			) ;

			return  { 
				hour , 
				available : hasAppoinmentInHour , 
			} ; 
		} ) ;

		return  availability ; 
	} 
}

export  default  ListProviderDayAvailabilityService ;
describe ( 'ListProviderDayAvailability' ,  ( )  =>  { 
	beforeEach ( '' ,  ( )  =>) ;

	it ( 'should be able to list day availability of a provider' ,  ( )  =>  { 
		await  fakeAppointmentsRepository . create ( { 
			provider_id : 'provider-id' , 
			date : new  Date ( 2020 ,  7 ,  16 ,  9 ,  0 ,  0 ) , 
		} ) ;

		await  fakeAppointmentsRepository . create ( { 
			provider_id : 'provider-id' , 
			date : new  Date ( 2020 ,  7 ,  16 ,  10 ,  0 ,  0 ) , 
		} ) ;

		const  availability  =  await  listProviderDayAvailability . execute ( { 
			provider_id : 'provider-id' , 
			year : 2020 , 
			month : 8 , 
			day : 16 , 
		} ) ;

		expect ( availability ) . toEqual ( expect . arrayContaining ( [ { 
			{ 
				hour : 8 , 
				available : true , 
			} , 
			{ 
				hour : 9 , 
				available : false , 
			} , 
			{ 
				hour : 10 , 
				available : false , 
			} , 
			{ 
				hour : 11 , 
				available : true , 
			} , 
		} ] ) 
		) ;

	} ) ; 
} )

Deleting old times

When working to get the current time, use the new Date (Date.now ()) , as it is easier to apply a mock (as used in the test password reset) and change the date as necessary.

  1. Refactor the ListProviderDayAvailabilityService so that it is possible to show if a day is available only if the date that the schedule will be scheduled is after the date that the schedule is being made .
class  ListProviderDayAvailabilityService  { 
	public  async  execute ( {  
		provider_id , 
		year , 
		month , 
		day , 
	} : IFindAllInDayFromProvider ) : Promise < Appointemnt [ ] > { 
		const  appointmentsInDay  =  await  this . appointmentsRepository . findAllInDayFromProvider ( { 
			provider_id , 
			year , 
			month,
			day , 
		} ) ;

		const  hourStart  =  8 ;

		const  eachHour  =  Array . from ( 
			{  length : 10  } , 
			( _ ,  index )  =>  index  +  8 ) , 
		) ;

		const  currentDate  =  new  Date ( Date . now ( ) ) ;

		const  availability  =  eachHour ( hour  =>  { 
			const  hasAppointmentInHour  =  appointments . find ( appointment  => 
				getHours ( new  Date ( appointment . date ) )  ===  hour ; 
			) ;

			const  compareDate  =  new  Date ( year ,  month  -  1 ,  day ,  hour ) ;

			return  { 
				hour , 
				available : hasAppoinmentInHour  &&  isAfter ( compareDate ,  currentDate ) , 
			} ; 
		} ) ;

		return  availability ; 
	} 
}

export  default  ListProviderDayAvailabilityService ;
  1. Refactor the test ListProviderDayAvailabilityService.spec.ts
it ( 'should be able to list the day availability of a provider' ,  async  ( )  =>  { 
		await  fakeAppointmentsRepository . create ( { 
			provider_id : 'provider-id' , 
			date : new  Date ( 2020 ,  7 ,  16 ,  9 ,  0 ,  0 ) , 
		} ) ;

		await  fakeAppointmentsRepository . create ( { 
			provider_id : 'provider-id' , 
			date : new  Date ( 2020 ,  7 ,  16 ,  11 ,  0 ,  0 ) , 
		} ) ;

		jest . spyOn ( Date ,  'now' ) . mockImplementationOnce ( ( )  =>  { 
			return  new  Date ( 2020 ,  7 ,  16 ,  10 ) . getTime ( ) ; 
		} )

		const  availability  =  await  listProviderDayAvailability . execute ( { 
			provider_id : 'provider-id' , 
			month : 8 , 
			year : 2020 , 
			day : 16 , 
		} ) ;

		expect ( availability ) . toEqual ( 
			expect . arrayContaining ( [ 
				{ 
					hour : 8 , 
					available : false , 
				} , 
				{ 
					hour : 9 , 
					available : false , 
				} , 
				{ 
					hour : 10 , 
					available : false , 
				} , 
				{ 
					hour : 11 , 
					available : false, 
				} , 
				{ 
					hour : 16 , 
					available : true , 
				} , 
				{ 
					hour : 17 , 
					available : true , 
				} , 
			] ) , 
		) ; 
	} ) ;

Schedule Creation

It must be refactored, because when creating a schedule, the user is able to make an appointment with himself.

  1. Create migration to add a new column in the bank and create a foreign key.
$ yarn typeorm migration: create -n AddUserIdToAppointments
import  { 
	MigrationInterface , 
	QueryRunner , 
	TableColumn , 
	TableForeignKey , 
}  from  'typeorm' ;

export  default  class  AddUserIdToAppointments 
	implements  MigrationInterface  { 
	public  async  up ( queryRunner : QueryRunner ) : Promise < void >  { 
		await  queryRunner . addColumn ( 'appointments' , 
		new  TableColumn ( { 
			name : 'user_id' , 
			type : 'uuid' , 
			isNullable : true , 
		} ) , 
		);

		await  queryRunner . addForeignKey ( 'appointments' , 
			new  TableForeignKey ( { 
				name : 'ApppointmentUser' , 
				columnNames : [ 'user_id' ] , 
				referencedColumnNames : [ 'id' ] , 
				referencedTableName : 'users' , 
				onDelete : 'SET NULL' , 
				onUpdate : 'CASCADE' , 
			} ) , 
		) ; 
	}

	public  async  down ( queryRunner : QueryRunner ) : Promise < void >  { 
		await  queryRunner . dropForeignKey ( 'appointments' ,  'AppointmentUser' ) ;

		await  queryRunner . dropColumn ( 'appointments' ,  'user_id' ) ; 
	} 
}
  1. Go to the Appointments entity and map this new column
import  { 
	Entity , 
	Column , 
	PrimaryGeneratedColumn , 
	CreateDateColumn , 
	UpdateDateColumn , 
	ManyToOne , 
	JoinColumn , 
}  from  'typeorm' ; 
import  User  from  '@ modules / users / infra / typeorm / entities / User' ;

@ Entity ( 'appointments' ) 
class  Appointment  { 
	@ PrimaryGeneratedColumn ( 'uuid' ) 
	id : string ;

	@ Column ( ) 
	provider_id : string ;

	@ ManyToOne ( ( )  =>  User ) 
	@ JoinColumn ( {  name : 'provider_id'  } ) 
	provider : User ;

	// map the new column created in the user_id database 
	// will be converted to a column in the database 
	@ Column ( ) 
	user_id : string ;

	// relationship that exists only here in javascript, but not in the bank 
	// multiple schedules for a user 
	@ ManyToOne ( ( )  =>  User )

	// which column in this table makes the relationship 
	@ JoinColumn ( {  name : 'user_id'  } ) 
	user : User ;

	@ Column ( 'timestamp with time zone' ) 
	date : Date ;

	@ CreateDateColumn ( ) 
	created_at : Date ;

	@ UpdateDateColumn ( ) 
	updated_at : Date ; 
}

export  default  Appointment ;
  1. Refactor the repository interface
  2. Refactor the fake and typeorm repository
  3. Go to the controller of AppointmentController.create and get user_id from request.user.id so that it is possible to pass it as a parameter for creating a new schedule.
  4. Update the tests to receive this new parameter.

Scheduling rules

  1. Create new tests:
  • should not be able to create an appointment in past date
 jest 
 	. spyOn ( Date ,  'now' ) 
 	. mockImplementationOnce ( ( )  =>  new  Date ( 2020 ,  7 ,  15 ,  10 ) . getTime ( ) ) ;

 await  expect ( 
 	createAppointment . execute ( { 
 		provider_id : 'provider-id' , 
 		user_id : 'user-id' , 
 		date : new  Date ( 2020 ,  7 ,  15 ,  9 ) , 
 	} ) , 
 ) . rejects . toBeInstanceOf ( AppError ) ;
  • should not be able to create an appointment with same provider as user
 jest 
 	. spyOn ( Date ,  'now' ) 
 	. mockImplementationOnce ( ( )  =>  new  Date ( 2020 ,  7 ,  15 ,  10 ) . getTime ( ) ) ;

 await  expect ( 
 	createAppointment . execute ( { 
 		provider_id : 'provider-id' , 
 		user_id : 'provider-id' , 
 		date : new  Date ( 2020 ,  7 ,  15 ,  11 ) , 
 	} ) , 
 ) . rejects . toBeInstanceOf ( AppError ) ;
  • should not be able to create an appointment before 8am and after 5pm
 jest 
 	. spyOn ( Date ,  'now' ) 
 	. mockImplementationOnce ( ( )  =>  new  Date ( 2020 ,  7 ,  15 ,  10 ) . getTime ( ) ) ;

 await  expect ( 
 	createAppointment . execute ( { 
 		provider_id : 'provider-id' , 
 		user_id : 'user-id' , 
 		date : new  Date ( 2020 ,  7 ,  15 ,  7 ) , 
 	} ) , 
 ) . rejects . toBeInstanceOf ( AppError ) ; 
 await  expect ( 
 	createAppointment . execute ( {
 		provider_id : 'provider-id' , 
 		user_id : 'user-id' , 
 		date : new  Date ( 2020 ,  7 ,  15 ,  18 ) , 
 	} ) , 
 ) . rejects . toBeInstanceOf ( AppError ) ;
  1. }); ``
  2. Adjust CreateAppointmentService for these tests
import  {  startOfHour ,  isBefore ,  getHours  }  from  'date-fns' ; 
import  {  injectable ,  inject  }  from  'tsyringe' ;

import  Appointment  from  '@ modules / appointments / infra / typeorm / entities / Appointment' ; 
import  AppError  from  '@ shared / errors / AppError' ; 
import  IAppointmentsRepository  from  '../repositories/IAppointmentsRepository' ;

interface  IRequest  { 
	provider_id : string ; 
	user_id : string ; 
	date : Date ; 
}

@ injectable ( ) 
class  CreateAppointmentService  { 
	constructor ( 
		@ inject ( 'AppointmentsRepository' ) 
		private  appointmentsRepository : IAppointmentsRepository , 
	)  { }

	public  async  execute ( { 
		provider_id , 
		date , 
		user_id , 
	} : IRequest ) : Promise < Appointment >  { 
		const  appointmentDate  =  startOfHour ( date ) ;

		if  ( isBefore ( appointmentDate ,  Date . now ( ) ) )  { 
			throw  new  AppError ( "You can't create an appointment on a past date." ) ; 
		}

		if ( provider_id  ===  user_id )  { 
			throw  new  AppError ( "You can't create an appointment with yourself." ) ; 
		}

		if  ( getHours ( appointmentDate )  <  8  ||  getHours ( appointmentDate )  >  17  )  { 
			throw  new  AppError ( 'You can only create an appointment between 8am and 5pm.' ) ; 
		}

		const  findAppointmentInSameDate  =  await  this . appointmentsRepository . findByDate ( 
			appointmentDate , 
		) ;

		if  ( findAppointmentInSameDate )  { 
			throw  new  AppError ( 'This Appointment is already booked!' ) ; 
		}

		const  appointment  =  await  this . appointmentsRepository . create ( { 
			provider_id , 
			user_id , 
			date : appointmentDate , 
		} ) ;

		return  appointment ; 
	} 
}

export  default  CreateAppointmentService ;

Routes and Controllers

Who should finish if the business rules are working are the tests

  1. Create ProviderDayAvailabilityController.rs , ProviderMonthAvailabilityController.ts
import  {  Request ,  Response  }  from  'express' ; 
import  {  container  }  from  'tsyringe' ;

import  ListProviderDayAvailabilityService  from  '@ modules / appointments / services / ListProviderDayAvailabilityService' ;

class  ProviderDayAvailabilityController  { 
	public  async  index ( request : Request ,  response : Response ) : Promise < Response >  { 
		const  { provider_id }  =  request . params ; 
		const  { year , month , day }  =  request . body ;

		const  listProviderDayAvailability  =  container . resolve ( ListProviderDayAvailabilityService ) ;

		const  availability  =  await  listProviderDayAvailabilityService . execute ( { 
			provider_id , 
			year , 
			month , 
			day , 
		} ) ;

		return  response . json ( availability ) ; 
	} 
}

export  default  ProviderDayAvailabilityController ;
  1. Create the route at provider.routes.ts.
import  {  Router  }  from  'express' ;

import  ensureAuthenticated  from  '@ modules / users / infra / http / middlewares / ensureAuthenticated' ; 
import  ProvidersController  from  '../controllers/ProvidersController' ; 
import  ProviderDayAvailabilityController  from  '../controllers/ProviderDayAvailabilityController' ; 
import  ProviderMonthAvailabilityController  from  '../controllers/ProviderMonthAvailabilityController' ;

const  providersRouter  =  Router ( ) ; 
const  providersController  =  new  ProvidersController ( ) ; 
const  providersDayAvailabilityController  =  new  ProviderDayAvailabilityController ( ) ; 
const  providersMonthAvailabilityController  =  new  ProviderMonthAvailabilityController ( ) ;

providersRouter . use ( ensureAuthenticated ) ;

providersRouter . get ( '/' ,  providersController . index ) ; 
providersRouter . get ( 
	'/: provider_id / day-availability' , 
	providersDayAvailabilityController . index , 
) ; 
providersRouter . get ( 
	'/: provider_id / month-availability' , 
	providersMonthAvailabilityController . index , 
) ;

export  default  providersRouter ;
  1. Testing on insomnia

Provider’s Agenda

  1. Create the service for listing the schedules of a provider ListProviderAppointmentsService.ts and the ListProviderAppointmentsService.spect.ts tests .
class  ListProviderAppointmentService  {

	constructor ( 
		@ inject ( 'AppointmentsRepository' ) 
		private  appointmentsRepository : IAppointmentsRepository , 
	) { }

	public  async  execute ( { 
		provider_id , 
		year , 
		month , 
		day , 
	} : IRequest ) : Promise < Appointment [ ] > { 
		const  appointments  =  await  this . appointmentsRepository . findAllInDayFromProvider ( { 
			provider_id , 
			year , 
			month ,
			day
		} ) ;

		return  appointments ; 
	} 
}

export  default  ListProviderAppointmentsService ;
describe ( 'ListProviderAppointments' ,  ( )  =>  { 
	beforeEach ( ( ) => { } ) ;

	it ( 'should be able to list the appointments on a specific day' ,  ( )  =>  { 
		const  appointment1  =  await  fakeAppointmentsRepository . create ( { 
			provider_id : 'provider-id' , 
			user_id : 'user-id' , 
			date : new  Date ( 2020 ,  8 ,  18 ,  10 ) , 
		} ) ; 
		const  appointment2  =  await fakeAppointmentsRepository . create ( { 
			provider_id : 'provider-id' , 
			user_id : 'user-id' , 
			date : new  Date ( 2020 ,  8 ,  18 ,  11 ) , 
		} ) ; 
		const  appointment3  =  await  fakeAppointmentsRepository . create ( { 
			provider_id : 'provider-id' , 
			user_id : 'user-id' , 
			date :new  Date ( 2020 ,  8 ,  18 ,  12 ) , 
		} ) ;

		const  appointments  =  await  listProviderAppointments . execute ( { 
			provider_id : 'provider-id' , 
			year : 2020 , 
			month : 9 , 
			day : 18 , 
		} ) ;

		expect ( appointments ) . toEqual ( [ 
			appointments1 , 
			appointments2 , 
			appointments3 , 
		] ) ; 
	} ) ; 
} )
  1. Create ProviderAppointmentsController.ts
class  ProviderAppointmentsController  { 
	public  async  index ( request : Request ,  response : Response ) : Promise < Response > { 
		const  provider_id  =  request . user . id ; 
		const  { year , month , day }  =  request . body ;

		const  listProviderAppointments  =  container . resolve ( 
			ListProviderAppointmentsService 
		) ;

		const  appointments  =  await  listProviderAppointments . execute ( { 
			provider_id , 
			year , 
			month , 
			day , 
		} ) ;

		return  response . json ( appointments ) ; 
	} 
}

export  default  ProviderAppointmentsController ;
  1. Create on appointments.routes.ts
import  {  Router  }  from  'express' ;

import  ensureAuthenticated  from  '@ modules / users / infra / http / middlewares / ensureAuthenticated' ; 
import  AppointmentsController  from  '../controllers/AppointmentsController' ; 
import  ProviderAppointmentsController  from  '../controllers/ProviderAppointmentsController' ;

const  appointmentsRouter  =  Router ( ) ; 
const  appointmentsController  =  new  AppointmentsController ( ) ; 
const  providerAppointmentsController  =  new  ProviderAppointmentsController ( ) ;

appointmentsRouter . use ( '/' ,  ensureAuthenticated ) ;

appointmentsRouter . post ( '/' ,  appointmentsController . create ) ; 
appointmentsRouter . get ( '/ me' ,  providerAppointmentsController . index ) ;

export  default  appointmentsRouter ;

MongoDB

Why and when to use MongoDB?

  • A relational bank the developer has more control because it uses tables, relationships, migrations (versioning control) and the like.
  • There is no migration, so if I want to make a change to previous records, I will have to take another approach, something manual.
  • The MongoDB is used when we have a wide range of data (data coming in and being updated) and few relationships between these data and when that data does not have much responsibility in the application . Despite this, I can use complex relationships.

For example, in Rocketseat PostgreSQL is used to store data about:

  • Students
  • videos
  • Courses … And there is the Student’s progress:
  • Student watched 10% of video X
  • Student watched 40% of the video Y This is done in real time and it happens a lot: the annotation of the progress I make in the video

That is, it is used when I want to:

  • Generate data for reports
  • Use data in some API
  • Notifications

For example, in the case of notifications, I don’t need to keep a topic’s ID, I can simply store the topic’s name. If I need to relate something to the topic, just pass the name of the topic instead of a key or something.

When using MongoDB, the data must be as little related as possible to the Entities

We must save the raw data

I will also use Redis for temporary information such as Cache, Queues …

Installing MongoDB

  • Download from the website if you are not using Docker.
  • If using, execute the command:
$ docker run --name mongodb -p 27017: 27017 -d -t mongo

Graphical Interface for MongoDB

  • MongoDB Compass Community

Connect with bank

  • Go to Compass and pass the address
mongodb: // localhost: 27017

Notification Structure

  1. Create connection to MongoDB using TypeORM
[
	{
		" name " : " default " ,
		 " type " : " postgres " ,
		 " host " : " localhost " ,
		 " port " : 5434 ,
		 " username " : " postgres " ,
		 " password " : " docker " ,
		 " database " :" gostack_gobarber ",
		 " entities " : [
			 " ./src/modules/**/infra/typeorm/entities/*.ts "
		],
		" migrations " : [
			 " ./src/shared/infra/typeorm/migrations/*.ts "
		],
		" cli " : {
			 " migrationsDir " : " ./src/shared/infra/typeorm/migrations "
		}
	},
	{
		" name " : " mongo " ,
		 " type " : " mongodb " ,
		 " host " : " localhost " ,
		 " port " : 27017 ,
		 " database " : " gobarber " ,
		 " useUnifiedTopology " : true ,
		 " entities " : [
			 " ./src/modules/**/infra/typeorm/schemas/*.ts "
		]
	}

]
  1. Install mongodb
$ yarn add mongodb
  1. Create the notifications domain, as there may come a time when I may send notifications from several modules
  • Create domain folder notifications
  • Create folders within it: repositories, services, infra, dtos (as is done in other domains).
  1. Create the Notification entity , which in the Mongo is called a schema
import  {  
	ObjectID , 
	ObjectIdColumn , 
	Column , 
	Entity , 
	CreateDateColumn , 
	UpdateDateColumn , 
}  from  'typeorm' ;

@ Entity ( 'notifications' ) 
class  Notification  {

	@ ObjectIdColumn ( ) 
	id : ObjectID ;

	@ Column ( ) 
	content : string ;

	@ Column ( 'uuid' ) 
	recipient_id : string ;

	@ Column ( {  default : false  } ) 
	read : boolean ;

	@ CreateDateColumn ( ) 
	created_at : Date ;

	@ UpdateDateColumn ( ) 
	updated_at : Date ; 
}

The column read value is defaulted, as we don’t have migrations to do this when using MongoDB.

  1. Create the notifications repository interface and its DTO
export  default  interface  ICreateNotificationDTO  { 
	content : string; 
	recipient_id: string ; 
}
import  Notification  from  '../infra/typeorm/schemas/Notification' ; 
import  ICreateNotificationDTO  from  '../dtos/ICreateNotificationDTO' ;

export  default  interface  INotificationsRepository  { 
	create ( data : ICreateNotificationDTO ) : Promise < Notification > ; 
}
  1. Creating the typeorm repository
import  {  MongoRepository ,  getMongoRepository  }  from  'typeorm' ;

import  INotificationsRepository  from  '@ modules / notifications / repositories / INotificationsRepository' ; 
import  ICreateNotificationDTO  from  '@ modules / notifications / dtos / ICreateNotificationDTO' ; 
import  Notification  from  '@ modules / notifications / infra / typeorm / schemas / Notification' ;

class  NotificationsRepository  implements  INotificationsRepository  { 
	private  ormRepository : MongoRepository < Notification >

	constructor ( ) { 
		this . ormRepository  =  getMongoRepository ( Notification ,  'mongo' ) ; 
	}

	public  async  create ( { 
		content , 
		recipient_id , 
	} : ICreateNotificationDTO ) : Promise < Notification >  { 
		const  notification  =  await  this . ormRepository . create ( { 
			content , 
			recipient_id , 
		} ) ;

		await  this . ormRepository . save ( notification ) ;

		return  notification ; 
	} 
}

export  default  NotificationsRepository ;

The MongoDB (non-relational) repository has its own methods, so importing MongoRepository instead of Repository

Instead of using getRepository as it is done in a relational database, I must use getMongoRepository () which receives the repository as the first parameter and according to what the connection name is. Where postgres is default and MongoDB is mongo

Sending notifications

  1. Create the container to inject NotificationsRepository
container . registerSingleTon < INotificationsRepository > ( 
	'NotificationsRepository' , 
	NotificationsRepository , 
) ;
  1. Dependency injection of NotificationsRepository in CreateAppointmentService , as a notification will be triggered whenever the provider receives a new schedule.
import  {  startOfHour ,  format  }  from  'date-fns' ;

@ injectable ( ) 
class  CreateAppointmentService  { 
	constructor ( 
		@ inject ( 'AppointmentsRepository' ) 
		private  appointmentsRepository : IAppointmentsRepository ,

		@ inject ( 'NotificationsRepository' ) 
		private  notificationsRepository : INotificationsRepository , 
	)

	public  async  execute ( { 
		provider_id , 
		date , 
		user_id , 
	} : IRequest ) : Promise < Appointment >  { 
		const  appointmentDate  =  startOfHour ( date ) ;
		...

		const  dateFormated  =  format ( appointmentDate ,  "dd / MM / yyyy 'at' HH: mm" ) ;

		await  this . notificationsRepository . run ( { 
			recipient_id : provider_id , 
			content : 'New Schedule for the day $ { dateFormated } h` , 
		} ) ;

		return  appointment ; 
	} 
}
  1. Create an insomnia schedule and check in mongodb if a notification has actually been created.

Refactoring the tests

  1. Create FakeNotificationsRepository , as CreateAppointmentService now expects a repository in its constructor.
import  {  ObjectID  }  from  'mongodb' ;

class  FakeNotificationsRepository  implements  INotificationsRepository  { 
	private  notifications : Notification [ ]  =  [ ] ;

	public  async  create ( { 
		content , 
		recipient_id , 
	} : ICreateNotificationDTO ) : Promise < Notification > { 
		const  notification  =  new  Notification ( ) ;

		Object . assign ( notification ,  {  id : new  ObjectID ( ) , content , recipient_id } ) ;

		this . notifications . push ( notification ) ;

		return  notification ; 
	} 
}

export  default  FakeNotificationsRepository ;
  1. Insert the FakeNotificationsRepository into the CreateAppointmentService.spec.ts constructor

Validating data

Use lib celebrate which is a validation middleware for express using lib Joi.

  1. Install lib celebrate
$ yarn add celebrate
  1. Go on all put / post routes or receive route parameters (route params) and use celebrate appointments.routes.ts
import  {  celebrate ,  Joi ,  Segments  }  from  'celebrate'

appointmentsRouter . post ( 
	'/' , 
	celebrate ( { 
		[ Segments . BODY ] : { 
			provider_id : Joi . string ( ) . uuid ( ) . required ( ) , 
			date : Joi . date ( ) , 
		} , 
	} ) 
	,  appointmentsController . create , 
) ;

I can pass ‘body’ or [Segments.BODY], because when the key of an object is a variable, I must use scale around.

providersRouter . get ( 
	'/: provider_id / day-availability' , 
	celebrate ( { 
		[ Segments . PARAMS ] : { 
			provider_id : Joi . string ( ) . uuid ( ) . required ( ) , 
		} 
	} ) , 
	providersDayAvailabilityController . index , 
) ; 
providersRouter . get ( 
	'/: provider_id / month-availability' ,
		celebrate ( { 
		[ Segments . PARAMS ] : { 
			provider_id : Joi . string ( ) . uuid ( ) . required ( ) , 
		} 
	} ) , 
	providersMonthAvailabilityController . index , 
) ;
passwordRouter . post ( 
	'/ forgot' , 
	celebrate ( { 
		[ Segments . BODY ] : { 
			email : Joi . string ( ) . email ( ) . required ( ) , 
		} , 
	} ) , 
	forgotPasswordControler . create , 
) ;

passwordRouter . post ( 
	'/ reset' , 
	celebrate ( { 
		[ Segments . BODY ] : { 
			token : Joi . string ( ) . uuid ( ) . required ( ) , 
			password : Joi . string ( ) . required ( ) , 
			password_confirmation : Joi . string ( ) .required ( ) . valid ( Joi . ref ( 'password' ) ) , 
		} 
	} ) , 
	resetPasswordController . create , 
) ;

Do this for the other routes! Thus, it will not be possible to call the controllers without all the fields being in agreement

Environment Variables (.env)

Information that contains different values ​​according to the environment (dev, production and the like) that our application is running.

Database access is another when I’m in production

The way to send email is another when I’m in production

  1. Install the dotenv lib
$ yarn add dotenv
  1. Import on server.ts (below reflect-metadata)
import  'dotenv / config'

Go to .tsconfig.json and add the attribute “allowJS”: true so that you can import js files.

  1. Create the .env file at the root
APP_SECRET=insert_your_secret_here
APP_WEB_URL=http://localhost:3000
  1. Add to .gitignore
.env
ormconfig.json
  1. Remove ormconfig.json from git
$ git rm --cached ormconfig.json
  1. Go to config / auth and replace the secret value for process.env.APP_SECRET

  2. Go to SendForgotPasswordEmailService and insert the process.env.APP_WEB_URL

	variables : { 
		name : user . name , 
		link : ` $ { process . env . APP_WEB_URL } / reset_password? Token = $ { token } ` , 
	} ,

Using class-transformer

Sometimes, we do not want to show any information when going to the frontend (such as deleting the user’s password when requested, for example), so transform the data / class before the data goes outside the API

Put the full avatar URL before you even go to the frontend

  1. Install lib class-transformer
$ yarn add class-transformer
  1. Go to User entity / schema
import  {  Exclude ,  Expose  }  from  'class-transformer' ;

@ Entity ( 'users' ) 
class  User  { 
	@ PrimaryGeneratedColumn ( 'uuid' ) 
	id : string ;

	@ Column ( ) 
	name : string ;

	@ Column ( ) 
	email : string ;

	@ Column ( ) 
	@ Exclude ( ) 
	password : string ;

	@ Column ( ) 
	avatar : string ;

	@ CreateDateColumn ( ) 
	created_at : Date ;

	@ UpdateDateColumn ( ) 
	updated_at : Date ;

	@ Expose ( {  name : 'avatar_url'  } ) 
	getAvatarUrl ( ) : string | null  { 
		return  this . avatar  
		? ` $ { process . env . APP_API_URL } / files / $ { this . avatar } ` 
		: null ; 
	}

}

export  default  User ;
  1. Go to .env and create the variable APP_API_URL
APP_API_URL = http : // localhost: 3333
  1. Go to controllers that return a user
// ProfileController.ts 
import  {  classToClass  }  from  'class-transformer' ;

	public async  show ( request : Request ,  response : Response ) : Promise < Response >  { 
		const  user_id  =  request . user . id ;

		const  showProfile  =  container . resolve ( ShowProfileService ) ;

		const  user  =  await  showProfile . execute ( { user_id } ) ;

		return  response . json ( classToClass ( user ) ) ; 
	}

Remover o delete user.password e envolver o retorno do usuário no método classToClass do class-transformer Dessa forma, irá modificar de acordo com os decorators que passamos na entidade/schema User

Download Details:

Author: danilobandeira29

Source Code: https://github.com/danilobandeira29/arquitetura-e-testes-nodejs

#nodejs #node #javascript

Architecture And Tests Nodejs
8.30 GEEK