Skip to content

Commit a38527d

Browse files
committed
Tests API project controller
1 parent dbeada5 commit a38527d

File tree

4 files changed

+153
-8
lines changed

4 files changed

+153
-8
lines changed

server/controllers/__test__/project.controller.test.js

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
* @jest-environment node
33
*/
44
import { Response } from 'jest-express';
5+
import sinon from 'sinon';
56

6-
import { createMock } from '../../models/project';
7-
import createProject from '../project.controller/createProject';
7+
import Project, { createMock, createInstanceMock } from '../../models/project';
8+
import createProject, { apiCreateProject } from '../project.controller/createProject';
89

910
jest.mock('../../models/project');
1011

@@ -152,4 +153,119 @@ describe('project.controller', () => {
152153
promise.then(expectations, expectations).catch(expectations);
153154
});
154155
});
156+
157+
describe('apiCreateProject()', () => {
158+
let ProjectMock;
159+
let ProjectInstanceMock;
160+
161+
beforeEach(() => {
162+
ProjectMock = createMock();
163+
ProjectInstanceMock = createInstanceMock();
164+
});
165+
166+
afterEach(() => {
167+
ProjectMock.restore();
168+
ProjectInstanceMock.restore();
169+
});
170+
171+
it('returns 201 with id of created sketch', (done) => {
172+
const request = {
173+
user: { _id: 'abc123' },
174+
body: {
175+
name: 'My sketch',
176+
files: {}
177+
}
178+
};
179+
const response = new Response();
180+
181+
const result = {
182+
_id: 'abc123',
183+
id: 'abc123',
184+
name: 'Project name',
185+
serveSecure: false,
186+
files: []
187+
};
188+
189+
ProjectInstanceMock.expects('save')
190+
.resolves(new Project(result));
191+
192+
const promise = apiCreateProject(request, response);
193+
194+
function expectations() {
195+
const doc = response.json.mock.calls[0][0];
196+
197+
expect(response.status).toHaveBeenCalledWith(201);
198+
expect(response.json).toHaveBeenCalled();
199+
200+
expect(JSON.parse(JSON.stringify(doc))).toMatchObject({
201+
id: 'abc123'
202+
});
203+
204+
done();
205+
}
206+
207+
promise.then(expectations, expectations).catch(expectations);
208+
});
209+
210+
it('returns validation errors on files input', (done) => {
211+
const request = {
212+
user: {},
213+
body: {
214+
name: 'My sketch',
215+
files: {
216+
'index.html': {
217+
// missing content or url
218+
}
219+
}
220+
}
221+
};
222+
const response = new Response();
223+
224+
const promise = apiCreateProject(request, response);
225+
226+
function expectations() {
227+
const doc = response.json.mock.calls[0][0];
228+
229+
const responseBody = JSON.parse(JSON.stringify(doc));
230+
231+
expect(response.status).toHaveBeenCalledWith(422);
232+
expect(responseBody.name).toBe('File Validation Failed');
233+
expect(responseBody.errors.length).toBe(1);
234+
expect(responseBody.errors).toEqual([
235+
{ name: 'index.html', message: 'missing \'url\' or \'content\'' }
236+
]);
237+
238+
done();
239+
}
240+
241+
promise.then(expectations, expectations).catch(expectations);
242+
});
243+
244+
it('rejects file parameters not in object format', (done) => {
245+
const request = {
246+
user: { _id: 'abc123' },
247+
body: {
248+
name: 'Wriggly worm',
249+
files: [{ name: 'file.js', content: 'var hello = true;' }]
250+
}
251+
};
252+
const response = new Response();
253+
254+
const promise = apiCreateProject(request, response);
255+
256+
function expectations() {
257+
const doc = response.json.mock.calls[0][0];
258+
259+
const responseBody = JSON.parse(JSON.stringify(doc));
260+
261+
expect(response.status).toHaveBeenCalledWith(422);
262+
expect(responseBody.name).toBe('File Validation Failed');
263+
expect(responseBody.message).toBe('\'files\' must be an object');
264+
265+
done();
266+
}
267+
268+
promise.then(expectations, expectations).catch(expectations);
269+
});
270+
});
155271
});

server/controllers/project.controller/createProject.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Project from '../../models/project';
2-
import { toModel } from '../../domain-objects/Project';
2+
import { toModel, FileValidationError } from '../../domain-objects/Project';
33

44
export default function createProject(req, res) {
55
let projectValues = {
@@ -36,22 +36,35 @@ export default function createProject(req, res) {
3636
export function apiCreateProject(req, res) {
3737
const params = Object.assign({ user: req.user._id }, req.body);
3838

39+
function sendValidationErrors(err) {
40+
res.status(422).json({
41+
name: 'File Validation Failed',
42+
message: err.message,
43+
errors: err.files,
44+
});
45+
}
46+
3947
// TODO: Error handling to match spec
4048
function sendFailure() {
41-
res.json({ success: false });
49+
res.status(500).json({ success: false });
4250
}
4351

4452
try {
4553
const model = toModel(params);
4654

47-
model
55+
return model
4856
.save()
4957
.then((newProject) => {
5058
res.status(201).json({ id: newProject.id });
5159
})
5260
.catch(sendFailure);
5361
} catch (err) {
54-
// TODO: Catch custom err object and return correct status code
55-
res.status(422).json({ error: 'Validation error' });
62+
if (err instanceof FileValidationError) {
63+
sendValidationErrors(err);
64+
} else {
65+
sendFailure();
66+
}
67+
68+
return Promise.reject();
5669
}
5770
}

server/domain-objects/Project.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import isPlainObject from 'lodash/isPlainObject';
12
import pick from 'lodash/pick';
23
import Project from '../models/project';
34
import createId from '../utils/createId';
@@ -110,8 +111,10 @@ export function transformFiles(tree = {}) {
110111
export function toModel(object) {
111112
let files = [];
112113

113-
if (typeof object.files === 'object') {
114+
if (isPlainObject(object.files)) {
114115
files = transformFiles(object.files);
116+
} else if (object.files != null) {
117+
throw new FileValidationError('\'files\' must be an object');
115118
}
116119

117120
const projectValues = pick(object, ['user', 'name', 'slug']);

server/models/__mocks__/project.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ export function createMock() {
1111
return sinon.mock(Project);
1212
}
1313

14+
// Wraps the Project.prototype i.e. the
15+
// instance methods in a mock so
16+
// Project.save() can be mocked
17+
export function createInstanceMock() {
18+
// See: https://stackoverflow.com/questions/40962960/sinon-mock-of-mongoose-save-method-for-all-future-instances-of-a-model-with-pro
19+
Object.defineProperty(Project.prototype, 'save', {
20+
value: Project.prototype.save,
21+
configurable: true,
22+
});
23+
24+
return sinon.mock(Project.prototype);
25+
}
26+
1427
// Re-export the model, it will be
1528
// altered by mockingoose whenever
1629
// we call methods on the MockConfig

0 commit comments

Comments
 (0)