diff --git a/packages/featureserver/src/helpers/combine-body-query-params.js b/packages/featureserver/src/helpers/combine-body-query-params.js new file mode 100644 index 00000000..dafbd92c --- /dev/null +++ b/packages/featureserver/src/helpers/combine-body-query-params.js @@ -0,0 +1,17 @@ +const _ = require('lodash'); + +function combineBodyQueryParameters(body, query) { + const definedQueryParams = _.pickBy(query, isNotEmptyString); + const definedBodyParams = _.pickBy(body, isNotEmptyString); + + return { + ...definedQueryParams, + ...definedBodyParams, + }; +} + +function isNotEmptyString(str) { + return !_.isString(str) || !_.isEmpty(str); +} + +module.exports = { combineBodyQueryParameters }; diff --git a/packages/featureserver/src/helpers/index.js b/packages/featureserver/src/helpers/index.js index 49b67583..90a97b61 100644 --- a/packages/featureserver/src/helpers/index.js +++ b/packages/featureserver/src/helpers/index.js @@ -12,4 +12,5 @@ module.exports = { ...require('./renderers'), ...require('./validate-inputs'), ...require('./normalize-request-params'), + ...require('./combine-body-query-params') }; diff --git a/packages/featureserver/src/helpers/normalize-request-params.js b/packages/featureserver/src/helpers/normalize-request-params.js index 559a66bc..62b42372 100644 --- a/packages/featureserver/src/helpers/normalize-request-params.js +++ b/packages/featureserver/src/helpers/normalize-request-params.js @@ -1,25 +1,18 @@ const _ = require('lodash'); const defaults = require('../metadata-defaults'); +const { combineBodyQueryParameters } = require('./combine-body-query-params'); function normalizeRequestParameters( query, body, maxRecordCount = defaults.maxRecordCount(), ) { - const definedQueryParams = _.chain(query) - .pickBy(isNotEmptyString) - .mapValues(coerceStrings) - .value(); - - const definedBodyParams = _.chain(body) - .pickBy(isNotEmptyString) - .mapValues(coerceStrings) - .value(); - - const { resultRecordCount, ...params } = { - ...definedQueryParams, - ...definedBodyParams, - }; + const requestParams = combineBodyQueryParameters(body, query); + + const { resultRecordCount, ...params } = _.mapValues( + requestParams, + coerceStrings, + ); return { ...params, @@ -27,10 +20,6 @@ function normalizeRequestParameters( }; } -function isNotEmptyString(str) { - return !_.isString(str) || !_.isEmpty(str); -} - function coerceStrings(val) { if (val === 'false') { return false; diff --git a/packages/featureserver/src/rest-info-route-handler.js b/packages/featureserver/src/rest-info-route-handler.js index b4d42246..ca99f5fd 100644 --- a/packages/featureserver/src/rest-info-route-handler.js +++ b/packages/featureserver/src/rest-info-route-handler.js @@ -1,27 +1,60 @@ const _ = require('lodash'); -const defaults = require('./metadata-defaults'); +const joi = require('joi'); +const metadataDefaults = require('./metadata-defaults'); +const { generalResponseHandler } = require('./response-handlers'); +const { combineBodyQueryParameters } = require('./helpers'); -function restInfo(data = {}, req) { - const versionDefaults = defaults.restInfoDefaults(); +const parameterSchema = joi + .object({ + f: joi.string().valid('json', 'pjson').default('json'), + }) + .unknown(); + +function restInfo(req, res, data = {}) { + const { currentVersion, fullVersion } = getVersions(req.app.locals); + + const requestParams = combineBodyQueryParameters(req.body, req.query); + + validate(requestParams); + + return generalResponseHandler( + res, + { + currentVersion, + fullVersion, + owningSystemUrl: data.owningSystemUrl, + authInfo: { + ...data.authInfo, + }, + }, + requestParams, + ); +} + +function getVersions(locals) { + const versionDefaults = metadataDefaults.restInfoDefaults(); const currentVersion = _.get( - req, - 'app.locals.config.featureServer.currentVersion', + locals, + 'config.featureServer.currentVersion', versionDefaults.currentVersion, ); + const fullVersion = _.get( - req, - 'app.locals.config.featureServer.fullVersion', + locals, + 'config.featureServer.fullVersion', versionDefaults.fullVersion, ); + return { currentVersion, fullVersion }; +} - return { - currentVersion, - fullVersion, - owningSystemUrl: data.owningSystemUrl, - authInfo: { - ...data.authInfo, - }, - }; +function validate(parameters) { + const { error } = parameterSchema.validate(parameters); + + if (error) { + const err = new Error('Invalid format'); + err.code = 400; + throw err; + } } module.exports = restInfo; diff --git a/packages/featureserver/src/rest-info-route-handler.spec.js b/packages/featureserver/src/rest-info-route-handler.spec.js index 6ad71e71..63bb3b54 100644 --- a/packages/featureserver/src/rest-info-route-handler.spec.js +++ b/packages/featureserver/src/rest-info-route-handler.spec.js @@ -1,22 +1,42 @@ const should = require('should'); // eslint-disable-line -const restInfo = require('./rest-info-route-handler'); +const proxyquire = require('proxyquire'); +const sinon = require('sinon'); const CURRENT_VERSION = 11.2; const FULL_VERSION = '11.2.0'; describe('rest/info handler', () => { + const handlerSpy = sinon.spy(); + const restInfo = proxyquire('./rest-info-route-handler', { + './response-handlers': { + generalResponseHandler: handlerSpy, + }, + }); + + afterEach(() => { + handlerSpy.resetHistory(); + }); + it('should return default info', () => { const req = { app: { locals: {}, }, + query: {}, + body: {}, }; - const result = restInfo(undefined, req); - result.should.deepEqual({ - currentVersion: CURRENT_VERSION, - fullVersion: FULL_VERSION, - authInfo: {}, - owningSystemUrl: undefined, - }); + + restInfo(req, {}); + handlerSpy.callCount.should.equal(1); + handlerSpy.firstCall.args.should.deepEqual([ + {}, + { + currentVersion: CURRENT_VERSION, + fullVersion: FULL_VERSION, + authInfo: {}, + owningSystemUrl: undefined, + }, + {}, + ]); }); it('should return default plus supplied info', () => { @@ -29,13 +49,18 @@ describe('rest/info handler', () => { locals: {}, }, }; - const result = restInfo(data, req); - result.should.deepEqual({ - currentVersion: CURRENT_VERSION, - fullVersion: FULL_VERSION, - authInfo: { foo: 'bar' }, - owningSystemUrl: 'helloworld', - }); + restInfo(req, {}, data); + handlerSpy.callCount.should.equal(1); + handlerSpy.firstCall.args.should.deepEqual([ + {}, + { + currentVersion: CURRENT_VERSION, + fullVersion: FULL_VERSION, + authInfo: { foo: 'bar' }, + owningSystemUrl: 'helloworld', + }, + {}, + ]); }); it('should return versions from app.locals', () => { @@ -51,17 +76,22 @@ describe('rest/info handler', () => { }, }, }; - const result = restInfo( - { authInfo: { foo: 'bar' }, owningSystemUrl: 'helloworld' }, + + restInfo( req, + {}, + { authInfo: { foo: 'bar' }, owningSystemUrl: 'helloworld' }, ); - result.should.deepEqual({ - currentVersion: 10.81, - fullVersion: '10.8.1', - authInfo: { - foo: 'bar', + handlerSpy.callCount.should.equal(1); + handlerSpy.firstCall.args.should.deepEqual([ + {}, + { + currentVersion: 10.81, + fullVersion: '10.8.1', + authInfo: { foo: 'bar' }, + owningSystemUrl: 'helloworld', }, - owningSystemUrl: 'helloworld', - }); + {}, + ]); }); }); diff --git a/packages/featureserver/src/route.js b/packages/featureserver/src/route.js index 76870b11..27d00f01 100644 --- a/packages/featureserver/src/route.js +++ b/packages/featureserver/src/route.js @@ -36,8 +36,7 @@ module.exports = function route(req, res, geojson = {}) { geojson.metadata = geojson.metadata || { maxRecordCount: 2000 }; if (isRestInfoRequest(route)) { - const result = restInfo(geojson, req); - return generalResponseHandler(res, result, req.query); + return restInfo(req, res, geojson); } if (isServerMetadataRequest(route)) { diff --git a/packages/featureserver/src/route.spec.js b/packages/featureserver/src/route.spec.js index 6db6187e..b182564c 100644 --- a/packages/featureserver/src/route.spec.js +++ b/packages/featureserver/src/route.spec.js @@ -111,21 +111,15 @@ describe('Route module unit tests', () => { ); restInfoSpy.calledOnce.should.equal(true); restInfoSpy.firstCall.args.should.deepEqual([ - { - metadata: { maxRecordCount: 2000 }, - }, { params: {}, query: { resultRecordCount: 2000 }, url: '/rest/info', }, - ]); - - responseHandlerSpy.calledOnce.should.equal(true); - responseHandlerSpy.firstCall.args.should.deepEqual([ {}, - { restInfo: true }, - { resultRecordCount: 2000 }, + { + metadata: { maxRecordCount: 2000 }, + }, ]); }); @@ -149,14 +143,15 @@ describe('Route module unit tests', () => { ); restInfoSpy.calledOnce.should.equal(true); restInfoSpy.firstCall.args.should.deepEqual([ - { - metadata: { maxRecordCount: 2000 }, - }, { params: {}, query: { resultRecordCount: 2000 }, url: '/rest/info', }, + {}, + { + metadata: { maxRecordCount: 2000 }, + }, ]); responseHandlerSpy.calledOnce.should.equal(true); diff --git a/packages/featureserver/test/integration/info.spec.js b/packages/featureserver/test/integration/info.spec.js index ca748c65..5ec6a212 100644 --- a/packages/featureserver/test/integration/info.spec.js +++ b/packages/featureserver/test/integration/info.spec.js @@ -6,30 +6,6 @@ const _ = require('lodash'); const CURRENT_VERSION = 11.2; describe('Info operations', () => { - describe('rest info', () => { - it('should conform to the prescribed schema', () => { - const req = { - app: { - locals: {}, - }, - }; - - const supplementalRestInfo = { - authInfo: { - isTokenBasedSecurity: true, - tokenServicesUrl: 'http://localhost/provider/generateToken', - }, - }; - const restInfo = FeatureServer.restInfo(supplementalRestInfo, req); - restInfo.should.have.property('currentVersion', CURRENT_VERSION); - restInfo.should.have.property('authInfo'); - restInfo.authInfo.should.have.property('isTokenBasedSecurity', true); - restInfo.authInfo.should.have - .property('tokenServicesUrl') - .be.type('string'); - }); - }); - describe('server info', () => { it('should conform to the prescribed schema', () => { const result = FeatureServer.serverInfo(data); diff --git a/packages/output-geoservices/src/index.js b/packages/output-geoservices/src/index.js index c072beae..5d26125b 100644 --- a/packages/output-geoservices/src/index.js +++ b/packages/output-geoservices/src/index.js @@ -1,4 +1,11 @@ const FeatureServer = require('@koopjs/featureserver'); +const { + restInfo, + // serverInfo, + // layerInfo, + // layersInfo, + // query, +} = require('@koopjs/featureserver'); const Logger = require('@koopjs/logger'); let logger = new Logger(); const ARCGIS_UNAUTHORIZED_MESSAGE = 'Item does not exist or is inaccessible.'; @@ -182,7 +189,11 @@ class GeoServices { ); } - FeatureServer.route(req, res, { + /* + const result = restInfo(geojson, req); + return generalResponseHandler(res, result, req.query); + */ + restInfo(req, res, { owningSystemUrl: this.#buildOwningSystemUrl( req.headers.host, req.baseUrl, diff --git a/packages/output-geoservices/src/index.spec.js b/packages/output-geoservices/src/index.spec.js index 82079f1b..2932019e 100644 --- a/packages/output-geoservices/src/index.spec.js +++ b/packages/output-geoservices/src/index.spec.js @@ -5,6 +5,7 @@ jest.mock('@koopjs/featureserver', () => ({ setLogger: jest.fn(), route: jest.fn(), setDefaults: jest.fn(), + restInfo: jest.fn() })); const loggerMock = { @@ -272,8 +273,8 @@ describe('Output Geoservices', () => { authInfo: { food: 'baz' }, }); await output.restInfoHandler(reqMock, resMock); - expect(FeatureServer.route.mock.calls.length).toBe(1); - expect(FeatureServer.route.mock.calls[0]).toEqual([ + expect(FeatureServer.restInfo.mock.calls.length).toBe(1); + expect(FeatureServer.restInfo.mock.calls[0]).toEqual([ reqMock, resMock, { @@ -290,8 +291,8 @@ describe('Output Geoservices', () => { }; const output = new OutputGeoServices(modelMock); await output.restInfoHandler(reqMock, resMock); - expect(FeatureServer.route.mock.calls.length).toBe(1); - expect(FeatureServer.route.mock.calls[0]).toEqual([ + expect(FeatureServer.restInfo.mock.calls.length).toBe(1); + expect(FeatureServer.restInfo.mock.calls[0]).toEqual([ reqMock, resMock, { @@ -314,8 +315,8 @@ describe('Output Geoservices', () => { useHttpForTokenUrl: true, }); await output.restInfoHandler(reqMock, resMock); - expect(FeatureServer.route.mock.calls.length).toBe(1); - expect(FeatureServer.route.mock.calls[0]).toEqual([ + expect(FeatureServer.restInfo.mock.calls.length).toBe(1); + expect(FeatureServer.restInfo.mock.calls[0]).toEqual([ reqMock, resMock, { @@ -338,8 +339,8 @@ describe('Output Geoservices', () => { process.env.GEOSERVICES_HTTP = 'true'; const output = new OutputGeoServices(modelMock); await output.restInfoHandler(reqMock, resMock); - expect(FeatureServer.route.mock.calls.length).toBe(1); - expect(FeatureServer.route.mock.calls[0]).toEqual([ + expect(FeatureServer.restInfo.mock.calls.length).toBe(1); + expect(FeatureServer.restInfo.mock.calls[0]).toEqual([ reqMock, resMock, { @@ -367,8 +368,8 @@ describe('Output Geoservices', () => { process.env.KOOP_AUTH_HTTP = 'true'; const output = new OutputGeoServices(modelMock); await output.restInfoHandler(reqMock, resMock); - expect(FeatureServer.route.mock.calls.length).toBe(1); - expect(FeatureServer.route.mock.calls[0]).toEqual([ + expect(FeatureServer.restInfo.mock.calls.length).toBe(1); + expect(FeatureServer.restInfo.mock.calls[0]).toEqual([ reqMock, resMock, { @@ -398,8 +399,8 @@ describe('Output Geoservices', () => { const output = new OutputGeoServices(modelMock); await output.restInfoHandler(reqMock, resMock); - expect(FeatureServer.route.mock.calls.length).toBe(1); - expect(FeatureServer.route.mock.calls[0]).toEqual([ + expect(FeatureServer.restInfo.mock.calls.length).toBe(1); + expect(FeatureServer.restInfo.mock.calls[0]).toEqual([ reqMock, resMock, { diff --git a/test/geoservice-rest-info.spec.js b/test/geoservice-rest-info.spec.js new file mode 100644 index 00000000..aa8ced52 --- /dev/null +++ b/test/geoservice-rest-info.spec.js @@ -0,0 +1,41 @@ +const Koop = require('@koopjs/koop-core'); +const provider = require('@koopjs/provider-file-geojson'); +const request = require('supertest'); +const mockLogger = { + debug: () => {}, + info: () => {}, + silly: () => {}, + warn: () => {}, + error: () => {}, +}; + +describe('Feature Server Output - rest/info', () => { + const koop = new Koop({ logLevel: 'error', logger: mockLogger }); + koop.register(provider, { dataDir: './test/provider-data' }); + + test('return expected result', async () => { + try { + const response = await request(koop.server).get( + '/file-geojson/rest/info', + ); + expect(response.status).toBe(200); + const { + body: { + authInfo: { isTokenBasedSecurity, tokenServicesUrl }, + currentVersion, + fullVersion, + owningSystemUrl, + }, + } = response; + + expect(isTokenBasedSecurity).toBe(true); + expect(tokenServicesUrl).toMatch(/file-geojson\/rest\/generateToken$/); + expect(currentVersion).toBe(11.2); + expect(fullVersion).toBe('11.2.0'); + expect(owningSystemUrl).toMatch(/file-geojson$/); + } catch (error) { + console.error(error); + throw error; + } + }); +});