Skip to content

Commit d6438aa

Browse files
committed
Merge branch 'master' into feature/public-api
2 parents a7b465d + d2cad74 commit d6438aa

File tree

8 files changed

+3924
-2062
lines changed

8 files changed

+3924
-2062
lines changed

package-lock.json

Lines changed: 3609 additions & 2035 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"@babel/preset-react": "^7.0.0",
6161
"babel-core": "^7.0.0-bridge.0",
6262
"babel-eslint": "^9.0.0",
63-
"babel-jest": "^23.4.2",
63+
"babel-jest": "^24.8.0",
6464
"babel-loader": "^8.0.0",
6565
"babel-plugin-transform-react-remove-prop-types": "^0.2.12",
6666
"chunk-manifest-webpack-plugin": "github:catarak/chunk-manifest-webpack-plugin",
@@ -75,7 +75,7 @@
7575
"eslint-plugin-react": "^7.12.3",
7676
"extract-text-webpack-plugin": "^3.0.2",
7777
"file-loader": "^2.0.0",
78-
"jest": "^23.6.0",
78+
"jest": "^24.8.0",
7979
"node-sass": "^4.11.0",
8080
"nodemon": "^1.18.9",
8181
"postcss-cssnext": "^2.11.0",
@@ -125,12 +125,14 @@
125125
"friendly-words": "^1.1.3",
126126
"htmlhint": "^0.10.1",
127127
"is-url": "^1.2.4",
128+
"jest-express": "^1.10.1",
128129
"js-beautify": "^1.8.9",
129130
"jsdom": "^9.8.3",
130131
"jshint": "^2.10.1",
131132
"lodash": "^4.17.11",
132133
"loop-protect": "github:catarak/loop-protect",
133134
"mjml": "^3.3.2",
135+
"mockingoose": "^2.13.0",
134136
"mongoose": "^4.6.8",
135137
"node-uuid": "^1.4.7",
136138
"nodemailer": "^2.6.4",
@@ -168,6 +170,8 @@
168170
"sass-extract-js": "^0.4.0",
169171
"sass-extract-loader": "^1.1.0",
170172
"shortid": "^2.2.14",
173+
"sinon": "^7.3.2",
174+
"sinon-mongoose": "^2.3.0",
171175
"slugify": "^1.3.4",
172176
"srcdoc-polyfill": "^0.2.0",
173177
"url": "^0.11.0",
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
import { Response } from 'jest-express';
5+
6+
import { createMock } from '../../models/project';
7+
import createProject from '../project.controller/createProject';
8+
9+
jest.mock('../../models/project');
10+
11+
describe('project.controller', () => {
12+
describe('createProject()', () => {
13+
let ProjectMock;
14+
15+
beforeEach(() => {
16+
ProjectMock = createMock();
17+
});
18+
19+
afterEach(() => {
20+
ProjectMock.restore();
21+
});
22+
23+
it('fails if create fails', (done) => {
24+
const error = new Error('An error');
25+
26+
ProjectMock
27+
.expects('create')
28+
.rejects(error);
29+
30+
const request = { user: {} };
31+
const response = new Response();
32+
33+
const promise = createProject(request, response);
34+
35+
function expectations() {
36+
expect(response.json).toHaveBeenCalledWith({ success: false });
37+
38+
done();
39+
}
40+
41+
promise.then(expectations, expectations).catch(expectations);
42+
});
43+
44+
it('extracts parameters from request body', (done) => {
45+
const request = {
46+
user: { _id: 'abc123' },
47+
body: {
48+
name: 'Wriggly worm',
49+
files: [{ name: 'file.js', content: 'var hello = true;' }]
50+
}
51+
};
52+
const response = new Response();
53+
54+
55+
ProjectMock
56+
.expects('create')
57+
.withArgs({
58+
user: 'abc123',
59+
name: 'Wriggly worm',
60+
files: [{ name: 'file.js', content: 'var hello = true;' }]
61+
})
62+
.resolves();
63+
64+
const promise = createProject(request, response);
65+
66+
function expectations() {
67+
expect(response.json).toHaveBeenCalled();
68+
69+
done();
70+
}
71+
72+
promise.then(expectations, expectations).catch(expectations);
73+
});
74+
75+
// TODO: This should be extracted to a new model object
76+
// so the controllers just have to call a single
77+
// method for this operation
78+
it('populates referenced user on project creation', (done) => {
79+
const request = { user: { _id: 'abc123' } };
80+
const response = new Response();
81+
82+
const result = {
83+
_id: 'abc123',
84+
id: 'abc123',
85+
name: 'Project name',
86+
serveSecure: false,
87+
files: []
88+
};
89+
90+
const resultWithUser = {
91+
...result,
92+
user: {}
93+
};
94+
95+
ProjectMock
96+
.expects('create')
97+
.withArgs({ user: 'abc123' })
98+
.resolves(result);
99+
100+
ProjectMock
101+
.expects('populate')
102+
.withArgs(result)
103+
.yields(null, resultWithUser)
104+
.resolves(resultWithUser);
105+
106+
const promise = createProject(request, response);
107+
108+
function expectations() {
109+
const doc = response.json.mock.calls[0][0];
110+
111+
expect(response.json).toHaveBeenCalled();
112+
113+
expect(JSON.parse(JSON.stringify(doc))).toMatchObject(resultWithUser);
114+
115+
done();
116+
}
117+
118+
promise.then(expectations, expectations).catch(expectations);
119+
});
120+
121+
it('fails if referenced user population fails', (done) => {
122+
const request = { user: { _id: 'abc123' } };
123+
const response = new Response();
124+
125+
const result = {
126+
_id: 'abc123',
127+
id: 'abc123',
128+
name: 'Project name',
129+
serveSecure: false,
130+
files: []
131+
};
132+
133+
const error = new Error('An error');
134+
135+
ProjectMock
136+
.expects('create')
137+
.resolves(result);
138+
139+
ProjectMock
140+
.expects('populate')
141+
.yields(error)
142+
.resolves(error);
143+
144+
const promise = createProject(request, response);
145+
146+
function expectations() {
147+
expect(response.json).toHaveBeenCalledWith({ success: false });
148+
149+
done();
150+
}
151+
152+
promise.then(expectations, expectations).catch(expectations);
153+
});
154+
});
155+
});

server/controllers/project.controller.js

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,7 @@ import { resolvePathToFile } from '../utils/filePath';
1212
import generateFileSystemSafeName from '../utils/generateFileSystemSafeName';
1313
import { deleteObjectsFromS3, getObjectKey } from './aws.controller';
1414

15-
export function createProject(req, res) {
16-
let projectValues = {
17-
user: req.user._id
18-
};
19-
20-
projectValues = Object.assign(projectValues, req.body);
21-
22-
Project.create(projectValues, (err, newProject) => {
23-
if (err) {
24-
res.json({ success: false });
25-
return;
26-
}
27-
Project.populate(
28-
newProject,
29-
{ path: 'user', select: 'username' },
30-
(innerErr, newProjectWithUser) => {
31-
if (innerErr) {
32-
res.json({ success: false });
33-
return;
34-
}
35-
res.json(newProjectWithUser);
36-
}
37-
);
38-
});
39-
}
15+
export { default as createProject } from './project.controller/createProject';
4016

4117
export function updateProject(req, res) {
4218
Project.findById(req.params.project_id, (findProjectErr, project) => {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import Project from '../../models/project';
2+
3+
export default function createProject(req, res) {
4+
let projectValues = {
5+
user: req.user._id
6+
};
7+
8+
projectValues = Object.assign(projectValues, req.body);
9+
10+
function sendFailure() {
11+
res.json({ success: false });
12+
}
13+
14+
function populateUserData(newProject) {
15+
return Project.populate(
16+
newProject,
17+
{ path: 'user', select: 'username' },
18+
(err, newProjectWithUser) => {
19+
if (err) {
20+
sendFailure();
21+
return;
22+
}
23+
res.json(newProjectWithUser);
24+
}
25+
);
26+
}
27+
28+
29+
return Project.create(projectValues)
30+
.then(populateUserData)
31+
.catch(sendFailure);
32+
}

server/models/__mocks__/project.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import sinon from 'sinon';
2+
import 'sinon-mongoose';
3+
4+
// Import the actual model to be mocked
5+
const Project = jest.requireActual('../project').default;
6+
7+
// Wrap Project in a sinon mock
8+
// The returned object is used to configure
9+
// the mocked model's behaviour
10+
export function createMock() {
11+
return sinon.mock(Project);
12+
}
13+
14+
// Re-export the model, it will be
15+
// altered by mockingoose whenever
16+
// we call methods on the MockConfig
17+
export default Project;
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import mockingoose from 'mockingoose';
2+
import differenceInSeconds from 'date-fns/difference_in_seconds';
3+
4+
import Project from '../project';
5+
6+
const datesWithinSeconds = (first, second) => differenceInSeconds(first, second) < 2;
7+
8+
describe('models/project', () => {
9+
beforeEach(() => {
10+
mockingoose.resetAll();
11+
});
12+
13+
describe('projectSchema', () => {
14+
it('sets default project properties', (done) => {
15+
const data = {};
16+
17+
mockingoose(Project).toReturn(data, 'create');
18+
19+
Project.create(data, (err, newProject) => {
20+
expect(err).toBeNull();
21+
expect(newProject).toBeDefined();
22+
expect(newProject.name).toBe("Hello p5.js, it's the server");
23+
expect(newProject.serveSecure).toBe(false);
24+
done();
25+
});
26+
});
27+
28+
it('creates a slug from the project name', (done) => {
29+
const data = { name: 'My project' };
30+
31+
mockingoose(Project).toReturn(data, 'create');
32+
33+
Project.create(data, (err, newProject) => {
34+
expect(newProject.slug).toBe('My_project');
35+
done();
36+
});
37+
});
38+
39+
it('exposes _id as id', (done) => {
40+
const data = { name: 'My project' };
41+
42+
mockingoose(Project).toReturn(data, 'create');
43+
44+
Project.create(data, (err, newProject) => {
45+
expect(newProject.id).toBe(newProject._id);
46+
done();
47+
});
48+
});
49+
50+
it('generates timestamps', (done) => {
51+
const data = { name: 'My project' };
52+
const now = new Date();
53+
54+
mockingoose(Project).toReturn(data, 'create');
55+
56+
Project.create(data, (err, newProject) => {
57+
// Dates should be near to now, by a few ms
58+
expect(newProject.createdAt).toBeInstanceOf(Date);
59+
60+
expect(datesWithinSeconds(newProject.createdAt, now)).toBe(true);
61+
62+
expect(newProject.updatedAt).toBeInstanceOf(Date);
63+
expect(datesWithinSeconds(newProject.updatedAt, now)).toBe(true);
64+
65+
done();
66+
});
67+
});
68+
69+
it('serializes to JSON', (done) => {
70+
const data = { name: 'My project' };
71+
72+
mockingoose(Project).toReturn(data, 'create');
73+
74+
Project.create(data, (err, newProject) => {
75+
const now = new Date();
76+
const object = JSON.parse(JSON.stringify(newProject));
77+
78+
expect(object).toMatchObject({
79+
_id: newProject._id,
80+
name: 'My project',
81+
id: newProject._id,
82+
slug: 'My_project',
83+
files: [],
84+
serveSecure: false
85+
});
86+
87+
// Check that the timestamps deserialise
88+
const createdAt = new Date(object.createdAt);
89+
const updatedAt = new Date(object.updatedAt);
90+
91+
expect(datesWithinSeconds(createdAt, now)).toBe(true);
92+
expect(datesWithinSeconds(updatedAt, now)).toBe(true);
93+
94+
done();
95+
});
96+
});
97+
});
98+
99+
describe('fileSchema', () => {
100+
});
101+
});

server/models/project.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import mongoose from 'mongoose';
22
import shortid from 'shortid';
33
import slugify from 'slugify';
44

5+
// Register User model as it's referenced by Project
6+
import './user';
7+
58
const { Schema } = mongoose;
69

710
const fileSchema = new Schema(

0 commit comments

Comments
 (0)