From 5bcc38e36b458cf0754a2664d8a4e456ae1891ff Mon Sep 17 00:00:00 2001 From: James George Date: Thu, 8 Feb 2024 22:08:18 +0530 Subject: [PATCH] feat: support secret environment variables in CLI (#3815) --- packages/hoppscotch-cli/package.json | 1 + .../src/__tests__/commands/test.spec.ts | 296 ++++++++++++------ .../collection-level-headers-auth-coll.json} | 0 .../env-flag-tests-coll.json} | 0 .../fails-coll.json} | 0 .../malformed-coll-2.json} | 0 .../malformed-coll.json} | 0 .../notjson-coll.txt} | 0 .../passes-coll.json} | 0 ...e-req-script-env-var-persistence-coll.json | 21 ++ .../req-body-env-vars-coll.json | 0 .../samples/collections/secret-envs-coll.json | 107 +++++++ .../secret-envs-persistence-coll.json | 143 +++++++++ ...ecret-envs-persistence-scripting-coll.json | 30 ++ .../samples/environments/bulk-envs.json | 32 ++ .../{ => environments}/env-flag-envs.json | 0 .../samples/environments/malformed-envs.json | 16 + .../req-body-env-vars-envs.json | 3 +- ...ecret-envs-persistence-scripting-envs.json | 27 ++ .../samples/environments/secret-envs.json | 40 +++ .../secret-supplied-values-envs.json | 46 +++ .../hoppscotch-cli/src/__tests__/utils.ts | 13 +- .../hoppscotch-cli/src/interfaces/request.ts | 2 + .../hoppscotch-cli/src/options/test/env.ts | 46 +-- packages/hoppscotch-cli/src/types/request.ts | 21 +- packages/hoppscotch-cli/src/utils/display.ts | 2 +- .../hoppscotch-cli/src/utils/pre-request.ts | 38 ++- packages/hoppscotch-cli/src/utils/request.ts | 47 ++- .../hoppscotch-data/src/environment/index.ts | 1 + pnpm-lock.yaml | 4 +- 30 files changed, 791 insertions(+), 145 deletions(-) rename packages/hoppscotch-cli/src/__tests__/samples/{collection-level-headers-auth.json => collections/collection-level-headers-auth-coll.json} (100%) rename packages/hoppscotch-cli/src/__tests__/samples/{env-flag-tests.json => collections/env-flag-tests-coll.json} (100%) rename packages/hoppscotch-cli/src/__tests__/samples/{fails.json => collections/fails-coll.json} (100%) rename packages/hoppscotch-cli/src/__tests__/samples/{malformed-collection2.json => collections/malformed-coll-2.json} (100%) rename packages/hoppscotch-cli/src/__tests__/samples/{malformed-collection.json => collections/malformed-coll.json} (100%) rename packages/hoppscotch-cli/src/__tests__/samples/{notjson.txt => collections/notjson-coll.txt} (100%) rename packages/hoppscotch-cli/src/__tests__/samples/{passes.json => collections/passes-coll.json} (100%) create mode 100644 packages/hoppscotch-cli/src/__tests__/samples/collections/pre-req-script-env-var-persistence-coll.json rename packages/hoppscotch-cli/src/__tests__/samples/{ => collections}/req-body-env-vars-coll.json (100%) create mode 100644 packages/hoppscotch-cli/src/__tests__/samples/collections/secret-envs-coll.json create mode 100644 packages/hoppscotch-cli/src/__tests__/samples/collections/secret-envs-persistence-coll.json create mode 100644 packages/hoppscotch-cli/src/__tests__/samples/collections/secret-envs-persistence-scripting-coll.json create mode 100644 packages/hoppscotch-cli/src/__tests__/samples/environments/bulk-envs.json rename packages/hoppscotch-cli/src/__tests__/samples/{ => environments}/env-flag-envs.json (100%) create mode 100644 packages/hoppscotch-cli/src/__tests__/samples/environments/malformed-envs.json rename packages/hoppscotch-cli/src/__tests__/samples/{ => environments}/req-body-env-vars-envs.json (98%) create mode 100644 packages/hoppscotch-cli/src/__tests__/samples/environments/secret-envs-persistence-scripting-envs.json create mode 100644 packages/hoppscotch-cli/src/__tests__/samples/environments/secret-envs.json create mode 100644 packages/hoppscotch-cli/src/__tests__/samples/environments/secret-supplied-values-envs.json diff --git a/packages/hoppscotch-cli/package.json b/packages/hoppscotch-cli/package.json index a5ace01d2..0582952f0 100644 --- a/packages/hoppscotch-cli/package.json +++ b/packages/hoppscotch-cli/package.json @@ -60,6 +60,7 @@ "ts-jest": "^29.1.1", "tsup": "^7.2.0", "typescript": "^5.2.2", + "verzod": "^0.2.2", "zod": "^3.22.4" } } diff --git a/packages/hoppscotch-cli/src/__tests__/commands/test.spec.ts b/packages/hoppscotch-cli/src/__tests__/commands/test.spec.ts index db69d61d5..675e31750 100644 --- a/packages/hoppscotch-cli/src/__tests__/commands/test.spec.ts +++ b/packages/hoppscotch-cli/src/__tests__/commands/test.spec.ts @@ -3,138 +3,247 @@ import { ExecException } from "child_process"; import { HoppErrorCode } from "../../types/errors"; import { runCLI, getErrorCode, getTestJsonFilePath } from "../utils"; -describe("Test 'hopp test ' command:", () => { - test("No collection file path provided.", async () => { - const args = "test"; - const { stderr } = await runCLI(args); +describe("Test `hopp test ` command:", () => { + describe("Argument parsing", () => { + test("Errors with the code `INVALID_ARGUMENT` for not supplying enough arguments", async () => { + const args = "test"; + const { stderr } = await runCLI(args); - const out = getErrorCode(stderr); - expect(out).toBe("INVALID_ARGUMENT"); - }); + const out = getErrorCode(stderr); + expect(out).toBe("INVALID_ARGUMENT"); + }); - test("Collection file not found.", async () => { - const args = "test notfound.json"; - const { stderr } = await runCLI(args); + test("Errors with the code `INVALID_ARGUMENT` for an invalid command", async () => { + const args = "invalid-arg"; + const { stderr } = await runCLI(args); - const out = getErrorCode(stderr); - expect(out).toBe("FILE_NOT_FOUND"); - }); + const out = getErrorCode(stderr); + expect(out).toBe("INVALID_ARGUMENT"); + }); + }) - test("Collection file is invalid JSON.", async () => { - const args = `test ${getTestJsonFilePath( - "malformed-collection.json" - )}`; - const { stderr } = await runCLI(args); + describe("Supplied collection export file validations", () => { + test("Errors with the code `FILE_NOT_FOUND` if the supplied collection export file doesn't exist", async () => { + const args = "test notfound.json"; + const { stderr } = await runCLI(args); - const out = getErrorCode(stderr); - expect(out).toBe("UNKNOWN_ERROR"); - }); + const out = getErrorCode(stderr); + expect(out).toBe("FILE_NOT_FOUND"); + }); - test("Malformed collection file.", async () => { - const args = `test ${getTestJsonFilePath( - "malformed-collection2.json" - )}`; - const { stderr } = await runCLI(args); + test("Errors with the code UNKNOWN_ERROR if the supplied collection export file content isn't valid JSON", async () => { + const args = `test ${getTestJsonFilePath("malformed-coll.json", "collection")}`; + const { stderr } = await runCLI(args); - const out = getErrorCode(stderr); - expect(out).toBe("MALFORMED_COLLECTION"); - }); + const out = getErrorCode(stderr); + expect(out).toBe("UNKNOWN_ERROR"); + }); - test("Invalid arguement.", async () => { - const args = "invalid-arg"; - const { stderr } = await runCLI(args); + test("Errors with the code `MALFORMED_COLLECTION` if the supplied collection export file content is malformed", async () => { + const args = `test ${getTestJsonFilePath("malformed-coll-2.json", "collection")}`; + const { stderr } = await runCLI(args); - const out = getErrorCode(stderr); - expect(out).toBe("INVALID_ARGUMENT"); - }); + const out = getErrorCode(stderr); + expect(out).toBe("MALFORMED_COLLECTION"); + }); - test("Collection file not JSON type.", async () => { - const args = `test ${getTestJsonFilePath("notjson.txt")}`; - const { stderr } = await runCLI(args); + test("Errors with the code `INVALID_FILE_TYPE` if the supplied collection export file doesn't end with the `.json` extension", async () => { + const args = `test ${getTestJsonFilePath("notjson-coll.txt", "collection")}`; + const { stderr } = await runCLI(args); - const out = getErrorCode(stderr); - expect(out).toBe("INVALID_FILE_TYPE"); - }); + const out = getErrorCode(stderr); + expect(out).toBe("INVALID_FILE_TYPE"); + }); - test("Some errors occured (exit code 1).", async () => { - const args = `test ${getTestJsonFilePath("fails.json")}`; - const { error } = await runCLI(args); + test("Fails if the collection file includes scripts with incorrect API usage and failed assertions", async () => { + const args = `test ${getTestJsonFilePath("fails-coll.json", "collection")}`; + const { error } = await runCLI(args); - expect(error).not.toBeNull(); - expect(error).toMatchObject({ - code: 1, + expect(error).not.toBeNull(); + expect(error).toMatchObject({ + code: 1, + }); }); }); - test("No errors occured (exit code 0).", async () => { - const args = `test ${getTestJsonFilePath("passes.json")}`; + test("Successfully processes a supplied collection export file of the expected format", async () => { + const args = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`; const { error } = await runCLI(args); expect(error).toBeNull(); }); - test("Supports inheriting headers and authorization set at the root collection", async () => { - const args = `test ${getTestJsonFilePath("collection-level-headers-auth.json")}`; + test("Successfully inherits headers and authorization set at the root collection", async () => { + const args = `test ${getTestJsonFilePath( + "collection-level-headers-auth-coll.json", "collection" + )}`; const { error } = await runCLI(args); expect(error).toBeNull(); - }) + }); + + test("Persists environment variables set in the pre-request script for consumption in the test script", async () => { + const args = `test ${getTestJsonFilePath( + "pre-req-script-env-var-persistence-coll.json", "collection" + )}`; + const { error } = await runCLI(args); + + expect(error).toBeNull(); + }); }); -describe("Test 'hopp test --env ' command:", () => { - const VALID_TEST_ARGS = `test ${getTestJsonFilePath( - "passes.json" - )}`; +describe("Test `hopp test --env ` command:", () => { + describe("Supplied environment export file validations", () => { + const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`; - test("No env file path provided.", async () => { - const args = `${VALID_TEST_ARGS} --env`; - const { stderr } = await runCLI(args); + test("Errors with the code `INVALID_ARGUMENT` if no file is supplied", async () => { + const args = `${VALID_TEST_ARGS} --env`; + const { stderr } = await runCLI(args); - const out = getErrorCode(stderr); - expect(out).toBe("INVALID_ARGUMENT"); + const out = getErrorCode(stderr); + expect(out).toBe("INVALID_ARGUMENT"); + }); + + test("Errors with the code `INVALID_FILE_TYPE` if the supplied environment export file doesn't end with the `.json` extension", async () => { + const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath( + "notjson-coll.txt", "collection" + )}`; + const { stderr } = await runCLI(args); + + const out = getErrorCode(stderr); + expect(out).toBe("INVALID_FILE_TYPE"); + }); + + test("Errors with the code `FILE_NOT_FOUND` if the supplied environment export file doesn't exist", async () => { + const args = `${VALID_TEST_ARGS} --env notfound.json`; + const { stderr } = await runCLI(args); + + const out = getErrorCode(stderr); + expect(out).toBe("FILE_NOT_FOUND"); + }); + + test("Errors with the code `MALFORMED_ENV_FILE` on supplying a malformed environment export file", async () => { + const ENV_PATH = getTestJsonFilePath("malformed-envs.json", "environment"); + const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`; + const { stderr } = await runCLI(args); + + const out = getErrorCode(stderr); + expect(out).toBe("MALFORMED_ENV_FILE"); + }); + + test("Errors with the code `BULK_ENV_FILE` on supplying an environment export file based on the bulk environment export format", async () => { + const ENV_PATH = getTestJsonFilePath("bulk-envs.json", "environment"); + const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`; + const { stderr } = await runCLI(args); + + const out = getErrorCode(stderr); + expect(out).toBe("BULK_ENV_FILE"); + }); }); - test("ENV file not JSON type.", async () => { - const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath("notjson.txt")}`; - const { stderr } = await runCLI(args); - - const out = getErrorCode(stderr); - expect(out).toBe("INVALID_FILE_TYPE"); - }); - - test("ENV file not found.", async () => { - const args = `${VALID_TEST_ARGS} --env notfound.json`; - const { stderr } = await runCLI(args); - - const out = getErrorCode(stderr); - expect(out).toBe("FILE_NOT_FOUND"); - }); - - test("No errors occured (exit code 0).", async () => { - const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json"); - const ENV_PATH = getTestJsonFilePath("env-flag-envs.json"); + test("Successfully resolves values from the supplied environment export file", async () => { + const TESTS_PATH = getTestJsonFilePath("env-flag-tests-coll.json", "collection"); + const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment"); const args = `test ${TESTS_PATH} --env ${ENV_PATH}`; const { error } = await runCLI(args); expect(error).toBeNull(); }); - test("Correctly resolves environment variables referenced in the request body", async () => { - const COLL_PATH = getTestJsonFilePath("req-body-env-vars-coll.json"); - const ENVS_PATH = getTestJsonFilePath("req-body-env-vars-envs.json"); + test("Successfully resolves environment variables referenced in the request body", async () => { + const COLL_PATH = getTestJsonFilePath("req-body-env-vars-coll.json", "collection"); + const ENVS_PATH = getTestJsonFilePath("req-body-env-vars-envs.json", "environment"); const args = `test ${COLL_PATH} --env ${ENVS_PATH}`; const { error } = await runCLI(args); expect(error).toBeNull(); }); + + test("Works with shorth `-e` flag", async () => { + const TESTS_PATH = getTestJsonFilePath("env-flag-tests-coll.json", "collection"); + const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment"); + const args = `test ${TESTS_PATH} -e ${ENV_PATH}`; + + const { error } = await runCLI(args); + expect(error).toBeNull(); + }); + + describe("Secret environment variables", () => { + jest.setTimeout(10000); + + // Reads secret environment values from system environment + test("Successfully picks the values for secret environment variables from `process.env` and persists the variables set from the pre-request script", async () => { + const env = { + ...process.env, + secretBearerToken: "test-token", + secretBasicAuthUsername: "test-user", + secretBasicAuthPassword: "test-pass", + secretQueryParamValue: "secret-query-param-value", + secretBodyValue: "secret-body-value", + secretHeaderValue: "secret-header-value", + }; + + const COLL_PATH = getTestJsonFilePath("secret-envs-coll.json", "collection"); + const ENVS_PATH = getTestJsonFilePath("secret-envs.json", "environment"); + const args = `test ${COLL_PATH} --env ${ENVS_PATH}`; + + const { error, stdout } = await runCLI(args, { env }); + + expect(stdout).toContain( + "https://httpbin.org/basic-auth/*********/*********" + ); + expect(error).toBeNull(); + }); + + // Prefers values specified in the environment export file over values set in the system environment + test("Successfully picks the values for secret environment variables set directly in the environment export file and persists the environment variables set from the pre-request script", async () => { + const COLL_PATH = getTestJsonFilePath("secret-envs-coll.json", "collection"); + const ENVS_PATH = getTestJsonFilePath("secret-supplied-values-envs.json", "environment"); + const args = `test ${COLL_PATH} --env ${ENVS_PATH}`; + + const { error, stdout } = await runCLI(args); + + expect(stdout).toContain( + "https://httpbin.org/basic-auth/*********/*********" + ); + expect(error).toBeNull(); + }); + + // Values set from the scripting context takes the highest precedence + test("Setting values for secret environment variables from the pre-request script overrides values set at the supplied environment export file", async () => { + const COLL_PATH = getTestJsonFilePath( + "secret-envs-persistence-coll.json", "collection" + ); + const ENVS_PATH = getTestJsonFilePath("secret-supplied-values-envs.json", "environment"); + const args = `test ${COLL_PATH} --env ${ENVS_PATH}`; + + const { error, stdout } = await runCLI(args); + + expect(stdout).toContain( + "https://httpbin.org/basic-auth/*********/*********" + ); + expect(error).toBeNull(); + }); + + test("Persists secret environment variable values set from the pre-request script for consumption in the request and post-request script context", async () => { + const COLL_PATH = getTestJsonFilePath( + "secret-envs-persistence-scripting-coll.json", "collection" + ); + const ENVS_PATH = getTestJsonFilePath( + "secret-envs-persistence-scripting-envs.json", "environment" + ); + const args = `test ${COLL_PATH} --env ${ENVS_PATH}`; + + const { error } = await runCLI(args); + expect(error).toBeNull(); + }); + }); }); -describe("Test 'hopp test --delay ' command:", () => { - const VALID_TEST_ARGS = `test ${getTestJsonFilePath( - "passes.json" - )}`; +describe("Test `hopp test --delay ` command:", () => { + const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`; - test("No value passed to delay flag.", async () => { + test("Errors with the code `INVALID_ARGUMENT` on not supplying a delay value", async () => { const args = `${VALID_TEST_ARGS} --delay`; const { stderr } = await runCLI(args); @@ -142,7 +251,7 @@ describe("Test 'hopp test --delay ' command:", () => { expect(out).toBe("INVALID_ARGUMENT"); }); - test("Invalid value passed to delay flag.", async () => { + test("Errors with the code `INVALID_ARGUMENT` on supplying an invalid delay value", async () => { const args = `${VALID_TEST_ARGS} --delay 'NaN'`; const { stderr } = await runCLI(args); @@ -150,10 +259,17 @@ describe("Test 'hopp test --delay ' command:", () => { expect(out).toBe("INVALID_ARGUMENT"); }); - test("Valid value passed to delay flag.", async () => { + test("Successfully performs delayed request execution for a valid delay value", async () => { const args = `${VALID_TEST_ARGS} --delay 1`; const { error } = await runCLI(args); expect(error).toBeNull(); }); + + test("Works with the short `-d` flag", async () => { + const args = `${VALID_TEST_ARGS} -d 1`; + const { error } = await runCLI(args); + + expect(error).toBeNull(); + }); }); diff --git a/packages/hoppscotch-cli/src/__tests__/samples/collection-level-headers-auth.json b/packages/hoppscotch-cli/src/__tests__/samples/collections/collection-level-headers-auth-coll.json similarity index 100% rename from packages/hoppscotch-cli/src/__tests__/samples/collection-level-headers-auth.json rename to packages/hoppscotch-cli/src/__tests__/samples/collections/collection-level-headers-auth-coll.json diff --git a/packages/hoppscotch-cli/src/__tests__/samples/env-flag-tests.json b/packages/hoppscotch-cli/src/__tests__/samples/collections/env-flag-tests-coll.json similarity index 100% rename from packages/hoppscotch-cli/src/__tests__/samples/env-flag-tests.json rename to packages/hoppscotch-cli/src/__tests__/samples/collections/env-flag-tests-coll.json diff --git a/packages/hoppscotch-cli/src/__tests__/samples/fails.json b/packages/hoppscotch-cli/src/__tests__/samples/collections/fails-coll.json similarity index 100% rename from packages/hoppscotch-cli/src/__tests__/samples/fails.json rename to packages/hoppscotch-cli/src/__tests__/samples/collections/fails-coll.json diff --git a/packages/hoppscotch-cli/src/__tests__/samples/malformed-collection2.json b/packages/hoppscotch-cli/src/__tests__/samples/collections/malformed-coll-2.json similarity index 100% rename from packages/hoppscotch-cli/src/__tests__/samples/malformed-collection2.json rename to packages/hoppscotch-cli/src/__tests__/samples/collections/malformed-coll-2.json diff --git a/packages/hoppscotch-cli/src/__tests__/samples/malformed-collection.json b/packages/hoppscotch-cli/src/__tests__/samples/collections/malformed-coll.json similarity index 100% rename from packages/hoppscotch-cli/src/__tests__/samples/malformed-collection.json rename to packages/hoppscotch-cli/src/__tests__/samples/collections/malformed-coll.json diff --git a/packages/hoppscotch-cli/src/__tests__/samples/notjson.txt b/packages/hoppscotch-cli/src/__tests__/samples/collections/notjson-coll.txt similarity index 100% rename from packages/hoppscotch-cli/src/__tests__/samples/notjson.txt rename to packages/hoppscotch-cli/src/__tests__/samples/collections/notjson-coll.txt diff --git a/packages/hoppscotch-cli/src/__tests__/samples/passes.json b/packages/hoppscotch-cli/src/__tests__/samples/collections/passes-coll.json similarity index 100% rename from packages/hoppscotch-cli/src/__tests__/samples/passes.json rename to packages/hoppscotch-cli/src/__tests__/samples/collections/passes-coll.json diff --git a/packages/hoppscotch-cli/src/__tests__/samples/collections/pre-req-script-env-var-persistence-coll.json b/packages/hoppscotch-cli/src/__tests__/samples/collections/pre-req-script-env-var-persistence-coll.json new file mode 100644 index 000000000..78d16cb82 --- /dev/null +++ b/packages/hoppscotch-cli/src/__tests__/samples/collections/pre-req-script-env-var-persistence-coll.json @@ -0,0 +1,21 @@ +{ + "v": 2, + "name": "pre-req-script-env-var-persistence-coll", + "folders": [], + "requests": [ + { + "v": "1", + "auth": { "authType": "none", "authActive": true }, + "body": { "body": null, "contentType": null }, + "name": "sample-req", + "method": "GET", + "params": [], + "headers": [], + "endpoint": "https://echo.hoppscotch.io", + "testScript": "pw.expect(pw.env.get(\"variable\")).toBe(\"value\")", + "preRequestScript": "pw.env.set(\"variable\", \"value\");" + } + ], + "auth": { "authType": "inherit", "authActive": true }, + "headers": [] +} diff --git a/packages/hoppscotch-cli/src/__tests__/samples/req-body-env-vars-coll.json b/packages/hoppscotch-cli/src/__tests__/samples/collections/req-body-env-vars-coll.json similarity index 100% rename from packages/hoppscotch-cli/src/__tests__/samples/req-body-env-vars-coll.json rename to packages/hoppscotch-cli/src/__tests__/samples/collections/req-body-env-vars-coll.json diff --git a/packages/hoppscotch-cli/src/__tests__/samples/collections/secret-envs-coll.json b/packages/hoppscotch-cli/src/__tests__/samples/collections/secret-envs-coll.json new file mode 100644 index 000000000..f07da4d2b --- /dev/null +++ b/packages/hoppscotch-cli/src/__tests__/samples/collections/secret-envs-coll.json @@ -0,0 +1,107 @@ +{ + "v": 2, + "name": "secret-envs-coll", + "folders": [], + "requests": [ + { + "v": "1", + "auth": { "authType": "none", "authActive": true }, + "body": { "body": null, "contentType": null }, + "name": "test-secret-headers", + "method": "GET", + "params": [], + "headers": [ + { + "key": "Secret-Header-Key", + "value": "<>", + "active": true + } + ], + "endpoint": "<>/headers", + "testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.get(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.get(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})", + "preRequestScript": "const secretHeaderValueFromPreReqScript = pw.env.get(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)" + }, + { + "v": "1", + "auth": { "authType": "none", "authActive": true }, + "body": { + "body": "{\n \"secretBodyKey\": \"<>\"\n}", + "contentType": "application/json" + }, + "name": "test-secret-body", + "method": "POST", + "params": [], + "headers": [], + "endpoint": "<>/post", + "testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})", + "preRequestScript": "const secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)" + }, + { + "v": "1", + "auth": { "authType": "none", "authActive": true }, + "body": { "body": null, "contentType": null }, + "name": "test-secret-query-params", + "method": "GET", + "params": [ + { + "key": "secretQueryParamKey", + "value": "<>", + "active": true + } + ], + "headers": [], + "endpoint": "<>/get", + "testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})", + "preRequestScript": "const secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)" + }, + { + "v": "1", + "auth": { + "authType": "basic", + "password": "<>", + "username": "<>", + "authActive": true + }, + "body": { "body": null, "contentType": null }, + "name": "test-secret-basic-auth", + "method": "GET", + "params": [], + "headers": [], + "endpoint": "<>/basic-auth/<>/<>", + "testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});", + "preRequestScript": "" + }, + { + "v": "1", + "auth": { + "token": "<>", + "authType": "bearer", + "password": "testpassword", + "username": "testuser", + "authActive": true + }, + "body": { "body": null, "contentType": null }, + "name": "test-secret-bearer-auth", + "method": "GET", + "params": [], + "headers": [], + "endpoint": "<>/bearer", + "testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.get(\"secretBearerToken\")\n const preReqSecretBearerToken = pw.env.get(\"preReqSecretBearerToken\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});", + "preRequestScript": "const secretBearerToken = pw.env.get(\"secretBearerToken\")\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)" + }, + { + "v": "1", + "auth": { "authType": "none", "authActive": true }, + "body": { "body": null, "contentType": null }, + "name": "test-secret-fallback", + "method": "GET", + "params": [], + "headers": [], + "endpoint": "<>", + "testScript": "pw.test(\"Returns an empty string if the value for a secret environment variable is not found in the system environment\", () => {\n pw.expect(pw.env.get(\"nonExistentValueInSystemEnv\")).toBe(\"\")\n})", + "preRequestScript": "" + } + ], + "auth": { "authType": "inherit", "authActive": false }, + "headers": [] +} diff --git a/packages/hoppscotch-cli/src/__tests__/samples/collections/secret-envs-persistence-coll.json b/packages/hoppscotch-cli/src/__tests__/samples/collections/secret-envs-persistence-coll.json new file mode 100644 index 000000000..809233e85 --- /dev/null +++ b/packages/hoppscotch-cli/src/__tests__/samples/collections/secret-envs-persistence-coll.json @@ -0,0 +1,143 @@ +{ + "v": 2, + "name": "secret-envs-setters-coll", + "folders": [], + "requests": [ + { + "v": "1", + "auth": { + "authType": "none", + "authActive": true + }, + "body": { + "body": null, + "contentType": null + }, + "name": "test-secret-headers", + "method": "GET", + "params": [], + "headers": [ + { + "key": "Secret-Header-Key", + "value": "<>", + "active": true + } + ], + "endpoint": "<>/headers", + "testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.getResolve(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.getResolve(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})", + "preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)" + }, + { + "v": "1", + "auth": { + "authType": "none", + "authActive": true + }, + "body": { + "body": null, + "contentType": null + }, + "name": "test-secret-headers-overrides", + "method": "GET", + "params": [], + "headers": [ + { + "key": "Secret-Header-Key", + "value": "<>", + "active": true + } + ], + "endpoint": "<>/headers", + "testScript": "pw.test(\"Value set at the pre-request script takes precedence\", () => {\n const secretHeaderValue = pw.env.getResolve(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value-overriden\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.getResolve(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value-overriden\")\n})", + "preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value-overriden\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)" + }, + { + "v": "1", + "auth": { + "authType": "none", + "authActive": true + }, + "body": { + "body": "{\n \"secretBodyKey\": \"<>\"\n}", + "contentType": "application/json" + }, + "name": "test-secret-body", + "method": "POST", + "params": [], + "headers": [], + "endpoint": "<>/post", + "testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})", + "preRequestScript": "const secretBodyValue = pw.env.get(\"secretBodyValue\")\n\nif (!secretBodyValue) { \n pw.env.set(\"secretBodyValue\", \"secret-body-value\")\n}\n\nconst secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)" + }, + { + "v": "1", + "auth": { + "authType": "none", + "authActive": true + }, + "body": { + "body": null, + "contentType": null + }, + "name": "test-secret-query-params", + "method": "GET", + "params": [ + { + "key": "secretQueryParamKey", + "value": "<>", + "active": true + } + ], + "headers": [], + "endpoint": "<>/get", + "testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})", + "preRequestScript": "const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n\nif (!secretQueryParamValue) {\n pw.env.set(\"secretQueryParamValue\", \"secret-query-param-value\")\n}\n\nconst secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)" + }, + { + "v": "1", + "auth": { + "authType": "basic", + "password": "<>", + "username": "<>", + "authActive": true + }, + "body": { + "body": null, + "contentType": null + }, + "name": "test-secret-basic-auth", + "method": "GET", + "params": [], + "headers": [], + "endpoint": "<>/basic-auth/<>/<>", + "testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});", + "preRequestScript": "let secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n\nlet secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\nif (!secretBasicAuthUsername) {\n pw.env.set(\"secretBasicAuthUsername\", \"test-user\")\n}\n\nif (!secretBasicAuthPassword) {\n pw.env.set(\"secretBasicAuthPassword\", \"test-pass\")\n}" + }, + { + "v": "1", + "auth": { + "token": "<>", + "authType": "bearer", + "password": "testpassword", + "username": "testuser", + "authActive": true + }, + "body": { + "body": null, + "contentType": null + }, + "name": "test-secret-bearer-auth", + "method": "GET", + "params": [], + "headers": [], + "endpoint": "<>/bearer", + "testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.resolve(\"<>\")\n const preReqSecretBearerToken = pw.env.resolve(\"<>\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});", + "preRequestScript": "let secretBearerToken = pw.env.resolve(\"<>\")\n\nif (!secretBearerToken) {\n pw.env.set(\"secretBearerToken\", \"test-token\")\n secretBearerToken = pw.env.resolve(\"<>\")\n}\n\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)" + } + ], + "auth": { + "authType": "inherit", + "authActive": false + }, + "headers": [] +} diff --git a/packages/hoppscotch-cli/src/__tests__/samples/collections/secret-envs-persistence-scripting-coll.json b/packages/hoppscotch-cli/src/__tests__/samples/collections/secret-envs-persistence-scripting-coll.json new file mode 100644 index 000000000..0bb77e2b1 --- /dev/null +++ b/packages/hoppscotch-cli/src/__tests__/samples/collections/secret-envs-persistence-scripting-coll.json @@ -0,0 +1,30 @@ +{ + "v": 2, + "name": "secret-envs-persistence-scripting-req", + "folders": [], + "requests": [ + { + "v": "1", + "endpoint": "https://httpbin.org/post", + "name": "req", + "params": [], + "headers": [ + { + "active": true, + "key": "Custom-Header", + "value": "<>" + } + ], + "method": "POST", + "auth": { "authType": "none", "authActive": true }, + "preRequestScript": "pw.env.set(\"preReqVarOne\", \"pre-req-value-one\")\n\npw.env.set(\"preReqVarTwo\", \"pre-req-value-two\")\n\npw.env.set(\"customHeaderValueFromSecretVar\", \"custom-header-secret-value\")\n\npw.env.set(\"customBodyValue\", \"custom-body-value\")", + "testScript": "pw.test(\"Secret environment value set from the pre-request script takes precedence\", () => {\n pw.expect(pw.env.get(\"preReqVarOne\")).toBe(\"pre-req-value-one\")\n})\n\npw.test(\"Successfully sets initial value for the secret variable from the pre-request script\", () => {\n pw.env.set(\"postReqVarTwo\", \"post-req-value-two\")\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(\"post-req-value-two\")\n})\n\npw.test(\"Successfully resolves secret variable values referred in request headers that are set in pre-request sccript\", () => {\n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"custom-header-secret-value\")\n})\n\npw.test(\"Successfully resolves secret variable values referred in request body that are set in pre-request sccript\", () => {\n pw.expect(pw.response.body.json.key).toBe(\"custom-body-value\")\n})\n\npw.test(\"Secret environment variable set from the post-request script takes precedence\", () => {\n pw.env.set(\"postReqVarOne\", \"post-req-value-one\")\n pw.expect(pw.env.get(\"postReqVarOne\")).toBe(\"post-req-value-one\")\n})\n\npw.test(\"Successfully sets initial value for the secret variable from the post-request script\", () => {\n pw.env.set(\"postReqVarTwo\", \"post-req-value-two\")\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(\"post-req-value-two\")\n})\n\npw.test(\"Successfully removes environment variables via the pw.env.unset method\", () => {\n pw.env.unset(\"preReqVarOne\")\n pw.env.unset(\"postReqVarTwo\")\n\n pw.expect(pw.env.get(\"preReqVarOne\")).toBe(undefined)\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(undefined)\n})", + "body": { + "contentType": "application/json", + "body": "{\n \"key\": \"<>\"\n}" + } + } + ], + "auth": { "authType": "inherit", "authActive": false }, + "headers": [] +} diff --git a/packages/hoppscotch-cli/src/__tests__/samples/environments/bulk-envs.json b/packages/hoppscotch-cli/src/__tests__/samples/environments/bulk-envs.json new file mode 100644 index 000000000..7c5633820 --- /dev/null +++ b/packages/hoppscotch-cli/src/__tests__/samples/environments/bulk-envs.json @@ -0,0 +1,32 @@ +[ + { + "v": 0, + "name": "Env-I", + "variables": [ + { + "key": "firstName", + "value": "John" + }, + { + "key": "lastName", + "value": "Doe" + } + ] + }, + { + "v": 1, + "id": "2", + "name": "Env-II", + "variables": [ + { + "key": "baseUrl", + "value": "https://echo.hoppscotch.io", + "secret": false + }, + { + "key": "secretVar", + "secret": true + } + ] + } +] diff --git a/packages/hoppscotch-cli/src/__tests__/samples/env-flag-envs.json b/packages/hoppscotch-cli/src/__tests__/samples/environments/env-flag-envs.json similarity index 100% rename from packages/hoppscotch-cli/src/__tests__/samples/env-flag-envs.json rename to packages/hoppscotch-cli/src/__tests__/samples/environments/env-flag-envs.json diff --git a/packages/hoppscotch-cli/src/__tests__/samples/environments/malformed-envs.json b/packages/hoppscotch-cli/src/__tests__/samples/environments/malformed-envs.json new file mode 100644 index 000000000..7d0752528 --- /dev/null +++ b/packages/hoppscotch-cli/src/__tests__/samples/environments/malformed-envs.json @@ -0,0 +1,16 @@ +{ + "id": 123, + "v": "1", + "name": "secret-envs", + "values": [ + { + "key": "secretVar", + "secret": true + }, + { + "key": "regularVar", + "secret": false, + "value": "regular-variable" + } + ] +} diff --git a/packages/hoppscotch-cli/src/__tests__/samples/req-body-env-vars-envs.json b/packages/hoppscotch-cli/src/__tests__/samples/environments/req-body-env-vars-envs.json similarity index 98% rename from packages/hoppscotch-cli/src/__tests__/samples/req-body-env-vars-envs.json rename to packages/hoppscotch-cli/src/__tests__/samples/environments/req-body-env-vars-envs.json index 323bbb881..eb3b90842 100644 --- a/packages/hoppscotch-cli/src/__tests__/samples/req-body-env-vars-envs.json +++ b/packages/hoppscotch-cli/src/__tests__/samples/environments/req-body-env-vars-envs.json @@ -1,4 +1,5 @@ { + "v": 0, "name": "Response body sample", "variables": [ { @@ -34,4 +35,4 @@ "value": "<> <>" } ] -} \ No newline at end of file +} diff --git a/packages/hoppscotch-cli/src/__tests__/samples/environments/secret-envs-persistence-scripting-envs.json b/packages/hoppscotch-cli/src/__tests__/samples/environments/secret-envs-persistence-scripting-envs.json new file mode 100644 index 000000000..b03e0508c --- /dev/null +++ b/packages/hoppscotch-cli/src/__tests__/samples/environments/secret-envs-persistence-scripting-envs.json @@ -0,0 +1,27 @@ +{ + "v": 1, + "id": "2", + "name": "secret-envs-persistence-scripting-envs", + "variables": [ + { + "key": "preReqVarOne", + "secret": true + }, + { + "key": "preReqVarTwo", + "secret": true + }, + { + "key": "postReqVarOne", + "secret": true + }, + { + "key": "preReqVarTwo", + "secret": true + }, + { + "key": "customHeaderValueFromSecretVar", + "secret": true + } + ] +} diff --git a/packages/hoppscotch-cli/src/__tests__/samples/environments/secret-envs.json b/packages/hoppscotch-cli/src/__tests__/samples/environments/secret-envs.json new file mode 100644 index 000000000..53d33bb35 --- /dev/null +++ b/packages/hoppscotch-cli/src/__tests__/samples/environments/secret-envs.json @@ -0,0 +1,40 @@ +{ + "id": "2", + "v": 1, + "name": "secret-envs", + "variables": [ + { + "key": "secretBearerToken", + "secret": true + }, + { + "key": "secretBasicAuthUsername", + "secret": true + }, + { + "key": "secretBasicAuthPassword", + "secret": true + }, + { + "key": "secretQueryParamValue", + "secret": true + }, + { + "key": "secretBodyValue", + "secret": true + }, + { + "key": "secretHeaderValue", + "secret": true + }, + { + "key": "nonExistentValueInSystemEnv", + "secret": true + }, + { + "key": "baseURL", + "value": "https://httpbin.org", + "secret": false + } + ] +} diff --git a/packages/hoppscotch-cli/src/__tests__/samples/environments/secret-supplied-values-envs.json b/packages/hoppscotch-cli/src/__tests__/samples/environments/secret-supplied-values-envs.json new file mode 100644 index 000000000..fec73a293 --- /dev/null +++ b/packages/hoppscotch-cli/src/__tests__/samples/environments/secret-supplied-values-envs.json @@ -0,0 +1,46 @@ +{ + "v": 1, + "id": "2", + "name": "secret-values-envs", + "variables": [ + { + "key": "secretBearerToken", + "value": "test-token", + "secret": true + }, + { + "key": "secretBasicAuthUsername", + "value": "test-user", + "secret": true + }, + { + "key": "secretBasicAuthPassword", + "value": "test-pass", + "secret": true + }, + { + "key": "secretQueryParamValue", + "value": "secret-query-param-value", + "secret": true + }, + { + "key": "secretBodyValue", + "value": "secret-body-value", + "secret": true + }, + { + "key": "secretHeaderValue", + "value": "secret-header-value", + "secret": true + }, + { + "key": "nonExistentValueInSystemEnv", + "secret": true + }, + { + "key": "baseURL", + "value": "https://httpbin.org", + "secret": false + } + ] +} diff --git a/packages/hoppscotch-cli/src/__tests__/utils.ts b/packages/hoppscotch-cli/src/__tests__/utils.ts index afe437136..f6068b2d6 100644 --- a/packages/hoppscotch-cli/src/__tests__/utils.ts +++ b/packages/hoppscotch-cli/src/__tests__/utils.ts @@ -3,13 +3,13 @@ import { resolve } from "path"; import { ExecResponse } from "./types"; -export const runCLI = (args: string): Promise => +export const runCLI = (args: string, options = {}): Promise => { const CLI_PATH = resolve(__dirname, "../../bin/hopp"); const command = `node ${CLI_PATH} ${args}` return new Promise((resolve) => - exec(command, (error, stdout, stderr) => resolve({ error, stdout, stderr })) + exec(command, options, (error, stdout, stderr) => resolve({ error, stdout, stderr })) ); } @@ -25,7 +25,12 @@ export const getErrorCode = (out: string) => { return ansiTrimmedStr.split(" ")[0]; }; -export const getTestJsonFilePath = (file: string) => { - const filePath = resolve(__dirname, `../../src/__tests__/samples/${file}`); +export const getTestJsonFilePath = (file: string, kind: "collection" | "environment") => { + const kindDir = { + collection: "collections", + environment: "environments", + }[kind]; + + const filePath = resolve(__dirname, `../../src/__tests__/samples/${kindDir}/${file}`); return filePath; }; diff --git a/packages/hoppscotch-cli/src/interfaces/request.ts b/packages/hoppscotch-cli/src/interfaces/request.ts index 6dd22576a..9eb3982f7 100644 --- a/packages/hoppscotch-cli/src/interfaces/request.ts +++ b/packages/hoppscotch-cli/src/interfaces/request.ts @@ -21,6 +21,7 @@ export interface RequestStack { */ export interface RequestConfig extends AxiosRequestConfig { supported: boolean; + displayUrl?: string } export interface EffectiveHoppRESTRequest extends HoppRESTRequest { @@ -30,6 +31,7 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest { * This contains path, params and environment variables all applied to it */ effectiveFinalURL: string; + effectiveFinalDisplayURL?: string; effectiveFinalHeaders: { key: string; value: string; active: boolean }[]; effectiveFinalParams: { key: string; value: string; active: boolean }[]; effectiveFinalBody: FormData | string | null; diff --git a/packages/hoppscotch-cli/src/options/test/env.ts b/packages/hoppscotch-cli/src/options/test/env.ts index 1cd8b140b..6e1c45271 100644 --- a/packages/hoppscotch-cli/src/options/test/env.ts +++ b/packages/hoppscotch-cli/src/options/test/env.ts @@ -1,34 +1,42 @@ +import { Environment } from "@hoppscotch/data"; +import { entityReference } from "verzod"; +import { z } from "zod"; + import { error } from "../../types/errors"; import { - HoppEnvs, - HoppEnvPair, HoppEnvKeyPairObject, - HoppEnvExportObject, - HoppBulkEnvExportObject, + HoppEnvPair, + HoppEnvs } from "../../types/request"; import { readJsonFile } from "../../utils/mutators"; + /** - * Parses env json file for given path and validates the parsed env json object. - * @param path Path of env.json file to be parsed. - * @returns For successful parsing we get HoppEnvs object. + * Parses env json file for given path and validates the parsed env json object + * @param path Path of env.json file to be parsed + * @returns For successful parsing we get HoppEnvs object */ export async function parseEnvsData(path: string) { const contents = await readJsonFile(path); - const envPairs: Array = []; - const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents); - const HoppEnvExportObjectResult = HoppEnvExportObject.safeParse(contents); - const HoppBulkEnvExportObjectResult = - HoppBulkEnvExportObject.safeParse(contents); + const envPairs: Array = []; - // CLI doesnt support bulk environments export. - // Hence we check for this case and throw an error if it matches the format. + // The legacy key-value pair format that is still supported + const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents); + + // Shape of the single environment export object that is exported from the app + const HoppEnvExportObjectResult = Environment.safeParse(contents); + + // Shape of the bulk environment export object that is exported from the app + const HoppBulkEnvExportObjectResult = z.array(entityReference(Environment)).safeParse(contents) + + // CLI doesnt support bulk environments export + // Hence we check for this case and throw an error if it matches the format if (HoppBulkEnvExportObjectResult.success) { throw error({ code: "BULK_ENV_FILE", path, data: error }); } - // Checks if the environment file is of the correct format. - // If it doesnt match either of them, we throw an error. - if (!(HoppEnvKeyPairResult.success || HoppEnvExportObjectResult.success)) { + // Checks if the environment file is of the correct format + // If it doesnt match either of them, we throw an error + if (!HoppEnvKeyPairResult.success && HoppEnvExportObjectResult.type === "err") { throw error({ code: "MALFORMED_ENV_FILE", path, data: error }); } @@ -36,8 +44,8 @@ export async function parseEnvsData(path: string) { for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) { envPairs.push({ key, value }); } - } else if (HoppEnvExportObjectResult.success) { - envPairs.push(...HoppEnvExportObjectResult.data.variables); + } else if (HoppEnvExportObjectResult.type === "ok") { + envPairs.push(...HoppEnvExportObjectResult.value.variables); } return { global: [], selected: envPairs }; diff --git a/packages/hoppscotch-cli/src/types/request.ts b/packages/hoppscotch-cli/src/types/request.ts index 2407542f1..21dffbd1c 100644 --- a/packages/hoppscotch-cli/src/types/request.ts +++ b/packages/hoppscotch-cli/src/types/request.ts @@ -1,31 +1,18 @@ -import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; +import { Environment, HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; +import { z } from "zod"; + import { TestReport } from "../interfaces/response"; import { HoppCLIError } from "./errors"; -import { z } from "zod"; export type FormDataEntry = { key: string; value: string | Blob; }; -export type HoppEnvPair = { key: string; value: string }; +export type HoppEnvPair = Environment["variables"][number]; export const HoppEnvKeyPairObject = z.record(z.string(), z.string()); -// Shape of the single environment export object that is exported from the app. -export const HoppEnvExportObject = z.object({ - name: z.string(), - variables: z.array( - z.object({ - key: z.string(), - value: z.string(), - }) - ), -}); - -// Shape of the bulk environment export object that is exported from the app. -export const HoppBulkEnvExportObject = z.array(HoppEnvExportObject); - export type HoppEnvs = { global: HoppEnvPair[]; selected: HoppEnvPair[]; diff --git a/packages/hoppscotch-cli/src/utils/display.ts b/packages/hoppscotch-cli/src/utils/display.ts index 8d6e35a69..f2461d109 100644 --- a/packages/hoppscotch-cli/src/utils/display.ts +++ b/packages/hoppscotch-cli/src/utils/display.ts @@ -176,7 +176,7 @@ export const printRequestRunner = { */ start: (requestConfig: RequestConfig) => { const METHOD = BG_INFO(` ${requestConfig.method} `); - const ENDPOINT = requestConfig.url; + const ENDPOINT = requestConfig.displayUrl || requestConfig.url; process.stdout.write(`${METHOD} ${ENDPOINT}`); }, diff --git a/packages/hoppscotch-cli/src/utils/pre-request.ts b/packages/hoppscotch-cli/src/utils/pre-request.ts index b180a4ac6..b576ac9aa 100644 --- a/packages/hoppscotch-cli/src/utils/pre-request.ts +++ b/packages/hoppscotch-cli/src/utils/pre-request.ts @@ -36,7 +36,10 @@ import { toFormData } from "./mutators"; export const preRequestScriptRunner = ( request: HoppRESTRequest, envs: HoppEnvs -): TE.TaskEither => +): TE.TaskEither< + HoppCLIError, + { effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs } +> => pipe( TE.of(request), TE.chain(({ preRequestScript }) => @@ -68,7 +71,10 @@ export const preRequestScriptRunner = ( export function getEffectiveRESTRequest( request: HoppRESTRequest, environment: Environment -): E.Either { +): E.Either< + HoppCLIError, + { effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs } +> { const envVariables = environment.variables; // Parsing final headers with applied ENVs. @@ -162,12 +168,30 @@ export function getEffectiveRESTRequest( } const effectiveFinalURL = _effectiveFinalURL.right; + // Secret environment variables referenced in the request endpoint should be masked + let effectiveFinalDisplayURL; + if (envVariables.some(({ secret }) => secret)) { + const _effectiveFinalDisplayURL = parseTemplateStringE( + request.endpoint, + envVariables, + true + ); + + if (E.isRight(_effectiveFinalDisplayURL)) { + effectiveFinalDisplayURL = _effectiveFinalDisplayURL.right; + } + } + return E.right({ - ...request, - effectiveFinalURL, - effectiveFinalHeaders, - effectiveFinalParams, - effectiveFinalBody, + effectiveRequest: { + ...request, + effectiveFinalURL, + effectiveFinalDisplayURL, + effectiveFinalHeaders, + effectiveFinalParams, + effectiveFinalBody, + }, + updatedEnvs: { global: [], selected: envVariables }, }); } diff --git a/packages/hoppscotch-cli/src/utils/request.ts b/packages/hoppscotch-cli/src/utils/request.ts index 343e0f199..3e15b2979 100644 --- a/packages/hoppscotch-cli/src/utils/request.ts +++ b/packages/hoppscotch-cli/src/utils/request.ts @@ -1,4 +1,4 @@ -import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; +import { Environment, HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; import axios, { Method } from "axios"; import * as A from "fp-ts/Array"; import * as E from "fp-ts/Either"; @@ -29,6 +29,38 @@ import { getTestScriptParams, hasFailedTestCases, testRunner } from "./test"; // !NOTE: The `config.supported` checks are temporary until OAuth2 and Multipart Forms are supported +/** + * Processes given variable, which includes checking for secret variables + * and getting value from system environment + * @param variable Variable to be processed + * @returns Updated variable with value from system environment + */ +const processVariables = (variable: Environment["variables"][number]) => { + if (variable.secret) { + return { + ...variable, + value: + "value" in variable ? variable.value : process.env[variable.key] || "", + } + } + return variable +} + +/** + * Processes given envs, which includes processing each variable in global + * and selected envs + * @param envs Global + selected envs used by requests with in collection + * @returns Processed envs with each variable processed + */ +const processEnvs = (envs: HoppEnvs) => { + const processedEnvs = { + global: envs.global.map(processVariables), + selected: envs.selected.map(processVariables), + } + + return processedEnvs +} + /** * Transforms given request data to request-config used by request-runner to * perform HTTP request. @@ -38,6 +70,7 @@ import { getTestScriptParams, hasFailedTestCases, testRunner } from "./test"; export const createRequest = (req: EffectiveHoppRESTRequest): RequestConfig => { const config: RequestConfig = { supported: true, + displayUrl: req.effectiveFinalDisplayURL }; const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest; const reqParams = finalParams(req); @@ -221,9 +254,13 @@ export const processRequest = effectiveFinalParams: [], effectiveFinalURL: "", }; + let updatedEnvs = {}; + + // Fetch values for secret environment variables from system environment + const processedEnvs = processEnvs(envs) // Executing pre-request-script - const preRequestRes = await preRequestScriptRunner(request, envs)(); + const preRequestRes = await preRequestScriptRunner(request, processedEnvs)(); if (E.isLeft(preRequestRes)) { printPreRequestRunner.fail(); @@ -231,8 +268,8 @@ export const processRequest = report.errors.push(preRequestRes.left); report.result = report.result && false; } else { - // Updating effective-request - effectiveRequest = preRequestRes.right; + // Updating effective-request and consuming updated envs after pre-request script execution + ({ effectiveRequest, updatedEnvs } = preRequestRes.right); } // Creating request-config for request-runner. @@ -270,7 +307,7 @@ export const processRequest = const testScriptParams = getTestScriptParams( _requestRunnerRes, request, - envs + updatedEnvs ); // Executing test-runner. diff --git a/packages/hoppscotch-data/src/environment/index.ts b/packages/hoppscotch-data/src/environment/index.ts index 94a4f04e1..3f801f2bd 100644 --- a/packages/hoppscotch-data/src/environment/index.ts +++ b/packages/hoppscotch-data/src/environment/index.ts @@ -105,6 +105,7 @@ export function parseTemplateStringE( while (result.match(REGEX_ENV_VAR) != null && depth <= ENV_MAX_EXPAND_LIMIT) { result = decodeURI(encodeURI(result)).replace(REGEX_ENV_VAR, (_, p1) => { const variable = variables.find((x) => x && x.key === p1) + if (variable && "value" in variable) { // Mask the value if it is a secret and explicitly specified if (variable.secret && maskValue) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d673550a9..5c376f6df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -351,6 +351,9 @@ importers: typescript: specifier: ^5.2.2 version: 5.2.2 + verzod: + specifier: ^0.2.2 + version: 0.2.2(zod@3.22.4) zod: specifier: ^3.22.4 version: 3.22.4 @@ -25770,7 +25773,6 @@ packages: zod: ^3.22.0 dependencies: zod: 3.22.4 - dev: false /vite-node@0.34.6(@types/node@18.18.8)(sass@1.69.5)(terser@5.27.0): resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==}