-
Notifications
You must be signed in to change notification settings - Fork 2.3k
fix: resolve variables in HTML documentation export #7768
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,178 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| import { interpolate, interpolateObject } from '@usebruno/common'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { each } from 'lodash'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { flattenItems, getAllVariables, isItemARequest } from 'utils/collections'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { interpolateUrl, interpolateUrlPathParams } from 'utils/url'; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const interpolateString = (value, variables, options = {}) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof value !== 'string') { | ||||||||||||||||||||||||||||||||||||||||||||||
| return value; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return interpolate(value, variables, options); | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const interpolateEntries = (entries = [], variables = {}) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!Array.isArray(entries)) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return entries; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return entries.map((entry) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!entry || typeof entry !== 'object') { | ||||||||||||||||||||||||||||||||||||||||||||||
| return entry; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||
| ...entry, | ||||||||||||||||||||||||||||||||||||||||||||||
| name: interpolateString(entry.name, variables), | ||||||||||||||||||||||||||||||||||||||||||||||
| value: Array.isArray(entry.value) | ||||||||||||||||||||||||||||||||||||||||||||||
| ? entry.value.map((value) => interpolateString(value, variables)) | ||||||||||||||||||||||||||||||||||||||||||||||
| : interpolateString(entry.value, variables) | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const interpolateWsMessages = (messages = [], variables = {}) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!Array.isArray(messages)) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return messages; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return messages.map((message) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!message || typeof message !== 'object') { | ||||||||||||||||||||||||||||||||||||||||||||||
| return message; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| let escapeJSONStrings = message?.type === 'json'; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (!escapeJSONStrings && typeof message?.content === 'string') { | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| JSON.parse(message.content); | ||||||||||||||||||||||||||||||||||||||||||||||
| escapeJSONStrings = true; | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||
| // no-op, plain text payload | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||
| ...message, | ||||||||||||||||||||||||||||||||||||||||||||||
| name: interpolateString(message.name, variables), | ||||||||||||||||||||||||||||||||||||||||||||||
| content: interpolateString(message.content, variables, { escapeJSONStrings }) | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const interpolateRequestBody = (body, variables = {}) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!body || typeof body !== 'object') { | ||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| switch (body.mode) { | ||||||||||||||||||||||||||||||||||||||||||||||
| case 'json': | ||||||||||||||||||||||||||||||||||||||||||||||
| body.json = interpolateString(body.json, variables, { escapeJSONStrings: true }); | ||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||
| case 'text': | ||||||||||||||||||||||||||||||||||||||||||||||
| body.text = interpolateString(body.text, variables); | ||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||
| case 'xml': | ||||||||||||||||||||||||||||||||||||||||||||||
| body.xml = interpolateString(body.xml, variables); | ||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||
| case 'graphql': | ||||||||||||||||||||||||||||||||||||||||||||||
| body.graphql = interpolateString(body.graphql, variables, { escapeJSONStrings: true }); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||
| case 'sparql': | ||||||||||||||||||||||||||||||||||||||||||||||
| body.sparql = interpolateString(body.sparql, variables); | ||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||
| case 'formUrlEncoded': | ||||||||||||||||||||||||||||||||||||||||||||||
| body.formUrlEncoded = interpolateEntries(body.formUrlEncoded, variables); | ||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||
| case 'multipartForm': | ||||||||||||||||||||||||||||||||||||||||||||||
| body.multipartForm = Array.isArray(body.multipartForm) | ||||||||||||||||||||||||||||||||||||||||||||||
| ? body.multipartForm.map((entry) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!entry || typeof entry !== 'object') { | ||||||||||||||||||||||||||||||||||||||||||||||
| return entry; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (entry.type && entry.type !== 'text') { | ||||||||||||||||||||||||||||||||||||||||||||||
| return entry; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||
| ...entry, | ||||||||||||||||||||||||||||||||||||||||||||||
| name: interpolateString(entry.name, variables), | ||||||||||||||||||||||||||||||||||||||||||||||
| value: interpolateString(entry.value, variables) | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interpolate multipart field names for non-text entries too. Line 94 currently returns early for non-text multipart entries, so 💡 Proposed fix- if (entry.type && entry.type !== 'text') {
- return entry;
- }
-
- return {
- ...entry,
- name: interpolateString(entry.name, variables),
- value: interpolateString(entry.value, variables)
- };
+ const interpolatedEntry = {
+ ...entry,
+ name: interpolateString(entry.name, variables)
+ };
+
+ if (entry.type && entry.type !== 'text') {
+ return interpolatedEntry;
+ }
+
+ return {
+ ...interpolatedEntry,
+ value: interpolateString(entry.value, variables)
+ };📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||
| : body.multipartForm; | ||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||
| case 'grpc': | ||||||||||||||||||||||||||||||||||||||||||||||
| body.grpc = Array.isArray(body.grpc) | ||||||||||||||||||||||||||||||||||||||||||||||
| ? body.grpc.map((message) => ({ | ||||||||||||||||||||||||||||||||||||||||||||||
| ...message, | ||||||||||||||||||||||||||||||||||||||||||||||
| name: interpolateString(message?.name, variables), | ||||||||||||||||||||||||||||||||||||||||||||||
| content: interpolateString(message?.content, variables, { escapeJSONStrings: true }) | ||||||||||||||||||||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||||||||||||||||||||
| : body.grpc; | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+152
to
+159
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard malformed gRPC messages before spreading. This branch assumes every 💡 Proposed fix case 'grpc':
body.grpc = Array.isArray(body.grpc)
- ? body.grpc.map((message) => ({
- ...message,
- name: interpolateString(message?.name, variables),
- content: interpolateString(message?.content, variables, { escapeJSONStrings: true })
- }))
+ ? body.grpc.map((message) => {
+ if (!message || typeof message !== 'object') {
+ return message;
+ }
+
+ return {
+ ...message,
+ name: interpolateString(message.name, variables),
+ content: interpolateString(message.content, variables, { escapeJSONStrings: true })
+ };
+ })
: body.grpc;
break;🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||
| case 'ws': | ||||||||||||||||||||||||||||||||||||||||||||||
| body.ws = interpolateWsMessages(body.ws, variables); | ||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const resolveRequestForHtmlDocumentation = (request, variables = {}) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!request || typeof request !== 'object') { | ||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const interpolatedUrl = interpolateUrl({ | ||||||||||||||||||||||||||||||||||||||||||||||
| url: request.url, | ||||||||||||||||||||||||||||||||||||||||||||||
| variables | ||||||||||||||||||||||||||||||||||||||||||||||
| }) || request.url; | ||||||||||||||||||||||||||||||||||||||||||||||
| const hasPathParams = Array.isArray(request.params) | ||||||||||||||||||||||||||||||||||||||||||||||
| ? request.params.some((param) => param?.type === 'path' && param?.enabled !== false) | ||||||||||||||||||||||||||||||||||||||||||||||
| : false; | ||||||||||||||||||||||||||||||||||||||||||||||
| const hasNonHttpProtocol = /^(ws|wss|grpc|grpcs):\/\//i.test(interpolatedUrl || ''); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| request.url = hasPathParams && !hasNonHttpProtocol | ||||||||||||||||||||||||||||||||||||||||||||||
| ? interpolateUrlPathParams(interpolatedUrl, request.params || [], variables, { raw: true }) | ||||||||||||||||||||||||||||||||||||||||||||||
| : interpolatedUrl; | ||||||||||||||||||||||||||||||||||||||||||||||
| request.headers = interpolateEntries(request.headers, variables); | ||||||||||||||||||||||||||||||||||||||||||||||
| request.params = interpolateEntries(request.params, variables); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (request.auth && typeof request.auth === 'object') { | ||||||||||||||||||||||||||||||||||||||||||||||
| request.auth = interpolateObject(request.auth, variables); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (request.body) { | ||||||||||||||||||||||||||||||||||||||||||||||
| interpolateRequestBody(request.body, variables); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| request.docs = interpolateString(request.docs, variables); | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export const resolveCollectionForHtmlDocumentation = (collection) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!collection?.items?.length) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return collection; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const items = flattenItems(collection.items); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| each(items, (item) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!isItemARequest(item) || item.isTransient) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const variables = getAllVariables(collection, item); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| resolveRequestForHtmlDocumentation(item.request, variables); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (Array.isArray(item.examples)) { | ||||||||||||||||||||||||||||||||||||||||||||||
| each(item.examples, (example) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| resolveRequestForHtmlDocumentation(example?.request, variables); | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return collection; | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| import { resolveCollectionForHtmlDocumentation } from './html-documentation'; | ||
|
|
||
| describe('resolveCollectionForHtmlDocumentation', () => { | ||
| it('resolves global variables in request URLs for HTML docs export', () => { | ||
| const collection = { | ||
| uid: 'collection-1', | ||
| activeEnvironmentUid: 'env-1', | ||
| globalEnvironmentVariables: { | ||
| url: 'https://postman-echo.com' | ||
| }, | ||
| environments: [ | ||
| { | ||
| uid: 'env-1', | ||
| variables: [] | ||
| } | ||
| ], | ||
| root: { | ||
| request: { | ||
| vars: { | ||
| req: [] | ||
| } | ||
| } | ||
| }, | ||
| items: [ | ||
| { | ||
| uid: 'request-1', | ||
| type: 'http-request', | ||
| request: { | ||
| url: '{{url}}/get', | ||
| method: 'get', | ||
| headers: [], | ||
| params: [], | ||
| body: { | ||
| mode: 'none' | ||
| }, | ||
| auth: { | ||
| mode: 'none' | ||
| }, | ||
| vars: { | ||
| req: [] | ||
| } | ||
| }, | ||
| examples: [] | ||
| } | ||
| ] | ||
| }; | ||
|
|
||
| resolveCollectionForHtmlDocumentation(collection); | ||
|
|
||
| expect(collection.items[0].request.url).toBe('https://postman-echo.com/get'); | ||
| expect(collection.items[0].request.url).not.toContain('{{url}}'); | ||
| }); | ||
|
|
||
| it('resolves global, collection, and environment variables in request payloads', () => { | ||
| const collection = { | ||
| uid: 'collection-2', | ||
| activeEnvironmentUid: 'env-1', | ||
| globalEnvironmentVariables: { | ||
| url: 'https://postman-echo.com' | ||
| }, | ||
| environments: [ | ||
| { | ||
| uid: 'env-1', | ||
| variables: [ | ||
| { | ||
| name: 'token', | ||
| value: 'env-token', | ||
| enabled: true | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| root: { | ||
| request: { | ||
| vars: { | ||
| req: [ | ||
| { | ||
| name: 'endpoint', | ||
| value: 'get', | ||
| enabled: true | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| }, | ||
| items: [ | ||
| { | ||
| uid: 'request-2', | ||
| type: 'http-request', | ||
| request: { | ||
| url: '{{url}}/:resource', | ||
| method: 'get', | ||
| headers: [ | ||
| { | ||
| uid: 'header-1', | ||
| name: 'Authorization', | ||
| value: 'Bearer {{token}}', | ||
| enabled: true | ||
| } | ||
| ], | ||
| params: [ | ||
| { | ||
| uid: 'param-1', | ||
| type: 'path', | ||
| name: 'resource', | ||
| value: '{{endpoint}}', | ||
| enabled: true | ||
| } | ||
| ], | ||
| body: { | ||
| mode: 'text', | ||
| text: '{{url}}/{{endpoint}}?token={{token}}' | ||
| }, | ||
| auth: { | ||
| mode: 'none' | ||
| }, | ||
| vars: { | ||
| req: [] | ||
| } | ||
| }, | ||
| examples: [] | ||
| } | ||
| ] | ||
| }; | ||
|
|
||
| resolveCollectionForHtmlDocumentation(collection); | ||
|
|
||
| const request = collection.items[0].request; | ||
|
|
||
| expect(request.url).toBe('https://postman-echo.com/get'); | ||
| expect(request.headers[0].value).toBe('Bearer env-token'); | ||
| expect(request.body.text).toBe('https://postman-echo.com/get?token=env-token'); | ||
| expect(JSON.stringify(request)).not.toContain('{{'); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved by sanitizing variables during documentation export:
process.envis excluded and all variables marked assecretare replaced withREDACTEDto prevent leakage of sensitive data.