feat(scripting-revamp): introduce hopp namespace (#5388)

This commit is contained in:
James George 2025-09-24 17:09:55 +05:30 committed by GitHub
parent 37060638df
commit b62b50c1e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
76 changed files with 10502 additions and 830 deletions

View file

@ -236,45 +236,55 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
expect(stdout).not.toContain("https://example.com/path?foo=bar&baz=qux");
expect(stdout).not.toContain("Encoded");
});
});
test("Ensures tests run in sequence order based on request path", async () => {
// Expected order of collection runs
const expectedOrder = [
"root-collection-request",
"folder-1/folder-1-request",
"folder-1/folder-11/folder-11-request",
"folder-1/folder-12/folder-12-request",
"folder-1/folder-13/folder-13-request",
"folder-2/folder-2-request",
"folder-2/folder-21/folder-21-request",
"folder-2/folder-22/folder-22-request",
"folder-2/folder-23/folder-23-request",
"folder-3/folder-3-request",
"folder-3/folder-31/folder-31-request",
"folder-3/folder-32/folder-32-request",
"folder-3/folder-33/folder-33-request",
];
test("Ensures tests run in sequence order based on request path", async () => {
// Expected order of collection runs
const expectedOrder = [
"root-collection-request",
"folder-1/folder-1-request",
"folder-1/folder-11/folder-11-request",
"folder-1/folder-12/folder-12-request",
"folder-1/folder-13/folder-13-request",
"folder-2/folder-2-request",
"folder-2/folder-21/folder-21-request",
"folder-2/folder-22/folder-22-request",
"folder-2/folder-23/folder-23-request",
"folder-3/folder-3-request",
"folder-3/folder-31/folder-31-request",
"folder-3/folder-32/folder-32-request",
"folder-3/folder-33/folder-33-request",
];
const normalizePath = (path: string) => path.replace(/\\/g, "/");
const normalizePath = (path: string) => path.replace(/\\/g, "/");
const extractRunningOrder = (stdout: string): string[] =>
[...stdout.matchAll(/Running:.*?\/(.*?)\r?\n/g)].map(
([, path]) => normalizePath(path.replace(/\x1b\[\d+m/g, "")) // Remove ANSI codes and normalize paths
);
const extractRunningOrder = (stdout: string): string[] =>
[...stdout.matchAll(/Running:.*?\/(.*?)\r?\n/g)].map(
([, path]) => normalizePath(path.replace(/\x1b\[\d+m/g, "")) // Remove ANSI codes and normalize paths
);
const args = `test ${getTestJsonFilePath(
"multiple-child-collections-auth-headers-coll.json",
"collection"
)}`;
const args = `test ${getTestJsonFilePath(
"multiple-child-collections-auth-headers-coll.json",
"collection"
)}`;
const { stdout, error } = await runCLI(args);
const { stdout, error } = await runCLI(args);
// Verify the actual order matches the expected order
expect(extractRunningOrder(stdout)).toStrictEqual(expectedOrder);
// Verify the actual order matches the expected order
expect(extractRunningOrder(stdout)).toStrictEqual(expectedOrder);
// Ensure no errors occurred
expect(error).toBeNull();
// Ensure no errors occurred
expect(error).toBeNull();
});
test("Supports the new scripting API method additions under the `hopp` and `pm` namespaces", async () => {
const args = `test ${getTestJsonFilePath(
"scripting-revamp-coll.json",
"collection"
)}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
describe("Test `hopp test <file_path_or_id> --env <file_path_or_id>` command:", () => {

View file

@ -0,0 +1,255 @@
{
"id": "cmfhzf0oo0091qt0iu8yy94rw",
"_ref_id": "coll_mfhz1cx0_5ae46b4c-d9d4-4ef8-92bc-af63525a73d7",
"v": 10,
"name": "scripting-revamp-coll",
"folders": [],
"requests": [
{
"v": "15",
"id": "cmfhzf0oo0092qt0if5rvd2g4",
"name": "json-response-test",
"method": "POST",
"endpoint": "https://echo.hoppscotch.io",
"params": [],
"headers": [
{
"key": "Test-Header",
"value": "test",
"active": true,
"description": "test header"
}
],
"preRequestScript": "export {};\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
},
"body": {
"contentType": "application/json",
"body": "{\n \"name\": \"John Doe\",\n \"age\": 35\n}"
},
"requestVariables": [],
"responses": {}
},
{
"v": "15",
"id": "cmfhzf0op0093qt0ictgoxymy",
"name": "html-response-test",
"method": "GET",
"endpoint": "https://hoppscotch.io",
"params": [],
"headers": [
{
"key": "Test-Header",
"value": "test",
"active": true,
"description": "Test header"
}
],
"preRequestScript": "export {};\n",
"testScript": "export {};\nhopp.test(\"`hopp.response.asText()` parses response body as plain text\", () => {\n const textResponse = hopp.response.body.asText()\n hopp.expect(textResponse).toInclude(\"Open source API development ecosystem\")\n})\n\npm.test(\"`pm.response.text()` parses response body as plain text\", () => {\n const textResponse = pm.response.text()\n pm.expect(textResponse).toInclude(\"Open source API development ecosystem\")\n})\n\nhopp.test(\"`hopp.response.body.bytes()` parses response body as raw bytes\", () => {\n const rawResponse = hopp.response.body.bytes()\n\n hopp.expect(rawResponse[0]).toBe(60)\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(60)\n})\n\n\n",
"auth": {
"authType": "inherit",
"authActive": true
},
"body": {
"contentType": null,
"body": null
},
"requestVariables": [],
"responses": {}
},
{
"v": "15",
"id": "cmfhzf0op0094qt0ixbo9rqnw",
"name": "environment-variables-test",
"method": "GET",
"endpoint": "https://echo.hoppscotch.io",
"params": [],
"headers": [],
"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": "export {};\n\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(null)\n pm.expect(pm.environment.get('non_existent')).toBe(null)\n pm.expect(pm.globals.get('non_existent')).toBe(null)\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",
"authActive": true
},
"body": {
"contentType": null,
"body": null
},
"requestVariables": [],
"responses": {}
},
{
"v": "15",
"id": "cmfhzf0op0095qt0ieogkxx1w",
"name": "request-modification-test",
"method": "GET",
"endpoint": "https://echo.hoppscotch.io",
"params": [
{
"key": "original_param",
"value": "original-param",
"active": true,
"description": ""
}
],
"headers": [
{
"key": "Original-Header",
"value": "original_value",
"active": true,
"description": ""
}
],
"preRequestScript": "export {};\nhopp.request.setUrl('https://echo.hoppscotch.io/modified')\nhopp.request.setMethod('POST')\nhopp.request.setHeader('Modified-Header', 'modified_value')\nhopp.request.setParam('new_param', 'new_value')\n\nhopp.request.setBody({\n contentType: 'application/json',\n body: JSON.stringify({ modified: true, timestamp: Date.now() })\n})\n\nhopp.request.setAuth({\n authType: 'bearer',\n token: 'test-bearer-token',\n authActive: true\n})",
"testScript": "export {};\n\nhopp.test('Request URL was modified by pre-request script', () => {\n hopp.expect(hopp.request.url).toInclude('/modified')\n pm.expect(pm.request.url.toString()).toInclude('/modified')\n})\n\nhopp.test('Request method was modified by pre-request script', () => {\n hopp.expect(hopp.request.method).toBe('POST')\n pm.expect(pm.request.method).toBe('POST')\n})\n\nhopp.test('Request headers contain both original and modified headers', () => {\n const headers = hopp.request.headers\n const hasOriginal = headers.some(h => h.key === 'Original-Header')\n const hasModified = headers.some(h => h.key === 'Modified-Header')\n hopp.expect(hasOriginal).toBe(true)\n hopp.expect(hasModified).toBe(true)\n})\n\npm.test('PM request headers can be accessed and checked', () => {\n pm.expect(pm.request.headers.has('Original-Header')).toBe(true)\n pm.expect(pm.request.headers.has('Modified-Header')).toBe(true)\n pm.expect(pm.request.headers.get('Modified-Header')).toBe('modified_value')\n})\n\nhopp.test('Request parameters contain both original and new parameters', () => {\n const params = hopp.request.params\n const hasOriginal = params.some(p => p.key === 'original_param')\n const hasNew = params.some(p => p.key === 'new_param')\n hopp.expect(hasOriginal).toBe(true)\n hopp.expect(hasNew).toBe(true)\n})\n\nhopp.test('Request body was modified by pre-request script', () => {\n hopp.expect(hopp.request.body.contentType).toBe('application/json')\n pm.expect(pm.request.body.contentType).toBe('application/json')\n const bodyData = hopp.request.body\n\n if (typeof bodyData.body === \"string\") {\n hopp.expect(JSON.parse(bodyData.body).modified).toBe(true)\n pm.expect(JSON.parse(bodyData.body).modified).toBe(true)\n } else {\n throw new Error(`Unexpected body type: ${bodyData.body}`)\n }\n})\n\n\nhopp.test('Request auth was modified by pre-request script', () => {\n const auth = hopp.request.auth\n\n if (auth.authType === 'bearer') {\n hopp.expect(auth.token).toBe('test-bearer-token')\n pm.expect(auth.token).toBe('test-bearer-token')\n } else {\n throw new Error(`Unexpected auth type: ${auth.authType}`)\n }\n\n hopp.expect(auth.token).toBe('test-bearer-token')\n pm.expect(auth.token).toBe('test-bearer-token')\n})\n\n",
"auth": {
"authType": "none",
"authActive": true
},
"body": {
"contentType": null,
"body": null
},
"requestVariables": [],
"responses": {}
},
{
"v": "15",
"id": "cmfhzf0op0096qt0i6wellfus",
"name": "response-parsing-test",
"method": "POST",
"endpoint": "https://echo.hoppscotch.io",
"params": [],
"headers": [
{
"key": "Content-Type",
"value": "application/json",
"active": true,
"description": ""
}
],
"preRequestScript": "export {};\n",
"testScript": "export {};\n\nhopp.test('`hopp.response.statusCode` returns the response status code', () => {\n hopp.expect(hopp.response.statusCode).toBe(200)\n})\n\npm.test('`pm.response.code` returns the response status code', () => {\n pm.expect(pm.response.code).toBe(200)\n})\n\nhopp.test('`hopp.response.statusText` returns the response status text', () => {\n hopp.expect(hopp.response.statusText).toBeType('string')\n})\n\npm.test('`pm.response.status` returns the response status text', () => {\n pm.expect(pm.response.status).toBeType('string')\n})\n\nhopp.test('`hopp.response.headers` contains response headers', () => {\n const { headers } = hopp.response\n\n hopp.expect(headers).toBeType('object')\n hopp.expect(headers.length > 0).toBe(true)\n})\n\npm.test('`pm.response.headers` contains response headers', () => {\n const headersAll = pm.response.headers.all()\n pm.expect(headersAll).toBeType('object')\n pm.expect(Object.keys(headersAll).length > 0).toBe(true)\n})\n\nhopp.test('`hopp.response.responseTime` is a positive number', () => {\n hopp.expect(hopp.response.responseTime).toBeType('number')\n hopp.expect(hopp.response.responseTime > 0).toBe(true)\n})\n\npm.test('`pm.response.responseTime` is a positive number', () => {\n pm.expect(pm.response.responseTime).toBeType('number')\n pm.expect(pm.response.responseTime > 0).toBe(true)\n})\n\nhopp.test('`hopp.response.text()` returns response as text', () => {\n const responseText = hopp.response.body.asText()\n hopp.expect(responseText).toBeType('string')\n hopp.expect(responseText.length > 0).toBe(true)\n})\n\npm.test('`pm.response.text()` returns response as text', () => {\n const responseText = pm.response.text()\n pm.expect(responseText).toBeType('string')\n pm.expect(responseText.length > 0).toBe(true)\n})\n\nhopp.test('`hopp.response.json()` parses JSON response', () => {\n const responseJSON = hopp.response.body.asJSON()\n hopp.expect(responseJSON).toBeType('object')\n})\n\npm.test('`pm.response.json()` parses JSON response', () => {\n const responseJSON = pm.response.json()\n pm.expect(responseJSON).toBeType('object')\n})\n\n\nhopp.test('`hopp.response.bytes()` returns the raw response', () => {\n const responseBuffer = hopp.response.body.bytes()\n hopp.expect(responseBuffer).toBeType('object')\n hopp.expect(responseBuffer.constructor.name).toBe('Object')\n})\n\npm.test('`pm.response.stream` returns the raw response', () => {\n const responseBuffer = pm.response.stream\n pm.expect(responseBuffer).toBeType('object')\n pm.expect(responseBuffer.constructor.name).toBe('Object')\n})",
"auth": {
"authType": "inherit",
"authActive": true
},
"body": {
"contentType": "application/json",
"body": "{\n \"test\": \"response parsing\",\n \"timestamp\": \"{{$timestamp}}\",\n \"data\": {\n \"nested\": true,\n \"value\": 42\n }\n}"
},
"requestVariables": [],
"responses": {}
},
{
"v": "15",
"id": "cmfhzf0op0097qt0ia4wf0lej",
"name": "request-variables-test",
"method": "GET",
"endpoint": "https://echo.hoppscotch.io",
"params": [],
"headers": [],
"preRequestScript": "// Test request variables\nhopp.request.variables.set('dynamic_var', 'dynamic_value')\nhopp.request.variables.set('calculated_var', `timestamp_${Date.now()}`)",
"testScript": "export {};\n\nhopp.test('`hopp.request.variables.get()` retrieves request variables', () => {\n const dynamicValue = hopp.request.variables.get('dynamic_var')\n hopp.expect(dynamicValue).toBe('dynamic_value')\n})\n\nhopp.test('Request variables can store calculated values', () => {\n const calculatedValue = hopp.request.variables.get('calculated_var')\n hopp.expect(calculatedValue).toInclude('timestamp_')\n})\n\nhopp.test('Request variables return null for non-existent keys', () => {\n const nonExistent = hopp.request.variables.get('non_existent_var')\n hopp.expect(nonExistent).toBe(null)\n})\n\nhopp.test('Pre-defined request variables are accessible', () => {\n const preDefinedVar = hopp.request.variables.get('req_var_1')\n hopp.expect(preDefinedVar).toBe('request_variable_value')\n})",
"auth": {
"authType": "inherit",
"authActive": true
},
"body": {
"contentType": null,
"body": null
},
"requestVariables": [
{
"key": "req_var_1",
"value": "request_variable_value",
"active": true
},
{
"key": "dynamic_var",
"value": "dynamic_value",
"active": true
},
{
"key": "calculated_var",
"value": "timestamp_1757751657020",
"active": true
}
],
"responses": {}
},
{
"v": "15",
"id": "cmfhzf0op0098qt0ii9fguj6e",
"name": "info-context-test",
"method": "GET",
"endpoint": "https://echo.hoppscotch.io",
"params": [],
"headers": [],
"preRequestScript": "export {};\n",
"testScript": "export {};\n\npm.test('`pm.info.eventName` indicates the script context', () => {\n pm.expect(pm.info.eventName).toBe('post-request')\n})\n\npm.test('`pm.info.requestName` returns the request name', () => {\n pm.expect(pm.info.requestName).toBe('info-context-test')\n})\n\npm.test('`pm.info.requestId` returns an optional request identifier', () => {\n const requestId = pm.info.requestId\n if (requestId) {\n pm.expect(requestId).toBeType('string')\n pm.expect(requestId?.length > 0).toBe(true)\n } else {\n pm.expect(requestId).toBe(undefined)\n }\n})",
"auth": {
"authType": "inherit",
"authActive": true
},
"body": {
"contentType": null,
"body": null
},
"requestVariables": [],
"responses": {}
},
{
"v": "15",
"id": "cmfhzf0op0099qt0iamthw97r",
"name": "pm-namespace-additional-test",
"method": "GET",
"endpoint": "https://echo.hoppscotch.io",
"params": [],
"headers": [],
"preRequestScript": "export {};\n// Test `pm` namespace specific features\npm.environment.set('pm_pre_key', 'pm_pre_value')\npm.globals.set('pm_global_pre', 'pm_global_pre_value')\npm.variables.set('pm_var_pre', 'pm_var_pre_value')\n",
"testScript": "export {};\n\npm.test('`pm` namespace environment operations work correctly', () => {\n // Test environment has() method\n pm.expect(pm.environment.has('pm_pre_key')).toBe(true)\n pm.expect(pm.environment.has('non_existent_key')).toBe(false)\n \n // Test globals has() method\n const globalValue = pm.globals.has('pm_global_pre')\n // `pm.globals` would be empty for the CLI\n if (globalValue) {\n pm.expect(pm.globals.has('pm_global_pre')).toBe(true)\n }\n \n pm.expect(pm.globals.has('non_existent_global')).toBe(false)\n \n // Test variables has() method\n pm.expect(pm.variables.has('pm_var_pre')).toBe(true)\n pm.expect(pm.variables.has('non_existent_var')).toBe(false)\n})\n\npm.test('`pm` variables.replaceIn() handles template replacement', () => {\n const template = 'Hello {{pm_pre_key}}, global: {{pm_global_pre}}'\n const resolved = pm.variables.replaceIn(template)\n pm.expect(resolved).toInclude('pm_pre_value')\n pm.expect(resolved).toInclude('pm_global_pre_value')\n})\n\npm.test('`pm` request object provides URL as object with toString', () => {\n const url = pm.request.url\n pm.expect(url.toString()).toBeType('string')\n pm.expect(url.toString()).toInclude('echo.hoppscotch.io')\n})\n\npm.test('`pm` request headers object methods work correctly', () => {\n // Test headers.all() returns object\n const allHeaders = pm.request.headers.all()\n pm.expect(allHeaders).toBeType('object')\n \n // Test headers.has() and headers.get() methods\n if (Object.keys(allHeaders).length > 0) {\n const firstHeaderKey = Object.keys(allHeaders)[0]\n pm.expect(pm.request.headers.has(firstHeaderKey)).toBe(true)\n pm.expect(pm.request.headers.get(firstHeaderKey)).toBeType('string')\n }\n \n // Test non-existent header\n pm.expect(pm.request.headers.has('non-existent-header')).toBe(false)\n pm.expect(pm.request.headers.get('non-existent-header')).toBe(null)\n})\n\npm.test('`pm` response headers work correctly', () => {\n // Test response headers all() method\n const allResponseHeaders = pm.response.headers.all()\n pm.expect(allResponseHeaders).toBeType('object')\n \n // Test headers has() and get() for common headers\n if (Object.keys(allResponseHeaders).length > 0) {\n const firstKey = Object.keys(allResponseHeaders)[0]\n pm.expect(pm.response.headers.has(firstKey)).toBe(true)\n pm.expect(pm.response.headers.get(firstKey)).toBeType('string')\n }\n})\n",
"auth": {
"authType": "inherit",
"authActive": true
},
"body": {
"contentType": null,
"body": null
},
"requestVariables": [],
"responses": {}
},
{
"v": "15",
"id": "cmfhzf0op009aqt0inw3j6dq9",
"name": "expectation-methods-test",
"method": "POST",
"endpoint": "https://echo.hoppscotch.io",
"params": [],
"headers": [],
"preRequestScript": "export {};\n",
"testScript": "export {};\n\nhopp.test('Basic equality expectations work correctly', () => {\n hopp.expect(1).toBe(1)\n hopp.expect('test').toBe('test')\n hopp.expect(true).toBe(true)\n hopp.expect(null).toBe(null)\n})\n\npm.test('`pm` basic equality expectations work correctly', () => {\n pm.expect(1).toBe(1)\n pm.expect('test').toBe('test')\n pm.expect(true).toBe(true)\n pm.expect(null).toBe(null)\n})\n\nhopp.test('Type checking expectations work correctly', () => {\n hopp.expect(42).toBeType('number')\n hopp.expect('hello').toBeType('string')\n hopp.expect(true).toBeType('boolean')\n hopp.expect({}).toBeType('object')\n hopp.expect([]).toBeType('object')\n})\n\npm.test('`pm` type checking expectations work correctly', () => {\n pm.expect(42).toBeType('number')\n pm.expect('hello').toBeType('string')\n pm.expect(true).toBeType('boolean')\n pm.expect({}).toBeType('object')\n pm.expect([]).toBeType('object')\n})\n\n\nhopp.test('String and array inclusion expectations work correctly', () => {\n hopp.expect('hello world').toInclude('world')\n hopp.expect([1, 2, 3]).toInclude(2)\n})\n\npm.test('`pm` string and array inclusion expectations work correctly', () => {\n pm.expect('hello world').toInclude('world')\n pm.expect([1, 2, 3]).toInclude(2)\n})\n\n\nhopp.test('Length expectations work correctly', () => {\n hopp.expect('hello').toHaveLength(5)\n hopp.expect([1, 2, 3]).toHaveLength(3)\n})\n\npm.test('`pm` length expectations work correctly', () => {\n pm.expect('hello').toHaveLength(5)\n pm.expect([1, 2, 3]).toHaveLength(3)\n})\n\nhopp.test('Response-based expectations work correctly', () => {\n const responseData = hopp.response.body.asJSON()\n hopp.expect(responseData).toBeType('object')\n hopp.expect(hopp.response.statusCode).toBe(200)\n})\n\npm.test('`pm` response-based expectations work correctly', () => {\n const responseData = pm.response.json()\n pm.expect(responseData).toBeType('object')\n pm.expect(pm.response.code).toBe(200)\n})",
"auth": {
"authType": "inherit",
"authActive": true
},
"body": {
"contentType": "application/json",
"body": "{\n \"message\": \"Test expectation methods\",\n \"numbers\": [1, 2, 3, 4, 5],\n \"metadata\": {\n \"timestamp\": \"{{$timestamp}}\",\n \"test\": true\n }\n}"
},
"requestVariables": [],
"responses": {}
}
],
"auth": {
"authType": "none",
"authActive": true
},
"headers": [],
"variables": []
}

View file

@ -2,6 +2,7 @@ import { TestResponse } from "@hoppscotch/js-sandbox";
import { Method } from "axios";
import { ExpectResult } from "../types/response";
import { HoppEnvs } from "../types/request";
import { HoppRESTRequest } from "@hoppscotch/data";
/**
* Defines column headers for table stream used to write table
@ -34,13 +35,13 @@ export interface RequestRunnerResponse extends TestResponse {
/**
* Describes test script details.
* @property {string} name Request name within collection.
* @property {string} testScript Stringified hoppscotch testScript, used while
* running testRunner.
* @property {HoppRESTRequest} request Supplied request.
* @property {TestResponse} response Response structure for test script runner.
* @property {HoppEnvs} envs Environment variables for test script runner.
* @property {boolean} legacySandbox Whether to use the legacy sandbox.
*/
export interface TestScriptParams {
testScript: string;
request: HoppRESTRequest;
response: TestResponse;
envs: HoppEnvs;
legacySandbox: boolean;

View file

@ -58,21 +58,37 @@ export const preRequestScriptRunner = (
return pipe(
TE.of(request),
TE.chain(({ preRequestScript }) =>
runPreRequestScript(preRequestScript, envs, experimentalScriptingSandbox)
runPreRequestScript(preRequestScript, {
envs,
experimentalScriptingSandbox,
request,
cookies: null,
})
),
TE.map(
({ selected, global }) =>
<Environment>{
TE.map(({ updatedEnvs, updatedRequest }) => {
const { selected, global } = updatedEnvs;
return {
updatedEnvs: <Environment>{
name: "Env",
variables: [...(selected ?? []), ...(global ?? [])],
}
),
TE.chainW((env) =>
TE.tryCatch(
() => getEffectiveRESTRequest(request, env, collectionVariables),
},
updatedRequest: updatedRequest ?? {},
};
}),
TE.chainW(({ updatedEnvs, updatedRequest }) => {
const finalRequest = { ...request, ...updatedRequest };
return TE.tryCatch(
() =>
getEffectiveRESTRequest(
finalRequest,
updatedEnvs,
collectionVariables
),
(reason) => error({ code: "PRE_REQUEST_SCRIPT_ERROR", data: reason })
)
),
);
}),
TE.chainEitherKW((effectiveRequest) => effectiveRequest),
TE.mapLeft((reason) =>
isHoppCLIError(reason)
@ -515,7 +531,7 @@ function getFinalBodyFromRequest(
// we split array blobs into separate entries (FormData will then join them together during exec)
arrayFlatMap((x) =>
x.isFile
? x.value.map((v) => ({
? (x.value as (Blob | null)[]).map((v: Blob | null) => ({
key: parseTemplateString(x.key, resolvedVariables),
value: v as string | Blob,
contentType: x.contentType,

View file

@ -111,25 +111,39 @@ export const requestRunner =
// NOTE: Temporary parsing check for request endpoint.
requestConfig.url = new URL(requestConfig.url ?? "").toString();
let status: number;
const baseResponse = await axios(requestConfig);
const { config } = baseResponse;
// PR-COMMENT: type error
const runnerResponse: RequestRunnerResponse = {
...baseResponse,
endpoint: getRequest.endpoint(config.url),
method: getRequest.method(config.method),
body: baseResponse.data,
duration: 0,
};
const end = hrtime(start);
const duration = getDurationInSeconds(end);
runnerResponse.duration = duration;
const responseTime = duration * 1000; // Convert seconds to milliseconds
// Transform axios headers to required format
const transformedHeaders: { key: string; value: string }[] = [];
if (baseResponse.headers) {
for (const [key, value] of Object.entries(baseResponse.headers)) {
if (value !== undefined) {
transformedHeaders.push({
key,
value: Array.isArray(value) ? value.join(", ") : String(value),
});
}
}
}
const runnerResponse: RequestRunnerResponse = {
endpoint: getRequest.endpoint(config.url),
method: getRequest.method(config.method),
body: baseResponse.data,
responseTime,
duration: duration,
status: baseResponse.status,
statusText: baseResponse.statusText,
headers: transformedHeaders,
};
return E.right(runnerResponse);
} catch (e) {
let status: number;
const runnerResponse: RequestRunnerResponse = {
endpoint: "",
method: "GET",
@ -138,6 +152,7 @@ export const requestRunner =
status: 400,
headers: [],
duration: 0,
responseTime: 0,
};
if (axios.isAxiosError(e)) {
@ -148,7 +163,22 @@ export const requestRunner =
runnerResponse.body = data;
runnerResponse.statusText = statusText;
runnerResponse.status = status;
runnerResponse.headers = headers;
// Transform axios headers to required format
const transformedHeaders: { key: string; value: string }[] = [];
if (headers) {
for (const [key, value] of Object.entries(headers)) {
if (value !== undefined) {
transformedHeaders.push({
key,
value: Array.isArray(value)
? value.join(", ")
: String(value),
});
}
}
}
runnerResponse.headers = transformedHeaders;
} else if (e.request) {
return E.left(error({ code: "REQUEST_ERROR", data: E.toError(e) }));
}
@ -237,7 +267,7 @@ export const processRequest =
const preRequestRes = await preRequestScriptRunner(
request,
processedEnvs,
legacySandbox,
legacySandbox ?? false,
collectionVariables
)();
if (E.isLeft(preRequestRes)) {
@ -265,6 +295,7 @@ export const processRequest =
headers: [],
status: 400,
statusText: "",
responseTime: 0,
body: Object(null),
duration: 0,
};
@ -289,9 +320,9 @@ export const processRequest =
// Extracting test-script-runner parameters.
const testScriptParams = getTestScriptParams(
_requestRunnerRes,
request,
effectiveRequest,
updatedEnvs,
legacySandbox
legacySandbox ?? false
);
// Executing test-runner.

View file

@ -37,14 +37,25 @@ export const testRunner = (
TE.bind("test_response", () =>
pipe(
TE.of(testScriptData),
TE.chain(({ testScript, response, envs, legacySandbox }) => {
TE.chain(({ request, response, envs, legacySandbox }) => {
const { status, statusText, headers, responseTime, body } = response;
const effectiveResponse = {
status,
statusText,
headers,
responseTime,
body,
};
const experimentalScriptingSandbox = !legacySandbox;
return runTestScript(
testScript,
return runTestScript(request.testScript, {
envs,
response,
experimentalScriptingSandbox
);
request,
response: effectiveResponse,
experimentalScriptingSandbox,
});
})
)
),
@ -147,10 +158,12 @@ export const getTestScriptParams = (
legacySandbox: boolean
) => {
const testScriptParams: TestScriptParams = {
testScript: request.testScript,
request,
response: {
body: reqRunnerRes.body,
status: reqRunnerRes.status,
statusText: reqRunnerRes.statusText,
responseTime: reqRunnerRes.responseTime,
headers: reqRunnerRes.headers,
},
envs,

View file

@ -13,10 +13,14 @@ import { VueMonacoEditor } from "@guolao/vue-monaco-editor"
import { watchDebounced } from "@vueuse/core"
import * as monaco from "monaco-editor"
import { v4 as uuidv4 } from "uuid"
import { computed, onMounted, ref } from "vue"
import { computed, onMounted, onUnmounted, ref } from "vue"
import { useColorMode } from "~/composables/theming"
// Import type definitions as raw strings
import preRequestTypes from "~/types/pre-request.d.ts?raw"
import postRequestTypes from "~/types/post-request.d.ts?raw"
const props = withDefaults(
defineProps<{
modelValue: string
@ -53,6 +57,9 @@ const typeDefCache = new Map<string, string>()
const extraLibRefs = new Map<string, monaco.IDisposable>()
// Track context-specific type definition for this editor instance
const contextTypeDefRef = ref<monaco.IDisposable | null>(null)
const MODULE_PREFIX = "export {};\n" as const
const ensureCompilerOptions = (() => {
@ -108,6 +115,28 @@ onMounted(() => {
"typescript",
scriptFileURI
)
// Load context-specific type definitions for this editor instance
const typeDefContent =
props.type === "pre-request" ? preRequestTypes : postRequestTypes
const typeDefUri = `inmemory://types/${props.type}-${uuid}.d.ts`
contextTypeDefRef.value =
monaco.languages.typescript.typescriptDefaults.addExtraLib(
typeDefContent,
typeDefUri
)
})
onUnmounted(() => {
// Clean up context-specific type definitions for this editor instance
contextTypeDefRef.value?.dispose()
// Clean up all extra libs for this editor
for (const disposable of extraLibRefs.values()) {
disposable.dispose()
}
extraLibRefs.clear()
})
const value = computed({

View file

@ -52,7 +52,7 @@
class="flex flex-col"
>
<div class="flex flex-1 items-center justify-between">
<label for="cookiesList" class="p-4">
<label class="p-4">
{{ domain }}
</label>
<div class="flex">
@ -81,14 +81,20 @@
<template v-else>
<div
v-for="(entry, entryIndex) in entries"
:key="`${entry}-${entryIndex}`"
class="flex divide-x divide-dividerLight"
:key="`${entry.name}-${entryIndex}`"
class="flex w-full divide-x divide-dividerLight items-center"
>
<input
class="flex flex-1 bg-transparent px-4 py-2"
:value="entry"
:value="`${entry.name} => ${entry.value}`"
readonly
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:icon="IconCopy"
@click="copyCookie(entry)"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.edit')"
@ -145,10 +151,12 @@
import { useI18n } from "@composables/i18n"
import { useService } from "dioc/vue"
import { CookieJarService } from "~/services/cookie-jar.service"
import { Cookie } from "@hoppscotch/data"
import IconTrash from "~icons/lucide/trash"
import IconEdit from "~icons/lucide/edit"
import IconTrash2 from "~icons/lucide/trash-2"
import IconPlus from "~icons/lucide/plus"
import IconCopy from "~icons/lucide/copy"
import { cloneDeep } from "lodash-es"
import { ref, watch, computed } from "vue"
import { EditCookieConfig } from "./EditCookie.vue"
@ -156,38 +164,29 @@ import { useColorMode } from "@composables/theming"
import { useToast } from "@composables/toast"
import { KernelInterceptorService } from "~/services/kernel-interceptor.service"
const props = defineProps<{
show: boolean
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const props = defineProps<{ show: boolean }>()
const emit = defineEmits<{ (e: "hide-modal"): void }>()
const t = useI18n()
const colorMode = useColorMode()
const toast = useToast()
const newDomainText = ref("")
const interceptorService = useService(KernelInterceptorService)
const cookieJarService = useService(CookieJarService)
const workingCookieJar = ref(cloneDeep(cookieJarService.cookieJar.value))
const currentInterceptorSupportsCookies = computed(() => {
const capabilities = interceptorService.current.value?.capabilities
const supportsCookies = capabilities["advanced"].has("cookies")
return supportsCookies ?? false
const caps = interceptorService.current.value?.capabilities
return caps?.advanced?.has("cookies") ?? false
})
function addNewDomain() {
if (newDomainText.value === "" || /^\s+$/.test(newDomainText.value)) {
if (newDomainText.value.trim() === "") {
toast.error(`${t("cookies.modal.empty_domain")}`)
return
}
workingCookieJar.value.set(newDomainText.value, [])
newDomainText.value = ""
}
@ -225,7 +224,7 @@ function cancelCookieChanges() {
hideModal()
}
function editCookie(domain: string, entryIndex: number, cookieEntry: string) {
function editCookie(domain: string, entryIndex: number, cookieEntry: Cookie) {
showEditModalFor.value = {
type: "edit",
domain,
@ -236,35 +235,42 @@ function editCookie(domain: string, entryIndex: number, cookieEntry: string) {
function deleteCookie(domain: string, entryIndex: number) {
const entry = workingCookieJar.value.get(domain)
if (entry) entry.splice(entryIndex, 1)
}
if (entry) {
entry.splice(entryIndex, 1)
function saveCookie(value: string) {
if (showEditModalFor.value?.type === "create") {
const { domain } = showEditModalFor.value
const entry = workingCookieJar.value.get(domain)!
const name = `Cookie-${entry.length}`
entry.push(makeUICookie(domain, value, name))
showEditModalFor.value = null
return
}
if (showEditModalFor.value?.type === "edit") {
const { domain, entryIndex } = showEditModalFor.value
const entry = workingCookieJar.value.get(domain)
if (entry) entry[entryIndex].value = value
showEditModalFor.value = null
}
}
function saveCookie(cookie: string) {
if (showEditModalFor.value?.type === "create") {
const { domain } = showEditModalFor.value
const entry = workingCookieJar.value.get(domain)!
entry.push(cookie)
showEditModalFor.value = null
return
function makeUICookie(domain: string, value: string, name: string): Cookie {
return {
name,
value,
domain,
path: "/",
httpOnly: false,
secure: false,
sameSite: "Lax",
}
}
if (showEditModalFor.value?.type !== "edit") return
const { domain, entryIndex } = showEditModalFor.value!
const entry = workingCookieJar.value.get(domain)
if (entry) {
entry[entryIndex] = cookie
}
showEditModalFor.value = null
function copyCookie(cookie: Cookie) {
navigator.clipboard.writeText(`${cookie.name}=${cookie.value}`)
toast.success(t("state.copied"))
}
const hideModal = () => {

View file

@ -84,7 +84,7 @@ export type EditCookieConfig =
type: "edit"
domain: string
entryIndex: number
currentCookieEntry: string
currentCookieEntry: Cookie
}
</script>
@ -104,6 +104,7 @@ import {
} from "~/composables/lens-actions"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
import { Cookie } from "@hoppscotch/data"
// TODO: Build Managed Mode!
@ -156,7 +157,7 @@ watch(
return
}
rawCookieString.value = props.entry.currentCookieEntry
rawCookieString.value = `${props.entry.currentCookieEntry.name}=${props.entry.currentCookieEntry.value}`
}
)

View file

@ -104,16 +104,12 @@ import {
HoppRESTResponseOriginalRequest,
} from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import * as monaco from "monaco-editor"
import { computed, onUnmounted, watch } from "vue"
import { computed } from "vue"
import { defineActionHandler } from "~/helpers/actions"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { AggregateEnvironment } from "~/newstore/environments"
import postRequestPWModDefn from "~/types/post-request.d.ts?raw"
import preRequestPWModDefn from "~/types/pre-request.d.ts?raw"
const VALID_OPTION_TABS = [
"params",
"bodyParams",
@ -150,36 +146,6 @@ const emit = defineEmits<{
const request = useVModel(props, "modelValue", emit)
const selectedOptionTab = useVModel(props, "optionTab", emit)
let extraLibRef: monaco.IDisposable | null = null
const libDefs = {
"pre-request": preRequestPWModDefn,
"post-request": postRequestPWModDefn,
}
const scriptEditorTabs = ["preRequestScript", "tests"]
onUnmounted(() => extraLibRef?.dispose())
watch(
() => selectedOptionTab.value,
(newTab) => {
if (!scriptEditorTabs.includes(newTab)) {
return
}
extraLibRef?.dispose()
monaco.languages.typescript.typescriptDefaults.setExtraLibs([])
extraLibRef = monaco.languages.typescript.typescriptDefaults.addExtraLib(
libDefs[newTab === "preRequestScript" ? "pre-request" : "post-request"],
`inmemory://lib/pw-${newTab}.d.ts`
)
},
{ immediate: true }
)
const showPreRequestScriptTab = computed(() => {
return (
props.properties?.includes("preRequestScript") ??

View file

@ -1,6 +1,8 @@
import {
Cookie,
Environment,
HoppCollectionVariable,
HoppRESTHeader,
HoppRESTHeaders,
HoppRESTRequest,
HoppRESTRequestVariable,
@ -22,7 +24,8 @@ import { Ref } from "vue"
import { map } from "fp-ts/Either"
import { runTestScript } from "@hoppscotch/js-sandbox/web"
import { runPreRequestScript, runTestScript } from "@hoppscotch/js-sandbox/web"
import { useSetting } from "~/composables/settings"
import { getService } from "~/modules/dioc"
import {
environmentsStore,
@ -32,6 +35,12 @@ import {
setGlobalEnvVariables,
updateEnvironment,
} from "~/newstore/environments"
import { platform } from "~/platform"
import { CookieJarService } from "~/services/cookie-jar.service"
import {
CurrentValueService,
Variable,
} from "~/services/current-environment-value.service"
import {
SecretEnvironmentService,
SecretVariable,
@ -39,27 +48,21 @@ import {
import { HoppTab } from "~/services/tab"
import { updateTeamEnvironment } from "./backend/mutations/TeamEnvironment"
import { createRESTNetworkRequestStream } from "./network"
import { getFinalEnvsFromPreRequest } from "./preRequest"
import { HoppRequestDocument } from "./rest/document"
import {
getTemporaryVariables,
setTemporaryVariables,
} from "./runner/temp_envs"
import {
CurrentValueService,
Variable,
} from "~/services/current-environment-value.service"
import { HoppRESTResponse } from "./types/HoppRESTResponse"
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
import { isJSONContentType } from "./utils/contenttypes"
import { getCombinedEnvVariables } from "./utils/environments"
import { useSetting } from "~/composables/settings"
import {
OutgoingSandboxPostRequestWorkerMessage,
OutgoingSandboxPreRequestWorkerMessage,
} from "./workers/sandbox.worker"
import { transformInheritedCollectionVariablesToAggregateEnv } from "./utils/inheritedCollectionVarTransformer"
import { isJSONContentType } from "./utils/contenttypes"
const sandboxWorker = new Worker(
new URL("./workers/sandbox.worker.ts", import.meta.url),
@ -70,6 +73,7 @@ const sandboxWorker = new Worker(
const secretEnvironmentService = getService(SecretEnvironmentService)
const currentEnvironmentValueService = getService(CurrentValueService)
const cookieJarService = getService(CookieJarService)
const EXPERIMENTAL_SCRIPTING_SANDBOX = useSetting(
"EXPERIMENTAL_SCRIPTING_SANDBOX"
@ -79,7 +83,7 @@ export const getTestableBody = (
res: HoppRESTResponse & { type: "success" | "fail" }
) => {
const contentTypeHeader = res.headers.find(
(h) => h.key.toLowerCase() === "content-type"
(h: HoppRESTHeader) => h.key.toLowerCase() === "content-type"
)
const rawBody = new TextDecoder("utf-8")
@ -277,16 +281,22 @@ const filterNonEmptyEnvironmentVariables = (
return Array.from(envsMap.values())
}
const runPreRequestScript = (
script: string,
const delegatePreRequestScriptRunner = (
request: HoppRESTRequest,
envs: {
global: Environment["variables"]
selected: Environment["variables"]
temp: Environment["variables"]
}
},
cookies: Cookie[] | null
): Promise<E.Either<string, SandboxPreRequestResult>> => {
const { preRequestScript } = request
if (!EXPERIMENTAL_SCRIPTING_SANDBOX.value) {
return getFinalEnvsFromPreRequest(script, envs, false)
return runPreRequestScript(preRequestScript, {
envs,
experimentalScriptingSandbox: false,
})
}
return new Promise((resolve) => {
@ -313,19 +323,27 @@ const runPreRequestScript = (
sandboxWorker.postMessage({
type: "pre",
script,
envs,
request: JSON.stringify(request),
cookies: cookies ? JSON.stringify(cookies) : null,
})
})
}
const runPostRequestScript = (
script: string,
envs: TestResult["envs"],
response: HoppRESTResponse
request: HoppRESTRequest,
response: HoppRESTResponse,
cookies: Cookie[] | null
): Promise<E.Either<string, SandboxTestResult>> => {
const { testScript } = request
if (!EXPERIMENTAL_SCRIPTING_SANDBOX.value) {
return runTestScript(script, envs, response, false)
return runTestScript(testScript, {
envs,
response,
experimentalScriptingSandbox: false,
})
}
return new Promise((resolve) => {
@ -352,9 +370,10 @@ const runPostRequestScript = (
sandboxWorker.postMessage({
type: "post",
script,
envs,
request: JSON.stringify(request),
response,
cookies: cookies ? JSON.stringify(cookies) : null,
})
})
}
@ -376,9 +395,12 @@ export function runRESTRequest$(
cancelFunc?.()
}
const res = runPreRequestScript(
tab.value.document.request.preRequestScript,
getCombinedEnvVariables()
const cookieJarEntries = getCookieJarEntries()
const res = delegatePreRequestScriptRunner(
tab.value.document.request,
getCombinedEnvVariables(),
cookieJarEntries
).then(async (preRequestScriptResult) => {
if (cancelCalled) return E.left("cancellation" as const)
@ -441,10 +463,14 @@ export function runRESTRequest$(
...tab.value.document.request,
auth: requestAuth ?? { authType: "none", authActive: false },
headers: requestHeaders as HoppRESTHeaders,
...(preRequestScriptResult.right.updatedRequest ?? {}),
}
// Propagate changes to request variables from the scripting context to the UI
tab.value.document.request.requestVariables = finalRequest.requestVariables
const finalEnvs = {
environments: preRequestScriptResult.right.envs,
environments: preRequestScriptResult.right.updatedEnvs,
requestVariables: finalRequestVariables as Environment["variables"],
collectionVariables,
}
@ -471,13 +497,16 @@ export function runRESTRequest$(
executedResponses$.next(res)
const postRequestScriptResult = await runPostRequestScript(
res.req.testScript,
preRequestScriptResult.right.envs,
preRequestScriptResult.right.updatedEnvs,
res.req,
{
status: res.statusCode,
body: getTestableBody(res),
headers: res.headers,
}
statusText: res.statusText,
responseTime: res.meta.responseDuration,
},
preRequestScriptResult.right.updatedCookies ?? null
)
if (E.isRight(postRequestScriptResult)) {
@ -500,6 +529,24 @@ export function runRESTRequest$(
combinedResult.right
)
updateEnvsAfterTestScript(combinedResult)
const updatedCookies = postRequestScriptResult.right.updatedCookies
if (updatedCookies) {
const newCookieMap = new Map<string, Cookie[]>()
for (const cookie of updatedCookies) {
const domain = cookie.domain
if (!newCookieMap.has(domain)) {
newCookieMap.set(domain, [])
}
newCookieMap.get(domain)!.push(cookie)
}
cookieJarService.cookieJar.value = newCookieMap
}
} else {
tab.value.document.testResults = {
description: "",
@ -575,6 +622,19 @@ function updateEnvsAfterTestScript(runResult: E.Right<SandboxTestResult>) {
}
}
const getCookieJarEntries = () => {
// Exclusive to the Desktop App
if (!platform.platformFeatureFlags.cookiesEnabled) {
return null
}
const cookieJarEntries = Array.from(
cookieJarService.cookieJar.value.values()
).flatMap((cookies) => cookies)
return cookieJarEntries
}
/**
* Run the test runner request
* @param request The request to run
@ -591,12 +651,16 @@ export function runTestRunnerRequest(
| E.Right<{
response: HoppRESTResponse
testResult: HoppTestResult
updatedRequest: HoppRESTRequest
}>
| undefined
> {
return runPreRequestScript(
request.preRequestScript,
getCombinedEnvVariables()
const cookieJarEntries = getCookieJarEntries()
return delegatePreRequestScriptRunner(
request,
getCombinedEnvVariables(),
cookieJarEntries
).then(async (preRequestScriptResult) => {
if (E.isLeft(preRequestScriptResult)) {
console.error(preRequestScriptResult.left)
@ -614,14 +678,20 @@ export function runTestRunnerRequest(
}))
)
const effectiveRequest = await getEffectiveRESTRequest(request, {
// Calculate the final updated request after pre-request script changes
const finalRequest = {
...request,
...(preRequestScriptResult.right.updatedRequest ?? {}),
}
const effectiveRequest = await getEffectiveRESTRequest(finalRequest, {
id: "env-id",
v: 2,
name: "Env",
variables: filterNonEmptyEnvironmentVariables(
combineEnvVariables({
environments: {
...preRequestScriptResult.right.envs,
...preRequestScriptResult.right.updatedEnvs,
temp: !persistEnv ? getTemporaryVariables() : [],
},
requestVariables: finalRequestVariables,
@ -640,13 +710,16 @@ export function runTestRunnerRequest(
executedResponses$.next(res)
const postRequestScriptResult = await runPostRequestScript(
res.req.testScript,
preRequestScriptResult.right.envs,
preRequestScriptResult.right.updatedEnvs,
res.req,
{
status: res.statusCode,
body: getTestableBody(res),
headers: res.headers,
}
statusText: res.statusText,
responseTime: res.meta.responseDuration,
},
preRequestScriptResult.right.updatedCookies ?? null
)
if (E.isRight(postRequestScriptResult)) {
@ -678,6 +751,7 @@ export function runTestRunnerRequest(
return E.right({
response: res,
testResult: sandboxTestResult,
updatedRequest: finalRequest,
})
}
const sandboxTestResult = {
@ -702,6 +776,7 @@ export function runTestRunnerRequest(
return E.right({
response: res,
testResult: sandboxTestResult,
updatedRequest: finalRequest,
})
}
})

View file

@ -1,16 +0,0 @@
import { Environment } from "@hoppscotch/data"
import { runPreRequestScript } from "@hoppscotch/js-sandbox/web"
import * as E from "fp-ts/Either"
import { SandboxPreRequestResult } from "@hoppscotch/js-sandbox"
export const getFinalEnvsFromPreRequest = (
script: string,
envs: {
global: Environment["variables"]
selected: Environment["variables"]
temp: Environment["variables"]
},
experimentalScriptingSandbox = true
): Promise<E.Either<string, SandboxPreRequestResult>> =>
runPreRequestScript(script, envs, experimentalScriptingSandbox)

View file

@ -1,30 +1,33 @@
import { Environment } from "@hoppscotch/data"
import { Cookie, Environment, HoppRESTRequest } from "@hoppscotch/data"
import {
RunPostRequestScriptOptions,
RunPreRequestScriptOptions,
SandboxPreRequestResult,
SandboxTestResult,
TestResult,
} from "@hoppscotch/js-sandbox"
import { runTestScript } from "@hoppscotch/js-sandbox/web"
import { runPreRequestScript, runTestScript } from "@hoppscotch/js-sandbox/web"
import * as E from "fp-ts/Either"
import { getFinalEnvsFromPreRequest } from "../preRequest"
import { HoppRESTResponse } from "../types/HoppRESTResponse"
interface PreRequestMessage {
type: "pre"
script: string
envs: {
global: Environment["variables"]
selected: Environment["variables"]
temp: Environment["variables"]
}
request: string // JSON stringified request
cookies: string | null // JSON stringified cookies subject to the feature flag
}
interface PostRequestMessage {
type: "post"
script: string
envs: TestResult["envs"]
request: string // JSON stringified request
response: HoppRESTResponse
cookies: string | null // JSON stringified cookies subject to the feature flag
}
type IncomingSandboxWorkerMessage = PreRequestMessage | PostRequestMessage
@ -60,13 +63,33 @@ export type OutgoingSandboxPostRequestWorkerMessage =
self.addEventListener(
"message",
async (event: MessageEvent<IncomingSandboxWorkerMessage>) => {
const { type, script, envs } = event.data
const { type, envs, request, cookies } = event.data
const parsedRequest = JSON.parse(request) as HoppRESTRequest
const parsedRequestResult = HoppRESTRequest.safeParse(parsedRequest)
const parsedCookies = cookies ? (JSON.parse(cookies) as Cookie[]) : null
if (type === "pre") {
if (parsedRequestResult.type === "err") {
const err: PreRequestScriptErrorMessage = {
type: "PRE_REQUEST_SCRIPT_ERROR",
data: parsedRequestResult.error,
}
self.postMessage(err)
return
}
try {
const preRequestScriptResult = await getFinalEnvsFromPreRequest(
script,
envs
const preRequestScriptResult = await runPreRequestScript(
parsedRequestResult.value.preRequestScript,
{
envs,
request: parsedRequestResult.value,
experimentalScriptingSandbox: true,
cookies: parsedCookies as unknown as Cookie[],
} satisfies RunPreRequestScriptOptions
)
const result: PreRequestScriptResultMessage = {
type: "PRE_REQUEST_SCRIPT_RESULT",
@ -83,13 +106,27 @@ self.addEventListener(
}
if (type === "post") {
if (parsedRequestResult.type === "err") {
const err: PostRequestScriptErrorMessage = {
type: "POST_REQUEST_SCRIPT_ERROR",
data: parsedRequestResult.error,
}
self.postMessage(err)
return
}
const { response } = event.data
try {
const postRequestScriptResult = await runTestScript(
script,
envs,
response
parsedRequestResult.value.testScript,
{
envs,
request: parsedRequestResult.value,
response,
experimentalScriptingSandbox: true,
cookies: parsedCookies as unknown as Cookie[],
} satisfies RunPostRequestScriptOptions
)
const result: PostRequestScriptResultMessage = {
type: "POST_REQUEST_SCRIPT_RESULT",

View file

@ -1,14 +1,7 @@
import { Service } from "dioc"
import { ref } from "vue"
import { parseString as setCookieParse } from "set-cookie-parser-es"
export type CookieDef = {
name: string
value: string
domain: string
path: string
expires: string
}
import { Cookie } from "@hoppscotch/data"
export class CookieJarService extends Service {
public static readonly ID = "COOKIE_JAR_SERVICE"
@ -18,13 +11,13 @@ export class CookieJarService extends Service {
* The keys correspond to the domain of the cookie.
* The cookie strings are stored as an array of strings corresponding to the domain
*/
public cookieJar = ref(new Map<string, string[]>())
public cookieJar = ref(new Map<string, Cookie[]>())
public parseSetCookieString(setCookieString: string) {
return setCookieParse(setCookieString)
}
public bulkApplyCookiesToDomain(cookies: string[], domain: string) {
public bulkApplyCookiesToDomain(cookies: Cookie[], domain: string) {
const existingDomainEntries = this.cookieJar.value.get(domain) ?? []
existingDomainEntries.push(...cookies)
@ -43,7 +36,7 @@ export class CookieJarService extends Service {
const cookieStrings = this.cookieJar.value.get(domain)! // We know not nullable from how we filter above
return cookieStrings.map((cookieString) =>
this.parseSetCookieString(cookieString)
this.parseSetCookieString(cookieString.value)
)
})
.filter((cookie) => {

View file

@ -299,15 +299,16 @@ export class TestRunnerService extends Service {
}
if (results && E.isRight(results)) {
const { response, testResult } = results.right
const { response, testResult, updatedRequest } = results.right
const { passed, failed } = this.getTestResultInfo(testResult)
tab.value.document.testRunnerMeta.totalTests += passed + failed
tab.value.document.testRunnerMeta.passedTests += passed
tab.value.document.testRunnerMeta.failedTests += failed
// Update request with results in the result collection
// Update request with results and propagate pre-request script changes in the result collection
this.updateRequestAtPath(tab.value.document.resultCollection!, path, {
...updatedRequest,
testResults: testResult,
response: options.persistResponses ? response : null,
isLoading: false,

View file

@ -1,22 +1,235 @@
declare namespace pw {
function test(name: string, func: () => void): void
function expect(value: any): Expectation
type HoppRESTContentType =
| null
| "multipart/form-data"
| "application/json"
| "application/ld+json"
| "application/hal+json"
| "application/vnd.api+json"
| "application/xml"
| "text/xml"
| "application/x-www-form-urlencoded"
| "binary"
| "text/html"
| "text/plain"
| "application/octet-stream"
const response: {
status: number
headers: any
body: any
}
namespace env {
function set(key: string, value: string): void
function unset(key: string): void
function get(key: string): string
function getResolve(key: string): string
function resolve(value: string): string
}
interface HoppRESTHeader {
key: string
value: string
active: boolean
description: string
}
interface HoppRESTResponseHeader {
key: string
value: string
}
interface HoppRESTParam {
key: string
value: string
active: boolean
description: string
}
// Form data key-value pair for multipart/form-data
interface FormDataKeyValue {
key: string
active: boolean
isFile: boolean
value: string | Blob[] | null
}
interface HoppRESTReqBody {
contentType: HoppRESTContentType
body: string | null | File | FormDataKeyValue[]
}
interface Cookie {
name: string
value: string
domain: string
path: string
expires?: string
maxAge?: number
secure: boolean
httpOnly: boolean
sameSite: "None" | "Lax" | "Strict"
}
type AuthLocation = "HEADERS" | "QUERY_PARAMS"
type DigestAlgorithm = "MD5" | "MD5-sess"
type DigestQOP = "auth" | "auth-int"
type JWTAlgorithm =
| "HS256"
| "HS384"
| "HS512"
| "RS256"
| "RS384"
| "RS512"
| "PS256"
| "PS384"
| "PS512"
| "ES256"
| "ES384"
| "ES512"
type HAWKAlgorithm = "sha256" | "sha1"
interface HoppRESTAuthNone {
authType: "none"
authActive: boolean
}
interface HoppRESTAuthInherit {
authType: "inherit"
authActive: boolean
}
interface HoppRESTAuthBasic {
authType: "basic"
authActive: boolean
username: string
password: string
}
interface HoppRESTAuthBearer {
authType: "bearer"
authActive: boolean
token: string
}
interface HoppRESTAuthAPIKey {
authType: "api-key"
authActive: boolean
key: string
value: string
addTo: AuthLocation
}
interface HoppRESTAuthOAuth2 {
authType: "oauth-2"
authActive: boolean
grantTypeInfo: OAuth2GrantTypeInfo
addTo: AuthLocation
}
type OAuth2GrantTypeInfo =
| {
grantType: "AUTHORIZATION_CODE"
authEndpoint: string
tokenEndpoint: string
clientID: string
clientSecret?: string
scopes?: string
isPKCE?: boolean
codeVerifierMethod?: "plain" | "S256"
token?: string
}
| {
grantType: "CLIENT_CREDENTIALS"
tokenEndpoint: string
clientID: string
clientSecret?: string
scopes?: string
clientAuthentication?: "AS_BASIC_AUTH_HEADERS" | "IN_BODY"
}
| {
grantType: "PASSWORD"
tokenEndpoint: string
clientID: string
clientSecret?: string
username: string
password: string
scopes?: string
}
| {
grantType: "IMPLICIT"
authEndpoint: string
clientID: string
scopes?: string
}
interface HoppRESTAuthAWSSignature {
authType: "aws-signature"
authActive: boolean
accessKey: string
secretKey: string
region: string
serviceName: string
serviceToken?: string
addTo: AuthLocation
}
interface HoppRESTAuthDigest {
authType: "digest"
authActive: boolean
username: string
password: string
realm?: string
nonce?: string
algorithm: DigestAlgorithm
qop: DigestQOP
nc?: string
cnonce?: string
opaque?: string
disableRetry: boolean
}
interface HoppRESTAuthHAWK {
authType: "hawk"
authActive: boolean
authId: string
authKey: string
algorithm: HAWKAlgorithm
includePayloadHash: boolean
user?: string
nonce?: string
ext?: string
app?: string
dlg?: string
timestamp?: string
}
interface HoppRESTAuthAkamaiEdgeGrid {
authType: "akamai-eg"
authActive: boolean
accessToken: string
clientToken: string
clientSecret: string
nonce?: string
timestamp?: string
host?: string
headersToSign?: string
maxBodySize?: string
}
interface HoppRESTAuthJWT {
authType: "jwt"
authActive: boolean
secret: string
privateKey?: string
algorithm: JWTAlgorithm
payload: string
addTo: AuthLocation
isSecretBase64Encoded: boolean
headerPrefix: string
paramName: string
jwtHeaders: string
}
// Discriminated union for all auth types
type HoppRESTAuth =
| HoppRESTAuthNone
| HoppRESTAuthInherit
| HoppRESTAuthBasic
| HoppRESTAuthBearer
| HoppRESTAuthAPIKey
| HoppRESTAuthOAuth2
| HoppRESTAuthAWSSignature
| HoppRESTAuthDigest
| HoppRESTAuthHAWK
| HoppRESTAuthAkamaiEdgeGrid
| HoppRESTAuthJWT
interface Expectation extends ExpectationMethods {
not: BaseExpectation
}
@ -33,3 +246,180 @@ interface ExpectationMethods {
toHaveLength(length: number): void
toInclude(value: any): void
}
declare namespace pw {
function test(name: string, func: () => void): void
function expect(value: any): Expectation
const response: Readonly<{
status: number
body: any
headers: HoppRESTResponseHeader[]
}>
namespace env {
function get(key: string): string
function getResolve(key: string): string
function resolve(key: string): string
}
}
declare namespace hopp {
const env: Readonly<{
get(key: string): string | null
getRaw(key: string): string | null
getInitialRaw(key: string): string | null
active: Readonly<{
get(key: string): string | null
getRaw(key: string): string | null
getInitialRaw(key: string): string | null
}>
global: Readonly<{
get(key: string): string | null
getRaw(key: string): string | null
getInitialRaw(key: string): string | null
}>
}>
const request: Readonly<{
readonly url: string
readonly method: string
readonly params: HoppRESTParam[]
readonly headers: HoppRESTHeader[]
readonly body: HoppRESTReqBody
readonly auth: HoppRESTAuth
variables: Readonly<{
get(key: string): string | null
}>
}>
const response: Readonly<{
readonly statusCode: number
readonly statusText: string
readonly headers: HoppRESTResponseHeader[]
readonly responseTime: number
body: Readonly<{
asText(): string
asJSON(): Record<string, any>
bytes(): Uint8Array
}>
}>
const cookies: Readonly<{
get(domain: string, name: string): Cookie | null
set(domain: string, cookie: Cookie): void
has(domain: string, name: string): boolean
getAll(domain: string): Cookie[]
delete(domain: string, name: string): void
clear(domain: string): void
}>
function test(name: string, testFunction: () => void): void
function expect(value: any): Expectation
const info: Readonly<{
readonly eventName: "post-request"
readonly requestName: string
readonly requestId: string
readonly iteration: never
readonly iterationCount: never
}>
}
declare namespace pm {
const environment: Readonly<{
get(key: string): string | null
set(key: string, value: string): void
unset(key: string): void
has(key: string): boolean
clear(): never
toObject(): never
}>
const globals: Readonly<{
get(key: string): string | null
set(key: string, value: string): void
unset(key: string): void
has(key: string): boolean
clear(): never
toObject(): never
}>
const variables: Readonly<{
get(key: string): string | null
set(key: string, value: string): void
has(key: string): boolean
replaceIn(template: string): string
}>
const request: Readonly<{
readonly url: { toString(): string }
readonly method: string
readonly headers: Readonly<{
get(name: string): string | null
has(name: string): boolean
all(): Record<string, string>
}>
readonly body: HoppRESTReqBody
readonly auth: HoppRESTAuth
}>
const response: Readonly<{
readonly code: number
readonly status: string
readonly responseTime: number
text(): string
json(): Record<string, any>
stream: Uint8Array
headers: Readonly<{
get(name: string): string | null
has(name: string): boolean
all(): HoppRESTResponseHeader[]
}>
cookies: Readonly<{
get(name: string): any
has(name: string): any
toObject(): any
}>
}>
const cookies: Readonly<{
get(name: string): any
set(name: string, value: string, options?: any): any
jar(): any
}>
function test(name: string, testFunction: () => void): void
function expect(value: any): Expectation
const info: Readonly<{
readonly eventName: "post-request"
readonly requestName: string
readonly requestId: string
readonly iteration: never
readonly iterationCount: never
}>
const sendRequest: () => never
const collectionVariables: Readonly<{
get(): never
set(): never
unset(): never
has(): never
clear(): never
toObject(): never
}>
const vault: Readonly<{
get(): never
set(): never
unset(): never
}>
const iterationData: Readonly<{
get(): never
set(): never
unset(): never
has(): never
toObject(): never
}>
const execution: Readonly<{
setNextRequest(): never
}>
}

View file

@ -1,9 +1,419 @@
declare const pw: {
env: {
get(key: string): string
set(key: string, value: string): void
unset(key: string): void
getResolve(key: string): string
resolve(value: string): string
type HoppRESTContentType =
| null
| "multipart/form-data"
| "application/json"
| "application/ld+json"
| "application/hal+json"
| "application/vnd.api+json"
| "application/xml"
| "text/xml"
| "application/x-www-form-urlencoded"
| "binary"
| "text/html"
| "text/plain"
| "application/octet-stream"
interface HoppRESTHeader {
key: string
value: string
active: boolean
description: string
}
interface HoppRESTParam {
key: string
value: string
active: boolean
description: string
}
// Form data key-value pair for multipart/form-data
interface FormDataKeyValue {
key: string
active: boolean
isFile: boolean
value: string | Blob[] | null
}
interface HoppRESTReqBody {
contentType: HoppRESTContentType
body: string | null | File | FormDataKeyValue[]
}
interface Cookie {
name: string
value: string
domain: string
path: string
expires?: string
maxAge?: number
secure: boolean
httpOnly: boolean
sameSite: "None" | "Lax" | "Strict"
}
type AuthLocation = "HEADERS" | "QUERY_PARAMS"
type DigestAlgorithm = "MD5" | "MD5-sess"
type DigestQOP = "auth" | "auth-int"
type JWTAlgorithm =
| "HS256"
| "HS384"
| "HS512"
| "RS256"
| "RS384"
| "RS512"
| "PS256"
| "PS384"
| "PS512"
| "ES256"
| "ES384"
| "ES512"
type HAWKAlgorithm = "sha256" | "sha1"
interface HoppRESTAuthNone {
authType: "none"
authActive: boolean
}
interface HoppRESTAuthInherit {
authType: "inherit"
authActive: boolean
}
interface HoppRESTAuthBasic {
authType: "basic"
authActive: boolean
username: string
password: string
}
interface HoppRESTAuthBearer {
authType: "bearer"
authActive: boolean
token: string
}
interface HoppRESTAuthAPIKey {
authType: "api-key"
authActive: boolean
key: string
value: string
addTo: AuthLocation
}
type OAuth2GrantTypeInfo =
| {
grantType: "AUTHORIZATION_CODE"
authEndpoint: string
tokenEndpoint: string
clientID: string
clientSecret?: string
scopes?: string
isPKCE?: boolean
codeVerifierMethod?: "plain" | "S256"
token?: string
}
| {
grantType: "CLIENT_CREDENTIALS"
tokenEndpoint: string
clientID: string
clientSecret?: string
scopes?: string
clientAuthentication?: "AS_BASIC_AUTH_HEADERS" | "IN_BODY"
}
| {
grantType: "PASSWORD"
tokenEndpoint: string
clientID: string
clientSecret?: string
username: string
password: string
scopes?: string
}
| {
grantType: "IMPLICIT"
authEndpoint: string
clientID: string
scopes?: string
}
interface HoppRESTAuthOAuth2 {
authType: "oauth-2"
authActive: boolean
grantTypeInfo: OAuth2GrantTypeInfo
addTo: AuthLocation
}
interface HoppRESTAuthAWSSignature {
authType: "aws-signature"
authActive: boolean
accessKey: string
secretKey: string
region: string
serviceName: string
serviceToken?: string
addTo: AuthLocation
}
interface HoppRESTAuthDigest {
authType: "digest"
authActive: boolean
username: string
password: string
realm?: string
nonce?: string
algorithm: DigestAlgorithm
qop: DigestQOP
nc?: string
cnonce?: string
opaque?: string
disableRetry: boolean
}
interface HoppRESTAuthHAWK {
authType: "hawk"
authActive: boolean
authId: string
authKey: string
algorithm: HAWKAlgorithm
includePayloadHash: boolean
user?: string
nonce?: string
ext?: string
app?: string
dlg?: string
timestamp?: string
}
interface HoppRESTAuthAkamaiEdgeGrid {
authType: "akamai-eg"
authActive: boolean
accessToken: string
clientToken: string
clientSecret: string
nonce?: string
timestamp?: string
host?: string
headersToSign?: string
maxBodySize?: string
}
interface HoppRESTAuthJWT {
authType: "jwt"
authActive: boolean
secret: string
privateKey?: string
algorithm: JWTAlgorithm
payload: string
addTo: AuthLocation
isSecretBase64Encoded: boolean
headerPrefix: string
paramName: string
jwtHeaders: string
}
// Discriminated union for all auth types
type HoppRESTAuth =
| HoppRESTAuthNone
| HoppRESTAuthInherit
| HoppRESTAuthBasic
| HoppRESTAuthBearer
| HoppRESTAuthAPIKey
| HoppRESTAuthOAuth2
| HoppRESTAuthAWSSignature
| HoppRESTAuthDigest
| HoppRESTAuthHAWK
| HoppRESTAuthAkamaiEdgeGrid
| HoppRESTAuthJWT
declare namespace pw {
namespace env {
function get(key: string): string
function getResolve(key: string): string
function set(key: string, value: string): void
function unset(key: string): void
function resolve(key: string): string
}
}
declare namespace hopp {
const env: Readonly<{
get(key: string): string | null
getRaw(key: string): string | null
set(key: string, value: string): void
delete(key: string): void
reset(key: string): void
getInitialRaw(key: string): string | null
setInitial(key: string, value: string): void
active: Readonly<{
get(key: string): string | null
getRaw(key: string): string | null
set(key: string, value: string): void
delete(key: string): void
reset(key: string): void
getInitialRaw(key: string): string | null
setInitial(key: string, value: string): void
}>
global: Readonly<{
get(key: string): string | null
getRaw(key: string): string | null
set(key: string, value: string): void
delete(key: string): void
reset(key: string): void
getInitialRaw(key: string): string | null
setInitial(key: string, value: string): void
}>
}>
const request: Readonly<{
readonly url: string
readonly method: string
readonly params: HoppRESTParam[]
readonly headers: HoppRESTHeader[]
readonly body: HoppRESTReqBody
readonly auth: HoppRESTAuth
setUrl(url: string): void
setMethod(method: string): void
setHeader(name: string, value: string): void
setHeaders(headers: HoppRESTHeader[]): void
removeHeader(key: string): void
setParam(name: string, value: string): void
setParams(params: HoppRESTParam[]): void
removeParam(key: string): void
/**
* Set or update request body with automatic merging
*
* This method supports both partial updates and complete replacement:
* - Partial updates: Merges provided fields with existing body configuration
* - Complete replacement: When all fields are provided, replaces entire body
*
* @param body - Partial or complete HoppRESTReqBody object
*
* @example
* // Partial update - just change content type
* hopp.request.setBody({ contentType: "application/xml" })
*
* // Partial update - just change body content
* hopp.request.setBody({ body: JSON.stringify({ updated: true }) })
*
* // Complete replacement
* hopp.request.setBody({
* contentType: "application/json",
* body: JSON.stringify({ name: "test", value: 123 })
* })
*/
setBody(body: Partial<HoppRESTReqBody>): void
/**
* Set or update authentication configuration with automatic merging
*
* This method supports both partial updates and complete replacement:
* - Partial updates: Merges provided fields with existing auth configuration
* - Complete replacement: When switching auth types, resets type-specific fields
*
* @param auth - Partial or complete HoppRESTAuth object
*
* @example
* // Partial update - just change the token (merges with existing)
* hopp.request.setAuth({ bearerToken: "new-token" })
*
* // Complete replacement - switch auth types
* hopp.request.setAuth({
* authType: "basic",
* authActive: true,
* username: "user",
* password: "pass"
* })
*
* // Update multiple fields while preserving others
* hopp.request.setAuth({
* accessToken: "updated-token"
* })
*/
setAuth(auth: Partial<HoppRESTAuth>): void
variables: Readonly<{
get(key: string): string | null
set(key: string, value: string): void
}>
}>
const cookies: Readonly<{
get(domain: string, name: string): Cookie | null
set(domain: string, cookie: Cookie): void
has(domain: string, name: string): boolean
getAll(domain: string): Cookie[]
delete(domain: string, name: string): void
clear(domain: string): void
}>
}
declare namespace pm {
const environment: Readonly<{
get(key: string): string | null
set(key: string, value: string): void
unset(key: string): void
has(key: string): boolean
clear(): never
toObject(): never
}>
const globals: Readonly<{
get(key: string): string | null
set(key: string, value: string): void
unset(key: string): void
has(key: string): boolean
clear(): never
toObject(): never
}>
const variables: Readonly<{
get(key: string): string | null
set(key: string, value: string): void
has(key: string): boolean
replaceIn(template: string): string
}>
const request: Readonly<{
readonly url: { toString(): string }
readonly method: string
readonly headers: Readonly<{
get(name: string): string | null
has(name: string): boolean
all(): HoppRESTHeader[]
}>
readonly body: any
readonly auth: any
}>
const info: Readonly<{
readonly eventName: "pre-request"
readonly requestName: string
readonly requestId: string
readonly iteration: never
readonly iterationCount: never
}>
const sendRequest: () => never
const collectionVariables: Readonly<{
get(): never
set(): never
unset(): never
has(): never
clear(): never
toObject(): never
}>
const vault: Readonly<{
get(): never
set(): never
unset(): never
}>
const iterationData: Readonly<{
get(): never
set(): never
unset(): never
has(): never
toObject(): never
}>
const execution: Readonly<{
setNextRequest(): never
}>
}

View file

@ -0,0 +1,16 @@
import { field } from "fp-ts"
import { z } from "zod"
export const CookieSchema = z.object({
name: z.string(), // Cookie name
value: z.string(), // Cookie value
domain: z.string(), // Domain the cookie belongs to
path: z.string(), // Path scope of the cookie (default: "/")
expires: z.string().optional(), // Expiration date in ISO format, null for session cookies
maxAge: z.number().optional(), // Maximum age in seconds, null if not set
httpOnly: z.boolean(), // Whether cookie is HTTP-only (not accessible via JavaScript)
secure: z.boolean(), // Whether cookie should only be sent over HTTPS
sameSite: z.enum(["None", "Lax", "Strict"]), // SameSite attribute for CSRF protection
})
export type Cookie = z.infer<typeof CookieSchema>

View file

@ -10,3 +10,4 @@ export * from "./utils/hawk"
export * from "./utils/akamai-eg"
export * from "./utils/jwt"
export * from "./rest-request-response"
export * from "./cookies"

View file

@ -0,0 +1,417 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import { describe, expect, test } from "vitest"
import { Cookie, TestResponse } from "~/types"
import { runPreRequestScript, runTestScript } from "~/web"
const baseCookies: Cookie[] = [
{
name: "session_id",
value: "abc123",
domain: "example.com",
path: "/",
httpOnly: true,
secure: true,
sameSite: "Lax",
},
{
name: "pref",
value: "dark",
domain: "example.com",
path: "/",
httpOnly: false,
secure: false,
sameSite: "Strict",
},
]
const defaultRequest = getDefaultRESTRequest()
describe("hopp.cookies", () => {
test("hopp.cookies.get should return a specific cookie", async () => {
await expect(
runPreRequestScript(
`console.log(hopp.cookies.get("example.com", "session_id"))`,
{
envs: { global: [], selected: [] },
request: defaultRequest,
cookies: baseCookies,
}
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [
expect.objectContaining({
args: [
{
name: "session_id",
value: "abc123",
domain: "example.com",
path: "/",
httpOnly: true,
secure: true,
sameSite: "Lax",
},
],
}),
],
})
)
})
test("hopp.cookies.get should return null for missing cookie", async () => {
await expect(
runPreRequestScript(
`console.log(hopp.cookies.get("example.com", "unknown"))`,
{
envs: { global: [], selected: [] },
request: defaultRequest,
cookies: baseCookies,
}
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [expect.objectContaining({ args: [null] })],
})
)
})
test("hopp.cookies.has should return true if cookie exists", async () => {
await expect(
runPreRequestScript(
`console.log(hopp.cookies.has("example.com", "session_id"))`,
{
envs: { global: [], selected: [] },
request: defaultRequest,
cookies: baseCookies,
}
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [
expect.objectContaining({
args: [true],
}),
],
})
)
})
test("hopp.cookies.has should return false if cookie does not exist", async () => {
await expect(
runPreRequestScript(
`console.log(hopp.cookies.has("example.com", "missing"))`,
{
envs: { global: [], selected: [] },
request: defaultRequest,
cookies: baseCookies,
}
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [
expect.objectContaining({
args: [false],
}),
],
})
)
})
test("hopp.cookies.getAll should return all cookies for a domain", () => {
return expect(
runPreRequestScript(`console.log(hopp.cookies.getAll("example.com"))`, {
envs: { global: [], selected: [] },
request: defaultRequest,
cookies: baseCookies,
})
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [
expect.objectContaining({
args: [
[
{
name: "session_id",
value: "abc123",
domain: "example.com",
path: "/",
httpOnly: true,
secure: true,
sameSite: "Lax",
},
{
name: "pref",
value: "dark",
domain: "example.com",
path: "/",
httpOnly: false,
secure: false,
sameSite: "Strict",
},
],
],
}),
],
})
)
})
test("hopp.cookies.set should add a new cookie", () => {
return expect(
runPreRequestScript(
`
const newCookie = {
name: "new_cookie",
value: "new_value",
domain: "example.com",
path: "/",
httpOnly: false,
secure: false,
sameSite: "None",
}
hopp.cookies.set("example.com", newCookie)
`,
{
envs: { global: [], selected: [] },
request: defaultRequest,
cookies: baseCookies,
}
)
).resolves.toEqualRight(
expect.objectContaining({
updatedCookies: expect.arrayContaining([
expect.objectContaining({
name: "new_cookie",
value: "new_value",
domain: "example.com",
path: "/",
httpOnly: false,
secure: false,
sameSite: "None",
}),
]),
})
)
})
test("hopp.cookies.set should replace existing cookie with same domain+name", () => {
const updated = { ...baseCookies[0], value: "updated123" }
return expect(
runPreRequestScript(
`hopp.cookies.set("example.com", ${JSON.stringify(updated)})`,
{
envs: { global: [], selected: [] },
request: defaultRequest,
cookies: baseCookies,
}
)
).resolves.toEqualRight(
expect.objectContaining({
updatedCookies: expect.arrayContaining([
expect.objectContaining({ value: "updated123" }),
]),
})
)
})
test("hopp.cookies.delete should remove a specific cookie", () => {
return expect(
runPreRequestScript(`hopp.cookies.delete("example.com", "pref")`, {
envs: { global: [], selected: [] },
request: defaultRequest,
cookies: baseCookies,
})
).resolves.toEqualRight(
expect.objectContaining({
updatedCookies: expect.not.arrayContaining([
expect.objectContaining({ name: "pref" }),
]),
})
)
})
test("hopp.cookies.clear should remove all cookies for a domain", () => {
return expect(
runPreRequestScript(`hopp.cookies.clear("example.com")`, {
envs: { global: [], selected: [] },
request: defaultRequest,
cookies: baseCookies,
})
).resolves.toEqualRight(
expect.objectContaining({
updatedCookies: expect.not.arrayContaining([
expect.objectContaining({ domain: "example.com" }),
]),
})
)
})
test("hopp.cookies methods throw for non-string domain and/or name args", async () => {
const envs = { global: [], selected: [] }
const response: TestResponse = {
status: 200,
body: "OK",
headers: [],
}
await expect(
runPreRequestScript(`hopp.cookies.get(123, "test")`, {
envs: { global: [], selected: [] },
request: defaultRequest,
cookies: baseCookies,
})
).resolves.toBeLeft()
await expect(
runPreRequestScript(`hopp.cookies.delete("example.com", 456)`, {
envs: { global: [], selected: [] },
request: defaultRequest,
cookies: baseCookies,
})
).resolves.toBeLeft()
await expect(
runTestScript(`hopp.cookies.get(123, "test")`, {
envs,
request: defaultRequest,
cookies: undefined,
response,
})
).resolves.toBeLeft()
await expect(
runTestScript(`hopp.cookies.delete("example.com", 456)`, {
envs,
request: defaultRequest,
cookies: undefined,
response,
})
).resolves.toBeLeft()
})
test("hopp.cookies.set throw if attempting to set cookie not conforming to the expected shape", async () => {
const envs = { global: [], selected: [] }
const response: TestResponse = {
status: 200,
body: "OK",
headers: [],
}
const script = `hopp.cookies.set("example.com", "test")`
await expect(
runPreRequestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
cookies: baseCookies,
})
).resolves.toBeLeft()
await expect(
runTestScript(script, {
envs,
request: defaultRequest,
cookies: undefined,
response,
})
).resolves.toBeLeft()
})
test("hopp.cookies throws an exception on unsupported platforms", async () => {
const envs = { global: [], selected: [] }
const response: TestResponse = {
status: 200,
body: "OK",
headers: [],
}
await expect(
runPreRequestScript(
`console.log(hopp.cookies.get("example.com", "session_id"))`,
{
envs: { global: [], selected: [] },
request: defaultRequest,
// `cookies` specified as `undefined` indicates unsupported platform
cookies: undefined,
}
)
).resolves.toBeLeft()
await expect(
runTestScript(
`console.log(hopp.cookies.get("example.com", "session_id"))`,
{
envs,
request: defaultRequest,
cookies: undefined,
response,
}
)
).resolves.toBeLeft()
})
test("hopp.cookies API should be available in post-request context", () => {
const envs = { global: [], selected: [] }
const response: TestResponse = {
status: 200,
body: "OK",
headers: [],
}
return expect(
runTestScript(
`
hopp.test("Cookies operations work correctly", () => {
hopp.expect(hopp.cookies.has("example.com", "session_id")).toBe(true)
hopp.expect(hopp.cookies.get("example.com", "pref").value).toBe("dark")
hopp.cookies.set("example.com", {
name: "post_cookie",
value: "post_value",
domain: "example.com",
path: "/",
httpOnly: false,
secure: false,
sameSite: "None",
})
hopp.expect(hopp.cookies.has("example.com", "post_cookie")).toBe(true)
hopp.expect(hopp.cookies.getAll("example.com").length).toBe(3)
hopp.cookies.delete("example.com", "session_id")
hopp.expect(hopp.cookies.has("example.com", "session_id")).toBe(false)
})
`,
{
envs,
request: defaultRequest,
cookies: baseCookies,
response,
}
)
).resolves.toEqualRight(
expect.objectContaining({
tests: expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "Cookies operations work correctly",
expectResults: [
{ status: "pass", message: "Expected 'true' to be 'true'" },
{ status: "pass", message: "Expected 'dark' to be 'dark'" },
{ status: "pass", message: "Expected 'true' to be 'true'" },
{ status: "pass", message: "Expected '3' to be '3'" },
{ status: "pass", message: "Expected 'false' to be 'false'" },
],
}),
],
}),
updatedCookies: expect.arrayContaining([
expect.objectContaining({ name: "post_cookie", value: "post_value" }),
]),
})
)
})
})

View file

@ -0,0 +1,354 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.envs)
)
const funcTest = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
describe("hopp.env.delete", () => {
test("removes variable from selected environment", () =>
expect(
func(`hopp.env.delete("baseUrl")`, {
global: [],
selected: [
{
key: "baseUrl",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
})()
).resolves.toEqualRight(expect.objectContaining({ selected: [] })))
test("removes variable from global environment", () =>
expect(
func(`hopp.env.delete("baseUrl")`, {
global: [
{
key: "baseUrl",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
selected: [],
})()
).resolves.toEqualRight(expect.objectContaining({ global: [] })))
test("removes only from selected if present in both", () =>
expect(
func(`hopp.env.delete("baseUrl")`, {
global: [
{
key: "baseUrl",
currentValue: "https://httpbin.org",
initialValue: "https://httpbin.org",
secret: false,
},
],
selected: [
{
key: "baseUrl",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
})()
).resolves.toEqualRight(
expect.objectContaining({
global: [
{
key: "baseUrl",
currentValue: "https://httpbin.org",
initialValue: "https://httpbin.org",
secret: false,
},
],
selected: [],
})
))
test("removes only first matching entry if duplicates exist in selected", () =>
expect(
func(`hopp.env.delete("baseUrl")`, {
global: [
{
key: "baseUrl",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
selected: [
{
key: "baseUrl",
currentValue: "https://httpbin.org",
initialValue: "https://httpbin.org",
secret: false,
},
{
key: "baseUrl",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
})()
).resolves.toEqualRight(
expect.objectContaining({
global: [
{
key: "baseUrl",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
selected: [
{
key: "baseUrl",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
})
))
test("removes only first matching entry if duplicates exist in global", () =>
expect(
func(`hopp.env.delete("baseUrl")`, {
global: [
{
key: "baseUrl",
currentValue: "https://httpbin.org",
initialValue: "https://httpbin.org",
secret: false,
},
{
key: "baseUrl",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
selected: [],
})()
).resolves.toEqualRight(
expect.objectContaining({
global: [
{
key: "baseUrl",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
selected: [],
})
))
test("no change if attempting to delete non-existent key", () =>
expect(
func(`hopp.env.delete("baseUrl")`, { global: [], selected: [] })()
).resolves.toEqualRight(
expect.objectContaining({ global: [], selected: [] })
))
test("key must be a string", () =>
expect(
func(`hopp.env.delete(5)`, { global: [], selected: [] })()
).resolves.toBeLeft())
test("reflected in script execution", () =>
expect(
funcTest(
`
hopp.env.delete("baseUrl")
hopp.expect(hopp.env.get("baseUrl")).toBe(null)
`,
{
global: [],
selected: [
{
key: "baseUrl",
currentValue: "https://echo.hoppscotch.io",
initialValue: "https://echo.hoppscotch.io",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'null' to be 'null'" },
],
}),
]))
})
describe("hopp.env.active.delete", () => {
test("removes variable from selected environment", () =>
expect(
func(`hopp.env.active.delete("foo")`, {
selected: [
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
global: [
{
key: "foo",
currentValue: "baz",
initialValue: "baz",
secret: false,
},
],
})()
).resolves.toEqualRight(
expect.objectContaining({
selected: [],
global: [
{
key: "foo",
currentValue: "baz",
initialValue: "baz",
secret: false,
},
],
})
))
test("no effect if not present in selected", () =>
expect(
func(`hopp.env.active.delete("nope")`, {
selected: [],
global: [
{
key: "nope",
currentValue: "baz",
initialValue: "baz",
secret: false,
},
],
})()
).resolves.toEqualRight(
expect.objectContaining({
selected: [],
global: [
{
key: "nope",
currentValue: "baz",
initialValue: "baz",
secret: false,
},
],
})
))
test("key must be a string", () =>
expect(
func(`hopp.env.active.delete({})`, { selected: [], global: [] })()
).resolves.toBeLeft())
})
describe("hopp.env.global.delete", () => {
test("removes variable from global environment", () =>
expect(
func(`hopp.env.global.delete("foo")`, {
selected: [
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
global: [
{
key: "foo",
currentValue: "baz",
initialValue: "baz",
secret: false,
},
],
})()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
global: [],
})
))
test("no effect if not present in global", () =>
expect(
func(`hopp.env.global.delete("missing")`, {
selected: [
{
key: "missing",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
global: [],
})()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{
key: "missing",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
global: [],
})
))
test("key must be a string", () =>
expect(
func(`hopp.env.global.delete([])`, { selected: [], global: [] })()
).resolves.toBeLeft())
})

View file

@ -0,0 +1,339 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
describe("hopp.env.get", () => {
test("returns the correct value for an existing selected environment value", () => {
return expect(
func(
`
const data = hopp.env.get("a")
hopp.expect(data).toBe("b")
`,
{
global: [],
selected: [
{
key: "a",
currentValue: "b",
initialValue: "b",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{ status: "pass", message: "Expected 'b' to be 'b'" }],
}),
])
})
test("returns the correct value for an existing global environment value", () => {
return expect(
func(
`
const data = hopp.env.get("a")
hopp.expect(data).toBe("b")
`,
{
global: [
{
key: "a",
currentValue: "b",
initialValue: "b",
secret: false,
},
],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{ status: "pass", message: "Expected 'b' to be 'b'" }],
}),
])
})
test("returns null for a key that is not present in both selected or global", () => {
return expect(
func(
`
const data = hopp.env.get("a")
hopp.expect(data).toBe(null)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'null' to be 'null'" },
],
}),
])
})
test("returns the value defined in selected environment if also present in global", () => {
return expect(
func(
`
const data = hopp.env.get("a")
hopp.expect(data).toBe("selected val")
`,
{
global: [
{
key: "a",
currentValue: "global val",
initialValue: "global val",
secret: false,
},
],
selected: [
{
key: "a",
currentValue: "selected val",
initialValue: "selected val",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'selected val' to be 'selected val'",
},
],
}),
])
})
test("resolves environment values recursively by default", () => {
return expect(
func(
`
const data = hopp.env.get("a")
hopp.expect(data).toBe("hello")
`,
{
global: [],
selected: [
{
key: "a",
currentValue: "<<hello>>",
initialValue: "<<hello>>",
secret: false,
},
{
key: "hello",
currentValue: "hello",
initialValue: "hello",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'hello' to be 'hello'" },
],
}),
])
})
test("errors if the key is not a string", () => {
return expect(
func(
`
const data = hopp.env.get(5)
`,
{
global: [],
selected: [],
}
)()
).resolves.toBeLeft()
})
})
describe("hopp.env.active.get", () => {
test("returns the value from selected environment if present", () => {
return expect(
func(
`
const data = hopp.env.active.get("a")
hopp.expect(data).toBe("selectedVal")
`,
{
selected: [
{
key: "a",
currentValue: "selectedVal",
initialValue: "selectedVal",
secret: false,
},
],
global: [
{
key: "a",
currentValue: "globalVal",
initialValue: "globalVal",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'selectedVal' to be 'selectedVal'",
},
],
}),
])
})
test("returns null if key does not exist in selected", () => {
return expect(
func(
`
const data = hopp.env.active.get("absent")
hopp.expect(data).toBe(null)
`,
{
selected: [],
global: [
{
key: "absent",
currentValue: "globalVal",
initialValue: "globalVal",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'null' to be 'null'" },
],
}),
])
})
test("errors if the key is not a string", () => {
return expect(
func(
`
hopp.env.active.get({})
`,
{ selected: [], global: [] }
)()
).resolves.toBeLeft()
})
})
describe("hopp.env.global.get", () => {
test("returns the value from global environment if present", () => {
return expect(
func(
`
const data = hopp.env.global.get("foo")
hopp.expect(data).toBe("globalVal")
`,
{
selected: [
{
key: "foo",
currentValue: "selVal",
initialValue: "selVal",
secret: false,
},
],
global: [
{
key: "foo",
currentValue: "globalVal",
initialValue: "globalVal",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'globalVal' to be 'globalVal'" },
],
}),
])
})
test("returns null if key does not exist in global", () => {
return expect(
func(
`
const data = hopp.env.global.get("not_here")
hopp.expect(data).toBe(null)
`,
{
selected: [
{
key: "not_here",
currentValue: "selVal",
initialValue: "selVal",
secret: false,
},
],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'null' to be 'null'" },
],
}),
])
})
test("errors if the key is not a string", () => {
return expect(
func(
`
hopp.env.global.get([])
`,
{ selected: [], global: [] }
)()
).resolves.toBeLeft()
})
})

View file

@ -0,0 +1,463 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
describe("hopp.env.getInitialRaw", () => {
test("returns initial value for existing selected env variable", () => {
return expect(
func(
`
const val = hopp.env.getInitialRaw("foo")
hopp.expect(val).toBe("bar")
`,
{
selected: [
{
key: "foo",
currentValue: "baz",
initialValue: "bar",
secret: false,
},
],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'bar' to be 'bar'" },
],
}),
])
})
test("returns initial value from global if not in selected", () => {
return expect(
func(
`
const val = hopp.env.getInitialRaw("foo")
hopp.expect(val).toBe("bar")
`,
{
selected: [],
global: [
{
key: "foo",
currentValue: "baz",
initialValue: "bar",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'bar' to be 'bar'" },
],
}),
])
})
test("selected shadows global when both present", () => {
return expect(
func(
`
const val = hopp.env.getInitialRaw("foo")
hopp.expect(val).toBe("selVal")
`,
{
selected: [
{
key: "foo",
currentValue: "selCur",
initialValue: "selVal",
secret: false,
},
],
global: [
{
key: "foo",
currentValue: "globCur",
initialValue: "globVal",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'selVal' to be 'selVal'" },
],
}),
])
})
test("returns null for missing key", () => {
return expect(
func(
`
const val = hopp.env.getInitialRaw("notFound")
hopp.expect(val).toBe(null)
`,
{ selected: [], global: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'null' to be 'null'" },
],
}),
])
})
test("returns empty string if initial value was empty", () => {
return expect(
func(
`
const val = hopp.env.getInitialRaw("empty")
hopp.expect(val).toBe("")
`,
{
selected: [
{ key: "empty", currentValue: "", initialValue: "", secret: false },
],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{ status: "pass", message: "Expected '' to be ''" }],
}),
])
})
test("returns literal template syntax, no resolution", () => {
return expect(
func(
`
const val = hopp.env.getInitialRaw("templ")
hopp.expect(val).toBe("<<FOO>>")
`,
{
selected: [
{
key: "templ",
currentValue: "baz",
initialValue: "<<FOO>>",
secret: false,
},
{
key: "FOO",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected '<<FOO>>' to be '<<FOO>>'" },
],
}),
])
})
test("errors for non-string key", () => {
return expect(
func(
`
hopp.env.getInitialRaw(5)
`,
{ selected: [], global: [] }
)()
).resolves.toBeLeft()
})
})
describe("hopp.env.active.getInitialRaw", () => {
test("returns initial value if present in selected env", () => {
return expect(
func(
`
const val = hopp.env.active.getInitialRaw("alpha")
hopp.expect(val).toBe("a_value")
`,
{
selected: [
{
key: "alpha",
currentValue: "changed",
initialValue: "a_value",
secret: false,
},
],
global: [
{
key: "alpha",
currentValue: "global",
initialValue: "g_value",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'a_value' to be 'a_value'" },
],
}),
])
})
test("returns null if not present in selected env", () => {
return expect(
func(
`
const val = hopp.env.active.getInitialRaw("missing")
hopp.expect(val).toBe(null)
`,
{
selected: [],
global: [
{
key: "missing",
currentValue: "glob",
initialValue: "g",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'null' to be 'null'" },
],
}),
])
})
test("returns '' if initial value was empty string", () => {
return expect(
func(
`
const val = hopp.env.active.getInitialRaw("blank")
hopp.expect(val).toBe("")
`,
{
selected: [
{ key: "blank", currentValue: "", initialValue: "", secret: false },
],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{ status: "pass", message: "Expected '' to be ''" }],
}),
])
})
test("returns literal template if present", () => {
return expect(
func(
`
const val = hopp.env.active.getInitialRaw("tmpl")
hopp.expect(val).toBe("<<BAR>>")
`,
{
selected: [
{
key: "tmpl",
currentValue: "baz",
initialValue: "<<BAR>>",
secret: false,
},
{
key: "BAR",
currentValue: "qux",
initialValue: "qux",
secret: false,
},
],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected '<<BAR>>' to be '<<BAR>>'" },
],
}),
])
})
test("errors for non-string key", () => {
return expect(
func(
`
hopp.env.active.getInitialRaw({})
`,
{ selected: [], global: [] }
)()
).resolves.toBeLeft()
})
})
describe("hopp.env.global.getInitialRaw", () => {
test("returns initial value if present in global env", () => {
return expect(
func(
`
const val = hopp.env.global.getInitialRaw("gamma")
hopp.expect(val).toBe("g_val")
`,
{
selected: [
{
key: "gamma",
currentValue: "s_val",
initialValue: "s_val",
secret: false,
},
],
global: [
{
key: "gamma",
currentValue: "current",
initialValue: "g_val",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'g_val' to be 'g_val'" },
],
}),
])
})
test("returns null if not present in global env", () => {
return expect(
func(
`
const val = hopp.env.global.getInitialRaw("none")
hopp.expect(val).toBe(null)
`,
{
selected: [
{
key: "none",
currentValue: "s",
initialValue: "s",
secret: false,
},
],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'null' to be 'null'" },
],
}),
])
})
test("returns '' if initial value was empty string", () => {
return expect(
func(
`
const val = hopp.env.global.getInitialRaw("empty")
hopp.expect(val).toBe("")
`,
{
selected: [],
global: [
{ key: "empty", currentValue: "", initialValue: "", secret: false },
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{ status: "pass", message: "Expected '' to be ''" }],
}),
])
})
test("returns literal template value if present", () => {
return expect(
func(
`
const val = hopp.env.global.getInitialRaw("tmpl")
hopp.expect(val).toBe("<<ZED>>")
`,
{
selected: [],
global: [
{
key: "tmpl",
currentValue: "zed-cur",
initialValue: "<<ZED>>",
secret: false,
},
{
key: "ZED",
currentValue: "42",
initialValue: "42",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected '<<ZED>>' to be '<<ZED>>'" },
],
}),
])
})
test("errors for non-string key", () => {
return expect(
func(
`
hopp.env.global.getInitialRaw([])
`,
{ selected: [], global: [] }
)()
).resolves.toBeLeft()
})
})

View file

@ -0,0 +1,361 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
describe("hopp.env.getRaw", () => {
test("returns the correct value for an existing selected environment value", () => {
return expect(
func(
`
const data = hopp.env.getRaw("a")
hopp.expect(data).toBe("b")
`,
{
global: [],
selected: [
{ key: "a", currentValue: "b", initialValue: "b", secret: false },
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{ status: "pass", message: "Expected 'b' to be 'b'" }],
}),
])
})
test("returns the correct value for an existing global environment value", () => {
return expect(
func(
`
const data = hopp.env.getRaw("a")
hopp.expect(data).toBe("b")
`,
{
global: [
{ key: "a", currentValue: "b", initialValue: "b", secret: false },
],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{ status: "pass", message: "Expected 'b' to be 'b'" }],
}),
])
})
test("returns null for a key that is not present in both selected and global", () => {
return expect(
func(
`
const data = hopp.env.getRaw("a")
hopp.expect(data).toBe(null)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'null' to be 'null'" },
],
}),
])
})
test("returns the value defined in selected if also present in global", () => {
return expect(
func(
`
const data = hopp.env.getRaw("a")
hopp.expect(data).toBe("selected val")
`,
{
global: [
{
key: "a",
currentValue: "global val",
initialValue: "global val",
secret: false,
},
],
selected: [
{
key: "a",
currentValue: "selected val",
initialValue: "selected val",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'selected val' to be 'selected val'",
},
],
}),
])
})
test("does not resolve values recursively", () => {
return expect(
func(
`
const data = hopp.env.getRaw("a")
hopp.expect(data).toBe("<<hello>>")
`,
{
global: [],
selected: [
{
key: "a",
currentValue: "<<hello>>",
initialValue: "<<hello>>",
secret: false,
},
{
key: "hello",
currentValue: "there",
initialValue: "there",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected '<<hello>>' to be '<<hello>>'" },
],
}),
])
})
test("returns the value as is even if there is a potential recursion", () => {
return expect(
func(
`
const data = hopp.env.getRaw("a")
hopp.expect(data).toBe("<<hello>>")
`,
{
global: [],
selected: [
{
key: "a",
currentValue: "<<hello>>",
initialValue: "<<hello>>",
secret: false,
},
{
key: "hello",
currentValue: "<<a>>",
initialValue: "<<a>>",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected '<<hello>>' to be '<<hello>>'" },
],
}),
])
})
test("errors if the key is not a string", () => {
return expect(
func(
`
const data = hopp.env.getRaw(5)
`,
{ global: [], selected: [] }
)()
).resolves.toBeLeft()
})
})
describe("hopp.env.active.getRaw", () => {
test("returns only from selected", () => {
return expect(
func(
`
hopp.expect(hopp.env.active.getRaw("a")).toBe("a-selected")
hopp.expect(hopp.env.active.getRaw("b")).toBe(null)
`,
{
selected: [
{
key: "a",
currentValue: "a-selected",
initialValue: "AS",
secret: false,
},
],
global: [
{
key: "a",
currentValue: "a-global",
initialValue: "AG",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'a-selected' to be 'a-selected'",
},
{ status: "pass", message: "Expected 'null' to be 'null'" },
],
}),
])
})
test("returns null if key absent in selected", () => {
return expect(
func(
`
hopp.expect(hopp.env.active.getRaw("missing")).toBe(null)
`,
{
selected: [],
global: [
{
key: "missing",
currentValue: "global",
initialValue: "global",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'null' to be 'null'" },
],
}),
])
})
test("errors if key is not a string", () => {
return expect(
func(
`
hopp.env.active.getRaw({})
`,
{ selected: [], global: [] }
)()
).resolves.toBeLeft()
})
})
describe("hopp.env.global.getRaw", () => {
test("returns only from global", () => {
return expect(
func(
`
hopp.expect(hopp.env.global.getRaw("b")).toBe("b-global")
hopp.expect(hopp.env.global.getRaw("a")).toBe(null)
`,
{
selected: [
{
key: "a",
currentValue: "a-selected",
initialValue: "AS",
secret: false,
},
],
global: [
{
key: "b",
currentValue: "b-global",
initialValue: "BG",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'b-global' to be 'b-global'" },
{ status: "pass", message: "Expected 'null' to be 'null'" },
],
}),
])
})
test("returns null if key absent in global", () => {
return expect(
func(
`
hopp.expect(hopp.env.global.getRaw("missing")).toBe(null)
`,
{
selected: [
{
key: "missing",
currentValue: "sel",
initialValue: "sel",
secret: false,
},
],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'null' to be 'null'" },
],
}),
])
})
test("errors if key is not a string", () => {
return expect(
func(
`
hopp.env.global.getRaw([])
`,
{ selected: [], global: [] }
)()
).resolves.toBeLeft()
})
})

View file

@ -0,0 +1,522 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.envs)
)
const funcTest = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
describe("hopp.env.reset", () => {
test("resets selected variable to its initial value", () =>
expect(
func(
`
hopp.env.set("foo", "changed")
hopp.env.reset("foo")
`,
{
selected: [
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
global: [],
}
)()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
})
))
test("resets global variable to its initial value if not in selected", () =>
expect(
func(
`
hopp.env.set("bar", "override")
hopp.env.reset("bar")
`,
{
selected: [],
global: [
{
key: "bar",
currentValue: "baz",
initialValue: "baz",
secret: false,
},
],
}
)()
).resolves.toEqualRight(
expect.objectContaining({
global: [
{
key: "bar",
currentValue: "baz",
initialValue: "baz",
secret: false,
},
],
})
))
test("if variable exists in both, only selected variable is reset", () =>
expect(
func(
`
hopp.env.set("a", "S")
hopp.env.global.set("a", "G")
hopp.env.reset("a")
`,
{
selected: [
{
key: "a",
currentValue: "sel",
initialValue: "initSel",
secret: false,
},
],
global: [
{
key: "a",
currentValue: "glob",
initialValue: "initGlob",
secret: false,
},
],
}
)()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{
key: "a",
currentValue: "initSel",
initialValue: "initSel",
secret: false,
},
],
global: [
{
key: "a",
currentValue: "G",
initialValue: "initGlob",
secret: false,
},
],
})
))
test("resets only the first occurrence if duplicates exist in selected", () =>
expect(
func(
`
hopp.env.set("dup", "X")
hopp.env.reset("dup")
`,
{
selected: [
{ key: "dup", currentValue: "A", initialValue: "A", secret: false },
{ key: "dup", currentValue: "B", initialValue: "B", secret: false },
],
global: [],
}
)()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{ key: "dup", currentValue: "A", initialValue: "A", secret: false },
{ key: "dup", currentValue: "B", initialValue: "B", secret: false },
],
})
))
test("resets only the first occurrence if duplicates exist in global", () =>
expect(
func(
`
hopp.env.global.set("gdup", "Y")
hopp.env.reset("gdup")
`,
{
selected: [],
global: [
{
key: "gdup",
currentValue: "G1",
initialValue: "I1",
secret: false,
},
{
key: "gdup",
currentValue: "G2",
initialValue: "I2",
secret: false,
},
],
}
)()
).resolves.toEqualRight(
expect.objectContaining({
global: [
{
key: "gdup",
currentValue: "I1",
initialValue: "I1",
secret: false,
},
{
key: "gdup",
currentValue: "G2",
initialValue: "I2",
secret: false,
},
],
})
))
test("no change if attempting to reset a non-existent key", () =>
expect(
func(`hopp.env.reset("none")`, { selected: [], global: [] })()
).resolves.toEqualRight(
expect.objectContaining({ selected: [], global: [] })
))
test("keys should be a string", () =>
expect(
func(`hopp.env.reset(123)`, { selected: [], global: [] })()
).resolves.toBeLeft())
test("reset reflected in subsequent get in the same script (selected)", () =>
expect(
funcTest(
`
hopp.env.set("foo", "override")
hopp.env.reset("foo")
hopp.expect(hopp.env.get("foo")).toBe("bar")
`,
{
selected: [
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'bar' to be 'bar'" },
],
}),
]))
test("reset works for secret variables", () =>
expect(
func(
`
hopp.env.set("secret", "newVal")
hopp.env.reset("secret")
`,
{
selected: [
{
key: "secret",
currentValue: "origi",
initialValue: "origi",
secret: true,
},
],
global: [],
}
)()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{
key: "secret",
currentValue: "origi",
initialValue: "origi",
secret: true,
},
],
})
))
})
describe("hopp.env.active.reset", () => {
test("resets variable only in selected environment", () =>
expect(
func(
`
hopp.env.active.set("xxx", "MUT")
hopp.env.active.reset("xxx")
`,
{
selected: [
{ key: "xxx", currentValue: "A", initialValue: "A", secret: false },
],
global: [
{ key: "xxx", currentValue: "B", initialValue: "B", secret: false },
],
}
)()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{ key: "xxx", currentValue: "A", initialValue: "A", secret: false },
],
global: [
{ key: "xxx", currentValue: "B", initialValue: "B", secret: false },
],
})
))
test("no effect if key not in selected", () =>
expect(
func(
`
hopp.env.active.reset("nonexistent")
`,
{
selected: [],
global: [
{
key: "nonexistent",
currentValue: "G",
initialValue: "G",
secret: false,
},
],
}
)()
).resolves.toEqualRight(
expect.objectContaining({
selected: [],
global: [
{
key: "nonexistent",
currentValue: "G",
initialValue: "G",
secret: false,
},
],
})
))
test("key must be a string", () =>
expect(
func(`hopp.env.active.reset(123)`, { selected: [], global: [] })()
).resolves.toBeLeft())
})
describe("hopp.env.global.reset", () => {
test("resets variable only in global environment", () =>
expect(
func(
`
hopp.env.global.set("yyy", "GGG")
hopp.env.global.reset("yyy")
`,
{
selected: [
{ key: "yyy", currentValue: "S", initialValue: "S", secret: false },
],
global: [
{
key: "yyy",
currentValue: "G",
initialValue: "GI",
secret: false,
},
],
}
)()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{ key: "yyy", currentValue: "S", initialValue: "S", secret: false },
],
global: [
{ key: "yyy", currentValue: "GI", initialValue: "GI", secret: false },
],
})
))
test("no effect if key not in global", () =>
expect(
func(
`
hopp.env.global.reset("nonexistent")
`,
{
selected: [
{
key: "nonexistent",
currentValue: "S",
initialValue: "S",
secret: false,
},
],
global: [],
}
)()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{
key: "nonexistent",
currentValue: "S",
initialValue: "S",
secret: false,
},
],
global: [],
})
))
// Additional regression tests ensuring reset uses the latest state
describe("hopp.env.reset - regression cases", () => {
test("create via setInitial then set, and reset restores to initial (selected)", () =>
expect(
func(
`
// Variable does not exist initially
hopp.env.setInitial("AUTH_TOKEN", "seeded-v1")
hopp.env.set("AUTH_TOKEN", "overridden-v2")
hopp.env.reset("AUTH_TOKEN")
`,
{
selected: [],
global: [],
}
)()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{
key: "AUTH_TOKEN",
currentValue: "seeded-v1",
initialValue: "seeded-v1",
secret: false,
},
],
global: [],
})
))
test("scope flip: remove from global, create in active, reset only affects active and not deleted global", () =>
expect(
func(
`
// Start by ensuring global is cleared
hopp.env.global.delete("API_KEY")
// Create in active with initial and then override
hopp.env.active.setInitial("API_KEY", "run-initial")
hopp.env.active.set("API_KEY", "run-override")
// Reset should restore to initial in active, global remains absent
hopp.env.active.reset("API_KEY")
`,
{
selected: [],
global: [
// Simulate global having had a value in the past; we delete within the script
{
key: "API_KEY",
currentValue: "old-glob",
initialValue: "old-glob",
secret: false,
},
],
}
)()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{
key: "API_KEY",
currentValue: "run-initial",
initialValue: "run-initial",
secret: false,
},
],
// After delete, global should not contain API_KEY
global: [],
})
))
test("delete then reset within same script should be a no-op (selected)", () =>
expect(
func(
`
hopp.env.active.delete("SESSION_ID")
// Reset after unset should not reintroduce or change anything
hopp.env.active.reset("SESSION_ID")
`,
{
selected: [
{
key: "SESSION_ID",
currentValue: "s-1",
initialValue: "s-1",
secret: false,
},
],
global: [],
}
)()
).resolves.toEqualRight(
expect.objectContaining({
selected: [],
global: [],
})
))
})
test("key must be a string", () =>
expect(
func(`hopp.env.global.reset([])`, { selected: [], global: [] })()
).resolves.toBeLeft())
})

View file

@ -0,0 +1,388 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const execEnv = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.envs)
)
const execTest = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
describe("hopp.env.set", () => {
test("updates the selected environment variable correctly", () => {
return expect(
execEnv(`hopp.env.set("a", "c")`, {
global: [],
selected: [
{ key: "a", currentValue: "b", initialValue: "b", secret: false },
],
})()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{ key: "a", currentValue: "c", initialValue: "b", secret: false },
],
})
)
})
test("updates the global environment variable correctly", () => {
return expect(
execEnv(`hopp.env.set("a", "c")`, {
global: [
{ key: "a", currentValue: "b", initialValue: "b", secret: false },
],
selected: [],
})()
).resolves.toEqualRight(
expect.objectContaining({
global: [
{ key: "a", currentValue: "c", initialValue: "b", secret: false },
],
})
)
})
test("if env exists in both, updates only selected", () => {
return expect(
execEnv(`hopp.env.set("a", "selC")`, {
global: [
{
key: "a",
currentValue: "globB",
initialValue: "globB",
secret: false,
},
],
selected: [
{
key: "a",
currentValue: "selD",
initialValue: "selD",
secret: false,
},
],
})()
).resolves.toEqualRight(
expect.objectContaining({
global: [
{
key: "a",
currentValue: "globB",
initialValue: "globB",
secret: false,
},
],
selected: [
{
key: "a",
currentValue: "selC",
initialValue: "selD",
secret: false,
},
],
})
)
})
test("creates non-existent key in selected environment", () => {
return expect(
execEnv(`hopp.env.set("a", "created")`, { global: [], selected: [] })()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{
key: "a",
currentValue: "created",
initialValue: "created",
secret: false,
},
],
global: [],
})
)
})
test("does not affect secret status for existing variable", () => {
return expect(
execEnv(`hopp.env.set("mysecret", "not-secret-anymore")`, {
selected: [
{
key: "mysecret",
currentValue: "secretvalue",
initialValue: "secretvalue",
secret: true,
},
],
global: [],
})()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{
key: "mysecret",
currentValue: "not-secret-anymore",
initialValue: "secretvalue",
secret: true,
},
],
})
)
})
test("rejects non-string key", () => {
return expect(
execEnv(`hopp.env.set(7, "foo")`, { selected: [], global: [] })()
).resolves.toBeLeft()
})
test("rejects non-string value", () => {
return expect(
execEnv(`hopp.env.set("key", 123)`, { selected: [], global: [] })()
).resolves.toBeLeft()
})
test("both key and value must be strings", () => {
return expect(
execEnv(`hopp.env.set(5, 6)`, { selected: [], global: [] })()
).resolves.toBeLeft()
})
test("set environment values are reflected immediately", () => {
return expect(
execTest(
`
hopp.env.set("foo", "bar")
hopp.expect(hopp.env.get("foo")).toBe("bar")
`,
{ selected: [], global: [] }
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'bar' to be 'bar'" },
],
}),
])
})
})
describe("hopp.env.active.set", () => {
test("sets in selected even if key exists in global", () => {
return expect(
execEnv(`hopp.env.active.set("b", "in-selected")`, {
selected: [],
global: [
{
key: "b",
currentValue: "in-global",
initialValue: "in-global",
secret: false,
},
],
})()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{
key: "b",
currentValue: "in-selected",
initialValue: "in-selected",
secret: false,
},
],
global: [
{
key: "b",
currentValue: "in-global",
initialValue: "in-global",
secret: false,
},
],
})
)
})
test("updates existing selected variable", () => {
return expect(
execEnv(`hopp.env.active.set("foo", "bar")`, {
selected: [
{
key: "foo",
currentValue: "baz",
initialValue: "baz",
secret: false,
},
],
global: [],
})()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{
key: "foo",
currentValue: "bar",
initialValue: "baz",
secret: false,
},
],
})
)
})
test("creates a new key in selected", () => {
return expect(
execEnv(`hopp.env.active.set("make", "new")`, {
selected: [],
global: [],
})()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{
key: "make",
currentValue: "new",
initialValue: "new",
secret: false,
},
],
global: [],
})
)
})
test("rejects non-string key", () => {
return expect(
execEnv(`hopp.env.active.set(null, "value")`, {
selected: [],
global: [],
})()
).resolves.toBeLeft()
})
test("rejects non-string value", () => {
return expect(
execEnv(`hopp.env.active.set("key", {})`, { selected: [], global: [] })()
).resolves.toBeLeft()
})
})
describe("hopp.env.global.set", () => {
test("creates in global, does not affect selected", () => {
return expect(
execEnv(`hopp.env.global.set("c", "in-global")`, {
selected: [
{
key: "c",
currentValue: "selected",
initialValue: "selected",
secret: false,
},
],
global: [],
})()
).resolves.toEqualRight(
expect.objectContaining({
selected: [
{
key: "c",
currentValue: "selected",
initialValue: "selected",
secret: false,
},
],
global: [
{
key: "c",
currentValue: "in-global",
initialValue: "in-global",
secret: false,
},
],
})
)
})
test("updates existing global variable", () => {
return expect(
execEnv(`hopp.env.global.set("d", "updated")`, {
selected: [],
global: [
{ key: "d", currentValue: "old", initialValue: "old", secret: false },
],
})()
).resolves.toEqualRight(
expect.objectContaining({
global: [
{
key: "d",
currentValue: "updated",
initialValue: "old",
secret: false,
},
],
})
)
})
test("creates new variable in global", () => {
return expect(
execEnv(`hopp.env.global.set("e", "new-value")`, {
selected: [],
global: [],
})()
).resolves.toEqualRight(
expect.objectContaining({
selected: [],
global: [
{
key: "e",
currentValue: "new-value",
initialValue: "new-value",
secret: false,
},
],
})
)
})
test("rejects non-string key", () => {
return expect(
execEnv(`hopp.env.global.set([], "foo")`, { selected: [], global: [] })()
).resolves.toBeLeft()
})
test("rejects non-string value", () => {
return expect(
execEnv(`hopp.env.global.set("key", true)`, {
selected: [],
global: [],
})()
).resolves.toBeLeft()
})
})

View file

@ -0,0 +1,526 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
describe("hopp.env.setInitial", () => {
test("sets initial value in selected env when key doesn't exist", () => {
return expect(
func(
`
hopp.env.setInitial("newKey", "newValue")
const val = hopp.env.getInitialRaw("newKey")
hopp.expect(val).toBe("newValue")
`,
{
selected: [],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'newValue' to be 'newValue'" },
],
}),
])
})
test("updates initial value in selected env when key exists", () => {
return expect(
func(
`
hopp.env.setInitial("existing", "updated")
const val = hopp.env.getInitialRaw("existing")
hopp.expect(val).toBe("updated")
`,
{
selected: [
{
key: "existing",
currentValue: "current",
initialValue: "original",
secret: false,
},
],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'updated' to be 'updated'" },
],
}),
])
})
test("updates selected env when key exists in both selected and global", () => {
return expect(
func(
`
hopp.env.setInitial("shared", "selectedUpdate")
const val = hopp.env.getInitialRaw("shared")
hopp.expect(val).toBe("selectedUpdate")
`,
{
selected: [
{
key: "shared",
currentValue: "selCur",
initialValue: "selInit",
secret: false,
},
],
global: [
{
key: "shared",
currentValue: "globCur",
initialValue: "globInit",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'selectedUpdate' to be 'selectedUpdate'",
},
],
}),
])
})
test("sets initial value in global env when only exists in global", () => {
return expect(
func(
`
hopp.env.setInitial("globalOnly", "globalUpdate")
const val = hopp.env.getInitialRaw("globalOnly")
hopp.expect(val).toBe("globalUpdate")
`,
{
selected: [],
global: [
{
key: "globalOnly",
currentValue: "current",
initialValue: "original",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'globalUpdate' to be 'globalUpdate'",
},
],
}),
])
})
test("allows setting empty string as initial value", () => {
return expect(
func(
`
hopp.env.setInitial("empty", "")
const val = hopp.env.getInitialRaw("empty")
hopp.expect(val).toBe("")
`,
{
selected: [],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{ status: "pass", message: "Expected '' to be ''" }],
}),
])
})
test("allows setting template syntax as initial value", () => {
return expect(
func(
`
hopp.env.setInitial("template", "<<FOO>>")
const val = hopp.env.getInitialRaw("template")
hopp.expect(val).toBe("<<FOO>>")
`,
{
selected: [],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected '<<FOO>>' to be '<<FOO>>'" },
],
}),
])
})
test("errors for non-string key", () => {
return expect(
func(
`
hopp.env.setInitial(123, "value")
`,
{ selected: [], global: [] }
)()
).resolves.toBeLeft()
})
test("errors for non-string value", () => {
return expect(
func(
`
hopp.env.setInitial("key", 456)
`,
{ selected: [], global: [] }
)()
).resolves.toBeLeft()
})
})
describe("hopp.env.active.setInitial", () => {
test("sets initial value in selected env only", () => {
return expect(
func(
`
hopp.env.active.setInitial("activeKey", "activeValue")
const activeVal = hopp.env.active.getInitialRaw("activeKey")
const globalVal = hopp.env.global.getInitialRaw("activeKey")
hopp.expect(activeVal).toBe("activeValue")
hopp.expect(globalVal).toBe(null)
`,
{
selected: [],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'activeValue' to be 'activeValue'",
},
{ status: "pass", message: "Expected 'null' to be 'null'" },
],
}),
])
})
test("updates existing selected env variable", () => {
return expect(
func(
`
hopp.env.active.setInitial("existing", "updated")
const val = hopp.env.active.getInitialRaw("existing")
hopp.expect(val).toBe("updated")
`,
{
selected: [
{
key: "existing",
currentValue: "current",
initialValue: "original",
secret: false,
},
],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'updated' to be 'updated'" },
],
}),
])
})
test("does not affect global env even if key exists there", () => {
return expect(
func(
`
hopp.env.active.setInitial("shared", "activeUpdate")
const activeVal = hopp.env.active.getInitialRaw("shared")
const globalVal = hopp.env.global.getInitialRaw("shared")
hopp.expect(activeVal).toBe("activeUpdate")
hopp.expect(globalVal).toBe("globalOriginal")
`,
{
selected: [
{
key: "shared",
currentValue: "selCur",
initialValue: "selInit",
secret: false,
},
],
global: [
{
key: "shared",
currentValue: "globCur",
initialValue: "globalOriginal",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'activeUpdate' to be 'activeUpdate'",
},
{
status: "pass",
message: "Expected 'globalOriginal' to be 'globalOriginal'",
},
],
}),
])
})
test("allows setting empty string", () => {
return expect(
func(
`
hopp.env.active.setInitial("blank", "")
const val = hopp.env.active.getInitialRaw("blank")
hopp.expect(val).toBe("")
`,
{
selected: [],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{ status: "pass", message: "Expected '' to be ''" }],
}),
])
})
test("errors for non-string key", () => {
return expect(
func(
`
hopp.env.active.setInitial(null, "value")
`,
{ selected: [], global: [] }
)()
).resolves.toBeLeft()
})
test("errors for non-string value", () => {
return expect(
func(
`
hopp.env.active.setInitial("key", {})
`,
{ selected: [], global: [] }
)()
).resolves.toBeLeft()
})
})
describe("hopp.env.global.setInitial", () => {
test("sets initial value in global env only", () => {
return expect(
func(
`
hopp.env.global.setInitial("globalKey", "globalValue")
const globalVal = hopp.env.global.getInitialRaw("globalKey")
const activeVal = hopp.env.active.getInitialRaw("globalKey")
hopp.expect(globalVal).toBe("globalValue")
hopp.expect(activeVal).toBe(null)
`,
{
selected: [],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'globalValue' to be 'globalValue'",
},
{ status: "pass", message: "Expected 'null' to be 'null'" },
],
}),
])
})
test("updates existing global env variable", () => {
return expect(
func(
`
hopp.env.global.setInitial("existing", "updated")
const val = hopp.env.global.getInitialRaw("existing")
hopp.expect(val).toBe("updated")
`,
{
selected: [],
global: [
{
key: "existing",
currentValue: "current",
initialValue: "original",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'updated' to be 'updated'" },
],
}),
])
})
test("does not affect selected env even if key exists there", () => {
return expect(
func(
`
hopp.env.global.setInitial("shared", "globalUpdate")
const globalVal = hopp.env.global.getInitialRaw("shared")
const activeVal = hopp.env.active.getInitialRaw("shared")
hopp.expect(globalVal).toBe("globalUpdate")
hopp.expect(activeVal).toBe("activeOriginal")
`,
{
selected: [
{
key: "shared",
currentValue: "selCur",
initialValue: "activeOriginal",
secret: false,
},
],
global: [
{
key: "shared",
currentValue: "globCur",
initialValue: "globInit",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'globalUpdate' to be 'globalUpdate'",
},
{
status: "pass",
message: "Expected 'activeOriginal' to be 'activeOriginal'",
},
],
}),
])
})
test("allows setting empty string", () => {
return expect(
func(
`
hopp.env.global.setInitial("empty", "")
const val = hopp.env.global.getInitialRaw("empty")
hopp.expect(val).toBe("")
`,
{
selected: [],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{ status: "pass", message: "Expected '' to be ''" }],
}),
])
})
test("allows setting template syntax", () => {
return expect(
func(
`
hopp.env.global.setInitial("template", "<<BAR>>")
const val = hopp.env.global.getInitialRaw("template")
hopp.expect(val).toBe("<<BAR>>")
`,
{
selected: [],
global: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected '<<BAR>>' to be '<<BAR>>'" },
],
}),
])
})
test("errors for non-string key", () => {
return expect(
func(
`
hopp.env.global.setInitial([], "value")
`,
{ selected: [], global: [] }
)()
).resolves.toBeLeft()
})
test("errors for non-string value", () => {
return expect(
func(
`
hopp.env.global.setInitial("key", true)
`,
{ selected: [], global: [] }
)()
).resolves.toBeLeft()
})
})

View file

@ -0,0 +1,725 @@
import {
HoppRESTAuth,
HoppRESTReqBody,
HoppRESTRequest,
} from "@hoppscotch/data"
import { describe, expect, test } from "vitest"
import { runPreRequestScript, runTestScript } from "~/web"
import { TestResponse } from "~/types"
const baseRequest: HoppRESTRequest = {
v: "15",
name: "Test Request",
endpoint: "https://example.com/api",
method: "GET",
headers: [{ key: "X-Test", value: "val1", active: true, description: "" }],
params: [{ key: "q", value: "search", active: true, description: "" }],
body: { contentType: null, body: null },
auth: { authType: "none", authActive: false },
preRequestScript: "",
testScript: "",
requestVariables: [{ key: "req-var-1", value: "value-1", active: true }],
responses: {},
}
const testResponse: TestResponse = {
status: 200,
body: "OK",
headers: [],
statusText: "OK",
responseTime: 200,
}
describe("hopp.request", () => {
test("hopp.request basic properties are accessible from pre-request script", () => {
const envs = { global: [], selected: [] }
return expect(
runPreRequestScript(
`
console.log("URL:", hopp.request.url);
console.log("Method:", hopp.request.method);
console.log("Params:", hopp.request.params);
console.log("Headers:", hopp.request.headers);
console.log("Body:", hopp.request.body);
console.log("Auth:", hopp.request.auth);`,
{
envs,
request: baseRequest,
}
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [
expect.objectContaining({
args: ["URL:", "https://example.com/api"],
}),
expect.objectContaining({
args: ["Method:", "GET"],
}),
expect.objectContaining({
args: ["Params:", baseRequest.params],
}),
expect.objectContaining({
args: ["Headers:", baseRequest.headers],
}),
expect.objectContaining({
args: ["Body:", baseRequest.body],
}),
expect.objectContaining({
args: ["Auth:", baseRequest.auth],
}),
],
})
)
})
test("hopp.request properties are read-only in both pre-request and test script contexts", () => {
const envs = { global: [], selected: [] }
const response: TestResponse = {
status: 200,
body: "test response",
headers: [],
}
// Properties that are read-only in both contexts
const basicReadOnlyTests = [
{ property: "url", value: "'https://new-url.com'" },
{ property: "method", value: "'PUT'" },
{ property: "params", value: "[]" },
{ property: "headers", value: "[]" },
{ property: "body", value: "{}" },
{ property: "auth", value: "{}" },
]
// Properties that are read-only only in test script context
const testScriptOnlyReadOnlyTests = [{ property: "variables", value: "{}" }]
// Test basic properties in pre-request script context
const preRequestTests = basicReadOnlyTests.map(({ property, value }) =>
expect(
runPreRequestScript(`hopp.request.${property} = ${value}`, {
envs,
request: baseRequest,
})
).resolves.toEqualLeft(
`Script execution failed: hopp.request.${property} is read-only`
)
)
// Test all properties in test script context
const allReadOnlyTests = [
...basicReadOnlyTests,
...testScriptOnlyReadOnlyTests,
]
const testScriptTests = allReadOnlyTests.map(({ property, value }) =>
expect(
runTestScript(`hopp.request.${property} = ${value}`, {
envs,
request: baseRequest,
response,
})
).resolves.toEqualLeft(
`Script execution failed: hopp.request.${property} is read-only`
)
)
return Promise.all([...preRequestTests, ...testScriptTests])
})
test("hopp.request.setUrl should update the URL", () => {
return expect(
runPreRequestScript(`hopp.request.setUrl("https://updated.com/api")`, {
envs: { global: [], selected: [] },
request: baseRequest,
})
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
endpoint: "https://updated.com/api",
}),
})
)
})
test("hopp.request.setMethod should update and uppercase the method", () => {
return expect(
runPreRequestScript(`hopp.request.setMethod("post")`, {
envs: { global: [], selected: [] },
request: baseRequest,
})
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
method: "POST",
}),
})
)
})
test("hopp.request.setHeader should update existing header case-insensitively", () => {
return expect(
runPreRequestScript(`hopp.request.setHeader("x-test", "updatedVal")`, {
envs: { global: [], selected: [] },
request: baseRequest,
})
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
headers: [
expect.objectContaining({
key: "X-Test",
value: "updatedVal",
}),
],
}),
})
)
})
test("hopp.request.setHeader should add new header if not present", () => {
return expect(
runPreRequestScript(
`hopp.request.setHeader("X-New-Header", "newValue")`,
{
envs: { global: [], selected: [] },
request: baseRequest,
}
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
headers: expect.arrayContaining([
expect.objectContaining({ key: "X-New-Header", value: "newValue" }),
]),
}),
})
)
})
test("hopp.request.removeHeader should remove a header", () => {
return expect(
runPreRequestScript(`hopp.request.removeHeader("X-Test")`, {
envs: { global: [], selected: [] },
request: baseRequest,
})
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
headers: [],
}),
})
)
})
test("hopp.request.setParam should update existing param case-insensitively", () => {
return expect(
runPreRequestScript(`hopp.request.setParam("Q", "updatedParam")`, {
envs: { global: [], selected: [] },
request: baseRequest,
})
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
params: [
expect.objectContaining({ key: "q", value: "updatedParam" }),
],
}),
})
)
})
test("hopp.request.setParam should add new param if absent", () => {
return expect(
runPreRequestScript(`hopp.request.setParam("newParam", "value")`, {
envs: { global: [], selected: [] },
request: baseRequest,
})
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
params: expect.arrayContaining([
expect.objectContaining({ key: "newParam", value: "value" }),
]),
}),
})
)
})
test("hopp.request.removeParam should remove a param", () => {
return expect(
runPreRequestScript(`hopp.request.removeParam("q")`, {
envs: { global: [], selected: [] },
request: baseRequest,
})
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
params: [],
}),
})
)
})
test("hopp.request.setBody should update the body with complete replacement", () => {
const newBody: HoppRESTReqBody = {
contentType: "application/json",
body: JSON.stringify({ changed: true }),
}
return expect(
runPreRequestScript(`hopp.request.setBody(${JSON.stringify(newBody)})`, {
envs: { global: [], selected: [] },
request: baseRequest,
})
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
body: newBody,
}),
})
)
})
test("hopp.request.setBody should support partial merge", () => {
// Base request with existing JSON body
const requestWithBody: HoppRESTRequest = {
...baseRequest,
body: {
contentType: "application/json",
body: JSON.stringify({ existing: "data", keep: true }),
},
}
// Script that only updates contentType, preserving body content
const partialUpdate = { contentType: "application/xml" }
return expect(
runPreRequestScript(
`hopp.request.setBody(${JSON.stringify(partialUpdate)})`,
{
envs: { global: [], selected: [] },
request: requestWithBody,
}
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
body: {
contentType: "application/xml",
body: JSON.stringify({ existing: "data", keep: true }),
},
}),
})
)
})
test("hopp.request.setAuth should update the auth with complete replacement", () => {
const newAuth: HoppRESTAuth = {
authType: "basic",
username: "abc",
password: "123",
authActive: true,
}
return expect(
runPreRequestScript(`hopp.request.setAuth(${JSON.stringify(newAuth)})`, {
envs: { global: [], selected: [] },
request: baseRequest,
})
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
auth: newAuth,
}),
})
)
})
test("hopp.request.setAuth should support partial merge", () => {
// Base request with existing basic auth
const requestWithAuth: HoppRESTRequest = {
...baseRequest,
auth: {
authType: "basic",
username: "original-user",
password: "original-pass",
authActive: true,
},
}
// Script that only updates the username, preserving other fields
const partialUpdate = { username: "updated-user" }
return expect(
runPreRequestScript(
`hopp.request.setAuth(${JSON.stringify(partialUpdate)})`,
{
envs: { global: [], selected: [] },
request: requestWithAuth,
}
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
auth: {
authType: "basic",
username: "updated-user",
password: "original-pass",
authActive: true,
},
}),
})
)
})
test("hopp.request.setAuth should handle auth type switching", () => {
// Base request with bearer auth
const requestWithBearerAuth: HoppRESTRequest = {
...baseRequest,
auth: {
authType: "bearer",
token: "old-bearer-token",
authActive: true,
},
}
// Switch to basic auth (complete replacement)
const switchToBasic: HoppRESTAuth = {
authType: "basic",
username: "new-user",
password: "new-pass",
authActive: true,
}
return expect(
runPreRequestScript(
`hopp.request.setAuth(${JSON.stringify(switchToBasic)})`,
{
envs: { global: [], selected: [] },
request: requestWithBearerAuth,
}
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
auth: switchToBasic,
}),
})
)
})
test("hopp.request.setHeaders throws error on invalid input", () => {
return expect(
runPreRequestScript(`hopp.request.setHeaders(null)`, {
envs: { global: [], selected: [] },
request: baseRequest,
})
).resolves.toBeLeft()
})
test("hopp.request.setParams throws error on invalid input", () => {
return expect(
runPreRequestScript(`hopp.request.setParams(null)`, {
envs: { global: [], selected: [] },
request: baseRequest,
})
).resolves.toBeLeft()
})
test("hopp.request.setBody throws error on invalid input", () => {
return expect(
runPreRequestScript(`hopp.request.setBody("invalid_body")`, {
envs: { global: [], selected: [] },
request: baseRequest,
})
).resolves.toBeLeft()
})
test("hopp.request.setAuth throws error on invalid input", () => {
return expect(
runPreRequestScript(`hopp.request.setAuth({})`, {
envs: { global: [], selected: [] },
request: baseRequest,
})
).resolves.toBeLeft()
})
test("hopp.request.variables.get should return the request variable", () => {
return expect(
runPreRequestScript(
`console.log(hopp.request.variables.get("req-var-1"))`,
{
envs: { global: [], selected: [] },
request: baseRequest,
}
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [expect.objectContaining({ args: ["value-1"] })],
})
)
})
test("hopp.request.variables.set should update the request variable", () => {
return expect(
runPreRequestScript(
`hopp.request.variables.set("req-var-1", "new-value-1")`,
{
envs: { global: [], selected: [] },
request: baseRequest,
}
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
requestVariables: [
{
key: "req-var-1",
value: "new-value-1",
active: true,
},
],
}),
})
)
})
test("hopp.request.variables.set should add a new request variable if the supplied key does not exist", () => {
return expect(
runPreRequestScript(
`hopp.request.variables.set("req-var-2", "value-2")`,
{
envs: { global: [], selected: [] },
request: baseRequest,
}
)
).resolves.toEqualRight(
expect.objectContaining({
updatedRequest: expect.objectContaining({
requestVariables: [
{
key: "req-var-1",
value: "value-1",
active: true,
},
{
key: "req-var-2",
value: "value-2",
active: true,
},
],
}),
})
)
})
test("hopp.request.variables.set should not work in post-request script context", () => {
const envs = { global: [], selected: [] }
return expect(
runTestScript(
`hopp.request.variables.set("req-var-1", "new-value-from-test")`,
{
envs,
request: baseRequest,
response: testResponse,
}
)
).resolves.toEqualLeft(`Script execution failed: not a function`)
})
test("hopp.request read-only properties are accessible from post-request script", () => {
const envs = { global: [], selected: [] }
const testRequest: HoppRESTRequest = {
...baseRequest,
method: "POST",
endpoint: "https://api.example.com/users",
params: [{ key: "page", value: "1", active: true, description: "" }],
headers: [
{
key: "Authorization",
value: "Bearer token123",
active: true,
description: "",
},
{
key: "Content-Type",
value: "application/json",
active: true,
description: "",
},
],
body: {
contentType: "application/json",
body: JSON.stringify({ name: "John", age: 30 }),
},
auth: {
authType: "bearer",
authActive: true,
token: "test-token-123",
},
}
return expect(
runTestScript(
`
hopp.expect(hopp.request.url).toBe("https://api.example.com/users")
hopp.expect(hopp.request.method).toBe("POST")
hopp.expect(hopp.request.params.length).toBe(1)
hopp.expect(hopp.request.params[0].key).toBe("page")
hopp.expect(hopp.request.params[0].value).toBe("1")
hopp.expect(hopp.request.headers.length).toBe(2)
hopp.expect(hopp.request.headers[0].key).toBe("Authorization")
hopp.expect(hopp.request.headers[0].value).toBe("Bearer token123")
hopp.expect(hopp.request.headers[1].key).toBe("Content-Type")
hopp.expect(hopp.request.headers[1].value).toBe("application/json")
hopp.expect(hopp.request.body.contentType).toBe("application/json")
hopp.expect(hopp.request.body.body).toBe('{"name":"John","age":30}')
hopp.expect(hopp.request.auth.authType).toBe("bearer")
hopp.expect(hopp.request.auth.token).toBe("test-token-123")
`,
{
envs,
request: testRequest,
response: testResponse,
}
)
).resolves.toEqualRight(
expect.objectContaining({
tests: expect.objectContaining({
expectResults: [
{
status: "pass",
message:
"Expected 'https://api.example.com/users' to be 'https://api.example.com/users'",
},
{ status: "pass", message: "Expected 'POST' to be 'POST'" },
{ status: "pass", message: "Expected '1' to be '1'" },
{ status: "pass", message: "Expected 'page' to be 'page'" },
{ status: "pass", message: "Expected '1' to be '1'" },
{ status: "pass", message: "Expected '2' to be '2'" },
{
status: "pass",
message: "Expected 'Authorization' to be 'Authorization'",
},
{
status: "pass",
message: "Expected 'Bearer token123' to be 'Bearer token123'",
},
{
status: "pass",
message: "Expected 'Content-Type' to be 'Content-Type'",
},
{
status: "pass",
message: "Expected 'application/json' to be 'application/json'",
},
{
status: "pass",
message: "Expected 'application/json' to be 'application/json'",
},
{
status: "pass",
message:
'Expected \'{"name":"John","age":30}\' to be \'{"name":"John","age":30}\'',
},
{ status: "pass", message: "Expected 'bearer' to be 'bearer'" },
{
status: "pass",
message: "Expected 'test-token-123' to be 'test-token-123'",
},
],
}),
})
)
})
test("hopp.request setter methods should not be available in post-request", async () => {
const script = `
hopp.request.setUrl("http://modified.com")
`
await expect(
runTestScript(script, {
envs: { global: [], selected: [] },
request: baseRequest,
response: testResponse,
})
).resolves.toEqualLeft(expect.stringContaining("not a function"))
})
test("hopp.request.setHeader should not be available in post-request", async () => {
const script = `
hopp.request.setHeader("X-Test", "value")
`
await expect(
runTestScript(script, {
envs: { global: [], selected: [] },
request: baseRequest,
response: testResponse,
})
).resolves.toEqualLeft(expect.stringContaining("not a function"))
})
// Request property immutability tests in post-request context
describe("property immutability in post-request context", () => {
test("hopp.request.url should be read-only", async () => {
const script = `
hopp.request.url = "http://modified.com"
`
await expect(
runTestScript(script, {
envs: { global: [], selected: [] },
request: baseRequest,
response: testResponse,
})
).resolves.toEqualLeft(expect.stringContaining("read-only"))
})
test("hopp.request.method should be read-only", async () => {
const script = `
hopp.request.method = "POST"
`
await expect(
runTestScript(script, {
envs: { global: [], selected: [] },
request: baseRequest,
response: testResponse,
})
).resolves.toEqualLeft(expect.stringContaining("read-only"))
})
test("hopp.request.headers should be read-only", async () => {
const script = `
hopp.request.headers = {}
`
await expect(
runTestScript(script, {
envs: { global: [], selected: [] },
request: baseRequest,
response: testResponse,
})
).resolves.toEqualLeft(expect.stringContaining("read-only"))
})
test("hopp.request.body should be read-only", async () => {
const script = `
hopp.request.body = "modified"
`
await expect(
runTestScript(script, {
envs: { global: [], selected: [] },
request: baseRequest,
response: testResponse,
})
).resolves.toEqualLeft(expect.stringContaining("read-only"))
})
})
})

View file

@ -0,0 +1,465 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/web"
import { TestResponse } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const sampleHeaders = [
{ key: "content-type", value: "application/json" },
{ key: "x-custom", value: "hello" },
]
const sampleJSONResponse: TestResponse = {
status: 201,
body: { ok: true, msg: "success" },
headers: sampleHeaders,
statusText: "Created",
responseTime: 123,
}
const sampleTextResponse: TestResponse = {
status: 200,
body: "Plaintext response",
headers: [{ key: "content-type", value: "text/plain" }],
statusText: "OK",
responseTime: 240,
}
describe("hopp.response", () => {
test("hopp.response.statusCode should return the status", async () => {
await expect(
runTestScript(`hopp.expect(hopp.response.statusCode).toBe(201)`, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: sampleJSONResponse,
})
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [],
envs: expect.objectContaining({
global: [],
selected: [],
}),
tests: expect.objectContaining({
children: [],
descriptor: "root",
expectResults: [
{ status: "pass", message: "Expected '201' to be '201'" },
],
}),
updatedCookies: null,
})
)
})
test("hopp.response.statusText should return the status text", async () => {
await expect(
runTestScript(`hopp.expect(hopp.response.statusText).toBe("Created")`, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: sampleJSONResponse,
})
).resolves.toEqualRight(
expect.objectContaining({
tests: expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'Created' to be 'Created'" },
],
}),
})
)
})
test("hopp.response.responseTime should return the response time", async () => {
await expect(
runTestScript(`hopp.expect(hopp.response.responseTime).toBe(123)`, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: sampleJSONResponse,
})
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [],
envs: expect.objectContaining({
global: [],
selected: [],
}),
tests: expect.objectContaining({
children: [],
descriptor: "root",
expectResults: [
{ status: "pass", message: "Expected '123' to be '123'" },
],
}),
updatedCookies: null,
})
)
})
test("hopp.response.headers should return all headers", async () => {
await expect(
runTestScript(
`
hopp.expect(hopp.response.headers.length).toBe(2)
hopp.expect(hopp.response.headers[0].key).toBe("content-type")
hopp.expect(hopp.response.headers[0].value).toBe("application/json")
hopp.expect(hopp.response.headers[1].key).toBe("x-custom")
hopp.expect(hopp.response.headers[1].value).toBe("hello")
`,
{
envs: { global: [], selected: [] },
request: defaultRequest,
response: sampleJSONResponse,
}
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [],
envs: expect.objectContaining({
global: [],
selected: [],
}),
tests: expect.objectContaining({
children: [],
descriptor: "root",
expectResults: [
{ status: "pass", message: "Expected '2' to be '2'" },
{
status: "pass",
message: "Expected 'content-type' to be 'content-type'",
},
{
status: "pass",
message: "Expected 'application/json' to be 'application/json'",
},
{ status: "pass", message: "Expected 'x-custom' to be 'x-custom'" },
{ status: "pass", message: "Expected 'hello' to be 'hello'" },
],
}),
updatedCookies: null,
})
)
})
test("hopp.response.body.asText returns response text", async () => {
await expect(
runTestScript(
`hopp.expect(hopp.response.body.asText()).toBe("Plaintext response")`,
{
envs: { global: [], selected: [] },
request: defaultRequest,
response: sampleTextResponse,
}
)
).resolves.toEqualRight(
expect.objectContaining({
tests: expect.objectContaining({
expectResults: [
{
status: "pass",
message:
"Expected 'Plaintext response' to be 'Plaintext response'",
},
],
}),
})
)
})
test("hopp.response.body.asJSON returns parsed object for JSON response", async () => {
await expect(
runTestScript(
`
const obj = hopp.response.body.asJSON()
hopp.expect(obj.ok).toBe(true)
hopp.expect(obj.msg).toBe("success")
`,
{
envs: { global: [], selected: [] },
request: defaultRequest,
response: sampleJSONResponse,
}
)
).resolves.toEqualRight(
expect.objectContaining({
tests: expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'true' to be 'true'",
},
{
status: "pass",
message: "Expected 'success' to be 'success'",
},
],
}),
})
)
})
test("hopp.response.body.asJSON throws for invalid JSON", async () => {
await expect(
runTestScript(`hopp.response.body.asJSON()`, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: sampleTextResponse, // text, not JSON
})
).resolves.toBeLeft()
})
test("hopp.response.body.asJSON throws error for invalid JSON body", async () => {
await expect(
runTestScript(`hopp.response.body.asJSON()`, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: {
...sampleJSONResponse,
body: "not a json!",
headers: [{ key: "content-type", value: "application/json" }],
},
})
).resolves.toBeLeft()
})
test("hopp.response.body.bytes returns UTF-8 encoded data for JSON body", async () => {
await expect(
runTestScript(
`
const obj = hopp.response.body.bytes()
hopp.expect(obj["0"]).toBe(123)
hopp.expect(obj["1"]).toBe(34)
hopp.expect(obj["2"]).toBe(111)
hopp.expect(obj["26"]).toBe(125)
`,
{
envs: { global: [], selected: [] },
request: defaultRequest,
response: sampleJSONResponse,
}
)
).resolves.toEqualRight(
expect.objectContaining({
tests: expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected '123' to be '123'",
},
{
status: "pass",
message: "Expected '34' to be '34'",
},
{
status: "pass",
message: "Expected '111' to be '111'",
},
{
status: "pass",
message: "Expected '125' to be '125'",
},
],
}),
})
)
})
test("hopp.response.body.bytes throws error for unsupported body type", async () => {
await expect(
runTestScript(`hopp.response.body.bytes()`, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: { ...sampleTextResponse, body: 1234 as any },
})
).resolves.toBeLeft()
})
test("hopp.response.bytes returns UTF-8 encoded data for plain text", async () => {
await expect(
runTestScript(
`
const bytes = hopp.response.body.bytes()
hopp.expect(bytes["0"]).toBe(80)
hopp.expect(bytes["1"]).toBe(108)
hopp.expect(bytes["17"]).toBe(101)
`,
{
envs: { global: [], selected: [] },
request: defaultRequest,
response: sampleTextResponse,
}
)
).resolves.toEqualRight(
expect.objectContaining({
tests: expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected '80' to be '80'",
},
{
status: "pass",
message: "Expected '108' to be '108'",
},
{
status: "pass",
message: "Expected '101' to be '101'",
},
],
}),
})
)
})
test("hopp.response.bytes returns empty array for null/undefined body", async () => {
await expect(
runTestScript(
`
const bytes = hopp.response.body.bytes()
const bytesArray = Array.from(bytes)
hopp.expect(bytesArray.length).toBe(0)
`,
{
envs: { global: [], selected: [] },
request: defaultRequest,
response: { ...sampleTextResponse, body: null },
}
)
).resolves.toEqualRight(
expect.objectContaining({
tests: expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected '0' to be '0'",
},
],
}),
})
)
})
// Response property immutability tests
describe("property immutability", () => {
const baseResponse: TestResponse = {
status: 200,
body: "OK",
headers: [],
statusText: "OK",
responseTime: 200,
}
test("hopp.response.statusCode should be read-only", async () => {
const script = `
hopp.response.statusCode = 500
`
await expect(
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: baseResponse,
})
).resolves.toEqualLeft(expect.stringContaining("read-only"))
})
test("hopp.response.statusText should be read-only", async () => {
const script = `
hopp.response.statusText = "Modified"
`
await expect(
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: baseResponse,
})
).resolves.toEqualLeft(expect.stringContaining("read-only"))
})
test("hopp.response.headers should be read-only", async () => {
const script = `
hopp.response.headers = {}
`
await expect(
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: baseResponse,
})
).resolves.toEqualLeft(expect.stringContaining("read-only"))
})
test("hopp.response.responseTime should be read-only", async () => {
const script = `
hopp.response.responseTime = 1000
`
await expect(
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: baseResponse,
})
).resolves.toEqualLeft(expect.stringContaining("read-only"))
})
test("hopp.response.body should be read-only", async () => {
const script = `
hopp.response.body = null
`
await expect(
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: baseResponse,
})
).resolves.toEqualLeft(expect.stringContaining("read-only"))
})
test("hopp.response.body.asJSON should be read-only", async () => {
const script = `
hopp.response.body.asJSON = null
`
await expect(
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: baseResponse,
})
).resolves.toEqualLeft(expect.stringContaining("read-only"))
})
test("hopp.response.body.asText should be read-only", async () => {
const script = `
hopp.response.body.asText = null
`
await expect(
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: baseResponse,
})
).resolves.toEqualLeft(expect.stringContaining("read-only"))
})
test("hopp.response.body.bytes should be read-only", async () => {
const script = `
hopp.response.body.bytes = null
`
await expect(
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: baseResponse,
})
).resolves.toEqualLeft(expect.stringContaining("read-only"))
})
})
})

View file

@ -0,0 +1,255 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
describe("pm.environment additional coverage", () => {
test("pm.environment.set creates and retrieves environment variable", () => {
return expect(
func(
`
pm.environment.set("test_set", "set_value")
const retrieved = pm.environment.get("test_set")
pw.expect(retrieved).toBe("set_value")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'set_value' to be 'set_value'",
},
],
}),
])
})
test("pm.environment.has correctly identifies existing and non-existing variables", () => {
return expect(
func(
`
const hasExisting = pm.environment.has("existing_var")
const hasNonExisting = pm.environment.has("non_existing_var")
pw.expect(hasExisting.toString()).toBe("true")
pw.expect(hasNonExisting.toString()).toBe("false")
`,
{
global: [],
selected: [
{
key: "existing_var",
currentValue: "existing_value",
initialValue: "existing_value",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'true' to be 'true'",
},
{
status: "pass",
message: "Expected 'false' to be 'false'",
},
],
}),
])
})
})
describe("pm.globals additional coverage", () => {
test("pm.globals.set creates and retrieves global variable", () => {
return expect(
func(
`
pm.globals.set("test_global", "global_value")
const retrieved = pm.globals.get("test_global")
pw.expect(retrieved).toBe("global_value")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'global_value' to be 'global_value'",
},
],
}),
])
})
})
describe("pm.variables additional coverage", () => {
test("pm.variables.set creates and retrieves variable in active environment", () => {
return expect(
func(
`
pm.variables.set("test_var", "test_value")
const retrieved = pm.variables.get("test_var")
pw.expect(retrieved).toBe("test_value")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'test_value' to be 'test_value'",
},
],
}),
])
})
test("pm.variables.has correctly identifies existing and non-existing variables", () => {
return expect(
func(
`
const hasExisting = pm.variables.has("existing_var")
const hasNonExisting = pm.variables.has("non_existing_var")
pw.expect(hasExisting.toString()).toBe("true")
pw.expect(hasNonExisting.toString()).toBe("false")
`,
{
global: [],
selected: [
{
key: "existing_var",
currentValue: "existing_value",
initialValue: "existing_value",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'true' to be 'true'",
},
{
status: "pass",
message: "Expected 'false' to be 'false'",
},
],
}),
])
})
test("pm.variables.get returns the correct value from any scope", () => {
return expect(
func(
`
const data = pm.variables.get("scopedVar")
pw.expect(data).toBe("scopedValue")
`,
{
global: [
{
key: "scopedVar",
currentValue: "scopedValue",
initialValue: "scopedValue",
secret: false,
},
],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'scopedValue' to be 'scopedValue'",
},
],
}),
])
})
test("pm.variables.replaceIn handles multiple variables", () => {
return expect(
func(
`
const template = "User {{name}} has {{count}} items in {{location}}"
const result = pm.variables.replaceIn(template)
pw.expect(result).toBe("User Alice has 10 items in Cart")
`,
{
global: [
{
key: "location",
currentValue: "Cart",
initialValue: "Cart",
secret: false,
},
],
selected: [
{
key: "name",
currentValue: "Alice",
initialValue: "Alice",
secret: false,
},
{
key: "count",
currentValue: "10",
initialValue: "10",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message:
"Expected 'User Alice has 10 items in Cart' to be 'User Alice has 10 items in Cart'",
},
],
}),
])
})
})

View file

@ -0,0 +1,114 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
import { runPreRequestScript } from "~/web"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "test response",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: { ...defaultRequest, name: "default-request", id: "test-id" },
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
describe("pm.info context", () => {
test("pm.info.eventName returns 'pre-request' in pre-request context", () => {
const defaultRequest = getDefaultRESTRequest()
return expect(
runPreRequestScript(
`
console.log("Event name: ", pm.info.eventName)
`,
{
envs: {
global: [],
selected: [],
},
request: defaultRequest,
}
)
).resolves.toEqualRight(
expect.objectContaining({
consoleEntries: [
expect.objectContaining({ args: ["Event name: ", "pre-request"] }),
],
})
)
})
test("pm.info.eventName returns 'post-request' in post-request context", () => {
return expect(
func(
`
pm.test("Event name is correct", () => {
pm.expect(pm.info.eventName).toBe("post-request")
})
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "Event name is correct",
expectResults: [
{
status: "pass",
message: "Expected 'post-request' to be 'post-request'",
},
],
}),
],
}),
])
})
test("pm.info provides requestName and requestId", () => {
return expect(
func(
`
pm.test("Request info is available", () => {
pm.expect(pm.info.requestName).toBe("default-request")
pm.expect(pm.info.requestId).toBe("test-id")
})
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "Request info is available",
expectResults: [
{
status: "pass",
message: "Expected 'default-request' to be 'default-request'",
},
{ status: "pass", message: "Expected 'test-id' to be 'test-id'" },
],
}),
],
}),
])
})
})

View file

@ -0,0 +1,126 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
describe("pm.request coverage", () => {
test("pm.request object provides access to request data", () => {
return expect(
func(
`
pw.expect(pm.request.url.toString()).toBe("https://echo.hoppscotch.io")
pw.expect(pm.request.method).toBe("GET")
pw.expect(pm.request.headers.get("Content-Type")).toBe(null)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message:
"Expected 'https://echo.hoppscotch.io' to be 'https://echo.hoppscotch.io'",
},
{
status: "pass",
message: "Expected 'GET' to be 'GET'",
},
{
status: "pass",
message: "Expected 'null' to be 'null'",
},
],
}),
])
})
test("pm.request.url provides correct URL value", () => {
return expect(
func(
`
pw.expect(pm.request.url.toString()).toBe("https://echo.hoppscotch.io")
pw.expect(pm.request.url.toString().length).toBe(26)
pw.expect(pm.request.url.toString().includes("hoppscotch")).toBe(true)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message:
"Expected 'https://echo.hoppscotch.io' to be 'https://echo.hoppscotch.io'",
},
{
status: "pass",
message: "Expected '26' to be '26'",
},
{
status: "pass",
message: "Expected 'true' to be 'true'",
},
],
}),
])
})
test("pm.request.headers functionality", () => {
return expect(
func(
`
pw.expect(pm.request.headers.get("Content-Type")).toBe(null)
pw.expect(pm.request.headers.has("Content-Type")).toBe(false)
pw.expect(pm.request.headers.has("User-Agent")).toBe(false)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'null' to be 'null'",
},
{
status: "pass",
message: "Expected 'false' to be 'false'",
},
{
status: "pass",
message: "Expected 'false' to be 'false'",
},
],
}),
])
})
})

View file

@ -0,0 +1,199 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
statusText: "OK",
body: JSON.stringify({ message: "Hello, World!" }),
headers: [
{ key: "Content-Type", value: "application/json" },
{ key: "Authorization", value: "Bearer token123" },
],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
describe("pm.response", () => {
test("pm.response.code provides access to status code", () => {
return expect(
func(
`
const code = pm.response.code
pw.expect(code.toString()).toBe("200")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected '200' to be '200'",
},
],
}),
])
})
test("pm.response.status provides access to status text", () => {
return expect(
func(
`
const status = pm.response.status
pw.expect(status).toBe("OK")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'OK' to be 'OK'",
},
],
}),
])
})
test("pm.response.text() provides response body as text", () => {
return expect(
func(
`
const text = pm.response.text()
pw.expect(text).toBe('{"message":"Hello, World!"}')
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message:
'Expected \'{"message":"Hello, World!"}\' to be \'{"message":"Hello, World!"}\'',
},
],
}),
])
})
test("pm.response.json() provides parsed JSON response", () => {
return expect(
func(
`
const json = pm.response.json()
pw.expect(json.message).toBe("Hello, World!")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'Hello, World!' to be 'Hello, World!'",
},
],
}),
])
})
test("pm.response.headers provides access to response headers", () => {
return expect(
func(
`
const headers = pm.response.headers
pw.expect(headers.get("Content-Type")).toBe("application/json")
pw.expect(headers.get("Authorization")).toBe("Bearer token123")
pw.expect(headers.get("nonexistent")).toBe(null)
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'application/json' to be 'application/json'",
},
{
status: "pass",
message: "Expected 'Bearer token123' to be 'Bearer token123'",
},
{
status: "pass",
message: "Expected 'null' to be 'null'",
},
],
}),
])
})
test("pm.response object has correct structure and values", () => {
return expect(
func(
`
pw.expect(pm.response.code).toBe(200)
pw.expect(pm.response.status).toBe("OK")
pw.expect(pm.response.text()).toBe('{"message":"Hello, World!"}')
pw.expect(pm.response.json().message).toBe("Hello, World!")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected '200' to be '200'",
},
{
status: "pass",
message: "Expected 'OK' to be 'OK'",
},
{
status: "pass",
message:
'Expected \'{"message":"Hello, World!"}\' to be \'{"message":"Hello, World!"}\'',
},
{
status: "pass",
message: "Expected 'Hello, World!' to be 'Hello, World!'",
},
],
}),
])
})
})

View file

@ -0,0 +1,249 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "test response",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
describe("pm namespace - unsupported features", () => {
test("pm.info.iteration throws error", () => {
return expect(
func(
`
try {
const iteration = pm.info.iteration
pm.test("Should not reach here", () => {
pm.expect(true).toBe(false)
})
} catch (error) {
pm.test("Throws correct error", () => {
pm.expect(error.message).toInclude("pm.info.iteration is not supported")
})
}
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "Throws correct error",
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
test("pm.info.iterationCount throws error", () => {
return expect(
func(
`
try {
const iterationCount = pm.info.iterationCount
pm.test("Should not reach here", () => {
pm.expect(true).toBe(false)
})
} catch (error) {
pm.test("Throws correct error", () => {
pm.expect(error.message).toInclude("pm.info.iterationCount is not supported")
})
}
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "Throws correct error",
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
test("pm.collectionVariables.get() throws error", () => {
return expect(
func(
`
try {
pm.collectionVariables.get("test")
pm.test("Should not reach here", () => {
pm.expect(true).toBe(false)
})
} catch (error) {
pm.test("Throws correct error", () => {
pm.expect(error.message).toInclude("pm.collectionVariables.get() is not supported")
})
}
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "Throws correct error",
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
test("pm.vault.get() throws error", () => {
return expect(
func(
`
try {
pm.vault.get("test")
pm.test("Should not reach here", () => {
pm.expect(true).toBe(false)
})
} catch (error) {
pm.test("Throws correct error", () => {
pm.expect(error.message).toInclude("pm.vault.get() is not supported")
})
}
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "Throws correct error",
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
test("pm.iterationData.get() throws error", () => {
return expect(
func(
`
try {
pm.iterationData.get("test")
pm.test("Should not reach here", () => {
pm.expect(true).toBe(false)
})
} catch (error) {
pm.test("Throws correct error", () => {
pm.expect(error.message).toInclude("pm.iterationData.get() is not supported")
})
}
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "Throws correct error",
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
test("pm.execution.setNextRequest() throws error", () => {
return expect(
func(
`
try {
pm.execution.setNextRequest("next-request")
pm.test("Should not reach here", () => {
pm.expect(true).toBe(false)
})
} catch (error) {
pm.test("Throws correct error", () => {
pm.expect(error.message).toInclude("pm.execution.setNextRequest() is not supported")
})
}
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "Throws correct error",
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
test("pm.sendRequest() throws error", () => {
return expect(
func(
`
try {
pm.sendRequest("https://example.com", () => {})
pm.test("Should not reach here", () => {
pm.expect(true).toBe(false)
})
} catch (error) {
pm.test("Throws correct error", () => {
pm.expect(error.message).toInclude("pm.sendRequest() is not yet implemented")
})
}
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "Throws correct error",
expectResults: [{ status: "pass", message: expect.any(String) }],
}),
],
}),
])
})
})

View file

@ -0,0 +1,492 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript, runPreRequestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: [],
}
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)
describe("pm.environment", () => {
test("pm.environment.get returns the correct value for an existing active environment value", () => {
return expect(
func(
`
const data = pm.environment.get("a")
pm.expect(data).toBe("b")
`,
{
global: [],
selected: [
{
key: "a",
currentValue: "b",
initialValue: "b",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{ status: "pass", message: "Expected 'b' to be 'b'" }],
}),
])
})
test("pm.environment.set creates and retrieves environment variable", () => {
return expect(
func(
`
pm.environment.set("test_set", "set_value")
const retrieved = pm.environment.get("test_set")
pw.expect(retrieved).toBe("set_value")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'set_value' to be 'set_value'",
},
],
}),
])
})
test("pm.environment.set works correctly", () => {
return expect(
func(
`
pm.environment.set("newVar", "newValue")
const data = pm.environment.get("newVar")
pm.expect(data).toBe("newValue")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected 'newValue' to be 'newValue'" },
],
}),
])
})
test("pm.environment.has correctly identifies existing and non-existing variables", () => {
return expect(
func(
`
const hasExisting = pm.environment.has("existing_var")
const hasNonExisting = pm.environment.has("non_existing_var")
pw.expect(hasExisting.toString()).toBe("true")
pw.expect(hasNonExisting.toString()).toBe("false")
`,
{
global: [],
selected: [
{
key: "existing_var",
currentValue: "existing_value",
initialValue: "existing_value",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'true' to be 'true'",
},
{
status: "pass",
message: "Expected 'false' to be 'false'",
},
],
}),
])
})
})
describe("pm.globals", () => {
test("pm.globals.get returns the correct value for an existing global environment value", () => {
return expect(
func(
`
const data = pm.globals.get("globalVar")
pm.expect(data).toBe("globalValue")
`,
{
global: [
{
key: "globalVar",
currentValue: "globalValue",
initialValue: "globalValue",
secret: false,
},
],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'globalValue' to be 'globalValue'",
},
],
}),
])
})
test("pm.globals.set creates and retrieves global variable", () => {
return expect(
func(
`
pm.globals.set("test_global", "global_value")
const retrieved = pm.globals.get("test_global")
pw.expect(retrieved).toBe("global_value")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'global_value' to be 'global_value'",
},
],
}),
])
})
})
describe("pm.variables", () => {
test("pm.variables.get returns the correct value from any scope", () => {
return expect(
func(
`
const data = pm.variables.get("scopedVar")
pm.expect(data).toBe("scopedValue")
`,
{
global: [
{
key: "scopedVar",
currentValue: "scopedValue",
initialValue: "scopedValue",
secret: false,
},
],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'scopedValue' to be 'scopedValue'",
},
],
}),
])
})
test("pm.variables.set creates and retrieves variable in active environment", () => {
return expect(
func(
`
pm.variables.set("test_var", "test_value")
const retrieved = pm.variables.get("test_var")
pw.expect(retrieved).toBe("test_value")
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'test_value' to be 'test_value'",
},
],
}),
])
})
test("pm.variables.has correctly identifies existing and non-existing variables", () => {
return expect(
func(
`
const hasExisting = pm.variables.has("existing_var")
const hasNonExisting = pm.variables.has("non_existing_var")
pw.expect(hasExisting.toString()).toBe("true")
pw.expect(hasNonExisting.toString()).toBe("false")
`,
{
global: [],
selected: [
{
key: "existing_var",
currentValue: "existing_value",
initialValue: "existing_value",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message: "Expected 'true' to be 'true'",
},
{
status: "pass",
message: "Expected 'false' to be 'false'",
},
],
}),
])
})
test("pm.variables.replaceIn works correctly", () => {
return expect(
func(
`
const template = "Hello {{name}}, welcome to {{place}}!"
const result = pm.variables.replaceIn(template)
pm.expect(result).toBe("Hello World, welcome to Hoppscotch!")
`,
{
global: [],
selected: [
{
key: "name",
currentValue: "World",
initialValue: "World",
secret: false,
},
{
key: "place",
currentValue: "Hoppscotch",
initialValue: "Hoppscotch",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message:
"Expected 'Hello World, welcome to Hoppscotch!' to be 'Hello World, welcome to Hoppscotch!'",
},
],
}),
])
})
test("pm.variables.replaceIn handles multiple variables", () => {
return expect(
func(
`
const template = "User {{name}} has {{count}} items in {{location}}"
const result = pm.variables.replaceIn(template)
pw.expect(result).toBe("User Alice has 10 items in Cart")
`,
{
global: [
{
key: "location",
currentValue: "Cart",
initialValue: "Cart",
secret: false,
},
],
selected: [
{
key: "name",
currentValue: "Alice",
initialValue: "Alice",
secret: false,
},
{
key: "count",
currentValue: "10",
initialValue: "10",
secret: false,
},
],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{
status: "pass",
message:
"Expected 'User Alice has 10 items in Cart' to be 'User Alice has 10 items in Cart'",
},
],
}),
])
})
})
describe("pm.test", () => {
test("pm.test works as expected", () => {
return expect(
func(
`
pm.test("Simple test", function() {
pm.expect(1 + 1).toBe(2)
})
`,
{
global: [],
selected: [],
}
)()
).resolves.toEqualRight([
expect.objectContaining({
children: [
expect.objectContaining({
descriptor: "Simple test",
expectResults: [
{ status: "pass", message: "Expected '2' to be '2'" },
],
}),
],
}),
])
})
})
describe("pm namespace - pre-request scripts", () => {
const DEFAULT_REQUEST = getDefaultRESTRequest()
test("pm.environment works in pre-request scripts", () => {
return expect(
runPreRequestScript(
`
pm.environment.set("preReqVar", "preReqValue")
const retrieved = pm.environment.get("preReqVar")
console.log("Retrieved:", retrieved)
`,
{
envs: {
global: [],
selected: [],
},
request: DEFAULT_REQUEST,
}
)()
).resolves.toEqualRight(
expect.objectContaining({
updatedEnvs: expect.objectContaining({
selected: expect.arrayContaining([
expect.objectContaining({
key: "preReqVar",
currentValue: "preReqValue",
}),
]),
}),
})
)
})
test("pm.globals works in pre-request scripts", () => {
return expect(
runPreRequestScript(
`
pm.globals.set("globalPreReq", "globalPreValue")
`,
{
envs: {
global: [],
selected: [],
},
request: DEFAULT_REQUEST,
}
)()
).resolves.toEqualRight(
expect.objectContaining({
updatedEnvs: expect.objectContaining({
global: expect.arrayContaining([
expect.objectContaining({
key: "globalPreReq",
currentValue: "globalPreValue",
}),
]),
}),
})
)
})
test("pm.variables.set works in pre-request scripts", () => {
return expect(
runPreRequestScript(
`
pm.variables.set("varPreReq", "varPreValue")
`,
{
envs: {
global: [],
selected: [],
},
request: DEFAULT_REQUEST,
}
)()
).resolves.toEqualRight(
expect.objectContaining({
updatedEnvs: expect.objectContaining({
selected: expect.arrayContaining([
expect.objectContaining({
key: "varPreReq",
currentValue: "varPreValue",
}),
]),
}),
})
)
})
})

View file

@ -1,145 +0,0 @@
import { describe, expect, test } from "vitest"
import { runPreRequestScript } from "~/node"
describe("runPreRequestScript", () => {
test("returns the updated environment properly", () => {
return expect(
runPreRequestScript(
`
pw.env.set("bob", "newbob")
`,
{
global: [],
selected: [
{
key: "bob",
currentValue: "oldbob",
initialValue: "oldbob",
secret: false,
},
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
}
)()
).resolves.toEqualRight({
global: [],
selected: [
{
key: "bob",
currentValue: "newbob",
initialValue: "oldbob",
secret: false,
},
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
})
})
test("fails if the key is not a string", () => {
return expect(
runPreRequestScript(
`
pw.env.set(10, "newbob")
`,
{
global: [],
selected: [
{
key: "bob",
currentValue: "oldbob",
initialValue: "oldbob",
secret: false,
},
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
}
)()
).resolves.toBeLeft()
})
test("fails if the value is not a string", () => {
return expect(
runPreRequestScript(
`
pw.env.set("bob", 10)
`,
{
global: [],
selected: [
{
key: "bob",
currentValue: "oldbob",
initialValue: "oldbob",
secret: false,
},
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
}
)()
).resolves.toBeLeft()
})
test("fails for invalid syntax", () => {
return expect(
runPreRequestScript(
`
pw.env.set("bob",
`,
{
global: [],
selected: [
{
key: "bob",
currentValue: "oldbob",
initialValue: "oldbob",
secret: false,
},
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
}
)()
).resolves.toBeLeft()
})
test("creates new env variable if doesn't exist", () => {
return expect(
runPreRequestScript(
`
pw.env.set("foo", "bar")
`,
{ selected: [], global: [] }
)()
).resolves.toEqualRight({
global: [],
selected: [
{ key: "foo", currentValue: "bar", initialValue: "bar", secret: false },
],
})
})
})

View file

@ -3,14 +3,18 @@ import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { getDefaultRESTRequest } from "@hoppscotch/data"
import { runPreRequestScript, runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
describe("Base64 helper functions", () => {
const scriptExpectations = {
atob: {
script: `pw.env.set("atob", atob("SGVsbG8gV29ybGQ="))`,
environment: {
global: [],
selected: [
{
key: "atob",
@ -24,6 +28,7 @@ describe("Base64 helper functions", () => {
btoa: {
script: `pw.env.set("btoa", btoa("Hello World"))`,
environment: {
global: [],
selected: [
{
key: "btoa",
@ -41,11 +46,16 @@ describe("Base64 helper functions", () => {
test("successfully decodes the input string", () => {
return expect(
runPreRequestScript(scriptExpectations.atob.script, {
global: [],
selected: [],
envs: {
global: [],
selected: [],
},
request: defaultRequest,
})()
).resolves.toEqualRight(
expect.objectContaining(scriptExpectations.atob.environment)
expect.objectContaining({
updatedEnvs: scriptExpectations.atob.environment,
})
)
})
})
@ -54,11 +64,16 @@ describe("Base64 helper functions", () => {
test("successfully encodes the input string", () => {
return expect(
runPreRequestScript(scriptExpectations.btoa.script, {
global: [],
selected: [],
envs: {
global: [],
selected: [],
},
request: defaultRequest,
})()
).resolves.toEqualRight(
expect.objectContaining(scriptExpectations.btoa.environment)
expect.objectContaining({
updatedEnvs: scriptExpectations.btoa.environment,
})
)
})
})
@ -73,7 +88,11 @@ describe("Base64 helper functions", () => {
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, envs, fakeResponse),
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.envs)
)

View file

@ -1,11 +1,12 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
@ -14,7 +15,11 @@ const fakeResponse: TestResponse = {
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, envs, fakeResponse),
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)

View file

@ -1,11 +1,12 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
@ -14,7 +15,11 @@ const fakeResponse: TestResponse = {
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, envs, fakeResponse),
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)

View file

@ -1,11 +1,12 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
@ -14,7 +15,11 @@ const fakeResponse: TestResponse = {
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, envs, fakeResponse),
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)

View file

@ -1,11 +1,12 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
@ -14,13 +15,21 @@ const fakeResponse: TestResponse = {
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, envs, fakeResponse),
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.envs)
)
const funcTest = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, envs, fakeResponse),
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)

View file

@ -1,11 +1,12 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse, TestResult } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
@ -14,13 +15,21 @@ const fakeResponse: TestResponse = {
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, envs, fakeResponse),
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.envs)
)
const funcTest = (script: string, envs: TestResult["envs"]) =>
pipe(
runTestScript(script, envs, fakeResponse),
runTestScript(script, {
envs,
request: defaultRequest,
response: fakeResponse,
}),
TE.map((x) => x.tests)
)

View file

@ -1,11 +1,12 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
@ -14,7 +15,11 @@ const fakeResponse: TestResponse = {
const func = (script: string, res: TestResponse) =>
pipe(
runTestScript(script, { global: [], selected: [] }, res),
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: res,
}),
TE.map((x) => x.tests)
)

View file

@ -1,11 +1,12 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
@ -14,7 +15,11 @@ const fakeResponse: TestResponse = {
const func = (script: string, res: TestResponse) =>
pipe(
runTestScript(script, { global: [], selected: [] }, res),
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: res,
}),
TE.map((x) => x.tests)
)

View file

@ -1,11 +1,12 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { getDefaultRESTRequest } from "@hoppscotch/data"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
@ -14,7 +15,11 @@ const fakeResponse: TestResponse = {
const func = (script: string, res: TestResponse) =>
pipe(
runTestScript(script, { global: [], selected: [] }, res),
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: res,
}),
TE.map((x) => x.tests)
)

View file

@ -1,11 +1,12 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
@ -14,7 +15,11 @@ const fakeResponse: TestResponse = {
const func = (script: string, res: TestResponse) =>
pipe(
runTestScript(script, { global: [], selected: [] }, res),
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: res,
}),
TE.map((x) => x.tests)
)

View file

@ -1,11 +1,12 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
@ -14,7 +15,11 @@ const fakeResponse: TestResponse = {
const func = (script: string, res: TestResponse) =>
pipe(
runTestScript(script, { global: [], selected: [] }, res),
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: res,
}),
TE.map((x) => x.tests)
)

View file

@ -0,0 +1,173 @@
import { describe, expect, test } from "vitest"
import { getDefaultRESTRequest } from "@hoppscotch/data"
import { runPreRequestScript } from "~/node"
const DEFAULT_REQUEST = getDefaultRESTRequest()
describe("runPreRequestScript", () => {
test("returns the updated environment properly", () => {
return expect(
runPreRequestScript(
`
pw.env.set("bob", "newbob")
`,
{
envs: {
global: [],
selected: [
{
key: "bob",
currentValue: "oldbob",
initialValue: "oldbob",
secret: false,
},
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
},
request: DEFAULT_REQUEST,
}
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [],
selected: [
{
key: "bob",
currentValue: "newbob",
initialValue: "oldbob",
secret: false,
},
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
test("fails if the key is not a string", () => {
return expect(
runPreRequestScript(
`
pw.env.set(10, "newbob")
`,
{
envs: {
global: [],
selected: [
{
key: "bob",
currentValue: "oldbob",
initialValue: "oldbob",
secret: false,
},
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
},
request: DEFAULT_REQUEST,
}
)()
).resolves.toBeLeft()
})
test("fails if the value is not a string", () => {
return expect(
runPreRequestScript(
`
pw.env.set("bob", 10)
`,
{
envs: {
global: [],
selected: [
{
key: "bob",
currentValue: "oldbob",
initialValue: "oldbob",
secret: false,
},
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
},
request: DEFAULT_REQUEST,
}
)()
).resolves.toBeLeft()
})
test("fails for invalid syntax", () => {
return expect(
runPreRequestScript(
`
pw.env.set("bob",
`,
{
envs: {
global: [],
selected: [
{
key: "bob",
currentValue: "oldbob",
initialValue: "oldbob",
secret: false,
},
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
},
request: DEFAULT_REQUEST,
}
)()
).resolves.toBeLeft()
})
test("creates new env variable if doesn't exist", () => {
return expect(
runPreRequestScript(
`
pw.env.set("foo", "bar")
`,
{ envs: { global: [], selected: [] }, request: DEFAULT_REQUEST }
)()
).resolves.toEqualRight({
updatedEnvs: {
global: [],
selected: [
{
key: "foo",
currentValue: "bar",
initialValue: "bar",
secret: false,
},
],
},
updatedRequest: DEFAULT_REQUEST,
updatedCookies: null,
})
})
})

View file

@ -1,11 +1,12 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { describe, expect, test } from "vitest"
import { runTestScript } from "~/node"
import { TestResponse } from "~/types"
const defaultRequest = getDefaultRESTRequest()
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
@ -14,7 +15,11 @@ const fakeResponse: TestResponse = {
const func = (script: string, res: TestResponse) =>
pipe(
runTestScript(script, { global: [], selected: [] }, res),
runTestScript(script, {
envs: { global: [], selected: [] },
request: defaultRequest,
response: res,
}),
TE.map((x) => x.tests)
)

View file

@ -1,24 +0,0 @@
import { preventCyclicObjects } from "~/shared-utils"
import { describe, expect, test } from "vitest"
describe("preventCyclicObjects", () => {
test("succeeds with a simple object", () => {
const testObj = {
a: 1,
}
expect(preventCyclicObjects(testObj)).toBeRight()
})
test("fails with a cyclic object", () => {
const testObj = {
a: 1,
b: null as any,
}
testObj.b = testObj
expect(preventCyclicObjects(testObj)).toBeLeft()
})
})

View file

@ -0,0 +1,183 @@
import {
HoppRESTAuth,
HoppRESTReqBody,
HoppRESTRequest,
} from "@hoppscotch/data"
import { describe, expect, test } from "vitest"
import { getRequestSetterMethods } from "~/utils/pre-request"
const baseRequest: HoppRESTRequest = {
v: "15",
name: "Test Request",
endpoint: "https://example.com/api",
method: "GET",
headers: [{ key: "X-Test", value: "val1", active: true, description: "" }],
params: [{ key: "q", value: "search", active: true, description: "" }],
body: { contentType: null, body: null },
auth: { authType: "none", authActive: false },
preRequestScript: "",
testScript: "",
requestVariables: [],
responses: {},
}
describe("getRequestSetterMethods", () => {
test("`setUrl` method", () => {
const { methods, updatedRequest } = getRequestSetterMethods(baseRequest)
methods.setUrl("https://updated.com/api")
expect(updatedRequest.endpoint).toBe("https://updated.com/api")
})
test("`setMethod` should update and uppercase the method", () => {
const { methods, updatedRequest } = getRequestSetterMethods(baseRequest)
methods.setMethod("post")
expect(updatedRequest.method).toBe("POST")
})
test("`setHeader` setter should update existing header case-insensitively", () => {
const { methods, updatedRequest } = getRequestSetterMethods(baseRequest)
methods.setHeader("x-test", "updatedVal")
expect(
updatedRequest.headers.find((h) => h.key.toLowerCase() === "x-test")
?.value
).toBe("updatedVal")
})
test("`setHeader` setter should add new header if not present", () => {
const { methods, updatedRequest } = getRequestSetterMethods(baseRequest)
methods.setHeader("X-New-Header", "newValue")
expect(updatedRequest.headers.some((h) => h.key === "X-New-Header")).toBe(
true
)
expect(
updatedRequest.headers.find((h) => h.key === "X-New-Header")?.value
).toBe("newValue")
})
test("`removeHeader` setter should remove a header", () => {
const { methods, updatedRequest } = getRequestSetterMethods(baseRequest)
methods.removeHeader("X-Test")
expect(
updatedRequest.headers.find((h) => h.key === "X-Test")
).toBeUndefined()
})
test("`setParam` setter should update existing param case-insensitively", () => {
const { methods, updatedRequest } = getRequestSetterMethods(baseRequest)
methods.setParam("Q", "updatedParam")
expect(
updatedRequest.params.find((p) => p.key.toLowerCase() === "q")?.value
).toBe("updatedParam")
})
test("`setParam` setter should add new param if absent", () => {
const { methods, updatedRequest } = getRequestSetterMethods(baseRequest)
methods.setParam("newParam", "value")
expect(updatedRequest.params.some((p) => p.key === "newParam")).toBe(true)
})
test("`removeParam` setter should remove a param", () => {
const { methods, updatedRequest } = getRequestSetterMethods(baseRequest)
methods.removeParam("q")
expect(updatedRequest.params.find((p) => p.key === "q")).toBeUndefined()
})
test("`setBody` setter should update the body with complete replacement", () => {
const { methods, updatedRequest } = getRequestSetterMethods(baseRequest)
const newBody: HoppRESTReqBody = {
contentType: "application/json",
body: JSON.stringify({ changed: true }),
}
methods.setBody(newBody)
expect(updatedRequest.body).toEqual(newBody)
})
test("`setBody` setter should support partial merge", () => {
// Set up a request with existing body
const requestWithBody: HoppRESTRequest = {
...baseRequest,
body: {
contentType: "application/json",
body: JSON.stringify({ existing: "data" }),
},
}
const { methods, updatedRequest } = getRequestSetterMethods(requestWithBody)
// Only update contentType, preserving existing body
methods.setBody({ contentType: "application/xml" })
expect(updatedRequest.body).toEqual({
contentType: "application/xml",
body: JSON.stringify({ existing: "data" }),
})
})
test("`setAuth` setter should update the authorization properties with complete replacement", () => {
const { methods, updatedRequest } = getRequestSetterMethods(baseRequest)
const newAuth: HoppRESTAuth = {
authType: "basic",
username: "abc",
password: "123",
authActive: true,
}
methods.setAuth(newAuth)
expect(updatedRequest.auth).toEqual(newAuth)
})
test("`setAuth` setter should support partial merge", () => {
// Set up a request with existing auth
const requestWithAuth: HoppRESTRequest = {
...baseRequest,
auth: {
authType: "basic",
username: "existing-user",
password: "existing-pass",
authActive: true,
},
}
const { methods, updatedRequest } = getRequestSetterMethods(requestWithAuth)
// Only update username, preserving other auth fields
methods.setAuth({ username: "updated-user" } as Partial<HoppRESTAuth>)
expect(updatedRequest.auth).toEqual({
authType: "basic",
username: "updated-user",
password: "existing-pass",
authActive: true,
})
})
test("`setHeaders` setter throws error on invalid input", () => {
const { methods } = getRequestSetterMethods(baseRequest)
expect(() => methods.setHeaders(null)).toThrow()
})
test("`setParams` setter throws error on invalid input", () => {
const { methods } = getRequestSetterMethods(baseRequest)
expect(() => methods.setParams(null)).toThrow()
})
test("`setBody` setter throws error on invalid input", () => {
const { methods } = getRequestSetterMethods(baseRequest)
expect(() => methods.setBody("invalid_body")).toThrow()
})
test("`setAuth` setter throws error on invalid input", () => {
const { methods } = getRequestSetterMethods(baseRequest)
expect(() => methods.setAuth([6])).toThrow()
})
})

View file

@ -0,0 +1,186 @@
import {
getSharedCookieMethods,
getSharedRequestProps,
preventCyclicObjects,
} from "~/utils/shared"
import { Cookie, HoppRESTRequest } from "@hoppscotch/data"
import { describe, expect, test } from "vitest"
describe("preventCyclicObjects", () => {
test("succeeds with a simple object", () => {
const testObj = {
a: 1,
}
expect(preventCyclicObjects(testObj)).toBeRight()
})
test("fails with a cyclic object", () => {
const testObj = {
a: 1,
b: null as any,
}
testObj.b = testObj
expect(preventCyclicObjects(testObj)).toBeLeft()
})
})
describe("getSharedRequestProps", () => {
const baseRequest: HoppRESTRequest = {
v: "15",
name: "Test Request",
endpoint: "https://example.com/api",
method: "GET",
headers: [{ key: "X-Test", value: "val1", active: true, description: "" }],
params: [{ key: "q", value: "search", active: true, description: "" }],
body: { contentType: null, body: null },
auth: { authType: "none", authActive: false },
preRequestScript: "",
testScript: "",
requestVariables: [],
responses: {},
}
test("`url` getter", () => {
const request = getSharedRequestProps(baseRequest)
expect(request.url).toBe("https://example.com/api")
})
test("`method` getter", () => {
const request = getSharedRequestProps(baseRequest)
expect(request.method).toBe("GET")
})
test("`headers` getter", () => {
const request = getSharedRequestProps(baseRequest)
expect(request.headers).toEqual(baseRequest.headers)
})
test("`params` getter", () => {
const request = getSharedRequestProps(baseRequest)
expect(request.params).toEqual(baseRequest.params)
})
test("`body` getter", () => {
const request = getSharedRequestProps(baseRequest)
expect(request.body).toEqual(baseRequest.body)
})
test("`auth` getter", () => {
const request = getSharedRequestProps(baseRequest)
expect(request.auth).toEqual(baseRequest.auth)
})
test("`params` getter", () => {
const request = getSharedRequestProps(baseRequest)
expect(request.params).toEqual(baseRequest.params)
})
test("`body` getter", () => {
const request = getSharedRequestProps(baseRequest)
expect(request.body).toEqual(baseRequest.body)
})
test("`auth` getter", () => {
const request = getSharedRequestProps(baseRequest)
expect(request.auth).toEqual(baseRequest.auth)
})
})
describe("getSharedCookieMethods", () => {
const validCookie: Cookie = {
name: "session",
value: "abc123",
domain: "example.com",
path: "/",
httpOnly: true,
secure: true,
sameSite: "Lax",
}
test("get() should return existing cookie or null", () => {
const { methods } = getSharedCookieMethods([
validCookie,
{ ...validCookie, name: "token", value: "xyz" },
])
expect(methods.get("example.com", "session")).toEqual(validCookie)
expect(methods.get("example.com", "missing")).toBeNull()
})
test("get() should throw for non-string args", () => {
const { methods } = getSharedCookieMethods([validCookie])
expect(() => methods.get(123, "session")).toThrow(
"Expected domain and cookieName to be strings"
)
})
test("set() should add a new cookie", () => {
const { methods } = getSharedCookieMethods([])
methods.set("example.com", validCookie)
expect(methods.get("example.com", "session")).toEqual(validCookie)
})
test("set() should replace an existing cookie with same domain+name", () => {
const oldCookie = { ...validCookie, value: "old" }
const { methods } = getSharedCookieMethods([oldCookie])
methods.set("example.com", validCookie)
expect(methods.getAll("example.com")).toHaveLength(1)
expect(methods.get("example.com", "session")?.value).toBe("abc123")
})
test("set() should throw for invalid cookie per schema", () => {
const { methods } = getSharedCookieMethods([])
expect(() => methods.set("example.com", { bad: "cookie" })).toThrow(
"Invalid cookie"
)
})
test("has() should return true if cookie exists", () => {
const { methods } = getSharedCookieMethods([validCookie])
expect(methods.has("example.com", "session")).toBe(true)
expect(methods.has("example.com", "missing")).toBe(false)
})
test("getAll() should return all cookies for a domain", () => {
const cookies = [
validCookie,
{ ...validCookie, name: "token", value: "x" },
{ ...validCookie, domain: "other.com" },
]
const { methods } = getSharedCookieMethods(cookies)
const result = methods.getAll("example.com")
expect(result).toHaveLength(2)
expect(result.every((c) => c.domain === "example.com")).toBe(true)
})
test("delete() should remove the specified cookie", () => {
const { methods } = getSharedCookieMethods([validCookie])
methods.delete("example.com", "session")
expect(methods.get("example.com", "session")).toBeNull()
})
test("clear() should remove all cookies for a domain", () => {
const cookies = [validCookie, { ...validCookie, name: "token", value: "x" }]
const { methods } = getSharedCookieMethods(cookies)
methods.clear("example.com")
expect(methods.getAll("example.com")).toHaveLength(0)
})
test("should throw for non-string args on has/delete/getAll/clear", () => {
const { methods } = getSharedCookieMethods([validCookie])
expect(() => methods.has(123 as any, "session")).toThrow()
expect(() => methods.delete(123 as any, "session")).toThrow()
expect(() => methods.getAll(123 as any)).toThrow()
expect(() => methods.clear(123 as any)).toThrow()
})
})

View file

@ -1,5 +1,105 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
;(inputs) => {
// Keep strict mode scoped to this IIFE to avoid leaking strictness to concatenated/bootstrapped code
"use strict"
const toJSON = (input) => {
if (input == null) return null
if (typeof input === "string") {
try {
return JSON.parse(input)
} catch {
throw new Error("Invalid JSON string")
}
}
if (typeof input === "object") {
return input
}
throw new Error(
"Unsupported input type for hopp.response.asJSON(). Expected string or object."
)
}
/**
* Convert input into text (stringify objects, pass strings through).
*/
const toText = (input) => {
if (input == null) return ""
if (typeof input === "string") return input
if (typeof input === "object") return JSON.stringify(input)
throw new Error("Unsupported input type for hopp.response.asText()")
}
/**
* Convert input into bytes (UTF-8 encode strings, stringify objects first).
*/
const toBytes = (input) => {
if (input == null) return new Uint8Array()
if (typeof input === "string") return new TextEncoder().encode(input)
if (typeof input === "object")
return new TextEncoder().encode(JSON.stringify(input))
throw new Error("Unsupported input type for hopp.response.bytes()")
}
const { status, statusText, headers, responseTime, body } =
inputs.getResponse()
const pwResponse = {
status,
body,
headers,
}
// Create response body object with read-only methods
const responseBody = {
asJSON: () => toJSON(body),
asText: () => toText(body),
bytes: () => toBytes(body),
}
// Make body methods read-only using a loop
;["asJSON", "asText", "bytes"].forEach((method) => {
Object.defineProperty(responseBody, method, {
value: responseBody[method],
writable: false,
configurable: false,
})
})
Object.freeze(responseBody)
// Create response object with read-only properties
const hoppResponse = {}
// Define response properties and their values
const responseProperties = {
statusCode: status,
statusText: statusText,
headers: headers,
responseTime: responseTime,
body: responseBody,
}
// Apply read-only protection to all response properties
Object.keys(responseProperties).forEach((prop) => {
Object.defineProperty(hoppResponse, prop, {
value: responseProperties[prop],
writable: false,
enumerable: true,
configurable: false,
})
})
// Freeze the entire response object
Object.freeze(hoppResponse)
globalThis.pw = {
env: {
get: (key) => inputs.envGet(key),
@ -46,6 +146,401 @@
testFn()
inputs.postTest()
},
response: inputs.getResponse(),
response: pwResponse,
}
// Immutable getters under the `request` namespace
const requestProps = {}
// Define all properties with unified read-only protection
;["url", "method", "params", "headers", "body", "auth"].forEach((prop) => {
Object.defineProperty(requestProps, prop, {
enumerable: true,
configurable: false,
get() {
return inputs.getRequestProps()[prop]
},
set(_value) {
throw new TypeError(`hopp.request.${prop} is read-only`)
},
})
})
// Special handling for variables property
Object.defineProperty(requestProps, "variables", {
enumerable: true,
configurable: false,
get() {
return Object.freeze({
get: (key) => inputs.getRequestVariable(key),
})
},
set(_value) {
throw new TypeError(`hopp.request.variables is read-only`)
},
})
// Freeze the entire requestProps object for additional protection
Object.freeze(requestProps)
globalThis.hopp = {
env: {
get: (key) =>
inputs.envGetResolve(key, { fallbackToNull: true, source: "all" }),
getRaw: (key) =>
inputs.envGet(key, { fallbackToNull: true, source: "all" }),
set: (key, value) => inputs.envSet(key, value),
delete: (key) => inputs.envUnset(key),
reset: (key) => inputs.envReset(key),
getInitialRaw: (key) => inputs.envGetInitialRaw(key),
setInitial: (key, value) => inputs.envSetInitial(key, value),
active: {
get: (key) =>
inputs.envGetResolve(key, { fallbackToNull: true, source: "active" }),
getRaw: (key) =>
inputs.envGet(key, { fallbackToNull: true, source: "active" }),
set: (key, value) => inputs.envSet(key, value, { source: "active" }),
delete: (key) => inputs.envUnset(key, { source: "active" }),
reset: (key) => inputs.envReset(key, { source: "active" }),
getInitialRaw: (key) =>
inputs.envGetInitialRaw(key, { source: "active" }),
setInitial: (key, value) =>
inputs.envSetInitial(key, value, { source: "active" }),
},
global: {
get: (key) =>
inputs.envGetResolve(key, { fallbackToNull: true, source: "global" }),
getRaw: (key) =>
inputs.envGet(key, { fallbackToNull: true, source: "global" }),
set: (key, value) => inputs.envSet(key, value, { source: "global" }),
delete: (key) => inputs.envUnset(key, { source: "global" }),
reset: (key) => inputs.envReset(key, { source: "global" }),
getInitialRaw: (key) =>
inputs.envGetInitialRaw(key, { source: "global" }),
setInitial: (key, value) =>
inputs.envSetInitial(key, value, { source: "global" }),
},
},
request: requestProps,
cookies: {
get: (domain, name) => inputs.cookieGet(domain, name),
set: (domain, cookie) => inputs.cookieSet(domain, cookie),
has: (domain, name) => inputs.cookieHas(domain, name),
getAll: (domain) => inputs.cookieGetAll(domain),
delete: (domain, name) => inputs.cookieDelete(domain, name),
clear: (domain) => inputs.cookieClear(domain),
},
expect: (expectVal) => {
const isDateInstance = expectVal instanceof Date
const expectation = {
toBe: (expectedVal) => inputs.expectToBe(expectVal, expectedVal),
toBeLevel2xx: () => inputs.expectToBeLevel2xx(expectVal),
toBeLevel3xx: () => inputs.expectToBeLevel3xx(expectVal),
toBeLevel4xx: () => inputs.expectToBeLevel4xx(expectVal),
toBeLevel5xx: () => inputs.expectToBeLevel5xx(expectVal),
toBeType: (expectedType) =>
inputs.expectToBeType(expectVal, expectedType, isDateInstance),
toHaveLength: (expectedLength) =>
inputs.expectToHaveLength(expectVal, expectedLength),
toInclude: (needle) => inputs.expectToInclude(expectVal, needle),
}
Object.defineProperty(expectation, "not", {
get: () => ({
toBe: (expectedVal) => inputs.expectNotToBe(expectVal, expectedVal),
toBeLevel2xx: () => inputs.expectNotToBeLevel2xx(expectVal),
toBeLevel3xx: () => inputs.expectNotToBeLevel3xx(expectVal),
toBeLevel4xx: () => inputs.expectNotToBeLevel4xx(expectVal),
toBeLevel5xx: () => inputs.expectNotToBeLevel5xx(expectVal),
toBeType: (expectedType) =>
inputs.expectNotToBeType(expectVal, expectedType, isDateInstance),
toHaveLength: (expectedLength) =>
inputs.expectNotToHaveLength(expectVal, expectedLength),
toInclude: (needle) => inputs.expectNotToInclude(expectVal, needle),
}),
})
return expectation
},
test: (descriptor, testFn) => {
inputs.preTest(descriptor)
testFn()
inputs.postTest()
},
response: hoppResponse,
}
// PM Namespace - Postman Compatibility Layer
globalThis.pm = {
environment: {
get: (key) => globalThis.hopp.env.active.get(key),
set: (key, value) => globalThis.hopp.env.active.set(key, value),
unset: (key) => globalThis.hopp.env.active.delete(key),
has: (key) => globalThis.hopp.env.active.get(key) !== null,
clear: () => {
throw new Error("pm.environment.clear() not yet implemented")
},
toObject: () => {
throw new Error("pm.environment.toObject() not yet implemented")
},
},
globals: {
get: (key) => globalThis.hopp.env.global.get(key),
set: (key, value) => globalThis.hopp.env.global.set(key, value),
unset: (key) => globalThis.hopp.env.global.delete(key),
has: (key) => globalThis.hopp.env.global.get(key) !== null,
clear: () => {
throw new Error("pm.globals.clear() not yet implemented")
},
toObject: () => {
throw new Error("pm.globals.toObject() not yet implemented")
},
},
variables: {
get: (key) => globalThis.hopp.env.get(key),
set: (key, value) => globalThis.hopp.env.active.set(key, value),
has: (key) => globalThis.hopp.env.get(key) !== null,
replaceIn: (template) => {
if (typeof template !== "string") return template
return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
const value = globalThis.hopp.env.get(key.trim())
return value !== null ? value : match
})
},
},
request: {
get url() {
const urlString = globalThis.hopp.request.url
return {
toString: () => urlString,
}
},
get method() {
return globalThis.hopp.request.method
},
get headers() {
return {
get: (name) => {
const headers = globalThis.hopp.request.headers
const header = headers.find(
(h) => h.key.toLowerCase() === name.toLowerCase()
)
return header ? header.value : null
},
has: (name) => {
const headers = globalThis.hopp.request.headers
return headers.some(
(h) => h.key.toLowerCase() === name.toLowerCase()
)
},
all: () => {
const result = {}
globalThis.hopp.request.headers.forEach((header) => {
result[header.key] = header.value
})
return result
},
}
},
get body() {
return globalThis.hopp.request.body
},
get auth() {
return globalThis.hopp.request.auth
},
},
response: {
get code() {
return globalThis.hopp.response.statusCode
},
get status() {
return globalThis.hopp.response.statusText
},
get responseTime() {
return globalThis.hopp.response.responseTime
},
text: () => globalThis.hopp.response.body.asText(),
json: () => globalThis.hopp.response.body.asJSON(),
stream: globalThis.hopp.response.body.bytes(),
headers: {
get: (name) => {
const headers = globalThis.hopp.response.headers
const header = headers.find(
(h) => h.key.toLowerCase() === name.toLowerCase()
)
return header ? header.value : null
},
has: (name) => {
const headers = globalThis.hopp.response.headers
return headers.some((h) => h.key.toLowerCase() === name.toLowerCase())
},
all: () => {
const result = {}
globalThis.hopp.response.headers.forEach((header) => {
result[header.key] = header.value
})
return result
},
},
cookies: {
get: (_name) => {
throw new Error("pm.response.cookies.get() not yet implemented")
},
has: (_name) => {
throw new Error("pm.response.cookies.has() not yet implemented")
},
toObject: () => {
throw new Error("pm.response.cookies.toObject() not yet implemented")
},
},
},
cookies: {
get: (_name) => {
throw new Error(
"pm.cookies.get() needs domain information - use hopp.cookies instead"
)
},
set: (_name, _value, _options) => {
throw new Error(
"pm.cookies.set() needs domain information - use hopp.cookies instead"
)
},
jar: () => {
throw new Error("pm.cookies.jar() not yet implemented")
},
},
test: (name, fn) => globalThis.hopp.test(name, fn),
expect: (value) => globalThis.hopp.expect(value),
// Script context information
info: {
eventName: "post-request", // post-request context
get requestName() {
return inputs.pmInfoRequestName()
},
get requestId() {
return inputs.pmInfoRequestId()
},
// Unsupported Collection Runner features
get iteration() {
throw new Error(
"pm.info.iteration is not supported in Hoppscotch (Collection Runner feature)"
)
},
get iterationCount() {
throw new Error(
"pm.info.iterationCount is not supported in Hoppscotch (Collection Runner feature)"
)
},
},
// Unsupported APIs that throw errors
sendRequest: () => {
throw new Error("pm.sendRequest() is not yet implemented in Hoppscotch")
},
// Collection variables (unsupported)
collectionVariables: {
get: () => {
throw new Error(
"pm.collectionVariables.get() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
set: () => {
throw new Error(
"pm.collectionVariables.set() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
unset: () => {
throw new Error(
"pm.collectionVariables.unset() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
has: () => {
throw new Error(
"pm.collectionVariables.has() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
clear: () => {
throw new Error(
"pm.collectionVariables.clear() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
toObject: () => {
throw new Error(
"pm.collectionVariables.toObject() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
},
// Postman Vault (unsupported)
vault: {
get: () => {
throw new Error(
"pm.vault.get() is not supported in Hoppscotch (Postman Vault feature)"
)
},
set: () => {
throw new Error(
"pm.vault.set() is not supported in Hoppscotch (Postman Vault feature)"
)
},
unset: () => {
throw new Error(
"pm.vault.unset() is not supported in Hoppscotch (Postman Vault feature)"
)
},
},
// Iteration data (unsupported)
iterationData: {
get: () => {
throw new Error(
"pm.iterationData.get() is not supported in Hoppscotch (Collection Runner feature)"
)
},
set: () => {
throw new Error(
"pm.iterationData.set() is not supported in Hoppscotch (Collection Runner feature)"
)
},
unset: () => {
throw new Error(
"pm.iterationData.unset() is not supported in Hoppscotch (Collection Runner feature)"
)
},
has: () => {
throw new Error(
"pm.iterationData.has() is not supported in Hoppscotch (Collection Runner feature)"
)
},
toObject: () => {
throw new Error(
"pm.iterationData.toObject() is not supported in Hoppscotch (Collection Runner feature)"
)
},
},
// Execution control (unsupported)
execution: {
setNextRequest: () => {
throw new Error(
"pm.execution.setNextRequest() is not supported in Hoppscotch (Collection Runner feature)"
)
},
},
}
}

View file

@ -1,5 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
;(inputs) => {
// Keep strict mode scoped to this IIFE to avoid leaking strictness to concatenated/bootstrapped code
"use strict"
globalThis.pw = {
env: {
get: (key) => inputs.envGet(key),
@ -9,4 +11,299 @@
resolve: (key) => inputs.envResolve(key),
},
}
const requestProps = {
// Setter methods
setUrl: (url) => inputs.setRequestUrl(url),
setMethod: (method) => inputs.setRequestMethod(method),
setHeader: (name, value) => inputs.setRequestHeader(name, value),
setHeaders: (headers) => inputs.setRequestHeaders(headers),
removeHeader: (key) => inputs.removeRequestHeader(key),
setParam: (name, value) => inputs.setRequestParam(name, value),
setParams: (params) => inputs.setRequestParams(params),
removeParam: (key) => inputs.removeRequestParam(key),
setBody: (body) => inputs.setRequestBody(body),
setAuth: (auth) => inputs.setRequestAuth(auth),
// Request variables
variables: {
get: (key) => inputs.getRequestVariable(key),
set: (key, value) => inputs.setRequestVariable(key, value),
},
}
// Define all properties with unified read-only protection
;["url", "method", "params", "headers", "body", "auth"].forEach((prop) => {
Object.defineProperty(requestProps, prop, {
enumerable: true,
configurable: false,
get() {
const currentValues = inputs.getRequestProps()
return currentValues[prop]
},
set(_value) {
throw new TypeError(`hopp.request.${prop} is read-only`)
},
})
})
// Freeze the entire requestProps object for additional protection
Object.freeze(requestProps)
globalThis.hopp = {
env: {
get: (key) =>
inputs.envGetResolve(key, { fallbackToNull: true, source: "all" }),
getRaw: (key) =>
inputs.envGet(key, { fallbackToNull: true, source: "all" }),
set: (key, value) => inputs.envSet(key, value),
delete: (key) => inputs.envUnset(key),
reset: (key) => inputs.envReset(key),
getInitialRaw: (key) => inputs.envGetInitialRaw(key),
setInitial: (key, value) => inputs.envSetInitial(key, value),
active: {
get: (key) =>
inputs.envGetResolve(key, { fallbackToNull: true, source: "active" }),
getRaw: (key) =>
inputs.envGet(key, { fallbackToNull: true, source: "active" }),
set: (key, value) => inputs.envSet(key, value, { source: "active" }),
delete: (key) => inputs.envUnset(key, { source: "active" }),
reset: (key) => inputs.envReset(key, { source: "active" }),
getInitialRaw: (key) =>
inputs.envGetInitialRaw(key, { source: "active" }),
setInitial: (key, value) =>
inputs.envSetInitial(key, value, { source: "active" }),
},
global: {
get: (key) =>
inputs.envGetResolve(key, { fallbackToNull: true, source: "global" }),
getRaw: (key) =>
inputs.envGet(key, { fallbackToNull: true, source: "global" }),
set: (key, value) => inputs.envSet(key, value, { source: "global" }),
delete: (key) => inputs.envUnset(key, { source: "global" }),
reset: (key) => inputs.envReset(key, { source: "global" }),
getInitialRaw: (key) =>
inputs.envGetInitialRaw(key, { source: "global" }),
setInitial: (key, value) =>
inputs.envSetInitial(key, value, { source: "global" }),
},
},
request: requestProps,
cookies: {
get: (domain, name) => inputs.cookieGet(domain, name),
set: (domain, cookie) => inputs.cookieSet(domain, cookie),
has: (domain, name) => inputs.cookieHas(domain, name),
getAll: (domain) => inputs.cookieGetAll(domain),
delete: (domain, name) => inputs.cookieDelete(domain, name),
clear: (domain) => inputs.cookieClear(domain),
},
}
// PM Namespace - Postman Compatibility Layer
globalThis.pm = {
environment: {
get: (key) => globalThis.hopp.env.active.get(key),
set: (key, value) => globalThis.hopp.env.active.set(key, value),
unset: (key) => globalThis.hopp.env.active.delete(key),
has: (key) => globalThis.hopp.env.active.get(key) !== null,
clear: () => {
throw new Error("pm.environment.clear() not yet implemented")
},
toObject: () => {
throw new Error("pm.environment.toObject() not yet implemented")
},
},
globals: {
get: (key) => globalThis.hopp.env.global.get(key),
set: (key, value) => globalThis.hopp.env.global.set(key, value),
unset: (key) => globalThis.hopp.env.global.delete(key),
has: (key) => globalThis.hopp.env.global.get(key) !== null,
clear: () => {
throw new Error("pm.globals.clear() not yet implemented")
},
toObject: () => {
throw new Error("pm.globals.toObject() not yet implemented")
},
},
variables: {
get: (key) => globalThis.hopp.env.get(key),
set: (key, value) => globalThis.hopp.env.active.set(key, value),
has: (key) => globalThis.hopp.env.get(key) !== null,
replaceIn: (template) => {
if (typeof template !== "string") return template
return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
const value = globalThis.hopp.env.get(key.trim())
return value !== null ? value : match
})
},
},
request: {
get url() {
const urlString = globalThis.hopp.request.url
return {
toString: () => urlString,
}
},
get method() {
return globalThis.hopp.request.method
},
get headers() {
return {
get: (name) => {
const headers = globalThis.hopp.request.headers
const header = headers.find(
(h) => h.key.toLowerCase() === name.toLowerCase()
)
return header ? header.value : null
},
has: (name) => {
const headers = globalThis.hopp.request.headers
return headers.some(
(h) => h.key.toLowerCase() === name.toLowerCase()
)
},
all: () => {
const result = {}
globalThis.hopp.request.headers.forEach((header) => {
result[header.key] = header.value
})
return result
},
}
},
get body() {
return globalThis.hopp.request.body
},
get auth() {
return globalThis.hopp.request.auth
},
},
// Script context information
info: {
eventName: "pre-request",
get requestName() {
return inputs.pmInfoRequestName()
},
get requestId() {
return inputs.pmInfoRequestId()
},
// Unsupported Collection Runner features
get iteration() {
throw new Error(
"pm.info.iteration is not supported in Hoppscotch (Collection Runner feature)"
)
},
get iterationCount() {
throw new Error(
"pm.info.iterationCount is not supported in Hoppscotch (Collection Runner feature)"
)
},
},
// Unsupported APIs that throw errors
sendRequest: () => {
throw new Error("pm.sendRequest() is not yet implemented in Hoppscotch")
},
// Collection variables (unsupported)
collectionVariables: {
get: () => {
throw new Error(
"pm.collectionVariables.get() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
set: () => {
throw new Error(
"pm.collectionVariables.set() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
unset: () => {
throw new Error(
"pm.collectionVariables.unset() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
has: () => {
throw new Error(
"pm.collectionVariables.has() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
clear: () => {
throw new Error(
"pm.collectionVariables.clear() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
toObject: () => {
throw new Error(
"pm.collectionVariables.toObject() is not supported in Hoppscotch (use environment or request variables instead)"
)
},
},
// Postman Vault (unsupported)
vault: {
get: () => {
throw new Error(
"pm.vault.get() is not supported in Hoppscotch (Postman Vault feature)"
)
},
set: () => {
throw new Error(
"pm.vault.set() is not supported in Hoppscotch (Postman Vault feature)"
)
},
unset: () => {
throw new Error(
"pm.vault.unset() is not supported in Hoppscotch (Postman Vault feature)"
)
},
},
// Iteration data (unsupported)
iterationData: {
get: () => {
throw new Error(
"pm.iterationData.get() is not supported in Hoppscotch (Collection Runner feature)"
)
},
set: () => {
throw new Error(
"pm.iterationData.set() is not supported in Hoppscotch (Collection Runner feature)"
)
},
unset: () => {
throw new Error(
"pm.iterationData.unset() is not supported in Hoppscotch (Collection Runner feature)"
)
},
has: () => {
throw new Error(
"pm.iterationData.has() is not supported in Hoppscotch (Collection Runner feature)"
)
},
toObject: () => {
throw new Error(
"pm.iterationData.toObject() is not supported in Hoppscotch (Collection Runner feature)"
)
},
},
// Execution control (unsupported)
execution: {
setNextRequest: () => {
throw new Error(
"pm.execution.setNextRequest() is not supported in Hoppscotch (Collection Runner feature)"
)
},
},
}
}

View file

@ -1,2 +1,2 @@
export { defaultModules } from "./default"
export { pwPostRequestModule, pwPreRequestModule } from "./pw"
export { postRequestModule, preRequestModule } from "./scripting-modules"

View file

@ -0,0 +1,77 @@
import { CageModuleCtx, defineSandboxFn } from "faraday-cage/modules"
import type { EnvMethods, RequestProps, HoppNamespaceMethods } from "~/types"
import type { EnvAPIOptions } from "~/utils/shared"
/**
* Creates hopp namespace methods for the sandbox environment
* Includes environment operations with hopp-specific API
*/
export const createHoppNamespaceMethods = (
ctx: CageModuleCtx,
envMethods: EnvMethods,
requestProps: RequestProps
): HoppNamespaceMethods => {
return {
// `hopp` namespace environment methods
envDelete: defineSandboxFn(
ctx,
"envDelete",
function (key: unknown, options?: unknown) {
return envMethods.hopp.delete(key as string, options as EnvAPIOptions)
}
),
envReset: defineSandboxFn(
ctx,
"envReset",
function (key: unknown, options?: unknown) {
return envMethods.hopp.reset(key as string, options as EnvAPIOptions)
}
),
envGetInitialRaw: defineSandboxFn(
ctx,
"envGetInitialRaw",
function (key: unknown, options?: unknown) {
return envMethods.hopp.getInitialRaw(
key as string,
options as EnvAPIOptions
)
}
),
envSetInitial: defineSandboxFn(
ctx,
"envSetInitial",
function (key: unknown, value: unknown, options?: unknown) {
return envMethods.hopp.setInitial(
key as string,
value as string,
options as EnvAPIOptions
)
}
),
// Request getter props
getRequestProps: defineSandboxFn(ctx, "getRequestProps", function () {
return {
get url() {
return requestProps.url
},
get method() {
return requestProps.method
},
get params() {
return requestProps.params
},
get headers() {
return requestProps.headers
},
get body() {
return requestProps.body
},
get auth() {
return requestProps.auth
},
}
}),
}
}

View file

@ -0,0 +1,23 @@
import { HoppRESTRequest } from "@hoppscotch/data"
import { CageModuleCtx, defineSandboxFn } from "faraday-cage/modules"
import type { PmNamespaceMethods } from "~/types"
/**
* Creates pm (Postman compatibility) namespace methods for the sandbox environment
* Provides Postman-compatible APIs for request information
*/
export const createPmNamespaceMethods = (
ctx: CageModuleCtx,
config: { request: HoppRESTRequest }
): PmNamespaceMethods => {
return {
// `pm` namespace methods for Postman compatibility
pmInfoRequestName: defineSandboxFn(ctx, "pmInfoRequestName", () => {
return config.request.name
}),
pmInfoRequestId: defineSandboxFn(ctx, "pmInfoRequestId", () => {
return config.request.id
}),
}
}

View file

@ -0,0 +1,56 @@
import { CageModuleCtx, defineSandboxFn } from "faraday-cage/modules"
import type { EnvMethods, RequestProps, PwNamespaceMethods } from "~/types"
/**
* Creates pw namespace methods for the sandbox environment
* Includes environment operations and request variable management
*/
export const createPwNamespaceMethods = (
ctx: CageModuleCtx,
envMethods: EnvMethods,
requestProps: RequestProps
): PwNamespaceMethods => {
return {
// `pw` namespace environment methods
envGet: defineSandboxFn(ctx, "envGet", function (key: any, options: any) {
return envMethods.pw.get(key, options)
}),
envGetResolve: defineSandboxFn(
ctx,
"envGetResolve",
function (key: any, options: any) {
return envMethods.pw.getResolve(key, options)
}
),
envSet: defineSandboxFn(
ctx,
"envSet",
function (key: any, value: any, options: any) {
return envMethods.pw.set(key, value, options)
}
),
envUnset: defineSandboxFn(
ctx,
"envUnset",
function (key: any, options: any) {
return envMethods.pw.unset(key, options)
}
),
envResolve: defineSandboxFn(ctx, "envResolve", function (key: any) {
return envMethods.pw.resolve(key)
}),
// Request variable operations
getRequestVariable: defineSandboxFn(
ctx,
"getRequestVariable",
function (key: any) {
const reqVarEntry = requestProps.requestVariables.find(
(reqVar: any) => reqVar.key === key
)
return reqVarEntry ? reqVarEntry.value : null
}
),
}
}

View file

@ -1,268 +0,0 @@
import {
defineCageModule,
defineSandboxFn,
defineSandboxObject,
} from "faraday-cage/modules"
import { createExpectation, getSharedMethods } from "~/shared-utils"
import { TestDescriptor, TestResponse, TestResult } from "~/types"
import postRequestBootstrapCode from "../bootstrap-code/post-request?raw"
import preRequestBootstrapCode from "../bootstrap-code/pre-request?raw"
type PwPostRequestModuleConfig = {
envs: TestResult["envs"]
testRunStack: TestDescriptor[]
response: TestResponse
handleSandboxResults: ({
envs,
testRunStack,
}: {
envs: TestResult["envs"]
testRunStack: TestDescriptor[]
}) => void
}
type PwPreRequestModuleConfig = {
envs: TestResult["envs"]
handleSandboxResults: ({ envs }: { envs: TestResult["envs"] }) => void
}
type PwModuleType = "pre" | "post"
type PwModuleConfig = PwPreRequestModuleConfig | PwPostRequestModuleConfig
const createPwInputsObj = (
ctx: any,
methods: any,
type: PwModuleType,
config: PwModuleConfig
) => {
const baseInputs = {
envGet: defineSandboxFn(ctx, "get", (key) => methods.env.get(key)),
envGetResolve: defineSandboxFn(ctx, "getResolve", (key) =>
methods.env.getResolve(key)
),
envSet: defineSandboxFn(ctx, "set", (key, value) => {
return methods.env.set(key, value)
}),
envUnset: defineSandboxFn(ctx, "unset", (key) => methods.env.unset(key)),
envResolve: defineSandboxFn(ctx, "resolve", (key) =>
methods.env.resolve(key)
),
}
if (type === "post") {
const postConfig = config as PwPostRequestModuleConfig
return {
...baseInputs,
expectToBe: defineSandboxFn(ctx, "toBe", (expectVal, expectedVal) =>
createExpectation(expectVal, false, postConfig.testRunStack).toBe(
expectedVal
)
),
expectToBeLevel2xx: defineSandboxFn(ctx, "toBeLevel2xx", (expectVal) =>
createExpectation(
expectVal,
false,
postConfig.testRunStack
).toBeLevel2xx()
),
expectToBeLevel3xx: defineSandboxFn(ctx, "toBeLevel3xx", (expectVal) =>
createExpectation(
expectVal,
false,
postConfig.testRunStack
).toBeLevel3xx()
),
expectToBeLevel4xx: defineSandboxFn(ctx, "toBeLevel4xx", (expectVal) =>
createExpectation(
expectVal,
false,
postConfig.testRunStack
).toBeLevel4xx()
),
expectToBeLevel5xx: defineSandboxFn(ctx, "toBeLevel5xx", (expectVal) =>
createExpectation(
expectVal,
false,
postConfig.testRunStack
).toBeLevel5xx()
),
expectToBeType: defineSandboxFn(
ctx,
"toBeType",
(expectVal, expectedType, isExpectValDateInstance) => {
// Supplying `new Date()` in the script gets serialized in the sandbox context
// Parse the string back to a date instance
const resolvedExpectVal =
isExpectValDateInstance && typeof expectVal === "string"
? new Date(expectVal)
: expectVal
return createExpectation(
resolvedExpectVal,
false,
postConfig.testRunStack
).toBeType(expectedType)
}
),
expectToHaveLength: defineSandboxFn(
ctx,
"toHaveLength",
(expectVal, expectedLength) =>
createExpectation(
expectVal,
false,
postConfig.testRunStack
).toHaveLength(expectedLength)
),
expectToInclude: defineSandboxFn(ctx, "toInclude", (expectVal, needle) =>
createExpectation(expectVal, false, postConfig.testRunStack).toInclude(
needle
)
),
expectNotToBe: defineSandboxFn(ctx, "notToBe", (expectVal, expectedVal) =>
createExpectation(expectVal, false, postConfig.testRunStack).not.toBe(
expectedVal
)
),
expectNotToBeLevel2xx: defineSandboxFn(
ctx,
"notToBeLevel2xx",
(expectVal) =>
createExpectation(
expectVal,
false,
postConfig.testRunStack
).not.toBeLevel2xx()
),
expectNotToBeLevel3xx: defineSandboxFn(
ctx,
"notToBeLevel3xx",
(expectVal) =>
createExpectation(
expectVal,
false,
postConfig.testRunStack
).not.toBeLevel3xx()
),
expectNotToBeLevel4xx: defineSandboxFn(
ctx,
"notToBeLevel4xx",
(expectVal) =>
createExpectation(
expectVal,
false,
postConfig.testRunStack
).not.toBeLevel4xx()
),
expectNotToBeLevel5xx: defineSandboxFn(
ctx,
"notToBeLevel5xx",
(expectVal) =>
createExpectation(
expectVal,
false,
postConfig.testRunStack
).not.toBeLevel5xx()
),
expectNotToBeType: defineSandboxFn(
ctx,
"notToBeType",
(expectVal, expectedType, isExpectValDateInstance) => {
// Supplying `new Date()` in the script gets serialized in the sandbox context
// Parse the string back to a date instance
const resolvedExpectVal =
isExpectValDateInstance && typeof expectVal === "string"
? new Date(expectVal)
: expectVal
return createExpectation(
resolvedExpectVal,
false,
postConfig.testRunStack
).not.toBeType(expectedType)
}
),
expectNotToHaveLength: defineSandboxFn(
ctx,
"notToHaveLength",
(expectVal, expectedLength) =>
createExpectation(
expectVal,
false,
postConfig.testRunStack
).not.toHaveLength(expectedLength)
),
expectNotToInclude: defineSandboxFn(
ctx,
"notToInclude",
(expectVal, needle) =>
createExpectation(
expectVal,
false,
postConfig.testRunStack
).not.toInclude(needle)
),
preTest: defineSandboxFn(ctx, "preTest", (descriptor: any) => {
postConfig.testRunStack.push({
descriptor,
expectResults: [],
children: [],
})
}),
postTest: defineSandboxFn(ctx, "postTest", () => {
const child = postConfig.testRunStack.pop() as TestDescriptor
postConfig.testRunStack[
postConfig.testRunStack.length - 1
].children.push(child)
}),
getResponse: defineSandboxFn(
ctx,
"getResponse",
() => postConfig.response
),
}
}
return baseInputs
}
const createPwModule = (
type: PwModuleType,
bootstrapCode: string,
config: PwModuleConfig
) => {
return defineCageModule((ctx) => {
const funcHandle = ctx.scope.manage(ctx.vm.evalCode(bootstrapCode)).unwrap()
const { methods, updatedEnvs } = getSharedMethods(config.envs)
const inputsObj = defineSandboxObject(
ctx,
createPwInputsObj(ctx, methods, type, config)
)
ctx.vm.callFunction(funcHandle, ctx.vm.undefined, inputsObj)
ctx.afterScriptExecutionHooks.push(() => {
if (type === "post") {
const postConfig = config as PwPostRequestModuleConfig
postConfig.handleSandboxResults({
envs: updatedEnvs,
testRunStack: postConfig.testRunStack,
})
} else {
const preConfig = config as PwPreRequestModuleConfig
preConfig.handleSandboxResults({
envs: updatedEnvs,
})
}
})
})
}
export const pwPreRequestModule = (config: PwPreRequestModuleConfig) =>
createPwModule("pre", preRequestBootstrapCode, config)
export const pwPostRequestModule = (config: PwPostRequestModuleConfig) =>
createPwModule("post", postRequestBootstrapCode, config)

View file

@ -0,0 +1,220 @@
import { Cookie, HoppRESTRequest } from "@hoppscotch/data"
import {
CageModuleCtx,
defineCageModule,
defineSandboxFn,
defineSandboxObject,
} from "faraday-cage/modules"
import { TestDescriptor, TestResponse, TestResult } from "~/types"
import postRequestBootstrapCode from "../bootstrap-code/post-request?raw"
import preRequestBootstrapCode from "../bootstrap-code/pre-request?raw"
import { createBaseInputs } from "./utils/base-inputs"
import { createExpectationMethods } from "./utils/expectation-helpers"
import { createRequestSetterMethods } from "./utils/request-setters"
type PostRequestModuleConfig = {
envs: TestResult["envs"]
testRunStack: TestDescriptor[]
request: HoppRESTRequest
response: TestResponse
cookies: Cookie[] | null
handleSandboxResults: ({
envs,
testRunStack,
cookies,
}: {
envs: TestResult["envs"]
testRunStack: TestDescriptor[]
cookies: Cookie[] | null
}) => void
}
type PreRequestModuleConfig = {
envs: TestResult["envs"]
request: HoppRESTRequest
cookies: Cookie[] | null
handleSandboxResults: ({
envs,
request,
cookies,
}: {
envs: TestResult["envs"]
request: HoppRESTRequest
cookies: Cookie[] | null
}) => void
}
type ModuleType = "pre" | "post"
type ModuleConfig = PreRequestModuleConfig | PostRequestModuleConfig
/**
* Additional results that may be required for hook registration
*/
type HookRegistrationAdditionalResults = {
getUpdatedRequest: () => HoppRESTRequest
}
/**
* Helper function to register after-script execution hooks with proper typing
* Overload for pre-request hooks (requires additionalResults)
*/
function registerAfterScriptExecutionHook(
ctx: CageModuleCtx,
type: "pre",
config: PreRequestModuleConfig,
baseInputs: ReturnType<typeof createBaseInputs>,
additionalResults: HookRegistrationAdditionalResults
): void
/**
* Overload for post-request hooks (no additionalResults needed)
*/
function registerAfterScriptExecutionHook(
ctx: CageModuleCtx,
type: "post",
config: PostRequestModuleConfig,
baseInputs: ReturnType<typeof createBaseInputs>
): void
/**
* Implementation of the hook registration function
*/
function registerAfterScriptExecutionHook(
ctx: CageModuleCtx,
type: ModuleType,
config: ModuleConfig,
baseInputs: ReturnType<typeof createBaseInputs>,
additionalResults?: HookRegistrationAdditionalResults
) {
if (type === "pre") {
const preConfig = config as PreRequestModuleConfig
const getUpdatedRequest = additionalResults?.getUpdatedRequest
if (!getUpdatedRequest) {
throw new Error(
"getUpdatedRequest is required for pre-request hook registration"
)
}
ctx.afterScriptExecutionHooks.push(() => {
preConfig.handleSandboxResults({
envs: baseInputs.getUpdatedEnvs(),
request: getUpdatedRequest(),
cookies: baseInputs.getUpdatedCookies(),
})
})
} else if (type === "post") {
const postConfig = config as PostRequestModuleConfig
ctx.afterScriptExecutionHooks.push(() => {
postConfig.handleSandboxResults({
envs: baseInputs.getUpdatedEnvs(),
testRunStack: postConfig.testRunStack,
cookies: baseInputs.getUpdatedCookies(),
})
})
}
}
/**
* Creates input object for scripting modules with appropriate methods based on type
*/
const createScriptingInputsObj = (
ctx: CageModuleCtx,
type: ModuleType,
config: ModuleConfig
) => {
// Create base inputs shared across all namespaces
const baseInputs = createBaseInputs(ctx, {
envs: config.envs,
request: config.request,
cookies: config.cookies,
})
if (type === "pre") {
const preConfig = config as PreRequestModuleConfig
// Create request setter methods for pre-request scripts
const { methods: requestSetterMethods, getUpdatedRequest } =
createRequestSetterMethods(ctx, preConfig.request)
// Register hook with helper function
registerAfterScriptExecutionHook(ctx, "pre", preConfig, baseInputs, {
getUpdatedRequest,
})
return {
...baseInputs,
...requestSetterMethods,
}
}
if (type === "post") {
const postConfig = config as PostRequestModuleConfig
// Create expectation methods for post-request scripts
const expectationMethods = createExpectationMethods(
ctx,
postConfig.testRunStack
)
// Register hook with helper function
registerAfterScriptExecutionHook(ctx, "post", postConfig, baseInputs)
return {
...baseInputs,
...expectationMethods,
// Test management methods
preTest: defineSandboxFn(
ctx,
"preTest",
function preTest(descriptor: any) {
postConfig.testRunStack.push({
descriptor,
expectResults: [],
children: [],
})
}
),
postTest: defineSandboxFn(ctx, "postTest", function postTest() {
const child = postConfig.testRunStack.pop() as TestDescriptor
postConfig.testRunStack[
postConfig.testRunStack.length - 1
].children.push(child)
}),
getResponse: defineSandboxFn(ctx, "getResponse", function getResponse() {
return postConfig.response
}),
}
}
return baseInputs
}
/**
* Creates a scripting module for pre or post request execution
*/
const createScriptingModule = (
type: ModuleType,
bootstrapCode: string,
config: ModuleConfig
) => {
return defineCageModule((ctx) => {
const funcHandle = ctx.scope.manage(ctx.vm.evalCode(bootstrapCode)).unwrap()
const inputsObj = defineSandboxObject(
ctx,
createScriptingInputsObj(ctx, type, config)
)
ctx.vm.callFunction(funcHandle, ctx.vm.undefined, inputsObj)
})
}
export const preRequestModule = (config: PreRequestModuleConfig) =>
createScriptingModule("pre", preRequestBootstrapCode, config)
export const postRequestModule = (config: PostRequestModuleConfig) =>
createScriptingModule("post", postRequestBootstrapCode, config)

View file

@ -0,0 +1,80 @@
import { Cookie, HoppRESTRequest } from "@hoppscotch/data"
import { CageModuleCtx, defineSandboxFn } from "faraday-cage/modules"
import { TestResult, BaseInputs } from "~/types"
import {
getSharedCookieMethods,
getSharedEnvMethods,
getSharedRequestProps,
} from "~/utils/shared"
import { createHoppNamespaceMethods } from "../namespaces/hopp-namespace"
import { createPmNamespaceMethods } from "../namespaces/pm-namespace"
import { createPwNamespaceMethods } from "../namespaces/pw-namespace"
type BaseInputsConfig = {
envs: TestResult["envs"]
request: HoppRESTRequest
cookies: Cookie[] | null
}
/**
* Creates the base input object containing all shared methods across namespaces
*/
export const createBaseInputs = (
ctx: CageModuleCtx,
config: BaseInputsConfig
): BaseInputs => {
// Get environment methods - Applicable to both hopp and pw namespaces
const { methods: envMethods, updatedEnvs } = getSharedEnvMethods(
config.envs,
true
)
const { methods: cookieMethods, getUpdatedCookies } = getSharedCookieMethods(
config.cookies
)
// Get request properties - shared across pre and post request contexts
const requestProps = getSharedRequestProps(config.request)
// Cookie accessors
const cookieProps = {
cookieGet: defineSandboxFn(ctx, "cookieGet", (domain: any, name: any) => {
return cookieMethods.get(domain, name) || null
}),
cookieSet: defineSandboxFn(ctx, "cookieSet", (domain: any, cookie: any) => {
return cookieMethods.set(domain, cookie)
}),
cookieHas: defineSandboxFn(ctx, "cookieHas", (domain: any, name: any) => {
return cookieMethods.has(domain, name)
}),
cookieGetAll: defineSandboxFn(ctx, "cookieGetAll", (domain: any) => {
return cookieMethods.getAll(domain)
}),
cookieDelete: defineSandboxFn(
ctx,
"cookieDelete",
(domain: any, name: any) => {
return cookieMethods.delete(domain, name)
}
),
cookieClear: defineSandboxFn(ctx, "cookieClear", (domain: any) => {
return cookieMethods.clear(domain)
}),
}
// Combine all namespace methods
const pwMethods = createPwNamespaceMethods(ctx, envMethods, requestProps)
const hoppMethods = createHoppNamespaceMethods(ctx, envMethods, requestProps)
const pmMethods = createPmNamespaceMethods(ctx, config)
return {
...pwMethods,
...hoppMethods,
...pmMethods,
...cookieProps,
// Expose the updated state accessors
getUpdatedEnvs: () => updatedEnvs,
getUpdatedCookies,
}
}

View file

@ -0,0 +1,144 @@
import { CageModuleCtx, defineSandboxFn } from "faraday-cage/modules"
import { TestDescriptor, ExpectationMethods } from "~/types"
import { createExpectation } from "~/utils/shared"
/**
* Creates expectation methods for test assertions in post-request scripts
*/
export const createExpectationMethods = (
ctx: CageModuleCtx,
testRunStack: TestDescriptor[]
): ExpectationMethods => {
const createExpect = (expectVal: any) =>
createExpectation(expectVal, false, testRunStack)
return {
expectToBe: defineSandboxFn(
ctx,
"expectToBe",
(expectVal: any, expectedVal: any) => {
return createExpect(expectVal).toBe(expectedVal)
}
),
expectToBeLevel2xx: defineSandboxFn(
ctx,
"expectToBeLevel2xx",
(expectVal: any) => {
return createExpect(expectVal).toBeLevel2xx()
}
),
expectToBeLevel3xx: defineSandboxFn(
ctx,
"expectToBeLevel3xx",
(expectVal: any) => {
return createExpect(expectVal).toBeLevel3xx()
}
),
expectToBeLevel4xx: defineSandboxFn(
ctx,
"expectToBeLevel4xx",
(expectVal: any) => {
return createExpect(expectVal).toBeLevel4xx()
}
),
expectToBeLevel5xx: defineSandboxFn(
ctx,
"expectToBeLevel5xx",
(expectVal: any) => {
return createExpect(expectVal).toBeLevel5xx()
}
),
expectToBeType: defineSandboxFn(
ctx,
"expectToBeType",
(expectVal: any, expectedType: any, isDate: any) => {
const resolved =
isDate && typeof expectVal === "string"
? new Date(expectVal)
: expectVal
return createExpectation(resolved, false, testRunStack).toBeType(
expectedType
)
}
),
expectToHaveLength: defineSandboxFn(
ctx,
"expectToHaveLength",
(expectVal: any, expectedLength: any) => {
return createExpect(expectVal).toHaveLength(expectedLength)
}
),
expectToInclude: defineSandboxFn(
ctx,
"expectToInclude",
(expectVal: any, needle: any) => {
return createExpect(expectVal).toInclude(needle)
}
),
// Negative expectations
expectNotToBe: defineSandboxFn(
ctx,
"expectNotToBe",
(expectVal: any, expectedVal: any) => {
return createExpect(expectVal).not.toBe(expectedVal)
}
),
expectNotToBeLevel2xx: defineSandboxFn(
ctx,
"expectNotToBeLevel2xx",
(expectVal: any) => {
return createExpect(expectVal).not.toBeLevel2xx()
}
),
expectNotToBeLevel3xx: defineSandboxFn(
ctx,
"expectNotToBeLevel3xx",
(expectVal: any) => {
return createExpect(expectVal).not.toBeLevel3xx()
}
),
expectNotToBeLevel4xx: defineSandboxFn(
ctx,
"expectNotToBeLevel4xx",
(expectVal: any) => {
return createExpect(expectVal).not.toBeLevel4xx()
}
),
expectNotToBeLevel5xx: defineSandboxFn(
ctx,
"expectNotToBeLevel5xx",
(expectVal: any) => {
return createExpect(expectVal).not.toBeLevel5xx()
}
),
expectNotToBeType: defineSandboxFn(
ctx,
"expectNotToBeType",
(expectVal: any, expectedType: any, isDate: any) => {
const resolved =
isDate && typeof expectVal === "string"
? new Date(expectVal)
: expectVal
return createExpectation(resolved, false, testRunStack).not.toBeType(
expectedType
)
}
),
expectNotToHaveLength: defineSandboxFn(
ctx,
"expectNotToHaveLength",
(expectVal: any, expectedLength: any) => {
return createExpect(expectVal).not.toHaveLength(expectedLength)
}
),
expectNotToInclude: defineSandboxFn(
ctx,
"expectNotToInclude",
(expectVal: any, needle: any) => {
return createExpect(expectVal).not.toInclude(needle)
}
),
}
}

View file

@ -0,0 +1,97 @@
import {
HoppRESTAuth,
HoppRESTHeaders,
HoppRESTParams,
HoppRESTReqBody,
HoppRESTRequest,
} from "@hoppscotch/data"
import { CageModuleCtx, defineSandboxFn } from "faraday-cage/modules"
import { getRequestSetterMethods } from "~/utils/pre-request"
import { RequestSetterMethodsResult } from "~/types"
/**
* Creates request setter methods for pre-request scripts
* These methods allow modification of request properties before execution
*/
export const createRequestSetterMethods = (
ctx: CageModuleCtx,
request: HoppRESTRequest
): RequestSetterMethodsResult => {
const { methods: requestMethods, updatedRequest } =
getRequestSetterMethods(request)
const setterMethods = {
// Request setter methods
setRequestUrl: defineSandboxFn(ctx, "setRequestUrl", (url: any) => {
requestMethods.setUrl(url as string)
}),
setRequestMethod: defineSandboxFn(
ctx,
"setRequestMethod",
(method: any) => {
requestMethods.setMethod(method as string)
}
),
setRequestHeader: defineSandboxFn(
ctx,
"setRequestHeader",
(name: any, value: any) => {
requestMethods.setHeader(name as string, value as string)
}
),
setRequestHeaders: defineSandboxFn(
ctx,
"setRequestHeaders",
(headers: any) => {
requestMethods.setHeaders(headers as HoppRESTHeaders)
}
),
removeRequestHeader: defineSandboxFn(
ctx,
"removeRequestHeader",
(key: any) => {
requestMethods.removeHeader(key as string)
}
),
setRequestParam: defineSandboxFn(
ctx,
"setRequestParam",
(name: any, value: any) => {
requestMethods.setParam(name as string, value as string)
}
),
setRequestParams: defineSandboxFn(
ctx,
"setRequestParams",
(params: any) => {
requestMethods.setParams(params as HoppRESTParams)
}
),
removeRequestParam: defineSandboxFn(
ctx,
"removeRequestParam",
(key: any) => {
requestMethods.removeParam(key as string)
}
),
setRequestBody: defineSandboxFn(ctx, "setRequestBody", (body: any) => {
requestMethods.setBody(body as HoppRESTReqBody)
}),
setRequestAuth: defineSandboxFn(ctx, "setRequestAuth", (auth: any) => {
requestMethods.setAuth(auth as HoppRESTAuth)
}),
setRequestVariable: defineSandboxFn(
ctx,
"setRequestVariable",
(key: any, value: any) => {
requestMethods.setRequestVariable(key as string, value as string)
}
),
}
return {
methods: setterMethods,
getUpdatedRequest: () => updatedRequest,
}
}

View file

@ -1,29 +1,38 @@
import { Cookie, HoppRESTRequest } from "@hoppscotch/data"
import { FaradayCage } from "faraday-cage"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/lib/TaskEither"
import { cloneDeep } from "lodash"
import { defaultModules, pwPreRequestModule } from "~/cage-modules"
import { TestResult } from "~/types"
import { defaultModules, preRequestModule } from "~/cage-modules"
import { SandboxPreRequestResult, TestResult } from "~/types"
export const runPreRequestScriptWithFaradayCage = (
preRequestScript: string,
envs: TestResult["envs"]
): TE.TaskEither<string, TestResult["envs"]> => {
envs: TestResult["envs"],
request: HoppRESTRequest,
cookies: Cookie[] | null
): TE.TaskEither<string, SandboxPreRequestResult> => {
return pipe(
TE.tryCatch(
async (): Promise<TestResult["envs"]> => {
async (): Promise<SandboxPreRequestResult> => {
let finalEnvs = envs
let finalRequest = request
let finalCookies = cookies
const cage = await FaradayCage.create()
const result = await cage.runCode(preRequestScript, [
...defaultModules(),
pwPreRequestModule({
preRequestModule({
envs: cloneDeep(envs),
handleSandboxResults: ({ envs }) => {
request: cloneDeep(request),
cookies: cookies ? cloneDeep(cookies) : null,
handleSandboxResults: ({ envs, request, cookies }) => {
finalEnvs = envs
finalRequest = request
finalCookies = cookies
},
}),
])
@ -32,7 +41,11 @@ export const runPreRequestScriptWithFaradayCage = (
throw result.err
}
return finalEnvs
return {
updatedEnvs: finalEnvs,
updatedRequest: finalRequest,
updatedCookies: finalCookies,
}
},
(error) => {
if (error !== null && typeof error === "object" && "message" in error) {

View file

@ -1,14 +1,28 @@
import * as TE from "fp-ts/lib/TaskEither"
import { TestResult } from "~/types"
import { RunPreRequestScriptOptions, SandboxPreRequestResult } from "~/types"
import { runPreRequestScriptWithFaradayCage } from "./experimental"
import { runPreRequestScriptWithIsolatedVm } from "./legacy"
export const runPreRequestScript = (
preRequestScript: string,
envs: TestResult["envs"],
experimentalScriptingSandbox = true
): TE.TaskEither<string, TestResult["envs"]> =>
experimentalScriptingSandbox
? runPreRequestScriptWithFaradayCage(preRequestScript, envs)
: runPreRequestScriptWithIsolatedVm(preRequestScript, envs)
options: RunPreRequestScriptOptions
): TE.TaskEither<string, SandboxPreRequestResult> => {
const { envs, experimentalScriptingSandbox = true } = options
if (experimentalScriptingSandbox) {
const { request, cookies } = options as Extract<
RunPreRequestScriptOptions,
{ experimentalScriptingSandbox: true }
>
return runPreRequestScriptWithFaradayCage(
preRequestScript,
envs,
request,
cookies
)
}
return runPreRequestScriptWithIsolatedVm(preRequestScript, envs)
}

View file

@ -3,8 +3,8 @@ import * as TE from "fp-ts/lib/TaskEither"
import type ivmT from "isolated-vm"
import { createRequire } from "module"
import { getPreRequestScriptMethods } from "~/shared-utils"
import { TestResult } from "~/types"
import { getPreRequestScriptMethods } from "~/utils/shared"
import { SandboxPreRequestResult, TestResult } from "~/types"
import { getSerializedAPIMethods } from "../utils"
const nodeRequire = createRequire(import.meta.url)
@ -13,7 +13,7 @@ const ivm = nodeRequire("isolated-vm")
export const runPreRequestScriptWithIsolatedVm = (
preRequestScript: string,
envs: TestResult["envs"]
): TE.TaskEither<string, TestResult["envs"]> => {
): TE.TaskEither<string, SandboxPreRequestResult> => {
return pipe(
TE.tryCatch(
async () => {
@ -59,7 +59,7 @@ export const runPreRequestScriptWithIsolatedVm = (
const script = await isolate.compileScript(finalScript)
// Run the pre-request script in the provided context
await script.run(context)
return updatedEnvs
return { updatedEnvs, updatedCookies: null }
},
(reason) => reason
),

View file

@ -1,14 +1,16 @@
import { HoppRESTRequest } from "@hoppscotch/data"
import { FaradayCage } from "faraday-cage"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { cloneDeep } from "lodash"
import { defaultModules, pwPostRequestModule } from "~/cage-modules"
import { defaultModules, postRequestModule } from "~/cage-modules"
import { TestDescriptor, TestResponse, TestResult } from "~/types"
export const runTestScriptWithFaradayCage = (
export const runPostRequestScriptWithFaradayCage = (
testScript: string,
envs: TestResult["envs"],
request: HoppRESTRequest,
response: TestResponse
): TE.TaskEither<string, TestResult> => {
return pipe(
@ -26,10 +28,13 @@ export const runTestScriptWithFaradayCage = (
const result = await cage.runCode(testScript, [
...defaultModules(),
pwPostRequestModule({
postRequestModule({
envs: cloneDeep(envs),
testRunStack: cloneDeep(testRunStack),
response,
request: cloneDeep(request),
response: cloneDeep(response),
// TODO: Post type update, accommodate for cookies although platform support is limited
cookies: null,
handleSandboxResults: ({ envs, testRunStack }) => {
finalEnvs = envs
finalTestResults = testRunStack

View file

@ -1,24 +1,39 @@
import * as E from "fp-ts/Either"
import * as TE from "fp-ts/TaskEither"
import { preventCyclicObjects } from "~/shared-utils"
import { TestResponse, TestResult } from "~/types"
import { runTestScriptWithFaradayCage } from "./experimental"
import { runTestScriptWithIsolatedVm } from "./legacy"
import { RunPostRequestScriptOptions, TestResponse, TestResult } from "~/types"
import { preventCyclicObjects } from "~/utils/shared"
import { runPostRequestScriptWithFaradayCage } from "./experimental"
import { runPostRequestScriptWithIsolatedVm } from "./legacy"
// Future TODO: Update return type to be based on `SandboxTestResult` (unified with the web implementation)
// No involvement of cookies in the CLI context currently
export const runTestScript = (
testScript: string,
envs: TestResult["envs"],
response: TestResponse,
experimentalScriptingSandbox = true
options: RunPostRequestScriptOptions
): TE.TaskEither<string, TestResult> => {
const responseObjHandle = preventCyclicObjects<TestResponse>(response)
const responseObjHandle = preventCyclicObjects<TestResponse>(options.response)
if (E.isLeft(responseObjHandle)) {
return TE.left(`Response marshalling failed: ${responseObjHandle.left}`)
}
return experimentalScriptingSandbox
? runTestScriptWithFaradayCage(testScript, envs, responseObjHandle.right)
: runTestScriptWithIsolatedVm(testScript, envs, responseObjHandle.right)
const resolvedResponse = responseObjHandle.right
const { envs, experimentalScriptingSandbox = true } = options
if (experimentalScriptingSandbox) {
const { request } = options as Extract<
RunPostRequestScriptOptions,
{ experimentalScriptingSandbox: true }
>
return runPostRequestScriptWithFaradayCage(
testScript,
envs,
request,
resolvedResponse
)
}
return runPostRequestScriptWithIsolatedVm(testScript, envs, resolvedResponse)
}

View file

@ -4,11 +4,11 @@ import { pipe } from "fp-ts/function"
import type ivmT from "isolated-vm"
import { createRequire } from "module"
import { TestResponse, TestResult } from "~/types"
import {
getTestRunnerScriptMethods,
preventCyclicObjects,
} from "~/shared-utils"
import { TestResponse, TestResult } from "~/types"
} from "~/utils/shared"
import { getSerializedAPIMethods } from "../utils"
const nodeRequire = createRequire(import.meta.url)
@ -176,7 +176,7 @@ const executeScriptInContext = (
})
}
export const runTestScriptWithIsolatedVm = (
export const runPostRequestScriptWithIsolatedVm = (
testScript: string,
envs: TestResult["envs"],
response: TestResponse

View file

@ -1,4 +1,10 @@
import { ConsoleEntry } from "faraday-cage/modules"
import { Cookie, HoppRESTRequest } from "@hoppscotch/data"
import { ConsoleEntry, defineSandboxFn } from "faraday-cage/modules"
import type { EnvAPIOptions } from "~/utils/shared"
// Infer the return type of defineSandboxFn from faraday-cage
type SandboxFunction = ReturnType<typeof defineSandboxFn>
/**
* The response object structure exposed to the test script
@ -6,8 +12,16 @@ import { ConsoleEntry } from "faraday-cage/modules"
export type TestResponse = {
/** Status Code of the response */
status: number
/** Status text of the response (e.g., "OK", "Not Found", "Internal Server Error") */
statusText: string
/** Time taken for the request to complete in milliseconds */
responseTime: number
/** List of headers returned */
headers: { key: string; value: string }[]
/**
* Body of the response, this will be the JSON object if it is a JSON content type, else body string
*/
@ -68,11 +82,14 @@ export type SelectedEnvItem = TestResult["envs"]["selected"][number]
export type SandboxTestResult = TestResult & { tests: TestDescriptor } & {
consoleEntries?: ConsoleEntry[]
updatedCookies: Cookie[] | null
}
export type SandboxPreRequestResult = {
envs: TestResult["envs"]
updatedEnvs: TestResult["envs"]
consoleEntries?: ConsoleEntry[]
updatedRequest?: HoppRESTRequest
updatedCookies: Cookie[] | null
}
export interface Expectation {
@ -86,3 +103,156 @@ export interface Expectation {
toInclude(needle: any): void
readonly not: Expectation
}
export type RunPreRequestScriptOptions =
| {
envs: TestResult["envs"]
request: HoppRESTRequest
cookies: Cookie[] | null // Exclusive to the Desktop App
experimentalScriptingSandbox: true
}
| {
envs: TestResult["envs"]
experimentalScriptingSandbox?: false
}
export type RunPostRequestScriptOptions =
| {
envs: TestResult["envs"]
request: HoppRESTRequest
response: TestResponse
cookies: Cookie[] | null // Exclusive to the Desktop App
experimentalScriptingSandbox: true
}
| {
envs: TestResult["envs"]
response: TestResponse
experimentalScriptingSandbox?: false
}
/**
* Request properties structure exposed to sandbox
*/
export type RequestProps = {
readonly url: string
readonly method: string
readonly params: HoppRESTRequest["params"]
readonly headers: HoppRESTRequest["headers"]
readonly body: HoppRESTRequest["body"]
readonly auth: HoppRESTRequest["auth"]
readonly requestVariables: HoppRESTRequest["requestVariables"]
}
/**
* Environment methods structure returned by getSharedEnvMethods
*/
export type EnvMethods = {
pw: {
get: (key: string, options?: EnvAPIOptions) => string | null | undefined
getResolve: (
key: string,
options?: EnvAPIOptions
) => string | null | undefined
set: (key: string, value: string, options?: EnvAPIOptions) => void
unset: (key: string, options?: EnvAPIOptions) => void
resolve: (key: string) => string
}
hopp: {
set: (key: string, value: string, options?: EnvAPIOptions) => void
delete: (key: string, options?: EnvAPIOptions) => void
reset: (key: string, options?: EnvAPIOptions) => void
getInitialRaw: (key: string, options?: EnvAPIOptions) => string | null
setInitial: (key: string, value: string, options?: EnvAPIOptions) => void
}
}
/**
* Return type for createHoppNamespaceMethods function
*/
export interface HoppNamespaceMethods {
envDelete: SandboxFunction
envReset: SandboxFunction
envGetInitialRaw: SandboxFunction
envSetInitial: SandboxFunction
getRequestProps: SandboxFunction
}
/**
* Return type for createPwNamespaceMethods function
*/
export interface PwNamespaceMethods {
envGet: SandboxFunction
envGetResolve: SandboxFunction
envSet: SandboxFunction
envUnset: SandboxFunction
envResolve: SandboxFunction
getRequestVariable: SandboxFunction
}
/**
* Return type for createPmNamespaceMethods function
*/
export interface PmNamespaceMethods {
pmInfoRequestName: SandboxFunction
pmInfoRequestId: SandboxFunction
}
/**
* Return type for createExpectationMethods function
*/
export interface ExpectationMethods {
expectToBe: SandboxFunction
expectToBeLevel2xx: SandboxFunction
expectToBeLevel3xx: SandboxFunction
expectToBeLevel4xx: SandboxFunction
expectToBeLevel5xx: SandboxFunction
expectToBeType: SandboxFunction
expectToHaveLength: SandboxFunction
expectToInclude: SandboxFunction
expectNotToBe: SandboxFunction
expectNotToBeLevel2xx: SandboxFunction
expectNotToBeLevel3xx: SandboxFunction
expectNotToBeLevel4xx: SandboxFunction
expectNotToBeLevel5xx: SandboxFunction
expectNotToBeType: SandboxFunction
expectNotToHaveLength: SandboxFunction
expectNotToInclude: SandboxFunction
}
/**
* Return type for createRequestSetterMethods function
*/
export interface RequestSetterMethodsResult {
methods: {
setRequestUrl: SandboxFunction
setRequestMethod: SandboxFunction
setRequestHeader: SandboxFunction
setRequestHeaders: SandboxFunction
setRequestParam: SandboxFunction
setRequestParams: SandboxFunction
removeRequestHeader: SandboxFunction
removeRequestParam: SandboxFunction
setRequestBody: SandboxFunction
setRequestAuth: SandboxFunction
setRequestVariable: SandboxFunction
}
getUpdatedRequest: () => HoppRESTRequest
}
/**
* Return type for createBaseInputs function
*/
export interface BaseInputs
extends PwNamespaceMethods,
HoppNamespaceMethods,
PmNamespaceMethods {
cookieGet: SandboxFunction
cookieSet: SandboxFunction
cookieHas: SandboxFunction
cookieGetAll: SandboxFunction
cookieDelete: SandboxFunction
cookieClear: SandboxFunction
getUpdatedEnvs: () => any
getUpdatedCookies: () => Cookie[] | null
[key: string]: any
}

View file

@ -0,0 +1,157 @@
import {
HoppRESTAuth,
HoppRESTHeaders,
HoppRESTParams,
HoppRESTReqBody,
HoppRESTRequest,
} from "@hoppscotch/data"
import { cloneDeep } from "lodash"
export const getRequestSetterMethods = (request: HoppRESTRequest) => {
// Clone to allow safe mutations internally
const updatedRequest = <HoppRESTRequest>cloneDeep(request)
// Mutation methods
const setUrl = (url: string) => {
updatedRequest.endpoint = url
}
const setMethod = (method: string) => {
updatedRequest.method = method.toUpperCase()
}
const setHeader = (name: string, value: string) => {
const headers = [...updatedRequest.headers]
const headerIndex = headers.findIndex(
(h) => h.key.toLowerCase() === name.toLowerCase()
)
if (headerIndex >= 0) {
headers[headerIndex].value = value
} else {
headers.push({ key: name, value, active: true, description: "" })
}
updatedRequest.headers = headers
}
const setHeaders = (headers: HoppRESTHeaders) => {
const parseResult = HoppRESTHeaders.safeParse(headers)
if (!parseResult.success) {
throw new Error("Invalid headers object")
}
updatedRequest.headers = parseResult.data
}
const removeHeader = (key: string) => {
updatedRequest.headers = updatedRequest.headers.filter((h) => h.key !== key)
}
const setParam = (name: string, value: string) => {
const params = [...updatedRequest.params]
const paramIndex = params.findIndex(
(p) => p.key.toLowerCase() === name.toLowerCase()
)
if (paramIndex >= 0) {
params[paramIndex].value = value
} else {
params.push({ key: name, value, active: true, description: "" })
}
updatedRequest.params = params
}
const setParams = (params: HoppRESTParams) => {
const parseResult = HoppRESTParams.safeParse(params)
if (!parseResult.success) {
throw new Error("Invalid params object")
}
updatedRequest.params = parseResult.data
}
const removeParam = (key: string) => {
updatedRequest.params = updatedRequest.params.filter((h) => h.key !== key)
}
const setBody = (newBody: Partial<HoppRESTReqBody>) => {
// Runtime validations given the input is user controlled
if (
typeof newBody !== "object" ||
newBody === null ||
Array.isArray(newBody) ||
Object.keys(newBody).length === 0
) {
throw new Error(
"Invalid body object. Expected a non-empty object with valid body properties."
)
}
const mergedBody = { ...updatedRequest.body, ...newBody }
const parseResult = HoppRESTReqBody.safeParse(mergedBody)
if (!parseResult.success) {
throw new Error(
"Invalid body object. Expected a non-empty object with valid body properties."
)
}
updatedRequest.body = { ...parseResult.data }
}
const setAuth = (newAuth: HoppRESTAuth) => {
// Runtime validations given the input is user controlled
if (
typeof newAuth !== "object" ||
newAuth === null ||
Array.isArray(newAuth) ||
Object.keys(newAuth).length === 0
) {
throw new Error("Invalid auth object")
}
const mergedAuth = { ...updatedRequest.auth, ...newAuth }
const parseResult = HoppRESTAuth.safeParse(mergedAuth)
if (!parseResult.success) {
throw new Error("Invalid auth object")
}
updatedRequest.auth = { ...parseResult.data }
}
const setRequestVariable = (key: string, value: string) => {
const reqVarIndex = updatedRequest.requestVariables.findIndex(
(reqVar) => reqVar.key === key
)
if (reqVarIndex >= 0) {
updatedRequest.requestVariables[reqVarIndex].value = value
} else {
updatedRequest.requestVariables.push({ key, value, active: true })
}
}
// Returns setter methods under the `request` namespace
return {
methods: {
setUrl,
setMethod,
setHeader,
setHeaders,
removeHeader,
setParam,
setParams,
removeParam,
setBody,
setAuth,
setRequestVariable,
},
updatedRequest,
}
}

View file

@ -1,4 +1,9 @@
import { parseTemplateStringE } from "@hoppscotch/data"
import {
Cookie,
CookieSchema,
HoppRESTRequest,
parseTemplateStringE,
} from "@hoppscotch/data"
import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option"
import { pipe } from "fp-ts/lib/function"
@ -10,9 +15,31 @@ import {
SelectedEnvItem,
TestDescriptor,
TestResult,
} from "./types"
} from "../types"
export type EnvSource = "active" | "global" | "all"
export type EnvAPIOptions = {
fallbackToNull?: boolean
source: EnvSource
}
const getEnv = (
envName: string,
envs: TestResult["envs"],
options = { source: "all" }
) => {
if (options.source === "active") {
return O.fromNullable(
envs.selected.find((x: SelectedEnvItem) => x.key === envName)
)
}
if (options.source === "global") {
return O.fromNullable(
envs.global.find((x: GlobalEnvItem) => x.key === envName)
)
}
const getEnv = (envName: string, envs: TestResult["envs"]) => {
return O.fromNullable(
envs.selected.find((x: SelectedEnvItem) => x.key === envName) ??
envs.global.find((x: GlobalEnvItem) => x.key === envName)
@ -31,29 +58,45 @@ const findEnvIndex = (
const setEnv = (
envName: string,
envValue: string,
envs: TestResult["envs"]
envs: TestResult["envs"],
options: { setInitialValue?: boolean; source: EnvSource } = {
setInitialValue: false,
source: "all",
}
): TestResult["envs"] => {
const { global, selected } = envs
const indexInSelected = findEnvIndex(envName, selected)
const indexInGlobal = findEnvIndex(envName, global)
if (indexInSelected >= 0) {
if (["all", "active"].includes(options.source) && indexInSelected >= 0) {
const selectedEnv = selected[indexInSelected]
if ("currentValue" in selectedEnv) {
selectedEnv.currentValue = envValue
}
} else if (indexInGlobal >= 0) {
if ("currentValue" in global[indexInGlobal])
(global[indexInGlobal] as { currentValue: string }).currentValue =
envValue
} else {
const targetProperty = options.setInitialValue
? "initialValue"
: "currentValue"
selectedEnv[targetProperty] = envValue
} else if (["all", "global"].includes(options.source) && indexInGlobal >= 0) {
const globalEnv = global[indexInGlobal]
const targetProperty = options.setInitialValue
? "initialValue"
: "currentValue"
globalEnv[targetProperty] = envValue
} else if (["all", "active"].includes(options.source)) {
selected.push({
key: envName,
currentValue: envValue,
initialValue: envValue,
secret: false,
})
} else if (["all", "global"].includes(options.source)) {
global.push({
key: envName,
currentValue: envValue,
initialValue: envValue,
secret: false,
})
}
return {
@ -64,16 +107,17 @@ const setEnv = (
const unsetEnv = (
envName: string,
envs: TestResult["envs"]
envs: TestResult["envs"],
options = { source: "all" }
): TestResult["envs"] => {
const { global, selected } = envs
const indexInSelected = findEnvIndex(envName, selected)
const indexInGlobal = findEnvIndex(envName, global)
if (indexInSelected >= 0) {
if (["all", "active"].includes(options.source) && indexInSelected >= 0) {
selected.splice(indexInSelected, 1)
} else if (indexInGlobal >= 0) {
} else if (["all", "global"].includes(options.source) && indexInGlobal >= 0) {
global.splice(indexInGlobal, 1)
}
@ -83,19 +127,74 @@ const unsetEnv = (
}
}
// Compiles shared scripting API methods for use in both pre and post request scripts
export const getSharedMethods = (envs: TestResult["envs"]) => {
/**
* Compiles shared scripting API methods (scoped to environments) for use in both pre and post request scripts
* Experimental sandbox version - Returns methods organized by namespace (`pw` and `hopp`)
*/
export function getSharedEnvMethods(
envs: TestResult["envs"],
isHoppNamespace: true
): {
methods: {
pw: {
get: (key: string, options?: EnvAPIOptions) => string | null | undefined
getResolve: (
key: string,
options?: EnvAPIOptions
) => string | null | undefined
set: (key: string, value: string, options?: EnvAPIOptions) => void
unset: (key: string, options?: EnvAPIOptions) => void
resolve: (key: string) => string
}
hopp: {
set: (key: string, value: string, options?: EnvAPIOptions) => void
delete: (key: string, options?: EnvAPIOptions) => void
reset: (key: string, options?: EnvAPIOptions) => void
getInitialRaw: (key: string, options?: EnvAPIOptions) => string | null
setInitial: (key: string, value: string, options?: EnvAPIOptions) => void
}
}
updatedEnvs: TestResult["envs"]
}
/**
* Legacy sandbox version - Returns flat methods for `pw` namespace only
*/
export function getSharedEnvMethods(
envs: TestResult["envs"],
isHoppNamespace?: false
): {
methods: {
get: (key: string, options?: EnvAPIOptions) => string | null | undefined
getResolve: (
key: string,
options?: EnvAPIOptions
) => string | null | undefined
set: (key: string, value: string, options?: EnvAPIOptions) => void
unset: (key: string, options?: EnvAPIOptions) => void
resolve: (key: string) => string
}
updatedEnvs: TestResult["envs"]
}
export function getSharedEnvMethods(
envs: TestResult["envs"],
isHoppNamespace = false
): unknown {
let updatedEnvs = envs
const envGetFn = (key: any) => {
const envGetFn = (
key: any,
options: EnvAPIOptions = { fallbackToNull: false, source: "all" }
) => {
if (typeof key !== "string") {
throw new Error("Expected key to be a string")
}
const result = pipe(
getEnv(key, updatedEnvs),
getEnv(key, updatedEnvs, options),
O.fold(
() => undefined,
() => (options.fallbackToNull ? null : undefined),
(env) => String(env.currentValue)
)
)
@ -103,33 +202,45 @@ export const getSharedMethods = (envs: TestResult["envs"]) => {
return result
}
const envGetResolveFn = (key: any) => {
const envGetResolveFn = (
key: any,
options: EnvAPIOptions = { fallbackToNull: false, source: "all" }
) => {
if (typeof key !== "string") {
throw new Error("Expected key to be a string")
}
const shouldIncludeSelected = ["all", "active"].includes(options.source)
const shouldIncludeGlobal = ["all", "global"].includes(options.source)
const envVars = [
...(shouldIncludeSelected ? updatedEnvs.selected : []),
...(shouldIncludeGlobal ? updatedEnvs.global : []),
]
const result = pipe(
getEnv(key, updatedEnvs),
getEnv(key, updatedEnvs, options),
E.fromOption(() => "INVALID_KEY" as const),
E.map((e) =>
pipe(
parseTemplateStringE(e.currentValue, [
...updatedEnvs.selected,
...updatedEnvs.global,
]), // If the recursive resolution failed, return the unresolved value
parseTemplateStringE(e.currentValue, envVars), // If the recursive resolution failed, return the unresolved value
E.getOrElse(() => e.currentValue)
)
),
E.map((x) => String(x)),
E.getOrElseW(() => undefined)
E.getOrElseW(() => (options.fallbackToNull ? null : undefined))
)
return result
}
const envSetFn = (key: any, value: any) => {
const envSetFn = (
key: any,
value: any,
options: EnvAPIOptions = { source: "all" }
) => {
if (typeof key !== "string") {
throw new Error("Expected key to be a string")
}
@ -138,17 +249,17 @@ export const getSharedMethods = (envs: TestResult["envs"]) => {
throw new Error("Expected value to be a string")
}
updatedEnvs = setEnv(key, value, updatedEnvs)
updatedEnvs = setEnv(key, value, updatedEnvs, options)
return undefined
}
const envUnsetFn = (key: any) => {
const envUnsetFn = (key: any, options: EnvAPIOptions = { source: "all" }) => {
if (typeof key !== "string") {
throw new Error("Expected key to be a string")
}
updatedEnvs = unsetEnv(key, updatedEnvs)
updatedEnvs = unsetEnv(key, updatedEnvs, options)
return undefined
}
@ -169,20 +280,213 @@ export const getSharedMethods = (envs: TestResult["envs"]) => {
return String(result)
}
// Methods exclusive to the `hopp` namespace
const envResetFn = (
key: string,
options: EnvAPIOptions = { source: "all" }
) => {
if (typeof key !== "string") {
throw new Error("Expected key to be a string")
}
// Always read from the live, mutated state. `updatedEnvs` is reassigned by setters,
// while `envs` may point to an older object (stale snapshot) even if arrays were mutated.
// Using `updatedEnvs` here avoids subtle drift if future changes replace arrays immutably.
const { global, selected } = updatedEnvs
const indexInSelected = findEnvIndex(key, selected)
const indexInGlobal = findEnvIndex(key, global)
if (["all", "active"].includes(options.source) && indexInSelected >= 0) {
const selectedEnv = selected[indexInSelected]
if ("currentValue" in selectedEnv) {
selectedEnv.currentValue = selectedEnv.initialValue
}
} else if (
["all", "global"].includes(options.source) &&
indexInGlobal >= 0
) {
if ("currentValue" in global[indexInGlobal]) {
global[indexInGlobal].currentValue = global[indexInGlobal].initialValue
}
}
}
const envGetInitialRawFn = (
key: any,
options: EnvAPIOptions = { source: "all" }
) => {
if (typeof key !== "string") {
throw new Error("Expected key to be a string")
}
const result = pipe(
getEnv(key, updatedEnvs, options),
O.fold(
() => undefined,
(env) => String(env.initialValue)
)
)
return result ?? null
}
const envSetInitialFn = (
key: string,
value: string,
options: EnvAPIOptions = { source: "all" }
) => {
if (typeof key !== "string") {
throw new Error("Expected key to be a string")
}
if (typeof value !== "string") {
throw new Error("Expected value to be a string")
}
updatedEnvs = setEnv(key, value, updatedEnvs, {
setInitialValue: true,
source: options.source,
})
return undefined
}
// Experimental scripting sandbox (Both `pw` and `hopp` namespaces)
if (isHoppNamespace) {
return {
methods: {
pw: {
get: envGetFn,
getResolve: envGetResolveFn,
set: envSetFn,
unset: envUnsetFn,
resolve: envResolveFn,
},
hopp: {
set: envSetFn,
delete: envUnsetFn,
reset: envResetFn,
getInitialRaw: envGetInitialRawFn,
setInitial: envSetInitialFn,
},
},
updatedEnvs,
}
}
// Legacy scripting sandbox (Only `pw` namespace)
return {
methods: {
env: {
get: envGetFn,
getResolve: envGetResolveFn,
set: envSetFn,
unset: envUnsetFn,
resolve: envResolveFn,
},
get: envGetFn,
getResolve: envGetResolveFn,
set: envSetFn,
unset: envUnsetFn,
resolve: envResolveFn,
},
updatedEnvs,
}
}
export const getSharedCookieMethods = (cookies: Cookie[] | null) => {
// Incoming `cookies` specified as `null` indicates unsupported platform
const cookiesSupported = cookies !== null
let updatedCookies: Cookie[] = cookies ?? []
const throwIfCookiesUnsupported = () => {
if (cookies === null) {
throw new Error(
"Cookies are not supported in the current platform and are exclusive to the Desktop App."
)
}
}
const cookieGetFn = (domain: any, name: any): Cookie | null => {
throwIfCookiesUnsupported()
if (typeof domain !== "string" || typeof name !== "string") {
throw new Error("Expected domain and cookieName to be strings")
}
return (
updatedCookies.find((c) => c.domain === domain && c.name === name) ?? null
)
}
const cookieSetFn = (domain: string, cookie: Cookie): void => {
throwIfCookiesUnsupported()
if (typeof domain !== "string") {
throw new Error("Expected domain to be a string")
}
const result = CookieSchema.safeParse(cookie)
if (!result.success) {
throw new Error("Invalid cookie")
}
updatedCookies = updatedCookies.filter(
(c) => !(c.domain === domain && c.name === cookie.name)
)
updatedCookies.push(cookie)
}
const cookieHasFn = (domain: string, name: string): boolean => {
throwIfCookiesUnsupported()
if (typeof domain !== "string" || typeof name !== "string") {
throw new Error("Expected domain and cookieName to be strings")
}
return updatedCookies.some((c) => c.domain === domain && c.name === name)
}
const cookieGetAllFn = (domain: string): Cookie[] => {
throwIfCookiesUnsupported()
if (typeof domain !== "string") {
throw new Error("Expected domain to be a string")
}
return updatedCookies.filter((c) => c.domain === domain)
}
const cookieDeleteFn = (domain: string, name: string): void => {
throwIfCookiesUnsupported()
if (typeof domain !== "string" || typeof name !== "string") {
throw new Error("Expected domain and cookieName to be strings")
}
updatedCookies = updatedCookies.filter(
(c) => !(c.domain === domain && c.name === name)
)
}
const cookieClearFn = (domain: string): void => {
throwIfCookiesUnsupported()
if (typeof domain !== "string") {
throw new Error("Expected domain to be a string")
}
updatedCookies = updatedCookies.filter((c) => c.domain !== domain)
}
return {
methods: {
get: cookieGetFn,
set: cookieSetFn,
has: cookieHasFn,
getAll: cookieGetAllFn,
delete: cookieDeleteFn,
clear: cookieClearFn,
},
// Use a function so we always read the latest `updatedCookies` (not a stale snapshot)
getUpdatedCookies: () =>
cookiesSupported ? cloneDeep(updatedCookies) : null,
}
}
const getResolvedExpectValue = (expectVal: any) => {
if (typeof expectVal !== "string") {
return expectVal
@ -470,7 +774,7 @@ export const createExpectation = (
* @returns Object with methods in the `pw` namespace
*/
export const getPreRequestScriptMethods = (envs: TestResult["envs"]) => {
const { methods, updatedEnvs } = getSharedMethods(cloneDeep(envs))
const { methods, updatedEnvs } = getSharedEnvMethods(cloneDeep(envs))
return { pw: methods, updatedEnvs }
}
@ -500,7 +804,7 @@ export const getTestRunnerScriptMethods = (envs: TestResult["envs"]) => {
const expectFn = (expectVal: any) =>
createExpectation(expectVal, false, testRunStack)
const { methods, updatedEnvs } = getSharedMethods(cloneDeep(envs))
const { methods, updatedEnvs } = getSharedEnvMethods(cloneDeep(envs))
const pw = {
...methods,
@ -510,3 +814,35 @@ export const getTestRunnerScriptMethods = (envs: TestResult["envs"]) => {
return { pw, testRunStack, updatedEnvs }
}
/**
* Compiles shared scripting API properties (scoped to requests) for use in both pre and post request scripts
* Extracts shared properties from a request object
* @param request The request object to extract shared properties from
* @returns An object containing the shared properties of the request
*/
export const getSharedRequestProps = (request: HoppRESTRequest) => {
return {
get url() {
return request.endpoint
},
get method() {
return request.method
},
get params() {
return request.params
},
get headers() {
return request.headers
},
get body() {
return request.body
},
get auth() {
return request.auth
},
get requestVariables() {
return request.requestVariables
},
}
}

View file

@ -2,10 +2,15 @@ import { FaradayCage } from "faraday-cage"
import { ConsoleEntry } from "faraday-cage/modules"
import * as E from "fp-ts/Either"
import { cloneDeep } from "lodash"
import { SandboxPreRequestResult, TestResult } from "~/types"
import {
RunPreRequestScriptOptions,
SandboxPreRequestResult,
TestResult,
} from "~/types"
import { defaultModules, pwPreRequestModule } from "~/cage-modules"
import { defaultModules, preRequestModule } from "~/cage-modules"
import { Cookie, HoppRESTRequest } from "@hoppscotch/data"
import Worker from "./worker?worker&inline"
const runPreRequestScriptWithWebWorker = (
@ -31,10 +36,14 @@ const runPreRequestScriptWithWebWorker = (
const runPreRequestScriptWithFaradayCage = async (
preRequestScript: string,
envs: TestResult["envs"]
envs: TestResult["envs"],
request: HoppRESTRequest,
cookies: Cookie[] | null
): Promise<E.Either<string, SandboxPreRequestResult>> => {
const consoleEntries: ConsoleEntry[] = []
let finalEnvs = envs
let finalRequest = request
let finalCookies = cookies
const cage = await FaradayCage.create()
@ -43,9 +52,15 @@ const runPreRequestScriptWithFaradayCage = async (
handleConsoleEntry: (consoleEntry) => consoleEntries.push(consoleEntry),
}),
pwPreRequestModule({
preRequestModule({
envs: cloneDeep(envs),
handleSandboxResults: ({ envs }) => (finalEnvs = envs),
request: cloneDeep(request),
cookies: cookies ? cloneDeep(cookies) : null,
handleSandboxResults: ({ envs, request, cookies }) => {
finalEnvs = envs
finalRequest = request
finalCookies = cookies
},
}),
])
@ -62,16 +77,32 @@ const runPreRequestScriptWithFaradayCage = async (
}
return E.right({
envs: finalEnvs,
updatedEnvs: finalEnvs,
consoleEntries,
})
updatedRequest: finalRequest,
updatedCookies: finalCookies,
} satisfies SandboxPreRequestResult)
}
export const runPreRequestScript = async (
export const runPreRequestScript = (
preRequestScript: string,
envs: TestResult["envs"],
experimentalScriptingSandbox = true
): Promise<E.Either<string, SandboxPreRequestResult>> =>
experimentalScriptingSandbox
? runPreRequestScriptWithFaradayCage(preRequestScript, envs)
: runPreRequestScriptWithWebWorker(preRequestScript, envs)
options: RunPreRequestScriptOptions
): Promise<E.Either<string, SandboxPreRequestResult>> => {
const { envs, experimentalScriptingSandbox = true } = options
if (experimentalScriptingSandbox) {
const { request, cookies } = options as Extract<
RunPreRequestScriptOptions,
{ experimentalScriptingSandbox: true }
>
return runPreRequestScriptWithFaradayCage(
preRequestScript,
envs,
request,
cookies
)
}
return runPreRequestScriptWithWebWorker(preRequestScript, envs)
}

View file

@ -1,6 +1,6 @@
import * as TE from "fp-ts/TaskEither"
import { getPreRequestScriptMethods } from "~/shared-utils"
import { getPreRequestScriptMethods } from "~/utils/shared"
import { SandboxPreRequestResult, TestResult } from "~/types"
const executeScriptInContext = (
@ -17,7 +17,8 @@ const executeScriptInContext = (
executeScript(pw)
return TE.right({
envs: updatedEnvs,
updatedEnvs,
updatedCookies: null,
})
} catch (error) {
return TE.left(`Script execution failed: ${(error as Error).message}`)

View file

@ -3,18 +3,20 @@ import { ConsoleEntry } from "faraday-cage/modules"
import * as E from "fp-ts/Either"
import { cloneDeep } from "lodash-es"
import { defaultModules, pwPostRequestModule } from "~/cage-modules"
import { preventCyclicObjects } from "~/shared-utils"
import { defaultModules, postRequestModule } from "~/cage-modules"
import {
RunPostRequestScriptOptions,
SandboxTestResult,
TestDescriptor,
TestResponse,
TestResult,
} from "~/types"
import { preventCyclicObjects } from "~/utils/shared"
import { Cookie, HoppRESTRequest } from "@hoppscotch/data"
import Worker from "./worker?worker&inline"
const runTestScriptWithWebWorker = (
const runPostRequestScriptWithWebWorker = (
testScript: string,
envs: TestResult["envs"],
response: TestResponse
@ -36,10 +38,12 @@ const runTestScriptWithWebWorker = (
})
}
const runTestScriptWithFaradayCage = async (
const runPostRequestScriptWithFaradayCage = async (
testScript: string,
envs: TestResult["envs"],
response: TestResponse
request: HoppRESTRequest,
response: TestResponse,
cookies: Cookie[] | null
): Promise<E.Either<string, SandboxTestResult>> => {
const testRunStack: TestDescriptor[] = [
{ descriptor: "root", expectResults: [], children: [] },
@ -48,6 +52,7 @@ const runTestScriptWithFaradayCage = async (
let finalEnvs = envs
let finalTestResults = testRunStack
const consoleEntries: ConsoleEntry[] = []
let finalCookies = cookies
const cage = await FaradayCage.create()
@ -56,13 +61,16 @@ const runTestScriptWithFaradayCage = async (
handleConsoleEntry: (consoleEntry) => consoleEntries.push(consoleEntry),
}),
pwPostRequestModule({
postRequestModule({
envs: cloneDeep(envs),
testRunStack: cloneDeep(testRunStack),
response,
handleSandboxResults: ({ envs, testRunStack }) => {
request: cloneDeep(request),
response: cloneDeep(response),
cookies: cookies ? cloneDeep(cookies) : null,
handleSandboxResults: ({ envs, testRunStack, cookies }) => {
finalEnvs = envs
finalTestResults = testRunStack
finalCookies = cookies
},
}),
])
@ -83,22 +91,38 @@ const runTestScriptWithFaradayCage = async (
tests: finalTestResults[0],
envs: finalEnvs,
consoleEntries,
updatedCookies: finalCookies,
})
}
export const runTestScript = async (
testScript: string,
envs: TestResult["envs"],
response: TestResponse,
experimentalScriptingSandbox = true
options: RunPostRequestScriptOptions
): Promise<E.Either<string, SandboxTestResult>> => {
const responseObjHandle = preventCyclicObjects<TestResponse>(response)
const responseObjHandle = preventCyclicObjects<TestResponse>(options.response)
if (E.isLeft(responseObjHandle)) {
return E.left(`Response marshalling failed: ${responseObjHandle.left}`)
}
return experimentalScriptingSandbox
? runTestScriptWithFaradayCage(testScript, envs, responseObjHandle.right)
: runTestScriptWithWebWorker(testScript, envs, responseObjHandle.right)
const resolvedResponse = responseObjHandle.right
const { envs, experimentalScriptingSandbox = true } = options
if (experimentalScriptingSandbox) {
const { request, cookies } = options as Extract<
RunPostRequestScriptOptions,
{ experimentalScriptingSandbox: true }
>
return runPostRequestScriptWithFaradayCage(
testScript,
envs,
request,
resolvedResponse,
cookies
)
}
return runPostRequestScriptWithWebWorker(testScript, envs, resolvedResponse)
}

View file

@ -1,11 +1,11 @@
import * as E from "fp-ts/Either"
import * as TE from "fp-ts/TaskEither"
import { SandboxTestResult, TestResponse, TestResult } from "~/types"
import {
getTestRunnerScriptMethods,
preventCyclicObjects,
} from "~/shared-utils"
import { SandboxTestResult, TestResponse, TestResult } from "~/types"
} from "~/utils/shared"
const executeScriptInContext = (
testScript: string,