├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .huskyrc.json ├── .lintstagedrc.json ├── .travis.yml ├── README.md ├── docker-compose.yml ├── jest-integration-config.js ├── jest-mongodb-config.js ├── jest-unit-config.js ├── jest.config.js ├── package.json ├── public └── img │ ├── logo-angular.png │ ├── logo-ember.png │ ├── logo-flutter.png │ ├── logo-ionic.png │ ├── logo-jquery.png │ ├── logo-js.png │ ├── logo-knockout.png │ ├── logo-native-script.png │ ├── logo-nativo.png │ ├── logo-npm.png │ ├── logo-phonegap.png │ ├── logo-polymer.png │ ├── logo-react.png │ ├── logo-riot.png │ ├── logo-svelte.png │ ├── logo-titanium.png │ ├── logo-ts.png │ ├── logo-vue.png │ ├── logo-xamarin.png │ └── logo-yarn.png ├── requirements ├── add-survey.md ├── load-survey-result.md ├── load-surveys.md ├── login.md ├── save-survey-result.md └── signup.md ├── src ├── data │ ├── protocols │ │ ├── criptography │ │ │ ├── decrypter.ts │ │ │ ├── encrypter.ts │ │ │ ├── hash-comparer.ts │ │ │ └── hasher.ts │ │ └── db │ │ │ ├── account │ │ │ ├── add-account-repository.ts │ │ │ ├── load-account-by-email-repository.ts │ │ │ ├── load-account-by-token-repository.ts │ │ │ └── update-access-token-repository.ts │ │ │ ├── log │ │ │ └── log-error-repository.ts │ │ │ ├── survey-result │ │ │ ├── load-survey-result-repository.ts │ │ │ └── save-survey-result-repository.ts │ │ │ └── survey │ │ │ ├── add-survey-repository.ts │ │ │ ├── load-survey-by-id-repository.ts │ │ │ └── load-surveys-repository.ts │ ├── test │ │ ├── index.ts │ │ ├── mock-criptography.ts │ │ ├── mock-db-account.ts │ │ ├── mock-db-log.ts │ │ ├── mock-db-survey-result.ts │ │ └── mock-db-survey.ts │ └── usecases │ │ ├── account │ │ ├── add-account │ │ │ ├── db-add-account-protocols.ts │ │ │ ├── db-add-account.spec.ts │ │ │ └── db-add-account.ts │ │ ├── authentication │ │ │ ├── db-authentication-protocols.ts │ │ │ ├── db-authentication.spec.ts │ │ │ └── db-authentication.ts │ │ └── load-account-by-token │ │ │ ├── db-load-account-by-token-protocols.ts │ │ │ ├── db-load-account-by-token.spec.ts │ │ │ └── db-load-account-by-token.ts │ │ ├── survey-result │ │ ├── load-survey-result │ │ │ ├── db-load-survey-result-protocols.ts │ │ │ ├── db-load-survey-result.spec.ts │ │ │ └── db-load-survey-result.ts │ │ └── save-survey-result │ │ │ ├── db-save-survey-result-protocols.ts │ │ │ ├── db-save-survey-result.spec.ts │ │ │ └── db-save-survey-result.ts │ │ └── survey │ │ ├── add-survey │ │ ├── db-add-survey-protocols.ts │ │ ├── db-add-survey.spec.ts │ │ └── db-add-survey.ts │ │ ├── load-survey-by-id │ │ ├── db-load-survey-by-id -protocols.ts │ │ ├── db-load-survey-by-id.spec.ts │ │ └── db-load-survey-by-id.ts │ │ └── load-surveys │ │ ├── db-load-surveys-protocols.ts │ │ ├── db-load-surveys.spec.ts │ │ └── db-load-surveys.ts ├── domain │ ├── models │ │ ├── account │ │ │ ├── account.ts │ │ │ └── authentication.ts │ │ ├── survey-result │ │ │ └── survey-result.ts │ │ └── survey │ │ │ └── survey.ts │ ├── test │ │ ├── index.ts │ │ ├── mock-account.ts │ │ ├── mock-survey-result.ts │ │ ├── mock-survey.ts │ │ └── test-helpers.ts │ └── usecases │ │ ├── account │ │ ├── add-account.ts │ │ ├── authentication.ts │ │ └── load-account-by-token.ts │ │ ├── survey-result │ │ ├── load-survey-result.ts │ │ └── save-survey-result.ts │ │ └── survey │ │ ├── add-survey.ts │ │ ├── load-survey-by-id.ts │ │ └── load-surveys.ts ├── infra │ ├── criptography │ │ ├── bcrypt-adapter │ │ │ ├── bcrypt-adapter.spec.ts │ │ │ └── bcrypt-adapter.ts │ │ └── jwt-adapter │ │ │ ├── jwt-adapter.spec.ts │ │ │ └── jwt-adapter.ts │ ├── db │ │ └── mongodb │ │ │ ├── account │ │ │ ├── account-mongo-repository.spec.ts │ │ │ └── account-mongo-repository.ts │ │ │ ├── helpers │ │ │ ├── index.ts │ │ │ ├── mongo-helper.spec.ts │ │ │ ├── mongo-helper.ts │ │ │ └── query-builder.ts │ │ │ ├── log │ │ │ ├── log-mongo-repository.spec.ts │ │ │ └── log-mongo-repository.ts │ │ │ ├── survey-result │ │ │ ├── survey-result-mongo-repository.spec.ts │ │ │ └── survey-result-mongo-repository.ts │ │ │ └── survey │ │ │ ├── survey-mongo-repository.spec.ts │ │ │ └── survey-mongo-repository.ts │ └── validators │ │ ├── email-validator-adapter.spec.ts │ │ └── email-validator-adapter.ts ├── main │ ├── adapters │ │ ├── express-middleware-adapter.ts.ts │ │ └── express-route-adapter.ts │ ├── config │ │ ├── app.ts │ │ ├── config-swagger.ts │ │ ├── custom-modules.d.ts │ │ ├── env.ts │ │ ├── middlewares.ts │ │ ├── routes.ts │ │ └── static-files.ts │ ├── decorators │ │ ├── log-controller-decorator.spec.ts │ │ └── log-controller-decorator.ts │ ├── docs │ │ ├── components.ts │ │ ├── components │ │ │ ├── bad-request.ts │ │ │ ├── forbidden.ts │ │ │ ├── index.ts │ │ │ ├── not-found.ts │ │ │ ├── server-error.ts │ │ │ └── unauthorized.ts │ │ ├── index.ts │ │ ├── paths.ts │ │ ├── paths │ │ │ ├── index.ts │ │ │ ├── login.path.ts │ │ │ ├── signup.path.ts │ │ │ ├── survey-path.ts │ │ │ └── survey-result-path.ts │ │ ├── schemas.ts │ │ └── schemas │ │ │ ├── account-schema.ts │ │ │ ├── add-survey-params-schema.ts │ │ │ ├── api-key-auth-schema.ts │ │ │ ├── error-schema.ts │ │ │ ├── index.ts │ │ │ ├── login-params-schema.ts │ │ │ ├── save-survey-params-schema.ts │ │ │ ├── signup-params-schema.ts │ │ │ ├── survey-answer-schema.ts │ │ │ ├── survey-result-answer-schema.ts │ │ │ ├── survey-result-schema.ts │ │ │ ├── survey-schema.ts │ │ │ └── surveys-schema.ts │ ├── factories │ │ ├── controllers │ │ │ ├── login │ │ │ │ ├── login │ │ │ │ │ ├── login-controller-factory.ts │ │ │ │ │ ├── login-validation-factory.spec.ts │ │ │ │ │ └── login-validation-factory.ts │ │ │ │ └── signup │ │ │ │ │ ├── signup-controller-factory.ts │ │ │ │ │ ├── signup-validation-factory.spec.ts │ │ │ │ │ └── signup-validation-factory.ts │ │ │ ├── survey-result │ │ │ │ ├── load-survey-result │ │ │ │ │ └── load-survey-result-controller-factories.ts │ │ │ │ └── save-survey-result │ │ │ │ │ └── save-survey-result-controller-factories.ts │ │ │ └── survey │ │ │ │ ├── add-survey │ │ │ │ ├── add-survey-controller-factory.ts │ │ │ │ ├── add-survey-validation-factory.spec.ts │ │ │ │ └── add-survey-validation-factory.ts │ │ │ │ └── load-surveys │ │ │ │ └── load-surveys-controller-factory.ts │ │ ├── decorators │ │ │ └── log-controller-decorator-factory.ts │ │ ├── middleware │ │ │ └── auth-middleware-factory.ts │ │ └── usecases │ │ │ ├── account │ │ │ ├── add-account │ │ │ │ └── db-add-account-factory.ts │ │ │ ├── authentication │ │ │ │ └── db-authentication-factory.ts │ │ │ └── load-account-by-token │ │ │ │ └── db-load-account-by-token-factory.ts │ │ │ ├── survey-result │ │ │ ├── load-survey-result │ │ │ │ └── db-load-survey-result-factory.ts │ │ │ └── save-survey-result │ │ │ │ └── db-save-survey-result-factory.ts │ │ │ └── survey │ │ │ ├── add-survey │ │ │ └── db-add-survey-factory.ts │ │ │ ├── load-survey-by-id │ │ │ └── db-load-survey-by-id-factory.ts │ │ │ └── load-surveys │ │ │ └── db-load-surveys-factory.ts │ ├── middlewares │ │ ├── admin-auth.ts │ │ ├── auth.ts │ │ ├── body-parser.test.ts │ │ ├── body-parser.ts │ │ ├── content-type.test.ts │ │ ├── content-type.ts │ │ ├── cors.test.ts │ │ ├── cors.ts │ │ ├── index.ts │ │ ├── no-cache.test.ts │ │ └── no-cache.ts │ ├── routes │ │ ├── login-routes.test.ts │ │ ├── login-routes.ts │ │ ├── survey-result-routes.test.ts │ │ ├── survey-result-routes.ts │ │ ├── survey-routes.test.ts │ │ └── survey-routes.ts │ └── server.ts ├── presentation │ ├── controllers │ │ ├── login │ │ │ ├── login │ │ │ │ ├── login-controller-protocols.ts │ │ │ │ ├── login-controller.spec.ts │ │ │ │ └── login-controller.ts │ │ │ └── signup │ │ │ │ ├── signup-controller-protocols.ts │ │ │ │ ├── signup-controller.spec.ts │ │ │ │ └── signup-controller.ts │ │ ├── survey-result │ │ │ ├── load-survey-result │ │ │ │ ├── load-survey-result-controller-protocols.ts │ │ │ │ ├── load-survey-result-controller.spec.ts │ │ │ │ └── load-survey-result-controller.ts │ │ │ └── save-survey-result │ │ │ │ ├── save-survey-result-controller-protocols.ts │ │ │ │ ├── save-survey-result-controller.spec.ts │ │ │ │ └── save-survey-result-controller.ts │ │ └── survey │ │ │ ├── add-survey │ │ │ ├── add-survey-controller-protocols.ts │ │ │ ├── add-survey-controller.spec.ts │ │ │ └── add-survey-controller.ts │ │ │ └── load-surveys │ │ │ ├── load-surveys-controller-protocols.ts │ │ │ ├── load-surveys-controller.spec.ts │ │ │ └── load-surveys-controller.ts │ ├── errors │ │ ├── access-denied-error.ts.ts │ │ ├── email-in-use-error.ts │ │ ├── index.ts │ │ ├── invalid-params-error.ts │ │ ├── missing-params-error.ts │ │ ├── server-error.ts │ │ └── unauthorized-error.ts │ ├── helpers │ │ └── http │ │ │ └── http-helper.ts │ ├── middleware │ │ ├── auth-middleware-protocols.ts │ │ ├── auth-middleware.spec.ts │ │ └── auth-middleware.ts │ ├── protocols │ │ ├── controller.ts │ │ ├── http.ts │ │ ├── index.ts │ │ ├── middleware.ts │ │ └── validation.ts │ └── test │ │ ├── index.ts │ │ ├── mock-account.ts │ │ ├── mock-survey-result.ts │ │ ├── mock-survey.ts │ │ └── mock-validation.ts └── validation │ ├── protocols │ └── email-validator.ts │ ├── test │ ├── index.ts │ └── mock-email-validator.ts │ └── validators │ ├── compare-field-validation.spec.ts │ ├── compare-fields-validation.ts │ ├── email-validation.spec.ts │ ├── email-validation.ts │ ├── index.ts │ ├── required-field-validation.spec.ts │ ├── required-field-validation.ts │ ├── validation-composite.spec.ts │ └── validation-composite.ts ├── tsconfig-build.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | ./data 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard-with-typescript", 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "rules": { 7 | "@typescript-eslint/strict-boolean-expressions": "off", 8 | "@typescript-eslint/consistent-type-definitions": "off", 9 | "@typescript-eslint/comma-spacing": "off", 10 | "@typescript-eslint/return-await": "off", 11 | "@typescript-eslint/restrict-template-expressions": "off", 12 | "@typescript-eslint/no-misused-promises": "off" 13 | } 14 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist 3 | coverage 4 | data 5 | !src/data 6 | .vscode 7 | globalConfig.json 8 | package-lock.json 9 | yarn.* -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged", 4 | "pre-push": "npm run test:ci" 5 | } 6 | } -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.ts": [ 3 | "eslint 'src/**' --fix", 4 | "npm run test:staged" 5 | ] 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | script: 5 | - eslint 'src/**' 6 | - npm run test:coveralls -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/williamkoller/clean-ts-api.svg?branch=master)](https://travis-ci.com/williamkoller/clean-ts-api) 2 | [![Coverage Status](https://coveralls.io/repos/github/williamkoller/clean-ts-api/badge.svg?branch=master)](https://coveralls.io/github/williamkoller/clean-ts-api?branch=master) 3 | [![Known Vulnerabilities](https://snyk.io/test/github/williamkoller/clean-ts-api/badge.svg)](https://snyk.io/test/github/williamkoller/clean-ts-api) 4 | [![GPLv3 License](https://img.shields.io/badge/License-GPL%20v3-yellow.svg)](https://opensource.org/licenses/) 5 | [![Open Source](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)](https://opensource.org/) 6 | ![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/williamkoller/clean-ts-api) 7 | [![time tracker](https://wakatime.com/badge/github/williamkoller/clean-ts-api.svg)](https://wakatime.com/badge/github/williamkoller/clean-ts-api) 8 | 9 | 10 | # **Clean Node API** 11 | 12 | Essa API faz parte do treinamento do professor Rodrigo Manguinho (Mango) na Udemy. 13 | 14 | O objetivo do treinamento é mostrar como criar uma API com uma arquitetura bem definida e desacoplada, utilizando TDD (programação orientada a testes) como metodologia de trabalho, Clean Architecture para fazer a distribuição de responsabilidades em camadas, sempre seguindo os princípios do SOLID e, sempre que possível, aplicando Design Patterns para resolver alguns problemas comuns. 15 | 16 | > ## APIs previstas para esse treinamento 17 | 18 | 1. [Cadastro](./requirements/signup.md) 19 | 2. [Login](./requirements/login.md) 20 | 3. [Criar enquete](./requirements/add-survey.md) 21 | 4. [Listar enquetes](./requirements/load-surveys.md) 22 | 5. [Responder enquete](./requirements/save-survey-result.md) 23 | 6. [Resultado da enquete](./requirements/load-survey-result.md) 24 | 25 | > ## Princípios 26 | 27 | * Single Responsibility Principle (SRP) 28 | * Open Closed Principle (OCP) 29 | * Liskov Substitution Principle (LSP) 30 | * Interface Segregation Principle (ISP) 31 | * Dependency Inversion Principle (DIP) 32 | * Don't Repeat Yourself (DRY) 33 | * You Aren't Gonna Need It (YAGNI) 34 | * Keep It Simple, Silly (KISS) 35 | * Composition Over Inheritance 36 | * Small Commits 37 | 38 | > ## Design Patterns 39 | 40 | * Factory 41 | * Adapter 42 | * Composite 43 | * Decorator 44 | * Proxy 45 | * Dependency Injection 46 | * Abstract Server 47 | * Composition Root 48 | 49 | > ## Metodologias e Designs 50 | 51 | * TDD 52 | * Clean Architecture 53 | * DDD 54 | * Conventional Commits 55 | * GitFlow 56 | * Modular Design 57 | * Dependency Diagrams 58 | * Use Cases 59 | * Continuous Integration 60 | * Continuous Delivery 61 | * Continuous Deployment 62 | 63 | > ## Bibliotecas e Ferramentas 64 | 65 | * NPM 66 | * Typescript 67 | * Git 68 | * Docker 69 | * Jest 70 | * MongoDb 71 | * Travis CI 72 | * Coveralls 73 | * Bcrypt 74 | * JsonWebToken 75 | * Validator 76 | * Express 77 | * Supertest 78 | * Husky 79 | * Lint Staged 80 | * Eslint 81 | * Standard Javascript Style 82 | * Sucrase 83 | * Nodemon 84 | * Rimraf 85 | * In-Memory MongoDb Server 86 | * MockDate 87 | * Module-Alias 88 | 89 | > ## Features do Node 90 | 91 | * Log de Erro 92 | * Segurança (Hashing, Encryption e Encoding) 93 | * CORS 94 | * Middlewares 95 | * Nível de Acesso nas Rotas (Admin, User e Anônimo) 96 | * Deploy no Heroku 97 | 98 | > ## Features do Git 99 | 100 | * Alias 101 | * Log Personalizado 102 | * Branch 103 | * Reset 104 | * Amend 105 | * Tag 106 | * Stash 107 | * Rebase 108 | * Merge 109 | 110 | > ## Features do Typescript 111 | 112 | * POO Avançado 113 | * Interface 114 | * TypeAlias 115 | * Utility Types 116 | * Modularização de Paths 117 | * Build 118 | * Deploy 119 | * Uso de Breakpoints 120 | 121 | > ## Features de Testes 122 | 123 | * Testes Unitários 124 | * Testes de Integração 125 | * Cobertura de Testes 126 | * Mocks 127 | * Stubs 128 | * Spies -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | mongodb: 4 | container_name: mongodb-container 5 | image: mongo:3 6 | restart: always 7 | volumes: 8 | - ./data:/data/db 9 | ports: 10 | - "27017:27017" 11 | api: 12 | container_name: api-node-container 13 | image: node:12 14 | working_dir: /usr/src/clean-node-api 15 | restart: always 16 | command: bash -c "npm install --only=prod && npm run debug" 17 | environment: 18 | - MONGODB_URI=mongodb://mongodb:27017/clean-node-api 19 | volumes: 20 | - ./dist/:/usr/src/clean-node-api/dist/ 21 | - ./package.json:/usr/src/clean-node-api/package.json 22 | ports: 23 | - "5050:5050" 24 | - "9222:9222" 25 | links: 26 | - mongodb -------------------------------------------------------------------------------- /jest-integration-config.js: -------------------------------------------------------------------------------- 1 | const config = require('./jest.config') 2 | config.testMatch = ['**/*.test.ts'] 3 | module.exports = config 4 | -------------------------------------------------------------------------------- /jest-mongodb-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mongodbMemoryServerOptions: { 3 | instance: { 4 | dbName: 'jest' 5 | }, 6 | binary: { 7 | version: '4.0.3', 8 | skipMD5: true 9 | }, 10 | autoStart: false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /jest-unit-config.js: -------------------------------------------------------------------------------- 1 | const config = require('./jest.config') 2 | config.testMatch = ['**/*.spec.ts'] 3 | module.exports = config 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src'], 3 | collectCoverageFrom: [ 4 | '/src/**/*.ts', 5 | '!/src/main/**', 6 | '!/src/**/*-protocols.ts', 7 | '!**/protocols/**', 8 | '!**/test/**' 9 | ], 10 | coverageDirectory: 'coverage', 11 | testEnvironment: 'node', 12 | preset: '@shelf/jest-mongodb', 13 | transform: { 14 | '.+\\.ts$': 'ts-jest' 15 | }, 16 | moduleNameMapper: { 17 | '@/(.*)': '/src/$1' 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clean-node-api", 3 | "version": "2.5.6", 4 | "description": "NodeJS Rest API using TDD, Clean Architecture and Typescript", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node dist/main/server.js", 8 | "build": "rimraf dist && tsc -p tsconfig-build.json", 9 | "postbuild": "copyfiles -u 1 public/**/* dist/static", 10 | "debug": "nodemon -L --watch ./dist --inspect=0.0.0.0:9222 --nolazy ./dist/main/server.js", 11 | "up": "npm run build && docker-compose up --remove-orphans", 12 | "down": "docker-compose down", 13 | "check": "npm-check -s -u", 14 | "test": "jest --passWithNoTests --silent --noStackTrace --runInBand", 15 | "test:verbose": "jest --passWithNoTests --runInBand", 16 | "test:unit": "npm test -- --watch -c jest-unit-config.js", 17 | "test:integration": "npm test -- --watch -c jest-integration-config.js", 18 | "test:staged": "npm test -- --findRelatedTests", 19 | "test:ci": "npm test -- --coverage", 20 | "test:coveralls": "npm run test:ci && coveralls < coverage/lcov.info" 21 | }, 22 | "author": "William Koller", 23 | "license": "GPL-3.0-or-later", 24 | "devDependencies": { 25 | "@shelf/jest-mongodb": "^2.1.0", 26 | "@types/bcrypt": "^5.0.0", 27 | "@types/express": "^4.17.8", 28 | "@types/faker": "^5.1.0", 29 | "@types/jest": "^27.0.2", 30 | "@types/jsonwebtoken": "^8.5.0", 31 | "@types/mongodb": "^4.0.7", 32 | "@types/node": "^16.10.1", 33 | "@types/supertest": "^2.0.10", 34 | "@types/swagger-ui-express": "^4.1.2", 35 | "@types/validator": "^13.1.0", 36 | "@typescript-eslint/eslint-plugin": "^4.32.0", 37 | "copyfiles": "^2.3.0", 38 | "coveralls": "^3.1.0", 39 | "eslint": "^7.9.0", 40 | "eslint-config-standard-with-typescript": "^21.0.1", 41 | "eslint-plugin-import": "^2.22.0", 42 | "eslint-plugin-node": "^11.1.0", 43 | "eslint-plugin-promise": "^5.1.0", 44 | "eslint-plugin-standard": "^5.0.0", 45 | "faker": "^5.1.0", 46 | "git-commit-msg-linter": "^3.2.8", 47 | "husky": "^7.0.2", 48 | "jest": "^27.2.2", 49 | "lint-staged": "^11.1.2", 50 | "mockdate": "^3.0.2", 51 | "npm-check": "^5.9.2", 52 | "rimraf": "^3.0.2", 53 | "supertest": "^6.1.6", 54 | "ts-jest": "^27.0.5", 55 | "typescript": "^4.4.3" 56 | }, 57 | "dependencies": { 58 | "bcrypt": "^5.0.0", 59 | "express": "^4.17.1", 60 | "jsonwebtoken": "^8.5.1", 61 | "module-alias": "^2.2.2", 62 | "mongodb": "^4.1.2", 63 | "nodemon": "^2.0.13", 64 | "swagger-ui-express": "^4.1.4", 65 | "validator": "^13.1.1" 66 | }, 67 | "engines": { 68 | "node": "14.x" 69 | }, 70 | "_moduleAliases": { 71 | "@": "dist" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /public/img/logo-angular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-angular.png -------------------------------------------------------------------------------- /public/img/logo-ember.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-ember.png -------------------------------------------------------------------------------- /public/img/logo-flutter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-flutter.png -------------------------------------------------------------------------------- /public/img/logo-ionic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-ionic.png -------------------------------------------------------------------------------- /public/img/logo-jquery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-jquery.png -------------------------------------------------------------------------------- /public/img/logo-js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-js.png -------------------------------------------------------------------------------- /public/img/logo-knockout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-knockout.png -------------------------------------------------------------------------------- /public/img/logo-native-script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-native-script.png -------------------------------------------------------------------------------- /public/img/logo-nativo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-nativo.png -------------------------------------------------------------------------------- /public/img/logo-npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-npm.png -------------------------------------------------------------------------------- /public/img/logo-phonegap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-phonegap.png -------------------------------------------------------------------------------- /public/img/logo-polymer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-polymer.png -------------------------------------------------------------------------------- /public/img/logo-react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-react.png -------------------------------------------------------------------------------- /public/img/logo-riot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-riot.png -------------------------------------------------------------------------------- /public/img/logo-svelte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-svelte.png -------------------------------------------------------------------------------- /public/img/logo-titanium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-titanium.png -------------------------------------------------------------------------------- /public/img/logo-ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-ts.png -------------------------------------------------------------------------------- /public/img/logo-vue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-vue.png -------------------------------------------------------------------------------- /public/img/logo-xamarin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-xamarin.png -------------------------------------------------------------------------------- /public/img/logo-yarn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamkoller/clean-ts-api/cc445e23c29dded73350f6da063a927bc983b0db/public/img/logo-yarn.png -------------------------------------------------------------------------------- /requirements/add-survey.md: -------------------------------------------------------------------------------- 1 | # Criar enquete 2 | 3 | > ## Caso de sucesso 4 | 5 | 1. ✅ Recebe uma requisição do tipo **POST** na rota **/api/surveys** 6 | 2. ✅ Valida se a requisição foi feita por um **admin** 7 | 3. ✅ Valida dados obrigatórios **question** e **answers** 8 | 4. ✅ **Cria** uma enquete com os dados fornecidos 9 | 5. ✅ Retorna **204**, sem dados 10 | 11 | > ## Exceções 12 | 13 | 1. ✅ Retorna erro **404** se a API não existir 14 | 2. ✅ Retorna erro **403** se o usuário não for admin 15 | 3. ✅ Retorna erro **400** se question ou answers não forem fornecidos pelo client 16 | 4. ✅ Retorna erro **500** se der erro ao tentar criar a enquete -------------------------------------------------------------------------------- /requirements/load-survey-result.md: -------------------------------------------------------------------------------- 1 | # Resultado da enquete 2 | 3 | > ## Caso de sucesso 4 | 5 | 1. ✅ Recebe uma requisição do tipo **GET** na rota **/api/surveys/{survey_id}/results** 6 | 2. ✅ Valida se a requisição foi feita por um **usuário** 7 | 3. ✅ Retorna **200** com os dados do resultado da enquete 8 | 9 | > ## Exceções 10 | 11 | 1. ✅ Retorna erro **404** se a API não existir 12 | 2. ✅ Retorna erro **403** se não for um usuário 13 | 3. ✅ Retorna erro **500** se der erro ao tentar listar o resultado da enquete -------------------------------------------------------------------------------- /requirements/load-surveys.md: -------------------------------------------------------------------------------- 1 | # Listar enquetes 2 | 3 | > ## Caso de sucesso 4 | 5 | 1. ✅ Recebe uma requisição do tipo **GET** na rota **/api/surveys** 6 | 2. ✅ Valida se a requisição foi feita por um **usuário** 7 | 3. ✅ Retorna **204** se não tiver nenhuma enquete 8 | 4. ✅ Retorna **200** com os dados das enquetes 9 | 10 | > ## Exceções 11 | 12 | 1. ✅ Retorna erro **404** se a API não existir 13 | 2. ✅ Retorna erro **403** se não for um usuário 14 | 3. ✅ Retorna erro **500** se der erro ao tentar listar as enquetes -------------------------------------------------------------------------------- /requirements/login.md: -------------------------------------------------------------------------------- 1 | # Login 2 | 3 | > ## Caso de sucesso 4 | 5 | 1. ✅ Recebe uma requisição do tipo **POST** na rota **/api/login** 6 | 2. ✅ Valida dados obrigatórios **email** e **password** 7 | 3. ✅ Valida que o campo **email** é um e-mail válido 8 | 4. ✅ **Busca** o usuário com o email e senha fornecidos 9 | 5. ✅ Gera um **token** de acesso a partir do ID do usuário 10 | 6. ✅ **Atualiza** os dados do usuário com o token de acesso gerado 11 | 7. ✅ Retorna **200** com o token de acesso e o nome do usuário 12 | 13 | > ## Exceções 14 | 15 | 1. ✅ Retorna erro **404** se a API não existir 16 | 2. ✅ Retorna erro **400** se email ou password não forem fornecidos pelo client 17 | 3. ✅ Retorna erro **400** se o campo email for um e-mail inválido 18 | 4. ✅ Retorna erro **401** se não encontrar um usuário com os dados fornecidos 19 | 5. ✅ Retorna erro **500** se der erro ao tentar gerar o token de acesso 20 | 6. ✅ Retorna erro **500** se der erro ao tentar atualizar o usuário com o token de acesso gerado -------------------------------------------------------------------------------- /requirements/save-survey-result.md: -------------------------------------------------------------------------------- 1 | # Responder enquete 2 | 3 | > ## Caso de sucesso 4 | 5 | 1. ✅ Recebe uma requisição do tipo **PUT** na rota **/api/surveys/{survey_id}/results** 6 | 2. ✅ Valida se a requisição foi feita por um **usuário** 7 | 3. ✅ Valida o parâmetro **survey_id** 8 | 4. ✅ Valida se o campo **answer** é uma resposta válida 9 | 5. ✅ **Cria** um resultado de enquete com os dados fornecidos caso não tenha um registro 10 | 6. ✅ **Atualiza** um resultado de enquete com os dados fornecidos caso já tenha um registro 11 | 7. ✅ Retorna **200** com os dados do resultado da enquete 12 | 13 | > ## Exceções 14 | 15 | 1. ✅ Retorna erro **404** se a API não existir 16 | 2. ✅ Retorna erro **403** se não for um usuário 17 | 3. ✅ Retorna erro **403** se o survey_id passado na URL for inválido 18 | 4. ✅ Retorna erro **403** se a resposta enviada pelo client for uma resposta inválida 19 | 5. ✅ Retorna erro **500** se der erro ao tentar criar o resultado da enquete 20 | 6. ✅ Retorna erro **500** se der erro ao tentar atualizar o resultado da enquete 21 | 7. ✅ Retorna erro **500** se der erro ao tentar carregar a enquete -------------------------------------------------------------------------------- /requirements/signup.md: -------------------------------------------------------------------------------- 1 | # Cadastro 2 | 3 | > ## Caso de sucesso 4 | 5 | 1. ✅ Recebe uma requisição do tipo **POST** na rota **/api/signup** 6 | 2. ✅ Valida dados obrigatórios **name**, **email**, **password** e **passwordConfirmation** 7 | 3. ✅ Valida que **password** e **passwordConfirmation** são iguais 8 | 4. ✅ Valida que o campo **email** é um e-mail válido 9 | 5. ✅ **Valida** se já existe um usuário com o email fornecido 10 | 6. ✅ Gera uma senha **criptografada** (essa senha não pode ser descriptografada) 11 | 7. ✅ **Cria** uma conta para o usuário com os dados informados, **substituindo** a senha pela senha criptorafada 12 | 8. ✅ Gera um **token** de acesso a partir do ID do usuário 13 | 9. ✅ **Atualiza** os dados do usuário com o token de acesso gerado 14 | 10. ✅ Retorna **200** com o token de acesso e o nome do usuário 15 | 16 | > ## Exceções 17 | 18 | 1. ✅ Retorna erro **404** se a API não existir 19 | 2. ✅ Retorna erro **400** se name, email, password ou passwordConfirmation não forem fornecidos pelo client 20 | 3. ✅ Retorna erro **400** se password e passwordConfirmation não forem iguais 21 | 4. ✅ Retorna erro **400** se o campo email for um e-mail inválido 22 | 5. ✅ Retorna erro **403** se o email fornecido já estiver em uso 23 | 6. ✅ Retorna erro **500** se der erro ao tentar gerar uma senha criptografada 24 | 7. ✅ Retorna erro **500** se der erro ao tentar criar a conta do usuário 25 | 8. ✅ Retorna erro **500** se der erro ao tentar gerar o token de acesso 26 | 9. ✅ Retorna erro **500** se der erro ao tentar atualizar o usuário com o token de acesso gerado -------------------------------------------------------------------------------- /src/data/protocols/criptography/decrypter.ts: -------------------------------------------------------------------------------- 1 | export interface Decrypter { 2 | decrypt: (ciphertext: string) => Promise 3 | } 4 | -------------------------------------------------------------------------------- /src/data/protocols/criptography/encrypter.ts: -------------------------------------------------------------------------------- 1 | export interface Encrypter { 2 | encrypt: (plaintext: string) => Promise 3 | } 4 | -------------------------------------------------------------------------------- /src/data/protocols/criptography/hash-comparer.ts: -------------------------------------------------------------------------------- 1 | export interface HashComparer { 2 | compare: (plaintext: string, digest: string) => Promise 3 | } 4 | -------------------------------------------------------------------------------- /src/data/protocols/criptography/hasher.ts: -------------------------------------------------------------------------------- 1 | export interface Hasher { 2 | hash: (plaintext: string) => Promise 3 | } 4 | -------------------------------------------------------------------------------- /src/data/protocols/db/account/add-account-repository.ts: -------------------------------------------------------------------------------- 1 | import { AddAccountParams } from '@/domain/usecases/account/add-account' 2 | import { AccountModel } from '@/domain/models/account/account' 3 | 4 | export interface AddAccountRepository { 5 | add: (data: AddAccountParams) => Promise 6 | } 7 | -------------------------------------------------------------------------------- /src/data/protocols/db/account/load-account-by-email-repository.ts: -------------------------------------------------------------------------------- 1 | import { AccountModel } from '@/domain/models/account/account' 2 | 3 | export interface LoadAccountByEmailRepository { 4 | loadByEmail: (email: string) => Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/data/protocols/db/account/load-account-by-token-repository.ts: -------------------------------------------------------------------------------- 1 | import { AccountModel } from '@/domain/models/account/account' 2 | 3 | export interface LoadAccountByTokenRepository { 4 | loadByToken: (token: string, role?: string) => Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/data/protocols/db/account/update-access-token-repository.ts: -------------------------------------------------------------------------------- 1 | export interface UpdateAccessTokenRepository { 2 | updateAccessToken: (id: string, token: string) => Promise 3 | } 4 | -------------------------------------------------------------------------------- /src/data/protocols/db/log/log-error-repository.ts: -------------------------------------------------------------------------------- 1 | export interface LogErrorRepository { 2 | logError: (stack: string) => Promise 3 | } 4 | -------------------------------------------------------------------------------- /src/data/protocols/db/survey-result/load-survey-result-repository.ts: -------------------------------------------------------------------------------- 1 | import { SurveyResultModel } from '@/domain/models/survey-result/survey-result' 2 | 3 | export interface LoadSurveyResultRepository { 4 | loadBySurveyId: (surveyId: string) => Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/data/protocols/db/survey-result/save-survey-result-repository.ts: -------------------------------------------------------------------------------- 1 | import { SaveSurveyResultParams } from '@/domain/usecases/survey-result/save-survey-result' 2 | 3 | export interface SaveSurveyResultRepository { 4 | save: (data: SaveSurveyResultParams) => Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/data/protocols/db/survey/add-survey-repository.ts: -------------------------------------------------------------------------------- 1 | import { AddSurveyParams } from '@/domain/usecases/survey/add-survey' 2 | 3 | export interface AddSurveyRepository { 4 | add: (data: AddSurveyParams) => Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/data/protocols/db/survey/load-survey-by-id-repository.ts: -------------------------------------------------------------------------------- 1 | import { SurveyModel } from '@/domain/models/survey/survey' 2 | 3 | export interface LoadSurveyByIdRepository { 4 | loadById: (id: string) => Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/data/protocols/db/survey/load-surveys-repository.ts: -------------------------------------------------------------------------------- 1 | import { SurveyModel } from '@/domain/models/survey/survey' 2 | 3 | export interface LoadSurveysRepository { 4 | loadAll: (accountId: string) => Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/data/test/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mock-criptography' 2 | export * from './mock-db-account' 3 | export * from './mock-db-log' 4 | export * from './mock-db-survey' 5 | export * from './mock-db-survey-result' 6 | -------------------------------------------------------------------------------- /src/data/test/mock-criptography.ts: -------------------------------------------------------------------------------- 1 | import { Hasher } from '@/data/protocols/criptography/hasher' 2 | import { HashComparer } from '@/data/protocols/criptography/hash-comparer' 3 | import { Encrypter } from '@/data/protocols/criptography/encrypter' 4 | import { Decrypter } from '@/data/protocols/criptography/decrypter' 5 | import faker from 'faker' 6 | 7 | export class HasherSpy implements Hasher { 8 | digest = faker.random.uuid() 9 | plaintext: string 10 | 11 | async hash (plaintext: string): Promise { 12 | this.plaintext = plaintext 13 | return this.digest 14 | } 15 | } 16 | 17 | export class HashComparerSpy implements HashComparer { 18 | plaintext: string 19 | digest: string 20 | isValid = true 21 | 22 | async compare (plaintext: string, digest: string): Promise { 23 | this.plaintext = plaintext 24 | this.digest = digest 25 | return this.isValid 26 | } 27 | } 28 | 29 | export class EncrypterSpy implements Encrypter { 30 | ciphertext = faker.random.uuid() 31 | plaintext: string 32 | 33 | async encrypt (plaintext: string): Promise { 34 | this.plaintext = plaintext 35 | return this.ciphertext 36 | } 37 | } 38 | 39 | export class DecrypterSpy implements Decrypter { 40 | plaintext = faker.internet.password() 41 | ciphertext: string 42 | 43 | async decrypt (ciphertext: string): Promise { 44 | this.ciphertext = ciphertext 45 | return this.plaintext 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/data/test/mock-db-account.ts: -------------------------------------------------------------------------------- 1 | import { AddAccountRepository } from '@/data/protocols/db/account/add-account-repository' 2 | import { LoadAccountByEmailRepository } from '@/data/protocols/db/account/load-account-by-email-repository' 3 | import { LoadAccountByTokenRepository } from '@/data/protocols/db/account/load-account-by-token-repository' 4 | import { UpdateAccessTokenRepository } from '@/data/protocols/db/account/update-access-token-repository' 5 | import { AddAccountParams } from '@/domain/usecases/account/add-account' 6 | import { AccountModel } from '@/domain/models/account/account' 7 | import { mockAccountModel } from '@/domain/test' 8 | 9 | export class AddAccountRepositorySpy implements AddAccountRepository { 10 | accountModel = mockAccountModel() 11 | addAccountParams: AddAccountParams 12 | 13 | async add (data: AddAccountParams): Promise { 14 | this.addAccountParams = data 15 | return this.accountModel 16 | } 17 | } 18 | 19 | export class LoadAccountByEmailRepositorySpy implements LoadAccountByEmailRepository { 20 | accountModel = mockAccountModel() 21 | email: string 22 | 23 | async loadByEmail (email: string): Promise { 24 | this.email = email 25 | return this.accountModel 26 | } 27 | } 28 | 29 | export class LoadAccountByTokenRepositorySpy implements LoadAccountByTokenRepository { 30 | accountModel = mockAccountModel() 31 | token: string 32 | role: string 33 | 34 | async loadByToken (token: string, role?: string): Promise { 35 | this.token = token 36 | this.role = role 37 | return this.accountModel 38 | } 39 | } 40 | 41 | export class UpdateAccessTokenRepositorySpy implements UpdateAccessTokenRepository { 42 | id: string 43 | token: string 44 | 45 | async updateAccessToken (id: string, token: string): Promise { 46 | this.id = id 47 | this.token = token 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/data/test/mock-db-log.ts: -------------------------------------------------------------------------------- 1 | import { LogErrorRepository } from '@/data/protocols/db/log/log-error-repository' 2 | 3 | export class LogErrorRepositorySpy implements LogErrorRepository { 4 | stack: string 5 | 6 | async logError (stack: string): Promise { 7 | this.stack = stack 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/data/test/mock-db-survey-result.ts: -------------------------------------------------------------------------------- 1 | import { SaveSurveyResultRepository } from '@/data/protocols/db/survey-result/save-survey-result-repository' 2 | import { LoadSurveyResultRepository } from '@/data/protocols/db/survey-result/load-survey-result-repository' 3 | import { SaveSurveyResultParams } from '@/domain/usecases/survey-result/save-survey-result' 4 | import { SurveyResultModel } from '@/domain/models/survey-result/survey-result' 5 | import { mockSurveyResultModel } from '@/domain/test' 6 | 7 | export class SaveSurveyResultRepositorySpy implements SaveSurveyResultRepository { 8 | saveSurveyResultParams: SaveSurveyResultParams 9 | 10 | async save (data: SaveSurveyResultParams): Promise { 11 | this.saveSurveyResultParams = data 12 | } 13 | } 14 | 15 | export class LoadSurveyResultRepositorySpy implements LoadSurveyResultRepository { 16 | surveyResultModel = mockSurveyResultModel() 17 | surveyId: string 18 | 19 | async loadBySurveyId (surveyId: string): Promise { 20 | this.surveyId = surveyId 21 | return this.surveyResultModel 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/data/test/mock-db-survey.ts: -------------------------------------------------------------------------------- 1 | import { AddSurveyRepository } from '@/data/protocols/db/survey/add-survey-repository' 2 | import { LoadSurveyByIdRepository } from '@/data/protocols/db/survey/load-survey-by-id-repository' 3 | import { LoadSurveysRepository } from '@/data/protocols/db/survey/load-surveys-repository' 4 | import { AddSurveyParams } from '@/domain/usecases/survey/add-survey' 5 | import { SurveyModel } from '@/domain/models/survey/survey' 6 | import { mockSurveyModel, mockSurveyModels } from '@/domain/test' 7 | 8 | export class AddSurveyRepositorySpy implements AddSurveyRepository { 9 | addSurveyParams: AddSurveyParams 10 | 11 | async add (data: AddSurveyParams): Promise { 12 | this.addSurveyParams = data 13 | } 14 | } 15 | 16 | export class LoadSurveyByIdRepositorySpy implements LoadSurveyByIdRepository { 17 | surveyModel = mockSurveyModel() 18 | id: string 19 | 20 | async loadById (id: string): Promise { 21 | this.id = id 22 | return this.surveyModel 23 | } 24 | } 25 | 26 | export class LoadSurveysRepositorySpy implements LoadSurveysRepository { 27 | surveyModels = mockSurveyModels() 28 | accountId: string 29 | 30 | async loadAll (accountId: string): Promise { 31 | this.accountId = accountId 32 | return this.surveyModels 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/data/usecases/account/add-account/db-add-account-protocols.ts: -------------------------------------------------------------------------------- 1 | export * from '@/domain/usecases/account/add-account' 2 | export * from '@/domain/models/account/account' 3 | export * from '@/data/protocols/criptography/hasher' 4 | export * from '@/data/protocols/db/account/add-account-repository' 5 | export * from '@/data/protocols/db/account/load-account-by-email-repository' 6 | -------------------------------------------------------------------------------- /src/data/usecases/account/add-account/db-add-account.spec.ts: -------------------------------------------------------------------------------- 1 | import { DbAddAccount } from './db-add-account' 2 | import { HasherSpy, AddAccountRepositorySpy, LoadAccountByEmailRepositorySpy } from '@/data/test' 3 | import { mockAccountModel, mockAddAccountParams, throwError } from '@/domain/test' 4 | 5 | type SutTypes = { 6 | sut: DbAddAccount 7 | hasherSpy: HasherSpy 8 | addAccountRepositorySpy: AddAccountRepositorySpy 9 | loadAccountByEmailRepositorySpy: LoadAccountByEmailRepositorySpy 10 | } 11 | 12 | const makeSut = (): SutTypes => { 13 | const loadAccountByEmailRepositorySpy = new LoadAccountByEmailRepositorySpy() 14 | loadAccountByEmailRepositorySpy.accountModel = null 15 | const hasherSpy = new HasherSpy() 16 | const addAccountRepositorySpy = new AddAccountRepositorySpy() 17 | const sut = new DbAddAccount(hasherSpy, addAccountRepositorySpy, loadAccountByEmailRepositorySpy) 18 | return { 19 | sut, 20 | hasherSpy, 21 | addAccountRepositorySpy, 22 | loadAccountByEmailRepositorySpy 23 | } 24 | } 25 | 26 | describe('DbAddAccount Usecase', () => { 27 | test('Should call Hasher with correct plaintext', async () => { 28 | const { sut, hasherSpy } = makeSut() 29 | const addAccountParams = mockAddAccountParams() 30 | await sut.add(addAccountParams) 31 | expect(hasherSpy.plaintext).toBe(addAccountParams.password) 32 | }) 33 | 34 | test('Should throw if Hasher throws', async () => { 35 | const { sut, hasherSpy } = makeSut() 36 | jest.spyOn(hasherSpy, 'hash').mockImplementationOnce(throwError) 37 | const promise = sut.add(mockAddAccountParams()) 38 | await expect(promise).rejects.toThrow() 39 | }) 40 | 41 | test('Should call AddAccountRepository with correct values', async () => { 42 | const { sut, addAccountRepositorySpy, hasherSpy } = makeSut() 43 | const addAccountParams = mockAddAccountParams() 44 | await sut.add(addAccountParams) 45 | expect(addAccountRepositorySpy.addAccountParams).toEqual({ 46 | name: addAccountParams.name, 47 | email: addAccountParams.email, 48 | password: hasherSpy.digest 49 | }) 50 | }) 51 | 52 | test('Should throw if AddAccountRepository throws', async () => { 53 | const { sut, addAccountRepositorySpy } = makeSut() 54 | jest.spyOn(addAccountRepositorySpy, 'add').mockImplementationOnce(throwError) 55 | const promise = sut.add(mockAddAccountParams()) 56 | await expect(promise).rejects.toThrow() 57 | }) 58 | 59 | test('Should return an account on success', async () => { 60 | const { sut, addAccountRepositorySpy } = makeSut() 61 | const account = await sut.add(mockAddAccountParams()) 62 | expect(account).toEqual(addAccountRepositorySpy.accountModel) 63 | }) 64 | 65 | test('Should return null if LoadAccountByEmailRepository not return null', async () => { 66 | const { sut, loadAccountByEmailRepositorySpy } = makeSut() 67 | loadAccountByEmailRepositorySpy.accountModel = mockAccountModel() 68 | const account = await sut.add(mockAddAccountParams()) 69 | expect(account).toBeNull() 70 | }) 71 | 72 | test('Should call LoadAccountByEmailRepository with correct email', async () => { 73 | const { sut, loadAccountByEmailRepositorySpy } = makeSut() 74 | const addAccountParams = mockAddAccountParams() 75 | await sut.add(addAccountParams) 76 | expect(loadAccountByEmailRepositorySpy.email).toBe(addAccountParams.email) 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /src/data/usecases/account/add-account/db-add-account.ts: -------------------------------------------------------------------------------- 1 | import { AddAccount, AddAccountParams, AccountModel, Hasher, AddAccountRepository, LoadAccountByEmailRepository } from './db-add-account-protocols' 2 | 3 | export class DbAddAccount implements AddAccount { 4 | constructor ( 5 | private readonly hasher: Hasher, 6 | private readonly addAccountRepository: AddAccountRepository, 7 | private readonly loadAccountByEmailRepository: LoadAccountByEmailRepository 8 | ) {} 9 | 10 | async add (accountData: AddAccountParams): Promise { 11 | const account = await this.loadAccountByEmailRepository.loadByEmail(accountData.email) 12 | if (!account) { 13 | const hashedPassword = await this.hasher.hash(accountData.password) 14 | const newAccount = await this.addAccountRepository.add(Object.assign({}, accountData, { password: hashedPassword })) 15 | return newAccount 16 | } 17 | return null 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/data/usecases/account/authentication/db-authentication-protocols.ts: -------------------------------------------------------------------------------- 1 | export * from '@/domain/models/account/account' 2 | export * from '@/domain/models/account/authentication' 3 | export * from '@/domain/usecases/account/authentication' 4 | export * from '@/data/protocols/db/account/load-account-by-email-repository' 5 | export * from '@/data/protocols/db/account/update-access-token-repository' 6 | export * from '@/data/protocols/criptography/hash-comparer' 7 | export * from '@/data/protocols/criptography/encrypter' 8 | -------------------------------------------------------------------------------- /src/data/usecases/account/authentication/db-authentication.spec.ts: -------------------------------------------------------------------------------- 1 | import { DbAuthentication } from './db-authentication' 2 | import { HashComparerSpy, EncrypterSpy, UpdateAccessTokenRepositorySpy, LoadAccountByEmailRepositorySpy } from '@/data/test' 3 | import { throwError, mockAuthenticationParams } from '@/domain/test' 4 | 5 | type SutTypes = { 6 | sut: DbAuthentication 7 | loadAccountByEmailRepositorySpy: LoadAccountByEmailRepositorySpy 8 | hashComparerSpy: HashComparerSpy 9 | encrypterSpy: EncrypterSpy 10 | updateAccessTokenRepositorySpy: UpdateAccessTokenRepositorySpy 11 | } 12 | 13 | const makeSut = (): SutTypes => { 14 | const loadAccountByEmailRepositorySpy = new LoadAccountByEmailRepositorySpy() 15 | const hashComparerSpy = new HashComparerSpy() 16 | const encrypterSpy = new EncrypterSpy() 17 | const updateAccessTokenRepositorySpy = new UpdateAccessTokenRepositorySpy() 18 | const sut = new DbAuthentication( 19 | loadAccountByEmailRepositorySpy, 20 | hashComparerSpy, 21 | encrypterSpy, 22 | updateAccessTokenRepositorySpy 23 | ) 24 | return { 25 | sut, 26 | loadAccountByEmailRepositorySpy, 27 | hashComparerSpy, 28 | encrypterSpy, 29 | updateAccessTokenRepositorySpy 30 | } 31 | } 32 | 33 | describe('DbAuthentication UseCase', () => { 34 | test('Should call LoadAccountByEmailRepository with correct email', async () => { 35 | const { sut, loadAccountByEmailRepositorySpy } = makeSut() 36 | const authenticationParams = mockAuthenticationParams() 37 | await sut.auth(authenticationParams) 38 | expect(loadAccountByEmailRepositorySpy.email).toBe(authenticationParams.email) 39 | }) 40 | 41 | test('Should throw if LoadAccountByEmailRepository throws', async () => { 42 | const { sut, loadAccountByEmailRepositorySpy } = makeSut() 43 | jest.spyOn(loadAccountByEmailRepositorySpy, 'loadByEmail').mockImplementationOnce(throwError) 44 | const promise = sut.auth(mockAuthenticationParams()) 45 | await expect(promise).rejects.toThrow() 46 | }) 47 | 48 | test('Should return null if LoadAccountByEmailRepository returns null', async () => { 49 | const { sut, loadAccountByEmailRepositorySpy } = makeSut() 50 | loadAccountByEmailRepositorySpy.accountModel = null 51 | const model = await sut.auth(mockAuthenticationParams()) 52 | expect(model).toBeNull() 53 | }) 54 | 55 | test('Should call HashComparer with correct values', async () => { 56 | const { sut, hashComparerSpy, loadAccountByEmailRepositorySpy } = makeSut() 57 | const authenticationParams = mockAuthenticationParams() 58 | await sut.auth(authenticationParams) 59 | expect(hashComparerSpy.plaintext).toBe(authenticationParams.password) 60 | expect(hashComparerSpy.digest).toBe(loadAccountByEmailRepositorySpy.accountModel.password) 61 | }) 62 | 63 | test('Should throw if HashComparer throws', async () => { 64 | const { sut, hashComparerSpy } = makeSut() 65 | jest.spyOn(hashComparerSpy, 'compare').mockImplementationOnce(throwError) 66 | const promise = sut.auth(mockAuthenticationParams()) 67 | await expect(promise).rejects.toThrow() 68 | }) 69 | 70 | test('Should return null if HashComparer returns false', async () => { 71 | const { sut, hashComparerSpy } = makeSut() 72 | hashComparerSpy.isValid = false 73 | const model = await sut.auth(mockAuthenticationParams()) 74 | expect(model).toBeNull() 75 | }) 76 | 77 | test('Should call Encrypter with correct plaintext', async () => { 78 | const { sut, encrypterSpy, loadAccountByEmailRepositorySpy } = makeSut() 79 | await sut.auth(mockAuthenticationParams()) 80 | expect(encrypterSpy.plaintext).toBe(loadAccountByEmailRepositorySpy.accountModel.id) 81 | }) 82 | 83 | test('Should throw if Encrypter throws', async () => { 84 | const { sut, encrypterSpy } = makeSut() 85 | jest.spyOn(encrypterSpy, 'encrypt').mockImplementationOnce(throwError) 86 | const promise = sut.auth(mockAuthenticationParams()) 87 | await expect(promise).rejects.toThrow() 88 | }) 89 | 90 | test('Should return an AuthenticationModel on success', async () => { 91 | const { sut, encrypterSpy, loadAccountByEmailRepositorySpy } = makeSut() 92 | const { accessToken, name } = await sut.auth(mockAuthenticationParams()) 93 | expect(accessToken).toBe(encrypterSpy.ciphertext) 94 | expect(name).toBe(loadAccountByEmailRepositorySpy.accountModel.name) 95 | }) 96 | 97 | test('Should call UpdateAccessTokenRepository with correct values', async () => { 98 | const { sut, updateAccessTokenRepositorySpy, loadAccountByEmailRepositorySpy, encrypterSpy } = makeSut() 99 | await sut.auth(mockAuthenticationParams()) 100 | expect(updateAccessTokenRepositorySpy.id).toBe(loadAccountByEmailRepositorySpy.accountModel.id) 101 | expect(updateAccessTokenRepositorySpy.token).toBe(encrypterSpy.ciphertext) 102 | }) 103 | 104 | test('Should throw if UpdateAccessTokenRepository throws', async () => { 105 | const { sut, updateAccessTokenRepositorySpy } = makeSut() 106 | jest.spyOn(updateAccessTokenRepositorySpy, 'updateAccessToken').mockImplementationOnce(throwError) 107 | const promise = sut.auth(mockAuthenticationParams()) 108 | await expect(promise).rejects.toThrow() 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /src/data/usecases/account/authentication/db-authentication.ts: -------------------------------------------------------------------------------- 1 | import { Authentication, AuthenticationParams, LoadAccountByEmailRepository, HashComparer, Encrypter, UpdateAccessTokenRepository, AuthenticationModel } from './db-authentication-protocols' 2 | 3 | export class DbAuthentication implements Authentication { 4 | constructor ( 5 | private readonly loadAccountByEmailRepository: LoadAccountByEmailRepository, 6 | private readonly hashComparer: HashComparer, 7 | private readonly encrypter: Encrypter, 8 | private readonly updateAccessTokenRepository: UpdateAccessTokenRepository 9 | ) {} 10 | 11 | async auth (authenticationParams: AuthenticationParams): Promise { 12 | const account = await this.loadAccountByEmailRepository.loadByEmail(authenticationParams.email) 13 | if (account) { 14 | const isValid = await this.hashComparer.compare(authenticationParams.password, account.password) 15 | if (isValid) { 16 | const accessToken = await this.encrypter.encrypt(account.id) 17 | await this.updateAccessTokenRepository.updateAccessToken(account.id, accessToken) 18 | return { 19 | accessToken, 20 | name: account.name 21 | } 22 | } 23 | } 24 | return null 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/data/usecases/account/load-account-by-token/db-load-account-by-token-protocols.ts: -------------------------------------------------------------------------------- 1 | export * from '@/domain/usecases/account/load-account-by-token' 2 | export * from '@/data/protocols/criptography/decrypter' 3 | export * from '@/data/protocols/db/account/load-account-by-token-repository' 4 | export * from '@/domain/models/account/account' 5 | export * from '@/domain/usecases/account/load-account-by-token' 6 | -------------------------------------------------------------------------------- /src/data/usecases/account/load-account-by-token/db-load-account-by-token.spec.ts: -------------------------------------------------------------------------------- 1 | import { DbLoadAccountByToken } from './db-load-account-by-token' 2 | import { DecrypterSpy, LoadAccountByTokenRepositorySpy } from '@/data/test' 3 | import { throwError } from '@/domain/test' 4 | import faker from 'faker' 5 | 6 | type SutTypes = { 7 | sut: DbLoadAccountByToken 8 | decrypterSpy: DecrypterSpy 9 | loadAccountByTokenRepositorySpy: LoadAccountByTokenRepositorySpy 10 | } 11 | 12 | const makeSut = (): SutTypes => { 13 | const decrypterSpy = new DecrypterSpy() 14 | const loadAccountByTokenRepositorySpy = new LoadAccountByTokenRepositorySpy() 15 | const sut = new DbLoadAccountByToken(decrypterSpy, loadAccountByTokenRepositorySpy) 16 | return { 17 | sut, 18 | decrypterSpy, 19 | loadAccountByTokenRepositorySpy 20 | } 21 | } 22 | 23 | let token: string 24 | let role: string 25 | 26 | describe('DbLoadAccountByToken Usecase', () => { 27 | beforeEach(() => { 28 | token = faker.random.uuid() 29 | role = faker.random.word() 30 | }) 31 | 32 | test('Should call Decrypter with correct ciphertext', async () => { 33 | const { sut, decrypterSpy } = makeSut() 34 | await sut.load(token, role) 35 | expect(decrypterSpy.ciphertext).toBe(token) 36 | }) 37 | 38 | test('Should return null if Decrypter returns null', async () => { 39 | const { sut, decrypterSpy } = makeSut() 40 | decrypterSpy.plaintext = null 41 | const account = await sut.load(token, role) 42 | expect(account).toBeNull() 43 | }) 44 | 45 | test('Should call LoadAccountByTokenRepository with correct values', async () => { 46 | const { sut, loadAccountByTokenRepositorySpy } = makeSut() 47 | await sut.load(token, role) 48 | expect(loadAccountByTokenRepositorySpy.token).toBe(token) 49 | expect(loadAccountByTokenRepositorySpy.role).toBe(role) 50 | }) 51 | 52 | test('Should return null if LoadAccountByTokenRepository returns null', async () => { 53 | const { sut, loadAccountByTokenRepositorySpy } = makeSut() 54 | loadAccountByTokenRepositorySpy.accountModel = null 55 | const account = await sut.load(token, role) 56 | expect(account).toBeNull() 57 | }) 58 | 59 | test('Should return an account on success', async () => { 60 | const { sut, loadAccountByTokenRepositorySpy } = makeSut() 61 | const account = await sut.load(token, role) 62 | expect(account).toEqual(loadAccountByTokenRepositorySpy.accountModel) 63 | }) 64 | 65 | test('Should throw if Decrypter throws', async () => { 66 | const { sut, decrypterSpy } = makeSut() 67 | jest.spyOn(decrypterSpy, 'decrypt').mockImplementationOnce(throwError) 68 | const promise = sut.load(token, role) 69 | await expect(promise).rejects.toThrow() 70 | }) 71 | 72 | test('Should throw if LoadAccountByTokenRepository throws', async () => { 73 | const { sut, loadAccountByTokenRepositorySpy } = makeSut() 74 | jest.spyOn(loadAccountByTokenRepositorySpy, 'loadByToken').mockImplementationOnce(throwError) 75 | const promise = sut.load(token, role) 76 | await expect(promise).rejects.toThrow() 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /src/data/usecases/account/load-account-by-token/db-load-account-by-token.ts: -------------------------------------------------------------------------------- 1 | import { Decrypter, AccountModel, LoadAccountByTokenRepository, LoadAccountByToken } from './db-load-account-by-token-protocols' 2 | 3 | export class DbLoadAccountByToken implements LoadAccountByToken { 4 | constructor ( 5 | private readonly decrypter: Decrypter, 6 | private readonly loadAccountByTokenRepository: LoadAccountByTokenRepository 7 | ) {} 8 | 9 | async load (accessToken: string, role?: string): Promise { 10 | const token = await this.decrypter.decrypt(accessToken) 11 | if (token) { 12 | const account = await this.loadAccountByTokenRepository.loadByToken(accessToken, role) 13 | if (account) { 14 | return account 15 | } 16 | } 17 | return null 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/data/usecases/survey-result/load-survey-result/db-load-survey-result-protocols.ts: -------------------------------------------------------------------------------- 1 | export * from './db-load-survey-result' 2 | export * from '@/domain/usecases/survey-result/load-survey-result' 3 | export * from '@/domain/test' 4 | export * from '@/data/test' 5 | export * from '@/data/usecases/survey-result/save-survey-result/db-save-survey-result-protocols' 6 | export * from '@/data/protocols/db/survey-result/load-survey-result-repository' 7 | export * from '@/data/protocols/db/survey/load-survey-by-id-repository' 8 | -------------------------------------------------------------------------------- /src/data/usecases/survey-result/load-survey-result/db-load-survey-result.spec.ts: -------------------------------------------------------------------------------- 1 | import { DbLoadSurveyResult } from './db-load-survey-result' 2 | import { LoadSurveyResultRepositorySpy, LoadSurveyByIdRepositorySpy } from '@/data/test' 3 | import { throwError } from '@/domain/test' 4 | import MockDate from 'mockdate' 5 | import faker from 'faker' 6 | 7 | type SutTypes = { 8 | sut: DbLoadSurveyResult 9 | loadSurveyResultRepositorySpy: LoadSurveyResultRepositorySpy 10 | loadSurveyByIdRepositorySpy: LoadSurveyByIdRepositorySpy 11 | } 12 | 13 | const makeSut = (): SutTypes => { 14 | const loadSurveyResultRepositorySpy = new LoadSurveyResultRepositorySpy() 15 | const loadSurveyByIdRepositorySpy = new LoadSurveyByIdRepositorySpy() 16 | const sut = new DbLoadSurveyResult(loadSurveyResultRepositorySpy, loadSurveyByIdRepositorySpy) 17 | return { 18 | sut, 19 | loadSurveyResultRepositorySpy, 20 | loadSurveyByIdRepositorySpy 21 | } 22 | } 23 | 24 | let surveyId: string 25 | 26 | describe('DbLoadSurveyResult UseCase', () => { 27 | beforeAll(() => { 28 | MockDate.set(new Date()) 29 | }) 30 | 31 | afterAll(() => { 32 | MockDate.reset() 33 | }) 34 | 35 | beforeEach(() => { 36 | surveyId = faker.random.uuid() 37 | }) 38 | 39 | test('Should call LoadSurveyResultRepository', async () => { 40 | const { sut, loadSurveyResultRepositorySpy } = makeSut() 41 | await sut.load(surveyId) 42 | expect(loadSurveyResultRepositorySpy.surveyId).toBe(surveyId) 43 | }) 44 | 45 | test('Should throw if LoadSurveyResultRepository throws', async () => { 46 | const { sut, loadSurveyResultRepositorySpy } = makeSut() 47 | jest.spyOn(loadSurveyResultRepositorySpy, 'loadBySurveyId').mockImplementationOnce(throwError) 48 | const promise = sut.load(surveyId) 49 | await expect(promise).rejects.toThrow() 50 | }) 51 | 52 | test('Should call LoadSurveyByIdRepository if LoadSurveyResultRepository returns null', async () => { 53 | const { sut, loadSurveyResultRepositorySpy, loadSurveyByIdRepositorySpy } = makeSut() 54 | loadSurveyResultRepositorySpy.surveyResultModel = null 55 | await sut.load(surveyId) 56 | expect(loadSurveyByIdRepositorySpy.id).toBe(surveyId) 57 | }) 58 | 59 | test('Should return surveyResultModel with all answers with count 0 if LoadSurveyResultRepository returns null', async () => { 60 | const { sut, loadSurveyResultRepositorySpy, loadSurveyByIdRepositorySpy } = makeSut() 61 | loadSurveyResultRepositorySpy.surveyResultModel = null 62 | const surveyResult = await sut.load(surveyId) 63 | const { surveyModel } = loadSurveyByIdRepositorySpy 64 | expect(surveyResult).toEqual({ 65 | surveyId: surveyModel.id, 66 | question: surveyModel.question, 67 | date: surveyModel.date, 68 | answers: surveyModel.answers.map(answer => Object.assign({}, answer, { 69 | count: 0, 70 | percent: 0 71 | })) 72 | }) 73 | }) 74 | 75 | test('Should return surveyResultModel on success', async () => { 76 | const { sut, loadSurveyResultRepositorySpy } = makeSut() 77 | const surveyResult = await sut.load(surveyId) 78 | expect(surveyResult).toEqual(loadSurveyResultRepositorySpy.surveyResultModel) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /src/data/usecases/survey-result/load-survey-result/db-load-survey-result.ts: -------------------------------------------------------------------------------- 1 | import { SurveyResultModel, LoadSurveyResult, LoadSurveyResultRepository, LoadSurveyByIdRepository } from './db-load-survey-result-protocols' 2 | 3 | export class DbLoadSurveyResult implements LoadSurveyResult { 4 | constructor ( 5 | private readonly loadSurveyResultRepository: LoadSurveyResultRepository, 6 | private readonly loadsurveyByIdRepository: LoadSurveyByIdRepository 7 | 8 | ) {} 9 | 10 | async load (surveyId: string): Promise { 11 | let surveyResult = await this.loadSurveyResultRepository.loadBySurveyId(surveyId) 12 | if (!surveyResult) { 13 | const survey = await this.loadsurveyByIdRepository.loadById(surveyId) 14 | surveyResult = { 15 | surveyId: survey.id, 16 | question: survey.question, 17 | date: survey.date, 18 | answers: survey.answers.map(answer => Object.assign({}, answer, { 19 | count: 0, 20 | percent: 0 21 | })) 22 | } 23 | } 24 | return surveyResult 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/data/usecases/survey-result/save-survey-result/db-save-survey-result-protocols.ts: -------------------------------------------------------------------------------- 1 | export * from '@/domain/usecases/survey-result/save-survey-result' 2 | export * from '@/domain/models/survey-result/survey-result' 3 | export * from '@/data/protocols/db/survey-result/save-survey-result-repository' 4 | export * from '@/data/protocols/db/survey-result/load-survey-result-repository' 5 | -------------------------------------------------------------------------------- /src/data/usecases/survey-result/save-survey-result/db-save-survey-result.spec.ts: -------------------------------------------------------------------------------- 1 | import { DbSaveSurveyResult } from './db-save-survey-result' 2 | import { SaveSurveyResultRepositorySpy, LoadSurveyResultRepositorySpy } from '@/data/test' 3 | import { throwError, mockSaveSurveyResultParams } from '@/domain/test' 4 | import MockDate from 'mockdate' 5 | 6 | type SutTypes = { 7 | sut: DbSaveSurveyResult 8 | saveSurveyResultRepositorySpy: SaveSurveyResultRepositorySpy 9 | loadSurveyResultRepositorySpy: LoadSurveyResultRepositorySpy 10 | } 11 | 12 | const makeSut = (): SutTypes => { 13 | const saveSurveyResultRepositorySpy = new SaveSurveyResultRepositorySpy() 14 | const loadSurveyResultRepositorySpy = new LoadSurveyResultRepositorySpy() 15 | const sut = new DbSaveSurveyResult(saveSurveyResultRepositorySpy, loadSurveyResultRepositorySpy) 16 | return { 17 | sut, 18 | saveSurveyResultRepositorySpy, 19 | loadSurveyResultRepositorySpy 20 | } 21 | } 22 | 23 | describe('DbSaveSurveyResult Usecase', () => { 24 | beforeAll(() => { 25 | MockDate.set(new Date()) 26 | }) 27 | 28 | afterAll(() => { 29 | MockDate.reset() 30 | }) 31 | 32 | test('Should call SaveSurveyResultRepository with correct values', async () => { 33 | const { sut, saveSurveyResultRepositorySpy } = makeSut() 34 | const surveyResultData = mockSaveSurveyResultParams() 35 | await sut.save(surveyResultData) 36 | expect(saveSurveyResultRepositorySpy.saveSurveyResultParams).toEqual(surveyResultData) 37 | }) 38 | 39 | test('Should throw if SaveSurveyResultRepository throws', async () => { 40 | const { sut, saveSurveyResultRepositorySpy } = makeSut() 41 | jest.spyOn(saveSurveyResultRepositorySpy, 'save').mockImplementationOnce(throwError) 42 | const promise = sut.save(mockSaveSurveyResultParams()) 43 | await expect(promise).rejects.toThrow() 44 | }) 45 | 46 | test('Should call LoadSurveyResultRepository with correct values', async () => { 47 | const { sut, loadSurveyResultRepositorySpy } = makeSut() 48 | const surveyResultData = mockSaveSurveyResultParams() 49 | await sut.save(surveyResultData) 50 | expect(loadSurveyResultRepositorySpy.surveyId).toBe(surveyResultData.surveyId) 51 | }) 52 | 53 | test('Should throw if LoadSurveyResultRepository throws', async () => { 54 | const { sut, loadSurveyResultRepositorySpy } = makeSut() 55 | jest.spyOn(loadSurveyResultRepositorySpy, 'loadBySurveyId').mockImplementationOnce(throwError) 56 | const promise = sut.save(mockSaveSurveyResultParams()) 57 | await expect(promise).rejects.toThrow() 58 | }) 59 | 60 | test('Should return SurveyResult on success', async () => { 61 | const { sut, loadSurveyResultRepositorySpy } = makeSut() 62 | const surveyResult = await sut.save(mockSaveSurveyResultParams()) 63 | expect(surveyResult).toEqual(loadSurveyResultRepositorySpy.surveyResultModel) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /src/data/usecases/survey-result/save-survey-result/db-save-survey-result.ts: -------------------------------------------------------------------------------- 1 | import { SaveSurveyResult, SaveSurveyResultParams, SurveyResultModel, SaveSurveyResultRepository } from './db-save-survey-result-protocols' 2 | import { LoadSurveyResultRepository } from '../load-survey-result/db-load-survey-result-protocols' 3 | 4 | export class DbSaveSurveyResult implements SaveSurveyResult { 5 | constructor ( 6 | private readonly saveSurveyResultRepository: SaveSurveyResultRepository, 7 | private readonly loadSurveyResultRepository: LoadSurveyResultRepository 8 | ) {} 9 | 10 | async save (data: SaveSurveyResultParams): Promise { 11 | await this.saveSurveyResultRepository.save(data) 12 | const surveyResult = await this.loadSurveyResultRepository.loadBySurveyId(data.surveyId) 13 | return surveyResult 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/data/usecases/survey/add-survey/db-add-survey-protocols.ts: -------------------------------------------------------------------------------- 1 | export * from '@/domain/usecases/survey/add-survey' 2 | export * from '@/data/protocols/db/survey/add-survey-repository' 3 | -------------------------------------------------------------------------------- /src/data/usecases/survey/add-survey/db-add-survey.spec.ts: -------------------------------------------------------------------------------- 1 | import { DbAddSurvey } from './db-add-survey' 2 | import { AddSurveyRepositorySpy } from '@/data/test' 3 | import { throwError, mockAddSurveyParams } from '@/domain/test' 4 | import MockDate from 'mockdate' 5 | 6 | type SutTypes = { 7 | sut: DbAddSurvey 8 | addSurveyRepositorySpy: AddSurveyRepositorySpy 9 | } 10 | 11 | const makeSut = (): SutTypes => { 12 | const addSurveyRepositorySpy = new AddSurveyRepositorySpy() 13 | const sut = new DbAddSurvey(addSurveyRepositorySpy) 14 | return { 15 | sut, 16 | addSurveyRepositorySpy 17 | } 18 | } 19 | 20 | describe('DbAddSurvey Usecase', () => { 21 | beforeAll(() => { 22 | MockDate.set(new Date()) 23 | }) 24 | 25 | afterAll(() => { 26 | MockDate.reset() 27 | }) 28 | 29 | test('Should call AddSurveyRepository with correct values', async () => { 30 | const { sut, addSurveyRepositorySpy } = makeSut() 31 | const surveyData = mockAddSurveyParams() 32 | await sut.add(surveyData) 33 | expect(addSurveyRepositorySpy.addSurveyParams).toEqual(surveyData) 34 | }) 35 | 36 | test('Should throw if AddSurveyRepository throws', async () => { 37 | const { sut, addSurveyRepositorySpy } = makeSut() 38 | jest.spyOn(addSurveyRepositorySpy, 'add').mockImplementationOnce(throwError) 39 | const promise = sut.add(mockAddSurveyParams()) 40 | await expect(promise).rejects.toThrow() 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/data/usecases/survey/add-survey/db-add-survey.ts: -------------------------------------------------------------------------------- 1 | import { AddSurvey, AddSurveyParams, AddSurveyRepository } from './db-add-survey-protocols' 2 | 3 | export class DbAddSurvey implements AddSurvey { 4 | constructor (private readonly addSurveyRepository: AddSurveyRepository) {} 5 | async add (data: AddSurveyParams): Promise { 6 | return await this.addSurveyRepository.add(data) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/data/usecases/survey/load-survey-by-id/db-load-survey-by-id -protocols.ts: -------------------------------------------------------------------------------- 1 | export * from '@/domain/usecases/survey/load-survey-by-id' 2 | export * from '@/domain/models/survey/survey' 3 | export * from '@/data/protocols/db/survey/load-survey-by-id-repository' 4 | -------------------------------------------------------------------------------- /src/data/usecases/survey/load-survey-by-id/db-load-survey-by-id.spec.ts: -------------------------------------------------------------------------------- 1 | import { DbLoadSurveyById } from './db-load-survey-by-id' 2 | import { LoadSurveyByIdRepositorySpy } from '@/data/test' 3 | import { throwError } from '@/domain/test' 4 | import MockDate from 'mockdate' 5 | import faker from 'faker' 6 | 7 | type SutTypes = { 8 | sut: DbLoadSurveyById 9 | loadSurveyByIdRepositorySpy: LoadSurveyByIdRepositorySpy 10 | } 11 | 12 | const makeSut = (): SutTypes => { 13 | const loadSurveyByIdRepositorySpy = new LoadSurveyByIdRepositorySpy() 14 | const sut = new DbLoadSurveyById(loadSurveyByIdRepositorySpy) 15 | return { 16 | sut, 17 | loadSurveyByIdRepositorySpy 18 | } 19 | } 20 | 21 | let surveyId: string 22 | 23 | describe('DbLoadSurveyById', () => { 24 | beforeAll(() => { 25 | MockDate.set(new Date()) 26 | }) 27 | 28 | afterAll(() => { 29 | MockDate.reset() 30 | }) 31 | 32 | beforeEach(() => { 33 | surveyId = faker.random.uuid() 34 | }) 35 | 36 | test('Should call LoadSurveyByIdRepository', async () => { 37 | const { sut, loadSurveyByIdRepositorySpy } = makeSut() 38 | await sut.loadById(surveyId) 39 | expect(loadSurveyByIdRepositorySpy.id).toBe(surveyId) 40 | }) 41 | 42 | test('Should return Survey on success', async () => { 43 | const { sut, loadSurveyByIdRepositorySpy } = makeSut() 44 | const survey = await sut.loadById(surveyId) 45 | expect(survey).toEqual(loadSurveyByIdRepositorySpy.surveyModel) 46 | }) 47 | 48 | test('Should throw if LoadSurveyByIdRepository throws', async () => { 49 | const { sut, loadSurveyByIdRepositorySpy } = makeSut() 50 | jest.spyOn(loadSurveyByIdRepositorySpy, 'loadById').mockImplementationOnce(throwError) 51 | const promise = sut.loadById(surveyId) 52 | await expect(promise).rejects.toThrow() 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /src/data/usecases/survey/load-survey-by-id/db-load-survey-by-id.ts: -------------------------------------------------------------------------------- 1 | import { LoadSurveyById, SurveyModel, LoadSurveyByIdRepository } from './db-load-survey-by-id -protocols' 2 | 3 | export class DbLoadSurveyById implements LoadSurveyById { 4 | constructor (private readonly loadSurveyByIdRepository: LoadSurveyByIdRepository) {} 5 | 6 | async loadById (id: string): Promise { 7 | const survey = await this.loadSurveyByIdRepository.loadById(id) 8 | return survey 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/data/usecases/survey/load-surveys/db-load-surveys-protocols.ts: -------------------------------------------------------------------------------- 1 | export * from '@/data/protocols/db/survey/load-surveys-repository' 2 | export * from '@/domain/usecases/survey/load-surveys' 3 | export * from '@/domain/models/survey/survey' 4 | -------------------------------------------------------------------------------- /src/data/usecases/survey/load-surveys/db-load-surveys.spec.ts: -------------------------------------------------------------------------------- 1 | import { DbLoadSurveys } from './db-load-surveys' 2 | import { LoadSurveysRepositorySpy } from '@/data/test' 3 | import { throwError } from '@/domain/test' 4 | import MockDate from 'mockdate' 5 | import faker from 'faker' 6 | 7 | type SutTypes = { 8 | sut: DbLoadSurveys 9 | loadSurveysRepositorySpy: LoadSurveysRepositorySpy 10 | } 11 | 12 | const makeSut = (): SutTypes => { 13 | const loadSurveysRepositorySpy = new LoadSurveysRepositorySpy() 14 | const sut = new DbLoadSurveys(loadSurveysRepositorySpy) 15 | return { 16 | sut, 17 | loadSurveysRepositorySpy 18 | } 19 | } 20 | 21 | describe('DbLoadSurveys', () => { 22 | beforeAll(() => { 23 | MockDate.set(new Date()) 24 | }) 25 | 26 | afterAll(() => { 27 | MockDate.reset() 28 | }) 29 | 30 | test('Should call LoadSurveysRepository', async () => { 31 | const { sut, loadSurveysRepositorySpy } = makeSut() 32 | const accountId = faker.random.uuid() 33 | await sut.load(accountId) 34 | expect(loadSurveysRepositorySpy.accountId).toBe(accountId) 35 | }) 36 | 37 | test('Should return a list of Surveys on success', async () => { 38 | const { sut, loadSurveysRepositorySpy } = makeSut() 39 | const surveys = await sut.load(faker.random.uuid()) 40 | expect(surveys).toEqual(loadSurveysRepositorySpy.surveyModels) 41 | }) 42 | 43 | test('Should throw if LoadSurveysRepository throws', async () => { 44 | const { sut, loadSurveysRepositorySpy } = makeSut() 45 | jest.spyOn(loadSurveysRepositorySpy, 'loadAll').mockImplementationOnce(throwError) 46 | const promise = sut.load(faker.random.uuid()) 47 | await expect(promise).rejects.toThrow() 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /src/data/usecases/survey/load-surveys/db-load-surveys.ts: -------------------------------------------------------------------------------- 1 | import { LoadSurveysRepository, LoadSurveys, SurveyModel } from './db-load-surveys-protocols' 2 | 3 | export class DbLoadSurveys implements LoadSurveys { 4 | constructor (private readonly loadSurveysRepository: LoadSurveysRepository) {} 5 | 6 | async load (accountId: string): Promise { 7 | const surveys = await this.loadSurveysRepository.loadAll(accountId) 8 | return surveys 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/domain/models/account/account.ts: -------------------------------------------------------------------------------- 1 | export type AccountModel = { 2 | id: string 3 | name: string 4 | email: string 5 | password: string 6 | } 7 | -------------------------------------------------------------------------------- /src/domain/models/account/authentication.ts: -------------------------------------------------------------------------------- 1 | export type AuthenticationModel = { 2 | accessToken: string 3 | name: string 4 | } 5 | -------------------------------------------------------------------------------- /src/domain/models/survey-result/survey-result.ts: -------------------------------------------------------------------------------- 1 | export type SurveyResultModel = { 2 | surveyId: string 3 | question: string 4 | answers: SurveyResultAnswerModel[] 5 | date: Date 6 | } 7 | 8 | type SurveyResultAnswerModel = { 9 | image?: string 10 | answer: string 11 | count: number 12 | percent: number 13 | } 14 | -------------------------------------------------------------------------------- /src/domain/models/survey/survey.ts: -------------------------------------------------------------------------------- 1 | export type SurveyModel = { 2 | id: string 3 | question: string 4 | answers: SurveyAnswerModel[] 5 | date: Date 6 | didAnswer?: boolean 7 | } 8 | 9 | type SurveyAnswerModel = { 10 | image?: string 11 | answer: string 12 | } 13 | -------------------------------------------------------------------------------- /src/domain/test/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mock-account' 2 | export * from './test-helpers' 3 | export * from './mock-survey' 4 | export * from './mock-survey-result' 5 | -------------------------------------------------------------------------------- /src/domain/test/mock-account.ts: -------------------------------------------------------------------------------- 1 | import { AccountModel } from '@/domain/models/account/account' 2 | import { AddAccountParams } from '@/domain/usecases/account/add-account' 3 | import { AuthenticationParams } from '@/domain/usecases/account/authentication' 4 | import faker from 'faker' 5 | 6 | export const mockAddAccountParams = (): AddAccountParams => ({ 7 | name: faker.name.findName(), 8 | email: faker.internet.email(), 9 | password: faker.internet.password() 10 | }) 11 | 12 | export const mockAccountModel = (): AccountModel => ({ 13 | id: faker.random.uuid(), 14 | name: faker.name.findName(), 15 | email: faker.internet.email(), 16 | password: faker.internet.password() 17 | }) 18 | 19 | export const mockAuthenticationParams = (): AuthenticationParams => ({ 20 | email: faker.internet.email(), 21 | password: faker.internet.password() 22 | }) 23 | -------------------------------------------------------------------------------- /src/domain/test/mock-survey-result.ts: -------------------------------------------------------------------------------- 1 | import { SurveyResultModel } from '@/domain/models/survey-result/survey-result' 2 | import { SaveSurveyResultParams } from '@/domain/usecases/survey-result/save-survey-result' 3 | import faker from 'faker' 4 | 5 | export const mockSaveSurveyResultParams = (): SaveSurveyResultParams => ({ 6 | accountId: faker.random.uuid(), 7 | surveyId: faker.random.uuid(), 8 | answer: faker.random.word(), 9 | date: faker.date.recent() 10 | }) 11 | 12 | export const mockSurveyResultModel = (): SurveyResultModel => ({ 13 | surveyId: faker.random.uuid(), 14 | question: faker.random.words(), 15 | answers: [{ 16 | answer: faker.random.word(), 17 | count: faker.random.number({ min: 0, max: 1000 }), 18 | percent: faker.random.number({ min: 0, max: 100 }) 19 | }, { 20 | answer: faker.random.word(), 21 | image: faker.image.imageUrl(), 22 | count: faker.random.number({ min: 0, max: 1000 }), 23 | percent: faker.random.number({ min: 0, max: 100 }) 24 | }], 25 | date: faker.date.recent() 26 | }) 27 | 28 | export const mockEmptySurveyResultModel = (): SurveyResultModel => ({ 29 | surveyId: faker.random.uuid(), 30 | question: faker.random.words(), 31 | answers: [{ 32 | answer: faker.random.word(), 33 | count: 0, 34 | percent: 0 35 | }, { 36 | answer: faker.random.word(), 37 | image: faker.image.imageUrl(), 38 | count: 0, 39 | percent: 0 40 | }], 41 | date: faker.date.recent() 42 | }) 43 | -------------------------------------------------------------------------------- /src/domain/test/mock-survey.ts: -------------------------------------------------------------------------------- 1 | import { SurveyModel } from '@/domain/models/survey/survey' 2 | import { AddSurveyParams } from '@/domain/usecases/survey/add-survey' 3 | import faker from 'faker' 4 | 5 | export const mockSurveyModel = (): SurveyModel => { 6 | return { 7 | id: faker.random.uuid(), 8 | question: faker.random.words(), 9 | answers: [{ 10 | answer: faker.random.word() 11 | }, { 12 | answer: faker.random.word(), 13 | image: faker.image.imageUrl() 14 | }], 15 | date: faker.date.recent() 16 | } 17 | } 18 | 19 | export const mockSurveyModels = (): SurveyModel[] => [ 20 | mockSurveyModel(), 21 | mockSurveyModel() 22 | ] 23 | 24 | export const mockAddSurveyParams = (): AddSurveyParams => ({ 25 | question: faker.random.words(), 26 | answers: [{ 27 | image: faker.image.imageUrl(), 28 | answer: faker.random.word() 29 | }, { 30 | answer: faker.random.word() 31 | }], 32 | date: faker.date.recent() 33 | }) 34 | -------------------------------------------------------------------------------- /src/domain/test/test-helpers.ts: -------------------------------------------------------------------------------- 1 | export const throwError = (): never => { 2 | throw new Error() 3 | } 4 | -------------------------------------------------------------------------------- /src/domain/usecases/account/add-account.ts: -------------------------------------------------------------------------------- 1 | import { AccountModel } from '@/domain/models/account/account' 2 | 3 | export type AddAccountParams = Omit 4 | 5 | export interface AddAccount { 6 | add: (account: AddAccountParams) => Promise 7 | } 8 | -------------------------------------------------------------------------------- /src/domain/usecases/account/authentication.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticationModel } from '@/domain/models/account/authentication' 2 | 3 | export type AuthenticationParams = { 4 | email: string 5 | password: string 6 | } 7 | 8 | export interface Authentication { 9 | auth: (authenticationParams: AuthenticationParams) => Promise 10 | } 11 | -------------------------------------------------------------------------------- /src/domain/usecases/account/load-account-by-token.ts: -------------------------------------------------------------------------------- 1 | import { AccountModel } from '@/domain/models/account/account' 2 | 3 | export interface LoadAccountByToken { 4 | load: (accessToken: string, role?: string) => Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/domain/usecases/survey-result/load-survey-result.ts: -------------------------------------------------------------------------------- 1 | import { SurveyResultModel } from '@/domain/models/survey-result/survey-result' 2 | 3 | export interface LoadSurveyResult { 4 | load: (surveyId: string) => Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/domain/usecases/survey-result/save-survey-result.ts: -------------------------------------------------------------------------------- 1 | import { SurveyResultModel } from '@/domain/models/survey-result/survey-result' 2 | 3 | export type SaveSurveyResultParams = { 4 | surveyId: string 5 | accountId: string 6 | answer: string 7 | date: Date 8 | } 9 | 10 | export interface SaveSurveyResult { 11 | save: (data: SaveSurveyResultParams) => Promise 12 | } 13 | -------------------------------------------------------------------------------- /src/domain/usecases/survey/add-survey.ts: -------------------------------------------------------------------------------- 1 | import { SurveyModel } from '@/domain/models/survey/survey' 2 | 3 | export type AddSurveyParams = Omit 4 | 5 | export interface AddSurvey { 6 | add: (data: AddSurveyParams) => Promise 7 | } 8 | -------------------------------------------------------------------------------- /src/domain/usecases/survey/load-survey-by-id.ts: -------------------------------------------------------------------------------- 1 | import { SurveyModel } from '@/domain/models/survey/survey' 2 | 3 | export interface LoadSurveyById { 4 | loadById: (id: string) => Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/domain/usecases/survey/load-surveys.ts: -------------------------------------------------------------------------------- 1 | import { SurveyModel } from '@/domain/models/survey/survey' 2 | 3 | export interface LoadSurveys { 4 | load: (accountId: string) => Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/infra/criptography/bcrypt-adapter/bcrypt-adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { BcryptAdapter } from './bcrypt-adapter' 2 | import { throwError } from '@/domain/test' 3 | import bcrypt from 'bcrypt' 4 | 5 | jest.mock('bcrypt', () => ({ 6 | async hash (): Promise { 7 | return 'hash' 8 | }, 9 | 10 | async compare (): Promise { 11 | return true 12 | } 13 | })) 14 | 15 | const salt = 12 16 | const makeSut = (): BcryptAdapter => { 17 | return new BcryptAdapter(salt) 18 | } 19 | 20 | describe('Bcrypt Adapter', () => { 21 | describe('hash()', () => { 22 | test('Should call hash with correct values', async () => { 23 | const sut = makeSut() 24 | const hashSpy = jest.spyOn(bcrypt, 'hash') 25 | await sut.hash('any_value') 26 | expect(hashSpy).toHaveBeenCalledWith('any_value', salt) 27 | }) 28 | 29 | test('Should return a valid hash on hash success', async () => { 30 | const sut = makeSut() 31 | const hash = await sut.hash('any_value') 32 | expect(hash).toBe('hash') 33 | }) 34 | 35 | test('Should throw if hash throws', async () => { 36 | const sut = makeSut() 37 | jest.spyOn(bcrypt, 'hash').mockImplementationOnce(throwError) 38 | const promise = sut.hash('any_value') 39 | await expect(promise).rejects.toThrow() 40 | }) 41 | }) 42 | 43 | describe('compare()', () => { 44 | test('Should call compare with correct values', async () => { 45 | const sut = makeSut() 46 | const compareSpy = jest.spyOn(bcrypt, 'compare') 47 | await sut.compare('any_value', 'any_hash') 48 | expect(compareSpy).toHaveBeenCalledWith('any_value', 'any_hash') 49 | }) 50 | 51 | test('Should return true when compare succeeds', async () => { 52 | const sut = makeSut() 53 | const isValid = await sut.compare('any_value', 'any_hash') 54 | expect(isValid).toBe(true) 55 | }) 56 | 57 | test('Should return false when compare fails', async () => { 58 | const sut = makeSut() 59 | jest.spyOn(bcrypt, 'compare').mockResolvedValueOnce(false) 60 | const isValid = await sut.compare('any_value', 'any_hash') 61 | expect(isValid).toBe(false) 62 | }) 63 | 64 | test('Should throw if compare throws', async () => { 65 | const sut = makeSut() 66 | jest.spyOn(bcrypt, 'compare').mockImplementationOnce(throwError) 67 | const promise = sut.compare('any_value', 'any_hash') 68 | await expect(promise).rejects.toThrow() 69 | }) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /src/infra/criptography/bcrypt-adapter/bcrypt-adapter.ts: -------------------------------------------------------------------------------- 1 | import { Hasher } from '@/data/protocols/criptography/hasher' 2 | import { HashComparer } from '@/data/protocols/criptography/hash-comparer' 3 | import bcrypt from 'bcrypt' 4 | 5 | export class BcryptAdapter implements Hasher, HashComparer { 6 | constructor (private readonly salt: number) {} 7 | 8 | async hash (plaintext: string): Promise { 9 | const digest = await bcrypt.hash(plaintext, this.salt) 10 | return digest 11 | } 12 | 13 | async compare (plaintext: string, digest: string): Promise { 14 | const isValid = await bcrypt.compare(plaintext, digest) 15 | return isValid 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/infra/criptography/jwt-adapter/jwt-adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { JwtAdapter } from './jwt-adapter' 2 | import { throwError } from '@/domain/test' 3 | import jwt from 'jsonwebtoken' 4 | 5 | jest.mock('jsonwebtoken', () => ({ 6 | async sign (): Promise { 7 | return 'any_token' 8 | }, 9 | 10 | async verify (): Promise { 11 | return 'any_value' 12 | } 13 | })) 14 | 15 | const makeSut = (): JwtAdapter => { 16 | return new JwtAdapter('secret') 17 | } 18 | 19 | describe('Jwt Adapter', () => { 20 | describe('sign()', () => { 21 | test('Should call sign with correct values', async () => { 22 | const sut = makeSut() 23 | const signSpy = jest.spyOn(jwt, 'sign') 24 | await sut.encrypt('any_id') 25 | expect(signSpy).toHaveBeenCalledWith({ id: 'any_id' }, 'secret') 26 | }) 27 | 28 | test('Should return a token on sign success', async () => { 29 | const sut = makeSut() 30 | const accessToken = await sut.encrypt('any_id') 31 | expect(accessToken).toBe('any_token') 32 | }) 33 | 34 | test('Should throw if sign throws', async () => { 35 | const sut = makeSut() 36 | jest.spyOn(jwt, 'sign').mockImplementationOnce(throwError) 37 | const promise = sut.encrypt('any_id') 38 | await expect(promise).rejects.toThrow() 39 | }) 40 | }) 41 | 42 | describe('verify()', () => { 43 | test('Should call verify with correct values', async () => { 44 | const sut = makeSut() 45 | const verifySpy = jest.spyOn(jwt, 'verify') 46 | await sut.decrypt('any_token') 47 | expect(verifySpy).toHaveBeenCalledWith('any_token', 'secret') 48 | }) 49 | 50 | test('Should return a value on verify success', async () => { 51 | const sut = makeSut() 52 | const value = await sut.decrypt('any_token') 53 | expect(value).toBe('any_value') 54 | }) 55 | 56 | test('Should throw if verify throws', async () => { 57 | const sut = makeSut() 58 | jest.spyOn(jwt, 'verify').mockImplementationOnce(throwError) 59 | const promise = sut.decrypt('any_token') 60 | await expect(promise).rejects.toThrow() 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /src/infra/criptography/jwt-adapter/jwt-adapter.ts: -------------------------------------------------------------------------------- 1 | import { Encrypter } from '@/data/protocols/criptography/encrypter' 2 | import { Decrypter } from '@/data/protocols/criptography/decrypter' 3 | import jwt from 'jsonwebtoken' 4 | 5 | export class JwtAdapter implements Encrypter, Decrypter { 6 | constructor (private readonly secret: string) {} 7 | 8 | async encrypt (plaintext: string): Promise { 9 | const ciphertext = await jwt.sign({ id: plaintext }, this.secret) 10 | return ciphertext 11 | } 12 | 13 | async decrypt (ciphertext: string): Promise { 14 | const plaintext: any = await jwt.verify(ciphertext, this.secret) 15 | return plaintext 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/infra/db/mongodb/account/account-mongo-repository.ts: -------------------------------------------------------------------------------- 1 | import { MongoHelper } from '../helpers/mongo-helper' 2 | import { AddAccountParams } from '@/domain/usecases/account/add-account' 3 | import { AccountModel } from '@/domain/models/account/account' 4 | import { AddAccountRepository } from '@/data/protocols/db/account/add-account-repository' 5 | import { LoadAccountByEmailRepository } from '@/data/protocols/db/account/load-account-by-email-repository' 6 | import { UpdateAccessTokenRepository } from '@/data/protocols/db/account/update-access-token-repository' 7 | import { LoadAccountByTokenRepository } from '@/data/protocols/db/account/load-account-by-token-repository' 8 | 9 | export class AccountMongoRepository implements AddAccountRepository, LoadAccountByEmailRepository, UpdateAccessTokenRepository, LoadAccountByTokenRepository { 10 | async add (data: AddAccountParams): Promise { 11 | const accountCollection = await MongoHelper.getCollection('accounts') 12 | const result = await accountCollection.insertOne(data) 13 | return MongoHelper.map(result.ops[0]) 14 | } 15 | 16 | async loadByEmail (email: string): Promise { 17 | const accountCollection = await MongoHelper.getCollection('accounts') 18 | const account = await accountCollection.findOne({ email }) 19 | return account && MongoHelper.map(account) 20 | } 21 | 22 | async updateAccessToken (id: string, token: string): Promise { 23 | const accountCollection = await MongoHelper.getCollection('accounts') 24 | await accountCollection.updateOne({ 25 | _id: id 26 | }, { 27 | $set: { 28 | accessToken: token 29 | } 30 | }) 31 | } 32 | 33 | async loadByToken (token: string, role?: string): Promise { 34 | const accountCollection = await MongoHelper.getCollection('accounts') 35 | const account = await accountCollection.findOne({ 36 | accessToken: token, 37 | $or: [{ 38 | role 39 | }, { 40 | role: 'admin' 41 | }] 42 | }) 43 | return account && MongoHelper.map(account) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/infra/db/mongodb/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mongo-helper' 2 | export * from './query-builder' 3 | -------------------------------------------------------------------------------- /src/infra/db/mongodb/helpers/mongo-helper.spec.ts: -------------------------------------------------------------------------------- 1 | import { MongoHelper as sut } from './mongo-helper' 2 | 3 | describe('Mongo Helper', () => { 4 | beforeAll(async () => { 5 | await sut.connect(process.env.MONGO_URL) 6 | }) 7 | 8 | afterAll(async () => { 9 | await sut.disconnect() 10 | }) 11 | 12 | test('Should reconnect if mongodb is down', async () => { 13 | let accountCollection = await sut.getCollection('accounts') 14 | expect(accountCollection).toBeTruthy() 15 | await sut.disconnect() 16 | accountCollection = await sut.getCollection('accounts') 17 | expect(accountCollection).toBeTruthy() 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/infra/db/mongodb/helpers/mongo-helper.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient, Collection } from 'mongodb' 2 | 3 | export const MongoHelper = { 4 | client: null as MongoClient, 5 | uri: null as string, 6 | 7 | async connect (uri: string): Promise { 8 | this.uri = uri 9 | this.client = await MongoClient.connect(uri, { 10 | useNewUrlParser: true, 11 | useUnifiedTopology: true 12 | }) 13 | }, 14 | 15 | async disconnect (): Promise { 16 | await this.client.close() 17 | this.client = null 18 | }, 19 | 20 | async getCollection (name: string): Promise { 21 | if (!this.client?.isConnected()) { 22 | await this.connect(this.uri) 23 | } 24 | return this.client.db().collection(name) 25 | }, 26 | 27 | map: (data: any): any => { 28 | const { _id, ...rest } = data 29 | return Object.assign({}, rest, { id: _id }) 30 | }, 31 | 32 | mapCollection: (collection: any[]): any[] => { 33 | return collection.map(c => MongoHelper.map(c)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/infra/db/mongodb/helpers/query-builder.ts: -------------------------------------------------------------------------------- 1 | export class QueryBuilder { 2 | private readonly query = [] 3 | 4 | private addStep (step: string, data: object): QueryBuilder { 5 | this.query.push({ 6 | [step]: data 7 | }) 8 | return this 9 | } 10 | 11 | match (data: object): QueryBuilder { 12 | return this.addStep('$match', data) 13 | } 14 | 15 | group (data: object): QueryBuilder { 16 | return this.addStep('$group', data) 17 | } 18 | 19 | sort (data: object): QueryBuilder { 20 | return this.addStep('$sort', data) 21 | } 22 | 23 | unwind (data: object): QueryBuilder { 24 | return this.addStep('$unwind', data) 25 | } 26 | 27 | lookup (data: object): QueryBuilder { 28 | return this.addStep('$lookup', data) 29 | } 30 | 31 | project (data: object): QueryBuilder { 32 | return this.addStep('$project', data) 33 | } 34 | 35 | build (): object[] { 36 | return this.query 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/infra/db/mongodb/log/log-mongo-repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { LogMongoRepository } from './log-mongo-repository' 2 | import { MongoHelper } from '../helpers/mongo-helper' 3 | import { Collection } from 'mongodb' 4 | import faker from 'faker' 5 | 6 | const makeSut = (): LogMongoRepository => { 7 | return new LogMongoRepository() 8 | } 9 | 10 | describe('LogMongoRepository', () => { 11 | let errorCollection: Collection 12 | 13 | beforeAll(async () => { 14 | await MongoHelper.connect(process.env.MONGO_URL) 15 | }) 16 | 17 | afterAll(async () => { 18 | await MongoHelper.disconnect() 19 | }) 20 | 21 | beforeEach(async () => { 22 | errorCollection = await MongoHelper.getCollection('errors') 23 | await errorCollection.deleteMany({}) 24 | }) 25 | 26 | test('Should create an error log on success', async () => { 27 | const sut = makeSut() 28 | await sut.logError(faker.random.words()) 29 | const count = await errorCollection.countDocuments() 30 | expect(count).toBe(1) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/infra/db/mongodb/log/log-mongo-repository.ts: -------------------------------------------------------------------------------- 1 | import { MongoHelper } from '../helpers/mongo-helper' 2 | import { LogErrorRepository } from '@/data/protocols/db/log/log-error-repository' 3 | 4 | export class LogMongoRepository implements LogErrorRepository { 5 | async logError (stack: string): Promise { 6 | const errorCollection = await MongoHelper.getCollection('errors') 7 | await errorCollection.insertOne({ 8 | stack, 9 | date: new Date() 10 | }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/infra/db/mongodb/survey-result/survey-result-mongo-repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { SurveyResultMongoRepository } from './survey-result-mongo-repository' 2 | import { MongoHelper } from '../helpers/mongo-helper' 3 | import { SurveyModel } from '@/domain/models/survey/survey' 4 | import { AccountModel } from '@/domain/models/account/account' 5 | import { Collection, ObjectId } from 'mongodb' 6 | import { mockAddSurveyParams, mockAddAccountParams } from '@/domain/test' 7 | 8 | let surveyCollection: Collection 9 | let surveyResultCollection: Collection 10 | let accountCollection: Collection 11 | 12 | const makeSut = (): SurveyResultMongoRepository => { 13 | return new SurveyResultMongoRepository() 14 | } 15 | 16 | const mockSurvey = async (): Promise => { 17 | const res = await surveyCollection.insertOne(mockAddSurveyParams()) 18 | return MongoHelper.map(res.ops[0]) 19 | } 20 | 21 | const mockAccount = async (): Promise => { 22 | const res = await accountCollection.insertOne(mockAddAccountParams()) 23 | return MongoHelper.map(res.ops[0]) 24 | } 25 | 26 | describe('SurveyMongoRepository', () => { 27 | beforeAll(async () => { 28 | await MongoHelper.connect(process.env.MONGO_URL) 29 | }) 30 | 31 | afterAll(async () => { 32 | await MongoHelper.disconnect() 33 | }) 34 | 35 | beforeEach(async () => { 36 | surveyCollection = await MongoHelper.getCollection('surveys') 37 | await surveyCollection.deleteMany({}) 38 | surveyResultCollection = await MongoHelper.getCollection('surveyResults') 39 | await surveyResultCollection.deleteMany({}) 40 | accountCollection = await MongoHelper.getCollection('accounts') 41 | await accountCollection.deleteMany({}) 42 | }) 43 | 44 | describe('save()', () => { 45 | test('Should add a survey result if its new', async () => { 46 | const survey = await mockSurvey() 47 | const account = await mockAccount() 48 | const sut = makeSut() 49 | await sut.save({ 50 | surveyId: survey.id, 51 | accountId: account.id, 52 | answer: survey.answers[0].answer, 53 | date: new Date() 54 | }) 55 | const surveyResult = await surveyResultCollection.findOne({ 56 | surveyId: survey.id, 57 | accountId: account.id 58 | }) 59 | expect(surveyResult).toBeTruthy() 60 | }) 61 | 62 | test('Should update survey result if its not new', async () => { 63 | const survey = await mockSurvey() 64 | const account = await mockAccount() 65 | await surveyResultCollection.insertOne({ 66 | surveyId: new ObjectId(survey.id), 67 | accountId: new ObjectId(account.id), 68 | answer: survey.answers[0].answer, 69 | date: new Date() 70 | }) 71 | const sut = makeSut() 72 | await sut.save({ 73 | surveyId: survey.id, 74 | accountId: account.id, 75 | answer: survey.answers[1].answer, 76 | date: new Date() 77 | }) 78 | const surveyResult = await surveyResultCollection 79 | .find({ 80 | surveyId: survey.id, 81 | accountId: account.id 82 | }) 83 | .toArray() 84 | expect(surveyResult).toBeTruthy() 85 | expect(surveyResult.length).toBe(1) 86 | }) 87 | }) 88 | 89 | describe('loadBySurveyId()', () => { 90 | test('Should load survey result', async () => { 91 | const survey = await mockSurvey() 92 | const account = await mockAccount() 93 | await surveyResultCollection.insertMany([{ 94 | surveyId: new ObjectId(survey.id), 95 | accountId: new ObjectId(account.id), 96 | answer: survey.answers[0].answer, 97 | date: new Date() 98 | }, { 99 | surveyId: new ObjectId(survey.id), 100 | accountId: new ObjectId(account.id), 101 | answer: survey.answers[0].answer, 102 | date: new Date() 103 | }]) 104 | const sut = makeSut() 105 | const surveyResult = await sut.loadBySurveyId(survey.id) 106 | expect(surveyResult).toBeTruthy() 107 | expect(surveyResult.surveyId).toEqual(survey.id) 108 | expect(surveyResult.answers[0].count).toBe(2) 109 | expect(surveyResult.answers[0].percent).toBe(100) 110 | expect(surveyResult.answers[1].count).toBe(0) 111 | expect(surveyResult.answers[1].percent).toBe(0) 112 | }) 113 | 114 | test('Should return null if there is no survey result', async () => { 115 | const survey = await mockSurvey() 116 | const sut = makeSut() 117 | const surveyResult = await sut.loadBySurveyId(survey.id) 118 | expect(surveyResult).toBeNull() 119 | }) 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /src/infra/db/mongodb/survey-result/survey-result-mongo-repository.ts: -------------------------------------------------------------------------------- 1 | import { MongoHelper, QueryBuilder } from '../helpers' 2 | import { SaveSurveyResultRepository } from '@/data/protocols/db/survey-result/save-survey-result-repository' 3 | import { LoadSurveyResultRepository } from '@/data/protocols/db/survey-result/load-survey-result-repository' 4 | import { SurveyResultModel } from '@/domain/models/survey-result/survey-result' 5 | import { SaveSurveyResultParams } from '@/domain/usecases/survey-result/save-survey-result' 6 | import { ObjectId } from 'mongodb' 7 | 8 | export class SurveyResultMongoRepository implements SaveSurveyResultRepository, LoadSurveyResultRepository { 9 | async save (data: SaveSurveyResultParams): Promise { 10 | const surveyResultCollection = await MongoHelper.getCollection('surveyResults') 11 | await surveyResultCollection.findOneAndUpdate({ 12 | surveyId: new ObjectId(data.surveyId), 13 | accountId: new ObjectId(data.accountId) 14 | }, { 15 | $set: { 16 | answer: data.answer, 17 | date: data.date 18 | } 19 | }, { 20 | upsert: true 21 | }) 22 | } 23 | 24 | async loadBySurveyId (surveyId: string): Promise { 25 | const surveyResultCollection = await MongoHelper.getCollection('surveyResults') 26 | const query = new QueryBuilder() 27 | .match({ 28 | surveyId: new ObjectId(surveyId) 29 | }) 30 | .group({ 31 | _id: 0, 32 | data: { 33 | $push: '$$ROOT' 34 | }, 35 | total: { 36 | $sum: 1 37 | } 38 | }) 39 | .unwind({ 40 | path: '$data' 41 | }) 42 | .lookup({ 43 | from: 'surveys', 44 | foreignField: '_id', 45 | localField: 'data.surveyId', 46 | as: 'survey' 47 | }) 48 | .unwind({ 49 | path: '$survey' 50 | }) 51 | .group({ 52 | _id: { 53 | surveyId: '$survey._id', 54 | question: '$survey.question', 55 | date: '$survey.date', 56 | total: '$total', 57 | answer: '$data.answer', 58 | answers: '$survey.answers' 59 | }, 60 | count: { 61 | $sum: 1 62 | } 63 | }) 64 | .project({ 65 | _id: 0, 66 | surveyId: '$_id.surveyId', 67 | question: '$_id.question', 68 | date: '$_id.date', 69 | answers: { 70 | $map: { 71 | input: '$_id.answers', 72 | as: 'item', 73 | in: { 74 | $mergeObjects: ['$$item', { 75 | count: { 76 | $cond: { 77 | if: { 78 | $eq: ['$$item.answer', '$_id.answer'] 79 | }, 80 | then: '$count', 81 | else: 0 82 | } 83 | }, 84 | percent: { 85 | $cond: { 86 | if: { 87 | $eq: ['$$item.answer', '$_id.answer'] 88 | }, 89 | then: { 90 | $multiply: [{ 91 | $divide: ['$count', '$_id.total'] 92 | }, 100] 93 | }, 94 | else: 0 95 | } 96 | } 97 | }] 98 | } 99 | } 100 | } 101 | }) 102 | .group({ 103 | _id: { 104 | surveyId: '$surveyId', 105 | question: '$question', 106 | date: '$date' 107 | }, 108 | answers: { 109 | $push: '$answers' 110 | } 111 | }) 112 | .project({ 113 | _id: 0, 114 | surveyId: '$_id.surveyId', 115 | question: '$_id.question', 116 | date: '$_id.date', 117 | answers: { 118 | $reduce: { 119 | input: '$answers', 120 | initialValue: [], 121 | in: { 122 | $concatArrays: ['$$value', '$$this'] 123 | } 124 | } 125 | } 126 | }) 127 | .unwind({ 128 | path: '$answers' 129 | }) 130 | .group({ 131 | _id: { 132 | surveyId: '$surveyId', 133 | question: '$question', 134 | date: '$date', 135 | answer: '$answers.answer', 136 | image: '$answers.image' 137 | }, 138 | count: { 139 | $sum: '$answers.count' 140 | }, 141 | percent: { 142 | $sum: '$answers.percent' 143 | } 144 | }) 145 | .project({ 146 | _id: 0, 147 | surveyId: '$_id.surveyId', 148 | question: '$_id.question', 149 | date: '$_id.date', 150 | answer: { 151 | answer: '$_id.answer', 152 | image: '$_id.image', 153 | count: '$count', 154 | percent: '$percent' 155 | } 156 | }) 157 | .sort({ 158 | 'answer.count': -1 159 | }) 160 | .group({ 161 | _id: { 162 | surveyId: '$surveyId', 163 | question: '$question', 164 | date: '$date' 165 | }, 166 | answers: { 167 | $push: '$answer' 168 | } 169 | }) 170 | .project({ 171 | _id: 0, 172 | surveyId: '$_id.surveyId', 173 | question: '$_id.question', 174 | date: '$_id.date', 175 | answers: '$answers' 176 | }) 177 | .build() 178 | const surveyResult = await surveyResultCollection.aggregate(query).toArray() 179 | return surveyResult.length ? surveyResult[0] : null 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/infra/db/mongodb/survey/survey-mongo-repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { SurveyMongoRepository } from './survey-mongo-repository' 2 | import { MongoHelper } from '../helpers/mongo-helper' 3 | import { mockAddSurveyParams, mockAddAccountParams } from '@/domain/test' 4 | import { Collection } from 'mongodb' 5 | import { AccountModel } from '@/domain/models/account/account' 6 | 7 | let surveyCollection: Collection 8 | let surveyResultCollection: Collection 9 | let accountCollection: Collection 10 | 11 | const mockAccount = async (): Promise => { 12 | const res = await accountCollection.insertOne(mockAddAccountParams()) 13 | return MongoHelper.map(res.ops[0]) 14 | } 15 | 16 | const makeSut = (): SurveyMongoRepository => { 17 | return new SurveyMongoRepository() 18 | } 19 | 20 | describe('SurveyMongoRepository', () => { 21 | beforeAll(async () => { 22 | await MongoHelper.connect(process.env.MONGO_URL) 23 | }) 24 | 25 | afterAll(async () => { 26 | await MongoHelper.disconnect() 27 | }) 28 | 29 | beforeEach(async () => { 30 | surveyCollection = await MongoHelper.getCollection('surveys') 31 | await surveyCollection.deleteMany({}) 32 | surveyResultCollection = await MongoHelper.getCollection('surveyResults') 33 | await surveyResultCollection.deleteMany({}) 34 | accountCollection = await MongoHelper.getCollection('accounts') 35 | await accountCollection.deleteMany({}) 36 | }) 37 | 38 | describe('add()', () => { 39 | test('Should add a survey on success', async () => { 40 | const sut = makeSut() 41 | await sut.add(mockAddSurveyParams()) 42 | const count = await surveyCollection.countDocuments() 43 | expect(count).toBe(1) 44 | }) 45 | }) 46 | 47 | describe('loadAll()', () => { 48 | test('Should load all surveys on success', async () => { 49 | const account = await mockAccount() 50 | const addSurveyModels = [mockAddSurveyParams(), mockAddSurveyParams()] 51 | const result = await surveyCollection.insertMany(addSurveyModels) 52 | const survey = result.ops[0] 53 | await surveyResultCollection.insertOne({ 54 | surveyId: survey._id, 55 | accountId: account.id, 56 | answer: survey.answers[0].answer, 57 | date: new Date() 58 | }) 59 | const sut = makeSut() 60 | const surveys = await sut.loadAll(account.id) 61 | expect(surveys.length).toBe(2) 62 | expect(surveys[0].id).toBeTruthy() 63 | expect(surveys[0].question).toBe(addSurveyModels[0].question) 64 | expect(surveys[0].didAnswer).toBe(true) 65 | expect(surveys[1].question).toBe(addSurveyModels[1].question) 66 | expect(surveys[1].didAnswer).toBe(false) 67 | }) 68 | 69 | test('Should load empty list', async () => { 70 | const account = await mockAccount() 71 | const sut = makeSut() 72 | const surveys = await sut.loadAll(account.id) 73 | expect(surveys.length).toBe(0) 74 | }) 75 | }) 76 | 77 | describe('loadById()', () => { 78 | test('Should load survey by id on success', async () => { 79 | const res = await surveyCollection.insertOne(mockAddSurveyParams()) 80 | const sut = makeSut() 81 | const survey = await sut.loadById(res.ops[0]._id) 82 | expect(survey).toBeTruthy() 83 | expect(survey.id).toBeTruthy() 84 | }) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /src/infra/db/mongodb/survey/survey-mongo-repository.ts: -------------------------------------------------------------------------------- 1 | import { MongoHelper, QueryBuilder } from '../helpers' 2 | import { AddSurveyParams } from '@/domain/usecases/survey/add-survey' 3 | import { SurveyModel } from '@/domain/models/survey/survey' 4 | import { AddSurveyRepository } from '@/data/protocols/db/survey/add-survey-repository' 5 | import { LoadSurveysRepository } from '@/data/protocols/db/survey/load-surveys-repository' 6 | import { LoadSurveyByIdRepository } from '@/data/protocols/db/survey/load-survey-by-id-repository' 7 | import { ObjectId } from 'mongodb' 8 | 9 | export class SurveyMongoRepository implements AddSurveyRepository, LoadSurveysRepository, LoadSurveyByIdRepository { 10 | async add (data: AddSurveyParams): Promise { 11 | const surveyCollection = await MongoHelper.getCollection('surveys') 12 | const addSurveys = await surveyCollection.insertOne(data) 13 | return addSurveys && MongoHelper.map(addSurveys.ops[0]) 14 | } 15 | 16 | async loadAll (accountId: string): Promise { 17 | const surveyCollection = await MongoHelper.getCollection('surveys') 18 | const query = new QueryBuilder() 19 | .lookup({ 20 | from: 'surveyResults', 21 | foreignField: 'surveyId', 22 | localField: '_id', 23 | as: 'result' 24 | }) 25 | .project({ 26 | _id: 1, 27 | question: 1, 28 | answers: 1, 29 | date: 1, 30 | didAnswer: { 31 | $gte: [{ 32 | $size: { 33 | $filter: { 34 | input: '$result', 35 | as: 'item', 36 | cond: { 37 | $eq: ['$$item.accountId', new ObjectId(accountId)] 38 | } 39 | } 40 | } 41 | }, 1] 42 | } 43 | }) 44 | .build() 45 | const surveys = await surveyCollection.aggregate(query).toArray() 46 | return MongoHelper.mapCollection(surveys) 47 | } 48 | 49 | async loadById (id: string): Promise { 50 | const surveyCollection = await MongoHelper.getCollection('surveys') 51 | const survey = await surveyCollection.findOne({ _id: new ObjectId(id) }) 52 | return survey && MongoHelper.map(survey) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/infra/validators/email-validator-adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmailValidatorAdapter } from './email-validator-adapter' 2 | import validator from 'validator' 3 | 4 | jest.mock('validator', () => ({ 5 | isEmail (): boolean { 6 | return true 7 | } 8 | })) 9 | 10 | const makeSut = (): EmailValidatorAdapter => { 11 | return new EmailValidatorAdapter() 12 | } 13 | 14 | describe('EmailValidatorAdapter', () => { 15 | test('Should return false if validator returns false', () => { 16 | const sut = makeSut() 17 | jest.spyOn(validator, 'isEmail').mockReturnValueOnce(false) 18 | const isValid = sut.isValid('invalid_email@mail.com') 19 | expect(isValid).toBe(false) 20 | }) 21 | 22 | test('Should return true if validator returns true', () => { 23 | const sut = makeSut() 24 | const isValid = sut.isValid('valid_email@mail.com') 25 | expect(isValid).toBe(true) 26 | }) 27 | 28 | test('Should call validator with correct email', () => { 29 | const sut = makeSut() 30 | const isEmailSpy = jest.spyOn(validator, 'isEmail') 31 | sut.isValid('any_email@mail.com') 32 | expect(isEmailSpy).toHaveBeenCalledWith('any_email@mail.com') 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /src/infra/validators/email-validator-adapter.ts: -------------------------------------------------------------------------------- 1 | import { EmailValidator } from '@/validation/protocols/email-validator' 2 | import validator from 'validator' 3 | 4 | export class EmailValidatorAdapter implements EmailValidator { 5 | isValid (email: string): boolean { 6 | return validator.isEmail(email) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/adapters/express-middleware-adapter.ts.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequest, Middleware } from '@/presentation/protocols' 2 | import { Request, Response, NextFunction } from 'express' 3 | 4 | export const adaptMiddleware = (middleware: Middleware) => { 5 | return async (req: Request, res: Response, next: NextFunction) => { 6 | const httpRequest: HttpRequest = { 7 | headers: req.headers 8 | } 9 | const httpResponse = await middleware.handle(httpRequest) 10 | if (httpResponse.statusCode === 200) { 11 | Object.assign(req, httpResponse.body) 12 | next() 13 | } else { 14 | res.status(httpResponse.statusCode).json({ 15 | error: httpResponse.body.message 16 | }) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/adapters/express-route-adapter.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpRequest } from '@/presentation/protocols' 2 | import { Request, Response } from 'express' 3 | 4 | export const adaptRoute = (controller: Controller) => { 5 | return async (req: Request, res: Response) => { 6 | const httpRequest: HttpRequest = { 7 | body: req.body, 8 | params: req.params, 9 | accountId: req.accountId 10 | } 11 | const httpResponse = await controller.handle(httpRequest) 12 | if (httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299) { 13 | res.status(httpResponse.statusCode).json(httpResponse.body) 14 | } else { 15 | res.status(httpResponse.statusCode).json({ 16 | error: httpResponse.body.message 17 | }) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/config/app.ts: -------------------------------------------------------------------------------- 1 | import setupMiddlewares from './middlewares' 2 | import setupRoutes from './routes' 3 | import setupStaticFiles from './static-files' 4 | import setupSwagger from './config-swagger' 5 | import express from 'express' 6 | 7 | const app = express() 8 | setupStaticFiles(app) 9 | setupSwagger(app) 10 | setupMiddlewares(app) 11 | setupRoutes(app) 12 | export default app 13 | -------------------------------------------------------------------------------- /src/main/config/config-swagger.ts: -------------------------------------------------------------------------------- 1 | import swaggerConfig from '@/main/docs' 2 | import { noCache } from '@/main/middlewares/no-cache' 3 | import { serve, setup } from 'swagger-ui-express' 4 | import { Express } from 'express' 5 | 6 | export default (app: Express): void => { 7 | app.use('/api-docs', noCache, serve, setup(swaggerConfig)) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/config/custom-modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module Express { 2 | interface Request { 3 | accountId?: string 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/main/config/env.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | mongoUrl: process.env.MONGODB_URI || 'mongodb://localhost:27017/clean-node-api', 3 | port: process.env.PORT || 5050, 4 | jwtSecret: process.env.JWTSECRET || 'TJ670==5H' 5 | } 6 | -------------------------------------------------------------------------------- /src/main/config/middlewares.ts: -------------------------------------------------------------------------------- 1 | import { Express } from 'express' 2 | import { bodyParser, cors, contentType } from '../middlewares' 3 | 4 | export default (app: Express): void => { 5 | app.use(bodyParser) 6 | app.use(cors) 7 | app.use(contentType) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/config/routes.ts: -------------------------------------------------------------------------------- 1 | import { Express, Router } from 'express' 2 | import { readdirSync } from 'fs' 3 | 4 | export default (app: Express): void => { 5 | const router = Router() 6 | app.use('/api', router) 7 | readdirSync(`${__dirname}/../routes`).map(async file => { 8 | if (!file.includes('.test.') && !file.endsWith('.map')) { 9 | (await import(`../routes/${file}`)).default(router) 10 | } 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /src/main/config/static-files.ts: -------------------------------------------------------------------------------- 1 | import express, { Express } from 'express' 2 | import { resolve } from 'path' 3 | 4 | export default (app: Express): void => { 5 | app.use('/static', express.static(resolve(__dirname, '../../static'))) 6 | } 7 | -------------------------------------------------------------------------------- /src/main/decorators/log-controller-decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { LogControllerDecorator } from './log-controller-decorator' 2 | import { Controller, HttpRequest, HttpResponse } from '@/presentation/protocols' 3 | import { serverError, ok } from '@/presentation/helpers/http/http-helper' 4 | import { LogErrorRepositorySpy } from '@/data/test' 5 | import { mockAccountModel } from '@/domain/test' 6 | import faker from 'faker' 7 | 8 | class ControllerSpy implements Controller { 9 | httpResponse = ok(mockAccountModel()) 10 | httpRequest: HttpRequest 11 | 12 | async handle (httpRequest: HttpRequest): Promise { 13 | this.httpRequest = httpRequest 14 | return this.httpResponse 15 | } 16 | } 17 | 18 | const mockRequest = (): HttpRequest => { 19 | const password = faker.internet.password() 20 | return { 21 | body: { 22 | name: faker.name.findName(), 23 | email: faker.internet.email(), 24 | password, 25 | passwordConfirmation: password 26 | } 27 | } 28 | } 29 | 30 | const mockServerError = (): HttpResponse => { 31 | const fakeError = new Error() 32 | fakeError.stack = 'any_stack' 33 | return serverError(fakeError) 34 | } 35 | 36 | type SutTypes = { 37 | sut: LogControllerDecorator 38 | controllerSpy: ControllerSpy 39 | logErrorRepositorySpy: LogErrorRepositorySpy 40 | } 41 | 42 | const makeSut = (): SutTypes => { 43 | const controllerSpy = new ControllerSpy() 44 | const logErrorRepositorySpy = new LogErrorRepositorySpy() 45 | const sut = new LogControllerDecorator(controllerSpy, logErrorRepositorySpy) 46 | return { 47 | sut, 48 | controllerSpy, 49 | logErrorRepositorySpy 50 | } 51 | } 52 | 53 | describe('LogController Decorator', () => { 54 | test('Should call controller handle', async () => { 55 | const { sut, controllerSpy } = makeSut() 56 | const httpRequest = mockRequest() 57 | await sut.handle(httpRequest) 58 | expect(controllerSpy.httpRequest).toEqual(httpRequest) 59 | }) 60 | 61 | test('Should return the same result of the controller', async () => { 62 | const { sut, controllerSpy } = makeSut() 63 | const httpResponse = await sut.handle(mockRequest()) 64 | expect(httpResponse).toEqual(controllerSpy.httpResponse) 65 | }) 66 | 67 | test('Should call LogErrorRepository with correct error if controller returns a server error', async () => { 68 | const { sut, controllerSpy, logErrorRepositorySpy } = makeSut() 69 | const serverError = mockServerError() 70 | controllerSpy.httpResponse = serverError 71 | await sut.handle(mockRequest()) 72 | expect(logErrorRepositorySpy.stack).toBe(serverError.body.stack) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /src/main/decorators/log-controller-decorator.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpRequest, HttpResponse } from '@/presentation/protocols' 2 | import { LogErrorRepository } from '@/data/protocols/db/log/log-error-repository' 3 | 4 | export class LogControllerDecorator implements Controller { 5 | constructor ( 6 | private readonly controller: Controller, 7 | private readonly logErrorRepository: LogErrorRepository 8 | ) {} 9 | 10 | async handle (httpRequest: HttpRequest): Promise { 11 | const httpResponse = await this.controller.handle(httpRequest) 12 | if (httpResponse.statusCode === 500) { 13 | await this.logErrorRepository.logError(httpResponse.body.stack) 14 | } 15 | return httpResponse 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/docs/components.ts: -------------------------------------------------------------------------------- 1 | import { apiKeyAuthSchema } from './schemas/' 2 | import { 3 | badRequest, 4 | serverError, 5 | unauthorized, 6 | notFound, 7 | forbidden 8 | } from './components/' 9 | 10 | export default { 11 | securitySchemes: { 12 | apiKeyAuth: apiKeyAuthSchema 13 | }, 14 | badRequest, 15 | serverError, 16 | unauthorized, 17 | notFound, 18 | forbidden 19 | } 20 | -------------------------------------------------------------------------------- /src/main/docs/components/bad-request.ts: -------------------------------------------------------------------------------- 1 | export const badRequest = { 2 | description: 'Requisição inválida', 3 | content: { 4 | 'application/json': { 5 | schema: { 6 | $ref: '#/schemas/error' 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/docs/components/forbidden.ts: -------------------------------------------------------------------------------- 1 | export const forbidden = { 2 | description: 'Acesso Negado', 3 | content: { 4 | 'application/json': { 5 | schema: { 6 | $ref: '#/schemas/error' 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/docs/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bad-request' 2 | export * from './server-error' 3 | export * from './unauthorized' 4 | export * from './not-found' 5 | export * from './forbidden' 6 | -------------------------------------------------------------------------------- /src/main/docs/components/not-found.ts: -------------------------------------------------------------------------------- 1 | export const notFound = { 2 | description: 'API não encontrada' 3 | } 4 | -------------------------------------------------------------------------------- /src/main/docs/components/server-error.ts: -------------------------------------------------------------------------------- 1 | export const serverError = { 2 | description: 'Problema no servidor', 3 | content: { 4 | 'application/json': { 5 | schema: { 6 | $ref: '#/schemas/error' 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/docs/components/unauthorized.ts: -------------------------------------------------------------------------------- 1 | export const unauthorized = { 2 | description: 'Credenciais inválidas', 3 | content: { 4 | 'application/json': { 5 | schema: { 6 | $ref: '#/schemas/error' 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/docs/index.ts: -------------------------------------------------------------------------------- 1 | import paths from './paths' 2 | import components from './components' 3 | import schemas from './schemas' 4 | 5 | export default { 6 | openapi: '3.0.0', 7 | info: { 8 | title: 'Clean Node API', 9 | description: 'API para realizar enquetes entre programadores', 10 | version: '1.0.0' 11 | }, 12 | license: { 13 | name: 'GPL-3.0-or-later', 14 | url: 'https://spdx.org/licenses/GPL-3.0-or-later.html' 15 | }, 16 | servers: [{ 17 | url: '/api' 18 | }], 19 | tags: [{ 20 | name: 'Login' 21 | }, { 22 | name: 'Enquete' 23 | }], 24 | paths, 25 | schemas, 26 | components 27 | } 28 | -------------------------------------------------------------------------------- /src/main/docs/paths.ts: -------------------------------------------------------------------------------- 1 | import { 2 | loginPath, 3 | surveyPath, 4 | signUpPath, 5 | surveyResultPath 6 | } from './paths/' 7 | 8 | export default { 9 | '/login': loginPath, 10 | '/signup': signUpPath, 11 | '/surveys': surveyPath, 12 | '/surveys/{surveyId}/results': surveyResultPath 13 | } 14 | -------------------------------------------------------------------------------- /src/main/docs/paths/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login.path' 2 | export * from './survey-path' 3 | export * from './signup.path' 4 | export * from './survey-result-path' 5 | -------------------------------------------------------------------------------- /src/main/docs/paths/login.path.ts: -------------------------------------------------------------------------------- 1 | export const loginPath = { 2 | post: { 3 | tags: ['Login'], 4 | summary: 'API para autenticar usuário', 5 | requestBody: { 6 | content: { 7 | 'application/json': { 8 | schema: { 9 | $ref: '#/schemas/loginParams' 10 | } 11 | } 12 | } 13 | }, 14 | responses: { 15 | 200: { 16 | description: 'Sucesso', 17 | content: { 18 | 'application/json': { 19 | schema: { 20 | $ref: '#/schemas/account' 21 | } 22 | } 23 | } 24 | }, 25 | 400: { 26 | $ref: '#/components/badRequest' 27 | }, 28 | 401: { 29 | $ref: '#/components/unauthorized' 30 | }, 31 | 404: { 32 | $ref: '#/components/notFound' 33 | }, 34 | 500: { 35 | $ref: '#/components/serverError' 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/docs/paths/signup.path.ts: -------------------------------------------------------------------------------- 1 | export const signUpPath = { 2 | post: { 3 | tags: ['Login'], 4 | summary: 'API para criar conta de um usuário', 5 | requestBody: { 6 | content: { 7 | 'application/json': { 8 | schema: { 9 | $ref: '#/schemas/signUpParams' 10 | } 11 | } 12 | } 13 | }, 14 | responses: { 15 | 200: { 16 | description: 'Sucesso', 17 | content: { 18 | 'application/json': { 19 | schema: { 20 | $ref: '#/schemas/account' 21 | } 22 | } 23 | } 24 | }, 25 | 400: { 26 | $ref: '#/components/badRequest' 27 | }, 28 | 403: { 29 | $ref: '#/components/forbidden' 30 | }, 31 | 404: { 32 | $ref: '#/components/notFound' 33 | }, 34 | 500: { 35 | $ref: '#/components/serverError' 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/docs/paths/survey-path.ts: -------------------------------------------------------------------------------- 1 | export const surveyPath = { 2 | get: { 3 | security: [{ 4 | apiKeyAuth: [] 5 | }], 6 | tags: ['Enquete'], 7 | summary: 'API para listar todas as enquetes', 8 | responses: { 9 | 200: { 10 | description: 'Sucesso', 11 | content: { 12 | 'application/json': { 13 | schema: { 14 | $ref: '#/schemas/surveys' 15 | } 16 | } 17 | } 18 | }, 19 | 403: { 20 | $ref: '#/components/forbidden' 21 | }, 22 | 404: { 23 | $ref: '#/components/notFound' 24 | }, 25 | 500: { 26 | $ref: '#/components/serverError' 27 | } 28 | } 29 | }, 30 | post: { 31 | security: [{ 32 | apiKeyAuth: [] 33 | }], 34 | tags: ['Enquete'], 35 | summary: 'API para criar uma enquete', 36 | requestBody: { 37 | content: { 38 | 'application/json': { 39 | schema: { 40 | $ref: '#/schemas/addSurveyParams' 41 | } 42 | } 43 | } 44 | }, 45 | responses: { 46 | 204: { 47 | description: 'Sucesso' 48 | }, 49 | 403: { 50 | $ref: '#/components/forbidden' 51 | }, 52 | 404: { 53 | $ref: '#/components/notFound' 54 | }, 55 | 500: { 56 | $ref: '#/components/serverError' 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/docs/paths/survey-result-path.ts: -------------------------------------------------------------------------------- 1 | export const surveyResultPath = { 2 | put: { 3 | security: [{ 4 | apiKeyAuth: [] 5 | }], 6 | tags: ['Enquete'], 7 | summary: 'API para criar a resposta de uma enquete', 8 | description: 'Essa rota só pode ser executada por **usuários autenticados**', 9 | parameters: [{ 10 | in: 'path', 11 | name: 'surveyId', 12 | description: 'ID da enquete a ser respondida', 13 | required: true, 14 | schema: { 15 | type: 'string' 16 | } 17 | }], 18 | requestBody: { 19 | content: { 20 | 'application/json': { 21 | schema: { 22 | $ref: '#/schemas/saveSurveyParams' 23 | } 24 | } 25 | } 26 | }, 27 | responses: { 28 | 200: { 29 | description: 'Sucesso', 30 | content: { 31 | 'application/json': { 32 | schema: { 33 | $ref: '#/schemas/surveyResult' 34 | } 35 | } 36 | } 37 | }, 38 | 403: { 39 | $ref: '#/components/forbidden' 40 | }, 41 | 404: { 42 | $ref: '#/components/notFound' 43 | }, 44 | 500: { 45 | $ref: '#/components/serverError' 46 | } 47 | } 48 | }, 49 | get: { 50 | security: [{ 51 | apiKeyAuth: [] 52 | }], 53 | tags: ['Enquete'], 54 | summary: 'API para consultar o resultado de uma enquete', 55 | description: 'Essa rota só pode ser executada por **usuários autenticados**', 56 | parameters: [{ 57 | in: 'path', 58 | name: 'surveyId', 59 | description: 'ID da enquete a ser respondida', 60 | required: true, 61 | schema: { 62 | type: 'string' 63 | } 64 | }], 65 | responses: { 66 | 200: { 67 | description: 'Sucesso', 68 | content: { 69 | 'application/json': { 70 | schema: { 71 | $ref: '#/schemas/surveyResult' 72 | } 73 | } 74 | } 75 | }, 76 | 403: { 77 | $ref: '#/components/forbidden' 78 | }, 79 | 404: { 80 | $ref: '#/components/notFound' 81 | }, 82 | 500: { 83 | $ref: '#/components/serverError' 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/docs/schemas.ts: -------------------------------------------------------------------------------- 1 | import { 2 | accountSchema, 3 | loginParamsSchema, 4 | errorSchema, 5 | surveyAnswerSchema, 6 | surveysSchema, 7 | surveySchema, 8 | signUpParamsSchema, 9 | addSurveyParamsSchema, 10 | saveSurveyParamsSchema, 11 | surveyResultSchema, 12 | surveyResultAnswerSchema 13 | } from './schemas/' 14 | 15 | export default { 16 | account: accountSchema, 17 | loginParams: loginParamsSchema, 18 | signUpParams: signUpParamsSchema, 19 | addSurveyParams: addSurveyParamsSchema, 20 | error: errorSchema, 21 | surveys: surveysSchema, 22 | survey: surveySchema, 23 | surveyAnswer: surveyAnswerSchema, 24 | saveSurveyParams: saveSurveyParamsSchema, 25 | surveyResult: surveyResultSchema, 26 | surveyResultAnswer: surveyResultAnswerSchema 27 | } 28 | -------------------------------------------------------------------------------- /src/main/docs/schemas/account-schema.ts: -------------------------------------------------------------------------------- 1 | export const accountSchema = { 2 | type: 'object', 3 | properties: { 4 | accessToken: { 5 | type: 'string' 6 | }, 7 | name: { 8 | type: 'string' 9 | } 10 | }, 11 | required: ['accessToken', 'name'] 12 | } 13 | -------------------------------------------------------------------------------- /src/main/docs/schemas/add-survey-params-schema.ts: -------------------------------------------------------------------------------- 1 | export const addSurveyParamsSchema = { 2 | type: 'object', 3 | properties: { 4 | question: { 5 | type: 'string' 6 | }, 7 | answers: { 8 | type: 'array', 9 | items: { 10 | $ref: '#/schemas/surveyAnswer' 11 | } 12 | } 13 | }, 14 | required: ['question', 'answers'] 15 | } 16 | -------------------------------------------------------------------------------- /src/main/docs/schemas/api-key-auth-schema.ts: -------------------------------------------------------------------------------- 1 | export const apiKeyAuthSchema = { 2 | type: 'apiKey', 3 | in: 'header', 4 | name: 'x-access-token' 5 | } 6 | -------------------------------------------------------------------------------- /src/main/docs/schemas/error-schema.ts: -------------------------------------------------------------------------------- 1 | export const errorSchema = { 2 | type: 'object', 3 | properties: { 4 | error: { 5 | type: 'string' 6 | } 7 | }, 8 | required: ['error'] 9 | } 10 | -------------------------------------------------------------------------------- /src/main/docs/schemas/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account-schema' 2 | export * from './error-schema' 3 | export * from './login-params-schema' 4 | export * from './signup-params-schema' 5 | export * from './survey-answer-schema' 6 | export * from './survey-schema' 7 | export * from './surveys-schema' 8 | export * from './api-key-auth-schema' 9 | export * from './add-survey-params-schema' 10 | export * from './save-survey-params-schema' 11 | export * from './survey-result-schema' 12 | export * from './survey-result-answer-schema' 13 | -------------------------------------------------------------------------------- /src/main/docs/schemas/login-params-schema.ts: -------------------------------------------------------------------------------- 1 | export const loginParamsSchema = { 2 | type: 'object', 3 | properties: { 4 | email: { 5 | type: 'string' 6 | }, 7 | password: { 8 | type: 'string' 9 | } 10 | }, 11 | required: ['email', 'password'] 12 | } 13 | -------------------------------------------------------------------------------- /src/main/docs/schemas/save-survey-params-schema.ts: -------------------------------------------------------------------------------- 1 | export const saveSurveyParamsSchema = { 2 | type: 'object', 3 | properties: { 4 | answer: { 5 | type: 'string' 6 | } 7 | }, 8 | required: ['answer'] 9 | } 10 | -------------------------------------------------------------------------------- /src/main/docs/schemas/signup-params-schema.ts: -------------------------------------------------------------------------------- 1 | export const signUpParamsSchema = { 2 | type: 'object', 3 | properties: { 4 | name: { 5 | type: 'string' 6 | }, 7 | email: { 8 | type: 'string' 9 | }, 10 | password: { 11 | type: 'string' 12 | }, 13 | passwordConfirmation: { 14 | type: 'string' 15 | } 16 | }, 17 | required: ['name', 'email', 'password', 'passwordConfirmation'] 18 | } 19 | -------------------------------------------------------------------------------- /src/main/docs/schemas/survey-answer-schema.ts: -------------------------------------------------------------------------------- 1 | export const surveyAnswerSchema = { 2 | type: 'object', 3 | properties: { 4 | image: { 5 | type: 'string' 6 | }, 7 | answer: { 8 | type: 'string' 9 | } 10 | }, 11 | required: ['answer'] 12 | } 13 | -------------------------------------------------------------------------------- /src/main/docs/schemas/survey-result-answer-schema.ts: -------------------------------------------------------------------------------- 1 | export const surveyResultAnswerSchema = { 2 | type: 'object', 3 | properties: { 4 | image: { 5 | type: 'string' 6 | }, 7 | answer: { 8 | type: 'string' 9 | }, 10 | count: { 11 | type: 'number' 12 | }, 13 | percent: { 14 | type: 'number' 15 | } 16 | }, 17 | required: ['answer', 'count', 'percent'] 18 | } 19 | -------------------------------------------------------------------------------- /src/main/docs/schemas/survey-result-schema.ts: -------------------------------------------------------------------------------- 1 | export const surveyResultSchema = { 2 | type: 'object', 3 | properties: { 4 | surveyId: { 5 | type: 'string' 6 | }, 7 | question: { 8 | type: 'string' 9 | }, 10 | answers: { 11 | type: 'array', 12 | items: { 13 | $ref: '#/schemas/surveyResultAnswer' 14 | } 15 | }, 16 | date: { 17 | type: 'string' 18 | } 19 | }, 20 | required: ['surveyId', 'question', 'answers', 'date'] 21 | } 22 | -------------------------------------------------------------------------------- /src/main/docs/schemas/survey-schema.ts: -------------------------------------------------------------------------------- 1 | export const surveySchema = { 2 | type: 'object', 3 | properties: { 4 | id: { 5 | type: 'string' 6 | }, 7 | question: { 8 | type: 'string' 9 | }, 10 | answers: { 11 | type: 'array', 12 | items: { 13 | $ref: '#/schemas/surveyAnswer' 14 | } 15 | }, 16 | date: { 17 | type: 'string' 18 | }, 19 | didAnswer: { 20 | type: 'boolean' 21 | } 22 | }, 23 | required: ['id', 'question', 'answers', 'date', 'didAnswer'] 24 | } 25 | -------------------------------------------------------------------------------- /src/main/docs/schemas/surveys-schema.ts: -------------------------------------------------------------------------------- 1 | export const surveysSchema = { 2 | type: 'array', 3 | items: { 4 | $ref: '#/schemas/survey' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/main/factories/controllers/login/login/login-controller-factory.ts: -------------------------------------------------------------------------------- 1 | import { makeLoginValidation } from './login-validation-factory' 2 | import { Controller } from '@/presentation/protocols' 3 | import { LoginController } from '@/presentation/controllers/login/login/login-controller' 4 | import { makeDbAuthentication } from '@/main/factories/usecases/account/authentication/db-authentication-factory' 5 | import { makeLogControllerDecorator } from '@/main/factories/decorators/log-controller-decorator-factory' 6 | 7 | export const makeLoginController = (): Controller => { 8 | const controller = new LoginController(makeDbAuthentication(), makeLoginValidation()) 9 | return makeLogControllerDecorator(controller) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/factories/controllers/login/login/login-validation-factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { makeLoginValidation } from './login-validation-factory' 2 | import { ValidationComposite, RequiredFieldValidation, EmailValidation } from '@/validation/validators' 3 | import { Validation } from '@/presentation/protocols/validation' 4 | import { EmailValidatorAdapter } from '@/infra/validators/email-validator-adapter' 5 | 6 | jest.mock('@/validation/validators/validation-composite') 7 | 8 | describe('LoginValidation Factory', () => { 9 | test('Should call ValidationComposite with all validations', () => { 10 | makeLoginValidation() 11 | const validations: Validation[] = [] 12 | for (const field of ['email', 'password']) { 13 | validations.push(new RequiredFieldValidation(field)) 14 | } 15 | validations.push(new EmailValidation('email', new EmailValidatorAdapter())) 16 | expect(ValidationComposite).toHaveBeenCalledWith(validations) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/main/factories/controllers/login/login/login-validation-factory.ts: -------------------------------------------------------------------------------- 1 | import { ValidationComposite, RequiredFieldValidation, EmailValidation } from '@/validation/validators' 2 | import { Validation } from '@/presentation/protocols/validation' 3 | import { EmailValidatorAdapter } from '@/infra/validators/email-validator-adapter' 4 | 5 | export const makeLoginValidation = (): ValidationComposite => { 6 | const validations: Validation[] = [] 7 | for (const field of ['email', 'password']) { 8 | validations.push(new RequiredFieldValidation(field)) 9 | } 10 | validations.push(new EmailValidation('email', new EmailValidatorAdapter())) 11 | return new ValidationComposite(validations) 12 | } 13 | -------------------------------------------------------------------------------- /src/main/factories/controllers/login/signup/signup-controller-factory.ts: -------------------------------------------------------------------------------- 1 | import { SignUpController } from '@/presentation/controllers/login/signup/signup-controller' 2 | import { Controller } from '@/presentation/protocols' 3 | import { makeSignUpValidation } from './signup-validation-factory' 4 | import { makeDbAuthentication } from '@/main/factories/usecases/account/authentication/db-authentication-factory' 5 | import { makeDbAddAccount } from '@/main/factories/usecases/account/add-account/db-add-account-factory' 6 | import { makeLogControllerDecorator } from '../../../decorators/log-controller-decorator-factory' 7 | 8 | export const makeSignUpController = (): Controller => { 9 | const controller = new SignUpController(makeDbAddAccount(), makeSignUpValidation(), makeDbAuthentication()) 10 | return makeLogControllerDecorator(controller) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/factories/controllers/login/signup/signup-validation-factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { makeSignUpValidation } from './signup-validation-factory' 2 | import { ValidationComposite, RequiredFieldValidation, CompareFieldsValidation, EmailValidation } from '@/validation/validators' 3 | import { Validation } from '@/presentation/protocols/validation' 4 | import { EmailValidatorAdapter } from '@/infra/validators/email-validator-adapter' 5 | 6 | jest.mock('@/validation/validators/validation-composite') 7 | 8 | describe('SignUpValidation Factory', () => { 9 | test('Should call ValidationComposite with all validations', () => { 10 | makeSignUpValidation() 11 | const validations: Validation[] = [] 12 | for (const field of ['name', 'email', 'password', 'passwordConfirmation']) { 13 | validations.push(new RequiredFieldValidation(field)) 14 | } 15 | validations.push(new CompareFieldsValidation('password', 'passwordConfirmation')) 16 | validations.push(new EmailValidation('email', new EmailValidatorAdapter())) 17 | expect(ValidationComposite).toHaveBeenCalledWith(validations) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/main/factories/controllers/login/signup/signup-validation-factory.ts: -------------------------------------------------------------------------------- 1 | import { EmailValidation, ValidationComposite, RequiredFieldValidation, CompareFieldsValidation } from '@/validation/validators' 2 | import { Validation } from '@/presentation/protocols/validation' 3 | import { EmailValidatorAdapter } from '@/infra/validators/email-validator-adapter' 4 | 5 | export const makeSignUpValidation = (): ValidationComposite => { 6 | const validations: Validation[] = [] 7 | for (const field of ['name', 'email', 'password', 'passwordConfirmation']) { 8 | validations.push(new RequiredFieldValidation(field)) 9 | } 10 | validations.push(new CompareFieldsValidation('password', 'passwordConfirmation')) 11 | validations.push(new EmailValidation('email', new EmailValidatorAdapter())) 12 | return new ValidationComposite(validations) 13 | } 14 | -------------------------------------------------------------------------------- /src/main/factories/controllers/survey-result/load-survey-result/load-survey-result-controller-factories.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@/presentation/protocols' 2 | import { makeLogControllerDecorator } from '@/main/factories/decorators/log-controller-decorator-factory' 3 | import { LoadSurveyResultController } from '@/presentation/controllers/survey-result/load-survey-result/load-survey-result-controller' 4 | import { makeDbLoadSurveyById } from '@/main/factories/usecases/survey/load-survey-by-id/db-load-survey-by-id-factory' 5 | import { makeDbLoadSurveyResult } from '@/main/factories/usecases/survey-result/load-survey-result/db-load-survey-result-factory' 6 | 7 | export const makeLoadSurveyResultController = (): Controller => { 8 | const controller = new LoadSurveyResultController(makeDbLoadSurveyById(), makeDbLoadSurveyResult()) 9 | return makeLogControllerDecorator(controller) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/factories/controllers/survey-result/save-survey-result/save-survey-result-controller-factories.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@/presentation/protocols' 2 | import { makeLogControllerDecorator } from '@/main/factories/decorators/log-controller-decorator-factory' 3 | import { SaveSurveyResultController } from '@/presentation/controllers/survey-result/save-survey-result/save-survey-result-controller' 4 | import { makeDbLoadSurveyById } from '@/main/factories/usecases/survey/load-survey-by-id/db-load-survey-by-id-factory' 5 | import { makeDbSaveSurveyResult } from '@/main/factories/usecases/survey-result/save-survey-result/db-save-survey-result-factory' 6 | 7 | export const makeSaveSurveyResultController = (): Controller => { 8 | const controller = new SaveSurveyResultController(makeDbLoadSurveyById(), makeDbSaveSurveyResult()) 9 | return makeLogControllerDecorator(controller) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/factories/controllers/survey/add-survey/add-survey-controller-factory.ts: -------------------------------------------------------------------------------- 1 | import { AddSurveyController } from '@/presentation/controllers/survey/add-survey/add-survey-controller' 2 | import { Controller } from '@/presentation/protocols' 3 | import { makeLogControllerDecorator } from '@/main/factories/decorators/log-controller-decorator-factory' 4 | import { makeAddSurveyValidation } from './add-survey-validation-factory' 5 | import { makeAddSurvey } from '@/main/factories/usecases/survey/add-survey/db-add-survey-factory' 6 | 7 | export const makeAddSurveyController = (): Controller => { 8 | const controller = new AddSurveyController(makeAddSurveyValidation(), makeAddSurvey()) 9 | return makeLogControllerDecorator(controller) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/factories/controllers/survey/add-survey/add-survey-validation-factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { ValidationComposite, RequiredFieldValidation } from '@/validation/validators' 2 | import { makeAddSurveyValidation } from './add-survey-validation-factory' 3 | import { Validation } from '@/presentation/protocols/validation' 4 | 5 | jest.mock('../../../../../validation/validators/validation-composite') 6 | 7 | describe('AddSurveyValidation Factory', () => { 8 | test('Should call ValidationComposite with call validations', () => { 9 | makeAddSurveyValidation() 10 | const validations: Validation[] = [] 11 | for (const field of ['question', 'answers']) { 12 | validations.push(new RequiredFieldValidation(field)) 13 | } 14 | expect(ValidationComposite).toHaveBeenCalledWith(validations) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/main/factories/controllers/survey/add-survey/add-survey-validation-factory.ts: -------------------------------------------------------------------------------- 1 | import { ValidationComposite, RequiredFieldValidation } from '@/validation/validators' 2 | import { Validation } from '@/presentation/protocols/validation' 3 | 4 | export const makeAddSurveyValidation = (): ValidationComposite => { 5 | const validations: Validation[] = [] 6 | for (const field of ['question', 'answers']) { 7 | validations.push(new RequiredFieldValidation(field)) 8 | } 9 | return new ValidationComposite(validations) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/factories/controllers/survey/load-surveys/load-surveys-controller-factory.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@/presentation/protocols' 2 | import { makeLogControllerDecorator } from '@/main/factories/decorators/log-controller-decorator-factory' 3 | import { LoadSurveysController } from '@/presentation/controllers/survey/load-surveys/load-surveys-controller' 4 | import { makeDbLoadSurveys } from '@/main/factories/usecases/survey/load-surveys/db-load-surveys-factory' 5 | 6 | export const makeLoadSurveysController = (): Controller => { 7 | const controller = new LoadSurveysController(makeDbLoadSurveys()) 8 | return makeLogControllerDecorator(controller) 9 | } 10 | -------------------------------------------------------------------------------- /src/main/factories/decorators/log-controller-decorator-factory.ts: -------------------------------------------------------------------------------- 1 | import { LogControllerDecorator } from '@/main/decorators/log-controller-decorator' 2 | import { LogMongoRepository } from '@/infra/db/mongodb/log/log-mongo-repository' 3 | import { Controller } from '@/presentation/protocols' 4 | 5 | export const makeLogControllerDecorator = (controller: Controller): Controller => { 6 | const logMongoRepository = new LogMongoRepository() 7 | return new LogControllerDecorator(controller, logMongoRepository) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/factories/middleware/auth-middleware-factory.ts: -------------------------------------------------------------------------------- 1 | import { AuthMiddleware } from '@/presentation/middleware/auth-middleware' 2 | import { Middleware } from '@/presentation/protocols/middleware' 3 | import { makeDbAccountByToken } from '@/main/factories/usecases/account/load-account-by-token/db-load-account-by-token-factory' 4 | 5 | export const makeAuthMiddleware = (role?: string): Middleware => { 6 | return new AuthMiddleware(makeDbAccountByToken(), role) 7 | } 8 | -------------------------------------------------------------------------------- /src/main/factories/usecases/account/add-account/db-add-account-factory.ts: -------------------------------------------------------------------------------- 1 | import { DbAddAccount } from '@/data/usecases/account/add-account/db-add-account' 2 | import { AddAccount } from '@/domain/usecases/account/add-account' 3 | import { AccountMongoRepository } from '@/infra/db/mongodb/account/account-mongo-repository' 4 | import { BcryptAdapter } from '@/infra/criptography/bcrypt-adapter/bcrypt-adapter' 5 | 6 | export const makeDbAddAccount = (): AddAccount => { 7 | const salt = 12 8 | const bcryptAdapter = new BcryptAdapter(salt) 9 | const accountMongoRepository = new AccountMongoRepository() 10 | return new DbAddAccount(bcryptAdapter, accountMongoRepository, accountMongoRepository) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/factories/usecases/account/authentication/db-authentication-factory.ts: -------------------------------------------------------------------------------- 1 | import env from '@/main/config/env' 2 | import { DbAuthentication } from '@/data/usecases/account/authentication/db-authentication' 3 | import { AccountMongoRepository } from '@/infra/db/mongodb/account/account-mongo-repository' 4 | import { BcryptAdapter } from '@/infra/criptography/bcrypt-adapter/bcrypt-adapter' 5 | import { JwtAdapter } from '@/infra/criptography/jwt-adapter/jwt-adapter' 6 | import { Authentication } from '@/domain/usecases/account/authentication' 7 | 8 | export const makeDbAuthentication = (): Authentication => { 9 | const salt = 12 10 | const bcryptAdapter = new BcryptAdapter(salt) 11 | const jwtAdapter = new JwtAdapter(env.jwtSecret) 12 | const accountMongoRepository = new AccountMongoRepository() 13 | return new DbAuthentication(accountMongoRepository, bcryptAdapter, jwtAdapter, accountMongoRepository) 14 | } 15 | -------------------------------------------------------------------------------- /src/main/factories/usecases/account/load-account-by-token/db-load-account-by-token-factory.ts: -------------------------------------------------------------------------------- 1 | 2 | import { AccountMongoRepository } from '@/infra/db/mongodb/account/account-mongo-repository' 3 | import { DbLoadAccountByToken } from '@/data/usecases/account/load-account-by-token/db-load-account-by-token' 4 | import { JwtAdapter } from '@/infra/criptography/jwt-adapter/jwt-adapter' 5 | import env from '@/main/config/env' 6 | import { LoadAccountByToken } from '@/domain/usecases/account/load-account-by-token' 7 | 8 | export const makeDbAccountByToken = (): LoadAccountByToken => { 9 | const jwtAdapter = new JwtAdapter(env.jwtSecret) 10 | const accountMongoRepository = new AccountMongoRepository() 11 | return new DbLoadAccountByToken(jwtAdapter, accountMongoRepository) 12 | } 13 | -------------------------------------------------------------------------------- /src/main/factories/usecases/survey-result/load-survey-result/db-load-survey-result-factory.ts: -------------------------------------------------------------------------------- 1 | import { LoadSurveyResult } from '@/domain/usecases/survey-result/load-survey-result' 2 | import { DbLoadSurveyResult } from '@/data/usecases/survey-result/load-survey-result/db-load-survey-result' 3 | import { SurveyResultMongoRepository } from '@/infra/db/mongodb/survey-result/survey-result-mongo-repository' 4 | import { SurveyMongoRepository } from '@/infra/db/mongodb/survey/survey-mongo-repository' 5 | 6 | export const makeDbLoadSurveyResult = (): LoadSurveyResult => { 7 | const surveyResultMongoRepository = new SurveyResultMongoRepository() 8 | const surveyMongoRepository = new SurveyMongoRepository() 9 | return new DbLoadSurveyResult(surveyResultMongoRepository, surveyMongoRepository) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/factories/usecases/survey-result/save-survey-result/db-save-survey-result-factory.ts: -------------------------------------------------------------------------------- 1 | import { SaveSurveyResult } from '@/domain/usecases/survey-result/save-survey-result' 2 | import { DbSaveSurveyResult } from '@/data/usecases/survey-result/save-survey-result/db-save-survey-result' 3 | import { SurveyResultMongoRepository } from '@/infra/db/mongodb/survey-result/survey-result-mongo-repository' 4 | 5 | export const makeDbSaveSurveyResult = (): SaveSurveyResult => { 6 | const surveyResultMongoRepository = new SurveyResultMongoRepository() 7 | return new DbSaveSurveyResult(surveyResultMongoRepository, surveyResultMongoRepository) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/factories/usecases/survey/add-survey/db-add-survey-factory.ts: -------------------------------------------------------------------------------- 1 | import { DbAddSurvey } from '@/data/usecases/survey/add-survey/db-add-survey' 2 | import { SurveyMongoRepository } from '@/infra/db/mongodb/survey/survey-mongo-repository' 3 | import { AddSurvey } from '@/domain/usecases/survey/add-survey' 4 | 5 | export const makeAddSurvey = (): AddSurvey => { 6 | const surveyMongoRepository = new SurveyMongoRepository() 7 | return new DbAddSurvey(surveyMongoRepository) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/factories/usecases/survey/load-survey-by-id/db-load-survey-by-id-factory.ts: -------------------------------------------------------------------------------- 1 | import { SurveyMongoRepository } from '@/infra/db/mongodb/survey/survey-mongo-repository' 2 | import { LoadSurveyById } from '@/domain/usecases/survey/load-survey-by-id' 3 | import { DbLoadSurveyById } from '@/data/usecases/survey/load-survey-by-id/db-load-survey-by-id' 4 | 5 | export const makeDbLoadSurveyById = (): LoadSurveyById => { 6 | const surveyMongoRepository = new SurveyMongoRepository() 7 | return new DbLoadSurveyById(surveyMongoRepository) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/factories/usecases/survey/load-surveys/db-load-surveys-factory.ts: -------------------------------------------------------------------------------- 1 | import { SurveyMongoRepository } from '@/infra/db/mongodb/survey/survey-mongo-repository' 2 | import { LoadSurveys } from '@/domain/usecases/survey/load-surveys' 3 | import { DbLoadSurveys } from '@/data/usecases/survey/load-surveys/db-load-surveys' 4 | 5 | export const makeDbLoadSurveys = (): LoadSurveys => { 6 | const surveyMongoRepository = new SurveyMongoRepository() 7 | return new DbLoadSurveys(surveyMongoRepository) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/middlewares/admin-auth.ts: -------------------------------------------------------------------------------- 1 | import { adaptMiddleware } from '@/main/adapters/express-middleware-adapter.ts' 2 | import { makeAuthMiddleware } from '@/main/factories/middleware/auth-middleware-factory' 3 | 4 | export const adminAuth = adaptMiddleware(makeAuthMiddleware('admin')) 5 | -------------------------------------------------------------------------------- /src/main/middlewares/auth.ts: -------------------------------------------------------------------------------- 1 | import { adaptMiddleware } from '@/main/adapters/express-middleware-adapter.ts' 2 | import { makeAuthMiddleware } from '@/main/factories/middleware/auth-middleware-factory' 3 | 4 | export const auth = adaptMiddleware(makeAuthMiddleware()) 5 | -------------------------------------------------------------------------------- /src/main/middlewares/body-parser.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest' 2 | import app from '@/main/config/app' 3 | 4 | describe('Body Parser Middlewares', () => { 5 | test('Should parse body as json', async () => { 6 | app.post('/test_body_parser', (req, res) => { 7 | res.send(req.body) 8 | }) 9 | await request(app) 10 | .post('/test_body_parser') 11 | .send({ name: 'William' }) 12 | .expect({ name: 'William' }) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /src/main/middlewares/body-parser.ts: -------------------------------------------------------------------------------- 1 | import { json } from 'express' 2 | 3 | export const bodyParser = json() 4 | -------------------------------------------------------------------------------- /src/main/middlewares/content-type.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest' 2 | import app from '@/main/config/app' 3 | 4 | describe('Content Type Middleware', () => { 5 | test('Should return default content type as json', async () => { 6 | app.get('/test_content_type', (req, res) => { 7 | res.send('') 8 | }) 9 | await request(app) 10 | .get('/test_content_type') 11 | .expect('content-type', /json/) 12 | }) 13 | 14 | test('Should return xml content type when forced', async () => { 15 | app.get('/test_content_type_xml', (req, res) => { 16 | res.type('xml') 17 | res.send('') 18 | }) 19 | await request(app) 20 | .get('/test_content_type_xml') 21 | .expect('content-type', /xml/) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/main/middlewares/content-type.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express' 2 | 3 | export const contentType = (req: Request, res: Response, next: NextFunction): void => { 4 | res.type('json') 5 | next() 6 | } 7 | -------------------------------------------------------------------------------- /src/main/middlewares/cors.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest' 2 | import app from '@/main/config/app' 3 | 4 | describe('Cors Middleware', () => { 5 | test('Should enable CORS', async () => { 6 | app.get('/test_cors', (req, res) => { 7 | res.send() 8 | }) 9 | await request(app) 10 | .get('/test_cors') 11 | .expect('access-control-allow-origin', '*') 12 | .expect('access-control-allow-methods', '*') 13 | .expect('access-control-allow-headers', '*') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/main/middlewares/cors.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express' 2 | 3 | export const cors = (req: Request, res: Response, next: NextFunction): void => { 4 | res.set('access-control-allow-origin', '*') 5 | res.set('access-control-allow-methods', '*') 6 | res.set('access-control-allow-headers', '*') 7 | next() 8 | } 9 | -------------------------------------------------------------------------------- /src/main/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from './body-parser' 2 | export * from './content-type' 3 | export * from './cors' 4 | export * from './admin-auth' 5 | export * from './auth' 6 | -------------------------------------------------------------------------------- /src/main/middlewares/no-cache.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest' 2 | import { noCache } from './no-cache' 3 | import app from '@/main/config/app' 4 | 5 | describe('NoCahe Middleware', () => { 6 | test('Should disable cache', async () => { 7 | app.get('/test_no_cache', noCache, (req, res) => { 8 | res.send() 9 | }) 10 | await request(app) 11 | .get('/test_no_cache') 12 | .expect('cache-control', 'no-store, no-cache, must-revalidate, proxy-revalidate') 13 | .expect('pragma', 'no-cache') 14 | .expect('expires', '0') 15 | .expect('surrogate-control', 'no-store') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/main/middlewares/no-cache.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express' 2 | 3 | export const noCache = (req: Request, res: Response, next: NextFunction): void => { 4 | res.set('cache-control', 'no-store, no-cache, must-revalidate, proxy-revalidate') 5 | res.set('pragma', 'no-cache') 6 | res.set('expires', '0') 7 | res.set('surrogate-control', 'no-store') 8 | next() 9 | } 10 | -------------------------------------------------------------------------------- /src/main/routes/login-routes.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest' 2 | import app from '@/main/config/app' 3 | import { MongoHelper } from '@/infra/db/mongodb/helpers/mongo-helper' 4 | import { Collection } from 'mongodb' 5 | import { hash } from 'bcrypt' 6 | 7 | let accountCollection: Collection 8 | 9 | describe('Login Routes', () => { 10 | beforeAll(async () => { 11 | await MongoHelper.connect(process.env.MONGO_URL) 12 | }) 13 | 14 | afterAll(async () => { 15 | await MongoHelper.disconnect() 16 | }) 17 | 18 | beforeEach(async () => { 19 | accountCollection = await MongoHelper.getCollection('accounts') 20 | await accountCollection.deleteMany({}) 21 | }) 22 | 23 | describe('POST /signup', () => { 24 | test('Should return 200 on signup', async () => { 25 | await request(app) 26 | .post('/api/signup') 27 | .send({ 28 | name: 'William', 29 | email: 'wkoller@gmail.com', 30 | password: '123', 31 | passwordConfirmation: '123' 32 | }) 33 | .expect(200) 34 | }) 35 | }) 36 | 37 | describe('POST /login', () => { 38 | test('Should return 200 on login', async () => { 39 | const password = await hash('123', 12) 40 | await accountCollection.insertOne({ 41 | name: 'William', 42 | email: 'wkoller@gmail.com', 43 | password 44 | }) 45 | await request(app) 46 | .post('/api/login') 47 | .send({ 48 | email: 'wkoller@gmail.com', 49 | password: '123' 50 | }) 51 | .expect(200) 52 | }) 53 | 54 | test('Should return 401 on login', async () => { 55 | await request(app) 56 | .post('/api/login') 57 | .send({ 58 | email: 'wkoller@gmail.com', 59 | password: '123' 60 | }) 61 | .expect(401) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /src/main/routes/login-routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { adaptRoute } from '@/main//adapters/express-route-adapter' 3 | import { makeSignUpController } from '@/main/factories/controllers/login/signup/signup-controller-factory' 4 | import { makeLoginController } from '@/main/factories/controllers/login/login/login-controller-factory' 5 | 6 | export default (router: Router): void => { 7 | router.post('/signup', adaptRoute(makeSignUpController())) 8 | router.post('/login', adaptRoute(makeLoginController())) 9 | } 10 | -------------------------------------------------------------------------------- /src/main/routes/survey-result-routes.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest' 2 | import app from '@/main/config/app' 3 | import { MongoHelper } from '@/infra/db/mongodb/helpers/mongo-helper' 4 | import { Collection } from 'mongodb' 5 | import { sign } from 'jsonwebtoken' 6 | import env from '../config/env' 7 | 8 | let surveyCollection: Collection 9 | let accountCollection: Collection 10 | 11 | const makeAccessToken = async (): Promise => { 12 | const res = await accountCollection.insertOne({ 13 | name: 'William Koller', 14 | email: 'williamkoller@gmail.com', 15 | password: '1234' 16 | }) 17 | const id = res.ops[0]._id 18 | const accessToken = sign({ id }, env.jwtSecret) 19 | await accountCollection.updateOne({ 20 | _id: id 21 | }, { 22 | $set: { 23 | accessToken 24 | } 25 | }) 26 | return accessToken 27 | } 28 | 29 | describe('Survey Routes', () => { 30 | beforeAll(async () => { 31 | await MongoHelper.connect(process.env.MONGO_URL) 32 | }) 33 | 34 | afterAll(async () => { 35 | await MongoHelper.disconnect() 36 | }) 37 | 38 | beforeEach(async () => { 39 | surveyCollection = await MongoHelper.getCollection('surveys') 40 | await surveyCollection.deleteMany({}) 41 | accountCollection = await MongoHelper.getCollection('accounts') 42 | await accountCollection.deleteMany({}) 43 | }) 44 | 45 | describe('PUT /surveys/:surveyId/results', () => { 46 | test('Should return 403 on save survey result without accessToken', async () => { 47 | await request(app) 48 | .put('/api/surveys/any_id/results') 49 | .send({ 50 | answer: 'any_answer' 51 | }) 52 | .expect(403) 53 | }) 54 | 55 | test('Should return 200 on save survey result with accessToken', async () => { 56 | const accessToken = await makeAccessToken() 57 | const res = await surveyCollection.insertOne({ 58 | question: 'Question', 59 | answers: [{ 60 | answer: 'Answer 1', 61 | image: 'http://image-name.com' 62 | }, { 63 | aswer: 'Answer 2' 64 | }], 65 | date: new Date() 66 | }) 67 | await request(app) 68 | .put(`/api/surveys/${res.ops[0]._id}/results`) 69 | .set('x-access-token', accessToken) 70 | .send({ 71 | answer: 'Answer 1' 72 | }) 73 | .expect(200) 74 | }) 75 | }) 76 | 77 | describe('GET /surveys/:surveyId/results', () => { 78 | test('Should return 403 on load survey result without accessToken', async () => { 79 | await request(app) 80 | .get('/api/surveys/any_id/results') 81 | .expect(403) 82 | }) 83 | 84 | test('Should return 200 on load survey result with accessToken', async () => { 85 | const accessToken = await makeAccessToken() 86 | const res = await surveyCollection.insertOne({ 87 | question: 'Question', 88 | answers: [{ 89 | answer: 'Answer 1', 90 | image: 'http://image-name.com' 91 | }, { 92 | aswer: 'Answer 2' 93 | }], 94 | date: new Date() 95 | }) 96 | await request(app) 97 | .get(`/api/surveys/${res.ops[0]._id}/results`) 98 | .set('x-access-token', accessToken) 99 | .expect(200) 100 | }) 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /src/main/routes/survey-result-routes.ts: -------------------------------------------------------------------------------- 1 | import { makeSaveSurveyResultController } from '@/main/factories/controllers/survey-result/save-survey-result/save-survey-result-controller-factories' 2 | import { makeLoadSurveyResultController } from '@/main/factories/controllers/survey-result/load-survey-result/load-survey-result-controller-factories' 3 | import { adaptRoute } from '@/main/adapters/express-route-adapter' 4 | import { auth } from '@/main/middlewares' 5 | import { Router } from 'express' 6 | 7 | export default (router: Router): void => { 8 | router.put('/surveys/:surveyId/results', auth, adaptRoute(makeSaveSurveyResultController())) 9 | router.get('/surveys/:surveyId/results', auth, adaptRoute(makeLoadSurveyResultController())) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/routes/survey-routes.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest' 2 | import app from '@/main/config/app' 3 | import { MongoHelper } from '@/infra/db/mongodb/helpers/mongo-helper' 4 | import { Collection } from 'mongodb' 5 | import { sign } from 'jsonwebtoken' 6 | import env from '../config/env' 7 | 8 | let surveyCollection: Collection 9 | let accountCollection: Collection 10 | 11 | const makeAccessToken = async (): Promise => { 12 | const res = await accountCollection.insertOne({ 13 | name: 'William', 14 | email: 'wkoller@gmail.com', 15 | password: '123456', 16 | role: 'admin' 17 | }) 18 | const id = res.ops[0]._id 19 | const accessToken = sign({ id }, env.jwtSecret) 20 | await accountCollection.updateOne({ 21 | _id: id 22 | }, { 23 | $set: { 24 | accessToken 25 | } 26 | }) 27 | return accessToken 28 | } 29 | 30 | describe('Survey Routes', () => { 31 | beforeAll(async () => { 32 | await MongoHelper.connect(process.env.MONGO_URL) 33 | }) 34 | 35 | afterAll(async () => { 36 | await MongoHelper.disconnect() 37 | }) 38 | 39 | beforeEach(async () => { 40 | surveyCollection = await MongoHelper.getCollection('surveys') 41 | await surveyCollection.deleteMany({}) 42 | accountCollection = await MongoHelper.getCollection('accounts') 43 | await accountCollection.deleteMany({}) 44 | }) 45 | 46 | describe('POST /surveys', () => { 47 | test('Should return 403 on add survey without accessToken', async () => { 48 | await request(app) 49 | .post('/api/surveys') 50 | .send({ 51 | question: 'Question', 52 | answers: [{ 53 | answer: 'Answer 1', 54 | image: 'http://image-name.com' 55 | }, { 56 | aswer: 'Answer 2' 57 | }] 58 | }) 59 | .expect(403) 60 | }) 61 | 62 | test('Should return 204 on add survey with valid accessToken', async () => { 63 | const accessToken = await makeAccessToken() 64 | await request(app) 65 | .post('/api/surveys') 66 | .set('x-access-token', accessToken) 67 | .send({ 68 | question: 'Question', 69 | answers: [{ 70 | answer: 'Answer 1', 71 | image: 'http://image-name.com' 72 | }, { 73 | aswer: 'Answer 2' 74 | }] 75 | }) 76 | .expect(200) 77 | }) 78 | }) 79 | 80 | describe('GET /surveys', () => { 81 | test('Should return 403 on load survey without accessToken', async () => { 82 | await request(app) 83 | .get('/api/surveys') 84 | .expect(403) 85 | }) 86 | 87 | test('Should return 204 on load surveys with valid accessToken', async () => { 88 | const accessToken = await makeAccessToken() 89 | await request(app) 90 | .get('/api/surveys') 91 | .set('x-access-token', accessToken) 92 | .expect(204) 93 | }) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /src/main/routes/survey-routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { adaptRoute } from '@/main/adapters/express-route-adapter' 3 | import { makeAddSurveyController } from '@/main/factories/controllers/survey/add-survey/add-survey-controller-factory' 4 | import { makeLoadSurveysController } from '@/main/factories/controllers/survey/load-surveys/load-surveys-controller-factory' 5 | import { adminAuth, auth } from '@/main/middlewares/index' 6 | 7 | export default (router: Router): void => { 8 | router.post('/surveys', adminAuth, adaptRoute(makeAddSurveyController())) 9 | router.get('/surveys', auth, adaptRoute(makeLoadSurveysController())) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/server.ts: -------------------------------------------------------------------------------- 1 | import 'module-alias/register' 2 | import { MongoHelper } from '@/infra/db/mongodb/helpers/mongo-helper' 3 | import env from '@/main/config/env' 4 | 5 | MongoHelper.connect(env.mongoUrl) 6 | .then(async () => { 7 | const app = (await import('./config/app')).default 8 | app.listen(env.port, () => console.log(`Server running at http://localhost:${env.port}`)) 9 | }) 10 | .catch(console.error) 11 | -------------------------------------------------------------------------------- /src/presentation/controllers/login/login/login-controller-protocols.ts: -------------------------------------------------------------------------------- 1 | export * from '@/presentation/protocols' 2 | export * from '@/domain/usecases/account/authentication' 3 | -------------------------------------------------------------------------------- /src/presentation/controllers/login/login/login-controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { LoginController } from './login-controller' 2 | import { HttpRequest } from '@/presentation/protocols' 3 | import { badRequest, serverError, unauthorized, ok } from '@/presentation/helpers/http/http-helper' 4 | import { MissingParamError } from '@/presentation/errors' 5 | import { AuthenticationSpy, ValidationSpy } from '@/presentation/test' 6 | import { throwError, mockAuthenticationParams } from '@/domain/test' 7 | import faker from 'faker' 8 | 9 | const mockRequest = (): HttpRequest => ({ 10 | body: mockAuthenticationParams() 11 | }) 12 | 13 | type SutTypes = { 14 | sut: LoginController 15 | authenticationSpy: AuthenticationSpy 16 | validationSpy: ValidationSpy 17 | } 18 | 19 | const makeSut = (): SutTypes => { 20 | const authenticationSpy = new AuthenticationSpy() 21 | const validationSpy = new ValidationSpy() 22 | const sut = new LoginController(authenticationSpy, validationSpy) 23 | return { 24 | sut, 25 | authenticationSpy, 26 | validationSpy 27 | } 28 | } 29 | 30 | describe('Login Controller', () => { 31 | test('Should call Authentication with correct values', async () => { 32 | const { sut, authenticationSpy } = makeSut() 33 | const httpRequest = mockRequest() 34 | await sut.handle(httpRequest) 35 | expect(authenticationSpy.authenticationParams).toEqual({ 36 | email: httpRequest.body.email, 37 | password: httpRequest.body.password 38 | }) 39 | }) 40 | 41 | test('Should return 401 if invalid credentials are provided', async () => { 42 | const { sut, authenticationSpy } = makeSut() 43 | authenticationSpy.authenticationModel = null 44 | const httpResponse = await sut.handle(mockRequest()) 45 | expect(httpResponse).toEqual(unauthorized()) 46 | }) 47 | 48 | test('Should return 500 if Authentication throws', async () => { 49 | const { sut, authenticationSpy } = makeSut() 50 | jest.spyOn(authenticationSpy, 'auth').mockImplementationOnce(throwError) 51 | const httpResponse = await sut.handle(mockRequest()) 52 | expect(httpResponse).toEqual(serverError(new Error())) 53 | }) 54 | 55 | test('Should return 200 if valid credentials are provided', async () => { 56 | const { sut, authenticationSpy } = makeSut() 57 | const httpResponse = await sut.handle(mockRequest()) 58 | expect(httpResponse).toEqual(ok(authenticationSpy.authenticationModel)) 59 | }) 60 | 61 | test('Should call Validation with correct value', async () => { 62 | const { sut, validationSpy } = makeSut() 63 | const httpRequest = mockRequest() 64 | await sut.handle(httpRequest) 65 | expect(validationSpy.input).toEqual(httpRequest.body) 66 | }) 67 | 68 | test('Should return 400 if Validation returns an error', async () => { 69 | const { sut, validationSpy } = makeSut() 70 | validationSpy.error = new MissingParamError(faker.random.word()) 71 | const httpResponse = await sut.handle(mockRequest()) 72 | expect(httpResponse).toEqual(badRequest(validationSpy.error)) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /src/presentation/controllers/login/login/login-controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpRequest, HttpResponse, Authentication, Validation } from './login-controller-protocols' 2 | import { badRequest, serverError, unauthorized, ok } from '@/presentation/helpers/http/http-helper' 3 | 4 | export class LoginController implements Controller { 5 | constructor ( 6 | private readonly authentication: Authentication, 7 | private readonly validation: Validation 8 | ) {} 9 | 10 | async handle (httpRequest: HttpRequest): Promise { 11 | try { 12 | const error = this.validation.validate(httpRequest.body) 13 | if (error) { 14 | return badRequest(error) 15 | } 16 | const { email, password } = httpRequest.body 17 | const authenticationModel = await this.authentication.auth({ 18 | email, 19 | password 20 | }) 21 | if (!authenticationModel) { 22 | return unauthorized() 23 | } 24 | return ok(authenticationModel) 25 | } catch (error) { 26 | return serverError(error) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/presentation/controllers/login/signup/signup-controller-protocols.ts: -------------------------------------------------------------------------------- 1 | export * from '@/presentation/protocols' 2 | export * from '@/domain/usecases/account/add-account' 3 | export * from '@/domain/usecases/account/authentication' 4 | export * from '@/domain/models/account/account' 5 | -------------------------------------------------------------------------------- /src/presentation/controllers/login/signup/signup-controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { SignUpController } from './signup-controller' 2 | import { MissingParamError, ServerError, EmailInUseError } from '@/presentation/errors' 3 | import { HttpRequest } from '@/presentation/protocols' 4 | import { ok, serverError, badRequest, forbidden } from '@/presentation/helpers/http/http-helper' 5 | import { AuthenticationSpy, ValidationSpy, AddAccountSpy } from '@/presentation/test' 6 | import { throwError } from '@/domain/test' 7 | import faker from 'faker' 8 | 9 | const mockRequest = (): HttpRequest => { 10 | const password = faker.internet.password() 11 | return { 12 | body: { 13 | name: faker.name.findName(), 14 | email: faker.internet.email(), 15 | password, 16 | passwordConfirmation: password 17 | } 18 | } 19 | } 20 | 21 | type SutTypes = { 22 | sut: SignUpController 23 | addAccountSpy: AddAccountSpy 24 | validationSpy: ValidationSpy 25 | authenticationSpy: AuthenticationSpy 26 | } 27 | 28 | const makeSut = (): SutTypes => { 29 | const authenticationSpy = new AuthenticationSpy() 30 | const addAccountSpy = new AddAccountSpy() 31 | const validationSpy = new ValidationSpy() 32 | const sut = new SignUpController(addAccountSpy, validationSpy, authenticationSpy) 33 | return { 34 | sut, 35 | addAccountSpy, 36 | validationSpy, 37 | authenticationSpy 38 | } 39 | } 40 | 41 | describe('SignUp Controller', () => { 42 | test('Should return 500 if AddAccount throws', async () => { 43 | const { sut, addAccountSpy } = makeSut() 44 | jest.spyOn(addAccountSpy, 'add').mockImplementationOnce(throwError) 45 | const httpResponse = await sut.handle(mockRequest()) 46 | expect(httpResponse).toEqual(serverError(new ServerError(null))) 47 | }) 48 | 49 | test('Should call AddAccount with correct values', async () => { 50 | const { sut, addAccountSpy } = makeSut() 51 | const httpRequest = mockRequest() 52 | await sut.handle(httpRequest) 53 | expect(addAccountSpy.addAccountParams).toEqual({ 54 | name: httpRequest.body.name, 55 | email: httpRequest.body.email, 56 | password: httpRequest.body.password 57 | }) 58 | }) 59 | 60 | test('Should return 403 if AddAccount returns null', async () => { 61 | const { sut, addAccountSpy } = makeSut() 62 | addAccountSpy.accountModel = null 63 | const httpResponse = await sut.handle(mockRequest()) 64 | expect(httpResponse).toEqual(forbidden(new EmailInUseError())) 65 | }) 66 | 67 | test('Should return 200 if valid data is provided', async () => { 68 | const { sut, authenticationSpy } = makeSut() 69 | const httpResponse = await sut.handle(mockRequest()) 70 | expect(httpResponse).toEqual(ok(authenticationSpy.authenticationModel)) 71 | }) 72 | 73 | test('Should call Validation with correct value', async () => { 74 | const { sut, validationSpy } = makeSut() 75 | const httpRequest = mockRequest() 76 | await sut.handle(httpRequest) 77 | expect(validationSpy.input).toEqual(httpRequest.body) 78 | }) 79 | 80 | test('Should return 400 if Validation returns an error', async () => { 81 | const { sut, validationSpy } = makeSut() 82 | validationSpy.error = new MissingParamError(faker.random.word()) 83 | const httpResponse = await sut.handle(mockRequest()) 84 | expect(httpResponse).toEqual(badRequest(validationSpy.error)) 85 | }) 86 | 87 | test('Should call Authentication with correct values', async () => { 88 | const { sut, authenticationSpy } = makeSut() 89 | const httpRequest = mockRequest() 90 | await sut.handle(httpRequest) 91 | expect(authenticationSpy.authenticationParams).toEqual({ 92 | email: httpRequest.body.email, 93 | password: httpRequest.body.password 94 | }) 95 | }) 96 | 97 | test('Should return 500 if Authentication throws', async () => { 98 | const { sut, authenticationSpy } = makeSut() 99 | jest.spyOn(authenticationSpy, 'auth').mockImplementationOnce(throwError) 100 | const httpResponse = await sut.handle(mockRequest()) 101 | expect(httpResponse).toEqual(serverError(new Error())) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /src/presentation/controllers/login/signup/signup-controller.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse, HttpRequest, Controller, AddAccount, Authentication } from './signup-controller-protocols' 2 | import { badRequest, serverError, ok, forbidden } from '@/presentation/helpers/http/http-helper' 3 | import { Validation } from '@/presentation/protocols/validation' 4 | import { EmailInUseError } from '@/presentation/errors' 5 | 6 | export class SignUpController implements Controller { 7 | constructor ( 8 | private readonly addAccount: AddAccount, 9 | private readonly validation: Validation, 10 | private readonly authentication: Authentication 11 | ) {} 12 | 13 | async handle (httpRequest: HttpRequest): Promise { 14 | try { 15 | const error = this.validation.validate(httpRequest.body) 16 | if (error) { 17 | return badRequest(error) 18 | } 19 | const { name, email, password } = httpRequest.body 20 | const account = await this.addAccount.add({ 21 | name, 22 | email, 23 | password 24 | }) 25 | if (!account) { 26 | return forbidden(new EmailInUseError()) 27 | } 28 | const authenticationModel = await this.authentication.auth({ 29 | email, 30 | password 31 | }) 32 | return ok(authenticationModel) 33 | } catch (error) { 34 | return serverError(error) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/presentation/controllers/survey-result/load-survey-result/load-survey-result-controller-protocols.ts: -------------------------------------------------------------------------------- 1 | export * from '@/presentation/protocols' 2 | export * from '@/domain/usecases/survey/load-survey-by-id' 3 | export * from '@/domain/usecases/survey-result/load-survey-result' 4 | -------------------------------------------------------------------------------- /src/presentation/controllers/survey-result/load-survey-result/load-survey-result-controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { LoadSurveyResultController } from './load-survey-result-controller' 2 | import { HttpRequest } from '@/presentation/protocols' 3 | import { LoadSurveyByIdSpy, LoadSurveyResultSpy } from '@/presentation/test' 4 | import { forbidden, serverError, ok } from '@/presentation/helpers/http/http-helper' 5 | import { InvalidParamError } from '@/presentation/errors' 6 | import { throwError } from '@/domain/test' 7 | import MockDate from 'mockdate' 8 | import faker from 'faker' 9 | 10 | const mockRequest = (): HttpRequest => ({ 11 | params: { 12 | surveyId: faker.random.uuid() 13 | } 14 | }) 15 | 16 | type SutTypes = { 17 | sut: LoadSurveyResultController 18 | loadSurveyByIdSpy: LoadSurveyByIdSpy 19 | loadSurveyResultSpy: LoadSurveyResultSpy 20 | } 21 | 22 | const makeSut = (): SutTypes => { 23 | const loadSurveyByIdSpy = new LoadSurveyByIdSpy() 24 | const loadSurveyResultSpy = new LoadSurveyResultSpy() 25 | const sut = new LoadSurveyResultController(loadSurveyByIdSpy, loadSurveyResultSpy) 26 | return { 27 | sut, 28 | loadSurveyByIdSpy, 29 | loadSurveyResultSpy 30 | } 31 | } 32 | 33 | describe('LoadSurveyResult Controller', () => { 34 | beforeAll(() => { 35 | MockDate.set(new Date()) 36 | }) 37 | 38 | afterAll(() => { 39 | MockDate.reset() 40 | }) 41 | 42 | test('Should call LoadSurveyById with correct value', async () => { 43 | const { sut, loadSurveyByIdSpy } = makeSut() 44 | const httpRequest = mockRequest() 45 | await sut.handle(httpRequest) 46 | expect(loadSurveyByIdSpy.id).toBe(httpRequest.params.surveyId) 47 | }) 48 | 49 | test('Should return 403 if LoadSurveyById returns null', async () => { 50 | const { sut, loadSurveyByIdSpy } = makeSut() 51 | loadSurveyByIdSpy.surveyModel = null 52 | const httpResponse = await sut.handle(mockRequest()) 53 | expect(httpResponse).toEqual(forbidden(new InvalidParamError('surveyId'))) 54 | }) 55 | 56 | test('Should return 500 if LoadSurveyById throws', async () => { 57 | const { sut, loadSurveyByIdSpy } = makeSut() 58 | jest.spyOn(loadSurveyByIdSpy, 'loadById').mockImplementationOnce(throwError) 59 | const httpResponse = await sut.handle(mockRequest()) 60 | expect(httpResponse).toEqual(serverError(new Error())) 61 | }) 62 | 63 | test('Should call LoadSurveyResult with correct value', async () => { 64 | const { sut, loadSurveyResultSpy } = makeSut() 65 | const httpRequest = mockRequest() 66 | await sut.handle(httpRequest) 67 | expect(loadSurveyResultSpy.surveyId).toBe(httpRequest.params.surveyId) 68 | }) 69 | 70 | test('Should return 500 if LoadSurveyResult throws', async () => { 71 | const { sut, loadSurveyResultSpy } = makeSut() 72 | jest.spyOn(loadSurveyResultSpy, 'load').mockImplementationOnce(throwError) 73 | const httpResponse = await sut.handle(mockRequest()) 74 | expect(httpResponse).toEqual(serverError(new Error())) 75 | }) 76 | 77 | test('Should return 200 on success', async () => { 78 | const { sut, loadSurveyResultSpy } = makeSut() 79 | const httpResponse = await sut.handle(mockRequest()) 80 | expect(httpResponse).toEqual(ok(loadSurveyResultSpy.surveyResultModel)) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /src/presentation/controllers/survey-result/load-survey-result/load-survey-result-controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpRequest, HttpResponse, LoadSurveyById, LoadSurveyResult } from './load-survey-result-controller-protocols' 2 | import { forbidden, serverError, ok } from '@/presentation/helpers/http/http-helper' 3 | import { InvalidParamError } from '@/presentation/errors' 4 | 5 | export class LoadSurveyResultController implements Controller { 6 | constructor ( 7 | private readonly loadSurveyById: LoadSurveyById, 8 | private readonly loadSurveyResult: LoadSurveyResult 9 | ) {} 10 | 11 | async handle (httpRequest: HttpRequest): Promise { 12 | try { 13 | const { surveyId } = httpRequest.params 14 | const survey = await this.loadSurveyById.loadById(surveyId) 15 | if (!survey) { 16 | return forbidden(new InvalidParamError('surveyId')) 17 | } 18 | const surveyResult = await this.loadSurveyResult.load(surveyId) 19 | return ok(surveyResult) 20 | } catch (error) { 21 | return serverError(error) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/presentation/controllers/survey-result/save-survey-result/save-survey-result-controller-protocols.ts: -------------------------------------------------------------------------------- 1 | export * from '@/presentation/protocols' 2 | export * from '@/domain/usecases/survey/load-survey-by-id' 3 | export * from '@/domain/usecases/survey-result/save-survey-result' 4 | export * from '@/domain/models/survey/survey' 5 | export * from '@/domain/models/survey-result/survey-result' 6 | -------------------------------------------------------------------------------- /src/presentation/controllers/survey-result/save-survey-result/save-survey-result-controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { SaveSurveyResultController } from './save-survey-result-controller' 2 | import { HttpRequest } from '@/presentation/protocols' 3 | import { InvalidParamError } from '@/presentation/errors' 4 | import { forbidden, serverError, ok } from '@/presentation/helpers/http/http-helper' 5 | import { SaveSurveyResultSpy, LoadSurveyByIdSpy } from '@/presentation/test' 6 | import { throwError } from '@/domain/test' 7 | import MockDate from 'mockdate' 8 | import faker from 'faker' 9 | 10 | const mockRequest = (answer: string = null): HttpRequest => ({ 11 | params: { 12 | surveyId: faker.random.uuid() 13 | }, 14 | body: { 15 | answer 16 | }, 17 | accountId: faker.random.uuid() 18 | }) 19 | 20 | type SutTypes = { 21 | sut: SaveSurveyResultController 22 | loadSurveyByIdSpy: LoadSurveyByIdSpy 23 | saveSurveyResultSpy: SaveSurveyResultSpy 24 | } 25 | 26 | const makeSut = (): SutTypes => { 27 | const loadSurveyByIdSpy = new LoadSurveyByIdSpy() 28 | const saveSurveyResultSpy = new SaveSurveyResultSpy() 29 | const sut = new SaveSurveyResultController(loadSurveyByIdSpy, saveSurveyResultSpy) 30 | return { 31 | sut, 32 | loadSurveyByIdSpy, 33 | saveSurveyResultSpy 34 | } 35 | } 36 | 37 | describe('SaveSurveyResult Controller', () => { 38 | beforeAll(() => { 39 | MockDate.set(new Date()) 40 | }) 41 | 42 | afterAll(() => { 43 | MockDate.reset() 44 | }) 45 | 46 | test('Should call LoadSurveyById with correct values', async () => { 47 | const { sut, loadSurveyByIdSpy } = makeSut() 48 | const httpRequest = mockRequest() 49 | await sut.handle(httpRequest) 50 | expect(loadSurveyByIdSpy.id).toBe(httpRequest.params.surveyId) 51 | }) 52 | 53 | test('Should return 403 if LoadSurveyById returns null', async () => { 54 | const { sut, loadSurveyByIdSpy } = makeSut() 55 | loadSurveyByIdSpy.surveyModel = null 56 | const httpResponse = await sut.handle(mockRequest()) 57 | expect(httpResponse).toEqual(forbidden(new InvalidParamError('surveyId'))) 58 | }) 59 | 60 | test('Should return 500 if LoadSurveyById throws', async () => { 61 | const { sut, loadSurveyByIdSpy } = makeSut() 62 | jest.spyOn(loadSurveyByIdSpy, 'loadById').mockImplementationOnce(throwError) 63 | const httpResponse = await sut.handle(mockRequest()) 64 | expect(httpResponse).toEqual(serverError(new Error())) 65 | }) 66 | 67 | test('Should return 403 if an invalid answer is provided', async () => { 68 | const { sut } = makeSut() 69 | const httpResponse = await sut.handle(mockRequest()) 70 | expect(httpResponse).toEqual(forbidden(new InvalidParamError('answer'))) 71 | }) 72 | 73 | test('Should call SaveSurveyResult with correct values', async () => { 74 | const { sut, saveSurveyResultSpy, loadSurveyByIdSpy } = makeSut() 75 | const httpRequest = mockRequest(loadSurveyByIdSpy.surveyModel.answers[0].answer) 76 | await sut.handle(httpRequest) 77 | expect(saveSurveyResultSpy.saveSurveyResultParams).toEqual({ 78 | surveyId: httpRequest.params.surveyId, 79 | accountId: httpRequest.accountId, 80 | date: new Date(), 81 | answer: httpRequest.body.answer 82 | }) 83 | }) 84 | 85 | test('Should return 500 if SaveSurveyResult throws', async () => { 86 | const { sut, saveSurveyResultSpy, loadSurveyByIdSpy } = makeSut() 87 | jest.spyOn(saveSurveyResultSpy, 'save').mockImplementationOnce(throwError) 88 | const httpRequest = mockRequest(loadSurveyByIdSpy.surveyModel.answers[0].answer) 89 | const httpResponse = await sut.handle(httpRequest) 90 | expect(httpResponse).toEqual(serverError(new Error())) 91 | }) 92 | 93 | test('Should return 200 on success', async () => { 94 | const { sut, saveSurveyResultSpy, loadSurveyByIdSpy } = makeSut() 95 | const httpRequest = mockRequest(loadSurveyByIdSpy.surveyModel.answers[0].answer) 96 | const httpResponse = await sut.handle(httpRequest) 97 | expect(httpResponse).toEqual(ok(saveSurveyResultSpy.surveyResultModel)) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /src/presentation/controllers/survey-result/save-survey-result/save-survey-result-controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpRequest, HttpResponse, LoadSurveyById, SaveSurveyResult } from './save-survey-result-controller-protocols' 2 | import { forbidden, serverError, ok } from '@/presentation/helpers/http/http-helper' 3 | import { InvalidParamError } from '@/presentation/errors' 4 | 5 | export class SaveSurveyResultController implements Controller { 6 | constructor ( 7 | private readonly loadSurveyById: LoadSurveyById, 8 | private readonly saveSurveyResult: SaveSurveyResult 9 | ) {} 10 | 11 | async handle (httpRequest: HttpRequest): Promise { 12 | try { 13 | const { surveyId } = httpRequest.params 14 | const { answer } = httpRequest.body 15 | const { accountId } = httpRequest 16 | const survey = await this.loadSurveyById.loadById(surveyId) 17 | if (survey) { 18 | const answers = survey.answers.map(a => a.answer) 19 | if (!answers.includes(answer)) { 20 | return forbidden(new InvalidParamError('answer')) 21 | } 22 | } else { 23 | return forbidden(new InvalidParamError('surveyId')) 24 | } 25 | const surveyResult = await this.saveSurveyResult.save({ 26 | accountId, 27 | surveyId, 28 | answer, 29 | date: new Date() 30 | }) 31 | return ok(surveyResult) 32 | } catch (error) { 33 | return serverError(error) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/presentation/controllers/survey/add-survey/add-survey-controller-protocols.ts: -------------------------------------------------------------------------------- 1 | export * from '@/presentation/protocols' 2 | export * from '@/domain/usecases/survey/add-survey' 3 | -------------------------------------------------------------------------------- /src/presentation/controllers/survey/add-survey/add-survey-controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequest } from '@/presentation/protocols' 2 | import { AddSurveyController } from './add-survey-controller' 3 | import { badRequest, serverError, ok } from '@/presentation/helpers/http/http-helper' 4 | import { ValidationSpy, AddSurveySpy } from '@/presentation/test' 5 | import { throwError } from '@/domain/test' 6 | import MockDate from 'mockdate' 7 | import faker from 'faker' 8 | 9 | const mockRequest = (): HttpRequest => ({ 10 | body: { 11 | question: faker.random.words(), 12 | answers: [{ 13 | image: faker.image.imageUrl(), 14 | answer: faker.random.word() 15 | }], 16 | date: new Date() 17 | } 18 | }) 19 | 20 | type SutTypes = { 21 | sut: AddSurveyController 22 | validationSpy: ValidationSpy 23 | addSurveySpy: AddSurveySpy 24 | } 25 | 26 | const makeSut = (): SutTypes => { 27 | const validationSpy = new ValidationSpy() 28 | const addSurveySpy = new AddSurveySpy() 29 | const sut = new AddSurveyController(validationSpy, addSurveySpy) 30 | return { 31 | sut, 32 | validationSpy, 33 | addSurveySpy 34 | } 35 | } 36 | 37 | describe('AddSurvey Controller', () => { 38 | beforeAll(() => { 39 | MockDate.set(new Date()) 40 | }) 41 | 42 | afterAll(() => { 43 | MockDate.reset() 44 | }) 45 | 46 | test('Should call Validation with correct values', async () => { 47 | const { sut, validationSpy } = makeSut() 48 | const httpRequest = mockRequest() 49 | await sut.handle(httpRequest) 50 | expect(validationSpy.input).toEqual(httpRequest.body) 51 | }) 52 | 53 | test('Should return 400 if Validation fails', async () => { 54 | const { sut, validationSpy } = makeSut() 55 | validationSpy.error = new Error() 56 | const httpResponse = await sut.handle(mockRequest()) 57 | expect(httpResponse).toEqual(badRequest(validationSpy.error)) 58 | }) 59 | 60 | test('Should call AddSurvey with correct values', async () => { 61 | const { sut, addSurveySpy } = makeSut() 62 | const httpRequest = mockRequest() 63 | await sut.handle(httpRequest) 64 | expect(addSurveySpy.addSurveyParams).toEqual(httpRequest.body) 65 | }) 66 | 67 | test('Should return 500 if AddSurvey throws', async () => { 68 | const { sut, addSurveySpy } = makeSut() 69 | jest.spyOn(addSurveySpy, 'add').mockImplementationOnce(throwError) 70 | const httpResponse = await sut.handle(mockRequest()) 71 | expect(httpResponse).toEqual(serverError(new Error())) 72 | }) 73 | 74 | test('Should return 204 on success', async () => { 75 | const { sut } = makeSut() 76 | const httpResponse = await sut.handle(mockRequest()) 77 | expect(httpResponse).toEqual(ok(httpResponse.body)) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /src/presentation/controllers/survey/add-survey/add-survey-controller.ts: -------------------------------------------------------------------------------- 1 | import { badRequest, serverError, ok } from '@/presentation/helpers/http/http-helper' 2 | import { Controller, HttpRequest, HttpResponse, Validation, AddSurvey } from './add-survey-controller-protocols' 3 | 4 | export class AddSurveyController implements Controller { 5 | constructor ( 6 | private readonly validation: Validation, 7 | private readonly addSurvey: AddSurvey 8 | ) { } 9 | 10 | async handle (httpRequest: HttpRequest): Promise { 11 | try { 12 | const error = this.validation.validate(httpRequest.body) 13 | if (error) { 14 | return badRequest(error) 15 | } 16 | const { question, answers } = httpRequest.body 17 | const addSurveys = await this.addSurvey.add({ 18 | question, 19 | answers, 20 | date: new Date() 21 | }) 22 | return ok(addSurveys) 23 | } catch (error) { 24 | return serverError(error) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/presentation/controllers/survey/load-surveys/load-surveys-controller-protocols.ts: -------------------------------------------------------------------------------- 1 | export * from '@/presentation/protocols' 2 | export * from '@/domain/usecases/survey/load-surveys' 3 | export * from '@/domain/models/survey/survey' 4 | -------------------------------------------------------------------------------- /src/presentation/controllers/survey/load-surveys/load-surveys-controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { LoadSurveysController } from './load-surveys-controller' 2 | import { HttpRequest } from './load-surveys-controller-protocols' 3 | import { ok, serverError, noContent } from '@/presentation/helpers/http/http-helper' 4 | import { LoadSurveysSpy } from '@/presentation/test' 5 | import { throwError } from '@/domain/test' 6 | import MockDate from 'mockdate' 7 | import faker from 'faker' 8 | 9 | const mockRequest = (): HttpRequest => ({ accountId: faker.random.uuid() }) 10 | 11 | type SutTypes = { 12 | sut: LoadSurveysController 13 | loadSurveysSpy: LoadSurveysSpy 14 | } 15 | 16 | const makeSut = (): SutTypes => { 17 | const loadSurveysSpy = new LoadSurveysSpy() 18 | const sut = new LoadSurveysController(loadSurveysSpy) 19 | return { 20 | sut, 21 | loadSurveysSpy 22 | } 23 | } 24 | 25 | describe('LoadSurveys Controller', () => { 26 | beforeAll(() => { 27 | MockDate.set(new Date()) 28 | }) 29 | 30 | afterAll(() => { 31 | MockDate.reset() 32 | }) 33 | 34 | test('Should call LoadSurveys with correct value', async () => { 35 | const { sut, loadSurveysSpy } = makeSut() 36 | const httpRequest = mockRequest() 37 | await sut.handle(httpRequest) 38 | expect(loadSurveysSpy.accountId).toBe(httpRequest.accountId) 39 | }) 40 | 41 | test('Should return 200 on success', async () => { 42 | const { sut, loadSurveysSpy } = makeSut() 43 | const httpResponse = await sut.handle(mockRequest()) 44 | expect(httpResponse).toEqual(ok(loadSurveysSpy.surveyModels)) 45 | }) 46 | 47 | test('Should return 204 if LoadSurveys returns empty', async () => { 48 | const { sut, loadSurveysSpy } = makeSut() 49 | loadSurveysSpy.surveyModels = [] 50 | const httpResponse = await sut.handle(mockRequest()) 51 | expect(httpResponse).toEqual(noContent()) 52 | }) 53 | 54 | test('Should return 500 if LoadSurveys throws', async () => { 55 | const { sut, loadSurveysSpy } = makeSut() 56 | jest.spyOn(loadSurveysSpy, 'load').mockImplementationOnce(throwError) 57 | const httpResponse = await sut.handle(mockRequest()) 58 | expect(httpResponse).toEqual(serverError(new Error())) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/presentation/controllers/survey/load-surveys/load-surveys-controller.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Controller, HttpRequest, HttpResponse, LoadSurveys } from './load-surveys-controller-protocols' 3 | import { ok, serverError, noContent } from '@/presentation/helpers/http/http-helper' 4 | 5 | export class LoadSurveysController implements Controller { 6 | constructor (private readonly loadSurveys: LoadSurveys) {} 7 | 8 | async handle (httpRequest: HttpRequest): Promise { 9 | try { 10 | const surveys = await this.loadSurveys.load(httpRequest.accountId) 11 | return surveys.length ? ok(surveys) : noContent() 12 | } catch (error) { 13 | return serverError(error) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/presentation/errors/access-denied-error.ts.ts: -------------------------------------------------------------------------------- 1 | export class AccessDeniedError extends Error { 2 | constructor () { 3 | super('Access denied') 4 | this.name = 'AccessDeniedError' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/presentation/errors/email-in-use-error.ts: -------------------------------------------------------------------------------- 1 | export class EmailInUseError extends Error { 2 | constructor () { 3 | super('The received email is already in use') 4 | this.name = 'EmailInUseError' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/presentation/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './invalid-params-error' 2 | export * from './missing-params-error' 3 | export * from './server-error' 4 | export * from './unauthorized-error' 5 | export * from './email-in-use-error' 6 | export * from './access-denied-error.ts' 7 | -------------------------------------------------------------------------------- /src/presentation/errors/invalid-params-error.ts: -------------------------------------------------------------------------------- 1 | export class InvalidParamError extends Error { 2 | constructor (paramName: string) { 3 | super(`Invalid param: ${paramName}`) 4 | this.name = 'InvalidParamError' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/presentation/errors/missing-params-error.ts: -------------------------------------------------------------------------------- 1 | export class MissingParamError extends Error { 2 | constructor (paramName: string) { 3 | super(`Missing param: ${paramName}`) 4 | this.name = 'MissingParamError' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/presentation/errors/server-error.ts: -------------------------------------------------------------------------------- 1 | export class ServerError extends Error { 2 | constructor (stack: string) { 3 | super('Internal server error') 4 | this.name = 'ServerError' 5 | this.stack = stack 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/presentation/errors/unauthorized-error.ts: -------------------------------------------------------------------------------- 1 | export class UnauthorizedError extends Error { 2 | constructor () { 3 | super('Unauthorized') 4 | this.name = 'UnauthorizedError' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/presentation/helpers/http/http-helper.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse } from '@/presentation/protocols/http' 2 | import { ServerError, UnauthorizedError } from '@/presentation/errors' 3 | 4 | export const badRequest = (error: Error): HttpResponse => ({ 5 | statusCode: 400, 6 | body: error 7 | }) 8 | 9 | export const forbidden = (error: Error): HttpResponse => ({ 10 | statusCode: 403, 11 | body: error 12 | }) 13 | 14 | export const unauthorized = (): HttpResponse => ({ 15 | statusCode: 401, 16 | body: new UnauthorizedError() 17 | }) 18 | 19 | export const serverError = (error: Error): HttpResponse => ({ 20 | statusCode: 500, 21 | body: new ServerError(error.stack) 22 | }) 23 | 24 | export const ok = (data: any): HttpResponse => ({ 25 | statusCode: 200, 26 | body: data 27 | }) 28 | 29 | export const noContent = (): HttpResponse => ({ 30 | statusCode: 204, 31 | body: null 32 | }) 33 | -------------------------------------------------------------------------------- /src/presentation/middleware/auth-middleware-protocols.ts: -------------------------------------------------------------------------------- 1 | export * from '@/presentation/protocols' 2 | export * from '@/domain/usecases/account/load-account-by-token' 3 | export * from '@/domain/models/account/account' 4 | -------------------------------------------------------------------------------- /src/presentation/middleware/auth-middleware.spec.ts: -------------------------------------------------------------------------------- 1 | import { AuthMiddleware } from './auth-middleware' 2 | import { HttpRequest } from './auth-middleware-protocols' 3 | import { forbidden, ok, serverError } from '@/presentation/helpers/http/http-helper' 4 | import { AccessDeniedError } from '@/presentation/errors' 5 | import { LoadAccountByTokenSpy } from '@/presentation/test' 6 | import { throwError } from '@/domain/test' 7 | 8 | const mockRequest = (): HttpRequest => ({ 9 | headers: { 10 | 'x-access-token': 'any_token' 11 | } 12 | }) 13 | 14 | type SutTypes = { 15 | sut: AuthMiddleware 16 | loadAccountByTokenSpy: LoadAccountByTokenSpy 17 | } 18 | 19 | const makeSut = (role?: string): SutTypes => { 20 | const loadAccountByTokenSpy = new LoadAccountByTokenSpy() 21 | const sut = new AuthMiddleware(loadAccountByTokenSpy, role) 22 | return { 23 | sut, 24 | loadAccountByTokenSpy 25 | } 26 | } 27 | 28 | describe('Auth Middleware', () => { 29 | test('Should return 403 if no x-access-token exists in headers', async () => { 30 | const { sut } = makeSut() 31 | const httpResponse = await sut.handle({}) 32 | expect(httpResponse).toEqual(forbidden(new AccessDeniedError())) 33 | }) 34 | 35 | test('Should call LoadAccountByToken with correct accessToken', async () => { 36 | const role = 'any_role' 37 | const { sut, loadAccountByTokenSpy } = makeSut(role) 38 | const httpRequest = mockRequest() 39 | await sut.handle(httpRequest) 40 | expect(loadAccountByTokenSpy.accessToken).toBe(httpRequest.headers['x-access-token']) 41 | expect(loadAccountByTokenSpy.role).toBe(role) 42 | }) 43 | 44 | test('Should return 403 if LoadAccountByToken returns null', async () => { 45 | const { sut, loadAccountByTokenSpy } = makeSut() 46 | loadAccountByTokenSpy.accountModel = null 47 | const httpResponse = await sut.handle(mockRequest()) 48 | expect(httpResponse).toEqual(forbidden(new AccessDeniedError())) 49 | }) 50 | 51 | test('Should return 200 if LoadAccountByToken returns an account', async () => { 52 | const { sut, loadAccountByTokenSpy } = makeSut() 53 | const httpResponse = await sut.handle(mockRequest()) 54 | expect(httpResponse).toEqual(ok({ 55 | accountId: loadAccountByTokenSpy.accountModel.id 56 | })) 57 | }) 58 | 59 | test('Should return 500 if LoadAccountByToken throws', async () => { 60 | const { sut, loadAccountByTokenSpy } = makeSut() 61 | jest.spyOn(loadAccountByTokenSpy, 'load').mockImplementationOnce(throwError) 62 | const httpResponse = await sut.handle(mockRequest()) 63 | expect(httpResponse).toEqual(serverError(new Error())) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /src/presentation/middleware/auth-middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware, HttpRequest, HttpResponse, LoadAccountByToken } from './auth-middleware-protocols' 2 | import { forbidden, ok, serverError } from '@/presentation/helpers/http/http-helper' 3 | import { AccessDeniedError } from '@/presentation/errors' 4 | 5 | export class AuthMiddleware implements Middleware { 6 | constructor ( 7 | private readonly loadAccountByToken: LoadAccountByToken, 8 | private readonly role?: string 9 | ) {} 10 | 11 | async handle (httpRequest: HttpRequest): Promise { 12 | try { 13 | const accessToken = httpRequest.headers?.['x-access-token'] 14 | if (accessToken) { 15 | const account = await this.loadAccountByToken.load(accessToken, this.role) 16 | if (account) { 17 | return ok({ accountId: account.id }) 18 | } 19 | } 20 | return forbidden(new AccessDeniedError()) 21 | } catch (error) { 22 | return serverError(error) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/presentation/protocols/controller.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequest, HttpResponse } from './http' 2 | 3 | export interface Controller { 4 | handle: (httpRequest: HttpRequest) => Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/presentation/protocols/http.ts: -------------------------------------------------------------------------------- 1 | export type HttpResponse = { 2 | statusCode: number 3 | body: any 4 | } 5 | 6 | export type HttpRequest = { 7 | body?: any 8 | headers?: any 9 | params?: any 10 | accountId?: string 11 | } 12 | -------------------------------------------------------------------------------- /src/presentation/protocols/index.ts: -------------------------------------------------------------------------------- 1 | export * from './controller' 2 | export * from './http' 3 | export * from './validation' 4 | export * from './middleware' 5 | -------------------------------------------------------------------------------- /src/presentation/protocols/middleware.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequest, HttpResponse } from './http' 2 | 3 | export interface Middleware { 4 | handle: (httpRequest: HttpRequest) => Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/presentation/protocols/validation.ts: -------------------------------------------------------------------------------- 1 | export interface Validation { 2 | validate: (input: any) => Error 3 | } 4 | -------------------------------------------------------------------------------- /src/presentation/test/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mock-validation' 2 | export * from './mock-account' 3 | export * from './mock-survey' 4 | export * from './mock-survey-result' 5 | -------------------------------------------------------------------------------- /src/presentation/test/mock-account.ts: -------------------------------------------------------------------------------- 1 | import { AddAccount, AddAccountParams } from '@/domain/usecases/account/add-account' 2 | import { Authentication, AuthenticationParams } from '@/domain/usecases/account/authentication' 3 | import { LoadAccountByToken } from '@/domain/usecases/account/load-account-by-token' 4 | import { AccountModel } from '@/domain/models/account/account' 5 | import { AuthenticationModel } from '@/domain/models/account/authentication' 6 | import { mockAccountModel } from '@/domain/test' 7 | import faker from 'faker' 8 | 9 | export class AddAccountSpy implements AddAccount { 10 | accountModel = mockAccountModel() 11 | addAccountParams: AddAccountParams 12 | 13 | async add (account: AddAccountParams): Promise { 14 | this.addAccountParams = account 15 | return this.accountModel 16 | } 17 | } 18 | 19 | export class AuthenticationSpy implements Authentication { 20 | authenticationParams: AuthenticationParams 21 | authenticationModel = { 22 | accessToken: faker.random.uuid(), 23 | name: faker.name.findName() 24 | } 25 | 26 | async auth (authenticationParams: AuthenticationParams): Promise { 27 | this.authenticationParams = authenticationParams 28 | return this.authenticationModel 29 | } 30 | } 31 | 32 | export class LoadAccountByTokenSpy implements LoadAccountByToken { 33 | accountModel = mockAccountModel() 34 | accessToken: string 35 | role: string 36 | 37 | async load (accessToken: string, role?: string): Promise { 38 | this.accessToken = accessToken 39 | this.role = role 40 | return this.accountModel 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/presentation/test/mock-survey-result.ts: -------------------------------------------------------------------------------- 1 | import { SaveSurveyResult, SaveSurveyResultParams } from '@/domain/usecases/survey-result/save-survey-result' 2 | import { SurveyResultModel } from '@/domain/models/survey-result/survey-result' 3 | import { mockSurveyResultModel } from '@/domain/test' 4 | import { LoadSurveyResult } from '@/domain/usecases/survey-result/load-survey-result' 5 | 6 | export class SaveSurveyResultSpy implements SaveSurveyResult { 7 | surveyResultModel = mockSurveyResultModel() 8 | saveSurveyResultParams: SaveSurveyResultParams 9 | 10 | async save (data: SaveSurveyResultParams): Promise { 11 | this.saveSurveyResultParams = data 12 | return this.surveyResultModel 13 | } 14 | } 15 | 16 | export class LoadSurveyResultSpy implements LoadSurveyResult { 17 | surveyResultModel = mockSurveyResultModel() 18 | surveyId: string 19 | 20 | async load (surveyId: string): Promise { 21 | this.surveyId = surveyId 22 | return this.surveyResultModel 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/presentation/test/mock-survey.ts: -------------------------------------------------------------------------------- 1 | 2 | import { AddSurveyParams, AddSurvey } from '@/domain/usecases/survey/add-survey' 3 | import { LoadSurveyById } from '@/domain/usecases/survey/load-survey-by-id' 4 | import { LoadSurveys } from '@/domain/usecases/survey/load-surveys' 5 | import { SurveyModel } from '@/domain/models/survey/survey' 6 | import { mockSurveyModels, mockSurveyModel } from '@/domain/test' 7 | 8 | export class AddSurveySpy implements AddSurvey { 9 | addSurveyParams: AddSurveyParams 10 | 11 | async add (data: AddSurveyParams): Promise { 12 | this.addSurveyParams = data 13 | } 14 | } 15 | 16 | export class LoadSurveysSpy implements LoadSurveys { 17 | surveyModels = mockSurveyModels() 18 | accountId: string 19 | 20 | async load (accountId: string): Promise { 21 | this.accountId = accountId 22 | return this.surveyModels 23 | } 24 | } 25 | 26 | export class LoadSurveyByIdSpy implements LoadSurveyById { 27 | surveyModel = mockSurveyModel() 28 | id: string 29 | 30 | async loadById (id: string): Promise { 31 | this.id = id 32 | return this.surveyModel 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/presentation/test/mock-validation.ts: -------------------------------------------------------------------------------- 1 | import { Validation } from '@/presentation/protocols' 2 | 3 | export class ValidationSpy implements Validation { 4 | error: Error = null 5 | input: any 6 | 7 | validate (input: any): Error { 8 | this.input = input 9 | return this.error 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/validation/protocols/email-validator.ts: -------------------------------------------------------------------------------- 1 | export interface EmailValidator { 2 | isValid: (email: string) => boolean 3 | } 4 | -------------------------------------------------------------------------------- /src/validation/test/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mock-email-validator' 2 | -------------------------------------------------------------------------------- /src/validation/test/mock-email-validator.ts: -------------------------------------------------------------------------------- 1 | import { EmailValidator } from '@/validation/protocols/email-validator' 2 | 3 | export class EmailValidatorSpy implements EmailValidator { 4 | isEmailValid = true 5 | email: string 6 | 7 | isValid (email: string): boolean { 8 | this.email = email 9 | return this.isEmailValid 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/validation/validators/compare-field-validation.spec.ts: -------------------------------------------------------------------------------- 1 | import { CompareFieldsValidation } from './compare-fields-validation' 2 | import { InvalidParamError } from '@/presentation/errors' 3 | import faker from 'faker' 4 | 5 | const field = faker.random.word() 6 | const fieldToCompare = faker.random.word() 7 | 8 | const makeSut = (): CompareFieldsValidation => { 9 | return new CompareFieldsValidation(field, fieldToCompare) 10 | } 11 | 12 | describe('CompareFieldsValidation', () => { 13 | test('Should return an InvalidParamError if validation fails', () => { 14 | const sut = makeSut() 15 | const error = sut.validate({ 16 | [field]: faker.random.word(), 17 | [fieldToCompare]: faker.random.word() 18 | }) 19 | expect(error).toEqual(new InvalidParamError(fieldToCompare)) 20 | }) 21 | 22 | test('Should not return if validation succeeds', () => { 23 | const sut = makeSut() 24 | const value = faker.random.word() 25 | const error = sut.validate({ 26 | [field]: value, 27 | [fieldToCompare]: value 28 | }) 29 | expect(error).toBeFalsy() 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/validation/validators/compare-fields-validation.ts: -------------------------------------------------------------------------------- 1 | import { Validation } from '@/presentation/protocols' 2 | import { InvalidParamError } from '@/presentation/errors' 3 | 4 | export class CompareFieldsValidation implements Validation { 5 | constructor ( 6 | private readonly fieldName: string, 7 | private readonly fieldToCompareName: string 8 | ) { } 9 | 10 | validate (input: any): Error { 11 | if (input[this.fieldName] !== input[this.fieldToCompareName]) { 12 | return new InvalidParamError(this.fieldToCompareName) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/validation/validators/email-validation.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmailValidation } from './email-validation' 2 | import { EmailValidatorSpy } from '@/validation/test' 3 | import { InvalidParamError } from '@/presentation/errors' 4 | import faker from 'faker' 5 | import { throwError } from '@/domain/test' 6 | 7 | const field = faker.random.word() 8 | 9 | type SutTypes = { 10 | sut: EmailValidation 11 | emailValidatorSpy: EmailValidatorSpy 12 | } 13 | 14 | const makeSut = (): SutTypes => { 15 | const emailValidatorSpy = new EmailValidatorSpy() 16 | const sut = new EmailValidation(field, emailValidatorSpy) 17 | return { 18 | sut, 19 | emailValidatorSpy 20 | } 21 | } 22 | 23 | describe('Email Validation', () => { 24 | test('Should return an error if EmailValidator returns false', () => { 25 | const { sut, emailValidatorSpy } = makeSut() 26 | emailValidatorSpy.isEmailValid = false 27 | const email = faker.internet.email() 28 | const error = sut.validate({ [field]: email }) 29 | expect(error).toEqual(new InvalidParamError(field)) 30 | }) 31 | 32 | test('Should call EmailValidator with correct email', () => { 33 | const { sut, emailValidatorSpy } = makeSut() 34 | const email = faker.internet.email() 35 | sut.validate({ [field]: email }) 36 | expect(emailValidatorSpy.email).toBe(email) 37 | }) 38 | 39 | test('Should throw if EmailValidator throws', () => { 40 | const { sut, emailValidatorSpy } = makeSut() 41 | jest.spyOn(emailValidatorSpy, 'isValid').mockImplementationOnce(throwError) 42 | expect(sut.validate).toThrow() 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /src/validation/validators/email-validation.ts: -------------------------------------------------------------------------------- 1 | import { Validation } from '@/presentation/protocols' 2 | import { InvalidParamError } from '@/presentation/errors' 3 | import { EmailValidator } from '@/validation/protocols/email-validator' 4 | 5 | export class EmailValidation implements Validation { 6 | constructor ( 7 | private readonly fieldName: string, 8 | private readonly emailValidator: EmailValidator) {} 9 | 10 | validate (input: any): Error { 11 | const isValid = this.emailValidator.isValid(input[this.fieldName]) 12 | if (!isValid) { 13 | return new InvalidParamError(this.fieldName) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/validation/validators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './compare-fields-validation' 2 | export * from './email-validation' 3 | export * from './required-field-validation' 4 | export * from './validation-composite' 5 | -------------------------------------------------------------------------------- /src/validation/validators/required-field-validation.spec.ts: -------------------------------------------------------------------------------- 1 | import { RequiredFieldValidation } from './required-field-validation' 2 | import { MissingParamError } from '@/presentation/errors' 3 | import faker from 'faker' 4 | 5 | const field = faker.random.word() 6 | 7 | const makeSut = (): RequiredFieldValidation => { 8 | return new RequiredFieldValidation(field) 9 | } 10 | 11 | describe('RequiredField Validation', () => { 12 | test('Should return a MissingParamError if validation fails', () => { 13 | const sut = makeSut() 14 | const error = sut.validate({ invalidField: faker.random.word() }) 15 | expect(error).toEqual(new MissingParamError(field)) 16 | }) 17 | 18 | test('Should not return if validation succeeds', () => { 19 | const sut = makeSut() 20 | const error = sut.validate({ [field]: faker.random.word() }) 21 | expect(error).toBeFalsy() 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/validation/validators/required-field-validation.ts: -------------------------------------------------------------------------------- 1 | import { Validation } from '@/presentation/protocols' 2 | import { MissingParamError } from '@/presentation/errors' 3 | 4 | export class RequiredFieldValidation implements Validation { 5 | constructor (private readonly fieldName: string) { 6 | this.fieldName = fieldName 7 | } 8 | 9 | validate (input: any): Error { 10 | if (!input[this.fieldName]) { 11 | return new MissingParamError(this.fieldName) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/validation/validators/validation-composite.spec.ts: -------------------------------------------------------------------------------- 1 | import { ValidationComposite } from './validation-composite' 2 | import { ValidationSpy } from '@/presentation/test' 3 | import { MissingParamError } from '@/presentation/errors' 4 | import faker from 'faker' 5 | 6 | const field = faker.random.word() 7 | 8 | type SutTypes = { 9 | sut: ValidationComposite 10 | validationSpies: ValidationSpy[] 11 | } 12 | 13 | const makeSut = (): SutTypes => { 14 | const validationSpies = [ 15 | new ValidationSpy(), 16 | new ValidationSpy() 17 | ] 18 | const sut = new ValidationComposite(validationSpies) 19 | return { 20 | sut, 21 | validationSpies 22 | } 23 | } 24 | 25 | describe('Validation Composite', () => { 26 | test('Should return an error if any validation fails', () => { 27 | const { sut, validationSpies } = makeSut() 28 | validationSpies[1].error = new MissingParamError(field) 29 | const error = sut.validate({ [field]: faker.random.word() }) 30 | expect(error).toEqual(validationSpies[1].error) 31 | }) 32 | 33 | test('Should return the first error if more then one validation fails', () => { 34 | const { sut, validationSpies } = makeSut() 35 | validationSpies[0].error = new Error() 36 | validationSpies[1].error = new MissingParamError(field) 37 | const error = sut.validate({ [field]: faker.random.word() }) 38 | expect(error).toEqual(validationSpies[0].error) 39 | }) 40 | 41 | test('Should not return if validation succeeds', () => { 42 | const { sut } = makeSut() 43 | const error = sut.validate({ [field]: faker.random.word() }) 44 | expect(error).toBeFalsy() 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /src/validation/validators/validation-composite.ts: -------------------------------------------------------------------------------- 1 | import { Validation } from '@/presentation/protocols' 2 | 3 | export class ValidationComposite implements Validation { 4 | constructor (private readonly validations: Validation[]) {} 5 | 6 | validate (input: any): Error { 7 | for (const validation of this.validations) { 8 | const error = validation.validate(input) 9 | if (error) { 10 | return error 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig-build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "**/*.spec.ts", 5 | "**/*.test.ts", 6 | "**/test/**" 7 | ] 8 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "commonjs", 5 | "target": "es2019", 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "baseUrl": "src", 10 | "paths": { 11 | "@/*": [ 12 | "*" 13 | ] 14 | }, 15 | "skipLibCheck": true 16 | }, 17 | "include": [ 18 | "src" 19 | ], 20 | "exclude": [] 21 | } --------------------------------------------------------------------------------