fix(cli): strip module prefix before script execution (#5835)
This commit is contained in:
parent
32114fc8ef
commit
5310b9fb40
4 changed files with 71 additions and 48 deletions
|
|
@ -19,7 +19,7 @@
|
|||
}
|
||||
],
|
||||
"preRequestScript": "",
|
||||
"testScript": "hopp.test(\"`hopp.response.body.asJSON()` parses response body as JSON\", () => {\n const parsedData = JSON.parse(hopp.response.body.asJSON().data)\n\n hopp.expect(parsedData.name).toBe('John Doe')\n hopp.expect(parsedData.age).toBeType(\"number\")\n})\n\npm.test(\"`pm.response.json()` parses response body as JSON\", () => {\n const parsedData = JSON.parse(pm.response.json().data)\n\n pm.expect(parsedData.name).toBe('John Doe')\n pm.expect(parsedData.age).toBeType(\"number\")\n})\n\nhopp.test(\"`hopp.response.body.asText()` parses response body as plain text\", () => {\n const textResponse = hopp.response.body.asText()\n hopp.expect(textResponse).toInclude('\\\"test-header\\\":\\\"test\\\"')\n})\n\npm.test(\"`pm.response.text()` parses response body as plain text\", () => {\n const textResponse = pm.response.text()\n pm.expect(textResponse).toInclude('\\\"test-header\\\":\\\"test\\\"')\n})\n\nhopp.test(\"hopp.response.bytes()` parses response body as raw bytes\", () => {\n const rawResponse = hopp.response.body.bytes()\n\n hopp.expect(rawResponse[0]).toBe(123)\n})\n\npm.test(\"pm.response.stream` parses response body as raw bytes\", () => {\n const rawResponse = pm.response.stream\n\n pm.expect(rawResponse[0]).toBe(123)\n})\n",
|
||||
"testScript": "export {};\nhopp.test(\"`hopp.response.body.asJSON()` parses response body as JSON\", () => {\n const parsedData = JSON.parse(hopp.response.body.asJSON().data)\n\n hopp.expect(parsedData.name).toBe('John Doe')\n hopp.expect(parsedData.age).toBeType(\"number\")\n})\n\npm.test(\"`pm.response.json()` parses response body as JSON\", () => {\n const parsedData = JSON.parse(pm.response.json().data)\n\n pm.expect(parsedData.name).toBe('John Doe')\n pm.expect(parsedData.age).toBeType(\"number\")\n})\n\nhopp.test(\"`hopp.response.body.asText()` parses response body as plain text\", () => {\n const textResponse = hopp.response.body.asText()\n hopp.expect(textResponse).toInclude('\\\"test-header\\\":\\\"test\\\"')\n})\n\npm.test(\"`pm.response.text()` parses response body as plain text\", () => {\n const textResponse = pm.response.text()\n pm.expect(textResponse).toInclude('\\\"test-header\\\":\\\"test\\\"')\n})\n\nhopp.test(\"hopp.response.bytes()` parses response body as raw bytes\", () => {\n const rawResponse = hopp.response.body.bytes()\n\n hopp.expect(rawResponse[0]).toBe(123)\n})\n\npm.test(\"pm.response.stream` parses response body as raw bytes\", () => {\n const rawResponse = pm.response.stream\n\n pm.expect(rawResponse[0]).toBe(123)\n})\n",
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": true
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"preRequestScript": "hopp.env.set('test_key', 'test_value')\nhopp.env.set('recursive_key', '<<test_key>>')\nhopp.env.global.set('global_key', 'global_value')\nhopp.env.active.set('active_key', 'active_value')\n\n// `pm` namespace equivalents\npm.variables.set('pm_test_key', 'pm_test_value')\npm.environment.set('pm_active_key', 'pm_active_value')\npm.globals.set('pm_global_key', 'pm_global_value')\n",
|
||||
"preRequestScript": "export {};\nhopp.env.set('test_key', 'test_value')\nhopp.env.set('recursive_key', '<<test_key>>')\nhopp.env.global.set('global_key', 'global_value')\nhopp.env.active.set('active_key', 'active_value')\n\n// `pm` namespace equivalents\npm.variables.set('pm_test_key', 'pm_test_value')\npm.environment.set('pm_active_key', 'pm_active_value')\npm.globals.set('pm_global_key', 'pm_global_value')\n",
|
||||
"testScript": "\nhopp.test('`hopp.env.get()` retrieves environment variables', () => {\n const value = hopp.env.get('test_key')\n hopp.expect(value).toBe('test_value')\n})\n\npm.test('`pm.variables.get()` retrieves environment variables', () => {\n const value = pm.variables.get('test_key')\n pm.expect(value).toBe('test_value')\n})\n\nhopp.test('`hopp.env.getRaw()` retrieves raw environment variables without resolution', () => {\n const rawValue = hopp.env.getRaw('recursive_key')\n hopp.expect(rawValue).toBe('<<test_key>>')\n})\n\nhopp.test('`hopp.env.get()` resolves recursive environment variables', () => {\n const resolvedValue = hopp.env.get('recursive_key')\n hopp.expect(resolvedValue).toBe('test_value')\n})\n\npm.test('`pm.variables.replaceIn()` resolves template variables', () => {\n const resolved = pm.variables.replaceIn('Value is {{test_key}}')\n pm.expect(resolved).toBe('Value is test_value')\n})\n\nhopp.test('`hopp.env.global.get()` retrieves global environment variables', () => {\n const globalValue = hopp.env.global.get('global_key')\n\n // `hopp.env.global` would be empty for the CLI\n if (globalValue) {\n hopp.expect(globalValue).toBe('global_value')\n }\n})\n\npm.test('`pm.globals.get()` retrieves global environment variables', () => {\n const globalValue = pm.globals.get('global_key')\n\n // `pm.globals` would be empty for the CLI\n if (globalValue) {\n pm.expect(globalValue).toBe('global_value')\n }\n})\n\nhopp.test('`hopp.env.active.get()` retrieves active environment variables', () => {\n const activeValue = hopp.env.active.get('active_key')\n hopp.expect(activeValue).toBe('active_value')\n})\n\npm.test('`pm.environment.get()` retrieves active environment variables', () => {\n const activeValue = pm.environment.get('active_key')\n pm.expect(activeValue).toBe('active_value')\n})\n\nhopp.test('Environment methods return null for non-existent keys', () => {\n hopp.expect(hopp.env.get('non_existent')).toBe(null)\n hopp.expect(hopp.env.getRaw('non_existent')).toBe(null)\n hopp.expect(hopp.env.global.get('non_existent')).toBe(null)\n hopp.expect(hopp.env.active.get('non_existent')).toBe(null)\n})\n\npm.test('`pm` environment methods handle non-existent keys correctly', () => {\n pm.expect(pm.variables.get('non_existent')).toBe(undefined)\n pm.expect(pm.environment.get('non_existent')).toBe(undefined)\n pm.expect(pm.globals.get('non_existent')).toBe(undefined)\n pm.expect(pm.variables.has('non_existent')).toBe(false)\n pm.expect(pm.environment.has('non_existent')).toBe(false)\n pm.expect(pm.globals.has('non_existent')).toBe(false)\n})\n\npm.test('`pm` variables set in pre-request script are accessible', () => {\n pm.expect(pm.variables.get('pm_test_key')).toBe('pm_test_value')\n pm.expect(pm.environment.get('pm_active_key')).toBe('pm_active_value')\n\n const pmGlobalValue = hopp.env.global.get('pm_global_key')\n\n // `hopp.env.global` would be empty for the CLI\n if (pmGlobalValue) {\n hopp.expect(pmGlobalValue).toBe('pm_global_value')\n }\n})\n",
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
|
|
@ -760,7 +760,7 @@
|
|||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"preRequestScript": "export {};",
|
||||
"preRequestScript": "export {};\n",
|
||||
"testScript": "\n// Map & Set Assertions\npm.test('Map assertions - size property', () => {\n const map = new Map([['key1', 'value1'], ['key2', 'value2']])\n pm.expect(map).to.have.property('size', 2)\n pm.expect(map.size).to.equal(2)\n})\n\npm.test('Set assertions - size property', () => {\n const set = new Set([1, 2, 3, 4])\n pm.expect(set).to.have.property('size', 4)\n pm.expect(set.size).to.equal(4)\n})\n\npm.test('Map instanceOf assertion', () => {\n const map = new Map()\n pm.expect(map).to.be.instanceOf(Map)\n pm.expect(map).to.be.an.instanceOf(Map)\n})\n\npm.test('Set instanceOf assertion', () => {\n const set = new Set()\n pm.expect(set).to.be.instanceOf(Set)\n pm.expect(set).to.be.an.instanceOf(Set)\n})\n\n// Advanced Chai - closeTo\npm.test('closeTo - validates numbers within delta', () => {\n pm.expect(3.14159).to.be.closeTo(3.14, 0.01)\n pm.expect(10.5).to.be.closeTo(11, 1)\n})\n\npm.test('closeTo - negation works', () => {\n pm.expect(100).to.not.be.closeTo(50, 10)\n pm.expect(3.14).to.not.be.closeTo(10, 0.1)\n})\n\npm.test('approximately - alias for closeTo', () => {\n pm.expect(2.5).to.approximately(2.4, 0.2)\n pm.expect(99.99).to.approximately(100, 0.1)\n})\n\n// Advanced Chai - finite\npm.test('finite - validates finite numbers', () => {\n pm.expect(123).to.be.finite\n pm.expect(0).to.be.finite\n pm.expect(-456).to.be.finite\n})\n\npm.test('finite - negation for Infinity', () => {\n pm.expect(Infinity).to.not.be.finite\n pm.expect(-Infinity).to.not.be.finite\n pm.expect(NaN).to.not.be.finite\n})\n\n// Advanced Chai - satisfy\npm.test('satisfy - custom predicate function', () => {\n pm.expect(10).to.satisfy((num) => num > 5)\n pm.expect('hello').to.satisfy((str) => str.length === 5)\n})\n\npm.test('satisfy - complex validation', () => {\n const obj = { name: 'test', value: 100 }\n pm.expect(obj).to.satisfy((o) => o.value > 50 && o.name.length > 0)\n})\n\npm.test('satisfy - negation works', () => {\n pm.expect(5).to.not.satisfy((num) => num > 10)\n pm.expect('abc').to.not.satisfy((str) => str.length > 5)\n})\n\n// Advanced Chai - respondTo\npm.test('respondTo - validates method existence', () => {\n class TestClass {\n testMethod() { return 'test' }\n anotherMethod() { return 'another' }\n }\n pm.expect(TestClass).to.respondTo('testMethod')\n pm.expect(TestClass).to.respondTo('anotherMethod')\n})\n\npm.test('respondTo - with itself for static methods', () => {\n class MyClass {\n static staticMethod() { return 'static' }\n instanceMethod() { return 'instance' }\n }\n pm.expect(MyClass).itself.to.respondTo('staticMethod')\n pm.expect(MyClass).to.not.itself.respondTo('instanceMethod')\n pm.expect(MyClass).to.respondTo('instanceMethod')\n})\n\n// Property Ownership - own.property\npm.test('own.property - distinguishes own vs inherited', () => {\n const parent = { inherited: true }\n const obj = Object.create(parent)\n obj.own = true\n pm.expect(obj).to.have.own.property('own')\n pm.expect(obj).to.not.have.own.property('inherited')\n pm.expect(obj).to.have.property('inherited')\n})\n\npm.test('deep.own.property - deep check with ownership', () => {\n const proto = { shared: 'inherited' }\n const obj = Object.create(proto)\n obj.data = { nested: 'value' }\n pm.expect(obj).to.have.deep.own.property('data', { nested: 'value' })\n pm.expect(obj).to.not.have.deep.own.property('shared')\n})\n\npm.test('ownProperty - alias for own.property', () => {\n const obj = { prop: 'value' }\n pm.expect(obj).to.have.ownProperty('prop')\n pm.expect(obj).to.have.ownProperty('prop', 'value')\n})\n\n// Hopp namespace parity tests\npm.test('hopp.expect Map/Set support', () => {\n const map = new Map([['x', 1]])\n const set = new Set([1, 2])\n hopp.expect(map.size).toBe(1)\n hopp.expect(set.size).toBe(2)\n})\n\npm.test('hopp.expect closeTo support', () => {\n hopp.expect(3.14).to.be.closeTo(3.1, 0.1)\n hopp.expect(10).to.be.closeTo(10.5, 1)\n})\n\npm.test('hopp.expect finite support', () => {\n hopp.expect(42).to.be.finite\n hopp.expect(Infinity).to.not.be.finite\n})\n\npm.test('hopp.expect satisfy support', () => {\n hopp.expect(100).to.satisfy((n) => n > 50)\n hopp.expect('test').to.satisfy((s) => s.length === 4)\n})\n\npm.test('hopp.expect respondTo support', () => {\n class TestClass { method() {} }\n hopp.expect(TestClass).to.respondTo('method')\n})\n\npm.test('hopp.expect own.property support', () => {\n const obj = Object.create({ inherited: 1 })\n obj.own = 2\n hopp.expect(obj).to.have.own.property('own')\n hopp.expect(obj).to.not.have.own.property('inherited')\n})\n\npm.test('hopp.expect ordered.members support', () => {\n const arr = ['a', 'b', 'c']\n hopp.expect(arr).to.have.ordered.members(['a', 'b', 'c'])\n})\n",
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
|
|
@ -1683,4 +1683,4 @@
|
|||
"headers": [],
|
||||
"variables": [],
|
||||
"description": null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,3 +158,19 @@ export async function parseCollectionData(
|
|||
|
||||
return getValidRequests(collectionSchemaParsedResult.data, pathOrId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Module prefix added by Monaco editor for TypeScript module mode.
|
||||
*/
|
||||
const MODULE_PREFIX = "export {};\n" as const;
|
||||
|
||||
/**
|
||||
* Strips `export {};\n` prefix from scripts before sandbox execution.
|
||||
* The prefix is added by the web app's Monaco editor for IntelliSense
|
||||
* and must be removed before execution.
|
||||
*/
|
||||
export const stripModulePrefix = (script: string): string => {
|
||||
return script.startsWith(MODULE_PREFIX)
|
||||
? script.slice(MODULE_PREFIX.length)
|
||||
: script;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import {
|
||||
Environment,
|
||||
EnvironmentVariable,
|
||||
HoppCollectionVariable,
|
||||
HoppRESTRequest,
|
||||
calculateHawkHeader,
|
||||
generateJWTToken,
|
||||
parseBodyEnvVariablesE,
|
||||
parseRawKeyValueEntriesE,
|
||||
parseTemplateString,
|
||||
parseTemplateStringE,
|
||||
generateJWTToken,
|
||||
HoppCollectionVariable,
|
||||
calculateHawkHeader,
|
||||
} from "@hoppscotch/data";
|
||||
import { runPreRequestScript } from "@hoppscotch/js-sandbox/node";
|
||||
import { createHoppFetchHook } from "./hopp-fetch";
|
||||
import { AwsV4Signer } from "aws4fetch";
|
||||
import * as A from "fp-ts/Array";
|
||||
import * as E from "fp-ts/Either";
|
||||
import * as O from "fp-ts/Option";
|
||||
|
|
@ -20,22 +20,22 @@ import * as TE from "fp-ts/TaskEither";
|
|||
import { flow, pipe } from "fp-ts/function";
|
||||
import * as S from "fp-ts/string";
|
||||
import qs from "qs";
|
||||
import { AwsV4Signer } from "aws4fetch";
|
||||
import { createHoppFetchHook } from "./hopp-fetch";
|
||||
|
||||
import { EffectiveHoppRESTRequest } from "../interfaces/request";
|
||||
import { HoppCLIError, error } from "../types/errors";
|
||||
import { HoppEnvs } from "../types/request";
|
||||
import { PreRequestMetrics } from "../types/response";
|
||||
import { isHoppCLIError } from "./checks";
|
||||
import { arrayFlatMap, arraySort, tupleToRecord } from "./functions/array";
|
||||
import { getEffectiveFinalMetaData, getResolvedVariables } from "./getters";
|
||||
import { toFormData } from "./mutators";
|
||||
import {
|
||||
DigestAuthParams,
|
||||
fetchInitialDigestAuthInfo,
|
||||
generateDigestAuthHeader,
|
||||
} from "./auth/digest";
|
||||
import { isHoppCLIError } from "./checks";
|
||||
import { arrayFlatMap, arraySort, tupleToRecord } from "./functions/array";
|
||||
import { getEffectiveFinalMetaData, getResolvedVariables } from "./getters";
|
||||
import { stripComments } from "./jsonc";
|
||||
import { stripModulePrefix, toFormData } from "./mutators";
|
||||
|
||||
/**
|
||||
* Runs pre-request-script runner over given request which extracts set ENVs and
|
||||
|
|
@ -60,7 +60,7 @@ export const preRequestScriptRunner = (
|
|||
return pipe(
|
||||
TE.of(request),
|
||||
TE.chain(({ preRequestScript }) =>
|
||||
runPreRequestScript(preRequestScript, {
|
||||
runPreRequestScript(stripModulePrefix(preRequestScript), {
|
||||
envs,
|
||||
experimentalScriptingSandbox,
|
||||
request,
|
||||
|
|
@ -586,7 +586,10 @@ function getFinalBodyFromRequest(
|
|||
// and vendor-specific JSON media types (for example those with a +json suffix
|
||||
// or subtypes whose names end with "json" or "-json").
|
||||
if (request.body.contentType) {
|
||||
const mimeType = request.body.contentType.split(";")[0].trim().toLowerCase();
|
||||
const mimeType = request.body.contentType
|
||||
.split(";")[0]
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
if (
|
||||
mimeType === "application/json" ||
|
||||
|
|
@ -594,43 +597,46 @@ function getFinalBodyFromRequest(
|
|||
mimeType.endsWith("/json") ||
|
||||
mimeType.endsWith("-json")
|
||||
) {
|
||||
const envResult = parseBodyEnvVariablesE(request.body.body, resolvedVariables);
|
||||
|
||||
if (E.isLeft(envResult)) {
|
||||
return E.left(
|
||||
error({
|
||||
code: "PARSING_ERROR",
|
||||
data: `${request.body.body} (${envResult.left})`,
|
||||
})
|
||||
const envResult = parseBodyEnvVariablesE(
|
||||
request.body.body,
|
||||
resolvedVariables
|
||||
);
|
||||
}
|
||||
|
||||
const bodyString = envResult.right;
|
||||
if (E.isLeft(envResult)) {
|
||||
return E.left(
|
||||
error({
|
||||
code: "PARSING_ERROR",
|
||||
data: `${request.body.body} (${envResult.left})`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// If the body string is empty or null, return null
|
||||
if (!bodyString || S.isEmpty(bodyString.trim())) {
|
||||
return E.right(null);
|
||||
}
|
||||
const bodyString = envResult.right;
|
||||
|
||||
// Strip comments and trailing commas from JSONC
|
||||
// This ensures collections with comments work the same in CLI as in desktop app
|
||||
const cleanedBody = stripComments(bodyString);
|
||||
// If the body string is empty or null, return null
|
||||
if (!bodyString || S.isEmpty(bodyString.trim())) {
|
||||
return E.right(null);
|
||||
}
|
||||
|
||||
// Try to parse the JSON body
|
||||
try {
|
||||
const parsedBody = JSON.parse(cleanedBody);
|
||||
return E.right(JSON.stringify(parsedBody));
|
||||
} catch (err) {
|
||||
// If parsing fails after stripping comments, return error to provide
|
||||
// immediate feedback instead of sending invalid JSON to the API.
|
||||
// Use original template string to avoid leaking secrets from env vars.
|
||||
return E.left(
|
||||
error({
|
||||
code: "PARSING_ERROR",
|
||||
data: `${request.body.body} (Invalid JSON in request body: ${err instanceof Error ? err.message : String(err)})`,
|
||||
})
|
||||
);
|
||||
}
|
||||
// Strip comments and trailing commas from JSONC
|
||||
// This ensures collections with comments work the same in CLI as in desktop app
|
||||
const cleanedBody = stripComments(bodyString);
|
||||
|
||||
// Try to parse the JSON body
|
||||
try {
|
||||
const parsedBody = JSON.parse(cleanedBody);
|
||||
return E.right(JSON.stringify(parsedBody));
|
||||
} catch (err) {
|
||||
// If parsing fails after stripping comments, return error to provide
|
||||
// immediate feedback instead of sending invalid JSON to the API.
|
||||
// Use original template string to avoid leaking secrets from env vars.
|
||||
return E.left(
|
||||
error({
|
||||
code: "PARSING_ERROR",
|
||||
data: `${request.body.body} (Invalid JSON in request body: ${err instanceof Error ? err.message : String(err)})`,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return pipe(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { HoppEnvs } from "../types/request";
|
|||
import { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response";
|
||||
import { getDurationInSeconds } from "./getters";
|
||||
import { createHoppFetchHook } from "./hopp-fetch";
|
||||
import { stripModulePrefix } from "./mutators";
|
||||
|
||||
/**
|
||||
* Executes test script and runs testDescriptorParser to generate test-report using
|
||||
|
|
@ -52,7 +53,7 @@ export const testRunner = (
|
|||
const experimentalScriptingSandbox = !legacySandbox;
|
||||
const hoppFetchHook = createHoppFetchHook();
|
||||
|
||||
return runTestScript(request.testScript, {
|
||||
return runTestScript(stripModulePrefix(request.testScript), {
|
||||
envs,
|
||||
request,
|
||||
response: effectiveResponse,
|
||||
|
|
|
|||
Loading…
Reference in a new issue