@skyramp/skyramp 1.3.13 → 1.3.15
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 +1 -1
- package/scripts/download-binary.js +83 -6
- package/src/classes/MockV2.d.ts +6 -0
- package/src/classes/MockV2.js +6 -0
- package/src/classes/MultipartParam.js +26 -12
- package/src/classes/RequestV2.js +1 -1
- package/src/classes/SkyrampClient.d.ts +1 -0
- package/src/classes/SkyrampClient.js +16 -9
- package/src/classes/SmartPlaywright.js +6 -0
- package/src/workspace.d.ts +1 -1
- package/src/workspace.js +1 -1
package/package.json
CHANGED
|
@@ -4,6 +4,18 @@ const path = require('path');
|
|
|
4
4
|
const crypto = require('crypto');
|
|
5
5
|
const { S3Client, GetObjectCommand } = require("@aws-sdk/client-s3");
|
|
6
6
|
|
|
7
|
+
const HEAD_TIMEOUT_MS = 30_000; // 30s for HEAD/ETag check
|
|
8
|
+
const DOWNLOAD_TIMEOUT_MS = 300_000; // 5min for binary download
|
|
9
|
+
const SCRIPT_TIMEOUT_MS = 600_000; // 10min global script timeout
|
|
10
|
+
|
|
11
|
+
// Global script timeout to prevent hanging indefinitely
|
|
12
|
+
const scriptTimer = setTimeout(() => {
|
|
13
|
+
log('error', `Script timed out after ${SCRIPT_TIMEOUT_MS / 1000}s. This is likely a network connectivity issue.`);
|
|
14
|
+
log('error', `Please check your network connection and retry by running: npm install @skyramp/skyramp`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}, SCRIPT_TIMEOUT_MS);
|
|
17
|
+
scriptTimer.unref();
|
|
18
|
+
|
|
7
19
|
function log(level, message) {
|
|
8
20
|
const timestamp = new Date().toISOString();
|
|
9
21
|
console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
|
|
@@ -79,13 +91,22 @@ async function calculateMD5(filePath) {
|
|
|
79
91
|
|
|
80
92
|
async function getETag(url) {
|
|
81
93
|
return new Promise((resolve, reject) => {
|
|
82
|
-
https.get(url, { method: 'HEAD' }, res => {
|
|
94
|
+
const req = https.get(url, { method: 'HEAD', timeout: HEAD_TIMEOUT_MS }, res => {
|
|
83
95
|
if (res.statusCode !== 200) {
|
|
96
|
+
res.resume(); // Drain response to free the socket
|
|
84
97
|
reject(new Error(`HTTP ${res.statusCode}: ${url}`));
|
|
85
98
|
return;
|
|
86
99
|
}
|
|
100
|
+
res.on('error', (err) => {
|
|
101
|
+
reject(new Error(`Response stream error during HEAD request: ${err.message}`));
|
|
102
|
+
});
|
|
87
103
|
resolve(res.headers['etag']?.replace(/"/g, '')); // Remove quotes from ETag
|
|
88
|
-
})
|
|
104
|
+
});
|
|
105
|
+
req.on('timeout', () => {
|
|
106
|
+
req.destroy();
|
|
107
|
+
reject(new Error(`HEAD request timed out after ${HEAD_TIMEOUT_MS / 1000}s: ${url}`));
|
|
108
|
+
});
|
|
109
|
+
req.on('error', (err) => {
|
|
89
110
|
log('error', `Error during HEAD request to ${url}: ${err.message}`);
|
|
90
111
|
reject(err);
|
|
91
112
|
});
|
|
@@ -119,22 +140,61 @@ async function download(url, dest, options = {}) {
|
|
|
119
140
|
});
|
|
120
141
|
} else {
|
|
121
142
|
// Public URL download using https
|
|
122
|
-
|
|
143
|
+
let rejected = false;
|
|
144
|
+
const fail = (err) => {
|
|
145
|
+
if (rejected) return;
|
|
146
|
+
rejected = true;
|
|
147
|
+
// Clean up partial file
|
|
148
|
+
try { fs.unlinkSync(dest); } catch (_) {}
|
|
149
|
+
reject(err);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const req = https.get(url, { timeout: DOWNLOAD_TIMEOUT_MS }, res => {
|
|
123
153
|
if (res.statusCode !== 200) {
|
|
154
|
+
res.resume(); // Drain response to free the socket
|
|
124
155
|
const error = new Error(`HTTP ${res.statusCode}: ${url}`);
|
|
125
156
|
log('error', `Failed to download ${url}: ${error.message}`);
|
|
126
|
-
|
|
157
|
+
fail(error);
|
|
127
158
|
return;
|
|
128
159
|
}
|
|
129
160
|
const file = fs.createWriteStream(dest);
|
|
161
|
+
let downloaded = 0;
|
|
162
|
+
const contentLength = parseInt(res.headers['content-length'], 10);
|
|
163
|
+
let lastLogTime = Date.now();
|
|
164
|
+
|
|
165
|
+
res.on('data', (chunk) => {
|
|
166
|
+
downloaded += chunk.length;
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
if (now - lastLogTime > 10_000) { // Log progress every 10s
|
|
169
|
+
const pct = contentLength ? ` (${Math.round(downloaded / contentLength * 100)}%)` : '';
|
|
170
|
+
log('info', `Downloading ${path.basename(dest)}: ${(downloaded / 1024 / 1024).toFixed(1)} MB${pct}`);
|
|
171
|
+
lastLogTime = now;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
res.on('error', (err) => {
|
|
176
|
+
file.destroy();
|
|
177
|
+
fail(new Error(`Response stream error: ${err.message}`));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
file.on('error', (err) => {
|
|
181
|
+
res.destroy();
|
|
182
|
+
fail(new Error(`File write error: ${err.message}`));
|
|
183
|
+
});
|
|
184
|
+
|
|
130
185
|
res.pipe(file);
|
|
131
186
|
file.on('finish', () => {
|
|
132
187
|
log('info', `Successfully downloaded ${url} to ${dest}`);
|
|
133
188
|
file.close(resolve);
|
|
134
189
|
});
|
|
135
|
-
})
|
|
190
|
+
});
|
|
191
|
+
req.on('timeout', () => {
|
|
192
|
+
req.destroy();
|
|
193
|
+
fail(new Error(`Download timed out after ${DOWNLOAD_TIMEOUT_MS / 1000}s: ${url}`));
|
|
194
|
+
});
|
|
195
|
+
req.on('error', (err) => {
|
|
136
196
|
log('error', `Error during download from ${url}: ${err.message}`);
|
|
137
|
-
|
|
197
|
+
fail(err);
|
|
138
198
|
});
|
|
139
199
|
}
|
|
140
200
|
});
|
|
@@ -175,6 +235,23 @@ async function download(url, dest, options = {}) {
|
|
|
175
235
|
}
|
|
176
236
|
|
|
177
237
|
await download(url, file.dest, options);
|
|
238
|
+
|
|
239
|
+
// Verify downloaded file integrity via ETag/MD5
|
|
240
|
+
if (!S3_PRIVATE) {
|
|
241
|
+
try {
|
|
242
|
+
const remoteETag = await getETag(url);
|
|
243
|
+
const localHash = await calculateMD5(file.dest);
|
|
244
|
+
if (remoteETag && remoteETag !== localHash) {
|
|
245
|
+
log('error', `Hash mismatch for ${file.name}: expected ${remoteETag}, got ${localHash}`);
|
|
246
|
+
try { fs.unlinkSync(file.dest); } catch (_) {}
|
|
247
|
+
throw new Error(`Integrity check failed for ${file.name}. Please check your network connection and retry.`);
|
|
248
|
+
}
|
|
249
|
+
} catch (hashErr) {
|
|
250
|
+
if (hashErr.message.startsWith('Integrity check failed')) throw hashErr;
|
|
251
|
+
log('warn', `Could not verify hash for ${file.name}: ${hashErr.message}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
178
255
|
log('info', `✅ Saved ${file.name} to ${file.dest}`);
|
|
179
256
|
} catch (e) {
|
|
180
257
|
log('error', `❌ Failed to process ${file.name}: ${e.message}`);
|
package/src/classes/MockV2.d.ts
CHANGED
|
@@ -36,6 +36,11 @@ export class MockV2 {
|
|
|
36
36
|
*/
|
|
37
37
|
requestBody?: string | null;
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Optional request form params for request-aware matching
|
|
41
|
+
*/
|
|
42
|
+
requestFormParams?: Record<string, unknown> | null;
|
|
43
|
+
|
|
39
44
|
/**
|
|
40
45
|
* Optional data override object
|
|
41
46
|
*/
|
|
@@ -83,6 +88,7 @@ export class MockV2 {
|
|
|
83
88
|
status_code: number;
|
|
84
89
|
response_body: string;
|
|
85
90
|
request_body?: string;
|
|
91
|
+
request_form_params?: Record<string, unknown>;
|
|
86
92
|
data_override?: Record<string, unknown>;
|
|
87
93
|
client_id?: string;
|
|
88
94
|
};
|
package/src/classes/MockV2.js
CHANGED
|
@@ -38,6 +38,7 @@ class MockV2 {
|
|
|
38
38
|
this.statusCode = options.statusCode || 201;
|
|
39
39
|
this.responseBody = (options.body !== undefined ? options.body : options.responseBody) || '';
|
|
40
40
|
this.requestBody = options.requestBody || null;
|
|
41
|
+
this.requestFormParams = options.requestFormParams || null;
|
|
41
42
|
this.dataOverride = options.dataOverride || null;
|
|
42
43
|
this.clientID = options.clientID || null;
|
|
43
44
|
} else {
|
|
@@ -48,6 +49,7 @@ class MockV2 {
|
|
|
48
49
|
this.statusCode = statusCode;
|
|
49
50
|
this.responseBody = responseBody;
|
|
50
51
|
this.requestBody = requestBody;
|
|
52
|
+
this.requestFormParams = null;
|
|
51
53
|
this.dataOverride = dataOverride;
|
|
52
54
|
this.clientID = null;
|
|
53
55
|
}
|
|
@@ -70,6 +72,10 @@ class MockV2 {
|
|
|
70
72
|
result.request_body = this.requestBody;
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
if (this.requestFormParams !== null) {
|
|
76
|
+
result.request_form_params = this.requestFormParams;
|
|
77
|
+
}
|
|
78
|
+
|
|
73
79
|
if (this.dataOverride !== null) {
|
|
74
80
|
result.data_override = this.dataOverride;
|
|
75
81
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* The `MultipartParam` class represents a multipart/form-data parameter for file uploads or value-based content.
|
|
3
6
|
* @class
|
|
@@ -8,21 +11,22 @@ class MultipartParam {
|
|
|
8
11
|
* @constructor
|
|
9
12
|
* @param {Object} options - An options object containing all parameters.
|
|
10
13
|
* @param {string} options.name - The name of the multipart parameter (required).
|
|
11
|
-
* @param {string} [options.filename=""] -
|
|
14
|
+
* @param {string} [options.filename=""] - Filename for file uploads. When this is a path to an
|
|
15
|
+
* existing file on disk, the file is read and base64-encoded automatically and the basename
|
|
16
|
+
* is used as the Content-Disposition filename sent to the server.
|
|
12
17
|
* @param {boolean} [options.decoded=false] - Whether the content should be decoded.
|
|
13
18
|
* @param {string} [options.value=null] - The value for value-based parameters.
|
|
14
|
-
*
|
|
19
|
+
*
|
|
15
20
|
* @example
|
|
16
|
-
* // File upload
|
|
17
|
-
* new MultipartParam({
|
|
21
|
+
* // File upload — filename is a local path, content is read automatically
|
|
22
|
+
* new MultipartParam({
|
|
18
23
|
* name: 'file',
|
|
19
|
-
* filename: 'document.pdf'
|
|
20
|
-
* decoded: true
|
|
24
|
+
* filename: '/path/to/document.pdf'
|
|
21
25
|
* })
|
|
22
|
-
*
|
|
26
|
+
*
|
|
23
27
|
* @example
|
|
24
28
|
* // Value-based parameter
|
|
25
|
-
* new MultipartParam({
|
|
29
|
+
* new MultipartParam({
|
|
26
30
|
* name: 'attributes',
|
|
27
31
|
* value: '{"product_id": 3}',
|
|
28
32
|
* filename: '',
|
|
@@ -33,11 +37,21 @@ class MultipartParam {
|
|
|
33
37
|
if (typeof options !== 'object' || options === null || !('name' in options)) {
|
|
34
38
|
throw new Error('MultipartParam requires an options object with a name property')
|
|
35
39
|
}
|
|
36
|
-
|
|
37
|
-
this.name
|
|
38
|
-
this.value = options.value || null
|
|
39
|
-
this.filename = options.filename || ""
|
|
40
|
+
|
|
41
|
+
this.name = options.name
|
|
40
42
|
this.decoded = options.decoded || false
|
|
43
|
+
|
|
44
|
+
const filenameOpt = options.filename || ""
|
|
45
|
+
|
|
46
|
+
if (filenameOpt && fs.existsSync(filenameOpt)) {
|
|
47
|
+
// filename is a local filesystem path — read content and use basename as display name
|
|
48
|
+
this.value = fs.readFileSync(filenameOpt).toString('base64')
|
|
49
|
+
this.decoded = false
|
|
50
|
+
this.filename = path.basename(filenameOpt)
|
|
51
|
+
} else {
|
|
52
|
+
this.filename = filenameOpt
|
|
53
|
+
this.value = options.value || null
|
|
54
|
+
}
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
toJson() {
|
package/src/classes/RequestV2.js
CHANGED
|
@@ -36,7 +36,7 @@ class RequestV2 {
|
|
|
36
36
|
this.pathParams = options.pathParams || {};
|
|
37
37
|
this.queryParams = options.queryParams || {};
|
|
38
38
|
this.formParams = options.formParams || {};
|
|
39
|
-
this.multipartParams = options.multipartParams || [];
|
|
39
|
+
this.multipartParams = options.multipartParams || options.multipart_params || [];
|
|
40
40
|
this.expectedCode = options.expectedCode || '';
|
|
41
41
|
this.description = options.description || '';
|
|
42
42
|
this.insecure = options.insecure || false;
|
|
@@ -140,6 +140,7 @@ interface GenerateRestTestOptions {
|
|
|
140
140
|
chainingKey?: string;
|
|
141
141
|
parentRequestData?: Record<string, string> | string;
|
|
142
142
|
parentStatusCode?: Record<string, string> | string;
|
|
143
|
+
parentFormParams?: Record<string, string> | string;
|
|
143
144
|
requestAware?: boolean;
|
|
144
145
|
providerMode?: boolean;
|
|
145
146
|
consumerMode?: boolean;
|
|
@@ -104,12 +104,14 @@ const generateRestTestWrapper = lib.func('generateRestTestWrapper', 'string', [
|
|
|
104
104
|
'string', // chainingKey
|
|
105
105
|
'string', // parentRequestData
|
|
106
106
|
'string', // parentStatusCode
|
|
107
|
+
'string', // parentFormParams
|
|
107
108
|
'bool', // requestAware
|
|
108
109
|
'bool', // providerMode
|
|
109
110
|
'bool', // consumerMode
|
|
110
111
|
'string', // providerOutput (contract test)
|
|
111
|
-
'string',
|
|
112
|
-
'bool'
|
|
112
|
+
'string', // consumerOutput (contract test)
|
|
113
|
+
'bool', // skipProvisionParents
|
|
114
|
+
'int' // mockPort
|
|
113
115
|
]);
|
|
114
116
|
const generateRestMockWrapper = lib.func('generateRestMockWrapper', 'string', [
|
|
115
117
|
'string', // uri
|
|
@@ -124,17 +126,17 @@ const generateRestMockWrapper = lib.func('generateRestMockWrapper', 'string', [
|
|
|
124
126
|
'string', // k8sNamespace
|
|
125
127
|
'string', // k8sConfig
|
|
126
128
|
'string', // k8sContext
|
|
129
|
+
'string', // requestData
|
|
127
130
|
'string', // responseData
|
|
128
131
|
'string', // responseStatusCode
|
|
129
132
|
'bool', // force
|
|
130
133
|
'bool', // deployDashboard
|
|
131
134
|
'bool', // requestAware
|
|
132
135
|
'string', // formParams
|
|
133
|
-
'string', // pathParams
|
|
134
|
-
'string', // queryParams
|
|
135
136
|
'string', // apiSchema
|
|
136
137
|
'string', // traceFilePath
|
|
137
|
-
'string'
|
|
138
|
+
'string', // entryPoint
|
|
139
|
+
'int' // mockPort
|
|
138
140
|
]);
|
|
139
141
|
const traceCollectWrapper = lib.func('traceCollectWrapper', 'string', ['string', 'string', 'bool', 'string', 'string']);
|
|
140
142
|
const analyzeOpenapiWrapper = lib.func('analyzeOpenapiWrapper', 'string', ['string', 'string']);
|
|
@@ -895,6 +897,9 @@ class SkyrampClient {
|
|
|
895
897
|
const parentStatusCode = typeof options.parentStatusCode === 'string'
|
|
896
898
|
? options.parentStatusCode
|
|
897
899
|
: JSON.stringify(options.parentStatusCode || {});
|
|
900
|
+
const parentFormParams = typeof options.parentFormParams === 'string'
|
|
901
|
+
? options.parentFormParams
|
|
902
|
+
: JSON.stringify(options.parentFormParams || {});
|
|
898
903
|
|
|
899
904
|
generateRestTestWrapper.async(
|
|
900
905
|
options.testType || "",
|
|
@@ -947,12 +952,14 @@ class SkyrampClient {
|
|
|
947
952
|
options.chainingKey || "",
|
|
948
953
|
parentRequestData || "",
|
|
949
954
|
parentStatusCode || "",
|
|
955
|
+
parentFormParams || "",
|
|
950
956
|
options.requestAware || false,
|
|
951
|
-
options.providerMode ||
|
|
957
|
+
options.providerMode || false,
|
|
952
958
|
options.consumerMode || false,
|
|
953
959
|
options.providerOutput || "",
|
|
954
960
|
options.consumerOutput || "",
|
|
955
|
-
options.skipProvisionParents ||
|
|
961
|
+
options.skipProvisionParents || false,
|
|
962
|
+
options.mockPort || 0,
|
|
956
963
|
(err, res) => {
|
|
957
964
|
if (err) {
|
|
958
965
|
reject(err);
|
|
@@ -979,17 +986,17 @@ class SkyrampClient {
|
|
|
979
986
|
options.k8sNamespace || "",
|
|
980
987
|
options.k8sConfig || "",
|
|
981
988
|
options.k8sContext || "",
|
|
989
|
+
options.requestData || "",
|
|
982
990
|
options.responseData || "",
|
|
983
991
|
options.responseStatusCode || "",
|
|
984
992
|
options.force || false,
|
|
985
993
|
options.deployDashboard || false,
|
|
986
994
|
options.requestAware || false,
|
|
987
995
|
options.formParams || "",
|
|
988
|
-
options.pathParams || "",
|
|
989
|
-
options.queryParams || "",
|
|
990
996
|
JSON.stringify(options.apiSchema || []),
|
|
991
997
|
options.traceFilePath || "",
|
|
992
998
|
options.entrypoint || "",
|
|
999
|
+
options.mockPort || 0,
|
|
993
1000
|
(err, res) => {
|
|
994
1001
|
if (err) {
|
|
995
1002
|
reject(err);
|
|
@@ -1175,6 +1175,11 @@ class SkyrampPlaywrightPage {
|
|
|
1175
1175
|
return newLocator
|
|
1176
1176
|
}
|
|
1177
1177
|
|
|
1178
|
+
frameLocator(selector) {
|
|
1179
|
+
const originalFrameLocator = this._page.frameLocator(selector);
|
|
1180
|
+
return new SkyrampPlaywrightFrameLocator(this, originalFrameLocator);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1178
1183
|
locator(selector, options) {
|
|
1179
1184
|
const originalLocator = this._page.locator(selector, options);
|
|
1180
1185
|
return this.newSkyrampPlaywrightLocator(originalLocator, selector, options);
|
|
@@ -1434,5 +1439,6 @@ function expect(obj, testInfo) {
|
|
|
1434
1439
|
|
|
1435
1440
|
module.exports = {
|
|
1436
1441
|
newSkyrampPlaywrightPage,
|
|
1442
|
+
SkyrampPlaywrightPage,
|
|
1437
1443
|
expect,
|
|
1438
1444
|
};
|
package/src/workspace.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface Service {
|
|
|
26
26
|
serviceName: string;
|
|
27
27
|
language?: "python" | "typescript" | "javascript" | "java";
|
|
28
28
|
framework?: "playwright" | "pytest" | "robot" | "junit";
|
|
29
|
-
|
|
29
|
+
testDirectory?: string;
|
|
30
30
|
api?: ServiceApi;
|
|
31
31
|
runtimeDetails?: ServiceRuntimeDetails;
|
|
32
32
|
}
|
package/src/workspace.js
CHANGED