├── .editorconfig
├── .env.example
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .huskyrc.json
├── .lintstagedrc.json
├── .prettierrc
├── .travis.yml
├── .vscode
└── launch.json
├── LICENSE.md
├── README.md
├── babel.config.js
├── docker-compose.yml
├── globalConfig.json
├── jest-integration-config.js
├── jest-mongodb-config.js
├── jest-unit-config.js
├── jest.config.js
├── package.json
├── public
├── nodejs-logo.png
└── typescript-logo.png
├── src
├── @types
│ └── express-request.d.ts
├── data
│ ├── interfaces
│ │ ├── cryptography
│ │ │ ├── decrypter.ts
│ │ │ ├── encrypter.ts
│ │ │ ├── hash-comparer.ts
│ │ │ └── hasher.ts
│ │ └── db
│ │ │ ├── account
│ │ │ ├── access-token-repository.ts
│ │ │ ├── add-account-repository.ts
│ │ │ ├── load-account-by-token-repository.ts
│ │ │ └── load-account-repository.ts
│ │ │ ├── log
│ │ │ └── log-error-repository.ts
│ │ │ └── survey
│ │ │ ├── add-survey-repository.ts
│ │ │ ├── load-survey-by-id-repository.ts
│ │ │ ├── load-survey-repository.ts
│ │ │ ├── load-survey-results-repository.ts
│ │ │ └── save-survey-result-repository.ts
│ └── usecases
│ │ ├── add-account
│ │ ├── db-add-account.spec.ts
│ │ └── db-add-account.ts
│ │ ├── add-survey
│ │ ├── db-add-survey.spec.ts
│ │ └── db-add-survey.ts
│ │ ├── authentication
│ │ ├── db-authentication.spec.ts
│ │ └── db-authentication.ts
│ │ ├── load-account
│ │ ├── db-load-account-by-token.spec.ts
│ │ └── db-load-account-by-token.ts
│ │ ├── load-survey-by-id
│ │ ├── load-survey-by-id.spec.ts
│ │ └── load-survey-by-id.ts
│ │ ├── load-survey-result
│ │ ├── db-load-survey-result.spec.ts
│ │ └── db-load-survey-result.ts
│ │ ├── load-survey
│ │ ├── db-load-survey.spec.ts
│ │ └── db-load-survey.ts
│ │ └── save-survey-result
│ │ ├── db-save-survey-result.spec.ts
│ │ └── db-save-survey-result.ts
├── domain
│ ├── mocks
│ │ ├── mock-account.ts
│ │ ├── mock-survey-result.ts
│ │ └── mock-survey.ts
│ ├── models
│ │ ├── account.ts
│ │ ├── survey-results.ts
│ │ └── survey.ts
│ └── usecases
│ │ ├── add-account.ts
│ │ ├── add-survey.ts
│ │ ├── authentication.ts
│ │ ├── load-account-by-token.ts
│ │ ├── load-survey-results.ts
│ │ ├── load-survey.ts
│ │ └── save-survey-results.ts
├── infra
│ ├── cryptography
│ │ ├── bcrypt-adapter
│ │ │ ├── bcrypt-adapter.spec.ts
│ │ │ └── bcrypt-adapter.ts
│ │ └── jwt-adapter
│ │ │ ├── jwt-adapter.spec.ts
│ │ │ └── jwt-adapter.ts
│ ├── db
│ │ └── mongodb
│ │ │ ├── account
│ │ │ ├── mongo-account-repository.spec.ts
│ │ │ └── mongo-account-repository.ts
│ │ │ ├── helpers
│ │ │ ├── index.ts
│ │ │ ├── mongo.spec.ts
│ │ │ ├── mongo.ts
│ │ │ └── query-bulder.ts
│ │ │ ├── log
│ │ │ ├── mongo-log-repository.spec.ts
│ │ │ └── mongo-log-repository.ts
│ │ │ ├── survey-result
│ │ │ ├── mongo-survey-results-repository.spec.ts
│ │ │ └── mongo-survey-results-repository.ts
│ │ │ └── survey
│ │ │ ├── mongo-survey-repository.spec.ts
│ │ │ └── mongo-survey-repository.ts
│ └── validation
│ │ ├── email-validator-adapter.spec.ts
│ │ └── email-validator-adapter.ts
├── main
│ ├── adapters
│ │ ├── apollo-server-resolver-adapter.ts
│ │ ├── express-middleware.ts
│ │ └── express-routes.ts
│ ├── config
│ │ ├── apollo-server.ts
│ │ ├── app.ts
│ │ ├── env.ts
│ │ ├── except-route.ts
│ │ ├── middlewares.ts
│ │ ├── routes.ts
│ │ ├── static-files.ts
│ │ └── swagger.ts
│ ├── decorators
│ │ ├── log-controller-decorator.spec.ts
│ │ └── log-controller-decorator.ts
│ ├── docs
│ │ ├── components
│ │ │ ├── bad-request.ts
│ │ │ ├── forbidden.ts
│ │ │ ├── not-found.ts
│ │ │ ├── server-error.ts
│ │ │ └── unauthorized.ts
│ │ ├── index.ts
│ │ ├── paths
│ │ │ ├── login-path.ts
│ │ │ ├── signup-path.ts
│ │ │ ├── survey-result-path.ts
│ │ │ └── surveys-path.ts
│ │ └── schemas
│ │ │ ├── account-schema.ts
│ │ │ ├── add-survey-schema.ts
│ │ │ ├── api-key-auth-schema.ts
│ │ │ ├── error-schema.ts
│ │ │ ├── login-schema.ts
│ │ │ ├── save-survey-schema.ts
│ │ │ ├── signup-schema.ts
│ │ │ ├── survey-answer-schema.ts
│ │ │ ├── survey-result-answer-schema.ts
│ │ │ ├── survey-result-schema.ts
│ │ │ ├── survey-schema.ts
│ │ │ └── surveys-schema.ts
│ ├── factories
│ │ ├── add-survey
│ │ │ ├── add-survey-controller-factory.ts
│ │ │ ├── add-survey-validations-factory.spec.ts
│ │ │ └── add-survey-validations-factory.ts
│ │ ├── decorators
│ │ │ └── log-controller-factory.ts
│ │ ├── load-survey-result
│ │ │ └── load-survey-result-controller.ts
│ │ ├── load-survey
│ │ │ └── load-survey-controller.ts
│ │ ├── login
│ │ │ ├── login-factory.ts
│ │ │ ├── login-validation-factory.spec.ts
│ │ │ └── login-validation-factory.ts
│ │ ├── middlewares
│ │ │ └── auth-middleware.ts
│ │ ├── save-survey-result
│ │ │ └── save-survey-result-controller.ts
│ │ ├── signup
│ │ │ ├── signup-factory.ts
│ │ │ ├── signup-validation-factory.spec.ts
│ │ │ └── signup-validation-factory.ts
│ │ └── usecases
│ │ │ ├── db-add-account-factory.ts
│ │ │ ├── db-add-survey-factory.ts
│ │ │ ├── db-authentication-factory.ts
│ │ │ ├── db-load-account-by-token.ts
│ │ │ ├── db-load-survey-by-id.ts
│ │ │ ├── db-load-survey-result.ts
│ │ │ ├── db-load-survey.ts
│ │ │ └── db-save-survey-result-factory.ts
│ ├── graphql
│ │ ├── __tests__
│ │ │ ├── helpers.ts
│ │ │ └── login.test.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
│ │ ├── adm-auth.ts
│ │ ├── auth.ts
│ │ ├── body-parser.spec.ts
│ │ ├── body-parser.ts
│ │ ├── content-type.spec.ts
│ │ ├── content-type.ts
│ │ ├── cors.test.ts
│ │ ├── cors.ts
│ │ ├── index.ts
│ │ ├── no-cache.test.ts
│ │ └── no-cache.ts
│ ├── routes
│ │ ├── login-routes.test.ts
│ │ ├── logn-routes.ts
│ │ ├── signup-routes.test.ts
│ │ ├── signup-routes.ts
│ │ ├── survey-result-routes.test.ts
│ │ ├── survey-results-routes.ts
│ │ ├── survey-routes.test.ts
│ │ └── survey-routes.ts
│ └── server.ts
├── presentation
│ ├── controllers
│ │ ├── login
│ │ │ ├── login-controller.spec.ts
│ │ │ └── login-controller.ts
│ │ ├── signup
│ │ │ ├── signup-controller.spec.ts
│ │ │ └── signup-controller.ts
│ │ ├── survey-result
│ │ │ ├── load-survey-result-controller.spec.ts
│ │ │ ├── load-survey-result-controller.ts
│ │ │ ├── save-survey-result-controller.spec.ts
│ │ │ └── save-survey-result-controller.ts
│ │ └── survey
│ │ │ ├── add-survey
│ │ │ ├── add-survey.spec.ts
│ │ │ └── add-survey.ts
│ │ │ └── load-survey
│ │ │ ├── load-survey-controller.spec.ts
│ │ │ └── load-survey-controller.ts
│ ├── errors
│ │ ├── access-denied.ts
│ │ ├── email-already-in-use.ts
│ │ ├── index.ts
│ │ ├── invalid-param.ts
│ │ ├── missing-param.ts
│ │ ├── server.ts
│ │ └── unauthorized.ts
│ ├── helpers
│ │ └── http
│ │ │ └── http.ts
│ ├── interfaces
│ │ ├── controller.ts
│ │ ├── http.ts
│ │ ├── index.ts
│ │ ├── middleware.ts
│ │ └── validation.ts
│ └── middlewares
│ │ ├── auth-middleware.spec.ts
│ │ └── auth-middleware.ts
└── validation
│ ├── interfaces
│ └── email-validator.ts
│ └── validator
│ ├── compare-fields-validation.ts
│ ├── email-validation.ts
│ ├── required-fields-validation.ts
│ ├── tests
│ ├── compare-fields-validation.spec.ts
│ ├── email-validation.spec.ts
│ ├── required-field-validation.spec.ts
│ └── validation-composite.spec.ts
│ └── validation-composite.ts
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = crlf
7 | charset = utf-8
8 | trim_trailing_whitespace = false
9 | insert_final_newline = false
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | PORT = 5050
2 | MONGO_URL = "mongodb://localhost:27017/clean-typescript-api"
3 | JWT_SECRET_KEY = "c1f1b5c0a90e9177daaf2f26f6b0a1af"
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | coverage/
4 | @types/
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es2021": true,
4 | "node": true,
5 | "jest": true
6 | },
7 | "extends": ["standard"],
8 | "parser": "@typescript-eslint/parser",
9 | "parserOptions": {
10 | "ecmaVersion": 12,
11 | // "tsconfigRootDir": ".",
12 | // "project": "tsconfig.json",
13 | "sourceType": "module"
14 | },
15 | "plugins": ["@typescript-eslint"],
16 | "rules": {
17 | "space-before-function-paren": [
18 | "error",
19 | {
20 | "anonymous": "always",
21 | "named": "never",
22 | "asyncArrow": "always"
23 | }
24 | ],
25 | "no-useless-constructor": "off",
26 | "import/export": "off",
27 | "no-redeclare": "off"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Docker generated folders
2 | ./data
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 |
12 | # Diagnostic reports (https://nodejs.org/api/report.html)
13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
14 |
15 | # Runtime data
16 | pids
17 | *.pid
18 | *.seed
19 | *.pid.lock
20 |
21 | # Directory for instrumented libs generated by jscoverage/JSCover
22 | lib-cov
23 |
24 | # Coverage directory used by tools like istanbul
25 | coverage
26 | *.lcov
27 |
28 | # nyc test coverage
29 | .nyc_output
30 |
31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
32 | .grunt
33 |
34 | # Bower dependency directory (https://bower.io/)
35 | bower_components
36 |
37 | # node-waf configuration
38 | .lock-wscript
39 |
40 | # Compiled binary addons (https://nodejs.org/api/addons.html)
41 | build/Release
42 |
43 | # Dependency directories
44 | node_modules/
45 | jspm_packages/
46 |
47 | # Snowpack dependency directory (https://snowpack.dev/)
48 | web_modules/
49 |
50 | # TypeScript cache
51 | *.tsbuildinfo
52 |
53 | # Optional npm cache directory
54 | .npm
55 |
56 | # Optional eslint cache
57 | .eslintcache
58 |
59 | # Microbundle cache
60 | .rpt2_cache/
61 | .rts2_cache_cjs/
62 | .rts2_cache_es/
63 | .rts2_cache_umd/
64 |
65 | # Optional REPL history
66 | .node_repl_history
67 |
68 | # Output of 'npm pack'
69 | *.tgz
70 |
71 | # Yarn Integrity file
72 | .yarn-integrity
73 |
74 | # dotenv environment variables file
75 | .env
76 | .env.test
77 |
78 | # parcel-bundler cache (https://parceljs.org/)
79 | .cache
80 | .parcel-cache
81 |
82 | # Next.js build output
83 | .next
84 | out
85 |
86 | # Nuxt.js build / generate output
87 | .nuxt
88 | dist
89 |
90 | # Gatsby files
91 | .cache/
92 | # Comment in the public line in if your project uses Gatsby and not Next.js
93 | # https://nextjs.org/blog/next-9-1#public-directory-support
94 | # public
95 |
96 | # vuepress build output
97 | .vuepress/dist
98 |
99 | # Serverless directories
100 | .serverless/
101 |
102 | # FuseBox cache
103 | .fusebox/
104 |
105 | # DynamoDB Local files
106 | .dynamodb/
107 |
108 | # TernJS port file
109 | .tern-port
110 |
111 | # Stores VSCode versions used for testing VSCode extensions
112 | .vscode-test
113 |
114 | # yarn v2
115 | .yarn/cache
116 | .yarn/unplugged
117 | .yarn/build-state.yml
118 | .yarn/install-state.gz
119 | .pnp.*
--------------------------------------------------------------------------------
/.huskyrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "hooks": {
3 | "pre-commit": "lint-staged",
4 | "pre-push": "yarn test:ci"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.lintstagedrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "*.ts": [
3 | "yarn lint",
4 | "yarn test"
5 | ]
6 | }
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "none",
4 | "tabWidth": 2,
5 | "semi": false,
6 | "printWidth": 100,
7 | "bracketSpacing": true,
8 | "arrowParens": "avoid"
9 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 14
4 | scripts:
5 | - eslint 'src/**'
6 | - npm run test:coveralls
7 | # deploy:
8 | # provider: heroku
9 | # api_key: $HEROKU_KEY
10 | # on:
11 | # all_branches: true
12 | # app: clean-typescript-api
13 | # skip_cleanup: true
14 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "type": "node",
6 | "request": "attach",
7 | "name": "Docker: Attach to Node",
8 | "remoteRoot": "/usr/src/clean-typescript-api",
9 | "port": 9222,
10 | "restart": true
11 | },
12 | {
13 | "type": "node",
14 | "request": "attach",
15 | "name": "Attach to Node",
16 | "port": 9222,
17 | "restart": true
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Gabriel Lopes
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Clean Typescript API
3 |
4 |
5 |
6 | 
7 | [](https://travis-ci.org/gabriellopes00/clean-typescript-api)
8 | [](https://coveralls.io/github/gabriellopes00/clean-typescript-api)
9 | 
10 | 
11 |
12 | ##### Application hosted at _[heroku](https://www.heroku.com/)_.
13 |
14 | ##### API url: _https://clean-typescript-api.herokuapp.com/_. See the [documentation](https://clean-typescript-api.herokuapp.com/api/docs).
15 |
16 | ###### An API mande with
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ## About this project ⚙
31 |
32 | This is an API made in [NodeJs, Typescript, TDD, Clean Architecture e SOLID Course](https://www.udemy.com/course/tdd-com-mango/), course led by [Rodrigo Mango](https://github.com/rmanguinho). In this course we develop an API using Node.JS, Typescript, Mongodb and all good programming practices, such as Clean Architecture, SOLID principles, TDD and Design Patterns. 🖋 Although i did this project following the course, i made my own changes and improvements.
33 |
34 | ## Building 🔧
35 |
36 | You'll need [Node.js](https://nodejs.org), [Mongodb](https://www.mongodb.com/) and i recommend that you have installed the [Yarn](https://yarnpkg.com/getting-started/install) on your computer. After, you can run the scripts below...
37 |
38 | ###### Cloning Repository
39 |
40 | ```cloning
41 | git clone https://github.com/gabriellopes00/clean-typescript-api.git &&
42 | cd clean-typescript-api &&
43 | yarn install || npm install
44 | ```
45 |
46 | ###### Running API (development environment)
47 |
48 | ```development
49 | yarn dev || npm run dev
50 | ```
51 |
52 | ###### Generating Build and running build
53 |
54 | ```build
55 | yarn build && yarn start || npm run build && npm run start
56 | ```
57 |
58 | ###### Docker 🐳
59 |
60 | You will need to have [docker](https://www.docker.com/) and [docker-compose](https://docs.docker.com/compose/) installed on your computer to run the commands below. Running this commands the containers will be pulled [node:14](https://hub.docker.com/_/node) image and [mongo:4](https://hub.docker.com/_/mongo) image, and the containers will be created on top of this images.
61 |
62 | ```upping
63 | yarn up || npm run up
64 | ```
65 |
66 | ```downing
67 | yarn down || npm run down
68 | ```
69 |
70 | ###### Tests (jest) 🧪
71 |
72 | - _**All**_ ❯ `yarn test`
73 | - _**Coverage**_ ❯ `yarn test:ci`
74 | - _**Watch**_ ❯ `yarn test:watch`
75 | - _**Unit**(.spec)_ ❯ `yarn test:unit`
76 | - _**Integration**(.test)_ ❯ `yarn test:integration`
77 | - _**Staged**_ ❯ `yarn test:staged`
78 | - _**Verbose**(view logs)_ ❯ `yarn test:verbose`
79 |
80 | ###### Lint (eslint) 🎭
81 |
82 | - _**Lint**(fix)_ ❯ `yarn lint`
83 |
84 | ###### Debug 🐞
85 |
86 | - _**Debug**_ ❯ `yarn debug`
87 |
88 | ###### Statistics of the types of commits 📊📈
89 |
90 | Following the standard of the [Conventional Commits](https://www.conventionalcommits.org/).
91 |
92 | - _**feature** commits(amount)_ ❯ `git shortlog -s --grep feat`
93 | - _**test** commits(amount)_ ❯ `git shortlog -s --grep test`
94 | - _**refactor** commits(amount)_ ❯ `git shortlog -s --grep refactor`
95 | - _**chore** commits(amount)_ ❯ `git shortlog -s --grep chore`
96 | - _**docs** commits(amount)_ ❯ `git shortlog -s --grep docs`
97 | - _**build** commits(amount)_ ❯ `git shortlog -s --grep build`
98 |
99 | ## Contact 📱
100 |
101 | [](https://github.com/gabriellopes00)
102 | [](https://www.linkedin.com/in/gabriel-lopes-6625631b0/)
103 | [](https://twitter.com/_gabrielllopes_)
104 | [](mailto:gabrielluislopes00@gmail.com)
105 | [](https://www.facebook.com/profile.php?id=100034920821684)
106 | [](https://www.instagram.com/_.gabriellopes/?hl=pt-br)
107 | [](https://stackoverflow.com/users/14099025/gabriel-lopes?tab=profile)
108 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@babel/preset-env',
5 | {
6 | targets: {
7 | node: 'current'
8 | }
9 | }
10 | ],
11 | '@babel/preset-typescript'
12 | ],
13 | plugins: [
14 | [
15 | 'module-resolver',
16 | {
17 | alias: {
18 | '@presentation': './src/presentation',
19 | '@main': './src/main',
20 | '@domain': './src/domain',
21 | '@data': './src/data',
22 | '@infra': './src/infra'
23 | }
24 | }
25 | ]
26 | ],
27 | ignore: ['**/*.spec.ts', '**/*.test.ts', '**/*.spec.js', '**/*.test.js']
28 | }
29 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | api:
4 | container_name: api_container
5 | image: node:14
6 | working_dir: /usr/src/clean-typescript-api
7 | restart: always
8 | command: bash -c "yarn install --only=prod && yarn start"
9 | environment:
10 | - PORT=5050
11 | - MONGO_URL=mongodb://mongo:27017/clean-typescript-api
12 | - JWT_SECRET_KEY="secret_key
13 | volumes:
14 | - ./dist/:/usr/src/clean-typescript-api/dist/
15 | - ./package.json:/usr/src/clean-typescript-api/package.json
16 | ports:
17 | - '5050:5050'
18 | - '9222:9222'
19 | networks:
20 | - clean-typescript-api
21 |
22 | mongo:
23 | container_name: mongo_container
24 | image: mongo:latest
25 | restart: always
26 | volumes:
27 | - mongodb:/data/db
28 | ports:
29 | - '27017:27017'
30 | networks:
31 | - clean-typescript-api
32 |
33 | networks:
34 | clean-typescript-api:
35 |
36 | volumes:
37 | mongodb:
38 |
--------------------------------------------------------------------------------
/globalConfig.json:
--------------------------------------------------------------------------------
1 | {"mongoUri":"mongodb://127.0.0.1:59894/9da61ef9-8ef8-40e9-898c-aa6ec6d5f34d?"}
--------------------------------------------------------------------------------
/jest-integration-config.js:
--------------------------------------------------------------------------------
1 | const config = require('./jest.config')
2 | config.testMatch = ['**/*.test.ts']
3 |
4 | module.exports = config
5 |
--------------------------------------------------------------------------------
/jest-mongodb-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | mongodbMemoryServerOptions: {
3 | binary: {
4 | version: '4.4.2',
5 | skipMD5: true
6 | },
7 | autoStart: false,
8 | instance: {}
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/jest-unit-config.js:
--------------------------------------------------------------------------------
1 | const config = require('./jest.config')
2 | config.testMatch = ['**/*.spec.ts']
3 |
4 | module.exports = config
5 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const { compilerOptions } = require('./tsconfig.json')
2 | const { pathsToModuleNameMapper } = require('ts-jest/utils')
3 |
4 | module.exports = {
5 | roots: ['/src'],
6 | collectCoverageFrom: [
7 | '/src/**/*.ts',
8 | '!/src/**/*-interfaces.ts',
9 | '!**/interfaces/**',
10 | '!/src/main/**',
11 | '!/src/@types/**'
12 | ],
13 | coverageDirectory: 'coverage',
14 | coverageProvider: 'babel',
15 | testEnvironment: 'node',
16 | preset: '@shelf/jest-mongodb',
17 | transform: {
18 | '.+\\.ts$': 'ts-jest'
19 | },
20 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
21 | prefix: ''
22 | })
23 | }
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "clean-typescript-api",
3 | "version": "2.0.0",
4 | "description": "A typescript api made with clean architecture and TDD",
5 | "main": "src/main/server.ts",
6 | "author": "Gabriel Lopes ",
7 | "license": "MIT",
8 | "scripts": {
9 | "test": "jest --passWithNoTests --silent --noStackTrace --runInBand --no-cache",
10 | "test:verbose": "jest --passWithNoTests --runInBand",
11 | "test:watch": "yarn test --watch",
12 | "test:unit": "yarn test:watch -c jest-unit-config.js",
13 | "test:integration": "yarn test:watch -c jest-integration-config.js",
14 | "test:staged": "yarn test --findRelatedTests",
15 | "test:ci": "yarn test --coverage",
16 | "test:coveralls": "yarn test:ci && coveralls < coverage/lcov.info",
17 | "lint": "eslint src/** --fix",
18 | "dev": "ts-node-dev -r tsconfig-paths/register --respawn --transpile-only --ignore-watch node_modules --no-notify src/main/server.ts",
19 | "up": "yarn build && docker-compose up -d",
20 | "down": "docker-compose down",
21 | "build": "rimraf ./dist && babel src --extensions \".js,.ts\" --out-dir dist --copy-files --no-copy-ignored",
22 | "postbuild": "yarn copyfiles -u 1 public/**/* dist/static",
23 | "start": "node ./dist/main/server.js",
24 | "debug": "node --inspect=0.0.0.0:9222 --nolazy ./dist/main/server.js",
25 | "start:live": "nodemon -L --watch ./dist ./dist/main/server.js",
26 | "debug:live": "nodemon -L --watch ./dist --inspect=0.0.0.0:9222 --nolazy ./dist/main/server.js"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.10.1",
30 | "@babel/core": "^7.10.2",
31 | "@babel/preset-env": "^7.10.2",
32 | "@babel/preset-typescript": "^7.10.1",
33 | "@shelf/jest-mongodb": "^1.2.3",
34 | "@types/bcrypt": "^3.0.0",
35 | "@types/express": "^4.17.9",
36 | "@types/graphql": "^14.5.0",
37 | "@types/graphql-iso-date": "^3.4.0",
38 | "@types/jest": "^26.0.20",
39 | "@types/jsonwebtoken": "^8.5.0",
40 | "@types/mocha": "^8.2.0",
41 | "@types/mongodb": "^3.6.3",
42 | "@types/node": "^14.14.12",
43 | "@types/supertest": "^2.0.10",
44 | "@types/swagger-ui-express": "^4.1.2",
45 | "@types/validator": "^13.1.1",
46 | "@typescript-eslint/eslint-plugin": "^4.9.1",
47 | "@typescript-eslint/parser": "^4.9.1",
48 | "apollo-server-integration-testing": "^2.3.1",
49 | "babel-eslint": "^10.1.0",
50 | "babel-plugin-module-resolver": "^4.0.0",
51 | "copyfiles": "^2.4.1",
52 | "coveralls": "^3.1.0",
53 | "eslint": "^7.12.1",
54 | "eslint-config-standard": "^16.0.2",
55 | "eslint-plugin-import": "^2.22.1",
56 | "eslint-plugin-jest": "^24.1.3",
57 | "eslint-plugin-node": "^11.1.0",
58 | "eslint-plugin-promise": "^4.2.1",
59 | "git-commit-msg-linter": "^3.0.0",
60 | "husky": "^4.3.5",
61 | "jest": "^26.6.3",
62 | "lint-staged": "^10.5.3",
63 | "mockdate": "^3.0.2",
64 | "rimraf": "^3.0.2",
65 | "supertest": "^6.0.1",
66 | "ts-jest": "^26.4.4",
67 | "ts-node-dev": "^1.1.1",
68 | "tsconfig-paths": "^3.9.0",
69 | "typescript": "^4.1.2"
70 | },
71 | "dependencies": {
72 | "apollo-server-express": "^2.22.2",
73 | "bcrypt": "^5.0.0",
74 | "dotenv": "^8.2.0",
75 | "express": "^4.17.1",
76 | "graphql": "^15.5.0",
77 | "graphql-iso-date": "^3.6.1",
78 | "jsonwebtoken": "^8.5.1",
79 | "mongodb": "^3.6.3",
80 | "nodemon": "^2.0.7",
81 | "swagger-ui-express": "^4.1.6",
82 | "validator": "^13.5.2"
83 | },
84 | "engines": {
85 | "node": "14"
86 | }
87 | }
--------------------------------------------------------------------------------
/public/nodejs-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabriellopes00/clean-typescript-api/4190ae2423e11e5b99880c29d4f384c70b329a78/public/nodejs-logo.png
--------------------------------------------------------------------------------
/public/typescript-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabriellopes00/clean-typescript-api/4190ae2423e11e5b99880c29d4f384c70b329a78/public/typescript-logo.png
--------------------------------------------------------------------------------
/src/@types/express-request.d.ts:
--------------------------------------------------------------------------------
1 | declare module Express {
2 | interface Request {
3 | accountId?: string
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/data/interfaces/cryptography/decrypter.ts:
--------------------------------------------------------------------------------
1 | export interface Decrypter {
2 | decript(value: string): Promise
3 | }
4 |
--------------------------------------------------------------------------------
/src/data/interfaces/cryptography/encrypter.ts:
--------------------------------------------------------------------------------
1 | export interface Encrypter {
2 | encrypt(value: string): Promise
3 | }
4 |
--------------------------------------------------------------------------------
/src/data/interfaces/cryptography/hash-comparer.ts:
--------------------------------------------------------------------------------
1 | export interface HashComparer {
2 | compare(value: string, hash: string): Promise
3 | }
4 |
--------------------------------------------------------------------------------
/src/data/interfaces/cryptography/hasher.ts:
--------------------------------------------------------------------------------
1 | export interface Hasher {
2 | hash(value: string): Promise
3 | }
4 |
--------------------------------------------------------------------------------
/src/data/interfaces/db/account/access-token-repository.ts:
--------------------------------------------------------------------------------
1 | export interface AccessTokenRepository {
2 | storeAccessToken(id: string, token: string): Promise
3 | }
4 |
--------------------------------------------------------------------------------
/src/data/interfaces/db/account/add-account-repository.ts:
--------------------------------------------------------------------------------
1 | import { AddAccountParams } from '@domain/usecases/add-account'
2 | import { AccountModel } from '@domain/models/account'
3 |
4 | export interface AddAccountRepository {
5 | add(accountData: AddAccountParams): Promise
6 | }
7 |
--------------------------------------------------------------------------------
/src/data/interfaces/db/account/load-account-by-token-repository.ts:
--------------------------------------------------------------------------------
1 | import { AccountModel } from '@domain/models/account'
2 |
3 | export interface LoadAccountByTokenRepository {
4 | loadByToken(token: string, role?: string): Promise
5 | }
6 |
--------------------------------------------------------------------------------
/src/data/interfaces/db/account/load-account-repository.ts:
--------------------------------------------------------------------------------
1 | import { AccountModel } from '@domain/models/account'
2 |
3 | export interface LoadAccountRepository {
4 | loadByEmail(email: string): Promise
5 | }
6 |
--------------------------------------------------------------------------------
/src/data/interfaces/db/log/log-error-repository.ts:
--------------------------------------------------------------------------------
1 | export interface LogErrorRepository {
2 | logError(stackError: string): Promise
3 | }
4 |
--------------------------------------------------------------------------------
/src/data/interfaces/db/survey/add-survey-repository.ts:
--------------------------------------------------------------------------------
1 | import { AddSurveyParams } from '@domain/usecases/add-survey'
2 |
3 | export interface AddSurveyRepository {
4 | add(accountData: AddSurveyParams): Promise
5 | }
6 |
--------------------------------------------------------------------------------
/src/data/interfaces/db/survey/load-survey-by-id-repository.ts:
--------------------------------------------------------------------------------
1 | import { SurveyModel } from '@domain/models/survey'
2 |
3 | export interface LoadSurveyByIdRepository {
4 | loadById(id: string): Promise
5 | }
6 |
--------------------------------------------------------------------------------
/src/data/interfaces/db/survey/load-survey-repository.ts:
--------------------------------------------------------------------------------
1 | import { SurveyModel } from '@domain/models/survey'
2 |
3 | export interface LoadSurveyRepository {
4 | loadAll(accountId: string): Promise
5 | }
6 |
--------------------------------------------------------------------------------
/src/data/interfaces/db/survey/load-survey-results-repository.ts:
--------------------------------------------------------------------------------
1 | import { SurveyResultsModel } from '@domain/models/survey-results'
2 |
3 | export interface LoadSurveyResultRepository {
4 | loadBySurveyId(surveyId: string, accountId: string): Promise
5 | }
6 |
--------------------------------------------------------------------------------
/src/data/interfaces/db/survey/save-survey-result-repository.ts:
--------------------------------------------------------------------------------
1 | import { SaveSurveyResultParams } from '@domain/usecases/save-survey-results'
2 |
3 | export interface SaveSurveyResultsRepository {
4 | save(data: SaveSurveyResultParams): Promise
5 | }
6 |
--------------------------------------------------------------------------------
/src/data/usecases/add-account/db-add-account.spec.ts:
--------------------------------------------------------------------------------
1 | import { fakeAccountModel, fakeAccountParams } from '../../../domain/mocks/mock-account'
2 | import { AccountModel } from '../../../domain/models/account'
3 | import { AddAccountParams } from '../../../domain/usecases/add-account'
4 | import { Hasher } from '../../interfaces/cryptography/hasher'
5 | import { AddAccountRepository } from '../../interfaces/db/account/add-account-repository'
6 | import { LoadAccountRepository } from '../../interfaces/db/account/load-account-repository'
7 | import { DbAddAccount } from './db-add-account'
8 |
9 | class MockHasher implements Hasher {
10 | async hash(value: string): Promise {
11 | return new Promise(resolve => resolve('hashed_password'))
12 | }
13 | }
14 | class MockAddAccountRepository implements AddAccountRepository {
15 | async add(values: AddAccountParams): Promise {
16 | return new Promise(resolve => resolve(fakeAccountModel))
17 | }
18 | }
19 |
20 | class MockLoadAccountRepository implements LoadAccountRepository {
21 | async loadByEmail(email: string): Promise {
22 | return new Promise(resolve => resolve(null))
23 | }
24 | }
25 |
26 | describe('DbAccount Usecase', () => {
27 | const mockHasher = new MockHasher() as jest.Mocked
28 | const mockAddAccountRepository = new MockAddAccountRepository() as jest.Mocked
29 | const mockLoadAccountRepository = new MockLoadAccountRepository() as jest.Mocked
30 | const sut = new DbAddAccount(mockHasher, mockAddAccountRepository, mockLoadAccountRepository)
31 |
32 | describe('Hasher', () => {
33 | test('Should call Hasher with correct password', async () => {
34 | const hashSpy = jest.spyOn(mockHasher, 'hash')
35 | await sut.add(fakeAccountParams)
36 | expect(hashSpy).toHaveBeenCalledWith(fakeAccountParams.password)
37 | })
38 |
39 | test('Should throw if Hasher throws', async () => {
40 | mockHasher.hash.mockRejectedValueOnce(new Error())
41 | const accountPromise = sut.add(fakeAccountParams)
42 | await expect(accountPromise).rejects.toThrow()
43 | })
44 | })
45 |
46 | describe('AddAccount Repository', () => {
47 | test('Should call AddAccountRepository with correct values', async () => {
48 | const addSpy = jest.spyOn(mockAddAccountRepository, 'add')
49 | await sut.add(fakeAccountParams)
50 | expect(addSpy).toHaveBeenCalledWith({ ...fakeAccountParams, password: 'hashed_password' })
51 | })
52 |
53 | test('Should throw if AddAccountRepository throws', async () => {
54 | mockAddAccountRepository.add.mockRejectedValueOnce(new Error())
55 | const accountPromise = sut.add(fakeAccountParams)
56 | await expect(accountPromise).rejects.toThrow()
57 | })
58 |
59 | test('Should return an account on success', async () => {
60 | const account = await sut.add(fakeAccountParams)
61 | expect(account).toEqual(fakeAccountModel)
62 | })
63 | })
64 |
65 | describe('LoadAccount Repository', () => {
66 | test('Should call LoadAccountRepository with correct email', async () => {
67 | const loadByEmailSpy = jest.spyOn(mockLoadAccountRepository, 'loadByEmail')
68 | await sut.add(fakeAccountParams)
69 | expect(loadByEmailSpy).toHaveBeenCalledWith(fakeAccountParams.email)
70 | })
71 |
72 | test('Should null if loadAccount not returns null', async () => {
73 | mockLoadAccountRepository.loadByEmail.mockResolvedValueOnce(fakeAccountModel)
74 | const account = await sut.add(fakeAccountParams)
75 | expect(account).toBeNull()
76 | })
77 | })
78 | })
79 |
--------------------------------------------------------------------------------
/src/data/usecases/add-account/db-add-account.ts:
--------------------------------------------------------------------------------
1 | import { Hasher } from '@data/interfaces/cryptography/hasher'
2 | import { AddAccountRepository } from '@data/interfaces/db/account/add-account-repository'
3 | import { LoadAccountRepository } from '@data/interfaces/db/account/load-account-repository'
4 | import { AccountModel } from '@domain/models/account'
5 | import { AddAccount, AddAccountParams } from '@domain/usecases/add-account'
6 |
7 | export class DbAddAccount implements AddAccount {
8 | constructor(
9 | private readonly hasher: Hasher,
10 | private readonly addAccountRepository: AddAccountRepository,
11 | private readonly loadAccountRepository: LoadAccountRepository
12 | ) {}
13 |
14 | async add(accountData: AddAccountParams): Promise {
15 | const account = await this.loadAccountRepository.loadByEmail(accountData.email)
16 | if (!account) {
17 | const hashedPassword = await this.hasher.hash(accountData.password)
18 | const newAccount = await this.addAccountRepository.add({
19 | ...accountData,
20 | password: hashedPassword
21 | })
22 |
23 | return newAccount
24 | }
25 |
26 | return null
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/data/usecases/add-survey/db-add-survey.spec.ts:
--------------------------------------------------------------------------------
1 | import { DbAddSurvey } from './db-add-survey'
2 | import { AddSurveyRepository } from '../../interfaces/db/survey/add-survey-repository'
3 | import { AddSurveyParams } from '../../../domain/usecases/add-survey'
4 | import MockDate from 'mockdate'
5 | import { fakeSurveyParams as surveyParams } from '../../../domain/mocks/mock-survey'
6 |
7 | const fakeSurveyParams = { ...surveyParams, date: new Date() }
8 |
9 | class MockAddSurveyRepository implements AddSurveyRepository {
10 | async add(data: AddSurveyParams): Promise {
11 | return new Promise(resolve => resolve())
12 | }
13 | }
14 |
15 | describe('DbAddSurvey Usecase', () => {
16 | const mockAddSurveyRepository = new MockAddSurveyRepository() as jest.Mocked
17 | const sut = new DbAddSurvey(mockAddSurveyRepository)
18 |
19 | beforeAll(() => MockDate.set(new Date()))
20 | afterAll(() => MockDate.reset())
21 |
22 | test('Should call AddSurveyRepository with correct values ', () => {
23 | const addSpy = jest.spyOn(mockAddSurveyRepository, 'add')
24 | sut.add(fakeSurveyParams)
25 | expect(addSpy).toHaveBeenCalledWith(fakeSurveyParams)
26 | })
27 |
28 | test('Should throw if AddSurveyRepository throws', async () => {
29 | mockAddSurveyRepository.add.mockRejectedValueOnce(new Error())
30 | const promise = sut.add(fakeSurveyParams)
31 | await expect(promise).rejects.toThrow()
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/src/data/usecases/add-survey/db-add-survey.ts:
--------------------------------------------------------------------------------
1 | import { AddSurveyRepository } from '@data/interfaces/db/survey/add-survey-repository'
2 | import { AddSurvey, AddSurveyParams } from '@domain/usecases/add-survey'
3 |
4 | export class DbAddSurvey implements AddSurvey {
5 | constructor(private readonly addSurveyRepository: AddSurveyRepository) {}
6 | async add(data: AddSurveyParams): Promise {
7 | await this.addSurveyRepository.add(data)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/data/usecases/authentication/db-authentication.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | fakeAccountModel,
3 | fakeLoginParams as fakeAuthParams
4 | } from '../../../domain/mocks/mock-account'
5 | import { AccountModel } from '../../../domain/models/account'
6 | import { Encrypter } from '../../interfaces/cryptography/encrypter'
7 | import { HashComparer } from '../../interfaces/cryptography/hash-comparer'
8 | import { AccessTokenRepository } from '../../interfaces/db/account/access-token-repository'
9 | import { LoadAccountRepository } from '../../interfaces/db/account/load-account-repository'
10 | import { DbAuthentication } from './db-authentication'
11 |
12 | class MockLoadAccountRepository implements LoadAccountRepository {
13 | async loadByEmail(email: string): Promise {
14 | return new Promise(resolve => resolve(fakeAccountModel))
15 | }
16 | }
17 |
18 | class MockHashComparer implements HashComparer {
19 | async compare(value: string, hash: string): Promise {
20 | return new Promise(resolve => resolve(true))
21 | }
22 | }
23 |
24 | class MockEncrypter implements Encrypter {
25 | async encrypt(value: string): Promise {
26 | return new Promise(resolve => resolve('any_token'))
27 | }
28 | }
29 |
30 | class MockAccessTokenRepository implements AccessTokenRepository {
31 | async storeAccessToken(id: string, token: string): Promise {}
32 | }
33 |
34 | describe('DbAuthentication Usecase', () => {
35 | const mockLoadAccountRepository = new MockLoadAccountRepository() as jest.Mocked
36 | const mockHashComparer = new MockHashComparer() as jest.Mocked
37 | const mockEncrypter = new MockEncrypter() as jest.Mocked
38 | const mockAccessTokenRepository = new MockAccessTokenRepository() as jest.Mocked
39 | const sut = new DbAuthentication(
40 | mockLoadAccountRepository,
41 | mockHashComparer,
42 | mockEncrypter,
43 | mockAccessTokenRepository
44 | )
45 |
46 | describe('LoadAccount Repository', () => {
47 | test('Should call LoadAccountRepository with correct email', async () => {
48 | const loadByEmailSpy = jest.spyOn(mockLoadAccountRepository, 'loadByEmail')
49 | await sut.authenticate(fakeAuthParams)
50 | expect(loadByEmailSpy).toHaveBeenCalledWith(fakeAuthParams.email)
51 | })
52 |
53 | test('Should throw if LoadAccountRepository throws', async () => {
54 | mockLoadAccountRepository.loadByEmail.mockRejectedValueOnce(new Error())
55 | const promiseError = sut.authenticate(fakeAuthParams)
56 | await expect(promiseError).rejects.toThrow()
57 | })
58 |
59 | test('Should return null if LoadAccountRepository returns null', async () => {
60 | mockLoadAccountRepository.loadByEmail.mockReturnValueOnce(null)
61 | const model = await sut.authenticate(fakeAuthParams)
62 | expect(model).toBeNull()
63 | })
64 | })
65 |
66 | describe('Hash Comparer', () => {
67 | test('Should call HashComparer with correct values', async () => {
68 | const compareSpy = jest.spyOn(mockHashComparer, 'compare')
69 | await sut.authenticate(fakeAuthParams)
70 | expect(compareSpy).toHaveBeenCalledWith(fakeAuthParams.password, fakeAccountModel.password)
71 | })
72 |
73 | test('Should throw if HashComparer throws', async () => {
74 | mockHashComparer.compare.mockRejectedValueOnce(new Error())
75 | const promiseError = sut.authenticate(fakeAuthParams)
76 | await expect(promiseError).rejects.toThrow()
77 | })
78 |
79 | test('Should return null if HashComparer returns false', async () => {
80 | mockHashComparer.compare.mockResolvedValueOnce(false)
81 | const model = await sut.authenticate(fakeAuthParams)
82 | expect(model).toBeNull()
83 | })
84 | })
85 |
86 | describe('Encrypter', () => {
87 | test('Should call Encrypter with correct id', async () => {
88 | const generateSpy = jest.spyOn(mockEncrypter, 'encrypt')
89 | await sut.authenticate(fakeAuthParams)
90 | expect(generateSpy).toHaveBeenCalledWith(fakeAccountModel.id)
91 | })
92 |
93 | test('Should throw if Encrypter throws', async () => {
94 | mockEncrypter.encrypt.mockRejectedValueOnce(new Error())
95 | const promiseError = sut.authenticate(fakeAuthParams)
96 | await expect(promiseError).rejects.toThrow()
97 | })
98 |
99 | test('Should return an authentication model on success', async () => {
100 | const { accessToken, name } = await sut.authenticate(fakeAuthParams)
101 | expect(accessToken).toBe('any_token')
102 | expect(name).toBe(fakeAccountModel.name)
103 | })
104 | test('Should throw if Encrypter throws', async () => {
105 | mockEncrypter.encrypt.mockRejectedValueOnce(new Error())
106 |
107 | const promiseError = sut.authenticate(fakeAuthParams)
108 | await expect(promiseError).rejects.toThrow()
109 | })
110 | })
111 |
112 | describe('AccessToken Repository', () => {
113 | test('Should call AccessTokenRepository with correct values', async () => {
114 | const storeAccessTokenSpy = jest.spyOn(mockAccessTokenRepository, 'storeAccessToken')
115 | await sut.authenticate(fakeAuthParams)
116 | expect(storeAccessTokenSpy).toHaveBeenCalledWith(fakeAccountModel.id, 'any_token')
117 | })
118 |
119 | test('Should throw if AccessTokenRepository throws', async () => {
120 | mockAccessTokenRepository.storeAccessToken.mockRejectedValueOnce(new Error())
121 | const promiseError = sut.authenticate(fakeAuthParams)
122 | await expect(promiseError).rejects.toThrow()
123 | })
124 | })
125 | })
126 |
--------------------------------------------------------------------------------
/src/data/usecases/authentication/db-authentication.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticationModel } from '@domain/models/account'
2 | import { AuthenticationParams, Authenticator } from '@domain/usecases/authentication'
3 | import { Encrypter } from '../../interfaces/cryptography/encrypter'
4 | import { HashComparer } from '../../interfaces/cryptography/hash-comparer'
5 | import { AccessTokenRepository } from '../../interfaces/db/account/access-token-repository'
6 | import { LoadAccountRepository } from '../../interfaces/db/account/load-account-repository'
7 |
8 | export class DbAuthentication implements Authenticator {
9 | constructor(
10 | private readonly loadAccountRepository: LoadAccountRepository,
11 | private readonly hashComparer: HashComparer,
12 | private readonly encrypter: Encrypter,
13 | private readonly accessTokenRepository: AccessTokenRepository
14 | ) {}
15 |
16 | async authenticate(data: AuthenticationParams): Promise {
17 | const account = await this.loadAccountRepository.loadByEmail(data.email)
18 | if (account) {
19 | const isValid = await this.hashComparer.compare(data.password, account.password)
20 | if (isValid) {
21 | const accessToken = await this.encrypter.encrypt(account.id)
22 | await this.accessTokenRepository.storeAccessToken(account.id, accessToken)
23 | return { accessToken, name: account.name }
24 | }
25 | }
26 | return null
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/data/usecases/load-account/db-load-account-by-token.spec.ts:
--------------------------------------------------------------------------------
1 | import { Decrypter } from '../../../data/interfaces/cryptography/decrypter'
2 | import { DbLoadAccountByToken } from './db-load-account-by-token'
3 | import { AccountModel } from '../../../domain/models/account'
4 | import { LoadAccountByTokenRepository } from '../../interfaces/db/account/load-account-by-token-repository'
5 | import { fakeAccountModel } from '../../../domain/mocks/mock-account'
6 |
7 | class MockDecrypter implements Decrypter {
8 | async decript(value: string): Promise {
9 | return 'any_token'
10 | }
11 | }
12 |
13 | class MockLoadAccountByTokenRepository implements LoadAccountByTokenRepository {
14 | async loadByToken(token: string, role?: string): Promise {
15 | return { ...fakeAccountModel, password: 'hashed_password' }
16 | }
17 | }
18 |
19 | describe('DbLoadAccountByToken', () => {
20 | const mockDecrypter = new MockDecrypter() as jest.Mocked
21 | const mockLoadAccountByTokenRepository = new MockLoadAccountByTokenRepository() as jest.Mocked
22 | const sut = new DbLoadAccountByToken(mockDecrypter, mockLoadAccountByTokenRepository)
23 |
24 | describe('Decrypter', () => {
25 | test('Should call decrypter with correct values', async () => {
26 | const decryptSpy = jest.spyOn(mockDecrypter, 'decript')
27 | await sut.load('any_token', 'any_role')
28 | expect(decryptSpy).toHaveBeenLastCalledWith('any_token')
29 | })
30 |
31 | test('Should return null if decrypter returns null', async () => {
32 | mockDecrypter.decript.mockReturnValueOnce(null)
33 | const account = await sut.load('any_token', 'any_role')
34 | expect(account).toBeNull()
35 | })
36 |
37 | test('Should throw if Decrypter throws', async () => {
38 | mockDecrypter.decript.mockRejectedValueOnce(new Error())
39 | const account = await sut.load('any_token', 'any_role')
40 | expect(account).toBeNull()
41 | })
42 | })
43 |
44 | describe('LoadAccountByToken Repository', () => {
45 | test('Should call LoadAccountByTokenRepository with correct values', async () => {
46 | const loadSpy = jest.spyOn(mockLoadAccountByTokenRepository, 'loadByToken')
47 | await sut.load('any_token', 'any_role')
48 | expect(loadSpy).toHaveBeenLastCalledWith('any_token', 'any_role')
49 | })
50 |
51 | test('Should return null if loadAccountRepository returns null', async () => {
52 | mockLoadAccountByTokenRepository.loadByToken.mockReturnValueOnce(null)
53 | const account = await sut.load('any_token', 'any_role')
54 | expect(account).toBeNull()
55 | })
56 |
57 | test('Should return null if loadAccountRepository returns null', async () => {
58 | const account = await sut.load('any_token', 'any_role')
59 | expect(account).toEqual({ ...fakeAccountModel, password: 'hashed_password' })
60 | })
61 |
62 | test('Should throw if LoadAccountByTokenRepository throws', async () => {
63 | mockLoadAccountByTokenRepository.loadByToken.mockRejectedValueOnce(new Error())
64 | const promise = sut.load('any_token', 'any_role')
65 | await expect(promise).rejects.toThrow()
66 | })
67 | })
68 | })
69 |
--------------------------------------------------------------------------------
/src/data/usecases/load-account/db-load-account-by-token.ts:
--------------------------------------------------------------------------------
1 | import { Decrypter } from '@data/interfaces/cryptography/decrypter'
2 | import { LoadAccountByTokenRepository } from '@data/interfaces/db/account/load-account-by-token-repository'
3 | import { AccountModel } from '@domain/models/account'
4 | import { LoadAccountByToken } from '@domain/usecases/load-account-by-token'
5 |
6 | export class DbLoadAccountByToken implements LoadAccountByToken {
7 | constructor(
8 | private readonly decrypter: Decrypter,
9 | private readonly loadAccountByTokenRepository: LoadAccountByTokenRepository
10 | ) {}
11 |
12 | async load(accessToken: string, role?: string): Promise {
13 | let token: string = null
14 | try {
15 | token = await this.decrypter.decript(accessToken)
16 | } catch (error) {
17 | return null
18 | }
19 |
20 | if (token) {
21 | const account = await this.loadAccountByTokenRepository.loadByToken(accessToken, role)
22 | if (account) return account
23 | }
24 | return null
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/data/usecases/load-survey-by-id/load-survey-by-id.spec.ts:
--------------------------------------------------------------------------------
1 | import { SurveyModel } from '../../../domain/models/survey'
2 | import { LoadSurveyByIdRepository } from '../../interfaces/db/survey/load-survey-by-id-repository'
3 | import { DbLoadSurveyById } from './load-survey-by-id'
4 | import mockDate from 'mockdate'
5 | import { fakeSurveyModel } from '../../../domain/mocks/mock-survey'
6 |
7 | class MockLoadSurveyByIdRepository implements LoadSurveyByIdRepository {
8 | async loadById(id: string): Promise {
9 | return new Promise(resolve => resolve(fakeSurveyModel))
10 | }
11 | }
12 |
13 | describe('DbLoadSurveyById', () => {
14 | const mockLoadSurveyByIdRepository = new MockLoadSurveyByIdRepository() as jest.Mocked
15 | const sut = new DbLoadSurveyById(mockLoadSurveyByIdRepository)
16 |
17 | beforeAll(() => mockDate.set(new Date()))
18 | afterAll(() => mockDate.reset())
19 |
20 | test('Should call LoadSurveyRepository', async () => {
21 | const loadSpy = jest.spyOn(mockLoadSurveyByIdRepository, 'loadById')
22 | await sut.loadById('any_id')
23 | expect(loadSpy).toHaveBeenCalledWith('any_id')
24 | })
25 |
26 | test('Should return a survey on success', async () => {
27 | const surveys = await sut.loadById('any_id')
28 | expect(surveys).toEqual(fakeSurveyModel)
29 | })
30 |
31 | test('Should throw LoadSurveyByIdRepository throws', async () => {
32 | mockLoadSurveyByIdRepository.loadById.mockRejectedValueOnce(new Error())
33 | const error = sut.loadById('any_id')
34 | await expect(error).rejects.toThrow()
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/src/data/usecases/load-survey-by-id/load-survey-by-id.ts:
--------------------------------------------------------------------------------
1 | import { LoadSurveyByIdRepository } from '@data/interfaces/db/survey/load-survey-by-id-repository'
2 | import { SurveyModel } from '@domain/models/survey'
3 |
4 | export class DbLoadSurveyById implements LoadSurveyByIdRepository {
5 | constructor(
6 | private readonly loadSurveyByIdRepository: LoadSurveyByIdRepository
7 | ) {}
8 |
9 | async loadById(id: string): Promise {
10 | const survey = await this.loadSurveyByIdRepository.loadById(id)
11 | return survey
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/data/usecases/load-survey-result/db-load-survey-result.spec.ts:
--------------------------------------------------------------------------------
1 | import mockDate from 'mockdate'
2 | import { fakeSurveyModel } from '../../../domain/mocks/mock-survey'
3 | import { fakeSurveyResultModel } from '../../../domain/mocks/mock-survey-result'
4 | import { SurveyModel } from '../../../domain/models/survey'
5 | import { SurveyResultsModel } from '../../../domain/models/survey-results'
6 | import { LoadSurveyByIdRepository } from '../../interfaces/db/survey/load-survey-by-id-repository'
7 | import { LoadSurveyResultRepository } from '../../interfaces/db/survey/load-survey-results-repository'
8 | import { DbLoadSurveyResults } from './db-load-survey-result'
9 |
10 | class MockLoadSurveyResultsRepository implements LoadSurveyResultRepository {
11 | async loadBySurveyId(fakeSurveyId: string): Promise {
12 | return fakeSurveyResultModel
13 | }
14 | }
15 |
16 | class MockLoadSurveyByIdRepository implements LoadSurveyByIdRepository {
17 | async loadById(id: string): Promise {
18 | return fakeSurveyModel
19 | }
20 | }
21 |
22 | describe('DbLoadSurveyResult Usecase', () => {
23 | const mockLoadSurveyResultsRepository = new MockLoadSurveyResultsRepository() as jest.Mocked
24 | const mockLoadSurveyByIdRepository = new MockLoadSurveyByIdRepository() as jest.Mocked
25 | const sut = new DbLoadSurveyResults(mockLoadSurveyResultsRepository, mockLoadSurveyByIdRepository)
26 | const fakeSurveyId = 'any_surveyId'
27 | const fakeAccountId = 'any_id'
28 |
29 | beforeAll(() => mockDate.set('2021-03-08T19:16:46.313Z'))
30 | afterAll(() => mockDate.reset())
31 |
32 | test('Should call LoadSurveyResultRepository with correct values', async () => {
33 | const load = jest.spyOn(mockLoadSurveyResultsRepository, 'loadBySurveyId')
34 | await sut.load(fakeSurveyId, fakeAccountId)
35 | expect(load).toHaveBeenCalledWith(fakeSurveyId, fakeAccountId)
36 | })
37 |
38 | test('Should throw if SaveSurveyRepository throws', async () => {
39 | mockLoadSurveyResultsRepository.loadBySurveyId.mockRejectedValueOnce(new Error())
40 | const error = sut.load(fakeSurveyId, fakeAccountId)
41 | await expect(error).rejects.toThrow()
42 | })
43 |
44 | test('Should return a surveyResult model on success', async () => {
45 | const surveyResult = await sut.load(fakeSurveyId, fakeAccountId)
46 | expect(surveyResult).toEqual(fakeSurveyResultModel)
47 | })
48 |
49 | describe('LoadSurveyById repository', () => {
50 | test('Should call LoadSurveyById repository if LoadSurveyResult repository returns null', async () => {
51 | mockLoadSurveyResultsRepository.loadBySurveyId.mockResolvedValueOnce(null)
52 | const loadSpy = jest.spyOn(mockLoadSurveyByIdRepository, 'loadById')
53 | await sut.load(fakeSurveyId, fakeAccountId)
54 | expect(loadSpy).toHaveBeenCalledWith(fakeSurveyId)
55 | })
56 |
57 | test('Should return a surveyResult with all answers if LoadSurveyResult repository returns null', async () => {
58 | mockLoadSurveyResultsRepository.loadBySurveyId.mockResolvedValueOnce(null)
59 | const surveyResult = await sut.load(fakeSurveyId, fakeAccountId)
60 | expect(surveyResult).toEqual(fakeSurveyResultModel)
61 | })
62 | })
63 | })
64 |
--------------------------------------------------------------------------------
/src/data/usecases/load-survey-result/db-load-survey-result.ts:
--------------------------------------------------------------------------------
1 | import { SurveyResultsModel } from '@domain/models/survey-results'
2 | import { LoadSurveyResult } from '@domain/usecases/load-survey-results'
3 | import { LoadSurveyResultRepository } from '@data/interfaces/db/survey/load-survey-results-repository'
4 | import { LoadSurveyByIdRepository } from '@data/interfaces/db/survey/load-survey-by-id-repository'
5 |
6 | export class DbLoadSurveyResults implements LoadSurveyResult {
7 | constructor(
8 | private readonly loadSurveyResultRepository: LoadSurveyResultRepository,
9 | private readonly loadSurveyByIdRepository: LoadSurveyByIdRepository
10 | ) {}
11 |
12 | async load(surveyId: string, accountId: string): Promise {
13 | let surveyResult = await this.loadSurveyResultRepository.loadBySurveyId(surveyId, accountId)
14 | if (!surveyResult) {
15 | const survey = await this.loadSurveyByIdRepository.loadById(surveyId)
16 | surveyResult = {
17 | surveyId: survey.id,
18 | date: survey.date,
19 | question: survey.question,
20 | answers: survey.answers.map(answer =>
21 | Object.assign({}, answer, { count: 0, percent: 0, isCurrentAccountResponse: false })
22 | )
23 | }
24 | }
25 | return surveyResult
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/data/usecases/load-survey/db-load-survey.spec.ts:
--------------------------------------------------------------------------------
1 | import { SurveyModel } from '../../../domain/models/survey'
2 | import { LoadSurveyRepository } from '../../interfaces/db/survey/load-survey-repository'
3 | import { DbLoadSurvey } from './db-load-survey'
4 | import { fakeSurveysModel } from '../../../domain/mocks/mock-survey'
5 |
6 | class MockAdSurveyRepository implements LoadSurveyRepository {
7 | async loadAll(): Promise {
8 | return new Promise(resolve => resolve(fakeSurveysModel))
9 | }
10 | }
11 |
12 | describe('DbLoadSurvey', () => {
13 | const mockAdSurveyRepository = new MockAdSurveyRepository() as jest.Mocked
14 | const sut = new DbLoadSurvey(mockAdSurveyRepository)
15 | const fakeId = 'any_id'
16 |
17 | test('Should call LoadSurveyRepository', async () => {
18 | const loadSpy = jest.spyOn(mockAdSurveyRepository, 'loadAll')
19 | await sut.load(fakeId)
20 | expect(loadSpy).toHaveBeenCalledWith(fakeId)
21 | })
22 |
23 | test('Should return a list of surveys on success', async () => {
24 | const surveys = await sut.load(fakeId)
25 | expect(surveys).toEqual(fakeSurveysModel)
26 | })
27 |
28 | test('Should throw LoadSurveyRepository throws', async () => {
29 | mockAdSurveyRepository.loadAll.mockRejectedValueOnce(new Error())
30 | const error = sut.load(fakeId)
31 | await expect(error).rejects.toThrow()
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/src/data/usecases/load-survey/db-load-survey.ts:
--------------------------------------------------------------------------------
1 | import { LoadSurveyRepository } from '@data/interfaces/db/survey/load-survey-repository'
2 | import { SurveyModel } from '@domain/models/survey'
3 | import { LoadSurvey } from '@domain/usecases/load-survey'
4 |
5 | export class DbLoadSurvey implements LoadSurvey {
6 | constructor(private readonly loadSurveyRepository: LoadSurveyRepository) {}
7 |
8 | async load(accountId: string): Promise {
9 | const surveys = await this.loadSurveyRepository.loadAll(accountId)
10 | return surveys
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/data/usecases/save-survey-result/db-save-survey-result.spec.ts:
--------------------------------------------------------------------------------
1 | import MockDate from 'mockdate'
2 | import {
3 | fakeSurveyResultModel,
4 | fakeSurveyResultParams
5 | } from '../../../domain/mocks/mock-survey-result'
6 | import { SurveyResultsModel } from '../../../domain/models/survey-results'
7 | import { SaveSurveyResultParams } from '../../../domain/usecases/save-survey-results'
8 | import { LoadSurveyResultRepository } from '../../interfaces/db/survey/load-survey-results-repository'
9 | import { SaveSurveyResultsRepository } from '../../interfaces/db/survey/save-survey-result-repository'
10 | import { DbSaveSurveyResults } from './db-save-survey-result'
11 |
12 | class MockSaveSurveyResultsRepository implements SaveSurveyResultsRepository {
13 | async save(data: SaveSurveyResultParams): Promise {
14 | return Promise.resolve()
15 | }
16 | }
17 |
18 | class MockLoadSurveyResultsRepository implements LoadSurveyResultRepository {
19 | async loadBySurveyId(surveyId: string, accountId: string): Promise {
20 | return fakeSurveyResultModel
21 | }
22 | }
23 |
24 | describe('DbSaveSurveyResults Usecase', () => {
25 | const mockSaveSurveyResultsRepository = new MockSaveSurveyResultsRepository() as jest.Mocked
26 | const mockLoadSurveyResultsRepository = new MockLoadSurveyResultsRepository() as jest.Mocked
27 | const sut = new DbSaveSurveyResults(
28 | mockSaveSurveyResultsRepository,
29 | mockLoadSurveyResultsRepository
30 | )
31 |
32 | beforeAll(() => MockDate.set(new Date()))
33 | afterAll(() => MockDate.reset())
34 | describe('SaveSurvey repository', () => {
35 | test('Should call SaveSurveyRepository with correct values ', async () => {
36 | const saveSpy = jest.spyOn(mockSaveSurveyResultsRepository, 'save')
37 | await sut.save(fakeSurveyResultParams)
38 | expect(saveSpy).toHaveBeenCalledWith(fakeSurveyResultParams)
39 | })
40 |
41 | test('Should return a SurveyModel on success', async () => {
42 | const result = await sut.save(fakeSurveyResultParams)
43 | expect(result).toEqual(fakeSurveyResultModel)
44 | })
45 |
46 | test('Should throw if SaveSurveyRepository throws', async () => {
47 | mockSaveSurveyResultsRepository.save.mockRejectedValueOnce(new Error())
48 | const error = sut.save(fakeSurveyResultParams)
49 | await expect(error).rejects.toThrow()
50 | })
51 | })
52 |
53 | describe('LoadSurveyResult Repository', () => {
54 | test('Should call LoadSurveyRepository with correct values ', async () => {
55 | const loadSpy = jest.spyOn(mockLoadSurveyResultsRepository, 'loadBySurveyId')
56 | await sut.save(fakeSurveyResultParams)
57 | expect(loadSpy).toHaveBeenCalledWith(
58 | fakeSurveyResultParams.surveyId,
59 | fakeSurveyResultParams.accountId
60 | )
61 | })
62 |
63 | test('Should throw if LoadSurveyRepository throws', async () => {
64 | mockLoadSurveyResultsRepository.loadBySurveyId.mockRejectedValueOnce(new Error())
65 | const error = sut.save(fakeSurveyResultParams)
66 | await expect(error).rejects.toThrow()
67 | })
68 | })
69 | })
70 |
--------------------------------------------------------------------------------
/src/data/usecases/save-survey-result/db-save-survey-result.ts:
--------------------------------------------------------------------------------
1 | import { LoadSurveyResultRepository } from '@data/interfaces/db/survey/load-survey-results-repository'
2 | import { SaveSurveyResultsRepository } from '@data/interfaces/db/survey/save-survey-result-repository'
3 | import { SurveyResultsModel } from '@domain/models/survey-results'
4 | import { SaveSurveyResults, SaveSurveyResultParams } from '@domain/usecases/save-survey-results'
5 |
6 | export class DbSaveSurveyResults implements SaveSurveyResults {
7 | constructor(
8 | private readonly saveSurveyResultRepository: SaveSurveyResultsRepository,
9 | private readonly loadSurveyResultsRepository: LoadSurveyResultRepository
10 | ) {}
11 |
12 | async save(data: SaveSurveyResultParams): Promise {
13 | await this.saveSurveyResultRepository.save(data)
14 | const surveyResults = await this.loadSurveyResultsRepository.loadBySurveyId(
15 | data.surveyId,
16 | data.accountId
17 | )
18 | return surveyResults
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/domain/mocks/mock-account.ts:
--------------------------------------------------------------------------------
1 | import { AccountModel } from '../models/account'
2 | import { AddAccountParams } from '../usecases/add-account'
3 |
4 | export const fakeAccountModel: AccountModel = {
5 | id: 'any_id',
6 | name: 'any_name',
7 | email: 'any@mail.com',
8 | password: 'any_password'
9 | }
10 |
11 | export const fakeAccountParams: AddAccountParams = {
12 | name: 'any_name',
13 | email: 'any@mail.com',
14 | password: 'any_password'
15 | }
16 |
17 | export const fakeLoginParams: Pick = {
18 | email: 'any@mail.com',
19 | password: 'any_password'
20 | }
21 |
--------------------------------------------------------------------------------
/src/domain/mocks/mock-survey-result.ts:
--------------------------------------------------------------------------------
1 | import { SurveyResultsModel } from '@domain/models/survey-results'
2 | import { SaveSurveyResultParams } from '@domain/usecases/save-survey-results'
3 |
4 | export const fakeSurveyResultModel: SurveyResultsModel = {
5 | surveyId: 'any_id',
6 | question: 'any_question',
7 | answers: [
8 | {
9 | answer: 'any_answer',
10 | image: 'any_image',
11 | count: 0,
12 | percent: 0,
13 | isCurrentAccountResponse: false
14 | },
15 | {
16 | answer: 'other_answer',
17 | image: 'other_image',
18 | count: 0,
19 | percent: 0,
20 | isCurrentAccountResponse: false
21 | }
22 | ],
23 | date: new Date('2021')
24 | }
25 |
26 | export const fakeSurveyResultParams: SaveSurveyResultParams = {
27 | date: new Date(),
28 | accountId: 'any_account_id',
29 | surveyId: 'any_survey_id',
30 | answer: 'any_answer'
31 | }
32 |
--------------------------------------------------------------------------------
/src/domain/mocks/mock-survey.ts:
--------------------------------------------------------------------------------
1 | import { AddSurveyParams } from '@domain/usecases/add-survey'
2 | import { SurveyModel } from '../models/survey'
3 |
4 | export const fakeSurveyModel: SurveyModel = {
5 | id: 'any_id',
6 | date: new Date('2021'),
7 | question: 'any_question',
8 | answers: [
9 | { image: 'any_image', answer: 'any_answer' },
10 | { image: 'other_image', answer: 'other_answer' }
11 | ]
12 | }
13 |
14 | export const fakeSurveyParams: Omit = {
15 | question: 'any_question',
16 | answers: [
17 | { image: 'any_image', answer: 'any_answer' },
18 | { image: 'other_image', answer: 'other_answer' }
19 | ]
20 | }
21 |
22 | export const fakeSurveysModel: SurveyModel[] = [
23 | {
24 | id: 'asdf',
25 | date: new Date(),
26 | question: 'any_question',
27 | answers: [{ image: 'any_image', answer: 'any_answer' }]
28 | },
29 | {
30 | id: 'fdas',
31 | date: new Date(),
32 | question: 'other_question',
33 | answers: [{ image: 'other_image', answer: 'other_answer' }]
34 | }
35 | ]
36 |
--------------------------------------------------------------------------------
/src/domain/models/account.ts:
--------------------------------------------------------------------------------
1 | export interface AccountModel {
2 | id: string
3 | name: string
4 | email: string
5 | password: string
6 | }
7 |
8 | export interface AuthenticationModel {
9 | accessToken: string
10 | name: string
11 | }
12 |
--------------------------------------------------------------------------------
/src/domain/models/survey-results.ts:
--------------------------------------------------------------------------------
1 | export interface SurveyResultAnswerModel {
2 | image?: string
3 | answer: string
4 | count: number
5 | percent: number
6 | isCurrentAccountResponse: boolean
7 | }
8 |
9 | export interface SurveyResultsModel {
10 | surveyId: string
11 | question: string
12 | answers: SurveyResultAnswerModel[]
13 | date: Date
14 | }
15 |
--------------------------------------------------------------------------------
/src/domain/models/survey.ts:
--------------------------------------------------------------------------------
1 | export interface SurveyAnswer {
2 | image?: string
3 | answer: string
4 | }
5 |
6 | export interface SurveyModel {
7 | id: string
8 | question: string
9 | answers: SurveyAnswer[]
10 | date: Date
11 | didAnswer?: boolean
12 | }
13 |
--------------------------------------------------------------------------------
/src/domain/usecases/add-account.ts:
--------------------------------------------------------------------------------
1 | import { AccountModel } from '@domain/models/account'
2 |
3 | export type AddAccountParams = Omit
4 |
5 | export interface AddAccount {
6 | add(account: AddAccountParams): Promise
7 | }
8 |
--------------------------------------------------------------------------------
/src/domain/usecases/add-survey.ts:
--------------------------------------------------------------------------------
1 | import { SurveyModel } from '@domain/models/survey'
2 |
3 | export interface AddSurveyParams extends Omit {}
4 |
5 | export interface AddSurvey {
6 | add(data: AddSurveyParams): Promise
7 | }
8 |
--------------------------------------------------------------------------------
/src/domain/usecases/authentication.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticationModel } from '@domain/models/account'
2 |
3 | export interface AuthenticationParams {
4 | email: string
5 | password: string
6 | }
7 |
8 | export interface Authenticator {
9 | authenticate(data: AuthenticationParams): Promise
10 | }
11 |
--------------------------------------------------------------------------------
/src/domain/usecases/load-account-by-token.ts:
--------------------------------------------------------------------------------
1 | import { AccountModel } from '@domain/models/account'
2 |
3 | export interface LoadAccountByToken {
4 | load(accessToken: string, role?: string): Promise
5 | }
6 |
--------------------------------------------------------------------------------
/src/domain/usecases/load-survey-results.ts:
--------------------------------------------------------------------------------
1 | import { SurveyResultsModel } from '@domain/models/survey-results'
2 |
3 | export interface LoadSurveyResult {
4 | load(surveyId: string, accountId: string): Promise
5 | }
6 |
--------------------------------------------------------------------------------
/src/domain/usecases/load-survey.ts:
--------------------------------------------------------------------------------
1 | import { SurveyModel } from '@domain/models/survey'
2 |
3 | export interface LoadSurvey {
4 | load(accountId: string): Promise
5 | }
6 |
7 | export interface LoadSurveyById {
8 | loadById(id: string): Promise
9 | }
10 |
--------------------------------------------------------------------------------
/src/domain/usecases/save-survey-results.ts:
--------------------------------------------------------------------------------
1 | import { SurveyResultsModel } from '@domain/models/survey-results'
2 |
3 | export interface SaveSurveyResultParams {
4 | surveyId: string
5 | accountId: string
6 | answer: string
7 | date: Date
8 | }
9 |
10 | export interface SaveSurveyResults {
11 | save(data: SaveSurveyResultParams): Promise
12 | }
13 |
--------------------------------------------------------------------------------
/src/infra/cryptography/bcrypt-adapter/bcrypt-adapter.spec.ts:
--------------------------------------------------------------------------------
1 | import bcrypt from 'bcrypt'
2 | import { BcryptAdapter } from './bcrypt-adapter'
3 |
4 | jest.mock('bcrypt', () => ({
5 | async hash(): Promise {
6 | return new Promise(resolve => resolve('hash'))
7 | },
8 | async compare(): Promise {
9 | return new Promise(resolve => resolve(true))
10 | }
11 | }))
12 |
13 | describe('Bcrypt Adapter', () => {
14 | const salt = 12
15 | const sut = new BcryptAdapter(salt)
16 |
17 | describe('Hash', () => {
18 | test('Should call hash with correct values', async () => {
19 | const hashSpy = jest.spyOn(bcrypt, 'hash')
20 | await sut.hash('value')
21 | expect(hashSpy).toHaveBeenLastCalledWith('value', salt)
22 | })
23 |
24 | test('Should return a valid hash on hash success', async () => {
25 | const hash = await sut.hash('value')
26 | expect(hash).toBe('hash')
27 | })
28 |
29 | test('Should throw if hash throws', async () => {
30 | jest.spyOn(bcrypt, 'hash').mockRejectedValueOnce(new Error())
31 | const hashPromise = sut.hash('value')
32 | await expect(hashPromise).rejects.toThrow()
33 | })
34 | })
35 |
36 | describe('Hash Comparer', () => {
37 | test('Should call compare with correct values', async () => {
38 | const compareSpy = jest.spyOn(bcrypt, 'compare')
39 | await sut.compare('any_value', 'any_hash')
40 | expect(compareSpy).toHaveBeenLastCalledWith('any_value', 'any_hash')
41 | })
42 |
43 | test('Should return true when compare succeeds', async () => {
44 | const isValid = await sut.compare('any_value', 'any_hash')
45 | expect(isValid).toBe(true)
46 | })
47 |
48 | test('Should return false when compare fails', async () => {
49 | jest.spyOn(bcrypt, 'compare').mockResolvedValueOnce(false)
50 | const isValid = await sut.compare('any_value', 'any_hash')
51 | expect(isValid).toBe(false)
52 | })
53 |
54 | test('Should throw if compre throws', async () => {
55 | jest.spyOn(bcrypt, 'compare').mockRejectedValueOnce(new Error())
56 | const isValid = sut.compare('any_value', 'any_hash')
57 | await expect(isValid).rejects.toThrow()
58 | })
59 | })
60 | })
61 |
--------------------------------------------------------------------------------
/src/infra/cryptography/bcrypt-adapter/bcrypt-adapter.ts:
--------------------------------------------------------------------------------
1 | import { hash, compare } from 'bcrypt'
2 | import { Hasher } from '@data/interfaces/cryptography/hasher'
3 | import { HashComparer } from '@data/interfaces/cryptography/hash-comparer'
4 |
5 | export class BcryptAdapter implements Hasher, HashComparer {
6 | constructor(private readonly salt: number) {}
7 |
8 | async hash(value: string): Promise {
9 | const generatedHash = await hash(value, this.salt)
10 | return generatedHash
11 | }
12 |
13 | async compare(value: string, hash: string): Promise {
14 | const isValid = await compare(value, hash)
15 | return isValid
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/infra/cryptography/jwt-adapter/jwt-adapter.spec.ts:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken'
2 | import { JwtAdapter } from './jwt-adapter'
3 |
4 | jest.mock('jsonwebtoken', () => ({
5 | async sign(): Promise {
6 | return new Promise(resolve => resolve('any_token'))
7 | },
8 |
9 | async verify(): Promise {
10 | return new Promise(resolve => resolve('any_value'))
11 | }
12 | }))
13 |
14 | describe('Jwt Adapter', () => {
15 | const sut = new JwtAdapter('secret')
16 |
17 | describe('Encrypter', () => {
18 | test('Should call sign with correct values', async () => {
19 | const signSpy = jest.spyOn(jwt, 'sign')
20 | await sut.encrypt('any_value')
21 | expect(signSpy).toHaveBeenCalledWith({ id: 'any_value' }, 'secret')
22 | })
23 |
24 | test('Should return a token on sign success', async () => {
25 | const token = await sut.encrypt('any_value')
26 | expect(token).toBe('any_token')
27 | })
28 |
29 | test('Should throw if sign throws', async () => {
30 | jest.spyOn(jwt, 'sign').mockImplementationOnce(() => {
31 | throw new Error()
32 | })
33 | const errorPromise = sut.encrypt('any_value')
34 | await expect(errorPromise).rejects.toThrow()
35 | })
36 | })
37 |
38 | describe('Decrypter', () => {
39 | test('Should call verify with correct values', async () => {
40 | const verifySpy = jest.spyOn(jwt, 'verify')
41 | await sut.decript('any_token')
42 | expect(verifySpy).toHaveBeenCalledWith('any_token', 'secret')
43 | })
44 |
45 | test('Should return a value on verify success', async () => {
46 | const value = await sut.decript('any_token')
47 | expect(value).toBe('any_value')
48 | })
49 |
50 | test('Should throw if verify throws', async () => {
51 | jest.spyOn(jwt, 'verify').mockImplementationOnce(() => {
52 | throw new Error()
53 | })
54 | const errorPromise = sut.decript('any_token')
55 | await expect(errorPromise).rejects.toThrow()
56 | })
57 | })
58 | })
59 |
--------------------------------------------------------------------------------
/src/infra/cryptography/jwt-adapter/jwt-adapter.ts:
--------------------------------------------------------------------------------
1 | import { Decrypter } from '@data/interfaces/cryptography/decrypter'
2 | import { Encrypter } from '@data/interfaces/cryptography/encrypter'
3 | import jwt from 'jsonwebtoken'
4 |
5 | export class JwtAdapter implements Encrypter, Decrypter {
6 | constructor(private readonly secret: string) {}
7 |
8 | async encrypt(value: string): Promise {
9 | const token = jwt.sign({ id: value }, this.secret)
10 | return token
11 | }
12 |
13 | async decript(token: string): Promise {
14 | const value: any = jwt.verify(token, this.secret)
15 | return value
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/infra/db/mongodb/account/mongo-account-repository.spec.ts:
--------------------------------------------------------------------------------
1 | import { Collection } from 'mongodb'
2 | import { MongoHelper } from '../helpers/mongo'
3 | import { MongoAccountRepository } from './mongo-account-repository'
4 |
5 | describe('Account Mongodb Repository', () => {
6 | const sut = new MongoAccountRepository()
7 | let accountCollection: Collection
8 | const fakeAccount = { name: 'any_name', email: 'any@mail.com', password: 'any_password' }
9 |
10 | beforeAll(async () => {
11 | await MongoHelper.connect(process.env.MONGO_URL)
12 | })
13 | afterAll(async () => {
14 | await MongoHelper.disconnect()
15 | })
16 | beforeEach(async () => {
17 | accountCollection = await MongoHelper.getCollection('accounts')
18 | await accountCollection.deleteMany({})
19 | })
20 |
21 | describe('AddAccount', () => {
22 | test('Should return an account on add success', async () => {
23 | const account = await sut.add(fakeAccount)
24 | expect(account).toBeTruthy()
25 | expect(account.id).toBeTruthy()
26 | expect(account.email).toBe('any@mail.com')
27 | })
28 | })
29 |
30 | describe('LoadByEmail', () => {
31 | test('Should return an account on loadByEmail success', async () => {
32 | accountCollection.insertOne({ ...fakeAccount })
33 | const account = await sut.loadByEmail('any@mail.com')
34 | expect(account).toBeTruthy()
35 | expect(account.id).toBeTruthy()
36 | expect(account.email).toBe('any@mail.com')
37 | })
38 |
39 | test('Should return null if loadByEmail fails', async () => {
40 | const account = await sut.loadByEmail('any@mail.com')
41 | expect(account).toBeFalsy()
42 | })
43 | })
44 |
45 | describe('AccessToken', () => {
46 | test('Should create the account accessToken on AccessTokenSuccess', async () => {
47 | const result = await accountCollection.insertOne({ ...fakeAccount })
48 | const newAccount = result.ops[0]
49 | expect(newAccount.accessToken).toBeFalsy()
50 |
51 | await sut.storeAccessToken(newAccount._id, 'any_token')
52 | const account = await accountCollection.findOne({ _id: newAccount._id })
53 | expect(account).toBeTruthy()
54 | expect(account.accessToken).toBeTruthy()
55 | })
56 | })
57 |
58 | describe('LoadByToken', () => {
59 | test('Should return an account on loadByToken success without role', async () => {
60 | accountCollection.insertOne({ ...fakeAccount, accessToken: 'any_token' })
61 | const account = await sut.loadByToken('any_token')
62 | expect(account).toBeTruthy()
63 | expect(account.id).toBeTruthy()
64 | expect(account.email).toBe('any@mail.com')
65 | })
66 |
67 | test('Should return an account on loadByToken success with role', async () => {
68 | accountCollection.insertOne({ ...fakeAccount, accessToken: 'any_token', role: 'any_role' })
69 | const account = await sut.loadByToken('any_token', 'any_role')
70 | expect(account).toBeTruthy()
71 | expect(account.id).toBeTruthy()
72 | expect(account.email).toBe('any@mail.com')
73 | })
74 |
75 | test('Should return an account on loadByToken success with adm role', async () => {
76 | accountCollection.insertOne({ ...fakeAccount, accessToken: 'any_token', role: 'adm' })
77 | const account = await sut.loadByToken('any_token', 'adm')
78 | expect(account).toBeTruthy()
79 | expect(account.id).toBeTruthy()
80 | expect(account.email).toBe('any@mail.com')
81 | })
82 |
83 | test('Should return null on loadByToken success with invalid role', async () => {
84 | accountCollection.insertOne({ ...fakeAccount, accessToken: 'any_token' })
85 | const account = await sut.loadByToken('any_token', 'adm')
86 | expect(account).toBeFalsy()
87 | })
88 |
89 | test('Should return an account on loadByToken success if user is adm', async () => {
90 | accountCollection.insertOne({ ...fakeAccount, accessToken: 'any_token', role: 'adm' })
91 | const account = await sut.loadByToken('any_token')
92 | expect(account).toBeTruthy()
93 | expect(account.id).toBeTruthy()
94 | expect(account.email).toBe('any@mail.com')
95 | })
96 |
97 | test('Should return null if loadByToken fails', async () => {
98 | const account = await sut.loadByToken('any_token')
99 | expect(account).toBeFalsy()
100 | })
101 | })
102 | })
103 |
--------------------------------------------------------------------------------
/src/infra/db/mongodb/account/mongo-account-repository.ts:
--------------------------------------------------------------------------------
1 | import { AddAccountRepository } from '@data/interfaces/db/account/add-account-repository'
2 | import { AddAccountParams } from '@domain/usecases/add-account'
3 | import { AccountModel } from '@domain/models/account'
4 | import { MongoHelper } from '../helpers/index'
5 | import { LoadAccountRepository } from '@data/interfaces/db/account/load-account-repository'
6 | import { AccessTokenRepository } from '@data/interfaces/db/account/access-token-repository'
7 | import { LoadAccountByTokenRepository } from '@data/interfaces/db/account/load-account-by-token-repository'
8 |
9 | export class MongoAccountRepository // eslint-disable-next-line indent
10 | implements
11 | AddAccountRepository,
12 | LoadAccountRepository,
13 | AccessTokenRepository,
14 | LoadAccountByTokenRepository {
15 | async add(accountData: AddAccountParams): Promise {
16 | const accountCollection = await MongoHelper.getCollection('accounts')
17 | const result = await accountCollection.insertOne(accountData)
18 | const account = result.ops[0] // account inserted
19 |
20 | return MongoHelper.map(account)
21 | }
22 |
23 | async loadByEmail(email: string): Promise {
24 | const accountCollection = await MongoHelper.getCollection('accounts')
25 | const account = await accountCollection.findOne({ email })
26 |
27 | return account !== null ? MongoHelper.map(account) : null
28 | }
29 |
30 | async storeAccessToken(id: string, token: string): Promise {
31 | const accountCollection = await MongoHelper.getCollection('accounts')
32 | await accountCollection.updateOne(
33 | { _id: id },
34 | {
35 | $set: { accessToken: token }
36 | }
37 | )
38 | }
39 |
40 | async loadByToken(token: string, role?: string): Promise {
41 | const accountCollection = await MongoHelper.getCollection('accounts')
42 | const account = await accountCollection.findOne({
43 | accessToken: token,
44 | $or: [{ role }, { role: 'adm' }]
45 | })
46 |
47 | return account !== null ? MongoHelper.map(account) : null
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/infra/db/mongodb/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './mongo'
2 |
--------------------------------------------------------------------------------
/src/infra/db/mongodb/helpers/mongo.spec.ts:
--------------------------------------------------------------------------------
1 | import { MongoHelper as sut } from './mongo'
2 |
3 | describe('Mongo Helper', () => {
4 | beforeAll(async () => await sut.connect(process.env.MONGO_URL))
5 | afterAll(async () => await sut.disconnect())
6 |
7 | test('Should reconnect if mongodb is down', async () => {
8 | let accountCollection = await sut.getCollection('accounts')
9 | expect(accountCollection).toBeTruthy()
10 | await sut.disconnect()
11 | accountCollection = await sut.getCollection('accounts')
12 | expect(accountCollection).toBeTruthy()
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/src/infra/db/mongodb/helpers/mongo.ts:
--------------------------------------------------------------------------------
1 | import { Collection, MongoClient } from 'mongodb'
2 |
3 | export const MongoHelper = {
4 | client: null as MongoClient,
5 | url: null as string,
6 |
7 | async connect(url: string): Promise {
8 | this.url = url
9 | this.client = await MongoClient.connect(url, {
10 | useNewUrlParser: true,
11 | useUnifiedTopology: true
12 | })
13 | },
14 |
15 | async disconnect(): Promise {
16 | await this.client.close()
17 | this.client = null
18 | },
19 |
20 | async getCollection(name: string): Promise {
21 | if (!this.client?.isConnected()) await this.connect(this.url)
22 | return this.client.db().collection(name)
23 | },
24 |
25 | // Change the "_id" returned from Mongodb to "id"
26 | map(data: any): any {
27 | const { _id, ...presentationCollection } = data
28 | return Object.assign({}, presentationCollection, { id: _id })
29 | },
30 |
31 | mapCollection(collection: any[]): any[] {
32 | return collection.map(item => MongoHelper.map(item))
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/infra/db/mongodb/helpers/query-bulder.ts:
--------------------------------------------------------------------------------
1 | export class QueryBuilder {
2 | private readonly query = []
3 |
4 | match(data: object): QueryBuilder {
5 | this.query.push({ $match: data })
6 | return this
7 | }
8 |
9 | unwind(data: object): QueryBuilder {
10 | this.query.push({ $unwind: data })
11 | return this
12 | }
13 |
14 | lookup(data: object): QueryBuilder {
15 | this.query.push({ $lookup: data })
16 | return this
17 | }
18 |
19 | project(data: object): QueryBuilder {
20 | this.query.push({ $project: data })
21 | return this
22 | }
23 |
24 | group(data: object): QueryBuilder {
25 | this.query.push({ $group: data })
26 | return this
27 | }
28 |
29 | sort(data: object): QueryBuilder {
30 | this.query.push({ $sort: data })
31 | return this
32 | }
33 |
34 | build(): object[] {
35 | return this.query
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/infra/db/mongodb/log/mongo-log-repository.spec.ts:
--------------------------------------------------------------------------------
1 | import { Collection } from 'mongodb'
2 | import { MongoHelper } from '../helpers/mongo'
3 | import { MongoLogRepository } from './mongo-log-repository'
4 |
5 | describe('Log Mongo Repository', () => {
6 | const sut = new MongoLogRepository()
7 | let errorCollection: Collection
8 |
9 | beforeAll(async () => {
10 | await MongoHelper.connect(process.env.MONGO_URL)
11 | })
12 | afterAll(async () => {
13 | await MongoHelper.disconnect()
14 | })
15 | beforeEach(async () => {
16 | errorCollection = await MongoHelper.getCollection('errors')
17 | await errorCollection.deleteMany({})
18 | })
19 |
20 | test('Should create an error log on success', async () => {
21 | await sut.logError('any_stack')
22 | const count = await errorCollection.countDocuments()
23 | expect(count).toBe(1)
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/src/infra/db/mongodb/log/mongo-log-repository.ts:
--------------------------------------------------------------------------------
1 | import { LogErrorRepository } from '@data/interfaces/db/log/log-error-repository'
2 | import { MongoHelper } from '../helpers/mongo'
3 |
4 | export class MongoLogRepository implements LogErrorRepository {
5 | async logError(stack: string): Promise {
6 | const errorCollection = await MongoHelper.getCollection('errors')
7 | await errorCollection.insertOne({
8 | stack,
9 | date: new Date()
10 | })
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/infra/db/mongodb/survey-result/mongo-survey-results-repository.spec.ts:
--------------------------------------------------------------------------------
1 | import { Collection, ObjectId } from 'mongodb'
2 | import { fakeAccountModel } from '../../../../domain/mocks/mock-account'
3 | import { fakeSurveyModel } from '../../../../domain/mocks/mock-survey'
4 | import { AccountModel } from '../../../../domain/models/account'
5 | import { SurveyModel } from '../../../../domain/models/survey'
6 | import { MongoHelper } from '../helpers/mongo'
7 | import { MongoSurveyResultRepository } from './mongo-survey-results-repository'
8 |
9 | let surveyCollection: Collection
10 | let surveyResultsCollection: Collection
11 | let accountCollection: Collection
12 |
13 | const makeSurvey = async (): Promise => {
14 | const res = await surveyCollection.insertOne({
15 | question: 'any_question',
16 | answers: [
17 | {
18 | image: 'any_image',
19 | answer: 'any_answer_1'
20 | },
21 | {
22 | answer: 'any_answer_2'
23 | },
24 | {
25 | answer: 'any_answer_3'
26 | }
27 | ],
28 | date: new Date()
29 | })
30 | return MongoHelper.map(res.ops[0])
31 | }
32 |
33 | const makeAccount = async (): Promise => {
34 | const res = await accountCollection.insertOne({
35 | name: 'any_name',
36 | email: 'any_email@mail.com',
37 | password: 'any_password'
38 | })
39 | return MongoHelper.map(res.ops[0])
40 | }
41 |
42 | describe('Survey Mongo Repository', () => {
43 | const sut = new MongoSurveyResultRepository()
44 | beforeAll(async () => {
45 | await MongoHelper.connect(process.env.MONGO_URL)
46 | })
47 |
48 | afterAll(async () => {
49 | await MongoHelper.disconnect()
50 | })
51 |
52 | beforeEach(async () => {
53 | surveyCollection = await MongoHelper.getCollection('surveys')
54 | await surveyCollection.deleteMany({})
55 | surveyResultsCollection = await MongoHelper.getCollection('surveyResults')
56 | await surveyResultsCollection.deleteMany({})
57 | accountCollection = await MongoHelper.getCollection('accounts')
58 | await accountCollection.deleteMany({})
59 | })
60 |
61 | describe('save()', () => {
62 | test('Should add a survey result if its new', async () => {
63 | const survey = await makeSurvey()
64 | const account = await makeAccount()
65 | await sut.save({
66 | surveyId: survey.id,
67 | accountId: account.id,
68 | answer: survey.answers[0].answer,
69 | date: new Date()
70 | })
71 | const surveyResult = await surveyResultsCollection.findOne({
72 | surveyId: survey.id,
73 | accountId: account.id
74 | })
75 | expect(surveyResult).toBeTruthy()
76 | })
77 |
78 | test('Should update a survey result if its not new', async () => {
79 | const survey = await makeSurvey()
80 | const account = await makeAccount()
81 | await surveyResultsCollection.insertOne({
82 | surveyId: new ObjectId(survey.id),
83 | accountId: new ObjectId(account.id),
84 | answer: survey.answers[0].answer,
85 | date: new Date()
86 | })
87 | await sut.save({
88 | surveyId: survey.id,
89 | accountId: account.id,
90 | answer: survey.answers[1].answer,
91 | date: new Date()
92 | })
93 | const surveyResult = await surveyResultsCollection
94 | .find({
95 | surveyId: survey.id,
96 | accountId: account.id
97 | })
98 | .toArray()
99 | expect(surveyResult).toBeTruthy()
100 | expect(surveyResult.length).toBe(1)
101 | })
102 | })
103 |
104 | describe('LoadSurveyResult', () => {
105 | test('Should load a survey result', async () => {
106 | const insertedSurvey = await surveyCollection.insertOne(fakeSurveyModel)
107 | const survey = insertedSurvey.ops[0]
108 |
109 | const insertedAccount = await accountCollection.insertOne(fakeAccountModel)
110 | const account = insertedAccount.ops[0]
111 |
112 | await surveyResultsCollection.insertMany([
113 | {
114 | surveyId: new ObjectId(survey._id),
115 | answer: survey.answers[0].answer,
116 | accountId: new ObjectId(account._id),
117 | date: new Date()
118 | },
119 | {
120 | surveyId: new ObjectId(survey._id),
121 | answer: survey.answers[1].answer,
122 | accountId: new ObjectId(account._id),
123 | date: new Date()
124 | }
125 | ])
126 |
127 | const surveyResults = await sut.loadBySurveyId(survey._id, account._id)
128 | expect(surveyResults).toBeTruthy()
129 | expect(surveyResults.surveyId).toEqual(survey._id)
130 | expect(surveyResults.answers[0].count).toBe(1)
131 | expect(surveyResults.answers[0].percent).toBe(50)
132 | expect(surveyResults.answers[1].count).toBe(1)
133 | expect(surveyResults.answers[1].percent).toBe(50)
134 | })
135 | })
136 |
137 | test('Should return null if there is no surveys results', async () => {
138 | const insertedSurvey = await surveyCollection.insertOne(fakeSurveyModel)
139 | const insertedAccount = await surveyCollection.insertOne(fakeAccountModel)
140 | const account = insertedAccount.ops[0]
141 | const survey = insertedSurvey.ops[0]
142 | const surveyResults = await sut.loadBySurveyId(survey._id, account._id)
143 | expect(surveyResults).toBeNull()
144 | })
145 | })
146 |
--------------------------------------------------------------------------------
/src/infra/db/mongodb/survey-result/mongo-survey-results-repository.ts:
--------------------------------------------------------------------------------
1 | import { LoadSurveyResultRepository } from '@data/interfaces/db/survey/load-survey-results-repository'
2 | import { SaveSurveyResultsRepository } from '@data/interfaces/db/survey/save-survey-result-repository'
3 | import { SurveyResultsModel } from '@domain/models/survey-results'
4 | import { SaveSurveyResultParams } from '@domain/usecases/save-survey-results'
5 | import { ObjectId } from 'mongodb'
6 | import { MongoHelper } from '../helpers/mongo'
7 | import { QueryBuilder } from '../helpers/query-bulder'
8 |
9 | export class MongoSurveyResultRepository
10 | implements SaveSurveyResultsRepository, LoadSurveyResultRepository {
11 | async save(data: SaveSurveyResultParams): Promise {
12 | const surveyResultCollection = await MongoHelper.getCollection('surveyResults')
13 | await surveyResultCollection.findOneAndUpdate(
14 | { surveyId: new ObjectId(data.surveyId), accountId: new ObjectId(data.accountId) },
15 | { $set: { answer: data.answer, date: data.date } },
16 | { upsert: true }
17 | )
18 | }
19 |
20 | async loadBySurveyId(surveyId: string, accountId: string): Promise {
21 | const surveyResultCollection = await MongoHelper.getCollection('surveyResults')
22 | const query = new QueryBuilder()
23 | .match({
24 | surveyId: new ObjectId(surveyId)
25 | })
26 | .group({ _id: 0, data: { $push: '$$ROOT' }, total: { $sum: 1 } })
27 | .unwind({ path: '$data' })
28 | .lookup({
29 | from: 'surveys',
30 | foreignField: '_id',
31 | localField: 'data.surveyId',
32 | as: 'survey'
33 | })
34 | .unwind({ path: '$survey' })
35 | .group({
36 | _id: {
37 | surveyId: '$survey._id',
38 | question: '$survey.question',
39 | date: '$survey.date',
40 | total: '$total',
41 | answer: '$data.answer',
42 | answers: '$survey.answers'
43 | },
44 | count: { $sum: 1 },
45 | currentAccountResponse: {
46 | $push: { $cond: [{ $eq: ['$data.accountId', accountId] }, '$data.answer', null] }
47 | }
48 | })
49 | .project({
50 | _id: 0,
51 | surveyId: '$_id.surveyId',
52 | question: '$_id.question',
53 | date: '$_id.date',
54 | answers: {
55 | $map: {
56 | input: '$_id.answers',
57 | as: 'item',
58 | in: {
59 | $mergeObjects: [
60 | '$$item',
61 | {
62 | count: {
63 | $cond: {
64 | if: { $eq: ['$$item.answer', '$_id.answer'] },
65 | then: '$count',
66 | else: 0
67 | }
68 | },
69 | percent: {
70 | $cond: {
71 | if: { $eq: ['$$item.answer', '$_id.answer'] },
72 | then: {
73 | $multiply: [{ $divide: ['$count', '$_id.total'] }, 100]
74 | },
75 | else: 0
76 | }
77 | },
78 | isCurrentAccountResponse: {
79 | $eq: ['$$item.answer', { $arrayElemAt: ['$currentAccountResponse', 0] }]
80 | }
81 | }
82 | ]
83 | }
84 | }
85 | }
86 | })
87 | .group({
88 | _id: { surveyId: '$surveyId', question: '$question', date: '$date' },
89 | answers: { $push: '$answers' }
90 | })
91 | .project({
92 | _id: 0,
93 | surveyId: '$_id.surveyId',
94 | question: '$_id.question',
95 | date: '$_id.date',
96 | answers: {
97 | $reduce: {
98 | input: '$answers',
99 | initialValue: [],
100 | in: { $concatArrays: ['$$value', '$$this'] }
101 | }
102 | }
103 | })
104 | .unwind({ path: '$answers' })
105 | .group({
106 | _id: {
107 | surveyId: '$surveyId',
108 | question: '$question',
109 | date: '$date',
110 | answer: '$answers.answer',
111 | image: '$answers.image',
112 | isCurrentAccountResponse: '$answers.isCurrentAccountAnswer'
113 | },
114 | count: { $sum: '$answers.count' },
115 | percent: { $sum: '$answers.percent' }
116 | })
117 | .project({
118 | _id: 0,
119 | surveyId: '$_id.surveyId',
120 | question: '$_id.question',
121 | date: '$_id.date',
122 | answer: {
123 | answer: '$_id.answer',
124 | image: '$_id.image',
125 | count: { $round: ['$count'] },
126 | percent: '$percent',
127 | isCurrentAccountResponse: '$_id.isCurrentAccountAnswer'
128 | }
129 | })
130 | .sort({ 'answer.count': -1 })
131 | .group({
132 | _id: {
133 | surveyId: '$surveyId',
134 | question: '$question',
135 | date: '$date'
136 | },
137 | answers: { $push: '$answer' }
138 | })
139 | .project({
140 | _id: 0,
141 | surveyId: '$_id.surveyId',
142 | question: '$_id.question',
143 | date: '$_id.date',
144 | answers: '$answers'
145 | })
146 | .build()
147 |
148 | const surveyResult = await surveyResultCollection.aggregate(query).toArray()
149 | return surveyResult.length ? surveyResult[0] : null
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/src/infra/db/mongodb/survey/mongo-survey-repository.spec.ts:
--------------------------------------------------------------------------------
1 | import { Collection } from 'mongodb'
2 | import { fakeSurveyParams, fakeSurveysModel } from '../../../../domain/mocks/mock-survey'
3 | import { AccountModel } from '../../../../domain/models/account'
4 | import { MongoHelper } from '../helpers/mongo'
5 | import { MongoSurveyRepository } from './mongo-survey-repository'
6 |
7 | let surveyCollection: Collection
8 | let surveyResultCollection: Collection
9 | let accountCollection: Collection
10 |
11 | const makeAccount = async (): Promise => {
12 | const res = await accountCollection.insertOne({
13 | name: 'any_name',
14 | email: 'any_email@mail.com',
15 | password: 'any_password'
16 | })
17 | return MongoHelper.map(res.ops[0])
18 | }
19 |
20 | describe('Survey Mongodb Repository', () => {
21 | const sut = new MongoSurveyRepository()
22 |
23 | beforeAll(async () => await MongoHelper.connect(process.env.MONGO_URL))
24 | afterAll(async () => await MongoHelper.disconnect())
25 | beforeEach(async () => {
26 | surveyCollection = await MongoHelper.getCollection('surveys')
27 | accountCollection = await MongoHelper.getCollection('account')
28 | surveyResultCollection = await MongoHelper.getCollection('surveyResult')
29 | await surveyCollection.deleteMany({})
30 | await surveyResultCollection.deleteMany({})
31 | await accountCollection.deleteMany({})
32 | })
33 |
34 | describe('AddSurvey', () => {
35 | test('Should add a survey on success', async () => {
36 | await sut.add({ ...fakeSurveyParams, date: new Date() })
37 | const survey = await surveyCollection.findOne({ question: 'any_question' })
38 | expect(survey).toBeTruthy()
39 | })
40 | })
41 |
42 | describe('LoadAllSurveys', () => {
43 | test('Should load all surveys on success', async () => {
44 | const account = await makeAccount()
45 | const result = await surveyCollection.insertMany(fakeSurveysModel)
46 | const survey = result.ops[0]
47 | await surveyResultCollection.insertOne({
48 | accountId: account.id,
49 | date: new Date(),
50 | surveyId: survey._id,
51 | answer: survey.answers[0].answer
52 | })
53 | const surveys = await sut.loadAll(account.id)
54 | expect(surveys.length).toBe(2)
55 | expect(surveys[0].id).toBeTruthy()
56 | expect(surveys[0].didAnswer).toBeTruthy()
57 | expect(surveys[1].id).toBeTruthy()
58 | expect(surveys[1].didAnswer).toBeFalsy()
59 | })
60 |
61 | test('Should load an empty list if there are not surveys registered', async () => {
62 | const account = await makeAccount()
63 | const surveys = await sut.loadAll(account.id)
64 | expect(surveys.length).toBe(0)
65 | expect(surveys).toBeInstanceOf(Array)
66 | })
67 | })
68 |
69 | describe('LoadSurveyById', () => {
70 | test('Should load a survey by id on success', async () => {
71 | const response = await surveyCollection.insertOne({ ...fakeSurveyParams })
72 |
73 | const survey = await sut.loadById(response.ops[0]._id)
74 | expect(survey).toBeTruthy()
75 | expect(survey.question).toBe('any_question')
76 | })
77 | })
78 | })
79 |
--------------------------------------------------------------------------------
/src/infra/db/mongodb/survey/mongo-survey-repository.ts:
--------------------------------------------------------------------------------
1 | import { AddSurveyRepository } from '@data/interfaces/db/survey/add-survey-repository'
2 | import { LoadSurveyByIdRepository } from '@data/interfaces/db/survey/load-survey-by-id-repository'
3 | import { LoadSurveyRepository } from '@data/interfaces/db/survey/load-survey-repository'
4 | import { SurveyModel } from '@domain/models/survey'
5 | import { AddSurveyParams } from '@domain/usecases/add-survey'
6 | import { ObjectId } from 'mongodb'
7 | import { MongoHelper } from '../helpers/mongo'
8 | import { QueryBuilder } from '../helpers/query-bulder'
9 |
10 | export class MongoSurveyRepository
11 | implements AddSurveyRepository, LoadSurveyRepository, LoadSurveyByIdRepository {
12 | async add(surveyData: AddSurveyParams): Promise {
13 | const surveyCollection = await MongoHelper.getCollection('surveys')
14 | await surveyCollection.insertOne(surveyData)
15 | }
16 |
17 | async loadAll(accountId: string): Promise {
18 | const surveyCollection = await MongoHelper.getCollection('surveys')
19 | const query = new QueryBuilder()
20 | .lookup({
21 | from: 'surveyResult',
22 | foreignField: 'surveyId',
23 | localField: '_id',
24 | as: 'result'
25 | })
26 | .project({
27 | _id: 1,
28 | question: 1,
29 | answers: 1,
30 | date: 1,
31 | didAnswer: {
32 | $gte: [
33 | {
34 | $size: {
35 | $filter: {
36 | input: '$result',
37 | as: 'item',
38 | cond: { $eq: ['$$item.accountId', new ObjectId(accountId)] }
39 | }
40 | }
41 | },
42 | 1
43 | ]
44 | }
45 | })
46 | .build()
47 | const surveys = await surveyCollection.aggregate(query).toArray()
48 | return MongoHelper.mapCollection(surveys)
49 | }
50 |
51 | async loadById(id: string): Promise {
52 | const surveyCollection = await MongoHelper.getCollection('surveys')
53 | const survey = await surveyCollection.findOne({ _id: new ObjectId(id) })
54 | return survey && MongoHelper.map(survey)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/infra/validation/email-validator-adapter.spec.ts:
--------------------------------------------------------------------------------
1 | import { EmailValidatorAdapter } from './email-validator-adapter'
2 | import validator from 'validator'
3 |
4 | jest.mock('validator', () => ({
5 | isEmail(): boolean {
6 | return true
7 | }
8 | }))
9 |
10 | describe('EmailValidator Adapter', () => {
11 | const sut = new EmailValidatorAdapter()
12 |
13 | test('Should return false if validator return false', () => {
14 | jest.spyOn(validator, 'isEmail').mockReturnValueOnce(false)
15 | const isValidEmail = sut.isValid('invalid_email@gmail.com')
16 | expect(isValidEmail).toBe(false)
17 | })
18 |
19 | test('Should return true if validator return true', () => {
20 | const isValidEmail = sut.isValid('valid_email@gmail.com')
21 | expect(isValidEmail).toBe(true)
22 | })
23 |
24 | test('Should call validator with correct email', () => {
25 | const isEmailSpy = jest.spyOn(validator, 'isEmail')
26 | sut.isValid('valid_email@gmail.com')
27 | expect(isEmailSpy).toHaveBeenCalledWith('valid_email@gmail.com')
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/src/infra/validation/email-validator-adapter.ts:
--------------------------------------------------------------------------------
1 | import { EmailValidator } from '../../validation/interfaces/email-validator'
2 | import validator from 'validator'
3 |
4 | export class EmailValidatorAdapter implements EmailValidator {
5 | isValid(email: string): boolean {
6 | return validator.isEmail(email)
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/adapters/apollo-server-resolver-adapter.ts:
--------------------------------------------------------------------------------
1 | import { Controller } from '@presentation/interfaces'
2 | import {
3 | ApolloError,
4 | AuthenticationError,
5 | ForbiddenError,
6 | UserInputError
7 | } from 'apollo-server-errors'
8 |
9 | export const adaptResolver = async (controller: Controller, args?: any): Promise => {
10 | const response = await controller.handle({ ...(args || {}) })
11 | switch (response.statusCode) {
12 | case 200:
13 | return response.body
14 | case 204:
15 | return response.body
16 | case 400:
17 | throw new UserInputError(response.body.message)
18 | case 401:
19 | throw new AuthenticationError(response.body.message)
20 | case 403:
21 | throw new ForbiddenError(response.body.message)
22 | default:
23 | throw new ApolloError(response.body.message)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/adapters/express-middleware.ts:
--------------------------------------------------------------------------------
1 | import { Middleware } from '@presentation/interfaces/middleware'
2 | import { Request, Response, NextFunction } from 'express'
3 |
4 | export const adaptMiddleware = (middleware: Middleware) => {
5 | return async (req: Request, res: Response, next: NextFunction) => {
6 | const request = { accessToken: req.headers?.['access-token'], ...(req.headers || {}) }
7 | const httpResponse = await middleware.handle(request)
8 | if (httpResponse.statusCode === 200) {
9 | Object.assign(req, httpResponse.body)
10 | next()
11 | } else {
12 | res.status(httpResponse.statusCode).json({ error: httpResponse.body.message })
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/adapters/express-routes.ts:
--------------------------------------------------------------------------------
1 | import { Controller } from '@presentation/interfaces'
2 | import { Request, Response } from 'express'
3 |
4 | export const adaptRoutes = (controller: Controller) => {
5 | return async (req: Request, res: Response) => {
6 | const request = {
7 | ...(req.body || {}),
8 | ...(req.params || {}),
9 | accountId: req.accountId
10 | }
11 | const httpResponse = await controller.handle(request)
12 | if (httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299) {
13 | res.status(httpResponse.statusCode).json(httpResponse.body)
14 | } else {
15 | res.status(httpResponse.statusCode).json({ error: httpResponse.body.message })
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/config/apollo-server.ts:
--------------------------------------------------------------------------------
1 | import { Express } from 'express'
2 | import { ApolloServer } from 'apollo-server-express'
3 | import resolvers from '../graphql/resolvers'
4 | import typeDefs from '../graphql/type-defs'
5 | import { GraphQLError } from 'graphql'
6 | import schemaDirectives from '../graphql/directives/index'
7 |
8 | const handleErrors = (response: any, errors: readonly GraphQLError[]): void => {
9 | errors?.forEach(error => {
10 | response.data = undefined
11 | if (validateError(error, 'AuthenticationError')) response.http.status = 401
12 | else if (validateError(error, 'ForbiddenError')) response.http.status = 403
13 | else if (validateError(error, 'UserInputError')) response.http.status = 400
14 | else response.http.status = 500
15 | })
16 | }
17 |
18 | const validateError = (error: GraphQLError, errorName: string): boolean => {
19 | return [error.name, error.originalError?.name].some(name => name === errorName)
20 | }
21 |
22 | export default (app: Express) => {
23 | const server = new ApolloServer({
24 | resolvers,
25 | typeDefs,
26 | schemaDirectives,
27 | context: ({ req }) => ({ req }),
28 | plugins: [
29 | {
30 | requestDidStart: () => ({
31 | willSendResponse: ({ response, errors }) => handleErrors(response, errors)
32 | })
33 | }
34 | ]
35 | })
36 | server.applyMiddleware({ app })
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/config/app.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import Middlewares from '../config/middlewares'
3 | import Routes from '../config/routes'
4 | import Swagger from './swagger'
5 | import StaticFiles from './static-files'
6 | import ApolloServer from './apollo-server'
7 | // import ExceptRoute from './except-route'
8 |
9 | const app = express()
10 | // ExceptRoute(app)
11 | StaticFiles(app)
12 | ApolloServer(app)
13 | Swagger(app)
14 | Middlewares(app)
15 | Routes(app)
16 |
17 | export default app
18 |
--------------------------------------------------------------------------------
/src/main/config/env.ts:
--------------------------------------------------------------------------------
1 | import 'dotenv/config'
2 |
3 | export default {
4 | mongoUrl:
5 | process.env.MONGO_URL || 'mongodb://localhost:27017/clean-typescript-api',
6 | port: process.env.PORT || 8080,
7 | jwtSecret: process.env.JWT_SECRET_KEY || 'jw3R4fdaF74Fd7dfH'
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/config/except-route.ts:
--------------------------------------------------------------------------------
1 | import { Express } from 'express'
2 |
3 | export default (app: Express) => {
4 | app.use((req, res, next) => {
5 | res.status(404).json({
6 | message:
7 | "Sorry, this route doesn't exists. See documentation at https://clean-typescript-api.herokuapp.com/api/docs"
8 | })
9 | })
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/config/middlewares.ts:
--------------------------------------------------------------------------------
1 | import { Express } from 'express'
2 |
3 | import { bodyParser, contentType, cors } from '../middlewares/index'
4 | export default (app: Express): void => {
5 | app.use(bodyParser)
6 | app.use(cors)
7 | app.use(contentType)
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/config/routes.ts:
--------------------------------------------------------------------------------
1 | import { Express, Router } from 'express'
2 | import { readdirSync } from 'fs'
3 | import path from 'path'
4 |
5 | export default (app: Express): void => {
6 | const router = Router()
7 | app.use('/api', router)
8 | readdirSync(path.resolve(__dirname, '..', 'routes')).map(async file => {
9 | if (!file.includes('.test.') && !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('/api/static', express.static(resolve(__dirname, '..', '..', 'static')))
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/config/swagger.ts:
--------------------------------------------------------------------------------
1 | import { Express } from 'express'
2 | import { serve, setup } from 'swagger-ui-express'
3 | import swaggerConfig from '../docs/index'
4 | import { noCache } from '@main/middlewares/no-cache'
5 |
6 | export default (app: Express) => {
7 | app.use('/api/docs', noCache, serve, setup(swaggerConfig))
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/decorators/log-controller-decorator.spec.ts:
--------------------------------------------------------------------------------
1 | import { LogErrorRepository } from '../../data/interfaces/db/log/log-error-repository'
2 | import { fakeAccountModel, fakeAccountParams } from '../../domain/mocks/mock-account'
3 | import { ok, serverError } from '../../presentation/helpers/http/http'
4 | import { Controller, HttpResponse } from '../../presentation/interfaces'
5 | import { LogControllerDecorator } from './log-controller-decorator'
6 |
7 | const fakeRequest = fakeAccountParams
8 |
9 | const fakeError = new Error()
10 | fakeError.stack = 'error_stack'
11 | const fakeServerError = serverError(fakeError)
12 |
13 | class MockLogErrorRepository implements LogErrorRepository {
14 | async logError(stackError: string): Promise {
15 | return new Promise(resolve => resolve())
16 | }
17 | }
18 |
19 | class MockController implements Controller {
20 | async handle(httpRequest): Promise {
21 | return new Promise(resolve => resolve(ok(fakeAccountModel)))
22 | }
23 | }
24 |
25 | describe('LogController Decorator', () => {
26 | const mockController = new MockController() as jest.Mocked
27 | const mockLogErrorRepository = new MockLogErrorRepository() as jest.Mocked
28 | const sut = new LogControllerDecorator(mockController, mockLogErrorRepository)
29 |
30 | test('Should call controller handle', async () => {
31 | const handleSpy = jest.spyOn(mockController, 'handle')
32 | await sut.handle(fakeRequest)
33 | expect(handleSpy).toHaveBeenCalledWith(fakeRequest)
34 | })
35 |
36 | test('Should return the same result of the controller', async () => {
37 | const httpResponse = await sut.handle(fakeRequest)
38 | expect(httpResponse).toEqual(ok(fakeAccountModel))
39 | })
40 |
41 | test('Should call LogError with correct error if controller returns a server error', async () => {
42 | const logSpy = jest.spyOn(mockLogErrorRepository, 'logError')
43 | mockController.handle.mockResolvedValueOnce(fakeServerError)
44 | await sut.handle(fakeRequest)
45 | expect(logSpy).toHaveBeenCalledWith('error_stack')
46 | })
47 | })
48 |
--------------------------------------------------------------------------------
/src/main/decorators/log-controller-decorator.ts:
--------------------------------------------------------------------------------
1 | import { Controller, HttpResponse } from '@presentation/interfaces'
2 | import { LogErrorRepository } from '@data/interfaces/db/log/log-error-repository'
3 |
4 | export class LogControllerDecorator implements Controller {
5 | constructor(
6 | private readonly controller: Controller,
7 | private readonly logErrorRepository: LogErrorRepository
8 | ) {}
9 |
10 | async handle(httpRequest: any): Promise {
11 | const httpResponse = await this.controller.handle(httpRequest)
12 | if (httpResponse.statusCode === 500) {
13 | await this.logErrorRepository.logError(httpResponse.body.stack)
14 | }
15 | return httpResponse
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/docs/components/bad-request.ts:
--------------------------------------------------------------------------------
1 | export const badRequest = {
2 | description: 'Invalid Request',
3 | content: { 'application/json': { schema: { $ref: '#schemas/error' } } }
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/docs/components/forbidden.ts:
--------------------------------------------------------------------------------
1 | export const forbidden = {
2 | description: 'Access Denied',
3 | content: { 'application/json': { schema: { $ref: '#schemas/error' } } }
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/docs/components/not-found.ts:
--------------------------------------------------------------------------------
1 | export const notFound = {
2 | description: 'API not found'
3 | }
4 |
--------------------------------------------------------------------------------
/src/main/docs/components/server-error.ts:
--------------------------------------------------------------------------------
1 | export const serverError = {
2 | description: 'Internal Server Error',
3 | content: { 'application/json': { schema: { $ref: '#schemas/error' } } }
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/docs/components/unauthorized.ts:
--------------------------------------------------------------------------------
1 | export const unauthorized = {
2 | description: 'Invalid Credentials',
3 | content: { 'application/json': { schema: { $ref: '#schemas/error' } } }
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/docs/index.ts:
--------------------------------------------------------------------------------
1 | import { badRequest } from './components/bad-request'
2 | import { forbidden } from './components/forbidden'
3 | import { notFound } from './components/not-found'
4 | import { serverError } from './components/server-error'
5 | import { unauthorized } from './components/unauthorized'
6 | import { loginPath } from './paths/login-path'
7 | import { signUpPath } from './paths/signup-path'
8 | import { surveyResultPath } from './paths/survey-result-path'
9 | import { surveysPath } from './paths/surveys-path'
10 | import { accountSchema } from './schemas/account-schema'
11 | import { addSurveySchema } from './schemas/add-survey-schema'
12 | import { apiKeyAuthSchema } from './schemas/api-key-auth-schema'
13 | import { errorSchema } from './schemas/error-schema'
14 | import { loginSchema } from './schemas/login-schema'
15 | import { saveSurveySchema } from './schemas/save-survey-schema'
16 | import { signUpSchema } from './schemas/signup-schema'
17 | import { surveyAnswerSchema } from './schemas/survey-answer-schema'
18 | import { surveyResultAnswerSchema } from './schemas/survey-result-answer-schema'
19 | import { surveyResultSchema } from './schemas/survey-result-schema'
20 | import { surveySchema } from './schemas/survey-schema'
21 | import { surveysSchema } from './schemas/surveys-schema'
22 |
23 | export default {
24 | openapi: '3.0.0',
25 | info: {
26 | title: 'Clean Typescript API',
27 | description: 'An API made with Node.JS, Typescript and MongoDB',
28 | version: '1.5.0'
29 | },
30 | license: {
31 | name: 'MIT',
32 | url: 'https://github.com/gabriellopes00/clean-typescript-api/blob/main/LICENSE.md'
33 | },
34 | servers: [{ url: '/api' }],
35 | tags: [{ name: 'Login' }, { name: 'Surveys' }],
36 | paths: {
37 | '/login': loginPath,
38 | '/signup': signUpPath,
39 | '/surveys': surveysPath,
40 | '/surveys/{surveyId}/results': surveyResultPath
41 | },
42 | schemas: {
43 | account: accountSchema,
44 | login: loginSchema,
45 | error: errorSchema,
46 | survey: surveySchema,
47 | surveys: surveysSchema,
48 | surveyAnswer: surveyAnswerSchema,
49 | signup: signUpSchema,
50 | addSurvey: addSurveySchema,
51 | saveSurvey: saveSurveySchema,
52 | saveResultSurvey: surveyResultSchema,
53 | surveyResult: surveyResultSchema,
54 | surveyResultAnswer: surveyResultAnswerSchema
55 | },
56 | components: {
57 | securitySchemes: {
58 | apiKeyAuth: apiKeyAuthSchema
59 | },
60 | badRequest: badRequest,
61 | serverError: serverError,
62 | unauthorized: unauthorized,
63 | notFound: notFound,
64 | forbidden: forbidden
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/docs/paths/login-path.ts:
--------------------------------------------------------------------------------
1 | export const loginPath = {
2 | post: {
3 | tags: ['Login'],
4 | summary: 'User authentication API route',
5 | requestBody: {
6 | description:
7 | 'This path must receive an email and a password of the user which is trying to log in.',
8 | content: { 'application/json': { schema: { $ref: '#/schemas/login' } } }
9 | },
10 | responses: {
11 | 200: {
12 | description: 'Success',
13 | content: { 'application/json': { schema: { $ref: '#schemas/account' } } }
14 | },
15 | 400: { $ref: '#/components/badRequest' },
16 | 401: { $ref: '#/components/unauthorized' },
17 | 404: { $ref: '#/components/notFound' },
18 | 500: { $ref: '#/components/serverError' }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/docs/paths/signup-path.ts:
--------------------------------------------------------------------------------
1 | export const signUpPath = {
2 | post: {
3 | tags: ['Login'],
4 | summary: 'User account creation API',
5 | requestBody: {
6 | description:
7 | 'This path must receive a name, email, password and a password confirmation, of the user which is trying to create a new account.',
8 | content: { 'application/json': { schema: { $ref: '#/schemas/signup' } } }
9 | },
10 | responses: {
11 | 200: {
12 | description: "Success. Already return the user's login token",
13 | content: { 'application/json': { schema: { $ref: '#schemas/account' } } }
14 | },
15 | 400: { $ref: '#/components/badRequest' },
16 | 403: { $ref: '#/components/forbidden' },
17 | 404: { $ref: '#/components/notFound' },
18 | 500: { $ref: '#/components/serverError' }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/docs/paths/survey-result-path.ts:
--------------------------------------------------------------------------------
1 | export const surveyResultPath = {
2 | put: {
3 | security: [{ apiKeyAuth: [] }],
4 | tags: ['Surveys'],
5 | summary: 'Surveys response creation API',
6 | requestBody: {
7 | content: { 'application/json': { schema: { $ref: '#/schemas/saveSurvey' } } }
8 | },
9 | parameters: [{ in: 'path', name: 'surveyId', required: true, schema: { type: 'string' } }],
10 | responses: {
11 | 200: {
12 | description: 'Success',
13 | content: { 'application/json': { schema: { $ref: '#schemas/surveyResult' } } }
14 | },
15 | 403: { $ref: '#/components/forbidden' },
16 | 404: { $ref: '#/components/notFound' },
17 | 500: { $ref: '#/components/serverError' }
18 | }
19 | },
20 | get: {
21 | security: [{ apiKeyAuth: [] }],
22 | tags: ['Surveys'],
23 | summary: 'Surveys result query API',
24 | parameters: [{ in: 'path', name: 'surveyId', required: true, schema: { type: 'string' } }],
25 | responses: {
26 | 200: {
27 | description: 'Success',
28 | content: { 'application/json': { schema: { $ref: '#schemas/surveyResult' } } }
29 | },
30 | 403: { $ref: '#/components/forbidden' },
31 | 404: { $ref: '#/components/notFound' },
32 | 500: { $ref: '#/components/serverError' }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/docs/paths/surveys-path.ts:
--------------------------------------------------------------------------------
1 | export const surveysPath = {
2 | get: {
3 | security: [{ apiKeyAuth: [] }],
4 | tags: ['Surveys'],
5 | summary: 'Surveys list API',
6 | responses: {
7 | 200: {
8 | description: 'Success',
9 | content: { 'application/json': { schema: { $ref: '#schemas/surveys' } } }
10 | },
11 | 403: { $ref: '#/components/forbidden' },
12 | 404: { $ref: '#/components/notFound' },
13 | 500: { $ref: '#/components/serverError' }
14 | }
15 | },
16 | post: {
17 | security: [{ apiKeyAuth: [] }],
18 | tags: ['Surveys'],
19 | summary: 'Surveys creation API',
20 | requestBody: {
21 | content: { 'application/json': { schema: { $ref: '#/schemas/addSurvey' } } }
22 | },
23 | responses: {
24 | 204: { description: 'Success' },
25 | 403: { $ref: '#/components/forbidden' },
26 | 404: { $ref: '#/components/notFound' },
27 | 500: { $ref: '#/components/serverError' }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/docs/schemas/account-schema.ts:
--------------------------------------------------------------------------------
1 | export const accountSchema = {
2 | type: 'object',
3 | properties: { accessToken: { type: 'string' }, name: { type: 'string' } },
4 | required: ['accessToken', 'name']
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/docs/schemas/add-survey-schema.ts:
--------------------------------------------------------------------------------
1 | export const addSurveySchema = {
2 | type: 'object',
3 | properties: {
4 | question: { type: 'string' },
5 | answer: { type: 'array', items: { $ref: '#/schemas/surveyAnswer' } }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/docs/schemas/api-key-auth-schema.ts:
--------------------------------------------------------------------------------
1 | export const apiKeyAuthSchema = {
2 | type: 'apiKey',
3 | in: 'header',
4 | name: 'access-token'
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/docs/schemas/error-schema.ts:
--------------------------------------------------------------------------------
1 | export const errorSchema = {
2 | type: 'object',
3 | properties: { error: { type: 'string' } }
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/docs/schemas/login-schema.ts:
--------------------------------------------------------------------------------
1 | export const loginSchema = {
2 | type: 'object',
3 | properties: { email: { type: 'string' }, password: { type: 'string' } },
4 | required: ['email', 'password']
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/docs/schemas/save-survey-schema.ts:
--------------------------------------------------------------------------------
1 | export const saveSurveySchema = {
2 | type: 'object',
3 | properties: { answer: { type: 'string' } }
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/docs/schemas/signup-schema.ts:
--------------------------------------------------------------------------------
1 | export const signUpSchema = {
2 | type: 'object',
3 | properties: {
4 | name: { type: 'string' },
5 | email: { type: 'string' },
6 | password: { type: 'string' },
7 | passwordConfirmation: { type: 'string' }
8 | },
9 | required: ['name', 'email', 'password', 'passwordConfirmation']
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/docs/schemas/survey-answer-schema.ts:
--------------------------------------------------------------------------------
1 | export const surveyAnswerSchema = {
2 | type: 'object',
3 | properties: {
4 | image: { type: 'string' },
5 | answer: { type: 'string' }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/docs/schemas/survey-result-answer-schema.ts:
--------------------------------------------------------------------------------
1 | export const surveyResultAnswerSchema = {
2 | type: 'object',
3 | properties: {
4 | image: { type: 'string' },
5 | answer: { type: 'string' },
6 | count: { type: 'integer' },
7 | percent: { type: 'number' },
8 | isCurrentAccountResponse: { type: 'boolean' }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/docs/schemas/survey-result-schema.ts:
--------------------------------------------------------------------------------
1 | export const surveyResultSchema = {
2 | type: 'object',
3 | properties: {
4 | surveyId: { type: 'string' },
5 | question: { type: 'string' },
6 | answers: { type: 'array', items: { $ref: '#/schemas/surveyResultAnswer' } },
7 | date: { type: 'string' }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/docs/schemas/survey-schema.ts:
--------------------------------------------------------------------------------
1 | export const surveySchema = {
2 | type: 'object',
3 | properties: {
4 | id: { type: 'string' },
5 | question: { type: 'string' },
6 | date: { type: 'string' },
7 | answer: { type: 'array', items: { $ref: '#/schemas/surveyAnswer' } },
8 | didAnswer: { type: 'boolean' }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/docs/schemas/surveys-schema.ts:
--------------------------------------------------------------------------------
1 | export const surveysSchema = {
2 | type: 'array',
3 | items: { $ref: '#/schemas/survey' }
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/factories/add-survey/add-survey-controller-factory.ts:
--------------------------------------------------------------------------------
1 | import { Controller } from '@presentation/interfaces/controller'
2 | import { AddSurveyController } from '@presentation/controllers/survey/add-survey/add-survey'
3 | import { makeLogController } from '../decorators/log-controller-factory'
4 | import { makeAddSurveyValidation } from './add-survey-validations-factory'
5 | import { makeDbAddSurvey } from '../usecases/db-add-survey-factory'
6 |
7 | export const makeAddSurveyController = (): Controller => {
8 | const controller = new AddSurveyController(
9 | makeAddSurveyValidation(),
10 | makeDbAddSurvey()
11 | )
12 | return makeLogController(controller)
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/factories/add-survey/add-survey-validations-factory.spec.ts:
--------------------------------------------------------------------------------
1 | import { ValidationComposite } from '../../../validation/validator/validation-composite'
2 | import { RequiredFieldValidation } from '../../../validation/validator/required-fields-validation'
3 | import { Validation } from '../../../presentation/interfaces/validation'
4 | import { makeAddSurveyController } from './add-survey-controller-factory'
5 |
6 | jest.mock('../../../validation/validator/validation-composite')
7 |
8 | describe('AddSurveyValidation factory', () => {
9 | test('Should call ValidationComposite with correct validations', () => {
10 | makeAddSurveyController()
11 | const validations: Validation[] = []
12 | for (const field of ['question', 'answers']) {
13 | validations.push(new RequiredFieldValidation(field))
14 | }
15 |
16 | expect(ValidationComposite).toHaveBeenCalledWith(validations)
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/src/main/factories/add-survey/add-survey-validations-factory.ts:
--------------------------------------------------------------------------------
1 | import { ValidationComposite } from '../../../validation/validator/validation-composite'
2 | import { RequiredFieldValidation } from '../../../validation/validator/required-fields-validation'
3 | import { Validation } from '@presentation/interfaces/validation'
4 |
5 | export const makeAddSurveyValidation = (): ValidationComposite => {
6 | const validations: Validation[] = []
7 | for (const field of ['question', 'answers']) {
8 | validations.push(new RequiredFieldValidation(field))
9 | }
10 | return new ValidationComposite(validations)
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/factories/decorators/log-controller-factory.ts:
--------------------------------------------------------------------------------
1 | import { Controller } from '@presentation/interfaces/controller'
2 | import { MongoLogRepository } from '@infra/db/mongodb/log/mongo-log-repository'
3 | import { LogControllerDecorator } from '../../decorators/log-controller-decorator'
4 |
5 | export const makeLogController = (controller: Controller): Controller => {
6 | const mongoLogRepository = new MongoLogRepository()
7 |
8 | return new LogControllerDecorator(controller, mongoLogRepository)
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/factories/load-survey-result/load-survey-result-controller.ts:
--------------------------------------------------------------------------------
1 | import { LoadSurveyResultController } from '@presentation/controllers/survey-result/load-survey-result-controller'
2 | import { Controller } from '../../../presentation/interfaces/controller'
3 | import { makeLogController } from '../decorators/log-controller-factory'
4 | import { makeDbLoadSurveyById } from '../usecases/db-load-survey-by-id'
5 | import { makeDbLoadSurveyResult } from '../usecases/db-load-survey-result'
6 |
7 | export const makeLoadSurveyResultController = (): Controller => {
8 | const controller = new LoadSurveyResultController(
9 | makeDbLoadSurveyById(),
10 | makeDbLoadSurveyResult()
11 | )
12 | return makeLogController(controller)
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/factories/load-survey/load-survey-controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller } from '../../../presentation/interfaces/controller'
2 | import { LoadSurveyController } from '../../../presentation/controllers/survey/load-survey/load-survey-controller'
3 | import { makeLogController } from '../decorators/log-controller-factory'
4 | import { makeDbLoadSurvey } from '../usecases/db-load-survey'
5 |
6 | export const makeLoadSurveysController = (): Controller => {
7 | const controller = new LoadSurveyController(makeDbLoadSurvey())
8 | return makeLogController(controller)
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/factories/login/login-factory.ts:
--------------------------------------------------------------------------------
1 | import { Controller } from '@presentation/interfaces/controller'
2 | import { LoginController } from '@presentation/controllers/login/login-controller'
3 | import { makeLoginValidation } from '../../factories/login/login-validation-factory'
4 | import { makeDbAuthentication } from '../usecases/db-authentication-factory'
5 | import { makeLogController } from '../decorators/log-controller-factory'
6 |
7 | export const makeLoginController = (): Controller => {
8 | const controller = new LoginController(
9 | makeLoginValidation(),
10 | makeDbAuthentication()
11 | )
12 |
13 | return makeLogController(controller)
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/factories/login/login-validation-factory.spec.ts:
--------------------------------------------------------------------------------
1 | import { ValidationComposite } from '../../../validation/validator/validation-composite'
2 | import { RequiredFieldValidation } from '../../../validation/validator/required-fields-validation'
3 | import { Validation } from '../../../presentation/interfaces/validation'
4 | import { EmailValidation } from '../../../validation/validator/email-validation'
5 | import { EmailValidator } from '../../../validation/interfaces/email-validator'
6 | import { makeLoginValidation } from './login-validation-factory'
7 |
8 | jest.mock('../../../validation/validator/validation-composite')
9 |
10 | class MockEmailValidation implements EmailValidator {
11 | isValid(email: string): boolean {
12 | return true
13 | }
14 | }
15 |
16 | describe('LoginValidation factory', () => {
17 | const mockEmailValidation = new MockEmailValidation()
18 |
19 | test('Should call ValidationComposite with correct validations', () => {
20 | makeLoginValidation()
21 | const validations: Validation[] = []
22 | for (const field of ['email', 'password']) {
23 | validations.push(new RequiredFieldValidation(field))
24 | }
25 | validations.push(new EmailValidation('email', mockEmailValidation))
26 |
27 | expect(ValidationComposite).toHaveBeenCalledWith(validations)
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/src/main/factories/login/login-validation-factory.ts:
--------------------------------------------------------------------------------
1 | import { ValidationComposite } from '../../../validation/validator/validation-composite'
2 | import { RequiredFieldValidation } from '../../../validation/validator/required-fields-validation'
3 | import { Validation } from '@presentation/interfaces/validation'
4 | import { EmailValidation } from '../../../validation/validator/email-validation'
5 | import { EmailValidatorAdapter } from '@infra/validation/email-validator-adapter'
6 |
7 | export const makeLoginValidation = (): ValidationComposite => {
8 | const validations: Validation[] = []
9 | for (const field of ['email', 'password']) {
10 | validations.push(new RequiredFieldValidation(field))
11 | }
12 | validations.push(new EmailValidation('email', new EmailValidatorAdapter()))
13 | return new ValidationComposite(validations)
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/factories/middlewares/auth-middleware.ts:
--------------------------------------------------------------------------------
1 | import { Middleware } from '@presentation/interfaces/middleware'
2 | import { AuthMiddleware } from '@presentation/middlewares/auth-middleware'
3 | import { makeDbLoadAccountByToken } from '../usecases/db-load-account-by-token'
4 |
5 | export const makeAuthMiddleware = (role?: string): Middleware => {
6 | return new AuthMiddleware(makeDbLoadAccountByToken(), role)
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/factories/save-survey-result/save-survey-result-controller.ts:
--------------------------------------------------------------------------------
1 | import { SaveSurveyResultController } from '@presentation/controllers/survey-result/save-survey-result-controller'
2 | import { Controller } from '../../../presentation/interfaces/controller'
3 | import { makeLogController } from '../decorators/log-controller-factory'
4 | import { makeDbLoadSurveyById } from '../usecases/db-load-survey-by-id'
5 | import { makeDbSaveSurveyResult } from '../usecases/db-save-survey-result-factory'
6 |
7 | export const makeSaveSurveyResultController = (): Controller => {
8 | const controller = new SaveSurveyResultController(
9 | makeDbLoadSurveyById(),
10 | makeDbSaveSurveyResult()
11 | )
12 | return makeLogController(controller)
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/factories/signup/signup-factory.ts:
--------------------------------------------------------------------------------
1 | import { SignUpController } from '@presentation/controllers/signup/signup-controller'
2 | import { Controller } from '@presentation/interfaces'
3 | import { makeSignUpValidation } from './signup-validation-factory'
4 | import { makeDbAuthentication } from '../usecases/db-authentication-factory'
5 | import { makeDbAddAccount } from '../usecases/db-add-account-factory'
6 | import { makeLogController } from '../decorators/log-controller-factory'
7 |
8 | export const makeSignUpController = (): Controller => {
9 | const controller = new SignUpController(
10 | makeSignUpValidation(),
11 | makeDbAddAccount(),
12 | makeDbAuthentication()
13 | )
14 | return makeLogController(controller)
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/factories/signup/signup-validation-factory.spec.ts:
--------------------------------------------------------------------------------
1 | import { ValidationComposite } from '../../../validation/validator/validation-composite'
2 | import { RequiredFieldValidation } from '../../../validation/validator/required-fields-validation'
3 | import { Validation } from '../../../presentation/interfaces/validation'
4 | import { CompareFieldsValidation } from '../../../validation/validator/compare-fields-validation'
5 | import { EmailValidation } from '../../../validation/validator/email-validation'
6 | import { EmailValidator } from '../../../validation/interfaces/email-validator'
7 | import { makeSignUpValidation } from './signup-validation-factory'
8 |
9 | jest.mock('../../../validation/validator/validation-composite')
10 |
11 | class MockEmailValidation implements EmailValidator {
12 | isValid(email: string): boolean {
13 | return true
14 | }
15 | }
16 |
17 | describe('SignUpValidation factory', () => {
18 | const mockEmailValidation = new MockEmailValidation()
19 |
20 | test('Should call ValidationComposite with correct validations', () => {
21 | makeSignUpValidation()
22 | const validations: Validation[] = []
23 | for (const field of ['name', 'email', 'password', 'passwordConfirmation']) {
24 | validations.push(new RequiredFieldValidation(field))
25 | }
26 |
27 | validations.push(new CompareFieldsValidation('password', 'passwordConfirmation'))
28 | validations.push(new EmailValidation('email', mockEmailValidation))
29 | expect(ValidationComposite).toHaveBeenCalledWith(validations)
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/src/main/factories/signup/signup-validation-factory.ts:
--------------------------------------------------------------------------------
1 | import { ValidationComposite } from '../../../validation/validator/validation-composite'
2 | import { RequiredFieldValidation } from '../../../validation/validator/required-fields-validation'
3 | import { Validation } from '@presentation/interfaces/validation'
4 | import { CompareFieldsValidation } from '../../../validation/validator/compare-fields-validation'
5 | import { EmailValidation } from '../../../validation/validator/email-validation'
6 | import { EmailValidatorAdapter } from '@infra/validation/email-validator-adapter'
7 |
8 | export const makeSignUpValidation = (): ValidationComposite => {
9 | const validations: Validation[] = []
10 | for (const field of ['name', 'email', 'password', 'passwordConfirmation']) {
11 | validations.push(new RequiredFieldValidation(field))
12 | }
13 | validations.push(
14 | new CompareFieldsValidation('password', 'passwordConfirmation')
15 | )
16 | validations.push(new EmailValidation('email', new EmailValidatorAdapter()))
17 | return new ValidationComposite(validations)
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/factories/usecases/db-add-account-factory.ts:
--------------------------------------------------------------------------------
1 | import { DbAddAccount } from '@data/usecases/add-account/db-add-account'
2 | import { BcryptAdapter } from '@infra/cryptography/bcrypt-adapter/bcrypt-adapter'
3 | import { MongoAccountRepository } from '@infra/db/mongodb/account/mongo-account-repository'
4 | export const makeDbAddAccount = (): DbAddAccount => {
5 | const salt = 12
6 | const bcryptAdapter = new BcryptAdapter(salt)
7 |
8 | const mongoAccountRepository = new MongoAccountRepository()
9 | return new DbAddAccount(
10 | bcryptAdapter,
11 | mongoAccountRepository,
12 | mongoAccountRepository
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/factories/usecases/db-add-survey-factory.ts:
--------------------------------------------------------------------------------
1 | import { DbAddSurvey } from '@data/usecases/add-survey/db-add-survey'
2 | import { MongoSurveyRepository } from '@infra/db/mongodb/survey/mongo-survey-repository'
3 |
4 | export const makeDbAddSurvey = (): DbAddSurvey => {
5 | const mongoSurveyRepository = new MongoSurveyRepository()
6 | return new DbAddSurvey(mongoSurveyRepository)
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/factories/usecases/db-authentication-factory.ts:
--------------------------------------------------------------------------------
1 | import env from '../../config/env'
2 | import { MongoAccountRepository } from '@infra/db/mongodb/account/mongo-account-repository'
3 | import { BcryptAdapter } from '@infra/cryptography/bcrypt-adapter/bcrypt-adapter'
4 | import { JwtAdapter } from '@infra/cryptography/jwt-adapter/jwt-adapter'
5 | import { DbAuthentication } from '@data/usecases/authentication/db-authentication'
6 | import { Authenticator } from '@domain/usecases/authentication'
7 |
8 | export const makeDbAuthentication = (): Authenticator => {
9 | const salt = 12
10 | const bcryptAdapter = new BcryptAdapter(salt)
11 | const jwtAdapter = new JwtAdapter(env.jwtSecret)
12 | const mongoAccountRepository = new MongoAccountRepository()
13 |
14 | return new DbAuthentication(
15 | mongoAccountRepository,
16 | bcryptAdapter,
17 | jwtAdapter,
18 | mongoAccountRepository
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/factories/usecases/db-load-account-by-token.ts:
--------------------------------------------------------------------------------
1 | import { LoadAccountByToken } from '@domain/usecases/load-account-by-token'
2 | import { DbLoadAccountByToken } from '@data/usecases/load-account/db-load-account-by-token'
3 | import { MongoAccountRepository } from '@infra/db/mongodb/account/mongo-account-repository'
4 | import { JwtAdapter } from '@infra/cryptography/jwt-adapter/jwt-adapter'
5 | import env from '../../config/env'
6 |
7 | export const makeDbLoadAccountByToken = (): LoadAccountByToken => {
8 | const mongoAccountRepository = new MongoAccountRepository()
9 | const decrypter = new JwtAdapter(env.jwtSecret)
10 | return new DbLoadAccountByToken(decrypter, mongoAccountRepository)
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/factories/usecases/db-load-survey-by-id.ts:
--------------------------------------------------------------------------------
1 | import { DbLoadSurveyById } from '@data/usecases/load-survey-by-id/load-survey-by-id'
2 | import { LoadSurveyById } from '@domain/usecases/load-survey'
3 | import { MongoSurveyRepository } from '@infra/db/mongodb/survey/mongo-survey-repository'
4 |
5 | export const makeDbLoadSurveyById = (): LoadSurveyById => {
6 | const mongoSurveyRepository = new MongoSurveyRepository()
7 | return new DbLoadSurveyById(mongoSurveyRepository)
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/factories/usecases/db-load-survey-result.ts:
--------------------------------------------------------------------------------
1 | import { DbLoadSurveyResults } from '@data/usecases/load-survey-result/db-load-survey-result'
2 | import { LoadSurveyResult } from '@domain/usecases/load-survey-results'
3 | import { MongoSurveyResultRepository } from '@infra/db/mongodb/survey-result/mongo-survey-results-repository'
4 | import { MongoSurveyRepository } from '@infra/db/mongodb/survey/mongo-survey-repository'
5 |
6 | export const makeDbLoadSurveyResult = (): LoadSurveyResult => {
7 | const mongoSurveyResultRepository = new MongoSurveyResultRepository()
8 | const mongoSurveyRepository = new MongoSurveyRepository()
9 | return new DbLoadSurveyResults(mongoSurveyResultRepository, mongoSurveyRepository)
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/factories/usecases/db-load-survey.ts:
--------------------------------------------------------------------------------
1 | import { DbLoadSurvey } from '@data/usecases/load-survey/db-load-survey'
2 | import { LoadSurvey } from '@domain/usecases/load-survey'
3 | import { MongoSurveyRepository } from '@infra/db/mongodb/survey/mongo-survey-repository'
4 |
5 | export const makeDbLoadSurvey = (): LoadSurvey => {
6 | const mongoSurveyRepository = new MongoSurveyRepository()
7 | return new DbLoadSurvey(mongoSurveyRepository)
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/factories/usecases/db-save-survey-result-factory.ts:
--------------------------------------------------------------------------------
1 | import { DbSaveSurveyResults } from '@data/usecases/save-survey-result/db-save-survey-result'
2 | import { SaveSurveyResults } from '@domain/usecases/save-survey-results'
3 | import { MongoSurveyResultRepository } from '@infra/db/mongodb/survey-result/mongo-survey-results-repository'
4 |
5 | export const makeDbSaveSurveyResult = (): SaveSurveyResults => {
6 | const mongoSurveyResultRepository = new MongoSurveyResultRepository()
7 | return new DbSaveSurveyResults(mongoSurveyResultRepository, mongoSurveyResultRepository)
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/graphql/__tests__/helpers.ts:
--------------------------------------------------------------------------------
1 | import { ApolloServer } from 'apollo-server-express'
2 | import resolvers from '../resolvers'
3 | import typeDefs from '../type-defs'
4 | import schemaDirectives from '../directives'
5 |
6 | export const makeApolloServer = (): ApolloServer =>
7 | new ApolloServer({
8 | resolvers,
9 | typeDefs,
10 | schemaDirectives
11 | })
12 |
13 | describe('', () => {
14 | test('', () => {
15 | expect(1).toBe(1)
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/src/main/graphql/__tests__/login.test.ts:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-server-express'
2 | import { createTestClient } from 'apollo-server-integration-testing'
3 | import { hash } from 'bcrypt'
4 | import { Collection } from 'mongodb'
5 | import { fakeAccountParams } from '../../../domain/mocks/mock-account'
6 | import { MongoHelper } from '../../../infra/db/mongodb/helpers'
7 | import { makeApolloServer } from './helpers'
8 |
9 | describe('Login GraphQL', () => {
10 | let accountCollection: Collection
11 | const apolloServer = makeApolloServer()
12 |
13 | beforeAll(async () => await MongoHelper.connect(process.env.MONGO_URL))
14 | afterAll(async () => await MongoHelper.disconnect())
15 | beforeEach(async () => {
16 | accountCollection = await MongoHelper.getCollection('accounts')
17 | await accountCollection.deleteMany({})
18 | })
19 |
20 | describe('Login Query', () => {
21 | const loginQuery = gql`
22 | query login($email: String!, $password: String!) {
23 | login(email: $email, password: $password) {
24 | accessToken
25 | name
26 | }
27 | }
28 | `
29 | test('Should return an account on valid credentials', async () => {
30 | const hashedPassword = await hash(fakeAccountParams.password, 12)
31 | await accountCollection.insertOne({ ...fakeAccountParams, password: hashedPassword })
32 |
33 | const { query } = createTestClient({ apolloServer })
34 | const response: any = await query(loginQuery, {
35 | variables: {
36 | email: fakeAccountParams.email,
37 | password: fakeAccountParams.password
38 | }
39 | })
40 | expect(response.data.login.accessToken).toBeTruthy()
41 | })
42 |
43 | test('Should return an unauthorized on invalid credentials', async () => {
44 | const { query } = createTestClient({ apolloServer })
45 | const response: any = await query(loginQuery, {
46 | variables: {
47 | email: fakeAccountParams.email,
48 | password: fakeAccountParams.password
49 | }
50 | })
51 | expect(response.data).toBeFalsy()
52 | expect(response.errors[0].message).toBe('Unauthorized error')
53 | })
54 | })
55 |
56 | describe('SignUp Mutation', () => {
57 | const signupMutation = gql`
58 | mutation signup(
59 | $name: String!
60 | $passwordConfirmation: String!
61 | $email: String!
62 | $password: String!
63 | ) {
64 | signup(
65 | email: $email
66 | password: $password
67 | name: $name
68 | passwordConfirmation: $passwordConfirmation
69 | ) {
70 | accessToken
71 | name
72 | }
73 | }
74 | `
75 | test('Should return an account on valid data', async () => {
76 | const { mutate } = createTestClient({ apolloServer })
77 | const response: any = await mutate(signupMutation, {
78 | variables: {
79 | name: fakeAccountParams.name,
80 | email: fakeAccountParams.email,
81 | password: fakeAccountParams.password,
82 | passwordConfirmation: fakeAccountParams.password
83 | }
84 | })
85 | expect(response.data.signup.accessToken).toBeTruthy()
86 | })
87 | })
88 | })
89 |
--------------------------------------------------------------------------------
/src/main/graphql/directives/auth-directive.ts:
--------------------------------------------------------------------------------
1 | import { makeAuthMiddleware } from '@main/factories/middlewares/auth-middleware'
2 | import { ForbiddenError } from 'apollo-server-errors'
3 | import { defaultFieldResolver, GraphQLField } from 'graphql'
4 | import { SchemaDirectiveVisitor } from 'graphql-tools'
5 |
6 | export class AuthDirective extends SchemaDirectiveVisitor {
7 | visitFieldDefinition(field: GraphQLField): any {
8 | const { resolve = defaultFieldResolver } = field
9 | field.resolve = async (parent, args, context, info) => {
10 | const request = { accessToken: context?.req?.headers?.['access-token'] }
11 | const response = await makeAuthMiddleware().handle(request)
12 | if (response.statusCode === 200) {
13 | Object.assign(context?.req, response.body)
14 | return resolve.call(this, parent, args, context, info)
15 | } else throw new ForbiddenError(response.body.message)
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/graphql/directives/index.ts:
--------------------------------------------------------------------------------
1 | import { AuthDirective } from './auth-directive'
2 |
3 | export default { auth: AuthDirective }
4 |
--------------------------------------------------------------------------------
/src/main/graphql/resolvers/base.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLDateTime } from 'graphql-iso-date'
2 |
3 | export default {
4 | DateTime: GraphQLDateTime
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/graphql/resolvers/index.ts:
--------------------------------------------------------------------------------
1 | import login from './login'
2 | import surveys from './survey'
3 | import surveyResult from './survey-result'
4 | import base from './base'
5 |
6 | export default [login, surveys, base, surveyResult]
7 |
--------------------------------------------------------------------------------
/src/main/graphql/resolvers/login.ts:
--------------------------------------------------------------------------------
1 | import { adaptResolver } from '@main/adapters/apollo-server-resolver-adapter'
2 | import { makeLoginController } from '@main/factories/login/login-factory'
3 | import { makeSignUpController } from '@main/factories/signup/signup-factory'
4 |
5 | export default {
6 | Query: {
7 | login: async (parent: any, args: any) => adaptResolver(makeLoginController(), args)
8 | },
9 |
10 | Mutation: {
11 | signup: async (parent: any, args: any) => adaptResolver(makeSignUpController(), args)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/graphql/resolvers/survey-result.ts:
--------------------------------------------------------------------------------
1 | import { adaptResolver } from '@main/adapters/apollo-server-resolver-adapter'
2 | import { makeLoadSurveyResultController } from '@main/factories/load-survey-result/load-survey-result-controller'
3 | import { makeSaveSurveyResultController } from '@main/factories/save-survey-result/save-survey-result-controller'
4 |
5 | export default {
6 | Query: {
7 | surveyResult: async (parent: any, args: any) =>
8 | adaptResolver(makeLoadSurveyResultController(), args)
9 | },
10 |
11 | Mutation: {
12 | saveSurveyResult: async (parent: any, args: any) =>
13 | adaptResolver(makeSaveSurveyResultController(), args)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/graphql/resolvers/survey.ts:
--------------------------------------------------------------------------------
1 | import { adaptResolver } from '@main/adapters/apollo-server-resolver-adapter'
2 | import { makeLoadSurveysController } from '@main/factories/load-survey/load-survey-controller'
3 |
4 | export default {
5 | Query: {
6 | surveys: async () => adaptResolver(makeLoadSurveysController())
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 | type Account {
5 | accessToken: String!
6 | name: String!
7 | }
8 |
9 | extend type Query {
10 | login(email: String!, password: String!): Account!
11 | }
12 |
13 | extend type Mutation {
14 | signup(
15 | email: String!
16 | name: String!
17 | password: String!
18 | passwordConfirmation: String!
19 | ): Account!
20 | }
21 | `
22 |
--------------------------------------------------------------------------------
/src/main/graphql/type-defs/survey-result.ts:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-server-express'
2 |
3 | export default gql`
4 | type SurveyResult {
5 | surveyId: String!
6 | question: String!
7 | date: DateTime!
8 | answers: [Answer!]!
9 | didAnswer: Boolean
10 | }
11 |
12 | type Answer {
13 | image: String
14 | answer: String!
15 | count: Int!
16 | percent: Int!
17 | isCurrentAccountResponse: Boolean!
18 | }
19 |
20 | extend type Query {
21 | surveyResult(surveyId: String!): SurveyResult! @auth
22 | }
23 |
24 | extend type Mutation {
25 | saveSurveyResult(surveyId: String!, answer: String!): SurveyResult! @auth
26 | }
27 | `
28 |
--------------------------------------------------------------------------------
/src/main/graphql/type-defs/survey.ts:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-server-express'
2 |
3 | export default gql`
4 | type Survey {
5 | id: ID!
6 | question: String!
7 | answers: [SurveyAnswer!]!
8 | date: DateTime!
9 | didAnswer: Boolean
10 | }
11 |
12 | type SurveyAnswer {
13 | image: String
14 | answer: String!
15 | }
16 |
17 | extend type Query {
18 | surveys: [Survey!]! @auth
19 | }
20 | `
21 |
--------------------------------------------------------------------------------
/src/main/middlewares/adm-auth.ts:
--------------------------------------------------------------------------------
1 | import { adaptMiddleware } from '@main/adapters/express-middleware'
2 | import { makeAuthMiddleware } from '@main/factories/middlewares/auth-middleware'
3 |
4 | export const AdmAuth = adaptMiddleware(makeAuthMiddleware('admin'))
5 |
--------------------------------------------------------------------------------
/src/main/middlewares/auth.ts:
--------------------------------------------------------------------------------
1 | import { adaptMiddleware } from '@main/adapters/express-middleware'
2 | import { makeAuthMiddleware } from '@main/factories/middlewares/auth-middleware'
3 |
4 | export const Auth = adaptMiddleware(makeAuthMiddleware())
5 |
--------------------------------------------------------------------------------
/src/main/middlewares/body-parser.spec.ts:
--------------------------------------------------------------------------------
1 | import request from 'supertest'
2 | import app from '../config/app'
3 |
4 | describe('Body Parser Middleware', () => {
5 | const data = { name: 'any_name' }
6 |
7 | test('Should parser body as json', async () => {
8 | app.post('/test_body_parser', (req, res) => res.send(req.body))
9 | await request(app).post('/test_body_parser').send(data).expect(data)
10 | })
11 | })
12 |
--------------------------------------------------------------------------------
/src/main/middlewares/body-parser.ts:
--------------------------------------------------------------------------------
1 | import { json } from 'express'
2 |
3 | export const bodyParser = json()
4 |
--------------------------------------------------------------------------------
/src/main/middlewares/content-type.spec.ts:
--------------------------------------------------------------------------------
1 | import request from 'supertest'
2 | import app from '../config/app'
3 |
4 | describe('Json Content Type Middleware', () => {
5 | test('Should return default content type as json', async () => {
6 | app.get('/test_content_type', (req, res) => res.send(''))
7 | await request(app).get('/test_content_type').expect('content-type', /json/)
8 | })
9 |
10 | test('Should return default xml content when forced', async () => {
11 | app.get('/test_xml_content_type', (req, res) => {
12 | res.type('xml')
13 | res.send('')
14 | })
15 | await request(app).get('/test_xml_content_type').expect('content-type', /xml/)
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/src/main/middlewares/content-type.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from 'express'
2 |
3 | export const contentType = (
4 | req: Request,
5 | res: Response,
6 | next: NextFunction
7 | ) => {
8 | res.type('json')
9 | next()
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/middlewares/cors.test.ts:
--------------------------------------------------------------------------------
1 | import request from 'supertest'
2 | import app from '../config/app'
3 |
4 | describe('CORS Middleware', () => {
5 | test('Should enable CORS', async () => {
6 | app.get('/test_cors', (req, res) => res.send())
7 | await request(app)
8 | .post('/test_cors')
9 | .expect('access-control-allow-origin', '*')
10 | .expect('access-control-allow-methods', '*')
11 | .expect('access-control-allow-headers', '*')
12 | })
13 | })
14 |
--------------------------------------------------------------------------------
/src/main/middlewares/cors.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from 'express'
2 |
3 | export const cors = (req: Request, res: Response, next: NextFunction) => {
4 | res.set('access-control-allow-origin', '*')
5 | res.set('access-control-allow-methods', '*')
6 | res.set('access-control-allow-headers', '*')
7 | next()
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/middlewares/index.ts:
--------------------------------------------------------------------------------
1 | export * from './cors'
2 | export * from './content-type'
3 | export * from './body-parser'
4 |
--------------------------------------------------------------------------------
/src/main/middlewares/no-cache.test.ts:
--------------------------------------------------------------------------------
1 | import request from 'supertest'
2 | import app from '../config/app'
3 | import { noCache } from './no-cache'
4 |
5 | describe('NoCache Middleware', () => {
6 | test('Should disable cache', async () => {
7 | app.get('/test_cache', noCache, (req, res) => res.send())
8 | await request(app)
9 | .get('/test_cache')
10 | .expect('cache-control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
11 | .expect('pragma', 'no-cache')
12 | .expect('expires', '0')
13 | .expect('surrogate-control', 'no-store')
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/src/main/middlewares/no-cache.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from 'express'
2 |
3 | export const noCache = (req: Request, res: Response, next: NextFunction) => {
4 | res.set('cache-control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
5 | res.set('pragma', 'no-cache')
6 | res.set('expires', '0')
7 | res.set('surrogate-control', 'no-store')
8 | next()
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/routes/login-routes.test.ts:
--------------------------------------------------------------------------------
1 | import { hash } from 'bcrypt'
2 | import { Collection } from 'mongodb'
3 | import request from 'supertest'
4 | import { fakeAccountParams, fakeLoginParams } from '../../domain/mocks/mock-account'
5 | import { MongoHelper } from '../../infra/db/mongodb/helpers/index'
6 | import app from '../config/app'
7 |
8 | let accountCollection: Collection
9 |
10 | describe('Login Routes', () => {
11 | beforeAll(async () => await MongoHelper.connect(process.env.MONGO_URL))
12 | afterAll(async () => await MongoHelper.disconnect())
13 | beforeEach(async () => {
14 | accountCollection = await MongoHelper.getCollection('accounts')
15 | await accountCollection.deleteMany({})
16 | })
17 |
18 | test('Should return 200 on login', async () => {
19 | const hashedPassword = await hash(fakeAccountParams.password, 12)
20 | await accountCollection.insertOne({ ...fakeAccountParams, password: hashedPassword })
21 |
22 | app.post('/api/login', (req, res) => res.send())
23 | await request(app).post('/api/login').send(fakeLoginParams).expect(200)
24 | })
25 |
26 | test('Should return 401 on unauthorized request', async () => {
27 | app.post('/api/login', (req, res) => res.send())
28 | await request(app).post('/api/login').send(fakeLoginParams).expect(401)
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/src/main/routes/logn-routes.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'express'
2 | import { makeLoginController } from '../factories/login/login-factory'
3 | import { adaptRoutes } from '../adapters/express-routes'
4 |
5 | const loginController = makeLoginController()
6 |
7 | export default (router: Router): void => {
8 | router.post('/login', adaptRoutes(loginController))
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/routes/signup-routes.test.ts:
--------------------------------------------------------------------------------
1 | import request from 'supertest'
2 | import { fakeAccountParams } from '../../domain/mocks/mock-account'
3 | import { MongoHelper } from '../../infra/db/mongodb/helpers/index'
4 | import app from '../config/app'
5 |
6 | describe('SigUp Routes', () => {
7 | beforeAll(async () => await MongoHelper.connect(process.env.MONGO_URL))
8 | afterAll(async () => await MongoHelper.disconnect())
9 | beforeEach(async () => {
10 | const accountCollection = await MongoHelper.getCollection('accounts')
11 | await accountCollection.deleteMany({})
12 | })
13 |
14 | test('Should return 200 on sign', async () => {
15 | app.post('/api/signup', (req, res) => res.send())
16 | await request(app)
17 | .post('/api/signup')
18 | .send({ ...fakeAccountParams, passwordConfirmation: 'any_password' })
19 | .expect(200)
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/src/main/routes/signup-routes.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'express'
2 | import { makeSignUpController } from '../factories/signup/signup-factory'
3 | import { adaptRoutes } from '../adapters/express-routes'
4 |
5 | const SignUpController = makeSignUpController()
6 |
7 | export default (router: Router): void => {
8 | router.post('/signup', adaptRoutes(SignUpController))
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/routes/survey-result-routes.test.ts:
--------------------------------------------------------------------------------
1 | import request from 'supertest'
2 | import { Collection } from 'mongodb'
3 | import app from '../config/app'
4 | import { MongoHelper } from '../../infra/db/mongodb/helpers/mongo'
5 | import { sign } from 'jsonwebtoken'
6 | import env from '../config/env'
7 | import { fakeSurveyParams } from '../../domain/mocks/mock-survey'
8 | import { fakeAccountParams } from '../../domain/mocks/mock-account'
9 |
10 | let surveyCollection: Collection
11 | let accountCollection: Collection
12 |
13 | describe('Survey Routes', () => {
14 | beforeAll(async () => await MongoHelper.connect(process.env.MONGO_URL))
15 | afterAll(async () => await MongoHelper.disconnect())
16 | beforeEach(async () => {
17 | surveyCollection = await MongoHelper.getCollection('surveys')
18 | await surveyCollection.deleteMany({})
19 |
20 | accountCollection = await MongoHelper.getCollection('accounts')
21 | await accountCollection.deleteMany({})
22 | })
23 |
24 | describe('PUT /surveys/:surveyId/results', () => {
25 | test('Should return 403 on save survey result without accessToken', async () => {
26 | await request(app)
27 | .put('/api/surveys/any_id/results')
28 | .send({ answer: 'any_answer' })
29 | .expect(403)
30 | })
31 |
32 | test('Should return 200 on save survey result with accessToken', async () => {
33 | const res = await surveyCollection.insertOne({ ...fakeSurveyParams, date: new Date() })
34 |
35 | const account = await accountCollection.insertOne(fakeAccountParams)
36 | const id = account.ops[0]._id
37 | const accessToken = sign({ id }, env.jwtSecret)
38 | await accountCollection.updateOne({ _id: id }, { $set: { accessToken } })
39 |
40 | await request(app)
41 | .put(`/api/surveys/${res.ops[0]._id}/results`)
42 | .set('access-token', accessToken)
43 | .send({ answer: 'any_answer' })
44 | .expect(200)
45 | })
46 | })
47 |
48 | describe('GET /surveys/:surveyId/results', () => {
49 | test('Should return 403 on GET, without accessToken', async () => {
50 | await request(app).get('/api/surveys/any_id/results').expect(403)
51 | })
52 |
53 | test('Should return 200 on load survey result with accessToken', async () => {
54 | const res = await surveyCollection.insertOne({ ...fakeSurveyParams, date: new Date() })
55 |
56 | const account = await accountCollection.insertOne(fakeAccountParams)
57 | const id = account.ops[0]._id
58 | const accessToken = sign({ id }, env.jwtSecret)
59 | await accountCollection.updateOne({ _id: id }, { $set: { accessToken } })
60 |
61 | await request(app)
62 | .get(`/api/surveys/${res.ops[0]._id}/results`)
63 | .set('access-token', accessToken)
64 | .expect(200)
65 | })
66 | })
67 | })
68 |
--------------------------------------------------------------------------------
/src/main/routes/survey-results-routes.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'express'
2 | import { adaptRoutes } from '../adapters/express-routes'
3 | import { makeSaveSurveyResultController } from '@main/factories/save-survey-result/save-survey-result-controller'
4 | import { makeLoadSurveyResultController } from '@main/factories/load-survey-result/load-survey-result-controller'
5 | import { Auth } from '@main/middlewares/auth'
6 |
7 | const saveSurveyResultController = makeSaveSurveyResultController()
8 | const loadSurveyResultController = makeLoadSurveyResultController()
9 |
10 | export default (router: Router): void => {
11 | router.put('/surveys/:surveyId/results', Auth, adaptRoutes(saveSurveyResultController))
12 | router.get('/surveys/:surveyId/results', Auth, adaptRoutes(loadSurveyResultController))
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/routes/survey-routes.test.ts:
--------------------------------------------------------------------------------
1 | import request from 'supertest'
2 | import { Collection } from 'mongodb'
3 | import app from '../config/app'
4 | import { MongoHelper } from '../../infra/db/mongodb/helpers/mongo'
5 | import { sign } from 'jsonwebtoken'
6 | import env from '../config/env'
7 | import { fakeSurveyParams } from '../../domain/mocks/mock-survey'
8 | import { fakeAccountParams } from '../../domain/mocks/mock-account'
9 |
10 | let surveyCollection: Collection
11 | let accountCollection: Collection
12 |
13 | describe('Survey Routes', () => {
14 | beforeAll(async () => await MongoHelper.connect(process.env.MONGO_URL))
15 | afterAll(async () => await MongoHelper.disconnect())
16 | beforeEach(async () => {
17 | surveyCollection = await MongoHelper.getCollection('surveys')
18 | await surveyCollection.deleteMany({})
19 |
20 | accountCollection = await MongoHelper.getCollection('accounts')
21 | await accountCollection.deleteMany({})
22 | })
23 |
24 | describe('POST /surveys', () => {
25 | test('Should return 403 on add survey without accessToken', async () => {
26 | await request(app).post('/api/surveys').send(fakeSurveyParams).expect(403)
27 | })
28 |
29 | test('Should return 204 on add survey with valid accessToken', async () => {
30 | const account = await accountCollection.insertOne({ ...fakeAccountParams, role: 'adm' })
31 | const id = account.ops[0]._id
32 | const accessToken = sign({ id }, env.jwtSecret)
33 | await accountCollection.updateOne({ _id: id }, { $set: { accessToken } })
34 |
35 | await request(app)
36 | .post('/api/surveys')
37 | .set('access-token', accessToken)
38 | .send(fakeSurveyParams)
39 | .expect(204)
40 | })
41 | })
42 |
43 | describe('GET /surveys', () => {
44 | test('Should return 403 on load survey without accessToken', async () => {
45 | await request(app).get('/api/surveys').expect(403)
46 | })
47 |
48 | test('Should return 204 on load survey with valid accessToken', async () => {
49 | const account = await accountCollection.insertOne(fakeAccountParams)
50 | const id = account.ops[0]._id
51 | const accessToken = sign({ id }, env.jwtSecret)
52 | await accountCollection.updateOne({ _id: id }, { $set: { accessToken } })
53 | await request(app).get('/api/surveys').set('access-token', accessToken).expect(204)
54 | })
55 | })
56 | })
57 |
--------------------------------------------------------------------------------
/src/main/routes/survey-routes.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'express'
2 | import { adaptRoutes } from '../adapters/express-routes'
3 | import { makeAddSurveyController } from '@main/factories/add-survey/add-survey-controller-factory'
4 | import { makeLoadSurveysController } from '@main/factories/load-survey/load-survey-controller'
5 | import { AdmAuth } from '@main/middlewares/adm-auth'
6 | import { Auth } from '@main/middlewares/auth'
7 |
8 | const addSurveyController = makeAddSurveyController()
9 | const loadSurveyController = makeLoadSurveysController()
10 |
11 | export default (router: Router): void => {
12 | router.post('/surveys', AdmAuth, adaptRoutes(addSurveyController))
13 | router.get('/surveys', Auth, adaptRoutes(loadSurveyController))
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/server.ts:
--------------------------------------------------------------------------------
1 | import { MongoHelper } from '@infra/db/mongodb/helpers/index'
2 | import env from './config/env'
3 |
4 | MongoHelper.connect(env.mongoUrl)
5 | .then(async () => {
6 | console.log(`Mongodb connected successfully at ${env.mongoUrl}`)
7 |
8 | const app = (await import('./config/app')).default
9 | const port = env.port
10 |
11 | app.listen(port, () => {
12 | console.log('Server running at http://localhost:' + port)
13 | })
14 | })
15 | .catch(err => console.error(err))
16 |
--------------------------------------------------------------------------------
/src/presentation/controllers/login/login-controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { fakeLoginParams } from '../../../domain/mocks/mock-account'
2 | import { AuthenticationModel } from '../../../domain/models/account'
3 | import { AuthenticationParams, Authenticator } from '../../../domain/usecases/authentication'
4 | import { MissingParamError } from '../../errors'
5 | import { badRequest, ok, serverError, unauthorized } from '../../helpers/http/http'
6 | import { Validation } from '../../interfaces/validation'
7 | import { LoginController } from './login-controller'
8 |
9 | class MockValidation implements Validation {
10 | validate(input: any): Error {
11 | return null
12 | }
13 | }
14 |
15 | class MockAuthenticator implements Authenticator {
16 | async authenticate(data: AuthenticationParams): Promise {
17 | return new Promise(resolve => resolve({ accessToken: 'access_token', name: 'any_name' }))
18 | }
19 | }
20 |
21 | export const fakeRequest: LoginController.Request = { ...fakeLoginParams }
22 |
23 | describe('Login Controller', () => {
24 | const mockValidation = new MockValidation() as jest.Mocked
25 | const mockAuthenticator = new MockAuthenticator() as jest.Mocked
26 | const sut = new LoginController(mockValidation, mockAuthenticator)
27 |
28 | describe('Authentication', () => {
29 | test('Should call Authenticator with correct values', async () => {
30 | const authenticateSpy = jest.spyOn(mockAuthenticator, 'authenticate')
31 | await sut.handle(fakeRequest)
32 | expect(authenticateSpy).toHaveBeenCalledWith({ ...fakeLoginParams })
33 | })
34 |
35 | test('Should return 401 if invalid credentials are provided', async () => {
36 | mockAuthenticator.authenticate.mockReturnValueOnce(null)
37 | const httpResponse = await sut.handle(fakeRequest)
38 | expect(httpResponse).toEqual(unauthorized())
39 | })
40 |
41 | test('Should return 500 if Authenticators throws', async () => {
42 | mockAuthenticator.authenticate.mockRejectedValueOnce(new Error())
43 | const httpResponse = await sut.handle(fakeRequest)
44 | expect(httpResponse).toEqual(serverError(new Error()))
45 | })
46 |
47 | test('Should return 200 if valid credentials are provided', async () => {
48 | const httpResponse = await sut.handle(fakeRequest)
49 | expect(httpResponse).toEqual(ok({ accessToken: 'access_token', name: 'any_name' }))
50 | })
51 | })
52 |
53 | describe('Validation', () => {
54 | test('Should call Validation with correct values', async () => {
55 | const validateSpy = jest.spyOn(mockValidation, 'validate')
56 | const httpRequest = fakeRequest
57 | await sut.handle(httpRequest)
58 | expect(validateSpy).toHaveBeenCalledWith(httpRequest)
59 | })
60 |
61 | test('Should return 400 if Validation returns an Error', async () => {
62 | // could be any error (below)
63 | mockValidation.validate.mockReturnValueOnce(new MissingParamError('any_field'))
64 | const httpResponse = await sut.handle(fakeRequest)
65 | expect(httpResponse).toEqual(badRequest(new MissingParamError('any_field')))
66 | })
67 | })
68 | })
69 |
--------------------------------------------------------------------------------
/src/presentation/controllers/login/login-controller.ts:
--------------------------------------------------------------------------------
1 | import { badRequest, serverError, unauthorized, ok } from '../../helpers/http/http'
2 | import { Authenticator } from '@domain/usecases/authentication'
3 | import { Controller, HttpResponse, Validation } from '../../interfaces'
4 |
5 | export class LoginController implements Controller {
6 | constructor(
7 | private readonly validation: Validation,
8 | private readonly authenticator: Authenticator
9 | ) {}
10 |
11 | async handle(request: LoginController.Request): Promise {
12 | try {
13 | const error = this.validation.validate(request)
14 | if (error) return badRequest(error)
15 |
16 | const authModel = await this.authenticator.authenticate(request)
17 | if (!authModel) return unauthorized()
18 |
19 | return ok(authModel)
20 | } catch (error) {
21 | return serverError(error)
22 | }
23 | }
24 | }
25 |
26 | export namespace LoginController {
27 | export interface Request {
28 | email: string
29 | password: string
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/presentation/controllers/signup/signup-controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { fakeAccountModel, fakeAccountParams } from '../../../domain/mocks/mock-account'
2 | import { AccountModel, AuthenticationModel } from '../../../domain/models/account'
3 | import { AddAccount, AddAccountParams } from '../../../domain/usecases/add-account'
4 | import { AuthenticationParams, Authenticator } from '../../../domain/usecases/authentication'
5 | import {
6 | EmailAlreadyInUseError,
7 | MissingParamError,
8 | ServerError
9 | } from '../../../presentation/errors/index'
10 | import { badRequest, forbidden, ok, serverError } from '../../helpers/http/http'
11 | import { Validation } from '../../interfaces/validation'
12 | import { SignUpController } from './signup-controller'
13 |
14 | class MockAddAccount implements AddAccount {
15 | async add(account: AddAccountParams): Promise {
16 | return new Promise(resolve => resolve(fakeAccountModel))
17 | }
18 | }
19 |
20 | class MockValidation implements Validation {
21 | validate(input: any): Error {
22 | return null
23 | }
24 | }
25 |
26 | class MockAuthenticator implements Authenticator {
27 | async authenticate(data: AuthenticationParams): Promise {
28 | return new Promise(resolve => resolve({ accessToken: 'access_token', name: 'any_name' }))
29 | }
30 | }
31 |
32 | export const fakeRequest: SignUpController.Request = {
33 | name: fakeAccountParams.name,
34 | email: fakeAccountParams.email,
35 | password: fakeAccountParams.password,
36 | passwordConfirmation: 'any_password'
37 | }
38 |
39 | describe('SingUp Controller', () => {
40 | const mockValidation = new MockValidation() as jest.Mocked
41 | const mockAddAccount = new MockAddAccount() as jest.Mocked
42 | const mockAuthenticator = new MockAuthenticator() as jest.Mocked
43 | const sut = new SignUpController(mockValidation, mockAddAccount, mockAuthenticator)
44 |
45 | describe('AddAccount', () => {
46 | test('Should call AddAccount with correct values', async () => {
47 | const addSpy = jest.spyOn(mockAddAccount, 'add')
48 | await sut.handle(fakeRequest)
49 | expect(addSpy).toHaveBeenCalledWith({ ...fakeAccountParams })
50 | })
51 |
52 | test('Should return 500 if AddAccount throws', async () => {
53 | mockAddAccount.add.mockRejectedValueOnce(new Error())
54 | const httpResponse = await sut.handle(fakeRequest)
55 | expect(httpResponse).toEqual(serverError(new ServerError()))
56 | })
57 |
58 | test('Should return 200 if valid data is provided', async () => {
59 | const httpResponse = await sut.handle(fakeRequest)
60 | expect(httpResponse).toEqual(ok({ accessToken: 'access_token', name: 'any_name' }))
61 | })
62 |
63 | test('Should return 403 if addAccount returns null', async () => {
64 | mockAddAccount.add.mockResolvedValueOnce(null)
65 | const httpResponse = await sut.handle(fakeRequest)
66 | expect(httpResponse).toEqual(forbidden(new EmailAlreadyInUseError()))
67 | })
68 | })
69 |
70 | describe('Validations', () => {
71 | test('Should call Validation with correct values', async () => {
72 | const validateSpy = jest.spyOn(mockValidation, 'validate')
73 | const httpRequest = fakeRequest
74 | await sut.handle(httpRequest)
75 | expect(validateSpy).toHaveBeenCalledWith(httpRequest)
76 | })
77 |
78 | test('Should return 400 if Validation returns an Error', async () => {
79 | // could be any error (below)
80 | mockValidation.validate.mockReturnValueOnce(new MissingParamError('any_field'))
81 | const httpResponse = await sut.handle(fakeRequest)
82 | expect(httpResponse).toEqual(badRequest(new MissingParamError('any_field')))
83 | })
84 | })
85 |
86 | describe('Authentication', () => {
87 | test('Should call Authenticator with correct values', async () => {
88 | const authenticateSpy = jest.spyOn(mockAuthenticator, 'authenticate')
89 | await sut.handle(fakeRequest)
90 | expect(authenticateSpy).toHaveBeenCalledWith({
91 | email: fakeAccountParams.email,
92 | password: fakeAccountParams.password
93 | })
94 | })
95 |
96 | test('Should return 500 if Authenticators throws', async () => {
97 | mockAuthenticator.authenticate.mockRejectedValueOnce(new Error())
98 | const httpResponse = await sut.handle(fakeRequest)
99 | expect(httpResponse).toEqual(serverError(new Error()))
100 | })
101 | })
102 | })
103 |
--------------------------------------------------------------------------------
/src/presentation/controllers/signup/signup-controller.ts:
--------------------------------------------------------------------------------
1 | import { badRequest, serverError, ok, forbidden } from '@presentation/helpers/http/http'
2 | import { AddAccount } from '@domain/usecases/add-account'
3 | import { Authenticator } from '@domain/usecases/authentication'
4 | import { EmailAlreadyInUseError } from '@presentation/errors'
5 | import { Controller, Validation, HttpResponse } from '@presentation/interfaces'
6 |
7 | export class SignUpController implements Controller {
8 | constructor(
9 | private readonly validation: Validation,
10 | private readonly addAccount: AddAccount,
11 | private readonly authentication: Authenticator
12 | ) {}
13 |
14 | async handle(request: SignUpController.Request): Promise {
15 | try {
16 | const error = this.validation.validate(request)
17 | if (error) return badRequest(error)
18 |
19 | const { email, name, password } = request
20 | const account = await this.addAccount.add({ name, email, password })
21 | if (!account) return forbidden(new EmailAlreadyInUseError())
22 | const authModel = await this.authentication.authenticate({ email, password })
23 |
24 | return ok(authModel)
25 | } catch (error) {
26 | return serverError(error)
27 | }
28 | }
29 | }
30 |
31 | export namespace SignUpController {
32 | export interface Request {
33 | name: string
34 | email: string
35 | password: string
36 | passwordConfirmation: string
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/presentation/controllers/survey-result/load-survey-result-controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { LoadSurveyResultController } from './load-survey-result-controller'
2 | import { LoadSurveyById } from '../../../domain/usecases/load-survey'
3 | import { SurveyModel } from '../../../domain/models/survey'
4 | import { InvalidParamError } from '../../errors/invalid-param'
5 | import { forbidden, ok, serverError } from '../../helpers/http/http'
6 | import { LoadSurveyResult } from '../../../domain/usecases/load-survey-results'
7 | import { SurveyResultsModel } from '../../../domain/models/survey-results'
8 | import { fakeSurveyResultModel } from '../../../domain/mocks/mock-survey-result'
9 | import { fakeSurveyModel } from '../../../domain/mocks/mock-survey'
10 | import mockDate from 'mockdate'
11 |
12 | const fakeRequest: LoadSurveyResultController.Request = { accountId: 'any_id', surveyId: 'any_id' }
13 |
14 | class MockLoadSurveyById implements LoadSurveyById {
15 | async loadById(id: string): Promise {
16 | return fakeSurveyModel
17 | }
18 | }
19 |
20 | class MockLoadSurveyResult implements LoadSurveyResult {
21 | async load(surveyId: string, accountId: string): Promise {
22 | return fakeSurveyResultModel
23 | }
24 | }
25 |
26 | describe('LoadSurvey Controller', () => {
27 | const mockLoadSurveyById = new MockLoadSurveyById() as jest.Mocked
28 | const mockLoadSurveyResult = new MockLoadSurveyResult() as jest.Mocked
29 | const sut = new LoadSurveyResultController(mockLoadSurveyById, mockLoadSurveyResult)
30 |
31 | beforeAll(() => mockDate.set(new Date()))
32 | afterAll(() => mockDate.reset())
33 |
34 | test('Should call LoadSurveyById with correct values', async () => {
35 | const loadSpy = jest.spyOn(mockLoadSurveyById, 'loadById')
36 | await sut.handle(fakeRequest)
37 | expect(loadSpy).toHaveBeenCalledWith(fakeRequest.surveyId)
38 | })
39 |
40 | test('Should return 403 if an invalid survey id is provided', async () => {
41 | mockLoadSurveyById.loadById.mockResolvedValueOnce(null)
42 | const response = await sut.handle(fakeRequest)
43 | expect(response).toEqual(forbidden(new InvalidParamError('surveyId')))
44 | })
45 |
46 | test('Should return 500 if LoadSurveyById throws', async () => {
47 | mockLoadSurveyById.loadById.mockRejectedValueOnce(new Error())
48 | const response = await sut.handle(fakeRequest)
49 | expect(response).toEqual(serverError(new Error()))
50 | })
51 |
52 | test('Should call LoadSurveyResult with correct values', async () => {
53 | const loadSpy = jest.spyOn(mockLoadSurveyResult, 'load')
54 | await sut.handle(fakeRequest)
55 | expect(loadSpy).toHaveBeenCalledWith(fakeRequest.surveyId, fakeRequest.accountId)
56 | })
57 |
58 | test('Should return 500 if LoadSurveyResult throws', async () => {
59 | mockLoadSurveyResult.load.mockRejectedValueOnce(new Error())
60 | const response = await sut.handle(fakeRequest)
61 | expect(response).toEqual(serverError(new Error()))
62 | })
63 |
64 | test('Should return 200 on success', async () => {
65 | const response = await sut.handle(fakeRequest)
66 | expect(response).toEqual(ok(fakeSurveyResultModel))
67 | })
68 | })
69 |
--------------------------------------------------------------------------------
/src/presentation/controllers/survey-result/load-survey-result-controller.ts:
--------------------------------------------------------------------------------
1 | import { LoadSurveyById } from '@domain/usecases/load-survey'
2 | import { forbidden, ok, serverError } from '@presentation/helpers/http/http'
3 | import { InvalidParamError } from '@presentation/errors'
4 | import { Controller, HttpResponse } from '@presentation/interfaces'
5 | import { LoadSurveyResult } from '@domain/usecases/load-survey-results'
6 |
7 | export class LoadSurveyResultController implements Controller {
8 | constructor(
9 | private readonly loadSurveyById: LoadSurveyById,
10 | private readonly loadSurveyResult: LoadSurveyResult
11 | ) {}
12 |
13 | async handle(request: LoadSurveyResultController.Request): Promise {
14 | try {
15 | const { surveyId } = request
16 | const survey = await this.loadSurveyById.loadById(surveyId)
17 | if (!survey) return forbidden(new InvalidParamError('surveyId'))
18 |
19 | const surveyResult = await this.loadSurveyResult.load(surveyId, request.accountId)
20 | return ok(surveyResult)
21 | } catch (error) {
22 | return serverError(error)
23 | }
24 | }
25 | }
26 |
27 | export namespace LoadSurveyResultController {
28 | export interface Request {
29 | surveyId: string
30 | accountId: string
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/presentation/controllers/survey-result/save-survey-result-controller.spec.ts:
--------------------------------------------------------------------------------
1 | import mockDate from 'mockdate'
2 | import { fakeSurveyModel } from '../../../domain/mocks/mock-survey'
3 | import { fakeSurveyResultModel } from '../../../domain/mocks/mock-survey-result'
4 | import { SurveyModel } from '../../../domain/models/survey'
5 | import { SurveyResultsModel } from '../../../domain/models/survey-results'
6 | import { LoadSurveyById } from '../../../domain/usecases/load-survey'
7 | import {
8 | SaveSurveyResultParams,
9 | SaveSurveyResults
10 | } from '../../../domain/usecases/save-survey-results'
11 | import { InvalidParamError } from '../../errors/invalid-param'
12 | import { forbidden, ok, serverError } from '../../helpers/http/http'
13 | import { SaveSurveyResultController } from './save-survey-result-controller'
14 |
15 | class MockLoadSurveyById implements LoadSurveyById {
16 | async loadById(id: string): Promise {
17 | return fakeSurveyModel
18 | }
19 | }
20 |
21 | class MockSaveSurveyResult implements SaveSurveyResults {
22 | async save(data: SaveSurveyResultParams): Promise {
23 | return fakeSurveyResultModel
24 | }
25 | }
26 |
27 | describe('Save survey result controller', () => {
28 | const mockLoadSurveyById = new MockLoadSurveyById() as jest.Mocked
29 | const mockSaveSurveyResult = new MockSaveSurveyResult() as jest.Mocked
30 | const sut = new SaveSurveyResultController(mockLoadSurveyById, mockSaveSurveyResult)
31 |
32 | const fakeRequest: SaveSurveyResultController.Request = {
33 | surveyId: 'any_id',
34 | answer: 'any_answer',
35 | accountId: 'any_account_id'
36 | }
37 |
38 | beforeAll(() => mockDate.set(new Date()))
39 | afterAll(() => mockDate.reset())
40 |
41 | describe('Load Survey By Id', () => {
42 | test('Should call LoadSurveyById with correct values', async () => {
43 | const loadSpy = jest.spyOn(mockLoadSurveyById, 'loadById')
44 | await sut.handle(fakeRequest)
45 | expect(loadSpy).toHaveBeenCalledWith('any_id')
46 | })
47 |
48 | test('Should return 403 if LoadSurveyById returns null', async () => {
49 | mockLoadSurveyById.loadById.mockResolvedValueOnce(null)
50 | const response = await sut.handle(fakeRequest)
51 | expect(response).toEqual(forbidden(new InvalidParamError('surveyId')))
52 | })
53 |
54 | test('Should return 500 if LoadSurveyById throws', async () => {
55 | mockLoadSurveyById.loadById.mockRejectedValueOnce(new Error())
56 | const response = await sut.handle(fakeRequest)
57 | expect(response).toEqual(serverError(new Error()))
58 | })
59 | })
60 |
61 | describe('Save Survey Result', () => {
62 | test('Should call SaveSurveyResult with correct values', async () => {
63 | const saveSpy = jest.spyOn(mockSaveSurveyResult, 'save')
64 | await sut.handle(fakeRequest)
65 | expect(saveSpy).toHaveBeenCalledWith({
66 | surveyId: 'any_id',
67 | accountId: 'any_account_id',
68 | date: new Date(),
69 | answer: 'any_answer'
70 | })
71 | })
72 |
73 | test('Should return 403 if an invalid response is provided', async () => {
74 | const response = await sut.handle({
75 | surveyId: 'any_id',
76 | answer: 'wrong_answer',
77 | accountId: 'any_account_id'
78 | })
79 | expect(response).toEqual(forbidden(new InvalidParamError('answer')))
80 | })
81 |
82 | test('Should return 500 if SaveSurveyResults throws', async () => {
83 | mockSaveSurveyResult.save.mockRejectedValueOnce(new Error())
84 | const response = await sut.handle(fakeRequest)
85 | expect(response).toEqual(serverError(new Error()))
86 | })
87 |
88 | test('Should return 200 on SaveSurveyResults success', async () => {
89 | const response = await sut.handle(fakeRequest)
90 | expect(response).toEqual(ok(fakeSurveyResultModel))
91 | })
92 | })
93 | })
94 |
--------------------------------------------------------------------------------
/src/presentation/controllers/survey-result/save-survey-result-controller.ts:
--------------------------------------------------------------------------------
1 | import { LoadSurveyById } from '@domain/usecases/load-survey'
2 | import { SaveSurveyResults } from '@domain/usecases/save-survey-results'
3 | import { InvalidParamError } from '@presentation/errors'
4 | import { forbidden, ok, serverError } from '@presentation/helpers/http/http'
5 | import { Controller, HttpResponse } from '../../interfaces/'
6 |
7 | export class SaveSurveyResultController implements Controller {
8 | constructor(
9 | private readonly loadSurveyById: LoadSurveyById,
10 | private readonly saveSurveyResult: SaveSurveyResults
11 | ) {}
12 |
13 | async handle(request: SaveSurveyResultController.Request): Promise {
14 | try {
15 | const { surveyId, answer, accountId } = request
16 | const survey = await this.loadSurveyById.loadById(surveyId)
17 | if (!survey) return forbidden(new InvalidParamError('surveyId'))
18 |
19 | const answers = survey.answers.map(a => a.answer)
20 | if (!answers.includes(answer)) return forbidden(new InvalidParamError('answer'))
21 |
22 | const surveyResult = await this.saveSurveyResult.save({
23 | accountId,
24 | surveyId,
25 | answer,
26 | date: new Date()
27 | })
28 | return ok(surveyResult)
29 | } catch (error) {
30 | console.log(error)
31 | return serverError(error) // λ
32 | }
33 | }
34 | }
35 |
36 | export namespace SaveSurveyResultController {
37 | export interface Request {
38 | surveyId: string
39 | accountId: string
40 | answer: string
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/presentation/controllers/survey/add-survey/add-survey.spec.ts:
--------------------------------------------------------------------------------
1 | import MockDate from 'mockdate'
2 | import { fakeSurveyParams } from '../../../../domain/mocks/mock-survey'
3 | import { AddSurvey, AddSurveyParams } from '../../../../domain/usecases/add-survey'
4 | import { badRequest, noContent, serverError } from '../../../helpers/http/http'
5 | import { Validation } from '../../../interfaces'
6 | import { AddSurveyController } from './add-survey'
7 |
8 | class MockValidation implements Validation {
9 | validate(input: any): Error {
10 | return null
11 | }
12 | }
13 |
14 | class MockAddSurvey implements AddSurvey {
15 | async add(data: AddSurveyParams): Promise {
16 | return new Promise(resolve => resolve())
17 | }
18 | }
19 |
20 | describe('AddSurvey Controller', () => {
21 | const mockValidation = new MockValidation() as jest.Mocked
22 | const mockAddSurvey = new MockAddSurvey() as jest.Mocked
23 | const sut = new AddSurveyController(mockValidation, mockAddSurvey)
24 |
25 | beforeAll(() => MockDate.set(new Date()))
26 | afterAll(() => MockDate.reset)
27 |
28 | const fakeRequest: AddSurveyController.Request = {
29 | question: fakeSurveyParams.question,
30 | answers: fakeSurveyParams.answers
31 | }
32 |
33 | describe('Validation', () => {
34 | test('Should call Validation with correct values', async () => {
35 | const validateSpy = jest.spyOn(mockValidation, 'validate')
36 | await sut.handle(fakeRequest)
37 | expect(validateSpy).toHaveBeenCalledWith(fakeRequest)
38 | })
39 |
40 | test('Should return 400 if Validation fails', async () => {
41 | mockValidation.validate.mockReturnValueOnce(new Error())
42 | const httpResponse = await sut.handle(fakeRequest)
43 | expect(httpResponse).toEqual(badRequest(new Error()))
44 | })
45 | })
46 |
47 | describe('AddSurvey', () => {
48 | test('Should call AddSurvey with correct values', async () => {
49 | const addSpy = jest.spyOn(mockAddSurvey, 'add')
50 | await sut.handle(fakeRequest)
51 | expect(addSpy).toHaveBeenCalledWith({ ...fakeRequest, date: new Date() })
52 | })
53 |
54 | test('Should return 500 if AddSurvey throws', async () => {
55 | mockAddSurvey.add.mockRejectedValueOnce(new Error())
56 | const error = await sut.handle(fakeRequest)
57 | expect(error).toEqual(serverError(new Error()))
58 | })
59 |
60 | test('Should return 204 on success', async () => {
61 | const httpResponse = await sut.handle(fakeRequest)
62 | expect(httpResponse).toEqual(noContent())
63 | })
64 | })
65 | })
66 |
--------------------------------------------------------------------------------
/src/presentation/controllers/survey/add-survey/add-survey.ts:
--------------------------------------------------------------------------------
1 | import { AddSurvey } from '@domain/usecases/add-survey'
2 | import { badRequest, noContent, serverError } from '@presentation/helpers/http/http'
3 | import { Controller, HttpResponse, Validation } from '@presentation/interfaces'
4 |
5 | export class AddSurveyController implements Controller {
6 | constructor(private readonly validation: Validation, private readonly addSurvey: AddSurvey) {}
7 |
8 | async handle(request: AddSurveyController.Request): Promise {
9 | try {
10 | const error = this.validation.validate(request)
11 | if (error) return badRequest(error)
12 |
13 | await this.addSurvey.add({
14 | date: new Date(),
15 | question: request.question,
16 | answers: request.answers
17 | })
18 | return noContent()
19 | } catch (error) {
20 | return serverError(error)
21 | }
22 | }
23 | }
24 |
25 | export namespace AddSurveyController {
26 | interface Answer {
27 | image?: string
28 | answer: string
29 | }
30 |
31 | export interface Request {
32 | question: string
33 | answers: Answer[]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/presentation/controllers/survey/load-survey/load-survey-controller.spec.ts:
--------------------------------------------------------------------------------
1 | import MockDate from 'mockdate'
2 | import { fakeSurveysModel } from '../../../../domain/mocks/mock-survey'
3 | import { SurveyModel } from '../../../../domain/models/survey'
4 | import { LoadSurvey } from '../../../../domain/usecases/load-survey'
5 | import { noContent, ok, serverError } from '../../../helpers/http/http'
6 | import { LoadSurveyController } from './load-survey-controller'
7 |
8 | class MockLoadSurvey implements LoadSurvey {
9 | async load(): Promise {
10 | return new Promise(resolve => resolve(fakeSurveysModel))
11 | }
12 | }
13 |
14 | describe('LoadSurvey Controller', () => {
15 | const mockLoadSurvey = new MockLoadSurvey() as jest.Mocked
16 | const sut = new LoadSurveyController(mockLoadSurvey)
17 | const fakeRequest = { accountId: 'any_id' }
18 |
19 | beforeAll(() => MockDate.set(new Date()))
20 | afterAll(() => MockDate.reset)
21 |
22 | test('Should call LoadSurvey with correct values', async () => {
23 | const loadSpy = jest.spyOn(mockLoadSurvey, 'load')
24 | await sut.handle(fakeRequest)
25 | expect(loadSpy).toHaveBeenCalledWith(fakeRequest.accountId)
26 | })
27 |
28 | test('Should return 200 with surveys data on success', async () => {
29 | const httpResponse = await sut.handle(fakeRequest)
30 | expect(httpResponse).toEqual(ok(fakeSurveysModel))
31 | })
32 |
33 | test('Should return 500 if loadSurvey throws', async () => {
34 | mockLoadSurvey.load.mockRejectedValueOnce(new Error())
35 | const error = await sut.handle(fakeRequest)
36 | expect(error).toEqual(serverError(new Error()))
37 | })
38 |
39 | test('Should return 204 if loadSurvey returns empty', async () => {
40 | mockLoadSurvey.load.mockResolvedValueOnce([])
41 | const httpResponse = await sut.handle(fakeRequest)
42 | expect(httpResponse).toEqual(noContent())
43 | })
44 | })
45 |
--------------------------------------------------------------------------------
/src/presentation/controllers/survey/load-survey/load-survey-controller.ts:
--------------------------------------------------------------------------------
1 | import { LoadSurvey } from '@domain/usecases/load-survey'
2 | import { noContent, ok, serverError } from '@presentation/helpers/http/http'
3 | import { Controller, HttpResponse } from '@presentation/interfaces'
4 |
5 | export class LoadSurveyController implements Controller {
6 | constructor(private readonly loadSurvey: LoadSurvey) {}
7 |
8 | async handle(request: LoadSurveyController.Request): Promise {
9 | try {
10 | const surveys = await this.loadSurvey.load(request.accountId)
11 | return surveys.length ? ok(surveys) : noContent()
12 | } catch (error) {
13 | return serverError(error)
14 | }
15 | }
16 | }
17 |
18 | export namespace LoadSurveyController {
19 | export interface Request {
20 | accountId: string
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/presentation/errors/access-denied.ts:
--------------------------------------------------------------------------------
1 | export class AccessDeniedError extends Error {
2 | constructor() {
3 | super('Access Denied')
4 | this.name = 'AccessDeniedError'
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/presentation/errors/email-already-in-use.ts:
--------------------------------------------------------------------------------
1 | export class EmailAlreadyInUseError extends Error {
2 | constructor() {
3 | super('The received email is already in use')
4 | this.name = 'EmailAlreadyInUseError'
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/presentation/errors/index.ts:
--------------------------------------------------------------------------------
1 | export * from './server'
2 | export * from './unauthorized'
3 | export * from './missing-param'
4 | export * from './invalid-param'
5 | export * from './email-already-in-use'
6 |
--------------------------------------------------------------------------------
/src/presentation/errors/invalid-param.ts:
--------------------------------------------------------------------------------
1 | export class InvalidParamError extends Error {
2 | constructor(paramName: string) {
3 | super(`Invalid param: ${paramName}`)
4 | this.name = paramName
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/presentation/errors/missing-param.ts:
--------------------------------------------------------------------------------
1 | export class MissingParamError extends Error {
2 | constructor(paramName: string) {
3 | super(`Missing param: ${paramName}`)
4 | this.name = paramName
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/presentation/errors/server.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.ts:
--------------------------------------------------------------------------------
1 | export class UnauthorizedError extends Error {
2 | constructor() {
3 | super('Unauthorized error')
4 | this.name = 'UnauthorizedError'
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/presentation/helpers/http/http.ts:
--------------------------------------------------------------------------------
1 | import { ServerError, UnauthorizedError } from '@presentation/errors/index'
2 | import { HttpResponse } from '@presentation/interfaces/http'
3 |
4 | export const forbidden = (error: Error): HttpResponse => ({
5 | statusCode: 403,
6 | body: error
7 | })
8 |
9 | export const badRequest = (error: Error): HttpResponse => ({
10 | statusCode: 400,
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/interfaces/controller.ts:
--------------------------------------------------------------------------------
1 | import { HttpResponse } from './http'
2 |
3 | export interface Controller {
4 | handle(request: T): Promise
5 | }
6 |
--------------------------------------------------------------------------------
/src/presentation/interfaces/http.ts:
--------------------------------------------------------------------------------
1 | export interface HttpResponse {
2 | statusCode: number
3 | body: T
4 | }
5 |
--------------------------------------------------------------------------------
/src/presentation/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | export * from './http'
2 | export * from './controller'
3 | export * from './validation'
4 |
--------------------------------------------------------------------------------
/src/presentation/interfaces/middleware.ts:
--------------------------------------------------------------------------------
1 | import { HttpRequest, HttpResponse } from './http'
2 |
3 | export interface Middleware {
4 | handle(httpRequest: HttpRequest): Promise
5 | }
6 |
--------------------------------------------------------------------------------
/src/presentation/interfaces/validation.ts:
--------------------------------------------------------------------------------
1 | export interface Validation {
2 | validate(input: any): Error
3 | }
4 |
--------------------------------------------------------------------------------
/src/presentation/middlewares/auth-middleware.spec.ts:
--------------------------------------------------------------------------------
1 | import { fakeAccountModel } from '../../domain/mocks/mock-account'
2 | import { AccountModel } from '../../domain/models/account'
3 | import { LoadAccountByToken } from '../../domain/usecases/load-account-by-token'
4 | import { AccessDeniedError } from '../errors/access-denied'
5 | import { forbidden, ok, serverError } from '../helpers/http/http'
6 | import { AuthMiddleware } from './auth-middleware'
7 |
8 | const fakeRequest: AuthMiddleware.Request = { accessToken: 'any_token' }
9 |
10 | class MockLoadAccountByToken implements LoadAccountByToken {
11 | load(accessToken: string): Promise {
12 | return new Promise(resolve => resolve(fakeAccountModel))
13 | }
14 | }
15 |
16 | describe('Auth Middleware', () => {
17 | const mockLoadAccountByToken = new MockLoadAccountByToken() as jest.Mocked
18 | const role = 'any_role'
19 | const sut = new AuthMiddleware(mockLoadAccountByToken, role)
20 |
21 | test('Should return 403 if no accessToken exists in request header', async () => {
22 | const httpResponse = await sut.handle({})
23 | expect(httpResponse).toEqual(forbidden(new AccessDeniedError()))
24 | })
25 |
26 | test('Should call LoadAccountByToken with correct accessToken', async () => {
27 | const loadSpy = jest.spyOn(mockLoadAccountByToken, 'load')
28 | await sut.handle(fakeRequest)
29 | expect(loadSpy).toHaveBeenCalledWith(fakeRequest.accessToken, role)
30 | })
31 |
32 | test('Should return 403 if LoadAccountByToken returns null', async () => {
33 | mockLoadAccountByToken.load.mockResolvedValueOnce(null)
34 | const httpResponse = await sut.handle(fakeRequest)
35 | expect(httpResponse).toEqual(forbidden(new AccessDeniedError()))
36 | })
37 |
38 | test('Should return 200 if LoadAccountByToken returns an account', async () => {
39 | const httpResponse = await sut.handle(fakeRequest)
40 | expect(httpResponse).toEqual(ok({ accountId: 'any_id' }))
41 | })
42 |
43 | test('Should return 500 if LoadAccountByToken throws', async () => {
44 | mockLoadAccountByToken.load.mockRejectedValueOnce(new Error())
45 | const httpResponse = await sut.handle(fakeRequest)
46 | expect(httpResponse).toEqual(serverError(new Error()))
47 | })
48 | })
49 |
--------------------------------------------------------------------------------
/src/presentation/middlewares/auth-middleware.ts:
--------------------------------------------------------------------------------
1 | import { AccessDeniedError } from '@presentation/errors/access-denied'
2 | import { HttpResponse } from '@presentation/interfaces'
3 | import { Middleware } from '../interfaces/middleware'
4 | import { forbidden, ok, serverError } from '../helpers/http/http'
5 | import { LoadAccountByToken } from '@domain/usecases/load-account-by-token'
6 |
7 | export class AuthMiddleware implements Middleware {
8 | constructor(
9 | private readonly loadAccountByToken: LoadAccountByToken,
10 | private readonly role?: string
11 | ) {}
12 |
13 | async handle(request: AuthMiddleware.Request): Promise {
14 | try {
15 | if (!request.accessToken) return forbidden(new AccessDeniedError())
16 | const { accessToken } = request
17 |
18 | if (accessToken) {
19 | const account = await this.loadAccountByToken.load(accessToken, this.role)
20 | if (account) return ok({ accountId: account.id })
21 | }
22 |
23 | return forbidden(new AccessDeniedError())
24 | } catch (error) {
25 | return serverError(error)
26 | }
27 | }
28 | }
29 |
30 | export namespace AuthMiddleware {
31 | export interface Request {
32 | accessToken?: string
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/validation/interfaces/email-validator.ts:
--------------------------------------------------------------------------------
1 | export interface EmailValidator {
2 | isValid(email: string): boolean
3 | }
4 |
--------------------------------------------------------------------------------
/src/validation/validator/compare-fields-validation.ts:
--------------------------------------------------------------------------------
1 | import { Validation } from '../../presentation/interfaces/validation'
2 | import { InvalidParamError } from '../../presentation/errors'
3 |
4 | export class CompareFieldsValidation implements Validation {
5 | constructor(
6 | private readonly fieldName: string,
7 | private readonly filedToCompare: string
8 | ) {}
9 |
10 | validate(input: any): Error {
11 | if (input[this.fieldName] !== input[this.filedToCompare]) {
12 | return new InvalidParamError(this.filedToCompare)
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/validation/validator/email-validation.ts:
--------------------------------------------------------------------------------
1 | import { Validation } from '../../presentation/interfaces/validation'
2 | import { InvalidParamError } from '../../presentation/errors'
3 | import { EmailValidator } from '../interfaces/email-validator'
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 isValidEmail = this.emailValidator.isValid(input[this.fieldName])
13 | if (!isValidEmail) return new InvalidParamError(this.fieldName)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/validation/validator/required-fields-validation.ts:
--------------------------------------------------------------------------------
1 | import { Validation } from '../../presentation/interfaces/validation'
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]) return new MissingParamError(this.fieldName)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/validation/validator/tests/compare-fields-validation.spec.ts:
--------------------------------------------------------------------------------
1 | import { InvalidParamError } from '../../../presentation/errors'
2 | import { CompareFieldsValidation } from '../compare-fields-validation'
3 |
4 | describe('CompareFields Validation', () => {
5 | const sut = new CompareFieldsValidation('field', 'fieldToCompare')
6 |
7 | test('Should return an InvalidParamError if validation fails', () => {
8 | const error = sut.validate({ field: 'any_field', fieldToCompare: 'wrong_field' })
9 | expect(error).toEqual(new InvalidParamError('fieldToCompare'))
10 | })
11 |
12 | test('Should not return if validation succeeds', () => {
13 | const error = sut.validate({ field: 'any_field', fieldToCompare: 'any_field' })
14 | expect(error).toBeFalsy()
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/src/validation/validator/tests/email-validation.spec.ts:
--------------------------------------------------------------------------------
1 | import { InvalidParamError } from '../../../presentation/errors'
2 | import { EmailValidator } from '../../interfaces/email-validator'
3 | import { EmailValidation } from '../email-validation'
4 |
5 | class MockEmailValidation implements EmailValidator {
6 | isValid(email: string): boolean {
7 | return true
8 | }
9 | }
10 |
11 | describe('EmailValidation', () => {
12 | const mockEmailValidation = new MockEmailValidation() as jest.Mocked
13 | const sut = new EmailValidation('email', mockEmailValidation)
14 |
15 | test('Should return an error if EmailValidator returns false', () => {
16 | jest.spyOn(mockEmailValidation, 'isValid').mockReturnValueOnce(false)
17 | const error = sut.validate({ email: 'gabriel@example.com' })
18 | expect(error).toEqual(new InvalidParamError('email'))
19 | })
20 |
21 | test('Should call EmailValidator with correct email', () => {
22 | const isValidSpy = jest.spyOn(mockEmailValidation, 'isValid')
23 | sut.validate({ email: 'gabriel@example.com' })
24 | expect(isValidSpy).toHaveBeenCalledWith('gabriel@example.com')
25 | })
26 |
27 | test('Should throw if EmailValidator throws', () => {
28 | jest.spyOn(mockEmailValidation, 'isValid').mockImplementationOnce(() => {
29 | throw new Error()
30 | })
31 | expect(sut.validate).toThrow()
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/src/validation/validator/tests/required-field-validation.spec.ts:
--------------------------------------------------------------------------------
1 | import { MissingParamError } from '../../../presentation/errors'
2 | import { RequiredFieldValidation } from '../required-fields-validation'
3 |
4 | describe('RequiredField Validation', () => {
5 | const sut = new RequiredFieldValidation('field')
6 |
7 | test('Should return a MissingParamError if validation fails', () => {
8 | const error = sut.validate({ name: 'any_name' })
9 | expect(error).toEqual(new MissingParamError('field'))
10 | })
11 |
12 | test('Should not return if validation succeeds', () => {
13 | const error = sut.validate({ field: 'any_field' })
14 | expect(error).toBeFalsy()
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/src/validation/validator/tests/validation-composite.spec.ts:
--------------------------------------------------------------------------------
1 | import { MissingParamError } from '../../../presentation/errors/'
2 | import { Validation } from '../../../presentation/interfaces/validation'
3 | import { ValidationComposite } from '../validation-composite'
4 |
5 | class ValidationStub implements Validation {
6 | validate(input: any): Error {
7 | return null
8 | }
9 | }
10 |
11 | const makeSut = () => {
12 | const validationStubs = [new ValidationStub(), new ValidationStub()]
13 | const sut = new ValidationComposite(validationStubs)
14 | return { sut, validationStubs }
15 | }
16 |
17 | describe('Validation Composite', () => {
18 | test('Should return an error if any validation fails', () => {
19 | const { sut, validationStubs } = makeSut()
20 | jest.spyOn(validationStubs[0], 'validate').mockReturnValueOnce(new MissingParamError('field'))
21 | const error = sut.validate({ field: 'any_field' })
22 | expect(error).toEqual(new MissingParamError('field'))
23 | })
24 |
25 | test('Should return the first error if more than one validation fails', () => {
26 | const { sut, validationStubs } = makeSut()
27 |
28 | jest.spyOn(validationStubs[0], 'validate').mockReturnValueOnce(new Error())
29 | jest.spyOn(validationStubs[1], 'validate').mockReturnValueOnce(new MissingParamError('field'))
30 |
31 | const error = sut.validate({ field: 'any_field' })
32 | expect(error).toEqual(new Error())
33 | })
34 |
35 | test('Should not return if validation succeeds', () => {
36 | const { sut } = makeSut()
37 | const error = sut.validate({ field: 'any_field' })
38 | expect(error).toBeFalsy()
39 | })
40 | })
41 |
--------------------------------------------------------------------------------
/src/validation/validator/validation-composite.ts:
--------------------------------------------------------------------------------
1 | import { Validation } from '../../presentation/interfaces/validation'
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) return error
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2019",
4 | "module": "commonjs",
5 | "lib": ["es6"],
6 | "allowJs": true,
7 | "outDir": "./dist",
8 | "rootDir": "./",
9 | "sourceMap": true,
10 | "removeComments": true,
11 | "typeRoots": ["./node_modules/@types", "./src/@types"],
12 | "esModuleInterop": true,
13 | "resolveJsonModule": true,
14 | "experimentalDecorators": true,
15 | "emitDecoratorMetadata": true,
16 | "skipLibCheck": true,
17 | "forceConsistentCasingInFileNames": true,
18 | "baseUrl": ".",
19 | "paths": {
20 | "@data/*": ["./src/data/*"],
21 | "@main/*": ["./src/main/*"],
22 | "@presentation/*": ["./src/presentation/*"],
23 | "@domain/*": ["./src/domain/*"],
24 | "@infra/*": ["./src/infra/*"]
25 | }
26 | },
27 | "include": ["src"],
28 | "exclude": ["**/*.spec.ts", "**/*.test.ts"]
29 | }
30 |
--------------------------------------------------------------------------------