UNPKG

kinvey-flex-sdk

Version:

SDK for creating Kinvey Flex Services

834 lines (758 loc) 30.4 kB
/** * Copyright (c) 2018 Kinvey Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ const proxyquire = require('proxyquire'); const should = require('should'); const sinon = require('sinon'); const loggerMock = require('./mocks/loggerMock'); const completionHandler = proxyquire('../../../lib/service/kinveyCompletionHandler', { './logger': loggerMock }); const functions = proxyquire('../../../lib/service/functions', { './kinveyCompletionHandler': completionHandler }); const moduleGenerator = require('../../../lib/service/moduleGenerator'); const testTaskName = 'myTaskName'; function quickRandom() { return Math.floor((Math.random() * (1000 - 1)) + 1); } function sampleTask(name) { return { taskType: 'functions', taskName: name, method: 'POST', endpoint: null, hookType: 'pre', request: { method: 'POST', headers: { }, entityId: '12345', objectName: null }, response: { status: 0, headers: {}, body: {} } }; } function samplePostTask(name) { return { taskType: 'functions', taskName: name, method: 'POST', endpoint: null, hookType: 'post', request: { method: 'POST', headers: { requestHeader: 'foo' }, entityId: '12345', objectName: null, body: { bar: 'foo' } }, response: { status: 0, headers: { responseHeader: 'bar' }, body: [{ id: quickRandom() }, { id: quickRandom() }, { id: quickRandom() }] } }; } function samplePreTask(name) { return { taskType: 'functions', taskName: name, method: 'POST', endpoint: null, hookType: 'pre', baasUrl: 'some-url', appMetadata: { _id: quickRandom() }, request: { method: 'POST', headers: { requestHeader: 'foo' }, entityId: '12345', objectName: null, body: { bar: 'foo' } }, response: { status: 0, headers: { responseHeader: 'bar' }, body: [{ id: quickRandom() }, { id: quickRandom() }, { id: quickRandom() }] } }; } function sampleCustomEndpoint(name) { return { taskType: 'functions', taskName: name, method: 'POST', endpoint: null, hookType: 'customEndpoint', request: { method: 'POST', headers: { requestHeader: 'foo' }, entityId: '12345', objectName: null, body: { bar: 'foo' } }, response: { status: 0, headers: { responseHeader: 'bar' }, body: [{ id: quickRandom() }, { id: quickRandom() }, { id: quickRandom() }] } }; } function sampleBadTask() { return { request: { body: 'abc' }, response: { status: 0, headers: {}, body: {} } }; } describe('FlexFunctions', () => { describe('function registration', () => { afterEach((done) => { functions.clearAll(); done(); }); it('can register a functions task', (done) => { functions.register(testTaskName, () => done()); const fn = functions.resolve(testTaskName); fn(); }); }); describe('discovery', () => { afterEach((done) => { functions.clearAll(); done(); }); it('returns an array of all registered function handlers', (done) => { const testHandlerName = 'testObj'; functions.register(testHandlerName, () => {}); const discoveredHandlers = functions.getHandlers(); should.exist(discoveredHandlers[0]); discoveredHandlers.length.should.eql(1); discoveredHandlers[0].should.eql(testHandlerName); done(); }); it('returns an empty array if no function handlers have been registered', (done) => { const discoveredHandlers = functions.getHandlers(); Array.isArray(discoveredHandlers).should.eql(true); discoveredHandlers.length.should.eql(0); done(); }); }); describe('functions processing', () => { afterEach((done) => { functions.clearAll(); done(); }); it('can process a functions task', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); functions.register(taskName, () => done()); functions.process(task, {}, () => {}); }); it('includes context, completion, and module handlers in a functions task', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); functions.register(taskName, (context, complete, modules) => { context.should.be.an.Object(); complete.should.be.a.Function(); modules.should.be.an.Object(); done(); }); functions.process(task, {}, () => {}); }); it('includes objectName if passed in request.objectName', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); task.request.objectName = 'foo'; functions.register(taskName, (context, complete, modules) => { context.should.be.an.Object(); complete.should.be.a.Function(); modules.should.be.an.Object(); context.objectName.should.eql('foo'); done(); }); functions.process(task, {}, () => {}); }); it('includes objectName if passed in request.collectionName', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); task.request.collectionName = 'foo'; functions.register(taskName, (context, complete, modules) => { context.should.be.an.Object(); complete.should.be.a.Function(); modules.should.be.an.Object(); context.objectName.should.eql('foo'); done(); }); functions.process(task, {}, () => {}); }); it('context includes loginOptions if passed', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); task.request.loginOptions = { type: 'kinvey' }; functions.register(taskName, (context, complete, modules) => { context.should.be.an.Object(); complete.should.be.a.Function(); modules.should.be.an.Object(); context.loginOptions.should.eql({ type: 'kinvey' }); return done(); }); functions.process(task, {}, () => {}); }); it('context does not includes loginOptions if not passed', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); functions.register(taskName, (context, complete, modules) => { context.should.be.an.Object(); complete.should.be.a.Function(); modules.should.be.an.Object(); should.not.exist(context.loginOptions); return done(); }); functions.process(task, {}, () => {}); }); it('context includes status of response.status is >399 and hookType is "post"', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); task.hookType = 'post'; task.response.status = 401; functions.register(taskName, (context, complete, modules) => { context.should.be.an.Object(); complete.should.be.a.Function(); modules.should.be.an.Object(); context.status.should.eql(401); return done(); }); functions.process(task, {}, () => {}); }); it('context does not include status of response.status is <399', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); task.hookType = 'post'; task.response.status = 200; functions.register(taskName, (context, complete, modules) => { context.should.be.an.Object(); complete.should.be.a.Function(); modules.should.be.an.Object(); should.not.exist(context.status); return done(); }); functions.process(task, {}, () => {}); }); it('context does not include status of hookType is "pre"', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); task.hookType = 'pre'; task.response.status = 401; functions.register(taskName, (context, complete, modules) => { context.should.be.an.Object(); complete.should.be.a.Function(); modules.should.be.an.Object(); should.not.exist(context.status); return done(); }); functions.process(task, {}, () => {}); }); }); describe('completion handlers', () => { afterEach((done) => { loggerMock.error.resetHistory(); functions.clearAll(); done(); }); it("should return a 'BadRequest' response with a null task name", (done) => { const task = sampleTask(null); functions.process(task, null, (err) => { err.response.statusCode.should.eql(400); err.response.body.debug.should.eql('No task name to execute'); done(); }); }); it("should return a 'BadRequest' response with a non-JSON task", (done) => { const task = sampleBadTask(null); functions.process(task, null, (err) => { err.response.statusCode.should.eql(400); err.response.body.debug.should.eql('Request body is not JSON'); done(); }); }); it('should return a successful response', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); functions.register(taskName, (context, complete) => complete().ok().next()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(200); result.response.body.should.eql({}); done(); }); }); it('should include a body', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); functions.register(taskName, (context, complete) => complete({ foo: 'bar' }).ok().done()); functions.process(task, {}, (err, result) => { result.response.statusCode.should.eql(200); result.response.body.should.eql({ foo: 'bar' }); done(); }); }); it('should explicitly set a body', (done) => { const taskName = quickRandom(); const task = samplePreTask(taskName); functions.register(taskName, (context, complete) => complete() .setBody({ foo: 'bar' }) .ok() .done()); functions.process(task, {}, (err, result) => { result.response.statusCode.should.eql(200); result.response.body.should.eql({ foo: 'bar' }); done(); }); }); it('should explicitly set a query', (done) => { const taskName = quickRandom(); const task = samplePreTask(taskName); const query = { foo: 'bar' }; const sort = { foo: 1 }; functions.register(taskName, (context, complete) => complete() .setQuery({ query, sort }) .ok() .next()); functions.process(task, {}, (err, result) => { result.response.statusCode.should.eql(200); result.request.query.should.eql({ query: JSON.stringify(query), sort: JSON.stringify(sort) }); result.request.params.should.eql({ query: JSON.stringify(query), sort: JSON.stringify(sort) }); done(); }); }); it('should include the response body and headers when task is a post task', (done) => { const taskName = quickRandom(); const task = samplePostTask(taskName); functions.register(taskName, (context, complete) => { context.body.should.eql(task.response.body); context.body.should.not.eql(task.request.body); context.headers.should.eql(task.response.headers); context.headers.should.not.eql(task.request.headers); complete(task.response.body).ok().next(); }); functions.process(task, {}, () => done()); }); it('should include the request body and headers when task is a pre task', (done) => { const taskName = quickRandom(); const task = samplePreTask(taskName); functions.register(taskName, (context, complete) => { context.body.should.eql(task.request.body); context.body.should.not.eql(task.response.body); context.headers.should.eql(task.request.headers); context.headers.should.not.eql(task.response.headers); complete(task.response.body).ok().next(); }); functions.process(task, {}, () => done()); }); it('should include the request body and headers when task is a custom endpoint task', (done) => { const taskName = quickRandom(); const task = sampleCustomEndpoint(taskName); functions.register(taskName, (context, complete) => { context.body.should.eql(task.request.body); context.body.should.not.eql(task.response.body); context.headers.should.eql(task.request.headers); context.headers.should.not.eql(task.response.headers); complete(task.response.body).ok().next(); }); functions.process(task, {}, () => done()); }); it('should set the response body when task is a post task and the request is ended', (done) => { const taskName = quickRandom(); const task = samplePostTask(taskName); functions.register(taskName, (context, complete) => complete({ foo: 'bar' }).ok().done()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(200); result.response.body.should.eql({ foo: 'bar' }); done(); }); }); it('should set the response body when task is a post task and the request is continued', (done) => { const taskName = quickRandom(); const task = samplePostTask(taskName); functions.register(taskName, (context, complete) => complete({ foo: 'bar' }).ok().next()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(200); result.response.body.should.eql({ foo: 'bar' }); done(); }); }); it('should add customRequestProperties to request.header.x-kinvey-custom-request-properties', (done) => { const taskName = quickRandom(); const task = samplePreTask(taskName); task.request.headers['x-kinvey-custom-request-properties'] = '{"keyFromRequest":"testValue"}'; functions.register(taskName, (context, complete, modules) => { modules.requestContext.setCustomRequestProperty('keyFromHook', 'testValue'); complete().ok().next(); }); functions.process(task, moduleGenerator.generate(task), (err, result) => { should.not.exist(err); result.request.headers.should.containEql({ 'x-kinvey-custom-request-properties': '{"keyFromRequest":"testValue","keyFromHook":"testValue"}', }); done(); }); }); it('should keep existing response body when task is a post task and no body is set and the request is ended', (done) => { const taskName = quickRandom(); const task = samplePostTask(taskName); functions.register(taskName, (context, complete) => complete({ foo: 'bar' }).ok().done()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(200); result.response.body.should.eql(task.response.body); done(); }); }); it('should keep existing response body when task is a post task and no body is set and the request is continued', (done) => { const taskName = quickRandom(); const task = samplePostTask(taskName); functions.register(taskName, (context, complete) => complete({ foo: 'bar' }).ok().next()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(200); result.response.body.should.eql(task.response.body); done(); }); }); it('should include the request body and headers when task is a pre task', (done) => { const taskName = quickRandom(); const task = samplePreTask(taskName); functions.register(taskName, (context, complete) => { context.body.should.eql(task.request.body); context.body.should.not.eql(task.response.body); context.headers.should.eql(task.request.headers); context.headers.should.not.eql(task.response.headers); complete(task.response.body).ok().next(); }); functions.process(task, {}, () => done()); }); it('should set the response body when task is a pre task and the request is ended', (done) => { const taskName = quickRandom(); const task = samplePreTask(taskName); functions.register(taskName, (context, complete) => complete({ foo: 'bar' }).ok().done()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(200); result.response.body.should.eql({ foo: 'bar' }); result.response.body.should.not.eql(result.request.body); done(); }); }); it('should set the request body when task is a pre task and the request is continued', (done) => { const taskName = quickRandom(); const task = samplePreTask(taskName); functions.register(taskName, (context, complete) => complete({ foo: 'bar' }).ok().next()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(200); result.request.body.should.eql({ foo: 'bar' }); result.request.body.should.not.eql(result.response.body); done(); }); }); it('should keep empty response body when task is a pre task and no body is set and the request is ended', (done) => { const taskName = quickRandom(); const task = samplePreTask(taskName); functions.register(taskName, (context, complete) => complete({ foo: 'bar' }).ok().done()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(200); result.response.body.should.eql(task.response.body); done(); }); }); it('should keep existing response body when task is a pre task and no body is set and the request is continued', (done) => { const taskName = quickRandom(); const task = samplePreTask(taskName); functions.register(taskName, (context, complete) => complete({ foo: 'bar' }).ok().next()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(200); result.request.body.should.eql({ foo: 'bar' }); result.request.body.should.not.eql(result.response.body); done(); }); }); it('should include the request body and headers when task is a custom endpoint task', (done) => { const taskName = quickRandom(); const task = sampleCustomEndpoint(taskName); functions.register(taskName, (context, complete) => { context.body.should.eql(task.request.body); context.body.should.not.eql(task.response.body); context.headers.should.eql(task.request.headers); context.headers.should.not.eql(task.response.headers); complete(task.response.body).ok().next(); }); functions.process(task, {}, () => done()); }); it('should set the response body when task is a custom endpoint task and the request is ended', (done) => { const taskName = quickRandom(); const task = sampleCustomEndpoint(taskName); functions.register(taskName, (context, complete) => complete({ foo: 'bar' }).ok().done()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(200); result.response.body.should.eql({ foo: 'bar' }); result.response.body.should.not.eql(result.request.body); done(); }); }); it('should set the response body when task is a custom endpoint task and the request is continued', (done) => { const taskName = quickRandom(); const task = sampleCustomEndpoint(taskName); functions.register(taskName, (context, complete) => complete({ foo: 'bar' }).ok().next()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(200); result.response.body.should.eql({ foo: 'bar' }); result.response.body.should.not.eql(result.request.body); done(); }); }); it('should keep empty response body when task is a custom endpoint and no body is set and the request is ended', (done) => { const taskName = quickRandom(); const task = sampleCustomEndpoint(taskName); functions.register(taskName, (context, complete) => complete({ foo: 'bar' }).ok().done()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(200); result.response.body.should.eql(task.response.body); done(); }); }); it('should keep existing response body when task is a custom endpoint, no body is set and the request is continued', (done) => { const taskName = quickRandom(); const task = sampleCustomEndpoint(taskName); functions.register(taskName, (context, complete) => complete({ foo: 'bar' }).ok().next()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(200); result.response.body.should.eql(task.response.body); result.request.body.should.not.eql(result.response.body); done(); }); }); it('should return a 201 created', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); functions.register(taskName, (context, complete) => complete({ foo: 'bar' }).created().done()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(201); result.response.body.should.eql({ foo: 'bar' }); done(); }); }); it('should return a 202 accepted', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); functions.register(taskName, (context, complete) => complete({ foo: 'bar' }).accepted().done()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(202); result.response.body.should.eql({ foo: 'bar' }); done(); }); }); it('should return a 400 bad request', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); functions.register(taskName, (context, complete) => complete('This is a bad request').badRequest().done()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(400); result.response.body.error.should.eql('BadRequest'); result.response.body.description.should.eql('Unable to understand request'); result.response.body.debug.should.eql('This is a bad request'); done(); }); }); it('should return a 401 unauthorized', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); functions.register(taskName, (context, complete) => complete('You are not authorized!').unauthorized().done()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(401); result.response.body.error.should.eql('InvalidCredentials'); result.response.body.description.should.eql( 'Invalid credentials. Please retry your request with correct credentials'); result.response.body.debug.should.eql('You are not authorized!'); done(); }); }); it('should return a 403 forbidden', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); functions.register(taskName, (context, complete) => complete('Forbidden!').forbidden().done()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(403); result.response.body.error.should.eql('Forbidden'); result.response.body.description.should.eql('The request is forbidden'); result.response.body.debug.should.eql('Forbidden!'); done(); }); }); it('should return a 404 not found', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); functions.register(taskName, (context, complete) => complete('The request is not found!').notFound().done()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(404); result.response.body.error.should.eql('NotFound'); result.response.body.description.should.eql( 'The requested entity or entities were not found in the serviceObject'); result.response.body.debug.should.eql('The request is not found!'); done(); }); }); it('should return a 405 not allowed', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); functions.register(taskName, (context, complete) => complete('The request is not allowed!').notAllowed().done()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(405); result.response.body.error.should.eql('NotAllowed'); result.response.body.description.should.eql('The request is not allowed'); result.response.body.debug.should.eql('The request is not allowed!'); done(); }); }); it('should return a 501 not implemented', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); functions.register(taskName, (context, complete) => complete('This isn\'t implemented').notImplemented().done()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(501); result.response.body.error.should.eql('NotImplemented'); result.response.body.description.should.eql('The request invoked a method that is not implemented'); result.response.body.debug.should.eql('This isn\'t implemented'); done(); }); }); it('should return a 550 runtime error', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); functions.register(taskName, (context, complete) => complete('There was some error in the app!') .runtimeError().done()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(550); result.response.body.error.should.eql('FlexRuntimeError'); result.response.body.description.should.eql( 'The Flex Service had a runtime error. See debug message for details' ); result.response.body.debug.should.eql('There was some error in the app!'); done(); }); }); it('should process a next (continuation) handler', (done) => { const taskName = quickRandom(); const task = samplePostTask(taskName); functions.register(taskName, (context, complete) => complete({ foo: 'bar' }).ok().next()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(200); result.response.body.should.eql({ foo: 'bar' }); result.response.continue = true; done(); }); }); it('should process a done (completion) handler', (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); functions.register(taskName, (context, complete) => complete({ foo: 'bar' }).ok().done()); functions.process(task, {}, (err, result) => { should.not.exist(err); result.response.statusCode.should.eql(200); result.response.body.should.eql({ foo: 'bar' }); result.response.continue = false; done(); }); }); ['next', 'done'].forEach((method1) => { ['next', 'done'].forEach((method2) => { it(`should log a message when attempting to respond more than once, by calling ${method1}() and then ${method2}()`, (done) => { const taskName = quickRandom(); const task = sampleTask(taskName); functions.register(taskName, (context, complete) => { complete({ baz: 'bar' }).ok()[method1](); setTimeout(() => { complete({ baz: 'not bar' }).ok()[method2](); }, 0); }); const processCallbackSpy = sinon.spy((err, result) => { should.not.exist(err); result.response.statusCode.should.eql(200); const expectedBody = method1 === 'next' ? result.request.body : result.response.body; expectedBody.should.eql({ baz: 'bar' }); result.response.continue.should.eql(method1 === 'next'); }); loggerMock.error = sinon.spy((message) => { message.should.eql(`Invoked done() or next() more than once to the same Flex Functions request to "${task.taskName}"`); done(); }); functions.process(task, {}, processCallbackSpy); }); }); }); }); });