@skyramp/skyramp 1.3.11 → 1.3.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/skyramp",
3
- "version": "1.3.11",
3
+ "version": "1.3.13",
4
4
  "description": "module for leveraging skyramp cli functionality",
5
5
  "scripts": {
6
6
  "lint": "eslint 'src/**/*.js' 'src/**/*.ts' --fix",
@@ -26,12 +26,12 @@
26
26
  "@aws-sdk/client-s3": "^3.812.0",
27
27
  "fs": "^0.0.1-security",
28
28
  "js-yaml": "^4.1.0",
29
- "koffi": "2.5.12",
29
+ "koffi": "^2.15.0",
30
30
  "zod": "^3.25.3"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@typescript-eslint/eslint-plugin": "^6.14.0",
34
34
  "@typescript-eslint/parser": "^6.14.0",
35
- "eslint": "^8.55.0"
35
+ "eslint": "^8.57.1"
36
36
  }
37
37
  }
@@ -10,7 +10,7 @@ export class MockV2 {
10
10
  * The URL of the service to mock
11
11
  */
12
12
  URL: string;
13
-
13
+
14
14
  /**
15
15
  * The path to mock (e.g., "/api/v1/products")
16
16
  */
@@ -20,7 +20,7 @@ export class MockV2 {
20
20
  * The HTTP method (GET, POST, PUT, DELETE, etc.)
21
21
  */
22
22
  method: string;
23
-
23
+
24
24
  /**
25
25
  * The HTTP status code to return
26
26
  */
@@ -30,19 +30,30 @@ export class MockV2 {
30
30
  * The response body to return
31
31
  */
32
32
  responseBody: string;
33
-
33
+
34
34
  /**
35
35
  * Optional request body to match
36
36
  */
37
37
  requestBody?: string | null;
38
-
38
+
39
39
  /**
40
40
  * Optional data override object
41
41
  */
42
42
  dataOverride?: Record<string, unknown> | null;
43
43
 
44
44
  /**
45
- * Creates a new MockV2 instance
45
+ * Optional client ID for mock identification
46
+ */
47
+ clientID?: string | null;
48
+
49
+ /**
50
+ * Creates a new MockV2 instance using an options object
51
+ * @param options - Configuration options for the mock
52
+ */
53
+ constructor(options: MockV2Options);
54
+
55
+ /**
56
+ * Creates a new MockV2 instance using positional arguments
46
57
  * @param URL - The URL of the service to mock
47
58
  * @param path - The path path to mock (e.g., "/api/v1/products")
48
59
  * @param method - The HTTP method (GET, POST, PUT, DELETE, etc.)
@@ -73,6 +84,7 @@ export class MockV2 {
73
84
  response_body: string;
74
85
  request_body?: string;
75
86
  data_override?: Record<string, unknown>;
87
+ client_id?: string;
76
88
  };
77
89
 
78
90
  /**
@@ -5,23 +5,52 @@
5
5
  class MockV2 {
6
6
  /**
7
7
  * Represents a mock object configuration for API endpoint mocking.
8
- * @param {Object} [options={}] - The options for creating the mock.
9
- * @param {string} [options.url=''] - The URL of the service to mock
10
- * @param {string} [options.path=''] - The path to mock (e.g., "/api/v1/products")
11
- * @param {string} [options.method=''] - The HTTP method (GET, POST, PUT, DELETE, etc.)
12
- * @param {number} [options.statusCode=201] - The HTTP status code to return
13
- * @param {string} [options.responseBody=''] - The response body to return
14
- * @param {string} [options.requestBody=''] - Optional request body to match
15
- * @param {Object.<string, *>} [options.dataOverride={}] - Optional data override object
8
+ * Supports both positional arguments and options object patterns.
9
+ *
10
+ * @example
11
+ * // Positional arguments
12
+ * new MockV2("http://localhost:8080", "/api/v1/products", 8080, "POST", 201, "{}")
13
+ *
14
+ * @example
15
+ * // Options object (preferred)
16
+ * new MockV2({
17
+ * url: "http://localhost:8080",
18
+ * path: "/api/v1/products",
19
+ * method: "POST",
20
+ * responseStatusCode: 201
21
+ * })
22
+ *
23
+ * @param {string|Object} URLOrOptions - URL string or options object
24
+ * @param {string} [path] - The endpoint path (positional only)
25
+ * @param {string} [method] - The HTTP method (positional only)
26
+ * @param {number} [statusCode] - The status code (positional only)
27
+ * @param {string} [responseBody] - The response body (positional only)
28
+ * @param {string} [requestBody] - Optional request body to match
29
+ * @param {Object} [dataOverride] - Optional data override object
16
30
  */
17
- constructor(options = {}) {
18
- this.url = options.url || '';
19
- this.path = options.path || '';
20
- this.method = options.method || '';
21
- this.statusCode = options.statusCode || 201;
22
- this.responseBody = options.responseBody || '';
23
- this.requestBody = options.requestBody || '';
24
- this.dataOverride = options.dataOverride || {};
31
+ constructor(URLOrOptions, path, method, statusCode, responseBody, requestBody = null, dataOverride = null) {
32
+ // Handle options object pattern
33
+ if (typeof URLOrOptions === 'object' && URLOrOptions !== null) {
34
+ const options = URLOrOptions;
35
+ this.url = options.url || '';
36
+ this.path = options.path || '';
37
+ this.method = options.method || '';
38
+ this.statusCode = options.statusCode || 201;
39
+ this.responseBody = (options.body !== undefined ? options.body : options.responseBody) || '';
40
+ this.requestBody = options.requestBody || null;
41
+ this.dataOverride = options.dataOverride || null;
42
+ this.clientID = options.clientID || null;
43
+ } else {
44
+ // Handle positional arguments pattern
45
+ this.url = URLOrOptions;
46
+ this.path = path;
47
+ this.method = method;
48
+ this.statusCode = statusCode;
49
+ this.responseBody = responseBody;
50
+ this.requestBody = requestBody;
51
+ this.dataOverride = dataOverride;
52
+ this.clientID = null;
53
+ }
25
54
  }
26
55
 
27
56
  /**
@@ -45,6 +74,10 @@ class MockV2 {
45
74
  result.data_override = this.dataOverride;
46
75
  }
47
76
 
77
+ if (this.clientID !== null) {
78
+ result.client_id = this.clientID;
79
+ }
80
+
48
81
  return result;
49
82
  }
50
83
 
@@ -138,6 +138,13 @@ interface GenerateRestTestOptions {
138
138
  unblock?: boolean;
139
139
  entrypoint?: string;
140
140
  chainingKey?: string;
141
+ parentRequestData?: Record<string, string> | string;
142
+ parentStatusCode?: Record<string, string> | string;
143
+ requestAware?: boolean;
144
+ providerMode?: boolean;
145
+ consumerMode?: boolean;
146
+ providerOutput?: string;
147
+ consumerOutput?: string;
141
148
  }
142
149
 
143
150
  interface GenerateRestMockOptions {
@@ -157,6 +164,7 @@ interface GenerateRestMockOptions {
157
164
  responseStatusCode?: string;
158
165
  force?: boolean;
159
166
  deployDashboard?: boolean;
167
+ requestAware?: boolean;
160
168
  formParams?: string;
161
169
  pathParams?: string;
162
170
  queryParams?: string;
@@ -174,11 +182,6 @@ interface SendScenarioOptions {
174
182
  skipCertVerification?: boolean;
175
183
  }
176
184
 
177
- interface InitAgentOptions {
178
- version: string;
179
- entryPoint?: string;
180
- }
181
-
182
185
  export declare class SkyrampClient {
183
186
  constructor(
184
187
  kubeconfigPath?: string,
@@ -250,6 +253,7 @@ export declare class SkyrampClient {
250
253
  mockYamlContent: string
251
254
  ): Promise<void>;
252
255
  applyMock(mock: MockV2 | MockV2[]): Promise<void>;
256
+ removeAllMocks(): Promise<void>;
253
257
  testerStart(
254
258
  namespace: string,
255
259
  kubePath: string,
@@ -314,10 +318,7 @@ export declare class SkyrampClient {
314
318
 
315
319
  /**
316
320
  * Initializes the agent by checking and pulling required Docker images (executor and worker).
317
- * @param {InitAgentOptions} options - The options for initializing the agent.
318
- * @param {string} options.version - The version of the agent to initialize.
319
- * @param {string} [options.entryPoint='vscode'] - The entry point identifier (e.g., 'vscode').
320
321
  * @returns {Promise<string>} A promise that resolves with the initialization output message.
321
322
  */
322
- initAgent(options: InitAgentOptions): Promise<string>;
323
+ initAgent(): Promise<string>;
323
324
  }
@@ -1,3 +1,4 @@
1
+ const { promisify } = require('util');
1
2
  const lib = require('../lib');
2
3
  const koffi = require('koffi');
3
4
  const TrafficConfig = require('./TrafficConfig');
@@ -46,16 +47,98 @@ const runTesterStartWrapper = lib.func('runTesterStartWrapper', testerInfoType,
46
47
  const runTesterStartWrapperv1 = lib.func('runTesterStartWrapperWithGlobalHeaders', testerInfoType, ['string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'bool', 'bool', 'string', 'string', 'string']);
47
48
  const applyMockDescriptionWrapper = lib.func('applyMockDescriptionWrapper', 'string', ['string', 'string', 'string', 'string', 'string', 'string', 'string', 'string']);
48
49
  const applyMockObjectWrapper = lib.func('applyMockObjectWrapper', 'string', ['string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'bool']);
50
+ const removeMocksObjectWrapper = lib.func('removeMocksObjectWrapper', 'string', []);
49
51
  // NPM only: for VS code extension use
50
52
  const initTargetWrapper = lib.func('initTargetWrapper', 'string', ['string']);
51
53
  const deployTargetWrapper = lib.func('deployTargetWrapper', 'string', ['string', 'string', 'string', 'string', 'string', 'string', 'bool']);
52
54
  const deleteTargetWrapper = lib.func('deleteTargetWrapper', 'string', ['string', 'string', 'string', 'string', 'string']);
53
55
 
54
- const generateRestTestWrapper = lib.func('generateRestTestWrapper', 'string', ['string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'bool', 'bool', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'bool', 'string', 'bool', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'bool', 'string', 'string']);
55
- const generateRestMockWrapper = lib.func('generateRestMockWrapper', 'string', ['string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'bool', 'bool', 'string', 'string', 'string', 'string', 'string', 'string']);
56
+ const generateRestTestWrapper = lib.func('generateRestTestWrapper', 'string', [
57
+ 'string', // test
58
+ 'string', // uri
59
+ 'string', // methodX
60
+ 'string', // language
61
+ 'string', // framework
62
+ 'string', // output
63
+ 'string', // outputDir
64
+ 'string', // runtime
65
+ 'string', // dockerNetwork
66
+ 'string', // dockerWorkerPort
67
+ 'string', // k8sNamespace
68
+ 'string', // k8sConfig
69
+ 'string', // k8sContxt
70
+ 'string', // authHeader
71
+ 'string', // authType
72
+ 'string', // requestData
73
+ 'string', // responseData
74
+ 'string', // responseStatusCode
75
+ 'bool', // force
76
+ 'bool', // deployDashboard
77
+ 'string', // formParams
78
+ 'string', // pathParams
79
+ 'string', // queryParams
80
+ 'string', // apiSchema
81
+ 'string', // trafeFilePath
82
+ 'string', // generateInclude
83
+ 'string', // generateExclude
84
+ 'string', // generateNoProxy
85
+ 'bool', // generateInsecure
86
+ 'string', // assertOption
87
+ 'bool', // playwright
88
+ 'string', // playwrightOutput
89
+ 'string', // playwrightInput
90
+ 'string', // playwrightViewportSize
91
+ 'string', // playwrightStoragePath
92
+ 'string', // playwrightSaveStoragePath
93
+ 'string', // browser
94
+ 'string', // device
95
+ 'string', // loadCount
96
+ 'string', // loadDuration
97
+ 'string', // loadNumThreads
98
+ 'string', // loadRampupDuration
99
+ 'string', // loadRampupInterval
100
+ 'string', // loadTargetRPS
101
+ 'string', // rawTrace
102
+ 'bool', // unblock
103
+ 'string', // entrypoint
104
+ 'string', // chainingKey
105
+ 'string', // parentRequestData
106
+ 'string', // parentStatusCode
107
+ 'bool', // requestAware
108
+ 'bool', // providerMode
109
+ 'bool', // consumerMode
110
+ 'string', // providerOutput (contract test)
111
+ 'string', // consumerOutput (contract test)
112
+ 'bool' // skipProvisionParents
113
+ ]);
114
+ const generateRestMockWrapper = lib.func('generateRestMockWrapper', 'string', [
115
+ 'string', // uri
116
+ 'string', // methodX
117
+ 'string', // language
118
+ 'string', // framework
119
+ 'string', // output
120
+ 'string', // outputDir
121
+ 'string', // runtime
122
+ 'string', // dockerNetwork
123
+ 'string', // dockerWorkerPort
124
+ 'string', // k8sNamespace
125
+ 'string', // k8sConfig
126
+ 'string', // k8sContext
127
+ 'string', // responseData
128
+ 'string', // responseStatusCode
129
+ 'bool', // force
130
+ 'bool', // deployDashboard
131
+ 'bool', // requestAware
132
+ 'string', // formParams
133
+ 'string', // pathParams
134
+ 'string', // queryParams
135
+ 'string', // apiSchema
136
+ 'string', // traceFilePath
137
+ 'string' // entryPoint
138
+ ]);
56
139
  const traceCollectWrapper = lib.func('traceCollectWrapper', 'string', ['string', 'string', 'bool', 'string', 'string']);
57
140
  const analyzeOpenapiWrapper = lib.func('analyzeOpenapiWrapper', 'string', ['string', 'string']);
58
- const initAgentWrapper = lib.func('initAgent', 'string', ['string', 'string']);
141
+ const initAgentWrapper = lib.func('initAgent', 'string', []);
59
142
 
60
143
  // Load test scenario support
61
144
  const sendScenarioWrapper = lib.func('sendScenarioWrapper', contractResponseType, ['string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'bool', 'bool', 'bool']);
@@ -471,6 +554,24 @@ class SkyrampClient {
471
554
  });
472
555
  }
473
556
 
557
+ /**
558
+ * Removes all mock configurations from the worker.
559
+ * @returns {Promise} A promise that resolves when all mocks are removed successfully
560
+ */
561
+ async removeAllMocks() {
562
+ return new Promise((resolve, reject) => {
563
+ removeMocksObjectWrapper.async((err, res) => {
564
+ if (err) {
565
+ reject(err);
566
+ } else if (res) {
567
+ reject(new Error(res));
568
+ } else {
569
+ resolve();
570
+ }
571
+ });
572
+ });
573
+ }
574
+
474
575
  async testerStart(namespace, kubePath, kubeContext, clusterName, address, scenario) {
475
576
  const preparedScenario = scenario.prepareTestDescription();
476
577
  const testDescription = createTestDescriptionFromScenario({ scenario: preparedScenario });
@@ -788,6 +889,13 @@ class SkyrampClient {
788
889
 
789
890
  async generateRestTest(options) {
790
891
  return new Promise((resolve, reject) => {
892
+ const parentRequestData = typeof options.parentRequestData === 'string'
893
+ ? options.parentRequestData
894
+ : JSON.stringify(options.parentRequestData || {});
895
+ const parentStatusCode = typeof options.parentStatusCode === 'string'
896
+ ? options.parentStatusCode
897
+ : JSON.stringify(options.parentStatusCode || {});
898
+
791
899
  generateRestTestWrapper.async(
792
900
  options.testType || "",
793
901
  options.uri || "",
@@ -837,6 +945,14 @@ class SkyrampClient {
837
945
  options.unblock || false,
838
946
  options.entrypoint || "",
839
947
  options.chainingKey || "",
948
+ parentRequestData || "",
949
+ parentStatusCode || "",
950
+ options.requestAware || false,
951
+ options.providerMode || true,
952
+ options.consumerMode || false,
953
+ options.providerOutput || "",
954
+ options.consumerOutput || "",
955
+ options.skipProvisionParents || true,
840
956
  (err, res) => {
841
957
  if (err) {
842
958
  reject(err);
@@ -867,6 +983,7 @@ class SkyrampClient {
867
983
  options.responseStatusCode || "",
868
984
  options.force || false,
869
985
  options.deployDashboard || false,
986
+ options.requestAware || false,
870
987
  options.formParams || "",
871
988
  options.pathParams || "",
872
989
  options.queryParams || "",
@@ -1007,26 +1124,11 @@ class SkyrampClient {
1007
1124
 
1008
1125
  /**
1009
1126
  * Initializes the agent by checking and pulling required Docker images (executor and worker).
1010
- * @param {Object} options - The options for initializing the agent.
1011
- * @param {string} options.version - The version of the agent to initialize.
1012
- * @param {string} [options.entryPoint='vscode'] - The entry point identifier (e.g., 'vscode').
1013
1127
  * @returns {Promise<string>} A promise that resolves with the initialization output message.
1014
1128
  */
1015
- async initAgent(options) {
1129
+ async initAgent() {
1016
1130
  await checkForUpdate("npm");
1017
- return new Promise((resolve, reject) => {
1018
- initAgentWrapper.async(
1019
- options.version || "",
1020
- options.entryPoint || "vscode",
1021
- (err, res) => {
1022
- if (err) {
1023
- reject(err);
1024
- } else {
1025
- resolve(res);
1026
- }
1027
- }
1028
- );
1029
- });
1131
+ return promisify(initAgentWrapper.async)();
1030
1132
  }
1031
1133
  }
1032
1134
 
@@ -724,6 +724,11 @@ class SkyrampPlaywrightLocator {
724
724
  return this._skyrampPage.newSkyrampPlaywrightLocator(new_locator, null, null);
725
725
  }
726
726
 
727
+ contentFrame() {
728
+ const frameLocator = this._locator.contentFrame();
729
+ return new SkyrampPlaywrightFrameLocator(this._skyrampPage, frameLocator);
730
+ }
731
+
727
732
  page() {
728
733
  return this._skyrampPage._page;
729
734
  }
@@ -734,6 +739,91 @@ class SkyrampPlaywrightLocator {
734
739
  }
735
740
  }
736
741
 
742
+ class SkyrampPlaywrightFrameLocator {
743
+ constructor(skyrampPage, frameLocator) {
744
+ this._skyrampPage = skyrampPage
745
+ this._frameLocator = frameLocator
746
+ debug(`SkyrampPlaywrightFrameLocator instantiated for ${frameLocator}`)
747
+ return new Proxy(this, {
748
+ get(wrapper, prop, receiver) {
749
+ if (Reflect.has(wrapper, prop)) {
750
+ return Reflect.get(wrapper, prop, receiver);
751
+ }
752
+ const value = Reflect.get(wrapper._frameLocator, prop, wrapper._frameLocator);
753
+ if (typeof value === 'function') {
754
+ return value.bind(wrapper._frameLocator);
755
+ }
756
+ return value;
757
+ }
758
+ });
759
+ }
760
+
761
+ locator(selector, options) {
762
+ const originalLocator = this._frameLocator.locator(selector, options);
763
+ return this._skyrampPage.newSkyrampPlaywrightLocator(originalLocator, selector, options);
764
+ }
765
+
766
+ getByRole(role, options) {
767
+ const originalLocator = this._frameLocator.getByRole(role, options);
768
+ return this._skyrampPage.newSkyrampPlaywrightLocator(originalLocator, role, options);
769
+ }
770
+
771
+ getByText(text, options) {
772
+ const originalLocator = this._frameLocator.getByText(text, options);
773
+ return this._skyrampPage.newSkyrampPlaywrightLocator(originalLocator, text, options);
774
+ }
775
+
776
+ getByLabel(label, options) {
777
+ const originalLocator = this._frameLocator.getByLabel(label, options);
778
+ return this._skyrampPage.newSkyrampPlaywrightLocator(originalLocator, label, options);
779
+ }
780
+
781
+ getByTestId(testId, options) {
782
+ const originalLocator = this._frameLocator.getByTestId(testId);
783
+ return this._skyrampPage.newSkyrampPlaywrightLocator(originalLocator, testId, options);
784
+ }
785
+
786
+ getByTitle(title, options) {
787
+ const originalLocator = this._frameLocator.getByTitle(title, options);
788
+ return this._skyrampPage.newSkyrampPlaywrightLocator(originalLocator, title, options);
789
+ }
790
+
791
+ getByPlaceholder(placeholder, options) {
792
+ const originalLocator = this._frameLocator.getByPlaceholder(placeholder, options);
793
+ return this._skyrampPage.newSkyrampPlaywrightLocator(originalLocator, placeholder, options);
794
+ }
795
+
796
+ getByAltText(alt, options) {
797
+ const originalLocator = this._frameLocator.getByAltText(alt, options);
798
+ return this._skyrampPage.newSkyrampPlaywrightLocator(originalLocator, alt, options);
799
+ }
800
+
801
+ owner() {
802
+ const originalLocator = this._frameLocator.owner();
803
+ return this._skyrampPage.newSkyrampPlaywrightLocator(originalLocator, null, null);
804
+ }
805
+
806
+ frameLocator(selector) {
807
+ const originalFrameLocator = this._frameLocator.frameLocator(selector);
808
+ return new SkyrampPlaywrightFrameLocator(this._skyrampPage, originalFrameLocator);
809
+ }
810
+
811
+ nth(index) {
812
+ const originalFrameLocator = this._frameLocator.nth(index);
813
+ return new SkyrampPlaywrightFrameLocator(this._skyrampPage, originalFrameLocator);
814
+ }
815
+
816
+ first() {
817
+ const originalFrameLocator = this._frameLocator.first();
818
+ return new SkyrampPlaywrightFrameLocator(this._skyrampPage, originalFrameLocator);
819
+ }
820
+
821
+ last() {
822
+ const originalFrameLocator = this._frameLocator.last();
823
+ return new SkyrampPlaywrightFrameLocator(this._skyrampPage, originalFrameLocator);
824
+ }
825
+ }
826
+
737
827
  class SkyrampPlaywrightPage {
738
828
  constructor(page, testInfo) {
739
829
  checkForUpdate("npm").catch((error) => {
package/src/function.d.ts CHANGED
@@ -18,3 +18,17 @@ export function checkStatusCode(
18
18
  response: ResponseV2,
19
19
  expectedStatus: string
20
20
  ): boolean;
21
+
22
+ /**
23
+ * Validates that a request payload matches the expected reference payload.
24
+ * The reference is a stripped version of the recorded payload (volatile/falsy
25
+ * values removed). Throws an Error with details if validation fails.
26
+ *
27
+ * @param payload - The actual request payload (e.g. from response.request().postDataJSON()).
28
+ * @param reference - The expected (stripped) reference payload.
29
+ * @throws {Error} If the payload does not match the reference.
30
+ */
31
+ export function checkRequestPayload(
32
+ payload: object,
33
+ reference: object
34
+ ): void;
package/src/function.js CHANGED
@@ -41,6 +41,151 @@ const checkStatusCodeWrapper = lib.func('checkStatusCodeWrapper', 'int', ['int',
41
41
  }
42
42
  }
43
43
 
44
+ /**
45
+ * Returns true if a value is "truthy" for payload comparison purposes.
46
+ * Truthy: true, non-empty string, non-zero number, non-empty object/array.
47
+ * Falsy: false, null, undefined, 0, "", empty object, empty array.
48
+ * @param {*} value
49
+ * @returns {boolean}
50
+ */
51
+ function _isTruthy(value) {
52
+ if (value === null || value === undefined || value === false || value === 0 || value === '') {
53
+ return false;
54
+ }
55
+ if (Array.isArray(value)) {
56
+ return value.length > 0 && value.some(elem => _isTruthy(elem));
57
+ }
58
+ if (typeof value === 'object') {
59
+ return Object.keys(value).length > 0;
60
+ }
61
+ return true;
62
+ }
63
+
64
+ /**
65
+ * Returns true if a field key indicates a server-generated ID.
66
+ * @param {string} key
67
+ * @returns {boolean}
68
+ */
69
+ function _isVolatileKey(key) {
70
+ return key === 'id' || key.endsWith('_id');
71
+ }
72
+
73
+ /**
74
+ * Returns true if a string value matches a known volatile pattern.
75
+ * @param {string} s
76
+ * @returns {boolean}
77
+ */
78
+ function _isVolatileValue(value) {
79
+ if (typeof value === 'string') {
80
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value) ||
81
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})$/.test(value) ||
82
+ /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value) ||
83
+ /^[0-9a-f]{24}$/i.test(value) ||
84
+ /^[0-9a-f]{32,}$/i.test(value) ||
85
+ /^https?:\/\/\S+\/[0-9a-f]{24,}/i.test(value);
86
+ }
87
+ if (typeof value === 'number') {
88
+ return value >= 1_000_000_000;
89
+ }
90
+ return false;
91
+ }
92
+
93
+ /**
94
+ * Recursively compares an actual request payload against a stripped reference
95
+ * payload. Returns an array of path-annotated error strings.
96
+ *
97
+ * Rules:
98
+ * - Every field in reference must exist in actual with the same value.
99
+ * - Every truthy field in actual that is NOT in reference is an error,
100
+ * unless the key or value matches a known volatile pattern.
101
+ * - Arrays must have the same length; null reference elements are skipped (volatile).
102
+ *
103
+ * @param {*} actual
104
+ * @param {*} reference
105
+ * @param {string} [path]
106
+ * @returns {string[]}
107
+ */
108
+ function _comparePayload(actual, reference, path) {
109
+ const loc = path || 'root';
110
+ const errors = [];
111
+
112
+ // Null reference element means volatile/falsy — skip
113
+ if (reference === null || reference === undefined) {
114
+ return errors;
115
+ }
116
+
117
+ if (actual === null || actual === undefined) {
118
+ errors.push(`${loc}: expected ${JSON.stringify(reference)}, got ${actual}`);
119
+ return errors;
120
+ }
121
+
122
+ // Array comparison
123
+ if (Array.isArray(reference)) {
124
+ if (!Array.isArray(actual)) {
125
+ errors.push(`${loc}: expected array, got ${typeof actual}`);
126
+ return errors;
127
+ }
128
+ if (actual.length !== reference.length) {
129
+ errors.push(`${loc}: expected array length ${reference.length}, got ${actual.length}`);
130
+ return errors;
131
+ }
132
+ for (let i = 0; i < reference.length; i++) {
133
+ if (reference[i] === null) continue; // volatile element — skip
134
+ errors.push(..._comparePayload(actual[i], reference[i], `${loc}[${i}]`));
135
+ }
136
+ return errors;
137
+ }
138
+
139
+ // Object comparison
140
+ if (typeof reference === 'object') {
141
+ if (typeof actual !== 'object' || Array.isArray(actual)) {
142
+ errors.push(`${loc}: expected object, got ${Array.isArray(actual) ? 'array' : typeof actual}`);
143
+ return errors;
144
+ }
145
+ // Check all reference fields exist and match
146
+ for (const key of Object.keys(reference)) {
147
+ if (!(key in actual)) {
148
+ errors.push(`${loc}.${key}: missing (expected ${JSON.stringify(reference[key])})`);
149
+ } else {
150
+ errors.push(..._comparePayload(actual[key], reference[key], `${loc}.${key}`));
151
+ }
152
+ }
153
+ // Check for unexpected truthy fields in actual (skip volatile keys/values)
154
+ for (const key of Object.keys(actual)) {
155
+ if (!(key in reference) && _isTruthy(actual[key]) && !_isVolatileKey(key) && !_isVolatileValue(actual[key])) {
156
+ errors.push(`${loc}.${key}: unexpected truthy field (value: ${JSON.stringify(actual[key])})`);
157
+ }
158
+ }
159
+ return errors;
160
+ }
161
+
162
+ // Primitive comparison
163
+ if (actual !== reference) {
164
+ errors.push(`${loc}: expected ${JSON.stringify(reference)}, got ${JSON.stringify(actual)}`);
165
+ }
166
+
167
+ return errors;
168
+ }
169
+
170
+ /**
171
+ * Validates that a request payload matches the expected reference payload.
172
+ * The reference is a stripped version of the recorded payload (volatile/falsy
173
+ * values removed). Throws an Error with details if validation fails.
174
+ *
175
+ * @param {object} payload - The actual request payload (e.g. from response.request().postDataJSON()).
176
+ * @param {object} reference - The expected (stripped) reference payload.
177
+ * @throws {Error} If the payload does not match the reference.
178
+ */
179
+ function checkRequestPayload(payload, reference) {
180
+ const errors = _comparePayload(payload, reference, '');
181
+ if (errors.length > 0) {
182
+ throw new Error(
183
+ `Request payload validation failed:\n${errors.join('\n')}`
184
+ );
185
+ }
186
+ }
187
+
44
188
  module.exports = {
45
- checkStatusCode
189
+ checkStatusCode,
190
+ checkRequestPayload,
46
191
  };
package/src/index.js CHANGED
@@ -17,8 +17,8 @@ const { AsyncScenario, AsyncRequest } = require('./classes/AsyncScenario');
17
17
  const { LoadTestConfig } = require('./classes/LoadTestConfig');
18
18
  const AsyncTestStatus = require('./classes/AsyncTestStatus');
19
19
  const MockV2 = require('./classes/MockV2');
20
- const { getValue, getResponseValue, checkSchema, iterate, pushToolEvent } = require('./utils');
21
- const { checkStatusCode } = require('./function');
20
+ const { getValue, getResponseValue, checkSchema, iterate, pushToolEvent, getBaseUrl } = require('./utils');
21
+ const { checkStatusCode, checkRequestPayload } = require('./function');
22
22
  const { newSkyrampPlaywrightPage, expect } = require('./classes/SmartPlaywright');
23
23
  const {
24
24
  workspaceConfigSchema,
@@ -54,9 +54,11 @@ module.exports = {
54
54
  getValue,
55
55
  getResponseValue,
56
56
  checkStatusCode,
57
+ checkRequestPayload,
57
58
  checkSchema,
58
59
  iterate,
59
60
  pushToolEvent,
61
+ getBaseUrl,
60
62
  newSkyrampPlaywrightPage,
61
63
  expect,
62
64
  workspaceConfigSchema,
package/src/utils.d.ts CHANGED
@@ -104,3 +104,11 @@ export function checkForUpdate(component: string): Promise<void>;
104
104
  * The Skyramp YAML version constant.
105
105
  */
106
106
  export const SKYRAMP_YAML_VERSION: string;
107
+
108
+ /**
109
+ * Returns the base URL from an environment variable, with a fallback default.
110
+ * @param envVar - The environment variable name
111
+ * @param defaultUrl - The default URL if the env var is not set
112
+ * @returns The resolved base URL
113
+ */
114
+ export function getBaseUrl(envVar: string, defaultUrl: string): string;
package/src/utils.js CHANGED
@@ -257,6 +257,10 @@ function checkForUpdate(component) {
257
257
  });
258
258
  }
259
259
 
260
+ function getBaseUrl(envVar, defaultUrl) {
261
+ return process.env[envVar] ?? defaultUrl;
262
+ }
263
+
260
264
  module.exports = {
261
265
  createTestDescriptionFromScenario,
262
266
  getYamlBytes,
@@ -267,5 +271,6 @@ module.exports = {
267
271
  iterate,
268
272
  pushToolEvent,
269
273
  checkForUpdate,
274
+ getBaseUrl,
270
275
  SKYRAMP_YAML_VERSION
271
276
  }