UNPKG

graphql2rest

Version:

GraphQL to REST converter: automatically generate a RESTful API from your existing GraphQL API

974 lines (862 loc) 28 kB
const path = require('path'); const {test} = require('mocha'); const chai = require('chai'); const chaifs = require('chai-fs'); const rmwareHttpMock = require('run-middleware'); const {buildSchema} = require('graphql'); const schemaFile = require('../test-fixtures/schemas/basic-graphql-schema-1'); const GraphQL2REST = require('../../src/index'); const {expect} = chai; const { loggerMock, clearBuffer, } = require('../mocks/logger'); const {execute} = require('../mocks/graphql'); const GQL_OUTPUT_FOLDER = path.resolve(__dirname, '../test-outputs/gqloutput'); const { clearEnv, getOptions } = require('../testUtils') chai.use(chaifs); const prepareEnv = (schema, restRouter) => { clearEnv([GQL_OUTPUT_FOLDER]); rmwareHttpMock(restRouter); } describe('GraphQL2REST router - forwards requests to underlying GraphQL layer and correctly processes responses', () => { const schema = buildSchema(schemaFile.schema); GraphQL2REST.generateGqlQueryFiles(schema, GQL_OUTPUT_FOLDER, undefined, loggerMock); let restRouter = GraphQL2REST.init(schema, execute, getOptions(GQL_OUTPUT_FOLDER)); let responseBody = null; let statusCode = 0; before(() => prepareEnv(schema, restRouter)); after(() => { responseBody = null; statusCode = 0; clearEnv([GQL_OUTPUT_FOLDER]); }); describe('GET', () => { describe('/testApp/v1/api/tweets/10 - successful GET call without body: ', () => { before((done) => { loggerMock.supressDebugLevel = true; clearBuffer(); restRouter.runMiddleware('/testApp/v1/api/tweets/10', { method: 'get' }, (code, body) => { responseBody = body; statusCode = code; done(); }); }); test('responds with 200 OK status code', () => { expect(statusCode).to.equal(200); }); test('response body is correct', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody.oid).to.equal('0123-45687'); expect(responseBody.active).to.equal(true); }); test('REST parameters were passed correctly to GraphQL mock layer', () => { expect(responseBody.vars).to.deep.equal({id: '10'}); }); test('GraphQL operation "Tweet()" was invoked', () => { expect(responseBody.queryStrReceived.definitions[0].name.value).to.equal('Tweet'); }); }); describe('/testApp/v1/api/users - successful GET call returning array of objects: ', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/users', { method: 'get', body: { returnArrayResponse: true } }, (code, body) => { responseBody = body; statusCode = code; done(); }); }); test('responds with 200 OK status code', () => { expect(statusCode).to.equal(200); }); test('response body is correct', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody).to.deep.equal( [{ oid: '1', title: 'The Best Result Ever #1', elements: ['element1', 'element2'], subObj: { subObjId: 500 } }, { oid: '2', title: 'The Best Result Ever #2', elements: ['element1', 'element2'], subObj: { subObjId: 501 } } ] ); }); }); describe('/testApp/v1/api/users with field filtering - successful GET call returning array of objects with only specified fields: ', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/users', { method: 'get', body: { returnArrayResponse: true }, query: { fields: 'subObj.subObjId, title' } }, (code, body) => { responseBody = body; statusCode = code; done(); }); }); test('responds with 200 OK status code', () => { expect(statusCode).to.equal(200); }); test('response body is correct', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); console.dir(responseBody); expect(responseBody).to.deep.equal( [{ title: 'The Best Result Ever #1', subObj: { subObjId: 500 } }, { title: 'The Best Result Ever #2', subObj: { subObjId: 501 } } ] ); }); }); describe('/testApp/v1/api/users with JMESPath filter expression: ', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/users', { method: 'get', body: { returnArrayResponse: true }, query: { fields: ':[].{Title: title, ObjId: oid, SubId: subObj.subObjId}' } }, (code, body) => { responseBody = body; statusCode = code; done(); }); }); test('responds with 200 OK status code', () => { expect(statusCode).to.equal(200); }); test('response body is correct', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody).to.deep.equal( [ { Title: 'The Best Result Ever #1', ObjId: '1', SubId: 500 }, { Title: 'The Best Result Ever #2', ObjId: '2', SubId: 501 } ] ); }); }); describe('/testApp/v1/api/users with JMESPath filter expression on an array, that results in null in JMESPath language returns empty array: ', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/users', { method: 'get', body: { returnArrayResponse: true }, query: { fields: ':someNonExistentArray.{Title: title, ObjId: oid, SubId: subObj.subObjId}' } }, (code, body) => { responseBody = body; statusCode = code; done(); }); }); test('responds with 200 OK status code', () => { expect(statusCode).to.equal(200); }); test('response body is correct', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody).to.deep.equal([]); }); }); describe('/testApp/v1/api/users with JMESPath filter expression on an object, that results in null in JMESPath language returns empty object: ', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/users', { method: 'get', query: { fields: ':someNonExistentObj.{Title: title, ObjId: oid, SubId: subObj.subObjId}' } }, (code, body) => { responseBody = body; statusCode = code; done(); }); }); test('responds with 200 OK status code', () => { expect(statusCode).to.equal(200); }); test('response body is correct', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody).to.deep.equal({}); }); }); describe('/testApp/v1/api/users with invalid JMESPath filter expression fails gracefully with empty array: ', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/users', { method: 'get', body: { returnArrayResponse: true }, query: { fields: ':[].[].[]' } }, (code, body) => { responseBody = body; statusCode = code; done(); }); }); test('responds with 200 OK status code', () => { expect(statusCode).to.equal(200); }); test('response body is correct', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody).to.deep.equal([]); }); }); describe('/api/notifications - successful GET call to test manifest condition fork (no query param "myquery", Notifications() should be invoked): ', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/notifications', { method: 'get', body: {} }, (code, body) => { responseBody = body; statusCode = code; done(); }); }); test('error mapping: responds on HTTP POST request with 200 OK', () => { expect(statusCode).to.equal(200); }); test('response body is correct', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody.oid).to.equal('0123-45687'); expect(responseBody.active).to.equal(true); }); test('REST parameters were passed correctly to GraphQL mock layer', () => { expect(responseBody.vars).to.deep.equal({}); }); test('GraphQL operation "Notifications()" was invoked', () => { expect(responseBody.queryStrReceived.definitions[0].name.value).to.equal('Notifications'); }); }); describe('/api/notifications - successful GET call to test manifest condition fork (WITH query param "myquery", NotificationsLegacy() should be invoked): ', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/notifications', { method: 'get', body: {}, query: { myquery: 'names' } }, (code, body) => { responseBody = body; statusCode = code; done(); }); }); test('error mapping: responds on HTTP POST request with 200 OK', () => { expect(statusCode).to.equal(200); }); test('response body is correct', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody.oid).to.equal('0123-45687'); expect(responseBody.active).to.equal(true); }); test('REST parameters were passed correctly to GraphQL mock layer', () => { expect(responseBody.vars).to.deep.equal({ myquery: 'names' }); }); test('GraphQL operation "Notifications()" was invoked', () => { expect(responseBody.queryStrReceived.definitions[0].name.value).to.equal('NotificationsLegacy'); }); }); describe('/api/notifications with "fields" filter; filter on SINGLE field (no query param "myquery", Notifications() should be invoked, response should have one field: ', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/notifications', { method: 'get', query: { fields: ' title ' // whitespaces are on purpose, to test string being trimmed }, body: {} }, (code, body) => { responseBody = body; statusCode = code; done(); }); }); test('error mapping: responds on HTTP POST request with 200 OK', () => { expect(statusCode).to.equal(200); }); test('response body is correct as expected', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody.oid).to.equal(undefined); expect(responseBody).to.deep.equal({ title: 'The Best Result Ever' }); }); }); describe('/api/notifications with "fields" filter; filter on MULTIPLE fields (no query param "myquery", Notifications() should be invoked, response should have two fields: ', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/notifications', { method: 'get', query: { fields: 'title , elements ' // whitespaces are on purpose, to test string being trimmed }, body: {} }, (code, body) => { responseBody = body; statusCode = code; done(); }); }); test('error mapping: responds on HTTP POST request with 200 OK', () => { expect(statusCode).to.equal(200); }); test('response body is correct as expected', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody.oid).to.equal(undefined); expect(responseBody).to.deep.equal({ title: 'The Best Result Ever', elements: ['element1', 'element2'] }); }); }); }) describe('POST', () => { describe('/testApp/v1/api/tweets - successful POST call with body and params renaming: ', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/tweets', { method: 'post', body: { text: 'My Tweet' } }, (code, body) => { responseBody = body; statusCode = code; done(); }); }); test('error mapping: responds on HTTP POST request with 201 CREATED status code', () => { expect(statusCode).to.equal(201); }); test('response body is correct as expected', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody.oid).to.equal('0123-45687'); expect(responseBody.active).to.equal(true); }); test('REST parameters were passed correctly to GraphQL mock layer', () => { expect(responseBody.vars).to.deep.equal({ text: 'My Tweet' // variables are not renamed; instead, GraphQL query param names are reassigned }); }); test('GraphQL parameters are mapped and reassigned correctly', () => { expect(responseBody.queryStrReceived.loc.source.body).to.include('createTweet(tweetBody: $text)'); // parameter reassignment }); test('GraphQL operation "createTweet()" was invoked', () => { expect(responseBody.queryStrReceived.definitions[0].name.value).to.equal('createTweet'); }); }); describe('/testApp/v1/api/tweets - successful POST call with body and params renaming: ', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/tweets', { method: 'post', body: { text: 'My Tweet' } }, (code, body) => { responseBody = body; statusCode = code; done(); }); }); test('error mapping: responds on HTTP POST request with 201 CREATED status code', () => { expect(statusCode).to.equal(201); }); test('response body is correct as expected', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody.oid).to.equal('0123-45687'); expect(responseBody.active).to.equal(true); }); test('REST parameters were passed correctly to GraphQL mock layer', () => { expect(responseBody.vars).to.deep.equal({ text: 'My Tweet' // variables are not renamed; instead, GraphQL query param names are reassigned }); }); test('GraphQL parameters are mapped and reassigned correctly', () => { expect(responseBody.queryStrReceived.loc.source.body).to.include('createTweet(tweetBody: $text)'); // parameter reassignment }); test('GraphQL operation "createTweet()" was invoked', () => { expect(responseBody.queryStrReceived.definitions[0].name.value).to.equal('createTweet'); }); }); describe('/api/notifications - conditional fork with request body wrapped ONCE only): ', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/notifications', { method: 'post', body: { userId: 100, command: '$preprocess', notification: 'notificationDummy' }, }, (code, body) => { responseBody = body; statusCode = code; done(); }); }); test('error mapping: responds on HTTP POST request with 201 CREATED', () => { expect(statusCode).to.equal(201); }); test('response body is correct as expected', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody.oid).to.equal('0123-45687'); expect(responseBody.active).to.equal(true); }); test('request body was mutated correctly - wrapped ONCE with "{wrapper: data: {..} }', () => { const {body} = responseBody.restRequest; expect(body).to.deep.equal({ wrapper: { data: { userId: 100, command: '$preprocess' } } }); }); test('Last GraphQL operation in the chain, "createNotificationLegacy()", was invoked', () => { expect(responseBody.queryStrReceived.definitions[0].name.value).to.equal('createNotificationLegacy'); }); }); describe('/testApp/v1/api/relationships/100/101 - request body should be wrapped in custom property when GraphQL receives it:', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/relationships/100/10', { method: 'post', body: { url: 'http://twitter/rels/100/101/', relationshipDate: '1970-1-1' } }, (code, body) => { responseBody = body; done(); }); }); test('request body is wrapped with "relationship" property before GraphQL receives it, as indicated in the manifest:', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); const {body} = responseBody.restRequest; expect(body).to.deep.equal({ relationship: { url: 'http://twitter/rels/100/101/', relationshipDate: '1970-1-1' } }); }); test('path parameters are and passed as to GraphQL as expected:', () => { expect(responseBody.queryStrReceived.definitions[0].variableDefinitions[0].variable.name.value).to.equal('first'); expect(responseBody.queryStrReceived.definitions[0].variableDefinitions[1].variable.name.value).to.equal('second'); }); }); }) describe('PUT', () => { describe('/api/tweets - parameters marked for deletion in the manifest using "__DELETE__" should not be passed to GraphQL: ', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/tweets', { method: 'put', body: { sort_field: 'user', sort_order: 'asc', deleteMe1: 'This_Param_Should_Be_Ignored', deleteMe2: 'This_Param_Should_Also_Be_Ignored', text: '123_Text', nested: { delete: { me: 'This_Param_Should_Be_Ignored...', anotherParam: 'Should not be omitted.' }, someOtherParam: 'Should not be omitted.' }, array: { of: { objs: [{ kept: true, }, { kept: true, }, { alwaysKept: true } ], nonArray: 100 }, anotherParam: 'Yet another param - should not be omitted' } }, query: { limit: 100, skip: 0 } }, (code, body) => { responseBody = body; statusCode = code; done(); }); }); test('error mapping: responds on HTTP POST request with 204 code', () => { expect(statusCode).to.equal(204); }); test('response body is correct as expected', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody.oid).to.equal('0123-45687'); expect(responseBody.active).to.equal(true); }); test('REST parameters were passed correctly to GraphQL mock layer', () => { expect(responseBody.vars).to.deep.equal({ // "deleteMe1" & "deleteMe2" should be omitted from this object, as well as nested fields "nested.delete.me" and // "deleted" fields in all objects in "array.of.objs[]" array. text: '123_Text', sort_field: 'user', sort_order: 'asc', limit: 100, skip: 0, nested: { delete: { anotherParam: 'Should not be omitted.' }, someOtherParam: 'Should not be omitted.' }, array: { of: { objs: [{ kept: true, }, { kept: true, }, { alwaysKept: true } ], nonArray: 100 }, anotherParam: 'Yet another param - should not be omitted' } }); }); test('GraphQL operation "Tweets()" was invoked', () => { expect(responseBody.queryStrReceived.definitions[0].name.value).to.equal('Tweets'); }); }); describe('/api/tweets - parameters marked for deletion do not affect result if they don\'t exist in payload', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/tweets', { method: 'put', body: { sort_field: 'user', sort_order: 'asc', text: '123_Text', nested: { delete: { anotherParam: 'Should not be omitted.' }, someOtherParam: 'Should not be omitted.' }, array: { of: { objs: [{ kept: true, }, { kept: true, }, { alwaysKept: true } ], nonArray: 100 }, anotherParam: 'Yet another param - should not be omitted' } }, query: { limit: 100, skip: 0 } }, (code, body) => { responseBody = body; statusCode = code; done(); }); }); test('error mapping: responds on HTTP POST request with 204 code', () => { expect(statusCode).to.equal(204); }); test('response body is correct as expected', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody.oid).to.equal('0123-45687'); expect(responseBody.active).to.equal(true); }); test('REST parameters were passed correctly to GraphQL mock layer', () => { expect(responseBody.vars).to.deep.equal({ text: '123_Text', sort_field: 'user', sort_order: 'asc', limit: 100, skip: 0, nested: { delete: { anotherParam: 'Should not be omitted.' }, someOtherParam: 'Should not be omitted.' }, array: { of: { objs: [{ kept: true, }, { kept: true, }, { alwaysKept: true } ], nonArray: 100 }, anotherParam: 'Yet another param - should not be omitted' } }); }); test('GraphQL operation "Tweets()" was invoked', () => { expect(responseBody.queryStrReceived.definitions[0].name.value).to.equal('Tweets'); }); }); describe('/testApp/v1/api/relationships/100/101 - middleware function should be called and modify request object passed to GraphQL:', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/relationships/100/10', { method: 'put', body: { url: 'http://twitter/rels/100/101/', relationshipDate: '1970-1-1' } }, (code, body) => { responseBody = body; done(); }); }); test('request body and route params are changed by the middleware function:', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); const {body, params} = responseBody.restRequest; expect(body).to.deep.equal({ message: 'Touched by GraphQL2REST middleware!', verb: 'PUT', route: '/testApp/v1/api/relationships/:first/:second', operation: 'updateTweetRelationship' }); expect(params).to.deep.equal({ first: 'Touched by GraphQL2REST middleware!', second: 'Touched by GraphQL2REST middleware!' }); }); test('path parameters are and passed as to GraphQL as expected:', () => { expect(responseBody.queryStrReceived.definitions[0].variableDefinitions[0].variable.name.value).to.equal('first'); expect(responseBody.queryStrReceived.definitions[0].variableDefinitions[1].variable.name.value).to.equal('second'); }); test('response body is correct as expected', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody.oid).to.equal('0123-45687'); expect(responseBody.active).to.equal(true); }); }); }) describe('DELETE', () => { describe('request that should emit an error propagating from GraphQL to REST layer: ', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/tweets/2010', { method: 'delete', body: { emitError: 4003, isErrorWithData: false }, query: { myquery: 'names' } }, (code, body) => { responseBody = body; statusCode = code; done(); }); }); test('error mapping: responds on HTTP POST request with 403 FORBIDDEN', () => { expect(statusCode).to.equal(403); }); test('response has no data', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody.oid).to.equal(undefined); expect(responseBody.active).to.equal(undefined); expect(responseBody).to.not.have.property('data'); }); test('response is error only', () => { expect(responseBody).to.have.property('errors'); expect(responseBody.errors).to.be.an('array').that.includes({ message: 'Unauthorized', name: 'UnauthorizedError', locations: [{ line: 2, column: 3 }], path: [ 'testGetOperationToFail' ], extensions: { code: 4003 }, errorDescription: 'User is unauthorized. Please use a correct Bearer token.' }); }); test('GraphQL operation "deleteTweet()" was NOT invoked', () => { expect(responseBody.queryStrReceived).to.equal(undefined); }); }); describe('request on which GraphQL responds with BOTH error AND data should NOT emit an error, should return SUCCESS with data only: ', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/tweets/2010', { method: 'delete', body: { emitError: 999, isErrorWithData: true }, query: { myquery: 'names' } }, (code, body) => { responseBody = body; statusCode = code; done(); }); }); test('error mapping: responds on HTTP POST request with 204 NO CONTENT', () => { expect(statusCode).to.equal(204); }); test('response has correct data', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody).not.to.have.property('data'); // explanation: current GraphQL2REST conversion logic is: { data: { hero: { name: "luke" } } } => { name: "luke"} (REST response favors flat object!) expect(responseBody).not.to.have.property('hero'); expect(responseBody).to.have.property('name'); expect(responseBody.name).to.equal('R2-D2'); }); test('response has no errors array attached', () => { expect(responseBody).not.to.have.property('errors'); }); }); }) describe('PATCH', () => { describe('/testApp/v1/api/tweets/50 - hidden fields in manifest should be omitted from GraphQL response:', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/tweets/50', { method: 'patch', body: { text: 'Body is not important here' } }, (code, body) => { responseBody = body; done(); }); }); test('hidden fields in manifest are omitted from response', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); expect(responseBody.oid).to.equal(undefined); expect(responseBody.active).to.equal(undefined); }); test('fields which are not hidden are sent in the REST response', () => { expect(responseBody.title).to.equal('The Best Result Ever'); }); }); describe('/testApp/v1/api/relationships/100/101 - request body should be inside nested objected when GraphQL receives it:', () => { before((done) => { restRouter.runMiddleware('/testApp/v1/api/relationships/100/10', { method: 'patch', body: { url: 'http://twitter/rels/100/101/', relationshipDate: '1970-1-1' } }, (code, body) => { responseBody = body; done(); }); }); test('request body is inside "{ updateData: { vars: { relationship: { ... } } } }" object before GraphQL receives it, as indicated in the manifest:', () => { expect(responseBody).to.not.equal(null); expect(responseBody).to.not.equal(undefined); const {body} = responseBody.restRequest; expect(body).to.deep.equal({ updateData: { vars: { relationship: { url: 'http://twitter/rels/100/101/', relationshipDate: '1970-1-1' } } } }); }); test('path parameters are and passed as to GraphQL as expected:', () => { expect(responseBody.queryStrReceived.definitions[0].variableDefinitions[0].variable.name.value).to.equal('first'); expect(responseBody.queryStrReceived.definitions[0].variableDefinitions[1].variable.name.value).to.equal('second'); }); }); }) });