From b46a7377030d2b1b0d298004b56e32d8017457c5 Mon Sep 17 00:00:00 2001 From: Youhei Sakurai Date: Sat, 9 Sep 2023 10:33:56 +0900 Subject: [PATCH] Add support for PATCH requests in Console (#165634) ## Summary This PR adds support for PATCH requests in Console. ![patch-request](https://github.com/elastic/kibana/assets/721858/8257ca4b-303e-4f46-bbcc-6e6f95336c30) Closes #154274 ### Checklist - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ## Release note Adds support for PATCH requests in Console. --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/user/api.asciidoc | 2 ++ .../legacy_core_editor/mode/worker/worker.js | 16 ++++++++++++++-- .../application/models/sense_editor/curl.ts | 4 ++-- .../models/sense_editor/integration.test.js | 2 +- .../public/lib/autocomplete/autocomplete.ts | 2 +- .../components/url_pattern_matcher.js | 2 +- .../console/public/lib/curl_parsing/curl.js | 4 ++-- .../server/routes/api/console/proxy/body.test.ts | 6 ++++++ .../api/console/proxy/validation_config.ts | 4 ++-- test/functional/apps/console/_autocomplete.ts | 4 +++- test/functional/apps/console/_console.ts | 13 +++++++++++++ 11 files changed, 47 insertions(+), 12 deletions(-) diff --git a/docs/user/api.asciidoc b/docs/user/api.asciidoc index f71b32fa5b9ba..32e4115fe59dc 100644 --- a/docs/user/api.asciidoc +++ b/docs/user/api.asciidoc @@ -53,6 +53,8 @@ Calls to the API endpoints require different operations. To interact with the {k * *PUT* - Updates the existing information. +* *PATCH* - Applies partial modifications to the existing information. + * *DELETE* - Removes the information. [float] diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js index fa03617f5824f..e8953152a5932 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js @@ -2090,6 +2090,12 @@ ace.define( case 'p': next('p'); switch (ch) { + case 'a': + next('a'); + next('t'); + next('c'); + next('h'); + return 'patch'; case 'u': next('u'); next('t'); @@ -2106,6 +2112,12 @@ ace.define( case 'P': next('P'); switch (ch) { + case 'A': + next('A'); + next('T'); + next('C'); + next('H'); + return 'PATCH'; case 'U': next('U'); next('T'); @@ -2120,7 +2132,7 @@ ace.define( } break; default: - error('Expected one of GET/POST/PUT/DELETE/HEAD'); + error('Expected one of GET/POST/PUT/DELETE/HEAD/PATCH'); } }, value, // Place holder for the value function. @@ -2254,7 +2266,7 @@ ace.define( annotate('error', e.message); // snap const substring = text.substr(at); - const nextMatch = substring.search(/^POST|HEAD|GET|PUT|DELETE/m); + const nextMatch = substring.search(/^POST|HEAD|GET|PUT|DELETE|PATCH/m); if (nextMatch < 1) return; reset(at + nextMatch); } diff --git a/src/plugins/console/public/application/models/sense_editor/curl.ts b/src/plugins/console/public/application/models/sense_editor/curl.ts index 74cbebf051d03..894ee2bf70168 100644 --- a/src/plugins/console/public/application/models/sense_editor/curl.ts +++ b/src/plugins/console/public/application/models/sense_editor/curl.ts @@ -38,13 +38,13 @@ export function parseCURL(text: string) { const EscapedQuotes = /^((?:[^\\"']|\\.)+)/; const LooksLikeCurl = /^\s*curl\s+/; - const CurlVerb = /-X ?(GET|HEAD|POST|PUT|DELETE)/; + const CurlVerb = /-X ?(GET|HEAD|POST|PUT|DELETE|PATCH)/; const HasProtocol = /[\s"']https?:\/\//; const CurlRequestWithProto = /[\s"']https?:\/\/[^\/ ]+\/+([^\s"']+)/; const CurlRequestWithoutProto = /[\s"'][^\/ ]+\/+([^\s"']+)/; const CurlData = /^.+\s(--data|-d)\s*/; - const SenseLine = /^\s*(GET|HEAD|POST|PUT|DELETE)\s+\/?(.+)/; + const SenseLine = /^\s*(GET|HEAD|POST|PUT|DELETE|PATCH)\s+\/?(.+)/; if (lines.length > 0 && ExecutionComment.test(lines[0])) { lines.shift(); diff --git a/src/plugins/console/public/application/models/sense_editor/integration.test.js b/src/plugins/console/public/application/models/sense_editor/integration.test.js index cd7e13d5c6a56..e47439a899edd 100644 --- a/src/plugins/console/public/application/models/sense_editor/integration.test.js +++ b/src/plugins/console/public/application/models/sense_editor/integration.test.js @@ -985,7 +985,7 @@ describe('Integration', () => { { name: 'Cursor rows after request end', cursor: { lineNumber: 5, column: 1 }, - autoCompleteSet: ['GET', 'PUT', 'POST', 'DELETE', 'HEAD'], + autoCompleteSet: ['GET', 'PUT', 'POST', 'DELETE', 'HEAD', 'PATCH'], prefixToAdd: '', suffixToAdd: ' ', }, diff --git a/src/plugins/console/public/lib/autocomplete/autocomplete.ts b/src/plugins/console/public/lib/autocomplete/autocomplete.ts index 167a0e0ab1bd3..74d06cd21ed70 100644 --- a/src/plugins/console/public/lib/autocomplete/autocomplete.ts +++ b/src/plugins/console/public/lib/autocomplete/autocomplete.ts @@ -967,7 +967,7 @@ export default function ({ } function addMethodAutoCompleteSetToContext(context: AutoCompleteContext) { - context.autoCompleteSet = ['GET', 'PUT', 'POST', 'DELETE', 'HEAD'].map((m, i) => ({ + context.autoCompleteSet = ['GET', 'PUT', 'POST', 'DELETE', 'HEAD', 'PATCH'].map((m, i) => ({ name: m, score: -i, meta: i18n.translate('console.autocomplete.addMethodMetaText', { defaultMessage: 'method' }), diff --git a/src/plugins/console/public/lib/autocomplete/components/url_pattern_matcher.js b/src/plugins/console/public/lib/autocomplete/components/url_pattern_matcher.js index f2666052b988f..8d6a0a8f60b12 100644 --- a/src/plugins/console/public/lib/autocomplete/components/url_pattern_matcher.js +++ b/src/plugins/console/public/lib/autocomplete/components/url_pattern_matcher.js @@ -33,7 +33,7 @@ export class UrlPatternMatcher { // We'll group endpoints by the methods which are attached to them, //to avoid suggesting endpoints that are incompatible with the //method that the user has entered. - ['HEAD', 'GET', 'PUT', 'POST', 'DELETE'].forEach((method) => { + ['HEAD', 'GET', 'PUT', 'POST', 'DELETE', 'PATCH'].forEach((method) => { this[method] = { rootComponent: new SharedComponent('ROOT'), parametrizedComponentFactories: parametrizedComponentFactories || { diff --git a/src/plugins/console/public/lib/curl_parsing/curl.js b/src/plugins/console/public/lib/curl_parsing/curl.js index 1ae6335f3249e..519cb3a3bbd0a 100644 --- a/src/plugins/console/public/lib/curl_parsing/curl.js +++ b/src/plugins/console/public/lib/curl_parsing/curl.js @@ -38,13 +38,13 @@ export function parseCURL(text) { const EscapedQuotes = /^((?:[^\\"']|\\.)+)/; const LooksLikeCurl = /^\s*curl\s+/; - const CurlVerb = /-X ?(GET|HEAD|POST|PUT|DELETE)/; + const CurlVerb = /-X ?(GET|HEAD|POST|PUT|DELETE|PATCH)/; const HasProtocol = /[\s"']https?:\/\//; const CurlRequestWithProto = /[\s"']https?:\/\/[^\/ ]+\/+([^\s"']+)/; const CurlRequestWithoutProto = /[\s"'][^\/ ]+\/+([^\s"']+)/; const CurlData = /^.+\s(--data|-d)\s*/; - const SenseLine = /^\s*(GET|HEAD|POST|PUT|DELETE)\s+\/?(.+)/; + const SenseLine = /^\s*(GET|HEAD|POST|PUT|DELETE|PATCH)\s+\/?(.+)/; if (lines.length > 0 && ExecutionComment.test(lines[0])) { lines.shift(); diff --git a/src/plugins/console/server/routes/api/console/proxy/body.test.ts b/src/plugins/console/server/routes/api/console/proxy/body.test.ts index 893f00f975e89..5500be776dcbc 100644 --- a/src/plugins/console/server/routes/api/console/proxy/body.test.ts +++ b/src/plugins/console/server/routes/api/console/proxy/body.test.ts @@ -90,5 +90,11 @@ describe('Console Proxy Route', () => { }); }); }); + describe('PATCH request', () => { + it('returns the exact body', async () => { + const { payload } = await request('PATCH', '/', 'foobar'); + expect(await readStream(payload)).toBe('foobar'); + }); + }); }); }); diff --git a/src/plugins/console/server/routes/api/console/proxy/validation_config.ts b/src/plugins/console/server/routes/api/console/proxy/validation_config.ts index 4492863a16bcb..9a3ee2efd66c1 100644 --- a/src/plugins/console/server/routes/api/console/proxy/validation_config.ts +++ b/src/plugins/console/server/routes/api/console/proxy/validation_config.ts @@ -13,11 +13,11 @@ export type Body = TypeOf; const acceptedHttpVerb = schema.string({ validate: (method) => { - return ['HEAD', 'GET', 'POST', 'PUT', 'DELETE'].some( + return ['HEAD', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH'].some( (verb) => verb.toLowerCase() === method.toLowerCase() ) ? undefined - : `Method must be one of, case insensitive ['HEAD', 'GET', 'POST', 'PUT', 'DELETE']. Received '${method}'.`; + : `Method must be one of, case insensitive ['HEAD', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH']. Received '${method}'.`; }, }); diff --git a/test/functional/apps/console/_autocomplete.ts b/test/functional/apps/console/_autocomplete.ts index 8f2a927a4ff24..1944b5ec24761 100644 --- a/test/functional/apps/console/_autocomplete.ts +++ b/test/functional/apps/console/_autocomplete.ts @@ -54,7 +54,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('HTTP methods', async () => { const suggestions = { G: ['GET'], - P: ['PUT', 'POST'], + P: ['PUT', 'POST', 'PATCH'], D: ['DELETE'], H: ['HEAD'], }; @@ -234,6 +234,8 @@ GET _search dELETE dELETe dELEtE dELEte dELeTE dELeTe dELetE dELete dElETE dElETe dElEtE dElEte dEleTE dEleTe dEletE dElete deLETE deLETe deLEtE deLEte deLeTE deLeTe deLetE deLete delETE delETe delEtE delEte deleTE deleTe deletE delete HEAD HEAd HEaD HEad HeAD HeAd HeaD Head hEAD hEAd hEaD hEad heAD heAd heaD head + PATCH PATCh PATcH PATch PAtCH PAtCh PAtcH PAtch PaTCH PaTCh PaTcH PaTch PatCH PatCh PatcH Patch pATCH pATCh pATcH + pATch pAtCH pAtCh pAtcH pAtch paTCH paTCh paTcH paTch patCH patCh patcH patch `.split(/\s+/m) ), 20 // 20 of 112 (approx. one-fifth) should be enough for testing diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index 091ad8ee5a2e8..b8324ce58ce6c 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -68,6 +68,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(initialSize.width).to.be.greaterThan(afterSize.width); }); + it('should return statusCode 400 to unsupported HTTP verbs', async () => { + const expectedResponseContains = '"statusCode": 400'; + await PageObjects.console.enterRequest('\n OPTIONS /'); + await PageObjects.console.clickPlay(); + await retry.try(async () => { + const actualResponse = await PageObjects.console.getResponse(); + log.debug(actualResponse); + expect(actualResponse).to.contain(expectedResponseContains); + + expect(await PageObjects.console.hasSuccessBadge()).to.be(false); + }); + }); + describe('with kbn: prefix in request', () => { before(async () => { await PageObjects.console.clearTextArea();