diff --git a/TODO b/TODO index b07e161..d24494e 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,5 @@ - create a cli +- test that passthrough works, timing options work, coalasceFindRequestWorks, do the one throw() - derived schema attribute for $Model: schema: { inserted_at: { diff --git a/lib/mem-server/model.js b/lib/mem-server/model.js index 16f9183..1f58837 100644 --- a/lib/mem-server/model.js +++ b/lib/mem-server/model.js @@ -145,19 +145,25 @@ export default function(options) { }, embedReferences: {}, serializer(objectOrArray) { - if (Array.isArray(objectOrArray)) { + if (!objectOrArray) { + return; + } else if (Array.isArray(objectOrArray)) { return objectOrArray.map((object) => this.serialize(object), []); } return this.serialize(objectOrArray); }, serialize(object) { // NOTE: add links object ? + if (Array.isArray(object)) { + throw new Error(chalk.red(`[MemServer] ${this.modelName}.serialize(object) expects an object not an array. Use ${this.modelName}.serializer(data) for serializing array of records`)); + } + const objectWithAllAttributes = this.attributes.reduce((result, attribute) => { if (result[attribute] === undefined) { result[attribute] = null; } - return object; + return result; }, object); return Object.keys(this.embedReferences).reduce((result, embedKey) => { diff --git a/lib/mem-server/pretender-hacks.js b/lib/mem-server/pretender-hacks.js index 266a613..91548f4 100644 --- a/lib/mem-server/pretender-hacks.js +++ b/lib/mem-server/pretender-hacks.js @@ -17,7 +17,9 @@ window.Pretender.prototype._handlerFor = function(verb, url, request) { request.queryParams = Object.keys(matches.queryParams).reduce((result, key) => { var value = matches.queryParams[key]; - return Object.assign(result, { [key]: parseInt(value, 10) || value }); + const targetValue = castCorrectType(value); // NOTE: maybe to qs.parse() here? + + return Object.assign(result, { [key]: targetValue }); }, {}); if (request.requestBody && request.requestHeaders['Content-Type'] === 'application/json') { @@ -29,6 +31,18 @@ window.Pretender.prototype._handlerFor = function(verb, url, request) { return match; }; + +function castCorrectType(value) { + if (parseInt(value, 10)) { + return parseInt(value, 10); + } else if (value === 'false') { + return false; + } else if (value === 'true') { + return true; + } + + return value; +} // END: Pretender Request Parameter Type Casting Hack // HACK START: Pretender Response Defaults UX Hack: Because Pretender Response types suck UX-wise. diff --git a/package.json b/package.json index a2778ef..064ae29 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "license": "ISC", "scripts": { - "test": "mocha --retries 3" + "test": "sh run-tests.sh" }, "repository": { "type": "git", diff --git a/run-tests.sh b/run-tests.sh new file mode 100644 index 0000000..27b637e --- /dev/null +++ b/run-tests.sh @@ -0,0 +1,2 @@ +mocha --retries 3 +mocha ./test/server/mem-server.server.js diff --git a/test/mem-server.model.serialize.js b/test/mem-server.model.serialize.js index 79b63e2..7a2d1a7 100644 --- a/test/mem-server.model.serialize.js +++ b/test/mem-server.model.serialize.js @@ -108,7 +108,7 @@ describe('MemServer.Model Serialize Interface', function() { const photos = Photo.findAll({ is_public: false }); const photoComments = PhotoComment.findAll({ photo_id: 1 }); - assert.deepEqual(Photo.serialize(photos), [ + assert.deepEqual(Photo.serializer(photos), [ { id: 1, name: 'Ski trip', @@ -122,7 +122,7 @@ describe('MemServer.Model Serialize Interface', function() { is_public: false } ]); - assert.deepEqual(PhotoComment.serialize(photoComments), [ + assert.deepEqual(PhotoComment.serializer(photoComments), [ { uuid: '499ec646-493f-4eea-b92e-e383d94182f4', content: 'What a nice photo!', @@ -155,12 +155,16 @@ describe('MemServer.Model Serialize Interface', function() { const notFoundComment = PhotoComment.findBy({ uuid: '374c7f4a-85d6-429a-bf2a-0719525f5111' }); const notFoundComments = Photo.findAll({ content: 'Aint easy' }); - assert.equal(Photo.serialize(notFoundPhoto), undefined); - assert.deepEqual(Photo.serialize({}), {}); - assert.deepEqual(Photo.serialize(notFoundPhotos), []); - assert.equal(PhotoComment.serialize(notFoundComment), undefined); - assert.deepEqual(PhotoComment.serialize({}), {}); - assert.deepEqual(PhotoComment.serialize(notFoundComments), []); + assert.equal(Photo.serializer(notFoundPhoto), undefined); + assert.deepEqual(Photo.serializer({}), { + id: null, href: null, is_public: null, name: null + }); + assert.deepEqual(Photo.serializer(notFoundPhotos), []); + assert.equal(PhotoComment.serializer(notFoundComment), undefined); + assert.deepEqual(PhotoComment.serializer({}), { + uuid: null, content: null, photo_id: null, user_id: null + }); + assert.deepEqual(PhotoComment.serializer(notFoundComments), []); }); it('can serialize embeded records recursively', function() { diff --git a/test/mem-server.server.js b/test/server/mem-server.server.js similarity index 59% rename from test/mem-server.server.js rename to test/server/mem-server.server.js index 3233162..8dc2e9b 100644 --- a/test/mem-server.server.js +++ b/test/server/mem-server.server.js @@ -7,6 +7,8 @@ const AJAX_AUTHORIZATION_HEADERS = { 'Content-Type': 'application/json', 'Authorization': `Token ${AUTHENTICATION_TOKEN}` }; +process.setMaxListeners(0); + describe('MemServer.Server functionality', function() { before(function() { fs.mkdirSync(`./memserver`); @@ -17,9 +19,9 @@ describe('MemServer.Server functionality', function() { export default Model({ findFromHeaderToken(headers) { const authorizationHeader = headers.Authorization; - const token = authorizationHeader ? authorizationHeader.slice(6) : null; + const token = authorizationHeader ? authorizationHeader.slice(6) : false; - return this.findBy({ authentication_token: token }); + return this.findBy({ authentication_token: token }) || false; } }); `); @@ -136,7 +138,7 @@ describe('MemServer.Server functionality', function() { it('POST /resources work with shortcut', async function() { this.timeout(5000); - const MemServer = require('../index.js'); + const MemServer = require('../../index.js'); const { Photo } = MemServer.Models; MemServer.start(); @@ -157,7 +159,7 @@ describe('MemServer.Server functionality', function() { }); it('GET /resources works with shortcut', async function() { - const MemServer = require('../index.js'); + const MemServer = require('../../index.js'); const { Photo } = MemServer.Models; MemServer.start(); @@ -174,7 +176,7 @@ describe('MemServer.Server functionality', function() { }); it('GET /resources/:id works with shortcut', async function() { - const MemServer = require('../index.js'); + const MemServer = require('../../index.js'); const { Photo } = MemServer.Models; MemServer.start(); @@ -188,7 +190,7 @@ describe('MemServer.Server functionality', function() { }); it('PUT /resources/:id works with shortcut', async function() { - const MemServer = require('../index.js'); + const MemServer = require('../../index.js'); const { Photo } = MemServer.Models; MemServer.start(); @@ -208,18 +210,15 @@ describe('MemServer.Server functionality', function() { }); it('DELETE /resources/:id works with shortcut', async function() { - const MemServer = require('../index.js'); + const MemServer = require('../../index.js'); const { Photo } = MemServer.Models; MemServer.start(); - window.$.ajaxSetup({ headers: { 'Content-Type': 'application/json' } }); assert.equal(Photo.count(), 3); await window.$.ajax({ - type: 'DELETE', - url: '/photos/1', - headers: { 'Content-Type': 'application/json' } + type: 'DELETE', url: '/photos/1', headers: { 'Content-Type': 'application/json' } }, (data, textStatus, jqXHR) => { assert.equal(jqXHR.status, 204); assert.deepEqual(data, {}); @@ -237,7 +236,6 @@ describe('MemServer.Server functionality', function() { export default function({ User, Photo }) { this.post('/photos', ({ headers }) => { const user = User.findFromHeaderToken(headers); - console.log('user is', user); if (!user) { return Response(401, { error: 'Unauthorized' }); @@ -286,8 +284,6 @@ describe('MemServer.Server functionality', function() { this.delete('/photos/:id', ({ headers, params }) => { const user = User.findFromHeaderToken(headers); - console.log('called'); - console.log(user); if (user && Photo.findBy({ id: params.id, user_id: user.id })) { return Photo.delete({ id: params.id }); @@ -300,15 +296,16 @@ describe('MemServer.Server functionality', function() { it('POST /resources work with custom headers and responses', async function() { this.timeout(5000); - const MemServer = require('../index.js'); + const MemServer = require('../../index.js'); const { Photo } = MemServer.Models; MemServer.start(); - window.$.ajaxSetup({ headers: { 'Content-Type': 'application/json' } }); assert.equal(Photo.count(), 3); - await window.$.post('/photos').catch((jqXHR) => { + await window.$.ajax({ + type: 'POST', url: '/photos', headers: { 'Content-Type': 'application/json' } + }).catch((jqXHR) => { assert.equal(jqXHR.status, 401); assert.deepEqual(jqXHR.responseJSON, { error: 'Unauthorized' }); }); @@ -324,7 +321,7 @@ describe('MemServer.Server functionality', function() { }); it('GET /resources works with custom headers and responses', async function() { - const MemServer = require('../index.js'); + const MemServer = require('../../index.js'); const { Photo } = MemServer.Models; MemServer.start(); @@ -345,7 +342,7 @@ describe('MemServer.Server functionality', function() { }); it('GET /resources/:id works with custom headers and responses', async function() { - const MemServer = require('../index.js'); + const MemServer = require('../../index.js'); const { Photo } = MemServer.Models; MemServer.start(); @@ -366,7 +363,7 @@ describe('MemServer.Server functionality', function() { }); it('PUT /resources/:id works with custom headers and responses', async function() { - const MemServer = require('../index.js'); + const MemServer = require('../../index.js'); const { Photo } = MemServer.Models; MemServer.start(); @@ -390,7 +387,7 @@ describe('MemServer.Server functionality', function() { }); it('DELETE /resources/:id works with custom headers and responses', async function() { - const MemServer = require('../index.js'); + const MemServer = require('../../index.js'); const { Photo } = MemServer.Models; MemServer.start(); @@ -415,94 +412,206 @@ describe('MemServer.Server functionality', function() { }); }); - // describe('server can process custom queryParams and responses', function() { - // fs.writeFileSync(`${process.cwd()}/memserver/server.js`, ` - // import Response from ''; - // - // export default function(Models) { - // this.post('/photos', ({ headers, queryParams }) => { - // const user = User.findFromToken(request); - // - // if (!user && queryParams.is_admin) { - // return Response(401, { error: 'Unauthorized' }); - // } - // - // const photo = Photo.insert({ user_id: user.id }); - // - // return Photo.serialize(photo); - // }); - // - // this.get('/photos'), ({ headers, queryParams }) => { - // const user = User.findFromToken(request); - // - // if (!user) { - // return Response(404, { error: 'Not found' }); - // } - // - // const photos = Photo.findAll(Object.assign({ user_id: user.id }, queryParams)); - // - // if (!photos) { // NOTE: change here maybe - // return Response(404, { error: 'Not found' }); - // } - // - // return Photo.serialize(photos); - // }); - // - // this.get('/photos/:id', ({ params, headers, queryParams }) => { - // const user = User.findFromToken(request); - // - // if (!user) { - // return Response(401, { error: 'Unauthorized' }); - // } else if (queryParams.nonce === '123123123') { - // const photo = Photo.findBy({ id: params.id, user_id: user.id }); - // - // return photo ? Photo.serialize(photo) : Response(404, { error: 'Not found'}) - // } - // }); - // - // this.put('/photos/:id', ({ params, headers, queryParams }) => { - // const user = User.findFromToken(request); - // const validRequest = user && queryParams.nonce === '123123123' && - // Photo.findBy({ id: params.id, user_id: user.id }); - // - // if (validRequest) { - // return Photo.update(request.params); - // } - // }); - // - // this.delete('/photos/:id', ({ params, headers }) => { - // const user = User.findFromToken(request); - // - // if (!(queryParams.nonce === '123123123') { - // return Response(500, { error: 'Invalid nonce to delete a photo' }); - // } else if (user && Photo.findBy({ id: params.id, user_id: user.id })) { - // return Photo.delete(request.params); // NOTE: what to do with this response - // } - // }); - // } - // `); - - // it('POST /resources work with custom headers, queryParams and responses', function() { - // - // }); - // - // it('GET /resources works with custom headers, queryParams and responses', function() { - // - // }); - // - // it('GET /resources/:id works with custom headers, queryParams and responses', function() { - // - // }); - // - // it('PUT /resources/:id works with custom headers, queryParams and responses', function() { - // - // }); - // - // it('DELETE /resources/:id works with custom headers, queryParams and responses', function() { - // - // }); - // }); + describe('server can process custom queryParams and responses', function() { + before(function(){ + fs.writeFileSync(`${process.cwd()}/memserver/server.js`, ` + import Response from '../lib/mem-server/response'; + + export default function({ User, Photo }) { + this.post('/photos', ({ headers, params, queryParams }) => { + const user = User.findFromHeaderToken(headers); + + if (!user || !queryParams.is_admin) { + return Response(401, { error: 'Unauthorized' }); + } + + const photo = Photo.insert(Object.assign({}, params.photo, { user_id: user.id })); + + return { photo: Photo.serializer(photo) }; + }); + + this.get('/photos', ({ headers, queryParams }) => { + const user = User.findFromHeaderToken(headers); + + if (!user) { + return Response(404, { error: 'Not found' }); + } + + const photos = Photo.findAll(Object.assign({}, { user_id: user.id }, queryParams)); + + if (!photos || photos.length === 0) { + return Response(404, { error: 'Not found' }); + } + + return { photos: Photo.serializer(photos) }; + }); + + this.get('/photos/:id', ({ headers, params, queryParams }) => { + const user = User.findFromHeaderToken(headers); + + if (!user) { + return Response(401, { error: 'Unauthorized' }); + } else if (queryParams.nonce === 123123123) { + const photo = Photo.findBy({ id: params.id, user_id: user.id }); + + return photo ? { photo: Photo.serializer(photo) } : Response(404, { error: 'Not found' }); + } + + return Response(404, { error: 'Not found' }); + }); + + this.put('/photos/:id', ({ headers, params, queryParams }) => { + const user = User.findFromHeaderToken(headers); + const validRequest = user && queryParams.nonce === 123123123 && + Photo.findBy({ id: params.id, user_id: user.id }); + + if (validRequest) { + return { photo: Photo.serializer(Photo.update(params.photo)) }; + } + + return Response(500, { error: 'Unexpected error occured' }); + }); + + this.delete('/photos/:id', ({ headers, params, queryParams }) => { + const user = User.findFromHeaderToken(headers); + + if (!(queryParams.nonce === 123123123)) { + return Response(500, { error: 'Invalid nonce to delete a photo' }); + } else if (!user && !Photo.findBy({ id: params.id, user_id: user.id })) { + return Response(404, { error: 'Not found' }); + } + + Photo.delete({ id: params.id }); // NOTE: what to do with this response + }); + } + `); + }); + + it('POST /resources work with custom headers, queryParams and responses', async function() { + const MemServer = require('../../index.js'); + const { Photo } = MemServer.Models; + + MemServer.start(); + + assert.equal(Photo.count(), 3); + + await window.$.ajax({ + type: 'POST', url: '/photos', headers: { 'Content-Type': 'application/json' } + }).catch((jqXHR) => { + assert.equal(jqXHR.status, 401); + assert.deepEqual(jqXHR.responseJSON, { error: 'Unauthorized' }); + }); + + await window.$.ajax({ + type: 'POST', url: '/photos', headers: AJAX_AUTHORIZATION_HEADERS + }).catch((jqXHR) => { + assert.equal(jqXHR.status, 401); + assert.deepEqual(jqXHR.responseJSON, { error: 'Unauthorized' }); + }); + + await window.$.ajax({ + type: 'POST', url: '/photos?is_admin=true', headers: AJAX_AUTHORIZATION_HEADERS + }).then((data, textStatus, jqXHR) => { + assert.equal(jqXHR.status, 201); + assert.deepEqual(data, { photo: Photo.serializer(Photo.find(4)) }); + assert.equal(Photo.count(), 4); + }); + }); + + it('GET /resources works with custom headers, queryParams and responses', async function() { + const MemServer = require('../../index.js'); + const { Photo } = MemServer.Models; + + MemServer.start(); + + await window.$.ajax({ + type: 'GET', url: '/photos', headers: { 'Content-Type': 'application/json' } + }).catch((jqXHR) => { + assert.equal(jqXHR.status, 404); + assert.deepEqual(jqXHR.responseJSON, { error: 'Not found' }); + }); + + await window.$.ajax({ + type: 'GET', url: '/photos?is_public=false', headers: AJAX_AUTHORIZATION_HEADERS + }).then((data, textStatus, jqXHR) => { + assert.equal(jqXHR.status, 200); + assert.deepEqual(data, { photos: Photo.serializer(Photo.findAll({ is_public: false })) }); + }); + + await window.$.ajax({ + type: 'GET', url: '/photos?href=family-photo.jpeg', headers: AJAX_AUTHORIZATION_HEADERS + }).then((data, textStatus, jqXHR) => { + assert.equal(jqXHR.status, 200); + assert.deepEqual(data, { photos: Photo.serializer(Photo.findAll({ href: 'family-photo.jpeg' })) }); + }); + }); + + it('GET /resources/:id works with custom headers, queryParams and responses', async function() { + const MemServer = require('../../index.js'); + const { Photo } = MemServer.Models; + + MemServer.start(); + + await window.$.ajax({ + type: 'GET', url: '/photos/1', headers: AJAX_AUTHORIZATION_HEADERS + }).catch((jqXHR) => { + assert.equal(jqXHR.status, 404); + assert.deepEqual(jqXHR.responseJSON, { error: 'Not found' }); + }); + + await window.$.ajax({ + type: 'GET', url: '/photos/1?nonce=123123123', headers: AJAX_AUTHORIZATION_HEADERS + }).then((data, textStatus, jqXHR) => { + assert.equal(jqXHR.status, 200); + assert.deepEqual(data, { photo: Photo.serializer(Photo.find(1)) }); + }); + }); + + it('PUT /resources/:id works with custom headers, queryParams and responses', async function() { + const MemServer = require('../../index.js'); + const { Photo } = MemServer.Models; + + MemServer.start(); + + await window.$.ajax({ + type: 'PUT', url: '/photos/1', headers: AJAX_AUTHORIZATION_HEADERS, + data: JSON.stringify({ photo: { id: 1, name: 'Life' } }) + }).catch((jqXHR) => { + assert.equal(jqXHR.status, 500); + assert.deepEqual(jqXHR.responseJSON, { error: 'Unexpected error occured' }); + }); + + await window.$.ajax({ + type: 'PUT', url: '/photos/1?nonce=123123123', headers: AJAX_AUTHORIZATION_HEADERS, + data: JSON.stringify({ photo: { id: 1, name: 'Life' } }) + }).then((data, textStatus, jqXHR) => { + assert.equal(jqXHR.status, 200); + assert.deepEqual(data, { photo: Photo.serializer(Photo.find(1)) }); + }); + }); + + it('DELETE /resources/:id works with custom headers, queryParams and responses', async function() { + const MemServer = require('../../index.js'); + const { Photo } = MemServer.Models; + + MemServer.start(); + + await window.$.ajax({ + type: 'DELETE', url: '/photos/1', headers: AJAX_AUTHORIZATION_HEADERS + }).catch((jqXHR) => { + assert.equal(jqXHR.status, 500); + assert.deepEqual(jqXHR.responseJSON, { error: 'Invalid nonce to delete a photo' }); + }); + + await window.$.ajax({ + type: 'DELETE', url: '/photos/1?nonce=123123123', headers: AJAX_AUTHORIZATION_HEADERS + }).then((data, textStatus, jqXHR) => { + assert.equal(jqXHR.status, 204); + }); + }); + }); // TODO: passthrough works // TODO: by default returning undefined should return Response(500) ? + // TODO: test that passthrough works, timing options work, coalasceFindRequestWorks, do the one throw() });