diff --git a/src/extension/ipc/network/interpolate-vars.spec.ts b/src/extension/ipc/network/interpolate-vars.spec.ts new file mode 100644 index 0000000..0653c1c --- /dev/null +++ b/src/extension/ipc/network/interpolate-vars.spec.ts @@ -0,0 +1,85 @@ +import { describe, it, expect } from 'vitest'; +import { interpolateVars } from './interpolate-vars'; + +const formUrlEncodedHeaders = { 'Content-Type': 'application/x-www-form-urlencoded' }; + +describe('interpolateVars - form-urlencoded body', () => { + it('interpolates values in array form data', () => { + const request = { + url: 'https://example.com/token', + headers: { ...formUrlEncodedHeaders }, + data: [ + { name: 'grant_type', value: 'client_credentials', enabled: true }, + { name: 'client_secret', value: '{{ClientSecret}}', enabled: true } + ] + }; + + const result = interpolateVars(request, { + envVars: { ClientSecret: 's3cr3t-value' } + }); + + expect(result.data[1].value).toBe('s3cr3t-value'); + }); + + it('interpolates variables in a pre-serialized string body (send-http-request flow)', () => { + // The send-http-request handler stringifies the form body via qs.stringify + // before scripts run, so interpolateVars receives a string with encoded braces + const request = { + url: 'https://example.com/token', + headers: { ...formUrlEncodedHeaders }, + data: 'grant_type=client_credentials&client_secret=%7B%7BClientSecret%7D%7D&client_id=my-client' + }; + + const result = interpolateVars(request, { + envVars: { ClientSecret: 's3cr3t-value' } + }); + + const params = new URLSearchParams(result.data as string); + expect(params.get('client_secret')).toBe('s3cr3t-value'); + expect(params.get('grant_type')).toBe('client_credentials'); + expect(params.get('client_id')).toBe('my-client'); + }); + + it('interpolates unencoded braces in a string body', () => { + const request = { + url: 'https://example.com/token', + headers: { ...formUrlEncodedHeaders }, + data: 'client_secret={{ClientSecret}}' + }; + + const result = interpolateVars(request, { + runtimeVariables: { ClientSecret: 'runtime-value' } + }); + + const params = new URLSearchParams(result.data as string); + expect(params.get('client_secret')).toBe('runtime-value'); + }); + + it('resolves process.env variables in a string body', () => { + const request = { + url: 'https://example.com/token', + headers: { ...formUrlEncodedHeaders }, + data: 'client_secret=%7B%7Bprocess.env.CLIENT_SECRET%7D%7D' + }; + + const result = interpolateVars(request, { + processEnvVars: { CLIENT_SECRET: 'env-file-value' } + }); + + const params = new URLSearchParams(result.data as string); + expect(params.get('client_secret')).toBe('env-file-value'); + }); + + it('leaves a string body without variables untouched', () => { + const data = 'grant_type=client_credentials&client_secret=plain-value'; + const request = { + url: 'https://example.com/token', + headers: { ...formUrlEncodedHeaders }, + data + }; + + const result = interpolateVars(request, { envVars: {} }); + + expect(result.data).toBe(data); + }); +}); diff --git a/src/extension/ipc/network/interpolate-vars.ts b/src/extension/ipc/network/interpolate-vars.ts index 7d561c9..7e859e1 100644 --- a/src/extension/ipc/network/interpolate-vars.ts +++ b/src/extension/ipc/network/interpolate-vars.ts @@ -228,6 +228,20 @@ const interpolateVars = (request: any, options: InterpolationOptions): any => { ...d, value: _interpolate(d?.value) })); + } else if (typeof request.data === 'string' && request.data.length && (request.data.includes('{{') || request.data.includes('%7B%7B'))) { + // The body may have been serialized to a form-urlencoded string before + // script execution (see send-http-request handler). Parse it, interpolate + // each name/value pair, then re-serialize so {{variables}} are resolved. + try { + const parsedParams = new URLSearchParams(request.data); + const interpolatedParams = new URLSearchParams(); + for (const [name, value] of parsedParams) { + interpolatedParams.append(_interpolate(name) as string, _interpolate(value) as string); + } + request.data = interpolatedParams.toString(); + } catch { + // Leave the body as-is if it cannot be parsed + } } } else if (contentType === 'multipart/form-data') { if (Array.isArray(request?.data) && !(request.data instanceof FormData)) {