From d197c6e2bcfe498821c1802b9341ff8ce4bb1916 Mon Sep 17 00:00:00 2001 From: Thim Date: Sun, 18 Aug 2024 18:47:20 +0200 Subject: [PATCH] Extended pm.request with headers, url, method, name, id, body, body.raw (#138) * Extended pm.request with headers, url, method, name, id, body, body.raw --- CHANGELOG.md | 2 + README.md | 9 +- lib/shim/core.js | 106 +++++++++++++++++++++++- test/com/shim/request.js | 173 ++++++++++++++++++++++++++++++++++----- 4 files changed, 258 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd66c36..d3860a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/apideck-libraries/postman-to-k6/compare/v1.8.7...HEAD) +- Extended pm.request with support for headers, url, method, name, id, body, body.raw + ## [1.12.0] - 2024-07-31 - Fix initialization of headers for each K6 request (#98) diff --git a/README.md b/README.md index 08dfaea..d1c8f36 100644 --- a/README.md +++ b/README.md @@ -317,16 +317,13 @@ $ postman-to-k6 example/v2/echo.json -o k6-script.js ## Unsupported Features - Sending requests from scripts using `pm.sendRequest`. -- Controlling request execution order using `postman.setNextRequest`. +- Controlling request execution order using [`postman.setNextRequest`](https://github.com/apideck-libraries/postman-to-k6/discussions/135#discussioncomment-10229573). - Cookie properties, like `hostOnly`, `session`, and `storeId`. -- Textual response messages: - - `responseCode.name` - - `responseCode.detail` +- Postman methods: - `pm.response.reason` - `pm.response.to.have.status(reason)` - `pm.response.to.not.have.status(reason)` -- Properties returning Postman classes: - - `pm.request.url` `pm.request.headers` + - `pm.request.auth` - The Hawk authentication method. - Deprecated `xmlToJson` method. - Request IDs are changed. Postman doesn't provide them in the export, so we have to generate new ones. diff --git a/lib/shim/core.js b/lib/shim/core.js index e421ed0..674b65a 100644 --- a/lib/shim/core.js +++ b/lib/shim/core.js @@ -453,11 +453,52 @@ const pm = Object.freeze({ /* Request information */ request: Object.freeze({ + get data() { + return store.request.data; + }, get headers() { - throw new Error('pm.request.headers not supported'); + if ( + !store.request || + typeof store.request.headers !== 'object' || + store.request.headers === null + ) { + return []; // Return an empty array if headers are not available or invalid + } + + return Object.entries(store.request.headers).map(([key, value]) => ({ + key: key, + value: value, + })); + }, + get id() { + return store.request.id; + }, + get method() { + return store.request.method; + }, + get name() { + return store.request.name; }, get url() { - throw new Error('pm.request.url not supported'); + if (store.request.url) { + return parseUrl(store.request.url); + } + }, + get auth() { + console.warn('pm.request.auth is not supported by postman-to-k6'); + }, + get body() { + let body; + if (store.request.body && typeof store.request.body === 'string') { + body = { raw: store.request.body, mode: 'raw' }; + } else if (store.request.data && typeof store.request.data === 'object') { + let items = Object.entries(store.request.data).map(([key, value]) => ({ + key: key, + value: value, + })); + body = { urlencoded: items, mode: 'urlencoded' }; + } + return body; }, }), @@ -908,6 +949,9 @@ const request = Object.freeze({ get url() { return store.request.url; }, + get body() { + return store.request.body; + }, }); /* responseCode */ @@ -946,6 +990,49 @@ function parseBodyJson() { } } +function parseUrl(url) { + // Use a regular expression to parse the URL + const urlPattern = /^(https?):\/\/([^\/:]+)(?::(\d+))?(\/[^\?]*)?\??([^#]*)#?(.*)?$/; + const match = urlPattern.exec(url); + + if (!match) { + return undefined; + } + + const protocol = match[1]; + const host = match[2].split('.'); + const port = match[3] || (protocol === 'http' ? '80' : '443'); + const path = match[4] ? match[4].split('/').filter(Boolean) : []; + const queryString = match[5]; + const fragment = match[6]; + + // Parse the query string into an array of objects { key: value } + const queryParams = queryString + ? queryString.split('&').map(param => { + const [key, value] = param.split('='); + return { + key: decodeURIComponent(key), + value: decodeURIComponent(value), + }; + }) + : []; + + // Parse path variables (assuming variables are denoted by ":variableName") + const variableParts = path + .filter(part => part.startsWith(':')) + .map(part => part.slice(1)); + + return { + protocol: protocol, + port: port, + path: path, + host: host, + query: queryParams, + variable: variableParts, + fragment: fragment || '', + }; +} + function deepEqual(x, y) { // From https://stackoverflow.com/a/32922084/10086414 const ok = Object.keys; @@ -1028,8 +1115,11 @@ function executeRequest({ post, }) { try { - enterRequest(name, id, method, address, data, headers); + // Prepare Postman request data + enterRequest(name, id, method, address, data, headers, options); + // Convert Postman PreRequest executePrerequest(postman[Pre], pre); + // Prepare Request config const config = makeRequestConfig( method, address, @@ -1041,11 +1131,16 @@ function executeRequest({ if (auth) { auth(config, Var); } + // Convert to K6 request arguments const args = makeRequestArgs(config); + // Execute K6 requests const response = http.request(...args); if (post) { + // Prepare response data enterPost(response); + // Convert Postman PostRequest executePostrequest(postman[Post], post, response); + // Convert Postman Tests executeTests(); } return response; @@ -1078,6 +1173,7 @@ function makeRequestConfig(method, address, data, headers, options, tags) { } else { config.data = {}; } + if (headers) { for (const key of Object.keys(headers)) { headers[key] = evaluate(headers[key]); @@ -1156,7 +1252,9 @@ function enterRequest(name, id, method, address, data, headers) { if (address) { store.request.url = address; } - if (data && typeof data === 'object') { + if (data && typeof data === 'string') { + store.request.body = data; + } else if (data && typeof data === 'object') { store.request.data = Object.freeze(Object.assign({}, data)); } if (headers) { diff --git a/test/com/shim/request.js b/test/com/shim/request.js index 86e68cd..54a545f 100644 --- a/test/com/shim/request.js +++ b/test/com/shim/request.js @@ -29,7 +29,7 @@ test.serial('pre', t => { postman[Request]({ pre() { t.pass(); - } + }, }); }); @@ -37,7 +37,7 @@ test.serial('post', t => { postman[Request]({ post() { t.pass(); - } + }, }); }); @@ -50,7 +50,7 @@ test.serial('request', t => { }, post() { t.is(typeof request, 'object'); - } + }, }); t.is(request, undef); }); @@ -59,13 +59,13 @@ test.serial('request.data', t => { const data = { First: 'One', Second: 'Two', - Third: 'Three' + Third: 'Three', }; postman[Request]({ data, pre() { t.deepEqual(request.data, data); - } + }, }); }); @@ -73,13 +73,13 @@ test.serial('request.headers', t => { const headers = { First: 'One', Second: 'Two', - Third: 'Three' + Third: 'Three', }; postman[Request]({ headers, pre() { t.deepEqual(request.headers, headers); - } + }, }); }); @@ -89,7 +89,7 @@ test.serial('request.id', t => { id, pre() { t.is(request.id, id); - } + }, }); }); @@ -98,7 +98,7 @@ test.serial('request.method', t => { method: 'get', pre() { t.is(request.method, 'GET'); - } + }, }); }); @@ -107,7 +107,7 @@ test.serial('request.name', t => { name: 'Test Request', pre() { t.is(request.name, 'Test Request'); - } + }, }); }); @@ -116,37 +116,147 @@ test.serial('request.url', t => { address: 'http://example.com', pre() { t.is(request.url, 'http://example.com'); - } + }, }); }); test.serial('pm.request.headers', t => { postman[Request]({ + headers: { + marco: 'polo', + foo: 'bar', + }, pre() { - t.throws(() => { - pm.request.headers; /* eslint-disable-line no-unused-expressions */ - }); - } + t.deepEqual(pm.request.headers, [ + { key: 'marco', value: 'polo' }, + { key: 'foo', value: 'bar' }, + ]); + }, + }); +}); + +test.serial('pm.request.method', t => { + postman[Request]({ + method: 'POST', + pre() { + t.deepEqual(pm.request.method, 'POST'); + }, + }); +}); + +test.serial('pm.request.id', t => { + const input = '33d2dd9f-e4fc-46fb-9885-df53f1b2310b'; + postman[Request]({ + id: input, + pre() { + t.deepEqual(pm.request.id, input); + }, + }); +}); + +test.serial('pm.request.name', t => { + const input = 'Postman request name'; + postman[Request]({ + name: input, + pre() { + t.deepEqual(pm.request.name, input); + }, + }); +}); + +test.serial('pm.request.auth', t => { + postman[Request]({ + pre() { + t.deepEqual(pm.request.auth, undefined); + }, + auth(config, Var) { + config.headers.Authorization = 'Bearer access_token'; + config.options.auth = 'bearer'; + }, + }); +}); + +test.serial('pm.request.body', t => { + const bodyRaw = + '{\n "booleanVar": true,\n "dynVar": "{{$randomCity}}",\n "numberVar": 12345,\n "stringVar": "my-tax"\n}'; + postman[Request]({ + data: bodyRaw, + pre() { + t.deepEqual(pm.request.body, { mode: 'raw', raw: bodyRaw }); + }, + }); +}); + +test.serial('pm.request.body.raw', t => { + const bodyRaw = + '{\n "booleanVar": true,\n "dynVar": "{{$randomCity}}",\n "numberVar": 12345,\n "stringVar": "my-tax"\n}'; + postman[Request]({ + data: bodyRaw, + pre() { + t.deepEqual(pm.request.body.raw, bodyRaw); + }, + }); +}); + +test.serial('pm.request.body.urlencoded', t => { + const bodyUrlencoded = { + booleanVar: true, + dynVar: '{{$randomCity}}', + numberVar: 12345, + stringVar: 'my-tax', + }; + postman[Request]({ + data: bodyUrlencoded, + pre() { + t.deepEqual(pm.request.body.urlencoded, [ + { key: 'booleanVar', value: true }, + { + key: 'dynVar', + value: '{{$randomCity}}', + }, + { + key: 'numberVar', + value: 12345, + }, + { + key: 'stringVar', + value: 'my-tax', + }, + ]); + }, }); }); test.serial('pm.request.url', t => { postman[Request]({ - address: 'http://example.com', + address: + 'http://127.0.0.1:4010/widget/:id?name=widget1&type=small#section2', pre() { - t.throws(() => { - pm.request.url; /* eslint-disable-line no-unused-expressions */ + t.deepEqual(pm.request.url, { + fragment: 'section2', + host: ['127', '0', '0', '1'], + path: ['widget', ':id'], + port: '4010', + protocol: 'http', + query: [ + { key: 'name', value: 'widget1' }, + { + key: 'type', + value: 'small', + }, + ], + variable: ['id'], }); - } + }, }); }); test.serial('variable', t => { postman[Initial]({ - global: { domain: 'example.com', path: '/index.html' } + global: { domain: 'example.com', path: '/index.html' }, }); postman[Request]({ - address: 'http://{{domain}}{{path}}' + address: 'http://{{domain}}{{path}}', }); t.true(http.request.calledOnce); const args = http.request.firstCall.args; @@ -159,7 +269,7 @@ test.serial('args', t => { address: 'http://example.com', data: { test: 'a', test2: 'b' }, headers: { Test: 'a', Test2: 'b' }, - options: { auth: 'basic' } + options: { auth: 'basic' }, }); t.true(http.request.calledOnce); const args = http.request.firstCall.args; @@ -174,3 +284,22 @@ test.serial('pm.sendRequest', t => { pm.sendRequest(); }); }); + +test.serial('request.body.raw', t => { + const rawBody = JSON.stringify({ key1: 'value1', key2: 'value2' }); + + postman[Request]({ + method: 'POST', + address: 'http://example.com', + data: rawBody, + pre() { + t.is(request.body, rawBody); + }, + }); + + t.true(http.request.calledOnce); + const args = http.request.firstCall.args; + t.is(args[0], 'POST'); + t.is(args[1], 'http://example.com'); + t.is(args[2], rawBody); +});