├── .DS_Store ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .husky ├── pre-commit └── pre-push ├── .lintstagedrc.json ├── .travis.yml ├── docker-compose.yml ├── jest-integration-config.js ├── jest-mongodb-config.js ├── jest-unit-config.js ├── jest.config.js ├── license ├── package-lock.json ├── package.json ├── public └── img │ ├── logo-angular.png │ ├── logo-course.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 ├── readme.md ├── requirements ├── add-survey.md ├── load-survey-result.md ├── load-surveys.md ├── login.md ├── save-survey-result.md └── signup.md ├── src ├── data │ ├── protocols │ │ ├── cryptography │ │ │ ├── decrypter.ts │ │ │ ├── encrypter.ts │ │ │ ├── hash-comparer.ts │ │ │ ├── hasher.ts │ │ │ └── index.ts │ │ ├── db │ │ │ ├── account │ │ │ │ ├── add-account-repository.ts │ │ │ │ ├── check-account-by-email-repository.ts │ │ │ │ ├── index.ts │ │ │ │ ├── load-account-by-email-repository.ts │ │ │ │ ├── load-account-by-token-repository.ts │ │ │ │ └── update-access-token-repository.ts │ │ │ ├── index.ts │ │ │ ├── log │ │ │ │ ├── index.ts │ │ │ │ └── log-error-repository.ts │ │ │ ├── survey-result │ │ │ │ ├── index.ts │ │ │ │ ├── load-survey-result-repository.ts │ │ │ │ └── save-survey-result-repository.ts │ │ │ └── survey │ │ │ │ ├── add-survey-repository.ts │ │ │ │ ├── check-survey-by-id-repository.ts │ │ │ │ ├── index.ts │ │ │ │ ├── load-answers-by-survey-repository.ts │ │ │ │ ├── load-survey-by-id-repository.ts │ │ │ │ └── load-surveys-repository.ts │ │ └── index.ts │ └── usecases │ │ ├── db-add-account.ts │ │ ├── db-add-survey.ts │ │ ├── db-authentication.ts │ │ ├── db-check-survey-by-id.ts │ │ ├── db-load-account-by-token.ts │ │ ├── db-load-answers-by-survey.ts │ │ ├── db-load-survey-result.ts │ │ ├── db-load-surveys.ts │ │ ├── db-save-survey-result.ts │ │ └── index.ts ├── domain │ ├── models │ │ ├── index.ts │ │ ├── survey-result.ts │ │ └── survey.ts │ └── usecases │ │ ├── add-account.ts │ │ ├── add-survey.ts │ │ ├── authentication.ts │ │ ├── check-survey-by-id.ts │ │ ├── index.ts │ │ ├── load-account-by-token.ts │ │ ├── load-answers-by-survey.ts │ │ ├── load-survey-result.ts │ │ ├── load-surveys.ts │ │ └── save-survey-result.ts ├── infra │ ├── cryptography │ │ ├── bcrypt-adapter.ts │ │ ├── index.ts │ │ └── jwt-adapter.ts │ ├── db │ │ ├── index.ts │ │ └── mongodb │ │ │ ├── account-mongo-repository.ts │ │ │ ├── index.ts │ │ │ ├── log-mongo-repository.ts │ │ │ ├── mongo-helper.ts │ │ │ ├── query-builder.ts │ │ │ ├── survey-mongo-repository.ts │ │ │ └── survey-result-mongo-repository.ts │ └── validators │ │ ├── email-validator-adapter.ts │ │ └── index.ts ├── main │ ├── adapters │ │ ├── apollo-server-resolver-adapter.ts │ │ ├── express-middleware-adapter.ts │ │ ├── express-route-adapter.ts │ │ └── index.ts │ ├── config │ │ ├── app.ts │ │ ├── custom-modules.d.ts │ │ ├── env.ts │ │ ├── middlewares.ts │ │ ├── routes.ts │ │ ├── static-files.ts │ │ └── swagger.ts │ ├── decorators │ │ ├── index.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 │ │ │ ├── add-survey-controller-factory.ts │ │ │ ├── add-survey-validation-factory.ts │ │ │ ├── index.ts │ │ │ ├── load-survey-result-controller-factory.ts │ │ │ ├── load-surveys-controller-factory.ts │ │ │ ├── login-controller-factory.ts │ │ │ ├── login-validation-factory.ts │ │ │ ├── save-survey-result-controller-factory.ts │ │ │ ├── signup-controller-factory.ts │ │ │ └── signup-validation-factory.ts │ │ ├── decorators │ │ │ ├── index.ts │ │ │ └── log-controller-decorator-factory.ts │ │ ├── index.ts │ │ ├── middlewares │ │ │ ├── auth-middleware-factory.ts │ │ │ └── index.ts │ │ └── usecases │ │ │ ├── add-account-factory.ts │ │ │ ├── add-survey-factory.ts │ │ │ ├── authentication-factory.ts │ │ │ ├── check-survey-by-id-factory.ts │ │ │ ├── index.ts │ │ │ ├── load-account-by-token-factory.ts │ │ │ ├── load-answers-by-survey-factory.ts │ │ │ ├── load-survey-result-factory.ts │ │ │ ├── load-surveys-factory.ts │ │ │ └── save-survey-result-factory.ts │ ├── graphql │ │ ├── apollo │ │ │ ├── apollo-server.ts │ │ │ └── index.ts │ │ ├── directives │ │ │ ├── auth-directive.ts │ │ │ └── index.ts │ │ ├── resolvers │ │ │ ├── base.ts │ │ │ ├── index.ts │ │ │ ├── login.ts │ │ │ ├── survey-result.ts │ │ │ └── survey.ts │ │ └── type-defs │ │ │ ├── base.ts │ │ │ ├── index.ts │ │ │ ├── login.ts │ │ │ ├── survey-result.ts │ │ │ └── survey.ts │ ├── middlewares │ │ ├── admin-auth.ts │ │ ├── auth.ts │ │ ├── body-parser.ts │ │ ├── content-type.ts │ │ ├── cors.ts │ │ ├── index.ts │ │ └── no-cache.ts │ ├── routes │ │ ├── login-routes.ts │ │ ├── survey-result-routes.ts │ │ └── survey-routes.ts │ └── server.ts ├── presentation │ ├── controllers │ │ ├── add-survey-controller.ts │ │ ├── index.ts │ │ ├── load-survey-result-controller.ts │ │ ├── load-surveys-controller.ts │ │ ├── login-controller.ts │ │ ├── save-survey-result-controller.ts │ │ └── signup-controller.ts │ ├── errors │ │ ├── access-denied-error.ts │ │ ├── email-in-use-error.ts │ │ ├── index.ts │ │ ├── invalid-param-error.ts │ │ ├── missing-param-error.ts │ │ ├── server-error.ts │ │ └── unauthorized-error.ts │ ├── helpers │ │ ├── http-helper.ts │ │ └── index.ts │ ├── middlewares │ │ ├── auth-middleware.ts │ │ └── index.ts │ └── protocols │ │ ├── controller.ts │ │ ├── http.ts │ │ ├── index.ts │ │ ├── middleware.ts │ │ └── validation.ts └── validation │ ├── protocols │ ├── email-validator.ts │ └── index.ts │ └── validators │ ├── compare-fields-validation.ts │ ├── email-validation.ts │ ├── index.ts │ ├── required-field-validation.ts │ └── validation-composite.ts ├── tests ├── data │ ├── mocks │ │ ├── index.ts │ │ ├── mock-cryptography.ts │ │ ├── mock-db-account.ts │ │ ├── mock-db-log.ts │ │ ├── mock-db-survey-result.ts │ │ └── mock-db-survey.ts │ └── usecases │ │ ├── db-add-account.spec.ts │ │ ├── db-add-survey.spec.ts │ │ ├── db-authentication.spec.ts │ │ ├── db-check-survey-by-id.spec.ts │ │ ├── db-load-account-by-token.spec.ts │ │ ├── db-load-answers-by-survey.spec.ts │ │ ├── db-load-survey-result.spec.ts │ │ ├── db-load-surveys.spec.ts │ │ └── db-save-survey-result.spec.ts ├── domain │ └── mocks │ │ ├── index.ts │ │ ├── mock-account.ts │ │ ├── mock-survey-result.ts │ │ ├── mock-survey.ts │ │ └── test-helpers.ts ├── infra │ ├── cryptography │ │ ├── bcrypt-adapter.spec.ts │ │ └── jwt-adapter.spec.ts │ ├── db │ │ └── mongodb │ │ │ ├── account-mongo-repository.spec.ts │ │ │ ├── log-mongo-repository.spec.ts │ │ │ ├── survey-mongo-repository.spec.ts │ │ │ └── survey-result-mongo-repository.spec.ts │ └── validators │ │ └── email-validator-adapter.spec.ts ├── main │ ├── decorators │ │ └── log-controller-decorator.spec.ts │ ├── factories │ │ ├── add-survey-validation-factory.spec.ts │ │ ├── login-validation-factory.spec.ts │ │ └── signup-validation-factory.spec.ts │ ├── graphql │ │ ├── login.test.ts │ │ ├── survey-result.test.ts │ │ └── survey.test.ts │ ├── middlewares │ │ ├── body-parser.test.ts │ │ ├── content-type.test.ts │ │ ├── cors.test.ts │ │ └── no-cache.test.ts │ └── routes │ │ ├── login-routes.test.ts │ │ ├── survey-result-routes.test.ts │ │ └── survey-routes.test.ts ├── presentation │ ├── controllers │ │ ├── add-survey-controller.spec.ts │ │ ├── load-survey-result-controller.spec.ts │ │ ├── load-surveys-controller.spec.ts │ │ ├── login-controller.spec.ts │ │ ├── save-survey-result-controller.spec.ts │ │ └── signup-controller.spec.ts │ ├── middlewares │ │ └── auth-middleware.spec.ts │ └── mocks │ │ ├── index.ts │ │ ├── mock-account.ts │ │ ├── mock-survey-result.ts │ │ ├── mock-survey.ts │ │ └── mock-validation.ts └── validation │ ├── mocks │ ├── index.ts │ └── mock-email-validator.ts │ └── validators │ ├── compare-fields-validation.spec.ts │ ├── email-validation.spec.ts │ ├── required-field-validation.spec.ts │ └── validation-composite.spec.ts ├── tsconfig-build.json └── tsconfig.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/.DS_Store -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | ./data 5 | requirements 6 | .vscode -------------------------------------------------------------------------------- /.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 | "@typescript-eslint/no-namespace": "off", 14 | "import/export": "off" 15 | } 16 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | data 5 | !src/data 6 | !tests/data 7 | .vscode 8 | globalConfig.json -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run test:ci 5 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.ts": [ 3 | "eslint . --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 -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | mongo: 4 | container_name: mongo-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-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 | - MONGO_URL=mongodb://mongo: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 | - mongo -------------------------------------------------------------------------------- /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 | mongodbMemoryServer: { 3 | version: 'latest' 4 | }, 5 | mongodbMemoryServerOptions: { 6 | instance: { 7 | dbName: 'jest' 8 | }, 9 | binary: { 10 | version: '4.0.3', 11 | skipMD5: true 12 | }, 13 | autoStart: false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /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: ['/tests'], 3 | collectCoverageFrom: [ 4 | '/src/**/*.ts', 5 | '!/src/main/**' 6 | ], 7 | coverageDirectory: 'coverage', 8 | coverageProvider: 'babel', 9 | testEnvironment: 'node', 10 | preset: '@shelf/jest-mongodb', 11 | transform: { 12 | '.+\\.ts$': 'ts-jest' 13 | }, 14 | moduleNameMapper: { 15 | '@/tests/(.*)': '/tests/$1', 16 | '@/(.*)': '/src/$1' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clean-node-api", 3 | "version": "3.0.0", 4 | "description": "NodeJs Rest API and GraphQL using TDD, Clean Architecture, Typescript and Design Patterns", 5 | "author": "Rodrigo Manguinho", 6 | "license": "GPL-3.0-or-later", 7 | "repository": "github:rmanguinho/clean-ts-api", 8 | "homepage": "https://rmanguinho.github.io", 9 | "scripts": { 10 | "start": "node dist/main/server.js", 11 | "debug": "nodemon -L --watch ./dist --inspect=0.0.0.0:9222 --nolazy ./dist/main/server.js", 12 | "build": "rimraf dist && tsc -p tsconfig-build.json", 13 | "build:watch": "rimraf dist && tsc -p tsconfig-build.json -w", 14 | "postbuild": "copyfiles -u 1 public/**/* dist/static", 15 | "up": "npm run build && docker-compose up -d", 16 | "down": "docker-compose down", 17 | "check": "npm-check -s -u", 18 | "test": "jest --passWithNoTests --runInBand --no-cache", 19 | "test:unit": "npm test -- --watch -c jest-unit-config.js", 20 | "test:integration": "npm test -- --watch -c jest-integration-config.js", 21 | "test:staged": "npm test -- --findRelatedTests", 22 | "test:ci": "npm test -- --coverage", 23 | "test:coveralls": "npm run test:ci && coveralls < coverage/lcov.info", 24 | "prepare": "husky install" 25 | }, 26 | "devDependencies": { 27 | "@shelf/jest-mongodb": "^2.0.3", 28 | "@types/bcrypt": "^5.0.0", 29 | "@types/express": "^4.17.13", 30 | "@types/express-serve-static-core": "^4.17.24", 31 | "@types/faker": "^5.5.8", 32 | "@types/graphql": "^14.5.0", 33 | "@types/graphql-iso-date": "^3.4.0", 34 | "@types/jest": "^27.0.1", 35 | "@types/jsonwebtoken": "^8.5.5", 36 | "@types/mongodb": "^4.0.7", 37 | "@types/node": "^16.9.1", 38 | "@types/supertest": "^2.0.11", 39 | "@types/swagger-ui-express": "^4.1.3", 40 | "@types/validator": "^13.6.3", 41 | "@typescript-eslint/eslint-plugin": "^4.31.1", 42 | "bson-objectid": "^2.0.1", 43 | "copyfiles": "^2.4.1", 44 | "coveralls": "^3.1.1", 45 | "eslint": "^7.32.0", 46 | "eslint-config-standard-with-typescript": "^21.0.1", 47 | "eslint-plugin-import": "^2.24.2", 48 | "eslint-plugin-node": "^11.1.0", 49 | "eslint-plugin-promise": "^5.1.0", 50 | "eslint-plugin-standard": "^5.0.0", 51 | "faker": "^5.5.3", 52 | "git-commit-msg-linter": "^3.2.8", 53 | "husky": "^7.0.2", 54 | "jest": "^27.2.0", 55 | "lint-staged": "^11.1.2", 56 | "mockdate": "^3.0.5", 57 | "rimraf": "^3.0.2", 58 | "supertest": "^6.1.6", 59 | "ts-jest": "^27.0.5", 60 | "typescript": "^4.4.3" 61 | }, 62 | "dependencies": { 63 | "@graphql-tools/schema": "^8.2.0", 64 | "@graphql-tools/utils": "^8.2.2", 65 | "apollo-server-express": "^3.3.0", 66 | "bcrypt": "^5.0.1", 67 | "express": "^4.17.1", 68 | "graphql": "^15.5.3", 69 | "graphql-scalars": "^1.10.1", 70 | "jsonwebtoken": "^8.5.1", 71 | "module-alias": "^2.2.2", 72 | "mongo-round": "^1.0.0", 73 | "mongodb": "^4.1.1", 74 | "nodemon": "^2.0.12", 75 | "swagger-ui-express": "^4.1.6", 76 | "validator": "^13.6.0" 77 | }, 78 | "engines": { 79 | "node": "16.x" 80 | }, 81 | "_moduleAliases": { 82 | "@": "dist" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /public/img/logo-angular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-angular.png -------------------------------------------------------------------------------- /public/img/logo-course.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-course.png -------------------------------------------------------------------------------- /public/img/logo-ember.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-ember.png -------------------------------------------------------------------------------- /public/img/logo-flutter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-flutter.png -------------------------------------------------------------------------------- /public/img/logo-ionic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-ionic.png -------------------------------------------------------------------------------- /public/img/logo-jquery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-jquery.png -------------------------------------------------------------------------------- /public/img/logo-js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-js.png -------------------------------------------------------------------------------- /public/img/logo-knockout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-knockout.png -------------------------------------------------------------------------------- /public/img/logo-native-script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-native-script.png -------------------------------------------------------------------------------- /public/img/logo-nativo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-nativo.png -------------------------------------------------------------------------------- /public/img/logo-npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-npm.png -------------------------------------------------------------------------------- /public/img/logo-phonegap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-phonegap.png -------------------------------------------------------------------------------- /public/img/logo-polymer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-polymer.png -------------------------------------------------------------------------------- /public/img/logo-react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-react.png -------------------------------------------------------------------------------- /public/img/logo-riot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-riot.png -------------------------------------------------------------------------------- /public/img/logo-svelte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-svelte.png -------------------------------------------------------------------------------- /public/img/logo-titanium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-titanium.png -------------------------------------------------------------------------------- /public/img/logo-ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-ts.png -------------------------------------------------------------------------------- /public/img/logo-vue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-vue.png -------------------------------------------------------------------------------- /public/img/logo-xamarin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/public/img/logo-xamarin.png -------------------------------------------------------------------------------- /public/img/logo-yarn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/clean-ts-api/476cef900107183e3e48997fffe874dcab3db933/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/cryptography/decrypter.ts: -------------------------------------------------------------------------------- 1 | export interface Decrypter { 2 | decrypt: (ciphertext: string) => Promise 3 | } 4 | -------------------------------------------------------------------------------- /src/data/protocols/cryptography/encrypter.ts: -------------------------------------------------------------------------------- 1 | export interface Encrypter { 2 | encrypt: (plaintext: string) => Promise 3 | } 4 | -------------------------------------------------------------------------------- /src/data/protocols/cryptography/hash-comparer.ts: -------------------------------------------------------------------------------- 1 | export interface HashComparer { 2 | compare: (plaitext: string, digest: string) => Promise 3 | } 4 | -------------------------------------------------------------------------------- /src/data/protocols/cryptography/hasher.ts: -------------------------------------------------------------------------------- 1 | export interface Hasher { 2 | hash: (plaintext: string) => Promise 3 | } 4 | -------------------------------------------------------------------------------- /src/data/protocols/cryptography/index.ts: -------------------------------------------------------------------------------- 1 | export * from './decrypter' 2 | export * from './encrypter' 3 | export * from './hash-comparer' 4 | export * from './hasher' 5 | -------------------------------------------------------------------------------- /src/data/protocols/db/account/add-account-repository.ts: -------------------------------------------------------------------------------- 1 | import { AddAccount } from '@/domain/usecases' 2 | 3 | export interface AddAccountRepository { 4 | add: (data: AddAccountRepository.Params) => Promise 5 | } 6 | 7 | export namespace AddAccountRepository { 8 | export type Params = AddAccount.Params 9 | export type Result = boolean 10 | } 11 | -------------------------------------------------------------------------------- /src/data/protocols/db/account/check-account-by-email-repository.ts: -------------------------------------------------------------------------------- 1 | export interface CheckAccountByEmailRepository { 2 | checkByEmail: (email: string) => Promise 3 | } 4 | 5 | export namespace CheckAccountByEmailRepository { 6 | export type Result = boolean 7 | } 8 | -------------------------------------------------------------------------------- /src/data/protocols/db/account/index.ts: -------------------------------------------------------------------------------- 1 | export * from './add-account-repository' 2 | export * from './load-account-by-email-repository' 3 | export * from './check-account-by-email-repository' 4 | export * from './load-account-by-token-repository' 5 | export * from './update-access-token-repository' 6 | -------------------------------------------------------------------------------- /src/data/protocols/db/account/load-account-by-email-repository.ts: -------------------------------------------------------------------------------- 1 | export interface LoadAccountByEmailRepository { 2 | loadByEmail: (email: string) => Promise 3 | } 4 | 5 | export namespace LoadAccountByEmailRepository { 6 | export type Result = { 7 | id: string 8 | name: string 9 | password: string 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/data/protocols/db/account/load-account-by-token-repository.ts: -------------------------------------------------------------------------------- 1 | export interface LoadAccountByTokenRepository { 2 | loadByToken: (token: string, role?: string) => Promise 3 | } 4 | 5 | export namespace LoadAccountByTokenRepository { 6 | export type Result = { 7 | id: string 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /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/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account' 2 | export * from './log' 3 | export * from './survey' 4 | export * from './survey-result' 5 | -------------------------------------------------------------------------------- /src/data/protocols/db/log/index.ts: -------------------------------------------------------------------------------- 1 | export * from './log-error-repository' 2 | -------------------------------------------------------------------------------- /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/index.ts: -------------------------------------------------------------------------------- 1 | export * from './load-survey-result-repository' 2 | export * from './save-survey-result-repository' 3 | -------------------------------------------------------------------------------- /src/data/protocols/db/survey-result/load-survey-result-repository.ts: -------------------------------------------------------------------------------- 1 | import { SurveyResultModel } from '@/domain/models' 2 | 3 | export interface LoadSurveyResultRepository { 4 | loadBySurveyId: (surveyId: string, accountId: string) => Promise 5 | } 6 | 7 | export namespace LoadSurveyResultRepository { 8 | export type Result = SurveyResultModel 9 | } 10 | -------------------------------------------------------------------------------- /src/data/protocols/db/survey-result/save-survey-result-repository.ts: -------------------------------------------------------------------------------- 1 | import { SaveSurveyResult } from '@/domain/usecases' 2 | 3 | export interface SaveSurveyResultRepository { 4 | save: (data: SaveSurveyResultRepository.Params) => Promise 5 | } 6 | 7 | export namespace SaveSurveyResultRepository { 8 | export type Params = SaveSurveyResult.Params 9 | } 10 | -------------------------------------------------------------------------------- /src/data/protocols/db/survey/add-survey-repository.ts: -------------------------------------------------------------------------------- 1 | import { AddSurvey } from '@/domain/usecases' 2 | 3 | export interface AddSurveyRepository { 4 | add: (data: AddSurveyRepository.Params) => Promise 5 | } 6 | 7 | export namespace AddSurveyRepository { 8 | export type Params = AddSurvey.Params 9 | } 10 | -------------------------------------------------------------------------------- /src/data/protocols/db/survey/check-survey-by-id-repository.ts: -------------------------------------------------------------------------------- 1 | export interface CheckSurveyByIdRepository { 2 | checkById: (id: string) => Promise 3 | } 4 | 5 | export namespace CheckSurveyByIdRepository { 6 | export type Result = boolean 7 | } 8 | -------------------------------------------------------------------------------- /src/data/protocols/db/survey/index.ts: -------------------------------------------------------------------------------- 1 | export * from './add-survey-repository' 2 | export * from './load-survey-by-id-repository' 3 | export * from './load-answers-by-survey-repository' 4 | export * from './check-survey-by-id-repository' 5 | export * from './load-surveys-repository' 6 | -------------------------------------------------------------------------------- /src/data/protocols/db/survey/load-answers-by-survey-repository.ts: -------------------------------------------------------------------------------- 1 | export interface LoadAnswersBySurveyRepository { 2 | loadAnswers: (id: string) => Promise 3 | } 4 | 5 | export namespace LoadAnswersBySurveyRepository { 6 | export type Result = string[] 7 | } 8 | -------------------------------------------------------------------------------- /src/data/protocols/db/survey/load-survey-by-id-repository.ts: -------------------------------------------------------------------------------- 1 | import { SurveyModel } from '@/domain/models' 2 | 3 | export interface LoadSurveyByIdRepository { 4 | loadById: (id: string) => Promise 5 | } 6 | 7 | export namespace LoadSurveyByIdRepository { 8 | export type Result = SurveyModel 9 | } 10 | -------------------------------------------------------------------------------- /src/data/protocols/db/survey/load-surveys-repository.ts: -------------------------------------------------------------------------------- 1 | import { SurveyModel } from '@/domain/models' 2 | 3 | export interface LoadSurveysRepository { 4 | loadAll: (accountId: string) => Promise 5 | } 6 | 7 | export namespace LoadSurveysRepository { 8 | export type Result = SurveyModel[] 9 | } 10 | -------------------------------------------------------------------------------- /src/data/protocols/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cryptography' 2 | export * from './db' 3 | -------------------------------------------------------------------------------- /src/data/usecases/db-add-account.ts: -------------------------------------------------------------------------------- 1 | import { AddAccount } from '@/domain/usecases' 2 | import { Hasher, AddAccountRepository, CheckAccountByEmailRepository } from '@/data/protocols' 3 | 4 | export class DbAddAccount implements AddAccount { 5 | constructor ( 6 | private readonly hasher: Hasher, 7 | private readonly addAccountRepository: AddAccountRepository, 8 | private readonly checkAccountByEmailRepository: CheckAccountByEmailRepository 9 | ) {} 10 | 11 | async add (accountData: AddAccount.Params): Promise { 12 | const exists = await this.checkAccountByEmailRepository.checkByEmail(accountData.email) 13 | let isValid = false 14 | if (!exists) { 15 | const hashedPassword = await this.hasher.hash(accountData.password) 16 | isValid = await this.addAccountRepository.add({ ...accountData, password: hashedPassword }) 17 | } 18 | return isValid 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/data/usecases/db-add-survey.ts: -------------------------------------------------------------------------------- 1 | import { AddSurvey } from '@/domain/usecases' 2 | import { AddSurveyRepository } from '@/data/protocols' 3 | 4 | export class DbAddSurvey implements AddSurvey { 5 | constructor (private readonly addSurveyRepository: AddSurveyRepository) {} 6 | 7 | async add (data: AddSurvey.Params): Promise { 8 | await this.addSurveyRepository.add(data) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/data/usecases/db-authentication.ts: -------------------------------------------------------------------------------- 1 | import { Authentication } from '@/domain/usecases' 2 | import { HashComparer, Encrypter, LoadAccountByEmailRepository, UpdateAccessTokenRepository } from '@/data/protocols' 3 | 4 | export class DbAuthentication implements Authentication { 5 | constructor ( 6 | private readonly loadAccountByEmailRepository: LoadAccountByEmailRepository, 7 | private readonly hashComparer: HashComparer, 8 | private readonly encrypter: Encrypter, 9 | private readonly updateAccessTokenRepository: UpdateAccessTokenRepository 10 | ) {} 11 | 12 | async auth (authenticationParams: Authentication.Params): Promise { 13 | const account = await this.loadAccountByEmailRepository.loadByEmail(authenticationParams.email) 14 | if (account) { 15 | const isValid = await this.hashComparer.compare(authenticationParams.password, account.password) 16 | if (isValid) { 17 | const accessToken = await this.encrypter.encrypt(account.id) 18 | await this.updateAccessTokenRepository.updateAccessToken(account.id, accessToken) 19 | return { 20 | accessToken, 21 | name: account.name 22 | } 23 | } 24 | } 25 | return null 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/data/usecases/db-check-survey-by-id.ts: -------------------------------------------------------------------------------- 1 | import { CheckSurveyById } from '@/domain/usecases' 2 | import { CheckSurveyByIdRepository } from '@/data/protocols' 3 | 4 | export class DbCheckSurveyById implements CheckSurveyById { 5 | constructor (private readonly checkSurveyByIdRepository: CheckSurveyByIdRepository) {} 6 | 7 | async checkById (id: string): Promise { 8 | return this.checkSurveyByIdRepository.checkById(id) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/data/usecases/db-load-account-by-token.ts: -------------------------------------------------------------------------------- 1 | import { LoadAccountByToken } from '@/domain/usecases' 2 | import { Decrypter, LoadAccountByTokenRepository } from '@/data/protocols' 3 | 4 | export class DbLoadAccountByToken implements LoadAccountByToken { 5 | constructor ( 6 | private readonly decrypter: Decrypter, 7 | private readonly loadAccountByTokenRepository: LoadAccountByTokenRepository 8 | ) {} 9 | 10 | async load (accessToken: string, role?: string): Promise { 11 | let token: string 12 | try { 13 | token = await this.decrypter.decrypt(accessToken) 14 | } catch (error) { 15 | return null 16 | } 17 | if (token) { 18 | const account = await this.loadAccountByTokenRepository.loadByToken(accessToken, role) 19 | if (account) { 20 | return account 21 | } 22 | } 23 | return null 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/data/usecases/db-load-answers-by-survey.ts: -------------------------------------------------------------------------------- 1 | import { LoadAnswersBySurvey } from '@/domain/usecases' 2 | import { LoadAnswersBySurveyRepository } from '@/data/protocols' 3 | 4 | export class DbLoadAnswersBySurvey implements LoadAnswersBySurvey { 5 | constructor (private readonly loadAnswersBySurveyRepository: LoadAnswersBySurveyRepository) {} 6 | 7 | async loadAnswers (id: string): Promise { 8 | return this.loadAnswersBySurveyRepository.loadAnswers(id) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/data/usecases/db-load-survey-result.ts: -------------------------------------------------------------------------------- 1 | import { LoadSurveyResult } from '@/domain/usecases' 2 | import { SurveyModel, SurveyResultModel } from '@/domain/models' 3 | import { LoadSurveyResultRepository, LoadSurveyByIdRepository } from '@/data/protocols' 4 | 5 | export class DbLoadSurveyResult implements LoadSurveyResult { 6 | constructor ( 7 | private readonly loadSurveyResultRepository: LoadSurveyResultRepository, 8 | private readonly loadSurveyByIdRepository: LoadSurveyByIdRepository 9 | ) {} 10 | 11 | async load (surveyId: string, accountId: string): Promise { 12 | let surveyResult = await this.loadSurveyResultRepository.loadBySurveyId(surveyId, accountId) 13 | if (!surveyResult) { 14 | const survey = await this.loadSurveyByIdRepository.loadById(surveyId) 15 | surveyResult = this.makeEmptyResult(survey) 16 | } 17 | return surveyResult 18 | } 19 | 20 | private makeEmptyResult (survey: SurveyModel): SurveyResultModel { 21 | return { 22 | surveyId: survey.id, 23 | question: survey.question, 24 | date: survey.date, 25 | answers: survey.answers.map(answer => ({ 26 | ...answer, 27 | count: 0, 28 | percent: 0, 29 | isCurrentAccountAnswer: false 30 | })) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/data/usecases/db-load-surveys.ts: -------------------------------------------------------------------------------- 1 | import { LoadSurveys } from '@/domain/usecases' 2 | import { LoadSurveysRepository } from '@/data/protocols' 3 | 4 | export class DbLoadSurveys implements LoadSurveys { 5 | constructor (private readonly loadSurveysRepository: LoadSurveysRepository) {} 6 | 7 | async load (accountId: string): Promise { 8 | return this.loadSurveysRepository.loadAll(accountId) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/data/usecases/db-save-survey-result.ts: -------------------------------------------------------------------------------- 1 | import { SaveSurveyResult } from '@/domain/usecases' 2 | import { SaveSurveyResultRepository, LoadSurveyResultRepository } from '@/data/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: SaveSurveyResult.Params): Promise { 11 | await this.saveSurveyResultRepository.save(data) 12 | return this.loadSurveyResultRepository.loadBySurveyId(data.surveyId, data.accountId) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/data/usecases/index.ts: -------------------------------------------------------------------------------- 1 | export * from './db-add-account' 2 | export * from './db-add-survey' 3 | export * from './db-authentication' 4 | export * from './db-load-account-by-token' 5 | export * from './db-load-answers-by-survey' 6 | export * from './db-check-survey-by-id' 7 | export * from './db-load-survey-result' 8 | export * from './db-load-surveys' 9 | export * from './db-save-survey-result' 10 | -------------------------------------------------------------------------------- /src/domain/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './survey' 2 | export * from './survey-result' 3 | -------------------------------------------------------------------------------- /src/domain/models/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 | isCurrentAccountAnswer: boolean 14 | } 15 | -------------------------------------------------------------------------------- /src/domain/models/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/usecases/add-account.ts: -------------------------------------------------------------------------------- 1 | export interface AddAccount { 2 | add: (account: AddAccount.Params) => Promise 3 | } 4 | 5 | export namespace AddAccount { 6 | export type Params = { 7 | name: string 8 | email: string 9 | password: string 10 | } 11 | 12 | export type Result = boolean 13 | } 14 | -------------------------------------------------------------------------------- /src/domain/usecases/add-survey.ts: -------------------------------------------------------------------------------- 1 | import { SurveyModel } from '@/domain/models' 2 | 3 | export interface AddSurvey { 4 | add: (data: AddSurvey.Params) => Promise 5 | } 6 | 7 | export namespace AddSurvey { 8 | export type Params = Omit 9 | } 10 | -------------------------------------------------------------------------------- /src/domain/usecases/authentication.ts: -------------------------------------------------------------------------------- 1 | export interface Authentication { 2 | auth: (authenticationParams: Authentication.Params) => Promise 3 | } 4 | 5 | export namespace Authentication { 6 | export type Params = { 7 | email: string 8 | password: string 9 | } 10 | 11 | export type Result = { 12 | accessToken: string 13 | name: string 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/domain/usecases/check-survey-by-id.ts: -------------------------------------------------------------------------------- 1 | export interface CheckSurveyById { 2 | checkById: (id: string) => Promise 3 | } 4 | 5 | export namespace CheckSurveyById { 6 | export type Result = boolean 7 | } 8 | -------------------------------------------------------------------------------- /src/domain/usecases/index.ts: -------------------------------------------------------------------------------- 1 | export * from './add-account' 2 | export * from './authentication' 3 | export * from './load-account-by-token' 4 | export * from './add-survey' 5 | export * from './load-answers-by-survey' 6 | export * from './check-survey-by-id' 7 | export * from './load-surveys' 8 | export * from './load-survey-result' 9 | export * from './save-survey-result' 10 | -------------------------------------------------------------------------------- /src/domain/usecases/load-account-by-token.ts: -------------------------------------------------------------------------------- 1 | export interface LoadAccountByToken { 2 | load: (accessToken: string, role?: string) => Promise 3 | } 4 | 5 | export namespace LoadAccountByToken { 6 | export type Result = { 7 | id: string 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/domain/usecases/load-answers-by-survey.ts: -------------------------------------------------------------------------------- 1 | export interface LoadAnswersBySurvey { 2 | loadAnswers: (id: string) => Promise 3 | } 4 | 5 | export namespace LoadAnswersBySurvey { 6 | export type Result = string[] 7 | } 8 | -------------------------------------------------------------------------------- /src/domain/usecases/load-survey-result.ts: -------------------------------------------------------------------------------- 1 | import { SurveyResultModel } from '@/domain/models' 2 | 3 | export interface LoadSurveyResult { 4 | load: (surveyId: string, accountId: string) => Promise 5 | } 6 | 7 | export namespace LoadSurveyResult { 8 | export type Result = SurveyResultModel 9 | } 10 | -------------------------------------------------------------------------------- /src/domain/usecases/load-surveys.ts: -------------------------------------------------------------------------------- 1 | import { SurveyModel } from '@/domain/models' 2 | 3 | export interface LoadSurveys { 4 | load: (accountId: string) => Promise 5 | } 6 | 7 | export namespace LoadSurveys { 8 | export type Result = SurveyModel[] 9 | } 10 | -------------------------------------------------------------------------------- /src/domain/usecases/save-survey-result.ts: -------------------------------------------------------------------------------- 1 | import { SurveyResultModel } from '@/domain/models' 2 | 3 | export interface SaveSurveyResult { 4 | save: (data: SaveSurveyResult.Params) => Promise 5 | } 6 | 7 | export namespace SaveSurveyResult { 8 | export type Params = { 9 | surveyId: string 10 | accountId: string 11 | answer: string 12 | date: Date 13 | } 14 | 15 | export type Result = SurveyResultModel 16 | } 17 | -------------------------------------------------------------------------------- /src/infra/cryptography/bcrypt-adapter.ts: -------------------------------------------------------------------------------- 1 | import { Hasher, HashComparer } from '@/data/protocols' 2 | 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 | return bcrypt.hash(plaintext, this.salt) 10 | } 11 | 12 | async compare (plaintext: string, digest: string): Promise { 13 | return bcrypt.compare(plaintext, digest) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/infra/cryptography/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bcrypt-adapter' 2 | export * from './jwt-adapter' 3 | -------------------------------------------------------------------------------- /src/infra/cryptography/jwt-adapter.ts: -------------------------------------------------------------------------------- 1 | import { Encrypter, Decrypter } from '@/data/protocols' 2 | 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 | return jwt.sign({ id: plaintext }, this.secret) 10 | } 11 | 12 | async decrypt (ciphertext: string): Promise { 13 | return jwt.verify(ciphertext, this.secret) as any 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/infra/db/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mongodb' 2 | -------------------------------------------------------------------------------- /src/infra/db/mongodb/account-mongo-repository.ts: -------------------------------------------------------------------------------- 1 | import { MongoHelper } from '@/infra/db' 2 | import { AddAccountRepository, LoadAccountByEmailRepository, UpdateAccessTokenRepository, LoadAccountByTokenRepository, CheckAccountByEmailRepository } from '@/data/protocols/db' 3 | import { ObjectId } from 'mongodb' 4 | 5 | export class AccountMongoRepository implements AddAccountRepository, LoadAccountByEmailRepository, UpdateAccessTokenRepository, LoadAccountByTokenRepository, CheckAccountByEmailRepository { 6 | async add (data: AddAccountRepository.Params): Promise { 7 | const accountCollection = MongoHelper.getCollection('accounts') 8 | const result = await accountCollection.insertOne(data) 9 | return result.insertedId !== null 10 | } 11 | 12 | async loadByEmail (email: string): Promise { 13 | const accountCollection = MongoHelper.getCollection('accounts') 14 | const account = await accountCollection.findOne({ 15 | email 16 | }, { 17 | projection: { 18 | _id: 1, 19 | name: 1, 20 | password: 1 21 | } 22 | }) 23 | return account && MongoHelper.map(account) 24 | } 25 | 26 | async checkByEmail (email: string): Promise { 27 | const accountCollection = MongoHelper.getCollection('accounts') 28 | const account = await accountCollection.findOne({ 29 | email 30 | }, { 31 | projection: { 32 | _id: 1 33 | } 34 | }) 35 | return account !== null 36 | } 37 | 38 | async updateAccessToken (id: string, token: string): Promise { 39 | const accountCollection = MongoHelper.getCollection('accounts') 40 | await accountCollection.updateOne({ 41 | _id: new ObjectId(id) 42 | }, { 43 | $set: { 44 | accessToken: token 45 | } 46 | }) 47 | } 48 | 49 | async loadByToken (token: string, role?: string): Promise { 50 | const accountCollection = MongoHelper.getCollection('accounts') 51 | const account = await accountCollection.findOne({ 52 | accessToken: token, 53 | $or: [{ 54 | role 55 | }, { 56 | role: 'admin' 57 | }] 58 | }, { 59 | projection: { 60 | _id: 1 61 | } 62 | }) 63 | return account && MongoHelper.map(account) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/infra/db/mongodb/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account-mongo-repository' 2 | export * from './log-mongo-repository' 3 | export * from './mongo-helper' 4 | export * from './query-builder' 5 | export * from './survey-mongo-repository' 6 | export * from './survey-result-mongo-repository' 7 | -------------------------------------------------------------------------------- /src/infra/db/mongodb/log-mongo-repository.ts: -------------------------------------------------------------------------------- 1 | import { MongoHelper } from '@/infra/db' 2 | import { LogErrorRepository } from '@/data/protocols/db' 3 | 4 | export class LogMongoRepository implements LogErrorRepository { 5 | async logError (stack: string): Promise { 6 | const errorCollection = MongoHelper.getCollection('errors') 7 | await errorCollection.insertOne({ 8 | stack, 9 | date: new Date() 10 | }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/infra/db/mongodb/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 | }, 11 | 12 | async disconnect (): Promise { 13 | await this.client.close() 14 | this.client = null 15 | }, 16 | 17 | getCollection (name: string): Collection { 18 | return this.client.db().collection(name) 19 | }, 20 | 21 | map: (data: any): any => { 22 | const { _id, ...rest } = data 23 | return { ...rest, id: _id.toHexString() } 24 | }, 25 | 26 | mapCollection: (collection: any[]): any[] => { 27 | return collection.map(c => MongoHelper.map(c)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/infra/db/mongodb/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/survey-mongo-repository.ts: -------------------------------------------------------------------------------- 1 | import { MongoHelper, QueryBuilder } from '@/infra/db' 2 | import { AddSurveyRepository, LoadSurveysRepository, LoadSurveyByIdRepository, CheckSurveyByIdRepository, LoadAnswersBySurveyRepository } from '@/data/protocols/db' 3 | 4 | import { ObjectId } from 'mongodb' 5 | 6 | export class SurveyMongoRepository implements AddSurveyRepository, LoadSurveysRepository, LoadSurveyByIdRepository, CheckSurveyByIdRepository, LoadAnswersBySurveyRepository { 7 | async add (data: AddSurveyRepository.Params): Promise { 8 | const surveyCollection = MongoHelper.getCollection('surveys') 9 | await surveyCollection.insertOne(data) 10 | } 11 | 12 | async loadAll (accountId: string): Promise { 13 | const surveyCollection = MongoHelper.getCollection('surveys') 14 | const query = new QueryBuilder() 15 | .lookup({ 16 | from: 'surveyResults', 17 | foreignField: 'surveyId', 18 | localField: '_id', 19 | as: 'result' 20 | }) 21 | .project({ 22 | _id: 1, 23 | question: 1, 24 | answers: 1, 25 | date: 1, 26 | didAnswer: { 27 | $gte: [{ 28 | $size: { 29 | $filter: { 30 | input: '$result', 31 | as: 'item', 32 | cond: { 33 | $eq: ['$$item.accountId', new ObjectId(accountId)] 34 | } 35 | } 36 | } 37 | }, 1] 38 | } 39 | }) 40 | .build() 41 | const surveys = await surveyCollection.aggregate(query).toArray() 42 | return MongoHelper.mapCollection(surveys) 43 | } 44 | 45 | async loadById (id: string): Promise { 46 | const surveyCollection = MongoHelper.getCollection('surveys') 47 | const survey = await surveyCollection.findOne({ _id: new ObjectId(id) }) 48 | return survey && MongoHelper.map(survey) 49 | } 50 | 51 | async loadAnswers (id: string): Promise { 52 | const surveyCollection = MongoHelper.getCollection('surveys') 53 | const query = new QueryBuilder() 54 | .match({ 55 | _id: new ObjectId(id) 56 | }) 57 | .project({ 58 | _id: 0, 59 | answers: '$answers.answer' 60 | }) 61 | .build() 62 | const surveys = await surveyCollection.aggregate(query).toArray() 63 | return surveys[0]?.answers || [] 64 | } 65 | 66 | async checkById (id: string): Promise { 67 | const surveyCollection = MongoHelper.getCollection('surveys') 68 | const survey = await surveyCollection.findOne({ 69 | _id: new ObjectId(id) 70 | }, { 71 | projection: { 72 | _id: 1 73 | } 74 | }) 75 | return survey !== null 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/infra/validators/email-validator-adapter.ts: -------------------------------------------------------------------------------- 1 | import { EmailValidator } from '@/validation/protocols' 2 | 3 | import validator from 'validator' 4 | 5 | export class EmailValidatorAdapter implements EmailValidator { 6 | isValid (email: string): boolean { 7 | return validator.isEmail(email) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/infra/validators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './email-validator-adapter' 2 | -------------------------------------------------------------------------------- /src/main/adapters/apollo-server-resolver-adapter.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@/presentation/protocols' 2 | 3 | import { UserInputError, AuthenticationError, ForbiddenError, ApolloError } from 'apollo-server-express' 4 | 5 | export const adaptResolver = async (controller: Controller, args?: any, context?: any): Promise => { 6 | const request = { 7 | ...(args || {}), 8 | accountId: context?.req?.accountId 9 | } 10 | const httpResponse = await controller.handle(request) 11 | switch (httpResponse.statusCode) { 12 | case 200: 13 | case 204: return httpResponse.body 14 | case 400: throw new UserInputError(httpResponse.body.message) 15 | case 401: throw new AuthenticationError(httpResponse.body.message) 16 | case 403: throw new ForbiddenError(httpResponse.body.message) 17 | default: throw new ApolloError(httpResponse.body.message) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/adapters/express-middleware-adapter.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from '@/presentation/protocols' 2 | 3 | import { Request, Response, NextFunction } from 'express' 4 | 5 | export const adaptMiddleware = (middleware: Middleware) => { 6 | return async (req: Request, res: Response, next: NextFunction) => { 7 | const request = { 8 | accessToken: req.headers?.['x-access-token'], 9 | ...(req.headers || {}) 10 | } 11 | const httpResponse = await middleware.handle(request) 12 | if (httpResponse.statusCode === 200) { 13 | Object.assign(req, httpResponse.body) 14 | next() 15 | } else { 16 | res.status(httpResponse.statusCode).json({ 17 | error: httpResponse.body.message 18 | }) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/adapters/express-route-adapter.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@/presentation/protocols' 2 | 3 | import { Request, Response } from 'express' 4 | 5 | export const adaptRoute = (controller: Controller) => { 6 | return async (req: Request, res: Response) => { 7 | const request = { 8 | ...(req.body || {}), 9 | ...(req.params || {}), 10 | accountId: req.accountId 11 | } 12 | const httpResponse = await controller.handle(request) 13 | if (httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299) { 14 | res.status(httpResponse.statusCode).json(httpResponse.body) 15 | } else { 16 | res.status(httpResponse.statusCode).json({ 17 | error: httpResponse.body.message 18 | }) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/adapters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './express-middleware-adapter' 2 | export * from './express-route-adapter' 3 | export * from './apollo-server-resolver-adapter' 4 | -------------------------------------------------------------------------------- /src/main/config/app.ts: -------------------------------------------------------------------------------- 1 | import setupMiddlewares from '@/main/config/middlewares' 2 | import setupRoutes from '@/main/config/routes' 3 | import setupStaticFiles from '@/main/config/static-files' 4 | import setupSwagger from '@/main/config/swagger' 5 | import { setupApolloServer } from '@/main/graphql/apollo' 6 | 7 | import express, { Express } from 'express' 8 | 9 | export const setupApp = async (): Promise => { 10 | const app = express() 11 | setupStaticFiles(app) 12 | setupSwagger(app) 13 | setupMiddlewares(app) 14 | setupRoutes(app) 15 | const server = setupApolloServer() 16 | await server.start() 17 | server.applyMiddleware({ app }) 18 | return app 19 | } 20 | -------------------------------------------------------------------------------- /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.MONGO_URL || 'mongodb://localhost:27017/clean-node-api', 3 | port: process.env.PORT || 5050, 4 | jwtSecret: process.env.JWT_SECRET || 'tj67O==5H' 5 | } 6 | -------------------------------------------------------------------------------- /src/main/config/middlewares.ts: -------------------------------------------------------------------------------- 1 | import { bodyParser, cors, contentType } from '@/main/middlewares' 2 | 3 | import { Express } from 'express' 4 | 5 | export default (app: Express): void => { 6 | app.use(bodyParser) 7 | app.use(cors) 8 | app.use(contentType) 9 | } 10 | -------------------------------------------------------------------------------- /src/main/config/routes.ts: -------------------------------------------------------------------------------- 1 | import { Express, Router } from 'express' 2 | import { readdirSync } from 'fs' 3 | import { join } from 'path' 4 | 5 | export default (app: Express): void => { 6 | const router = Router() 7 | app.use('/api', router) 8 | readdirSync(join(__dirname, '../routes')).map(async file => { 9 | if (!file.endsWith('.map')) { 10 | (await import(`../routes/${file}`)).default(router) 11 | } 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /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/config/swagger.ts: -------------------------------------------------------------------------------- 1 | import swaggerConfig from '@/main/docs' 2 | import { noCache } from '@/main/middlewares' 3 | 4 | import { serve, setup } from 'swagger-ui-express' 5 | import { Express } from 'express' 6 | 7 | export default (app: Express): void => { 8 | app.use('/api-docs', noCache, serve, setup(swaggerConfig)) 9 | } 10 | -------------------------------------------------------------------------------- /src/main/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './log-controller-decorator' 2 | -------------------------------------------------------------------------------- /src/main/decorators/log-controller-decorator.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpResponse } from '@/presentation/protocols' 2 | import { LogErrorRepository } from '@/data/protocols/db' 3 | 4 | export class LogControllerDecorator implements Controller { 5 | constructor ( 6 | private readonly controller: Controller, 7 | private readonly logErrorRepository: LogErrorRepository 8 | ) {} 9 | 10 | async handle (request: any): Promise { 11 | const httpResponse = await this.controller.handle(request) 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: 'Erro interno 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: '4Dev - Enquetes para Programadores', 9 | description: 'Essa é a documentação da API feita pelo instrutor Rodrigo Manguinho no curso da Udemy de NodeJs usando Typescript, TDD, Clean Architecture e seguindo os princípios do SOLID e Design Patterns.', 10 | version: '1.0.0', 11 | contact: { 12 | name: 'Rodrigo Manguinho', 13 | email: 'rodrigo.manguinho@gmail.com', 14 | url: 'https://www.linkedin.com/in/rmanguinho' 15 | }, 16 | license: { 17 | name: 'GPL-3.0-or-later', 18 | url: 'https://spdx.org/licenses/GPL-3.0-or-later.html' 19 | } 20 | }, 21 | externalDocs: { 22 | description: 'Link para o treinamento completo', 23 | url: 'https://www.udemy.com/course/tdd-com-mango/?referralCode=B53CE5CA2B9AFA5A6FA1' 24 | }, 25 | servers: [{ 26 | url: '/api', 27 | description: 'Servidor Principal' 28 | }], 29 | tags: [{ 30 | name: 'Login', 31 | description: 'APIs relacionadas a Login' 32 | }, { 33 | name: 'Enquete', 34 | description: 'APIs relacionadas a Enquete' 35 | }], 36 | paths, 37 | schemas, 38 | components 39 | } 40 | -------------------------------------------------------------------------------- /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 | description: 'Essa rota pode ser executada por **qualquer usuário**', 6 | requestBody: { 7 | required: true, 8 | content: { 9 | 'application/json': { 10 | schema: { 11 | $ref: '#/schemas/loginParams' 12 | } 13 | } 14 | } 15 | }, 16 | responses: { 17 | 200: { 18 | description: 'Sucesso', 19 | content: { 20 | 'application/json': { 21 | schema: { 22 | $ref: '#/schemas/account' 23 | } 24 | } 25 | } 26 | }, 27 | 400: { 28 | $ref: '#/components/badRequest' 29 | }, 30 | 401: { 31 | $ref: '#/components/unauthorized' 32 | }, 33 | 404: { 34 | $ref: '#/components/notFound' 35 | }, 36 | 500: { 37 | $ref: '#/components/serverError' 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /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 | description: 'Essa rota pode ser executada por **qualquer usuário**', 6 | requestBody: { 7 | required: true, 8 | content: { 9 | 'application/json': { 10 | schema: { 11 | $ref: '#/schemas/signUpParams' 12 | } 13 | } 14 | } 15 | }, 16 | responses: { 17 | 200: { 18 | description: 'Sucesso', 19 | content: { 20 | 'application/json': { 21 | schema: { 22 | $ref: '#/schemas/account' 23 | } 24 | } 25 | } 26 | }, 27 | 400: { 28 | $ref: '#/components/badRequest' 29 | }, 30 | 403: { 31 | $ref: '#/components/forbidden' 32 | }, 33 | 404: { 34 | $ref: '#/components/notFound' 35 | }, 36 | 500: { 37 | $ref: '#/components/serverError' 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /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 | description: 'Essa rota só pode ser executada por **usuários autenticados**', 9 | responses: { 10 | 200: { 11 | description: 'Sucesso', 12 | content: { 13 | 'application/json': { 14 | schema: { 15 | $ref: '#/schemas/surveys' 16 | } 17 | } 18 | } 19 | }, 20 | 204: { 21 | description: 'Sucesso, mas sem dados para exibir' 22 | }, 23 | 403: { 24 | $ref: '#/components/forbidden' 25 | }, 26 | 404: { 27 | $ref: '#/components/notFound' 28 | }, 29 | 500: { 30 | $ref: '#/components/serverError' 31 | } 32 | } 33 | }, 34 | post: { 35 | security: [{ 36 | apiKeyAuth: [] 37 | }], 38 | tags: ['Enquete'], 39 | summary: 'API para criar uma enquete', 40 | description: 'Essa rota só pode ser executada por **administradores**', 41 | requestBody: { 42 | required: true, 43 | content: { 44 | 'application/json': { 45 | schema: { 46 | $ref: '#/schemas/addSurveyParams' 47 | } 48 | } 49 | } 50 | }, 51 | responses: { 52 | 204: { 53 | description: 'Sucesso, mas sem dados para exibir' 54 | }, 55 | 403: { 56 | $ref: '#/components/forbidden' 57 | }, 58 | 404: { 59 | $ref: '#/components/notFound' 60 | }, 61 | 500: { 62 | $ref: '#/components/serverError' 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /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 | required: true, 20 | content: { 21 | 'application/json': { 22 | schema: { 23 | $ref: '#/schemas/saveSurveyParams' 24 | } 25 | } 26 | } 27 | }, 28 | responses: { 29 | 200: { 30 | description: 'Sucesso', 31 | content: { 32 | 'application/json': { 33 | schema: { 34 | $ref: '#/schemas/surveyResult' 35 | } 36 | } 37 | } 38 | }, 39 | 403: { 40 | $ref: '#/components/forbidden' 41 | }, 42 | 404: { 43 | $ref: '#/components/notFound' 44 | }, 45 | 500: { 46 | $ref: '#/components/serverError' 47 | } 48 | } 49 | }, 50 | get: { 51 | security: [{ 52 | apiKeyAuth: [] 53 | }], 54 | tags: ['Enquete'], 55 | summary: 'API para consultar o resultado de uma enquete', 56 | description: 'Essa rota só pode ser executada por **usuários autenticados**', 57 | parameters: [{ 58 | in: 'path', 59 | name: 'surveyId', 60 | description: 'ID da enquete a ser respondida', 61 | required: true, 62 | schema: { 63 | type: 'string' 64 | } 65 | }], 66 | responses: { 67 | 200: { 68 | description: 'Sucesso', 69 | content: { 70 | 'application/json': { 71 | schema: { 72 | $ref: '#/schemas/surveyResult' 73 | } 74 | } 75 | } 76 | }, 77 | 403: { 78 | $ref: '#/components/forbidden' 79 | }, 80 | 404: { 81 | $ref: '#/components/notFound' 82 | }, 83 | 500: { 84 | $ref: '#/components/serverError' 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /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 './add-survey-params-schema' 6 | export * from './survey-schema' 7 | export * from './surveys-schema' 8 | export * from './survey-answer-schema' 9 | export * from './api-key-auth-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 | isCurrentAccountAnswer: { 17 | type: 'boolean' 18 | } 19 | }, 20 | required: ['answer', 'count', 'percent', 'isCurrentAccountAnswer'] 21 | } 22 | -------------------------------------------------------------------------------- /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/add-survey-controller-factory.ts: -------------------------------------------------------------------------------- 1 | import { makeAddSurveyValidation, makeLogControllerDecorator, makeDbAddSurvey } from '@/main/factories' 2 | import { Controller } from '@/presentation/protocols' 3 | import { AddSurveyController } from '@/presentation/controllers' 4 | 5 | export const makeAddSurveyController = (): Controller => { 6 | const controller = new AddSurveyController(makeAddSurveyValidation(), makeDbAddSurvey()) 7 | return makeLogControllerDecorator(controller) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/factories/controllers/add-survey-validation-factory.ts: -------------------------------------------------------------------------------- 1 | import { ValidationComposite, RequiredFieldValidation } from '@/validation/validators' 2 | import { Validation } from '@/presentation/protocols' 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/index.ts: -------------------------------------------------------------------------------- 1 | export * from './add-survey-controller-factory' 2 | export * from './add-survey-validation-factory' 3 | export * from './load-survey-result-controller-factory' 4 | export * from './load-surveys-controller-factory' 5 | export * from './login-validation-factory' 6 | export * from './login-controller-factory' 7 | export * from './save-survey-result-controller-factory' 8 | export * from './signup-controller-factory' 9 | export * from './signup-validation-factory' 10 | -------------------------------------------------------------------------------- /src/main/factories/controllers/load-survey-result-controller-factory.ts: -------------------------------------------------------------------------------- 1 | import { makeLogControllerDecorator, makeDbCheckSurveyById, makeDbLoadSurveyResult } from '@/main/factories' 2 | import { Controller } from '@/presentation/protocols' 3 | import { LoadSurveyResultController } from '@/presentation/controllers' 4 | 5 | export const makeLoadSurveyResultController = (): Controller => { 6 | const controller = new LoadSurveyResultController(makeDbCheckSurveyById(), makeDbLoadSurveyResult()) 7 | return makeLogControllerDecorator(controller) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/factories/controllers/load-surveys-controller-factory.ts: -------------------------------------------------------------------------------- 1 | import { makeLogControllerDecorator, makeDbLoadSurveys } from '@/main/factories' 2 | import { Controller } from '@/presentation/protocols' 3 | import { LoadSurveysController } from '@/presentation/controllers' 4 | 5 | export const makeLoadSurveysController = (): Controller => { 6 | const controller = new LoadSurveysController(makeDbLoadSurveys()) 7 | return makeLogControllerDecorator(controller) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/factories/controllers/login-controller-factory.ts: -------------------------------------------------------------------------------- 1 | import { makeDbAuthentication, makeLoginValidation, makeLogControllerDecorator } from '@/main/factories' 2 | import { Controller } from '@/presentation/protocols' 3 | import { LoginController } from '@/presentation/controllers' 4 | 5 | export const makeLoginController = (): Controller => { 6 | const controller = new LoginController(makeDbAuthentication(), makeLoginValidation()) 7 | return makeLogControllerDecorator(controller) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/factories/controllers/login-validation-factory.ts: -------------------------------------------------------------------------------- 1 | import { ValidationComposite, RequiredFieldValidation, EmailValidation } from '@/validation/validators' 2 | import { Validation } from '@/presentation/protocols' 3 | import { EmailValidatorAdapter } from '@/infra/validators' 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/save-survey-result-controller-factory.ts: -------------------------------------------------------------------------------- 1 | import { makeLogControllerDecorator, makeDbLoadAnswersBySurvey, makeDbSaveSurveyResult } from '@/main/factories' 2 | import { Controller } from '@/presentation/protocols' 3 | import { SaveSurveyResultController } from '@/presentation/controllers' 4 | 5 | export const makeSaveSurveyResultController = (): Controller => { 6 | const controller = new SaveSurveyResultController(makeDbLoadAnswersBySurvey(), makeDbSaveSurveyResult()) 7 | return makeLogControllerDecorator(controller) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/factories/controllers/signup-controller-factory.ts: -------------------------------------------------------------------------------- 1 | import { makeDbAuthentication, makeSignUpValidation, makeLogControllerDecorator, makeDbAddAccount } from '@/main/factories' 2 | import { SignUpController } from '@/presentation/controllers' 3 | import { Controller } from '@/presentation/protocols' 4 | 5 | export const makeSignUpController = (): Controller => { 6 | const controller = new SignUpController(makeDbAddAccount(), makeSignUpValidation(), makeDbAuthentication()) 7 | return makeLogControllerDecorator(controller) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/factories/controllers/signup-validation-factory.ts: -------------------------------------------------------------------------------- 1 | import { ValidationComposite, RequiredFieldValidation, CompareFieldsValidation, EmailValidation } from '@/validation/validators' 2 | import { Validation } from '@/presentation/protocols' 3 | import { EmailValidatorAdapter } from '@/infra/validators' 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/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './log-controller-decorator-factory' 2 | -------------------------------------------------------------------------------- /src/main/factories/decorators/log-controller-decorator-factory.ts: -------------------------------------------------------------------------------- 1 | import { LogControllerDecorator } from '@/main/decorators' 2 | import { LogMongoRepository } from '@/infra/db' 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/index.ts: -------------------------------------------------------------------------------- 1 | export * from './controllers' 2 | export * from './decorators' 3 | export * from './middlewares' 4 | export * from './usecases' 5 | -------------------------------------------------------------------------------- /src/main/factories/middlewares/auth-middleware-factory.ts: -------------------------------------------------------------------------------- 1 | import { makeDbLoadAccountByToken } from '@/main/factories' 2 | import { Middleware } from '@/presentation/protocols' 3 | import { AuthMiddleware } from '@/presentation/middlewares' 4 | 5 | export const makeAuthMiddleware = (role?: string): Middleware => { 6 | return new AuthMiddleware(makeDbLoadAccountByToken(), role) 7 | } 8 | -------------------------------------------------------------------------------- /src/main/factories/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth-middleware-factory' 2 | -------------------------------------------------------------------------------- /src/main/factories/usecases/add-account-factory.ts: -------------------------------------------------------------------------------- 1 | import { DbAddAccount } from '@/data/usecases' 2 | import { AddAccount } from '@/domain/usecases' 3 | import { AccountMongoRepository } from '@/infra/db' 4 | import { BcryptAdapter } from '@/infra/cryptography' 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/add-survey-factory.ts: -------------------------------------------------------------------------------- 1 | import { AddSurvey } from '@/domain/usecases' 2 | import { SurveyMongoRepository } from '@/infra/db' 3 | import { DbAddSurvey } from '@/data/usecases' 4 | 5 | export const makeDbAddSurvey = (): AddSurvey => { 6 | const surveyMongoRepository = new SurveyMongoRepository() 7 | return new DbAddSurvey(surveyMongoRepository) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/factories/usecases/authentication-factory.ts: -------------------------------------------------------------------------------- 1 | import env from '@/main/config/env' 2 | import { AccountMongoRepository } from '@/infra/db' 3 | import { BcryptAdapter, JwtAdapter } from '@/infra/cryptography' 4 | import { DbAuthentication } from '@/data/usecases' 5 | import { Authentication } from '@/domain/usecases' 6 | 7 | export const makeDbAuthentication = (): Authentication => { 8 | const salt = 12 9 | const bcryptAdapter = new BcryptAdapter(salt) 10 | const jwtAdapter = new JwtAdapter(env.jwtSecret) 11 | const accountMongoRepository = new AccountMongoRepository() 12 | return new DbAuthentication(accountMongoRepository, bcryptAdapter, jwtAdapter, accountMongoRepository) 13 | } 14 | -------------------------------------------------------------------------------- /src/main/factories/usecases/check-survey-by-id-factory.ts: -------------------------------------------------------------------------------- 1 | import { SurveyMongoRepository } from '@/infra/db' 2 | import { CheckSurveyById } from '@/domain/usecases' 3 | import { DbCheckSurveyById } from '@/data/usecases' 4 | 5 | export const makeDbCheckSurveyById = (): CheckSurveyById => { 6 | const surveyMongoRepository = new SurveyMongoRepository() 7 | return new DbCheckSurveyById(surveyMongoRepository) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/factories/usecases/index.ts: -------------------------------------------------------------------------------- 1 | export * from './add-account-factory' 2 | export * from './load-account-by-token-factory' 3 | export * from './authentication-factory' 4 | export * from './add-survey-factory' 5 | export * from './load-answers-by-survey-factory' 6 | export * from './check-survey-by-id-factory' 7 | export * from './load-surveys-factory' 8 | export * from './load-survey-result-factory' 9 | export * from './save-survey-result-factory' 10 | -------------------------------------------------------------------------------- /src/main/factories/usecases/load-account-by-token-factory.ts: -------------------------------------------------------------------------------- 1 | import env from '@/main/config/env' 2 | import { LoadAccountByToken } from '@/domain/usecases' 3 | import { DbLoadAccountByToken } from '@/data/usecases' 4 | import { AccountMongoRepository } from '@/infra/db' 5 | import { JwtAdapter } from '@/infra/cryptography' 6 | 7 | export const makeDbLoadAccountByToken = (): LoadAccountByToken => { 8 | const jwtAdapter = new JwtAdapter(env.jwtSecret) 9 | const accountMongoRepository = new AccountMongoRepository() 10 | return new DbLoadAccountByToken(jwtAdapter, accountMongoRepository) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/factories/usecases/load-answers-by-survey-factory.ts: -------------------------------------------------------------------------------- 1 | import { SurveyMongoRepository } from '@/infra/db' 2 | import { LoadAnswersBySurvey } from '@/domain/usecases' 3 | import { DbLoadAnswersBySurvey } from '@/data/usecases' 4 | 5 | export const makeDbLoadAnswersBySurvey = (): LoadAnswersBySurvey => { 6 | const surveyMongoRepository = new SurveyMongoRepository() 7 | return new DbLoadAnswersBySurvey(surveyMongoRepository) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/factories/usecases/load-survey-result-factory.ts: -------------------------------------------------------------------------------- 1 | import { LoadSurveyResult } from '@/domain/usecases' 2 | import { DbLoadSurveyResult } from '@/data/usecases' 3 | import { SurveyResultMongoRepository, SurveyMongoRepository } from '@/infra/db' 4 | 5 | export const makeDbLoadSurveyResult = (): LoadSurveyResult => { 6 | const surveyResultMongoRepository = new SurveyResultMongoRepository() 7 | const surveyMongoRepository = new SurveyMongoRepository() 8 | return new DbLoadSurveyResult(surveyResultMongoRepository, surveyMongoRepository) 9 | } 10 | -------------------------------------------------------------------------------- /src/main/factories/usecases/load-surveys-factory.ts: -------------------------------------------------------------------------------- 1 | import { SurveyMongoRepository } from '@/infra/db' 2 | import { LoadSurveys } from '@/domain/usecases' 3 | import { DbLoadSurveys } from '@/data/usecases' 4 | 5 | export const makeDbLoadSurveys = (): LoadSurveys => { 6 | const surveyMongoRepository = new SurveyMongoRepository() 7 | return new DbLoadSurveys(surveyMongoRepository) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/factories/usecases/save-survey-result-factory.ts: -------------------------------------------------------------------------------- 1 | import { SaveSurveyResult } from '@/domain/usecases' 2 | import { DbSaveSurveyResult } from '@/data/usecases' 3 | import { SurveyResultMongoRepository } from '@/infra/db' 4 | 5 | export const makeDbSaveSurveyResult = (): SaveSurveyResult => { 6 | const surveyResultMongoRepository = new SurveyResultMongoRepository() 7 | return new DbSaveSurveyResult(surveyResultMongoRepository, surveyResultMongoRepository) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/graphql/apollo/apollo-server.ts: -------------------------------------------------------------------------------- 1 | import typeDefs from '@/main/graphql/type-defs' 2 | import resolvers from '@/main/graphql/resolvers' 3 | import { authDirectiveTransformer } from '@/main/graphql/directives' 4 | 5 | import { makeExecutableSchema } from '@graphql-tools/schema' 6 | import { ApolloServer } from 'apollo-server-express' 7 | import { GraphQLError } from 'graphql' 8 | 9 | const handleErrors = (response: any, errors: readonly GraphQLError[]): void => { 10 | errors?.forEach(error => { 11 | response.data = undefined 12 | if (checkError(error, 'UserInputError')) { 13 | response.http.status = 400 14 | } else if (checkError(error, 'AuthenticationError')) { 15 | response.http.status = 401 16 | } else if (checkError(error, 'ForbiddenError')) { 17 | response.http.status = 403 18 | } else { 19 | response.http.status = 500 20 | } 21 | }) 22 | } 23 | 24 | const checkError = (error: GraphQLError, errorName: string): boolean => { 25 | return [error.name, error.originalError?.name].some(name => name === errorName) 26 | } 27 | 28 | let schema = makeExecutableSchema({ resolvers, typeDefs }) 29 | schema = authDirectiveTransformer(schema) 30 | 31 | export const setupApolloServer = (): ApolloServer => new ApolloServer({ 32 | schema, 33 | context: ({ req }) => ({ req }), 34 | plugins: [{ 35 | requestDidStart: async () => ({ 36 | willSendResponse: async ({ response, errors }) => handleErrors(response, errors) 37 | }) 38 | }] 39 | }) 40 | -------------------------------------------------------------------------------- /src/main/graphql/apollo/index.ts: -------------------------------------------------------------------------------- 1 | export * from './apollo-server' 2 | -------------------------------------------------------------------------------- /src/main/graphql/directives/auth-directive.ts: -------------------------------------------------------------------------------- 1 | import { makeAuthMiddleware } from '@/main/factories' 2 | 3 | import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils' 4 | import { ForbiddenError } from 'apollo-server-express' 5 | import { GraphQLSchema } from 'graphql' 6 | 7 | export const authDirectiveTransformer = (schema: GraphQLSchema): GraphQLSchema => { 8 | return mapSchema(schema, { 9 | [MapperKind.OBJECT_FIELD]: (fieldConfig) => { 10 | const authDirective = getDirective(schema, fieldConfig, 'auth') 11 | if (authDirective) { 12 | const { resolve } = fieldConfig 13 | fieldConfig.resolve = async (parent, args, context, info) => { 14 | const request = { 15 | accessToken: context?.req?.headers?.['x-access-token'] 16 | } 17 | const httpResponse = await makeAuthMiddleware().handle(request) 18 | if (httpResponse.statusCode === 200) { 19 | Object.assign(context?.req, httpResponse.body) 20 | return resolve.call(this, parent, args, context, info) 21 | } else { 22 | throw new ForbiddenError(httpResponse.body.message) 23 | } 24 | } 25 | } 26 | return fieldConfig 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/main/graphql/directives/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth-directive' 2 | -------------------------------------------------------------------------------- /src/main/graphql/resolvers/base.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLDateTime } from 'graphql-scalars' 2 | 3 | export default { 4 | DateTime: GraphQLDateTime 5 | } 6 | -------------------------------------------------------------------------------- /src/main/graphql/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | import base from './base' 2 | import login from './login' 3 | import survey from './survey' 4 | import surveyResult from './survey-result' 5 | 6 | export default [base, login, survey, surveyResult] 7 | -------------------------------------------------------------------------------- /src/main/graphql/resolvers/login.ts: -------------------------------------------------------------------------------- 1 | import { adaptResolver } from '@/main/adapters' 2 | import { makeLoginController, makeSignUpController } from '@/main/factories' 3 | 4 | export default { 5 | Query: { 6 | login: async (parent: any, args: any) => adaptResolver(makeLoginController(), args) 7 | }, 8 | 9 | Mutation: { 10 | signUp: async (parent: any, args: any) => adaptResolver(makeSignUpController(), args) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/graphql/resolvers/survey-result.ts: -------------------------------------------------------------------------------- 1 | import { adaptResolver } from '@/main/adapters' 2 | import { makeLoadSurveyResultController, makeSaveSurveyResultController } from '@/main/factories' 3 | 4 | export default { 5 | Query: { 6 | surveyResult: async (parent: any, args: any, context: any) => adaptResolver(makeLoadSurveyResultController(), args, context) 7 | }, 8 | 9 | Mutation: { 10 | saveSurveyResult: async (parent: any, args: any, context: any) => adaptResolver(makeSaveSurveyResultController(), args, context) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/graphql/resolvers/survey.ts: -------------------------------------------------------------------------------- 1 | import { adaptResolver } from '@/main/adapters' 2 | import { makeLoadSurveysController } from '@/main/factories' 3 | 4 | export default { 5 | Query: { 6 | surveys: async (parent: any, args: any, context: any) => adaptResolver(makeLoadSurveysController(), args, context) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/graphql/type-defs/base.ts: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-server-express' 2 | 3 | export default gql` 4 | scalar DateTime 5 | 6 | directive @auth on FIELD_DEFINITION 7 | 8 | type Query { 9 | _: String 10 | } 11 | 12 | type Mutation { 13 | _: String 14 | } 15 | ` 16 | -------------------------------------------------------------------------------- /src/main/graphql/type-defs/index.ts: -------------------------------------------------------------------------------- 1 | import base from './base' 2 | import login from './login' 3 | import survey from './survey' 4 | import surveyResult from './survey-result' 5 | 6 | export default [base, login, survey, surveyResult] 7 | -------------------------------------------------------------------------------- /src/main/graphql/type-defs/login.ts: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-server-express' 2 | 3 | export default gql` 4 | extend type Query { 5 | login (email: String!, password: String!): Account! 6 | } 7 | 8 | extend type Mutation { 9 | signUp (name: String!, email: String!, password: String!, passwordConfirmation: String!): Account! 10 | } 11 | 12 | type Account { 13 | accessToken: String! 14 | name: String! 15 | } 16 | ` 17 | -------------------------------------------------------------------------------- /src/main/graphql/type-defs/survey-result.ts: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-server-express' 2 | 3 | export default gql` 4 | extend type Query { 5 | surveyResult (surveyId: String!): SurveyResult! @auth 6 | } 7 | 8 | extend type Mutation { 9 | saveSurveyResult (surveyId: String!, answer: String!): SurveyResult! @auth 10 | } 11 | 12 | type SurveyResult { 13 | surveyId: String! 14 | question: String! 15 | answers: [Answer!]! 16 | date: DateTime! 17 | } 18 | 19 | type Answer { 20 | image: String 21 | answer: String! 22 | count: Int! 23 | percent: Int! 24 | isCurrentAccountAnswer: Boolean! 25 | } 26 | ` 27 | -------------------------------------------------------------------------------- /src/main/graphql/type-defs/survey.ts: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-server-express' 2 | 3 | export default gql` 4 | extend type Query { 5 | surveys: [Survey!]! @auth 6 | } 7 | 8 | type Survey { 9 | id: ID! 10 | question: String! 11 | answers: [SurveyAnswer!]! 12 | date: DateTime! 13 | didAnswer: Boolean 14 | } 15 | 16 | type SurveyAnswer { 17 | image: String 18 | answer: String! 19 | } 20 | ` 21 | -------------------------------------------------------------------------------- /src/main/middlewares/admin-auth.ts: -------------------------------------------------------------------------------- 1 | import { adaptMiddleware } from '@/main/adapters' 2 | import { makeAuthMiddleware } from '@/main/factories' 3 | 4 | export const adminAuth = adaptMiddleware(makeAuthMiddleware('admin')) 5 | -------------------------------------------------------------------------------- /src/main/middlewares/auth.ts: -------------------------------------------------------------------------------- 1 | import { adaptMiddleware } from '@/main/adapters' 2 | import { makeAuthMiddleware } from '@/main/factories' 3 | 4 | export const auth = adaptMiddleware(makeAuthMiddleware()) 5 | -------------------------------------------------------------------------------- /src/main/middlewares/body-parser.ts: -------------------------------------------------------------------------------- 1 | import { json } from 'express' 2 | 3 | export const bodyParser = json() 4 | -------------------------------------------------------------------------------- /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.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-headers', '*') 6 | res.set('access-control-allow-methods', '*') 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 './no-cache' 5 | export * from './auth' 6 | export * from './admin-auth' 7 | -------------------------------------------------------------------------------- /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.ts: -------------------------------------------------------------------------------- 1 | import { adaptRoute } from '@/main/adapters' 2 | import { makeSignUpController, makeLoginController } from '@/main/factories' 3 | 4 | import { Router } from 'express' 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.ts: -------------------------------------------------------------------------------- 1 | import { makeSaveSurveyResultController, makeLoadSurveyResultController } from '@/main/factories' 2 | import { adaptRoute } from '@/main/adapters' 3 | import { auth } from '@/main/middlewares' 4 | 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.ts: -------------------------------------------------------------------------------- 1 | import { adaptRoute } from '@/main/adapters' 2 | import { makeAddSurveyController, makeLoadSurveysController } from '@/main/factories' 3 | import { adminAuth, auth } from '@/main/middlewares' 4 | 5 | import { Router } from 'express' 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 env from '@/main/config/env' 3 | import { MongoHelper } from '@/infra/db' 4 | 5 | MongoHelper.connect(env.mongoUrl) 6 | .then(async () => { 7 | const { setupApp } = await import('./config/app') 8 | const app = await setupApp() 9 | app.listen(env.port, () => console.log(`Server running at http://localhost:${env.port}`)) 10 | }) 11 | .catch(console.error) 12 | -------------------------------------------------------------------------------- /src/presentation/controllers/add-survey-controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpResponse, Validation } from '@/presentation/protocols' 2 | import { badRequest, serverError, noContent } from '@/presentation/helpers' 3 | import { AddSurvey } from '@/domain/usecases' 4 | 5 | export class AddSurveyController implements Controller { 6 | constructor ( 7 | private readonly validation: Validation, 8 | private readonly addSurvey: AddSurvey 9 | ) {} 10 | 11 | async handle (request: AddSurveyController.Request): Promise { 12 | try { 13 | const error = this.validation.validate(request) 14 | if (error) { 15 | return badRequest(error) 16 | } 17 | await this.addSurvey.add({ 18 | ...request, 19 | date: new Date() 20 | }) 21 | return noContent() 22 | } catch (error) { 23 | return serverError(error) 24 | } 25 | } 26 | } 27 | 28 | export namespace AddSurveyController { 29 | export type Request = { 30 | question: string 31 | answers: Answer[] 32 | } 33 | 34 | type Answer = { 35 | image?: string 36 | answer: string 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/presentation/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './add-survey-controller' 2 | export * from './load-survey-result-controller' 3 | export * from './load-surveys-controller' 4 | export * from './login-controller' 5 | export * from './save-survey-result-controller' 6 | export * from './signup-controller' 7 | -------------------------------------------------------------------------------- /src/presentation/controllers/load-survey-result-controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpResponse } from '@/presentation/protocols' 2 | import { forbidden, serverError, ok } from '@/presentation/helpers' 3 | import { InvalidParamError } from '@/presentation/errors' 4 | import { CheckSurveyById, LoadSurveyResult } from '@/domain/usecases' 5 | 6 | export class LoadSurveyResultController implements Controller { 7 | constructor ( 8 | private readonly checkSurveyById: CheckSurveyById, 9 | private readonly loadSurveyResult: LoadSurveyResult 10 | ) {} 11 | 12 | async handle (request: LoadSurveyResultController.Request): Promise { 13 | try { 14 | const { surveyId, accountId } = request 15 | const exists = await this.checkSurveyById.checkById(surveyId) 16 | if (!exists) { 17 | return forbidden(new InvalidParamError('surveyId')) 18 | } 19 | const surveyResult = await this.loadSurveyResult.load(surveyId, accountId) 20 | return ok(surveyResult) 21 | } catch (error) { 22 | return serverError(error) 23 | } 24 | } 25 | } 26 | 27 | export namespace LoadSurveyResultController { 28 | export type Request = { 29 | surveyId: string 30 | accountId: string 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/presentation/controllers/load-surveys-controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpResponse } from '@/presentation/protocols' 2 | import { noContent, serverError, ok } from '@/presentation/helpers' 3 | import { LoadSurveys } from '@/domain/usecases' 4 | 5 | export class LoadSurveysController implements Controller { 6 | constructor (private readonly loadSurveys: LoadSurveys) {} 7 | 8 | async handle (request: LoadSurveysController.Request): Promise { 9 | try { 10 | const surveys = await this.loadSurveys.load(request.accountId) 11 | return surveys.length ? ok(surveys) : noContent() 12 | } catch (error) { 13 | return serverError(error) 14 | } 15 | } 16 | } 17 | 18 | export namespace LoadSurveysController { 19 | export type Request = { 20 | accountId: string 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/presentation/controllers/login-controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpResponse, Validation } from '@/presentation/protocols' 2 | import { badRequest, serverError, unauthorized, ok } from '@/presentation/helpers' 3 | import { Authentication } from '@/domain/usecases' 4 | 5 | export class LoginController implements Controller { 6 | constructor ( 7 | private readonly authentication: Authentication, 8 | private readonly validation: Validation 9 | ) {} 10 | 11 | async handle (request: LoginController.Request): Promise { 12 | try { 13 | const error = this.validation.validate(request) 14 | if (error) { 15 | return badRequest(error) 16 | } 17 | const authenticationModel = await this.authentication.auth(request) 18 | if (!authenticationModel) { 19 | return unauthorized() 20 | } 21 | return ok(authenticationModel) 22 | } catch (error) { 23 | return serverError(error) 24 | } 25 | } 26 | } 27 | 28 | export namespace LoginController { 29 | export type Request = { 30 | email: string 31 | password: string 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/presentation/controllers/save-survey-result-controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpResponse } from '@/presentation/protocols' 2 | import { forbidden, serverError, ok } from '@/presentation/helpers' 3 | import { InvalidParamError } from '@/presentation/errors' 4 | import { LoadAnswersBySurvey, SaveSurveyResult } from '@/domain/usecases' 5 | 6 | export class SaveSurveyResultController implements Controller { 7 | constructor ( 8 | private readonly loadAnswersBySurvey: LoadAnswersBySurvey, 9 | private readonly saveSurveyResult: SaveSurveyResult 10 | ) {} 11 | 12 | async handle (request: SaveSurveyResultController.Request): Promise { 13 | try { 14 | const { surveyId, answer } = request 15 | const answers = await this.loadAnswersBySurvey.loadAnswers(surveyId) 16 | if (!answers.length) { 17 | return forbidden(new InvalidParamError('surveyId')) 18 | } else if (!answers.includes(answer)) { 19 | return forbidden(new InvalidParamError('answer')) 20 | } 21 | const surveyResult = await this.saveSurveyResult.save({ 22 | ...request, 23 | date: new Date() 24 | }) 25 | return ok(surveyResult) 26 | } catch (error) { 27 | return serverError(error) 28 | } 29 | } 30 | } 31 | 32 | export namespace SaveSurveyResultController { 33 | export type Request = { 34 | surveyId: string 35 | answer: string 36 | accountId: string 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/presentation/controllers/signup-controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpResponse, Validation } from '@/presentation/protocols' 2 | import { badRequest, serverError, ok, forbidden } from '@/presentation/helpers' 3 | import { EmailInUseError } from '@/presentation/errors' 4 | import { AddAccount, Authentication } from '@/domain/usecases' 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 (request: SignUpController.Request): Promise { 14 | try { 15 | const error = this.validation.validate(request) 16 | if (error) { 17 | return badRequest(error) 18 | } 19 | const { name, email, password } = request 20 | const isValid = await this.addAccount.add({ 21 | name, 22 | email, 23 | password 24 | }) 25 | if (!isValid) { 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 | 39 | export namespace SignUpController { 40 | export type Request = { 41 | name: string 42 | email: string 43 | password: string 44 | passwordConfirmation: string 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/presentation/errors/access-denied-error.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-param-error' 2 | export * from './missing-param-error' 3 | export * from './server-error' 4 | export * from './unauthorized-error' 5 | export * from './email-in-use-error' 6 | export * from './access-denied-error' 7 | -------------------------------------------------------------------------------- /src/presentation/errors/invalid-param-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-param-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-helper.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse } from '@/presentation/protocols' 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/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http-helper' 2 | -------------------------------------------------------------------------------- /src/presentation/middlewares/auth-middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware, HttpResponse } from '@/presentation/protocols' 2 | import { forbidden, ok, serverError } from '@/presentation/helpers' 3 | import { AccessDeniedError } from '@/presentation/errors' 4 | import { LoadAccountByToken } from '@/domain/usecases' 5 | 6 | export class AuthMiddleware implements Middleware { 7 | constructor ( 8 | private readonly loadAccountByToken: LoadAccountByToken, 9 | private readonly role?: string 10 | ) {} 11 | 12 | async handle (request: AuthMiddleware.Request): Promise { 13 | try { 14 | const { accessToken } = request 15 | if (accessToken) { 16 | const account = await this.loadAccountByToken.load(accessToken, this.role) 17 | if (account) { 18 | return ok({ accountId: account.id }) 19 | } 20 | } 21 | return forbidden(new AccessDeniedError()) 22 | } catch (error) { 23 | return serverError(error) 24 | } 25 | } 26 | } 27 | 28 | export namespace AuthMiddleware { 29 | export type Request = { 30 | accessToken?: string 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/presentation/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth-middleware' 2 | -------------------------------------------------------------------------------- /src/presentation/protocols/controller.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse } from '@/presentation/protocols' 2 | 3 | export interface Controller { 4 | handle: (request: T) => Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/presentation/protocols/http.ts: -------------------------------------------------------------------------------- 1 | export type HttpResponse = { 2 | statusCode: number 3 | body: any 4 | } 5 | -------------------------------------------------------------------------------- /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 { HttpResponse } from '@/presentation/protocols' 2 | 3 | export interface Middleware { 4 | handle: (httpRequest: T) => Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/presentation/protocols/validation.ts: -------------------------------------------------------------------------------- 1 | export interface Validation { 2 | validate: (input: any) => Error 3 | } 4 | -------------------------------------------------------------------------------- /src/validation/protocols/email-validator.ts: -------------------------------------------------------------------------------- 1 | export interface EmailValidator { 2 | isValid: (email: string) => boolean 3 | } 4 | -------------------------------------------------------------------------------- /src/validation/protocols/index.ts: -------------------------------------------------------------------------------- 1 | export * from './email-validator' 2 | -------------------------------------------------------------------------------- /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.ts: -------------------------------------------------------------------------------- 1 | import { EmailValidator } from '@/validation/protocols' 2 | import { Validation } from '@/presentation/protocols' 3 | import { InvalidParamError } from '@/presentation/errors' 4 | 5 | export class EmailValidation implements Validation { 6 | constructor ( 7 | private readonly fieldName: string, 8 | private readonly emailValidator: EmailValidator 9 | ) {} 10 | 11 | validate (input: any): Error { 12 | const isValid = this.emailValidator.isValid(input[this.fieldName]) 13 | if (!isValid) { 14 | return new InvalidParamError(this.fieldName) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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.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 | 7 | validate (input: any): Error { 8 | if (!input[this.fieldName]) { 9 | return new MissingParamError(this.fieldName) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/data/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mock-cryptography' 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 | -------------------------------------------------------------------------------- /tests/data/mocks/mock-cryptography.ts: -------------------------------------------------------------------------------- 1 | import { Hasher, HashComparer, Encrypter, Decrypter } from '@/data/protocols' 2 | 3 | import faker from 'faker' 4 | 5 | export class HasherSpy implements Hasher { 6 | digest = faker.datatype.uuid() 7 | plaintext: string 8 | 9 | async hash (plaintext: string): Promise { 10 | this.plaintext = plaintext 11 | return this.digest 12 | } 13 | } 14 | 15 | export class HashComparerSpy implements HashComparer { 16 | plaintext: string 17 | digest: string 18 | isValid = true 19 | 20 | async compare (plaintext: string, digest: string): Promise { 21 | this.plaintext = plaintext 22 | this.digest = digest 23 | return this.isValid 24 | } 25 | } 26 | 27 | export class EncrypterSpy implements Encrypter { 28 | ciphertext = faker.datatype.uuid() 29 | plaintext: string 30 | 31 | async encrypt (plaintext: string): Promise { 32 | this.plaintext = plaintext 33 | return this.ciphertext 34 | } 35 | } 36 | 37 | export class DecrypterSpy implements Decrypter { 38 | plaintext = faker.internet.password() 39 | ciphertext: string 40 | 41 | async decrypt (ciphertext: string): Promise { 42 | this.ciphertext = ciphertext 43 | return this.plaintext 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/data/mocks/mock-db-account.ts: -------------------------------------------------------------------------------- 1 | import { AddAccountRepository, LoadAccountByEmailRepository, LoadAccountByTokenRepository, UpdateAccessTokenRepository, CheckAccountByEmailRepository } from '@/data/protocols' 2 | 3 | import faker from 'faker' 4 | 5 | export class AddAccountRepositorySpy implements AddAccountRepository { 6 | params: AddAccountRepository.Params 7 | result = true 8 | 9 | async add (params: AddAccountRepository.Params): Promise { 10 | this.params = params 11 | return this.result 12 | } 13 | } 14 | 15 | export class LoadAccountByEmailRepositorySpy implements LoadAccountByEmailRepository { 16 | email: string 17 | result = { 18 | id: faker.datatype.uuid(), 19 | name: faker.name.findName(), 20 | password: faker.internet.password() 21 | } 22 | 23 | async loadByEmail (email: string): Promise { 24 | this.email = email 25 | return this.result 26 | } 27 | } 28 | 29 | export class CheckAccountByEmailRepositorySpy implements CheckAccountByEmailRepository { 30 | email: string 31 | result = false 32 | 33 | async checkByEmail (email: string): Promise { 34 | this.email = email 35 | return this.result 36 | } 37 | } 38 | 39 | export class LoadAccountByTokenRepositorySpy implements LoadAccountByTokenRepository { 40 | token: string 41 | role: string 42 | result = { 43 | id: faker.datatype.uuid() 44 | } 45 | 46 | async loadByToken (token: string, role?: string): Promise { 47 | this.token = token 48 | this.role = role 49 | return this.result 50 | } 51 | } 52 | 53 | export class UpdateAccessTokenRepositorySpy implements UpdateAccessTokenRepository { 54 | id: string 55 | token: string 56 | 57 | async updateAccessToken (id: string, token: string): Promise { 58 | this.id = id 59 | this.token = token 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/data/mocks/mock-db-log.ts: -------------------------------------------------------------------------------- 1 | import { LogErrorRepository } from '@/data/protocols' 2 | 3 | export class LogErrorRepositorySpy implements LogErrorRepository { 4 | stack: string 5 | 6 | async logError (stack: string): Promise { 7 | this.stack = stack 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/data/mocks/mock-db-survey-result.ts: -------------------------------------------------------------------------------- 1 | import { SaveSurveyResultRepository, LoadSurveyResultRepository } from '@/data/protocols' 2 | import { mockSurveyResultModel } from '@/tests/domain/mocks' 3 | 4 | export class SaveSurveyResultRepositorySpy implements SaveSurveyResultRepository { 5 | params: SaveSurveyResultRepository.Params 6 | 7 | async save (params: SaveSurveyResultRepository.Params): Promise { 8 | this.params = params 9 | } 10 | } 11 | 12 | export class LoadSurveyResultRepositorySpy implements LoadSurveyResultRepository { 13 | surveyId: string 14 | accountId: string 15 | result = mockSurveyResultModel() 16 | 17 | async loadBySurveyId (surveyId: string, accountId: string): Promise { 18 | this.surveyId = surveyId 19 | this.accountId = accountId 20 | return this.result 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/data/mocks/mock-db-survey.ts: -------------------------------------------------------------------------------- 1 | import { AddSurveyRepository, LoadSurveyByIdRepository, LoadSurveysRepository, CheckSurveyByIdRepository, LoadAnswersBySurveyRepository } from '@/data/protocols' 2 | import { mockSurveyModel, mockSurveyModels } from '@/tests/domain/mocks' 3 | 4 | import faker from 'faker' 5 | 6 | export class AddSurveyRepositorySpy implements AddSurveyRepository { 7 | params: AddSurveyRepository.Params 8 | 9 | async add (params: AddSurveyRepository.Params): Promise { 10 | this.params = params 11 | } 12 | } 13 | 14 | export class LoadSurveyByIdRepositorySpy implements LoadSurveyByIdRepository { 15 | id: string 16 | result = mockSurveyModel() 17 | 18 | async loadById (id: string): Promise { 19 | this.id = id 20 | return this.result 21 | } 22 | } 23 | 24 | export class LoadAnswersBySurveyRepositorySpy implements LoadAnswersBySurveyRepository { 25 | id: string 26 | result = [ 27 | faker.random.word(), 28 | faker.random.word() 29 | ] 30 | 31 | async loadAnswers (id: string): Promise { 32 | this.id = id 33 | return this.result 34 | } 35 | } 36 | 37 | export class CheckSurveyByIdRepositorySpy implements CheckSurveyByIdRepository { 38 | id: string 39 | result = true 40 | 41 | async checkById (id: string): Promise { 42 | this.id = id 43 | return this.result 44 | } 45 | } 46 | 47 | export class LoadSurveysRepositorySpy implements LoadSurveysRepository { 48 | accountId: string 49 | result = mockSurveyModels() 50 | 51 | async loadAll (accountId: string): Promise { 52 | this.accountId = accountId 53 | return this.result 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/data/usecases/db-add-account.spec.ts: -------------------------------------------------------------------------------- 1 | import { DbAddAccount } from '@/data/usecases' 2 | import { mockAddAccountParams, throwError } from '@/tests/domain/mocks' 3 | import { HasherSpy, AddAccountRepositorySpy, CheckAccountByEmailRepositorySpy } from '@/tests/data/mocks' 4 | 5 | type SutTypes = { 6 | sut: DbAddAccount 7 | hasherSpy: HasherSpy 8 | addAccountRepositorySpy: AddAccountRepositorySpy 9 | checkAccountByEmailRepositorySpy: CheckAccountByEmailRepositorySpy 10 | } 11 | 12 | const makeSut = (): SutTypes => { 13 | const checkAccountByEmailRepositorySpy = new CheckAccountByEmailRepositorySpy() 14 | const hasherSpy = new HasherSpy() 15 | const addAccountRepositorySpy = new AddAccountRepositorySpy() 16 | const sut = new DbAddAccount(hasherSpy, addAccountRepositorySpy, checkAccountByEmailRepositorySpy) 17 | return { 18 | sut, 19 | hasherSpy, 20 | addAccountRepositorySpy, 21 | checkAccountByEmailRepositorySpy 22 | } 23 | } 24 | 25 | describe('DbAddAccount Usecase', () => { 26 | test('Should call Hasher with correct plaintext', async () => { 27 | const { sut, hasherSpy } = makeSut() 28 | const addAccountParams = mockAddAccountParams() 29 | await sut.add(addAccountParams) 30 | expect(hasherSpy.plaintext).toBe(addAccountParams.password) 31 | }) 32 | 33 | test('Should throw if Hasher throws', async () => { 34 | const { sut, hasherSpy } = makeSut() 35 | jest.spyOn(hasherSpy, 'hash').mockImplementationOnce(throwError) 36 | const promise = sut.add(mockAddAccountParams()) 37 | await expect(promise).rejects.toThrow() 38 | }) 39 | 40 | test('Should call AddAccountRepository with correct values', async () => { 41 | const { sut, addAccountRepositorySpy, hasherSpy } = makeSut() 42 | const addAccountParams = mockAddAccountParams() 43 | await sut.add(addAccountParams) 44 | expect(addAccountRepositorySpy.params).toEqual({ 45 | name: addAccountParams.name, 46 | email: addAccountParams.email, 47 | password: hasherSpy.digest 48 | }) 49 | }) 50 | 51 | test('Should throw if AddAccountRepository throws', async () => { 52 | const { sut, addAccountRepositorySpy } = makeSut() 53 | jest.spyOn(addAccountRepositorySpy, 'add').mockImplementationOnce(throwError) 54 | const promise = sut.add(mockAddAccountParams()) 55 | await expect(promise).rejects.toThrow() 56 | }) 57 | 58 | test('Should return true on success', async () => { 59 | const { sut } = makeSut() 60 | const isValid = await sut.add(mockAddAccountParams()) 61 | expect(isValid).toBe(true) 62 | }) 63 | 64 | test('Should return false if AddAccountRepository returns false', async () => { 65 | const { sut, addAccountRepositorySpy } = makeSut() 66 | addAccountRepositorySpy.result = false 67 | const isValid = await sut.add(mockAddAccountParams()) 68 | expect(isValid).toBe(false) 69 | }) 70 | 71 | test('Should return false if CheckAccountByEmailRepository returns true', async () => { 72 | const { sut, checkAccountByEmailRepositorySpy } = makeSut() 73 | checkAccountByEmailRepositorySpy.result = true 74 | const isValid = await sut.add(mockAddAccountParams()) 75 | expect(isValid).toBe(false) 76 | }) 77 | 78 | test('Should call LoadAccountByEmailRepository with correct email', async () => { 79 | const { sut, checkAccountByEmailRepositorySpy } = makeSut() 80 | const addAccountParams = mockAddAccountParams() 81 | await sut.add(addAccountParams) 82 | expect(checkAccountByEmailRepositorySpy.email).toBe(addAccountParams.email) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /tests/data/usecases/db-add-survey.spec.ts: -------------------------------------------------------------------------------- 1 | import { DbAddSurvey } from '@/data/usecases' 2 | import { AddSurveyRepositorySpy } from '@/tests/data/mocks' 3 | import { throwError, mockAddSurveyParams } from '@/tests/domain/mocks' 4 | 5 | import MockDate from 'mockdate' 6 | 7 | type SutTypes = { 8 | sut: DbAddSurvey 9 | addSurveyRepositorySpy: AddSurveyRepositorySpy 10 | } 11 | 12 | const makeSut = (): SutTypes => { 13 | const addSurveyRepositorySpy = new AddSurveyRepositorySpy() 14 | const sut = new DbAddSurvey(addSurveyRepositorySpy) 15 | return { 16 | sut, 17 | addSurveyRepositorySpy 18 | } 19 | } 20 | 21 | describe('DbAddSurvey Usecase', () => { 22 | beforeAll(() => { 23 | MockDate.set(new Date()) 24 | }) 25 | 26 | afterAll(() => { 27 | MockDate.reset() 28 | }) 29 | 30 | test('Should call AddSurveyRepository with correct values', async () => { 31 | const { sut, addSurveyRepositorySpy } = makeSut() 32 | const surveyData = mockAddSurveyParams() 33 | await sut.add(surveyData) 34 | expect(addSurveyRepositorySpy.params).toEqual(surveyData) 35 | }) 36 | 37 | test('Should throw if AddSurveyRepository throws', async () => { 38 | const { sut, addSurveyRepositorySpy } = makeSut() 39 | jest.spyOn(addSurveyRepositorySpy, 'add').mockImplementationOnce(throwError) 40 | const promise = sut.add(mockAddSurveyParams()) 41 | await expect(promise).rejects.toThrow() 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /tests/data/usecases/db-check-survey-by-id.spec.ts: -------------------------------------------------------------------------------- 1 | import { DbCheckSurveyById } from '@/data/usecases' 2 | import { CheckSurveyByIdRepositorySpy } from '@/tests/data/mocks' 3 | import { throwError } from '@/tests/domain/mocks' 4 | 5 | import faker from 'faker' 6 | 7 | type SutTypes = { 8 | sut: DbCheckSurveyById 9 | checkSurveyByIdRepositorySpy: CheckSurveyByIdRepositorySpy 10 | } 11 | 12 | const makeSut = (): SutTypes => { 13 | const checkSurveyByIdRepositorySpy = new CheckSurveyByIdRepositorySpy() 14 | const sut = new DbCheckSurveyById(checkSurveyByIdRepositorySpy) 15 | return { 16 | sut, 17 | checkSurveyByIdRepositorySpy 18 | } 19 | } 20 | 21 | let surveyId: string 22 | 23 | describe('DbLoadSurveyById', () => { 24 | beforeEach(() => { 25 | surveyId = faker.datatype.uuid() 26 | }) 27 | 28 | test('Should call CheckSurveyByIdRepository', async () => { 29 | const { sut, checkSurveyByIdRepositorySpy } = makeSut() 30 | await sut.checkById(surveyId) 31 | expect(checkSurveyByIdRepositorySpy.id).toBe(surveyId) 32 | }) 33 | 34 | test('Should return true if CheckSurveyByIdRepository returns true', async () => { 35 | const { sut } = makeSut() 36 | const exists = await sut.checkById(surveyId) 37 | expect(exists).toBe(true) 38 | }) 39 | 40 | test('Should return false if CheckSurveyByIdRepository returns false', async () => { 41 | const { sut, checkSurveyByIdRepositorySpy } = makeSut() 42 | checkSurveyByIdRepositorySpy.result = false 43 | const exists = await sut.checkById(surveyId) 44 | expect(exists).toBe(false) 45 | }) 46 | 47 | test('Should throw if CheckSurveyByIdRepository throws', async () => { 48 | const { sut, checkSurveyByIdRepositorySpy } = makeSut() 49 | jest.spyOn(checkSurveyByIdRepositorySpy, 'checkById').mockImplementationOnce(throwError) 50 | const promise = sut.checkById(surveyId) 51 | await expect(promise).rejects.toThrow() 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /tests/data/usecases/db-load-account-by-token.spec.ts: -------------------------------------------------------------------------------- 1 | import { DbLoadAccountByToken } from '@/data/usecases' 2 | import { DecrypterSpy, LoadAccountByTokenRepositorySpy } from '@/tests/data/mocks' 3 | import { throwError } from '@/tests/domain/mocks' 4 | 5 | import faker from 'faker' 6 | 7 | type SutTypes = { 8 | sut: DbLoadAccountByToken 9 | decrypterSpy: DecrypterSpy 10 | loadAccountByTokenRepositorySpy: LoadAccountByTokenRepositorySpy 11 | } 12 | 13 | const makeSut = (): SutTypes => { 14 | const decrypterSpy = new DecrypterSpy() 15 | const loadAccountByTokenRepositorySpy = new LoadAccountByTokenRepositorySpy() 16 | const sut = new DbLoadAccountByToken(decrypterSpy, loadAccountByTokenRepositorySpy) 17 | return { 18 | sut, 19 | decrypterSpy, 20 | loadAccountByTokenRepositorySpy 21 | } 22 | } 23 | 24 | let token: string 25 | let role: string 26 | 27 | describe('DbLoadAccountByToken Usecase', () => { 28 | beforeEach(() => { 29 | token = faker.datatype.uuid() 30 | role = faker.random.word() 31 | }) 32 | 33 | test('Should call Decrypter with correct ciphertext', async () => { 34 | const { sut, decrypterSpy } = makeSut() 35 | await sut.load(token, role) 36 | expect(decrypterSpy.ciphertext).toBe(token) 37 | }) 38 | 39 | test('Should return null if Decrypter returns null', async () => { 40 | const { sut, decrypterSpy } = makeSut() 41 | decrypterSpy.plaintext = null 42 | const account = await sut.load(token, role) 43 | expect(account).toBeNull() 44 | }) 45 | 46 | test('Should call LoadAccountByTokenRepository with correct values', async () => { 47 | const { sut, loadAccountByTokenRepositorySpy } = makeSut() 48 | await sut.load(token, role) 49 | expect(loadAccountByTokenRepositorySpy.token).toBe(token) 50 | expect(loadAccountByTokenRepositorySpy.role).toBe(role) 51 | }) 52 | 53 | test('Should return null if LoadAccountByTokenRepository returns null', async () => { 54 | const { sut, loadAccountByTokenRepositorySpy } = makeSut() 55 | loadAccountByTokenRepositorySpy.result = null 56 | const account = await sut.load(token, role) 57 | expect(account).toBeNull() 58 | }) 59 | 60 | test('Should return an account on success', async () => { 61 | const { sut, loadAccountByTokenRepositorySpy } = makeSut() 62 | const account = await sut.load(token, role) 63 | expect(account).toEqual(loadAccountByTokenRepositorySpy.result) 64 | }) 65 | 66 | test('Should throw if Decrypter throws', async () => { 67 | const { sut, decrypterSpy } = makeSut() 68 | jest.spyOn(decrypterSpy, 'decrypt').mockImplementationOnce(throwError) 69 | const account = await sut.load(token, role) 70 | await expect(account).toBeNull() 71 | }) 72 | 73 | test('Should throw if LoadAccountByTokenRepository throws', async () => { 74 | const { sut, loadAccountByTokenRepositorySpy } = makeSut() 75 | jest.spyOn(loadAccountByTokenRepositorySpy, 'loadByToken').mockImplementationOnce(throwError) 76 | const promise = sut.load(token, role) 77 | await expect(promise).rejects.toThrow() 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /tests/data/usecases/db-load-answers-by-survey.spec.ts: -------------------------------------------------------------------------------- 1 | import { DbLoadAnswersBySurvey } from '@/data/usecases' 2 | import { LoadAnswersBySurveyRepositorySpy } from '@/tests/data/mocks' 3 | import { throwError } from '@/tests/domain/mocks' 4 | 5 | import faker from 'faker' 6 | 7 | type SutTypes = { 8 | sut: DbLoadAnswersBySurvey 9 | loadAnswersBySurveyRepositorySpy: LoadAnswersBySurveyRepositorySpy 10 | } 11 | 12 | const makeSut = (): SutTypes => { 13 | const loadAnswersBySurveyRepositorySpy = new LoadAnswersBySurveyRepositorySpy() 14 | const sut = new DbLoadAnswersBySurvey(loadAnswersBySurveyRepositorySpy) 15 | return { 16 | sut, 17 | loadAnswersBySurveyRepositorySpy 18 | } 19 | } 20 | 21 | let surveyId: string 22 | 23 | describe('DbLoadAnswersBySurvey', () => { 24 | beforeEach(() => { 25 | surveyId = faker.datatype.uuid() 26 | }) 27 | 28 | test('Should call LoadAnswersBySurveyRepository', async () => { 29 | const { sut, loadAnswersBySurveyRepositorySpy } = makeSut() 30 | await sut.loadAnswers(surveyId) 31 | expect(loadAnswersBySurveyRepositorySpy.id).toBe(surveyId) 32 | }) 33 | 34 | test('Should return answers on success', async () => { 35 | const { sut, loadAnswersBySurveyRepositorySpy } = makeSut() 36 | const answers = await sut.loadAnswers(surveyId) 37 | expect(answers).toEqual([ 38 | loadAnswersBySurveyRepositorySpy.result[0], 39 | loadAnswersBySurveyRepositorySpy.result[1] 40 | ]) 41 | }) 42 | 43 | test('Should return empty array if LoadAnswersBySurveyRepository returns []', async () => { 44 | const { sut, loadAnswersBySurveyRepositorySpy } = makeSut() 45 | loadAnswersBySurveyRepositorySpy.result = [] 46 | const answers = await sut.loadAnswers(surveyId) 47 | expect(answers).toEqual([]) 48 | }) 49 | 50 | test('Should throw if LoadAnswersBySurveyRepository throws', async () => { 51 | const { sut, loadAnswersBySurveyRepositorySpy } = makeSut() 52 | jest.spyOn(loadAnswersBySurveyRepositorySpy, 'loadAnswers').mockImplementationOnce(throwError) 53 | const promise = sut.loadAnswers(surveyId) 54 | await expect(promise).rejects.toThrow() 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /tests/data/usecases/db-load-survey-result.spec.ts: -------------------------------------------------------------------------------- 1 | import { DbLoadSurveyResult } from '@/data/usecases' 2 | import { LoadSurveyResultRepositorySpy, LoadSurveyByIdRepositorySpy } from '@/tests/data/mocks' 3 | import { throwError } from '@/tests/domain/mocks' 4 | 5 | import MockDate from 'mockdate' 6 | import faker from 'faker' 7 | 8 | type SutTypes = { 9 | sut: DbLoadSurveyResult 10 | loadSurveyResultRepositorySpy: LoadSurveyResultRepositorySpy 11 | loadSurveyByIdRepositorySpy: LoadSurveyByIdRepositorySpy 12 | } 13 | 14 | const makeSut = (): SutTypes => { 15 | const loadSurveyResultRepositorySpy = new LoadSurveyResultRepositorySpy() 16 | const loadSurveyByIdRepositorySpy = new LoadSurveyByIdRepositorySpy() 17 | const sut = new DbLoadSurveyResult(loadSurveyResultRepositorySpy, loadSurveyByIdRepositorySpy) 18 | return { 19 | sut, 20 | loadSurveyResultRepositorySpy, 21 | loadSurveyByIdRepositorySpy 22 | } 23 | } 24 | 25 | let surveyId: string 26 | let accountId: string 27 | 28 | describe('DbLoadSurveyResult UseCase', () => { 29 | beforeAll(() => { 30 | MockDate.set(new Date()) 31 | }) 32 | 33 | afterAll(() => { 34 | MockDate.reset() 35 | }) 36 | 37 | beforeEach(() => { 38 | surveyId = faker.datatype.uuid() 39 | accountId = faker.datatype.uuid() 40 | }) 41 | 42 | test('Should call LoadSurveyResultRepository with correct values', async () => { 43 | const { sut, loadSurveyResultRepositorySpy } = makeSut() 44 | await sut.load(surveyId, accountId) 45 | expect(loadSurveyResultRepositorySpy.surveyId).toBe(surveyId) 46 | expect(loadSurveyResultRepositorySpy.accountId).toBe(accountId) 47 | }) 48 | 49 | test('Should throw if LoadSurveyResultRepository throws', async () => { 50 | const { sut, loadSurveyResultRepositorySpy } = makeSut() 51 | jest.spyOn(loadSurveyResultRepositorySpy, 'loadBySurveyId').mockImplementationOnce(throwError) 52 | const promise = sut.load(surveyId, accountId) 53 | await expect(promise).rejects.toThrow() 54 | }) 55 | 56 | test('Should call LoadSurveyByIdRepository if LoadSurveyResultRepository returns null', async () => { 57 | const { sut, loadSurveyResultRepositorySpy, loadSurveyByIdRepositorySpy } = makeSut() 58 | loadSurveyResultRepositorySpy.result = null 59 | await sut.load(surveyId, accountId) 60 | expect(loadSurveyByIdRepositorySpy.id).toBe(surveyId) 61 | }) 62 | 63 | test('Should return surveyResultModel with all answers with count 0 if LoadSurveyResultRepository returns null', async () => { 64 | const { sut, loadSurveyResultRepositorySpy, loadSurveyByIdRepositorySpy } = makeSut() 65 | loadSurveyResultRepositorySpy.result = null 66 | const surveyResult = await sut.load(surveyId, accountId) 67 | const { result } = loadSurveyByIdRepositorySpy 68 | expect(surveyResult).toEqual({ 69 | surveyId: result.id, 70 | question: result.question, 71 | date: result.date, 72 | answers: result.answers.map(answer => ({ 73 | ...answer, 74 | count: 0, 75 | percent: 0, 76 | isCurrentAccountAnswer: false 77 | })) 78 | }) 79 | }) 80 | 81 | test('Should return surveyResultModel on success', async () => { 82 | const { sut, loadSurveyResultRepositorySpy } = makeSut() 83 | const surveyResult = await sut.load(surveyId, accountId) 84 | expect(surveyResult).toEqual(loadSurveyResultRepositorySpy.result) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /tests/data/usecases/db-load-surveys.spec.ts: -------------------------------------------------------------------------------- 1 | import { DbLoadSurveys } from '@/data/usecases' 2 | import { LoadSurveysRepositorySpy } from '@/tests/data/mocks' 3 | import { throwError } from '@/tests/domain/mocks' 4 | 5 | import MockDate from 'mockdate' 6 | import faker from 'faker' 7 | 8 | type SutTypes = { 9 | sut: DbLoadSurveys 10 | loadSurveysRepositorySpy: LoadSurveysRepositorySpy 11 | } 12 | 13 | const makeSut = (): SutTypes => { 14 | const loadSurveysRepositorySpy = new LoadSurveysRepositorySpy() 15 | const sut = new DbLoadSurveys(loadSurveysRepositorySpy) 16 | return { 17 | sut, 18 | loadSurveysRepositorySpy 19 | } 20 | } 21 | 22 | describe('DbLoadSurveys', () => { 23 | beforeAll(() => { 24 | MockDate.set(new Date()) 25 | }) 26 | 27 | afterAll(() => { 28 | MockDate.reset() 29 | }) 30 | 31 | test('Should call LoadSurveysRepository', async () => { 32 | const { sut, loadSurveysRepositorySpy } = makeSut() 33 | const accountId = faker.datatype.uuid() 34 | await sut.load(accountId) 35 | expect(loadSurveysRepositorySpy.accountId).toBe(accountId) 36 | }) 37 | 38 | test('Should return a list of Surveys on success', async () => { 39 | const { sut, loadSurveysRepositorySpy } = makeSut() 40 | const surveys = await sut.load(faker.datatype.uuid()) 41 | expect(surveys).toEqual(loadSurveysRepositorySpy.result) 42 | }) 43 | 44 | test('Should throw if LoadSurveysRepository throws', async () => { 45 | const { sut, loadSurveysRepositorySpy } = makeSut() 46 | jest.spyOn(loadSurveysRepositorySpy, 'loadAll').mockImplementationOnce(throwError) 47 | const promise = sut.load(faker.datatype.uuid()) 48 | await expect(promise).rejects.toThrow() 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /tests/data/usecases/db-save-survey-result.spec.ts: -------------------------------------------------------------------------------- 1 | import { DbSaveSurveyResult } from '@/data/usecases' 2 | import { SaveSurveyResultRepositorySpy, LoadSurveyResultRepositorySpy } from '@/tests/data/mocks' 3 | import { throwError, mockSaveSurveyResultParams } from '@/tests/domain/mocks' 4 | 5 | import MockDate from 'mockdate' 6 | 7 | type SutTypes = { 8 | sut: DbSaveSurveyResult 9 | saveSurveyResultRepositorySpy: SaveSurveyResultRepositorySpy 10 | loadSurveyResultRepositorySpy: LoadSurveyResultRepositorySpy 11 | } 12 | 13 | const makeSut = (): SutTypes => { 14 | const saveSurveyResultRepositorySpy = new SaveSurveyResultRepositorySpy() 15 | const loadSurveyResultRepositorySpy = new LoadSurveyResultRepositorySpy() 16 | const sut = new DbSaveSurveyResult(saveSurveyResultRepositorySpy, loadSurveyResultRepositorySpy) 17 | return { 18 | sut, 19 | saveSurveyResultRepositorySpy, 20 | loadSurveyResultRepositorySpy 21 | } 22 | } 23 | 24 | describe('DbSaveSurveyResult Usecase', () => { 25 | beforeAll(() => { 26 | MockDate.set(new Date()) 27 | }) 28 | 29 | afterAll(() => { 30 | MockDate.reset() 31 | }) 32 | 33 | test('Should call SaveSurveyResultRepository with correct values', async () => { 34 | const { sut, saveSurveyResultRepositorySpy } = makeSut() 35 | const surveyResultData = mockSaveSurveyResultParams() 36 | await sut.save(surveyResultData) 37 | expect(saveSurveyResultRepositorySpy.params).toEqual(surveyResultData) 38 | }) 39 | 40 | test('Should throw if SaveSurveyResultRepository throws', async () => { 41 | const { sut, saveSurveyResultRepositorySpy } = makeSut() 42 | jest.spyOn(saveSurveyResultRepositorySpy, 'save').mockImplementationOnce(throwError) 43 | const promise = sut.save(mockSaveSurveyResultParams()) 44 | await expect(promise).rejects.toThrow() 45 | }) 46 | 47 | test('Should call LoadSurveyResultRepository with correct values', async () => { 48 | const { sut, loadSurveyResultRepositorySpy } = makeSut() 49 | const surveyResultData = mockSaveSurveyResultParams() 50 | await sut.save(surveyResultData) 51 | expect(loadSurveyResultRepositorySpy.surveyId).toBe(surveyResultData.surveyId) 52 | }) 53 | 54 | test('Should throw if LoadSurveyResultRepository throws', async () => { 55 | const { sut, loadSurveyResultRepositorySpy } = makeSut() 56 | jest.spyOn(loadSurveyResultRepositorySpy, 'loadBySurveyId').mockImplementationOnce(throwError) 57 | const promise = sut.save(mockSaveSurveyResultParams()) 58 | await expect(promise).rejects.toThrow() 59 | }) 60 | 61 | test('Should return SurveyResult on success', async () => { 62 | const { sut, loadSurveyResultRepositorySpy } = makeSut() 63 | const surveyResult = await sut.save(mockSaveSurveyResultParams()) 64 | expect(surveyResult).toEqual(loadSurveyResultRepositorySpy.result) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /tests/domain/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mock-account' 2 | export * from './mock-survey' 3 | export * from './mock-survey-result' 4 | export * from './test-helpers' 5 | -------------------------------------------------------------------------------- /tests/domain/mocks/mock-account.ts: -------------------------------------------------------------------------------- 1 | import { AddAccount, Authentication } from '@/domain/usecases' 2 | 3 | import faker from 'faker' 4 | 5 | export const mockAddAccountParams = (): AddAccount.Params => ({ 6 | name: faker.name.findName(), 7 | email: faker.internet.email(), 8 | password: faker.internet.password() 9 | }) 10 | 11 | export const mockAuthenticationParams = (): Authentication.Params => ({ 12 | email: faker.internet.email(), 13 | password: faker.internet.password() 14 | }) 15 | -------------------------------------------------------------------------------- /tests/domain/mocks/mock-survey-result.ts: -------------------------------------------------------------------------------- 1 | import { SurveyResultModel } from '@/domain/models' 2 | import { SaveSurveyResult } from '@/domain/usecases' 3 | 4 | import faker from 'faker' 5 | 6 | export const mockSaveSurveyResultParams = (): SaveSurveyResult.Params => ({ 7 | accountId: faker.datatype.uuid(), 8 | surveyId: faker.datatype.uuid(), 9 | answer: faker.random.word(), 10 | date: faker.date.recent() 11 | }) 12 | 13 | export const mockSurveyResultModel = (): SurveyResultModel => ({ 14 | surveyId: faker.datatype.uuid(), 15 | question: faker.random.words(), 16 | answers: [{ 17 | answer: faker.random.word(), 18 | count: faker.datatype.number({ min: 0, max: 1000 }), 19 | percent: faker.datatype.number({ min: 0, max: 100 }), 20 | isCurrentAccountAnswer: faker.datatype.boolean() 21 | }, { 22 | answer: faker.random.word(), 23 | image: faker.image.imageUrl(), 24 | count: faker.datatype.number({ min: 0, max: 1000 }), 25 | percent: faker.datatype.number({ min: 0, max: 100 }), 26 | isCurrentAccountAnswer: faker.datatype.boolean() 27 | }], 28 | date: faker.date.recent() 29 | }) 30 | 31 | export const mockEmptySurveyResultModel = (): SurveyResultModel => ({ 32 | surveyId: faker.datatype.uuid(), 33 | question: faker.random.words(), 34 | answers: [{ 35 | answer: faker.random.word(), 36 | count: 0, 37 | percent: 0, 38 | isCurrentAccountAnswer: false 39 | }, { 40 | answer: faker.random.word(), 41 | image: faker.image.imageUrl(), 42 | count: 0, 43 | percent: 0, 44 | isCurrentAccountAnswer: false 45 | }], 46 | date: faker.date.recent() 47 | }) 48 | -------------------------------------------------------------------------------- /tests/domain/mocks/mock-survey.ts: -------------------------------------------------------------------------------- 1 | import { SurveyModel } from '@/domain/models' 2 | import { AddSurvey } from '@/domain/usecases' 3 | 4 | import faker from 'faker' 5 | 6 | export const mockSurveyModel = (): SurveyModel => { 7 | return { 8 | id: faker.datatype.uuid(), 9 | question: faker.random.words(), 10 | answers: [{ 11 | answer: faker.random.word() 12 | }, { 13 | answer: faker.random.word(), 14 | image: faker.image.imageUrl() 15 | }], 16 | date: faker.date.recent() 17 | } 18 | } 19 | 20 | export const mockSurveyModels = (): SurveyModel[] => [ 21 | mockSurveyModel(), 22 | mockSurveyModel() 23 | ] 24 | 25 | export const mockAddSurveyParams = (): AddSurvey.Params => ({ 26 | question: faker.random.words(), 27 | answers: [{ 28 | image: faker.image.imageUrl(), 29 | answer: faker.random.word() 30 | }, { 31 | answer: faker.random.word() 32 | }], 33 | date: faker.date.recent() 34 | }) 35 | -------------------------------------------------------------------------------- /tests/domain/mocks/test-helpers.ts: -------------------------------------------------------------------------------- 1 | export const throwError = (): never => { 2 | throw new Error() 3 | } 4 | -------------------------------------------------------------------------------- /tests/infra/cryptography/bcrypt-adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { BcryptAdapter } from '@/infra/cryptography' 2 | import { throwError } from '@/tests/domain/mocks' 3 | 4 | import bcrypt from 'bcrypt' 5 | 6 | jest.mock('bcrypt', () => ({ 7 | async hash (): Promise { 8 | return 'hash' 9 | }, 10 | 11 | async compare (): Promise { 12 | return true 13 | } 14 | })) 15 | 16 | const salt = 12 17 | const makeSut = (): BcryptAdapter => { 18 | return new BcryptAdapter(salt) 19 | } 20 | 21 | describe('Bcrypt Adapter', () => { 22 | describe('hash()', () => { 23 | test('Should call hash with correct values', async () => { 24 | const sut = makeSut() 25 | const hashSpy = jest.spyOn(bcrypt, 'hash') 26 | await sut.hash('any_value') 27 | expect(hashSpy).toHaveBeenCalledWith('any_value', salt) 28 | }) 29 | 30 | test('Should return a valid hash on hash success', async () => { 31 | const sut = makeSut() 32 | const hash = await sut.hash('any_value') 33 | expect(hash).toBe('hash') 34 | }) 35 | 36 | test('Should throw if hash throws', async () => { 37 | const sut = makeSut() 38 | jest.spyOn(bcrypt, 'hash').mockImplementationOnce(throwError) 39 | const promise = sut.hash('any_value') 40 | await expect(promise).rejects.toThrow() 41 | }) 42 | }) 43 | 44 | describe('compare()', () => { 45 | test('Should call compare with correct values', async () => { 46 | const sut = makeSut() 47 | const compareSpy = jest.spyOn(bcrypt, 'compare') 48 | await sut.compare('any_value', 'any_hash') 49 | expect(compareSpy).toHaveBeenCalledWith('any_value', 'any_hash') 50 | }) 51 | 52 | test('Should return true when compare succeeds', async () => { 53 | const sut = makeSut() 54 | const isValid = await sut.compare('any_value', 'any_hash') 55 | expect(isValid).toBe(true) 56 | }) 57 | 58 | test('Should return false when compare fails', async () => { 59 | const sut = makeSut() 60 | jest.spyOn(bcrypt, 'compare').mockImplementationOnce(() => false) 61 | const isValid = await sut.compare('any_value', 'any_hash') 62 | expect(isValid).toBe(false) 63 | }) 64 | 65 | test('Should throw if compare throws', async () => { 66 | const sut = makeSut() 67 | jest.spyOn(bcrypt, 'compare').mockImplementationOnce(throwError) 68 | const promise = sut.compare('any_value', 'any_hash') 69 | await expect(promise).rejects.toThrow() 70 | }) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /tests/infra/cryptography/jwt-adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { JwtAdapter } from '@/infra/cryptography' 2 | import { throwError } from '@/tests/domain/mocks' 3 | 4 | import jwt from 'jsonwebtoken' 5 | 6 | jest.mock('jsonwebtoken', () => ({ 7 | async sign (): Promise { 8 | return 'any_token' 9 | }, 10 | 11 | async verify (): Promise { 12 | return 'any_value' 13 | } 14 | })) 15 | 16 | const makeSut = (): JwtAdapter => { 17 | return new JwtAdapter('secret') 18 | } 19 | 20 | describe('Jwt Adapter', () => { 21 | describe('sign()', () => { 22 | test('Should call sign with correct values', async () => { 23 | const sut = makeSut() 24 | const signSpy = jest.spyOn(jwt, 'sign') 25 | await sut.encrypt('any_id') 26 | expect(signSpy).toHaveBeenCalledWith({ id: 'any_id' }, 'secret') 27 | }) 28 | 29 | test('Should return a token on sign success', async () => { 30 | const sut = makeSut() 31 | const accessToken = await sut.encrypt('any_id') 32 | expect(accessToken).toBe('any_token') 33 | }) 34 | 35 | test('Should throw if sign throws', async () => { 36 | const sut = makeSut() 37 | jest.spyOn(jwt, 'sign').mockImplementationOnce(throwError) 38 | const promise = sut.encrypt('any_id') 39 | await expect(promise).rejects.toThrow() 40 | }) 41 | }) 42 | 43 | describe('verify()', () => { 44 | test('Should call verify with correct values', async () => { 45 | const sut = makeSut() 46 | const verifySpy = jest.spyOn(jwt, 'verify') 47 | await sut.decrypt('any_token') 48 | expect(verifySpy).toHaveBeenCalledWith('any_token', 'secret') 49 | }) 50 | 51 | test('Should return a value on verify success', async () => { 52 | const sut = makeSut() 53 | const value = await sut.decrypt('any_token') 54 | expect(value).toBe('any_value') 55 | }) 56 | 57 | test('Should throw if verify throws', async () => { 58 | const sut = makeSut() 59 | jest.spyOn(jwt, 'verify').mockImplementationOnce(throwError) 60 | const promise = sut.decrypt('any_token') 61 | await expect(promise).rejects.toThrow() 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /tests/infra/db/mongodb/log-mongo-repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { LogMongoRepository, MongoHelper } from '@/infra/db' 2 | 3 | import { Collection } from 'mongodb' 4 | import faker from 'faker' 5 | 6 | const makeSut = (): LogMongoRepository => { 7 | return new LogMongoRepository() 8 | } 9 | 10 | let errorCollection: Collection 11 | 12 | describe('LogMongoRepository', () => { 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 = 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 | -------------------------------------------------------------------------------- /tests/infra/db/mongodb/survey-mongo-repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { SurveyMongoRepository, MongoHelper } from '@/infra/db' 2 | import { mockAddSurveyParams, mockAddAccountParams } from '@/tests/domain/mocks' 3 | 4 | import { Collection, ObjectId } from 'mongodb' 5 | import FakeObjectId from 'bson-objectid' 6 | 7 | let surveyCollection: Collection 8 | let surveyResultCollection: Collection 9 | let accountCollection: Collection 10 | 11 | const mockAccountId = async (): Promise => { 12 | const res = await accountCollection.insertOne(mockAddAccountParams()) 13 | return res.insertedId.toHexString() 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 = MongoHelper.getCollection('surveys') 31 | await surveyCollection.deleteMany({}) 32 | surveyResultCollection = MongoHelper.getCollection('surveyResults') 33 | await surveyResultCollection.deleteMany({}) 34 | accountCollection = 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 accountId = await mockAccountId() 50 | const addSurveyModels = [mockAddSurveyParams(), mockAddSurveyParams()] 51 | const result = await surveyCollection.insertMany(addSurveyModels) 52 | const survey = await surveyCollection.findOne({ _id: result.insertedIds[0] }) 53 | await surveyResultCollection.insertOne({ 54 | surveyId: survey._id, 55 | accountId: new ObjectId(accountId), 56 | answer: survey.answers[0].answer, 57 | date: new Date() 58 | }) 59 | const sut = makeSut() 60 | const surveys = await sut.loadAll(accountId) 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 accountId = await mockAccountId() 71 | const sut = makeSut() 72 | const surveys = await sut.loadAll(accountId) 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.insertedId.toHexString()) 82 | expect(survey).toBeTruthy() 83 | expect(survey.id).toBeTruthy() 84 | }) 85 | 86 | test('Should return null if survey does not exists', async () => { 87 | const sut = makeSut() 88 | const survey = await sut.loadById(new FakeObjectId().toHexString()) 89 | expect(survey).toBeFalsy() 90 | }) 91 | }) 92 | 93 | describe('loadAnswers()', () => { 94 | test('Should load answers on success', async () => { 95 | const res = await surveyCollection.insertOne(mockAddSurveyParams()) 96 | const survey = await surveyCollection.findOne({ _id: res.insertedId }) 97 | const sut = makeSut() 98 | const answers = await sut.loadAnswers(survey._id.toHexString()) 99 | expect(answers).toEqual([survey.answers[0].answer, survey.answers[1].answer]) 100 | }) 101 | 102 | test('Should return empty array if survey does not exists', async () => { 103 | const sut = makeSut() 104 | const answers = await sut.loadAnswers(new FakeObjectId().toHexString()) 105 | expect(answers).toEqual([]) 106 | }) 107 | }) 108 | 109 | describe('checkById()', () => { 110 | test('Should return true if survey exists', async () => { 111 | const res = await surveyCollection.insertOne(mockAddSurveyParams()) 112 | const sut = makeSut() 113 | const exists = await sut.checkById(res.insertedId.toHexString()) 114 | expect(exists).toBe(true) 115 | }) 116 | 117 | test('Should return false if survey exists', async () => { 118 | const sut = makeSut() 119 | const exists = await sut.checkById(new FakeObjectId().toHexString()) 120 | expect(exists).toBe(false) 121 | }) 122 | }) 123 | }) 124 | -------------------------------------------------------------------------------- /tests/infra/validators/email-validator-adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmailValidatorAdapter } from '@/infra/validators' 2 | 3 | import validator from 'validator' 4 | 5 | jest.mock('validator', () => ({ 6 | isEmail (): boolean { 7 | return true 8 | } 9 | })) 10 | 11 | const makeSut = (): EmailValidatorAdapter => { 12 | return new EmailValidatorAdapter() 13 | } 14 | 15 | describe('EmailValidatorAdapter', () => { 16 | test('Should return false if validator returns false', () => { 17 | const sut = makeSut() 18 | jest.spyOn(validator, 'isEmail').mockReturnValueOnce(false) 19 | const isValid = sut.isValid('invalid_email@mail.com') 20 | expect(isValid).toBe(false) 21 | }) 22 | 23 | test('Should return true if validator returns true', () => { 24 | const sut = makeSut() 25 | const isValid = sut.isValid('valid_email@mail.com') 26 | expect(isValid).toBe(true) 27 | }) 28 | 29 | test('Should call validator with correct email', () => { 30 | const sut = makeSut() 31 | const isEmailSpy = jest.spyOn(validator, 'isEmail') 32 | sut.isValid('any_email@mail.com') 33 | expect(isEmailSpy).toHaveBeenCalledWith('any_email@mail.com') 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /tests/main/decorators/log-controller-decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { LogControllerDecorator } from '@/main/decorators' 2 | import { Controller, HttpResponse } from '@/presentation/protocols' 3 | import { serverError, ok } from '@/presentation/helpers' 4 | import { LogErrorRepositorySpy } from '@/tests/data/mocks' 5 | 6 | import faker from 'faker' 7 | 8 | class ControllerSpy implements Controller { 9 | httpResponse = ok(faker.datatype.uuid()) 10 | request: any 11 | 12 | async handle (request: any): Promise { 13 | this.request = request 14 | return this.httpResponse 15 | } 16 | } 17 | 18 | const mockServerError = (): HttpResponse => { 19 | const fakeError = new Error() 20 | fakeError.stack = 'any_stack' 21 | return serverError(fakeError) 22 | } 23 | 24 | type SutTypes = { 25 | sut: LogControllerDecorator 26 | controllerSpy: ControllerSpy 27 | logErrorRepositorySpy: LogErrorRepositorySpy 28 | } 29 | 30 | const makeSut = (): SutTypes => { 31 | const controllerSpy = new ControllerSpy() 32 | const logErrorRepositorySpy = new LogErrorRepositorySpy() 33 | const sut = new LogControllerDecorator(controllerSpy, logErrorRepositorySpy) 34 | return { 35 | sut, 36 | controllerSpy, 37 | logErrorRepositorySpy 38 | } 39 | } 40 | 41 | describe('LogController Decorator', () => { 42 | test('Should call controller handle', async () => { 43 | const { sut, controllerSpy } = makeSut() 44 | const request = faker.lorem.sentence() 45 | await sut.handle(request) 46 | expect(controllerSpy.request).toEqual(request) 47 | }) 48 | 49 | test('Should return the same result of the controller', async () => { 50 | const { sut, controllerSpy } = makeSut() 51 | const httpResponse = await sut.handle(faker.lorem.sentence()) 52 | expect(httpResponse).toEqual(controllerSpy.httpResponse) 53 | }) 54 | 55 | test('Should call LogErrorRepository with correct error if controller returns a server error', async () => { 56 | const { sut, controllerSpy, logErrorRepositorySpy } = makeSut() 57 | const serverError = mockServerError() 58 | controllerSpy.httpResponse = serverError 59 | await sut.handle(faker.lorem.sentence()) 60 | expect(logErrorRepositorySpy.stack).toBe(serverError.body.stack) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /tests/main/factories/add-survey-validation-factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { makeAddSurveyValidation } from '@/main/factories' 2 | import { ValidationComposite, RequiredFieldValidation } from '@/validation/validators' 3 | import { Validation } from '@/presentation/protocols' 4 | 5 | jest.mock('@/validation/validators/validation-composite') 6 | 7 | describe('AddSurveyValidation Factory', () => { 8 | test('Should call ValidationComposite with all 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 | -------------------------------------------------------------------------------- /tests/main/factories/login-validation-factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { makeLoginValidation } from '@/main/factories' 2 | import { ValidationComposite, RequiredFieldValidation, EmailValidation } from '@/validation/validators' 3 | import { Validation } from '@/presentation/protocols' 4 | import { EmailValidatorAdapter } from '@/infra/validators' 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 | -------------------------------------------------------------------------------- /tests/main/factories/signup-validation-factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { makeSignUpValidation } from '@/main/factories' 2 | import { ValidationComposite, RequiredFieldValidation, CompareFieldsValidation, EmailValidation } from '@/validation/validators' 3 | import { Validation } from '@/presentation/protocols' 4 | import { EmailValidatorAdapter } from '@/infra/validators' 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 | -------------------------------------------------------------------------------- /tests/main/graphql/login.test.ts: -------------------------------------------------------------------------------- 1 | import { MongoHelper } from '@/infra/db' 2 | import { setupApp } from '@/main/config/app' 3 | 4 | import { Collection } from 'mongodb' 5 | import { hash } from 'bcrypt' 6 | import { Express } from 'express' 7 | import request from 'supertest' 8 | 9 | let accountCollection: Collection 10 | let app: Express 11 | 12 | describe('Login GraphQL', () => { 13 | beforeAll(async () => { 14 | app = await setupApp() 15 | await MongoHelper.connect(process.env.MONGO_URL) 16 | }) 17 | 18 | afterAll(async () => { 19 | await MongoHelper.disconnect() 20 | }) 21 | 22 | beforeEach(async () => { 23 | accountCollection = MongoHelper.getCollection('accounts') 24 | await accountCollection.deleteMany({}) 25 | }) 26 | 27 | describe('Login Query', () => { 28 | const query = `query { 29 | login (email: "rodrigo.manguinho@gmail.com", password: "123") { 30 | accessToken 31 | name 32 | } 33 | }` 34 | 35 | test('Should return an Account on valid credentials', async () => { 36 | const password = await hash('123', 12) 37 | await accountCollection.insertOne({ 38 | name: 'Rodrigo', 39 | email: 'rodrigo.manguinho@gmail.com', 40 | password 41 | }) 42 | const res = await request(app) 43 | .post('/graphql') 44 | .send({ query }) 45 | expect(res.status).toBe(200) 46 | expect(res.body.data.login.accessToken).toBeTruthy() 47 | expect(res.body.data.login.name).toBe('Rodrigo') 48 | }) 49 | 50 | test('Should return UnauthorizedError on invalid credentials', async () => { 51 | const res = await request(app) 52 | .post('/graphql') 53 | .send({ query }) 54 | expect(res.status).toBe(401) 55 | expect(res.body.data).toBeFalsy() 56 | expect(res.body.errors[0].message).toBe('Unauthorized') 57 | }) 58 | }) 59 | 60 | describe('SignUp Mutation', () => { 61 | const query = `mutation { 62 | signUp (name: "Rodrigo", email: "rodrigo.manguinho@gmail.com", password: "123", passwordConfirmation: "123") { 63 | accessToken 64 | name 65 | } 66 | }` 67 | 68 | test('Should return an Account on valid data', async () => { 69 | const res = await request(app) 70 | .post('/graphql') 71 | .send({ query }) 72 | expect(res.status).toBe(200) 73 | expect(res.body.data.signUp.accessToken).toBeTruthy() 74 | expect(res.body.data.signUp.name).toBe('Rodrigo') 75 | }) 76 | 77 | test('Should return EmailInUseError on invalid data', async () => { 78 | const password = await hash('123', 12) 79 | await accountCollection.insertOne({ 80 | name: 'Rodrigo', 81 | email: 'rodrigo.manguinho@gmail.com', 82 | password 83 | }) 84 | const res = await request(app) 85 | .post('/graphql') 86 | .send({ query }) 87 | expect(res.status).toBe(403) 88 | expect(res.body.data).toBeFalsy() 89 | expect(res.body.errors[0].message).toBe('The received email is already in use') 90 | }) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /tests/main/graphql/survey.test.ts: -------------------------------------------------------------------------------- 1 | import { MongoHelper } from '@/infra/db' 2 | import env from '@/main/config/env' 3 | import { setupApp } from '@/main/config/app' 4 | 5 | import { Collection } from 'mongodb' 6 | import { sign } from 'jsonwebtoken' 7 | import { Express } from 'express' 8 | import request from 'supertest' 9 | 10 | let surveyCollection: Collection 11 | let accountCollection: Collection 12 | let app: Express 13 | 14 | const mockAccessToken = async (): Promise => { 15 | const res = await accountCollection.insertOne({ 16 | name: 'Rodrigo', 17 | email: 'rodrigo.manguinho@gmail.com', 18 | password: '123', 19 | role: 'admin' 20 | }) 21 | const id = res.insertedId.toHexString() 22 | const accessToken = sign({ id }, env.jwtSecret) 23 | await accountCollection.updateOne({ 24 | _id: res.insertedId 25 | }, { 26 | $set: { 27 | accessToken 28 | } 29 | }) 30 | return accessToken 31 | } 32 | 33 | describe('Survey GraphQL', () => { 34 | beforeAll(async () => { 35 | app = await setupApp() 36 | await MongoHelper.connect(process.env.MONGO_URL) 37 | }) 38 | 39 | afterAll(async () => { 40 | await MongoHelper.disconnect() 41 | }) 42 | 43 | beforeEach(async () => { 44 | surveyCollection = MongoHelper.getCollection('surveys') 45 | await surveyCollection.deleteMany({}) 46 | accountCollection = MongoHelper.getCollection('accounts') 47 | await accountCollection.deleteMany({}) 48 | }) 49 | 50 | describe('Surveys Query', () => { 51 | const query = `query { 52 | surveys { 53 | id 54 | question 55 | answers { 56 | image 57 | answer 58 | } 59 | date 60 | didAnswer 61 | } 62 | }` 63 | 64 | test('Should return Surveys', async () => { 65 | const accessToken = await mockAccessToken() 66 | const now = new Date() 67 | await surveyCollection.insertOne({ 68 | question: 'Question', 69 | answers: [{ 70 | answer: 'Answer 1', 71 | image: 'http://image-name.com' 72 | }, { 73 | answer: 'Answer 2' 74 | }], 75 | date: now 76 | }) 77 | const res = await request(app) 78 | .post('/graphql') 79 | .set('x-access-token', accessToken) 80 | .send({ query }) 81 | expect(res.status).toBe(200) 82 | expect(res.body.data.surveys.length).toBe(1) 83 | expect(res.body.data.surveys[0].id).toBeTruthy() 84 | expect(res.body.data.surveys[0].question).toBe('Question') 85 | expect(res.body.data.surveys[0].date).toBe(now.toISOString()) 86 | expect(res.body.data.surveys[0].didAnswer).toBe(false) 87 | expect(res.body.data.surveys[0].answers).toEqual([{ 88 | answer: 'Answer 1', 89 | image: 'http://image-name.com' 90 | }, { 91 | answer: 'Answer 2', 92 | image: null 93 | }]) 94 | }) 95 | 96 | test('Should return AccessDeniedError if no token is provided', async () => { 97 | await surveyCollection.insertOne({ 98 | question: 'Question', 99 | answers: [{ 100 | answer: 'Answer 1', 101 | image: 'http://image-name.com' 102 | }, { 103 | answer: 'Answer 2' 104 | }], 105 | date: new Date() 106 | }) 107 | const res = await request(app) 108 | .post('/graphql') 109 | .send({ query }) 110 | expect(res.status).toBe(403) 111 | expect(res.body.data).toBeFalsy() 112 | expect(res.body.errors[0].message).toBe('Access denied') 113 | }) 114 | }) 115 | }) 116 | -------------------------------------------------------------------------------- /tests/main/middlewares/body-parser.test.ts: -------------------------------------------------------------------------------- 1 | import { setupApp } from '@/main/config/app' 2 | 3 | import { Express } from 'express' 4 | import request from 'supertest' 5 | 6 | let app: Express 7 | 8 | describe('Body Parser Middleware', () => { 9 | beforeAll(async () => { 10 | app = await setupApp() 11 | }) 12 | 13 | test('Should parse body as json', async () => { 14 | app.post('/test_body_parser', (req, res) => { 15 | res.send(req.body) 16 | }) 17 | await request(app) 18 | .post('/test_body_parser') 19 | .send({ name: 'Rodrigo' }) 20 | .expect({ name: 'Rodrigo' }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /tests/main/middlewares/content-type.test.ts: -------------------------------------------------------------------------------- 1 | import { setupApp } from '@/main/config/app' 2 | 3 | import { Express } from 'express' 4 | import request from 'supertest' 5 | 6 | let app: Express 7 | 8 | describe('Content Type Middleware', () => { 9 | beforeAll(async () => { 10 | app = await setupApp() 11 | }) 12 | 13 | test('Should return default content type as json', async () => { 14 | app.get('/test_content_type', (req, res) => { 15 | res.send('') 16 | }) 17 | await request(app) 18 | .get('/test_content_type') 19 | .expect('content-type', /json/) 20 | }) 21 | 22 | test('Should return xml content type when forced', async () => { 23 | app.get('/test_content_type_xml', (req, res) => { 24 | res.type('xml') 25 | res.send('') 26 | }) 27 | await request(app) 28 | .get('/test_content_type_xml') 29 | .expect('content-type', /xml/) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /tests/main/middlewares/cors.test.ts: -------------------------------------------------------------------------------- 1 | import { setupApp } from '@/main/config/app' 2 | 3 | import { Express } from 'express' 4 | import request from 'supertest' 5 | 6 | let app: Express 7 | 8 | describe('CORS Middleware', () => { 9 | beforeAll(async () => { 10 | app = await setupApp() 11 | }) 12 | 13 | test('Should enable CORS', async () => { 14 | app.get('/test_cors', (req, res) => { 15 | res.send() 16 | }) 17 | await request(app) 18 | .get('/test_cors') 19 | .expect('access-control-allow-origin', '*') 20 | .expect('access-control-allow-methods', '*') 21 | .expect('access-control-allow-headers', '*') 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /tests/main/middlewares/no-cache.test.ts: -------------------------------------------------------------------------------- 1 | import { setupApp } from '@/main/config/app' 2 | import { noCache } from '@/main/middlewares' 3 | 4 | import { Express } from 'express' 5 | import request from 'supertest' 6 | 7 | let app: Express 8 | 9 | describe('NoCache Middleware', () => { 10 | beforeAll(async () => { 11 | app = await setupApp() 12 | }) 13 | 14 | test('Should disable cache', async () => { 15 | app.get('/test_no_cache', noCache, (req, res) => { 16 | res.send() 17 | }) 18 | await request(app) 19 | .get('/test_no_cache') 20 | .expect('cache-control', 'no-store, no-cache, must-revalidate, proxy-revalidate') 21 | .expect('pragma', 'no-cache') 22 | .expect('expires', '0') 23 | .expect('surrogate-control', 'no-store') 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /tests/main/routes/login-routes.test.ts: -------------------------------------------------------------------------------- 1 | import { MongoHelper } from '@/infra/db' 2 | import { setupApp } from '@/main/config/app' 3 | 4 | import { Collection } from 'mongodb' 5 | import { hash } from 'bcrypt' 6 | import { Express } from 'express' 7 | import request from 'supertest' 8 | 9 | let accountCollection: Collection 10 | let app: Express 11 | 12 | describe('Login Routes', () => { 13 | beforeAll(async () => { 14 | app = await setupApp() 15 | await MongoHelper.connect(process.env.MONGO_URL) 16 | }) 17 | 18 | afterAll(async () => { 19 | await MongoHelper.disconnect() 20 | }) 21 | 22 | beforeEach(async () => { 23 | accountCollection = MongoHelper.getCollection('accounts') 24 | await accountCollection.deleteMany({}) 25 | }) 26 | 27 | describe('POST /signup', () => { 28 | test('Should return 200 on signup', async () => { 29 | await request(app) 30 | .post('/api/signup') 31 | .send({ 32 | name: 'Rodrigo', 33 | email: 'rodrigo.manguinho@gmail.com', 34 | password: '123', 35 | passwordConfirmation: '123' 36 | }) 37 | .expect(200) 38 | await request(app) 39 | .post('/api/signup') 40 | .send({ 41 | name: 'Rodrigo', 42 | email: 'rodrigo.manguinho@gmail.com', 43 | password: '123', 44 | passwordConfirmation: '123' 45 | }) 46 | .expect(403) 47 | }) 48 | }) 49 | 50 | describe('POST /login', () => { 51 | test('Should return 200 on login', async () => { 52 | const password = await hash('123', 12) 53 | await accountCollection.insertOne({ 54 | name: 'Rodrigo', 55 | email: 'rodrigo.manguinho@gmail.com', 56 | password 57 | }) 58 | await request(app) 59 | .post('/api/login') 60 | .send({ 61 | email: 'rodrigo.manguinho@gmail.com', 62 | password: '123' 63 | }) 64 | .expect(200) 65 | }) 66 | 67 | test('Should return 401 on login', async () => { 68 | await request(app) 69 | .post('/api/login') 70 | .send({ 71 | email: 'rodrigo.manguinho@gmail.com', 72 | password: '123' 73 | }) 74 | .expect(401) 75 | }) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /tests/main/routes/survey-result-routes.test.ts: -------------------------------------------------------------------------------- 1 | import env from '@/main/config/env' 2 | import { MongoHelper } from '@/infra/db' 3 | import { setupApp } from '@/main/config/app' 4 | 5 | import { sign } from 'jsonwebtoken' 6 | import { Collection } from 'mongodb' 7 | import { Express } from 'express' 8 | import request from 'supertest' 9 | 10 | let surveyCollection: Collection 11 | let accountCollection: Collection 12 | let app: Express 13 | 14 | const mockAccessToken = async (): Promise => { 15 | const res = await accountCollection.insertOne({ 16 | name: 'Rodrigo', 17 | email: 'rodrigo.manguinho@gmail.com', 18 | password: '123' 19 | }) 20 | const id = res.insertedId.toHexString() 21 | const accessToken = sign({ id }, env.jwtSecret) 22 | await accountCollection.updateOne({ 23 | _id: res.insertedId 24 | }, { 25 | $set: { 26 | accessToken 27 | } 28 | }) 29 | return accessToken 30 | } 31 | 32 | describe('Survey Routes', () => { 33 | beforeAll(async () => { 34 | app = await setupApp() 35 | await MongoHelper.connect(process.env.MONGO_URL) 36 | }) 37 | 38 | afterAll(async () => { 39 | await MongoHelper.disconnect() 40 | }) 41 | 42 | beforeEach(async () => { 43 | surveyCollection = MongoHelper.getCollection('surveys') 44 | await surveyCollection.deleteMany({}) 45 | accountCollection = MongoHelper.getCollection('accounts') 46 | await accountCollection.deleteMany({}) 47 | }) 48 | 49 | describe('PUT /surveys/:surveyId/results', () => { 50 | test('Should return 403 on save survey result without accessToken', async () => { 51 | await request(app) 52 | .put('/api/surveys/any_id/results') 53 | .send({ 54 | answer: 'any_answer' 55 | }) 56 | .expect(403) 57 | }) 58 | 59 | test('Should return 200 on save survey result with accessToken', async () => { 60 | const accessToken = await mockAccessToken() 61 | const res = await surveyCollection.insertOne({ 62 | question: 'Question', 63 | answers: [{ 64 | answer: 'Answer 1', 65 | image: 'http://image-name.com' 66 | }, { 67 | answer: 'Answer 2' 68 | }], 69 | date: new Date() 70 | }) 71 | await request(app) 72 | .put(`/api/surveys/${res.insertedId.toHexString()}/results`) 73 | .set('x-access-token', accessToken) 74 | .send({ 75 | answer: 'Answer 1' 76 | }) 77 | .expect(200) 78 | }) 79 | }) 80 | 81 | describe('GET /surveys/:surveyId/results', () => { 82 | test('Should return 403 on load survey result without accessToken', async () => { 83 | await request(app) 84 | .get('/api/surveys/any_id/results') 85 | .expect(403) 86 | }) 87 | 88 | test('Should return 200 on load survey result with accessToken', async () => { 89 | const accessToken = await mockAccessToken() 90 | const res = await surveyCollection.insertOne({ 91 | question: 'Question', 92 | answers: [{ 93 | answer: 'Answer 1', 94 | image: 'http://image-name.com' 95 | }, { 96 | answer: 'Answer 2' 97 | }], 98 | date: new Date() 99 | }) 100 | await request(app) 101 | .get(`/api/surveys/${res.insertedId.toHexString()}/results`) 102 | .set('x-access-token', accessToken) 103 | .expect(200) 104 | }) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /tests/main/routes/survey-routes.test.ts: -------------------------------------------------------------------------------- 1 | import env from '@/main/config/env' 2 | import { MongoHelper } from '@/infra/db' 3 | import { setupApp } from '@/main/config/app' 4 | 5 | import { sign } from 'jsonwebtoken' 6 | import { Collection } from 'mongodb' 7 | import { Express } from 'express' 8 | import request from 'supertest' 9 | 10 | let surveyCollection: Collection 11 | let accountCollection: Collection 12 | let app: Express 13 | 14 | const mockAccessToken = async (): Promise => { 15 | const res = await accountCollection.insertOne({ 16 | name: 'Rodrigo', 17 | email: 'rodrigo.manguinho@gmail.com', 18 | password: '123', 19 | role: 'admin' 20 | }) 21 | const id = res.insertedId.toHexString() 22 | const accessToken = sign({ id }, env.jwtSecret) 23 | await accountCollection.updateOne({ 24 | _id: res.insertedId 25 | }, { 26 | $set: { 27 | accessToken 28 | } 29 | }) 30 | return accessToken 31 | } 32 | 33 | describe('Survey Routes', () => { 34 | beforeAll(async () => { 35 | app = await setupApp() 36 | await MongoHelper.connect(process.env.MONGO_URL) 37 | }) 38 | 39 | afterAll(async () => { 40 | await MongoHelper.disconnect() 41 | }) 42 | 43 | beforeEach(async () => { 44 | surveyCollection = MongoHelper.getCollection('surveys') 45 | await surveyCollection.deleteMany({}) 46 | accountCollection = MongoHelper.getCollection('accounts') 47 | await accountCollection.deleteMany({}) 48 | }) 49 | 50 | describe('POST /surveys', () => { 51 | test('Should return 403 on add survey without accessToken', async () => { 52 | await request(app) 53 | .post('/api/surveys') 54 | .send({ 55 | question: 'Question', 56 | answers: [{ 57 | answer: 'Answer 1', 58 | image: 'http://image-name.com' 59 | }, { 60 | answer: 'Answer 2' 61 | }] 62 | }) 63 | .expect(403) 64 | }) 65 | 66 | test('Should return 204 on add survey with valid accessToken', async () => { 67 | const accessToken = await mockAccessToken() 68 | await request(app) 69 | .post('/api/surveys') 70 | .set('x-access-token', accessToken) 71 | .send({ 72 | question: 'Question', 73 | answers: [{ 74 | answer: 'Answer 1', 75 | image: 'http://image-name.com' 76 | }, { 77 | answer: 'Answer 2' 78 | }] 79 | }) 80 | .expect(204) 81 | }) 82 | }) 83 | 84 | describe('GET /surveys', () => { 85 | test('Should return 403 on load surveys without accessToken', async () => { 86 | await request(app) 87 | .get('/api/surveys') 88 | .expect(403) 89 | }) 90 | 91 | test('Should return 204 on load surveys with valid accessToken', async () => { 92 | const accessToken = await mockAccessToken() 93 | await request(app) 94 | .get('/api/surveys') 95 | .set('x-access-token', accessToken) 96 | .expect(204) 97 | }) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /tests/presentation/controllers/add-survey-controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { AddSurveyController } from '@/presentation/controllers' 2 | import { badRequest, serverError, noContent } from '@/presentation/helpers' 3 | import { ValidationSpy, AddSurveySpy } from '@/tests/presentation/mocks' 4 | import { throwError } from '@/tests/domain/mocks' 5 | 6 | import MockDate from 'mockdate' 7 | import faker from 'faker' 8 | 9 | const mockRequest = (): AddSurveyController.Request => ({ 10 | question: faker.random.words(), 11 | answers: [{ 12 | image: faker.image.imageUrl(), 13 | answer: faker.random.word() 14 | }] 15 | }) 16 | 17 | type SutTypes = { 18 | sut: AddSurveyController 19 | validationSpy: ValidationSpy 20 | addSurveySpy: AddSurveySpy 21 | } 22 | 23 | const makeSut = (): SutTypes => { 24 | const validationSpy = new ValidationSpy() 25 | const addSurveySpy = new AddSurveySpy() 26 | const sut = new AddSurveyController(validationSpy, addSurveySpy) 27 | return { 28 | sut, 29 | validationSpy, 30 | addSurveySpy 31 | } 32 | } 33 | 34 | describe('AddSurvey Controller', () => { 35 | beforeAll(() => { 36 | MockDate.set(new Date()) 37 | }) 38 | 39 | afterAll(() => { 40 | MockDate.reset() 41 | }) 42 | 43 | test('Should call Validation with correct values', async () => { 44 | const { sut, validationSpy } = makeSut() 45 | const request = mockRequest() 46 | await sut.handle(request) 47 | expect(validationSpy.input).toEqual(request) 48 | }) 49 | 50 | test('Should return 400 if Validation fails', async () => { 51 | const { sut, validationSpy } = makeSut() 52 | validationSpy.error = new Error() 53 | const httpResponse = await sut.handle(mockRequest()) 54 | expect(httpResponse).toEqual(badRequest(validationSpy.error)) 55 | }) 56 | 57 | test('Should call AddSurvey with correct values', async () => { 58 | const { sut, addSurveySpy } = makeSut() 59 | const request = mockRequest() 60 | await sut.handle(request) 61 | expect(addSurveySpy.params).toEqual({ ...request, date: new Date() }) 62 | }) 63 | 64 | test('Should return 500 if AddSurvey throws', async () => { 65 | const { sut, addSurveySpy } = makeSut() 66 | jest.spyOn(addSurveySpy, 'add').mockImplementationOnce(throwError) 67 | const httpResponse = await sut.handle(mockRequest()) 68 | expect(httpResponse).toEqual(serverError(new Error())) 69 | }) 70 | 71 | test('Should return 204 on success', async () => { 72 | const { sut } = makeSut() 73 | const httpResponse = await sut.handle(mockRequest()) 74 | expect(httpResponse).toEqual(noContent()) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /tests/presentation/controllers/load-survey-result-controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { LoadSurveyResultController } from '@/presentation/controllers' 2 | import { forbidden, serverError, ok } from '@/presentation/helpers' 3 | import { InvalidParamError } from '@/presentation/errors' 4 | import { CheckSurveyByIdSpy, LoadSurveyResultSpy } from '@/tests/presentation/mocks' 5 | import { throwError } from '@/tests/domain/mocks' 6 | 7 | import MockDate from 'mockdate' 8 | import faker from 'faker' 9 | 10 | const mockRequest = (): LoadSurveyResultController.Request => ({ 11 | accountId: faker.datatype.uuid(), 12 | surveyId: faker.datatype.uuid() 13 | }) 14 | 15 | type SutTypes = { 16 | sut: LoadSurveyResultController 17 | checkSurveyByIdSpy: CheckSurveyByIdSpy 18 | loadSurveyResultSpy: LoadSurveyResultSpy 19 | } 20 | 21 | const makeSut = (): SutTypes => { 22 | const checkSurveyByIdSpy = new CheckSurveyByIdSpy() 23 | const loadSurveyResultSpy = new LoadSurveyResultSpy() 24 | const sut = new LoadSurveyResultController(checkSurveyByIdSpy, loadSurveyResultSpy) 25 | return { 26 | sut, 27 | checkSurveyByIdSpy, 28 | loadSurveyResultSpy 29 | } 30 | } 31 | 32 | describe('LoadSurveyResult Controller', () => { 33 | beforeAll(() => { 34 | MockDate.set(new Date()) 35 | }) 36 | 37 | afterAll(() => { 38 | MockDate.reset() 39 | }) 40 | 41 | test('Should call CheckSurveyById with correct value', async () => { 42 | const { sut, checkSurveyByIdSpy } = makeSut() 43 | const request = mockRequest() 44 | await sut.handle(request) 45 | expect(checkSurveyByIdSpy.id).toBe(request.surveyId) 46 | }) 47 | 48 | test('Should return 403 if CheckSurveyById returns false', async () => { 49 | const { sut, checkSurveyByIdSpy } = makeSut() 50 | checkSurveyByIdSpy.result = false 51 | const httpResponse = await sut.handle(mockRequest()) 52 | expect(httpResponse).toEqual(forbidden(new InvalidParamError('surveyId'))) 53 | }) 54 | 55 | test('Should return 500 if CheckSurveyById throws', async () => { 56 | const { sut, checkSurveyByIdSpy } = makeSut() 57 | jest.spyOn(checkSurveyByIdSpy, 'checkById').mockImplementationOnce(throwError) 58 | const httpResponse = await sut.handle(mockRequest()) 59 | expect(httpResponse).toEqual(serverError(new Error())) 60 | }) 61 | 62 | test('Should call LoadSurveyResult with correct values', async () => { 63 | const { sut, loadSurveyResultSpy } = makeSut() 64 | const request = mockRequest() 65 | await sut.handle(request) 66 | expect(loadSurveyResultSpy.surveyId).toBe(request.surveyId) 67 | expect(loadSurveyResultSpy.accountId).toBe(request.accountId) 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.result)) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /tests/presentation/controllers/load-surveys-controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { LoadSurveysController } from '@/presentation/controllers' 2 | import { ok, serverError, noContent } from '@/presentation/helpers' 3 | import { LoadSurveysSpy } from '@/tests/presentation/mocks' 4 | import { throwError } from '@/tests/domain/mocks' 5 | 6 | import MockDate from 'mockdate' 7 | import faker from 'faker' 8 | 9 | const mockRequest = (): LoadSurveysController.Request => ({ accountId: faker.datatype.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 request = mockRequest() 37 | await sut.handle(request) 38 | expect(loadSurveysSpy.accountId).toBe(request.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.result)) 45 | }) 46 | 47 | test('Should return 204 if LoadSurveys returns empty', async () => { 48 | const { sut, loadSurveysSpy } = makeSut() 49 | loadSurveysSpy.result = [] 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 | -------------------------------------------------------------------------------- /tests/presentation/controllers/login-controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { LoginController } from '@/presentation/controllers' 2 | import { badRequest, serverError, unauthorized, ok } from '@/presentation/helpers' 3 | import { MissingParamError } from '@/presentation/errors' 4 | import { AuthenticationSpy, ValidationSpy } from '@/tests/presentation/mocks' 5 | import { throwError } from '@/tests/domain/mocks' 6 | 7 | import faker from 'faker' 8 | 9 | const mockRequest = (): LoginController.Request => ({ 10 | email: faker.internet.email(), 11 | password: faker.internet.password() 12 | }) 13 | 14 | type SutTypes = { 15 | sut: LoginController 16 | authenticationSpy: AuthenticationSpy 17 | validationSpy: ValidationSpy 18 | } 19 | 20 | const makeSut = (): SutTypes => { 21 | const authenticationSpy = new AuthenticationSpy() 22 | const validationSpy = new ValidationSpy() 23 | const sut = new LoginController(authenticationSpy, validationSpy) 24 | return { 25 | sut, 26 | authenticationSpy, 27 | validationSpy 28 | } 29 | } 30 | 31 | describe('Login Controller', () => { 32 | test('Should call Authentication with correct values', async () => { 33 | const { sut, authenticationSpy } = makeSut() 34 | const request = mockRequest() 35 | await sut.handle(request) 36 | expect(authenticationSpy.params).toEqual({ 37 | email: request.email, 38 | password: request.password 39 | }) 40 | }) 41 | 42 | test('Should return 401 if invalid credentials are provided', async () => { 43 | const { sut, authenticationSpy } = makeSut() 44 | authenticationSpy.result = null 45 | const httpResponse = await sut.handle(mockRequest()) 46 | expect(httpResponse).toEqual(unauthorized()) 47 | }) 48 | 49 | test('Should return 500 if Authentication throws', async () => { 50 | const { sut, authenticationSpy } = makeSut() 51 | jest.spyOn(authenticationSpy, 'auth').mockImplementationOnce(throwError) 52 | const httpResponse = await sut.handle(mockRequest()) 53 | expect(httpResponse).toEqual(serverError(new Error())) 54 | }) 55 | 56 | test('Should return 200 if valid credentials are provided', async () => { 57 | const { sut, authenticationSpy } = makeSut() 58 | const httpResponse = await sut.handle(mockRequest()) 59 | expect(httpResponse).toEqual(ok(authenticationSpy.result)) 60 | }) 61 | 62 | test('Should call Validation with correct value', async () => { 63 | const { sut, validationSpy } = makeSut() 64 | const request = mockRequest() 65 | await sut.handle(request) 66 | expect(validationSpy.input).toEqual(request) 67 | }) 68 | 69 | test('Should return 400 if Validation returns an error', async () => { 70 | const { sut, validationSpy } = makeSut() 71 | validationSpy.error = new MissingParamError(faker.random.word()) 72 | const httpResponse = await sut.handle(mockRequest()) 73 | expect(httpResponse).toEqual(badRequest(validationSpy.error)) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /tests/presentation/controllers/save-survey-result-controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { SaveSurveyResultController } from '@/presentation/controllers' 2 | import { InvalidParamError } from '@/presentation/errors' 3 | import { forbidden, serverError, ok } from '@/presentation/helpers' 4 | import { SaveSurveyResultSpy, LoadAnswersBySurveySpy } from '@/tests/presentation/mocks' 5 | import { throwError } from '@/tests/domain/mocks' 6 | 7 | import MockDate from 'mockdate' 8 | import faker from 'faker' 9 | 10 | const mockRequest = (answer: string = null): SaveSurveyResultController.Request => ({ 11 | surveyId: faker.datatype.uuid(), 12 | answer, 13 | accountId: faker.datatype.uuid() 14 | }) 15 | 16 | type SutTypes = { 17 | sut: SaveSurveyResultController 18 | loadAnswersBySurveySpy: LoadAnswersBySurveySpy 19 | saveSurveyResultSpy: SaveSurveyResultSpy 20 | } 21 | 22 | const makeSut = (): SutTypes => { 23 | const loadAnswersBySurveySpy = new LoadAnswersBySurveySpy() 24 | const saveSurveyResultSpy = new SaveSurveyResultSpy() 25 | const sut = new SaveSurveyResultController(loadAnswersBySurveySpy, saveSurveyResultSpy) 26 | return { 27 | sut, 28 | loadAnswersBySurveySpy, 29 | saveSurveyResultSpy 30 | } 31 | } 32 | 33 | describe('SaveSurveyResult Controller', () => { 34 | beforeAll(() => { 35 | MockDate.set(new Date()) 36 | }) 37 | 38 | afterAll(() => { 39 | MockDate.reset() 40 | }) 41 | 42 | test('Should call LoadAnswersBySurvey with correct values', async () => { 43 | const { sut, loadAnswersBySurveySpy } = makeSut() 44 | const request = mockRequest() 45 | await sut.handle(request) 46 | expect(loadAnswersBySurveySpy.id).toBe(request.surveyId) 47 | }) 48 | 49 | test('Should return 403 if LoadAnswersBySurvey returns null', async () => { 50 | const { sut, loadAnswersBySurveySpy } = makeSut() 51 | loadAnswersBySurveySpy.result = [] 52 | const httpResponse = await sut.handle(mockRequest()) 53 | expect(httpResponse).toEqual(forbidden(new InvalidParamError('surveyId'))) 54 | }) 55 | 56 | test('Should return 500 if LoadAnswersBySurvey throws', async () => { 57 | const { sut, loadAnswersBySurveySpy } = makeSut() 58 | jest.spyOn(loadAnswersBySurveySpy, 'loadAnswers').mockImplementationOnce(throwError) 59 | const httpResponse = await sut.handle(mockRequest()) 60 | expect(httpResponse).toEqual(serverError(new Error())) 61 | }) 62 | 63 | test('Should return 403 if an invalid answer is provided', async () => { 64 | const { sut } = makeSut() 65 | const httpResponse = await sut.handle(mockRequest()) 66 | expect(httpResponse).toEqual(forbidden(new InvalidParamError('answer'))) 67 | }) 68 | 69 | test('Should call SaveSurveyResult with correct values', async () => { 70 | const { sut, saveSurveyResultSpy, loadAnswersBySurveySpy } = makeSut() 71 | const request = mockRequest(loadAnswersBySurveySpy.result[0]) 72 | await sut.handle(request) 73 | expect(saveSurveyResultSpy.params).toEqual({ 74 | surveyId: request.surveyId, 75 | accountId: request.accountId, 76 | date: new Date(), 77 | answer: request.answer 78 | }) 79 | }) 80 | 81 | test('Should return 500 if SaveSurveyResult throws', async () => { 82 | const { sut, saveSurveyResultSpy, loadAnswersBySurveySpy } = makeSut() 83 | jest.spyOn(saveSurveyResultSpy, 'save').mockImplementationOnce(throwError) 84 | const request = mockRequest(loadAnswersBySurveySpy.result[0]) 85 | const httpResponse = await sut.handle(request) 86 | expect(httpResponse).toEqual(serverError(new Error())) 87 | }) 88 | 89 | test('Should return 200 on success', async () => { 90 | const { sut, saveSurveyResultSpy, loadAnswersBySurveySpy } = makeSut() 91 | const request = mockRequest(loadAnswersBySurveySpy.result[0]) 92 | const httpResponse = await sut.handle(request) 93 | expect(httpResponse).toEqual(ok(saveSurveyResultSpy.result)) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /tests/presentation/controllers/signup-controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { SignUpController } from '@/presentation/controllers' 2 | import { MissingParamError, ServerError, EmailInUseError } from '@/presentation/errors' 3 | import { ok, serverError, badRequest, forbidden } from '@/presentation/helpers' 4 | import { AuthenticationSpy, ValidationSpy, AddAccountSpy } from '@/tests/presentation/mocks' 5 | import { throwError } from '@/tests/domain/mocks' 6 | 7 | import faker from 'faker' 8 | 9 | const mockRequest = (): SignUpController.Request => { 10 | const password = faker.internet.password() 11 | return { 12 | name: faker.name.findName(), 13 | email: faker.internet.email(), 14 | password, 15 | passwordConfirmation: password 16 | } 17 | } 18 | 19 | type SutTypes = { 20 | sut: SignUpController 21 | addAccountSpy: AddAccountSpy 22 | validationSpy: ValidationSpy 23 | authenticationSpy: AuthenticationSpy 24 | } 25 | 26 | const makeSut = (): SutTypes => { 27 | const authenticationSpy = new AuthenticationSpy() 28 | const addAccountSpy = new AddAccountSpy() 29 | const validationSpy = new ValidationSpy() 30 | const sut = new SignUpController(addAccountSpy, validationSpy, authenticationSpy) 31 | return { 32 | sut, 33 | addAccountSpy, 34 | validationSpy, 35 | authenticationSpy 36 | } 37 | } 38 | 39 | describe('SignUp Controller', () => { 40 | test('Should return 500 if AddAccount throws', async () => { 41 | const { sut, addAccountSpy } = makeSut() 42 | jest.spyOn(addAccountSpy, 'add').mockImplementationOnce(throwError) 43 | const httpResponse = await sut.handle(mockRequest()) 44 | expect(httpResponse).toEqual(serverError(new ServerError(null))) 45 | }) 46 | 47 | test('Should call AddAccount with correct values', async () => { 48 | const { sut, addAccountSpy } = makeSut() 49 | const request = mockRequest() 50 | await sut.handle(request) 51 | expect(addAccountSpy.params).toEqual({ 52 | name: request.name, 53 | email: request.email, 54 | password: request.password 55 | }) 56 | }) 57 | 58 | test('Should return 403 if AddAccount returns false', async () => { 59 | const { sut, addAccountSpy } = makeSut() 60 | addAccountSpy.result = false 61 | const httpResponse = await sut.handle(mockRequest()) 62 | expect(httpResponse).toEqual(forbidden(new EmailInUseError())) 63 | }) 64 | 65 | test('Should return 200 if valid data is provided', async () => { 66 | const { sut, authenticationSpy } = makeSut() 67 | const httpResponse = await sut.handle(mockRequest()) 68 | expect(httpResponse).toEqual(ok(authenticationSpy.result)) 69 | }) 70 | 71 | test('Should call Validation with correct value', async () => { 72 | const { sut, validationSpy } = makeSut() 73 | const request = mockRequest() 74 | await sut.handle(request) 75 | expect(validationSpy.input).toEqual(request) 76 | }) 77 | 78 | test('Should return 400 if Validation returns an error', async () => { 79 | const { sut, validationSpy } = makeSut() 80 | validationSpy.error = new MissingParamError(faker.random.word()) 81 | const httpResponse = await sut.handle(mockRequest()) 82 | expect(httpResponse).toEqual(badRequest(validationSpy.error)) 83 | }) 84 | 85 | test('Should call Authentication with correct values', async () => { 86 | const { sut, authenticationSpy } = makeSut() 87 | const request = mockRequest() 88 | await sut.handle(request) 89 | expect(authenticationSpy.params).toEqual({ 90 | email: request.email, 91 | password: request.password 92 | }) 93 | }) 94 | 95 | test('Should return 500 if Authentication throws', async () => { 96 | const { sut, authenticationSpy } = makeSut() 97 | jest.spyOn(authenticationSpy, 'auth').mockImplementationOnce(throwError) 98 | const httpResponse = await sut.handle(mockRequest()) 99 | expect(httpResponse).toEqual(serverError(new Error())) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /tests/presentation/middlewares/auth-middleware.spec.ts: -------------------------------------------------------------------------------- 1 | import { AuthMiddleware } from '@/presentation/middlewares' 2 | import { forbidden, ok, serverError } from '@/presentation/helpers' 3 | import { AccessDeniedError } from '@/presentation/errors' 4 | import { LoadAccountByTokenSpy } from '@/tests/presentation/mocks' 5 | import { throwError } from '@/tests/domain/mocks' 6 | 7 | const mockRequest = (): AuthMiddleware.Request => ({ 8 | accessToken: 'any_token' 9 | }) 10 | 11 | type SutTypes = { 12 | sut: AuthMiddleware 13 | loadAccountByTokenSpy: LoadAccountByTokenSpy 14 | } 15 | 16 | const makeSut = (role?: string): SutTypes => { 17 | const loadAccountByTokenSpy = new LoadAccountByTokenSpy() 18 | const sut = new AuthMiddleware(loadAccountByTokenSpy, role) 19 | return { 20 | sut, 21 | loadAccountByTokenSpy 22 | } 23 | } 24 | 25 | describe('Auth Middleware', () => { 26 | test('Should return 403 if no x-access-token exists in headers', async () => { 27 | const { sut } = makeSut() 28 | const httpResponse = await sut.handle({}) 29 | expect(httpResponse).toEqual(forbidden(new AccessDeniedError())) 30 | }) 31 | 32 | test('Should call LoadAccountByToken with correct accessToken', async () => { 33 | const role = 'any_role' 34 | const { sut, loadAccountByTokenSpy } = makeSut(role) 35 | const httpRequest = mockRequest() 36 | await sut.handle(httpRequest) 37 | expect(loadAccountByTokenSpy.accessToken).toBe(httpRequest.accessToken) 38 | expect(loadAccountByTokenSpy.role).toBe(role) 39 | }) 40 | 41 | test('Should return 403 if LoadAccountByToken returns null', async () => { 42 | const { sut, loadAccountByTokenSpy } = makeSut() 43 | loadAccountByTokenSpy.result = null 44 | const httpResponse = await sut.handle(mockRequest()) 45 | expect(httpResponse).toEqual(forbidden(new AccessDeniedError())) 46 | }) 47 | 48 | test('Should return 200 if LoadAccountByToken returns an account', async () => { 49 | const { sut, loadAccountByTokenSpy } = makeSut() 50 | const httpResponse = await sut.handle(mockRequest()) 51 | expect(httpResponse).toEqual(ok({ 52 | accountId: loadAccountByTokenSpy.result.id 53 | })) 54 | }) 55 | 56 | test('Should return 500 if LoadAccountByToken throws', async () => { 57 | const { sut, loadAccountByTokenSpy } = makeSut() 58 | jest.spyOn(loadAccountByTokenSpy, 'load').mockImplementationOnce(throwError) 59 | const httpResponse = await sut.handle(mockRequest()) 60 | expect(httpResponse).toEqual(serverError(new Error())) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /tests/presentation/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mock-validation' 2 | export * from './mock-account' 3 | export * from './mock-survey' 4 | export * from './mock-survey-result' 5 | -------------------------------------------------------------------------------- /tests/presentation/mocks/mock-account.ts: -------------------------------------------------------------------------------- 1 | import { AddAccount, Authentication, LoadAccountByToken } from '@/domain/usecases' 2 | 3 | import faker from 'faker' 4 | 5 | export class AddAccountSpy implements AddAccount { 6 | params: AddAccount.Params 7 | result = true 8 | 9 | async add (params: AddAccount.Params): Promise { 10 | this.params = params 11 | return this.result 12 | } 13 | } 14 | 15 | export class AuthenticationSpy implements Authentication { 16 | params: Authentication.Params 17 | result = { 18 | accessToken: faker.datatype.uuid(), 19 | name: faker.name.findName() 20 | } 21 | 22 | async auth (params: Authentication.Params): Promise { 23 | this.params = params 24 | return this.result 25 | } 26 | } 27 | 28 | export class LoadAccountByTokenSpy implements LoadAccountByToken { 29 | accessToken: string 30 | role: string 31 | result = { 32 | id: faker.datatype.uuid() 33 | } 34 | 35 | async load (accessToken: string, role?: string): Promise { 36 | this.accessToken = accessToken 37 | this.role = role 38 | return this.result 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/presentation/mocks/mock-survey-result.ts: -------------------------------------------------------------------------------- 1 | import { SaveSurveyResult, LoadSurveyResult } from '@/domain/usecases' 2 | import { mockSurveyResultModel } from '@/tests/domain/mocks' 3 | 4 | export class SaveSurveyResultSpy implements SaveSurveyResult { 5 | params: SaveSurveyResult.Params 6 | result = mockSurveyResultModel() 7 | 8 | async save (params: SaveSurveyResult.Params): Promise { 9 | this.params = params 10 | return this.result 11 | } 12 | } 13 | 14 | export class LoadSurveyResultSpy implements LoadSurveyResult { 15 | surveyId: string 16 | accountId: string 17 | result = mockSurveyResultModel() 18 | 19 | async load (surveyId: string, accountId: string): Promise { 20 | this.surveyId = surveyId 21 | this.accountId = accountId 22 | return this.result 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/presentation/mocks/mock-survey.ts: -------------------------------------------------------------------------------- 1 | import { AddSurvey, LoadAnswersBySurvey, LoadSurveys, CheckSurveyById } from '@/domain/usecases' 2 | import { mockSurveyModels } from '@/tests/domain/mocks' 3 | 4 | import faker from 'faker' 5 | 6 | export class AddSurveySpy implements AddSurvey { 7 | params: AddSurvey.Params 8 | 9 | async add (params: AddSurvey.Params): Promise { 10 | this.params = params 11 | } 12 | } 13 | 14 | export class LoadSurveysSpy implements LoadSurveys { 15 | accountId: string 16 | result = mockSurveyModels() 17 | 18 | async load (accountId: string): Promise { 19 | this.accountId = accountId 20 | return this.result 21 | } 22 | } 23 | 24 | export class LoadAnswersBySurveySpy implements LoadAnswersBySurvey { 25 | id: string 26 | result = [ 27 | faker.random.word(), 28 | faker.random.word() 29 | ] 30 | 31 | async loadAnswers (id: string): Promise { 32 | this.id = id 33 | return this.result 34 | } 35 | } 36 | 37 | export class CheckSurveyByIdSpy implements CheckSurveyById { 38 | id: string 39 | result = true 40 | 41 | async checkById (id: string): Promise { 42 | this.id = id 43 | return this.result 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/presentation/mocks/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 | -------------------------------------------------------------------------------- /tests/validation/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mock-email-validator' 2 | -------------------------------------------------------------------------------- /tests/validation/mocks/mock-email-validator.ts: -------------------------------------------------------------------------------- 1 | import { EmailValidator } from '@/validation/protocols' 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 | -------------------------------------------------------------------------------- /tests/validation/validators/compare-fields-validation.spec.ts: -------------------------------------------------------------------------------- 1 | import { CompareFieldsValidation } from '@/validation/validators' 2 | import { InvalidParamError } from '@/presentation/errors' 3 | 4 | import faker from 'faker' 5 | 6 | const field = faker.random.word() 7 | const fieldToCompare = faker.random.word() 8 | 9 | const makeSut = (): CompareFieldsValidation => { 10 | return new CompareFieldsValidation(field, fieldToCompare) 11 | } 12 | 13 | describe('CompareFieldsValidation', () => { 14 | test('Should return an InvalidParamError if validation fails', () => { 15 | const sut = makeSut() 16 | const error = sut.validate({ 17 | [field]: 'any_field', 18 | [fieldToCompare]: 'other_field' 19 | }) 20 | expect(error).toEqual(new InvalidParamError(fieldToCompare)) 21 | }) 22 | 23 | test('Should not return if validation succeeds', () => { 24 | const sut = makeSut() 25 | const value = faker.random.word() 26 | const error = sut.validate({ 27 | [field]: value, 28 | [fieldToCompare]: value 29 | }) 30 | expect(error).toBeFalsy() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /tests/validation/validators/email-validation.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmailValidation } from '@/validation/validators' 2 | import { InvalidParamError } from '@/presentation/errors' 3 | import { EmailValidatorSpy } from '@/tests/validation/mocks' 4 | import { throwError } from '@/tests/domain/mocks' 5 | 6 | import faker from 'faker' 7 | 8 | const field = faker.random.word() 9 | 10 | type SutTypes = { 11 | sut: EmailValidation 12 | emailValidatorSpy: EmailValidatorSpy 13 | } 14 | 15 | const makeSut = (): SutTypes => { 16 | const emailValidatorSpy = new EmailValidatorSpy() 17 | const sut = new EmailValidation(field, emailValidatorSpy) 18 | return { 19 | sut, 20 | emailValidatorSpy 21 | } 22 | } 23 | 24 | describe('Email Validation', () => { 25 | test('Should return an error if EmailValidator returns false', () => { 26 | const { sut, emailValidatorSpy } = makeSut() 27 | emailValidatorSpy.isEmailValid = false 28 | const email = faker.internet.email() 29 | const error = sut.validate({ [field]: email }) 30 | expect(error).toEqual(new InvalidParamError(field)) 31 | }) 32 | 33 | test('Should call EmailValidator with correct email', () => { 34 | const { sut, emailValidatorSpy } = makeSut() 35 | const email = faker.internet.email() 36 | sut.validate({ [field]: email }) 37 | expect(emailValidatorSpy.email).toBe(email) 38 | }) 39 | 40 | test('Should throw if EmailValidator throws', () => { 41 | const { sut, emailValidatorSpy } = makeSut() 42 | jest.spyOn(emailValidatorSpy, 'isValid').mockImplementationOnce(throwError) 43 | expect(sut.validate).toThrow() 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /tests/validation/validators/required-field-validation.spec.ts: -------------------------------------------------------------------------------- 1 | import { RequiredFieldValidation } from '@/validation/validators' 2 | import { MissingParamError } from '@/presentation/errors' 3 | 4 | import faker from 'faker' 5 | 6 | const field = faker.random.word() 7 | 8 | const makeSut = (): RequiredFieldValidation => { 9 | return new RequiredFieldValidation(field) 10 | } 11 | 12 | describe('RequiredField Validation', () => { 13 | test('Should return a MissingParamError if validation fails', () => { 14 | const sut = makeSut() 15 | const error = sut.validate({ invalidField: faker.random.word() }) 16 | expect(error).toEqual(new MissingParamError(field)) 17 | }) 18 | 19 | test('Should not return if validation succeeds', () => { 20 | const sut = makeSut() 21 | const error = sut.validate({ [field]: faker.random.word() }) 22 | expect(error).toBeFalsy() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /tests/validation/validators/validation-composite.spec.ts: -------------------------------------------------------------------------------- 1 | import { ValidationComposite } from '@/validation/validators' 2 | import { MissingParamError } from '@/presentation/errors' 3 | import { ValidationSpy } from '@/tests/presentation/mocks' 4 | 5 | import faker from 'faker' 6 | 7 | const field = faker.random.word() 8 | 9 | type SutTypes = { 10 | sut: ValidationComposite 11 | validationSpies: ValidationSpy[] 12 | } 13 | 14 | const makeSut = (): SutTypes => { 15 | const validationSpies = [ 16 | new ValidationSpy(), 17 | new ValidationSpy() 18 | ] 19 | const sut = new ValidationComposite(validationSpies) 20 | return { 21 | sut, 22 | validationSpies 23 | } 24 | } 25 | 26 | describe('Validation Composite', () => { 27 | test('Should return an error if any validation fails', () => { 28 | const { sut, validationSpies } = makeSut() 29 | validationSpies[1].error = new MissingParamError(field) 30 | const error = sut.validate({ [field]: faker.random.word() }) 31 | expect(error).toEqual(validationSpies[1].error) 32 | }) 33 | 34 | test('Should return the first error if more then one validation fails', () => { 35 | const { sut, validationSpies } = makeSut() 36 | validationSpies[0].error = new Error() 37 | validationSpies[1].error = new MissingParamError(field) 38 | const error = sut.validate({ [field]: faker.random.word() }) 39 | expect(error).toEqual(validationSpies[0].error) 40 | }) 41 | 42 | test('Should not return if validation succeeds', () => { 43 | const { sut } = makeSut() 44 | const error = sut.validate({ [field]: faker.random.word() }) 45 | expect(error).toBeFalsy() 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /tsconfig-build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["tests"] 4 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "commonjs", 5 | "target": "es2019", 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "rootDirs": ["src", "tests"], 9 | "baseUrl": "src", 10 | "paths": { 11 | "@/tests/*": ["../tests/*"], 12 | "@/*": ["*"] 13 | } 14 | }, 15 | "include": ["src", "tests"], 16 | "exclude": [] 17 | } --------------------------------------------------------------------------------