From 03c00226168233e1653a8f34008bb1d2aa57d297 Mon Sep 17 00:00:00 2001 From: ele Date: Thu, 4 Jul 2019 23:23:26 +0200 Subject: [PATCH] feat(setup-authentication): implement jwt strategy for authentication. - Ensure user receives jwt on successful registration and signin - Ensure user is registered - Ensure user signs in [Maintains #166789989] --- .env.example | 3 + .eslintrc.json | 3 +- .gitignore | 3 +- .sequelizerc | 4 +- .travis.yml | 9 +- config/index.js | 10 +-- controllers/auth.js | 60 ++++++++++++++ controllers/index.js | 2 + controllers/welcomeController.js | 18 ++-- helpers/auth.js | 27 ++++++ index.js | 3 +- middlewares/checkToken.js | 35 ++++++++ middlewares/index.js | 1 + migrations/20190704183748-create-user.js | 39 +++++++++ models/User.js | 11 +++ models/databaseUrl.js | 24 ------ models/index.js | 32 ++++---- package.json | 6 +- routes/api/index.js | 4 +- routes/api/routes.js | 80 ++++++++++++++++++ routes/api/welcome.js | 7 -- routes/index.js | 2 +- swaggerSetUp/ah-92explorers-api.js | 24 +++--- tests/auth.test.js | 100 +++++++++++++++++++++++ tests/bcrypt.test.js | 19 +++++ tests/token.test.js | 51 ++++++++++++ tests/user.test.js | 9 -- tests/welcome.test.js | 2 +- 28 files changed, 493 insertions(+), 95 deletions(-) create mode 100644 .env.example create mode 100644 controllers/auth.js create mode 100644 helpers/auth.js create mode 100644 middlewares/checkToken.js create mode 100644 migrations/20190704183748-create-user.js delete mode 100644 models/databaseUrl.js create mode 100644 routes/api/routes.js delete mode 100644 routes/api/welcome.js create mode 100644 tests/auth.test.js create mode 100644 tests/bcrypt.test.js create mode 100644 tests/token.test.js diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..e98e4cdb --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +DATABASE_URL=postgres://wmqxaqjr:tTHUvMlywe2owxdrqEaofzdC2RKF4XYp@raja.db.elephantsql.com:5432/wmqxaqjr +DATABASE_TEST=postgres://wmqxaqjr:tTHUvMlywe2owxdrqEaofzdC2RKF4XYp@raja.db.elephantsql.com:5432/wmqxaqjr +SECRET=qwertyuiop[]\';lkjhgfdsa`zxcvbnm,./+_)(*&^%$#@!) \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 840481f9..f5a374dc 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -15,7 +15,7 @@ "sourceType": "module" }, "rules": { - "one-var": 0, + "one-var": 0, "one-var-declaration-per-line": 0, "new-cap": 0, "consistent-return": 0, @@ -23,6 +23,7 @@ "comma-dangle": 0, "curly": ["error", "multi-line"], "import/no-unresolved": [2, { "commonjs": true }], + "import/prefer-default-export": 0, "no-shadow": ["error", { "allow": ["req", "res", "err"] }], "valid-jsdoc": ["error", { "requireReturn": true, diff --git a/.gitignore b/.gitignore index e984997f..060fd052 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,11 @@ # Logs logs *.log +.dist .DS_Store npm-debug.log* - +seeders # Runtime data pids *.pid diff --git a/.sequelizerc b/.sequelizerc index e624aab8..a347692f 100644 --- a/.sequelizerc +++ b/.sequelizerc @@ -1,7 +1,7 @@ -import path from 'path'; +const path = require('path'); module.exports = { - "config": path.resolve('./config', 'config.js'), + "config": path.resolve('./config', 'index.js'), "models-path": path.resolve('./models'), "migrations-path": path.resolve('./migrations') }; diff --git a/.travis.yml b/.travis.yml index 9d5ae47f..93603be5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,12 @@ language: node_js -node_js: +node_js: - "stable" services: - postgresql +before_script: + - psql -c 'CREATE DATABASE users_test;' -U postgres + - npm i sequelize -g + - sequelize db:migrate after_success: - - npm test + - npm run coveralls + \ No newline at end of file diff --git a/config/index.js b/config/index.js index 26c41de8..d159c5d3 100644 --- a/config/index.js +++ b/config/index.js @@ -1,20 +1,20 @@ -import dotenv from 'dotenv'; +const dotenv = require('dotenv'); dotenv.config(); module.exports = { development: { - DATABASE_URL: 'DATABASE_URL', + use_env_variable: 'DATABASE_URL', dialect: 'postgres', logging: false }, test: { - DATABASE_URL: 'DATABASE_TEST', + use_env_variable: 'DATABASE_TEST', dialect: 'postgres', - logging: false, + logging: false }, production: { - DATABASE_URL: 'DATABASE_URL', + use_env_variable: 'DATABASE_URL', dialect: 'postgres', logging: false } diff --git a/controllers/auth.js b/controllers/auth.js new file mode 100644 index 00000000..f5ebc8b2 --- /dev/null +++ b/controllers/auth.js @@ -0,0 +1,60 @@ +import db from '../models'; +import Auth from '../helpers/auth'; + +export const signup = async (req, res) => { + const { + userName, firstName, lastName, email + } = req.body; + const password = Auth.hashPassword(req.body.password); + const transaction = await db.sequelize.transaction(); + try { + const newUser = await db.User.create({ + userName, firstName, lastName, email, password + }, { + transaction + }); + await transaction.commit(); + if (newUser) { + res.status(201).json({ + success: 1, + token: Auth.genToken(userName, firstName, lastName, email), + user: { + username: newUser.userName, + email: newUser.email + }, + message: 'created successfully' + }); + } + } catch (ex) { + await transaction.rollback(); + res.json({ message: `${ex.errors[0].path.toLowerCase()} already exists` }); + } +}; + +export const signin = async (req, res) => { + const { email, password } = req.body; + const transaction = await db.sequelize.transaction(); + try { + const user = await db.User.findOne({ where: { email } }, { transaction }); + await transaction.commit(); + if (!user) { + return res.status(401).json({ message: 'user doesnot exit' }); + } + const passBool = Auth.comparePassword(password, user.password); + if (passBool) { + return res.status(200).json({ + success: 1, + token: Auth.genToken(user.userName, user.firstName, user.lastName, user.email), + user: { + username: user.userName, + email: user.email + }, + message: 'logged in' + }); + } + return res.status(401).json({ message: 'wrong username or password' }); + } catch (ex) { + await transaction.rollback(); + res.json({ message: ex }); + } +}; diff --git a/controllers/index.js b/controllers/index.js index e69de29b..da0dc229 100644 --- a/controllers/index.js +++ b/controllers/index.js @@ -0,0 +1,2 @@ +export * from './auth'; +export * from './welcomeController'; diff --git a/controllers/welcomeController.js b/controllers/welcomeController.js index cbfe07a6..41bf6a6f 100644 --- a/controllers/welcomeController.js +++ b/controllers/welcomeController.js @@ -1,13 +1,9 @@ -import dotenv from "dotenv"; -import db from "../models/databaseUrl"; -dotenv.config(); +import dotenv from 'dotenv'; -class WelcomeController { - static welcome(req, res) { - res.status(200).json({ - msg: "Welcome to Authors Haven API, a #92Explorers Product." - }); - } -} +dotenv.config(); -export default WelcomeController; +export const welcome = (req, res) => { + res.status(200).json({ + message: 'Welcome to Authors Haven API, a #92Explorers Product.' + }); +}; diff --git a/helpers/auth.js b/helpers/auth.js new file mode 100644 index 00000000..624d0f0d --- /dev/null +++ b/helpers/auth.js @@ -0,0 +1,27 @@ +import bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; +import dontenv from 'dotenv'; + +dontenv.config(); + +class Auth { + static hashPassword(password) { + return bcrypt.hashSync(password, bcrypt.genSaltSync(10)); + } + + static comparePassword(password, hashedPassword) { + return bcrypt.compareSync(password, hashedPassword); + } + + static genToken(username, firstname, lastname, email) { + const token = jwt.sign({ + email, + username, + firstname, + lastname, + }, process.env.SECRET, { expiresIn: '24h' }); + return token; + } +} + +export default Auth; diff --git a/index.js b/index.js index 36b6e96f..9818bb74 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,4 @@ +import '@babel/polyfill'; import express from 'express'; import bodyParser from 'body-parser'; import cors from 'cors'; @@ -25,8 +26,6 @@ app.get('/swagger.json', (req, res) => { }); app.use('/docs', swaggerUI.serve, swaggerUI.setup(swagger.swaggerSpec)); -require('./models/User'); - app.use(router); // / catch 404 and forward to error handler diff --git a/middlewares/checkToken.js b/middlewares/checkToken.js new file mode 100644 index 00000000..fc0ab740 --- /dev/null +++ b/middlewares/checkToken.js @@ -0,0 +1,35 @@ +import jwt from 'jsonwebtoken'; +import dotenv from 'dotenv'; + +dotenv.config(); + +export const checkToken = (req, res, next) => { + let token = req.headers.authorization || req.headers['x-access-token']; + if (token === undefined) { + return res.json({ + message: 'token undefined', + }); + } + + if (token.startsWith('Bearer ')) { + token = token.slice(7, token.length); + } + + if (token) { + jwt.verify(token, process.env.SECRET, (error, decoded) => { + if (error) { + return res.status(401).json({ + success: false, + message: error.message, + }); + } + req.decoded = decoded; + next(); + }); + } else { + return res.status(401).json({ + success: false, + message: 'token empty', + }); + } +}; diff --git a/middlewares/index.js b/middlewares/index.js index e69de29b..f6ea5e4d 100644 --- a/middlewares/index.js +++ b/middlewares/index.js @@ -0,0 +1 @@ +export * from './checkToken'; diff --git a/migrations/20190704183748-create-user.js b/migrations/20190704183748-create-user.js new file mode 100644 index 00000000..fd101421 --- /dev/null +++ b/migrations/20190704183748-create-user.js @@ -0,0 +1,39 @@ +module.exports = { + up: (queryInterface, Sequelize) => queryInterface.createTable('Users', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + firstName: { + type: Sequelize.STRING + }, + lastName: { + type: Sequelize.STRING + }, + email: { + type: Sequelize.STRING, + unique: true + }, + userName: { + type: Sequelize.STRING, + unique: true, + }, + provider: { + type: Sequelize.STRING + }, + password: { + type: Sequelize.STRING + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }), + down: queryInterface => queryInterface.dropTable('Users') +}; diff --git a/models/User.js b/models/User.js index e69de29b..2add6237 100644 --- a/models/User.js +++ b/models/User.js @@ -0,0 +1,11 @@ +export default (sequelize, DataTypes) => { + const User = sequelize.define('User', { + firstName: DataTypes.STRING, + lastName: DataTypes.STRING, + email: DataTypes.STRING, + userName: DataTypes.STRING, + provider: DataTypes.STRING, + password: DataTypes.STRING + }, {}); + return User; +}; diff --git a/models/databaseUrl.js b/models/databaseUrl.js deleted file mode 100644 index ea6192b1..00000000 --- a/models/databaseUrl.js +++ /dev/null @@ -1,24 +0,0 @@ -const pg = require("pg"); -const dotenv = require("dotenv"); - -dotenv.config(); - -const connString = process.env.DATABASE_URL; -const pool = new pg.Pool({ - connectionString: connString -}); - -module.exports = { - query(text, params) { - return new Promise((resolve, reject) => { - pool - .query(text, params) - .then(res => { - resolve(res); - }) - .catch(err => { - reject(err); - }); - }); - } -}; diff --git a/models/index.js b/models/index.js index 1a92074e..7172f61b 100644 --- a/models/index.js +++ b/models/index.js @@ -1,7 +1,8 @@ import fs from 'fs'; -import path from 'path'; import Sequelize from 'sequelize'; -import configs from '../config/index'; // Importing configuration file +import configs from '../config'; // Importing configuration file + +const path = require('path'); require('dotenv').config(); // Enabling the use of the env variables @@ -11,23 +12,27 @@ const config = configs[env]; // Config variable takes in our new configuration const db = {}; let sequelize; -if (config.DATABASE_URL) { - sequelize = new Sequelize(process.env[config.DATABASE_URL], config); +if (config.use_env_variable) { + sequelize = new Sequelize(process.env[config.use_env_variable], config); } else { - sequelize = new Sequelize(config.database, config.username, config.password, config); + sequelize = new Sequelize( + config.database, + config.username, + config.password, + config + ); } -fs - .readdirSync(__dirname) - .filter(file => { - return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); - }) - .forEach(file => { - const model = sequelize['import'](path.join(__dirname, file)); +fs.readdirSync(__dirname) + .filter( + file => file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js' + ) + .forEach((file) => { + const model = sequelize.import(path.join(__dirname, file)); db[model.name] = model; }); -Object.keys(db).forEach(modelName => { +Object.keys(db).forEach((modelName) => { if (db[modelName].associate) { db[modelName].associate(db); } @@ -37,4 +42,3 @@ db.sequelize = sequelize; db.Sequelize = Sequelize; module.exports = db; - diff --git a/package.json b/package.json index b8b16b79..af2d72c0 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@babel/polyfill": "^7.4.4", "@babel/preset-env": "^7.4.5", "@babel/register": "^7.4.4", + "bcrypt": "^3.0.6", "body-parser": "^1.18.3", "cors": "^2.8.4", "dotenv": "^6.2.0", @@ -30,12 +31,14 @@ "express": "^4.17.1", "express-jwt": "^5.3.1", "express-session": "^1.15.6", - "jsonwebtoken": "^8.3.0", + "faker": "^4.1.0", + "jsonwebtoken": "^8.5.1", "method-override": "^2.3.10", "methods": "^1.1.2", "mongoose": "^5.2.2", "mongoose-unique-validator": "^2.0.1", "morgan": "^1.9.1", + "mysql2": "^1.6.5", "nodemon": "^1.19.1", "passport": "^0.4.0", "passport-local": "^1.0.0", @@ -43,6 +46,7 @@ "request": "^2.87.0", "sequelize": "^5.8.12", "sequelize-cli": "^5.5.0", + "sinon": "^7.3.2", "swagger-jsdoc": "^3.2.9", "swagger-ui-express": "^4.0.6", "underscore": "^1.9.1" diff --git a/routes/api/index.js b/routes/api/index.js index b5668f43..1334907a 100644 --- a/routes/api/index.js +++ b/routes/api/index.js @@ -1,8 +1,8 @@ import express from 'express'; -import welcome from './welcome'; +import routes from './routes'; const router = express.Router(); -router.use('/', welcome); +router.use('/', routes); export default router; diff --git a/routes/api/routes.js b/routes/api/routes.js new file mode 100644 index 00000000..11d641f3 --- /dev/null +++ b/routes/api/routes.js @@ -0,0 +1,80 @@ +import express from 'express'; +import { welcome, signup, signin } from '../../controllers'; +import { checkToken } from '../../middlewares'; + +const router = express.Router(); +router.get('/welcome', checkToken, welcome); +/** +* @swagger +* /api/users: +* post: +* tags: +* - Auth +* name: Signup +* summary: Signs up in a User/Admin +* consumes: +* - application/json +* parameters: +* - name: body +* in: body +* properties: +* userName: +* type: string +* example: abel +* firstName: +* type: string +* example: jojo +* lastName: +* type: string +* example: abtex +* email: +* type: string +* example: abtex@gmail.com +* password: +* type: string +* format: password +* example: stealth +* required: +* - userName +* - firstName +* - lastName +* - email +* - password +* responses: +* 201: +* description: User creeated +*/ +router.post('/users', signup); +/** +* @swagger +* /api/users/login: +* post: +* tags: +* - Auth +* name: Signin +* summary: Signs in in a User/Admin +* consumes: +* - application/json +* parameters: +* - name: body +* in: body +* properties: +* email: +* type: string +* example: abtex@gmail.com +* password: +* type: string +* format: password +* example: stealth +* required: +* - email +* - password +* responses: +* 200: +* description: User Has Successfully Logged In +* 401: +* description: Invalid Email or Password +*/ +router.post('/users/login', signin); + +export default router; diff --git a/routes/api/welcome.js b/routes/api/welcome.js deleted file mode 100644 index dca8301d..00000000 --- a/routes/api/welcome.js +++ /dev/null @@ -1,7 +0,0 @@ -import express from 'express'; -import welcomeController from '../../controllers/welcomeController'; - -const router = express.Router(); -router.get('/welcome', welcomeController.welcome); - -export default router; diff --git a/routes/index.js b/routes/index.js index a949efb3..665a5f64 100644 --- a/routes/index.js +++ b/routes/index.js @@ -3,6 +3,6 @@ import router from './api'; const route = express.Router(); -route.use('/api/v1', router); +route.use('/api', router); export default route; diff --git a/swaggerSetUp/ah-92explorers-api.js b/swaggerSetUp/ah-92explorers-api.js index 2a90fa5e..e983631b 100644 --- a/swaggerSetUp/ah-92explorers-api.js +++ b/swaggerSetUp/ah-92explorers-api.js @@ -1,28 +1,28 @@ -import swaggerDoc from "swagger-jsdoc"; +import swaggerDoc from 'swagger-jsdoc'; const swaggerDefinition = { info: { - title: "Authors Haven", - version: "1.0.0", + title: 'Authors Haven', + version: '1.0.0', description: - "Create a community of like minded authors to foster inspiration and innovation by leveraging the modern web" + 'Create a community of like minded authors to foster inspiration and innovation by leveraging the modern web' }, - host: "ah-92explorers-api.herokuapp.com", - schemes: ["https"], - basePath: "/welcome", + host: 'ah-92explorers-api.herokuapp.com', + schemes: ['https'], + basePath: '/welcome', securityDefinitions: { bearerAuth: { - type: "apiKey", - name: "authorization", - scheme: "bearer", - in: "header" + type: 'apiKey', + name: 'authorization', + scheme: 'bearer', + in: 'header' } } }; const options = { swaggerDefinition, - apis: ["[]"] + apis: ['../routes/api/*.js'] }; exports.swaggerSpec = swaggerDoc(options); diff --git a/tests/auth.test.js b/tests/auth.test.js new file mode 100644 index 00000000..44942ff1 --- /dev/null +++ b/tests/auth.test.js @@ -0,0 +1,100 @@ +import chai, { expect } from 'chai'; +import chaiHttp from 'chai-http'; +import faker from 'faker'; +import app from '../index'; + +chai.use(chaiHttp); + +const email = faker.internet.email(); +const userName = faker.internet.userName(); + +const dummyData = { + userName, + firstName: 'eleman', + lastName: 'hillary', + email, + password: '123456', +}; + +const dummySignin = { + email, + password: '123456', +}; + +const invalidDummy = { + email, + password: '12345', +}; + +const invalidDummy1 = { + email: 'me@fdjfkj.com', + password: '123456' +}; + +describe('User Authentication Routes', () => { + it('should signup user with valid crendentials', (done) => { + chai.request(app) + .post('/api/users') + .send(dummyData) + .then((res) => { + expect(res.statusCode).to.be.equal(201); + expect(res.body).to.be.an('object'); + expect(typeof res.body.token).to.be.equal('string'); + expect(res.body.user.username).to.be.equal(userName); + expect(typeof res.body.user.email).to.be.equal('string'); + expect(res.body.user.email).to.be.equal(email); + expect(res.body.message).to.be.equal('created successfully'); + done(); + }) + .catch(err => done(err)); + }); + it('should login user with valid credentials', (done) => { + chai.request(app) + .post('/api/users/login') + .send(dummySignin) + .then((res) => { + expect(res.statusCode).to.be.equal(200); + expect(typeof res.body.token).to.be.equal('string'); + expect(Object.keys(res.body.user).length).to.be.equal(2); + expect(res.body.user.username).to.be.equal(userName); + expect(typeof res.body.user.email).to.be.equal('string'); + expect(res.body.user.email).to.be.equal(email); + expect(res.body.message).to.be.equal('logged in'); + done(); + }) + .catch(err => done(err)); + }); + it('should not login user with invalid email id', (done) => { + chai.request(app) + .post('/api/users/login') + .send(invalidDummy1) + .then((res) => { + expect(res.statusCode).to.be.equal(401); + expect(res.body.message).to.be.equal('user doesnot exit'); + done(); + }) + .catch(err => done(err)); + }); + it('should not login user with invalid password', (done) => { + chai.request(app) + .post('/api/users/login') + .send(invalidDummy) + .then((res) => { + expect(res.statusCode).to.be.equal(401); + expect(res.body.message).to.be.equal('wrong username or password'); + done(); + }) + .catch(err => done(err)); + }); + it('should not signup user with already existing email id', (done) => { + chai.request(app) + .post('/api/users') + .send(dummyData) + .then((res) => { + expect(res.statusCode).to.be.equal(200); + expect(res.body.message).to.be.equal('email already exists'); + done(); + }) + .catch(err => done(err)); + }); +}); diff --git a/tests/bcrypt.test.js b/tests/bcrypt.test.js new file mode 100644 index 00000000..db241da1 --- /dev/null +++ b/tests/bcrypt.test.js @@ -0,0 +1,19 @@ +import { expect } from 'chai'; +import Auth from '../helpers/auth'; + +const password = '{[}]:?><,.12345'; +describe('Bcrpyt Password Hashing', () => { + let result; + it('should return hashed password(typeof \'string\')', () => { + result = Auth.hashPassword(password); + expect(typeof result).to.be.equal('string'); + }); + it('should return true if password and hashed password match', () => { + result = Auth.comparePassword(password, result); + expect(result).to.be.equal(true); + }); + it('should return false if password and hashed password doesnot match', () => { + result = Auth.comparePassword('ejkkjeb', `${result}`); + expect(result).to.be.equal(false); + }); +}); diff --git a/tests/token.test.js b/tests/token.test.js new file mode 100644 index 00000000..828462f8 --- /dev/null +++ b/tests/token.test.js @@ -0,0 +1,51 @@ +import chai from 'chai'; +import sinon from 'sinon'; +import jwt from 'jsonwebtoken'; +import dotenv from 'dotenv'; +import { checkToken } from '../middlewares'; + +dotenv.config(); +const { expect } = chai; + +const dummyData = { + userName: 'elema', + firstName: 'eleman', + lastName: 'hillary', + email: 'eleman@gmail.com', +}; + +describe('Test checkToken Middleware', () => { + let request; + let response; + let next; + + beforeEach(() => { + request = {}; + response = { + status: sinon.stub().returnsThis(), + json: sinon.spy(), + }; + next = sinon.spy(); + }); + it('next should not be called if bad token was provided', () => { + request.headers = {}; + request.headers.authorization = 'some authorization header'; + checkToken(request, response, next); + expect(next.called).to.equal(false); + }); + + it('request should contain user info if good token was provided', () => { + request.headers = {}; + request.headers.authorization = jwt.sign(dummyData, process.env.SECRET); + checkToken(request, response, next); + expect(request.decoded).to.have.property('email'); + expect(request.decoded.email).to.be.equal('eleman@gmail.com'); + }); + + it('next should be called once if good token was provided', () => { + request.headers = {}; + request.headers.authorization = jwt.sign(dummyData, process.env.SECRET); + checkToken(request, response, next); + expect(next.calledOnce).to.equal(true); + }); +}); diff --git a/tests/user.test.js b/tests/user.test.js index 3bdb6a70..139597f9 100644 --- a/tests/user.test.js +++ b/tests/user.test.js @@ -1,11 +1,2 @@ -import chaiHttp from "chai-http"; -import chai, { expect, should } from "chai"; -import app from "../index"; -chai.should(); -chai.use(chaiHttp); - -describe('User Endpoints', () => { - -}); diff --git a/tests/welcome.test.js b/tests/welcome.test.js index 6267d80d..a1f32330 100644 --- a/tests/welcome.test.js +++ b/tests/welcome.test.js @@ -3,7 +3,7 @@ import { assert } from 'chai'; describe('Express Server', () => { it('should return 200', (done) => { - http.get('http://localhost:3000/api/v1/welcome', (res) => { + http.get('http://localhost:3000/api/welcome', (res) => { assert.equal(200, res.statusCode); done(); });