/* * Copyright (C) 2018 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ import { SCIndexResponse, SCIndexRoute, SCMessage, SCMultiSearchResponse, SCMultiSearchRoute, SCSearchRequest, SCSearchResponse, SCSearchRoute, SCThingOriginType, SCThingType, } from '@openstapps/core'; import {expect} from 'chai'; import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import chaiSpies from 'chai-spies'; import {suite, test} from '@testdeck/mocha'; import {Client} from '../src/client'; import {ApiError, OutOfRangeError} from '../src/errors'; import {HttpClient} from '../src/http-client'; import {HttpClientResponse} from '../src/http-client-interface'; chai.should(); chai.use(chaiSpies); chai.use(chaiAsPromised); const sandbox = chai.spy.sandbox(); const indexRoute = new SCIndexRoute(); const multiSearchRoute = new SCMultiSearchRoute(); const searchRoute = new SCSearchRoute(); const httpClient = new HttpClient(); /** * Recursive Partial * * @see https://stackoverflow.com/a/51365037 */ export type RecursivePartial = { [P in keyof T]?: T[P] extends Array<(infer U)> ? Array> : T[P] extends object ? RecursivePartial : T[P]; }; async function invokeIndexRoute(): Promise>> { return { body: { app: { features: {}, }, backend: { SCVersion: 'foo.bar.dummy', }, }, statusCode: indexRoute.statusCodeSuccess, }; } async function invokeIndexRouteFails(): Promise>> { return { body: { backend: { SCVersion: 'foo.bar.dummy', }, }, statusCode: indexRoute.statusCodeSuccess + 1, }; } @suite() export class ClientSpec { async after() { sandbox.restore(); } @test async construct() { expect(() => { return new Client(httpClient, 'http://localhost'); }).not.to.throw(); } @test async constructWithHeaders() { sandbox.on(httpClient, 'request', invokeIndexRoute); expect(httpClient.request).not.to.have.been.first.called(); const client = new Client(httpClient, 'http://localhost', 'foo.foo.foo'); await client.handshake('foo.bar.dummy'); expect(httpClient.request).to.have.been.first.called.with({ body: {}, headers: { 'Content-Type': 'application/json', 'X-StApps-Version': 'foo.foo.foo', }, method: indexRoute.method, url: new URL('http://localhost' + indexRoute.getUrlPath()), }); } @test async getThing() { const message: SCMessage = { audiences: [ 'employees', ], categories: [ 'news' ], messageBody: 'Lorem ipsum.', name: 'foo', origin: { indexed: 'foo', name: 'foo', type: SCThingOriginType.Remote, }, type: SCThingType.Message, uid: 'foo', }; sandbox.on(httpClient, 'request', async (): Promise> => { return { body: { data: [message], facets: [], pagination: { count: 0, offset: 0, total: 0, }, stats: { time: 0, }, }, headers: {}, statusCode: searchRoute.statusCodeSuccess, }; }); expect(httpClient.request).not.to.have.been.first.called(); const client = new Client(httpClient, 'http://localhost'); await client.getThing('foo'); expect(httpClient.request).to.have.been.first.called.with({ body: { filter: { arguments: { field: 'uid', value: 'foo', }, type: 'value', }, size: 1, }, headers: { "Content-Type": "application/json", }, method: searchRoute.method, url: new URL('http://localhost' + searchRoute.getUrlPath()), }); } @test async getThingFailsByEmptyResponse() { sandbox.on(httpClient, 'request', async (): Promise> => { return { body: { data: [], facets: [], pagination: { count: 0, offset: 0, total: 0, }, stats: { time: 0, }, }, headers: {}, statusCode: searchRoute.statusCodeSuccess, }; }); expect(httpClient.request).not.to.have.been.first.called(); const client = new Client(httpClient, 'http://localhost'); return client.getThing('bar').should.be.rejected; } @test async getThingFailsByUid() { const message: SCMessage = { audiences: [ 'employees', ], categories: [ 'news' ], messageBody: 'Lorem ipsum.', name: 'foo', origin: { indexed: 'foo', name: 'foo', type: SCThingOriginType.Remote, }, type: SCThingType.Message, uid: 'foo', }; sandbox.on(httpClient, 'request', async (): Promise> => { return { body: { data: [message], facets: [], pagination: { count: 0, offset: 0, total: 0, }, stats: { time: 0, }, }, headers: {}, statusCode: searchRoute.statusCodeSuccess, }; }); expect(httpClient.request).not.to.have.been.first.called(); const client = new Client(httpClient, 'http://localhost'); return client.getThing('bar').should.be.rejected; } @test async handshake() { sandbox.on(httpClient, 'request', invokeIndexRoute); expect(httpClient.request).not.to.have.been.first.called(); const client = new Client(httpClient, 'http://localhost'); await client.handshake('foo.bar.dummy'); expect(httpClient.request).to.have.been.first.called.with({ body: {}, headers: { "Content-Type": "application/json", }, method: indexRoute.method, url: new URL('http://localhost' + indexRoute.getUrlPath()), }); } @test async handshakeFails() { sandbox.on(httpClient, 'request', invokeIndexRoute); expect(httpClient.request).not.to.have.been.first.called(); const client = new Client(httpClient, 'http://localhost'); return client.handshake('bar.bar.dummy').should.be.rejectedWith(ApiError); } @test async invokePlugin() { sandbox.on(httpClient, 'request', async(): Promise>> => { return { body: { app: { features: { plugins: { "supportedPlugin": { urlPath: "/" } }, }, }, }, statusCode: indexRoute.statusCodeSuccess, }; }); expect(httpClient.request).not.to.have.been.first.called(); const client = new Client(httpClient, 'http://localhost'); await client.invokePlugin('unsupportedPlugin').should.be.rejectedWith(ApiError,/.*supportedPlugin.*/gmi); // again with cached feature definitions return client.invokePlugin('supportedPlugin') .should.not.be.rejectedWith(ApiError,/.*supportedPlugin.*/gmi); } @test async invokePluginUnavailable() { sandbox.on(httpClient, 'request', async(): Promise>> => { return { body: {}, statusCode: indexRoute.statusCodeSuccess, }; }); expect(httpClient.request).not.to.have.been.first.called(); const client = new Client(httpClient, 'http://localhost'); await client.invokePlugin('supportedPlugin').should.be.rejectedWith(ApiError,/.*supportedPlugin.*/gmi); sandbox.restore(); sandbox.on(httpClient, 'request', async(): Promise>> => { return { body: { app: { features: { plugins: { 'unsupportedPlugin': { urlPath: '/unsupported-plugin' }, }, }, }, }, statusCode: indexRoute.statusCodeSuccess, }; }); // again with cached feature definitions return client.invokePlugin('supportedPlugin') .should.be.rejectedWith(ApiError,/.*supportedPlugin.*/gmi); } @test async invokeRoute() { sandbox.on(httpClient, 'request', invokeIndexRoute); expect(httpClient.request).not.to.have.been.first.called(); const client = new Client(httpClient, 'http://localhost'); await client.invokeRoute(indexRoute); expect(httpClient.request).to.have.been.first.called.with({ body: undefined, headers: { "Content-Type": "application/json", }, method: indexRoute.method, url: new URL('http://localhost' + indexRoute.getUrlPath()), }); } @test async invokeRouteFails() { sandbox.on(httpClient, 'request', invokeIndexRouteFails); expect(httpClient.request).not.to.have.been.first.called(); const client = new Client(httpClient, 'http://localhost'); return client.invokeRoute(indexRoute).should.be.rejectedWith(ApiError); } @test async multiSearch() { sandbox.on(httpClient, 'request', async (): Promise> => { return { body: { a: { data: [], facets: [], pagination: { count: 0, offset: 0, total: 0, }, stats: { time: 0, }, }, b: { data: [], facets: [], pagination: { count: 0, offset: 0, total: 0, }, stats: { time: 0, }, }, }, headers: {}, statusCode: searchRoute.statusCodeSuccess, }; }); expect(httpClient.request).not.to.have.been.first.called(); const client = new Client(httpClient, 'http://localhost'); await client.multiSearch({a: {size: 1}, b: {size: 1}}); expect(httpClient.request).to.have.been.first.called.with({ body: {a: {size: 1}, b: {size: 1}}, headers: { "Content-Type": "application/json", }, method: multiSearchRoute.method, url: new URL('http://localhost' + multiSearchRoute.getUrlPath()), }); } @test async multiSearchWithPreflight() { sandbox.on(httpClient, 'request', async (): Promise> => { return { body: { bar: { data: [], facets: [], pagination: { count: 0, offset: 0, total: 500, }, stats: { time: 0, }, }, foo: { data: [], facets: [], pagination: { count: 0, offset: 0, total: 1000, }, stats: { time: 0, }, }, }, headers: {}, statusCode: searchRoute.statusCodeSuccess, }; }); expect(httpClient.request).not.to.have.been.first.called(); const client = new Client(httpClient, 'http://localhost'); await client.multiSearch({foo: {}, bar: {}, foobar: {size: 30}}); expect(httpClient.request).to.have.been.first.called.with({ body: {foo: {size: 0}, bar: {size: 0}}, headers: { "Content-Type": "application/json", }, method: multiSearchRoute.method, url: new URL('http://localhost' + multiSearchRoute.getUrlPath()), }); expect(httpClient.request).to.have.been.second.called.with({ body: {foo: {size: 1000}, bar: {size: 500}, foobar: {size: 30}}, headers: { "Content-Type": "application/json", }, method: multiSearchRoute.method, url: new URL('http://localhost' + multiSearchRoute.getUrlPath()), }); } @test nextWindow() { let searchRequest: SCSearchRequest = {size: 30}; const searchResponse: SCSearchResponse = { data: [], facets: [], pagination: { count: 30, offset: 0, total: 60, }, stats: { time: 0, }, }; searchRequest = Client.nextWindow(searchRequest, searchResponse); expect(searchRequest.from).to.equal(30); searchResponse.pagination.offset = 30; expect(() => { Client.nextWindow(searchRequest, searchResponse); }).to.throw(OutOfRangeError); } @test async search() { sandbox.on(httpClient, 'request', async (): Promise> => { return { body: { data: [], facets: [], pagination: { count: 0, offset: 0, total: 0, }, stats: { time: 0, }, }, headers: {}, statusCode: searchRoute.statusCodeSuccess, }; }); expect(httpClient.request).not.to.have.been.first.called(); const client = new Client(httpClient, 'http://localhost'); await client.search({size: 1}); expect(httpClient.request).to.have.been.first.called.with({ body: {size: 1}, headers: { "Content-Type": "application/json", }, method: searchRoute.method, url: new URL('http://localhost' + searchRoute.getUrlPath()), }); } @test async searchNext() { const searchResponse: SCSearchResponse = { data: [], facets: [], pagination: { count: 30, offset: 0, total: 60, }, stats: { time: 0, }, }; sandbox.on(httpClient, 'request', async (): Promise> => { return { body: searchResponse, headers: {}, statusCode: searchRoute.statusCodeSuccess, }; }); expect(httpClient.request).not.to.have.been.first.called(); const client = new Client(httpClient, 'http://localhost'); await client.searchNext({from: 0, size: 30}, searchResponse); expect(httpClient.request).to.have.been.first.called.with({ body: {from: 30, size: 30}, headers: { "Content-Type": "application/json", }, method: searchRoute.method, url: new URL('http://localhost' + searchRoute.getUrlPath()), }); } @test async searchWithPreflight() { sandbox.on(httpClient, 'request', async (): Promise> => { return { body: { data: [], facets: [], pagination: { count: 0, offset: 0, total: 1000, }, stats: { time: 0, }, }, headers: {}, statusCode: searchRoute.statusCodeSuccess, }; }); expect(httpClient.request).not.to.have.been.first.called(); const client = new Client(httpClient, 'http://localhost'); await client.search({}); expect(httpClient.request).to.have.been.first.called.with({ body: {size: 0}, headers: { "Content-Type": "application/json", }, method: searchRoute.method, url: new URL('http://localhost' + searchRoute.getUrlPath()), }); expect(httpClient.request).to.have.been.second.called.with({ body: {size: 1000}, headers: { "Content-Type": "application/json", }, method: searchRoute.method, url: new URL('http://localhost' + searchRoute.getUrlPath()), }); } }