├── test ├── setupFiles.js ├── disconnectMongoose.ts ├── babel-transformer.js ├── global.d.ts ├── setupTestFramework.js ├── clearDatabase.ts ├── counters.ts ├── connectMongoose.ts ├── index.ts ├── getObjectId.ts ├── environment │ └── mongodb.js ├── restUtils.ts └── sanitizeTestObject.ts ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── test.yml │ └── update-changelog.yaml ├── src ├── modules │ ├── index.ts │ └── user │ │ ├── fixtures │ │ └── createUser.ts │ │ └── UserModel.ts ├── config.ts ├── __tests__ │ ├── __snapshots__ │ │ └── app.spec.ts.snap │ └── app.spec.ts ├── shared │ ├── index.ts │ └── server.config.ts ├── auth │ ├── generateToken.ts │ ├── base64.ts │ └── auth.ts ├── database │ └── database.ts ├── api │ ├── auth │ │ ├── authForgotPassword.ts │ │ ├── authLogin.ts │ │ └── __tests__ │ │ │ ├── __snapshots__ │ │ │ └── authLogin.spec.ts.snap │ │ │ └── authLogin.spec.ts │ ├── user │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ ├── userGet.spec.ts.snap │ │ │ │ ├── userDelete.spec.ts.snap │ │ │ │ ├── userPost.spec.ts.snap │ │ │ │ └── userGetAll.spec.ts.snap │ │ │ ├── userGet.spec.ts │ │ │ ├── userGetAll.spec.ts │ │ │ ├── userDelete.spec.ts │ │ │ └── userPost.spec.ts │ │ ├── userUtils.ts │ │ ├── userGetAll.ts │ │ ├── userDelete.ts │ │ ├── userPost.ts │ │ ├── userUpdate.ts │ │ └── userGet.ts │ └── apiHelpers.ts ├── routes │ └── userRoutes.ts ├── server.ts └── app.ts ├── .env.example ├── babel.config.js ├── jest.config.js ├── .gitignore ├── README.md ├── webpack └── ReloadServerPlugin.js ├── webpack.config.js ├── package.json └── tsconfig.json /test/setupFiles.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # AUTOMATIC REVIEWERS 2 | * @biantris -------------------------------------------------------------------------------- /src/modules/index.ts: -------------------------------------------------------------------------------- 1 | import User from "./user/UserModel"; 2 | 3 | export { User }; -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # application 2 | PORT= 3 | 4 | # auth 5 | JWT_SECRET= 6 | 7 | # database 8 | MONGO_URI= -------------------------------------------------------------------------------- /test/disconnectMongoose.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | export const disconnectMongoose = () => mongoose.disconnect(); -------------------------------------------------------------------------------- /test/babel-transformer.js: -------------------------------------------------------------------------------- 1 | const config = require("../babel.config"); 2 | 3 | const { createTransformer } = require("babel-jest"); 4 | 5 | module.exports = createTransformer({ 6 | ...config, 7 | }); 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "01:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | PORT: process.env.PORT || 9000, 3 | MONGO_URI: process.env.MONGO_URI || 'mongodb://localhost/restris', 4 | JWT_SECRET: process.env.JWT_KEY || 'secret_key', 5 | }; -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/app.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should get api version correctly 1`] = ` 4 | Object { 5 | "status": "OK", 6 | "version": "1.0.0", 7 | } 8 | `; 9 | -------------------------------------------------------------------------------- /test/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface Global { 4 | __COUNTERS__: Record; 5 | __MONGO_URI__: string; 6 | __MONGO_DB_NAME__: string; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | import SERVER_ENV from './server.config'; 2 | 3 | var environment = process.env.NODE_ENV || 'development'; 4 | 5 | var serverConf = SERVER_ENV[environment]; 6 | 7 | export { 8 | environment, 9 | serverConf, 10 | }; -------------------------------------------------------------------------------- /src/auth/generateToken.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import { IUser } from "../../src/modules/user/UserModel"; 3 | 4 | import { config } from "../config"; 5 | 6 | export const generateToken = (user: IUser) => { 7 | return `JWT ${jwt.sign({ user: user._id }, config.JWT_SECRET)}`; 8 | }; 9 | -------------------------------------------------------------------------------- /src/shared/server.config.ts: -------------------------------------------------------------------------------- 1 | const SERVER_ENV = { 2 | production: { 3 | SERVER_PORT: process.env.PORT || 9000, 4 | }, 5 | preproduction: { 6 | SERVER_PORT: 9000, 7 | }, 8 | development: { 9 | SERVER_PORT: 9000, 10 | }, 11 | }; 12 | 13 | export default SERVER_ENV; -------------------------------------------------------------------------------- /test/setupTestFramework.js: -------------------------------------------------------------------------------- 1 | // this file is ran right after the test framework is setup for some test file. 2 | require("@babel/polyfill"); 3 | 4 | // jest.mock('graphql-redis-subscriptions'); 5 | 6 | // https://jestjs.io/docs/en/es6-class-mocks#simple-mock-using-module-factory-parameter 7 | 8 | require("jest-fetch-mock").enableMocks(); 9 | -------------------------------------------------------------------------------- /test/clearDatabase.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | import { restartCounters } from './counters'; 4 | 5 | export async function clearDatabase() { 6 | await mongoose.connection.db.dropDatabase(); 7 | } 8 | 9 | export async function clearDbAndRestartCounters() { 10 | await clearDatabase(); 11 | restartCounters(); 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: "14" 21 | - run: yarn 22 | - run: yarn jest 23 | -------------------------------------------------------------------------------- /test/counters.ts: -------------------------------------------------------------------------------- 1 | export const getCounter = (key: string) => { 2 | if (key in global.__COUNTERS__) { 3 | global.__COUNTERS__[key]++; 4 | 5 | return global.__COUNTERS__[key]; 6 | } 7 | 8 | global.__COUNTERS__[key] = 0; 9 | 10 | return global.__COUNTERS__[key]; 11 | }; 12 | 13 | export const restartCounters = () => { 14 | global.__COUNTERS__ = Object.keys(global.__COUNTERS__).reduce((prev, curr) => ({ ...prev, [curr]: 0 }), {}); 15 | }; 16 | -------------------------------------------------------------------------------- /src/database/database.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { config } from "../config"; 3 | 4 | export const connectDB = () => { 5 | // @ts-ignore 6 | mongoose.connect(config.MONGO_URI, { 7 | useNewUrlParser: true, 8 | useCreateIndex: true, 9 | useUnifiedTopology: true, 10 | }); 11 | 12 | const db = mongoose.connection; 13 | db.on("error", console.error.bind(console, "connection error:")); 14 | db.once("open", () => console.log("Database connected ✅")); 15 | }; -------------------------------------------------------------------------------- /src/modules/user/fixtures/createUser.ts: -------------------------------------------------------------------------------- 1 | import { getCounter } from "../../../../test"; 2 | 3 | import User from "../UserModel"; 4 | 5 | export const createUser = async (args) => { 6 | const n = getCounter("user"); 7 | const { 8 | name = `User ${n}`, 9 | email = `user${n}@test.com`, 10 | password = `password123${n}321`, 11 | ...payload 12 | } = args; 13 | 14 | return await new User({ 15 | name, 16 | email, 17 | password, 18 | ...payload, 19 | }).save(); 20 | }; 21 | -------------------------------------------------------------------------------- /src/api/auth/authForgotPassword.ts: -------------------------------------------------------------------------------- 1 | import UserModel from "../../modules/user/UserModel"; 2 | 3 | export const authForgotPassword = async (ctx) => { 4 | const user = await UserModel.findOne({ 5 | email: ctx.query.user.trim(), 6 | }); 7 | 8 | ctx.status = 200; 9 | 10 | if (!user) { 11 | ctx.body = { 12 | message: "User not found", 13 | }; 14 | return; 15 | } 16 | 17 | ctx.body = { 18 | user, 19 | password: 123456, //change to user.password 20 | }; 21 | return; 22 | }; 23 | -------------------------------------------------------------------------------- /test/connectMongoose.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | declare global { 4 | // eslint-disable-next-line @typescript-eslint/no-namespace 5 | namespace NodeJS { 6 | interface Global { 7 | __MONGO_URI__: string; 8 | __MONGO_DB_NAME__: string; 9 | } 10 | } 11 | } 12 | 13 | export async function connectMongoose() { 14 | jest.setTimeout(20000); 15 | return mongoose.connect(global.__MONGO_URI__, { 16 | connectTimeoutMS: 10000, 17 | dbName: global.__MONGO_DB_NAME__, 18 | }); 19 | } -------------------------------------------------------------------------------- /src/__tests__/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | clearDbAndRestartCounters, 3 | connectMongoose, 4 | disconnectMongoose, 5 | createGetApiCall, 6 | } from "../../test"; 7 | 8 | beforeAll(connectMongoose); 9 | 10 | beforeEach(clearDbAndRestartCounters); 11 | 12 | afterAll(disconnectMongoose); 13 | 14 | const url = "/api/version"; 15 | 16 | it("should get api version correctly", async () => { 17 | const response = await createGetApiCall({ url }); 18 | 19 | expect(response.body).toMatchSnapshot(); 20 | expect(response.status).toBe(200); 21 | }); 22 | -------------------------------------------------------------------------------- /src/routes/userRoutes.ts: -------------------------------------------------------------------------------- 1 | import Router from "koa-router"; 2 | 3 | import { userUpdate } from "../api/user/userUpdate"; 4 | import { userDelete } from "../api/user/userDelete"; 5 | import { userGet } from "../api/user/userGet"; 6 | import { userGetAll } from "../api/user/userGetAll"; 7 | 8 | const routerUser = new Router({ 9 | prefix: '/api/user' 10 | }); 11 | 12 | routerUser.get("/", userGetAll); 13 | routerUser.get("/:id", userGet); 14 | routerUser.delete("/:id", userDelete); 15 | routerUser.put("/:id", userUpdate); 16 | 17 | export default routerUser; -------------------------------------------------------------------------------- /src/api/user/__tests__/__snapshots__/userGet.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should return error if authorization header does not exist 1`] = ` 4 | Object { 5 | "message": "Unauthorized", 6 | } 7 | `; 8 | 9 | exports[`should return error if user it was not found 1`] = ` 10 | Object { 11 | "message": "User not found", 12 | } 13 | `; 14 | 15 | exports[`should return user by id 1`] = ` 16 | Object { 17 | "user": Object { 18 | "_id": "ObjectId", 19 | "email": "user2@test.com", 20 | "name": "User 2", 21 | }, 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /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 | '@babel/plugin-proposal-class-properties', 15 | '@babel/plugin-proposal-export-default-from', 16 | '@babel/plugin-proposal-export-namespace-from', 17 | '@babel/plugin-proposal-nullish-coalescing-operator', 18 | '@babel/plugin-proposal-optional-chaining', 19 | ], 20 | }; -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import { getObjectId } from "./getObjectId"; 2 | import { getCounter } from "./counters"; 3 | 4 | export { 5 | createApiCall, 6 | createDeleteApiCall, 7 | createGetApiCall, 8 | } from "./restUtils"; 9 | export { clearDatabase, clearDbAndRestartCounters } from "./clearDatabase"; 10 | export { connectMongoose } from "./connectMongoose"; 11 | export { disconnectMongoose } from "./disconnectMongoose"; 12 | export { getObjectId } from "./getObjectId"; 13 | export { getCounter } from "./counters"; 14 | export { 15 | sanitizeTestObject, 16 | sanitizeValue, 17 | defaultFrozenKeys, 18 | } from "./sanitizeTestObject"; 19 | -------------------------------------------------------------------------------- /src/auth/base64.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/graphql/graphql-relay-js/blob/4fdadd3bbf3d5aaf66f1799be3e4eb010c115a4a/src/utils/base64.js 2 | /** 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * @flow 9 | */ 10 | 11 | export type Base64String = string; 12 | 13 | export function base64(i: string): Base64String { 14 | return Buffer.from(i, "utf8").toString("base64"); 15 | } 16 | 17 | export function unbase64(i: Base64String): string { 18 | return Buffer.from(i, "base64").toString("utf8"); 19 | } 20 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const pack = require("./package"); 2 | 3 | module.exports = { 4 | displayName: pack.name, 5 | name: pack.name, 6 | testEnvironment: "/test/environment/mongodb", 7 | testPathIgnorePatterns: ["/node_modules/", "./dist"], 8 | coverageReporters: ["lcov", "html"], 9 | setupFiles: ["/test/setupFiles.js"], 10 | setupFilesAfterEnv: ["/test/setupTestFramework.js"], 11 | resetModules: false, 12 | reporters: ["default", "jest-junit"], 13 | transform: { 14 | "^.+\\.(js|ts|tsx)?$": "/test/babel-transformer", 15 | }, 16 | testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts|tsx)?$", 17 | moduleFileExtensions: ["ts", "js", "tsx", "json"], 18 | }; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.sublime-project 3 | *.sublime-workspace 4 | .idea/ 5 | 6 | _cacache 7 | _logs 8 | _update-notifier-last-checked 9 | 10 | lib-cov 11 | *.seed 12 | *.log 13 | *.csv 14 | *.dat 15 | *.out 16 | *.pid 17 | *.gz 18 | *.map 19 | *.icloud 20 | 21 | pids 22 | logs 23 | results 24 | test-results 25 | 26 | node_modules 27 | npm-debug.log 28 | 29 | dump.rdb 30 | bundle.js 31 | 32 | build 33 | dist 34 | coverage 35 | .nyc_output 36 | .env 37 | 38 | graphql.*.json 39 | junit.xml 40 | 41 | .vs 42 | 43 | test/globalConfig.json 44 | distTs 45 | 46 | # Random things to ignore 47 | ignore/ 48 | package-lock.json 49 | /yarn-offline-cache 50 | .cache 51 | .webpack 52 | /functions 53 | .netlify 54 | globalConfig.json 55 | -------------------------------------------------------------------------------- /src/api/user/userUtils.ts: -------------------------------------------------------------------------------- 1 | import * as yup from "yup"; 2 | 3 | type ApiUser = { 4 | name: string; 5 | email: string; 6 | password: string; 7 | }; 8 | 9 | const userSchema = yup.object().shape({ 10 | name: yup.string().required(), 11 | email: yup.string().required().email(), 12 | password: yup.string().required(), 13 | }); 14 | 15 | export const validateUserApi = async (apiUser: ApiUser) => { 16 | try { 17 | await userSchema.validate(apiUser); 18 | } catch (err) { 19 | if (err instanceof yup.ValidationError) { 20 | return { 21 | error: err.message, 22 | user: null, 23 | }; 24 | } 25 | 26 | return { 27 | err, 28 | user: null, 29 | }; 30 | } 31 | 32 | return { 33 | user: apiUser, 34 | error: null, 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { connectDB }from './database/database'; 2 | import dotenv from "dotenv"; 3 | import { createServer } from "http"; 4 | import { environment, serverConf } from "./shared"; 5 | import app from './app'; 6 | 7 | (async () => { 8 | dotenv.config(); 9 | // starting db 10 | try { 11 | await connectDB(); 12 | } catch (error) { 13 | console.error("Unable to connect to database"); 14 | process.exit(1); 15 | } 16 | 17 | const server = createServer(app.callback()); 18 | 19 | server.listen(process.env.PORT, () => console.log('Server running 🚀')); 20 | console.log( 21 | `App running on ${environment.toUpperCase()} mode and listening on port ${ 22 | serverConf.SERVER_PORT 23 | } ...` 24 | ); 25 | console.log( 26 | `Server is now running on http://localhost:${process.env.PORT}/api/version` 27 | ); 28 | })(); 29 | -------------------------------------------------------------------------------- /.github/workflows/update-changelog.yaml: -------------------------------------------------------------------------------- 1 | name: "Update Changelog" 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | update: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | with: 15 | ref: ${{ github.event.release.target_commitish }} 16 | 17 | - name: Update Changelog 18 | uses: stefanzweifel/changelog-updater-action@v1 19 | with: 20 | latest-version: ${{ github.event.release.tag_name }} 21 | release-notes: ${{ github.event.release.body }} 22 | 23 | - name: Commit updated CHANGELOG 24 | uses: stefanzweifel/git-auto-commit-action@v4 25 | with: 26 | branch: ${{ github.event.release.target_commitish }} 27 | commit_message: Update CHANGELOG 28 | file_pattern: CHANGELOG.md -------------------------------------------------------------------------------- /src/api/user/__tests__/__snapshots__/userDelete.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should delete an user 1`] = ` 4 | Object { 5 | "user": Object { 6 | "__v": 0, 7 | "_id": "ObjectId", 8 | "email": "user2@test.com", 9 | "name": "User 2", 10 | "removedAt": "FROZEN-REMOVEDAT", 11 | }, 12 | } 13 | `; 14 | 15 | exports[`should return error if authorization header does not exist 1`] = ` 16 | Object { 17 | "message": "Unauthorized", 18 | } 19 | `; 20 | 21 | exports[`should return error if id it is not valid 1`] = ` 22 | Object { 23 | "message": "User not found", 24 | } 25 | `; 26 | 27 | exports[`should return error if user it was not found 1`] = ` 28 | Object { 29 | "message": "User not found", 30 | } 31 | `; 32 | 33 | exports[`should return user not found for a user already deleted 1`] = ` 34 | Object { 35 | "message": "User not found", 36 | } 37 | `; 38 | -------------------------------------------------------------------------------- /test/getObjectId.ts: -------------------------------------------------------------------------------- 1 | import { fromGlobalId } from 'graphql-relay'; 2 | import { Model, Types } from 'mongoose'; 3 | 4 | // returns an ObjectId given an param of unknown type 5 | export const getObjectId = (target: string | Model | Types.ObjectId): Types.ObjectId | null => { 6 | if (target instanceof Types.ObjectId) { 7 | return new Types.ObjectId(target.toString()); 8 | } 9 | 10 | if (typeof target === 'object') { 11 | return target && target._id ? new Types.ObjectId(target._id) : null; 12 | } 13 | 14 | if (Types.ObjectId.isValid(target)) { 15 | return new Types.ObjectId(target.toString()); 16 | } 17 | 18 | if (typeof target === 'string') { 19 | const result = fromGlobalId(target); 20 | 21 | if (result.type && result.id && Types.ObjectId.isValid(result.id)) { 22 | return new Types.ObjectId(result.id); 23 | } 24 | 25 | if (Types.ObjectId.isValid(target)) { 26 | return new Types.ObjectId(target); 27 | } 28 | 29 | return null; 30 | } 31 | 32 | return null; 33 | }; 34 | -------------------------------------------------------------------------------- /src/api/user/__tests__/__snapshots__/userPost.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should create user with success 1`] = ` 4 | Object { 5 | "user": Object { 6 | "_id": "ObjectId", 7 | "email": "test@test.com", 8 | "name": "obi wan kenobi", 9 | }, 10 | } 11 | `; 12 | 13 | exports[`should return error if user email it was not passed 1`] = ` 14 | Object { 15 | "message": "email is a required field", 16 | } 17 | `; 18 | 19 | exports[`should return error if user it was not passed 1`] = ` 20 | Object { 21 | "message": "User is required", 22 | } 23 | `; 24 | 25 | exports[`should return error if user name it was not passed 1`] = ` 26 | Object { 27 | "message": "name is a required field", 28 | } 29 | `; 30 | 31 | exports[`should return error if user password it was not passed 1`] = ` 32 | Object { 33 | "message": "password is a required field", 34 | } 35 | `; 36 | 37 | exports[`should return error is email is already in use 1`] = ` 38 | Object { 39 | "message": "Email already in use", 40 | } 41 | `; 42 | -------------------------------------------------------------------------------- /src/api/user/userGetAll.ts: -------------------------------------------------------------------------------- 1 | import User from "../../modules/user/UserModel"; 2 | import { userSelection } from "./userGet"; 3 | import { getPageInfo, getSkipAndLimit } from "../apiHelpers"; 4 | 5 | export const userGetAll = async (ctx) => { 6 | const { skip, limit } = getSkipAndLimit(ctx); 7 | try { 8 | //@ts-ignore 9 | const users = await User.find({ 10 | removedAt: null, 11 | }) 12 | .skip(skip) 13 | .limit(limit) 14 | .select(userSelection) 15 | .lean(); 16 | 17 | const pageInfo = await getPageInfo(ctx, User); 18 | 19 | if (pageInfo.errors) { 20 | ctx.status = 422; 21 | ctx.body = { 22 | errors: pageInfo.errors, 23 | }; 24 | 25 | return; 26 | } 27 | 28 | ctx.status = 200; 29 | ctx.body = { 30 | pageInfo, 31 | users, 32 | }; 33 | } catch (err) { 34 | // eslint-disable-next-line 35 | console.log("err: ", err); 36 | 37 | ctx.status = 500; 38 | ctx.body = { 39 | message: "Unknown error", 40 | }; 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

RESTRIS

4 | 5 | Functional Backend implementation of REST api with NodeJS(KoaJS) MongoDB and Jest Tests. 6 | 7 | 8 | Tests Passing 9 | 10 | 11 |
12 | 13 | ### Tools 14 | - [x] TypeScript 15 | - [x] Node 16 | - [x] KoaJS 17 | - [x] MongoDB 18 | - [x] Mongoose 19 | - [x] Jest 20 | - [x] Supertest 21 | - [ ] Swagger 22 | - [ ] Eslint 23 | - [ ] Prettier 24 | 25 | ### Modules 26 | - [x] userModel 27 | 28 | #### APIs 29 | - [x] userGet 30 | - [x] userGetAll 31 | - [x] userPost 32 | - [x] userUpdate 33 | - [x] userDelete 34 | 35 | ### Auth Flow 36 | - [x] login 37 | - [x] validate user token 38 | - [x] logout 39 | 40 | ### Getting Started 41 | - clone this repo 42 | ```sh 43 | # install dependencies 44 | > yarn 45 | # or 46 | > yarn install 47 | 48 | # copy .env file 49 | > cp .env.example .env 50 | 51 | # start project 52 | > yarn start 53 | 54 | # open in 55 | http://localhost:9000/api/version 56 | ``` 57 | 58 | ##### 🔗 Demo: 59 | [wip] 60 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import Koa from "koa"; 2 | import bodyParser from "koa-bodyparser"; 3 | import logger from "koa-logger"; 4 | import Router from "koa-router"; 5 | import cors from "koa-cors"; 6 | 7 | import routerUser from "./routes/userRoutes"; 8 | 9 | import { version } from "../package.json"; 10 | import { authLogin } from "./api/auth/authLogin"; 11 | import { auth } from "./auth/auth"; 12 | import { userPost } from "./api/user/userPost"; 13 | import { userUpdate } from "./api/user/userUpdate"; 14 | 15 | const app = new Koa(); 16 | 17 | const routerOpen = new Router(); 18 | 19 | app.use(logger()); 20 | app.use(cors({ maxAge: 86400 })); 21 | app.use(bodyParser()); 22 | 23 | routerOpen.get("/api/version", (ctx) => { 24 | ctx.status = 200; 25 | ctx.body = { 26 | status: "OK", 27 | version, 28 | }; 29 | }); 30 | 31 | routerOpen.post("/api/auth/login", authLogin); 32 | routerOpen.post("/api/user", userPost); 33 | routerOpen.put("/api/user/:id", userUpdate); 34 | app.use(routerOpen.routes()); 35 | 36 | app.use(auth); 37 | app.use(routerUser.routes()); 38 | 39 | // Default not found 404 40 | app.use((ctx) => { 41 | ctx.status = 404; 42 | }); 43 | 44 | export default app; 45 | -------------------------------------------------------------------------------- /webpack/ReloadServerPlugin.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster'); 2 | const path = require('path') 3 | 4 | const defaultOptions = { 5 | script: 'server.ts', 6 | }; 7 | 8 | class ReloadServerPlugin { 9 | constructor({ script } = defaultOptions) { 10 | this.done = null; 11 | this.workers = []; 12 | 13 | cluster.setupMaster({ 14 | exec: path.resolve(process.cwd(), script), 15 | }); 16 | 17 | cluster.on('online', (worker) => { 18 | this.workers.push(worker); 19 | 20 | if (this.done) { 21 | this.done(); 22 | } 23 | }); 24 | } 25 | 26 | apply(compiler) { 27 | compiler.hooks.afterEmit.tap( 28 | { 29 | name: 'reload-server', 30 | }, 31 | (compilation, callback) => { 32 | this.done = callback; 33 | this.workers.forEach((worker) => { 34 | try { 35 | process.kill(worker.process.pid, 'SIGTERM'); 36 | } catch (e) { 37 | // eslint-disable-next-line 38 | console.warn(`Unable to kill process #${worker.process.pid}`); 39 | } 40 | }); 41 | 42 | this.workers = []; 43 | 44 | cluster.fork(); 45 | }, 46 | ); 47 | } 48 | } 49 | 50 | module.exports = ReloadServerPlugin; 51 | -------------------------------------------------------------------------------- /src/api/auth/authLogin.ts: -------------------------------------------------------------------------------- 1 | import User from "../../modules/user/UserModel"; 2 | import { generateToken } from "../../auth/generateToken"; 3 | import { base64 } from "../../auth/base64"; 4 | 5 | export const authLogin = async (ctx) => { 6 | const { email, password } = ctx.request.body; 7 | 8 | if (!email || !password) { 9 | ctx.status = 401; 10 | ctx.body = { 11 | message: "Email or password incorrect", 12 | }; 13 | return; 14 | } 15 | 16 | const user = await User.findOne({ 17 | email, 18 | removeAt: null, 19 | }); 20 | 21 | if (!user) { 22 | ctx.status = 401; 23 | ctx.body = { 24 | message: "Email or password incorrect", 25 | }; 26 | return; 27 | } 28 | 29 | let correctPassword; 30 | 31 | try { 32 | correctPassword = user.authenticate(password); 33 | } catch { 34 | ctx.status = 401; 35 | ctx.body = { 36 | message: "Email or password incorrect", 37 | }; 38 | return; 39 | } 40 | 41 | if(!correctPassword) { 42 | ctx.status = 401; 43 | ctx.body = { 44 | message: "Email or password incorrect", 45 | } 46 | return; 47 | } 48 | 49 | ctx.status = 200; 50 | ctx.body = { 51 | message: "User authenticated with success", 52 | token: base64(generateToken(user)), 53 | user 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const webpack = require("webpack"); 4 | 5 | const WebpackNodeExternals = require("webpack-node-externals"); 6 | const ReloadServerPlugin = require("./webpack/ReloadServerPlugin"); 7 | 8 | const cwd = process.cwd(); 9 | 10 | const filename = "api.js"; 11 | 12 | module.exports = { 13 | mode: "development", 14 | devtool: "eval-cheap-source-map", 15 | entry: { 16 | server: ["./src/server.ts"], 17 | }, 18 | output: { 19 | path: path.resolve("build"), 20 | filename, 21 | }, 22 | target: "node", 23 | node: { 24 | __dirname: true, 25 | }, 26 | externals: [ 27 | WebpackNodeExternals({ 28 | allowlist: ["webpack/hot/poll?1000"], 29 | }), 30 | ], 31 | resolve: { 32 | extensions: [".ts", ".tsx", ".js", ".json", ".mjs"], 33 | }, 34 | module: { 35 | rules: [ 36 | { 37 | test: /\.mjs$/, 38 | include: /node_modules/, 39 | type: "javascript/auto", 40 | }, 41 | { 42 | test: /\.(js|jsx|ts|tsx)?$/, 43 | use: { 44 | loader: "babel-loader?cacheDirectory", 45 | }, 46 | exclude: [/node_modules/], 47 | }, 48 | ], 49 | }, 50 | plugins: [ 51 | new ReloadServerPlugin({ 52 | script: path.resolve("build", filename), 53 | }), 54 | new webpack.DefinePlugin({ 55 | "process.env.NODE_ENV": JSON.stringify("development"), 56 | }), 57 | ], 58 | }; 59 | -------------------------------------------------------------------------------- /src/api/user/userDelete.ts: -------------------------------------------------------------------------------- 1 | import { checkObjectId } from "../apiHelpers"; 2 | import User from "../../modules/user/UserModel"; 3 | import { getUserApi } from "./userGet"; 4 | 5 | export const userDelete = async (ctx) => { 6 | const { id } = ctx.params; 7 | 8 | const _id = checkObjectId(id); 9 | 10 | if (!_id) { 11 | ctx.status = 400; 12 | ctx.body = { 13 | message: "User not found", 14 | }; 15 | return; 16 | } 17 | 18 | const user = await User.findOne({ 19 | _id, 20 | removedAt: null, 21 | }); 22 | 23 | if (!user) { 24 | ctx.status = 400; 25 | ctx.body = { 26 | message: "User not found", 27 | }; 28 | return; 29 | } 30 | 31 | try { 32 | await User.updateOne( 33 | { 34 | _id: user._id, 35 | }, 36 | { 37 | $set: { 38 | removedAt: new Date(), 39 | }, 40 | } 41 | ); 42 | 43 | const { user: userUpdated, error } = await getUserApi({id: user._id, isDelete: true}); 44 | 45 | if (error) { 46 | ctx.status = 400; 47 | ctx.body = { 48 | message: "Error while deleting user", 49 | }; 50 | return; 51 | } 52 | 53 | ctx.status = 200; 54 | ctx.body = { 55 | user: userUpdated, 56 | }; 57 | } catch (err) { 58 | // eslint-disable-next-line 59 | console.log("err: ", err); 60 | 61 | ctx.status = 500; 62 | ctx.body = { 63 | message: "Unknown error", 64 | }; 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /test/environment/mongodb.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const MMS = require('mongodb-memory-server'); 3 | const NodeEnvironment = require('jest-environment-node'); 4 | 5 | const { MongoMemoryServer } = MMS; 6 | 7 | class MongoDbEnvironment extends NodeEnvironment { 8 | constructor(config) { 9 | super(config); 10 | 11 | // TODO - enable replset if needed 12 | // this.mongod = new MongoMemoryReplSet({ 13 | this.mongod = new MongoMemoryServer({ 14 | instance: { 15 | // settings here 16 | // dbName is null, so it's random 17 | // dbName: MONGO_DB_NAME, 18 | }, 19 | binary: { 20 | version: '4.0.5', 21 | }, 22 | // debug: true, 23 | autoStart: false, 24 | }); 25 | } 26 | 27 | async setup() { 28 | await super.setup(); 29 | // console.error('\n# MongoDB Environment Setup #\n'); 30 | await this.mongod.start(); 31 | this.global.__MONGO_URI__ = await this.mongod.getUri(); 32 | // this.global.__MONGO_DB_NAME__ = await this.mongod.getDbName(); 33 | this.global.__COUNTERS__ = { 34 | user: 0, 35 | company: 0, 36 | }; 37 | } 38 | 39 | async teardown() { 40 | await super.teardown(); 41 | // console.error('\n# MongoDB Environment Teardown #\n'); 42 | await this.mongod.stop(); 43 | this.mongod = null; 44 | this.global = {}; 45 | } 46 | 47 | runScript(script) { 48 | return super.runScript(script); 49 | } 50 | } 51 | 52 | module.exports = MongoDbEnvironment; -------------------------------------------------------------------------------- /src/auth/auth.ts: -------------------------------------------------------------------------------- 1 | import { Base64String, unbase64 } from "./base64"; 2 | import { getObjectId } from "../../test"; 3 | 4 | import User from "../modules/user/UserModel"; 5 | import jwt_decode from "jwt-decode"; 6 | 7 | export type Token = { 8 | userId: string | null; 9 | }; 10 | 11 | export type TokenDecoded = { 12 | user: string; 13 | } 14 | 15 | export const getToken = (authorization: Base64String): Token => { 16 | const tokenDecoded: TokenDecoded = jwt_decode(unbase64(authorization)); 17 | const userId = tokenDecoded.user; 18 | 19 | if (!userId) { 20 | return { 21 | userId: null, 22 | }; 23 | } 24 | 25 | return { 26 | userId: userId, 27 | }; 28 | }; 29 | 30 | export const getUser = async (token: string) => { 31 | const { userId } = getToken(token); 32 | 33 | if (!userId) { 34 | return null; 35 | } 36 | 37 | const user = await User.findOne({ 38 | _id: getObjectId(userId), 39 | removedAt: null, 40 | }); 41 | 42 | return user; 43 | }; 44 | 45 | export const auth = async (ctx, next) => { 46 | const { authorization } = ctx.header; 47 | 48 | if (!authorization) { 49 | ctx.status = 401; 50 | ctx.body = { 51 | message: "Unauthorized", 52 | }; 53 | return; 54 | } 55 | 56 | const user = await getUser(authorization); 57 | 58 | if (!user) { 59 | ctx.status = 401; 60 | ctx.body = { 61 | message: "Unauthorized", 62 | }; 63 | return; 64 | } 65 | 66 | ctx.user = user; 67 | 68 | await next(); 69 | }; 70 | -------------------------------------------------------------------------------- /src/api/user/userPost.ts: -------------------------------------------------------------------------------- 1 | import User from "../../modules/user/UserModel"; 2 | 3 | import { validateUserApi } from "./userUtils"; 4 | import { getUserApi } from "./userGet"; 5 | 6 | export const userPost = async (ctx) => { 7 | const { user = null } = ctx.request.body; 8 | 9 | if (!user) { 10 | ctx.status = 400; 11 | ctx.body = { 12 | message: "User is required", 13 | }; 14 | return; 15 | } 16 | 17 | const { user: userValidated, error } = await validateUserApi(user); 18 | 19 | if (error) { 20 | ctx.status = 400; 21 | ctx.body = { 22 | message: error, 23 | }; 24 | return; 25 | } 26 | 27 | const { user: userExist } = await getUserApi({ email: userValidated?.email }); 28 | 29 | if (userExist) { 30 | ctx.status = 400; 31 | ctx.body = { 32 | message: "Email already in use", 33 | }; 34 | return; 35 | } 36 | 37 | try { 38 | const userNew = await new User({ 39 | ...userValidated, 40 | }).save(); 41 | 42 | const { user: userNormalized, error } = await getUserApi({ id: userNew._id }); 43 | 44 | if (error) { 45 | ctx.status = 400; 46 | ctx.body = { 47 | message: error, 48 | }; 49 | return; 50 | } 51 | 52 | ctx.status = 200; 53 | ctx.body = { 54 | user: userNormalized, 55 | }; 56 | 57 | return; 58 | } catch (err) { 59 | // eslint-disable-next-line 60 | console.log("err: ", err); 61 | 62 | ctx.status = 500; 63 | ctx.body = { 64 | message: "Unknown error", 65 | }; 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /test/restUtils.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | 3 | import app from "../src/app"; 4 | 5 | type ApiArgs = { 6 | url?: string | null; 7 | authorization?: string | null; 8 | payload?: {} | null; 9 | domainname?: string | null; 10 | }; 11 | 12 | export const createApiCall = async (args: ApiArgs) => { 13 | const { url, payload: body, authorization } = args; 14 | 15 | const payload = { 16 | ...body, 17 | }; 18 | 19 | const response = await request(app.callback()) 20 | .post(url) 21 | .set({ 22 | Accept: "application/json", 23 | "Content-Type": "application/json", 24 | ...(authorization ? { authorization } : {}), 25 | }) 26 | .send(JSON.stringify(payload)); 27 | 28 | return response; 29 | }; 30 | 31 | export const createGetApiCall = async (args: ApiArgs) => { 32 | const { url, authorization } = args; 33 | 34 | const response = await request(app.callback()) 35 | .get(url) 36 | .set({ 37 | Accept: "application/json", 38 | "Content-Type": "application/json", 39 | ...(authorization ? { authorization } : {}), 40 | }) 41 | .send(); 42 | 43 | return response; 44 | }; 45 | 46 | export const createDeleteApiCall = async (args: ApiArgs) => { 47 | const { url, authorization } = args; 48 | 49 | const response = await request(app.callback()) 50 | .delete(url) 51 | .set({ 52 | Accept: "application/json", 53 | "Content-Type": "application/json", 54 | ...(authorization ? { authorization } : {}), 55 | }) 56 | .send(); 57 | 58 | return response; 59 | }; 60 | -------------------------------------------------------------------------------- /src/api/auth/__tests__/__snapshots__/authLogin.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should return error if email it was not passed 1`] = ` 4 | Object { 5 | "message": "Email or password incorrect", 6 | } 7 | `; 8 | 9 | exports[`should return error if login and password it was not passed 1`] = ` 10 | Object { 11 | "message": "Email or password incorrect", 12 | } 13 | `; 14 | 15 | exports[`should return error if password it was not passed 1`] = ` 16 | Object { 17 | "message": "Email or password incorrect", 18 | } 19 | `; 20 | 21 | exports[`should return error if user doest not exist 1`] = ` 22 | Object { 23 | "message": "Email or password incorrect", 24 | } 25 | `; 26 | 27 | exports[`should return for wrong email 1`] = ` 28 | Object { 29 | "message": "Email or password incorrect", 30 | } 31 | `; 32 | 33 | exports[`should return for wrong password 1`] = ` 34 | Object { 35 | "message": "Email or password incorrect", 36 | } 37 | `; 38 | 39 | exports[`should return token for user email and password valid 1`] = ` 40 | Object { 41 | "message": "User authenticated with success", 42 | "token": "FROZEN-TOKEN", 43 | "user": Object { 44 | "__v": 0, 45 | "_id": "ObjectId", 46 | "createdAt": "FROZEN-CREATEDAT", 47 | "email": "biazita@test.com", 48 | "name": "User 1", 49 | "password": "FROZEN-PASSWORD", 50 | "removedAt": "EMPTY", 51 | "updatedAt": "FROZEN-UPDATEDAT", 52 | }, 53 | } 54 | `; 55 | 56 | exports[`should return user not found if user is already removed 1`] = ` 57 | Object { 58 | "message": "Email or password incorrect", 59 | } 60 | `; 61 | -------------------------------------------------------------------------------- /src/modules/user/UserModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document, Model } from "mongoose"; 2 | import bcrypt from "bcryptjs"; 3 | 4 | const UserSchema = new mongoose.Schema( 5 | { 6 | name: { 7 | type: String, 8 | required: true, 9 | }, 10 | email: { 11 | type: String, 12 | required: true, 13 | index: true, 14 | }, 15 | password: { 16 | type: String, 17 | hidden: true, 18 | }, 19 | removedAt: { 20 | type: Date, 21 | index: true, 22 | default: null, 23 | 24 | es_indexed: true, 25 | }, 26 | }, 27 | { 28 | timestamps: { 29 | createdAt: "createdAt", 30 | updatedAt: "updatedAt", 31 | }, 32 | collection: "User", 33 | } 34 | ); 35 | 36 | export interface IUser extends Document { 37 | name: string; 38 | email: string; 39 | password: string; 40 | removedAt: Date | null; 41 | authenticate: (plainTextPassword: string) => boolean; 42 | encryptPassword: (password: string | undefined) => string; 43 | createdAt: Date; 44 | updatedAt: Date; 45 | } 46 | 47 | UserSchema.pre("save", function encryptPasswordHook(next) { 48 | // Hash the password 49 | if (this.isModified("password")) { 50 | this.password = this.encryptPassword(this.password); 51 | } 52 | 53 | return next(); 54 | }); 55 | 56 | UserSchema.methods = { 57 | authenticate(plainTextPassword: string) { 58 | return bcrypt.compareSync(plainTextPassword, this.password); 59 | }, 60 | encryptPassword(password: string) { 61 | return bcrypt.hashSync(password, 8); 62 | }, 63 | }; 64 | 65 | const UserModel: Model = mongoose.model("User", UserSchema); 66 | 67 | export default UserModel; 68 | -------------------------------------------------------------------------------- /src/api/apiHelpers.ts: -------------------------------------------------------------------------------- 1 | import { Model, Types } from "mongoose"; 2 | import { Context } from "koa"; 3 | import { getObjectId } from "../../test"; 4 | 5 | export const getSkipAndLimit = (ctx: Context) => { 6 | const { skip = 0, limit = 100 } = ctx.query; 7 | 8 | if (skip < 0 || limit < 0) { 9 | return { 10 | skip: null, 11 | limit: null, 12 | errors: [ 13 | { 14 | data: { skip, limit }, 15 | message: "Pagination values should be positive values", 16 | }, 17 | ], 18 | }; 19 | } 20 | 21 | const mongoLimit = Math.min(parseInt(limit, 10), 100); 22 | const mongoSkip = parseInt(skip, 10); 23 | 24 | return { 25 | skip: mongoSkip, 26 | limit: mongoLimit, 27 | errors: null, 28 | }; 29 | }; 30 | 31 | type ErrorValidate = { 32 | data: {}; 33 | message: string; 34 | }; 35 | 36 | type PageInfo = { 37 | errors?: ErrorValidate[]; 38 | skip: number; 39 | limit: number; 40 | totalCount: number; 41 | hasPreviousPage: number; 42 | hasNextPage: number; 43 | }; 44 | 45 | export const getPageInfo = async (ctx: Context, model: Model): PageInfo => { 46 | const { skip, limit, errors } = getSkipAndLimit(ctx); 47 | 48 | if (errors) { 49 | return { 50 | errors, 51 | skip, 52 | limit, 53 | totalCount: null, 54 | hasPreviousPage: null, 55 | hasNextPage: null, 56 | }; 57 | } 58 | 59 | const conditionsTotalCount = { 60 | removedAt: null, 61 | }; 62 | 63 | const totalCount = await model.count(conditionsTotalCount); 64 | 65 | const hasPreviousPage = skip > 0; 66 | const hasNextPage = skip + limit < totalCount; 67 | 68 | return { 69 | skip, 70 | limit, 71 | totalCount, 72 | hasPreviousPage, 73 | hasNextPage, 74 | }; 75 | }; 76 | 77 | export const checkObjectId = (id: string) => { 78 | if (!Types.ObjectId.isValid(id)) { 79 | return null; 80 | } 81 | 82 | return getObjectId(id); 83 | }; 84 | -------------------------------------------------------------------------------- /src/api/user/__tests__/userGet.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | clearDbAndRestartCounters, 3 | connectMongoose, 4 | disconnectMongoose, 5 | createGetApiCall, 6 | sanitizeTestObject, 7 | } from "../../../../test"; 8 | import { createUser } from "../../../modules/user/fixtures/createUser"; 9 | import { base64 } from "../../../auth/base64"; 10 | import { generateToken } from "../../../auth/generateToken"; 11 | 12 | beforeAll(connectMongoose); 13 | 14 | beforeEach(clearDbAndRestartCounters); 15 | 16 | afterAll(disconnectMongoose); 17 | 18 | const getUrl = (id: string) => `/api/user/${id}`; 19 | 20 | it("should return error if authorization header does not exist", async () => { 21 | const response = await createGetApiCall({ 22 | url: getUrl("5c42132aa591a2001ad46264"), 23 | }); 24 | 25 | expect(response.status).toBe(401); 26 | expect(response.body.message).toBe("Unauthorized"); 27 | expect(response.body).toMatchSnapshot(); 28 | }); 29 | 30 | it("should return error if user it was not found", async () => { 31 | const admin = await createUser({}); 32 | const authorization = base64(generateToken(admin)); 33 | 34 | const response = await createGetApiCall({ 35 | url: getUrl("5c42132aa591a2001ad46264"), 36 | authorization, 37 | }); 38 | 39 | expect(response.status).toBe(400); 40 | expect(response.body.message).toBe("User not found"); 41 | expect(response.body).toMatchSnapshot(); 42 | }); 43 | 44 | it("should return user by id", async () => { 45 | const admin = await createUser({}); 46 | const authorization = base64(generateToken(admin)); 47 | const user = await createUser({}); 48 | const response = await createGetApiCall({ 49 | url: getUrl(user._id), 50 | authorization, 51 | }) 52 | 53 | expect(response.status).toBe(200); 54 | expect(response.body.user.name).toBe(user.name); 55 | expect(response.body.user.email).toBe(user.email); 56 | expect(sanitizeTestObject(response.body)).toMatchSnapshot(); 57 | }); 58 | -------------------------------------------------------------------------------- /src/api/user/__tests__/userGetAll.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | clearDbAndRestartCounters, 3 | connectMongoose, 4 | createGetApiCall, 5 | disconnectMongoose, 6 | sanitizeTestObject, 7 | } from "../../../../test"; 8 | import { createUser } from "../../../modules/user/fixtures/createUser"; 9 | import { base64 } from "../../../auth/base64"; 10 | import { generateToken } from "../../../auth/generateToken"; 11 | 12 | beforeAll(connectMongoose); 13 | 14 | beforeEach(clearDbAndRestartCounters); 15 | 16 | afterAll(disconnectMongoose); 17 | 18 | const url = "/api/user"; 19 | 20 | it("should return error if authorization header does not exist", async () => { 21 | const user = await createUser({}); 22 | const userB = await createUser({}); 23 | 24 | const response = await createGetApiCall({ 25 | url, 26 | }); 27 | 28 | expect(response.status).toBe(401); 29 | expect(response.body.message).toBe("Unauthorized"); 30 | expect(sanitizeTestObject(response.body)).toMatchSnapshot(); 31 | }); 32 | 33 | it("should return a list of users", async () => { 34 | const admin = await createUser({}); 35 | const authorization = base64(generateToken(admin)); 36 | 37 | const user = await createUser({}); 38 | const userB = await createUser({}); 39 | 40 | const response = await createGetApiCall({ 41 | url, 42 | authorization, 43 | }); 44 | 45 | expect(response.status).toBe(200); 46 | expect(sanitizeTestObject(response.body)).toMatchSnapshot(); 47 | }); 48 | 49 | it("should return 100 users if no skip limit is not specific", async () => { 50 | const admin = await createUser({}); 51 | const authorization = base64(generateToken(admin)); 52 | for (const i of Array.from(Array(110).keys())) { 53 | await createUser({ name: `user#${i + 2}` }); 54 | } 55 | 56 | const response = await createGetApiCall({ 57 | url, 58 | authorization, 59 | }); 60 | 61 | expect(response.status).toBe(200); 62 | expect(response.body.users.length).toBe(100); 63 | expect(sanitizeTestObject(response.body)).toMatchSnapshot(); 64 | }); 65 | 66 | it("should paginate skipping 90 users and limit 10", async () => { 67 | const admin = await createUser({}); 68 | const authorization = base64(generateToken(admin)); 69 | for (const i of Array.from(Array(110).keys())) { 70 | await createUser({ name: `user#${i + 2}` }); 71 | } 72 | 73 | const response = await createGetApiCall({ 74 | url: `${url}?skip=90&limit=10`, 75 | authorization, 76 | }); 77 | 78 | expect(response.status).toBe(200); 79 | expect(response.body.users.length).toBe(10); 80 | expect(sanitizeTestObject(response.body)).toMatchSnapshot(); 81 | }); 82 | -------------------------------------------------------------------------------- /src/api/user/userUpdate.ts: -------------------------------------------------------------------------------- 1 | import { checkObjectId } from "../apiHelpers"; 2 | import UserModel from "../../modules/user/UserModel"; 3 | import { getUserApi } from "./userGet"; 4 | 5 | import bcrypt from 'bcryptjs'; 6 | 7 | export const userUpdate = async (ctx: any) => { 8 | const { id } = ctx.params; 9 | 10 | const _id = checkObjectId(id); 11 | 12 | if (!_id) { 13 | ctx.status = 400; 14 | ctx.body = { 15 | message: "User not found", 16 | }; 17 | return; 18 | } 19 | 20 | const user = await UserModel.findOne({ 21 | _id, 22 | removedAt: null, 23 | }); 24 | 25 | if (!user) { 26 | ctx.status = 400; 27 | ctx.body = { 28 | message: "User not found", 29 | }; 30 | return; 31 | } 32 | 33 | const { user: userExist } = await getUserApi({ email: user?.email }); 34 | 35 | if (userExist?.email == ctx.request.body?.user?.email) { 36 | ctx.status = 400; 37 | ctx.body = { 38 | message: "Email already in use", 39 | }; 40 | return; 41 | } 42 | interface dataToUpdate { 43 | email?: string; 44 | name?: string; 45 | password?: string; 46 | } 47 | 48 | const dataToUpdate: dataToUpdate = {}; 49 | 50 | if (ctx.request.body?.user?.name) { 51 | 52 | dataToUpdate.name = ctx.request.body?.user?.name; 53 | 54 | } 55 | 56 | if (ctx.request.body?.user?.password) { 57 | 58 | const newPassword = bcrypt.hashSync(ctx.request.body?.user?.password, 8); 59 | 60 | dataToUpdate.password = newPassword 61 | 62 | } 63 | 64 | if (ctx.request.body?.user?.email) { 65 | 66 | dataToUpdate.email = ctx.request.body?.user?.email; 67 | 68 | if (userExist?.email == ctx.request.body?.user?.email) { 69 | ctx.status = 400; 70 | ctx.body = { 71 | message: "Email already in use", 72 | }; 73 | return; 74 | } 75 | } 76 | 77 | try { 78 | await UserModel.updateOne( 79 | { 80 | _id: user._id, 81 | }, 82 | { 83 | $set: { 84 | ...dataToUpdate 85 | }, 86 | }, 87 | ); 88 | 89 | const { user: userUpdated, error } = await getUserApi({ id: user._id, isUpdate: true }); 90 | 91 | if (error) { 92 | ctx.status = 400; 93 | ctx.body = { 94 | message: "Error while editing user", 95 | }; 96 | return; 97 | } 98 | 99 | ctx.status = 200; 100 | ctx.body = { 101 | user: userUpdated, 102 | }; 103 | } catch (err) { 104 | // eslint-disable-next-line 105 | console.log("err: ", err); 106 | 107 | ctx.status = 500; 108 | ctx.body = { 109 | message: "Unknown error", 110 | }; 111 | } 112 | }; 113 | -------------------------------------------------------------------------------- /src/api/user/userGet.ts: -------------------------------------------------------------------------------- 1 | import { getObjectId } from "../../../test"; 2 | import User from "../../modules/user/UserModel"; 3 | 4 | export const userSelection = { 5 | _id: 1, 6 | name: 1, 7 | email: 1, 8 | removeAt: 1, 9 | }; 10 | 11 | type UserPayload = { 12 | _id: string; 13 | name: string; 14 | email: string; 15 | }; 16 | 17 | type GetUserApiPayload = { 18 | error: string | null; 19 | user: UserPayload | null; 20 | }; 21 | 22 | const getConditions = (id?: string, email?: string) => { 23 | if (id) { 24 | return { 25 | error: null, 26 | conditions: { 27 | _id: getObjectId(id), 28 | }, 29 | }; 30 | } 31 | 32 | if (email) { 33 | return { 34 | error: null, 35 | conditions: { 36 | email, 37 | }, 38 | }; 39 | } 40 | 41 | return { 42 | error: "Invalid user", 43 | }; 44 | }; 45 | 46 | interface IGetUserAPI { 47 | id?: string; 48 | email?: string; 49 | isDelete?: boolean; 50 | isUpdate?: boolean; 51 | } 52 | 53 | export const getUserApi = async ({ 54 | email, 55 | id, 56 | isDelete, 57 | isUpdate, 58 | }: IGetUserAPI): Promise => { 59 | const { conditions, error } = getConditions(id, email); 60 | 61 | if (error) { 62 | return { 63 | error, 64 | user: null, 65 | }; 66 | } 67 | 68 | let user; 69 | 70 | if (isDelete) { 71 | user = await User.findOne({ 72 | ...conditions, 73 | }) 74 | .select("-password -createdAt -updatedAt") 75 | .lean(); 76 | } else { 77 | user = await User.findOne({ 78 | ...conditions, 79 | }) 80 | .select(userSelection) 81 | .lean(); 82 | } 83 | 84 | if(isUpdate) { 85 | user = await User.findOne({ 86 | ...conditions, 87 | }) 88 | .select(userSelection) 89 | .lean(); 90 | } 91 | 92 | if (!user) { 93 | return { 94 | error: "User not found", 95 | user: null, 96 | }; 97 | } 98 | 99 | return { 100 | error: null, 101 | user, 102 | }; 103 | }; 104 | 105 | export const userGet = async (ctx) => { 106 | const { id } = ctx.params; 107 | 108 | try { 109 | if (!id) { 110 | ctx.status = 400; 111 | ctx.body = { 112 | message: "You must provide an id", 113 | }; 114 | return; 115 | } 116 | 117 | const { user, error } = await getUserApi({ id }); 118 | 119 | if (error) { 120 | ctx.status = 400; 121 | ctx.body = { 122 | message: error, 123 | }; 124 | return; 125 | } 126 | 127 | ctx.status = 200; 128 | ctx.body = { 129 | user, 130 | }; 131 | 132 | return; 133 | } catch (err) { 134 | console.log("err:", err); 135 | 136 | ctx.body = { 137 | message: "Unknown error", 138 | }; 139 | } 140 | }; 141 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "restris", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "dependencies": { 6 | "@koa/cors": "^5.0.0", 7 | "@koa/router": "^12.0.1", 8 | "@types/jest": "^28.1.6", 9 | "bcryptjs": "^2.4.3", 10 | "dotenv-safe": "8.2.0", 11 | "graphql": "^16.10.0", 12 | "graphql-relay": "^0.10.2", 13 | "isomorphic-fetch": "3.0.0", 14 | "jsonwebtoken": "^9.0.2", 15 | "jwt-decode": "^3.1.2", 16 | "koa": "2.15.3", 17 | "koa-bodyparser": "^4.4.1", 18 | "koa-convert": "^2.0.0", 19 | "koa-cors": "^0.0.16", 20 | "koa-logger": "3.2.1", 21 | "koa-router": "12.0.1", 22 | "mongoose": "7.6.4", 23 | "swagger-jsdoc": "^6.2.8", 24 | "uuid": "10.0.0", 25 | "yup": "^1.4.0" 26 | }, 27 | "devDependencies": { 28 | "@babel/cli": "7.26.4", 29 | "@babel/core": "7.26.0", 30 | "@babel/node": "7.25.0", 31 | "@babel/plugin-proposal-async-generator-functions": "7.20.7", 32 | "@babel/plugin-proposal-class-properties": "7.18.6", 33 | "@babel/plugin-proposal-export-default-from": "7.24.7", 34 | "@babel/plugin-proposal-export-namespace-from": "7.18.9", 35 | "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6", 36 | "@babel/plugin-proposal-object-rest-spread": "7.20.7", 37 | "@babel/plugin-proposal-optional-chaining": "7.21.0", 38 | "@babel/plugin-transform-async-to-generator": "7.24.7", 39 | "@babel/polyfill": "7.12.1", 40 | "@babel/preset-env": "7.25.4", 41 | "@babel/preset-typescript": "7.26.0", 42 | "@types/babel__core": "7.20.5", 43 | "@types/babel__preset-env": "7.9.7", 44 | "@types/bcryptjs": "^2.4.6", 45 | "@types/dotenv-safe": "8.1.5", 46 | "@types/isomorphic-fetch": "0.0.39", 47 | "@types/koa": "2.15.0", 48 | "@types/koa-bodyparser": "^4.3.12", 49 | "@types/koa-convert": "^1.2.7", 50 | "@types/koa-cors": "^0.0.6", 51 | "@types/koa-logger": "3.1.5", 52 | "@types/koa-router": "7.4.8", 53 | "@types/koa__cors": "^5.0.0", 54 | "@types/koa__router": "^12.0.4", 55 | "@types/webpack-dev-server": "4.7.2", 56 | "@types/webpack-node-externals": "3.0.4", 57 | "babel-jest": "26.0.1", 58 | "babel-loader": "9.1.3", 59 | "clean-webpack-plugin": "4.0.0", 60 | "jest": "25.1.0", 61 | "jest-fetch-mock": "^3.0.3", 62 | "jest-junit": "16.0.0", 63 | "mongodb-memory-server": "9.4.0", 64 | "reload-server-webpack-plugin": "1.0.1", 65 | "supertest": "^6.3.4", 66 | "webpack": "5.94.0", 67 | "webpack-cli": "5.1.4", 68 | "webpack-dev-server": "4.15.1", 69 | "webpack-merge": "5.10.0", 70 | "webpack-node-externals": "3.0.0" 71 | }, 72 | "scripts": { 73 | "jest": "jest", 74 | "start": "webpack --watch --progress --config webpack.config.js", 75 | "swagger": "yarn swagger:json && yarn swagger:yml", 76 | "swagger:json": "yarn swagger-jsdoc -d src/swagger/config.js 'src/api/**/*.yml' -o ./src/swagger.json", 77 | "swagger:yml": "yarn swagger-jsdoc -d src/swagger/config.js 'src/api/**/*.yml' -o ./src/swagger.yml" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/api/user/__tests__/userDelete.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | clearDbAndRestartCounters, 3 | connectMongoose, 4 | createDeleteApiCall, 5 | defaultFrozenKeys, 6 | disconnectMongoose, 7 | sanitizeTestObject, 8 | } from "../../../../test"; 9 | import { createUser } from "../../../modules/user/fixtures/createUser"; 10 | import { base64 } from "../../../auth/base64"; 11 | import { generateToken } from "../../../auth/generateToken"; 12 | 13 | beforeAll(connectMongoose); 14 | 15 | beforeEach(clearDbAndRestartCounters); 16 | 17 | afterAll(disconnectMongoose); 18 | 19 | const getUrl = (id: string) => `/api/user/${id}`; 20 | 21 | it("should return error if authorization header does not exist", async () => { 22 | const response = await createDeleteApiCall({ 23 | url: getUrl("123"), 24 | }); 25 | 26 | expect(response.status).toBe(401); 27 | expect(response.body.message).toBe("Unauthorized"); 28 | expect(response.body).toMatchSnapshot(); 29 | }); 30 | 31 | it("should return error if id it is not valid", async () => { 32 | const admin = await createUser({}); 33 | const authorization = base64(generateToken(admin)); 34 | const response = await createDeleteApiCall({ 35 | url: getUrl("123"), 36 | authorization, 37 | }); 38 | 39 | expect(response.status).toBe(400); 40 | expect(response.body.message).toBe("User not found"); 41 | expect(response.body).toMatchSnapshot(); 42 | }); 43 | 44 | it("should return error if user it was not found", async () => { 45 | const admin = await createUser({}); 46 | const authorization = base64(generateToken(admin)); 47 | 48 | const response = await createDeleteApiCall({ 49 | url: getUrl("5c42132aa591a2001ad46264"), 50 | authorization, 51 | }); 52 | 53 | expect(response.status).toBe(400); 54 | expect(response.body.message).toBe("User not found"); 55 | expect(response.body).toMatchSnapshot(); 56 | }); 57 | 58 | it("should delete an user", async () => { 59 | const admin = await createUser({}); 60 | const authorization = base64(generateToken(admin)); 61 | const user = await createUser({}); 62 | 63 | const response = await createDeleteApiCall({ 64 | url: getUrl(user._id), 65 | authorization, 66 | }); 67 | 68 | expect(response.status).toBe(200); 69 | expect(response.body.user.name).toBe(user.name); 70 | expect(response.body.user.email).toBe(user.email); 71 | expect(response.body.user.removedAt).toBeDefined(); 72 | expect( 73 | sanitizeTestObject(response.body, [...defaultFrozenKeys, "removedAt"]) 74 | ).toMatchSnapshot(); 75 | }); 76 | 77 | it("should return user not found for a user already deleted", async () => { 78 | const admin = await createUser({}); 79 | const authorization = base64(generateToken(admin)); 80 | const user = await createUser({ 81 | removedAt: new Date(), 82 | }); 83 | 84 | const response = await createDeleteApiCall({ 85 | url: getUrl(user._id), 86 | authorization, 87 | }); 88 | 89 | expect(response.status).toBe(400); 90 | expect(response.body.message).toBe("User not found"); 91 | expect(sanitizeTestObject(response.body)).toMatchSnapshot(); 92 | }); 93 | -------------------------------------------------------------------------------- /src/api/user/__tests__/userPost.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | clearDbAndRestartCounters, 3 | connectMongoose, 4 | disconnectMongoose, 5 | createApiCall, 6 | sanitizeTestObject, 7 | } from "../../../../test"; 8 | import { createUser } from "../../../modules/user/fixtures/createUser"; 9 | 10 | beforeAll(connectMongoose); 11 | 12 | beforeEach(clearDbAndRestartCounters); 13 | 14 | afterAll(disconnectMongoose); 15 | 16 | const url = "/api/user"; 17 | 18 | it("should return error if user it was not passed", async () => { 19 | const response = await createApiCall({ url, payload: {} }); 20 | 21 | expect(response.status).toBe(400); 22 | expect(response.body.message).toBe("User is required"); 23 | expect(response.body).toMatchSnapshot(); 24 | }); 25 | 26 | it("should return error if user name it was not passed", async () => { 27 | const user = { email: "test@test.com", password: "123456" }; 28 | const response = await createApiCall({ 29 | url, 30 | payload: { user }, 31 | }); 32 | 33 | expect(response.status).toBe(400); 34 | expect(response.body.message).toBe("name is a required field"); 35 | expect(response.body).toMatchSnapshot(); 36 | }); 37 | 38 | it("should return error if user email it was not passed", async () => { 39 | const user = { name: "obi wan kenobi", password: "123456" }; 40 | const response = await createApiCall({ 41 | url, 42 | payload: { user }, 43 | }); 44 | 45 | expect(response.status).toBe(400); 46 | expect(response.body.message).toBe("email is a required field"); 47 | expect(response.body).toMatchSnapshot(); 48 | }); 49 | 50 | it("should return error if user password it was not passed", async () => { 51 | 52 | const user = { name: "obi wan kenobi", email: "test@test.com" }; 53 | const response = await createApiCall({ 54 | url, 55 | payload: { user }, 56 | }); 57 | 58 | expect(response.status).toBe(400); 59 | expect(response.body.message).toBe("password is a required field"); 60 | expect(response.body).toMatchSnapshot(); 61 | }); 62 | 63 | it("should create user with success", async () => { 64 | const user = { 65 | name: "obi wan kenobi", 66 | email: "test@test.com", 67 | password: "123456", 68 | }; 69 | 70 | const response = await createApiCall({ 71 | url, 72 | payload: { user }, 73 | }); 74 | 75 | expect(response.status).toBe(200); 76 | expect(response.body.user.name).toBe(user.name); 77 | expect(response.body.user.email).toBe(user.email); 78 | expect(sanitizeTestObject(response.body)).toMatchSnapshot(); 79 | }); 80 | 81 | it("should return error is email is already in use", async () => { 82 | await createUser({ 83 | name: "obi wan kenobi", 84 | email: "test@test.com", 85 | password: "123456", 86 | }); 87 | 88 | const userB = { 89 | name: "jay prichett", 90 | email: "test@test.com", 91 | password: "654321", 92 | }; 93 | 94 | const response = await createApiCall({ 95 | url, 96 | payload: { user: userB }, 97 | }); 98 | 99 | expect(response.status).toBe(400); 100 | expect(response.body.message).toBe("Email already in use"); 101 | expect(sanitizeTestObject(response.body)).toMatchSnapshot(); 102 | }); 103 | -------------------------------------------------------------------------------- /src/api/auth/__tests__/authLogin.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | clearDbAndRestartCounters, 3 | connectMongoose, 4 | createApiCall, 5 | defaultFrozenKeys, 6 | disconnectMongoose, 7 | sanitizeTestObject, 8 | } from "../../../../test"; 9 | import { createUser } from "../../../modules/user/fixtures/createUser"; 10 | 11 | beforeAll(connectMongoose); 12 | 13 | beforeEach(clearDbAndRestartCounters); 14 | 15 | afterAll(disconnectMongoose); 16 | 17 | const url = "/api/auth/login"; 18 | 19 | it("should return error if login and password it was not passed", async () => { 20 | const response = await createApiCall({ url, payload: {} }); 21 | 22 | expect(response.status).toBe(401); 23 | expect(response.body.message).toBe("Email or password incorrect"); 24 | expect(response.body).toMatchSnapshot(); 25 | }); 26 | 27 | it("should return error if email it was not passed", async () => { 28 | const response = await createApiCall({ 29 | url, 30 | payload: { 31 | password: "123456", 32 | }, 33 | }); 34 | 35 | expect(response.status).toBe(401); 36 | expect(response.body.message).toBe("Email or password incorrect"); 37 | expect(response.body).toMatchSnapshot(); 38 | }); 39 | 40 | it("should return error if password it was not passed", async () => { 41 | const response = await createApiCall({ 42 | url, 43 | payload: { email: "test@test.com" }, 44 | }); 45 | 46 | expect(response.status).toBe(401); 47 | expect(response.body.message).toBe("Email or password incorrect"); 48 | expect(response.body).toMatchSnapshot(); 49 | }); 50 | 51 | it("should return error if user doest not exist", async () => { 52 | const response = await createApiCall({ 53 | url, 54 | payload: { 55 | email: "test@test.com", 56 | password: "123456", 57 | }, 58 | }); 59 | 60 | expect(response.status).toBe(401); 61 | expect(response.body.message).toBe("Email or password incorrect"); 62 | expect(response.body).toMatchSnapshot(); 63 | }); 64 | 65 | it("should return user not found if user is already removed", async () => { 66 | const user = await createUser({ 67 | removedAt: new Date(), 68 | }); 69 | 70 | const response = await createApiCall({ 71 | url, 72 | payload: { email: user.email, password: user.password }, 73 | }); 74 | 75 | expect(response.status).toBe(401); 76 | expect(response.body.message).toBe("Email or password incorrect"); 77 | expect(response.body).toMatchSnapshot(); 78 | }); 79 | 80 | it("should return for wrong email", async () => { 81 | const user = await createUser({ 82 | email: "biazita@test.com", 83 | password: "123456bleble", 84 | }); 85 | 86 | const response = await createApiCall({ 87 | url, 88 | payload: { 89 | email: "agositnhoo@test.com", 90 | password: user.password, 91 | }, 92 | }); 93 | 94 | expect(response.status).toBe(401); 95 | expect(response.body.message).toBe("Email or password incorrect"); 96 | expect(response.body).toMatchSnapshot(); 97 | }); 98 | 99 | it("should return for wrong password", async () => { 100 | const user = await createUser({ 101 | email: "biazita@test.com", 102 | password: "123456taxi", 103 | }); 104 | 105 | const response = await createApiCall({ 106 | url, 107 | payload: { email: user.email, password: "taxi" }, 108 | }); 109 | 110 | expect(response.status).toBe(401); 111 | expect(response.body.message).toBe("Email or password incorrect"); 112 | expect(response.body).toMatchSnapshot(); 113 | }); 114 | 115 | it("should return token for user email and password valid", async () => { 116 | const user = await createUser({ 117 | email: "biazita@test.com", 118 | password: "123456taxi", 119 | }); 120 | 121 | const response = await createApiCall({ 122 | url, 123 | payload: { email: user.email, password: "123456taxi" }, 124 | }); 125 | 126 | expect(response.status).toBe(200); 127 | expect(response.body.message).toBe("User authenticated with success"); 128 | expect(response.body.token).toBeDefined(); 129 | expect(response.body.user); 130 | expect( 131 | sanitizeTestObject(response.body, [...defaultFrozenKeys, "token"]) 132 | ).toMatchSnapshot(); 133 | }); 134 | -------------------------------------------------------------------------------- /test/sanitizeTestObject.ts: -------------------------------------------------------------------------------- 1 | import { fromGlobalId } from "graphql-relay"; 2 | import mongoose from "mongoose"; 3 | 4 | const { ObjectId } = mongoose.Types; 5 | 6 | // https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540 7 | type Value = 8 | | string 9 | | boolean 10 | | null 11 | | undefined 12 | | IValueObject 13 | | IValueArray 14 | | object; 15 | interface IValueObject { 16 | [x: string]: Value; 17 | } 18 | type IValueArray = Array; 19 | export const sanitizeValue = ( 20 | value: Value, 21 | field: string | null, 22 | keys: string[], 23 | ignore: string[] = [], 24 | jsonKeys: string[] = [] 25 | ): Value => { 26 | // If value is empty, return `EMPTY` value so it's easier to debug 27 | // Check if value is boolean 28 | if (typeof value === "boolean") { 29 | return value; 30 | } 31 | 32 | if (!value && value !== 0) { 33 | return "EMPTY"; 34 | } 35 | // If this current field is specified on the `keys` array, we simply redefine it 36 | // so it stays the same on the snapshot 37 | if (keys.indexOf(field) !== -1) { 38 | return `FROZEN-${field.toUpperCase()}`; 39 | } 40 | 41 | if (jsonKeys.indexOf(field) !== -1) { 42 | const jsonData = JSON.parse(value); 43 | 44 | return sanitizeTestObject(jsonData, keys, ignore, jsonKeys); 45 | } 46 | 47 | // if it's an array, sanitize the field 48 | if (Array.isArray(value)) { 49 | // normalize CoreMongooseArray to simple array to avoid breaking snapshot 50 | if ("isMongooseArray" in value && value.isMongooseArray) { 51 | const simpleArray = value.toObject(); 52 | 53 | return simpleArray.map((item) => sanitizeValue(item, null, keys, ignore)); 54 | } 55 | 56 | return value.map((item) => sanitizeValue(item, null, keys, ignore)); 57 | } 58 | 59 | // Check if it's not an array and can be transformed into a string 60 | if (!Array.isArray(value) && typeof value.toString === "function") { 61 | // Remove any non-alphanumeric character from value 62 | const cleanValue = value.toString().replace(/[^a-z0-9]/gi, ""); 63 | 64 | // Check if it's a valid `ObjectId`, if so, replace it with a static value 65 | if ( 66 | ObjectId.isValid(cleanValue) && 67 | value.toString().indexOf(cleanValue) !== -1 68 | ) { 69 | return value.toString().replace(cleanValue, "ObjectId"); 70 | } 71 | 72 | if (value.constructor === Date) { 73 | // TODO - should we always freeze Date ? 74 | return value; 75 | // return `FROZEN-${field.toUpperCase()}`; 76 | } 77 | 78 | // If it's an object, we call sanitizeTestObject function again to handle nested fields 79 | if (typeof value === "object") { 80 | return sanitizeTestObject(value, keys, ignore, jsonKeys); 81 | } 82 | 83 | // Check if it's a valid globalId, if so, replace it with a static value 84 | const result = fromGlobalId(cleanValue); 85 | if (result.type && result.id && ObjectId.isValid(result.id)) { 86 | return "GlobalID"; 87 | } 88 | } 89 | 90 | // If it's an object, we call sanitizeTestObject function again to handle nested fields 91 | if (typeof value === "object") { 92 | return sanitizeTestObject(value, keys, ignore, jsonKeys); 93 | } 94 | 95 | return value; 96 | }; 97 | 98 | export const defaultFrozenKeys = ["id", "createdAt", "updatedAt", "password"]; 99 | 100 | /** 101 | * Sanitize a test object removing the mentions of a `ObjectId` 102 | * @param payload {object} The object to be sanitized 103 | * @param keys {[string]} Array of keys to redefine the value on the payload 104 | * @param ignore {[string]} Array of keys to ignore 105 | * @returns {object} The sanitized object 106 | */ 107 | export const sanitizeTestObject = ( 108 | payload: Value, 109 | keys = defaultFrozenKeys, 110 | ignore: string[] = [], 111 | jsonKeys: string[] = [] 112 | ) => 113 | // TODO - treat array as arrays 114 | payload && 115 | Object.keys(payload).reduce((sanitizedObj, field) => { 116 | const value = payload[field]; 117 | 118 | if (ignore.indexOf(field) !== -1) { 119 | return { 120 | ...sanitizedObj, 121 | [field]: value, 122 | }; 123 | } 124 | 125 | const sanitizedValue = sanitizeValue(value, field, keys, ignore, jsonKeys); 126 | 127 | return { 128 | ...sanitizedObj, 129 | [field]: sanitizedValue, 130 | }; 131 | }, {}); 132 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "moduleResolution": "node", 7 | "lib": [ /* Specify library files to be included in the compilation. */ 8 | "esnext", 9 | "dom", 10 | "dom.iterable" 11 | ], 12 | // "allowJs": true, /* Allow javascript files to be compiled. */ 13 | // "checkJs": true, /* Report errors in .js files. */ 14 | // "jsx": "react-jsxdev", 15 | "jsx": "react-jsx", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 16 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 17 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 18 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 19 | // "outFile": "./", /* Concatenate and emit output to single file. */ 20 | "outDir": "./distTs", /* Redirect output structure to the directory. */ 21 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 22 | // "composite": true, /* Enable project compilation */ 23 | // "removeComments": true, /* Do not emit comments to output. */ 24 | "noEmit": true, /* Do not emit outputs. */ 25 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 28 | 29 | /* Strict Type-Checking Options */ 30 | "strict": true, /* Enable all strict type-checking options. */ 31 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 32 | // "strictNullChecks": true, /* Enable strict null checks. */ 33 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 34 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 35 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 36 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 37 | 38 | /* Additional Checks */ 39 | "noUnusedLocals": true, /* Report errors on unused locals. */ 40 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 41 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 42 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 43 | 44 | /* Module Resolution Options */ 45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // }, 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "resolveJsonModule": true, 54 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | 57 | /* Source Map Options */ 58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | 63 | /* Experimental Options */ 64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | "skipLibCheck": true 67 | } 68 | } -------------------------------------------------------------------------------- /src/api/user/__tests__/__snapshots__/userGetAll.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should paginate skipping 90 users and limit 10 1`] = ` 4 | Object { 5 | "pageInfo": Object { 6 | "hasNextPage": true, 7 | "hasPreviousPage": true, 8 | "limit": 10, 9 | "skip": 90, 10 | "totalCount": 111, 11 | }, 12 | "users": Array [ 13 | Object { 14 | "_id": "ObjectId", 15 | "email": "user91@test.com", 16 | "name": "user#91", 17 | }, 18 | Object { 19 | "_id": "ObjectId", 20 | "email": "user92@test.com", 21 | "name": "user#92", 22 | }, 23 | Object { 24 | "_id": "ObjectId", 25 | "email": "user93@test.com", 26 | "name": "user#93", 27 | }, 28 | Object { 29 | "_id": "ObjectId", 30 | "email": "user94@test.com", 31 | "name": "user#94", 32 | }, 33 | Object { 34 | "_id": "ObjectId", 35 | "email": "user95@test.com", 36 | "name": "user#95", 37 | }, 38 | Object { 39 | "_id": "ObjectId", 40 | "email": "user96@test.com", 41 | "name": "user#96", 42 | }, 43 | Object { 44 | "_id": "ObjectId", 45 | "email": "user97@test.com", 46 | "name": "user#97", 47 | }, 48 | Object { 49 | "_id": "ObjectId", 50 | "email": "user98@test.com", 51 | "name": "user#98", 52 | }, 53 | Object { 54 | "_id": "ObjectId", 55 | "email": "user99@test.com", 56 | "name": "user#99", 57 | }, 58 | Object { 59 | "_id": "ObjectId", 60 | "email": "user100@test.com", 61 | "name": "user#100", 62 | }, 63 | ], 64 | } 65 | `; 66 | 67 | exports[`should return 100 users if no skip limit is not specific 1`] = ` 68 | Object { 69 | "pageInfo": Object { 70 | "hasNextPage": true, 71 | "hasPreviousPage": false, 72 | "limit": 100, 73 | "skip": 0, 74 | "totalCount": 111, 75 | }, 76 | "users": Array [ 77 | Object { 78 | "_id": "ObjectId", 79 | "email": "user1@test.com", 80 | "name": "User 1", 81 | }, 82 | Object { 83 | "_id": "ObjectId", 84 | "email": "user2@test.com", 85 | "name": "user#2", 86 | }, 87 | Object { 88 | "_id": "ObjectId", 89 | "email": "user3@test.com", 90 | "name": "user#3", 91 | }, 92 | Object { 93 | "_id": "ObjectId", 94 | "email": "user4@test.com", 95 | "name": "user#4", 96 | }, 97 | Object { 98 | "_id": "ObjectId", 99 | "email": "user5@test.com", 100 | "name": "user#5", 101 | }, 102 | Object { 103 | "_id": "ObjectId", 104 | "email": "user6@test.com", 105 | "name": "user#6", 106 | }, 107 | Object { 108 | "_id": "ObjectId", 109 | "email": "user7@test.com", 110 | "name": "user#7", 111 | }, 112 | Object { 113 | "_id": "ObjectId", 114 | "email": "user8@test.com", 115 | "name": "user#8", 116 | }, 117 | Object { 118 | "_id": "ObjectId", 119 | "email": "user9@test.com", 120 | "name": "user#9", 121 | }, 122 | Object { 123 | "_id": "ObjectId", 124 | "email": "user10@test.com", 125 | "name": "user#10", 126 | }, 127 | Object { 128 | "_id": "ObjectId", 129 | "email": "user11@test.com", 130 | "name": "user#11", 131 | }, 132 | Object { 133 | "_id": "ObjectId", 134 | "email": "user12@test.com", 135 | "name": "user#12", 136 | }, 137 | Object { 138 | "_id": "ObjectId", 139 | "email": "user13@test.com", 140 | "name": "user#13", 141 | }, 142 | Object { 143 | "_id": "ObjectId", 144 | "email": "user14@test.com", 145 | "name": "user#14", 146 | }, 147 | Object { 148 | "_id": "ObjectId", 149 | "email": "user15@test.com", 150 | "name": "user#15", 151 | }, 152 | Object { 153 | "_id": "ObjectId", 154 | "email": "user16@test.com", 155 | "name": "user#16", 156 | }, 157 | Object { 158 | "_id": "ObjectId", 159 | "email": "user17@test.com", 160 | "name": "user#17", 161 | }, 162 | Object { 163 | "_id": "ObjectId", 164 | "email": "user18@test.com", 165 | "name": "user#18", 166 | }, 167 | Object { 168 | "_id": "ObjectId", 169 | "email": "user19@test.com", 170 | "name": "user#19", 171 | }, 172 | Object { 173 | "_id": "ObjectId", 174 | "email": "user20@test.com", 175 | "name": "user#20", 176 | }, 177 | Object { 178 | "_id": "ObjectId", 179 | "email": "user21@test.com", 180 | "name": "user#21", 181 | }, 182 | Object { 183 | "_id": "ObjectId", 184 | "email": "user22@test.com", 185 | "name": "user#22", 186 | }, 187 | Object { 188 | "_id": "ObjectId", 189 | "email": "user23@test.com", 190 | "name": "user#23", 191 | }, 192 | Object { 193 | "_id": "ObjectId", 194 | "email": "user24@test.com", 195 | "name": "user#24", 196 | }, 197 | Object { 198 | "_id": "ObjectId", 199 | "email": "user25@test.com", 200 | "name": "user#25", 201 | }, 202 | Object { 203 | "_id": "ObjectId", 204 | "email": "user26@test.com", 205 | "name": "user#26", 206 | }, 207 | Object { 208 | "_id": "ObjectId", 209 | "email": "user27@test.com", 210 | "name": "user#27", 211 | }, 212 | Object { 213 | "_id": "ObjectId", 214 | "email": "user28@test.com", 215 | "name": "user#28", 216 | }, 217 | Object { 218 | "_id": "ObjectId", 219 | "email": "user29@test.com", 220 | "name": "user#29", 221 | }, 222 | Object { 223 | "_id": "ObjectId", 224 | "email": "user30@test.com", 225 | "name": "user#30", 226 | }, 227 | Object { 228 | "_id": "ObjectId", 229 | "email": "user31@test.com", 230 | "name": "user#31", 231 | }, 232 | Object { 233 | "_id": "ObjectId", 234 | "email": "user32@test.com", 235 | "name": "user#32", 236 | }, 237 | Object { 238 | "_id": "ObjectId", 239 | "email": "user33@test.com", 240 | "name": "user#33", 241 | }, 242 | Object { 243 | "_id": "ObjectId", 244 | "email": "user34@test.com", 245 | "name": "user#34", 246 | }, 247 | Object { 248 | "_id": "ObjectId", 249 | "email": "user35@test.com", 250 | "name": "user#35", 251 | }, 252 | Object { 253 | "_id": "ObjectId", 254 | "email": "user36@test.com", 255 | "name": "user#36", 256 | }, 257 | Object { 258 | "_id": "ObjectId", 259 | "email": "user37@test.com", 260 | "name": "user#37", 261 | }, 262 | Object { 263 | "_id": "ObjectId", 264 | "email": "user38@test.com", 265 | "name": "user#38", 266 | }, 267 | Object { 268 | "_id": "ObjectId", 269 | "email": "user39@test.com", 270 | "name": "user#39", 271 | }, 272 | Object { 273 | "_id": "ObjectId", 274 | "email": "user40@test.com", 275 | "name": "user#40", 276 | }, 277 | Object { 278 | "_id": "ObjectId", 279 | "email": "user41@test.com", 280 | "name": "user#41", 281 | }, 282 | Object { 283 | "_id": "ObjectId", 284 | "email": "user42@test.com", 285 | "name": "user#42", 286 | }, 287 | Object { 288 | "_id": "ObjectId", 289 | "email": "user43@test.com", 290 | "name": "user#43", 291 | }, 292 | Object { 293 | "_id": "ObjectId", 294 | "email": "user44@test.com", 295 | "name": "user#44", 296 | }, 297 | Object { 298 | "_id": "ObjectId", 299 | "email": "user45@test.com", 300 | "name": "user#45", 301 | }, 302 | Object { 303 | "_id": "ObjectId", 304 | "email": "user46@test.com", 305 | "name": "user#46", 306 | }, 307 | Object { 308 | "_id": "ObjectId", 309 | "email": "user47@test.com", 310 | "name": "user#47", 311 | }, 312 | Object { 313 | "_id": "ObjectId", 314 | "email": "user48@test.com", 315 | "name": "user#48", 316 | }, 317 | Object { 318 | "_id": "ObjectId", 319 | "email": "user49@test.com", 320 | "name": "user#49", 321 | }, 322 | Object { 323 | "_id": "ObjectId", 324 | "email": "user50@test.com", 325 | "name": "user#50", 326 | }, 327 | Object { 328 | "_id": "ObjectId", 329 | "email": "user51@test.com", 330 | "name": "user#51", 331 | }, 332 | Object { 333 | "_id": "ObjectId", 334 | "email": "user52@test.com", 335 | "name": "user#52", 336 | }, 337 | Object { 338 | "_id": "ObjectId", 339 | "email": "user53@test.com", 340 | "name": "user#53", 341 | }, 342 | Object { 343 | "_id": "ObjectId", 344 | "email": "user54@test.com", 345 | "name": "user#54", 346 | }, 347 | Object { 348 | "_id": "ObjectId", 349 | "email": "user55@test.com", 350 | "name": "user#55", 351 | }, 352 | Object { 353 | "_id": "ObjectId", 354 | "email": "user56@test.com", 355 | "name": "user#56", 356 | }, 357 | Object { 358 | "_id": "ObjectId", 359 | "email": "user57@test.com", 360 | "name": "user#57", 361 | }, 362 | Object { 363 | "_id": "ObjectId", 364 | "email": "user58@test.com", 365 | "name": "user#58", 366 | }, 367 | Object { 368 | "_id": "ObjectId", 369 | "email": "user59@test.com", 370 | "name": "user#59", 371 | }, 372 | Object { 373 | "_id": "ObjectId", 374 | "email": "user60@test.com", 375 | "name": "user#60", 376 | }, 377 | Object { 378 | "_id": "ObjectId", 379 | "email": "user61@test.com", 380 | "name": "user#61", 381 | }, 382 | Object { 383 | "_id": "ObjectId", 384 | "email": "user62@test.com", 385 | "name": "user#62", 386 | }, 387 | Object { 388 | "_id": "ObjectId", 389 | "email": "user63@test.com", 390 | "name": "user#63", 391 | }, 392 | Object { 393 | "_id": "ObjectId", 394 | "email": "user64@test.com", 395 | "name": "user#64", 396 | }, 397 | Object { 398 | "_id": "ObjectId", 399 | "email": "user65@test.com", 400 | "name": "user#65", 401 | }, 402 | Object { 403 | "_id": "ObjectId", 404 | "email": "user66@test.com", 405 | "name": "user#66", 406 | }, 407 | Object { 408 | "_id": "ObjectId", 409 | "email": "user67@test.com", 410 | "name": "user#67", 411 | }, 412 | Object { 413 | "_id": "ObjectId", 414 | "email": "user68@test.com", 415 | "name": "user#68", 416 | }, 417 | Object { 418 | "_id": "ObjectId", 419 | "email": "user69@test.com", 420 | "name": "user#69", 421 | }, 422 | Object { 423 | "_id": "ObjectId", 424 | "email": "user70@test.com", 425 | "name": "user#70", 426 | }, 427 | Object { 428 | "_id": "ObjectId", 429 | "email": "user71@test.com", 430 | "name": "user#71", 431 | }, 432 | Object { 433 | "_id": "ObjectId", 434 | "email": "user72@test.com", 435 | "name": "user#72", 436 | }, 437 | Object { 438 | "_id": "ObjectId", 439 | "email": "user73@test.com", 440 | "name": "user#73", 441 | }, 442 | Object { 443 | "_id": "ObjectId", 444 | "email": "user74@test.com", 445 | "name": "user#74", 446 | }, 447 | Object { 448 | "_id": "ObjectId", 449 | "email": "user75@test.com", 450 | "name": "user#75", 451 | }, 452 | Object { 453 | "_id": "ObjectId", 454 | "email": "user76@test.com", 455 | "name": "user#76", 456 | }, 457 | Object { 458 | "_id": "ObjectId", 459 | "email": "user77@test.com", 460 | "name": "user#77", 461 | }, 462 | Object { 463 | "_id": "ObjectId", 464 | "email": "user78@test.com", 465 | "name": "user#78", 466 | }, 467 | Object { 468 | "_id": "ObjectId", 469 | "email": "user79@test.com", 470 | "name": "user#79", 471 | }, 472 | Object { 473 | "_id": "ObjectId", 474 | "email": "user80@test.com", 475 | "name": "user#80", 476 | }, 477 | Object { 478 | "_id": "ObjectId", 479 | "email": "user81@test.com", 480 | "name": "user#81", 481 | }, 482 | Object { 483 | "_id": "ObjectId", 484 | "email": "user82@test.com", 485 | "name": "user#82", 486 | }, 487 | Object { 488 | "_id": "ObjectId", 489 | "email": "user83@test.com", 490 | "name": "user#83", 491 | }, 492 | Object { 493 | "_id": "ObjectId", 494 | "email": "user84@test.com", 495 | "name": "user#84", 496 | }, 497 | Object { 498 | "_id": "ObjectId", 499 | "email": "user85@test.com", 500 | "name": "user#85", 501 | }, 502 | Object { 503 | "_id": "ObjectId", 504 | "email": "user86@test.com", 505 | "name": "user#86", 506 | }, 507 | Object { 508 | "_id": "ObjectId", 509 | "email": "user87@test.com", 510 | "name": "user#87", 511 | }, 512 | Object { 513 | "_id": "ObjectId", 514 | "email": "user88@test.com", 515 | "name": "user#88", 516 | }, 517 | Object { 518 | "_id": "ObjectId", 519 | "email": "user89@test.com", 520 | "name": "user#89", 521 | }, 522 | Object { 523 | "_id": "ObjectId", 524 | "email": "user90@test.com", 525 | "name": "user#90", 526 | }, 527 | Object { 528 | "_id": "ObjectId", 529 | "email": "user91@test.com", 530 | "name": "user#91", 531 | }, 532 | Object { 533 | "_id": "ObjectId", 534 | "email": "user92@test.com", 535 | "name": "user#92", 536 | }, 537 | Object { 538 | "_id": "ObjectId", 539 | "email": "user93@test.com", 540 | "name": "user#93", 541 | }, 542 | Object { 543 | "_id": "ObjectId", 544 | "email": "user94@test.com", 545 | "name": "user#94", 546 | }, 547 | Object { 548 | "_id": "ObjectId", 549 | "email": "user95@test.com", 550 | "name": "user#95", 551 | }, 552 | Object { 553 | "_id": "ObjectId", 554 | "email": "user96@test.com", 555 | "name": "user#96", 556 | }, 557 | Object { 558 | "_id": "ObjectId", 559 | "email": "user97@test.com", 560 | "name": "user#97", 561 | }, 562 | Object { 563 | "_id": "ObjectId", 564 | "email": "user98@test.com", 565 | "name": "user#98", 566 | }, 567 | Object { 568 | "_id": "ObjectId", 569 | "email": "user99@test.com", 570 | "name": "user#99", 571 | }, 572 | Object { 573 | "_id": "ObjectId", 574 | "email": "user100@test.com", 575 | "name": "user#100", 576 | }, 577 | ], 578 | } 579 | `; 580 | 581 | exports[`should return a list of users 1`] = ` 582 | Object { 583 | "pageInfo": Object { 584 | "hasNextPage": false, 585 | "hasPreviousPage": false, 586 | "limit": 100, 587 | "skip": 0, 588 | "totalCount": 3, 589 | }, 590 | "users": Array [ 591 | Object { 592 | "_id": "ObjectId", 593 | "email": "user1@test.com", 594 | "name": "User 1", 595 | }, 596 | Object { 597 | "_id": "ObjectId", 598 | "email": "user2@test.com", 599 | "name": "User 2", 600 | }, 601 | Object { 602 | "_id": "ObjectId", 603 | "email": "user3@test.com", 604 | "name": "User 3", 605 | }, 606 | ], 607 | } 608 | `; 609 | 610 | exports[`should return error if authorization header does not exist 1`] = ` 611 | Object { 612 | "message": "ObjectId", 613 | } 614 | `; 615 | --------------------------------------------------------------------------------