Construindo aplicação do zero com node.js: Parte 2
Tags: node, javascript, programming, webdev
Published: 2022-11-07 14:41:46

Fala galera, depois de um [primeiro artigo](https://dev.to/erandirjunior/construindo-aplicacao-do-zero-com-nodejs-parte-1-3hdj) bem teórico, dessa vez iremos colocar a mão na massa. Mas antes, é necessário informar que os códigos a seguir já estão adaptados para a versão final do projeto, isso é, todos os exemplos já serão utilizando **Jest** e **ES6 Module**, além de algumas correções que encontrei pelo caminho.

## Configurando Docker
Inicialmente você precisa ter configurado tanto o **docker** quanto o **docker compose** em sua máquina, até mesmo para termos um padrão no ambiente de desenvolvimento.

Com a máquina já configurada, crie um diretório qualquer, no meu caso foi **Two-Factor-Authentication** e dentro dele vamos criar os arquivos **Dockerfile**, sem extensão, e o arquivo **docker-compose.yml**.

No arquivo *Dockerfile*, deixaremos ele assim:
```Dockerfile
FROM node:lts

RUN mkdir /app

WORKDIR /app

COPY --chown=node:node . .

CMD /bin/sh
```

No arquivo *docker-compose.yml*, deixaremos ele assim:
```yml
version: '3.9'
services:
  two_factor_authentication:
    container_name: authentication_project
    build: .
    volumes:
      - .:/app:rw
    ports:
      - "${APP_PORT}:${APP_PORT}"
    restart: "no"
    command: sh
    stdin_open: true
    tty: true
    environment:
      - CHOKIDAR_USEPOLLING=true
    depends_on:
      - db_two_factor
    networks:
      - pg-network
  adminer:
    image: adminer
    container_name: adminer_sgbd_two_factor_authentication
    ports:
      - "8081:8080"
    depends_on:
      - db_two_factor
    restart: always
    networks:
      - pg-network
  db_two_factor:
    image: postgres:latest
    container_name: db_two_factor_authentication
    ports:
      - 5432:5432
    environment:
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=${DB_DATABASE}
      - POSTGRES_HOST_AUTH_METHOD=trust
    networks:
      - pg-network
    volumes:
      - dba:/var/lib/postgresql

volumes:
  dba:

networks:
  pg-network:
    driver: bridge
```
Só para esclarecer o que estamos fazendo, iremos utilizar a última versão do **node lts**, usaremos a última versão do **postgres**, para armazenar os dados, e utilizaremos o **adminer**, que é uma ferramenta visual para auxiliar no gerenciamento do banco de dados.

Além disso, algumas informações da nossa configuração acima são dinâmicas, como por exemplo as informações de conexão com o banco e a porta da aplicação, para isso, vamos criar um arquivo de configuração chamado **.env**, com o seguinte conteúdo;
```env
APP_PORT=8001

DB_DIALECT=postgres
DB_HOST=db_two_factor
DB_DATABASE=two_factors
DB_USER=root
DB_PASSWORD=root
```
**Esse arquivo será bem útil posteriormente**.

Agora iremos subir todos os nossos serviços. Acesse o terminal e rode o comando abaixo:
```bash
docker-compose up --buid
```

Ou se quiser deixar os serviços rodando em segundo plano, basta rodar o comando abaixo:
```bash
docker compose up -d --build
```

Agora que estamos com nossos serviços rodando, vamos entrar no serviço onde está o *node* para começarmos a construir nossa aplicação, no terminal, rode o comando abaixo:
```bash
docker exec -it authentication_project bash
```
**_authentication_project_ é o nome do meu serviço node, para verificar o seu, basta rodar o comando docker ps**.

## Iniciando estrutura do projeto
Depois de entrarmos no serviço do *node*, vamos rodar o seguinte comando para de fato iniciarmos o projeto:
```bash
npm init -y
```
Um arquivo chamado **package.json** vai ser criado, ele será bem útil, por enquanto o deixaremos de lado. Em relação a organização do projeto, deixei claro que me baseei na **arquitetura hexagonal** (falarei um pouco mais posteriormente), então vamos criar diretórios que vão facilitar o entendimento do projeto, minha estrutura ficou como o da imagem abaixo:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dqoayj28fyp7604xl3ks.png)

* **src/domain**: onde ficará toda a nossa regra de negócio;
* **src/infra**: onde ficará todas as implementações das dependências do nosso projeto, irá encapsular os recursos de libs, por exemplo;
* **tests**: onde ficará nossos testes.

## Fluxo do projeto
O fluxo do projeto será dividido em duas partes: Primeiro vamos construir a lógica de login "tradicional", com e-mail e senha, depois vamos construir a lógica de logar com o token.

E pensando na primeira lógica do sistema, a gente vai ter uma entrada de e-mail e senha, vamos precisar buscar as informações do usuário pelo e-mail, validar sua senha, gerar um token e mandar por e-mail. Nesta breve descrição, podemos perceber que teremos uma comunicação com o banco de dados, uma validação de senha, comunicação com o serviço de e-mail e geração de token, ou seja, temos 4 dependências.

Vamos primeiramente definir essas "**interfaces**", lembrando que *JS* não possui o recurso de interface como conhecemos em outras linguagens, então dentro de **src/domain**, vamos criar alguns arquivos:
```js
// src/domain/iemail.js
export default class IEmail {
    send(user) {
        throw new Error('Method must be implemented!');
    }
}

// src/domain/irepository.js
export default class IRepository {
    findByEmail(email) {
        throw Error('Method must be implemented!');
    }

    update(user) {
        throw Error('Method must be implemented!');
    }
}

// src/domain/ipassword-hash.js
export default class IPasswordHash {
    compare(password, hash) {
        throw Error('Method must be implemented!');
    }
}

// src/domain/igenerate-token.js
export default class IGenerateToken {
    getToken() {
        throw Error('Method must be implemented!');
    }

    getEmailToken() {
        throw Error('Method must be implemented!');
    }
}
```

Basicamente definimos nossas "interfaces" de comunicação. Um detalhe muito importante fica por conta da classe **IGenerateToken**, contendo 2 métodos de geração de token, isso porque pensei na seguinte lógica: quando o usuário logar, ele vai receber por e-mail um token, porém também vai receber um outro token como resposta da requisição (que será armazenada temporariamente pelo cliente), quando o usuário for fazer a segunda autenticação, ele vai precisar mandar os 2 tokens, aqui basicamente é uma forma de aumentar a segurança.

Outra coisa que vamos encapsular, é em relação ao erros, eu basicamente criei 3 classes de erros, isso vai ser útil mais na frente:
```js
// src/domain/domain-error.js
export default class DomainError extends Error {
    constructor(message) {
        super(message);
    }
}

// src/domain/gateway-error.js
export default class GatewayError extends Error {
    constructor(message) {
        super(message);
    }
}

// invalid-argument-error.js
export default class InvalidArgumentError extends Error {
    constructor(message) {
        super(message);
    }
}
```

Também vamos criar uma ação para trabalhar com esses erros:
```js
// throw-error.js
const throwError = (error, message) => {
    throw new error(message);
};

export default throwError;
```

Agora vamos construir a classe de login:
```js
// src/domain/user-authentication.js
import IRepository from './irepository.js';
import IEmail from './iemail.js';
import IPasswordHash from './ipassword-hash.js';
import IGenerateToken from './igenerate-token.js';
import DomainError from './domain-error.js';
import GatewayError from './gateway-error.js';
import InvalidArgumentError from './invalid-argument-error.js';
import throwError from './throw-error.js';

export default class UserAuthentication {
    #repository;
    #emailGateway;
    #hashService;
    #tokenService;

    constructor(
        repository,
        emailGateway,
        passwordHash,
        tokenService
    ) {
        this.#repository = repository;
        this.#emailGateway = emailGateway;
        this.#hashService = passwordHash;
        this.#tokenService = tokenService;
    }
}
```

Como *JS* é uma linguagem de tipagem fraca, vamos criar algo que valide a instância dos objetos recebidos, para isso vamos criar um novo arquivo para fazer essa validação:
```js
// src/domain/intanceof.js
const isInstanceOf = (instance, instanceBase) => instance instanceof instanceBase;

export default isInstanceOf;
```

Agora importamos em UserAuthentication e vamos validar as entradas:

```js
import IRepository from './irepository.js';
import IEmail from './iemail.js';
import IPasswordHash from './ipassword-hash.js';
import IGenerateToken from './igenerate-token.js';
import isInstanceOf from './instanceof.js';
import DomainError from './domain-error.js';
import GatewayError from './gateway-error.js';
import InvalidArgumentError from './invalid-argument-error.js';
import throwError from './throw-error.js';

export default class UserAuthentication {
    #repository;
    #emailGateway;
    #hashService;
    #tokenService;

    constructor(
        repository,
        emailGateway,
        passwordHash,
        tokenService
    ) {
        this.#validateDependencies(repository, emailGateway, passwordHash, tokenService);
        this.#repository = repository;
        this.#emailGateway = emailGateway;
        this.#hashService = passwordHash;
        this.#tokenService = tokenService;
    }

    #validateDependencies(repository, emailGateway, passwordHash, tokenService) {
        if (!this.#isInstanceOf(repository, IRepository)) {
            throwError(DomainError, 'Invalid repository dependency');
        }

        if (!this.#isInstanceOf(emailGateway, IEmail)) {
            throwError(DomainError, 'Invalid email gateway dependency');
        }

        if (!this.#isInstanceOf(passwordHash, IPasswordHash)) {
            throwError(DomainError, 'Invalid hash service dependency');
        }

        if (!this.#isInstanceOf(tokenService, IGenerateToken)) {
            throwError(DomainError, 'Invalid token service dependency');
        }
    }

    #isInstanceOf(object, instanceBase) {
        return isInstanceOf(object, instanceBase);
    }
}
```

Antes de criarmos nosso método de autenticação, vamos criar duas classes, uma que vai encapsular os dados de entrada, e outra que vai encapsular os dados do usuário que vem do banco:
```js
// src/domain/login-payload.js
import InvalidArgumentError from './invalid-argument-error.js';
import throwError from './throw-error.js';

export default class LoginPayload {
    #email;
    #password;

    constructor(email, password) {
        if (!email || !password) {
            throwError(InvalidArgumentError, 'Fields email and password must be filled!');
        }

        this.#email = email;
        this.#password = password;
    }

    get email() {
        return this.#email;
    }

    get password() {
        return this.#password;
    }
}

// src/domain/user.js
import DomainError from'./domain-error.js';
import throwError from './throw-error.js';

export default class User {
    token;
    emailToken;

    #id;
    #email;
    #password;
    #expired;

    constructor({id, email, password, token, emailToken, expired}) {
        if (!id || !email || !password) {
            throwError(DomainError, 'Invalid user data!')
        }

        this.#id = id;
        this.#email = email;
        this.#password = password;
        this.token = token;
        this.emailToken = emailToken;
        this.#expired = expired;
    }

    get id() {
        return parseInt(this.#id);
    }

    get expired() {
        return !!this.#expired;
    }

    get email() {
        return this.#email;
    }

    get password() {
        return this.#password;
    }
}
```

Importem essas classes na nossa classe de negócio, e agora vamos implementar nossa lógica de autenticação, observe o fluxo abaixo:
```js
async authenticate(loginPayload) {
        this.#validateUserDataInput(loginPayload);
        const registeredUser = await this.getUserRegistered(loginPayload);

        try {
            const user = this.#getUserWithUpdatedData(registeredUser);
            await this.#repository.update(user);
            this.#emailGateway.send(user);
            return user.token;
        } catch (error) {
            throwError(GatewayError, 'Generic error, see the integrations!');
        }
    }

    #validateUserDataInput(loginPayload) {
        if (!this.#isInstanceOf(loginPayload, LoginPayload)) {
            throwError(DomainError, 'Invalid payload dependency');
        }
    }

    async getUserRegistered(user) {
        let registeredUser = null;

        try {
            registeredUser = await this.#repository.findByEmail(user.email);
        } catch (e) {
            throwError(GatewayError, 'Error connection database!');
        }

        return this.#getUserValidated(registeredUser, user);
    }

    async #getUserValidated(registeredUser, user) {
        if (!registeredUser) {
            throwError(InvalidArgumentError, 'User not found with data sent!');
        }

        if (!this.#isInstanceOf(registeredUser, User)) {
            throwError(DomainError, 'Invalid user instance!');
        }

        const passwordsAreEquals = await this.#hashService.compare(user.password, registeredUser.password);

        if (!passwordsAreEquals) {
            throwError(InvalidArgumentError, 'Invalid user data!');
        }

        return registeredUser;
    }

    #isInstanceOf(object, instanceBase) {
        return isInstanceOf(object, instanceBase);
    }

    #getUserWithUpdatedData(user) {
        return new User({
            id: user.id,
            email: user.email,
            ...this.#getTokens(),
            password: user.password,
            expired: false
        });
    }

    #getTokens() {
        return {
            token: this.#tokenService.getToken(),
            emailToken: this.#tokenService.getEmailToken()
        }
    }
}
```

Temos o método **authenticate** que recebe um objeto de entrada, é feita a validação desse objeto, depois é feita a busca do usuário pelo e-mail e novamente validamos o objeto recebido, e por último validamos a senha. Volte ao código acima e veja que a gente envolve algumas chamadas em try/catch e em caso de erro, ou validações mal sucedidas, lançamos sempre as nossas classes de erros personalizados.

Após obtermos o usuário válido com a chamada do método **#getUserRegistered**, nós vamos criar um outro objeto, contendo tanto as informações do usuário do banco, como também as informações dos tokens. Tudo isso chamando o método **#getUserWithUpdatedData**, após isso, a gente chama o método de atualização do banco, depois manda os dados necessários por e-mail e retornamos um dos tokens gerados.

Note que o método de envio de e-mail, não esperamos o fim da execução, pois provavelmente o envio de e-mail seja um processo demorado, que pode travar a execução do nosso sistema.

Agora observem o objeto **User**, e principalmente o campo **expired**, esse campo é o responsável por verificar se os tokens enviados já foram utilizados ou não (trabalharemos nisso depois). Fazendo com que um par de tokens seja utilizado apenas uma vez.

Por fim, o arquivo *user-authentication*, deverá ficar com o conteúdo abaixo:
```js
import User from './user.js';
import IRepository from './irepository.js';
import IEmail from './iemail.js';
import IPasswordHash from './ipassword-hash.js';
import IGenerateToken from './igenerate-token.js';
import LoginPayload from './login-payload.js';
import isInstanceOf from './instanceof.js';
import DomainError from './domain-error.js';
import GatewayError from './gateway-error.js';
import InvalidArgumentError from './invalid-argument-error.js';
import throwError from './throw-error.js';

export default class UserAuthentication {
    #repository;
    #emailGateway;
    #hashService;
    #tokenService;

    constructor(
        repository,
        emailGateway,
        passwordHash,
        tokenService
    ) {
        this.#validateDependencies(repository, emailGateway, passwordHash, tokenService);
        this.#repository = repository;
        this.#emailGateway = emailGateway;
        this.#hashService = passwordHash;
        this.#tokenService = tokenService;
    }

    #validateDependencies(repository, emailGateway, passwordHash, tokenService) {
        if (!this.#isInstanceOf(repository, IRepository)) {
            throwError(DomainError, 'Invalid repository dependency');
        }

        if (!this.#isInstanceOf(emailGateway, IEmail)) {
            throwError(DomainError, 'Invalid email gateway dependency');
        }

        if (!this.#isInstanceOf(passwordHash, IPasswordHash)) {
            throwError(DomainError, 'Invalid hash service dependency');
        }

        if (!this.#isInstanceOf(tokenService, IGenerateToken)) {
            throwError(DomainError, 'Invalid token service dependency');
        }
    }

    async authenticate(loginPayload) {
        this.#validateUserDataInput(loginPayload);
        const registeredUser = await this.#getUserRegistered(loginPayload);

        try {
            const user = this.#getUserWithUpdatedData(registeredUser);
            await this.#repository.update(user);
            this.#emailGateway.send(user);
            return user.token;
        } catch (error) {
            throwError(GatewayError, 'Generic error, see the integrations!');
        }
    }

    #validateUserDataInput(loginPayload) {
        if (!this.#isInstanceOf(loginPayload, LoginPayload)) {
            throwError(DomainError, 'Invalid payload dependency');
        }
    }

    async #getUserRegistered(user) {
        let registeredUser = null;

        try {
            registeredUser = await this.#repository.findByEmail(user.email);
        } catch (e) {
            throwError(GatewayError, 'Error connection database!');
        }

        return this.#getUserValidated(registeredUser, user);
    }

    async #getUserValidated(registeredUser, user) {
        if (!registeredUser) {
            throwError(InvalidArgumentError, 'User not found with data sent!');
        }

        if (!this.#isInstanceOf(registeredUser, User)) {
            throwError(DomainError, 'Invalid user instance!');
        }

        const passwordsAreEquals = await this.#hashService.compare(user.password, registeredUser.password);

        if (!passwordsAreEquals) {
            throwError(InvalidArgumentError, 'Invalid user data!');
        }

        return registeredUser;
    }

    #isInstanceOf(object, instanceBase) {
        return isInstanceOf(object, instanceBase);
    }

    #getUserWithUpdatedData(user) {
        return new User({
            id: user.id,
            email: user.email,
            ...this.#getTokens(),
            password: user.password,
            expired: false
        });
    }

    #getTokens() {
        return {
            token: this.#tokenService.getToken(),
            emailToken: this.#tokenService.getEmailToken()
        }
    }
}
```

## Teste
Agora que nossa lógica foi feita, vamos testar todos os fluxos. Acesse o terminal do container e rode o comando abaixo para instalar o **Jest**:
```bash
npm i --save-dev jest
```

Instamos essa biblioteca somente como dependência de desenvolvimento. Outra coisa que faremos é alterar o comando padrão de testes, para que ele chame o *Jest*, então, em nosso arquivo package.json, altere ele modificando o campo *script* para isso:
```json
"scripts": {
    "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js ./tests/* --coverage --config='{ \"coverageReporters\": [\"html\"] }'\n"
  }
```

E agora vamos criar nossos testes, dentro do diretório *tests*, crie um diretório chamado *unit*, e dentro dele um diretório chamado *mocks*, essa pasta *mocks* vai servir para armazenar todas as implementações das interfaces do domínio. Poderíamos utilizar alguma lib para fazer isso, mas eu preferi fazer tudo na mão, então dentro de *mocks*, crie os arquivos abaixo contendo os seguintes códigos:
```js
// tests/unit/mocks/email-mock.js
import IEmail from '../../../src/domain/iemail.js';

class EmailMock extends IEmail {
    throwException = false;

    send(user) {
        if (this.throwException) {
            throw Error();
        }
        return Promise.resolve('Sent!');
    }
}

export default new EmailMock();

// tests/unit/mocks/repository-mock.js
import IRepository from '../../../src/domain/irepository.js';
import User from '../../../src/domain/user.js';

class RepositoryMock extends IRepository {
    throwException = false;
    throwExceptionUpdate = false;
    returnEmptyObject = false;
    returnEmpty = false;

    constructor() {
        super();
    }

    findByEmail(email) {
        if (this.throwException) {
            throw Error();
        }

        if (this.returnEmpty) {
            return '';
        }

        if (this.returnEmptyObject) {
            return {};
        }

        const user = new User({id: 1, email: 'erandir@email.com', password: '123456'});

        return Promise.resolve(user);
    }

    update(user) {
        if (this.throwExceptionUpdate) {
            throw Error();
        }
        return Promise.resolve(true);
    }
}

export default new RepositoryMock();

// tests/unit/mocks/hash-mock.js
import IPasswordHash from '../../../src/domain/ipassword-hash.js';

class HashMock extends IPasswordHash {
    passwordsAreEquals = true;

    constructor() {
        super();
    }

    compare(password, hash) {
        return this.passwordsAreEquals;
    }
}

export default new HashMock();

// tests/unit/mocks/token-mock.js
import IGenerateToken from '../../../src/domain/igenerate-token.js';

class TokenMock extends IGenerateToken {
    constructor() {
        super();
    }

    getToken() {
        return '13eb4cb6-35dd-4536-97e6-0ed0e4fb1fb3'
    }

    getEmailToken() {
        return  '4RV651gR93hDAGiTCYhmhh';
    }
}

export default new TokenMock();
```

Agora dentro de *tests/unit*, crie um arquivo chamado **user-authentication.test.js**, é nesse arquivo que escreveremos todos os nosso testes para a classe **UserAuthentication**:
```js
import UserAuthentication from '../../src/domain/user-authentication.js';
import EmailMock from './mocks/email-mock.js';
import RepositoryMock from './mocks/repository-mock.js';
import HashMock from './mocks/hash-mock.js';
import TokenMock from './mocks/token-mock.js';
import LoginPayload from '../../src/domain/login-payload.js';
const payload = new LoginPayload('erandir@email.com', '1234567');
const userAuthentication = new UserAuthentication(
    RepositoryMock,
    EmailMock,
    HashMock,
    TokenMock
);

test('Invalid object repository', function () {
    const result = () => new UserAuthentication(
        {},
        EmailMock,
        HashMock,
        TokenMock
    );
    expect(result).toThrow('Invalid repository dependency');
});

test('Invalid object email', function () {
    const result = () => new UserAuthentication(
        RepositoryMock,
        {},
        HashMock,
        TokenMock
    );
    expect(result).toThrow('Invalid email gateway dependency');
});

test('Invalid object hash', function () {
    const result = () => new UserAuthentication(
        RepositoryMock,
        EmailMock,
        {},
        TokenMock
    );
    expect(result).toThrow('Invalid hash service dependency');
});

test('Invalid object token', function () {
    const result = () => new UserAuthentication(
        RepositoryMock,
        EmailMock,
        HashMock,
        {}
    );
    expect(result).toThrow('Invalid token service dependency');
});

test('Invalid object login payload', async () => {
    const result = async () => await userAuthentication.authenticate({});
    expect(result).rejects.toThrow('Invalid payload dependency');
});

test('Error exception find user', async () => {
    RepositoryMock.throwException = true;
    const result = async () => await userAuthentication.authenticate(payload);
    expect(result).rejects.toThrow('Error connection database!');
});

test('Error find user empty', async () => {
    RepositoryMock.throwException = false;
    RepositoryMock.returnEmpty = true;
    const result = async () => await userAuthentication.authenticate(payload);
    expect(result).rejects.toThrow('User not found with data sent!');
});

test('Error user without id', async () => {
    RepositoryMock.returnEmpty = false;
    RepositoryMock.returnEmptyObject = true;
    const result = async () => await userAuthentication.authenticate(payload);
    expect(result).rejects.toThrow('Invalid user instance!');
});

test('Error update user', async () => {
    RepositoryMock.returnEmptyObject = false;
    RepositoryMock.throwExceptionUpdate = true;
    const result = async () => await userAuthentication.authenticate(payload);
    expect(result).rejects.toThrow('Generic error, see the integrations!');
});

test('Error email exception', async () => {
    RepositoryMock.throwExceptionUpdate = false;
    EmailMock.throwException = true;
    const result = async () => await userAuthentication.authenticate(payload);
    expect(result).rejects.toThrow('Generic error, see the integrations!');
});

test('Error user divergent password', async () => {
    EmailMock.throwException = false;
    HashMock.passwordsAreEquals = false;
    const result = async () => await userAuthentication.authenticate(payload);
    expect(result).rejects.toThrow('Invalid user data!');
});

test('Get token', async () => {
    HashMock.passwordsAreEquals = true;
    const result = await userAuthentication.authenticate(payload);
    expect(result).toBe('13eb4cb6-35dd-4536-97e6-0ed0e4fb1fb3');
});
```
Importamos as dependências e fizemos todos os testes em cima da nossa classe de negócios, seja passando dados errados, ou gerando exceção, passando por todos os caminhos errados, para só no final realizarmos o teste de caminho feliz.

Antes de executarmos nossos testes, precisamos configurar a modularização do nosso projeto, que por padrão é a do **commonjs**, e em nosso caso, utilizamos a do **es6 module**, então precisamos informar isso nas configurações, modifique o arquivo **package.json** e deixe-o assim:
```json
{
  "name": "nome_do_seu_projeto",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "directories": {
    "test": "tests"
  },
  "scripts": {
    "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js ./tests/* --coverage --config='{ \"coverageReporters\": [\"html\"] }'\n"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "jest": "^29.2.2"
  },
  "type": "module"
}
```

Vá ao terminal do serviço node e executar o código abaixo:
```bash
npm t
```
Será executado todos os teste e após isso uma pasta chamada **coverage** será criada na raiz do projeto. Dentro de **coverage**, vai ter inúmeros arquivos html, procure o **index.html** e abra ele no seu navegador. Você notará que temos um relatório de cobertura de testes, onde sabemos o que foi testado, o que falta testar, etc. Isso é muito útil.

## Tópicos interessantes
Sabe-se que em JavaScript, podemos definir objetos de algumas formas diferentes, nesse projeto, fiz uso de classes, que é algo mais recente, isso porque tenho mais familiaridade com essa forma. O projeto inteiro poderia ter sido feito utilizando objetos literais, funções construtoras, etc.

Ainda falando sobre classes, perceba que para criar atributos e métodos privados, eu faço uso do caractere #, que também é um recurso recente. Outro detalhe fica por conta das subclasses, que sempre definimos um *construtor* e que sempre o primeiro comando é chamar o *construtor* do pai.

Além disso, percebam como estamos isolando nossa camada de regra de negócio, dependemos sempre de interfaces, isso nos possibilita simular comportamentos, o que nos ajudou nos testes.

Se você é um desenvolvedor mais experiente, possivelmente pode ter notado que não implementei uma validação importante, que é se os tokens gerados são únicos, isso foi de certa forma proposital, caso alguém queira mexer no código, alterar e adicionar essa validação.

E sobre a questão da modularização do projeto, caso quiséssemos utilizar a padrão, que é o **commonjs**, iriamos substituir os comando de exportar e importar, por exemplo:
```js
// es6 Module
export default class InvalidArgumentError extends Error {
    constructor(message) {
        super(message);
    }
}

// E para importar fazemos assim
import InvalidArgumentError from './invalid-argument-error.js';

// Em commonjs fariamos da seguinte forma
module.exports = class InvalidArgumentError extends Error {
    constructor(message) {
        super(message);
    }
}

// E para importar faríamos assim
const InvalidArgumentError = require('./invalid-argument-error.js');
```
Existem algumas pequenas diferenças entre usar uma ou outra, mas eu gosto de utilizar a modularização do *ES6*.

## Resumo
Neste artigo, começamos a montar nosso projeto, criamos testes, relatório de teste, aprendemos a importar e exportar módulos, validamos objetos, etc. Vale a pena notar que essa aqui foi a minha linha de raciocínio, talvez você tenha pensado em uma forma diferente de fazer tudo que fiz nessa primeira parte, você pode implementar do jeito. Continuaremos desenvolvendo nossa lógica no próximo artigo, até lá.