feat(scripting-revamp): introduce hopp namespace (#5388)
This commit is contained in:
parent
37060638df
commit
b62b50c1e2
76 changed files with 10502 additions and 830 deletions
|
|
@ -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:", () => {
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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") ??
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}>
|
||||
}
|
||||
|
|
|
|||
16
packages/hoppscotch-data/src/cookies.ts
Normal file
16
packages/hoppscotch-data/src/cookies.ts
Normal 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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" }),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
354
packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/delete.spec.ts
vendored
Normal file
354
packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/delete.spec.ts
vendored
Normal 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())
|
||||
})
|
||||
339
packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/get.spec.ts
vendored
Normal file
339
packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/get.spec.ts
vendored
Normal 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()
|
||||
})
|
||||
})
|
||||
463
packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/getInitialRaw.spec.ts
vendored
Normal file
463
packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/getInitialRaw.spec.ts
vendored
Normal 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()
|
||||
})
|
||||
})
|
||||
361
packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/getRaw.spec.ts
vendored
Normal file
361
packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/getRaw.spec.ts
vendored
Normal 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()
|
||||
})
|
||||
})
|
||||
522
packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/reset.spec.ts
vendored
Normal file
522
packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/reset.spec.ts
vendored
Normal 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())
|
||||
})
|
||||
388
packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/set.spec.ts
vendored
Normal file
388
packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/set.spec.ts
vendored
Normal 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()
|
||||
})
|
||||
})
|
||||
526
packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/setInitial.spec.ts
vendored
Normal file
526
packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/env/setInitial.spec.ts
vendored
Normal 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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
@ -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'" },
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
@ -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'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
@ -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!'",
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
@ -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) }],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
@ -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",
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -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 },
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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)"
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
export { defaultModules } from "./default"
|
||||
export { pwPostRequestModule, pwPreRequestModule } from "./pw"
|
||||
export { postRequestModule, preRequestModule } from "./scripting-modules"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
157
packages/hoppscotch-js-sandbox/src/utils/pre-request.ts
Normal file
157
packages/hoppscotch-js-sandbox/src/utils/pre-request.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue