feat: added support for passing env.json file to test cmd (#2373)
This commit is contained in:
@@ -24,13 +24,26 @@ hopp [options or commands] arguments
|
|||||||
|
|
||||||
- Displays the help text
|
- Displays the help text
|
||||||
|
|
||||||
3. #### **`hopp test <file_path>`**
|
3. #### **`hopp test [options] <file_path>`**
|
||||||
- Interactive CLI to accept Hoppscotch collection JSON path
|
- Interactive CLI to accept Hoppscotch collection JSON path
|
||||||
- Parses the collection JSON and executes each requests
|
- Parses the collection JSON and executes each requests
|
||||||
- Executes pre-request script.
|
- Executes pre-request script.
|
||||||
- Outputs the response of each request.
|
- Outputs the response of each request.
|
||||||
- Executes and outputs test-script response.
|
- Executes and outputs test-script response.
|
||||||
|
|
||||||
|
#### Options:
|
||||||
|
##### `-e <file_path>` / `--env <file_path>`
|
||||||
|
- Accepts path to env.json with contents in below format:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ENV1":"value1",
|
||||||
|
"ENV2":"value2"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- You can now access those variables using `pw.env.get('<var_name>')`
|
||||||
|
|
||||||
|
Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Install [@hoppscotch/cli](https://www.npmjs.com/package/@hoppscotch/cli) from npm by running:
|
Install [@hoppscotch/cli](https://www.npmjs.com/package/@hoppscotch/cli) from npm by running:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ describe("Test 'hopp test <file>' command:", () => {
|
|||||||
const { stdout } = await execAsync(cmd);
|
const { stdout } = await execAsync(cmd);
|
||||||
const out = getErrorCode(stdout);
|
const out = getErrorCode(stdout);
|
||||||
|
|
||||||
expect(out).toBe<HoppErrorCode>("NO_FILE_PATH");
|
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Collection file not found.", async () => {
|
test("Collection file not found.", async () => {
|
||||||
@@ -42,7 +42,7 @@ describe("Test 'hopp test <file>' command:", () => {
|
|||||||
const { stdout } = await execAsync(cmd);
|
const { stdout } = await execAsync(cmd);
|
||||||
const out = getErrorCode(stdout);
|
const out = getErrorCode(stdout);
|
||||||
|
|
||||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_JSON");
|
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Some errors occured (exit code 1).", async () => {
|
test("Some errors occured (exit code 1).", async () => {
|
||||||
@@ -62,3 +62,42 @@ describe("Test 'hopp test <file>' command:", () => {
|
|||||||
expect(error).toBeNull();
|
expect(error).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Test 'hopp test <file> --env <file>' command:", () => {
|
||||||
|
const VALID_TEST_CMD = `node ./bin/hopp test ${getTestJsonFilePath(
|
||||||
|
"passes.json"
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
test("No env file path provided.", async () => {
|
||||||
|
const cmd = `${VALID_TEST_CMD} --env`;
|
||||||
|
const { stdout } = await execAsync(cmd);
|
||||||
|
const out = getErrorCode(stdout);
|
||||||
|
|
||||||
|
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ENV file not JSON type.", async () => {
|
||||||
|
const cmd = `${VALID_TEST_CMD} --env ${getTestJsonFilePath("notjson.txt")}`;
|
||||||
|
const { stdout } = await execAsync(cmd);
|
||||||
|
const out = getErrorCode(stdout);
|
||||||
|
|
||||||
|
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ENV file not found.", async () => {
|
||||||
|
const cmd = `${VALID_TEST_CMD} --env notfound.json`;
|
||||||
|
const { stdout } = await execAsync(cmd);
|
||||||
|
const out = getErrorCode(stdout);
|
||||||
|
|
||||||
|
expect(out).toBe<HoppErrorCode>("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");
|
||||||
|
// const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
|
||||||
|
// const { error } = await execAsync(cmd);
|
||||||
|
|
||||||
|
// expect(error).toBeNull();
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { HoppCLIError } from "../../../types/errors";
|
import { HoppCLIError } from "../../../types/errors";
|
||||||
import { checkFilePath } from "../../../utils/checks";
|
import { checkFile } from "../../../utils/checks";
|
||||||
|
|
||||||
describe("checkFilePath", () => {
|
import "@relmify/jest-fp-ts";
|
||||||
|
|
||||||
|
describe("checkFile", () => {
|
||||||
test("File doesn't exists.", () => {
|
test("File doesn't exists.", () => {
|
||||||
return expect(
|
return expect(
|
||||||
checkFilePath("./src/samples/this-file-not-exists.json")()
|
checkFile("./src/samples/this-file-not-exists.json")()
|
||||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
||||||
code: "FILE_NOT_FOUND",
|
code: "FILE_NOT_FOUND",
|
||||||
});
|
});
|
||||||
@@ -12,15 +14,15 @@ describe("checkFilePath", () => {
|
|||||||
|
|
||||||
test("File not of JSON type.", () => {
|
test("File not of JSON type.", () => {
|
||||||
return expect(
|
return expect(
|
||||||
checkFilePath("./src/__tests__/samples/notjson.txt")()
|
checkFile("./src/__tests__/samples/notjson.txt")()
|
||||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
||||||
code: "FILE_NOT_JSON",
|
code: "INVALID_FILE_TYPE",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Existing JSON file.", () => {
|
test("Existing JSON file.", () => {
|
||||||
return expect(
|
return expect(
|
||||||
checkFilePath("./src/__tests__/samples/passes.json")()
|
checkFile("./src/__tests__/samples/passes.json")()
|
||||||
).resolves.toBeRight();
|
).resolves.toBeRight();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -37,6 +37,8 @@ const SAMPLE_RESOLVED_RESPONSE = <AxiosResponse>{
|
|||||||
headers: [],
|
headers: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SAMPLE_ENVS = { global: [], selected: [] };
|
||||||
|
|
||||||
describe("collectionsRunner", () => {
|
describe("collectionsRunner", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
@@ -47,19 +49,24 @@ describe("collectionsRunner", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Empty HoppCollection.", () => {
|
test("Empty HoppCollection.", () => {
|
||||||
return expect(collectionsRunner([])()).resolves.toStrictEqual([]);
|
return expect(
|
||||||
|
collectionsRunner({ collections: [], envs: SAMPLE_ENVS })()
|
||||||
|
).resolves.toStrictEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Empty requests and folders in collection.", () => {
|
test("Empty requests and folders in collection.", () => {
|
||||||
return expect(
|
return expect(
|
||||||
collectionsRunner([
|
collectionsRunner({
|
||||||
{
|
collections: [
|
||||||
v: 1,
|
{
|
||||||
name: "name",
|
v: 1,
|
||||||
folders: [],
|
name: "name",
|
||||||
requests: [],
|
folders: [],
|
||||||
},
|
requests: [],
|
||||||
])()
|
},
|
||||||
|
],
|
||||||
|
envs: SAMPLE_ENVS,
|
||||||
|
})()
|
||||||
).resolves.toMatchObject([]);
|
).resolves.toMatchObject([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,14 +74,17 @@ describe("collectionsRunner", () => {
|
|||||||
(axios as unknown as jest.Mock).mockResolvedValue(SAMPLE_RESOLVED_RESPONSE);
|
(axios as unknown as jest.Mock).mockResolvedValue(SAMPLE_RESOLVED_RESPONSE);
|
||||||
|
|
||||||
return expect(
|
return expect(
|
||||||
collectionsRunner([
|
collectionsRunner({
|
||||||
{
|
collections: [
|
||||||
v: 1,
|
{
|
||||||
name: "collection",
|
v: 1,
|
||||||
folders: [],
|
name: "collection",
|
||||||
requests: [SAMPLE_HOPP_REQUEST],
|
folders: [],
|
||||||
},
|
requests: [SAMPLE_HOPP_REQUEST],
|
||||||
])()
|
},
|
||||||
|
],
|
||||||
|
envs: SAMPLE_ENVS,
|
||||||
|
})()
|
||||||
).resolves.toMatchObject([
|
).resolves.toMatchObject([
|
||||||
{
|
{
|
||||||
path: "collection/request",
|
path: "collection/request",
|
||||||
@@ -89,21 +99,24 @@ describe("collectionsRunner", () => {
|
|||||||
(axios as unknown as jest.Mock).mockResolvedValue(SAMPLE_RESOLVED_RESPONSE);
|
(axios as unknown as jest.Mock).mockResolvedValue(SAMPLE_RESOLVED_RESPONSE);
|
||||||
|
|
||||||
return expect(
|
return expect(
|
||||||
collectionsRunner([
|
collectionsRunner({
|
||||||
{
|
collections: [
|
||||||
v: 1,
|
{
|
||||||
name: "collection",
|
v: 1,
|
||||||
folders: [
|
name: "collection",
|
||||||
{
|
folders: [
|
||||||
v: 1,
|
{
|
||||||
name: "folder",
|
v: 1,
|
||||||
folders: [],
|
name: "folder",
|
||||||
requests: [SAMPLE_HOPP_REQUEST],
|
folders: [],
|
||||||
},
|
requests: [SAMPLE_HOPP_REQUEST],
|
||||||
],
|
},
|
||||||
requests: [],
|
],
|
||||||
},
|
requests: [],
|
||||||
])()
|
},
|
||||||
|
],
|
||||||
|
envs: SAMPLE_ENVS,
|
||||||
|
})()
|
||||||
).resolves.toMatchObject([
|
).resolves.toMatchObject([
|
||||||
{
|
{
|
||||||
path: "collection/folder/request",
|
path: "collection/folder/request",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Environment } from "@hoppscotch/data";
|
import { Environment } from "@hoppscotch/data";
|
||||||
import { getEffectiveFinalMetaData } from "../../../utils/getters";
|
import { getEffectiveFinalMetaData } from "../../../utils/getters";
|
||||||
|
|
||||||
|
import "@relmify/jest-fp-ts";
|
||||||
|
|
||||||
const DEFAULT_ENV = <Environment>{
|
const DEFAULT_ENV = <Environment>{
|
||||||
name: "name",
|
name: "name",
|
||||||
variables: [{ key: "PARAM", value: "parsed_param" }],
|
variables: [{ key: "PARAM", value: "parsed_param" }],
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { HoppCLIError } from "../../../types/errors";
|
import { HoppCLIError } from "../../../types/errors";
|
||||||
import { parseCollectionData } from "../../../utils/mutators";
|
import { parseCollectionData } from "../../../utils/mutators";
|
||||||
|
|
||||||
|
import "@relmify/jest-fp-ts";
|
||||||
|
|
||||||
describe("parseCollectionData", () => {
|
describe("parseCollectionData", () => {
|
||||||
test("Reading non-existing file.", () => {
|
test("Reading non-existing file.", () => {
|
||||||
return expect(
|
return expect(
|
||||||
parseCollectionData("./src/__tests__/samples/notexist.txt")()
|
parseCollectionData("./src/__tests__/samples/notexist.json")()
|
||||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
||||||
code: "UNKNOWN_ERROR",
|
code: "FILE_NOT_FOUND",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { EffectiveHoppRESTRequest } from "../../../interfaces/request";
|
|||||||
import { HoppCLIError } from "../../../types/errors";
|
import { HoppCLIError } from "../../../types/errors";
|
||||||
import { getEffectiveRESTRequest } from "../../../utils/pre-request";
|
import { getEffectiveRESTRequest } from "../../../utils/pre-request";
|
||||||
|
|
||||||
|
import "@relmify/jest-fp-ts";
|
||||||
|
|
||||||
const DEFAULT_ENV = <Environment>{
|
const DEFAULT_ENV = <Environment>{
|
||||||
name: "name",
|
name: "name",
|
||||||
variables: [
|
variables: [
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"URL": "https://echo.hoppscotch.io",
|
||||||
|
"HOST": "echo.hoppscotch.io",
|
||||||
|
"X-COUNTRY": "IN",
|
||||||
|
"BODY_VALUE": "body_value",
|
||||||
|
"BODY_KEY": "body_key"
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"name": "env-flag-tests",
|
||||||
|
"folders": [],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"v": "1",
|
||||||
|
"endpoint": "<<URL>>",
|
||||||
|
"name": "test1",
|
||||||
|
"params": [],
|
||||||
|
"headers": [],
|
||||||
|
"method": "POST",
|
||||||
|
"auth": { "authType": "none", "authActive": true },
|
||||||
|
"preRequestScript": "",
|
||||||
|
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst X_COUNTRY = pw.env.get(\"X-COUNTRY\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n\t pw.expect(pw.response.body.headers[\"x-country\"]).toBe(X_COUNTRY); \n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
|
||||||
|
"body": {
|
||||||
|
"contentType": "application/json",
|
||||||
|
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -6,14 +6,15 @@ import {
|
|||||||
collectionsRunnerResult,
|
collectionsRunnerResult,
|
||||||
} from "../utils/collections";
|
} from "../utils/collections";
|
||||||
import { handleError } from "../handlers/error";
|
import { handleError } from "../handlers/error";
|
||||||
import { checkFilePath } from "../utils/checks";
|
|
||||||
import { parseCollectionData } from "../utils/mutators";
|
import { parseCollectionData } from "../utils/mutators";
|
||||||
|
import { parseEnvsData } from "../options/test/env";
|
||||||
|
import { TestCmdOptions } from "../types/commands";
|
||||||
|
|
||||||
export const test = (path: string) => async () => {
|
export const test = (path: string, options: TestCmdOptions) => async () => {
|
||||||
await pipe(
|
await pipe(
|
||||||
path,
|
TE.Do,
|
||||||
checkFilePath,
|
TE.bind("envs", () => parseEnvsData(options.env)),
|
||||||
TE.chain(parseCollectionData),
|
TE.bind("collections", () => parseCollectionData(path)),
|
||||||
TE.chainTaskK(collectionsRunner),
|
TE.chainTaskK(collectionsRunner),
|
||||||
TE.chainW(flow(collectionsRunnerResult, collectionsRunnerExit, TE.of)),
|
TE.chainW(flow(collectionsRunnerResult, collectionsRunnerExit, TE.of)),
|
||||||
TE.mapLeft((e) => {
|
TE.mapLeft((e) => {
|
||||||
|
|||||||
@@ -48,9 +48,7 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
|
|||||||
case "UNKNOWN_COMMAND":
|
case "UNKNOWN_COMMAND":
|
||||||
ERROR_MSG = `Unavailable command: ${error.command}`;
|
ERROR_MSG = `Unavailable command: ${error.command}`;
|
||||||
break;
|
break;
|
||||||
case "FILE_NOT_JSON":
|
case "MALFORMED_ENV_FILE":
|
||||||
ERROR_MSG = `Please check file type: ${error.path}`;
|
|
||||||
break;
|
|
||||||
case "MALFORMED_COLLECTION":
|
case "MALFORMED_COLLECTION":
|
||||||
ERROR_MSG = `${error.path}\n${parseErrorData(error.data)}`;
|
ERROR_MSG = `${error.path}\n${parseErrorData(error.data)}`;
|
||||||
break;
|
break;
|
||||||
@@ -60,6 +58,9 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
|
|||||||
case "PARSING_ERROR":
|
case "PARSING_ERROR":
|
||||||
ERROR_MSG = `Unable to parse -\n${error.data}`;
|
ERROR_MSG = `Unable to parse -\n${error.data}`;
|
||||||
break;
|
break;
|
||||||
|
case "INVALID_FILE_TYPE":
|
||||||
|
ERROR_MSG = `Please provide file of extension type: ${error.data}`;
|
||||||
|
break;
|
||||||
case "REQUEST_ERROR":
|
case "REQUEST_ERROR":
|
||||||
case "TEST_SCRIPT_ERROR":
|
case "TEST_SCRIPT_ERROR":
|
||||||
case "PRE_REQUEST_SCRIPT_ERROR":
|
case "PRE_REQUEST_SCRIPT_ERROR":
|
||||||
|
|||||||
@@ -5,14 +5,16 @@ import { version } from "../package.json";
|
|||||||
import { test } from "./commands/test";
|
import { test } from "./commands/test";
|
||||||
import { handleError } from "./handlers/error";
|
import { handleError } from "./handlers/error";
|
||||||
|
|
||||||
const accent = chalk.greenBright
|
const accent = chalk.greenBright;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* * Program Default Configuration
|
* * Program Default Configuration
|
||||||
*/
|
*/
|
||||||
const CLI_BEFORE_ALL_TXT = `hopp: The ${accent(
|
const CLI_BEFORE_ALL_TXT = `hopp: The ${accent(
|
||||||
"Hoppscotch"
|
"Hoppscotch"
|
||||||
)} CLI - Version ${version} (${accent("https://hoppscotch.io")}) ${chalk.black.bold.bgYellowBright(" ALPHA ")} \n`;
|
)} CLI - Version ${version} (${accent(
|
||||||
|
"https://hoppscotch.io"
|
||||||
|
)}) ${chalk.black.bold.bgYellowBright(" ALPHA ")} \n`;
|
||||||
|
|
||||||
const CLI_AFTER_ALL_TXT = `\nFor more help, head on to ${accent(
|
const CLI_AFTER_ALL_TXT = `\nFor more help, head on to ${accent(
|
||||||
"https://docs.hoppscotch.io/cli"
|
"https://docs.hoppscotch.io/cli"
|
||||||
@@ -44,14 +46,18 @@ program.exitOverride().configureOutput({
|
|||||||
program
|
program
|
||||||
.command("test")
|
.command("test")
|
||||||
.argument(
|
.argument(
|
||||||
"[file]",
|
"<file_path>",
|
||||||
"path to a hoppscotch collection.json file for CI testing"
|
"path to a hoppscotch collection.json file for CI testing"
|
||||||
)
|
)
|
||||||
|
.option("-e, --env <file_path>", "path to an environment variables json file")
|
||||||
.allowExcessArguments(false)
|
.allowExcessArguments(false)
|
||||||
.allowUnknownOption(false)
|
.allowUnknownOption(false)
|
||||||
.description("running hoppscotch collection.json file")
|
.description("running hoppscotch collection.json file")
|
||||||
.addHelpText("after", `\nFor help, head on to ${accent("https://docs.hoppscotch.io/cli#test")}`)
|
.addHelpText(
|
||||||
.action(async (path) => await test(path)());
|
"after",
|
||||||
|
`\nFor help, head on to ${accent("https://docs.hoppscotch.io/cli#test")}`
|
||||||
|
)
|
||||||
|
.action(async (path, options) => await test(path, options)());
|
||||||
|
|
||||||
export const cli = async (args: string[]) => {
|
export const cli = async (args: string[]) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
64
packages/hoppscotch-cli/src/options/test/env.ts
Normal file
64
packages/hoppscotch-cli/src/options/test/env.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import fs from "fs/promises";
|
||||||
|
import { pipe } from "fp-ts/function";
|
||||||
|
import * as TE from "fp-ts/TaskEither";
|
||||||
|
import * as E from "fp-ts/Either";
|
||||||
|
import * as J from "fp-ts/Json";
|
||||||
|
import * as A from "fp-ts/Array";
|
||||||
|
import * as S from "fp-ts/string";
|
||||||
|
import isArray from "lodash/isArray";
|
||||||
|
import { HoppCLIError, error } from "../../types/errors";
|
||||||
|
import { HoppEnvs, HoppEnvPair } from "../../types/request";
|
||||||
|
import { checkFile } from "../../utils/checks";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 const parseEnvsData = (
|
||||||
|
path: unknown
|
||||||
|
): TE.TaskEither<HoppCLIError, HoppEnvs> =>
|
||||||
|
!S.isString(path)
|
||||||
|
? TE.right({ global: [], selected: [] })
|
||||||
|
: pipe(
|
||||||
|
// Checking if the env.json file exists or not.
|
||||||
|
checkFile(path),
|
||||||
|
|
||||||
|
// Trying to read given env json file path.
|
||||||
|
TE.chainW((checkedPath) =>
|
||||||
|
TE.tryCatch(
|
||||||
|
() => fs.readFile(checkedPath),
|
||||||
|
(reason) =>
|
||||||
|
error({ code: "UNKNOWN_ERROR", data: E.toError(reason) })
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Trying to JSON parse the read file data and mapping the entries to HoppEnvPairs.
|
||||||
|
TE.chainEitherKW((data) =>
|
||||||
|
pipe(
|
||||||
|
data.toString(),
|
||||||
|
J.parse,
|
||||||
|
E.map((jsonData) =>
|
||||||
|
jsonData && typeof jsonData === "object" && !isArray(jsonData)
|
||||||
|
? pipe(
|
||||||
|
jsonData,
|
||||||
|
Object.entries,
|
||||||
|
A.map(
|
||||||
|
([key, value]) =>
|
||||||
|
<HoppEnvPair>{
|
||||||
|
key,
|
||||||
|
value: S.isString(value)
|
||||||
|
? value
|
||||||
|
: JSON.stringify(value),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
),
|
||||||
|
E.map((envPairs) => <HoppEnvs>{ global: [], selected: envPairs }),
|
||||||
|
E.mapLeft((e) =>
|
||||||
|
error({ code: "MALFORMED_ENV_FILE", path, data: E.toError(e) })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
9
packages/hoppscotch-cli/src/types/collections.ts
Normal file
9
packages/hoppscotch-cli/src/types/collections.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||||
|
import { HoppEnvs } from "./request";
|
||||||
|
|
||||||
|
export type CollectionRunnerParam = {
|
||||||
|
collections: HoppCollection<HoppRESTRequest>[];
|
||||||
|
envs: HoppEnvs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HoppCollectionFileExt = "json";
|
||||||
5
packages/hoppscotch-cli/src/types/commands.ts
Normal file
5
packages/hoppscotch-cli/src/types/commands.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export type TestCmdOptions = {
|
||||||
|
env: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HoppEnvFileExt = "json";
|
||||||
@@ -15,7 +15,6 @@ type HoppErrors = {
|
|||||||
FILE_NOT_FOUND: HoppErrorPath;
|
FILE_NOT_FOUND: HoppErrorPath;
|
||||||
UNKNOWN_COMMAND: HoppErrorCmd;
|
UNKNOWN_COMMAND: HoppErrorCmd;
|
||||||
MALFORMED_COLLECTION: HoppErrorPath & HoppErrorData;
|
MALFORMED_COLLECTION: HoppErrorPath & HoppErrorData;
|
||||||
FILE_NOT_JSON: HoppErrorPath;
|
|
||||||
NO_FILE_PATH: {};
|
NO_FILE_PATH: {};
|
||||||
PRE_REQUEST_SCRIPT_ERROR: HoppErrorData;
|
PRE_REQUEST_SCRIPT_ERROR: HoppErrorData;
|
||||||
PARSING_ERROR: HoppErrorData;
|
PARSING_ERROR: HoppErrorData;
|
||||||
@@ -24,6 +23,8 @@ type HoppErrors = {
|
|||||||
SYNTAX_ERROR: HoppErrorData;
|
SYNTAX_ERROR: HoppErrorData;
|
||||||
REQUEST_ERROR: HoppErrorData;
|
REQUEST_ERROR: HoppErrorData;
|
||||||
INVALID_ARGUMENT: HoppErrorData;
|
INVALID_ARGUMENT: HoppErrorData;
|
||||||
|
MALFORMED_ENV_FILE: HoppErrorPath & HoppErrorData;
|
||||||
|
INVALID_FILE_TYPE: HoppErrorData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HoppErrorCode = keyof HoppErrors;
|
export type HoppErrorCode = keyof HoppErrors;
|
||||||
|
|||||||
@@ -7,15 +7,11 @@ export type FormDataEntry = {
|
|||||||
value: string | Blob;
|
value: string | Blob;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type HoppEnvPair = { key: string; value: string };
|
||||||
|
|
||||||
export type HoppEnvs = {
|
export type HoppEnvs = {
|
||||||
global: {
|
global: HoppEnvPair[];
|
||||||
key: string;
|
selected: HoppEnvPair[];
|
||||||
value: string;
|
|
||||||
}[];
|
|
||||||
selected: {
|
|
||||||
key: string;
|
|
||||||
value: string;
|
|
||||||
}[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CollectionStack = {
|
export type CollectionStack = {
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ import {
|
|||||||
import * as A from "fp-ts/Array";
|
import * as A from "fp-ts/Array";
|
||||||
import * as S from "fp-ts/string";
|
import * as S from "fp-ts/string";
|
||||||
import * as TE from "fp-ts/TaskEither";
|
import * as TE from "fp-ts/TaskEither";
|
||||||
import { error, HoppCLIError, HoppErrnoException } from "../types/errors";
|
import * as E from "fp-ts/Either";
|
||||||
|
import curryRight from "lodash/curryRight";
|
||||||
import { CommanderError } from "commander";
|
import { CommanderError } from "commander";
|
||||||
|
import { error, HoppCLIError, HoppErrnoException } from "../types/errors";
|
||||||
|
import { HoppCollectionFileExt } from "../types/collections";
|
||||||
|
import { HoppEnvFileExt } from "../types/commands";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether an object has a property with given name.
|
* Determines whether an object has a property with given name.
|
||||||
@@ -68,42 +72,56 @@ export const isRESTCollection = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the given file path exists and is of JSON type.
|
* Checks if the file path matches the requried file type with of required extension.
|
||||||
|
* @param path The input file path to check.
|
||||||
|
* @param extension The required extension for input file path.
|
||||||
|
* @returns Absolute path for valid file extension OR HoppCLIError in case of error.
|
||||||
|
*/
|
||||||
|
export const checkFileExt = curryRight(
|
||||||
|
(
|
||||||
|
path: unknown,
|
||||||
|
extension: HoppCollectionFileExt | HoppEnvFileExt
|
||||||
|
): E.Either<HoppCLIError, string> =>
|
||||||
|
pipe(
|
||||||
|
path,
|
||||||
|
E.fromPredicate(S.isString, (_) => error({ code: "NO_FILE_PATH" })),
|
||||||
|
E.chainW(
|
||||||
|
E.fromPredicate(S.endsWith(`.${extension}`), (_) =>
|
||||||
|
error({ code: "INVALID_FILE_TYPE", data: extension })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given file path exists and is of given type.
|
||||||
* @param path The input file path to check.
|
* @param path The input file path to check.
|
||||||
* @returns Absolute path for valid file path OR HoppCLIError in case of error.
|
* @returns Absolute path for valid file path OR HoppCLIError in case of error.
|
||||||
*/
|
*/
|
||||||
export const checkFilePath = (
|
export const checkFile = (path: unknown): TE.TaskEither<HoppCLIError, string> =>
|
||||||
path: string
|
|
||||||
): TE.TaskEither<HoppCLIError, string> =>
|
|
||||||
pipe(
|
pipe(
|
||||||
path,
|
path,
|
||||||
|
|
||||||
/**
|
// Checking if path is string.
|
||||||
* Check the path type and returns string if passes else HoppCLIError.
|
|
||||||
*/
|
|
||||||
TE.fromPredicate(S.isString, () => error({ code: "NO_FILE_PATH" })),
|
TE.fromPredicate(S.isString, () => error({ code: "NO_FILE_PATH" })),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After checking file path, we map file path to absolute path and check
|
||||||
|
* if file is of given extension type.
|
||||||
|
*/
|
||||||
|
TE.map(join),
|
||||||
|
TE.chainEitherK(checkFileExt("json")),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trying to access given file path.
|
* Trying to access given file path.
|
||||||
* If successfully accessed, we return the path from predicate step.
|
* If successfully accessed, we return the path from predicate step.
|
||||||
* Else return HoppCLIError with code FILE_NOT_FOUND.
|
* Else return HoppCLIError with code FILE_NOT_FOUND.
|
||||||
*/
|
*/
|
||||||
TE.chainFirstW(
|
TE.chainFirstW((checkedPath) =>
|
||||||
TE.tryCatchK(
|
TE.tryCatchK(
|
||||||
() => pipe(path, join, fs.access),
|
() => fs.access(checkedPath),
|
||||||
() => error({ code: "FILE_NOT_FOUND", path: path })
|
() => error({ code: "FILE_NOT_FOUND", path: checkedPath })
|
||||||
)
|
)()
|
||||||
),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On successfully accessing given file path, we map file path to
|
|
||||||
* absolute path and return abs file path if file is JSON type.
|
|
||||||
*/
|
|
||||||
TE.map(join),
|
|
||||||
TE.chainW(
|
|
||||||
TE.fromPredicate(S.endsWith(".json"), (absPath) =>
|
|
||||||
error({ code: "FILE_NOT_JSON", path: absPath })
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -27,21 +27,24 @@ import {
|
|||||||
import { getTestMetrics } from "./test";
|
import { getTestMetrics } from "./test";
|
||||||
import { DEFAULT_DURATION_PRECISION } from "./constants";
|
import { DEFAULT_DURATION_PRECISION } from "./constants";
|
||||||
import { getPreRequestMetrics } from "./pre-request";
|
import { getPreRequestMetrics } from "./pre-request";
|
||||||
|
import { CollectionRunnerParam } from "../types/collections";
|
||||||
|
|
||||||
const { WARN, FAIL } = exceptionColors;
|
const { WARN, FAIL } = exceptionColors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes each requests within collections to prints details of subsequent requests,
|
* Processes each requests within collections to prints details of subsequent requests,
|
||||||
* tests and to display complete errors-report, failed-tests-report and test-metrics.
|
* tests and to display complete errors-report, failed-tests-report and test-metrics.
|
||||||
* @param collections Array of hopp-collection with hopp-requests to be processed.
|
* @param param Data of hopp-collection with hopp-requests, envs to be processed.
|
||||||
* @returns List of report for each processed request.
|
* @returns List of report for each processed request.
|
||||||
*/
|
*/
|
||||||
export const collectionsRunner =
|
export const collectionsRunner =
|
||||||
(collections: HoppCollection<HoppRESTRequest>[]): T.Task<RequestReport[]> =>
|
(param: CollectionRunnerParam): T.Task<RequestReport[]> =>
|
||||||
async () => {
|
async () => {
|
||||||
const envs: HoppEnvs = { global: [], selected: [] };
|
const envs: HoppEnvs = param.envs;
|
||||||
const requestsReport: RequestReport[] = [];
|
const requestsReport: RequestReport[] = [];
|
||||||
const collectionStack: CollectionStack[] = getCollectionStack(collections);
|
const collectionStack: CollectionStack[] = getCollectionStack(
|
||||||
|
param.collections
|
||||||
|
);
|
||||||
|
|
||||||
while (collectionStack.length) {
|
while (collectionStack.length) {
|
||||||
// Pop out top-most collection from stack to be processed.
|
// Pop out top-most collection from stack to be processed.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import * as J from "fp-ts/Json";
|
|||||||
import { pipe } from "fp-ts/function";
|
import { pipe } from "fp-ts/function";
|
||||||
import { FormDataEntry } from "../types/request";
|
import { FormDataEntry } from "../types/request";
|
||||||
import { error, HoppCLIError } from "../types/errors";
|
import { error, HoppCLIError } from "../types/errors";
|
||||||
import { isRESTCollection, isHoppErrnoException } from "./checks";
|
import { isRESTCollection, isHoppErrnoException, checkFile } from "./checks";
|
||||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,10 +49,17 @@ export const parseCollectionData = (
|
|||||||
path: string
|
path: string
|
||||||
): TE.TaskEither<HoppCLIError, HoppCollection<HoppRESTRequest>[]> =>
|
): TE.TaskEither<HoppCLIError, HoppCollection<HoppRESTRequest>[]> =>
|
||||||
pipe(
|
pipe(
|
||||||
|
TE.of(path),
|
||||||
|
|
||||||
|
// Checking if given file path exists or not.
|
||||||
|
TE.chain(checkFile),
|
||||||
|
|
||||||
// Trying to read give collection json path.
|
// Trying to read give collection json path.
|
||||||
TE.tryCatch(
|
TE.chainW((checkedPath) =>
|
||||||
() => pipe(path, fs.readFile),
|
TE.tryCatch(
|
||||||
(reason) => error({ code: "UNKNOWN_ERROR", data: E.toError(reason) })
|
() => fs.readFile(checkedPath),
|
||||||
|
(reason) => error({ code: "UNKNOWN_ERROR", data: E.toError(reason) })
|
||||||
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
// Checking if parsed file data is array.
|
// Checking if parsed file data is array.
|
||||||
|
|||||||
Reference in New Issue
Block a user