@rbaileysr/zephyr-managed-api 1.2.0 → 1.2.8
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 +200 -863
- package/dist/README.md +200 -863
- package/dist/groups/Private/PrivateAttachments.d.ts +697 -0
- package/dist/groups/Private/PrivateAttachments.d.ts.map +1 -0
- package/dist/groups/Private/PrivateAttachments.js +2109 -0
- package/dist/groups/Private/PrivateAuthentication.d.ts +29 -0
- package/dist/groups/Private/PrivateAuthentication.d.ts.map +1 -0
- package/dist/groups/Private/PrivateAuthentication.js +24 -0
- package/dist/groups/Private/PrivateBase.d.ts +32 -0
- package/dist/groups/Private/PrivateBase.d.ts.map +1 -0
- package/dist/groups/Private/PrivateBase.js +77 -0
- package/dist/groups/Private/PrivateComments.d.ts +149 -0
- package/dist/groups/Private/PrivateComments.d.ts.map +1 -0
- package/dist/groups/Private/PrivateComments.js +493 -0
- package/dist/groups/Private/PrivateCustomFields.d.ts +54 -0
- package/dist/groups/Private/PrivateCustomFields.d.ts.map +1 -0
- package/dist/groups/Private/PrivateCustomFields.js +150 -0
- package/dist/groups/Private/PrivateVersions.d.ts +38 -0
- package/dist/groups/Private/PrivateVersions.d.ts.map +1 -0
- package/dist/groups/Private/PrivateVersions.js +99 -0
- package/dist/groups/Private.d.ts +144 -176
- package/dist/groups/Private.d.ts.map +1 -1
- package/dist/groups/Private.js +181 -673
- package/dist/package.json +1 -1
- package/dist/types.d.ts +339 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,2109 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright Adaptavist 2025 (c) All rights reserved
|
|
3
|
+
*/
|
|
4
|
+
import { PrivateBase } from './PrivateBase';
|
|
5
|
+
import { BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ServerError, UnexpectedError } from '../../utils';
|
|
6
|
+
export class PrivateAttachments extends PrivateBase {
|
|
7
|
+
constructor(apiConnection) {
|
|
8
|
+
super(apiConnection);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Get attachments for a test case using private API
|
|
12
|
+
*
|
|
13
|
+
* Retrieves all attachment details for a test case, including signed S3 URLs
|
|
14
|
+
* for downloading the attachments.
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ WARNING: This uses a private Zephyr API endpoint that is not officially supported.
|
|
17
|
+
* The endpoint may change or be removed at any time without notice.
|
|
18
|
+
*
|
|
19
|
+
* @param credentials - Private API credentials
|
|
20
|
+
* @param request - Get attachments request
|
|
21
|
+
* @param request.testCaseKey - Test case key (e.g., 'PROJ-T1'). The numeric ID will be looked up automatically if Zephyr Connector is available.
|
|
22
|
+
* @param request.projectId - Jira project ID (numeric, not the project key)
|
|
23
|
+
* @returns Test case attachments response with array of attachment details
|
|
24
|
+
* @throws {BadRequestError} If the request is invalid
|
|
25
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
26
|
+
* @throws {ForbiddenError} If the user doesn't have permission
|
|
27
|
+
* @throws {NotFoundError} If the test case is not found
|
|
28
|
+
* @throws {ServerError} If the server returns an error
|
|
29
|
+
* @throws {UnexpectedError} If test case ID cannot be looked up from key and Zephyr Connector is not available
|
|
30
|
+
*/
|
|
31
|
+
async getTestCaseAttachments(credentials, request) {
|
|
32
|
+
// Get test case ID from key if we have API connection
|
|
33
|
+
let testCaseId;
|
|
34
|
+
if (this.testCaseGroup) {
|
|
35
|
+
try {
|
|
36
|
+
const testCase = await this.testCaseGroup.getTestCase({ testCaseKey: request.testCaseKey });
|
|
37
|
+
if (!testCase) {
|
|
38
|
+
throw new NotFoundError(`Test case with key '${request.testCaseKey}' not found.`);
|
|
39
|
+
}
|
|
40
|
+
testCaseId = testCase.id;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof NotFoundError) {
|
|
44
|
+
throw new NotFoundError(`Test case with key '${request.testCaseKey}' not found.`);
|
|
45
|
+
}
|
|
46
|
+
throw new UnexpectedError(`Failed to look up test case ID from key '${request.testCaseKey}'. Ensure Zephyr Connector is configured.`, error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
throw new UnexpectedError('Cannot look up test case ID from key. This method requires Zephyr Connector to be configured. Use createZephyrApi() with an API Connection.');
|
|
51
|
+
}
|
|
52
|
+
// Get Context JWT
|
|
53
|
+
const contextJwt = await this.getContextJwt(credentials);
|
|
54
|
+
const url = `${this.privateApiBaseUrl}/testcase/${testCaseId}?fields=attachments`;
|
|
55
|
+
const headers = {
|
|
56
|
+
'accept': 'application/json',
|
|
57
|
+
'authorization': `JWT ${contextJwt}`,
|
|
58
|
+
'jira-project-id': String(request.projectId),
|
|
59
|
+
};
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(url, {
|
|
62
|
+
method: 'GET',
|
|
63
|
+
headers,
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
if (response.status === 400) {
|
|
67
|
+
throw new BadRequestError('Invalid request parameters for getting attachments.');
|
|
68
|
+
}
|
|
69
|
+
if (response.status === 401) {
|
|
70
|
+
throw new UnauthorizedError('Failed to authenticate. Please check your credentials.');
|
|
71
|
+
}
|
|
72
|
+
if (response.status === 403) {
|
|
73
|
+
throw new ForbiddenError('Insufficient permissions to get attachments.');
|
|
74
|
+
}
|
|
75
|
+
if (response.status === 404) {
|
|
76
|
+
throw new NotFoundError(`Test case with key '${request.testCaseKey}' not found.`);
|
|
77
|
+
}
|
|
78
|
+
throw new ServerError(`Failed to get test case attachments. Status: ${response.status}`, response.status, response.statusText);
|
|
79
|
+
}
|
|
80
|
+
return await response.json();
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof BadRequestError ||
|
|
84
|
+
error instanceof UnauthorizedError ||
|
|
85
|
+
error instanceof ForbiddenError ||
|
|
86
|
+
error instanceof NotFoundError ||
|
|
87
|
+
error instanceof ServerError ||
|
|
88
|
+
error instanceof UnexpectedError) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
throw new UnexpectedError('Unexpected error while getting test case attachments', error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Download an attachment from a test case using private API
|
|
96
|
+
*
|
|
97
|
+
* Downloads an attachment file into memory. This method:
|
|
98
|
+
* 1. Gets fresh attachment details (including a fresh signed S3 URL)
|
|
99
|
+
* 2. Downloads the file from the signed URL
|
|
100
|
+
* 3. Returns the file as a Blob
|
|
101
|
+
*
|
|
102
|
+
* ⚠️ WARNING: This uses private Zephyr API endpoints that are not officially supported.
|
|
103
|
+
* The endpoints may change or be removed at any time without notice.
|
|
104
|
+
*
|
|
105
|
+
* @param credentials - Private API credentials
|
|
106
|
+
* @param request - Download attachment request
|
|
107
|
+
* @param request.testCaseKey - Test case key (e.g., 'PROJ-T1'). The numeric ID will be looked up automatically if Zephyr Connector is available.
|
|
108
|
+
* @param request.projectId - Jira project ID (numeric, not the project key)
|
|
109
|
+
* @param request.attachmentId - Attachment ID (UUID string, e.g., 'c3f14125-638f-47f9-9211-12a9777c09e7')
|
|
110
|
+
* @returns Blob containing the attachment file data
|
|
111
|
+
* @throws {BadRequestError} If the request is invalid
|
|
112
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
113
|
+
* @throws {ForbiddenError} If the user doesn't have permission
|
|
114
|
+
* @throws {NotFoundError} If the test case or attachment is not found
|
|
115
|
+
* @throws {ServerError} If the server returns an error
|
|
116
|
+
* @throws {UnexpectedError} If test case ID cannot be looked up from key and Zephyr Connector is not available, or if download fails
|
|
117
|
+
*/
|
|
118
|
+
async downloadAttachment(credentials, request) {
|
|
119
|
+
// Step 1: Get fresh attachment details (including fresh signed URL)
|
|
120
|
+
const attachmentsResponse = await this.getTestCaseAttachments(credentials, {
|
|
121
|
+
testCaseKey: request.testCaseKey,
|
|
122
|
+
projectId: request.projectId,
|
|
123
|
+
});
|
|
124
|
+
// Step 2: Find the requested attachment
|
|
125
|
+
const attachment = attachmentsResponse.attachments.find((att) => att.id === request.attachmentId);
|
|
126
|
+
if (!attachment) {
|
|
127
|
+
throw new NotFoundError(`Attachment with ID '${request.attachmentId}' not found for test case '${request.testCaseKey}'.`);
|
|
128
|
+
}
|
|
129
|
+
// Step 3: Download the file from the signed S3 URL
|
|
130
|
+
try {
|
|
131
|
+
const downloadResponse = await fetch(attachment.url, {
|
|
132
|
+
method: 'GET',
|
|
133
|
+
});
|
|
134
|
+
if (!downloadResponse.ok) {
|
|
135
|
+
if (downloadResponse.status === 403) {
|
|
136
|
+
throw new ForbiddenError('Failed to download attachment. The signed URL may have expired. Please try again.');
|
|
137
|
+
}
|
|
138
|
+
if (downloadResponse.status === 404) {
|
|
139
|
+
throw new NotFoundError('Attachment file not found on S3.');
|
|
140
|
+
}
|
|
141
|
+
throw new ServerError(`Failed to download attachment. Status: ${downloadResponse.status}`, downloadResponse.status, downloadResponse.statusText);
|
|
142
|
+
}
|
|
143
|
+
// Return as Blob
|
|
144
|
+
return await downloadResponse.blob();
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (error instanceof ForbiddenError ||
|
|
148
|
+
error instanceof NotFoundError ||
|
|
149
|
+
error instanceof ServerError) {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
throw new UnexpectedError('Unexpected error while downloading attachment', error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Create an attachment for a test case using private API
|
|
157
|
+
*
|
|
158
|
+
* Uploads a file attachment to a test case. This involves:
|
|
159
|
+
* 1. Getting upload details (S3 credentials)
|
|
160
|
+
* 2. Uploading the file to S3
|
|
161
|
+
* 3. Saving attachment metadata
|
|
162
|
+
*
|
|
163
|
+
* ⚠️ WARNING: This uses private Zephyr API endpoints that are not officially supported.
|
|
164
|
+
* The endpoints may change or be removed at any time without notice.
|
|
165
|
+
*
|
|
166
|
+
* @param credentials - Private API credentials
|
|
167
|
+
* @param request - Attachment creation request
|
|
168
|
+
* @param request.testCaseKey - Test case key (e.g., 'PROJ-T1'). The numeric ID will be looked up automatically if Zephyr Connector is available.
|
|
169
|
+
* @param request.projectId - Jira project ID (numeric, not the project key)
|
|
170
|
+
* @param request.file - File to upload (Blob or ArrayBuffer)
|
|
171
|
+
* @param request.fileName - Name of the file
|
|
172
|
+
* @param request.userAccountId - Atlassian account ID (e.g., '5d6fdc98dc6e480dbc021aae')
|
|
173
|
+
* @returns Attachment metadata response
|
|
174
|
+
* @throws {BadRequestError} If the request is invalid
|
|
175
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
176
|
+
* @throws {ForbiddenError} If the user doesn't have permission
|
|
177
|
+
* @throws {NotFoundError} If the test case is not found
|
|
178
|
+
* @throws {ServerError} If the server returns an error
|
|
179
|
+
* @throws {UnexpectedError} If test case ID cannot be looked up from key and Zephyr Connector is not available
|
|
180
|
+
*/
|
|
181
|
+
async createAttachment(credentials, request) {
|
|
182
|
+
// Get test case ID from key if we have API connection
|
|
183
|
+
let testCaseId;
|
|
184
|
+
if (this.testCaseGroup) {
|
|
185
|
+
try {
|
|
186
|
+
const testCase = await this.testCaseGroup.getTestCase({ testCaseKey: request.testCaseKey });
|
|
187
|
+
if (!testCase) {
|
|
188
|
+
throw new NotFoundError(`Test case with key '${request.testCaseKey}' not found.`);
|
|
189
|
+
}
|
|
190
|
+
testCaseId = testCase.id;
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (error instanceof NotFoundError) {
|
|
194
|
+
throw new NotFoundError(`Test case with key '${request.testCaseKey}' not found.`);
|
|
195
|
+
}
|
|
196
|
+
throw new UnexpectedError(`Failed to look up test case ID from key '${request.testCaseKey}'. Ensure Zephyr Connector is configured.`, error);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
throw new UnexpectedError('Cannot look up test case ID from key. This method requires Zephyr Connector to be configured. Use createZephyrApi() with an API Connection.');
|
|
201
|
+
}
|
|
202
|
+
// Get Context JWT
|
|
203
|
+
const contextJwt = await this.getContextJwt(credentials);
|
|
204
|
+
// Step 1: Get upload details
|
|
205
|
+
const uploadDetails = await this.getUploadDetails(contextJwt);
|
|
206
|
+
// Step 2: Upload to S3
|
|
207
|
+
const s3Info = await this.uploadToS3(uploadDetails, request.projectId, testCaseId, request.userAccountId, request.file, request.fileName);
|
|
208
|
+
// Step 3: Save metadata
|
|
209
|
+
return await this.saveAttachmentMetadata(contextJwt, request.projectId, testCaseId, request.userAccountId, s3Info.s3Key, s3Info.fileName, s3Info.mimeType, s3Info.fileSize);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Get upload details for attachment upload
|
|
213
|
+
*
|
|
214
|
+
* Retrieves S3 upload credentials and configuration needed to upload an attachment.
|
|
215
|
+
*
|
|
216
|
+
* ⚠️ WARNING: This uses a private Zephyr API endpoint that is not officially supported.
|
|
217
|
+
* The endpoint may change or be removed at any time without notice.
|
|
218
|
+
*
|
|
219
|
+
* @param contextJwt - Jira Context JWT token
|
|
220
|
+
* @returns Upload details including S3 bucket URL, credentials, and policy
|
|
221
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
222
|
+
* @throws {ServerError} If the server returns an error
|
|
223
|
+
*/
|
|
224
|
+
async getUploadDetails(contextJwt) {
|
|
225
|
+
const url = 'https://app.tm4j.smartbear.com/backend/rest/tests/2.0/uploaddetails/attachment';
|
|
226
|
+
const headers = {
|
|
227
|
+
'Authorization': `JWT ${contextJwt}`,
|
|
228
|
+
'Accept': 'application/json',
|
|
229
|
+
};
|
|
230
|
+
try {
|
|
231
|
+
const response = await fetch(url, {
|
|
232
|
+
method: 'GET',
|
|
233
|
+
headers,
|
|
234
|
+
});
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
if (response.status === 401) {
|
|
237
|
+
throw new UnauthorizedError('Authentication failed. Please check your credentials.');
|
|
238
|
+
}
|
|
239
|
+
throw new ServerError(`Failed to get upload details. Status: ${response.status}`, response.status, response.statusText);
|
|
240
|
+
}
|
|
241
|
+
return await response.json();
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
if (error instanceof UnauthorizedError || error instanceof ServerError) {
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
throw new UnexpectedError('Unexpected error while getting upload details', error);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Upload file to S3
|
|
252
|
+
*
|
|
253
|
+
* Uploads a file to S3 using the credentials from upload details.
|
|
254
|
+
*
|
|
255
|
+
* @param upload - Upload details from getUploadDetails
|
|
256
|
+
* @param projectId - Jira project ID
|
|
257
|
+
* @param testCaseId - Numeric test case ID
|
|
258
|
+
* @param userAccountId - Atlassian account ID
|
|
259
|
+
* @param file - File to upload (Blob or ArrayBuffer)
|
|
260
|
+
* @param fileName - Name of the file
|
|
261
|
+
* @returns S3 upload information
|
|
262
|
+
* @throws {UnexpectedError} If upload fails
|
|
263
|
+
*/
|
|
264
|
+
async uploadToS3(upload, projectId, testCaseId, userAccountId, file, fileName) {
|
|
265
|
+
const bucketUrl = upload.bucketUrl;
|
|
266
|
+
const keyPrefix = upload.keyPrefix;
|
|
267
|
+
const credential = upload.credential;
|
|
268
|
+
const date = upload.date;
|
|
269
|
+
const policy = upload.policy;
|
|
270
|
+
const signature = upload.signature;
|
|
271
|
+
// Generate UUID for file
|
|
272
|
+
const fileUuid = this.generateUUID();
|
|
273
|
+
const s3Key = `${keyPrefix}/project/${projectId}/testcase/${testCaseId}/${fileUuid}`;
|
|
274
|
+
// Determine MIME type
|
|
275
|
+
const mimeType = this.getMimeType(fileName);
|
|
276
|
+
// Get file size
|
|
277
|
+
const fileSize = file instanceof Blob ? file.size : file.byteLength;
|
|
278
|
+
// Create form data
|
|
279
|
+
const formData = new FormData();
|
|
280
|
+
formData.append('key', s3Key);
|
|
281
|
+
formData.append('acl', 'private');
|
|
282
|
+
formData.append('Policy', policy);
|
|
283
|
+
formData.append('X-Amz-Credential', credential);
|
|
284
|
+
formData.append('X-Amz-Algorithm', 'AWS4-HMAC-SHA256');
|
|
285
|
+
formData.append('X-Amz-Date', date);
|
|
286
|
+
formData.append('X-Amz-Signature', signature);
|
|
287
|
+
formData.append('X-Amz-Meta-user-account-id', userAccountId);
|
|
288
|
+
formData.append('X-Amz-Meta-name', fileName);
|
|
289
|
+
formData.append('Content-Type', mimeType);
|
|
290
|
+
// Add file
|
|
291
|
+
if (file instanceof Blob) {
|
|
292
|
+
formData.append('file', file, fileName);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
formData.append('file', new Blob([file], { type: mimeType }), fileName);
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const response = await fetch(bucketUrl, {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
body: formData,
|
|
301
|
+
});
|
|
302
|
+
if (!response.ok && response.status !== 200 && response.status !== 201 && response.status !== 204) {
|
|
303
|
+
const errorText = await response.text().catch(() => 'Upload failed');
|
|
304
|
+
throw new UnexpectedError(`Failed to upload file to S3: ${errorText}`);
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
s3Key,
|
|
308
|
+
fileName,
|
|
309
|
+
mimeType,
|
|
310
|
+
fileSize,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if (error instanceof UnexpectedError) {
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
throw new UnexpectedError('Unexpected error while uploading file to S3', error);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Save attachment metadata
|
|
322
|
+
*
|
|
323
|
+
* Saves metadata for an uploaded attachment.
|
|
324
|
+
*
|
|
325
|
+
* @param contextJwt - Jira Context JWT token
|
|
326
|
+
* @param projectId - Jira project ID
|
|
327
|
+
* @param testCaseId - Numeric test case ID
|
|
328
|
+
* @param userAccountId - Atlassian account ID
|
|
329
|
+
* @param s3Key - S3 key from upload
|
|
330
|
+
* @param fileName - File name
|
|
331
|
+
* @param mimeType - MIME type
|
|
332
|
+
* @param fileSize - File size in bytes
|
|
333
|
+
* @returns Attachment metadata response
|
|
334
|
+
* @throws {BadRequestError} If the request is invalid
|
|
335
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
336
|
+
* @throws {ServerError} If the server returns an error
|
|
337
|
+
*/
|
|
338
|
+
async saveAttachmentMetadata(contextJwt, projectId, testCaseId, userAccountId, s3Key, fileName, mimeType, fileSize) {
|
|
339
|
+
const url = 'https://app.tm4j.smartbear.com/backend/rest/tests/2.0/attachment/metadata';
|
|
340
|
+
const headers = {
|
|
341
|
+
'Authorization': `JWT ${contextJwt}`,
|
|
342
|
+
'jira-project-id': String(projectId),
|
|
343
|
+
'Content-Type': 'application/json',
|
|
344
|
+
'Accept': 'application/json',
|
|
345
|
+
};
|
|
346
|
+
const createdOn = new Date().toISOString();
|
|
347
|
+
const payload = {
|
|
348
|
+
createdOn,
|
|
349
|
+
mimeType,
|
|
350
|
+
name: fileName,
|
|
351
|
+
s3Key,
|
|
352
|
+
size: fileSize,
|
|
353
|
+
testCaseId,
|
|
354
|
+
userAccountId,
|
|
355
|
+
};
|
|
356
|
+
try {
|
|
357
|
+
const response = await fetch(url, {
|
|
358
|
+
method: 'POST',
|
|
359
|
+
headers,
|
|
360
|
+
body: JSON.stringify(payload),
|
|
361
|
+
});
|
|
362
|
+
if (!response.ok) {
|
|
363
|
+
if (response.status === 400) {
|
|
364
|
+
const errorText = await response.text().catch(() => 'Bad Request');
|
|
365
|
+
throw new BadRequestError(`Failed to save attachment metadata: ${errorText}`, response.statusText);
|
|
366
|
+
}
|
|
367
|
+
if (response.status === 401) {
|
|
368
|
+
throw new UnauthorizedError('Authentication failed. Please check your credentials.');
|
|
369
|
+
}
|
|
370
|
+
throw new ServerError(`Failed to save attachment metadata. Status: ${response.status}`, response.status, response.statusText);
|
|
371
|
+
}
|
|
372
|
+
return await response.json();
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
if (error instanceof BadRequestError ||
|
|
376
|
+
error instanceof UnauthorizedError ||
|
|
377
|
+
error instanceof ServerError) {
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
throw new UnexpectedError('Unexpected error while saving attachment metadata', error);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Generate a UUID v4 (compatible with ScriptRunner Connect runtime)
|
|
385
|
+
*
|
|
386
|
+
* Uses crypto.getRandomValues() which is available in web standards
|
|
387
|
+
*/
|
|
388
|
+
generateUUID() {
|
|
389
|
+
// Use crypto.getRandomValues() which is available in ScriptRunner Connect
|
|
390
|
+
const bytes = new Uint8Array(16);
|
|
391
|
+
crypto.getRandomValues(bytes);
|
|
392
|
+
// Set version (4) and variant bits
|
|
393
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4
|
|
394
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant 10
|
|
395
|
+
// Convert to UUID string format
|
|
396
|
+
const hex = [];
|
|
397
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
398
|
+
hex.push(bytes[i].toString(16).padStart(2, '0'));
|
|
399
|
+
}
|
|
400
|
+
return [
|
|
401
|
+
hex.slice(0, 4).join(''),
|
|
402
|
+
hex.slice(4, 6).join(''),
|
|
403
|
+
hex.slice(6, 8).join(''),
|
|
404
|
+
hex.slice(8, 10).join(''),
|
|
405
|
+
hex.slice(10, 16).join(''),
|
|
406
|
+
].join('-');
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Get MIME type from file name
|
|
410
|
+
*/
|
|
411
|
+
getMimeType(fileName) {
|
|
412
|
+
// Simple MIME type detection based on extension
|
|
413
|
+
const extension = fileName.split('.').pop()?.toLowerCase();
|
|
414
|
+
const mimeTypes = {
|
|
415
|
+
png: 'image/png',
|
|
416
|
+
jpg: 'image/jpeg',
|
|
417
|
+
jpeg: 'image/jpeg',
|
|
418
|
+
gif: 'image/gif',
|
|
419
|
+
pdf: 'application/pdf',
|
|
420
|
+
txt: 'text/plain',
|
|
421
|
+
csv: 'text/csv',
|
|
422
|
+
json: 'application/json',
|
|
423
|
+
xml: 'application/xml',
|
|
424
|
+
zip: 'application/zip',
|
|
425
|
+
};
|
|
426
|
+
return mimeTypes[extension || ''] || 'application/octet-stream';
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Get attachments for a test cycle using private API
|
|
430
|
+
*
|
|
431
|
+
* Retrieves all attachment details for a test cycle, including signed S3 URLs
|
|
432
|
+
* for downloading the attachments.
|
|
433
|
+
*
|
|
434
|
+
* ⚠️ WARNING: This uses a private Zephyr API endpoint that is not officially supported.
|
|
435
|
+
* The endpoint may change or be removed at any time without notice.
|
|
436
|
+
*
|
|
437
|
+
* @param credentials - Private API credentials
|
|
438
|
+
* @param request - Get attachments request
|
|
439
|
+
* @param request.testCycleKey - Test cycle key (e.g., 'PROJ-R1'). The numeric ID will be looked up automatically if Zephyr Connector is available.
|
|
440
|
+
* @param request.projectId - Jira project ID (numeric, not the project key)
|
|
441
|
+
* @returns Test cycle attachments response with array of attachment details
|
|
442
|
+
* @throws {BadRequestError} If the request is invalid
|
|
443
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
444
|
+
* @throws {ForbiddenError} If the user doesn't have permission
|
|
445
|
+
* @throws {NotFoundError} If the test cycle is not found
|
|
446
|
+
* @throws {ServerError} If the server returns an error
|
|
447
|
+
* @throws {UnexpectedError} If test cycle ID cannot be looked up from key and Zephyr Connector is not available
|
|
448
|
+
*/
|
|
449
|
+
async getTestCycleAttachments(credentials, request) {
|
|
450
|
+
// Get test cycle ID from key if we have API connection
|
|
451
|
+
let testCycleId;
|
|
452
|
+
if (this.testCycleGroup) {
|
|
453
|
+
try {
|
|
454
|
+
const testCycle = await this.testCycleGroup.getTestCycle({ testCycleIdOrKey: request.testCycleKey });
|
|
455
|
+
if (!testCycle) {
|
|
456
|
+
throw new NotFoundError(`Test cycle with key '${request.testCycleKey}' not found.`);
|
|
457
|
+
}
|
|
458
|
+
testCycleId = testCycle.id;
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
if (error instanceof NotFoundError) {
|
|
462
|
+
throw new NotFoundError(`Test cycle with key '${request.testCycleKey}' not found.`);
|
|
463
|
+
}
|
|
464
|
+
throw new UnexpectedError(`Failed to look up test cycle ID from key '${request.testCycleKey}'. Ensure Zephyr Connector is configured.`, error);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
throw new UnexpectedError('Cannot look up test cycle ID from key. This method requires Zephyr Connector to be configured. Use createZephyrApi() with an API Connection.');
|
|
469
|
+
}
|
|
470
|
+
// Get Context JWT
|
|
471
|
+
const contextJwt = await this.getContextJwt(credentials);
|
|
472
|
+
const url = `${this.privateApiBaseUrl}/testrun/${testCycleId}?fields=attachments`;
|
|
473
|
+
const headers = {
|
|
474
|
+
'accept': 'application/json',
|
|
475
|
+
'authorization': `JWT ${contextJwt}`,
|
|
476
|
+
'jira-project-id': String(request.projectId),
|
|
477
|
+
};
|
|
478
|
+
try {
|
|
479
|
+
const response = await fetch(url, {
|
|
480
|
+
method: 'GET',
|
|
481
|
+
headers,
|
|
482
|
+
});
|
|
483
|
+
if (!response.ok) {
|
|
484
|
+
if (response.status === 400) {
|
|
485
|
+
throw new BadRequestError('Invalid request parameters for getting attachments.');
|
|
486
|
+
}
|
|
487
|
+
if (response.status === 401) {
|
|
488
|
+
throw new UnauthorizedError('Failed to authenticate. Please check your credentials.');
|
|
489
|
+
}
|
|
490
|
+
if (response.status === 403) {
|
|
491
|
+
throw new ForbiddenError('Insufficient permissions to get attachments.');
|
|
492
|
+
}
|
|
493
|
+
if (response.status === 404) {
|
|
494
|
+
throw new NotFoundError(`Test cycle with key '${request.testCycleKey}' not found.`);
|
|
495
|
+
}
|
|
496
|
+
throw new ServerError(`Failed to get test cycle attachments. Status: ${response.status}`, response.status, response.statusText);
|
|
497
|
+
}
|
|
498
|
+
return await response.json();
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
if (error instanceof BadRequestError ||
|
|
502
|
+
error instanceof UnauthorizedError ||
|
|
503
|
+
error instanceof ForbiddenError ||
|
|
504
|
+
error instanceof NotFoundError ||
|
|
505
|
+
error instanceof ServerError ||
|
|
506
|
+
error instanceof UnexpectedError) {
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
throw new UnexpectedError('Unexpected error while getting test cycle attachments', error);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Download an attachment from a test cycle using private API
|
|
514
|
+
*
|
|
515
|
+
* Downloads an attachment file into memory. This method:
|
|
516
|
+
* 1. Gets fresh attachment details (including a fresh signed S3 URL)
|
|
517
|
+
* 2. Downloads the file from the signed URL
|
|
518
|
+
* 3. Returns the file as a Blob
|
|
519
|
+
*
|
|
520
|
+
* ⚠️ WARNING: This uses private Zephyr API endpoints that are not officially supported.
|
|
521
|
+
* The endpoints may change or be removed at any time without notice.
|
|
522
|
+
*
|
|
523
|
+
* @param credentials - Private API credentials
|
|
524
|
+
* @param request - Download attachment request
|
|
525
|
+
* @param request.testCycleKey - Test cycle key (e.g., 'PROJ-R1'). The numeric ID will be looked up automatically if Zephyr Connector is available.
|
|
526
|
+
* @param request.projectId - Jira project ID (numeric, not the project key)
|
|
527
|
+
* @param request.attachmentId - Attachment ID (UUID string, e.g., '13fbe8bc-a87b-4db1-bf01-8b4451db79fb')
|
|
528
|
+
* @returns Blob containing the attachment file data
|
|
529
|
+
* @throws {BadRequestError} If the request is invalid
|
|
530
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
531
|
+
* @throws {ForbiddenError} If the user doesn't have permission
|
|
532
|
+
* @throws {NotFoundError} If the test cycle or attachment is not found
|
|
533
|
+
* @throws {ServerError} If the server returns an error
|
|
534
|
+
* @throws {UnexpectedError} If test cycle ID cannot be looked up from key and Zephyr Connector is not available, or if download fails
|
|
535
|
+
*/
|
|
536
|
+
async downloadTestCycleAttachment(credentials, request) {
|
|
537
|
+
// Step 1: Get fresh attachment details (including fresh signed URL)
|
|
538
|
+
const attachmentsResponse = await this.getTestCycleAttachments(credentials, {
|
|
539
|
+
testCycleKey: request.testCycleKey,
|
|
540
|
+
projectId: request.projectId,
|
|
541
|
+
});
|
|
542
|
+
// Step 2: Find the requested attachment
|
|
543
|
+
const attachment = attachmentsResponse.attachments.find((att) => att.id === request.attachmentId);
|
|
544
|
+
if (!attachment) {
|
|
545
|
+
throw new NotFoundError(`Attachment with ID '${request.attachmentId}' not found for test cycle '${request.testCycleKey}'.`);
|
|
546
|
+
}
|
|
547
|
+
// Step 3: Download the file from the signed S3 URL
|
|
548
|
+
try {
|
|
549
|
+
const downloadResponse = await fetch(attachment.url, {
|
|
550
|
+
method: 'GET',
|
|
551
|
+
});
|
|
552
|
+
if (!downloadResponse.ok) {
|
|
553
|
+
if (downloadResponse.status === 403) {
|
|
554
|
+
throw new ForbiddenError('Failed to download attachment. The signed URL may have expired. Please try again.');
|
|
555
|
+
}
|
|
556
|
+
if (downloadResponse.status === 404) {
|
|
557
|
+
throw new NotFoundError('Attachment file not found on S3.');
|
|
558
|
+
}
|
|
559
|
+
throw new ServerError(`Failed to download attachment. Status: ${downloadResponse.status}`, downloadResponse.status, downloadResponse.statusText);
|
|
560
|
+
}
|
|
561
|
+
// Return as Blob
|
|
562
|
+
return await downloadResponse.blob();
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
if (error instanceof ForbiddenError ||
|
|
566
|
+
error instanceof NotFoundError ||
|
|
567
|
+
error instanceof ServerError) {
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
throw new UnexpectedError('Unexpected error while downloading attachment', error);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Create an attachment for a test cycle using private API
|
|
575
|
+
*
|
|
576
|
+
* Uploads a file attachment to a test cycle. This involves:
|
|
577
|
+
* 1. Getting upload details (S3 credentials)
|
|
578
|
+
* 2. Uploading the file to S3
|
|
579
|
+
* 3. Saving attachment metadata
|
|
580
|
+
*
|
|
581
|
+
* ⚠️ WARNING: This uses private Zephyr API endpoints that are not officially supported.
|
|
582
|
+
* The endpoints may change or be removed at any time without notice.
|
|
583
|
+
*
|
|
584
|
+
* @param credentials - Private API credentials
|
|
585
|
+
* @param request - Attachment creation request
|
|
586
|
+
* @param request.testCycleKey - Test cycle key (e.g., 'PROJ-R1'). The numeric ID will be looked up automatically if Zephyr Connector is available.
|
|
587
|
+
* @param request.projectId - Jira project ID (numeric, not the project key)
|
|
588
|
+
* @param request.file - File to upload (Blob or ArrayBuffer)
|
|
589
|
+
* @param request.fileName - Name of the file
|
|
590
|
+
* @param request.userAccountId - Atlassian account ID (e.g., '5d6fdc98dc6e480dbc021aae')
|
|
591
|
+
* @returns Attachment metadata response
|
|
592
|
+
* @throws {BadRequestError} If the request is invalid
|
|
593
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
594
|
+
* @throws {ForbiddenError} If the user doesn't have permission
|
|
595
|
+
* @throws {NotFoundError} If the test cycle is not found
|
|
596
|
+
* @throws {ServerError} If the server returns an error
|
|
597
|
+
* @throws {UnexpectedError} If test cycle ID cannot be looked up from key and Zephyr Connector is not available
|
|
598
|
+
*/
|
|
599
|
+
async createTestCycleAttachment(credentials, request) {
|
|
600
|
+
// Get test cycle ID from key if we have API connection
|
|
601
|
+
let testCycleId;
|
|
602
|
+
if (this.testCycleGroup) {
|
|
603
|
+
try {
|
|
604
|
+
const testCycle = await this.testCycleGroup.getTestCycle({ testCycleIdOrKey: request.testCycleKey });
|
|
605
|
+
if (!testCycle) {
|
|
606
|
+
throw new NotFoundError(`Test cycle with key '${request.testCycleKey}' not found.`);
|
|
607
|
+
}
|
|
608
|
+
testCycleId = testCycle.id;
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
if (error instanceof NotFoundError) {
|
|
612
|
+
throw new NotFoundError(`Test cycle with key '${request.testCycleKey}' not found.`);
|
|
613
|
+
}
|
|
614
|
+
throw new UnexpectedError(`Failed to look up test cycle ID from key '${request.testCycleKey}'. Ensure Zephyr Connector is configured.`, error);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
throw new UnexpectedError('Cannot look up test cycle ID from key. This method requires Zephyr Connector to be configured. Use createZephyrApi() with an API Connection.');
|
|
619
|
+
}
|
|
620
|
+
// Get Context JWT
|
|
621
|
+
const contextJwt = await this.getContextJwt(credentials);
|
|
622
|
+
// Step 1: Get upload details
|
|
623
|
+
const uploadDetails = await this.getUploadDetails(contextJwt);
|
|
624
|
+
// Step 2: Upload to S3
|
|
625
|
+
const s3Info = await this.uploadToS3ForTestCycle(uploadDetails, request.projectId, testCycleId, request.userAccountId, request.file, request.fileName);
|
|
626
|
+
// Step 3: Save metadata
|
|
627
|
+
return await this.saveTestCycleAttachmentMetadata(contextJwt, request.projectId, testCycleId, request.userAccountId, s3Info.s3Key, s3Info.fileName, s3Info.mimeType, s3Info.fileSize);
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Get attachments for a test plan using private API
|
|
631
|
+
*
|
|
632
|
+
* Retrieves all attachment details for a test plan, including signed S3 URLs
|
|
633
|
+
* for downloading the attachments.
|
|
634
|
+
*
|
|
635
|
+
* ⚠️ WARNING: This uses a private Zephyr API endpoint that is not officially supported.
|
|
636
|
+
* The endpoint may change or be removed at any time without notice.
|
|
637
|
+
*
|
|
638
|
+
* @param credentials - Private API credentials
|
|
639
|
+
* @param request - Get attachments request
|
|
640
|
+
* @param request.testPlanKey - Test plan key (e.g., 'PROJ-P1'). The numeric ID will be looked up automatically if Zephyr Connector is available.
|
|
641
|
+
* @param request.projectId - Jira project ID (numeric, not the project key)
|
|
642
|
+
* @returns Test plan attachments response with array of attachment details
|
|
643
|
+
* @throws {BadRequestError} If the request is invalid
|
|
644
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
645
|
+
* @throws {ForbiddenError} If the user doesn't have permission
|
|
646
|
+
* @throws {NotFoundError} If the test plan is not found
|
|
647
|
+
* @throws {ServerError} If the server returns an error
|
|
648
|
+
* @throws {UnexpectedError} If test plan ID cannot be looked up from key and Zephyr Connector is not available
|
|
649
|
+
*/
|
|
650
|
+
async getTestPlanAttachments(credentials, request) {
|
|
651
|
+
// Get test plan ID from key if we have API connection
|
|
652
|
+
let testPlanId;
|
|
653
|
+
if (this.testPlanGroup) {
|
|
654
|
+
try {
|
|
655
|
+
const testPlan = await this.testPlanGroup.getTestPlan({ testPlanIdOrKey: request.testPlanKey });
|
|
656
|
+
if (!testPlan) {
|
|
657
|
+
throw new NotFoundError(`Test plan with key '${request.testPlanKey}' not found.`);
|
|
658
|
+
}
|
|
659
|
+
testPlanId = testPlan.id;
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
if (error instanceof NotFoundError) {
|
|
663
|
+
throw new NotFoundError(`Test plan with key '${request.testPlanKey}' not found.`);
|
|
664
|
+
}
|
|
665
|
+
throw new UnexpectedError(`Failed to look up test plan ID from key '${request.testPlanKey}'. Ensure Zephyr Connector is configured.`, error);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
throw new UnexpectedError('Cannot look up test plan ID from key. This method requires Zephyr Connector to be configured. Use createZephyrApi() with an API Connection.');
|
|
670
|
+
}
|
|
671
|
+
// Get Context JWT
|
|
672
|
+
const contextJwt = await this.getContextJwt(credentials);
|
|
673
|
+
const url = `${this.privateApiBaseUrl}/testplan/${testPlanId}?fields=attachments`;
|
|
674
|
+
const headers = {
|
|
675
|
+
'accept': 'application/json',
|
|
676
|
+
'authorization': `JWT ${contextJwt}`,
|
|
677
|
+
'jira-project-id': String(request.projectId),
|
|
678
|
+
};
|
|
679
|
+
try {
|
|
680
|
+
const response = await fetch(url, {
|
|
681
|
+
method: 'GET',
|
|
682
|
+
headers,
|
|
683
|
+
});
|
|
684
|
+
if (!response.ok) {
|
|
685
|
+
if (response.status === 400) {
|
|
686
|
+
throw new BadRequestError('Invalid request parameters for getting attachments.');
|
|
687
|
+
}
|
|
688
|
+
if (response.status === 401) {
|
|
689
|
+
throw new UnauthorizedError('Failed to authenticate. Please check your credentials.');
|
|
690
|
+
}
|
|
691
|
+
if (response.status === 403) {
|
|
692
|
+
throw new ForbiddenError('Insufficient permissions to get attachments.');
|
|
693
|
+
}
|
|
694
|
+
if (response.status === 404) {
|
|
695
|
+
throw new NotFoundError(`Test plan with key '${request.testPlanKey}' not found.`);
|
|
696
|
+
}
|
|
697
|
+
throw new ServerError(`Failed to get test plan attachments. Status: ${response.status}`, response.status, response.statusText);
|
|
698
|
+
}
|
|
699
|
+
return await response.json();
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
if (error instanceof BadRequestError ||
|
|
703
|
+
error instanceof UnauthorizedError ||
|
|
704
|
+
error instanceof ForbiddenError ||
|
|
705
|
+
error instanceof NotFoundError ||
|
|
706
|
+
error instanceof ServerError ||
|
|
707
|
+
error instanceof UnexpectedError) {
|
|
708
|
+
throw error;
|
|
709
|
+
}
|
|
710
|
+
throw new UnexpectedError('Unexpected error while getting test plan attachments', error);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Download an attachment from a test plan using private API
|
|
715
|
+
*
|
|
716
|
+
* Downloads an attachment file into memory. This method:
|
|
717
|
+
* 1. Gets fresh attachment details (including a fresh signed S3 URL)
|
|
718
|
+
* 2. Downloads the file from the signed URL
|
|
719
|
+
* 3. Returns the file as a Blob
|
|
720
|
+
*
|
|
721
|
+
* ⚠️ WARNING: This uses private Zephyr API endpoints that are not officially supported.
|
|
722
|
+
* The endpoints may change or be removed at any time without notice.
|
|
723
|
+
*
|
|
724
|
+
* @param credentials - Private API credentials
|
|
725
|
+
* @param request - Download attachment request
|
|
726
|
+
* @param request.testPlanKey - Test plan key (e.g., 'PROJ-P1'). The numeric ID will be looked up automatically if Zephyr Connector is available.
|
|
727
|
+
* @param request.projectId - Jira project ID (numeric, not the project key)
|
|
728
|
+
* @param request.attachmentId - Attachment ID (UUID string)
|
|
729
|
+
* @returns Blob containing the attachment file data
|
|
730
|
+
* @throws {BadRequestError} If the request is invalid
|
|
731
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
732
|
+
* @throws {ForbiddenError} If the user doesn't have permission
|
|
733
|
+
* @throws {NotFoundError} If the test plan or attachment is not found
|
|
734
|
+
* @throws {ServerError} If the server returns an error
|
|
735
|
+
* @throws {UnexpectedError} If test plan ID cannot be looked up from key and Zephyr Connector is not available, or if download fails
|
|
736
|
+
*/
|
|
737
|
+
async downloadTestPlanAttachment(credentials, request) {
|
|
738
|
+
// Step 1: Get fresh attachment details (including fresh signed URL)
|
|
739
|
+
const attachmentsResponse = await this.getTestPlanAttachments(credentials, {
|
|
740
|
+
testPlanKey: request.testPlanKey,
|
|
741
|
+
projectId: request.projectId,
|
|
742
|
+
});
|
|
743
|
+
// Step 2: Find the requested attachment
|
|
744
|
+
const attachment = attachmentsResponse.attachments.find((att) => att.id === request.attachmentId);
|
|
745
|
+
if (!attachment) {
|
|
746
|
+
throw new NotFoundError(`Attachment with ID '${request.attachmentId}' not found for test plan '${request.testPlanKey}'.`);
|
|
747
|
+
}
|
|
748
|
+
// Step 3: Download the file from the signed S3 URL
|
|
749
|
+
try {
|
|
750
|
+
const downloadResponse = await fetch(attachment.url, {
|
|
751
|
+
method: 'GET',
|
|
752
|
+
});
|
|
753
|
+
if (!downloadResponse.ok) {
|
|
754
|
+
if (downloadResponse.status === 403) {
|
|
755
|
+
throw new ForbiddenError('Failed to download attachment. The signed URL may have expired. Please try again.');
|
|
756
|
+
}
|
|
757
|
+
if (downloadResponse.status === 404) {
|
|
758
|
+
throw new NotFoundError('Attachment file not found on S3.');
|
|
759
|
+
}
|
|
760
|
+
throw new ServerError(`Failed to download attachment. Status: ${downloadResponse.status}`, downloadResponse.status, downloadResponse.statusText);
|
|
761
|
+
}
|
|
762
|
+
// Return as Blob
|
|
763
|
+
return await downloadResponse.blob();
|
|
764
|
+
}
|
|
765
|
+
catch (error) {
|
|
766
|
+
if (error instanceof ForbiddenError ||
|
|
767
|
+
error instanceof NotFoundError ||
|
|
768
|
+
error instanceof ServerError) {
|
|
769
|
+
throw error;
|
|
770
|
+
}
|
|
771
|
+
throw new UnexpectedError('Unexpected error while downloading attachment', error);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Create an attachment for a test plan using private API
|
|
776
|
+
*
|
|
777
|
+
* Uploads a file attachment to a test plan. This involves:
|
|
778
|
+
* 1. Getting upload details (S3 credentials)
|
|
779
|
+
* 2. Uploading the file to S3
|
|
780
|
+
* 3. Saving attachment metadata
|
|
781
|
+
*
|
|
782
|
+
* ⚠️ WARNING: This uses private Zephyr API endpoints that are not officially supported.
|
|
783
|
+
* The endpoints may change or be removed at any time without notice.
|
|
784
|
+
*
|
|
785
|
+
* @param credentials - Private API credentials
|
|
786
|
+
* @param request - Attachment creation request
|
|
787
|
+
* @param request.testPlanKey - Test plan key (e.g., 'PROJ-P1'). The numeric ID will be looked up automatically if Zephyr Connector is available.
|
|
788
|
+
* @param request.projectId - Jira project ID (numeric, not the project key)
|
|
789
|
+
* @param request.file - File to upload (Blob or ArrayBuffer)
|
|
790
|
+
* @param request.fileName - Name of the file
|
|
791
|
+
* @param request.userAccountId - Atlassian account ID (e.g., '5d6fdc98dc6e480dbc021aae')
|
|
792
|
+
* @returns Attachment metadata response
|
|
793
|
+
* @throws {BadRequestError} If the request is invalid
|
|
794
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
795
|
+
* @throws {ForbiddenError} If the user doesn't have permission
|
|
796
|
+
* @throws {NotFoundError} If the test plan is not found
|
|
797
|
+
* @throws {ServerError} If the server returns an error
|
|
798
|
+
* @throws {UnexpectedError} If test plan ID cannot be looked up from key and Zephyr Connector is not available
|
|
799
|
+
*/
|
|
800
|
+
async createTestPlanAttachment(credentials, request) {
|
|
801
|
+
// Get test plan ID from key if we have API connection
|
|
802
|
+
let testPlanId;
|
|
803
|
+
if (this.testPlanGroup) {
|
|
804
|
+
try {
|
|
805
|
+
const testPlan = await this.testPlanGroup.getTestPlan({ testPlanIdOrKey: request.testPlanKey });
|
|
806
|
+
if (!testPlan) {
|
|
807
|
+
throw new NotFoundError(`Test plan with key '${request.testPlanKey}' not found.`);
|
|
808
|
+
}
|
|
809
|
+
testPlanId = testPlan.id;
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
if (error instanceof NotFoundError) {
|
|
813
|
+
throw new NotFoundError(`Test plan with key '${request.testPlanKey}' not found.`);
|
|
814
|
+
}
|
|
815
|
+
throw new UnexpectedError(`Failed to look up test plan ID from key '${request.testPlanKey}'. Ensure Zephyr Connector is configured.`, error);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
throw new UnexpectedError('Cannot look up test plan ID from key. This method requires Zephyr Connector to be configured. Use createZephyrApi() with an API Connection.');
|
|
820
|
+
}
|
|
821
|
+
// Get Context JWT
|
|
822
|
+
const contextJwt = await this.getContextJwt(credentials);
|
|
823
|
+
// Step 1: Get upload details
|
|
824
|
+
const uploadDetails = await this.getUploadDetails(contextJwt);
|
|
825
|
+
// Step 2: Upload to S3
|
|
826
|
+
const s3Info = await this.uploadToS3ForTestPlan(uploadDetails, request.projectId, testPlanId, request.userAccountId, request.file, request.fileName);
|
|
827
|
+
// Step 3: Save metadata
|
|
828
|
+
return await this.saveTestPlanAttachmentMetadata(contextJwt, request.projectId, testPlanId, request.userAccountId, s3Info.s3Key, s3Info.fileName, s3Info.mimeType, s3Info.fileSize);
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Upload file to S3 for test cycle
|
|
832
|
+
*
|
|
833
|
+
* Uploads a file to S3 using the credentials from upload details.
|
|
834
|
+
* Uses "testrun" in the S3 key path.
|
|
835
|
+
*
|
|
836
|
+
* @param upload - Upload details from getUploadDetails
|
|
837
|
+
* @param projectId - Jira project ID
|
|
838
|
+
* @param testCycleId - Numeric test cycle ID
|
|
839
|
+
* @param userAccountId - Atlassian account ID
|
|
840
|
+
* @param file - File to upload (Blob or ArrayBuffer)
|
|
841
|
+
* @param fileName - Name of the file
|
|
842
|
+
* @returns S3 upload information
|
|
843
|
+
* @throws {UnexpectedError} If upload fails
|
|
844
|
+
*/
|
|
845
|
+
async uploadToS3ForTestCycle(upload, projectId, testCycleId, userAccountId, file, fileName) {
|
|
846
|
+
const bucketUrl = upload.bucketUrl;
|
|
847
|
+
const keyPrefix = upload.keyPrefix;
|
|
848
|
+
const credential = upload.credential;
|
|
849
|
+
const date = upload.date;
|
|
850
|
+
const policy = upload.policy;
|
|
851
|
+
const signature = upload.signature;
|
|
852
|
+
// Generate UUID for file
|
|
853
|
+
const fileUuid = this.generateUUID();
|
|
854
|
+
// Note: Test cycles use "testrun" in the S3 key path, and the keyPrefix includes "write/"
|
|
855
|
+
const s3Key = `${keyPrefix}/project/${projectId}/testrun/${testCycleId}/${fileUuid}`;
|
|
856
|
+
// Determine MIME type
|
|
857
|
+
const mimeType = this.getMimeType(fileName);
|
|
858
|
+
// Get file size
|
|
859
|
+
const fileSize = file instanceof Blob ? file.size : file.byteLength;
|
|
860
|
+
// Create form data
|
|
861
|
+
const formData = new FormData();
|
|
862
|
+
formData.append('key', s3Key);
|
|
863
|
+
formData.append('acl', 'private');
|
|
864
|
+
formData.append('Policy', policy);
|
|
865
|
+
formData.append('X-Amz-Credential', credential);
|
|
866
|
+
formData.append('X-Amz-Algorithm', 'AWS4-HMAC-SHA256');
|
|
867
|
+
formData.append('X-Amz-Date', date);
|
|
868
|
+
formData.append('X-Amz-Signature', signature);
|
|
869
|
+
formData.append('X-Amz-Meta-user-account-id', userAccountId);
|
|
870
|
+
formData.append('X-Amz-Meta-name', fileName);
|
|
871
|
+
formData.append('Content-Type', mimeType);
|
|
872
|
+
// Add file
|
|
873
|
+
if (file instanceof Blob) {
|
|
874
|
+
formData.append('file', file, fileName);
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
formData.append('file', new Blob([file], { type: mimeType }), fileName);
|
|
878
|
+
}
|
|
879
|
+
try {
|
|
880
|
+
const response = await fetch(bucketUrl, {
|
|
881
|
+
method: 'POST',
|
|
882
|
+
body: formData,
|
|
883
|
+
});
|
|
884
|
+
if (!response.ok && response.status !== 200 && response.status !== 201 && response.status !== 204) {
|
|
885
|
+
const errorText = await response.text().catch(() => 'Upload failed');
|
|
886
|
+
throw new UnexpectedError(`Failed to upload file to S3: ${errorText}`);
|
|
887
|
+
}
|
|
888
|
+
return {
|
|
889
|
+
s3Key,
|
|
890
|
+
fileName,
|
|
891
|
+
mimeType,
|
|
892
|
+
fileSize,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
catch (error) {
|
|
896
|
+
if (error instanceof UnexpectedError) {
|
|
897
|
+
throw error;
|
|
898
|
+
}
|
|
899
|
+
throw new UnexpectedError('Unexpected error while uploading file to S3', error);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Save test cycle attachment metadata
|
|
904
|
+
*
|
|
905
|
+
* Saves metadata for an uploaded test cycle attachment.
|
|
906
|
+
*
|
|
907
|
+
* @param contextJwt - Jira Context JWT token
|
|
908
|
+
* @param projectId - Jira project ID
|
|
909
|
+
* @param testCycleId - Numeric test cycle ID
|
|
910
|
+
* @param userAccountId - Atlassian account ID
|
|
911
|
+
* @param s3Key - S3 key from upload
|
|
912
|
+
* @param fileName - File name
|
|
913
|
+
* @param mimeType - MIME type
|
|
914
|
+
* @param fileSize - File size in bytes
|
|
915
|
+
* @returns Attachment metadata response
|
|
916
|
+
* @throws {BadRequestError} If the request is invalid
|
|
917
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
918
|
+
* @throws {ServerError} If the server returns an error
|
|
919
|
+
*/
|
|
920
|
+
async saveTestCycleAttachmentMetadata(contextJwt, projectId, testCycleId, userAccountId, s3Key, fileName, mimeType, fileSize) {
|
|
921
|
+
const url = 'https://app.tm4j.smartbear.com/backend/rest/tests/2.0/attachment/metadata';
|
|
922
|
+
const headers = {
|
|
923
|
+
'Authorization': `JWT ${contextJwt}`,
|
|
924
|
+
'jira-project-id': String(projectId),
|
|
925
|
+
'Content-Type': 'application/json',
|
|
926
|
+
'Accept': 'application/json',
|
|
927
|
+
};
|
|
928
|
+
const createdOn = new Date().toISOString();
|
|
929
|
+
const payload = {
|
|
930
|
+
createdOn,
|
|
931
|
+
mimeType,
|
|
932
|
+
name: fileName,
|
|
933
|
+
s3Key,
|
|
934
|
+
size: fileSize,
|
|
935
|
+
testCycleId,
|
|
936
|
+
userAccountId,
|
|
937
|
+
};
|
|
938
|
+
try {
|
|
939
|
+
const response = await fetch(url, {
|
|
940
|
+
method: 'POST',
|
|
941
|
+
headers,
|
|
942
|
+
body: JSON.stringify(payload),
|
|
943
|
+
});
|
|
944
|
+
if (!response.ok) {
|
|
945
|
+
if (response.status === 400) {
|
|
946
|
+
const errorText = await response.text().catch(() => 'Bad Request');
|
|
947
|
+
throw new BadRequestError(`Failed to save attachment metadata: ${errorText}`, response.statusText);
|
|
948
|
+
}
|
|
949
|
+
if (response.status === 401) {
|
|
950
|
+
throw new UnauthorizedError('Authentication failed. Please check your credentials.');
|
|
951
|
+
}
|
|
952
|
+
throw new ServerError(`Failed to save attachment metadata. Status: ${response.status}`, response.status, response.statusText);
|
|
953
|
+
}
|
|
954
|
+
return await response.json();
|
|
955
|
+
}
|
|
956
|
+
catch (error) {
|
|
957
|
+
if (error instanceof BadRequestError ||
|
|
958
|
+
error instanceof UnauthorizedError ||
|
|
959
|
+
error instanceof ServerError) {
|
|
960
|
+
throw error;
|
|
961
|
+
}
|
|
962
|
+
throw new UnexpectedError('Unexpected error while saving attachment metadata', error);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Upload file to S3 for test plan
|
|
967
|
+
*
|
|
968
|
+
* Uploads a file to S3 using the credentials from upload details.
|
|
969
|
+
* Uses "testplan" in the S3 key path.
|
|
970
|
+
*
|
|
971
|
+
* @param upload - Upload details from getUploadDetails
|
|
972
|
+
* @param projectId - Jira project ID
|
|
973
|
+
* @param testPlanId - Numeric test plan ID
|
|
974
|
+
* @param userAccountId - Atlassian account ID
|
|
975
|
+
* @param file - File to upload (Blob or ArrayBuffer)
|
|
976
|
+
* @param fileName - Name of the file
|
|
977
|
+
* @returns S3 upload information
|
|
978
|
+
* @throws {UnexpectedError} If upload fails
|
|
979
|
+
*/
|
|
980
|
+
async uploadToS3ForTestPlan(upload, projectId, testPlanId, userAccountId, file, fileName) {
|
|
981
|
+
const bucketUrl = upload.bucketUrl;
|
|
982
|
+
const keyPrefix = upload.keyPrefix;
|
|
983
|
+
const credential = upload.credential;
|
|
984
|
+
const date = upload.date;
|
|
985
|
+
const policy = upload.policy;
|
|
986
|
+
const signature = upload.signature;
|
|
987
|
+
// Generate UUID for file
|
|
988
|
+
const fileUuid = this.generateUUID();
|
|
989
|
+
// Note: Test plans use "testplan" in the S3 key path, and the keyPrefix includes "write/"
|
|
990
|
+
const s3Key = `${keyPrefix}/project/${projectId}/testplan/${testPlanId}/${fileUuid}`;
|
|
991
|
+
// Determine MIME type
|
|
992
|
+
const mimeType = this.getMimeType(fileName);
|
|
993
|
+
// Get file size
|
|
994
|
+
const fileSize = file instanceof Blob ? file.size : file.byteLength;
|
|
995
|
+
// Create form data
|
|
996
|
+
const formData = new FormData();
|
|
997
|
+
formData.append('key', s3Key);
|
|
998
|
+
formData.append('acl', 'private');
|
|
999
|
+
formData.append('Policy', policy);
|
|
1000
|
+
formData.append('X-Amz-Credential', credential);
|
|
1001
|
+
formData.append('X-Amz-Algorithm', 'AWS4-HMAC-SHA256');
|
|
1002
|
+
formData.append('X-Amz-Date', date);
|
|
1003
|
+
formData.append('X-Amz-Signature', signature);
|
|
1004
|
+
formData.append('X-Amz-Meta-user-account-id', userAccountId);
|
|
1005
|
+
formData.append('X-Amz-Meta-name', fileName);
|
|
1006
|
+
formData.append('Content-Type', mimeType);
|
|
1007
|
+
// Add file
|
|
1008
|
+
if (file instanceof Blob) {
|
|
1009
|
+
formData.append('file', file, fileName);
|
|
1010
|
+
}
|
|
1011
|
+
else {
|
|
1012
|
+
formData.append('file', new Blob([file], { type: mimeType }), fileName);
|
|
1013
|
+
}
|
|
1014
|
+
try {
|
|
1015
|
+
const response = await fetch(bucketUrl, {
|
|
1016
|
+
method: 'POST',
|
|
1017
|
+
body: formData,
|
|
1018
|
+
});
|
|
1019
|
+
if (!response.ok && response.status !== 200 && response.status !== 201 && response.status !== 204) {
|
|
1020
|
+
const errorText = await response.text().catch(() => 'Upload failed');
|
|
1021
|
+
throw new UnexpectedError(`Failed to upload file to S3: ${errorText}`);
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
s3Key,
|
|
1025
|
+
fileName,
|
|
1026
|
+
mimeType,
|
|
1027
|
+
fileSize,
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
catch (error) {
|
|
1031
|
+
if (error instanceof UnexpectedError) {
|
|
1032
|
+
throw error;
|
|
1033
|
+
}
|
|
1034
|
+
throw new UnexpectedError('Unexpected error while uploading file to S3', error);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Save test plan attachment metadata
|
|
1039
|
+
*
|
|
1040
|
+
* Saves metadata for an uploaded test plan attachment.
|
|
1041
|
+
*
|
|
1042
|
+
* @param contextJwt - Jira Context JWT token
|
|
1043
|
+
* @param projectId - Jira project ID
|
|
1044
|
+
* @param testPlanId - Numeric test plan ID
|
|
1045
|
+
* @param userAccountId - Atlassian account ID
|
|
1046
|
+
* @param s3Key - S3 key from upload
|
|
1047
|
+
* @param fileName - File name
|
|
1048
|
+
* @param mimeType - MIME type
|
|
1049
|
+
* @param fileSize - File size in bytes
|
|
1050
|
+
* @returns Attachment metadata response
|
|
1051
|
+
* @throws {BadRequestError} If the request is invalid
|
|
1052
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
1053
|
+
* @throws {ServerError} If the server returns an error
|
|
1054
|
+
*/
|
|
1055
|
+
async saveTestPlanAttachmentMetadata(contextJwt, projectId, testPlanId, userAccountId, s3Key, fileName, mimeType, fileSize) {
|
|
1056
|
+
const url = 'https://app.tm4j.smartbear.com/backend/rest/tests/2.0/attachment/metadata';
|
|
1057
|
+
const headers = {
|
|
1058
|
+
'Authorization': `JWT ${contextJwt}`,
|
|
1059
|
+
'jira-project-id': String(projectId),
|
|
1060
|
+
'Content-Type': 'application/json',
|
|
1061
|
+
'Accept': 'application/json',
|
|
1062
|
+
};
|
|
1063
|
+
const createdOn = new Date().toISOString();
|
|
1064
|
+
const payload = {
|
|
1065
|
+
createdOn,
|
|
1066
|
+
mimeType,
|
|
1067
|
+
name: fileName,
|
|
1068
|
+
s3Key,
|
|
1069
|
+
size: fileSize,
|
|
1070
|
+
testPlanId,
|
|
1071
|
+
userAccountId,
|
|
1072
|
+
};
|
|
1073
|
+
try {
|
|
1074
|
+
const response = await fetch(url, {
|
|
1075
|
+
method: 'POST',
|
|
1076
|
+
headers,
|
|
1077
|
+
body: JSON.stringify(payload),
|
|
1078
|
+
});
|
|
1079
|
+
if (!response.ok) {
|
|
1080
|
+
if (response.status === 400) {
|
|
1081
|
+
const errorText = await response.text().catch(() => 'Bad Request');
|
|
1082
|
+
throw new BadRequestError(`Failed to save attachment metadata: ${errorText}`, response.statusText);
|
|
1083
|
+
}
|
|
1084
|
+
if (response.status === 401) {
|
|
1085
|
+
throw new UnauthorizedError('Authentication failed. Please check your credentials.');
|
|
1086
|
+
}
|
|
1087
|
+
throw new ServerError(`Failed to save attachment metadata. Status: ${response.status}`, response.status, response.statusText);
|
|
1088
|
+
}
|
|
1089
|
+
return await response.json();
|
|
1090
|
+
}
|
|
1091
|
+
catch (error) {
|
|
1092
|
+
if (error instanceof BadRequestError ||
|
|
1093
|
+
error instanceof UnauthorizedError ||
|
|
1094
|
+
error instanceof ServerError) {
|
|
1095
|
+
throw error;
|
|
1096
|
+
}
|
|
1097
|
+
throw new UnexpectedError('Unexpected error while saving attachment metadata', error);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Get attachments for a test step using private API
|
|
1102
|
+
*
|
|
1103
|
+
* Retrieves all attachment details for a specific test step within a test case.
|
|
1104
|
+
* Attachments are retrieved by getting the test case with fields including testScript.steps.attachments.
|
|
1105
|
+
*
|
|
1106
|
+
* ⚠️ WARNING: This uses a private Zephyr API endpoint that is not officially supported.
|
|
1107
|
+
* The endpoint may change or be removed at any time without notice.
|
|
1108
|
+
*
|
|
1109
|
+
* @param credentials - Private API credentials
|
|
1110
|
+
* @param request - Get attachments request
|
|
1111
|
+
* @param request.testCaseKey - Test case key (e.g., 'PROJ-T1')
|
|
1112
|
+
* @param request.stepId - Numeric test step ID
|
|
1113
|
+
* @param request.projectId - Jira project ID (numeric, not the project key)
|
|
1114
|
+
* @returns Test step attachments response with array of attachment details
|
|
1115
|
+
* @throws {BadRequestError} If the request is invalid
|
|
1116
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
1117
|
+
* @throws {ForbiddenError} If the user doesn't have permission
|
|
1118
|
+
* @throws {NotFoundError} If the test case or step is not found
|
|
1119
|
+
* @throws {ServerError} If the server returns an error
|
|
1120
|
+
* @throws {UnexpectedError} If the test case cannot be retrieved
|
|
1121
|
+
*/
|
|
1122
|
+
async getTestStepAttachments(credentials, request) {
|
|
1123
|
+
// Get Context JWT
|
|
1124
|
+
const contextJwt = await this.getContextJwt(credentials);
|
|
1125
|
+
// Get test case ID from key if we have API connection
|
|
1126
|
+
let testCaseId;
|
|
1127
|
+
if (this.testCaseGroup) {
|
|
1128
|
+
try {
|
|
1129
|
+
const testCase = await this.testCaseGroup.getTestCase({ testCaseKey: request.testCaseKey });
|
|
1130
|
+
if (!testCase) {
|
|
1131
|
+
throw new NotFoundError(`Test case with key '${request.testCaseKey}' not found.`);
|
|
1132
|
+
}
|
|
1133
|
+
testCaseId = testCase.id;
|
|
1134
|
+
}
|
|
1135
|
+
catch (error) {
|
|
1136
|
+
if (error instanceof NotFoundError) {
|
|
1137
|
+
throw new NotFoundError(`Test case with key '${request.testCaseKey}' not found.`);
|
|
1138
|
+
}
|
|
1139
|
+
throw new UnexpectedError(`Failed to look up test case ID from key '${request.testCaseKey}'. Ensure Zephyr Connector is configured.`, error);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
throw new UnexpectedError('Cannot look up test case ID from key. This method requires Zephyr Connector to be configured. Use createZephyrApi() with an API Connection.');
|
|
1144
|
+
}
|
|
1145
|
+
// Get test case with testScript.steps.attachments fields
|
|
1146
|
+
const fields = 'testScript(id,text,steps(index,reflectRef,description,text,expectedResult,testData,customFieldValues,attachments,id,stepParameters(id,testCaseParameterId,value),testCase(id,key,name,archived,majorVersion,latestVersion,projectKey,parameters(id,name,defaultValue,index))))';
|
|
1147
|
+
const url = `${this.privateApiBaseUrl}/testcase/${request.testCaseKey}?fields=${encodeURIComponent(fields)}`;
|
|
1148
|
+
const headers = {
|
|
1149
|
+
'accept': 'application/json',
|
|
1150
|
+
'authorization': `JWT ${contextJwt}`,
|
|
1151
|
+
'jira-project-id': String(request.projectId),
|
|
1152
|
+
};
|
|
1153
|
+
try {
|
|
1154
|
+
const response = await fetch(url, {
|
|
1155
|
+
method: 'GET',
|
|
1156
|
+
headers,
|
|
1157
|
+
});
|
|
1158
|
+
if (!response.ok) {
|
|
1159
|
+
if (response.status === 400) {
|
|
1160
|
+
throw new BadRequestError('Invalid request parameters for getting attachments.');
|
|
1161
|
+
}
|
|
1162
|
+
if (response.status === 401) {
|
|
1163
|
+
throw new UnauthorizedError('Failed to authenticate. Please check your credentials.');
|
|
1164
|
+
}
|
|
1165
|
+
if (response.status === 403) {
|
|
1166
|
+
throw new ForbiddenError('Insufficient permissions to get attachments.');
|
|
1167
|
+
}
|
|
1168
|
+
if (response.status === 404) {
|
|
1169
|
+
throw new NotFoundError(`Test case with key '${request.testCaseKey}' not found.`);
|
|
1170
|
+
}
|
|
1171
|
+
throw new ServerError(`Failed to get test step attachments. Status: ${response.status}`, response.status, response.statusText);
|
|
1172
|
+
}
|
|
1173
|
+
const testCaseData = await response.json();
|
|
1174
|
+
// Find the specific step
|
|
1175
|
+
const step = testCaseData.testScript?.stepByStepScript?.steps?.find((s) => s.id === request.stepId);
|
|
1176
|
+
if (!step) {
|
|
1177
|
+
throw new NotFoundError(`Test step with ID ${request.stepId} not found in test case '${request.testCaseKey}'.`);
|
|
1178
|
+
}
|
|
1179
|
+
return {
|
|
1180
|
+
attachments: (step.attachments || []).map((att) => ({
|
|
1181
|
+
id: att.id,
|
|
1182
|
+
dbAttachmentId: att.dbAttachmentId,
|
|
1183
|
+
url: att.url,
|
|
1184
|
+
userKey: att.userKey,
|
|
1185
|
+
createdOn: att.createdOn,
|
|
1186
|
+
name: att.name,
|
|
1187
|
+
mimeType: att.mimeType,
|
|
1188
|
+
fileSize: att.fileSize,
|
|
1189
|
+
copyFromStep: att.copyFromStep,
|
|
1190
|
+
s3Key: att.s3Key,
|
|
1191
|
+
stepId: att.stepId,
|
|
1192
|
+
})),
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
catch (error) {
|
|
1196
|
+
if (error instanceof BadRequestError ||
|
|
1197
|
+
error instanceof UnauthorizedError ||
|
|
1198
|
+
error instanceof ForbiddenError ||
|
|
1199
|
+
error instanceof NotFoundError ||
|
|
1200
|
+
error instanceof ServerError ||
|
|
1201
|
+
error instanceof UnexpectedError) {
|
|
1202
|
+
throw error;
|
|
1203
|
+
}
|
|
1204
|
+
throw new UnexpectedError('Unexpected error while getting test step attachments', error);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Download an attachment from a test step using private API
|
|
1209
|
+
*
|
|
1210
|
+
* Downloads an attachment file into memory. This method:
|
|
1211
|
+
* 1. Gets fresh attachment details (including a fresh signed S3 URL)
|
|
1212
|
+
* 2. Downloads the file from the signed URL
|
|
1213
|
+
* 3. Returns the file as a Blob
|
|
1214
|
+
*
|
|
1215
|
+
* ⚠️ WARNING: This uses private Zephyr API endpoints that are not officially supported.
|
|
1216
|
+
* The endpoints may change or be removed at any time without notice.
|
|
1217
|
+
*
|
|
1218
|
+
* @param credentials - Private API credentials
|
|
1219
|
+
* @param request - Download attachment request
|
|
1220
|
+
* @param request.testCaseKey - Test case key (e.g., 'PROJ-T1')
|
|
1221
|
+
* @param request.stepId - Numeric test step ID
|
|
1222
|
+
* @param request.projectId - Jira project ID (numeric, not the project key)
|
|
1223
|
+
* @param request.attachmentId - Attachment ID (UUID string)
|
|
1224
|
+
* @returns Blob containing the attachment file data
|
|
1225
|
+
* @throws {BadRequestError} If the request is invalid
|
|
1226
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
1227
|
+
* @throws {ForbiddenError} If the user doesn't have permission
|
|
1228
|
+
* @throws {NotFoundError} If the test case, step, or attachment is not found
|
|
1229
|
+
* @throws {ServerError} If the server returns an error
|
|
1230
|
+
* @throws {UnexpectedError} If download fails
|
|
1231
|
+
*/
|
|
1232
|
+
async downloadTestStepAttachment(credentials, request) {
|
|
1233
|
+
// Step 1: Get fresh attachment details (including fresh signed URL)
|
|
1234
|
+
const attachmentsResponse = await this.getTestStepAttachments(credentials, {
|
|
1235
|
+
testCaseKey: request.testCaseKey,
|
|
1236
|
+
stepId: request.stepId,
|
|
1237
|
+
projectId: request.projectId,
|
|
1238
|
+
});
|
|
1239
|
+
// Step 2: Find the requested attachment
|
|
1240
|
+
const attachment = attachmentsResponse.attachments.find((att) => att.id === request.attachmentId);
|
|
1241
|
+
if (!attachment) {
|
|
1242
|
+
throw new NotFoundError(`Attachment with ID '${request.attachmentId}' not found for test step ${request.stepId} in test case '${request.testCaseKey}'.`);
|
|
1243
|
+
}
|
|
1244
|
+
// Step 3: Download the file from the signed S3 URL
|
|
1245
|
+
try {
|
|
1246
|
+
const downloadResponse = await fetch(attachment.url, {
|
|
1247
|
+
method: 'GET',
|
|
1248
|
+
});
|
|
1249
|
+
if (!downloadResponse.ok) {
|
|
1250
|
+
if (downloadResponse.status === 403) {
|
|
1251
|
+
throw new ForbiddenError('Failed to download attachment. The signed URL may have expired. Please try again.');
|
|
1252
|
+
}
|
|
1253
|
+
if (downloadResponse.status === 404) {
|
|
1254
|
+
throw new NotFoundError('Attachment file not found on S3.');
|
|
1255
|
+
}
|
|
1256
|
+
throw new ServerError(`Failed to download attachment. Status: ${downloadResponse.status}`, downloadResponse.status, downloadResponse.statusText);
|
|
1257
|
+
}
|
|
1258
|
+
// Return as Blob
|
|
1259
|
+
return await downloadResponse.blob();
|
|
1260
|
+
}
|
|
1261
|
+
catch (error) {
|
|
1262
|
+
if (error instanceof ForbiddenError ||
|
|
1263
|
+
error instanceof NotFoundError ||
|
|
1264
|
+
error instanceof ServerError) {
|
|
1265
|
+
throw error;
|
|
1266
|
+
}
|
|
1267
|
+
throw new UnexpectedError('Unexpected error while downloading attachment', error);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Create an attachment for a test step using private API
|
|
1272
|
+
*
|
|
1273
|
+
* Uploads a file attachment to a test step. This involves:
|
|
1274
|
+
* 1. Getting upload details (S3 credentials)
|
|
1275
|
+
* 2. Uploading the file to S3
|
|
1276
|
+
* 3. Saving attachment metadata
|
|
1277
|
+
*
|
|
1278
|
+
* ⚠️ WARNING: This uses private Zephyr API endpoints that are not officially supported.
|
|
1279
|
+
* The endpoints may change or be removed at any time without notice.
|
|
1280
|
+
*
|
|
1281
|
+
* @param credentials - Private API credentials
|
|
1282
|
+
* @param request - Attachment creation request
|
|
1283
|
+
* @param request.testCaseKey - Test case key (e.g., 'PROJ-T1')
|
|
1284
|
+
* @param request.stepId - Numeric test step ID
|
|
1285
|
+
* @param request.projectId - Jira project ID (numeric, not the project key)
|
|
1286
|
+
* @param request.file - File to upload (Blob or ArrayBuffer)
|
|
1287
|
+
* @param request.fileName - Name of the file
|
|
1288
|
+
* @param request.userAccountId - Atlassian account ID (e.g., '5d6fdc98dc6e480dbc021aae')
|
|
1289
|
+
* @returns Attachment metadata response
|
|
1290
|
+
* @throws {BadRequestError} If the request is invalid
|
|
1291
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
1292
|
+
* @throws {ForbiddenError} If the user doesn't have permission
|
|
1293
|
+
* @throws {NotFoundError} If the test case or step is not found
|
|
1294
|
+
* @throws {ServerError} If the server returns an error
|
|
1295
|
+
*/
|
|
1296
|
+
async createTestStepAttachment(credentials, request) {
|
|
1297
|
+
// Get Context JWT
|
|
1298
|
+
const contextJwt = await this.getContextJwt(credentials);
|
|
1299
|
+
// Step 1: Get upload details
|
|
1300
|
+
const uploadDetails = await this.getUploadDetails(contextJwt);
|
|
1301
|
+
// Step 2: Upload to S3
|
|
1302
|
+
const s3Info = await this.uploadToS3ForTestStep(uploadDetails, request.projectId, request.stepId, request.userAccountId, request.file, request.fileName);
|
|
1303
|
+
// Step 3: Save metadata
|
|
1304
|
+
return await this.saveTestStepAttachmentMetadata(contextJwt, request.projectId, request.stepId, request.userAccountId, s3Info.s3Key, s3Info.fileName, s3Info.mimeType, s3Info.fileSize);
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Get attachments for a test execution using private API
|
|
1308
|
+
*
|
|
1309
|
+
* Retrieves all attachment details for a test execution, including signed S3 URLs
|
|
1310
|
+
* for downloading the attachments.
|
|
1311
|
+
*
|
|
1312
|
+
* ⚠️ WARNING: This uses a private Zephyr API endpoint that is not officially supported.
|
|
1313
|
+
* The endpoint may change or be removed at any time without notice.
|
|
1314
|
+
*
|
|
1315
|
+
* @param credentials - Private API credentials
|
|
1316
|
+
* @param request - Get attachments request
|
|
1317
|
+
* @param request.testExecutionKey - Test execution key (e.g., 'PROJ-E1'). The numeric ID will be looked up automatically if Zephyr Connector is available.
|
|
1318
|
+
* @param request.projectId - Jira project ID (numeric, not the project key)
|
|
1319
|
+
* @returns Test execution attachments response with array of attachment details
|
|
1320
|
+
* @throws {BadRequestError} If the request is invalid
|
|
1321
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
1322
|
+
* @throws {ForbiddenError} If the user doesn't have permission
|
|
1323
|
+
* @throws {NotFoundError} If the test execution is not found
|
|
1324
|
+
* @throws {ServerError} If the server returns an error
|
|
1325
|
+
* @throws {UnexpectedError} If test execution ID cannot be looked up from key and Zephyr Connector is not available
|
|
1326
|
+
*/
|
|
1327
|
+
async getTestExecutionAttachments(credentials, request) {
|
|
1328
|
+
// Get test execution ID from key if we have API connection
|
|
1329
|
+
let testExecutionId;
|
|
1330
|
+
if (this.testExecutionGroup) {
|
|
1331
|
+
try {
|
|
1332
|
+
const testExecution = await this.testExecutionGroup.getTestExecution({
|
|
1333
|
+
testExecutionIdOrKey: request.testExecutionKey,
|
|
1334
|
+
});
|
|
1335
|
+
testExecutionId = testExecution.id;
|
|
1336
|
+
}
|
|
1337
|
+
catch (error) {
|
|
1338
|
+
if (error instanceof NotFoundError) {
|
|
1339
|
+
throw new NotFoundError(`Test execution with key '${request.testExecutionKey}' not found.`);
|
|
1340
|
+
}
|
|
1341
|
+
throw new UnexpectedError(`Failed to look up test execution ID from key '${request.testExecutionKey}'. Ensure Zephyr Connector is configured.`, error);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
throw new UnexpectedError('Cannot look up test execution ID from key. This method requires Zephyr Connector to be configured. Use createZephyrApi() with an API Connection.');
|
|
1346
|
+
}
|
|
1347
|
+
// Get Context JWT
|
|
1348
|
+
const contextJwt = await this.getContextJwt(credentials);
|
|
1349
|
+
const url = `${this.privateApiBaseUrl}/testresult/${request.testExecutionKey}?fields=attachments&itemId=${request.testExecutionKey}`;
|
|
1350
|
+
const headers = {
|
|
1351
|
+
'accept': 'application/json',
|
|
1352
|
+
'authorization': `JWT ${contextJwt}`,
|
|
1353
|
+
'jira-project-id': String(request.projectId),
|
|
1354
|
+
};
|
|
1355
|
+
try {
|
|
1356
|
+
const response = await fetch(url, {
|
|
1357
|
+
method: 'GET',
|
|
1358
|
+
headers,
|
|
1359
|
+
});
|
|
1360
|
+
if (!response.ok) {
|
|
1361
|
+
if (response.status === 400) {
|
|
1362
|
+
throw new BadRequestError('Invalid request parameters for getting attachments.');
|
|
1363
|
+
}
|
|
1364
|
+
if (response.status === 401) {
|
|
1365
|
+
throw new UnauthorizedError('Failed to authenticate. Please check your credentials.');
|
|
1366
|
+
}
|
|
1367
|
+
if (response.status === 403) {
|
|
1368
|
+
throw new ForbiddenError('Insufficient permissions to get attachments.');
|
|
1369
|
+
}
|
|
1370
|
+
if (response.status === 404) {
|
|
1371
|
+
throw new NotFoundError(`Test execution with key '${request.testExecutionKey}' not found.`);
|
|
1372
|
+
}
|
|
1373
|
+
throw new ServerError(`Failed to get test execution attachments. Status: ${response.status}`, response.status, response.statusText);
|
|
1374
|
+
}
|
|
1375
|
+
const executionData = await response.json();
|
|
1376
|
+
return {
|
|
1377
|
+
attachments: (executionData.attachments || []).map((att) => ({
|
|
1378
|
+
id: att.id,
|
|
1379
|
+
dbAttachmentId: att.dbAttachmentId,
|
|
1380
|
+
url: att.url,
|
|
1381
|
+
userKey: att.userKey,
|
|
1382
|
+
createdOn: att.createdOn,
|
|
1383
|
+
name: att.name,
|
|
1384
|
+
mimeType: att.mimeType,
|
|
1385
|
+
fileSize: att.fileSize,
|
|
1386
|
+
copyFromStep: att.copyFromStep,
|
|
1387
|
+
s3Key: att.s3Key,
|
|
1388
|
+
testExecutionId: att.testExecutionId,
|
|
1389
|
+
})),
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
catch (error) {
|
|
1393
|
+
if (error instanceof BadRequestError ||
|
|
1394
|
+
error instanceof UnauthorizedError ||
|
|
1395
|
+
error instanceof ForbiddenError ||
|
|
1396
|
+
error instanceof NotFoundError ||
|
|
1397
|
+
error instanceof ServerError ||
|
|
1398
|
+
error instanceof UnexpectedError) {
|
|
1399
|
+
throw error;
|
|
1400
|
+
}
|
|
1401
|
+
throw new UnexpectedError('Unexpected error while getting test execution attachments', error);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Download an attachment from a test execution using private API
|
|
1406
|
+
*
|
|
1407
|
+
* Downloads an attachment file into memory. This method:
|
|
1408
|
+
* 1. Gets fresh attachment details (including a fresh signed S3 URL)
|
|
1409
|
+
* 2. Downloads the file from the signed URL
|
|
1410
|
+
* 3. Returns the file as a Blob
|
|
1411
|
+
*
|
|
1412
|
+
* ⚠️ WARNING: This uses private Zephyr API endpoints that are not officially supported.
|
|
1413
|
+
* The endpoints may change or be removed at any time without notice.
|
|
1414
|
+
*
|
|
1415
|
+
* @param credentials - Private API credentials
|
|
1416
|
+
* @param request - Download attachment request
|
|
1417
|
+
* @param request.testExecutionKey - Test execution key (e.g., 'PROJ-E1'). The numeric ID will be looked up automatically if Zephyr Connector is available.
|
|
1418
|
+
* @param request.projectId - Jira project ID (numeric, not the project key)
|
|
1419
|
+
* @param request.attachmentId - Attachment ID (UUID string)
|
|
1420
|
+
* @returns Blob containing the attachment file data
|
|
1421
|
+
* @throws {BadRequestError} If the request is invalid
|
|
1422
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
1423
|
+
* @throws {ForbiddenError} If the user doesn't have permission
|
|
1424
|
+
* @throws {NotFoundError} If the test execution or attachment is not found
|
|
1425
|
+
* @throws {ServerError} If the server returns an error
|
|
1426
|
+
* @throws {UnexpectedError} If test execution ID cannot be looked up from key and Zephyr Connector is not available, or if download fails
|
|
1427
|
+
*/
|
|
1428
|
+
async downloadTestExecutionAttachment(credentials, request) {
|
|
1429
|
+
// Step 1: Get fresh attachment details (including fresh signed URL)
|
|
1430
|
+
const attachmentsResponse = await this.getTestExecutionAttachments(credentials, {
|
|
1431
|
+
testExecutionKey: request.testExecutionKey,
|
|
1432
|
+
projectId: request.projectId,
|
|
1433
|
+
});
|
|
1434
|
+
// Step 2: Find the requested attachment
|
|
1435
|
+
const attachment = attachmentsResponse.attachments.find((att) => att.id === request.attachmentId);
|
|
1436
|
+
if (!attachment) {
|
|
1437
|
+
throw new NotFoundError(`Attachment with ID '${request.attachmentId}' not found for test execution '${request.testExecutionKey}'.`);
|
|
1438
|
+
}
|
|
1439
|
+
// Step 3: Download the file from the signed S3 URL
|
|
1440
|
+
try {
|
|
1441
|
+
const downloadResponse = await fetch(attachment.url, {
|
|
1442
|
+
method: 'GET',
|
|
1443
|
+
});
|
|
1444
|
+
if (!downloadResponse.ok) {
|
|
1445
|
+
if (downloadResponse.status === 403) {
|
|
1446
|
+
throw new ForbiddenError('Failed to download attachment. The signed URL may have expired. Please try again.');
|
|
1447
|
+
}
|
|
1448
|
+
if (downloadResponse.status === 404) {
|
|
1449
|
+
throw new NotFoundError('Attachment file not found on S3.');
|
|
1450
|
+
}
|
|
1451
|
+
throw new ServerError(`Failed to download attachment. Status: ${downloadResponse.status}`, downloadResponse.status, downloadResponse.statusText);
|
|
1452
|
+
}
|
|
1453
|
+
// Return as Blob
|
|
1454
|
+
return await downloadResponse.blob();
|
|
1455
|
+
}
|
|
1456
|
+
catch (error) {
|
|
1457
|
+
if (error instanceof ForbiddenError ||
|
|
1458
|
+
error instanceof NotFoundError ||
|
|
1459
|
+
error instanceof ServerError) {
|
|
1460
|
+
throw error;
|
|
1461
|
+
}
|
|
1462
|
+
throw new UnexpectedError('Unexpected error while downloading attachment', error);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Create an attachment for a test execution using private API
|
|
1467
|
+
*
|
|
1468
|
+
* Uploads a file attachment to a test execution. This involves:
|
|
1469
|
+
* 1. Getting upload details (S3 credentials)
|
|
1470
|
+
* 2. Uploading the file to S3
|
|
1471
|
+
* 3. Saving attachment metadata
|
|
1472
|
+
*
|
|
1473
|
+
* ⚠️ WARNING: This uses private Zephyr API endpoints that are not officially supported.
|
|
1474
|
+
* The endpoints may change or be removed at any time without notice.
|
|
1475
|
+
*
|
|
1476
|
+
* @param credentials - Private API credentials
|
|
1477
|
+
* @param request - Attachment creation request
|
|
1478
|
+
* @param request.testExecutionKey - Test execution key (e.g., 'PROJ-E1'). The numeric ID will be looked up automatically if Zephyr Connector is available.
|
|
1479
|
+
* @param request.projectId - Jira project ID (numeric, not the project key)
|
|
1480
|
+
* @param request.file - File to upload (Blob or ArrayBuffer)
|
|
1481
|
+
* @param request.fileName - Name of the file
|
|
1482
|
+
* @param request.userAccountId - Atlassian account ID (e.g., '5d6fdc98dc6e480dbc021aae')
|
|
1483
|
+
* @returns Attachment metadata response
|
|
1484
|
+
* @throws {BadRequestError} If the request is invalid
|
|
1485
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
1486
|
+
* @throws {ForbiddenError} If the user doesn't have permission
|
|
1487
|
+
* @throws {NotFoundError} If the test execution is not found
|
|
1488
|
+
* @throws {ServerError} If the server returns an error
|
|
1489
|
+
* @throws {UnexpectedError} If test execution ID cannot be looked up from key and Zephyr Connector is not available
|
|
1490
|
+
*/
|
|
1491
|
+
async createTestExecutionAttachment(credentials, request) {
|
|
1492
|
+
// Get test execution ID from key if we have API connection
|
|
1493
|
+
let testExecutionId;
|
|
1494
|
+
if (this.testExecutionGroup) {
|
|
1495
|
+
try {
|
|
1496
|
+
const testExecution = await this.testExecutionGroup.getTestExecution({
|
|
1497
|
+
testExecutionIdOrKey: request.testExecutionKey,
|
|
1498
|
+
});
|
|
1499
|
+
testExecutionId = testExecution.id;
|
|
1500
|
+
}
|
|
1501
|
+
catch (error) {
|
|
1502
|
+
if (error instanceof NotFoundError) {
|
|
1503
|
+
throw new NotFoundError(`Test execution with key '${request.testExecutionKey}' not found.`);
|
|
1504
|
+
}
|
|
1505
|
+
throw new UnexpectedError(`Failed to look up test execution ID from key '${request.testExecutionKey}'. Ensure Zephyr Connector is configured.`, error);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
else {
|
|
1509
|
+
throw new UnexpectedError('Cannot look up test execution ID from key. This method requires Zephyr Connector to be configured. Use createZephyrApi() with an API Connection.');
|
|
1510
|
+
}
|
|
1511
|
+
// Get Context JWT
|
|
1512
|
+
const contextJwt = await this.getContextJwt(credentials);
|
|
1513
|
+
// Step 1: Get upload details
|
|
1514
|
+
const uploadDetails = await this.getUploadDetails(contextJwt);
|
|
1515
|
+
// Step 2: Upload to S3
|
|
1516
|
+
const s3Info = await this.uploadToS3ForTestExecution(uploadDetails, request.projectId, testExecutionId, request.userAccountId, request.file, request.fileName);
|
|
1517
|
+
// Step 3: Save metadata
|
|
1518
|
+
return await this.saveTestExecutionAttachmentMetadata(contextJwt, request.projectId, testExecutionId, request.userAccountId, s3Info.s3Key, s3Info.fileName, s3Info.mimeType, s3Info.fileSize);
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Get attachments for a test execution step using private API
|
|
1522
|
+
*
|
|
1523
|
+
* Retrieves all attachment details for a specific test execution step (testScriptResult).
|
|
1524
|
+
* Attachments are retrieved by getting the test execution with fields including testScriptResults.attachments.
|
|
1525
|
+
*
|
|
1526
|
+
* ⚠️ WARNING: This uses a private Zephyr API endpoint that is not officially supported.
|
|
1527
|
+
* The endpoint may change or be removed at any time without notice.
|
|
1528
|
+
*
|
|
1529
|
+
* @param credentials - Private API credentials
|
|
1530
|
+
* @param request - Get attachments request
|
|
1531
|
+
* @param request.testExecutionKey - Test execution key (e.g., 'PROJ-E1')
|
|
1532
|
+
* @param request.testScriptResultId - Numeric test script result ID
|
|
1533
|
+
* @param request.projectId - Jira project ID (numeric, not the project key)
|
|
1534
|
+
* @returns Test execution step attachments response with array of attachment details
|
|
1535
|
+
* @throws {BadRequestError} If the request is invalid
|
|
1536
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
1537
|
+
* @throws {ForbiddenError} If the user doesn't have permission
|
|
1538
|
+
* @throws {NotFoundError} If the test execution or step is not found
|
|
1539
|
+
* @throws {ServerError} If the server returns an error
|
|
1540
|
+
* @throws {UnexpectedError} If the test execution cannot be retrieved
|
|
1541
|
+
*/
|
|
1542
|
+
async getTestExecutionStepAttachments(credentials, request) {
|
|
1543
|
+
// Get Context JWT
|
|
1544
|
+
const contextJwt = await this.getContextJwt(credentials);
|
|
1545
|
+
const url = `${this.privateApiBaseUrl}/testresult/${request.testExecutionKey}?fields=testScriptResults(id,testResultStatusId,executionDate,comment,index,description,expectedResult,testData,traceLinks,attachments,sourceScriptType,parameterSetId,customFieldValues,stepAttachmentsMapping,reflectRef),attachments&itemId=${request.testExecutionKey}`;
|
|
1546
|
+
const headers = {
|
|
1547
|
+
'accept': 'application/json',
|
|
1548
|
+
'authorization': `JWT ${contextJwt}`,
|
|
1549
|
+
'jira-project-id': String(request.projectId),
|
|
1550
|
+
};
|
|
1551
|
+
try {
|
|
1552
|
+
const response = await fetch(url, {
|
|
1553
|
+
method: 'GET',
|
|
1554
|
+
headers,
|
|
1555
|
+
});
|
|
1556
|
+
if (!response.ok) {
|
|
1557
|
+
if (response.status === 400) {
|
|
1558
|
+
throw new BadRequestError('Invalid request parameters for getting attachments.');
|
|
1559
|
+
}
|
|
1560
|
+
if (response.status === 401) {
|
|
1561
|
+
throw new UnauthorizedError('Failed to authenticate. Please check your credentials.');
|
|
1562
|
+
}
|
|
1563
|
+
if (response.status === 403) {
|
|
1564
|
+
throw new ForbiddenError('Insufficient permissions to get attachments.');
|
|
1565
|
+
}
|
|
1566
|
+
if (response.status === 404) {
|
|
1567
|
+
throw new NotFoundError(`Test execution with key '${request.testExecutionKey}' not found.`);
|
|
1568
|
+
}
|
|
1569
|
+
throw new ServerError(`Failed to get test execution step attachments. Status: ${response.status}`, response.status, response.statusText);
|
|
1570
|
+
}
|
|
1571
|
+
const executionData = await response.json();
|
|
1572
|
+
// Find the specific testScriptResult
|
|
1573
|
+
const step = executionData.testScriptResults?.find((s) => s.id === request.testScriptResultId);
|
|
1574
|
+
if (!step) {
|
|
1575
|
+
throw new NotFoundError(`Test execution step with ID ${request.testScriptResultId} not found in test execution '${request.testExecutionKey}'.`);
|
|
1576
|
+
}
|
|
1577
|
+
return {
|
|
1578
|
+
attachments: (step.attachments || []).map((att) => ({
|
|
1579
|
+
id: att.id,
|
|
1580
|
+
dbAttachmentId: att.dbAttachmentId,
|
|
1581
|
+
url: att.url,
|
|
1582
|
+
userKey: att.userKey,
|
|
1583
|
+
createdOn: att.createdOn,
|
|
1584
|
+
name: att.name,
|
|
1585
|
+
mimeType: att.mimeType,
|
|
1586
|
+
fileSize: att.fileSize,
|
|
1587
|
+
copyFromStep: att.copyFromStep,
|
|
1588
|
+
s3Key: att.s3Key,
|
|
1589
|
+
testScriptResultId: att.testScriptResultId,
|
|
1590
|
+
})),
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
catch (error) {
|
|
1594
|
+
if (error instanceof BadRequestError ||
|
|
1595
|
+
error instanceof UnauthorizedError ||
|
|
1596
|
+
error instanceof ForbiddenError ||
|
|
1597
|
+
error instanceof NotFoundError ||
|
|
1598
|
+
error instanceof ServerError ||
|
|
1599
|
+
error instanceof UnexpectedError) {
|
|
1600
|
+
throw error;
|
|
1601
|
+
}
|
|
1602
|
+
throw new UnexpectedError('Unexpected error while getting test execution step attachments', error);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Download an attachment from a test execution step using private API
|
|
1607
|
+
*
|
|
1608
|
+
* Downloads an attachment file into memory. This method:
|
|
1609
|
+
* 1. Gets fresh attachment details (including a fresh signed S3 URL)
|
|
1610
|
+
* 2. Downloads the file from the signed URL
|
|
1611
|
+
* 3. Returns the file as a Blob
|
|
1612
|
+
*
|
|
1613
|
+
* ⚠️ WARNING: This uses private Zephyr API endpoints that are not officially supported.
|
|
1614
|
+
* The endpoints may change or be removed at any time without notice.
|
|
1615
|
+
*
|
|
1616
|
+
* @param credentials - Private API credentials
|
|
1617
|
+
* @param request - Download attachment request
|
|
1618
|
+
* @param request.testExecutionKey - Test execution key (e.g., 'PROJ-E1')
|
|
1619
|
+
* @param request.testScriptResultId - Numeric test script result ID
|
|
1620
|
+
* @param request.projectId - Jira project ID (numeric, not the project key)
|
|
1621
|
+
* @param request.attachmentId - Attachment ID (UUID string)
|
|
1622
|
+
* @returns Blob containing the attachment file data
|
|
1623
|
+
* @throws {BadRequestError} If the request is invalid
|
|
1624
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
1625
|
+
* @throws {ForbiddenError} If the user doesn't have permission
|
|
1626
|
+
* @throws {NotFoundError} If the test execution, step, or attachment is not found
|
|
1627
|
+
* @throws {ServerError} If the server returns an error
|
|
1628
|
+
* @throws {UnexpectedError} If download fails
|
|
1629
|
+
*/
|
|
1630
|
+
async downloadTestExecutionStepAttachment(credentials, request) {
|
|
1631
|
+
// Step 1: Get fresh attachment details (including fresh signed URL)
|
|
1632
|
+
const attachmentsResponse = await this.getTestExecutionStepAttachments(credentials, {
|
|
1633
|
+
testExecutionKey: request.testExecutionKey,
|
|
1634
|
+
testScriptResultId: request.testScriptResultId,
|
|
1635
|
+
projectId: request.projectId,
|
|
1636
|
+
});
|
|
1637
|
+
// Step 2: Find the requested attachment
|
|
1638
|
+
const attachment = attachmentsResponse.attachments.find((att) => att.id === request.attachmentId);
|
|
1639
|
+
if (!attachment) {
|
|
1640
|
+
throw new NotFoundError(`Attachment with ID '${request.attachmentId}' not found for test execution step ${request.testScriptResultId} in test execution '${request.testExecutionKey}'.`);
|
|
1641
|
+
}
|
|
1642
|
+
// Step 3: Download the file from the signed S3 URL
|
|
1643
|
+
try {
|
|
1644
|
+
const downloadResponse = await fetch(attachment.url, {
|
|
1645
|
+
method: 'GET',
|
|
1646
|
+
});
|
|
1647
|
+
if (!downloadResponse.ok) {
|
|
1648
|
+
if (downloadResponse.status === 403) {
|
|
1649
|
+
throw new ForbiddenError('Failed to download attachment. The signed URL may have expired. Please try again.');
|
|
1650
|
+
}
|
|
1651
|
+
if (downloadResponse.status === 404) {
|
|
1652
|
+
throw new NotFoundError('Attachment file not found on S3.');
|
|
1653
|
+
}
|
|
1654
|
+
throw new ServerError(`Failed to download attachment. Status: ${downloadResponse.status}`, downloadResponse.status, downloadResponse.statusText);
|
|
1655
|
+
}
|
|
1656
|
+
// Return as Blob
|
|
1657
|
+
return await downloadResponse.blob();
|
|
1658
|
+
}
|
|
1659
|
+
catch (error) {
|
|
1660
|
+
if (error instanceof ForbiddenError ||
|
|
1661
|
+
error instanceof NotFoundError ||
|
|
1662
|
+
error instanceof ServerError) {
|
|
1663
|
+
throw error;
|
|
1664
|
+
}
|
|
1665
|
+
throw new UnexpectedError('Unexpected error while downloading attachment', error);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Create an attachment for a test execution step using private API
|
|
1670
|
+
*
|
|
1671
|
+
* Uploads a file attachment to a test execution step. This involves:
|
|
1672
|
+
* 1. Getting upload details (S3 credentials)
|
|
1673
|
+
* 2. Uploading the file to S3
|
|
1674
|
+
* 3. Saving attachment metadata
|
|
1675
|
+
*
|
|
1676
|
+
* ⚠️ WARNING: This uses private Zephyr API endpoints that are not officially supported.
|
|
1677
|
+
* The endpoints may change or be removed at any time without notice.
|
|
1678
|
+
*
|
|
1679
|
+
* @param credentials - Private API credentials
|
|
1680
|
+
* @param request - Attachment creation request
|
|
1681
|
+
* @param request.testExecutionKey - Test execution key (e.g., 'PROJ-E1')
|
|
1682
|
+
* @param request.testScriptResultId - Numeric test script result ID
|
|
1683
|
+
* @param request.projectId - Jira project ID (numeric, not the project key)
|
|
1684
|
+
* @param request.file - File to upload (Blob or ArrayBuffer)
|
|
1685
|
+
* @param request.fileName - Name of the file
|
|
1686
|
+
* @param request.userAccountId - Atlassian account ID (e.g., '5d6fdc98dc6e480dbc021aae')
|
|
1687
|
+
* @returns Attachment metadata response
|
|
1688
|
+
* @throws {BadRequestError} If the request is invalid
|
|
1689
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
1690
|
+
* @throws {ForbiddenError} If the user doesn't have permission
|
|
1691
|
+
* @throws {NotFoundError} If the test execution or step is not found
|
|
1692
|
+
* @throws {ServerError} If the server returns an error
|
|
1693
|
+
*/
|
|
1694
|
+
async createTestExecutionStepAttachment(credentials, request) {
|
|
1695
|
+
// Get Context JWT
|
|
1696
|
+
const contextJwt = await this.getContextJwt(credentials);
|
|
1697
|
+
// Step 1: Get upload details
|
|
1698
|
+
const uploadDetails = await this.getUploadDetails(contextJwt);
|
|
1699
|
+
// Step 2: Upload to S3
|
|
1700
|
+
const s3Info = await this.uploadToS3ForTestExecutionStep(uploadDetails, request.projectId, request.testScriptResultId, request.userAccountId, request.file, request.fileName);
|
|
1701
|
+
// Step 3: Save metadata
|
|
1702
|
+
return await this.saveTestExecutionStepAttachmentMetadata(contextJwt, request.projectId, request.testScriptResultId, request.userAccountId, s3Info.s3Key, s3Info.fileName, s3Info.mimeType, s3Info.fileSize);
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Upload file to S3 for test step
|
|
1706
|
+
*
|
|
1707
|
+
* Uploads a file to S3 using the credentials from upload details.
|
|
1708
|
+
* Uses "step" in the S3 key path.
|
|
1709
|
+
*
|
|
1710
|
+
* @param upload - Upload details from getUploadDetails
|
|
1711
|
+
* @param projectId - Jira project ID
|
|
1712
|
+
* @param stepId - Numeric test step ID
|
|
1713
|
+
* @param userAccountId - Atlassian account ID
|
|
1714
|
+
* @param file - File to upload (Blob or ArrayBuffer)
|
|
1715
|
+
* @param fileName - Name of the file
|
|
1716
|
+
* @returns S3 upload information
|
|
1717
|
+
* @throws {UnexpectedError} If upload fails
|
|
1718
|
+
*/
|
|
1719
|
+
async uploadToS3ForTestStep(upload, projectId, stepId, userAccountId, file, fileName) {
|
|
1720
|
+
const bucketUrl = upload.bucketUrl;
|
|
1721
|
+
const keyPrefix = upload.keyPrefix;
|
|
1722
|
+
const credential = upload.credential;
|
|
1723
|
+
const date = upload.date;
|
|
1724
|
+
const policy = upload.policy;
|
|
1725
|
+
const signature = upload.signature;
|
|
1726
|
+
// Generate UUID for file
|
|
1727
|
+
const fileUuid = this.generateUUID();
|
|
1728
|
+
// Note: Test steps use "step" in the S3 key path, and the keyPrefix includes "write/"
|
|
1729
|
+
const s3Key = `${keyPrefix}/project/${projectId}/step/${stepId}/${fileUuid}`;
|
|
1730
|
+
// Determine MIME type
|
|
1731
|
+
const mimeType = this.getMimeType(fileName);
|
|
1732
|
+
// Get file size
|
|
1733
|
+
const fileSize = file instanceof Blob ? file.size : file.byteLength;
|
|
1734
|
+
// Create form data
|
|
1735
|
+
const formData = new FormData();
|
|
1736
|
+
formData.append('key', s3Key);
|
|
1737
|
+
formData.append('acl', 'private');
|
|
1738
|
+
formData.append('Policy', policy);
|
|
1739
|
+
formData.append('X-Amz-Credential', credential);
|
|
1740
|
+
formData.append('X-Amz-Algorithm', 'AWS4-HMAC-SHA256');
|
|
1741
|
+
formData.append('X-Amz-Date', date);
|
|
1742
|
+
formData.append('X-Amz-Signature', signature);
|
|
1743
|
+
formData.append('X-Amz-Meta-user-account-id', userAccountId);
|
|
1744
|
+
formData.append('X-Amz-Meta-name', fileName);
|
|
1745
|
+
formData.append('Content-Type', mimeType);
|
|
1746
|
+
// Add file
|
|
1747
|
+
if (file instanceof Blob) {
|
|
1748
|
+
formData.append('file', file, fileName);
|
|
1749
|
+
}
|
|
1750
|
+
else {
|
|
1751
|
+
formData.append('file', new Blob([file], { type: mimeType }), fileName);
|
|
1752
|
+
}
|
|
1753
|
+
try {
|
|
1754
|
+
const response = await fetch(bucketUrl, {
|
|
1755
|
+
method: 'POST',
|
|
1756
|
+
body: formData,
|
|
1757
|
+
});
|
|
1758
|
+
if (!response.ok && response.status !== 200 && response.status !== 201 && response.status !== 204) {
|
|
1759
|
+
const errorText = await response.text().catch(() => 'Upload failed');
|
|
1760
|
+
throw new UnexpectedError(`Failed to upload file to S3: ${errorText}`);
|
|
1761
|
+
}
|
|
1762
|
+
return {
|
|
1763
|
+
s3Key,
|
|
1764
|
+
fileName,
|
|
1765
|
+
mimeType,
|
|
1766
|
+
fileSize,
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
catch (error) {
|
|
1770
|
+
if (error instanceof UnexpectedError) {
|
|
1771
|
+
throw error;
|
|
1772
|
+
}
|
|
1773
|
+
throw new UnexpectedError('Unexpected error while uploading file to S3', error);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Save test step attachment metadata
|
|
1778
|
+
*
|
|
1779
|
+
* Saves metadata for an uploaded test step attachment.
|
|
1780
|
+
*
|
|
1781
|
+
* @param contextJwt - Jira Context JWT token
|
|
1782
|
+
* @param projectId - Jira project ID
|
|
1783
|
+
* @param stepId - Numeric test step ID
|
|
1784
|
+
* @param userAccountId - Atlassian account ID
|
|
1785
|
+
* @param s3Key - S3 key from upload
|
|
1786
|
+
* @param fileName - File name
|
|
1787
|
+
* @param mimeType - MIME type
|
|
1788
|
+
* @param fileSize - File size in bytes
|
|
1789
|
+
* @returns Attachment metadata response
|
|
1790
|
+
* @throws {BadRequestError} If the request is invalid
|
|
1791
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
1792
|
+
* @throws {ServerError} If the server returns an error
|
|
1793
|
+
*/
|
|
1794
|
+
async saveTestStepAttachmentMetadata(contextJwt, projectId, stepId, userAccountId, s3Key, fileName, mimeType, fileSize) {
|
|
1795
|
+
const url = 'https://app.tm4j.smartbear.com/backend/rest/tests/2.0/attachment/metadata';
|
|
1796
|
+
const headers = {
|
|
1797
|
+
'Authorization': `JWT ${contextJwt}`,
|
|
1798
|
+
'jira-project-id': String(projectId),
|
|
1799
|
+
'Content-Type': 'application/json',
|
|
1800
|
+
'Accept': 'application/json',
|
|
1801
|
+
};
|
|
1802
|
+
const createdOn = new Date().toISOString();
|
|
1803
|
+
const payload = {
|
|
1804
|
+
createdOn,
|
|
1805
|
+
mimeType,
|
|
1806
|
+
name: fileName,
|
|
1807
|
+
s3Key,
|
|
1808
|
+
size: fileSize,
|
|
1809
|
+
stepId,
|
|
1810
|
+
userAccountId,
|
|
1811
|
+
};
|
|
1812
|
+
try {
|
|
1813
|
+
const response = await fetch(url, {
|
|
1814
|
+
method: 'POST',
|
|
1815
|
+
headers,
|
|
1816
|
+
body: JSON.stringify(payload),
|
|
1817
|
+
});
|
|
1818
|
+
if (!response.ok) {
|
|
1819
|
+
if (response.status === 400) {
|
|
1820
|
+
const errorText = await response.text().catch(() => 'Bad Request');
|
|
1821
|
+
throw new BadRequestError(`Failed to save attachment metadata: ${errorText}`, response.statusText);
|
|
1822
|
+
}
|
|
1823
|
+
if (response.status === 401) {
|
|
1824
|
+
throw new UnauthorizedError('Authentication failed. Please check your credentials.');
|
|
1825
|
+
}
|
|
1826
|
+
throw new ServerError(`Failed to save attachment metadata. Status: ${response.status}`, response.status, response.statusText);
|
|
1827
|
+
}
|
|
1828
|
+
return await response.json();
|
|
1829
|
+
}
|
|
1830
|
+
catch (error) {
|
|
1831
|
+
if (error instanceof BadRequestError ||
|
|
1832
|
+
error instanceof UnauthorizedError ||
|
|
1833
|
+
error instanceof ServerError) {
|
|
1834
|
+
throw error;
|
|
1835
|
+
}
|
|
1836
|
+
throw new UnexpectedError('Unexpected error while saving attachment metadata', error);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Upload file to S3 for test execution
|
|
1841
|
+
*
|
|
1842
|
+
* Uploads a file to S3 using the credentials from upload details.
|
|
1843
|
+
* Uses "testresult" in the S3 key path.
|
|
1844
|
+
*
|
|
1845
|
+
* @param upload - Upload details from getUploadDetails
|
|
1846
|
+
* @param projectId - Jira project ID
|
|
1847
|
+
* @param testExecutionId - Numeric test execution ID
|
|
1848
|
+
* @param userAccountId - Atlassian account ID
|
|
1849
|
+
* @param file - File to upload (Blob or ArrayBuffer)
|
|
1850
|
+
* @param fileName - Name of the file
|
|
1851
|
+
* @returns S3 upload information
|
|
1852
|
+
* @throws {UnexpectedError} If upload fails
|
|
1853
|
+
*/
|
|
1854
|
+
async uploadToS3ForTestExecution(upload, projectId, testExecutionId, userAccountId, file, fileName) {
|
|
1855
|
+
const bucketUrl = upload.bucketUrl;
|
|
1856
|
+
const keyPrefix = upload.keyPrefix;
|
|
1857
|
+
const credential = upload.credential;
|
|
1858
|
+
const date = upload.date;
|
|
1859
|
+
const policy = upload.policy;
|
|
1860
|
+
const signature = upload.signature;
|
|
1861
|
+
// Generate UUID for file
|
|
1862
|
+
const fileUuid = this.generateUUID();
|
|
1863
|
+
// Note: Test executions use "testresult" in the S3 key path, and the keyPrefix includes "write/"
|
|
1864
|
+
const s3Key = `${keyPrefix}/project/${projectId}/testresult/${testExecutionId}/${fileUuid}`;
|
|
1865
|
+
// Determine MIME type
|
|
1866
|
+
const mimeType = this.getMimeType(fileName);
|
|
1867
|
+
// Get file size
|
|
1868
|
+
const fileSize = file instanceof Blob ? file.size : file.byteLength;
|
|
1869
|
+
// Create form data
|
|
1870
|
+
const formData = new FormData();
|
|
1871
|
+
formData.append('key', s3Key);
|
|
1872
|
+
formData.append('acl', 'private');
|
|
1873
|
+
formData.append('Policy', policy);
|
|
1874
|
+
formData.append('X-Amz-Credential', credential);
|
|
1875
|
+
formData.append('X-Amz-Algorithm', 'AWS4-HMAC-SHA256');
|
|
1876
|
+
formData.append('X-Amz-Date', date);
|
|
1877
|
+
formData.append('X-Amz-Signature', signature);
|
|
1878
|
+
formData.append('X-Amz-Meta-user-account-id', userAccountId);
|
|
1879
|
+
formData.append('X-Amz-Meta-name', fileName);
|
|
1880
|
+
formData.append('Content-Type', mimeType);
|
|
1881
|
+
// Add file
|
|
1882
|
+
if (file instanceof Blob) {
|
|
1883
|
+
formData.append('file', file, fileName);
|
|
1884
|
+
}
|
|
1885
|
+
else {
|
|
1886
|
+
formData.append('file', new Blob([file], { type: mimeType }), fileName);
|
|
1887
|
+
}
|
|
1888
|
+
try {
|
|
1889
|
+
const response = await fetch(bucketUrl, {
|
|
1890
|
+
method: 'POST',
|
|
1891
|
+
body: formData,
|
|
1892
|
+
});
|
|
1893
|
+
if (!response.ok && response.status !== 200 && response.status !== 201 && response.status !== 204) {
|
|
1894
|
+
const errorText = await response.text().catch(() => 'Upload failed');
|
|
1895
|
+
throw new UnexpectedError(`Failed to upload file to S3: ${errorText}`);
|
|
1896
|
+
}
|
|
1897
|
+
return {
|
|
1898
|
+
s3Key,
|
|
1899
|
+
fileName,
|
|
1900
|
+
mimeType,
|
|
1901
|
+
fileSize,
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
catch (error) {
|
|
1905
|
+
if (error instanceof UnexpectedError) {
|
|
1906
|
+
throw error;
|
|
1907
|
+
}
|
|
1908
|
+
throw new UnexpectedError('Unexpected error while uploading file to S3', error);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Save test execution attachment metadata
|
|
1913
|
+
*
|
|
1914
|
+
* Saves metadata for an uploaded test execution attachment.
|
|
1915
|
+
*
|
|
1916
|
+
* @param contextJwt - Jira Context JWT token
|
|
1917
|
+
* @param projectId - Jira project ID
|
|
1918
|
+
* @param testExecutionId - Numeric test execution ID
|
|
1919
|
+
* @param userAccountId - Atlassian account ID
|
|
1920
|
+
* @param s3Key - S3 key from upload
|
|
1921
|
+
* @param fileName - File name
|
|
1922
|
+
* @param mimeType - MIME type
|
|
1923
|
+
* @param fileSize - File size in bytes
|
|
1924
|
+
* @returns Attachment metadata response
|
|
1925
|
+
* @throws {BadRequestError} If the request is invalid
|
|
1926
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
1927
|
+
* @throws {ServerError} If the server returns an error
|
|
1928
|
+
*/
|
|
1929
|
+
async saveTestExecutionAttachmentMetadata(contextJwt, projectId, testExecutionId, userAccountId, s3Key, fileName, mimeType, fileSize) {
|
|
1930
|
+
const url = 'https://app.tm4j.smartbear.com/backend/rest/tests/2.0/attachment/metadata';
|
|
1931
|
+
const headers = {
|
|
1932
|
+
'Authorization': `JWT ${contextJwt}`,
|
|
1933
|
+
'jira-project-id': String(projectId),
|
|
1934
|
+
'Content-Type': 'application/json',
|
|
1935
|
+
'Accept': 'application/json',
|
|
1936
|
+
};
|
|
1937
|
+
const createdOn = new Date().toISOString();
|
|
1938
|
+
const payload = {
|
|
1939
|
+
createdOn,
|
|
1940
|
+
mimeType,
|
|
1941
|
+
name: fileName,
|
|
1942
|
+
s3Key,
|
|
1943
|
+
size: fileSize,
|
|
1944
|
+
testExecutionId,
|
|
1945
|
+
userAccountId,
|
|
1946
|
+
};
|
|
1947
|
+
try {
|
|
1948
|
+
const response = await fetch(url, {
|
|
1949
|
+
method: 'POST',
|
|
1950
|
+
headers,
|
|
1951
|
+
body: JSON.stringify(payload),
|
|
1952
|
+
});
|
|
1953
|
+
if (!response.ok) {
|
|
1954
|
+
if (response.status === 400) {
|
|
1955
|
+
const errorText = await response.text().catch(() => 'Bad Request');
|
|
1956
|
+
throw new BadRequestError(`Failed to save attachment metadata: ${errorText}`, response.statusText);
|
|
1957
|
+
}
|
|
1958
|
+
if (response.status === 401) {
|
|
1959
|
+
throw new UnauthorizedError('Authentication failed. Please check your credentials.');
|
|
1960
|
+
}
|
|
1961
|
+
throw new ServerError(`Failed to save attachment metadata. Status: ${response.status}`, response.status, response.statusText);
|
|
1962
|
+
}
|
|
1963
|
+
return await response.json();
|
|
1964
|
+
}
|
|
1965
|
+
catch (error) {
|
|
1966
|
+
if (error instanceof BadRequestError ||
|
|
1967
|
+
error instanceof UnauthorizedError ||
|
|
1968
|
+
error instanceof ServerError) {
|
|
1969
|
+
throw error;
|
|
1970
|
+
}
|
|
1971
|
+
throw new UnexpectedError('Unexpected error while saving attachment metadata', error);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* Upload file to S3 for test execution step
|
|
1976
|
+
*
|
|
1977
|
+
* Uploads a file to S3 using the credentials from upload details.
|
|
1978
|
+
* Uses "testscriptresult" in the S3 key path.
|
|
1979
|
+
*
|
|
1980
|
+
* @param upload - Upload details from getUploadDetails
|
|
1981
|
+
* @param projectId - Jira project ID
|
|
1982
|
+
* @param testScriptResultId - Numeric test script result ID
|
|
1983
|
+
* @param userAccountId - Atlassian account ID
|
|
1984
|
+
* @param file - File to upload (Blob or ArrayBuffer)
|
|
1985
|
+
* @param fileName - Name of the file
|
|
1986
|
+
* @returns S3 upload information
|
|
1987
|
+
* @throws {UnexpectedError} If upload fails
|
|
1988
|
+
*/
|
|
1989
|
+
async uploadToS3ForTestExecutionStep(upload, projectId, testScriptResultId, userAccountId, file, fileName) {
|
|
1990
|
+
const bucketUrl = upload.bucketUrl;
|
|
1991
|
+
const keyPrefix = upload.keyPrefix;
|
|
1992
|
+
const credential = upload.credential;
|
|
1993
|
+
const date = upload.date;
|
|
1994
|
+
const policy = upload.policy;
|
|
1995
|
+
const signature = upload.signature;
|
|
1996
|
+
// Generate UUID for file
|
|
1997
|
+
const fileUuid = this.generateUUID();
|
|
1998
|
+
// Note: Test execution steps use "testscriptresult" in the S3 key path, and the keyPrefix includes "write/"
|
|
1999
|
+
const s3Key = `${keyPrefix}/project/${projectId}/testscriptresult/${testScriptResultId}/${fileUuid}`;
|
|
2000
|
+
// Determine MIME type
|
|
2001
|
+
const mimeType = this.getMimeType(fileName);
|
|
2002
|
+
// Get file size
|
|
2003
|
+
const fileSize = file instanceof Blob ? file.size : file.byteLength;
|
|
2004
|
+
// Create form data
|
|
2005
|
+
const formData = new FormData();
|
|
2006
|
+
formData.append('key', s3Key);
|
|
2007
|
+
formData.append('acl', 'private');
|
|
2008
|
+
formData.append('Policy', policy);
|
|
2009
|
+
formData.append('X-Amz-Credential', credential);
|
|
2010
|
+
formData.append('X-Amz-Algorithm', 'AWS4-HMAC-SHA256');
|
|
2011
|
+
formData.append('X-Amz-Date', date);
|
|
2012
|
+
formData.append('X-Amz-Signature', signature);
|
|
2013
|
+
formData.append('X-Amz-Meta-user-account-id', userAccountId);
|
|
2014
|
+
formData.append('X-Amz-Meta-name', fileName);
|
|
2015
|
+
formData.append('Content-Type', mimeType);
|
|
2016
|
+
// Add file
|
|
2017
|
+
if (file instanceof Blob) {
|
|
2018
|
+
formData.append('file', file, fileName);
|
|
2019
|
+
}
|
|
2020
|
+
else {
|
|
2021
|
+
formData.append('file', new Blob([file], { type: mimeType }), fileName);
|
|
2022
|
+
}
|
|
2023
|
+
try {
|
|
2024
|
+
const response = await fetch(bucketUrl, {
|
|
2025
|
+
method: 'POST',
|
|
2026
|
+
body: formData,
|
|
2027
|
+
});
|
|
2028
|
+
if (!response.ok && response.status !== 200 && response.status !== 201 && response.status !== 204) {
|
|
2029
|
+
const errorText = await response.text().catch(() => 'Upload failed');
|
|
2030
|
+
throw new UnexpectedError(`Failed to upload file to S3: ${errorText}`);
|
|
2031
|
+
}
|
|
2032
|
+
return {
|
|
2033
|
+
s3Key,
|
|
2034
|
+
fileName,
|
|
2035
|
+
mimeType,
|
|
2036
|
+
fileSize,
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
catch (error) {
|
|
2040
|
+
if (error instanceof UnexpectedError) {
|
|
2041
|
+
throw error;
|
|
2042
|
+
}
|
|
2043
|
+
throw new UnexpectedError('Unexpected error while uploading file to S3', error);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Save test execution step attachment metadata
|
|
2048
|
+
*
|
|
2049
|
+
* Saves metadata for an uploaded test execution step attachment.
|
|
2050
|
+
*
|
|
2051
|
+
* @param contextJwt - Jira Context JWT token
|
|
2052
|
+
* @param projectId - Jira project ID
|
|
2053
|
+
* @param testScriptResultId - Numeric test script result ID
|
|
2054
|
+
* @param userAccountId - Atlassian account ID
|
|
2055
|
+
* @param s3Key - S3 key from upload
|
|
2056
|
+
* @param fileName - File name
|
|
2057
|
+
* @param mimeType - MIME type
|
|
2058
|
+
* @param fileSize - File size in bytes
|
|
2059
|
+
* @returns Attachment metadata response
|
|
2060
|
+
* @throws {BadRequestError} If the request is invalid
|
|
2061
|
+
* @throws {UnauthorizedError} If authentication fails
|
|
2062
|
+
* @throws {ServerError} If the server returns an error
|
|
2063
|
+
*/
|
|
2064
|
+
async saveTestExecutionStepAttachmentMetadata(contextJwt, projectId, testScriptResultId, userAccountId, s3Key, fileName, mimeType, fileSize) {
|
|
2065
|
+
const url = 'https://app.tm4j.smartbear.com/backend/rest/tests/2.0/attachment/metadata';
|
|
2066
|
+
const headers = {
|
|
2067
|
+
'Authorization': `JWT ${contextJwt}`,
|
|
2068
|
+
'jira-project-id': String(projectId),
|
|
2069
|
+
'Content-Type': 'application/json',
|
|
2070
|
+
'Accept': 'application/json',
|
|
2071
|
+
};
|
|
2072
|
+
const createdOn = new Date().toISOString();
|
|
2073
|
+
const payload = {
|
|
2074
|
+
createdOn,
|
|
2075
|
+
mimeType,
|
|
2076
|
+
name: fileName,
|
|
2077
|
+
s3Key,
|
|
2078
|
+
size: fileSize,
|
|
2079
|
+
testScriptResultId,
|
|
2080
|
+
userAccountId,
|
|
2081
|
+
};
|
|
2082
|
+
try {
|
|
2083
|
+
const response = await fetch(url, {
|
|
2084
|
+
method: 'POST',
|
|
2085
|
+
headers,
|
|
2086
|
+
body: JSON.stringify(payload),
|
|
2087
|
+
});
|
|
2088
|
+
if (!response.ok) {
|
|
2089
|
+
if (response.status === 400) {
|
|
2090
|
+
const errorText = await response.text().catch(() => 'Bad Request');
|
|
2091
|
+
throw new BadRequestError(`Failed to save attachment metadata: ${errorText}`, response.statusText);
|
|
2092
|
+
}
|
|
2093
|
+
if (response.status === 401) {
|
|
2094
|
+
throw new UnauthorizedError('Authentication failed. Please check your credentials.');
|
|
2095
|
+
}
|
|
2096
|
+
throw new ServerError(`Failed to save attachment metadata. Status: ${response.status}`, response.status, response.statusText);
|
|
2097
|
+
}
|
|
2098
|
+
return await response.json();
|
|
2099
|
+
}
|
|
2100
|
+
catch (error) {
|
|
2101
|
+
if (error instanceof BadRequestError ||
|
|
2102
|
+
error instanceof UnauthorizedError ||
|
|
2103
|
+
error instanceof ServerError) {
|
|
2104
|
+
throw error;
|
|
2105
|
+
}
|
|
2106
|
+
throw new UnexpectedError('Unexpected error while saving attachment metadata', error);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
}
|