Merge branch 'master' into feature/public-api
This commit is contained in:
commit
d6438aa3b2
8 changed files with 2906 additions and 1044 deletions
3608
package-lock.json
generated
3608
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -60,7 +60,7 @@
|
||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/preset-react": "^7.0.0",
|
||||||
"babel-core": "^7.0.0-bridge.0",
|
"babel-core": "^7.0.0-bridge.0",
|
||||||
"babel-eslint": "^9.0.0",
|
"babel-eslint": "^9.0.0",
|
||||||
"babel-jest": "^23.4.2",
|
"babel-jest": "^24.8.0",
|
||||||
"babel-loader": "^8.0.0",
|
"babel-loader": "^8.0.0",
|
||||||
"babel-plugin-transform-react-remove-prop-types": "^0.2.12",
|
"babel-plugin-transform-react-remove-prop-types": "^0.2.12",
|
||||||
"chunk-manifest-webpack-plugin": "github:catarak/chunk-manifest-webpack-plugin",
|
"chunk-manifest-webpack-plugin": "github:catarak/chunk-manifest-webpack-plugin",
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
"eslint-plugin-react": "^7.12.3",
|
"eslint-plugin-react": "^7.12.3",
|
||||||
"extract-text-webpack-plugin": "^3.0.2",
|
"extract-text-webpack-plugin": "^3.0.2",
|
||||||
"file-loader": "^2.0.0",
|
"file-loader": "^2.0.0",
|
||||||
"jest": "^23.6.0",
|
"jest": "^24.8.0",
|
||||||
"node-sass": "^4.11.0",
|
"node-sass": "^4.11.0",
|
||||||
"nodemon": "^1.18.9",
|
"nodemon": "^1.18.9",
|
||||||
"postcss-cssnext": "^2.11.0",
|
"postcss-cssnext": "^2.11.0",
|
||||||
|
@ -125,12 +125,14 @@
|
||||||
"friendly-words": "^1.1.3",
|
"friendly-words": "^1.1.3",
|
||||||
"htmlhint": "^0.10.1",
|
"htmlhint": "^0.10.1",
|
||||||
"is-url": "^1.2.4",
|
"is-url": "^1.2.4",
|
||||||
|
"jest-express": "^1.10.1",
|
||||||
"js-beautify": "^1.8.9",
|
"js-beautify": "^1.8.9",
|
||||||
"jsdom": "^9.8.3",
|
"jsdom": "^9.8.3",
|
||||||
"jshint": "^2.10.1",
|
"jshint": "^2.10.1",
|
||||||
"lodash": "^4.17.11",
|
"lodash": "^4.17.11",
|
||||||
"loop-protect": "github:catarak/loop-protect",
|
"loop-protect": "github:catarak/loop-protect",
|
||||||
"mjml": "^3.3.2",
|
"mjml": "^3.3.2",
|
||||||
|
"mockingoose": "^2.13.0",
|
||||||
"mongoose": "^4.6.8",
|
"mongoose": "^4.6.8",
|
||||||
"node-uuid": "^1.4.7",
|
"node-uuid": "^1.4.7",
|
||||||
"nodemailer": "^2.6.4",
|
"nodemailer": "^2.6.4",
|
||||||
|
@ -168,6 +170,8 @@
|
||||||
"sass-extract-js": "^0.4.0",
|
"sass-extract-js": "^0.4.0",
|
||||||
"sass-extract-loader": "^1.1.0",
|
"sass-extract-loader": "^1.1.0",
|
||||||
"shortid": "^2.2.14",
|
"shortid": "^2.2.14",
|
||||||
|
"sinon": "^7.3.2",
|
||||||
|
"sinon-mongoose": "^2.3.0",
|
||||||
"slugify": "^1.3.4",
|
"slugify": "^1.3.4",
|
||||||
"srcdoc-polyfill": "^0.2.0",
|
"srcdoc-polyfill": "^0.2.0",
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
|
|
155
server/controllers/__test__/project.controller.test.js
Normal file
155
server/controllers/__test__/project.controller.test.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -12,31 +12,7 @@ import { resolvePathToFile } from '../utils/filePath';
|
||||||
import generateFileSystemSafeName from '../utils/generateFileSystemSafeName';
|
import generateFileSystemSafeName from '../utils/generateFileSystemSafeName';
|
||||||
import { deleteObjectsFromS3, getObjectKey } from './aws.controller';
|
import { deleteObjectsFromS3, getObjectKey } from './aws.controller';
|
||||||
|
|
||||||
export function createProject(req, res) {
|
export { default as createProject } from './project.controller/createProject';
|
||||||
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 function updateProject(req, res) {
|
export function updateProject(req, res) {
|
||||||
Project.findById(req.params.project_id, (findProjectErr, project) => {
|
Project.findById(req.params.project_id, (findProjectErr, project) => {
|
||||||
|
|
32
server/controllers/project.controller/createProject.js
Normal file
32
server/controllers/project.controller/createProject.js
Normal 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);
|
||||||
|
}
|
17
server/models/__mocks__/project.js
Normal file
17
server/models/__mocks__/project.js
Normal 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;
|
101
server/models/__test__/project.test.js
Normal file
101
server/models/__test__/project.test.js
Normal 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', () => {
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,6 +2,9 @@ import mongoose from 'mongoose';
|
||||||
import shortid from 'shortid';
|
import shortid from 'shortid';
|
||||||
import slugify from 'slugify';
|
import slugify from 'slugify';
|
||||||
|
|
||||||
|
// Register User model as it's referenced by Project
|
||||||
|
import './user';
|
||||||
|
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
const fileSchema = new Schema(
|
const fileSchema = new Schema(
|
||||||
|
|
Loading…
Reference in a new issue