resourcejs
Version:
A simple Express library to reflect Mongoose models to a REST interface.
1,551 lines (1,400 loc) • 87.3 kB
JavaScript
/* eslint-disable no-prototype-builtins */
'use strict';
const express = require('express');
const bodyParser = require('body-parser');
const request = require('supertest');
const assert = require('assert');
const moment = require('moment');
const mongoose = require('mongoose');
const Resource = require('../Resource');
const app = express();
const _ = require('lodash');
const MongoClient = require('mongodb').MongoClient;
const ObjectId = require('mongodb').ObjectId;
const chance = (new require('chance'))();
const baseTestDate = moment.utc('2018-04-12T12:00:00.000Z');
const testDates = [
baseTestDate, // actual
moment(baseTestDate).subtract(1, 'day'), // oneDayAgo
moment(baseTestDate).subtract(1, 'month'), // oneMonthAgo
moment(baseTestDate).subtract(1, 'year'), // oneYearAgo
];
// Use the body parser.
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
// An object to store handler events.
let handlers = {};
// The raw connection to mongo, for consistency checks with mongoose.
let db = null;
/**
* Updates the reference for the handler invocation using the given sequence and method.
*
* @param entity
* The entity this handler is associated with.
* @param sequence
* The sequence of invocation: `before` or `after`.
* @param req
* The express request to manipulate.
*/
function setInvoked(entity, sequence, req) {
// Get the url fragments, to determine if this request is a get or index.
const parts = req.url.split('/');
parts.shift(); // Remove empty string element.
let method = req.method.toLowerCase();
if (method === 'get' && (parts.length % 2 === 0)) {
method = 'index';
}
if (!handlers.hasOwnProperty(entity)) {
handlers[entity] = {};
}
if (!handlers[entity].hasOwnProperty(sequence)) {
handlers[entity][sequence] = {};
}
handlers[entity][sequence][method] = true;
}
/**
* Determines if the handler for the sequence and method was invoked.
*
* @param entity
* The entity this handler is associated with.
* @param sequence
* The sequence of invocation: `before` or `after`.
* @param method
* The HTTP method for the invocation: `post`, `get`, `put`, `delete`, or `patch`
*
* @return
* If the given handler was invoked or not.
*/
function wasInvoked(entity, sequence, method) {
if (
handlers.hasOwnProperty(entity)
&& handlers[entity].hasOwnProperty(sequence)
&& handlers[entity][sequence].hasOwnProperty(method)
) {
return handlers[entity][sequence][method];
}
else {
return false;
}
}
describe('Connect to MongoDB', () => {
it('Connect to MongoDB', () => mongoose.connect('mongodb://localhost:27017/test', {
useUnifiedTopology: true,
useNewUrlParser: true,
}));
it('Drop test database', () => mongoose.connection.db.dropDatabase());
it('Should connect MongoDB without mongoose', () => MongoClient.connect('mongodb://localhost:27017', {
useUnifiedTopology: true,
useNewUrlParser: true,
})
.then((client) => db = client.db('test')));
});
describe('Build Resources for following tests', () => {
it('Build the /test/ref endpoints', () => {
// Create the schema.
const RefSchema = new mongoose.Schema({
data: String,
}, { collection: 'ref' });
// Create the model.
const RefModel = mongoose.model('ref', RefSchema);
// Create the REST resource and continue.
const test = Resource(app, '/test', 'ref', RefModel).rest();
const testSwaggerio = require('./snippets/testSwaggerio.json');
const swaggerio = test.swagger();
assert.equal(Object.values(swaggerio).length,2);
assert.ok(swaggerio.definitions);
assert.ok(swaggerio.definitions.ref);
assert.equal(swaggerio.definitions.ref.title, 'ref');
assert.equal(Object.values(swaggerio.paths).length, 2);
assert.deepEqual(swaggerio, testSwaggerio);
});
it('Build the /test/resource1 endpoints', () => {
// Create the schema.
const R1SubdocumentSchema = new mongoose.Schema({
label: {
type: String,
},
data: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'ref',
}],
}, { _id: false });
const Resource1Schema = new mongoose.Schema({
title: {
type: String,
required: true,
},
name: {
type: String,
},
age: {
type: Number,
},
description: {
type: String,
},
list: [R1SubdocumentSchema],
list2: [String],
});
// Create the model.
const Resource1Model = mongoose.model('resource1', Resource1Schema);
// Create the REST resource and continue.
const resource1 = Resource(app, '/test', 'resource1', Resource1Model).rest({
afterDelete(req, res, next) {
// Check that the delete item is still being returned via resourcejs.
assert.notEqual(res.resource.item, {});
assert.notEqual(res.resource.item, []);
assert.equal(res.resource.status, 204);
assert.equal(res.statusCode, 200);
next();
},
});
const resource1Swaggerio = require('./snippets/resource1Swaggerio.json');
const swaggerio = resource1.swagger();
assert.equal(Object.values(swaggerio).length,2);
assert.ok(swaggerio.definitions);
assert.ok(swaggerio.definitions.resource1);
assert.equal(swaggerio.definitions.resource1.title, 'resource1');
assert.equal(Object.values(swaggerio.paths).length, 2);
assert.deepEqual(swaggerio, resource1Swaggerio);
});
it('Build the /test/resource2 endpoints', () => {
// Create the schema.
const Resource2Schema = new mongoose.Schema({
title: {
type: String,
required: true,
},
age: {
type: Number,
},
married: {
type: Boolean,
default: false,
},
updated: {
type: Number,
default: null,
},
description: {
type: String,
},
});
// Create the model.
const Resource2Model = mongoose.model('resource2', Resource2Schema);
// Create the REST resource and continue.
const resource2 = Resource(app, '/test', 'resource2', Resource2Model).rest({
// Register before/after global handlers.
before(req, res, next) {
// Store the invoked handler and continue.
setInvoked('resource2', 'before', req);
next();
},
beforePost(req, res, next) {
// Store the invoked handler and continue.
setInvoked('resource2', 'beforePost', req);
next();
},
after(req, res, next) {
// Store the invoked handler and continue.
setInvoked('resource2', 'after', req);
next();
},
afterPost(req, res, next) {
// Store the invoked handler and continue.
setInvoked('resource2', 'afterPost', req);
next();
},
});
const resource2Swaggerio = require('./snippets/resource2Swaggerio.json');
const swaggerio = resource2.swagger();
assert.equal(Object.values(swaggerio).length,2);
assert.ok(swaggerio.definitions);
assert.ok(swaggerio.definitions.resource2);
assert.equal(swaggerio.definitions.resource2.title, 'resource2');
assert.equal(Object.values(swaggerio.paths).length, 2);
assert.deepEqual(swaggerio, resource2Swaggerio);
});
it('Build the /test/date endpoints and fill it with data', () => {
const Schema = new mongoose.Schema({
date: {
type: Date,
},
});
// Create the model.
const Model = mongoose.model('date', Schema);
const date = Resource(app, '/test', 'date', Model).rest();
const resource3Swaggerio = require('./snippets/dateSwaggerio.json');
const swaggerio = date.swagger();
assert.equal(Object.values(swaggerio).length,2);
assert.ok(swaggerio.definitions);
assert.ok(swaggerio.definitions.date);
assert.equal(swaggerio.definitions.date.title, 'date');
assert.equal(Object.values(swaggerio.paths).length, 2);
assert.deepEqual(swaggerio, resource3Swaggerio);
return Promise.all(testDates.map((date) => request(app)
.post('/test/date')
.send({
date: date.toDate(),
})));
});
it('Build the /test/resource1/:resource1Id/nested1 endpoints', () => {
// Create the schema.
const Nested1Schema = new mongoose.Schema({
resource1: {
type: mongoose.Schema.Types.ObjectId,
ref: 'resource1',
index: true,
required: true,
},
title: {
type: String,
required: true,
},
age: {
type: Number,
},
description: {
type: String,
},
});
// Create the model.
const Nested1Model = mongoose.model('nested1', Nested1Schema);
// Create the REST resource and continue.
const nested1 = Resource(app, '/test/resource1/:resource1Id', 'nested1', Nested1Model).rest({
// Register before global handlers to set the resource1 variable.
before(req, res, next) {
req.body.resource1 = req.params.resource1Id;
next();
},
});
const nested1Swaggerio = require('./snippets/nested1Swaggerio.json');
const swaggerio = nested1.swagger();
assert.equal(Object.values(swaggerio).length,2);
assert.ok(swaggerio.definitions);
assert.ok(swaggerio.definitions.nested1);
assert.equal(swaggerio.definitions.nested1.title, 'nested1');
assert.equal(Object.values(swaggerio.paths).length, 2);
assert.deepEqual(swaggerio, nested1Swaggerio);
});
it('Build the /test/resource2/:resource2Id/nested2 endpoints', () => {
// Create the schema.
const Nested2Schema = new mongoose.Schema({
resource2: {
type: mongoose.Schema.Types.ObjectId,
ref: 'resource2',
index: true,
required: true,
},
title: {
type: String,
required: true,
},
age: {
type: Number,
},
description: {
type: String,
},
});
// Create the model.
const Nested2Model = mongoose.model('nested2', Nested2Schema);
// Create the REST resource and continue.
const nested2 = Resource(app, '/test/resource2/:resource2Id', 'nested2', Nested2Model).rest({
// Register before/after global handlers.
before(req, res, next) {
req.body.resource2 = req.params.resource2Id;
req.modelQuery = this.model.where('resource2', req.params.resource2Id);
// Store the invoked handler and continue.
setInvoked('nested2', 'before', req);
next();
},
after(req, res, next) {
// Store the invoked handler and continue.
setInvoked('nested2', 'after', req);
next();
},
});
const nested2Swaggerio = require('./snippets/nested2Swaggerio.json');
const swaggerio = nested2.swagger();
assert.equal(Object.values(swaggerio).length,2);
assert.ok(swaggerio.definitions);
assert.ok(swaggerio.definitions.nested2);
assert.equal(swaggerio.definitions.nested2.title, 'nested2');
assert.equal(Object.values(swaggerio.paths).length, 2);
assert.deepEqual(swaggerio, nested2Swaggerio);
});
it('Build the /test/resource3 endpoints', () => {
// Create the schema.
const Resource3Schema = new mongoose.Schema({
title: String,
writeOption: String,
});
Resource3Schema.pre('save', function(next, options) {
if (options && options.writeSetting) {
return next();
}
next(new Error('Save options not passed to middleware'));
});
Resource3Schema.pre('remove', function(next, options) {
if (options && options.writeSetting) {
return next();
}
return next(new Error('DeleteOptions not passed to middleware'));
});
// Create the model.
const Resource3Model = mongoose.model('resource3', Resource3Schema);
// Create the REST resource and continue.
const resource3 = Resource(app, '/test', 'resource3', Resource3Model).rest({
before(req, res, next) {
// This setting should be passed down to the underlying `save()` command
req.writeOptions = { writeSetting: true };
next();
},
});
const resource3Swaggerio = require('./snippets/resource3Swaggerio.json');
const swaggerio = resource3.swagger();
assert.equal(Object.values(swaggerio).length,2);
assert.ok(swaggerio.definitions);
assert.ok(swaggerio.definitions.resource3);
assert.equal(swaggerio.definitions.resource3.title, 'resource3');
assert.equal(Object.values(swaggerio.paths).length, 2);
assert.deepEqual(swaggerio, resource3Swaggerio);
});
it('Build the /test/resource4 endpoints', async () => {
// Create the schema.
const Resource4Schema = new mongoose.Schema({
title: String,
writeOption: String,
});
// Create the model.
const Resource4Model = mongoose.model('resource4', Resource4Schema);
const doc = new Resource4Model({ title: 'Foo' });
await doc.save();
// Create the REST resource and continue.
const resource4 = Resource(app, '/test', 'resource4', Resource4Model)
.rest({
beforePatch(req, res, next) {
req.modelQuery = {
findOne: function findOne() {
return new Promise((resolve, reject) => {
reject(new Error('failed'));
});
},
};
next();
},
})
.virtual({
path: 'undefined_query',
before: function(req, res, next) {
req.modelQuery = undefined;
return next();
},
})
.virtual({
path: 'defined',
before: function(req, res, next) {
req.modelQuery = Resource4Model.aggregate([
{ $group: { _id: null, titles: { $sum: '$title' } } },
]);
return next();
},
})
.virtual({
path: 'error',
before: function(req, res, next) {
req.modelQuery = {
exec: function exec() {
return new Promise((resolve, reject) => {
reject(new Error('Failed'));
});
}
};
return next();
},
})
.virtual({
path: 'empty',
before: function(req, res, next) {
req.modelQuery = {
exec: function exec() {
return new Promise((resolve, reject) => {
resolve(undefined);
});
},
};
return next();
},
});
const resource4Swaggerio = require('./snippets/resource4Swaggerio.json');
const swaggerio = resource4.swagger();
assert.equal(Object.values(swaggerio).length,2);
assert.ok(swaggerio.definitions);
assert.ok(swaggerio.definitions.resource4);
assert.equal(swaggerio.definitions.resource4.title, 'resource4');
assert.equal(Object.values(swaggerio.paths).length, 6);
assert.deepEqual(swaggerio, resource4Swaggerio);
});
it('Build the /test/skip endpoints', () => {
// Create the schema.
const SkipSchema = new mongoose.Schema({
title: String,
});
// Create the model.
const SkipModel = mongoose.model('skip', SkipSchema);
// Create the REST resource and continue.
const skipResource = Resource(app, '/test', 'skip', SkipModel)
.rest({
before(req, res, next) {
req.skipResource = true;
next();
},
})
.virtual({
path: 'resource',
before: function(req, res, next) {
req.skipResource = true;
return next();
},
});
const skipSwaggerio = require('./snippets/skipSwaggerio.json');
const swaggerio = skipResource.swagger();
assert.equal(Object.values(swaggerio).length,2);
assert.ok(swaggerio.definitions);
assert.ok(swaggerio.definitions.skip);
assert.equal(swaggerio.definitions.skip.title, 'skip');
assert.equal(Object.values(swaggerio.paths).length, 3);
assert.deepEqual(swaggerio, skipSwaggerio);
});
});
describe('Test skipResource', () => {
const resource = {};
it('/GET empty list', () => request(app)
.get('/test/skip')
.expect('Content-Type', /text\/html/)
.expect(404)
.then((res) => {
const response = res.text;
const expected = 'Cannot GET /test/skip';
assert(response.includes(expected), 'Response not found.');
}));
it('/POST Create new resource', () => request(app)
.post('/test/skip')
.send({
title: 'Test1',
description: '12345678',
})
.expect('Content-Type', /text\/html/)
.expect(404)
.then((res) => {
const response = res.text;
const expected = 'Cannot POST /test/skip';
assert(response.includes(expected), 'Response not found.');
}));
it('/GET The new resource', () => request(app)
.get(`/test/skip/${resource._id}`)
.expect('Content-Type', /text\/html/)
.expect(404)
.then((res) => {
const response = res.text;
const expected = `Cannot GET /test/skip/${resource._id}`;
assert(response.includes(expected), 'Response not found.');
}));
it('/PUT Change data on the resource', () => request(app)
.put(`/test/skip/${resource._id}`)
.send({
title: 'Test2',
})
.expect('Content-Type', /text\/html/)
.expect(404)
.then((res) => {
const response = res.text;
const expected = `Cannot PUT /test/skip/${resource._id}`;
assert(response.includes(expected), 'Response not found.');
}));
it('/PATCH Change data on the resource', () => request(app)
.patch(`/test/skip/${resource._id}`)
.expect('Content-Type', /text\/html/)
.expect(404)
.then((res) => {
const response = res.text;
const expected = `Cannot PATCH /test/skip/${resource._id}`;
assert(response.includes(expected), 'Response not found.');
}));
it('/DELETE the resource', () => request(app)
.delete(`/test/skip/${resource._id}`)
.expect('Content-Type', /text\/html/)
.expect(404)
.then((res) => {
const response = res.text;
const expected = `Cannot DELETE /test/skip/${resource._id}`;
assert(response.includes(expected), 'Response not found.');
}));
it('/VIRTUAL the resource', () => request(app)
.get('/test/skip/virtual/resource')
.expect('Content-Type', /text\/html/)
.expect(404)
.then((res) => {
const response = res.text;
const expected = 'Cannot GET /test/skip/virtual/resource';
assert(response.includes(expected), 'Response not found.');
}));
});
describe('Test Virtual resource and Patch errors', () => {
it('/VIRTUAL undefined resource query', () => request(app)
.get('/test/resource4/virtual/undefined_query')
.expect('Content-Type', /json/)
.expect(404)
.then((res) => {
assert.equal(res.body.errors[0], 'Resource not found');
}));
it('/VIRTUAL resource query', () => request(app)
.get('/test/resource4/virtual/defined')
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
const response = res.body;
assert.equal(response[0]._id, null);
assert.equal(response[0].titles, 0);
}));
it('/VIRTUAL errorous resource query', () => request(app)
.get('/test/resource4/virtual/error')
.expect('Content-Type', /json/)
.expect(400)
.then((res) => {
const response = res.body;
assert.equal(response.message, 'Failed');
}));
it('/VIRTUAL empty resource response', () => request(app)
.get('/test/resource4/virtual/empty')
.expect('Content-Type', /json/)
.expect(404)
.then((res) => {
const response = res.body;
assert.equal(response.errors[0], 'Resource not found');
}));
it('/PATCH with errorous modelquery', () => request(app)
.patch('/test/resource4/1234')
.expect('Content-Type', /json/)
.expect(400)
.then((res) => {
const response = res.body;
assert.equal(response.message, 'failed');
}));
});
describe('Test single resource CRUD capabilities', () => {
let resource = {};
it('/GET empty list', () => request(app)
.get('/test/resource1')
.expect('Content-Type', /json/)
.expect('Content-Range', '*/0')
.expect(200)
.then((res) => {
assert.deepEqual(res.body, []);
}));
it('/POST Create new resource', () => request(app)
.post('/test/resource1')
.send({
title: 'Test1',
description: '12345678',
})
.expect('Content-Type', /json/)
.expect(201)
.then((res) => {
resource = res.body;
assert.equal(resource.title, 'Test1');
assert.equal(resource.description, '12345678');
assert(resource.hasOwnProperty('_id'), 'Resource ID not found');
}));
it('/GET The new resource', () => request(app)
.get(`/test/resource1/${resource._id}`)
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
assert.equal(res.body.title, resource.title);
assert.equal(res.body.description, resource.description);
assert.equal(res.body._id, resource._id);
}));
it('/PUT Change data on the resource', () => request(app)
.put(`/test/resource1/${resource._id}`)
.send({
title: 'Test2',
})
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
assert.equal(res.body.title, 'Test2');
assert.equal(res.body.description, resource.description);
assert.equal(res.body._id, resource._id);
resource = res.body;
}));
it('/PATCH Change data on the resource', () => request(app)
.patch(`/test/resource1/${resource._id}`)
.send([{ 'op': 'replace', 'path': '/title', 'value': 'Test3' }])
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
assert.equal(res.body.title, 'Test3');
resource = res.body;
}));
it('/PATCH Reject update due to failed test op', () => request(app)
.patch(`/test/resource1/${resource._id}`)
.send([
{ 'op': 'test', 'path': '/title', 'value': 'not-the-title' },
{ 'op': 'replace', 'path': '/title', 'value': 'Test4' },
])
.expect('Content-Type', /json/)
.expect(412)
.then((res) => {
assert.equal(res.body.title, 'Test3');
resource = res.body;
}));
it('/PATCH Reject update due to incorrect patch operation', () => request(app)
.patch(`/test/resource1/${resource._id}`)
.send([{ 'op': 'does-not-exist', 'path': '/title', 'value': 'Test4' }])
.expect('Content-Type', /json/)
.expect(400)
.then((res) => {
assert.equal(res.body.errors[0].name, 'OPERATION_OP_INVALID');
}));
it('/PATCH Should not care whether patch is array or not', () => request(app)
.patch(`/test/resource1/${resource._id}`)
.send({ 'op': 'test', 'path': '/title', 'value': 'Test3' })
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
assert.equal(res.body.title, 'Test3');
}));
it('/PATCH Reject update due to incorrect patch object', () => request(app)
.patch(`/test/resource1/${resource._id}`)
.send(['invalid-patch'])
.expect('Content-Type', /json/)
.expect(400)
.then((res) => {
assert.equal(res.body.errors[0].name, 'OPERATION_NOT_AN_OBJECT');
}));
it('/PATCH Reject update due to incorrect patch value', () => request(app)
.patch(`/test/resource1/${resource._id}`)
.send([{ 'op': 'replace', 'path': '/title', 'value': undefined }])
.expect('Content-Type', /json/)
.expect(400)
.then((res) => {
assert.equal(res.body.errors[0].name, 'OPERATION_VALUE_REQUIRED');
}));
it('/PATCH Reject update due to incorrect patch add path', () => request(app)
.patch(`/test/resource1/${resource._id}`)
.send([{ 'op': 'add', 'path': '/path/does/not/exist', 'value': 'Test4' }])
.expect('Content-Type', /json/)
.expect(400)
.then((res) => {
assert.equal(res.body.errors[0].name, 'OPERATION_PATH_CANNOT_ADD');
}));
it('/PATCH Reject update due to incorrect patch path', () => request(app)
.patch(`/test/resource1/${resource._id}`)
.send([{ 'op': 'replace', 'path': '/path/does/not/exist', 'value': 'Test4' }])
.expect('Content-Type', /json/)
.expect(400)
.then((res) => {
assert.equal(res.body.errors[0].name, 'OPERATION_PATH_UNRESOLVABLE');
}));
it('/PATCH Reject update due to incorrect patch path', () => request(app)
.patch(`/test/resource1/${resource._id}`)
.send([{ 'op': 'replace', 'path': 1, 'value': 'Test4' }])
.expect('Content-Type', /json/)
.expect(400)
.then((res) => {
assert.equal(res.body.errors[0].name, 'OPERATION_PATH_INVALID');
}));
it('/PATCH Reject update due to incorrect patch path', () => request(app)
.patch(`/test/resource1/${resource._id}`)
.send([{ 'op': 'add', 'path': '/path/does/not/exist', 'value': 'Test4' }])
.expect('Content-Type', /json/)
.expect(400)
.then((res) => {
assert.equal(res.body.errors[0].name, 'OPERATION_PATH_CANNOT_ADD');
}));
it('/PATCH Reject update due to incorrect patch path', () => request(app)
.patch(`/test/resource1/${resource._id}`)
.send([{ 'op': 'move', 'from': '/path/does/not/exist', 'path': '/path/does/not/exist', 'value': 'Test4' }])
.expect('Content-Type', /json/)
.expect(400)
.then((res) => {
assert.equal(res.body.errors[0].name, 'OPERATION_FROM_UNRESOLVABLE');
}));
it('/PATCH Reject update due to incorrect patch array', () => request(app)
.patch(`/test/resource1/${resource._id}`)
.send([{ 'op': 'add', 'path': '/list/invalidindex', 'value': '2' }])
.expect('Content-Type', /json/)
.expect(400)
.then((res) => {
assert.equal(res.body.errors[0].name, 'OPERATION_PATH_ILLEGAL_ARRAY_INDEX');
}));
it('/PATCH Reject update due to incorrect patch array', () => request(app)
.patch(`/test/resource1/${resource._id}`)
.send([{ 'op': 'add', 'path': '/list/9999', 'value': '2' }])
.expect('Content-Type', /json/)
.expect(400)
.then((res) => {
assert.equal(res.body.errors[0].name, 'OPERATION_VALUE_OUT_OF_BOUNDS');
}));
it('/GET The changed resource', () => request(app)
.get(`/test/resource1/${resource._id}`)
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
assert.equal(res.body.title, resource.title);
assert.equal(res.body.description, resource.description);
assert.equal(res.body._id, resource._id);
}));
it('/GET index of resources', () => request(app)
.get('/test/resource1')
.expect('Content-Type', /json/)
.expect('Content-Range', '0-0/1')
.expect(200)
.then((res) => {
assert.equal(res.body.length, 1);
assert.equal(res.body[0].title, 'Test3');
assert.equal(res.body[0].description, resource.description);
assert.equal(res.body[0]._id, resource._id);
}));
it('Cannot /POST to an existing resource', () => request(app)
.post(`/test/resource1/${resource._id}`)
.expect('Content-Type', /text\/html/)
.expect(404)
.then((res) => {
const response = res.text;
const expected = `Cannot POST /test/resource1/${resource._id}`;
assert(response.includes(expected), 'Response not found.');
}));
it('/DELETE the resource', () => request(app)
.delete(`/test/resource1/${resource._id}`)
.expect(200)
.then((res) => {
assert.deepEqual(res.body, {});
}));
it('/GET empty list', () => request(app)
.get('/test/resource1')
.expect('Content-Type', /json/)
.expect('Content-Range', '*/0')
.expect(200)
.then((res) => {
assert.deepEqual(res.body, []);
}));
describe('Test single resource subdocument updates', () => {
// Ensure that resource reference is empty.
resource = {};
let doc1 = null;
let doc2 = null;
describe('Bootstrap', () => {
it('Should create a reference doc with mongoose', () => {
const doc = { data: 'test1' };
return request(app)
.post('/test/ref')
.send(doc)
.expect('Content-Type', /json/)
.expect(201)
.then((res) => {
const response = _.omit(res.body, '__v');
assert.equal(response.data, doc.data);
doc1 = response;
});
});
it('Should be able to create a reference doc directly with mongo', async () => {
const doc = { data: 'test2' };
const compare = _.clone(doc);
const ref = db.collection('ref');
const inserted = await ref.insertOne(doc);
const response = await ref.findOne(inserted.insertedId);
assert.deepEqual(_.omit(response, '_id'), compare);
response._id = response._id.toString();
doc2 = response;
});
it('Should be able to directly create a resource with subdocuments using mongo', async () => {
// Set the resource collection for direct mongo queries.
const resource1 = db.collection('resource1');
const tmp = {
title: 'Test2',
description: '987654321',
list: [
{ label: 'one', data: [doc1._id] },
],
};
const compare = _.clone(tmp);
const inserted = await resource1.insertOne(tmp);
resource = await resource1.findOne({_id: inserted.insertedId});
assert.deepEqual(_.omit(resource, '_id'), compare);
});
});
describe('Subdocument Tests', () => {
it('/PUT to a resource with subdocuments should not mangle the subdocuments', () => {
const two = { label: 'two', data: [doc2._id] };
return request(app)
.put(`/test/resource1/${resource._id}`)
.send({ list: resource.list.concat(two) })
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
const response = res.body;
assert.equal(response.title, resource.title);
assert.equal(response.description, resource.description);
assert.equal(response._id, resource._id);
assert.deepEqual(response.list, resource.list.concat(two));
resource = response;
});
});
it('Manual DB updates to a resource with subdocuments should not mangle the subdocuments', async () => {
const updates = [
{ label: '1', data: [doc1._id] },
{ label: '2', data: [doc2._id] },
{ label: '3', data: [doc1._id, doc2._id] },
];
const resource1 = db.collection('resource1');
await resource1.updateOne(
{ _id: new ObjectId(resource._id) },
{ $set: { list: updates } })
resource1.findOne({_id: new ObjectId(resource._id)})
.then (response => {
assert.equal(response.title, resource.title);
assert.equal(response.description, resource.description);
assert.equal(response._id, resource._id);
assert.deepEqual(response.list, updates);
resource = response;
});
});
it('/PUT to a resource subdocument should not mangle the subdocuments', () => {
// Update a subdocument property.
const update = _.clone(resource.list);
return request(app)
.put(`/test/resource1/${resource._id}`)
.send({ list: update })
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
const response = res.body;
assert.equal(response.title, resource.title);
assert.equal(response.description, resource.description);
assert.equal(response._id, resource._id);
assert.deepEqual(response.list, update);
resource = response;
});
});
it('/PUT to a top-level property should not mangle the other collection properties', () => {
const tempTitle = 'an update without docs';
return request(app)
.put(`/test/resource1/${resource._id}`)
.send({ title: tempTitle })
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
const response = res.body;
assert.equal(response.title, tempTitle);
assert.equal(response.description, resource.description);
assert.equal(response._id, resource._id);
assert.deepEqual(response.list, resource.list);
resource = response;
});
});
});
// Remove the test resource.
describe('Subdocument cleanup', () => {
it('Should remove the test resource', () => {
const resource1 = db.collection('resource1');
resource1.deleteOne({ _id: new ObjectId(resource._id) });
});
it('Should remove the test ref resources', () => {
const ref = db.collection('ref');
ref.deleteOne({ _id: new ObjectId(doc1._id) });
ref.deleteOne({ _id: new ObjectId(doc2._id) });
});
});
});
});
let refDoc1Content = null;
let refDoc1Response = null;
const resourceNames = [];
// eslint-disable-next-line max-statements
function testSearch(testPath) {
it('Should populate', () => request(app)
.get(`${testPath}?name=noage&populate=list.data`)
.then((res) => {
const response = res.body;
// Check statusCode
assert.equal(res.statusCode, 200);
// Check main resource
assert.equal(response[0].title, 'No Age');
assert.equal(response[0].description, 'No age');
assert.equal(response[0].name, 'noage');
assert.equal(response[0].list.length, 1);
// Check populated resource
assert.equal(response[0].list[0].label, '1');
assert.equal(response[0].list[0].data.length, 1);
assert.equal(response[0].list[0].data[0]._id, refDoc1Response._id);
assert.equal(response[0].list[0].data[0].data, refDoc1Content.data);
}));
it('Should ignore empty populate query parameter', () => request(app)
.get(`${testPath}?name=noage&populate=`)
.then((res) => {
const response = res.body;
// Check statusCode
assert.equal(res.statusCode, 200);
// Check main resource
assert.equal(response[0].title, 'No Age');
assert.equal(response[0].description, 'No age');
assert.equal(response[0].name, 'noage');
assert.equal(response[0].list.length, 1);
// Check populated resource
assert.equal(response[0].list[0].label, '1');
assert.equal(response[0].list[0].data.length, 1);
assert.equal(response[0].list[0].data[0], refDoc1Response._id);
}));
it('Should not populate paths that are not a reference', () => request(app)
.get(`${testPath}?name=noage&populate=list2`)
.then((res) => {
const response = res.body;
// Check statusCode
assert.equal(res.statusCode, 200);
// Check main resource
assert.equal(response[0].title, 'No Age');
assert.equal(response[0].description, 'No age');
assert.equal(response[0].name, 'noage');
assert.equal(response[0].list.length, 1);
// Check populated resource
assert.equal(response[0].list[0].label, '1');
assert.equal(response[0].list[0].data.length, 1);
assert.equal(response[0].list[0].data[0], refDoc1Response._id);
}));
it('Should populate with options', () => request(app)
.get(`${testPath}?name=noage&populate[path]=list.data`)
.then((res) => {
const response = res.body;
// Check statusCode
assert.equal(res.statusCode, 200);
// Check main resource
assert.equal(response[0].title, 'No Age');
assert.equal(response[0].description, 'No age');
assert.equal(response[0].name, 'noage');
assert.equal(response[0].list.length, 1);
// Check populated resource
assert.equal(response[0].list[0].label, '1');
assert.equal(response[0].list[0].data.length, 1);
assert.equal(response[0].list[0].data[0]._id, refDoc1Response._id);
assert.equal(response[0].list[0].data[0].data, refDoc1Content.data);
}));
it('Should limit 10', () => request(app)
.get(testPath)
.expect('Content-Type', /json/)
.expect(206)
.then((res) => {
const response = res.body;
assert.equal(response.length, 10);
let age = 0;
response.forEach((resource) => {
assert.equal(resource.title, `Test Age ${age}`);
assert.equal(resource.description, `Description of test age ${age}`);
assert.equal(resource.age, age);
age++;
});
}));
it('Should accept a change in limit', () => request(app)
.get(`${testPath}?limit=5`)
.expect('Content-Type', /json/)
.expect('Content-Range', '0-4/26')
.expect(206)
.then((res) => {
const response = res.body;
assert.equal(response.length, 5);
let age = 0;
response.forEach((resource) => {
assert.equal(resource.title, `Test Age ${age}`);
assert.equal(resource.description, `Description of test age ${age}`);
assert.equal(resource.age, age);
age++;
});
}));
it('Should be able to skip and limit', () => request(app)
.get(`${testPath}?limit=5&skip=4`)
.expect('Content-Type', /json/)
.expect('Content-Range', '4-8/26')
.expect(206)
.then((res) => {
const response = res.body;
assert.equal(response.length, 5);
let age = 4;
response.forEach((resource) => {
assert.equal(resource.title, `Test Age ${age}`);
assert.equal(resource.description, `Description of test age ${age}`);
assert.equal(resource.age, age);
age++;
});
}));
it('Should default negative limit to 10', () => request(app)
.get(`${testPath}?limit=-5&skip=4`)
.expect('Content-Type', /json/)
.expect('Content-Range', '4-13/26')
.expect(206)
.then((res) => {
const response = res.body;
assert.equal(response.length, 10);
let age = 4;
response.forEach((resource) => {
assert.equal(resource.title, `Test Age ${age}`);
assert.equal(resource.description, `Description of test age ${age}`);
assert.equal(resource.age, age);
age++;
});
}));
it('Should default negative skip to 0', () => request(app)
.get(`${testPath}?limit=5&skip=-4`)
.expect('Content-Type', /json/)
.expect('Content-Range', '0-4/26')
.expect(206)
.then((res) => {
const response = res.body;
assert.equal(response.length, 5);
let age = 0;
response.forEach((resource) => {
assert.equal(resource.title, `Test Age ${age}`);
assert.equal(resource.description, `Description of test age ${age}`);
assert.equal(resource.age, age);
age++;
});
}));
it('Should default negative skip and negative limit to 0 and 10', () => request(app)
.get(`${testPath}?limit=-5&skip=-4`)
.expect('Content-Type', /json/)
.expect('Content-Range', '0-9/26')
.expect(206)
.then((res) => {
const response = res.body;
assert.equal(response.length, 10);
let age = 0;
response.forEach((resource) => {
assert.equal(resource.title, `Test Age ${age}`);
assert.equal(resource.description, `Description of test age ${age}`);
assert.equal(resource.age, age);
age++;
});
}));
it('Should default non numeric limit to 10', () => request(app)
.get(`${testPath}?limit=badlimit&skip=4`)
.expect('Content-Type', /json/)
.expect('Content-Range', '4-13/26')
.expect(206)
.then((res) => {
const response = res.body;
assert.equal(response.length, 10);
let age = 4;
response.forEach((resource) => {
assert.equal(resource.title, `Test Age ${age}`);
assert.equal(resource.description, `Description of test age ${age}`);
assert.equal(resource.age, age);
age++;
});
}));
it('Should default non numeric skip to 0', () => request(app)
.get(`${testPath}?limit=5&skip=badskip`)
.expect('Content-Type', /json/)
.expect('Content-Range', '0-4/26')
.expect(206)
.then((res) => {
const response = res.body;
assert.equal(response.length, 5);
let age = 0;
response.forEach((resource) => {
assert.equal(resource.title, `Test Age ${age}`);
assert.equal(resource.description, `Description of test age ${age}`);
assert.equal(resource.age, age);
age++;
});
}));
it('Should be able to select fields', () => request(app)
.get(`${testPath}?limit=10&skip=10&select=title,age`)
.expect('Content-Type', /json/)
.expect('Content-Range', '10-19/26')
.expect(206)
.then((res) => {
const response = res.body;
assert.equal(response.length, 10);
let age = 10;
response.forEach((resource) => {
assert.equal(resource.title, `Test Age ${age}`);
assert.equal(resource.description, undefined);
assert.equal(resource.age, age);
age++;
});
}));
it('Should be able to select fields with multiple select queries', () => request(app)
.get(`${testPath}?limit=10&skip=10&select=title&select=age`)
.expect('Content-Type', /json/)
.expect('Content-Range', '10-19/26')
.expect(206)
.then((res) => {
const response = res.body;
assert.equal(response.length, 10);
let age = 10;
response.forEach((resource) => {
assert.equal(resource.title, `Test Age ${age}`);
assert.equal(resource.description, undefined);
assert.equal(resource.age, age);
age++;
});
}));
it('Should be able to sort', () => request(app)
.get(`${testPath}?select=age&sort=-age`)
.expect('Content-Type', /json/)
.expect('Content-Range', '0-9/26')
.expect(206)
.then((res) => {
const response = res.body;
assert.equal(response.length, 10);
let age = 24;
response.forEach((resource) => {
assert.equal(resource.title, undefined);
assert.equal(resource.description, undefined);
assert.equal(resource.age, age);
age--;
});
}));
it('Should paginate with a sort', () => request(app)
.get(`${testPath}?limit=5&skip=5&select=age&sort=-age`)
.expect('Content-Type', /json/)
.expect('Content-Range', '5-9/26')
.expect(206)
.then((res) => {
const response = res.body;
assert.equal(response.length, 5);
let age = 19;
response.forEach((resource) => {
assert.equal(resource.title, undefined);
assert.equal(resource.description, undefined);
assert.equal(resource.age, age);
age--;
});
}));
it('Should be able to find', () => request(app)
.get(`${testPath}?limit=5&select=age&age=5`)
.expect('Content-Type', /json/)
.expect('Content-Range', '0-0/1')
.expect(200)
.then((res) => {
const response = res.body;
assert.equal(response.length, 1);
assert.equal(response[0].title, undefined);
assert.equal(response[0].description, undefined);
assert.equal(response[0].age, 5);
}));
it('eq search selector', () => request(app)
.get(`${testPath}?age__eq=5`)
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
const response = res.body;
assert.equal(response.length, 1);
response.forEach((resource) => {
assert.equal(resource.age, 5);
});
}));
it('equals (alternative) search selector', () => request(app)
.get(`${testPath}?age=5`)
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
const response = res.body;
assert.equal(response.length, 1);
response.forEach((resource) => {
assert.equal(resource.age, 5);
});
}));
it('ne search selector', () => request(app)
.get(`${testPath}?age__ne=5&limit=100`)
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
const response = res.body;
assert.equal(response.length, 25);
response.forEach((resource) => {
assert.notEqual(resource.age, 5);
});
}));
it('in search selector', () => request(app)
.get(`${testPath}?title__in=Test Age 1,Test Age 5,Test Age 9,Test Age 20`)
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
const response = res.body;
assert.equal(response.length, 4);
response.forEach((resource) => {
let found = false;
[1, 5, 9, 20].forEach((a) => {
if (resource.age && resource.age === a) {
found = true;
}
});
assert(found);
});
}));
it('nin search selector', () => request(app)
.get(`${testPath}?title__nin=Test Age 1,Test Age 5`)
.expect('Content-Type', /json/)
.expect(206)
.then((res) => {
const response = res.body;
assert.equal(response.length, 10);
response.forEach((resource) => {
let found = false;
[1, 5].forEach((a) => {
if (resource.age && resource.age === a) {
found = true;
}
});
assert(!found);
});
}));
it('exists=false search selector', () => request(app)
.get(`${testPath}?age__exists=false`)
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
const response = res.body;
assert.equal(response.length, 1);
assert.equal(response[0].name, 'noage');
}));
it('exists=0 search selector', () => request(app)
.get(`${testPath}?age__exists=0`)
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
const response = res.body;
assert.equal(response.length, 1);
assert.equal(response[0].name, 'noage');
}));
it('exists=true search selector', () => request(app)
.get(`${testPath}?age__exists=true&limit=1000`)
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
const response = res.body;
assert.equal(response.length, 25);
response.forEach((resource) => {
assert(resource.name !== 'noage', 'No age should be found.');
});
}));
it('exists=1 search selector', () => request(app)
.get(`${testPath}?age__exists=true&limit=1000`)
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
const response = res.body;
assert.equal(response.length, 25);
response.forEach((resource) => {
assert(resource.name !== 'noage', 'No age should be found.');
});
}));
it('lt search selector', () => request(app)
.get(`${testPath}?age__lt=5`)
.expect('Content-Range', '0-4/5')
.then((res) => {
const response = res.body;
assert.equal(response.length, 5);
response.forEach((resource) => {
assert.ok(resource.age < 5);
});
}));
it('lte search selector', () => request(app)
.get(`${testPath}?age__lte=5`)
.expect('Content-Range', '0-5/6')
.then((res) => {
const response = res.body;
assert.equal(response.length, 6);
response.forEach((resource) => {
assert.ok(resource.age <= 5);
});
}));
it('gt search selector', () => request(app)
.get(`${testPath}?age__gt=5`)
.expect('Content-Range', '0-9/19')
.then((res) => {
const response = res.body;
assert.equal(response.length, 10);
response.forEach((resource) => {
assert.ok(resource.age > 5);
});
}));
it('gte search selector', () => request(app)
.get(`${testPath}?age__gte=5`)
.expect('Content-Range', '0-9/20')
.then((res) => {
const response = res.body;
assert.equal(response.length, 10);
response.forEach((resource) => {
assert.ok(resource.age >= 5);
});
}));
it('regex search selector', () => request(app)
.get(`${testPath}?title__regex=/.*Age [0-1]?[0-3]$/g`)
.expect('Content-Range', '0-7/8')
.then((res) => {
const response = res.body;
const valid = [0, 1, 2, 3, 10, 11, 12, 13];
assert.equal(response.length, valid.length);
response.forEach((resource) => {
assert.ok(valid.includes(resource.age));
});
}));
it('regex search selector should be case insensitive', () => {
const name = resourceNames[0].toString();
return request(app)
.get(`${testPath}?name__regex=${name.toUpperCase()}`)
.then((res) => {
const uppercaseResponse = res.body;
return request(app)
.get(`/test/resource1?name__regex=${name.toLowerCase()}`)
.then((res) => {
const lowercaseResponse = res.body;
assert.equal(uppercaseResponse.length, lowercaseResponse.length);
});
});
});
}
describe('Test single resource search capabilities', () => {
let singleResource1Id = undefined;
it('Should create a reference doc with mongoose', () => {
refDoc1Content = { data: 'test1' };
return request(app)
.post('/test/ref')
.send(refDoc1Content)
.expect('Content-Type', /json/)
.expect(201)
.then((res) => {
const response = _.omit(res.body, '__v');
assert.equal(response.data, refDoc1Content.data);
refDoc1Response = response;
});
});
it('Create a full index of resources', () => _.range(25).reduce((promise, age) => {
const name = (chance.name()).toUpperCase();
resourceNames.push(name);
return promise.then(() => request(app)
.post('/test/resource1')
.send({
title: `Test Age ${age}`,
description: `Description of test age ${age}`,
name,
age,
})
.then((res) => {
const response = res.body;
assert.equal(response.title, `Test Age ${age}`);
assert.equal(response.description, `Description of test age ${age}`);
assert.equal(response.age, age);
}));
}, Promise.resolve())
.then(() => {
const refList = [{ label: '1', data: [refDoc1Response._id] }];
// Insert a record with no age.
return request(app)
.post('/test/resource1')
.send({
title: 'No Age',
na