From 9d51f30e3db5030ad3827c86d3aa5dfc694d7b4a Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 28 Aug 2018 13:41:23 -0400 Subject: [PATCH 1/4] start to make a public api --- .../server/lib/{errors.js => errors.ts} | 4 +- ..._license.js => route_pre_check_license.ts} | 4 +- .../lib/{space_schema.js => space_schema.ts} | 0 ...ces_url_parser.js => spaces_url_parser.ts} | 6 +- .../spaces/server/routes/api/public/index.ts | 0 .../spaces/server/routes/api/public/spaces.ts | 179 ++++++++++++++++++ 6 files changed, 186 insertions(+), 7 deletions(-) rename x-pack/plugins/spaces/server/lib/{errors.js => errors.ts} (86%) rename x-pack/plugins/spaces/server/lib/{route_pre_check_license.js => route_pre_check_license.ts} (84%) rename x-pack/plugins/spaces/server/lib/{space_schema.js => space_schema.ts} (100%) rename x-pack/plugins/spaces/server/lib/{spaces_url_parser.js => spaces_url_parser.ts} (81%) create mode 100644 x-pack/plugins/spaces/server/routes/api/public/index.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/public/spaces.ts diff --git a/x-pack/plugins/spaces/server/lib/errors.js b/x-pack/plugins/spaces/server/lib/errors.ts similarity index 86% rename from x-pack/plugins/spaces/server/lib/errors.js rename to x-pack/plugins/spaces/server/lib/errors.ts index 6996faeaac8de..59d618e8a0212 100644 --- a/x-pack/plugins/spaces/server/lib/errors.js +++ b/x-pack/plugins/spaces/server/lib/errors.ts @@ -3,9 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +//@ts-ignore import { wrap as wrapBoom } from 'boom'; -export function wrapError(error) { +export function wrapError(error: any) { return wrapBoom(error, error.status); } diff --git a/x-pack/plugins/spaces/server/lib/route_pre_check_license.js b/x-pack/plugins/spaces/server/lib/route_pre_check_license.ts similarity index 84% rename from x-pack/plugins/spaces/server/lib/route_pre_check_license.js rename to x-pack/plugins/spaces/server/lib/route_pre_check_license.ts index 891e9fc4125a9..42fd71703fa0b 100644 --- a/x-pack/plugins/spaces/server/lib/route_pre_check_license.js +++ b/x-pack/plugins/spaces/server/lib/route_pre_check_license.ts @@ -6,10 +6,10 @@ const Boom = require('boom'); -export function routePreCheckLicense(server) { +export function routePreCheckLicense(server: any) { const xpackMainPlugin = server.plugins.xpack_main; const pluginId = 'spaces'; - return function forbidApiAccess(request, reply) { + return function forbidApiAccess(request: any, reply: any) { const licenseCheckResults = xpackMainPlugin.info.feature(pluginId).getLicenseCheckResults(); if (!licenseCheckResults.showSpaces) { reply(Boom.forbidden(licenseCheckResults.linksMessage)); diff --git a/x-pack/plugins/spaces/server/lib/space_schema.js b/x-pack/plugins/spaces/server/lib/space_schema.ts similarity index 100% rename from x-pack/plugins/spaces/server/lib/space_schema.js rename to x-pack/plugins/spaces/server/lib/space_schema.ts diff --git a/x-pack/plugins/spaces/server/lib/spaces_url_parser.js b/x-pack/plugins/spaces/server/lib/spaces_url_parser.ts similarity index 81% rename from x-pack/plugins/spaces/server/lib/spaces_url_parser.js rename to x-pack/plugins/spaces/server/lib/spaces_url_parser.ts index 397863785e86c..787572cc4b36b 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_url_parser.js +++ b/x-pack/plugins/spaces/server/lib/spaces_url_parser.ts @@ -5,8 +5,8 @@ */ import { DEFAULT_SPACE_ID } from '../../common/constants'; -export function getSpaceIdFromPath(requestBasePath = '/', serverBasePath = '/') { - let pathToCheck = requestBasePath; +export function getSpaceIdFromPath(requestBasePath: string = '/', serverBasePath: string = '/'): string { + let pathToCheck: string = requestBasePath; if (serverBasePath && serverBasePath !== '/' && requestBasePath.startsWith(serverBasePath)) { pathToCheck = requestBasePath.substr(serverBasePath.length); @@ -28,7 +28,7 @@ export function getSpaceIdFromPath(requestBasePath = '/', serverBasePath = '/') return spaceId; } -export function addSpaceIdToPath(basePath = '/', spaceId = '', requestedPath = '') { +export function addSpaceIdToPath(basePath: string = '/', spaceId: string = '', requestedPath: string = ''): string { if (requestedPath && !requestedPath.startsWith('/')) { throw new Error(`path must start with a /`); } diff --git a/x-pack/plugins/spaces/server/routes/api/public/index.ts b/x-pack/plugins/spaces/server/routes/api/public/index.ts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/x-pack/plugins/spaces/server/routes/api/public/spaces.ts b/x-pack/plugins/spaces/server/routes/api/public/spaces.ts new file mode 100644 index 0000000000000..44498ac345ac1 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/public/spaces.ts @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { omit } from 'lodash'; +import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; +import { spaceSchema } from '../../../lib/space_schema'; +import { wrapError } from '../../../lib/errors'; +import { isReservedSpace } from '../../../../common/is_reserved_space'; +import { Space } from '../../../../common/model/space'; + +export function initPublicSpacesApi(server: any) { + const routePreCheckLicenseFn = routePreCheckLicense(server); + + function convertSavedObjectToSpace(savedObject: any) { + return { + id: savedObject.id, + ...savedObject.attributes + }; + } + + server.route({ + method: 'GET', + path: '/api/spaces/', + async handler(request: any, reply: any) { + const client = request.getSavedObjectsClient(); + + let spaces; + + try { + const result = await client.find({ + type: 'space', + sortField: 'name.keyword', + }); + + spaces = result.saved_objects.map(convertSavedObjectToSpace); + } catch (error) { + return reply(wrapError(error)); + } + + return reply(spaces); + }, + config: { + pre: [routePreCheckLicenseFn] + } + }); + + server.route({ + method: 'GET', + path: '/api/spaces/{id}', + async handler(request: any, reply: any) { + const spaceId = request.params.id; + + const client = request.getSavedObjectsClient(); + + try { + const response = await client.get('space', spaceId); + + return reply(convertSavedObjectToSpace(response)); + } catch (error) { + return reply(wrapError(error)); + } + }, + config: { + pre: [routePreCheckLicenseFn] + } + }); + + server.route({ + method: 'POST', + path: '/api/spaces', + async handler(request: any, reply: any) { + const client = request.getSavedObjectsClient(); + + const space = omit(request.payload, ['id', '_reserved']); + + const id = request.payload.id; + + const existingSpace = await getSpaceById(client, id); + if (existingSpace) { + return reply(Boom.conflict(`A space with the identifier ${id} already exists. Please choose a different identifier`)); + } + + try { + return reply(await client.create('space', { ...space }, { id, overwrite: false })); + } catch (error) { + return reply(wrapError(error)); + } + + }, + config: { + validate: { + payload: spaceSchema + }, + pre: [routePreCheckLicenseFn] + } + }); + + server.route({ + method: 'PUT', + path: '/api/spaces/{id}', + async handler(request: any, reply: any) { + const client = request.getSavedObjectsClient(); + + const space: Space = omit(request.payload, ['id']); + const id = request.params.id; + + const existingSpace = await getSpaceById(client, id); + + if (existingSpace) { + space._reserved = existingSpace._reserved; + } else { + return reply(Boom.notFound(`Unable to find space with ID ${id}`)); + } + + let result; + try { + result = await client.update('space', id, { ...space }); + } catch (error) { + return reply(wrapError(error)); + } + + const updatedSpace = convertSavedObjectToSpace(result); + return reply(updatedSpace); + }, + config: { + validate: { + payload: spaceSchema + }, + pre: [routePreCheckLicenseFn] + } + }); + + server.route({ + method: 'DELETE', + path: '/api/spaces/{id}', + async handler(request: any, reply: any) { + const client = request.getSavedObjectsClient(); + + const id = request.params.id; + + let result; + + try { + const existingSpace = await getSpaceById(client, id); + if (isReservedSpace(existingSpace)) { + return reply(wrapError(Boom.badRequest('This Space cannot be deleted because it is reserved.'))); + } + + result = await client.delete('space', id); + } catch (error) { + return reply(wrapError(error)); + } + + return reply(result).code(204); + }, + config: { + pre: [routePreCheckLicenseFn] + } + }); + + async function getSpaceById(client: any, spaceId: string) { + try { + const existingSpace = await client.get('space', spaceId); + return { + id: existingSpace.id, + ...existingSpace.attributes + }; + } catch (error) { + if (client.errors.isNotFoundError(error)) { + return null; + } + throw error; + } + } +} From 3efe0eca72fe526fbca8357da61aa93d9d2fa719 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 29 Aug 2018 11:14:57 -0400 Subject: [PATCH 2/4] all Spaces routes to TS, with passing tests --- .../spaces/common/is_reserved_space.ts | 2 +- x-pack/plugins/spaces/index.js | 15 +- .../spaces/public/lib/spaces_manager.ts | 14 +- x-pack/plugins/spaces/server/lib/errors.ts | 2 +- .../server/lib/route_pre_check_license.ts | 2 +- .../plugins/spaces/server/lib/space_schema.ts | 2 +- .../spaces/server/lib/spaces_url_parser.ts | 11 +- .../routes/api/__fixtures__/create_spaces.ts | 29 ++ .../api/__fixtures__/create_test_handler.ts | 121 ++++++++ .../server/routes/api/__fixtures__/index.ts | 15 + .../server/routes/api/public/delete.test.ts | 71 +++++ .../spaces/server/routes/api/public/delete.ts | 45 +++ .../server/routes/api/public/get.test.ts | 72 +++++ .../spaces/server/routes/api/public/get.ts | 64 ++++ .../spaces/server/routes/api/public/index.ts | 10 + .../server/routes/api/public/post.test.ts | 82 +++++ .../spaces/server/routes/api/public/post.ts | 49 +++ .../server/routes/api/public/put.test.ts | 76 +++++ .../spaces/server/routes/api/public/put.ts | 52 ++++ .../spaces/server/routes/api/public/spaces.ts | 179 ----------- .../spaces/server/routes/api/v1/spaces.js | 202 ------------- .../server/routes/api/v1/spaces.test.js | 281 ------------------ .../server/routes/api/v1/spaces.test.ts | 80 +++++ .../spaces/server/routes/api/v1/spaces.ts | 42 +++ .../lib/convert_saved_object_to_space.test.ts | 27 ++ .../lib/convert_saved_object_to_space.ts | 20 ++ .../server/routes/lib/get_space_by_id.ts | 19 ++ .../plugins/spaces/server/routes/lib/index.ts | 8 + 28 files changed, 915 insertions(+), 677 deletions(-) create mode 100644 x-pack/plugins/spaces/server/routes/api/__fixtures__/create_spaces.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/__fixtures__/index.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/public/delete.test.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/public/delete.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/public/get.test.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/public/get.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/public/post.test.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/public/post.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/public/put.test.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/public/put.ts delete mode 100644 x-pack/plugins/spaces/server/routes/api/public/spaces.ts delete mode 100644 x-pack/plugins/spaces/server/routes/api/v1/spaces.js delete mode 100644 x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js create mode 100644 x-pack/plugins/spaces/server/routes/api/v1/spaces.test.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/v1/spaces.ts create mode 100644 x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.test.ts create mode 100644 x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.ts create mode 100644 x-pack/plugins/spaces/server/routes/lib/get_space_by_id.ts create mode 100644 x-pack/plugins/spaces/server/routes/lib/index.ts diff --git a/x-pack/plugins/spaces/common/is_reserved_space.ts b/x-pack/plugins/spaces/common/is_reserved_space.ts index 0889686aa77f5..40acd7630b66c 100644 --- a/x-pack/plugins/spaces/common/is_reserved_space.ts +++ b/x-pack/plugins/spaces/common/is_reserved_space.ts @@ -13,6 +13,6 @@ import { Space } from './model/space'; * @param space the space * @returns boolean */ -export function isReservedSpace(space: Space): boolean { +export function isReservedSpace(space: Space | null): boolean { return get(space, '_reserved', false); } diff --git a/x-pack/plugins/spaces/index.js b/x-pack/plugins/spaces/index.js index 480641cc792f2..b2b533ef98c34 100644 --- a/x-pack/plugins/spaces/index.js +++ b/x-pack/plugins/spaces/index.js @@ -7,7 +7,13 @@ import { resolve } from 'path'; import { validateConfig } from './server/lib/validate_config'; import { checkLicense } from './server/lib/check_license'; -import { initSpacesApi } from './server/routes/api/v1/spaces'; +import { + initGetSpacesApi, + initPostSpacesApi, + initPutSpacesApi, + initDeleteSpacesApi +} from './server/routes/api/public'; +import { initPrivateSpacesApi } from './server/routes/api/v1/spaces'; import { initSpacesRequestInterceptors } from './server/lib/space_request_interceptors'; import { createDefaultSpace } from './server/lib/create_default_space'; import { createSpacesService } from './server/lib/create_spaces_service'; @@ -87,7 +93,12 @@ export const spaces = (kibana) => new kibana.Plugin({ spacesSavedObjectsClientWrapperFactory(spacesService, types) ); - initSpacesApi(server); + initPrivateSpacesApi(server); + + initGetSpacesApi(server); + initPostSpacesApi(server); + initPutSpacesApi(server); + initDeleteSpacesApi(server); initSpacesRequestInterceptors(server); } diff --git a/x-pack/plugins/spaces/public/lib/spaces_manager.ts b/x-pack/plugins/spaces/public/lib/spaces_manager.ts index a6b21f2fa5229..2f9c6687ee9da 100644 --- a/x-pack/plugins/spaces/public/lib/spaces_manager.ts +++ b/x-pack/plugins/spaces/public/lib/spaces_manager.ts @@ -16,34 +16,34 @@ export class SpacesManager extends EventEmitter { constructor(httpAgent: any, chrome: any) { super(); this.httpAgent = httpAgent; - this.baseUrl = chrome.addBasePath(`/api/spaces/v1`); + this.baseUrl = chrome.addBasePath(`/api/spaces`); } public async getSpaces(): Promise { return await this.httpAgent - .get(`${this.baseUrl}/spaces`) + .get(`${this.baseUrl}`) .then((response: IHttpResponse) => response.data); } public async getSpace(id: string): Promise { - return await this.httpAgent.get(`${this.baseUrl}/space/${id}`); + return await this.httpAgent.get(`${this.baseUrl}/${id}`); } public async createSpace(space: Space) { - return await this.httpAgent.post(`${this.baseUrl}/space`, space); + return await this.httpAgent.post(`${this.baseUrl}`, space); } public async updateSpace(space: Space) { - return await this.httpAgent.put(`${this.baseUrl}/space/${space.id}?overwrite=true`, space); + return await this.httpAgent.put(`${this.baseUrl}/${space.id}?overwrite=true`, space); } public async deleteSpace(space: Space) { - return await this.httpAgent.delete(`${this.baseUrl}/space/${space.id}`); + return await this.httpAgent.delete(`${this.baseUrl}/${space.id}`); } public async changeSelectedSpace(space: Space) { return await this.httpAgent - .post(`${this.baseUrl}/space/${space.id}/select`) + .post(`${this.baseUrl}/v1/space/${space.id}/select`) .then((response: IHttpResponse) => { if (response.data && response.data.location) { window.location = response.data.location; diff --git a/x-pack/plugins/spaces/server/lib/errors.ts b/x-pack/plugins/spaces/server/lib/errors.ts index 59d618e8a0212..4f95c175b0f15 100644 --- a/x-pack/plugins/spaces/server/lib/errors.ts +++ b/x-pack/plugins/spaces/server/lib/errors.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -//@ts-ignore +// @ts-ignore import { wrap as wrapBoom } from 'boom'; export function wrapError(error: any) { diff --git a/x-pack/plugins/spaces/server/lib/route_pre_check_license.ts b/x-pack/plugins/spaces/server/lib/route_pre_check_license.ts index 42fd71703fa0b..449836633993c 100644 --- a/x-pack/plugins/spaces/server/lib/route_pre_check_license.ts +++ b/x-pack/plugins/spaces/server/lib/route_pre_check_license.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -const Boom = require('boom'); +import Boom from 'boom'; export function routePreCheckLicense(server: any) { const xpackMainPlugin = server.plugins.xpack_main; diff --git a/x-pack/plugins/spaces/server/lib/space_schema.ts b/x-pack/plugins/spaces/server/lib/space_schema.ts index dc60c10a36bc2..043856235acba 100644 --- a/x-pack/plugins/spaces/server/lib/space_schema.ts +++ b/x-pack/plugins/spaces/server/lib/space_schema.ts @@ -13,5 +13,5 @@ export const spaceSchema = Joi.object({ description: Joi.string(), initials: Joi.string().max(MAX_SPACE_INITIALS), color: Joi.string().regex(/^#[a-z0-9]{6}$/, `6 digit hex color, starting with a #`), - _reserved: Joi.boolean() + _reserved: Joi.boolean(), }).default(); diff --git a/x-pack/plugins/spaces/server/lib/spaces_url_parser.ts b/x-pack/plugins/spaces/server/lib/spaces_url_parser.ts index 787572cc4b36b..14113cbf9d807 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_url_parser.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_url_parser.ts @@ -5,7 +5,10 @@ */ import { DEFAULT_SPACE_ID } from '../../common/constants'; -export function getSpaceIdFromPath(requestBasePath: string = '/', serverBasePath: string = '/'): string { +export function getSpaceIdFromPath( + requestBasePath: string = '/', + serverBasePath: string = '/' +): string { let pathToCheck: string = requestBasePath; if (serverBasePath && serverBasePath !== '/' && requestBasePath.startsWith(serverBasePath)) { @@ -28,7 +31,11 @@ export function getSpaceIdFromPath(requestBasePath: string = '/', serverBasePath return spaceId; } -export function addSpaceIdToPath(basePath: string = '/', spaceId: string = '', requestedPath: string = ''): string { +export function addSpaceIdToPath( + basePath: string = '/', + spaceId: string = '', + requestedPath: string = '' +): string { if (requestedPath && !requestedPath.startsWith('/')) { throw new Error(`path must start with a /`); } diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_spaces.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_spaces.ts new file mode 100644 index 0000000000000..85284e3fc3a1c --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_spaces.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function createSpaces() { + return [ + { + id: 'a-space', + attributes: { + name: 'a space', + }, + }, + { + id: 'b-space', + attributes: { + name: 'b space', + }, + }, + { + id: 'default', + attributes: { + name: 'Default Space', + _reserved: true, + }, + }, + ]; +} diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts new file mode 100644 index 0000000000000..5cfd8626531d0 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-ignore +import { Server } from 'hapi'; +import { createSpaces } from './create_spaces'; + +export interface TestConfig { + [configKey: string]: any; +} + +export interface TestOptions { + setupFn?: (server: any) => void; + testConfig?: TestConfig; + payload?: any; +} + +export type TeardownFn = () => void; + +export interface RequestRunnerResult { + server: any; + mockSavedObjectsClient: any; + response: any; +} + +export type RequestRunner = ( + method: string, + path: string, + options?: TestOptions +) => Promise; + +const baseConfig: TestConfig = { + 'server.basePath': '', +}; + +export function createTestHandler(initApiFn: (server: any) => void) { + const teardowns: TeardownFn[] = []; + + const spaces = createSpaces(); + + const request: RequestRunner = async ( + method: string, + path: string, + options: TestOptions = {} + ) => { + const { + setupFn = () => { + return; + }, + testConfig = {}, + payload, + } = options; + + const server = new Server(); + + const config = { + ...baseConfig, + ...testConfig, + }; + + server.connection({ port: 0 }); + + await setupFn(server); + + server.decorate( + 'server', + 'config', + jest.fn(() => { + return { + get: (key: string) => config[key], + }; + }) + ); + + initApiFn(server); + + server.decorate('request', 'getBasePath', jest.fn()); + server.decorate('request', 'setBasePath', jest.fn()); + + // Mock server.getSavedObjectsClient() + const mockSavedObjectsClient = { + get: jest.fn((type, id) => { + return spaces.filter(s => s.id === id)[0]; + }), + find: jest.fn(() => { + return { + total: spaces.length, + saved_objects: spaces, + }; + }), + create: jest.fn(() => ({})), + update: jest.fn(() => ({})), + delete: jest.fn(), + errors: { + isNotFoundError: jest.fn(() => true), + }, + }; + + server.decorate('request', 'getSavedObjectsClient', () => mockSavedObjectsClient); + + teardowns.push(() => server.stop()); + + return { + server, + mockSavedObjectsClient, + response: await server.inject({ + method, + url: path, + payload, + }), + }; + }; + + return { + request, + teardowns, + }; +} diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/index.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/index.ts new file mode 100644 index 0000000000000..37fe32c80032e --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createSpaces } from './create_spaces'; +export { + createTestHandler, + TestConfig, + TestOptions, + TeardownFn, + RequestRunner, + RequestRunnerResult, +} from './create_test_handler'; diff --git a/x-pack/plugins/spaces/server/routes/api/public/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/public/delete.test.ts new file mode 100644 index 0000000000000..605129449a02d --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/public/delete.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../lib/route_pre_check_license', () => { + return { + routePreCheckLicense: () => (request: any, reply: any) => reply.continue(), + }; +}); + +jest.mock('../../../../../../server/lib/get_client_shield', () => { + return { + getClient: () => { + return { + callWithInternalUser: jest.fn(() => { + return; + }), + }; + }, + }; +}); + +import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__'; +import { initDeleteSpacesApi } from './'; + +describe('Spaces Public API', () => { + let request: RequestRunner; + let teardowns: TeardownFn[]; + + beforeEach(() => { + const setup = createTestHandler(initDeleteSpacesApi); + + request = setup.request; + teardowns = setup.teardowns; + }); + + afterEach(async () => { + await Promise.all(teardowns.splice(0).map(fn => fn())); + }); + + test(`'DELETE spaces/{id}' deletes the space`, async () => { + const { response } = await request('DELETE', '/api/spaces/a-space'); + + const { statusCode } = response; + + expect(statusCode).toEqual(204); + }); + + test('DELETE spaces/{id} pretends to delete a non-existent space', async () => { + const { response } = await request('DELETE', '/api/spaces/not-a-space'); + + const { statusCode } = response; + + expect(statusCode).toEqual(204); + }); + + test(`'DELETE spaces/{id}' cannot delete reserved spaces`, async () => { + const { response } = await request('DELETE', '/api/spaces/default'); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(400); + expect(JSON.parse(payload)).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: 'This Space cannot be deleted because it is reserved.', + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/routes/api/public/delete.ts b/x-pack/plugins/spaces/server/routes/api/public/delete.ts new file mode 100644 index 0000000000000..d73b47e47a2b1 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/public/delete.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { isReservedSpace } from '../../../../common/is_reserved_space'; +import { wrapError } from '../../../lib/errors'; +import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; +import { getSpaceById } from '../../lib'; + +export function initDeleteSpacesApi(server: any) { + const routePreCheckLicenseFn = routePreCheckLicense(server); + + server.route({ + method: 'DELETE', + path: '/api/spaces/{id}', + async handler(request: any, reply: any) { + const client = request.getSavedObjectsClient(); + + const id = request.params.id; + + let result; + + try { + const existingSpace = await getSpaceById(client, id); + if (isReservedSpace(existingSpace)) { + return reply( + wrapError(Boom.badRequest('This Space cannot be deleted because it is reserved.')) + ); + } + + result = await client.delete('space', id); + } catch (error) { + return reply(wrapError(error)); + } + + return reply(result).code(204); + }, + config: { + pre: [routePreCheckLicenseFn], + }, + }); +} diff --git a/x-pack/plugins/spaces/server/routes/api/public/get.test.ts b/x-pack/plugins/spaces/server/routes/api/public/get.test.ts new file mode 100644 index 0000000000000..1755364c14172 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/public/get.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../lib/route_pre_check_license', () => { + return { + routePreCheckLicense: () => (request: any, reply: any) => reply.continue(), + }; +}); + +jest.mock('../../../../../../server/lib/get_client_shield', () => { + return { + getClient: () => { + return { + callWithInternalUser: jest.fn(() => { + return; + }), + }; + }, + }; +}); + +import { Space } from '../../../../common/model/space'; +import { createSpaces, createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__'; +import { initGetSpacesApi } from './get'; + +describe('GET spaces', () => { + let request: RequestRunner; + let teardowns: TeardownFn[]; + const spaces = createSpaces(); + + beforeEach(() => { + const setup = createTestHandler(initGetSpacesApi); + + request = setup.request; + teardowns = setup.teardowns; + }); + + afterEach(async () => { + await Promise.all(teardowns.splice(0).map(fn => fn())); + }); + + test(`'GET spaces' returns all available spaces`, async () => { + const { response } = await request('GET', '/api/spaces'); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(200); + const resultSpaces: Space[] = JSON.parse(payload); + expect(resultSpaces.map(s => s.id)).toEqual(spaces.map(s => s.id)); + }); + + test(`'GET spaces/{id}' returns the space with that id`, async () => { + const { response } = await request('GET', '/api/spaces/default'); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(200); + const resultSpace = JSON.parse(payload); + expect(resultSpace.id).toEqual('default'); + }); + + test(`'GET spaces/{id}' returns 404 when retrieving a non-existent space`, async () => { + const { response } = await request('GET', '/api/spaces/not-a-space'); + + const { statusCode } = response; + + expect(statusCode).toEqual(404); + }); +}); diff --git a/x-pack/plugins/spaces/server/routes/api/public/get.ts b/x-pack/plugins/spaces/server/routes/api/public/get.ts new file mode 100644 index 0000000000000..a99e8c8dd33eb --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/public/get.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { wrapError } from '../../../lib/errors'; +import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; +import { convertSavedObjectToSpace } from '../../lib'; + +export function initGetSpacesApi(server: any) { + const routePreCheckLicenseFn = routePreCheckLicense(server); + + server.route({ + method: 'GET', + path: '/api/spaces', + async handler(request: any, reply: any) { + const client = request.getSavedObjectsClient(); + + let spaces; + + try { + const result = await client.find({ + type: 'space', + sortField: 'name.keyword', + }); + + spaces = result.saved_objects.map(convertSavedObjectToSpace); + } catch (error) { + return reply(wrapError(error)); + } + + return reply(spaces); + }, + config: { + pre: [routePreCheckLicenseFn], + }, + }); + + server.route({ + method: 'GET', + path: '/api/spaces/{id}', + async handler(request: any, reply: any) { + const spaceId = request.params.id; + + const client = request.getSavedObjectsClient(); + + try { + const response = await client.get('space', spaceId); + + return reply(convertSavedObjectToSpace(response)); + } catch (error) { + if (client.errors.isNotFoundError(error)) { + return reply(Boom.notFound()); + } + return reply(wrapError(error)); + } + }, + config: { + pre: [routePreCheckLicenseFn], + }, + }); +} diff --git a/x-pack/plugins/spaces/server/routes/api/public/index.ts b/x-pack/plugins/spaces/server/routes/api/public/index.ts index e69de29bb2d1d..f3a37ff29417e 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/index.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { initDeleteSpacesApi } from './delete'; +export { initGetSpacesApi } from './get'; +export { initPostSpacesApi } from './post'; +export { initPutSpacesApi } from './put'; diff --git a/x-pack/plugins/spaces/server/routes/api/public/post.test.ts b/x-pack/plugins/spaces/server/routes/api/public/post.test.ts new file mode 100644 index 0000000000000..cae6297281166 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/public/post.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../lib/route_pre_check_license', () => { + return { + routePreCheckLicense: () => (request: any, reply: any) => reply.continue(), + }; +}); + +jest.mock('../../../../../../server/lib/get_client_shield', () => { + return { + getClient: () => { + return { + callWithInternalUser: jest.fn(() => { + return; + }), + }; + }, + }; +}); + +import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__'; +import { initPostSpacesApi } from './'; + +describe('Spaces Public API', () => { + let request: RequestRunner; + let teardowns: TeardownFn[]; + + beforeEach(() => { + const setup = createTestHandler(initPostSpacesApi); + + request = setup.request; + teardowns = setup.teardowns; + }); + + afterEach(async () => { + await Promise.all(teardowns.splice(0).map(fn => fn())); + }); + + test('POST /space should create a new space with the provided ID', async () => { + const payload = { + id: 'my-space-id', + name: 'my new space', + description: 'with a description', + }; + + const { mockSavedObjectsClient, response } = await request('POST', '/api/spaces', { payload }); + + const { statusCode } = response; + + expect(statusCode).toEqual(200); + expect(mockSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsClient.create).toHaveBeenCalledWith( + 'space', + { name: 'my new space', description: 'with a description' }, + { id: 'my-space-id', overwrite: false } + ); + }); + + test('POST /space should not allow a space to be updated', async () => { + const payload = { + id: 'a-space', + name: 'my updated space', + description: 'with a description', + }; + + const { response } = await request('POST', '/api/spaces', { payload }); + + const { statusCode, payload: responsePayload } = response; + + expect(statusCode).toEqual(409); + expect(JSON.parse(responsePayload)).toEqual({ + error: 'Conflict', + message: + 'A space with the identifier a-space already exists. Please choose a different identifier', + statusCode: 409, + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/routes/api/public/post.ts b/x-pack/plugins/spaces/server/routes/api/public/post.ts new file mode 100644 index 0000000000000..8631160a2b1ff --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/public/post.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { omit } from 'lodash'; +import { wrapError } from '../../../lib/errors'; +import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; +import { spaceSchema } from '../../../lib/space_schema'; +import { getSpaceById } from '../../lib'; + +export function initPostSpacesApi(server: any) { + const routePreCheckLicenseFn = routePreCheckLicense(server); + + server.route({ + method: 'POST', + path: '/api/spaces', + async handler(request: any, reply: any) { + const client = request.getSavedObjectsClient(); + + const space = omit(request.payload, ['id', '_reserved']); + + const id = request.payload.id; + + const existingSpace = await getSpaceById(client, id); + if (existingSpace) { + return reply( + Boom.conflict( + `A space with the identifier ${id} already exists. Please choose a different identifier` + ) + ); + } + + try { + return reply(await client.create('space', { ...space }, { id, overwrite: false })); + } catch (error) { + return reply(wrapError(error)); + } + }, + config: { + validate: { + payload: spaceSchema, + }, + pre: [routePreCheckLicenseFn], + }, + }); +} diff --git a/x-pack/plugins/spaces/server/routes/api/public/put.test.ts b/x-pack/plugins/spaces/server/routes/api/public/put.test.ts new file mode 100644 index 0000000000000..1acd4fe395565 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/public/put.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +jest.mock('../../../lib/route_pre_check_license', () => { + return { + routePreCheckLicense: () => (request: any, reply: any) => reply.continue(), + }; +}); + +jest.mock('../../../../../../server/lib/get_client_shield', () => { + return { + getClient: () => { + return { + callWithInternalUser: jest.fn(() => { + return; + }), + }; + }, + }; +}); + +import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__'; +import { initPutSpacesApi } from './'; + +describe('Spaces Public API', () => { + let request: RequestRunner; + let teardowns: TeardownFn[]; + + beforeEach(() => { + const setup = createTestHandler(initPutSpacesApi); + + request = setup.request; + teardowns = setup.teardowns; + }); + + afterEach(async () => { + await Promise.all(teardowns.splice(0).map(fn => fn())); + }); + + test('PUT /space should update an existing space with the provided ID', async () => { + const payload = { + id: 'a-space', + name: 'my updated space', + description: 'with a description', + }; + + const { mockSavedObjectsClient, response } = await request('PUT', '/api/spaces/a-space', { + payload, + }); + + const { statusCode } = response; + + expect(statusCode).toEqual(200); + expect(mockSavedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsClient.update).toHaveBeenCalledWith('space', 'a-space', { + name: 'my updated space', + description: 'with a description', + }); + }); + + test('PUT /space should not allow a new space to be created', async () => { + const payload = { + id: 'a-new-space', + name: 'my new space', + description: 'with a description', + }; + + const { response } = await request('PUT', '/api/spaces/a-new-space', { payload }); + + const { statusCode } = response; + + expect(statusCode).toEqual(404); + }); +}); diff --git a/x-pack/plugins/spaces/server/routes/api/public/put.ts b/x-pack/plugins/spaces/server/routes/api/public/put.ts new file mode 100644 index 0000000000000..f75b309bdfe7c --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/public/put.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { omit } from 'lodash'; +import { Space } from '../../../../common/model/space'; +import { wrapError } from '../../../lib/errors'; +import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; +import { spaceSchema } from '../../../lib/space_schema'; +import { convertSavedObjectToSpace, getSpaceById } from '../../lib'; + +export function initPutSpacesApi(server: any) { + const routePreCheckLicenseFn = routePreCheckLicense(server); + + server.route({ + method: 'PUT', + path: '/api/spaces/{id}', + async handler(request: any, reply: any) { + const client = request.getSavedObjectsClient(); + + const space: Space = omit(request.payload, ['id']); + const id = request.params.id; + + const existingSpace = await getSpaceById(client, id); + + if (existingSpace) { + space._reserved = existingSpace._reserved; + } else { + return reply(Boom.notFound(`Unable to find space with ID ${id}`)); + } + + let result; + try { + result = await client.update('space', id, { ...space }); + } catch (error) { + return reply(wrapError(error)); + } + + const updatedSpace = convertSavedObjectToSpace(result); + return reply(updatedSpace); + }, + config: { + validate: { + payload: spaceSchema, + }, + pre: [routePreCheckLicenseFn], + }, + }); +} diff --git a/x-pack/plugins/spaces/server/routes/api/public/spaces.ts b/x-pack/plugins/spaces/server/routes/api/public/spaces.ts deleted file mode 100644 index 44498ac345ac1..0000000000000 --- a/x-pack/plugins/spaces/server/routes/api/public/spaces.ts +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; -import { omit } from 'lodash'; -import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; -import { spaceSchema } from '../../../lib/space_schema'; -import { wrapError } from '../../../lib/errors'; -import { isReservedSpace } from '../../../../common/is_reserved_space'; -import { Space } from '../../../../common/model/space'; - -export function initPublicSpacesApi(server: any) { - const routePreCheckLicenseFn = routePreCheckLicense(server); - - function convertSavedObjectToSpace(savedObject: any) { - return { - id: savedObject.id, - ...savedObject.attributes - }; - } - - server.route({ - method: 'GET', - path: '/api/spaces/', - async handler(request: any, reply: any) { - const client = request.getSavedObjectsClient(); - - let spaces; - - try { - const result = await client.find({ - type: 'space', - sortField: 'name.keyword', - }); - - spaces = result.saved_objects.map(convertSavedObjectToSpace); - } catch (error) { - return reply(wrapError(error)); - } - - return reply(spaces); - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'GET', - path: '/api/spaces/{id}', - async handler(request: any, reply: any) { - const spaceId = request.params.id; - - const client = request.getSavedObjectsClient(); - - try { - const response = await client.get('space', spaceId); - - return reply(convertSavedObjectToSpace(response)); - } catch (error) { - return reply(wrapError(error)); - } - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'POST', - path: '/api/spaces', - async handler(request: any, reply: any) { - const client = request.getSavedObjectsClient(); - - const space = omit(request.payload, ['id', '_reserved']); - - const id = request.payload.id; - - const existingSpace = await getSpaceById(client, id); - if (existingSpace) { - return reply(Boom.conflict(`A space with the identifier ${id} already exists. Please choose a different identifier`)); - } - - try { - return reply(await client.create('space', { ...space }, { id, overwrite: false })); - } catch (error) { - return reply(wrapError(error)); - } - - }, - config: { - validate: { - payload: spaceSchema - }, - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'PUT', - path: '/api/spaces/{id}', - async handler(request: any, reply: any) { - const client = request.getSavedObjectsClient(); - - const space: Space = omit(request.payload, ['id']); - const id = request.params.id; - - const existingSpace = await getSpaceById(client, id); - - if (existingSpace) { - space._reserved = existingSpace._reserved; - } else { - return reply(Boom.notFound(`Unable to find space with ID ${id}`)); - } - - let result; - try { - result = await client.update('space', id, { ...space }); - } catch (error) { - return reply(wrapError(error)); - } - - const updatedSpace = convertSavedObjectToSpace(result); - return reply(updatedSpace); - }, - config: { - validate: { - payload: spaceSchema - }, - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'DELETE', - path: '/api/spaces/{id}', - async handler(request: any, reply: any) { - const client = request.getSavedObjectsClient(); - - const id = request.params.id; - - let result; - - try { - const existingSpace = await getSpaceById(client, id); - if (isReservedSpace(existingSpace)) { - return reply(wrapError(Boom.badRequest('This Space cannot be deleted because it is reserved.'))); - } - - result = await client.delete('space', id); - } catch (error) { - return reply(wrapError(error)); - } - - return reply(result).code(204); - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); - - async function getSpaceById(client: any, spaceId: string) { - try { - const existingSpace = await client.get('space', spaceId); - return { - id: existingSpace.id, - ...existingSpace.attributes - }; - } catch (error) { - if (client.errors.isNotFoundError(error)) { - return null; - } - throw error; - } - } -} diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.js b/x-pack/plugins/spaces/server/routes/api/v1/spaces.js deleted file mode 100644 index 5d250c1c92c07..0000000000000 --- a/x-pack/plugins/spaces/server/routes/api/v1/spaces.js +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; -import { omit } from 'lodash'; -import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; -import { spaceSchema } from '../../../lib/space_schema'; -import { wrapError } from '../../../lib/errors'; -import { isReservedSpace } from '../../../../common/is_reserved_space'; -import { addSpaceIdToPath } from '../../../lib/spaces_url_parser'; - -export function initSpacesApi(server) { - const routePreCheckLicenseFn = routePreCheckLicense(server); - - function convertSavedObjectToSpace(savedObject) { - return { - id: savedObject.id, - ...savedObject.attributes - }; - } - - server.route({ - method: 'GET', - path: '/api/spaces/v1/spaces', - async handler(request, reply) { - const client = request.getSavedObjectsClient(); - - let spaces; - - try { - const result = await client.find({ - type: 'space', - sortField: 'name.keyword', - }); - - spaces = result.saved_objects.map(convertSavedObjectToSpace); - } catch (error) { - return reply(wrapError(error)); - } - - return reply(spaces); - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'GET', - path: '/api/spaces/v1/space/{id}', - async handler(request, reply) { - const spaceId = request.params.id; - - const client = request.getSavedObjectsClient(); - - try { - const response = await client.get('space', spaceId); - - return reply(convertSavedObjectToSpace(response)); - } catch (error) { - return reply(wrapError(error)); - } - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'POST', - path: '/api/spaces/v1/space', - async handler(request, reply) { - const client = request.getSavedObjectsClient(); - - const space = omit(request.payload, ['id', '_reserved']); - - const id = request.payload.id; - - const existingSpace = await getSpaceById(client, id); - if (existingSpace) { - return reply(Boom.conflict(`A space with the identifier ${id} already exists. Please choose a different identifier`)); - } - - try { - return reply(await client.create('space', { ...space }, { id, overwrite: false })); - } catch (error) { - return reply(wrapError(error)); - } - - }, - config: { - validate: { - payload: spaceSchema - }, - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'PUT', - path: '/api/spaces/v1/space/{id}', - async handler(request, reply) { - const client = request.getSavedObjectsClient(); - - const space = omit(request.payload, ['id']); - const id = request.params.id; - - const existingSpace = await getSpaceById(client, id); - - if (existingSpace) { - space._reserved = existingSpace._reserved; - } else { - return reply(Boom.notFound(`Unable to find space with ID ${id}`)); - } - - let result; - try { - result = await client.update('space', id, { ...space }); - } catch (error) { - return reply(wrapError(error)); - } - - const updatedSpace = convertSavedObjectToSpace(result); - return reply(updatedSpace); - }, - config: { - validate: { - payload: spaceSchema - }, - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'DELETE', - path: '/api/spaces/v1/space/{id}', - async handler(request, reply) { - const client = request.getSavedObjectsClient(); - - const id = request.params.id; - - let result; - - try { - const existingSpace = await getSpaceById(client, id); - if (isReservedSpace(existingSpace)) { - return reply(wrapError(Boom.badRequest('This Space cannot be deleted because it is reserved.'))); - } - - result = await client.delete('space', id); - } catch (error) { - return reply(wrapError(error)); - } - - return reply(result).code(204); - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'POST', - path: '/api/spaces/v1/space/{id}/select', - async handler(request, reply) { - const client = request.getSavedObjectsClient(); - - const id = request.params.id; - - try { - const existingSpace = await getSpaceById(client, id); - - const config = server.config(); - - return reply({ - location: addSpaceIdToPath(config.get('server.basePath'), existingSpace.id, config.get('server.defaultRoute')) - }); - - } catch (error) { - return reply(wrapError(error)); - } - } - }); - - async function getSpaceById(client, spaceId) { - try { - const existingSpace = await client.get('space', spaceId); - return { - id: existingSpace.id, - ...existingSpace.attributes - }; - } catch (error) { - if (client.errors.isNotFoundError(error)) { - return null; - } - throw error; - } - } -} diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js b/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js deleted file mode 100644 index fb957e4586343..0000000000000 --- a/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js +++ /dev/null @@ -1,281 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Server } from 'hapi'; -import { initSpacesApi } from './spaces'; - -jest.mock('../../../lib/route_pre_check_license', () => { - return { - routePreCheckLicense: () => (request, reply) => reply.continue() - }; -}); - -jest.mock('../../../../../../server/lib/get_client_shield', () => { - return { - getClient: () => { - return { - callWithInternalUser: jest.fn(() => { }) - }; - } - }; -}); - -const spaces = [{ - id: 'a-space', - attributes: { - name: 'a space', - } -}, { - id: 'b-space', - attributes: { - name: 'b space', - } -}, { - id: 'default', - attributes: { - name: 'Default Space', - _reserved: true - } -}]; - -describe('Spaces API', () => { - const teardowns = []; - let request; - - const baseConfig = { - 'server.basePath': '' - }; - - beforeEach(() => { - request = async (method, path, options = {}) => { - - const { - setupFn = () => { }, - testConfig = {}, - payload, - } = options; - - const server = new Server(); - - const config = { - ...baseConfig, - ...testConfig - }; - - server.connection({ port: 0 }); - - await setupFn(server); - - server.decorate('server', 'config', jest.fn(() => { - return { - get: (key) => config[key] - }; - })); - - initSpacesApi(server); - - server.decorate('request', 'getBasePath', jest.fn()); - server.decorate('request', 'setBasePath', jest.fn()); - - // Mock server.getSavedObjectsClient() - const mockSavedObjectsClient = { - get: jest.fn((type, id) => { - return spaces.filter(s => s.id === id)[0]; - }), - find: jest.fn(() => { - return { - total: spaces.length, - saved_objects: spaces - }; - }), - create: jest.fn(() => ({})), - update: jest.fn(() => ({})), - delete: jest.fn(), - errors: { - isNotFoundError: jest.fn(() => true) - } - }; - - server.decorate('request', 'getSavedObjectsClient', () => mockSavedObjectsClient); - - teardowns.push(() => server.stop()); - - return { - server, - mockSavedObjectsClient, - response: await server.inject({ - method, - url: path, - payload, - }) - }; - }; - }); - - afterEach(async () => { - await Promise.all(teardowns.splice(0).map(fn => fn())); - }); - - test(`'GET spaces' returns all available spaces`, async () => { - const { response } = await request('GET', '/api/spaces/v1/spaces'); - - const { - statusCode, - payload - } = response; - - expect(statusCode).toEqual(200); - const resultSpaces = JSON.parse(payload); - expect(resultSpaces.map(s => s.id)).toEqual(spaces.map(s => s.id)); - }); - - test(`'GET spaces/{id}' returns the space with that id`, async () => { - const { response } = await request('GET', '/api/spaces/v1/space/default'); - - const { - statusCode, - payload - } = response; - - expect(statusCode).toEqual(200); - const resultSpace = JSON.parse(payload); - expect(resultSpace.id).toEqual('default'); - }); - - test(`'DELETE spaces/{id}' deletes the space`, async () => { - const { response } = await request('DELETE', '/api/spaces/v1/space/a-space'); - - const { - statusCode - } = response; - - expect(statusCode).toEqual(204); - }); - - test(`'DELETE spaces/{id}' cannot delete reserved spaces`, async () => { - const { response } = await request('DELETE', '/api/spaces/v1/space/default'); - - const { - statusCode, - payload - } = response; - - expect(statusCode).toEqual(400); - expect(JSON.parse(payload)).toEqual({ - statusCode: 400, - error: "Bad Request", - message: "This Space cannot be deleted because it is reserved." - }); - }); - - test('POST /space should create a new space with the provided ID', async () => { - const payload = { - id: 'my-space-id', - name: 'my new space', - description: 'with a description', - }; - - const { mockSavedObjectsClient, response } = await request('POST', '/api/spaces/v1/space', { payload }); - - const { - statusCode, - } = response; - - expect(statusCode).toEqual(200); - expect(mockSavedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(mockSavedObjectsClient.create) - .toHaveBeenCalledWith('space', { name: 'my new space', description: 'with a description' }, { id: 'my-space-id', overwrite: false }); - }); - - test('POST /space should not allow a space to be updated', async () => { - const payload = { - id: 'a-space', - name: 'my updated space', - description: 'with a description', - }; - - const { response } = await request('POST', '/api/spaces/v1/space', { payload }); - - const { - statusCode, - payload: responsePayload, - } = response; - - expect(statusCode).toEqual(409); - expect(JSON.parse(responsePayload)).toEqual({ - error: 'Conflict', - message: "A space with the identifier a-space already exists. Please choose a different identifier", - statusCode: 409 - }); - }); - - test('PUT /space should update an existing space with the provided ID', async () => { - const payload = { - id: 'a-space', - name: 'my updated space', - description: 'with a description', - }; - - const { mockSavedObjectsClient, response } = await request('PUT', '/api/spaces/v1/space/a-space', { payload }); - - const { - statusCode, - } = response; - - expect(statusCode).toEqual(200); - expect(mockSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(mockSavedObjectsClient.update) - .toHaveBeenCalledWith('space', 'a-space', { name: 'my updated space', description: 'with a description' }); - }); - - test('PUT /space should not allow a new space to be created', async () => { - const payload = { - id: 'a-new-space', - name: 'my new space', - description: 'with a description', - }; - - const { response } = await request('PUT', '/api/spaces/v1/space/a-new-space', { payload }); - - const { - statusCode, - } = response; - - expect(statusCode).toEqual(404); - }); - - test('POST space/{id}/select should respond with the new space location', async () => { - const { response } = await request('POST', '/api/spaces/v1/space/a-space/select'); - - const { - statusCode, - payload - } = response; - - expect(statusCode).toEqual(200); - - const result = JSON.parse(payload); - expect(result.location).toEqual('/s/a-space'); - }); - - test('POST space/{id}/select should respond with the new space location when a server.basePath is in use', async () => { - const testConfig = { - 'server.basePath': '/my/base/path' - }; - - const { response } = await request('POST', '/api/spaces/v1/space/a-space/select', { testConfig }); - - const { - statusCode, - payload - } = response; - - expect(statusCode).toEqual(200); - - const result = JSON.parse(payload); - expect(result.location).toEqual('/my/base/path/s/a-space'); - }); -}); diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.ts b/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.ts new file mode 100644 index 0000000000000..3756eac18d5ea --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../lib/route_pre_check_license', () => { + return { + routePreCheckLicense: () => (request: any, reply: any) => reply.continue(), + }; +}); + +jest.mock('../../../../../../server/lib/get_client_shield', () => { + return { + getClient: () => { + return { + callWithInternalUser: jest.fn(() => { + return; + }), + }; + }, + }; +}); + +// @ts-ignore +import { Server } from 'hapi'; +import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__'; +import { initPrivateSpacesApi } from './spaces'; + +describe('Spaces API', () => { + let request: RequestRunner; + let teardowns: TeardownFn[]; + + beforeEach(() => { + const setup = createTestHandler(initPrivateSpacesApi); + + request = setup.request; + teardowns = setup.teardowns; + }); + + afterEach(async () => { + await Promise.all(teardowns.splice(0).map(fn => fn())); + }); + + test('POST space/{id}/select should respond with the new space location', async () => { + const { response } = await request('POST', '/api/spaces/v1/space/a-space/select'); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(200); + + const result = JSON.parse(payload); + expect(result.location).toEqual('/s/a-space'); + }); + + test('POST space/{id}/select should respond with 404 when the space is not found', async () => { + const { response } = await request('POST', '/api/spaces/v1/space/not-a-space/select'); + + const { statusCode } = response; + + expect(statusCode).toEqual(404); + }); + + test('POST space/{id}/select should respond with the new space location when a server.basePath is in use', async () => { + const testConfig = { + 'server.basePath': '/my/base/path', + }; + + const { response } = await request('POST', '/api/spaces/v1/space/a-space/select', { + testConfig, + }); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(200); + + const result = JSON.parse(payload); + expect(result.location).toEqual('/my/base/path/s/a-space'); + }); +}); diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.ts b/x-pack/plugins/spaces/server/routes/api/v1/spaces.ts new file mode 100644 index 0000000000000..7a27fa2f385fa --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/v1/spaces.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { Space } from '../../../../common/model/space'; +import { wrapError } from '../../../lib/errors'; +import { addSpaceIdToPath } from '../../../lib/spaces_url_parser'; +import { getSpaceById } from '../../lib'; + +export function initPrivateSpacesApi(server: any) { + server.route({ + method: 'POST', + path: '/api/spaces/v1/space/{id}/select', + async handler(request: any, reply: any) { + const client = request.getSavedObjectsClient(); + + const id = request.params.id; + + try { + const existingSpace: Space | null = await getSpaceById(client, id); + if (!existingSpace) { + return reply(Boom.notFound()); + } + + const config = server.config(); + + return reply({ + location: addSpaceIdToPath( + config.get('server.basePath'), + existingSpace.id, + config.get('server.defaultRoute') + ), + }); + } catch (error) { + return reply(wrapError(error)); + } + }, + }); +} diff --git a/x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.test.ts b/x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.test.ts new file mode 100644 index 0000000000000..31738ff562865 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { convertSavedObjectToSpace } from './convert_saved_object_to_space'; + +describe('convertSavedObjectToSpace', () => { + it('converts a saved object representation to a Space object', () => { + const savedObject = { + id: 'foo', + attributes: { + name: 'Foo Space', + description: 'no fighting', + _reserved: false, + }, + }; + + expect(convertSavedObjectToSpace(savedObject)).toEqual({ + id: 'foo', + name: 'Foo Space', + description: 'no fighting', + _reserved: false, + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.ts b/x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.ts new file mode 100644 index 0000000000000..d3ee173a2e80f --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Space } from '../../../common/model/space'; + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function convertSavedObjectToSpace(savedObject: any): Space { + return { + id: savedObject.id, + ...savedObject.attributes, + }; +} diff --git a/x-pack/plugins/spaces/server/routes/lib/get_space_by_id.ts b/x-pack/plugins/spaces/server/routes/lib/get_space_by_id.ts new file mode 100644 index 0000000000000..4143c09a79a93 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/lib/get_space_by_id.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Space } from '../../../common/model/space'; +import { convertSavedObjectToSpace } from './convert_saved_object_to_space'; + +export async function getSpaceById(client: any, spaceId: string): Promise { + try { + const existingSpace = await client.get('space', spaceId); + return convertSavedObjectToSpace(existingSpace); + } catch (error) { + if (client.errors.isNotFoundError(error)) { + return null; + } + throw error; + } +} diff --git a/x-pack/plugins/spaces/server/routes/lib/index.ts b/x-pack/plugins/spaces/server/routes/lib/index.ts new file mode 100644 index 0000000000000..af67388792565 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/lib/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { convertSavedObjectToSpace } from './convert_saved_object_to_space'; +export { getSpaceById } from './get_space_by_id'; From 8489cfe22d2bcecb84fbcd5c5a3a6bb768814f14 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 31 Aug 2018 10:57:32 -0400 Subject: [PATCH 3/4] namespace public spaces api, and start rough draft of API docs --- docs/api/spaces-management/delete.asciidoc | 25 ++++++ docs/api/spaces-management/get.asciidoc | 77 +++++++++++++++++++ docs/api/spaces-management/post.asciidoc | 50 ++++++++++++ docs/api/spaces-management/put.asciidoc | 50 ++++++++++++ docs/api/spaces.asciidoc | 17 ++++ .../spaces/public/lib/spaces_manager.ts | 10 +-- .../server/routes/api/public/delete.test.ts | 6 +- .../spaces/server/routes/api/public/delete.ts | 2 +- .../server/routes/api/public/get.test.ts | 6 +- .../spaces/server/routes/api/public/get.ts | 4 +- .../server/routes/api/public/post.test.ts | 6 +- .../spaces/server/routes/api/public/post.ts | 2 +- .../server/routes/api/public/put.test.ts | 4 +- .../spaces/server/routes/api/public/put.ts | 2 +- 14 files changed, 241 insertions(+), 20 deletions(-) create mode 100644 docs/api/spaces-management/delete.asciidoc create mode 100644 docs/api/spaces-management/get.asciidoc create mode 100644 docs/api/spaces-management/post.asciidoc create mode 100644 docs/api/spaces-management/put.asciidoc create mode 100644 docs/api/spaces.asciidoc diff --git a/docs/api/spaces-management/delete.asciidoc b/docs/api/spaces-management/delete.asciidoc new file mode 100644 index 0000000000000..c5ae025dd9e2e --- /dev/null +++ b/docs/api/spaces-management/delete.asciidoc @@ -0,0 +1,25 @@ +[[spaces-api-delete]] +=== Delete space + +experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] + +[WARNING] +================================================== +Deleting a space will automatically delete all saved objects that belong to that space. This operation cannot be undone! +================================================== + +==== Request + +To delete a space, submit a DELETE request to the `/api/spaces/space/` +endpoint: + +[source,js] +-------------------------------------------------- +DELETE /api/spaces/space/marketing +-------------------------------------------------- +// KIBANA + +==== Response + +If the space is successfully deleted, the response code is `204`; otherwise, the response +code is 404. diff --git a/docs/api/spaces-management/get.asciidoc b/docs/api/spaces-management/get.asciidoc new file mode 100644 index 0000000000000..c79a883a80e4b --- /dev/null +++ b/docs/api/spaces-management/get.asciidoc @@ -0,0 +1,77 @@ +[[spaces-api-get]] +=== Get Space + +experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] + +Retrieves all {kib} spaces, or a specific space. + +==== Get all {kib} spaces + +===== Request + +To retrieve all spaces, issue a GET request to the +/api/spaces/space endpoint. + +[source,js] +-------------------------------------------------- +GET /api/spaces/space +-------------------------------------------------- +// KIBANA + +===== Response + +A successful call returns a response code of `200` and a response body containing a JSON +representation of the spaces. + +[source,js] +-------------------------------------------------- +[ + { + "id": "default", + "name": "Default", + "description" : "This is the Default Space", + "_reserved": true + }, + { + "id": "marketing", + "name": "Marketing", + "description" : "This is the Marketing Space", + "color": "#aabbcc", + "initials": "MK" + }, + { + "id": "sales", + "name": "Sales", + "initials": "MK" + }, +] +-------------------------------------------------- + +==== Get a specific space + +===== Request + +To retrieve a specific space, issue a GET request to +the `/api/spaces/space/` endpoint: + +[source,js] +-------------------------------------------------- +GET /api/spaces/space/marketing +-------------------------------------------------- +// KIBANA + +===== Response + +A successful call returns a response code of `200` and a response body containing a JSON +representation of the space. + +[source,js] +-------------------------------------------------- +{ + "id": "marketing", + "name": "Marketing", + "description" : "This is the Marketing Space", + "color": "#aabbcc", + "initials": "MK" +} +-------------------------------------------------- diff --git a/docs/api/spaces-management/post.asciidoc b/docs/api/spaces-management/post.asciidoc new file mode 100644 index 0000000000000..569835c78b2f8 --- /dev/null +++ b/docs/api/spaces-management/post.asciidoc @@ -0,0 +1,50 @@ +[[spaces-api-post]] +=== Create Space + +experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] + +Creates a new {kib} space. To update an existing space, use the PUT command. + +==== Request + +To create a space, issue a POST request to the +`/api/spaces/space` endpoint. + +[source,js] +-------------------------------------------------- +PUT /api/spaces/space +-------------------------------------------------- + +==== Request Body + +The following parameters can be specified in the body of a POST request to create a space: + +`id`:: (string) Required identifier for the space. This identifier becomes part of Kibana's URL when inside the space. This cannot be changed by the update operation. + +`name`:: (string) Required display name for the space. + +`description`:: (string) Optional description for the space. + +`initials`:: (string) Optionally specify the initials shown in the Space Avatar for this space. By default, the initials will be automatically generated from the space name. +If specified, initials should be either 1 or 2 characters. + +`color`:: (string) Optioanlly specify the hex color code used in the Space Avatar for this space. By default, the color will be automatically generated from the space name. + +===== Example + +[source,js] +-------------------------------------------------- +POST /api/spaces/space +{ + "id": "marketing", + "name": "Marketing", + "description" : "This is the Marketing Space", + "color": "#aabbcc", + "initials": "MK" +} +-------------------------------------------------- +// KIBANA + +==== Response + +A successful call returns a response code of `200` with the created Space. diff --git a/docs/api/spaces-management/put.asciidoc b/docs/api/spaces-management/put.asciidoc new file mode 100644 index 0000000000000..529742bf2ce66 --- /dev/null +++ b/docs/api/spaces-management/put.asciidoc @@ -0,0 +1,50 @@ +[[spaces-api-put]] +=== Update Space + +experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] + +Updates an existing {kib} space. To create a new space, use the POST command. + +==== Request + +To update a space, issue a PUT request to the +`/api/spaces/space/` endpoint. + +[source,js] +-------------------------------------------------- +PUT /api/spaces/space/ +-------------------------------------------------- + +==== Request Body + +The following parameters can be specified in the body of a PUT request to update a space: + +`id`:: (string) Required identifier for the space. This identifier becomes part of Kibana's URL when inside the space. This cannot be changed by the update operation. + +`name`:: (string) Required display name for the space. + +`description`:: (string) Optional description for the space. + +`initials`:: (string) Optionally specify the initials shown in the Space Avatar for this space. By default, the initials will be automatically generated from the space name. +If specified, initials should be either 1 or 2 characters. + +`color`:: (string) Optioanlly specify the hex color code used in the Space Avatar for this space. By default, the color will be automatically generated from the space name. + +===== Example + +[source,js] +-------------------------------------------------- +PUT /api/spaces/space/marketing +{ + "id": "marketing", + "name": "Marketing", + "description" : "This is the Marketing Space", + "color": "#aabbcc", + "initials": "MK" +} +-------------------------------------------------- +// KIBANA + +==== Response + +A successful call returns a response code of `200` with the updated Space. diff --git a/docs/api/spaces.asciidoc b/docs/api/spaces.asciidoc new file mode 100644 index 0000000000000..ea66d50d396b9 --- /dev/null +++ b/docs/api/spaces.asciidoc @@ -0,0 +1,17 @@ +[role="xpack"] +[[spaces-api]] +== Kibana Spaces API + +experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] + +The spaces API allows people to manage their spaces within {kib}. + +* <> +* <> +* <> +* <> + +include::spaces-management/put.asciidoc[] +include::spaces-management/post.asciidoc[] +include::spaces-management/get.asciidoc[] +include::spaces-management/delete.asciidoc[] diff --git a/x-pack/plugins/spaces/public/lib/spaces_manager.ts b/x-pack/plugins/spaces/public/lib/spaces_manager.ts index 2f9c6687ee9da..53835ab122f87 100644 --- a/x-pack/plugins/spaces/public/lib/spaces_manager.ts +++ b/x-pack/plugins/spaces/public/lib/spaces_manager.ts @@ -21,24 +21,24 @@ export class SpacesManager extends EventEmitter { public async getSpaces(): Promise { return await this.httpAgent - .get(`${this.baseUrl}`) + .get(`${this.baseUrl}/space`) .then((response: IHttpResponse) => response.data); } public async getSpace(id: string): Promise { - return await this.httpAgent.get(`${this.baseUrl}/${id}`); + return await this.httpAgent.get(`${this.baseUrl}/space/${id}`); } public async createSpace(space: Space) { - return await this.httpAgent.post(`${this.baseUrl}`, space); + return await this.httpAgent.post(`${this.baseUrl}/space`, space); } public async updateSpace(space: Space) { - return await this.httpAgent.put(`${this.baseUrl}/${space.id}?overwrite=true`, space); + return await this.httpAgent.put(`${this.baseUrl}/space/${space.id}?overwrite=true`, space); } public async deleteSpace(space: Space) { - return await this.httpAgent.delete(`${this.baseUrl}/${space.id}`); + return await this.httpAgent.delete(`${this.baseUrl}/space/${space.id}`); } public async changeSelectedSpace(space: Space) { diff --git a/x-pack/plugins/spaces/server/routes/api/public/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/public/delete.test.ts index 605129449a02d..1e46b951997a2 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/delete.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/delete.test.ts @@ -41,7 +41,7 @@ describe('Spaces Public API', () => { }); test(`'DELETE spaces/{id}' deletes the space`, async () => { - const { response } = await request('DELETE', '/api/spaces/a-space'); + const { response } = await request('DELETE', '/api/spaces/space/a-space'); const { statusCode } = response; @@ -49,7 +49,7 @@ describe('Spaces Public API', () => { }); test('DELETE spaces/{id} pretends to delete a non-existent space', async () => { - const { response } = await request('DELETE', '/api/spaces/not-a-space'); + const { response } = await request('DELETE', '/api/spaces/space/not-a-space'); const { statusCode } = response; @@ -57,7 +57,7 @@ describe('Spaces Public API', () => { }); test(`'DELETE spaces/{id}' cannot delete reserved spaces`, async () => { - const { response } = await request('DELETE', '/api/spaces/default'); + const { response } = await request('DELETE', '/api/spaces/space/default'); const { statusCode, payload } = response; diff --git a/x-pack/plugins/spaces/server/routes/api/public/delete.ts b/x-pack/plugins/spaces/server/routes/api/public/delete.ts index d73b47e47a2b1..ea1650c2f4dd1 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/delete.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/delete.ts @@ -15,7 +15,7 @@ export function initDeleteSpacesApi(server: any) { server.route({ method: 'DELETE', - path: '/api/spaces/{id}', + path: '/api/spaces/space/{id}', async handler(request: any, reply: any) { const client = request.getSavedObjectsClient(); diff --git a/x-pack/plugins/spaces/server/routes/api/public/get.test.ts b/x-pack/plugins/spaces/server/routes/api/public/get.test.ts index 1755364c14172..909f61d3f3309 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/get.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/get.test.ts @@ -43,7 +43,7 @@ describe('GET spaces', () => { }); test(`'GET spaces' returns all available spaces`, async () => { - const { response } = await request('GET', '/api/spaces'); + const { response } = await request('GET', '/api/spaces/space'); const { statusCode, payload } = response; @@ -53,7 +53,7 @@ describe('GET spaces', () => { }); test(`'GET spaces/{id}' returns the space with that id`, async () => { - const { response } = await request('GET', '/api/spaces/default'); + const { response } = await request('GET', '/api/spaces/space/default'); const { statusCode, payload } = response; @@ -63,7 +63,7 @@ describe('GET spaces', () => { }); test(`'GET spaces/{id}' returns 404 when retrieving a non-existent space`, async () => { - const { response } = await request('GET', '/api/spaces/not-a-space'); + const { response } = await request('GET', '/api/spaces/space/not-a-space'); const { statusCode } = response; diff --git a/x-pack/plugins/spaces/server/routes/api/public/get.ts b/x-pack/plugins/spaces/server/routes/api/public/get.ts index a99e8c8dd33eb..79619e5a8f514 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/get.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/get.ts @@ -14,7 +14,7 @@ export function initGetSpacesApi(server: any) { server.route({ method: 'GET', - path: '/api/spaces', + path: '/api/spaces/space', async handler(request: any, reply: any) { const client = request.getSavedObjectsClient(); @@ -40,7 +40,7 @@ export function initGetSpacesApi(server: any) { server.route({ method: 'GET', - path: '/api/spaces/{id}', + path: '/api/spaces/space/{id}', async handler(request: any, reply: any) { const spaceId = request.params.id; diff --git a/x-pack/plugins/spaces/server/routes/api/public/post.test.ts b/x-pack/plugins/spaces/server/routes/api/public/post.test.ts index cae6297281166..3adcab2e6451e 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/post.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/post.test.ts @@ -47,7 +47,9 @@ describe('Spaces Public API', () => { description: 'with a description', }; - const { mockSavedObjectsClient, response } = await request('POST', '/api/spaces', { payload }); + const { mockSavedObjectsClient, response } = await request('POST', '/api/spaces/space', { + payload, + }); const { statusCode } = response; @@ -67,7 +69,7 @@ describe('Spaces Public API', () => { description: 'with a description', }; - const { response } = await request('POST', '/api/spaces', { payload }); + const { response } = await request('POST', '/api/spaces/space', { payload }); const { statusCode, payload: responsePayload } = response; diff --git a/x-pack/plugins/spaces/server/routes/api/public/post.ts b/x-pack/plugins/spaces/server/routes/api/public/post.ts index 8631160a2b1ff..5fcaa46ccfd88 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/post.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/post.ts @@ -16,7 +16,7 @@ export function initPostSpacesApi(server: any) { server.route({ method: 'POST', - path: '/api/spaces', + path: '/api/spaces/space', async handler(request: any, reply: any) { const client = request.getSavedObjectsClient(); diff --git a/x-pack/plugins/spaces/server/routes/api/public/put.test.ts b/x-pack/plugins/spaces/server/routes/api/public/put.test.ts index 1acd4fe395565..6570219dc4eb8 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/put.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/put.test.ts @@ -46,7 +46,7 @@ describe('Spaces Public API', () => { description: 'with a description', }; - const { mockSavedObjectsClient, response } = await request('PUT', '/api/spaces/a-space', { + const { mockSavedObjectsClient, response } = await request('PUT', '/api/spaces/space/a-space', { payload, }); @@ -67,7 +67,7 @@ describe('Spaces Public API', () => { description: 'with a description', }; - const { response } = await request('PUT', '/api/spaces/a-new-space', { payload }); + const { response } = await request('PUT', '/api/spaces/space/a-new-space', { payload }); const { statusCode } = response; diff --git a/x-pack/plugins/spaces/server/routes/api/public/put.ts b/x-pack/plugins/spaces/server/routes/api/public/put.ts index f75b309bdfe7c..7f25850112d49 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/put.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/put.ts @@ -17,7 +17,7 @@ export function initPutSpacesApi(server: any) { server.route({ method: 'PUT', - path: '/api/spaces/{id}', + path: '/api/spaces/space/{id}', async handler(request: any, reply: any) { const client = request.getSavedObjectsClient(); From 70a82f58d16848a03d5d4d8d72b679e442339134 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 4 Sep 2018 11:41:57 -0400 Subject: [PATCH 4/4] add license check tests --- x-pack/plugins/spaces/index.js | 17 +++------ .../api/__fixtures__/create_test_handler.ts | 35 +++++++++++++++---- .../server/routes/api/public/delete.test.ts | 18 ++++++++-- .../spaces/server/routes/api/public/delete.ts | 5 +-- .../server/routes/api/public/get.test.ts | 16 ++++++++- .../spaces/server/routes/api/public/get.ts | 5 +-- .../spaces/server/routes/api/public/index.ts | 18 +++++++--- .../server/routes/api/public/post.test.ts | 24 ++++++++++++- .../spaces/server/routes/api/public/post.ts | 5 +-- .../server/routes/api/public/put.test.ts | 25 +++++++++++-- .../spaces/server/routes/api/public/put.ts | 5 +-- .../spaces/server/routes/api/v1/index.ts | 13 +++++++ .../server/routes/api/v1/spaces.test.ts | 17 +++++++-- .../spaces/server/routes/api/v1/spaces.ts | 5 ++- 14 files changed, 159 insertions(+), 49 deletions(-) create mode 100644 x-pack/plugins/spaces/server/routes/api/v1/index.ts diff --git a/x-pack/plugins/spaces/index.js b/x-pack/plugins/spaces/index.js index b2b533ef98c34..97f3bfe39993c 100644 --- a/x-pack/plugins/spaces/index.js +++ b/x-pack/plugins/spaces/index.js @@ -7,13 +7,8 @@ import { resolve } from 'path'; import { validateConfig } from './server/lib/validate_config'; import { checkLicense } from './server/lib/check_license'; -import { - initGetSpacesApi, - initPostSpacesApi, - initPutSpacesApi, - initDeleteSpacesApi -} from './server/routes/api/public'; -import { initPrivateSpacesApi } from './server/routes/api/v1/spaces'; +import { initPublicSpacesApi } from './server/routes/api/public'; +import { initPrivateApis } from './server/routes/api/v1'; import { initSpacesRequestInterceptors } from './server/lib/space_request_interceptors'; import { createDefaultSpace } from './server/lib/create_default_space'; import { createSpacesService } from './server/lib/create_spaces_service'; @@ -93,12 +88,8 @@ export const spaces = (kibana) => new kibana.Plugin({ spacesSavedObjectsClientWrapperFactory(spacesService, types) ); - initPrivateSpacesApi(server); - - initGetSpacesApi(server); - initPostSpacesApi(server); - initPutSpacesApi(server); - initDeleteSpacesApi(server); + initPrivateApis(server); + initPublicSpacesApi(server); initSpacesRequestInterceptors(server); } diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts index 5cfd8626531d0..a184f21076d4f 100644 --- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts @@ -16,6 +16,7 @@ export interface TestOptions { setupFn?: (server: any) => void; testConfig?: TestConfig; payload?: any; + preCheckLicenseImpl?: (req: any, reply: any) => any; } export type TeardownFn = () => void; @@ -32,11 +33,13 @@ export type RequestRunner = ( options?: TestOptions ) => Promise; +export const defaultPreCheckLicenseImpl = (request: any, reply: any) => reply(); + const baseConfig: TestConfig = { 'server.basePath': '', }; -export function createTestHandler(initApiFn: (server: any) => void) { +export function createTestHandler(initApiFn: (server: any, preCheckLicenseImpl: any) => void) { const teardowns: TeardownFn[] = []; const spaces = createSpaces(); @@ -52,8 +55,14 @@ export function createTestHandler(initApiFn: (server: any) => void) { }, testConfig = {}, payload, + preCheckLicenseImpl = defaultPreCheckLicenseImpl, } = options; + let pre = jest.fn(); + if (preCheckLicenseImpl) { + pre = pre.mockImplementation(preCheckLicenseImpl); + } + const server = new Server(); const config = { @@ -75,7 +84,7 @@ export function createTestHandler(initApiFn: (server: any) => void) { }) ); - initApiFn(server); + initApiFn(server, pre); server.decorate('request', 'getBasePath', jest.fn()); server.decorate('request', 'setBasePath', jest.fn()); @@ -103,14 +112,26 @@ export function createTestHandler(initApiFn: (server: any) => void) { teardowns.push(() => server.stop()); - return { - server, - mockSavedObjectsClient, - response: await server.inject({ + const testRun = async () => { + const response = await server.inject({ method, url: path, payload, - }), + }); + + if (preCheckLicenseImpl) { + expect(pre).toHaveBeenCalled(); + } else { + expect(pre).not.toHaveBeenCalled(); + } + + return response; + }; + + return { + server, + mockSavedObjectsClient, + response: await testRun(), }; }; diff --git a/x-pack/plugins/spaces/server/routes/api/public/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/public/delete.test.ts index 1e46b951997a2..523b5a2cb2e7b 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/delete.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/delete.test.ts @@ -21,9 +21,9 @@ jest.mock('../../../../../../server/lib/get_client_shield', () => { }, }; }); - +import Boom from 'boom'; import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__'; -import { initDeleteSpacesApi } from './'; +import { initDeleteSpacesApi } from './delete'; describe('Spaces Public API', () => { let request: RequestRunner; @@ -48,6 +48,20 @@ describe('Spaces Public API', () => { expect(statusCode).toEqual(204); }); + test(`returns result of routePreCheckLicense`, async () => { + const { response } = await request('DELETE', '/api/spaces/space/a-space', { + preCheckLicenseImpl: (req: any, reply: any) => + reply(Boom.forbidden('test forbidden message')), + }); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(403); + expect(JSON.parse(payload)).toMatchObject({ + message: 'test forbidden message', + }); + }); + test('DELETE spaces/{id} pretends to delete a non-existent space', async () => { const { response } = await request('DELETE', '/api/spaces/space/not-a-space'); diff --git a/x-pack/plugins/spaces/server/routes/api/public/delete.ts b/x-pack/plugins/spaces/server/routes/api/public/delete.ts index ea1650c2f4dd1..9937b786ccfa7 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/delete.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/delete.ts @@ -7,12 +7,9 @@ import Boom from 'boom'; import { isReservedSpace } from '../../../../common/is_reserved_space'; import { wrapError } from '../../../lib/errors'; -import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; import { getSpaceById } from '../../lib'; -export function initDeleteSpacesApi(server: any) { - const routePreCheckLicenseFn = routePreCheckLicense(server); - +export function initDeleteSpacesApi(server: any, routePreCheckLicenseFn: any) { server.route({ method: 'DELETE', path: '/api/spaces/space/{id}', diff --git a/x-pack/plugins/spaces/server/routes/api/public/get.test.ts b/x-pack/plugins/spaces/server/routes/api/public/get.test.ts index 909f61d3f3309..4d04759b283a8 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/get.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/get.test.ts @@ -21,7 +21,7 @@ jest.mock('../../../../../../server/lib/get_client_shield', () => { }, }; }); - +import Boom from 'boom'; import { Space } from '../../../../common/model/space'; import { createSpaces, createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__'; import { initGetSpacesApi } from './get'; @@ -52,6 +52,20 @@ describe('GET spaces', () => { expect(resultSpaces.map(s => s.id)).toEqual(spaces.map(s => s.id)); }); + test(`returns result of routePreCheckLicense`, async () => { + const { response } = await request('GET', '/api/spaces/space', { + preCheckLicenseImpl: (req: any, reply: any) => + reply(Boom.forbidden('test forbidden message')), + }); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(403); + expect(JSON.parse(payload)).toMatchObject({ + message: 'test forbidden message', + }); + }); + test(`'GET spaces/{id}' returns the space with that id`, async () => { const { response } = await request('GET', '/api/spaces/space/default'); diff --git a/x-pack/plugins/spaces/server/routes/api/public/get.ts b/x-pack/plugins/spaces/server/routes/api/public/get.ts index 79619e5a8f514..95f1d273a8f6b 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/get.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/get.ts @@ -6,12 +6,9 @@ import Boom from 'boom'; import { wrapError } from '../../../lib/errors'; -import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; import { convertSavedObjectToSpace } from '../../lib'; -export function initGetSpacesApi(server: any) { - const routePreCheckLicenseFn = routePreCheckLicense(server); - +export function initGetSpacesApi(server: any, routePreCheckLicenseFn: any) { server.route({ method: 'GET', path: '/api/spaces/space', diff --git a/x-pack/plugins/spaces/server/routes/api/public/index.ts b/x-pack/plugins/spaces/server/routes/api/public/index.ts index f3a37ff29417e..602b62ab26d06 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/index.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/index.ts @@ -4,7 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -export { initDeleteSpacesApi } from './delete'; -export { initGetSpacesApi } from './get'; -export { initPostSpacesApi } from './post'; -export { initPutSpacesApi } from './put'; +import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; +import { initDeleteSpacesApi } from './delete'; +import { initGetSpacesApi } from './get'; +import { initPostSpacesApi } from './post'; +import { initPutSpacesApi } from './put'; + +export function initPublicSpacesApi(server: any) { + const routePreCheckLicenseFn = routePreCheckLicense(server); + + initDeleteSpacesApi(server, routePreCheckLicenseFn); + initGetSpacesApi(server, routePreCheckLicenseFn); + initPostSpacesApi(server, routePreCheckLicenseFn); + initPutSpacesApi(server, routePreCheckLicenseFn); +} diff --git a/x-pack/plugins/spaces/server/routes/api/public/post.test.ts b/x-pack/plugins/spaces/server/routes/api/public/post.test.ts index 3adcab2e6451e..f97931d36ed66 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/post.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/post.test.ts @@ -22,8 +22,9 @@ jest.mock('../../../../../../server/lib/get_client_shield', () => { }; }); +import Boom from 'boom'; import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__'; -import { initPostSpacesApi } from './'; +import { initPostSpacesApi } from './post'; describe('Spaces Public API', () => { let request: RequestRunner; @@ -62,6 +63,27 @@ describe('Spaces Public API', () => { ); }); + test(`returns result of routePreCheckLicense`, async () => { + const payload = { + id: 'my-space-id', + name: 'my new space', + description: 'with a description', + }; + + const { response } = await request('POST', '/api/spaces/space', { + preCheckLicenseImpl: (req: any, reply: any) => + reply(Boom.forbidden('test forbidden message')), + payload, + }); + + const { statusCode, payload: responsePayload } = response; + + expect(statusCode).toEqual(403); + expect(JSON.parse(responsePayload)).toMatchObject({ + message: 'test forbidden message', + }); + }); + test('POST /space should not allow a space to be updated', async () => { const payload = { id: 'a-space', diff --git a/x-pack/plugins/spaces/server/routes/api/public/post.ts b/x-pack/plugins/spaces/server/routes/api/public/post.ts index 5fcaa46ccfd88..fd51390af5023 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/post.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/post.ts @@ -7,13 +7,10 @@ import Boom from 'boom'; import { omit } from 'lodash'; import { wrapError } from '../../../lib/errors'; -import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; import { spaceSchema } from '../../../lib/space_schema'; import { getSpaceById } from '../../lib'; -export function initPostSpacesApi(server: any) { - const routePreCheckLicenseFn = routePreCheckLicense(server); - +export function initPostSpacesApi(server: any, routePreCheckLicenseFn: any) { server.route({ method: 'POST', path: '/api/spaces/space', diff --git a/x-pack/plugins/spaces/server/routes/api/public/put.test.ts b/x-pack/plugins/spaces/server/routes/api/public/put.test.ts index 6570219dc4eb8..2af4fc9bbeaf3 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/put.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/put.test.ts @@ -20,9 +20,9 @@ jest.mock('../../../../../../server/lib/get_client_shield', () => { }, }; }); - +import Boom from 'boom'; import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__'; -import { initPutSpacesApi } from './'; +import { initPutSpacesApi } from './put'; describe('Spaces Public API', () => { let request: RequestRunner; @@ -60,6 +60,27 @@ describe('Spaces Public API', () => { }); }); + test(`returns result of routePreCheckLicense`, async () => { + const payload = { + id: 'a-space', + name: 'my updated space', + description: 'with a description', + }; + + const { response } = await request('PUT', '/api/spaces/space/a-space', { + preCheckLicenseImpl: (req: any, reply: any) => + reply(Boom.forbidden('test forbidden message')), + payload, + }); + + const { statusCode, payload: responsePayload } = response; + + expect(statusCode).toEqual(403); + expect(JSON.parse(responsePayload)).toMatchObject({ + message: 'test forbidden message', + }); + }); + test('PUT /space should not allow a new space to be created', async () => { const payload = { id: 'a-new-space', diff --git a/x-pack/plugins/spaces/server/routes/api/public/put.ts b/x-pack/plugins/spaces/server/routes/api/public/put.ts index 7f25850112d49..093d7c777e786 100644 --- a/x-pack/plugins/spaces/server/routes/api/public/put.ts +++ b/x-pack/plugins/spaces/server/routes/api/public/put.ts @@ -8,13 +8,10 @@ import Boom from 'boom'; import { omit } from 'lodash'; import { Space } from '../../../../common/model/space'; import { wrapError } from '../../../lib/errors'; -import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; import { spaceSchema } from '../../../lib/space_schema'; import { convertSavedObjectToSpace, getSpaceById } from '../../lib'; -export function initPutSpacesApi(server: any) { - const routePreCheckLicenseFn = routePreCheckLicense(server); - +export function initPutSpacesApi(server: any, routePreCheckLicenseFn: any) { server.route({ method: 'PUT', path: '/api/spaces/space/{id}', diff --git a/x-pack/plugins/spaces/server/routes/api/v1/index.ts b/x-pack/plugins/spaces/server/routes/api/v1/index.ts new file mode 100644 index 0000000000000..75659c14c03ae --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/v1/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; +import { initPrivateSpacesApi } from './spaces'; + +export function initPrivateApis(server: any) { + const routePreCheckLicenseFn = routePreCheckLicense(server); + initPrivateSpacesApi(server, routePreCheckLicenseFn); +} diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.ts b/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.ts index 3756eac18d5ea..bbdab1be34910 100644 --- a/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.ts @@ -22,8 +22,7 @@ jest.mock('../../../../../../server/lib/get_client_shield', () => { }; }); -// @ts-ignore -import { Server } from 'hapi'; +import Boom from 'boom'; import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__'; import { initPrivateSpacesApi } from './spaces'; @@ -53,6 +52,20 @@ describe('Spaces API', () => { expect(result.location).toEqual('/s/a-space'); }); + test(`returns result of routePreCheckLicense`, async () => { + const { response } = await request('POST', '/api/spaces/v1/space/a-space/select', { + preCheckLicenseImpl: (req: any, reply: any) => + reply(Boom.forbidden('test forbidden message')), + }); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(403); + expect(JSON.parse(payload)).toMatchObject({ + message: 'test forbidden message', + }); + }); + test('POST space/{id}/select should respond with 404 when the space is not found', async () => { const { response } = await request('POST', '/api/spaces/v1/space/not-a-space/select'); diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.ts b/x-pack/plugins/spaces/server/routes/api/v1/spaces.ts index 7a27fa2f385fa..0233cb76b96d8 100644 --- a/x-pack/plugins/spaces/server/routes/api/v1/spaces.ts +++ b/x-pack/plugins/spaces/server/routes/api/v1/spaces.ts @@ -10,7 +10,7 @@ import { wrapError } from '../../../lib/errors'; import { addSpaceIdToPath } from '../../../lib/spaces_url_parser'; import { getSpaceById } from '../../lib'; -export function initPrivateSpacesApi(server: any) { +export function initPrivateSpacesApi(server: any, routePreCheckLicenseFn: any) { server.route({ method: 'POST', path: '/api/spaces/v1/space/{id}/select', @@ -38,5 +38,8 @@ export function initPrivateSpacesApi(server: any) { return reply(wrapError(error)); } }, + config: { + pre: [routePreCheckLicenseFn], + }, }); }