@testrigor/testrigor-cli 0.0.44-beta → 0.0.46-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,18 +19,19 @@ This is a command line tool for TestRigor. It allows you to run TestRigor tests
19
19
  8. With rules `testrigor test-suite run <test-suite-id> --token <token> --rules-path rules/**/*.yaml`
20
20
  9. With settings `testrigor test-suite run <test-suite-id> --token <token> --settings-path settings.yaml` (Can be YAML or JSON)
21
21
  10. Run a single test case `testrigor test-suite run <test-suite-id> --token <token> --test-case-uuid <test-case-uuid>`
22
- 11. Run with auto create AI rules enabled `testrigor test-suite run <test-suite-id> --token <token> --test-cases-path test_cases/**/*.{yaml,txt} --auto-create-ai-rules`
23
- 12. Run with explicit mutations enabled `testrigor test-suite run <test-suite-id> --token <token> --test-cases-path test_cases/**/*.{yaml,txt} --explicit-mutations`
24
- 13. With all options `testrigor test-suite run <test-suite-id> --token <token> --url <url> --branch <branch> --commit <commit> --variables-path file.json --localhost --test-cases-path test_cases/**/*.{yaml,txt} --rules-path rules/**/*.{yaml,txt} --auto-create-ai-rules --explicit-mutations`
25
- 14. Save JUnit report to `./junit-report.xml` (works only in sync mode): `testrigor test-suite run <test-suite-id> --token <token> --junit-report-save-path ./report.xml`
26
- 15. With comma-separated labels `testrigor test-suite run <test-suite-id> --token <token> --labels <labels>`
27
- 16. Excluding comma-separated labels `testrigor test-suite run <test-suite-id> --token <token> --excluded-labels <excluded-labels>`
28
- 17. With different WEB and API URLs `testrigor test-suite run <test-suite-id> --token <token> --base-url-web=<base-web-url> --base-url-api=<base_api_url>`
29
- 18. Set the default test suite ID: `testrigor test-suite config --default <test-suite-id>`
30
- 19. Remove the default test suite ID: `testrigor test-suite config --delete`
31
- 20. Show current configuration: `testrigor test-suite config --show`
32
- 21. Authenticate with TestRigor and store credentials (interactive prompt): `testrigor authenticate`
33
- 22. Run with override chrome extensions at runtime (one or more chrome extensions): `test-suite run <test-suite-id> --token <token> --chrome-extension "name=<extension-name> file=<extension-file>" --chrome-extension "name=<extension-name> file=<extension-file>"`
22
+ 11. Run multiple test cases `testrigor test-suite run <test-suite-id> --token <token> --test-case-uuid <test-case-uuid-1> --test-case-uuid <test-case-uuid-2>`
23
+ 12. Run with auto create AI rules enabled `testrigor test-suite run <test-suite-id> --token <token> --test-cases-path test_cases/**/*.{yaml,txt} --auto-create-ai-rules`
24
+ 13. Run with explicit mutations enabled `testrigor test-suite run <test-suite-id> --token <token> --test-cases-path test_cases/**/*.{yaml,txt} --explicit-mutations`
25
+ 14. With all options `testrigor test-suite run <test-suite-id> --token <token> --url <url> --branch <branch> --commit <commit> --variables-path file.json --localhost --test-cases-path test_cases/**/*.{yaml,txt} --rules-path rules/**/*.{yaml,txt} --auto-create-ai-rules --explicit-mutations`
26
+ 15. Save JUnit report to `./junit-report.xml` (works only in sync mode): `testrigor test-suite run <test-suite-id> --token <token> --junit-report-save-path ./report.xml`
27
+ 16. With comma-separated labels `testrigor test-suite run <test-suite-id> --token <token> --labels <labels>`
28
+ 17. Excluding comma-separated labels `testrigor test-suite run <test-suite-id> --token <token> --excluded-labels <excluded-labels>`
29
+ 18. With different WEB and API URLs `testrigor test-suite run <test-suite-id> --token <token> --base-url-web=<base-web-url> --base-url-api=<base_api_url>`
30
+ 19. Set the default test suite ID: `testrigor test-suite config --default <test-suite-id>`
31
+ 20. Remove the default test suite ID: `testrigor test-suite config --delete`
32
+ 21. Show current configuration: `testrigor test-suite config --show`
33
+ 22. Authenticate with TestRigor and store credentials (interactive prompt): `testrigor authenticate`
34
+ 23. Run with override chrome extensions at runtime (one or more chrome extensions): `test-suite run <test-suite-id> --token <token> --chrome-extension "name=<extension-name> file=<extension-file>" --chrome-extension "name=<extension-name> file=<extension-file>"`
34
35
 
35
36
  ### Mobile Testing
36
37
 
@@ -18,7 +18,7 @@ export default class Run extends CommandBase<typeof Run> {
18
18
  "rules-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
19
19
  "settings-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
20
20
  "local-test-nodes-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
21
- "test-case-uuid": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
21
+ "test-case-uuid": import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
22
22
  "auto-create-ai-rules": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
23
23
  "explicit-mutations": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
24
24
  labels: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
@@ -36,8 +36,11 @@ export default class Run extends CommandBase<typeof Run> {
36
36
  };
37
37
  private initializeUrls;
38
38
  run(): Promise<void>;
39
+ private isSingleTestCaseExecution;
39
40
  private fetchStatus;
40
41
  private getStatusApiEndpoint;
42
+ private uploadVariableFiles;
43
+ private uploadClientCertificates;
41
44
  private callRetest;
42
45
  private fetchSuiteType;
43
46
  private isMobileSuiteType;
@@ -11,6 +11,9 @@ const chrome_extension_1 = require("../../flags/chrome-extension");
11
11
  const parser_1 = require("../../util/parser");
12
12
  const request_builder_1 = require("../../util/request-builder");
13
13
  const tunnel_1 = require("../../util/tunnel");
14
+ const certificate_uploader_1 = require("../../util/certificate-uploader");
15
+ const variable_file_uploader_1 = require("../../util/variable-file-uploader");
16
+ const variables_path_parser_1 = require("../../util/variables-path-parser");
14
17
  const appium_1 = require("../../util/appium");
15
18
  const utils_1 = require("../../util/utils");
16
19
  const sleep = async (milliseconds) => {
@@ -76,7 +79,7 @@ class Run extends command_base_1.CommandBase {
76
79
  const testCasesPath = flags["test-cases-path"];
77
80
  const rulesPath = flags["rules-path"];
78
81
  const settingsPath = flags["settings-path"];
79
- const testCaseUuid = flags["test-case-uuid"];
82
+ const testCaseUuids = flags["test-case-uuid"];
80
83
  const isAutoCreateAIBasedRulesEnabled = flags["auto-create-ai-rules"];
81
84
  const explicitMutations = flags["explicit-mutations"];
82
85
  const labels = flags["labels"];
@@ -105,8 +108,8 @@ class Run extends command_base_1.CommandBase {
105
108
  if (minPort > maxPort) {
106
109
  core_1.ux.error("--minPort cannot be greater than --maxPort");
107
110
  }
108
- if (testCaseUuid) {
109
- context.configuration.testCaseUuids = [testCaseUuid];
111
+ if (testCaseUuids && testCaseUuids.length > 0) {
112
+ context.configuration.testCaseUuids = testCaseUuids;
110
113
  }
111
114
  if (isAutoCreateAIBasedRulesEnabled) {
112
115
  context.configuration.isAutoCreateAIBasedRulesEnabled =
@@ -118,10 +121,23 @@ class Run extends command_base_1.CommandBase {
118
121
  if (variablesPath) {
119
122
  try {
120
123
  const file = await Fs.content(variablesPath);
121
- context.configuration.storedValues = JSON.parse(file.toString());
124
+ const parsed = JSON.parse(file.toString());
125
+ const { textValues, fileInputs, certInputs } = (0, variables_path_parser_1.parseVariablesJson)(parsed);
126
+ context.configuration.storedValues = textValues;
127
+ if (fileInputs.length > 0) {
128
+ context.storedValueFileInputs = fileInputs;
129
+ }
130
+ if (certInputs.length > 0) {
131
+ context.storedValueCertInputs = certInputs;
132
+ }
122
133
  }
123
134
  catch (e) {
124
- core_1.ux.error("--variables-path must be a valid JSON path");
135
+ if (e instanceof SyntaxError) {
136
+ core_1.ux.error("--variables-path must be a valid JSON path");
137
+ }
138
+ else {
139
+ core_1.ux.error(e instanceof Error ? e.message : String(e));
140
+ }
125
141
  }
126
142
  }
127
143
  if (testCasesPath) {
@@ -159,7 +175,7 @@ class Run extends command_base_1.CommandBase {
159
175
  }
160
176
  if (localTestNodesPath) {
161
177
  try {
162
- const localTestNodes = await (0, parser_1.parseSettings)(localTestNodesPath);
178
+ const localTestNodes = (await (0, parser_1.parseSettings)(localTestNodesPath));
163
179
  if (localTestNodes) {
164
180
  if (Array.isArray(localTestNodes.testNodes)) {
165
181
  for (const node of localTestNodes.testNodes) {
@@ -285,14 +301,26 @@ class Run extends command_base_1.CommandBase {
285
301
  // Checking last run id
286
302
  task: async (ctx) => {
287
303
  let pollBranch = flags.branch;
288
- if (testCaseUuid && !flags.branch) {
289
- pollBranch = `single-test-execution-${testCaseUuid}`;
304
+ if (this.isSingleTestCaseExecution(testCaseUuids, flags.branch)) {
305
+ pollBranch = `single-test-execution-${testCaseUuids[0]}`;
290
306
  }
291
307
  const statusResponse = await this.fetchStatus(testSuiteId, token, pollBranch, ctx.configuration.labels, ctx.configuration.excludedLabels);
292
308
  context.lastRunId = (0, utils_1.extractRunIdFromDetailsUrl)(statusResponse.detailsUrl);
293
309
  },
294
310
  exitOnError: false,
295
311
  },
312
+ {
313
+ title: "Uploading variable files",
314
+ enabled: () => !!context.storedValueFileInputs?.length,
315
+ task: (ctx) => this.uploadVariableFiles(ctx, testSuiteId, token),
316
+ exitOnError: false,
317
+ },
318
+ {
319
+ title: "Uploading client certificates",
320
+ enabled: () => !!context.storedValueCertInputs?.length,
321
+ task: (ctx, task) => this.uploadClientCertificates(ctx, testSuiteId, token, task),
322
+ exitOnError: false,
323
+ },
296
324
  {
297
325
  enabled: () => !flags["file-path"] && !flags["chrome-extension"],
298
326
  task: async (ctx) => {
@@ -451,8 +479,8 @@ class Run extends command_base_1.CommandBase {
451
479
  enabled: (ctx) => ctx.response?.queueId,
452
480
  task: async (ctx, currentTask) => {
453
481
  let pollBranch = flags.branch;
454
- if (testCaseUuid && !flags.branch) {
455
- pollBranch = `single-test-execution-${testCaseUuid}`;
482
+ if (this.isSingleTestCaseExecution(testCaseUuids, flags.branch)) {
483
+ pollBranch = `single-test-execution-${testCaseUuids[0]}`;
456
484
  }
457
485
  const detailsUrl = await this.waitForTestSuiteToBeCreated(testSuiteId, token, ctx.lastRunId, pollBranch, ctx.configuration.labels, ctx.configuration.excludedLabels);
458
486
  task.title = `Running test suite [ ${detailsUrl} ]`;
@@ -465,8 +493,8 @@ class Run extends command_base_1.CommandBase {
465
493
  enabled: (ctx) => ctx.response?.result === "ok" && !flags.async,
466
494
  task: async (ctx, task) => {
467
495
  let pollBranch = flags.branch;
468
- if (testCaseUuid && !flags.branch) {
469
- pollBranch = `single-test-execution-${testCaseUuid}`;
496
+ if (this.isSingleTestCaseExecution(testCaseUuids, flags.branch)) {
497
+ pollBranch = `single-test-execution-${testCaseUuids[0]}`;
470
498
  }
471
499
  return task.newListr([
472
500
  {
@@ -535,6 +563,9 @@ class Run extends command_base_1.CommandBase {
535
563
  }
536
564
  this.exit(0);
537
565
  }
566
+ isSingleTestCaseExecution(testCaseUuids, branch) {
567
+ return !!testCaseUuids && testCaseUuids.length === 1 && !branch;
568
+ }
538
569
  async fetchStatus(testSuiteId, token, branch, labels, excludedLabels) {
539
570
  const statusApiEndpoint = this.getStatusApiEndpoint(testSuiteId, branch, labels, excludedLabels);
540
571
  const response = await (0, node_fetch_commonjs_1.default)(statusApiEndpoint, {
@@ -564,6 +595,69 @@ class Run extends command_base_1.CommandBase {
564
595
  const queryString = queryParams.toString();
565
596
  return queryString ? `${apiEndpoint}?${queryString}` : apiEndpoint;
566
597
  }
598
+ async uploadVariableFiles(ctx, testSuiteId, token) {
599
+ const controller = new AbortController();
600
+ const timeoutId = setTimeout(() => controller.abort(), utils_1.UPLOAD_TIMEOUT_MS);
601
+ try {
602
+ ctx.configuration.storedValues = ctx.configuration.storedValues || {};
603
+ const uploads = await Promise.all(ctx.storedValueFileInputs.map(async (input) => ({
604
+ name: input.name,
605
+ url: await (0, variable_file_uploader_1.uploadVariableFile)(this.TESTRIGOR_API2_URL, testSuiteId, token, input.file),
606
+ })));
607
+ for (const { name, url } of uploads) {
608
+ ctx.configuration.storedValues[name] = url;
609
+ }
610
+ }
611
+ catch (error) {
612
+ if (error.name === "AbortError") {
613
+ throw new Error("Variable files upload timed out. Please check your network connection.");
614
+ }
615
+ throw error;
616
+ }
617
+ finally {
618
+ clearTimeout(timeoutId);
619
+ }
620
+ }
621
+ async uploadClientCertificates(ctx, testSuiteId, token, task) {
622
+ const controller = new AbortController();
623
+ const timeoutId = setTimeout(() => controller.abort(), utils_1.UPLOAD_TIMEOUT_MS);
624
+ try {
625
+ ctx.configuration.clientCertificateOverrides =
626
+ ctx.configuration.clientCertificateOverrides || {};
627
+ const uploads = await Promise.all(ctx.storedValueCertInputs.map(async (input) => {
628
+ const uploaded = await (0, certificate_uploader_1.uploadClientCertificate)(this.TESTRIGOR_API2_URL, testSuiteId, token, input.file, input.password);
629
+ return {
630
+ name: input.name,
631
+ certificateId: uploaded.certificateId,
632
+ info: uploaded.info,
633
+ };
634
+ }));
635
+ for (const { name, certificateId } of uploads) {
636
+ ctx.configuration.clientCertificateOverrides[name] = certificateId;
637
+ }
638
+ const summary = uploads
639
+ .map(({ name, info }) => {
640
+ const subject = info?.subject?.commonName || info?.subject?.distinguishedName;
641
+ const notAfter = info?.notAfter
642
+ ? ` (expires ${info.notAfter.slice(0, 10)})`
643
+ : "";
644
+ return subject ? `${name}: ${subject}${notAfter}` : name;
645
+ })
646
+ .join("; ");
647
+ if (summary) {
648
+ task.title = `Uploaded client certificate(s): ${summary}`;
649
+ }
650
+ }
651
+ catch (error) {
652
+ if (error.name === "AbortError") {
653
+ throw new Error("Client certificates upload timed out. Please check your network connection.");
654
+ }
655
+ throw error;
656
+ }
657
+ finally {
658
+ clearTimeout(timeoutId);
659
+ }
660
+ }
567
661
  async callRetest(testSuiteId, configuration, token) {
568
662
  return (0, node_fetch_commonjs_1.default)(`${this.TESTRIGOR_API_URL}/apps/${testSuiteId}/retest`, {
569
663
  method: "POST",
@@ -794,7 +888,8 @@ Run.flags = {
794
888
  description: "Path to a YAML or JSON file with local test nodes configuration",
795
889
  }),
796
890
  "test-case-uuid": core_1.Flags.string({
797
- description: "Test case uuid",
891
+ description: "Test case uuid (can be repeated to run multiple test cases)",
892
+ multiple: true,
798
893
  }),
799
894
  "auto-create-ai-rules": core_1.Flags.boolean({
800
895
  description: "Enable auto create AI based rules",
package/dist/types.d.ts CHANGED
@@ -65,3 +65,54 @@ export type ChromeExtensionOverrideItem = {
65
65
  export type ChromeExtensionOverridesRequest = {
66
66
  chromeExtensions: ChromeExtensionOverrideItem[];
67
67
  };
68
+ export type StoredValueFileInput = {
69
+ name: string;
70
+ file: string;
71
+ };
72
+ export type StoredValueCertInput = {
73
+ name: string;
74
+ file: string;
75
+ password: string;
76
+ };
77
+ export type ParsedVariables = {
78
+ textValues: Record<string, string>;
79
+ fileInputs: StoredValueFileInput[];
80
+ certInputs: StoredValueCertInput[];
81
+ };
82
+ export type ClientCertificateDistinguishedName = {
83
+ distinguishedName?: string;
84
+ commonName?: string;
85
+ };
86
+ export type ClientCertificateInfo = {
87
+ serialNumber?: string;
88
+ notBefore?: string;
89
+ notAfter?: string;
90
+ issuer?: ClientCertificateDistinguishedName;
91
+ subject?: ClientCertificateDistinguishedName;
92
+ };
93
+ export type CertificateUploadDataApi2Response = {
94
+ certificateId: string;
95
+ info?: ClientCertificateInfo;
96
+ };
97
+ export type UploadedClientCertificate = {
98
+ certificateId: string;
99
+ info?: ClientCertificateInfo;
100
+ };
101
+ export type CertificateUploadApi2Response = Api2Wrapper & {
102
+ data: CertificateUploadDataApi2Response;
103
+ };
104
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
105
+ [key: string]: JsonValue;
106
+ };
107
+ export type JsonObject = {
108
+ [key: string]: JsonValue;
109
+ };
110
+ export type FileVariableObject = {
111
+ type: "FILE";
112
+ value: string;
113
+ };
114
+ export type CertVariableObject = {
115
+ type: "CLIENT_CERTIFICATE";
116
+ value: string;
117
+ password: string;
118
+ };
@@ -0,0 +1,2 @@
1
+ import { UploadedClientCertificate } from "../types";
2
+ export declare function uploadClientCertificate(api2BaseUrl: string, testSuiteId: string, token: string, localFilePath: string, password: string): Promise<UploadedClientCertificate>;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.uploadClientCertificate = void 0;
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const node_fetch_commonjs_1 = require("node-fetch-commonjs");
7
+ const type_guards_1 = require("./type-guards");
8
+ async function uploadClientCertificate(api2BaseUrl, testSuiteId, token, localFilePath, password) {
9
+ const fileName = path.basename(localFilePath);
10
+ const certBase64 = fs.readFileSync(localFilePath).toString("base64");
11
+ const response = await (0, node_fetch_commonjs_1.default)(`${api2BaseUrl}/apps/${testSuiteId}/test_data/global_variables/client_certificate`, {
12
+ method: "POST",
13
+ body: JSON.stringify({ fileName, certBase64, password }),
14
+ headers: {
15
+ "Content-Type": "application/json",
16
+ Accept: "application/json",
17
+ "auth-token": token,
18
+ },
19
+ });
20
+ const responseJson = await response.json();
21
+ if (!(0, type_guards_1.isCertificateUploadApi2Response)(responseJson)) {
22
+ throw new Error(`Unexpected response shape from client certificate upload endpoint for '${fileName}'`);
23
+ }
24
+ if (!response.ok || responseJson.status !== 200 || !responseJson.data) {
25
+ throw new Error(`Failed to upload client certificate '${fileName}': ${responseJson.message}`);
26
+ }
27
+ return {
28
+ certificateId: responseJson.data.certificateId,
29
+ info: responseJson.data.info,
30
+ };
31
+ }
32
+ exports.uploadClientCertificate = uploadClientCertificate;
@@ -0,0 +1,4 @@
1
+ import { CertificateUploadApi2Response, SignedUrlApi2Response } from "../types";
2
+ export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
3
+ export declare function isSignedUrlApi2Response(value: unknown): value is SignedUrlApi2Response;
4
+ export declare function isCertificateUploadApi2Response(value: unknown): value is CertificateUploadApi2Response;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isCertificateUploadApi2Response = exports.isSignedUrlApi2Response = exports.isPlainObject = void 0;
4
+ function isPlainObject(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+ exports.isPlainObject = isPlainObject;
8
+ function isSignedUrlApi2Response(value) {
9
+ return isPlainObject(value) && "status" in value;
10
+ }
11
+ exports.isSignedUrlApi2Response = isSignedUrlApi2Response;
12
+ function isCertificateUploadApi2Response(value) {
13
+ return isPlainObject(value) && "status" in value;
14
+ }
15
+ exports.isCertificateUploadApi2Response = isCertificateUploadApi2Response;
@@ -0,0 +1 @@
1
+ export declare function uploadVariableFile(api2BaseUrl: string, testSuiteId: string, token: string, localFilePath: string): Promise<string>;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.uploadVariableFile = void 0;
4
+ const Fs = require("@supercharge/fs");
5
+ const path = require("path");
6
+ const node_fetch_commonjs_1 = require("node-fetch-commonjs");
7
+ const utils_1 = require("./utils");
8
+ const type_guards_1 = require("./type-guards");
9
+ async function uploadVariableFile(api2BaseUrl, testSuiteId, token, localFilePath) {
10
+ const fileName = path.basename(localFilePath);
11
+ const fileSize = Fs.statSync(localFilePath).size;
12
+ const presignedResp = await (0, node_fetch_commonjs_1.default)(`${api2BaseUrl}/apps/${testSuiteId}/test_data/global_variables/file/pre_signed_url`, {
13
+ method: "POST",
14
+ body: JSON.stringify({ fileName }),
15
+ headers: {
16
+ "Content-Type": "application/json",
17
+ Accept: "application/json",
18
+ "auth-token": token,
19
+ },
20
+ });
21
+ const presignedJson = await presignedResp.json();
22
+ if (!(0, type_guards_1.isSignedUrlApi2Response)(presignedJson)) {
23
+ throw new Error(`Unexpected response shape from presigned URL endpoint for '${fileName}'`);
24
+ }
25
+ if (!presignedResp.ok ||
26
+ presignedJson.status !== 200 ||
27
+ !presignedJson.data) {
28
+ throw new Error(`Failed to request presigned URL for variable file '${fileName}': ${presignedJson.message}`);
29
+ }
30
+ const fileStream = Fs.createReadStream(localFilePath);
31
+ const putResp = await (0, node_fetch_commonjs_1.default)(presignedJson.data.url, {
32
+ method: "PUT",
33
+ body: fileStream,
34
+ headers: {
35
+ "Content-length": `${fileSize}`,
36
+ },
37
+ });
38
+ if (!putResp.ok) {
39
+ throw new Error(`Failed to upload variable file '${fileName}' to S3: ${putResp.statusText}`);
40
+ }
41
+ return (0, utils_1.extractUrlWithoutQueryParams)(presignedJson.data.url);
42
+ }
43
+ exports.uploadVariableFile = uploadVariableFile;
@@ -0,0 +1,2 @@
1
+ import { JsonValue, ParsedVariables } from "../types";
2
+ export declare function parseVariablesJson(parsed: JsonValue): ParsedVariables;
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseVariablesJson = void 0;
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const type_guards_1 = require("./type-guards");
7
+ const MAX_FILE_SIZE_BYTES = 100 * 1024 * 1024;
8
+ const MAX_COMBINED_SIZE_BYTES = 100 * 1024 * 1024;
9
+ const MAX_SIZE_LABEL = "100 MB";
10
+ const MAX_CERT_FILE_SIZE_BYTES = 20 * 1024; // 20 KB
11
+ const MAX_CERT_PASSWORD_LENGTH = 100;
12
+ const MAX_CERT_FILENAME_LENGTH = 100;
13
+ const ERROR_ROOT_NOT_OBJECT = "--variables-path must contain a JSON object at the top level";
14
+ const ERROR_COMBINED_SIZE_EXCEEDED = `Variable files combined size exceeds the maximum allowed (${MAX_SIZE_LABEL})`;
15
+ function isUrl(value) {
16
+ return /^https?:\/\//i.test(value);
17
+ }
18
+ function isFileVariableObject(value) {
19
+ return ((0, type_guards_1.isPlainObject)(value) &&
20
+ value.type === "FILE" &&
21
+ typeof value.value === "string");
22
+ }
23
+ function isCertVariableObject(value) {
24
+ return ((0, type_guards_1.isPlainObject)(value) &&
25
+ value.type === "CLIENT_CERTIFICATE" &&
26
+ typeof value.value === "string" &&
27
+ typeof value.password === "string");
28
+ }
29
+ function readLocalFileSize(key, filePath) {
30
+ if (!fs.existsSync(filePath)) {
31
+ throw new Error(`Variable "${key}": file not found at path "${filePath}"`);
32
+ }
33
+ const size = fs.statSync(filePath).size;
34
+ if (size > MAX_FILE_SIZE_BYTES) {
35
+ throw new Error(`Variable "${key}": file size exceeds the maximum allowed (${MAX_SIZE_LABEL})`);
36
+ }
37
+ return size;
38
+ }
39
+ function validateLocalCertFile(key, value) {
40
+ if (isUrl(value)) {
41
+ throw new Error(`Variable "${key}": CLIENT_CERTIFICATE does not support URL values; provide a local .pfx file path`);
42
+ }
43
+ if (!fs.existsSync(value)) {
44
+ throw new Error(`Variable "${key}": file not found at path "${value}"`);
45
+ }
46
+ if (!/\.pfx$/i.test(path.basename(value))) {
47
+ throw new Error(`Variable "${key}": CLIENT_CERTIFICATE requires a .pfx file`);
48
+ }
49
+ if (path.basename(value).length > MAX_CERT_FILENAME_LENGTH) {
50
+ throw new Error(`Variable "${key}": filename is too long (max ${MAX_CERT_FILENAME_LENGTH} chars)`);
51
+ }
52
+ if (fs.statSync(value).size > MAX_CERT_FILE_SIZE_BYTES) {
53
+ throw new Error(`Variable "${key}": certificate file exceeds the maximum allowed (20 KB)`);
54
+ }
55
+ }
56
+ function parseVariablesJson(parsed) {
57
+ if (!(0, type_guards_1.isPlainObject)(parsed)) {
58
+ throw new Error(ERROR_ROOT_NOT_OBJECT);
59
+ }
60
+ const textValues = {};
61
+ const fileInputs = [];
62
+ const certInputs = [];
63
+ let combinedSize = 0;
64
+ for (const [key, raw] of Object.entries(parsed)) {
65
+ if (typeof raw === "string") {
66
+ textValues[key] = raw;
67
+ continue;
68
+ }
69
+ if (!(0, type_guards_1.isPlainObject)(raw)) {
70
+ throw new Error(`Variable "${key}": expected a string or { type: "FILE", value: "..." }`);
71
+ }
72
+ if (raw.type === "FILE" && raw.password !== undefined) {
73
+ throw new Error(`Variable "${key}": "password" field is only valid for CLIENT_CERTIFICATE. Did you mean type:"CLIENT_CERTIFICATE"?`);
74
+ }
75
+ if (raw.type === "FILE") {
76
+ if (!isFileVariableObject(raw)) {
77
+ throw new Error(`Variable "${key}": missing or empty "value" for type:FILE`);
78
+ }
79
+ const value = raw.value.trim();
80
+ if (value.length === 0) {
81
+ throw new Error(`Variable "${key}": missing or empty "value" for type:FILE`);
82
+ }
83
+ if (isUrl(value)) {
84
+ textValues[key] = value;
85
+ continue;
86
+ }
87
+ combinedSize += readLocalFileSize(key, value);
88
+ if (combinedSize > MAX_COMBINED_SIZE_BYTES) {
89
+ throw new Error(ERROR_COMBINED_SIZE_EXCEEDED);
90
+ }
91
+ fileInputs.push({ name: key, file: value });
92
+ continue;
93
+ }
94
+ if (raw.type === "CLIENT_CERTIFICATE") {
95
+ if (!isCertVariableObject(raw)) {
96
+ if (typeof raw.value !== "string" || raw.value.trim().length === 0) {
97
+ throw new Error(`Variable "${key}": missing or empty "value" for type:CLIENT_CERTIFICATE`);
98
+ }
99
+ throw new Error(`Variable "${key}": missing or empty "password" for type:CLIENT_CERTIFICATE`);
100
+ }
101
+ const value = raw.value.trim();
102
+ if (value.length === 0) {
103
+ throw new Error(`Variable "${key}": missing or empty "value" for type:CLIENT_CERTIFICATE`);
104
+ }
105
+ if (raw.password.length === 0) {
106
+ throw new Error(`Variable "${key}": missing or empty "password" for type:CLIENT_CERTIFICATE`);
107
+ }
108
+ if (raw.password.length > MAX_CERT_PASSWORD_LENGTH) {
109
+ throw new Error(`Variable "${key}": "password" exceeds the maximum length of ${MAX_CERT_PASSWORD_LENGTH} characters`);
110
+ }
111
+ validateLocalCertFile(key, value);
112
+ certInputs.push({
113
+ name: key,
114
+ file: value,
115
+ password: raw.password,
116
+ });
117
+ continue;
118
+ }
119
+ if (typeof raw.type === "string" && raw.type.toUpperCase() === "FILE") {
120
+ throw new Error(`Variable "${key}": unsupported type "${String(raw.type)}". Only "FILE" (uppercase) and "CLIENT_CERTIFICATE" are supported.`);
121
+ }
122
+ throw new Error(`Variable "${key}": unsupported type "${String(raw.type)}". Only "FILE" and "CLIENT_CERTIFICATE" are supported.`);
123
+ }
124
+ return { textValues, fileInputs, certInputs };
125
+ }
126
+ exports.parseVariablesJson = parseVariablesJson;
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.44-beta",
2
+ "version": "0.0.46-beta",
3
3
  "commands": {
4
4
  "authenticate": {
5
5
  "id": "authenticate",
@@ -204,8 +204,8 @@
204
204
  "test-case-uuid": {
205
205
  "name": "test-case-uuid",
206
206
  "type": "option",
207
- "description": "Test case uuid",
208
- "multiple": false
207
+ "description": "Test case uuid (can be repeated to run multiple test cases)",
208
+ "multiple": true
209
209
  },
210
210
  "auto-create-ai-rules": {
211
211
  "name": "auto-create-ai-rules",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testrigor/testrigor-cli",
3
- "version": "0.0.44-beta",
3
+ "version": "0.0.46-beta",
4
4
  "description": "TestRigor Command Line",
5
5
  "author": "Ivo Parun Rua @parunruaivo",
6
6
  "bin": {