Test create project server code (prep for #1095)

* upgrades jest to latest version

* tests Project model with mockingoose

* extracts createProject controller to own file

* tests createProject controller with sinon-mongoose
This commit is contained in:
Andrew Nicolaou 2019-06-12 11:27:28 +02:00 committed by GitHub
parent eb82968a9f
commit d2cad7438e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 2906 additions and 1044 deletions

3608
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -60,7 +60,7 @@
"@babel/preset-react": "^7.0.0",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^9.0.0",
"babel-jest": "^23.4.2",
"babel-jest": "^24.8.0",
"babel-loader": "^8.0.0",
"babel-plugin-transform-react-remove-prop-types": "^0.2.12",
"chunk-manifest-webpack-plugin": "github:catarak/chunk-manifest-webpack-plugin",
@ -75,7 +75,7 @@
"eslint-plugin-react": "^7.12.3",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^2.0.0",
"jest": "^23.6.0",
"jest": "^24.8.0",
"node-sass": "^4.11.0",
"nodemon": "^1.18.9",
"postcss-cssnext": "^2.11.0",
@ -125,12 +125,14 @@
"friendly-words": "^1.1.3",
"htmlhint": "^0.10.1",
"is-url": "^1.2.4",
"jest-express": "^1.10.1",
"js-beautify": "^1.8.9",
"jsdom": "^9.8.3",
"jshint": "^2.10.1",
"lodash": "^4.17.11",
"loop-protect": "github:catarak/loop-protect",
"mjml": "^3.3.2",
"mockingoose": "^2.13.0",
"mongoose": "^4.6.8",
"node-uuid": "^1.4.7",
"nodemailer": "^2.6.4",
@ -167,6 +169,8 @@
"sass-extract-js": "^0.4.0",
"sass-extract-loader": "^1.1.0",
"shortid": "^2.2.14",
"sinon": "^7.3.2",
"sinon-mongoose": "^2.3.0",
"slugify": "^1.3.4",
"srcdoc-polyfill": "^0.2.0",
"url": "^0.11.0",

View File

@ -0,0 +1,155 @@
/**
* @jest-environment node
*/
import { Response } from 'jest-express';
import { createMock } from '../../models/project';
import createProject from '../project.controller/createProject';
jest.mock('../../models/project');
describe('project.controller', () => {
describe('createProject()', () => {
let ProjectMock;
beforeEach(() => {
ProjectMock = createMock();
});
afterEach(() => {
ProjectMock.restore();
});
it('fails if create fails', (done) => {
const error = new Error('An error');
ProjectMock
.expects('create')
.rejects(error);
const request = { user: {} };
const response = new Response();
const promise = createProject(request, response);
function expectations() {
expect(response.json).toHaveBeenCalledWith({ success: false });
done();
}
promise.then(expectations, expectations).catch(expectations);
});
it('extracts parameters from request body', (done) => {
const request = {
user: { _id: 'abc123' },
body: {
name: 'Wriggly worm',
files: [{ name: 'file.js', content: 'var hello = true;' }]
}
};
const response = new Response();
ProjectMock
.expects('create')
.withArgs({
user: 'abc123',
name: 'Wriggly worm',
files: [{ name: 'file.js', content: 'var hello = true;' }]
})
.resolves();
const promise = createProject(request, response);
function expectations() {
expect(response.json).toHaveBeenCalled();
done();
}
promise.then(expectations, expectations).catch(expectations);
});
// TODO: This should be extracted to a new model object
// so the controllers just have to call a single
// method for this operation
it('populates referenced user on project creation', (done) => {
const request = { user: { _id: 'abc123' } };
const response = new Response();
const result = {
_id: 'abc123',
id: 'abc123',
name: 'Project name',
serveSecure: false,
files: []
};
const resultWithUser = {
...result,
user: {}
};
ProjectMock
.expects('create')
.withArgs({ user: 'abc123' })
.resolves(result);
ProjectMock
.expects('populate')
.withArgs(result)
.yields(null, resultWithUser)
.resolves(resultWithUser);
const promise = createProject(request, response);
function expectations() {
const doc = response.json.mock.calls[0][0];
expect(response.json).toHaveBeenCalled();
expect(JSON.parse(JSON.stringify(doc))).toMatchObject(resultWithUser);
done();
}
promise.then(expectations, expectations).catch(expectations);
});
it('fails if referenced user population fails', (done) => {
const request = { user: { _id: 'abc123' } };
const response = new Response();
const result = {
_id: 'abc123',
id: 'abc123',
name: 'Project name',
serveSecure: false,
files: []
};
const error = new Error('An error');
ProjectMock
.expects('create')
.resolves(result);
ProjectMock
.expects('populate')
.yields(error)
.resolves(error);
const promise = createProject(request, response);
function expectations() {
expect(response.json).toHaveBeenCalledWith({ success: false });
done();
}
promise.then(expectations, expectations).catch(expectations);
});
});
});

View File

@ -12,31 +12,7 @@ import { resolvePathToFile } from '../utils/filePath';
import generateFileSystemSafeName from '../utils/generateFileSystemSafeName';
import { deleteObjectsFromS3, getObjectKey } from './aws.controller';
export function createProject(req, res) {
let projectValues = {
user: req.user._id
};
projectValues = Object.assign(projectValues, req.body);
Project.create(projectValues, (err, newProject) => {
if (err) {
res.json({ success: false });
return;
}
Project.populate(
newProject,
{ path: 'user', select: 'username' },
(innerErr, newProjectWithUser) => {
if (innerErr) {
res.json({ success: false });
return;
}
res.json(newProjectWithUser);
}
);
});
}
export { default as createProject } from './project.controller/createProject';
export function updateProject(req, res) {
Project.findById(req.params.project_id, (findProjectErr, project) => {

View File

@ -0,0 +1,32 @@
import Project from '../../models/project';
export default function createProject(req, res) {
let projectValues = {
user: req.user._id
};
projectValues = Object.assign(projectValues, req.body);
function sendFailure() {
res.json({ success: false });
}
function populateUserData(newProject) {
return Project.populate(
newProject,
{ path: 'user', select: 'username' },
(err, newProjectWithUser) => {
if (err) {
sendFailure();
return;
}
res.json(newProjectWithUser);
}
);
}
return Project.create(projectValues)
.then(populateUserData)
.catch(sendFailure);
}

View File

@ -0,0 +1,17 @@
import sinon from 'sinon';
import 'sinon-mongoose';
// Import the actual model to be mocked
const Project = jest.requireActual('../project').default;
// Wrap Project in a sinon mock
// The returned object is used to configure
// the mocked model's behaviour
export function createMock() {
return sinon.mock(Project);
}
// Re-export the model, it will be
// altered by mockingoose whenever
// we call methods on the MockConfig
export default Project;

View File

@ -0,0 +1,101 @@
import mockingoose from 'mockingoose';
import differenceInSeconds from 'date-fns/difference_in_seconds';
import Project from '../project';
const datesWithinSeconds = (first, second) => differenceInSeconds(first, second) < 2;
describe('models/project', () => {
beforeEach(() => {
mockingoose.resetAll();
});
describe('projectSchema', () => {
it('sets default project properties', (done) => {
const data = {};
mockingoose(Project).toReturn(data, 'create');
Project.create(data, (err, newProject) => {
expect(err).toBeNull();
expect(newProject).toBeDefined();
expect(newProject.name).toBe("Hello p5.js, it's the server");
expect(newProject.serveSecure).toBe(false);
done();
});
});
it('creates a slug from the project name', (done) => {
const data = { name: 'My project' };
mockingoose(Project).toReturn(data, 'create');
Project.create(data, (err, newProject) => {
expect(newProject.slug).toBe('My_project');
done();
});
});
it('exposes _id as id', (done) => {
const data = { name: 'My project' };
mockingoose(Project).toReturn(data, 'create');
Project.create(data, (err, newProject) => {
expect(newProject.id).toBe(newProject._id);
done();
});
});
it('generates timestamps', (done) => {
const data = { name: 'My project' };
const now = new Date();
mockingoose(Project).toReturn(data, 'create');
Project.create(data, (err, newProject) => {
// Dates should be near to now, by a few ms
expect(newProject.createdAt).toBeInstanceOf(Date);
expect(datesWithinSeconds(newProject.createdAt, now)).toBe(true);
expect(newProject.updatedAt).toBeInstanceOf(Date);
expect(datesWithinSeconds(newProject.updatedAt, now)).toBe(true);
done();
});
});
it('serializes to JSON', (done) => {
const data = { name: 'My project' };
mockingoose(Project).toReturn(data, 'create');
Project.create(data, (err, newProject) => {
const now = new Date();
const object = JSON.parse(JSON.stringify(newProject));
expect(object).toMatchObject({
_id: newProject._id,
name: 'My project',
id: newProject._id,
slug: 'My_project',
files: [],
serveSecure: false
});
// Check that the timestamps deserialise
const createdAt = new Date(object.createdAt);
const updatedAt = new Date(object.updatedAt);
expect(datesWithinSeconds(createdAt, now)).toBe(true);
expect(datesWithinSeconds(updatedAt, now)).toBe(true);
done();
});
});
});
describe('fileSchema', () => {
});
});

View File

@ -2,6 +2,9 @@ import mongoose from 'mongoose';
import shortid from 'shortid';
import slugify from 'slugify';
// Register User model as it's referenced by Project
import './user';
const { Schema } = mongoose;
const fileSchema = new Schema(