qase-javascript-commons 2.6.2 → 2.6.4
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/changelog.md +47 -0
- package/dist/client/clientV1.d.ts +6 -83
- package/dist/client/clientV1.js +15 -549
- package/dist/client/clientV2.d.ts +6 -24
- package/dist/client/clientV2.js +8 -255
- package/dist/client/services/api-error-handler.d.ts +10 -0
- package/dist/client/services/api-error-handler.js +44 -0
- package/dist/client/services/attachment-service.d.ts +16 -0
- package/dist/client/services/attachment-service.js +209 -0
- package/dist/client/services/configuration-service.d.ts +12 -0
- package/dist/client/services/configuration-service.js +110 -0
- package/dist/client/services/result-transformer.d.ts +23 -0
- package/dist/client/services/result-transformer.js +188 -0
- package/dist/client/services/run-service.d.ts +17 -0
- package/dist/client/services/run-service.js +114 -0
- package/dist/client/transport/api-config-builder.d.ts +8 -0
- package/dist/client/transport/api-config-builder.js +96 -0
- package/dist/formatter/index.d.ts +1 -0
- package/dist/formatter/index.js +3 -1
- package/dist/formatter/report-serializer.d.ts +20 -0
- package/dist/formatter/report-serializer.js +89 -0
- package/dist/qase/options-resolver.d.ts +19 -0
- package/dist/qase/options-resolver.js +47 -0
- package/dist/qase/reporter-factory.d.ts +19 -0
- package/dist/qase/reporter-factory.js +67 -0
- package/dist/qase/status-processor.d.ts +17 -0
- package/dist/qase/status-processor.js +48 -0
- package/dist/qase.d.ts +17 -85
- package/dist/qase.js +133 -415
- package/dist/reporters/report-reporter.d.ts +4 -35
- package/dist/reporters/report-reporter.js +6 -130
- package/dist/reporters/shared/fallback-coordinator.d.ts +47 -0
- package/dist/reporters/shared/fallback-coordinator.js +119 -0
- package/dist/reporters/shared/testops-constants.d.ts +5 -0
- package/dist/reporters/shared/testops-constants.js +8 -0
- package/dist/reporters/shared/testops-url.d.ts +9 -0
- package/dist/reporters/shared/testops-url.js +17 -0
- package/dist/reporters/testops-multi-reporter.d.ts +0 -1
- package/dist/reporters/testops-multi-reporter.js +4 -9
- package/dist/reporters/testops-reporter.d.ts +0 -6
- package/dist/reporters/testops-reporter.js +7 -17
- package/dist/utils/token-masker.d.ts +11 -0
- package/dist/utils/token-masker.js +26 -0
- package/package.json +1 -1
package/dist/client/clientV1.js
CHANGED
|
@@ -1,576 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.ClientV1 = void 0;
|
|
7
4
|
const qase_api_client_1 = require("qase-api-client");
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const dateUtils_1 = require("./dateUtils");
|
|
13
|
-
const form_data_1 = __importDefault(require("form-data"));
|
|
14
|
-
const DEFAULT_API_HOST = 'qase.io';
|
|
15
|
-
const API_BASE_URL = 'https://api-';
|
|
16
|
-
const APP_BASE_URL = 'https://';
|
|
17
|
-
const API_VERSION = '/v1';
|
|
18
|
-
// Attachment upload limits
|
|
19
|
-
const MAX_FILE_SIZE = 32 * 1024 * 1024; // 32 MB per file
|
|
20
|
-
const MAX_REQUEST_SIZE = 128 * 1024 * 1024; // 128 MB per request
|
|
21
|
-
const MAX_FILES_PER_REQUEST = 20; // 20 files per request
|
|
22
|
-
var ApiErrorCode;
|
|
23
|
-
(function (ApiErrorCode) {
|
|
24
|
-
ApiErrorCode[ApiErrorCode["UNAUTHORIZED"] = 401] = "UNAUTHORIZED";
|
|
25
|
-
ApiErrorCode[ApiErrorCode["FORBIDDEN"] = 403] = "FORBIDDEN";
|
|
26
|
-
ApiErrorCode[ApiErrorCode["NOT_FOUND"] = 404] = "NOT_FOUND";
|
|
27
|
-
ApiErrorCode[ApiErrorCode["BAD_REQUEST"] = 400] = "BAD_REQUEST";
|
|
28
|
-
ApiErrorCode[ApiErrorCode["UNPROCESSABLE_ENTITY"] = 422] = "UNPROCESSABLE_ENTITY";
|
|
29
|
-
})(ApiErrorCode || (ApiErrorCode = {}));
|
|
5
|
+
const api_config_builder_1 = require("./transport/api-config-builder");
|
|
6
|
+
const attachment_service_1 = require("./services/attachment-service");
|
|
7
|
+
const configuration_service_1 = require("./services/configuration-service");
|
|
8
|
+
const run_service_1 = require("./services/run-service");
|
|
30
9
|
class ClientV1 {
|
|
31
10
|
logger;
|
|
32
11
|
config;
|
|
33
12
|
environment;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
environmentClient;
|
|
37
|
-
attachmentClient;
|
|
38
|
-
configurationClient;
|
|
13
|
+
attachmentService;
|
|
14
|
+
runService;
|
|
39
15
|
constructor(logger, config, environment) {
|
|
40
16
|
this.logger = logger;
|
|
41
17
|
this.config = config;
|
|
42
18
|
this.environment = environment;
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
this.
|
|
47
|
-
this.
|
|
48
|
-
this.configurationClient = new qase_api_client_1.ConfigurationsApi(apiConfig);
|
|
49
|
-
}
|
|
50
|
-
createApiConfig() {
|
|
51
|
-
const apiConfig = new qase_api_client_1.Configuration({ apiKey: this.config.api.token, formDataCtor: form_data_1.default });
|
|
52
|
-
if (this.config.api.host && this.config.api.host != DEFAULT_API_HOST) {
|
|
53
|
-
apiConfig.basePath = `${API_BASE_URL}${this.config.api.host}${API_VERSION}`;
|
|
54
|
-
return { apiConfig, appUrl: `${APP_BASE_URL}${this.config.api.host}` };
|
|
55
|
-
}
|
|
56
|
-
apiConfig.basePath = `https://api.${DEFAULT_API_HOST}${API_VERSION}`;
|
|
57
|
-
return { apiConfig, appUrl: `https://app.${DEFAULT_API_HOST}` };
|
|
19
|
+
const apiConfig = (0, api_config_builder_1.createApiConfigV1)(config);
|
|
20
|
+
const appUrl = (0, api_config_builder_1.resolveAppUrl)(config);
|
|
21
|
+
const configurationService = new configuration_service_1.ConfigurationService(logger, new qase_api_client_1.ConfigurationsApi(apiConfig));
|
|
22
|
+
this.runService = new run_service_1.RunService(logger, new qase_api_client_1.RunsApi(apiConfig), new qase_api_client_1.EnvironmentsApi(apiConfig), configurationService, appUrl);
|
|
23
|
+
this.attachmentService = new attachment_service_1.AttachmentService(logger, new qase_api_client_1.AttachmentsApi(apiConfig));
|
|
58
24
|
}
|
|
59
25
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
60
26
|
uploadResults(_runId, _results) {
|
|
61
27
|
throw new Error('Use ClientV2 to upload results');
|
|
62
28
|
}
|
|
63
29
|
async createRun() {
|
|
64
|
-
|
|
65
|
-
return this.config.run.id;
|
|
66
|
-
}
|
|
67
|
-
try {
|
|
68
|
-
// Handle configurations if provided
|
|
69
|
-
let configurationIds = [];
|
|
70
|
-
if (this.config.configurations) {
|
|
71
|
-
configurationIds = await this.handleConfigurations();
|
|
72
|
-
}
|
|
73
|
-
const environmentId = await this.getEnvironmentId();
|
|
74
|
-
const runObject = this.prepareRunObject(environmentId, configurationIds);
|
|
75
|
-
this.logger.logDebug(`Creating test run: ${JSON.stringify(runObject)}`);
|
|
76
|
-
const { data } = await this.runClient.createRun(this.config.project, runObject);
|
|
77
|
-
if (!data.result?.id) {
|
|
78
|
-
throw new qase_error_1.QaseError('Failed to create test run');
|
|
79
|
-
}
|
|
80
|
-
this.logger.logDebug(`Test run created: ${JSON.stringify(data)}`);
|
|
81
|
-
if (this.config.run.externalLink && data.result.id) {
|
|
82
|
-
// Map our enum values to API enum values
|
|
83
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
84
|
-
const apiType = this.config.run.externalLink.type === 'jiraCloud'
|
|
85
|
-
? qase_api_client_1.RunExternalIssuesTypeEnum.JIRA_CLOUD
|
|
86
|
-
: qase_api_client_1.RunExternalIssuesTypeEnum.JIRA_SERVER;
|
|
87
|
-
await this.runClient.runUpdateExternalIssue(this.config.project, {
|
|
88
|
-
type: apiType,
|
|
89
|
-
links: [
|
|
90
|
-
{
|
|
91
|
-
run_id: data.result.id,
|
|
92
|
-
external_issue: this.config.run.externalLink.link,
|
|
93
|
-
},
|
|
94
|
-
],
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
return data.result.id;
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
throw this.processError(error, 'Error creating test run');
|
|
101
|
-
}
|
|
30
|
+
return this.runService.createRun(this.config, this.environment);
|
|
102
31
|
}
|
|
103
32
|
async completeRun(runId) {
|
|
104
|
-
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
try {
|
|
108
|
-
await this.runClient.completeRun(this.config.project, runId);
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
throw this.processError(error, 'Error on completing run');
|
|
112
|
-
}
|
|
113
|
-
if (this.appUrl) {
|
|
114
|
-
const runUrl = `${this.appUrl}/run/${this.config.project}/dashboard/${runId}`;
|
|
115
|
-
this.logger.log((0, chalk_1.default) `{blue Test run link: ${runUrl}}`);
|
|
116
|
-
}
|
|
33
|
+
return this.runService.completeRun(runId, this.config);
|
|
117
34
|
}
|
|
118
35
|
async enablePublicReport(runId) {
|
|
119
|
-
|
|
120
|
-
const { data } = await this.runClient.updateRunPublicity(this.config.project, runId, { status: true });
|
|
121
|
-
if (data.result?.url) {
|
|
122
|
-
this.logger.log((0, chalk_1.default) `{blue Public report link: ${data.result.url}}`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
catch (error) {
|
|
126
|
-
this.logger.log((0, chalk_1.default) `{yellow Failed to generate public report link: ${this.getErrorMessage(error)}}`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
getErrorMessage(error) {
|
|
130
|
-
if ((0, is_axios_error_1.isAxiosError)(error)) {
|
|
131
|
-
const err = error;
|
|
132
|
-
const errorData = err.response?.data;
|
|
133
|
-
return errorData?.errorMessage ?? errorData?.error ?? errorData?.message ?? 'Unknown API error';
|
|
134
|
-
}
|
|
135
|
-
return error instanceof Error ? error.message : 'Unknown error';
|
|
36
|
+
return this.runService.enablePublicReport(this.config.project, runId);
|
|
136
37
|
}
|
|
137
38
|
async uploadAttachment(attachment) {
|
|
138
|
-
|
|
139
|
-
const data = this.prepareAttachmentData(attachment);
|
|
140
|
-
const response = await this.attachmentClient.uploadAttachment(this.config.project, [data]);
|
|
141
|
-
return response.data.result?.[0]?.hash ?? '';
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
throw this.processError(error, 'Error on uploading attachment');
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
async uploadAttachments(attachments) {
|
|
148
|
-
if (!this.config.uploadAttachments) {
|
|
149
|
-
return [];
|
|
150
|
-
}
|
|
151
|
-
const uploadedHashes = [];
|
|
152
|
-
// Filter out invalid attachments and check file size limits
|
|
153
|
-
const validAttachments = [];
|
|
154
|
-
for (const attachment of attachments) {
|
|
155
|
-
if (!attachment) {
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
// Ensure attachment size is calculated if not set or is 0
|
|
159
|
-
this.ensureAttachmentSize(attachment);
|
|
160
|
-
// Skip attachments with unknown size (0)
|
|
161
|
-
if (attachment.size === 0) {
|
|
162
|
-
this.logger.logError(`Cannot determine size for attachment "${attachment.file_path ?? attachment.file_name}". Skipping.`);
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
// Check if file exceeds maximum size per file (32 MB)
|
|
166
|
-
if (attachment.size > MAX_FILE_SIZE) {
|
|
167
|
-
this.logger.logError(`Attachment "${attachment.file_path ?? attachment.file_name}" exceeds maximum file size (32 MB). ` +
|
|
168
|
-
`File size: ${(attachment.size / (1024 * 1024)).toFixed(2)} MB. Skipping.`);
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
validAttachments.push(attachment);
|
|
172
|
-
}
|
|
173
|
-
if (validAttachments.length === 0) {
|
|
174
|
-
return uploadedHashes;
|
|
175
|
-
}
|
|
176
|
-
// Add initial random delay to spread out requests from different workers/shard
|
|
177
|
-
// This helps prevent all workers from hitting the API at the same time
|
|
178
|
-
const initialJitter = Math.random() * 500; // 0-500ms random delay
|
|
179
|
-
await this.delay(initialJitter);
|
|
180
|
-
// Group attachments into batches that respect API limits
|
|
181
|
-
const batches = this.groupAttachmentsIntoBatches(validAttachments);
|
|
182
|
-
this.logger.logDebug(`Uploading ${validAttachments.length} attachments in ${batches.length} batch(es)`);
|
|
183
|
-
// Upload each batch
|
|
184
|
-
for (let i = 0; i < batches.length; i++) {
|
|
185
|
-
const batch = batches[i];
|
|
186
|
-
if (!batch || batch.length === 0) {
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
try {
|
|
190
|
-
const batchNames = batch.map(a => a.file_path ?? a.file_name).join(', ');
|
|
191
|
-
this.logger.logDebug(`Uploading batch ${i + 1}/${batches.length} with ${batch.length} file(s): ${batchNames}`);
|
|
192
|
-
const batchData = batch.map(attachment => this.prepareAttachmentData(attachment));
|
|
193
|
-
const response = await this.uploadAttachmentWithRetry(this.config.project, batchData, batchNames);
|
|
194
|
-
// Extract all hashes from the response
|
|
195
|
-
if (response.data.result) {
|
|
196
|
-
for (const result of response.data.result) {
|
|
197
|
-
if (result.hash) {
|
|
198
|
-
uploadedHashes.push(result.hash);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
catch (error) {
|
|
204
|
-
this.logger.logError(`Cannot upload batch ${i + 1}:`, error);
|
|
205
|
-
// Continue with next batch even if current batch fails
|
|
206
|
-
}
|
|
207
|
-
// Add delay between batches to avoid rate limiting
|
|
208
|
-
// Skip delay after the last batch
|
|
209
|
-
if (i < batches.length - 1) {
|
|
210
|
-
// Increased delay with random jitter to prevent synchronization
|
|
211
|
-
const baseDelay = 1000; // 1000ms (1 second) base delay
|
|
212
|
-
const jitter = Math.random() * 300; // 0-300ms random jitter
|
|
213
|
-
await this.delay(baseDelay + jitter);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return uploadedHashes;
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Group attachments into batches respecting API limits:
|
|
220
|
-
* - Up to 20 files per batch
|
|
221
|
-
* - Up to 128 MB per batch
|
|
222
|
-
* @param attachments Array of attachments to group
|
|
223
|
-
* @returns Array of attachment batches
|
|
224
|
-
*/
|
|
225
|
-
groupAttachmentsIntoBatches(attachments) {
|
|
226
|
-
const batches = [];
|
|
227
|
-
let currentBatch = [];
|
|
228
|
-
let currentBatchSize = 0;
|
|
229
|
-
for (const attachment of attachments) {
|
|
230
|
-
const attachmentSize = attachment.size;
|
|
231
|
-
// Check if adding this attachment would exceed limits
|
|
232
|
-
const wouldExceedFileLimit = currentBatch.length >= MAX_FILES_PER_REQUEST;
|
|
233
|
-
const wouldExceedSizeLimit = currentBatchSize + attachmentSize > MAX_REQUEST_SIZE;
|
|
234
|
-
// If current batch is full or would exceed limits, start a new batch
|
|
235
|
-
if (wouldExceedFileLimit || wouldExceedSizeLimit) {
|
|
236
|
-
if (currentBatch.length > 0) {
|
|
237
|
-
batches.push(currentBatch);
|
|
238
|
-
currentBatch = [];
|
|
239
|
-
currentBatchSize = 0;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
// If a single file exceeds request size limit, it should have been filtered earlier
|
|
243
|
-
// but we check again as a safety measure
|
|
244
|
-
if (attachmentSize > MAX_REQUEST_SIZE) {
|
|
245
|
-
this.logger.logError(`Attachment "${attachment.file_path ?? attachment.file_name}" exceeds maximum request size (128 MB). ` +
|
|
246
|
-
`File size: ${(attachmentSize / (1024 * 1024)).toFixed(2)} MB. Skipping.`);
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
currentBatch.push(attachment);
|
|
250
|
-
currentBatchSize += attachmentSize;
|
|
251
|
-
}
|
|
252
|
-
// Add the last batch if it's not empty
|
|
253
|
-
if (currentBatch.length > 0) {
|
|
254
|
-
batches.push(currentBatch);
|
|
255
|
-
}
|
|
256
|
-
return batches;
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Upload attachment with retry logic for 429 errors
|
|
260
|
-
* @param project Project code
|
|
261
|
-
* @param data Attachment data array (can contain multiple files)
|
|
262
|
-
* @param attachmentNames Attachment names for logging (comma-separated for batches)
|
|
263
|
-
* @param maxRetries Maximum number of retry attempts
|
|
264
|
-
* @param initialDelay Initial delay in milliseconds
|
|
265
|
-
* @returns Promise with upload response
|
|
266
|
-
*/
|
|
267
|
-
async uploadAttachmentWithRetry(project, data, attachmentNames, maxRetries = 5, initialDelay = 1000) {
|
|
268
|
-
let lastError;
|
|
269
|
-
let delay = initialDelay;
|
|
270
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
271
|
-
try {
|
|
272
|
-
return await this.attachmentClient.uploadAttachment(project, data);
|
|
273
|
-
}
|
|
274
|
-
catch (error) {
|
|
275
|
-
lastError = error;
|
|
276
|
-
// Check if it's a 429 error (Too Many Requests)
|
|
277
|
-
if ((0, is_axios_error_1.isAxiosError)(error)) {
|
|
278
|
-
if (error.response?.status === 429) {
|
|
279
|
-
if (attempt < maxRetries) {
|
|
280
|
-
const retryAfter = this.getRetryAfter(error);
|
|
281
|
-
const baseWaitTime = retryAfter ?? delay;
|
|
282
|
-
// Add jitter (random delay) to prevent all workers from retrying simultaneously
|
|
283
|
-
// Jitter is 10-30% of the wait time to spread out retry attempts
|
|
284
|
-
const jitterPercent = 0.1 + Math.random() * 0.2; // 10-30%
|
|
285
|
-
const jitter = baseWaitTime * jitterPercent;
|
|
286
|
-
const waitTime = Math.floor(baseWaitTime + jitter);
|
|
287
|
-
this.logger.logDebug(`Rate limit exceeded (429) for attachment(s) "${attachmentNames}". ` +
|
|
288
|
-
`Retrying in ${waitTime}ms (attempt ${attempt + 1}/${maxRetries})`);
|
|
289
|
-
await this.delay(waitTime);
|
|
290
|
-
// Exponential backoff: double the delay for next attempt
|
|
291
|
-
delay = Math.min(delay * 2, 30000); // Cap at 30 seconds
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
this.logger.logError(`Failed to upload attachment(s) "${attachmentNames}" after ${maxRetries} retries due to rate limiting`);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
else {
|
|
298
|
-
// For non-429 errors, throw immediately
|
|
299
|
-
throw error;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
// For non-Axios errors, throw immediately
|
|
304
|
-
throw error;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
// If we exhausted all retries, throw the last error
|
|
309
|
-
throw lastError;
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Extract Retry-After header value from response or return null
|
|
313
|
-
* @param error Axios error
|
|
314
|
-
* @returns Retry-After value in milliseconds or null
|
|
315
|
-
*/
|
|
316
|
-
getRetryAfter(error) {
|
|
317
|
-
const headers = error.response?.headers;
|
|
318
|
-
if (!headers) {
|
|
319
|
-
return null;
|
|
320
|
-
}
|
|
321
|
-
const retryAfterHeader = headers['retry-after'];
|
|
322
|
-
if (retryAfterHeader && typeof retryAfterHeader === 'string') {
|
|
323
|
-
const retryAfterSeconds = parseInt(retryAfterHeader, 10);
|
|
324
|
-
if (!isNaN(retryAfterSeconds)) {
|
|
325
|
-
return retryAfterSeconds * 1000; // Convert to milliseconds
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
return null;
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Delay execution for specified milliseconds
|
|
332
|
-
* @param ms Milliseconds to delay
|
|
333
|
-
*/
|
|
334
|
-
delay(ms) {
|
|
335
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* Ensure attachment size is calculated if not set or is 0
|
|
339
|
-
* @param attachment Attachment to ensure size for
|
|
340
|
-
*/
|
|
341
|
-
ensureAttachmentSize(attachment) {
|
|
342
|
-
// If size is already set and greater than 0, use it
|
|
343
|
-
if (attachment.size > 0) {
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
try {
|
|
347
|
-
if (attachment.file_path) {
|
|
348
|
-
// Get file size from file system
|
|
349
|
-
const stats = (0, fs_1.statSync)(attachment.file_path);
|
|
350
|
-
attachment.size = stats.size;
|
|
351
|
-
}
|
|
352
|
-
else if (attachment.content) {
|
|
353
|
-
// Calculate size from content
|
|
354
|
-
if (typeof attachment.content === 'string') {
|
|
355
|
-
// For strings, check if it's base64 encoded
|
|
356
|
-
if (attachment.content.match(/^[A-Za-z0-9+/=]+$/)) {
|
|
357
|
-
// Base64 encoded string
|
|
358
|
-
attachment.size = Buffer.from(attachment.content, 'base64').length;
|
|
359
|
-
}
|
|
360
|
-
else {
|
|
361
|
-
// Regular string - use byte length
|
|
362
|
-
attachment.size = Buffer.byteLength(attachment.content, 'utf8');
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
else if (Buffer.isBuffer(attachment.content)) {
|
|
366
|
-
// Buffer - use length
|
|
367
|
-
attachment.size = attachment.content.length;
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
// Fallback: try to convert to string and get byte length
|
|
371
|
-
attachment.size = Buffer.byteLength(String(attachment.content), 'utf8');
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
catch (error) {
|
|
376
|
-
// If we can't determine size, log warning and set to 0
|
|
377
|
-
this.logger.logDebug(`Could not determine size for attachment "${attachment.file_path ?? attachment.file_name}": ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
378
|
-
attachment.size = 0;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
prepareAttachmentData(attachment) {
|
|
382
|
-
if (attachment.file_path) {
|
|
383
|
-
return {
|
|
384
|
-
name: attachment.file_name,
|
|
385
|
-
value: (0, fs_1.createReadStream)(attachment.file_path),
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
return {
|
|
389
|
-
name: attachment.file_name,
|
|
390
|
-
value: typeof attachment.content === 'string'
|
|
391
|
-
? Buffer.from(attachment.content, attachment.content.match(/^[A-Za-z0-9+/=]+$/) ? 'base64' : undefined)
|
|
392
|
-
: attachment.content,
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
async getEnvironmentId() {
|
|
396
|
-
if (!this.environment)
|
|
397
|
-
return undefined;
|
|
398
|
-
const { data } = await this.environmentClient.getEnvironments(this.config.project, undefined, this.environment, 100);
|
|
399
|
-
return data.result?.entities?.find((env) => env.slug === this.environment)?.id;
|
|
400
|
-
}
|
|
401
|
-
prepareRunObject(environmentId, configurationIds) {
|
|
402
|
-
const runObject = {
|
|
403
|
-
title: this.config.run.title ?? `Automated run ${new Date().toISOString()}`,
|
|
404
|
-
description: this.config.run.description ?? '',
|
|
405
|
-
is_autotest: true,
|
|
406
|
-
cases: [],
|
|
407
|
-
start_time: (0, dateUtils_1.getStartTime)(),
|
|
408
|
-
tags: this.config.run.tags ?? [],
|
|
409
|
-
};
|
|
410
|
-
if (environmentId !== undefined) {
|
|
411
|
-
runObject.environment_id = environmentId;
|
|
412
|
-
}
|
|
413
|
-
if (this.config.plan.id) {
|
|
414
|
-
runObject.plan_id = this.config.plan.id;
|
|
415
|
-
}
|
|
416
|
-
if (configurationIds && configurationIds.length > 0) {
|
|
417
|
-
runObject.configurations = configurationIds;
|
|
418
|
-
}
|
|
419
|
-
return runObject;
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Get all configuration groups with their configurations
|
|
423
|
-
* @returns Promise<ConfigurationGroup[]> Array of configuration groups
|
|
424
|
-
* @private
|
|
425
|
-
*/
|
|
426
|
-
async getConfigurations() {
|
|
427
|
-
try {
|
|
428
|
-
const { data } = await this.configurationClient.getConfigurations(this.config.project);
|
|
429
|
-
const entities = data.result?.entities ?? [];
|
|
430
|
-
// Convert API response to domain model
|
|
431
|
-
return entities.map(group => ({
|
|
432
|
-
id: group.id ?? 0,
|
|
433
|
-
title: group.title ?? '',
|
|
434
|
-
configurations: group.configurations?.map(config => ({
|
|
435
|
-
id: config.id ?? 0,
|
|
436
|
-
title: config.title ?? ''
|
|
437
|
-
})) ?? []
|
|
438
|
-
}));
|
|
439
|
-
}
|
|
440
|
-
catch (error) {
|
|
441
|
-
throw this.processError(error, 'Error getting configurations');
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* Create a configuration group
|
|
446
|
-
* @param title Group title
|
|
447
|
-
* @returns Promise<number | undefined> Created group ID
|
|
448
|
-
* @private
|
|
449
|
-
*/
|
|
450
|
-
async createConfigurationGroup(title) {
|
|
451
|
-
try {
|
|
452
|
-
const group = { title };
|
|
453
|
-
const { data } = await this.configurationClient.createConfigurationGroup(this.config.project, group);
|
|
454
|
-
return data.result?.id;
|
|
455
|
-
}
|
|
456
|
-
catch (error) {
|
|
457
|
-
throw this.processError(error, 'Error creating configuration group');
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
/**
|
|
461
|
-
* Create a configuration in a group
|
|
462
|
-
* @param title Configuration title
|
|
463
|
-
* @param groupId Group ID
|
|
464
|
-
* @returns Promise<number | undefined> Created configuration ID
|
|
465
|
-
* @private
|
|
466
|
-
*/
|
|
467
|
-
async createConfiguration(title, groupId) {
|
|
468
|
-
try {
|
|
469
|
-
const config = { title, group_id: groupId };
|
|
470
|
-
const { data } = await this.configurationClient.createConfiguration(this.config.project, config);
|
|
471
|
-
return data.result?.id;
|
|
472
|
-
}
|
|
473
|
-
catch (error) {
|
|
474
|
-
throw this.processError(error, 'Error creating configuration');
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* Handle configuration creation based on config settings
|
|
479
|
-
* @returns Promise<number[]> Array of configuration IDs
|
|
480
|
-
* @private
|
|
481
|
-
*/
|
|
482
|
-
async handleConfigurations() {
|
|
483
|
-
if (!this.config.configurations?.values.length) {
|
|
484
|
-
return [];
|
|
485
|
-
}
|
|
486
|
-
const configurationIds = [];
|
|
487
|
-
try {
|
|
488
|
-
// Get existing configuration groups
|
|
489
|
-
const existingGroups = await this.getConfigurations();
|
|
490
|
-
for (const configValue of this.config.configurations.values) {
|
|
491
|
-
const { name: groupName, value: configName } = configValue;
|
|
492
|
-
// Find existing group or create new one
|
|
493
|
-
const group = existingGroups.find(g => g.title === groupName);
|
|
494
|
-
let groupId;
|
|
495
|
-
if (group) {
|
|
496
|
-
groupId = group.id;
|
|
497
|
-
this.logger.logDebug(`Found existing configuration group: ${groupName}`);
|
|
498
|
-
}
|
|
499
|
-
else {
|
|
500
|
-
if (this.config.configurations.createIfNotExists) {
|
|
501
|
-
const newGroupId = await this.createConfigurationGroup(groupName);
|
|
502
|
-
if (newGroupId) {
|
|
503
|
-
groupId = newGroupId;
|
|
504
|
-
this.logger.logDebug(`Created new configuration group: ${groupName} with ID: ${groupId}`);
|
|
505
|
-
}
|
|
506
|
-
else {
|
|
507
|
-
this.logger.logDebug(`Failed to create configuration group: ${groupName}, skipping`);
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
else {
|
|
512
|
-
this.logger.logDebug(`Configuration group not found: ${groupName}, skipping`);
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
if (groupId) {
|
|
517
|
-
// Check if configuration already exists in the group
|
|
518
|
-
const existingConfig = group?.configurations.find(c => c.title === configName);
|
|
519
|
-
if (!existingConfig) {
|
|
520
|
-
// Check if we should create configuration if it doesn't exist
|
|
521
|
-
if (this.config.configurations.createIfNotExists) {
|
|
522
|
-
const configId = await this.createConfiguration(configName, groupId);
|
|
523
|
-
if (configId) {
|
|
524
|
-
configurationIds.push(configId);
|
|
525
|
-
}
|
|
526
|
-
this.logger.logDebug(`Created configuration: ${configName} in group: ${groupName}`);
|
|
527
|
-
}
|
|
528
|
-
else {
|
|
529
|
-
this.logger.logDebug(`Configuration not found: ${configName} in group: ${groupName}, skipping`);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
if (existingConfig.id) {
|
|
534
|
-
configurationIds.push(existingConfig.id);
|
|
535
|
-
}
|
|
536
|
-
this.logger.logDebug(`Configuration already exists: ${configName} in group: ${groupName}`);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
catch (error) {
|
|
542
|
-
this.logger.logError('Error handling configurations:', error);
|
|
543
|
-
// Don't throw error to avoid blocking test run creation
|
|
544
|
-
}
|
|
545
|
-
return configurationIds;
|
|
546
|
-
}
|
|
547
|
-
/**
|
|
548
|
-
* Process error and throw QaseError
|
|
549
|
-
* @param {Error | AxiosError} error
|
|
550
|
-
* @param {string} message
|
|
551
|
-
* @param {object} model
|
|
552
|
-
* @private
|
|
553
|
-
*/
|
|
554
|
-
processError(error, message, model) {
|
|
555
|
-
if (!(0, is_axios_error_1.isAxiosError)(error)) {
|
|
556
|
-
return new qase_error_1.QaseError(message, { cause: error });
|
|
557
|
-
}
|
|
558
|
-
const err = error;
|
|
559
|
-
const errorData = err.response?.data;
|
|
560
|
-
const status = err.response?.status;
|
|
561
|
-
switch (status) {
|
|
562
|
-
case ApiErrorCode.UNAUTHORIZED:
|
|
563
|
-
return new qase_error_1.QaseError(`${message}: Unauthorized. Please check your API token.`);
|
|
564
|
-
case ApiErrorCode.FORBIDDEN:
|
|
565
|
-
return new qase_error_1.QaseError(`${message}: ${errorData?.errorMessage ?? 'Forbidden'}`);
|
|
566
|
-
case ApiErrorCode.NOT_FOUND:
|
|
567
|
-
return new qase_error_1.QaseError(`${message}: Not found.`);
|
|
568
|
-
case ApiErrorCode.BAD_REQUEST:
|
|
569
|
-
case ApiErrorCode.UNPROCESSABLE_ENTITY:
|
|
570
|
-
return new qase_error_1.QaseError(`${message}: Bad request\n${JSON.stringify(errorData)}\nBody: ${JSON.stringify(model)}`);
|
|
571
|
-
default:
|
|
572
|
-
return new qase_error_1.QaseError(message, { cause: err });
|
|
573
|
-
}
|
|
39
|
+
return this.attachmentService.uploadAttachment(this.config.project, attachment);
|
|
574
40
|
}
|
|
575
41
|
}
|
|
576
42
|
exports.ClientV1 = ClientV1;
|
|
@@ -1,29 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { HostData } from "../models/host-data";
|
|
1
|
+
import { TestResultType } from '../models';
|
|
2
|
+
import { LoggerInterface } from '../utils/logger';
|
|
3
|
+
import { TestOpsOptionsType } from '../models/config/TestOpsOptionsType';
|
|
4
|
+
import { HostData } from '../models/host-data';
|
|
5
|
+
import { ClientV1 } from './clientV1';
|
|
7
6
|
export declare class ClientV2 extends ClientV1 {
|
|
8
|
-
private readonly rootSuite;
|
|
9
|
-
static statusMap: Record<TestStatusEnum, string>;
|
|
10
|
-
static stepStatusMap: Record<StepStatusEnum, ResultStepStatus>;
|
|
11
7
|
private readonly resultsClient;
|
|
8
|
+
private readonly resultTransformer;
|
|
12
9
|
constructor(logger: LoggerInterface, config: TestOpsOptionsType, environment: string | undefined, rootSuite: string | undefined, hostData?: HostData, reporterName?: string, frameworkName?: string);
|
|
13
|
-
private createApiConfigV2;
|
|
14
|
-
private buildHeaders;
|
|
15
10
|
uploadResults(runId: number, results: TestResultType[]): Promise<void>;
|
|
16
|
-
private transformTestResult;
|
|
17
|
-
private transformParams;
|
|
18
|
-
private transformGroupParams;
|
|
19
|
-
private transformSteps;
|
|
20
|
-
private transformStep;
|
|
21
|
-
private createBaseResultStep;
|
|
22
|
-
private processTextStep;
|
|
23
|
-
private processGherkinStep;
|
|
24
|
-
private processRequestStep;
|
|
25
|
-
private getExecution;
|
|
26
|
-
private getRelation;
|
|
27
|
-
private getDefaultSuiteRelation;
|
|
28
|
-
private buildSuiteData;
|
|
29
11
|
}
|