diff --git a/x-pack/plugins/canvas/server/routes/workpad.js b/x-pack/plugins/canvas/server/routes/workpad.js index 537a25f154eae..def4e2e9433c2 100644 --- a/x-pack/plugins/canvas/server/routes/workpad.js +++ b/x-pack/plugins/canvas/server/routes/workpad.js @@ -63,29 +63,10 @@ export function workpad(server) { ); } - function updateWorkpad(req) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - - const now = new Date().toISOString(); - - return savedObjectsClient.get(CANVAS_TYPE, id).then(workpad => { - // TODO: Using create with force over-write because of version conflict issues with update - return savedObjectsClient.create( - CANVAS_TYPE, - { - ...req.payload, - '@timestamp': now, - '@created': workpad.attributes['@created'], - }, - { overwrite: true, id } - ); - }); - } - - function updateWorkpadAssets(req) { + function updateWorkpad(req, newPayload) { const savedObjectsClient = req.getSavedObjectsClient(); const { id } = req.params; + const payload = newPayload ? newPayload : req.payload; const now = new Date().toISOString(); @@ -95,30 +76,9 @@ export function workpad(server) { CANVAS_TYPE, { ...workpad.attributes, - assets: req.payload, - '@timestamp': now, - '@created': workpad.attributes['@created'], - }, - { overwrite: true, id } - ); - }); - } - - function updateWorkpadStructures(req) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - - const now = new Date().toISOString(); - - return savedObjectsClient.get(CANVAS_TYPE, id).then(workpad => { - // TODO: Using create with force over-write because of version conflict issues with update - return savedObjectsClient.create( - CANVAS_TYPE, - { - ...workpad.attributes, // retain preexisting assets and prop order (or maybe better to call out the `assets` prop?) - ...req.payload, - '@timestamp': now, - '@created': workpad.attributes['@created'], + ...payload, + '@timestamp': now, // always update the modified time + '@created': workpad.attributes['@created'], // ensure created is not modified }, { overwrite: true, id } ); @@ -194,7 +154,8 @@ export function workpad(server) { path: `${routePrefixAssets}/{id}`, config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit handler: function(request) { - return updateWorkpadAssets(request) + const payload = { assets: request.payload }; + return updateWorkpad(request, payload) .then(() => ({ ok: true })) .catch(formatResponse); }, @@ -206,7 +167,7 @@ export function workpad(server) { path: `${routePrefixStructures}/{id}`, config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit handler: function(request) { - return updateWorkpadStructures(request) + return updateWorkpad(request) .then(() => ({ ok: true })) .catch(formatResponse); }, diff --git a/x-pack/plugins/canvas/server/routes/workpad.test.js b/x-pack/plugins/canvas/server/routes/workpad.test.js new file mode 100644 index 0000000000000..40b9cb844ad77 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad.test.js @@ -0,0 +1,559 @@ +/* + * 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 Hapi from 'hapi'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, + API_ROUTE_WORKPAD_ASSETS, + API_ROUTE_WORKPAD_STRUCTURES, +} from '../../common/lib/constants'; +import { workpad } from './workpad'; + +const routePrefix = API_ROUTE_WORKPAD; +const routePrefixAssets = API_ROUTE_WORKPAD_ASSETS; +const routePrefixStructures = API_ROUTE_WORKPAD_STRUCTURES; + +jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); + +describe(`${CANVAS_TYPE} API`, () => { + const savedObjectsClient = { + get: jest.fn(), + create: jest.fn(), + delete: jest.fn(), + find: jest.fn(), + }; + + afterEach(() => { + savedObjectsClient.get.mockReset(); + savedObjectsClient.create.mockReset(); + savedObjectsClient.delete.mockReset(); + savedObjectsClient.find.mockReset(); + }); + + // Mock toISOString function of all Date types + global.Date = class Date extends global.Date { + toISOString() { + return '2019-02-12T21:01:22.479Z'; + } + }; + + // Setup mock server + const mockServer = new Hapi.Server({ debug: false, port: 0 }); + mockServer.plugins = { + elasticsearch: { + getCluster: () => ({ + errors: { + // formatResponse will fail without objects here + '400': Error, + '401': Error, + '403': Error, + '404': Error, + }, + }), + }, + }; + mockServer.ext('onRequest', (req, h) => { + req.getSavedObjectsClient = () => savedObjectsClient; + return h.continue; + }); + workpad(mockServer); + + describe(`GET ${routePrefix}/{id}`, () => { + test('returns successful response', async () => { + const request = { + method: 'GET', + url: `${routePrefix}/123`, + }; + + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + attributes: { foo: true, id: '123' }, + }); + + const { payload, statusCode } = await mockServer.inject(request); + const response = JSON.parse(payload); + + expect(statusCode).toBe(200); + expect(response).toMatchInlineSnapshot(` +Object { + "foo": true, + "id": "123", +} +`); + expect(savedObjectsClient.get).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + "canvas-workpad", + "123", + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": Promise {}, + }, + ], +} +`); + }); + }); + + describe(`POST ${routePrefix}`, () => { + test('returns successful response without id in payload', async () => { + const request = { + method: 'POST', + url: routePrefix, + payload: { + foo: true, + }, + }; + + savedObjectsClient.create.mockResolvedValueOnce({}); + + const { payload, statusCode } = await mockServer.inject(request); + const response = JSON.parse(payload); + + expect(statusCode).toBe(200); + expect(response).toMatchInlineSnapshot(` +Object { + "ok": true, +} +`); + expect(savedObjectsClient.create).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + "canvas-workpad", + Object { + "@created": "2019-02-12T21:01:22.479Z", + "@timestamp": "2019-02-12T21:01:22.479Z", + "foo": true, + }, + Object { + "id": "workpad-123abc", + }, + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": Promise {}, + }, + ], +} +`); + }); + + test('returns successful response with id in payload', async () => { + const request = { + method: 'POST', + url: routePrefix, + payload: { + id: '123', + foo: true, + }, + }; + + savedObjectsClient.create.mockResolvedValueOnce({}); + + const { payload, statusCode } = await mockServer.inject(request); + const response = JSON.parse(payload); + + expect(statusCode).toBe(200); + expect(response).toMatchInlineSnapshot(` +Object { + "ok": true, +} +`); + expect(savedObjectsClient.create).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + "canvas-workpad", + Object { + "@created": "2019-02-12T21:01:22.479Z", + "@timestamp": "2019-02-12T21:01:22.479Z", + "foo": true, + "id": "123", + }, + Object { + "id": "123", + }, + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": Promise {}, + }, + ], +} +`); + }); + }); + + describe(`PUT ${routePrefix}/{id}`, () => { + test('formats successful response', async () => { + const request = { + method: 'PUT', + url: `${routePrefix}/123`, + payload: { + id: '234', + foo: true, + }, + }; + + savedObjectsClient.get.mockResolvedValueOnce({ + attributes: { + '@created': new Date().toISOString(), + }, + }); + savedObjectsClient.create.mockResolvedValueOnce({}); + + const { payload, statusCode } = await mockServer.inject(request); + const response = JSON.parse(payload); + + expect(statusCode).toBe(200); + expect(response).toMatchInlineSnapshot(` +Object { + "ok": true, +} +`); + expect(savedObjectsClient.get).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + "canvas-workpad", + "123", + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": Promise {}, + }, + ], +} +`); + expect(savedObjectsClient.create).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + "canvas-workpad", + Object { + "@created": "2019-02-12T21:01:22.479Z", + "@timestamp": "2019-02-12T21:01:22.479Z", + "foo": true, + "id": "234", + }, + Object { + "id": "123", + "overwrite": true, + }, + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": Promise {}, + }, + ], +} +`); + }); + }); + + describe(`DELETE ${routePrefix}/{id}`, () => { + test('formats successful response', async () => { + const request = { + method: 'DELETE', + url: `${routePrefix}/123`, + }; + + savedObjectsClient.delete.mockResolvedValueOnce({}); + + const { payload, statusCode } = await mockServer.inject(request); + const response = JSON.parse(payload); + + expect(statusCode).toBe(200); + expect(response).toMatchInlineSnapshot(` +Object { + "ok": true, +} +`); + expect(savedObjectsClient.delete).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + "canvas-workpad", + "123", + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": Promise {}, + }, + ], +} +`); + }); + }); + + describe(`GET ${routePrefix}/find`, () => { + test('formats successful response', async () => { + const request = { + method: 'GET', + url: `${routePrefix}/find?name=abc&page=2&perPage=10`, + }; + + savedObjectsClient.find.mockResolvedValueOnce({ + saved_objects: [ + { + id: '123', + attributes: { + foo: true, + id: '123', + }, + }, + ], + }); + + const { payload, statusCode } = await mockServer.inject(request); + const response = JSON.parse(payload); + + expect(statusCode).toBe(200); + expect(response).toMatchInlineSnapshot(` +Object { + "workpads": Array [ + Object { + "foo": true, + "id": "123", + }, + ], +} +`); + expect(savedObjectsClient.find).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + Object { + "fields": Array [ + "id", + "name", + "@created", + "@timestamp", + ], + "page": "2", + "perPage": "10", + "search": "abc* | abc", + "searchFields": Array [ + "name", + ], + "sortField": "@timestamp", + "sortOrder": "desc", + "type": "canvas-workpad", + }, + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": Promise {}, + }, + ], +} +`); + }); + }); + + describe(`PUT ${routePrefixAssets}/{id}`, () => { + test('only updates assets', async () => { + const request = { + method: 'PUT', + url: `${routePrefixAssets}/123`, + payload: { + 'asset-123': { + id: 'asset-123', + '@created': '2019-02-14T00:00:00.000Z', + type: 'dataurl', + value: 'mockbase64data', + }, + 'asset-456': { + id: 'asset-456', + '@created': '2019-02-15T00:00:00.000Z', + type: 'dataurl', + value: 'mockbase64data', + }, + }, + }; + + // provide some existing workpad data to check that it's preserved + savedObjectsClient.get.mockResolvedValueOnce({ + attributes: { + '@created': new Date().toISOString(), + name: 'fake workpad', + }, + }); + savedObjectsClient.create.mockResolvedValueOnce({}); + + const { payload, statusCode } = await mockServer.inject(request); + const response = JSON.parse(payload); + + expect(statusCode).toBe(200); + expect(response).toMatchInlineSnapshot(` +Object { + "ok": true, +} +`); + expect(savedObjectsClient.get).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + "canvas-workpad", + "123", + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": Promise {}, + }, + ], +} +`); + expect(savedObjectsClient.create).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + "canvas-workpad", + Object { + "@created": "2019-02-12T21:01:22.479Z", + "@timestamp": "2019-02-12T21:01:22.479Z", + "assets": Object { + "asset-123": Object { + "@created": "2019-02-14T00:00:00.000Z", + "id": "asset-123", + "type": "dataurl", + "value": "mockbase64data", + }, + "asset-456": Object { + "@created": "2019-02-15T00:00:00.000Z", + "id": "asset-456", + "type": "dataurl", + "value": "mockbase64data", + }, + }, + "name": "fake workpad", + }, + Object { + "id": "123", + "overwrite": true, + }, + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": Promise {}, + }, + ], +} +`); + }); + }); + + describe(`PUT ${routePrefixStructures}/{id}`, () => { + test('only updates workpad', async () => { + const request = { + method: 'PUT', + url: `${routePrefixStructures}/123`, + payload: { + name: 'renamed workpad', + css: '.canvasPage { color: LavenderBlush; }', + }, + }; + + // provide some existing asset data and a name to replace + savedObjectsClient.get.mockResolvedValueOnce({ + attributes: { + '@created': new Date().toISOString(), + name: 'fake workpad', + assets: { + 'asset-123': { + id: 'asset-123', + '@created': '2019-02-14T00:00:00.000Z', + type: 'dataurl', + value: 'mockbase64data', + }, + }, + }, + }); + savedObjectsClient.create.mockResolvedValueOnce({}); + + const { payload, statusCode } = await mockServer.inject(request); + const response = JSON.parse(payload); + + expect(statusCode).toBe(200); + expect(response).toMatchInlineSnapshot(` +Object { + "ok": true, +} +`); + expect(savedObjectsClient.get).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + "canvas-workpad", + "123", + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": Promise {}, + }, + ], +} +`); + expect(savedObjectsClient.create).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + "canvas-workpad", + Object { + "@created": "2019-02-12T21:01:22.479Z", + "@timestamp": "2019-02-12T21:01:22.479Z", + "assets": Object { + "asset-123": Object { + "@created": "2019-02-14T00:00:00.000Z", + "id": "asset-123", + "type": "dataurl", + "value": "mockbase64data", + }, + }, + "css": ".canvasPage { color: LavenderBlush; }", + "name": "renamed workpad", + }, + Object { + "id": "123", + "overwrite": true, + }, + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": Promise {}, + }, + ], +} +`); + }); + }); +});