Create sequelize transaction inside Express router - node.js

I'm trying to make a rest request using sequelize transaction, unfortunately it doesn't works:
undefined is not a function
This means that sequelize.transaction is undefined, sequelize is imported but not instantiated for using inside my route:
router.post('/', secret.ensureAuthorized, function(req, res) {
var sequelize = models.sequelize;
var newpost;
sequelize.transaction({autocommit: false}, function (t) {
return models.post.build().updateAttributes({
title: req.body.title,
shortdescription: req.body.description.substring(0,255),
description: req.body.description,
titleImage: req.body.titleImage,
link: req.body.link,
userid: req.body.userid
}, {transaction: t}).then(function(post){
newpost = post;
// create categories
var tags = req.body.categories;
models.hashtag.bulkCreate(tags, {transaction: t}).then(function(){
newpost.setTags(tags, {transaction: t});
});
});
}).then(function (result) {
// Transaction has been committed
// result is whatever the result of the promise chain returned to the transaction callback is
if (newpost) {
res.json(newpost);
}
console.log(result);
}).catch(function (e) {
// Transaction has been rolled back
// err is whatever rejected the promise chain returned to the transaction callback is
throw e;
});
});
My models works without any issues and the express routes are working too but not with transaction.
My package json
{
"name": "myapp",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"body-parser": "~1.12.4",
"cookie-parser": "~1.3.5",
"debug": "~2.2.0",
"express": "~4.12.4",
"jade": "~1.9.2",
"morgan": "~1.5.3",
"serve-favicon": "~2.2.1",
"jsonwebtoken": "^5.0.2",
"pg": "^4.4.0",
"pg-hstore": "^2.3.2",
"crypto-js": "^3.1.5",
"sequelize": "^3.2.0"
}
}
My index.js is working fine too, but don't know how to pass the same instance of sequelize here for express routes:
var Sequelize = require('sequelize');
var config = require('../config'); // we use node-config to handle environments
var fs = require("fs");
var path = require("path");
var models = require('../models');
// initialize database connection
var sequelize = new Sequelize(
config.database.name,
config.database.username,
config.database.password, {
dialect: 'postgres',
host: config.database.host,
port: config.database.port,
autoIncrement: true,
omitNull: true,
freezeTableName: true,
pool: {
max: 15,
min: 0,
idle: 10000
},
});
var db = {};
fs
.readdirSync(__dirname)
.filter(function(file) {
return (file.indexOf(".") !== 0) && (file !== "index.js");
})
.forEach(function(file) {
var model = sequelize["import"](path.join(__dirname, file));
db[model.name] = model;
});
Object.keys(db).forEach(function(modelName) {
if ("associate" in db[modelName]) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
sequelize.sync({
force: true
}).then(function(){
// load batch
if (process.env.BATCH) {
console.log("loading batch");
var batch = require("../config/batch");
batch.loadPosts();
}
});
module.exports = db;
best regards.
UPDATE
I changed the code as above explained.
Now my error is:
Unhandled rejection Error: commit has been called on this transaction(c457e532-b
164-43dc-9b0e-432be031fe36), you can no longer use it
I'm using Postgres database.

It seems to me that your database initialization code may be stored in a file called sequelize.js, which you are trying to import in your route handler.
However, you're importing the global sequelize module, and not your local one. You need to use a relative path to do that:
router.post('/', function(req, res) {
var sequelize = require('./sequelize');
sequelize.transaction(function(t){
console.log('transaction openned'+t);
});
});
(that assumes that your sequelize.js file is located in the same directory as your route handler file is; if not, you should change the ./ part)

This is my final solution, I didn't know about returning those promises, now I can do that like this:
var sequelize = models.sequelize;
var newpost;
// create categories
var tags = req.body.categories;
sequelize.transaction({autocommit: false}, function (t) {
return models.post.build().updateAttributes({
title: req.body.title,
shortdescription: req.body.description.substring(0,255),
description: req.body.description,
titleImage: req.body.titleImage,
link: req.body.link,
userid: req.body.userid
}, {transaction: t}).then(function(post){
newpost = post;
for (var i = 0; i < tags.length;i++) {
// ({where: {username: 'sdepold'}, defaults: {job: 'Technical Lead JavaScript'}})
return models.hashtag.findOrCreate(
{where: {description: tags[i].description},
defaults: {description: tags[i].description},
transaction: t}).spread(function(tag, created) {
return newpost.addTag(tag, {transaction: t});
});
}
});
}).then(function (result) {
// Transaction has been committed
// result is whatever the result of the promise chain returned to the transaction callback is
if (newpost) {
res.json(newpost);
}
console.log(result);
}).catch(function (e) {
// Transaction has been rolled back
// err is whatever rejected the promise chain returned to the transaction callback is
res.status(500).send({
type: false,
data: e
});
throw e;
});

Related

Node and MongoDB communication problem during valiadation and post route

I have a plain node express webserver that send MongoDB Atlas a post request. I use mongooose.
Before do this there is some express-validators work.
Two API key used in this project. Thats enabled and work. Of course not share here...
I test with Postman.
This code from Node and MongoDB expert.
If i change MongoDB cluster not help.
I got two error:
Validation error: Invalid inputs passed, please check your data 422
If I comment out validation got different error:
Other error on post route: Creating place failed, please try again 500
app.js
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const placesRoutes = require('./routes/places-routes');
const usersRoutes = require('./routes/users-routes');
const HttpError = require('./models/http-error');
const app = express();
app.use(bodyParser.json());
app.use('/api/places', placesRoutes); // => /api/places...
app.use('/api/users', usersRoutes);
app.use((req, res, next) => {
const error = new HttpError('Could not find this route.', 404);
throw error;
});
app.use((error, req, res, next) => {
if (res.headerSent) {
return next(error);
}
res.status(error.code || 500);
res.json({ message: error.message || 'An unknown error occurred!' });
});
mongoose
.connect('xxxx')
.then(() => {
app.listen(5000);
})
.catch(err => {
console.log(err);
});
places-routes.js
const express = require('express');
const { check } = require('express-validator');
const placesControllers = require('../controllers/places-controllers');
const router = express.Router();
router.get('/:pid', placesControllers.getPlaceById);
router.get('/user/:uid', placesControllers.getPlacesByUserId);
router.post(
'/',
[
// check('title')
// .not()
// .isEmpty(),
// check('description').isLength({ min: 5 }),
// check('address')
// .not()
// .isEmpty()
],
placesControllers.createPlace
);
router.patch(
'/:pid',
[
check('title')
.not()
.isEmpty(),
check('description').isLength({ min: 5 })
],
placesControllers.updatePlace
);
router.delete('/:pid', placesControllers.deletePlace);
module.exports = router;
places-controllers.js
const uuid = require('uuid/v4');
const { validationResult } = require('express-validator');
const HttpError = require('../models/http-error');
const getCoordsForAddress = require('../util/location');
const Place = require('../models/place');
let DUMMY_PLACES = [
{
id: 'p1',
title: 'Empire State Building',
description: 'One of the most famous sky scrapers in the world!',
location: {
lat: 40.7484474,
lng: -73.9871516
},
address: '20 W 34th St, New York, NY 10001',
creator: 'u1'
}
];
const getPlaceById = (req, res, next) => {
const placeId = req.params.pid; // { pid: 'p1' }
const place = DUMMY_PLACES.find(p => {
return p.id === placeId;
});
if (!place) {
throw new HttpError('Could not find a place for the provided id.', 404);
}
res.json({ place }); // => { place } => { place: place }
};
// function getPlaceById() { ... }
// const getPlaceById = function() { ... }
const getPlacesByUserId = (req, res, next) => {
const userId = req.params.uid;
const places = DUMMY_PLACES.filter(p => {
return p.creator === userId;
});
if (!places || places.length === 0) {
return next(
new HttpError('Could not find places for the provided user id.', 404)
);
}
res.json({ places });
};
const createPlace = async (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return next(
new HttpError('Invalid inputs passed, please check your data.', 422)
);
}
const { title, description, address, creator } = req.body;
let coordinates;
try {
coordinates = await getCoordsForAddress(address);
} catch (error) {
return next(error);
}
// const title = req.body.title;
const createdPlace = new Place({
title,
description,
address,
location: coordinates,
image: 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/10/Empire_State_Building_%28aerial_view%29.jpg/400px-Empire_State_Building_%28aerial_view%29.jpg',
creator
});
try {
await createdPlace.save();
} catch (err) {
const error = new HttpError(
'Creating place failed, please try again.',
500
);
return next(error);
}
res.status(201).json({ place: createdPlace });
};
const updatePlace = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
throw new HttpError('Invalid inputs passed, please check your data.', 422);
}
const { title, description } = req.body;
const placeId = req.params.pid;
const updatedPlace = { ...DUMMY_PLACES.find(p => p.id === placeId) };
const placeIndex = DUMMY_PLACES.findIndex(p => p.id === placeId);
updatedPlace.title = title;
updatedPlace.description = description;
DUMMY_PLACES[placeIndex] = updatedPlace;
res.status(200).json({ place: updatedPlace });
};
const deletePlace = (req, res, next) => {
const placeId = req.params.pid;
if (!DUMMY_PLACES.find(p => p.id === placeId)) {
throw new HttpError('Could not find a place for that id.', 404);
}
DUMMY_PLACES = DUMMY_PLACES.filter(p => p.id !== placeId);
res.status(200).json({ message: 'Deleted place.' });
};
exports.getPlaceById = getPlaceById;
exports.getPlacesByUserId = getPlacesByUserId;
exports.createPlace = createPlace;
exports.updatePlace = updatePlace;
exports.deletePlace = deletePlace;
package.json
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon app.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.19.0",
"body-parser": "^1.19.0",
"express": "^4.17.1",
"express-validator": "^6.2.0",
"mongoose": "^5.7.8",
"uuid": "^3.3.3"
},
"devDependencies": {
"nodemon": "^1.19.4"
}
}
Firstly use this middleware in app.js, and remove body-parser.
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
It parses your request and you get all values...
Only a plus ":" sign by title
wrong
{
"title:": "Empire State Building",
"description": "A very famous building in NY",
"address": "20 W 34th St, New York, NY 10001",
"creator": "u2"
}
and the good:
{
"title": "Empire State Building",
"description": "A very famous building in NY",
"address": "20 W 34th St, New York, NY 10001",
"creator": "u2"
}

Unit testing with Jest and Sequelize - Test passing when it shouldn't

I'm trying to write some unit tests, and at first they appeared to be working, but then I realized some of them were passing when they definitely shouldn't be, and I've included one such example from my code of a test passing when it shouldn't:
controller.js
const userModel = require('../models').users;
module.exports = {
async getUserByID(req, res) {
try {
var user = await userModel.findOne({
where: { id: req.params.id }
});
res.status(201).send(user);
catch (error) {
res.status(500).send(error)
}
}
}
controller.test.js
const controller = require('../controllers/controller');
const httpMocks = require('node-mocks-http');
//mock for main model that my controller function uses
jest.mock('../models/users', () => {
const SequelizeMock = require('sequelize-mock');
const dbMock = new SequelizeMock();
return dbMock.define('users', {
id: 1,
name: 'Tony',
email: 'test1#gmail.com',
uid: '12345'
})
});
//mock for model that's associated to my users model
jest.mock('../models/user_prefs', () => {
const SequelizeMock = require('sequelize-mock');
const dbMock = new SequelizeMock();
return dbMock.define('user_prefs', {
id: 1,
uid: '12345',
notification_settings: {}
})
});
//the actual test
describe('getUserByID', () => {
it('should get user from mock', () => {
var req = httpMocks.createRequest({
params: {
id: 1
}
});
var res = httpMocks.createResponse();
controller.getUserByID(req, res).then(function(user) {
expect(user.name).toEqual('Steve');
})
});
});
package.json
"scripts": {
"test": "jest --config ./jest.config.js --forceExit --coverage"
}
jest.config.js
module.exports = {
testEnvironment: "node"
}
When I run npm test everything runs just fine, but it tells me that the test passes, even though it obviously shouldn't if it was correctly retrieving the data from the mock, and I can't really figure out why.

server.register() function throwing "register is missing", even though i have properly registered the plugins for graphql

I have created a node js project with graph ql (with a very basic schema) but when i am trying to start the server after registering the plugins for graphql and graphiql, i am getting the register is missing error. Below is my code
const hapi=require('hapi');
const { graphqlHapi, graphiqlHapi } = require('apollo-server-hapi');
const { makeExecutableSchema } = require('graphql-tools');
const graphqlSchema = require('./graphql/schema');
const createResolvers = require('./graphql/resolvers');
const executableSchema = makeExecutableSchema({
typeDefs: [graphqlSchema],
resolvers: createResolvers(),
});
const server=hapi.server({
port: 4000,
host:'localhost'
});
server.register({
plugin: graphqlHapi,
options: {
path: '/graphql',
graphqlOptions: () => ({
pretty: true,
schema: executableSchema,
}),
},
});
server.register({
plugin: graphiqlHapi,
options: {
path: '/graphiql',
graphiqlOptions: {
endpointURL: '/graphql',
},
},
});
const init= async()=>{
routes(server);
await server.start();
console.log(`Server is running at: ${server.info.uri}`);
}
init();
I had initially given the key name as register instead of plugin in the server.register() functions. In either case, i am getting the below error
(node:19104) DeprecationWarning: current URL string parser is
deprecated, and will be removed in a future version. To use the new
parser, pass option { useNewUrlParser: true } to MongoClient.connect.
(node:19104) UnhandledPromiseRejectionWarning: AssertionError
[ERR_ASSERTION]: I nvalid plugin options {
"plugin": {
"options": {
"path": "/graphql",
"graphqlOptions": () => ({\r\n pretty: true,\r\n schema: exe cutableSchema,\r\n })
},
"register" [1]: -- missing -- } }
Please help me out in understanding whenter code herey this happening and how it can be rectified.
Below is the dependencies in my project
apollo-server-hapi": "^2.3.1", "graphql": "^14.0.2", "graphql-tools":
"^4.0.3", "hapi": "^17.8.1",
EDIT
Code after making the suggested changes
const hapi=require('hapi');
const { graphqlHapi, graphiqlHapi } = require('apollo-server-hapi');
const { makeExecutableSchema } = require('graphql-tools');
const graphqlSchema = require('./graphql/schema');
const createResolvers = require('./graphql/resolvers');
const executableSchema = makeExecutableSchema({
typeDefs: [graphqlSchema],
resolvers: createResolvers(),
});
async function start_server() {
const server=hapi.server({
port: 4000,
host:'localhost'
});
await server.register({
plugin: graphqlHapi,
options: {
path: '/graphql',
graphqlOptions: () => ({
pretty: true,
schema: executableSchema,
}),
route: {
cors: true,
},
},
});
await server.register({
plugin: graphiqlHapi,
options: {
path: '/graphiql',
graphiqlOptions: {
endpointURL: '/graphql',
},
route: {
cors: true,
},
},
});
try {
await server.start();
console.log(`Server is running at: ${server.info.uri}`);
} catch (err) {
console.log(`Error while starting server: ${err.message}`)
}
}
start_server();
There is no need to register the plugins in the latest release of apollo-server-hapi. It contains GraphQL playground instead of graphiql.
The below changes need to be done instead of registering.
const {ApolloServer} = require('apollo-server-hapi');
const executableSchema = makeExecutableSchema({
typeDefs: [graphqlSchema],
resolvers: createResolvers(),
});
const server = new ApolloServer({
schema:executableSchema
});
async function start_server() {
const app=hapi.server({
port: 4000,
host:'localhost'
});
await server.applyMiddleware({ app });
try {
await app.start();
console.log(`Server is running at: ${app.info.uri}`);
} catch (err) {
console.log(`Error while starting server: ${err.message}`)
}
}
start_server();

GraphQL Subscriptions - subscriptionsClient.subscribe is not a function

So, I'm trying create a basic GraphQL Subscription Server. Problem in request result in graphiql. It's - "subscriptionsClient.subscribe is not a function". I don't understand where's problem.
For GraphQL Subscription Server I have used: graphql-server-express,
subscriptions-transport-ws, graphql-subscriptions
So, it's the task for you, GraphQL masters.
Code:
index.js
const { createServer } = require('http')
const app = require('express')();
const bodyParser = require('body-parser')
const { graphqlExpress, graphiqlExpress } = require('graphql-server-express')
const { SubscriptionServer } = require('subscriptions-transport-ws')
const { subscribe, execute } = require('graphql');
const schema = require('./schema');
app.use(bodyParser.json());
app.use('/graphql', new graphqlExpress({
schema
}));
app.use('/graphiql', new graphiqlExpress({
endpointURL: '/graphql',
subscriptionsEndpoint: 'ws://localhost:4000/subscriptions'
}));
const server = createServer(app);
server.listen(4000, () => {
console.log("Server is listening on port 4000!");
subscriptionServer = SubscriptionServer.create(
{
schema,
execute,
subscribe,
onConnect: () => console.log("Client connected!")
}, {
server,
path: '/subscriptions'
}
);
});
schema.js
const {
GraphQLSchema,
GraphQLObjectType,
GraphQLNonNull,
GraphQLList,
GraphQLID,
GraphQLString
} = require('graphql');
const { PubSub, withFilter } = require('graphql-subscriptions');
const socket = new PubSub();
const store = [];
const NameType = new GraphQLObjectType({
name: "Name",
fields: {
id: { type: GraphQLID },
name: { type: GraphQLString }
}
});
const RootQuery = new GraphQLObjectType({
name: "RootQuery",
fields: {
names: {
type: new GraphQLList(NameType),
resolve: () => store
}
}
});
const RootMutation = new GraphQLObjectType({
name: "RootMutation",
fields: {
addName: {
type: NameType,
args: {
name: { type: new GraphQLNonNull(GraphQLString) }
},
resolve(_, { name }) {
let model = {
id: store.length,
name
}
socket.publish("names", model);
store.push(model);
return model;
}
}
}
});
const RootSubscription = new GraphQLObjectType({
name: "RootSubscription",
fields: {
names: {
type: NameType,
resolve() {
console.log("IS RUNNING");
},
subscribe: withFilter(() => pubsub.asyncIterator("names"), (payload, variables) => {
return payload.names.id === variables.relevantId;
})
}
}
});
module.exports = new GraphQLSchema({
query: RootQuery,
mutation: RootMutation,
subscription: RootSubscription
});
Ok, here's thing.
I've created a fully responsive GraphQL Subscriptions Server using Apollo-Server.
Just use apollo-server-express and apollo-server packages for this task. ApolloServer provides GraphQL Playground that is supports subscriptions. So it's easy to debug and use it on front-end.
Good Luck!
Here is the latest one. This is based on ApolloGraphql Documentation. This worked perfectly for me.
app.js
import mongoose from 'mongoose'
import cors from 'cors';
import dotEnv from 'dotenv'
import http from 'http';
import { ApolloServer, PubSub } from 'apollo-server-express';
import schema from './graphql/schema'
import express from 'express';
dotEnv.config();
const port = process.env.PORT || 3000;
const pubsub = new PubSub();
const app = express();
const server = new ApolloServer({
schema,
subscriptions: {
onConnect: () => console.log('πŸ•ΈοΈ Client connected to websocket'),
onDisconnect: (webSocket, context) => {
console.log('Client disconnected from websocket')
},
},
});
server.applyMiddleware({ app })
const httpServer = http.createServer(app);
server.installSubscriptionHandlers(httpServer);
httpServer.listen(port, () => {
console.log(`πŸš€ Apollo Server Server ready at http://localhost:${port}${server.graphqlPath}`)
})
package.json
"dependencies": {
"apollo-server": "^2.25.1",
"apollo-server-express": "^2.25.1",
"babel-node": "^0.0.1-security",
"body-parser": "^1.19.0",
"cors": "^2.8.5",
"cross-fetch": "^3.1.4",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"graphql": "^15.5.0",
"graphql-subscriptions": "^1.2.1",
"graphql-tools": "^7.0.5",
"moment": "^2.29.1",
"mongoose": "^5.12.13",
"subscriptions-transport-ws": "^0.9.19"
},
"devDependencies": {
"#babel/cli": "^7.14.3",
"#babel/core": "^7.14.3",
"#babel/node": "^7.14.2",
"#babel/preset-env": "^7.14.4",
"#babel/register": "^7.13.16",
"jest": "^27.0.4",
"nodemon": "^2.0.7",
"supertest": "^6.1.3"
}
import { w3cwebsocket } from 'websocket';
...
const myWSClient = createClient({
url: websockerUrl || "",
webSocketImpl: w3cwebsocket,
});
const wsLink = new GraphQLWsLink(myWSClient);

How to Unit Test a Node API using Sinon (Express with Mongo DB)

I am creating an API using Node but am struggling to understand how to properly Unit test the API. The API itself uses Express and Mongo (with Mongoose).
So far I have been able to create Integration tests for end to end testing of the API endpoints themselves. I have used supertest, mocha and chai for the integration tests along with dotenv to use a test database when running it. The npm test script sets the environment to test before the integration tests run. It works excellently.
But I would like to also create Unit Tests for various components such as the controller functions.
I'm keen to use Sinon for the Unit Tests but I'm struggling to know what next steps to take.
I'll detail a genericised version of the API rewritten to be everybody's favourite Todos.
The app has the following directory structure:
api
|- todo
| |- controller.js
| |- model.js
| |- routes.js
| |- serializer.js
|- test
| |- integration
| | |- todos.js
| |- unit
| | |- todos.js
|- index.js
|- package.json
package.json
{
"name": "todos",
"version": "1.0.0",
"description": "",
"main": "index.js",
"directories": {
"doc": "docs"
},
"scripts": {
"test": "mocha test/unit --recursive",
"test-int": "NODE_ENV=test mocha test/integration --recursive"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.15.0",
"express": "^4.13.4",
"jsonapi-serializer": "^3.1.0",
"mongoose": "^4.4.13"
},
"devDependencies": {
"chai": "^3.5.0",
"mocha": "^2.4.5",
"sinon": "^1.17.4",
"sinon-as-promised": "^4.0.0",
"sinon-mongoose": "^1.2.1",
"supertest": "^1.2.0"
}
}
index.js
var express = require('express');
var app = express();
var mongoose = require('mongoose');
var bodyParser = require('body-parser');
// Configs
// I really use 'dotenv' package to set config based on environment.
// removed and defaults put in place for brevity
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
// Database
mongoose.connect('mongodb://localhost/todosapi');
//Middleware
app.set('port', 3000);
app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json());
// Routers
var todosRouter = require('./api/todos/routes');
app.use('/todos', todosRouter);
app.listen(app.get('port'), function() {
console.log('App now running on http://localhost:' + app.get('port'));
});
module.exports = app;
serializer.js
(This purely takes the output from Mongo and serializes it into JsonAPI format. So it is a bit superfluous to this example but I left it in as it is something I currently make use of in the api.)
'use strict';
var JSONAPISerializer = require('jsonapi-serializer').Serializer;
module.exports = new JSONAPISerializer('todos', {
attributes: ['title', '_user']
,
_user: {
ref: 'id',
attributes: ['username']
}
});
routes.js
var router = require('express').Router();
var controller = require('./controller');
router.route('/')
.get(controller.getAll)
.post(controller.create);
router.route('/:id')
.get(controller.getOne)
.put(controller.update)
.delete(controller.delete);
module.exports = router;
model.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var todoSchema = new Schema({
title: {
type: String
},
_user: {
type: Schema.Types.ObjectId,
ref: 'User'
}
});
module.exports = mongoose.model('Todo', todoSchema);
controller.js
var Todo = require('./model');
var TodoSerializer = require('./serializer');
module.exports = {
getAll: function(req, res, next) {
Todo.find({})
.populate('_user', '-password')
.then(function(data) {
var todoJson = TodoSerializer.serialize(data);
res.json(todoJson);
}, function(err) {
next(err);
});
},
getOne: function(req, res, next) {
// I use passport for handling User authentication so assume the user._id is set at this point
Todo.findOne({'_id': req.params.id, '_user': req.user._id})
.populate('_user', '-password')
.then(function(todo) {
if (!todo) {
next(new Error('No todo item found.'));
} else {
var todoJson = TodoSerializer.serialize(todo);
return res.json(todoJson);
}
}, function(err) {
next(err);
});
},
create: function(req, res, next) {
// ...
},
update: function(req, res, next) {
// ...
},
delete: function(req, res, next) {
// ...
}
};
test/unit/todos.js
var mocha = require('mocha');
var sinon = require('sinon');
require('sinon-as-promised');
require('sinon-mongoose');
var expect = require('chai').expect;
var app = require('../../index');
var TodosModel = require('../../api/todos/model');
describe('Routes: Todos', function() {
it('getAllTodos', function (done) {
// What goes here?
});
it('getOneTodoForUser', function (done) {
// What goes here?
});
});
Now I don't want to test the routes themselves (I do that in the Integration Tests not detailed here).
My current thinking is that the next best thing is to actually unit test controller.getAll or controller.getOne functions. And then to Mock the calls to Mongo via Mongoose using Sinon stubs.
But I have no idea what to do next despite having read the sinon docs :/
Questions
How do I test controller functions if it requires req, res, next as parameters?
Do I move the model's find and populate (currently in the Controller function) into todoSchema.static functions?
How to mock the populate function to do a Mongoose JOIN?
Basically what goes into test/unit/todos.js to get the above in a solid Unit Test state :/
The end goal is to run mocha test/unit and have it unit test the various parts of that API section
Hi I've created some test for you to understand how to use mocks.
Full example github/nodejs_unit_tests_example
controller.test.js
const proxyquire = require('proxyquire')
const sinon = require('sinon')
const faker = require('faker')
const assert = require('chai').assert
describe('todo/controller', () => {
describe('controller', () => {
let mdl
let modelStub, serializerStub, populateMethodStub, fakeData
let fakeSerializedData, fakeError
let mongoResponse
before(() => {
fakeData = faker.helpers.createTransaction()
fakeError = faker.lorem.word()
populateMethodStub = {
populate: sinon.stub().callsFake(() => mongoResponse)
}
modelStub = {
find: sinon.stub().callsFake(() => {
return populateMethodStub
}),
findOne: sinon.stub().callsFake(() => {
return populateMethodStub
})
}
fakeSerializedData = faker.helpers.createTransaction()
serializerStub = {
serialize: sinon.stub().callsFake(() => {
return fakeSerializedData
})
}
mdl = proxyquire('../todo/controller.js',
{
'./model': modelStub,
'./serializer': serializerStub
}
)
})
beforeEach(() => {
modelStub.find.resetHistory()
modelStub.findOne.resetHistory()
populateMethodStub.populate.resetHistory()
serializerStub.serialize.resetHistory()
})
describe('getAll', () => {
it('should return serialized search result from mongodb', (done) => {
let resolveFn
let fakeCallback = new Promise((res, rej) => {
resolveFn = res
})
mongoResponse = Promise.resolve(fakeData)
let fakeRes = {
json: sinon.stub().callsFake(() => {
resolveFn()
})
}
mdl.getAll(null, fakeRes, null)
fakeCallback.then(() => {
sinon.assert.calledOnce(modelStub.find)
sinon.assert.calledWith(modelStub.find, {})
sinon.assert.calledOnce(populateMethodStub.populate)
sinon.assert.calledWith(populateMethodStub.populate, '_user', '-password')
sinon.assert.calledOnce(serializerStub.serialize)
sinon.assert.calledWith(serializerStub.serialize, fakeData)
sinon.assert.calledOnce(fakeRes.json)
sinon.assert.calledWith(fakeRes.json, fakeSerializedData)
done()
}).catch(done)
})
it('should call next callback if mongo db return exception', (done) => {
let fakeCallback = (err) => {
assert.equal(fakeError, err)
done()
}
mongoResponse = Promise.reject(fakeError)
let fakeRes = sinon.mock()
mdl.getAll(null, fakeRes, fakeCallback)
})
})
describe('getOne', () => {
it('should return serialized search result from mongodb', (done) => {
let resolveFn
let fakeCallback = new Promise((res, rej) => {
resolveFn = res
})
mongoResponse = Promise.resolve(fakeData)
let fakeRes = {
json: sinon.stub().callsFake(() => {
resolveFn()
})
}
let fakeReq = {
params: {
id: faker.random.number()
},
user: {
_id: faker.random.number()
}
}
let findParams = {
'_id': fakeReq.params.id,
'_user': fakeReq.user._id
}
mdl.getOne(fakeReq, fakeRes, null)
fakeCallback.then(() => {
sinon.assert.calledOnce(modelStub.findOne)
sinon.assert.calledWith(modelStub.findOne, findParams)
sinon.assert.calledOnce(populateMethodStub.populate)
sinon.assert.calledWith(populateMethodStub.populate, '_user', '-password')
sinon.assert.calledOnce(serializerStub.serialize)
sinon.assert.calledWith(serializerStub.serialize, fakeData)
sinon.assert.calledOnce(fakeRes.json)
sinon.assert.calledWith(fakeRes.json, fakeSerializedData)
done()
}).catch(done)
})
it('should call next callback if mongodb return exception', (done) => {
let fakeReq = {
params: {
id: faker.random.number()
},
user: {
_id: faker.random.number()
}
}
let fakeCallback = (err) => {
assert.equal(fakeError, err)
done()
}
mongoResponse = Promise.reject(fakeError)
let fakeRes = sinon.mock()
mdl.getOne(fakeReq, fakeRes, fakeCallback)
})
it('should call next callback with error if mongodb return empty result', (done) => {
let fakeReq = {
params: {
id: faker.random.number()
},
user: {
_id: faker.random.number()
}
}
let expectedError = new Error('No todo item found.')
let fakeCallback = (err) => {
assert.equal(expectedError.message, err.message)
done()
}
mongoResponse = Promise.resolve(null)
let fakeRes = sinon.mock()
mdl.getOne(fakeReq, fakeRes, fakeCallback)
})
})
})
})
model.test.js
const proxyquire = require('proxyquire')
const sinon = require('sinon')
const faker = require('faker')
describe('todo/model', () => {
describe('todo schema', () => {
let mongooseStub, SchemaConstructorSpy
let ObjectIdFake, mongooseModelSpy, SchemaSpy
before(() => {
ObjectIdFake = faker.lorem.word()
SchemaConstructorSpy = sinon.spy()
SchemaSpy = sinon.spy()
class SchemaStub {
constructor(...args) {
SchemaConstructorSpy(...args)
return SchemaSpy
}
}
SchemaStub.Types = {
ObjectId: ObjectIdFake
}
mongooseModelSpy = sinon.spy()
mongooseStub = {
"Schema": SchemaStub,
"model": mongooseModelSpy
}
proxyquire('../todo/model.js',
{
'mongoose': mongooseStub
}
)
})
it('should return new Todo model by schema', () => {
let todoSchema = {
title: {
type: String
},
_user: {
type: ObjectIdFake,
ref: 'User'
}
}
sinon.assert.calledOnce(SchemaConstructorSpy)
sinon.assert.calledWith(SchemaConstructorSpy, todoSchema)
sinon.assert.calledOnce(mongooseModelSpy)
sinon.assert.calledWith(mongooseModelSpy, 'Todo', SchemaSpy)
})
})
})
routes.test.js
const proxyquire = require('proxyquire')
const sinon = require('sinon')
const faker = require('faker')
describe('todo/routes', () => {
describe('router', () => {
let expressStub, controllerStub, RouterStub, rootRouteStub, idRouterStub
before(() => {
rootRouteStub = {
"get": sinon.stub().callsFake(() => rootRouteStub),
"post": sinon.stub().callsFake(() => rootRouteStub)
}
idRouterStub = {
"get": sinon.stub().callsFake(() => idRouterStub),
"put": sinon.stub().callsFake(() => idRouterStub),
"delete": sinon.stub().callsFake(() => idRouterStub)
}
RouterStub = {
route: sinon.stub().callsFake((route) => {
if (route === '/:id') {
return idRouterStub
}
return rootRouteStub
})
}
expressStub = {
Router: sinon.stub().returns(RouterStub)
}
controllerStub = {
getAll: sinon.mock(),
create: sinon.mock(),
getOne: sinon.mock(),
update: sinon.mock(),
delete: sinon.mock()
}
proxyquire('../todo/routes.js',
{
'express': expressStub,
'./controller': controllerStub
}
)
})
it('should map root get router with getAll controller', () => {
sinon.assert.calledWith(RouterStub.route, '/')
sinon.assert.calledWith(rootRouteStub.get, controllerStub.getAll)
})
it('should map root post router with create controller', () => {
sinon.assert.calledWith(RouterStub.route, '/')
sinon.assert.calledWith(rootRouteStub.post, controllerStub.create)
})
it('should map /:id get router with getOne controller', () => {
sinon.assert.calledWith(RouterStub.route, '/:id')
sinon.assert.calledWith(idRouterStub.get, controllerStub.getOne)
})
it('should map /:id put router with update controller', () => {
sinon.assert.calledWith(RouterStub.route, '/:id')
sinon.assert.calledWith(idRouterStub.put, controllerStub.update)
})
it('should map /:id delete router with delete controller', () => {
sinon.assert.calledWith(RouterStub.route, '/:id')
sinon.assert.calledWith(idRouterStub.delete, controllerStub.delete)
})
})
})
serializer.test.js
const proxyquire = require('proxyquire')
const sinon = require('sinon')
describe('todo/serializer', () => {
describe('json serializer', () => {
let JSONAPISerializerStub, SerializerConstructorSpy
before(() => {
SerializerConstructorSpy = sinon.spy()
class SerializerStub {
constructor(...args) {
SerializerConstructorSpy(...args)
}
}
JSONAPISerializerStub = {
Serializer: SerializerStub
}
proxyquire('../todo/serializer.js',
{
'jsonapi-serializer': JSONAPISerializerStub
}
)
})
it('should return new instance of Serializer', () => {
let schema = {
attributes: ['title', '_user']
,
_user: {
ref: 'id',
attributes: ['username']
}
}
sinon.assert.calledOnce(SerializerConstructorSpy)
sinon.assert.calledWith(SerializerConstructorSpy, 'todos', schema)
})
})
})

Resources