├── .gitignore ├── README.md ├── auth ├── .babelrc ├── .flowconfig ├── Dockerfile ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src │ ├── db │ │ ├── config │ │ │ ├── config.js │ │ │ └── index.js │ │ ├── index.js │ │ └── models │ │ │ ├── User.js │ │ │ └── index.js │ ├── enums │ │ ├── index.js │ │ └── resultCodes.js │ ├── exceptions │ │ ├── PasswordNotFoundException.js │ │ ├── UnauthorizedException.js │ │ ├── UserAlreadyExist.js │ │ ├── UserNotFound.js │ │ ├── WrongLoginInfoException.js │ │ └── index.js │ ├── index.js │ ├── middlewares │ │ ├── authHandler.js │ │ ├── globalErrorHandler.js │ │ └── index.js │ ├── routes.js │ ├── server.js │ ├── services │ │ ├── authService.js │ │ ├── index.js │ │ └── userService.js │ └── utils │ │ └── crypto.js └── tests │ ├── routes.test.js │ ├── sampleData.js │ ├── services │ ├── authService.test.js │ └── userService.test.js │ └── setup.js ├── deployment ├── Makefile ├── README.md ├── db │ ├── .env │ ├── Dockerfile │ └── init-db.sh ├── docker-compose.yaml ├── gateway │ └── src │ │ ├── Dockerfile │ │ └── conf │ │ └── prod.conf └── service │ ├── auth │ └── .env │ └── ticket │ └── .env └── ticket ├── .babelrc ├── .flowconfig ├── Dockerfile ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── db │ ├── config │ │ ├── config.js │ │ └── index.js │ ├── index.js │ └── models │ │ ├── Ticket.js │ │ └── index.js ├── enums │ ├── index.js │ └── resultCodes.js ├── exceptions │ ├── UserIdShouldProvided.js │ └── index.js ├── index.js ├── middlewares │ ├── globalErrorHandler.js │ └── index.js ├── routes.js ├── server.js └── services │ ├── index.js │ └── ticketService.js └── tests ├── routes.test.js ├── sampleData.js ├── services └── ticketService.test.js └── setup.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | build 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Designing Microservices with ExpressJS 2 | This repository aims to show designing microservices with ExpressJS and deploying with Docker. It consists of 3 sub-projects as 2 services and a deployment folder. 3 | 4 | There is a Medium post about this repository. 5 | - https://yildizberkay.medium.com/designing-microservices-with-expressjs-eb23e4f02192 6 | 7 | ## Subprojects 8 | - [Auth Service](./auth) 9 | - [Ticket Service](./ticket) 10 | - [Deployment Files](./ticket) 11 | -------------------------------------------------------------------------------- /auth/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "node": "14" 8 | } 9 | } 10 | ], 11 | "@babel/preset-flow" 12 | ], 13 | "plugins": [ 14 | "@babel/plugin-transform-runtime" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /auth/.flowconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yildizberkay/microservices-with-expressjs/29647645acbb8f21d4adbf719e99da7d42d64c41/auth/.flowconfig -------------------------------------------------------------------------------- /auth/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.15-alpine 2 | WORKDIR /usr/src/app 3 | RUN mkdir dist 4 | 5 | COPY package.json ./ 6 | RUN npm install --only=prod 7 | COPY dist ./dist 8 | 9 | EXPOSE 3000 10 | CMD [ "npm", "run", "server" ] 11 | -------------------------------------------------------------------------------- /auth/README.md: -------------------------------------------------------------------------------- 1 | # Auth Service 2 | 3 | This service aims to manage authentication and users. 4 | 5 | ## Technical Brief 6 | The service is built on the top of ExpressJS and Sequelize is used as ORM. While PostgreSQL is used as prod database, SQLite is preferred as test database. 7 | 8 | Babel 7 is used to convert ECMAScript 2015+ code and Node target version is 14. 9 | 10 | In the test side, Jest is used as the testing framework. 11 | 12 | ### Required Environment Variables 13 | #### ACTIVE_ENV 14 | Currently, only the database config is determined by this parameter. It takes the following values. 15 | - production 16 | - development 17 | - test 18 | 19 | *config.js* 20 | ``` 21 | export const configs = { 22 | development: { 23 | uri: process.env?.DEV_DB_URI ?? 'postgres://postgres:password@localhost:5432/dev_service_auth', 24 | logging: true 25 | }, 26 | test: { 27 | uri: process.env?.TEST_DB_URI ?? 'sqlite::memory:', 28 | logging: false 29 | }, 30 | production: { 31 | uri: process.env?.PROD_DB_URI ?? 'postgres://postgres:password@localhost:5432/service_auth', 32 | logging: false 33 | } 34 | } 35 | ``` 36 | 37 | ### PROD_DB_URI, DEV_DB_URI and TEST_DB_URI 38 | These variables take database URI like followings. 39 | ``` 40 | postgres://postgres:password@db:5432/service_auth 41 | sqlite::memory: 42 | ``` 43 | 44 | More samples: https://sequelize.org/master/manual/getting-started.html 45 | 46 | ### JWT_SALT 47 | This is the secret key for JWT, keep it safe! 48 | 49 | ### Available NPM Commands 50 | 51 | #### Test 52 | Runs all tests using Jest. 53 | ``` 54 | npm run test 55 | ``` 56 | 57 | #### Build 58 | Extracts a Node 14 compatible build folder using Babel. 59 | ``` 60 | npm run build 61 | ``` 62 | 63 | #### Server 64 | Starts a server from /dist folder. 65 | ``` 66 | npm run server 67 | ``` 68 | 69 | ### Folder Structure 70 | ``` 71 | - tests 72 | - src 73 | - db 74 | - config 75 | # Holds database profiles and connection object. 76 | - models 77 | # Holds Sequelize models. 78 | - enums 79 | - exceptions 80 | - middlewares 81 | # Middlewares like authHandler are held here. 82 | - services 83 | # Database related business logics are managed under this folder. 84 | - utils 85 | - index.js 86 | - routes.js 87 | - server.js 88 | ``` 89 | -------------------------------------------------------------------------------- /auth/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | 6 | module.exports = { 7 | setupFiles: [ 8 | '/tests/setup.js' 9 | ], 10 | // A set of global variables that need to be available in all test environments 11 | globals: {}, 12 | 13 | // The test environment that will be used for testing 14 | testEnvironment: 'node' 15 | } 16 | -------------------------------------------------------------------------------- /auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth-service", 3 | "author": "Berkay Yildiz", 4 | "license": "MIT", 5 | "version": "1.0.0", 6 | "description": "", 7 | "private": false, 8 | "standard": { 9 | "parser": "babel-eslint", 10 | "plugins": [ 11 | "flowtype" 12 | ], 13 | "env": [ 14 | "jest" 15 | ] 16 | }, 17 | "scripts": { 18 | "test": "jest", 19 | "build": "babel src/ -d dist", 20 | "server": "node dist/index.js" 21 | }, 22 | "dependencies": { 23 | "@babel/core": "^7.12.3", 24 | "@babel/preset-env": "^7.12.1", 25 | "body-parser": "^1.19.0", 26 | "express": "^4.17.1", 27 | "jsonwebtoken": "^8.5.1", 28 | "pg": "^8.5.1", 29 | "pg-hstore": "^2.3.3", 30 | "sequelize": "^6.3.5" 31 | }, 32 | "devDependencies": { 33 | "sqlite3": "^5.0.0", 34 | "jest": "^26.6.3", 35 | "supertest": "^6.0.1", 36 | "@babel/cli": "^7.12.1", 37 | "@babel/plugin-transform-runtime": "^7.12.1", 38 | "@babel/preset-flow": "^7.12.1", 39 | "babel-eslint": "^10.1.0", 40 | "eslint-plugin-flowtype": "^5.2.0", 41 | "sequelize-cli": "^6.2.0", 42 | "standard": "^16.0.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /auth/src/db/config/config.js: -------------------------------------------------------------------------------- 1 | export const configs = { 2 | development: { 3 | uri: process.env?.DEV_DB_URI ?? 'postgres://postgres:password@localhost:5432/dev_service_auth', 4 | logging: true 5 | }, 6 | test: { 7 | uri: process.env?.TEST_DB_URI ?? 'sqlite::memory:', 8 | logging: false 9 | }, 10 | production: { 11 | uri: process.env?.PROD_DB_URI ?? 'postgres://postgres:password@localhost:5432/service_auth', 12 | logging: false 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /auth/src/db/config/index.js: -------------------------------------------------------------------------------- 1 | import { configs } from './config' 2 | 3 | const { Sequelize } = require('sequelize') 4 | 5 | const activeConfig = configs[process.env?.ACTIVE_ENV] 6 | export const connection = new Sequelize(activeConfig.uri, { logging: activeConfig.logging }) 7 | -------------------------------------------------------------------------------- /auth/src/db/index.js: -------------------------------------------------------------------------------- 1 | import { connection } from './config' 2 | import { User } from './models' 3 | 4 | export const initializeDatabase = async () => { 5 | try { 6 | await connection.authenticate() 7 | await User.sync({}) 8 | 9 | console.log('Connection has been established and models synced successfully.') 10 | } catch (error) { 11 | console.error('Unable to connect to the database:', error) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /auth/src/db/models/User.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { connection } from '../config' 3 | 4 | const DataTypes = require('sequelize') 5 | 6 | const User = connection.define('user', { 7 | name: { 8 | type: DataTypes.STRING, 9 | allowNull: false, 10 | validate: { 11 | isAlphanumeric: true, 12 | len: [2, 255] 13 | } 14 | }, 15 | email: { 16 | type: DataTypes.STRING, 17 | unique: true, 18 | validate: { 19 | isEmail: true 20 | } 21 | }, 22 | password: { 23 | type: DataTypes.STRING, 24 | allowNull: false 25 | } 26 | }, { 27 | tableName: 'users' 28 | }) 29 | 30 | export { User } 31 | -------------------------------------------------------------------------------- /auth/src/db/models/index.js: -------------------------------------------------------------------------------- 1 | export { User } from './User' 2 | -------------------------------------------------------------------------------- /auth/src/enums/index.js: -------------------------------------------------------------------------------- 1 | export { resultCodes } from './resultCodes' 2 | -------------------------------------------------------------------------------- /auth/src/enums/resultCodes.js: -------------------------------------------------------------------------------- 1 | export const resultCodes = { 2 | SUCCESS: 'Success', 3 | ERROR: 'Error' 4 | } 5 | -------------------------------------------------------------------------------- /auth/src/exceptions/PasswordNotFoundException.js: -------------------------------------------------------------------------------- 1 | export class PasswordNotFoundException extends Error { 2 | constructor (message) { 3 | super(message) 4 | this.name = 'PasswordNotFoundException' 5 | this.httpStatusCode = 401 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /auth/src/exceptions/UnauthorizedException.js: -------------------------------------------------------------------------------- 1 | export class UnauthorizedException extends Error { 2 | constructor (message) { 3 | super(message) 4 | this.name = 'UnauthorizedException' 5 | this.httpStatusCode = 401 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /auth/src/exceptions/UserAlreadyExist.js: -------------------------------------------------------------------------------- 1 | export class UserAlreadyExist extends Error { 2 | constructor (message) { 3 | super(message) 4 | this.name = 'UserAlreadyExist' 5 | this.httpStatusCode = 402 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /auth/src/exceptions/UserNotFound.js: -------------------------------------------------------------------------------- 1 | export class UserNotFound extends Error { 2 | constructor (message) { 3 | super(message) 4 | this.name = 'UserNotFound' 5 | this.httpStatusCode = 204 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /auth/src/exceptions/WrongLoginInfoException.js: -------------------------------------------------------------------------------- 1 | export class WrongLoginInfoException extends Error { 2 | constructor (message) { 3 | super(message) 4 | this.name = 'WrongLoginInfoException' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /auth/src/exceptions/index.js: -------------------------------------------------------------------------------- 1 | export { PasswordNotFoundException } from './PasswordNotFoundException' 2 | export { WrongLoginInfoException } from './WrongLoginInfoException' 3 | export { UnauthorizedException } from './UnauthorizedException' 4 | export { UserAlreadyExist } from './UserAlreadyExist' 5 | export { UserNotFound } from './UserNotFound' 6 | -------------------------------------------------------------------------------- /auth/src/index.js: -------------------------------------------------------------------------------- 1 | import { initializeDatabase } from './db' 2 | const app = require('./server') 3 | 4 | const port = 3000 5 | 6 | app.listen(port, async () => { 7 | await initializeDatabase() 8 | console.log(`Auth service listening at http://localhost:${port}`) 9 | }) 10 | -------------------------------------------------------------------------------- /auth/src/middlewares/authHandler.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { NextFunction, Request, Response } from 'express' 3 | import { passwordSalt } from '../utils/crypto' 4 | 5 | const jwt = require('jsonwebtoken') 6 | 7 | export const authHandler = (req: Request, res: Response, next: NextFunction) => { 8 | const authHeader = req.headers?.authorization 9 | const token = authHeader && authHeader.split(' ')[1] 10 | 11 | jwt.verify(token, passwordSalt, (err: any, user: any) => { 12 | if (err) { 13 | return res.sendStatus(401) 14 | } 15 | req.user = user 16 | next() 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /auth/src/middlewares/globalErrorHandler.js: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express' 2 | import { resultCodes } from '../enums' 3 | 4 | export const globalErrorHandler = async (error, req: Request, res: Response, next: NextFunction) => { 5 | if (error) { 6 | let statusCode = 500 7 | if (error?.httpStatusCode) { 8 | statusCode = error.httpStatusCode 9 | } 10 | 11 | res.status(statusCode).json({ 12 | result: resultCodes.ERROR, 13 | error: { 14 | name: error.name, 15 | message: error.message 16 | } 17 | }) 18 | } else { 19 | next() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /auth/src/middlewares/index.js: -------------------------------------------------------------------------------- 1 | export { authHandler } from './authHandler' 2 | export { globalErrorHandler } from './globalErrorHandler' 3 | -------------------------------------------------------------------------------- /auth/src/routes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import express from 'express' 3 | import { getTokenFromEmailAndPassword, getUserById, registerUser } from './services' 4 | import { resultCodes } from './enums' 5 | import { authHandler } from './middlewares' 6 | 7 | const authRoute = express.Router() 8 | 9 | authRoute.get('/check-token', authHandler, function (req, res) { 10 | res.setHeader('User-Id', req.user.id) 11 | res.sendStatus(200) 12 | }) 13 | 14 | authRoute.get('/me', authHandler, function (req, res, next) { 15 | getUserById(req.user.id) 16 | .then(user => res.json({ result: resultCodes.SUCCESS, user })) 17 | .catch(next) 18 | }) 19 | 20 | authRoute.post('/register', async function (req, res, next) { 21 | const { name, email, password } = req.body 22 | registerUser(name, email, password) 23 | .then(() => res.status(201).json({ result: resultCodes.SUCCESS })) 24 | .catch(next) 25 | }) 26 | 27 | authRoute.post('/login', async function (req, res, next) { 28 | const { email, password } = req.body 29 | await getTokenFromEmailAndPassword(email, password) 30 | .then((data) => res.json({ result: resultCodes.SUCCESS, data })) 31 | .catch(next) 32 | }) 33 | 34 | export { authRoute } 35 | -------------------------------------------------------------------------------- /auth/src/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { authRoute } from './routes' 3 | import bodyParser from 'body-parser' 4 | import { globalErrorHandler } from './middlewares' 5 | 6 | const app = express() 7 | 8 | app.use(bodyParser.urlencoded({ extended: true })) 9 | app.use(bodyParser.json()) 10 | app.use(bodyParser.raw()) 11 | 12 | app.use(authRoute) 13 | app.use(globalErrorHandler) 14 | 15 | module.exports = app 16 | -------------------------------------------------------------------------------- /auth/src/services/authService.js: -------------------------------------------------------------------------------- 1 | import { getJWTToken, getSaltHashPassword, verifyPassword } from '../utils/crypto' 2 | import { User } from '../db/models' 3 | import { UserAlreadyExist, WrongLoginInfoException } from '../exceptions' 4 | 5 | export const registerUser = async (name: string, email: string, password: string) => { 6 | const checkUser = await User.findOne({ where: { email } }) 7 | if (checkUser) { 8 | throw new UserAlreadyExist() 9 | } 10 | 11 | const passObject = getSaltHashPassword(password) 12 | const userObject = User.build({ 13 | name, 14 | email, 15 | password: passObject.passwordHash 16 | }) 17 | 18 | await userObject.validate() 19 | await userObject.save() 20 | return { 21 | id: userObject.id, 22 | name: userObject.name, 23 | email: userObject.email 24 | } 25 | } 26 | 27 | export const getTokenFromEmailAndPassword = async (email: string, password: string) => { 28 | const userObject = await User.findOne({ where: { email } }) 29 | if (!userObject) { 30 | throw new WrongLoginInfoException() 31 | } 32 | 33 | const isVerified = verifyPassword(password, userObject.password) 34 | if (isVerified) { 35 | return { token: getJWTToken({ id: userObject.id }) } 36 | } else { 37 | throw new WrongLoginInfoException() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /auth/src/services/index.js: -------------------------------------------------------------------------------- 1 | export { getTokenFromEmailAndPassword, registerUser } from './authService' 2 | export { getUserById } from './userService' 3 | -------------------------------------------------------------------------------- /auth/src/services/userService.js: -------------------------------------------------------------------------------- 1 | import { User } from '../db/models' 2 | import { UserNotFound } from '../exceptions' 3 | 4 | export const getUserById = async (id) => { 5 | const user = await User.findOne({ where: { id } }) 6 | if (user) { 7 | return { 8 | id: user.id, 9 | name: user.name, 10 | email: user.email 11 | } 12 | } 13 | throw new UserNotFound() 14 | } 15 | -------------------------------------------------------------------------------- /auth/src/utils/crypto.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const crypto = require('crypto') 3 | const jwt = require('jsonwebtoken') 4 | export const passwordSalt = process.env?.JWT_SALT 5 | 6 | const sha512 = function (password, salt) { 7 | const hash = crypto.createHmac('sha512', salt) 8 | /** Hashing algorithm sha512 */ 9 | hash.update(password) 10 | const value = hash.digest('hex') 11 | return { 12 | salt: salt, 13 | passwordHash: value 14 | } 15 | } 16 | 17 | export function getSaltHashPassword (password: string) { 18 | return sha512(password, passwordSalt) 19 | } 20 | 21 | export const verifyPassword = (password, hashedPassword) => { 22 | return sha512(password, passwordSalt).passwordHash === hashedPassword 23 | } 24 | 25 | export function getJWTToken (payload) { 26 | return jwt.sign(payload, passwordSalt, { expiresIn: '3y' }) 27 | } 28 | -------------------------------------------------------------------------------- /auth/tests/routes.test.js: -------------------------------------------------------------------------------- 1 | import supertest from 'supertest' 2 | import { initializeDatabase } from '../src/db' 3 | import { User } from '../src/db/models' 4 | import { getTokenFromEmailAndPassword, registerUser } from '../src/services' 5 | import { sampleUser } from './sampleData' 6 | 7 | const app = require('../src/server') 8 | 9 | describe('auth Endpoints', () => { 10 | beforeAll(async () => { 11 | await initializeDatabase() 12 | }) 13 | 14 | describe('auth required tests', () => { 15 | let token 16 | beforeAll(async () => { 17 | await registerUser(sampleUser.name, sampleUser.email, sampleUser.password) 18 | const loginResult = await getTokenFromEmailAndPassword(sampleUser.email, sampleUser.password) 19 | token = loginResult.token 20 | }) 21 | 22 | test('check token', async () => { 23 | const res = await supertest(app) 24 | .get('/check-token') 25 | .set({ Authorization: `Bearer ${token}` }) 26 | 27 | expect(res.statusCode).toEqual(200) 28 | }) 29 | 30 | test('should give 401 if token is wrong while checking token', async () => { 31 | const res = await supertest(app) 32 | .get('/check-token') 33 | .set({ Authorization: 'Bearer WrongToken' }) 34 | expect(res.statusCode).toEqual(401) 35 | }) 36 | 37 | test('should give 401 if token is not set while checking token', async () => { 38 | const res = await supertest(app) 39 | .get('/check-token') 40 | expect(res.statusCode).toEqual(401) 41 | }) 42 | 43 | test('me', async () => { 44 | const res = await supertest(app) 45 | .get('/me') 46 | .set({ Authorization: `Bearer ${token}` }) 47 | 48 | expect(res.statusCode).toEqual(200) 49 | expect(res.body).toHaveProperty('result') 50 | expect(res.body).toHaveProperty('user') 51 | }) 52 | 53 | test('should give 401 if token is wrong on /me', async () => { 54 | const res = await supertest(app) 55 | .get('/me') 56 | .set({ Authorization: 'Bearer WrongToken' }) 57 | expect(res.statusCode).toEqual(401) 58 | }) 59 | 60 | test('should give 401 if token is not set on /me', async () => { 61 | const res = await supertest(app) 62 | .get('/me') 63 | expect(res.statusCode).toEqual(401) 64 | }) 65 | }) 66 | 67 | describe('register and login tests', () => { 68 | beforeEach(async () => { 69 | await User.destroy({ 70 | where: {}, 71 | truncate: true 72 | }) 73 | }) 74 | 75 | test('register new user', async () => { 76 | const res = await supertest(app) 77 | .post('/register') 78 | .send(sampleUser) 79 | 80 | expect(res.statusCode).toEqual(201) 81 | expect(res.body).toHaveProperty('result') 82 | }) 83 | 84 | test('should give error if user registered already', async () => { 85 | await registerUser(sampleUser.name, sampleUser.email, sampleUser.password) 86 | const res = await supertest(app) 87 | .post('/register') 88 | .send(sampleUser) 89 | 90 | expect(res.statusCode).toEqual(402) 91 | expect(res.body).toHaveProperty('result') 92 | }) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /auth/tests/sampleData.js: -------------------------------------------------------------------------------- 1 | export const sampleUser = { 2 | email: 'hello@world.com', 3 | name: 'HelloWorld', 4 | password: '1234' 5 | } 6 | -------------------------------------------------------------------------------- /auth/tests/services/authService.test.js: -------------------------------------------------------------------------------- 1 | import { initializeDatabase } from '../../src/db' 2 | import { User } from '../../src/db/models' 3 | import { getTokenFromEmailAndPassword, registerUser } from '../../src/services' 4 | import { UserAlreadyExist, WrongLoginInfoException } from '../../src/exceptions' 5 | import { getSaltHashPassword } from '../../src/utils/crypto' 6 | import { sampleUser } from '../sampleData' 7 | 8 | describe('authService', () => { 9 | beforeAll(async () => { 10 | await initializeDatabase() 11 | }) 12 | 13 | describe(('registration'), () => { 14 | beforeEach(async () => { 15 | await User.destroy({ 16 | where: {}, 17 | truncate: true 18 | }) 19 | }) 20 | 21 | test('register user', async () => { 22 | const result = await registerUser(sampleUser.name, sampleUser.email, sampleUser.password) 23 | expect(result).toEqual(expect.objectContaining({ 24 | id: expect.any(Number), 25 | name: sampleUser.name, 26 | email: sampleUser.email 27 | })) 28 | }) 29 | 30 | test('throw error if user is already registered', async () => { 31 | await registerUser(sampleUser.name, sampleUser.email, sampleUser.password) 32 | return expect(registerUser(sampleUser.name, sampleUser.email, sampleUser.password)) 33 | .rejects.toMatchObject(new UserAlreadyExist()) 34 | }) 35 | }) 36 | 37 | describe('verify user with email and password', () => { 38 | beforeAll(async () => { 39 | await User.destroy({ 40 | where: {}, 41 | truncate: true 42 | }) 43 | // Create sample user 44 | const passObject = getSaltHashPassword(sampleUser.password) 45 | await User.create({ 46 | name: sampleUser.name, 47 | email: sampleUser.email, 48 | password: passObject.passwordHash 49 | }) 50 | }) 51 | 52 | test('get token from email and password', async () => { 53 | const result = await getTokenFromEmailAndPassword(sampleUser.email, sampleUser.password) 54 | expect(result).toHaveProperty('token') 55 | }) 56 | 57 | test('throw error if email is wrong', () => { 58 | return expect(getTokenFromEmailAndPassword('wrong@wrong.com', sampleUser.password)) 59 | .rejects.toMatchObject(new WrongLoginInfoException()) 60 | }) 61 | 62 | test('throw error if password is wrong', () => { 63 | return expect(getTokenFromEmailAndPassword(sampleUser.email, '12345')) 64 | .rejects.toMatchObject(new WrongLoginInfoException()) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /auth/tests/services/userService.test.js: -------------------------------------------------------------------------------- 1 | import { initializeDatabase } from '../../src/db' 2 | import { getUserById } from '../../src/services' 3 | import { UserNotFound } from '../../src/exceptions' 4 | import { User } from '../../src/db/models' 5 | import { sampleUser } from '../sampleData' 6 | 7 | describe('userService', () => { 8 | let id 9 | 10 | beforeAll(async () => { 11 | await initializeDatabase() 12 | 13 | // Create sample user 14 | const user = await User.create(sampleUser) 15 | id = user.id 16 | }) 17 | 18 | test('get user by id', async () => { 19 | const result = await getUserById(1) 20 | expect(result).toStrictEqual({ 21 | id: id, 22 | name: sampleUser.name, 23 | email: sampleUser.email 24 | }) 25 | }) 26 | 27 | test('throw error if user doesn\'t exist', async () => { 28 | return expect(getUserById(99)).rejects.toMatchObject(new UserNotFound()) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /auth/tests/setup.js: -------------------------------------------------------------------------------- 1 | process.env.ACTIVE_ENV = 'test' 2 | process.env.JWT_SALT = 'ThisIsTestSecret' 3 | -------------------------------------------------------------------------------- /deployment/Makefile: -------------------------------------------------------------------------------- 1 | COMPOSE_PROJECT := ticket-management 2 | DOCKER_NETWORK_NAME := n1 3 | export 4 | 5 | .PHONY: up build npmBuild buildUp start down clean network 6 | 7 | docker_compose := docker-compose -p ${COMPOSE_PROJECT} -f docker-compose.yaml 8 | 9 | up: network 10 | ${docker_compose} up -d 11 | build: 12 | ${docker_compose} build ${service} 13 | npmBuild: 14 | @cd ../auth; npm run build 15 | @cd ../ticket; npm run build 16 | buildUp: npmBuild build up 17 | start: 18 | ${docker_compose} start 19 | down: 20 | ${docker_compose} down 21 | clean: 22 | ${docker_compose} down -v 23 | docker network rm ${DOCKER_NETWORK_NAME} 24 | network: 25 | docker network inspect ${DOCKER_NETWORK_NAME} >/dev/null 2>&1 || \ 26 | docker network create --driver bridge ${DOCKER_NETWORK_NAME} 27 | -------------------------------------------------------------------------------- /deployment/README.md: -------------------------------------------------------------------------------- 1 | # Docker Deployment Helpers 2 | This repository contains Docker deployment scripts of the micro-services. Makefile manages all Docker commands. 3 | 4 | ## Deployment 5 | Following command builds and deploys containers. 6 | ``` 7 | make buildUp 8 | ``` 9 | 10 | ## Available Make Commands 11 | 12 | ### up 13 | It deploys all containers and creates the network if it doesn't exist. 14 | 15 | ### build 16 | It builds Docker containers. 17 | ``` 18 | make build 19 | make build service="gateway" 20 | ``` 21 | 22 | ### npmBuild 23 | It runs `npm run build` commands. 24 | 25 | ### buildUp 26 | It runs `npmBuild`, `build` and `up` in order. 27 | 28 | ### start 29 | It runs docker-compose. 30 | 31 | ### down 32 | It downs docker-compose. 33 | 34 | ### clean 35 | It downs docker-compose with option -v removes volumes and removes network. 36 | 37 | ### network 38 | It creates the network if it doesn't exist. 39 | 40 | -------------------------------------------------------------------------------- /deployment/db/.env: -------------------------------------------------------------------------------- 1 | POSTGRES_USER=postgres 2 | POSTGRES_PASSWORD=password 3 | -------------------------------------------------------------------------------- /deployment/db/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres 2 | COPY init-db.sh /docker-entrypoint-initdb.d/init-db.sh 3 | -------------------------------------------------------------------------------- /deployment/db/init-db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL 5 | CREATE DATABASE service_auth; 6 | CREATE DATABASE service_ticket; 7 | GRANT ALL PRIVILEGES ON DATABASE service_auth TO postgres; 8 | GRANT ALL PRIVILEGES ON DATABASE service_ticket TO postgres; 9 | EOSQL 10 | -------------------------------------------------------------------------------- /deployment/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | db-service: 5 | container_name: db 6 | build: 7 | context: ./db 8 | restart: always 9 | env_file: 10 | - db/.env 11 | networks: 12 | - n1 13 | 14 | gateway: 15 | container_name: gateway 16 | build: 17 | context: ./gateway/src 18 | restart: always 19 | ports: 20 | - 80:8080 21 | networks: 22 | - n1 23 | 24 | service-auth: 25 | container_name: service-auth 26 | build: 27 | context: ../auth 28 | restart: always 29 | env_file: 30 | - service/auth/.env 31 | networks: 32 | - n1 33 | 34 | service-ticket: 35 | container_name: service-ticket 36 | build: 37 | context: ../ticket 38 | restart: always 39 | env_file: 40 | - service/ticket/.env 41 | networks: 42 | - n1 43 | 44 | networks: 45 | n1: 46 | external: true 47 | -------------------------------------------------------------------------------- /deployment/gateway/src/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.18 2 | COPY conf/prod.conf /etc/nginx/conf.d/prod.conf 3 | -------------------------------------------------------------------------------- /deployment/gateway/src/conf/prod.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | 4 | location /check-token { 5 | internal; 6 | proxy_pass http://service-auth:3000/check-token; 7 | proxy_redirect off; 8 | proxy_set_header Host $host; 9 | proxy_set_header X-Real-IP $remote_addr; 10 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 11 | proxy_set_header X-Forwarded-Host $server_name; 12 | proxy_set_header Content-Length ""; 13 | proxy_pass_request_body off; 14 | } 15 | 16 | location /auth { 17 | rewrite ^/auth/(.*) /$1 break; 18 | 19 | proxy_pass http://service-auth:3000; 20 | proxy_redirect off; 21 | proxy_set_header Host $host; 22 | proxy_set_header X-Real-IP $remote_addr; 23 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 24 | proxy_set_header X-Forwarded-Host $server_name; 25 | } 26 | 27 | 28 | location /ticket { 29 | auth_request /check-token; 30 | auth_request_set $user_id $sent_http_user_id; 31 | 32 | rewrite ^/ticket/(.*) /$1 break; 33 | 34 | proxy_pass http://service-ticket:3000; 35 | proxy_redirect off; 36 | proxy_set_header Host $host; 37 | proxy_set_header X-Real-IP $remote_addr; 38 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 39 | proxy_set_header X-Forwarded-Host $server_name; 40 | proxy_set_header User-Id $user_id; 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /deployment/service/auth/.env: -------------------------------------------------------------------------------- 1 | ACTIVE_ENV=production 2 | PROD_DB_URI=postgres://postgres:password@db:5432/service_auth 3 | JWT_SALT=ThisIsSecret 4 | -------------------------------------------------------------------------------- /deployment/service/ticket/.env: -------------------------------------------------------------------------------- 1 | ACTIVE_ENV=production 2 | PROD_DB_URI=postgres://postgres:password@db:5432/service_ticket 3 | -------------------------------------------------------------------------------- /ticket/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "node": "14" 8 | } 9 | } 10 | ], 11 | "@babel/preset-flow" 12 | ], 13 | "plugins": [ 14 | "@babel/plugin-transform-runtime" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /ticket/.flowconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yildizberkay/microservices-with-expressjs/29647645acbb8f21d4adbf719e99da7d42d64c41/ticket/.flowconfig -------------------------------------------------------------------------------- /ticket/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.15-alpine 2 | WORKDIR /usr/src/app 3 | RUN mkdir dist 4 | 5 | COPY package.json ./ 6 | RUN npm install --only=prod 7 | COPY dist ./dist 8 | 9 | EXPOSE 3000 10 | CMD [ "npm", "run", "server" ] 11 | -------------------------------------------------------------------------------- /ticket/README.md: -------------------------------------------------------------------------------- 1 | # Ticket Service 2 | 3 | This service aims to manage tickets. 4 | 5 | ## Technical Brief 6 | The service is built on the top of ExpressJS and Sequelize is used as ORM. While PostgreSQL is used as prod database, SQLite is preferred as test database. 7 | 8 | Babel 7 is used to convert ECMAScript 2015+ code and Node target version is 14. 9 | 10 | In the test side, Jest is used as the testing framework. 11 | 12 | ### Required Environment Variables 13 | #### ACTIVE_ENV 14 | Currently, only the database config is determined by this parameter. It takes the following values. 15 | - production 16 | - development 17 | - test 18 | 19 | *config.js* 20 | ``` 21 | export const configs = { 22 | development: { 23 | uri: process.env?.DEV_DB_URI ?? 'postgres://postgres:password@localhost:5432/dev_service_auth', 24 | logging: true 25 | }, 26 | test: { 27 | uri: process.env?.TEST_DB_URI ?? 'sqlite::memory:', 28 | logging: false 29 | }, 30 | production: { 31 | uri: process.env?.PROD_DB_URI ?? 'postgres://postgres:password@localhost:5432/service_auth', 32 | logging: false 33 | } 34 | } 35 | ``` 36 | 37 | ### PROD_DB_URI, DEV_DB_URI and TEST_DB_URI 38 | These variables take database URI like followings. 39 | ``` 40 | postgres://postgres:password@db:5432/service_auth 41 | sqlite::memory: 42 | ``` 43 | 44 | More samples: https://sequelize.org/master/manual/getting-started.html 45 | 46 | ### JWT_SALT 47 | This is a secret key for JWT, keep it safe! 48 | 49 | ### Available NPM Commands 50 | 51 | #### Test 52 | Runs all tests using Jest. 53 | ``` 54 | npm run test 55 | ``` 56 | 57 | #### Build 58 | Extracts a Node 14 compatible build folder using Babel. 59 | ``` 60 | npm run build 61 | ``` 62 | 63 | #### Server 64 | Starts a server from /dist folder. 65 | ``` 66 | npm run server 67 | ``` 68 | 69 | ### Folder Structure 70 | ``` 71 | - tests 72 | - src 73 | - db 74 | - config 75 | # Holds database profiles and connection object. 76 | - models 77 | # Holds Sequelize models. 78 | - enums 79 | - exceptions 80 | - middlewares 81 | # Middlewares like authHandler are held here. 82 | - services 83 | # Database related business logics are managed under this folder. 84 | - index.js 85 | - routes.js 86 | - server.js 87 | ``` 88 | -------------------------------------------------------------------------------- /ticket/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | 6 | module.exports = { 7 | setupFiles: [ 8 | '/tests/setup.js' 9 | ], 10 | // A set of global variables that need to be available in all test environments 11 | globals: {}, 12 | 13 | // The test environment that will be used for testing 14 | testEnvironment: 'node' 15 | } 16 | -------------------------------------------------------------------------------- /ticket/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ticket-service", 3 | "author": "Berkay Yildiz", 4 | "license": "MIT", 5 | "version": "1.0.0", 6 | "description": "", 7 | "standard": { 8 | "parser": "babel-eslint", 9 | "plugins": [ 10 | "flowtype" 11 | ], 12 | "env": [ 13 | "jest" 14 | ] 15 | }, 16 | "scripts": { 17 | "test": "jest", 18 | "build": "babel src/ -d dist", 19 | "server": "node dist/index.js" 20 | }, 21 | "dependencies": { 22 | "@babel/core": "^7.12.3", 23 | "@babel/preset-env": "^7.12.1", 24 | "body-parser": "^1.19.0", 25 | "express": "^4.17.1", 26 | "pg": "^8.5.1", 27 | "pg-hstore": "^2.3.3", 28 | "sequelize": "^6.3.5" 29 | }, 30 | "devDependencies": { 31 | "sqlite3": "^5.0.0", 32 | "supertest": "^6.0.1", 33 | "jest": "^26.6.3", 34 | "@babel/cli": "^7.12.1", 35 | "@babel/plugin-transform-runtime": "^7.12.1", 36 | "@babel/preset-flow": "^7.12.1", 37 | "babel-eslint": "^10.1.0", 38 | "eslint-plugin-flowtype": "^5.2.0", 39 | "sequelize-cli": "^6.2.0", 40 | "standard": "^16.0.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ticket/src/db/config/config.js: -------------------------------------------------------------------------------- 1 | export const configs = { 2 | development: { 3 | uri: process.env?.DEV_DB_URI ?? 'postgres://postgres:password@localhost:5432/dev_service_ticket', 4 | logging: true 5 | }, 6 | test: { 7 | uri: process.env?.TEST_DB_URI ?? 'sqlite::memory:', 8 | logging: false 9 | }, 10 | production: { 11 | uri: process.env?.PROD_DB_URI ?? 'postgres://postgres:password@localhost:5432/service_ticket', 12 | logging: false 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ticket/src/db/config/index.js: -------------------------------------------------------------------------------- 1 | import { configs } from './config' 2 | 3 | const { Sequelize } = require('sequelize') 4 | 5 | const activeConfig = configs[process.env?.ACTIVE_ENV] 6 | export const connection = new Sequelize(activeConfig.uri, { logging: activeConfig.logging }) 7 | -------------------------------------------------------------------------------- /ticket/src/db/index.js: -------------------------------------------------------------------------------- 1 | import { connection } from './config' 2 | import { Ticket } from './models' 3 | 4 | export const initializeDatabase = async () => { 5 | try { 6 | await connection.authenticate() 7 | await Ticket.sync({}) 8 | 9 | console.log('Connection has been established and models synced successfully.') 10 | } catch (error) { 11 | console.error('Unable to connect to the database:', error) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ticket/src/db/models/Ticket.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { connection } from '../config' 3 | 4 | const DataTypes = require('sequelize') 5 | 6 | const Ticket = connection.define('ticket', { 7 | title: { 8 | type: DataTypes.STRING, 9 | allowNull: false 10 | }, 11 | content: { 12 | type: DataTypes.STRING, 13 | allowNull: false 14 | }, 15 | userId: { 16 | type: DataTypes.INTEGER, 17 | allowNull: false 18 | } 19 | }, { 20 | tableName: 'tickets' 21 | }) 22 | 23 | export { Ticket } 24 | -------------------------------------------------------------------------------- /ticket/src/db/models/index.js: -------------------------------------------------------------------------------- 1 | export { Ticket } from './Ticket' 2 | -------------------------------------------------------------------------------- /ticket/src/enums/index.js: -------------------------------------------------------------------------------- 1 | export { resultCodes } from './resultCodes' 2 | -------------------------------------------------------------------------------- /ticket/src/enums/resultCodes.js: -------------------------------------------------------------------------------- 1 | export const resultCodes = { 2 | SUCCESS: 'Success', 3 | ERROR: 'Error' 4 | } 5 | -------------------------------------------------------------------------------- /ticket/src/exceptions/UserIdShouldProvided.js: -------------------------------------------------------------------------------- 1 | export class UserIdShouldProvided extends Error { 2 | constructor (message) { 3 | super(message) 4 | this.name = 'UserIdShouldProvided' 5 | this.httpStatusCode = 400 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ticket/src/exceptions/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yildizberkay/microservices-with-expressjs/29647645acbb8f21d4adbf719e99da7d42d64c41/ticket/src/exceptions/index.js -------------------------------------------------------------------------------- /ticket/src/index.js: -------------------------------------------------------------------------------- 1 | import { initializeDatabase } from './db' 2 | const app = require('./server') 3 | 4 | const port = 3000 5 | 6 | app.listen(port, async () => { 7 | await initializeDatabase() 8 | console.log(`Ticket service listening at http://localhost:${port}`) 9 | }) 10 | -------------------------------------------------------------------------------- /ticket/src/middlewares/globalErrorHandler.js: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express' 2 | import { resultCodes } from '../enums' 3 | 4 | export const globalErrorHandler = async (error, req: Request, res: Response, next: NextFunction) => { 5 | if (error) { 6 | let statusCode = 500 7 | if (error?.httpStatusCode) { 8 | statusCode = error.httpStatusCode 9 | } 10 | 11 | res.status(statusCode).json({ 12 | result: resultCodes.ERROR, 13 | error: { 14 | name: error.name, 15 | message: error.message 16 | } 17 | }) 18 | } else { 19 | next() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ticket/src/middlewares/index.js: -------------------------------------------------------------------------------- 1 | export { globalErrorHandler } from './globalErrorHandler' 2 | -------------------------------------------------------------------------------- /ticket/src/routes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import express from 'express' 3 | import { resultCodes } from './enums' 4 | import { createTicket, getTicket } from './services' 5 | 6 | const ticketRoute = express.Router() 7 | 8 | ticketRoute.get('/', function (req, res, next) { 9 | const userId = req.headers['user-id'] 10 | getTicket(userId) 11 | .then((tickets) => res.json({ result: resultCodes.SUCCESS, tickets })) 12 | .catch(next) 13 | }) 14 | 15 | ticketRoute.get('/:id', function (req, res, next) { 16 | const userId = req.headers['user-id'] 17 | getTicket(userId, req.params.id) 18 | .then((ticket) => res.json({ result: resultCodes.SUCCESS, ticket })) 19 | .catch(next) 20 | }) 21 | 22 | ticketRoute.post('/', function (req, res, next) { 23 | const { title, content } = req.body 24 | const userId = req.headers['user-id'] 25 | 26 | createTicket(title, content, userId) 27 | .then((ticket) => res.status(201).json({ result: resultCodes.SUCCESS, ticket })) 28 | .catch(next) 29 | }) 30 | 31 | export { ticketRoute } 32 | -------------------------------------------------------------------------------- /ticket/src/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { ticketRoute } from './routes' 3 | import bodyParser from 'body-parser' 4 | import { globalErrorHandler } from './middlewares' 5 | 6 | const app = express() 7 | 8 | app.use(bodyParser.urlencoded({ extended: true })) 9 | app.use(bodyParser.json()) 10 | app.use(bodyParser.raw()) 11 | 12 | app.use(ticketRoute) 13 | app.use(globalErrorHandler) 14 | 15 | module.exports = app 16 | -------------------------------------------------------------------------------- /ticket/src/services/index.js: -------------------------------------------------------------------------------- 1 | export { createTicket, getTicket } from './ticketService' 2 | -------------------------------------------------------------------------------- /ticket/src/services/ticketService.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Ticket } from '../db/models' 3 | import { UserIdShouldProvided } from '../exceptions/UserIdShouldProvided' 4 | 5 | export const getTicket = async (userId: string, id?: string) => { 6 | if (!userId) { 7 | throw new UserIdShouldProvided() 8 | } 9 | if (id) { 10 | return Ticket.findOne({ where: { userId, id } }) 11 | } 12 | return Ticket.findAll({ where: { userId } }) 13 | } 14 | 15 | export const createTicket = async (title: string, content: string, userId: number) => { 16 | const postObject = Ticket.build({ title, content, userId }) 17 | await postObject.validate() 18 | await postObject.save() 19 | return { 20 | id: postObject.id, 21 | title: postObject.title, 22 | content: postObject.content, 23 | userId: parseInt(postObject.userId) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ticket/tests/routes.test.js: -------------------------------------------------------------------------------- 1 | import supertest from 'supertest' 2 | import { initializeDatabase } from '../src/db' 3 | import { createTicket } from '../src/services' 4 | import { Ticket } from '../src/db/models' 5 | import { sampleTicket } from './sampleData' 6 | 7 | const app = require('../src/server') 8 | 9 | describe('ticket endpoints', () => { 10 | beforeAll(async () => { 11 | await initializeDatabase() 12 | }) 13 | 14 | describe('get', () => { 15 | let sampleTicketId 16 | 17 | beforeAll(async () => { 18 | // Crate a sample ticket 19 | const ticket = await createTicket(sampleTicket.title, sampleTicket.content, sampleTicket.userId) 20 | sampleTicketId = ticket.id 21 | }) 22 | 23 | test('get all tickets', async () => { 24 | const res = await supertest(app).get('/').set('User-Id', sampleTicket.userId) 25 | 26 | expect(res.statusCode).toEqual(200) 27 | expect(res.body).toHaveProperty('tickets') 28 | expect(res.body.tickets.length).toStrictEqual(1) 29 | }) 30 | 31 | test('get all tickets - gives error if user is not provided', async () => { 32 | const res = await supertest(app).get('/') 33 | 34 | expect(res.statusCode).toEqual(400) 35 | expect(res.body).toHaveProperty('error') 36 | expect(res.body.error.name).toEqual('UserIdShouldProvided') 37 | }) 38 | 39 | test('get a ticket', async () => { 40 | const res = await supertest(app).get(`/${sampleTicketId}`).set('User-Id', sampleTicket.userId) 41 | 42 | expect(res.statusCode).toEqual(200) 43 | expect(res.body).toHaveProperty('ticket') 44 | expect(res.body.ticket).toEqual(expect.objectContaining(sampleTicket)) 45 | }) 46 | 47 | test('get a ticket - gives error if user is not provided', async () => { 48 | const res = await supertest(app).get(`/${sampleTicketId}`) 49 | 50 | expect(res.statusCode).toEqual(400) 51 | expect(res.body).toHaveProperty('error') 52 | expect(res.body.error.name).toEqual('UserIdShouldProvided') 53 | }) 54 | }) 55 | 56 | describe('post', () => { 57 | beforeEach(async () => { 58 | await Ticket.destroy({ 59 | where: {}, 60 | truncate: true 61 | }) 62 | }) 63 | 64 | test('create ticket', async () => { 65 | const res = await supertest(app) 66 | .post('/') 67 | .set({ 'User-Id': sampleTicket.userId }) 68 | .send(sampleTicket) 69 | 70 | expect(res.statusCode).toEqual(201) 71 | expect(res.body).toHaveProperty('result') 72 | expect(res.body).toHaveProperty('ticket') 73 | expect(res.body.ticket).toEqual(expect.objectContaining(sampleTicket)) 74 | }) 75 | 76 | test('gives error if user id is not provided', async () => { 77 | const res = await supertest(app) 78 | .post('/') 79 | .send(sampleTicket) 80 | 81 | expect(res.statusCode).toEqual(500) 82 | }) 83 | 84 | test('gives error if user id and payload are not provided', async () => { 85 | const res = await supertest(app) 86 | .post('/') 87 | 88 | expect(res.statusCode).toEqual(500) 89 | }) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /ticket/tests/sampleData.js: -------------------------------------------------------------------------------- 1 | export const sampleTicket = { 2 | title: 'Account book doesn\'t run', 3 | content: 'When I try to open my account books, it gives following error', 4 | userId: 1 5 | } 6 | -------------------------------------------------------------------------------- /ticket/tests/services/ticketService.test.js: -------------------------------------------------------------------------------- 1 | import { initializeDatabase } from '../../src/db' 2 | import { Ticket } from '../../src/db/models' 3 | import { sampleTicket } from '../sampleData' 4 | import { createTicket, getTicket } from '../../src/services' 5 | import { UserIdShouldProvided } from '../../src/exceptions/UserIdShouldProvided' 6 | 7 | describe('ticketService', () => { 8 | beforeAll(async () => { 9 | await initializeDatabase() 10 | }) 11 | 12 | beforeEach(async () => { 13 | await Ticket.destroy({ 14 | where: {}, 15 | truncate: true 16 | }) 17 | }) 18 | 19 | describe('get', () => { 20 | let ticket 21 | beforeEach(async () => { 22 | ticket = await Ticket.create(sampleTicket) 23 | }) 24 | 25 | test('byId', async () => { 26 | return expect(getTicket(ticket.userId, ticket.id)).resolves.toEqual(expect.objectContaining(sampleTicket)) 27 | }) 28 | 29 | test('all', async () => { 30 | return expect(getTicket(ticket.userId)).resolves.toEqual(expect.arrayContaining([ 31 | expect.objectContaining(sampleTicket) 32 | ])) 33 | }) 34 | 35 | test('should throw error if user is not provided', () => { 36 | return expect(getTicket(null, ticket.id)).rejects.toMatchObject(new UserIdShouldProvided()) 37 | }) 38 | }) 39 | 40 | test('create ticket', () => { 41 | return expect(createTicket(sampleTicket.title, sampleTicket.content, sampleTicket.userId)) 42 | .resolves 43 | .toEqual(expect.objectContaining(sampleTicket)) 44 | }) 45 | 46 | test('gives error if parameters are not provided while creating ticket', () => { 47 | return expect(createTicket()) 48 | .rejects 49 | .toEqual(expect.any(Error)) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /ticket/tests/setup.js: -------------------------------------------------------------------------------- 1 | process.env.ACTIVE_ENV = 'test' 2 | process.env.JWT_SALT = 'ThisIsTestSecret' 3 | --------------------------------------------------------------------------------