@rbaileysr/zephyr-managed-api 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,709 @@
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 PrivateVersionControl extends PrivateBase {
7
+ constructor(apiConnection) {
8
+ super(apiConnection);
9
+ }
10
+ /**
11
+ * Get issue links for a test case using private API (with version support)
12
+ *
13
+ * Retrieves all issue links associated with a test case, optionally for a specific version.
14
+ * The response format matches the public API `getTestCaseLinks().issues` format.
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 issue links 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
+ * @param request.version - Optional version number (absolute, 1-based: 1 = first version ever created, 2 = second version, etc. The highest number is the current/latest version). If not provided, uses the latest version.
24
+ * @returns Array of issue links matching the public API format
25
+ * @throws {BadRequestError} If the request is invalid
26
+ * @throws {UnauthorizedError} If authentication fails
27
+ * @throws {ForbiddenError} If the user doesn't have permission
28
+ * @throws {NotFoundError} If the test case is not found
29
+ * @throws {ServerError} If the server returns an error
30
+ * @throws {UnexpectedError} If test case ID cannot be looked up from key and Zephyr Connector is not available
31
+ */
32
+ async getTestCaseIssueLinks(credentials, request) {
33
+ // Get test case ID from key if we have API connection
34
+ let testCaseId;
35
+ if (this.testCaseGroup) {
36
+ try {
37
+ // If version is specified, get the versioned test case; otherwise get the current version
38
+ const testCase = request.version !== undefined && request.version > 0
39
+ ? await this.testCaseGroup.getTestCaseVersion({
40
+ testCaseKey: request.testCaseKey,
41
+ version: request.version,
42
+ })
43
+ : await this.testCaseGroup.getTestCase({ testCaseKey: request.testCaseKey });
44
+ if (!testCase) {
45
+ throw new NotFoundError(`Test case with key '${request.testCaseKey}'${request.version !== undefined && request.version > 0 ? ` version ${request.version}` : ''} not found.`);
46
+ }
47
+ testCaseId = testCase.id;
48
+ }
49
+ catch (error) {
50
+ if (error instanceof NotFoundError) {
51
+ throw new NotFoundError(`Test case with key '${request.testCaseKey}'${request.version !== undefined && request.version > 0 ? ` version ${request.version}` : ''} not found.`);
52
+ }
53
+ throw new UnexpectedError(`Failed to look up test case ID from key '${request.testCaseKey}'. Ensure Zephyr Connector is configured.`, error);
54
+ }
55
+ }
56
+ else {
57
+ 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.');
58
+ }
59
+ // Get Context JWT
60
+ const contextJwt = await this.getContextJwt(credentials);
61
+ const fields = 'id,issueId,url,urlDescription,type(id,index,name,i18nKey,systemKey),testCaseId,testRunId,testPlanId';
62
+ const url = `${this.privateApiBaseUrl}/testcase/${testCaseId}/tracelinks/issue?fields=${encodeURIComponent(fields)}`;
63
+ const headers = {
64
+ accept: 'application/json',
65
+ authorization: `JWT ${contextJwt}`,
66
+ 'jira-project-id': String(request.projectId),
67
+ };
68
+ try {
69
+ const response = await fetch(url, {
70
+ method: 'GET',
71
+ headers,
72
+ });
73
+ if (!response.ok) {
74
+ if (response.status === 400) {
75
+ throw new BadRequestError('Invalid request parameters for getting issue links.');
76
+ }
77
+ if (response.status === 401) {
78
+ throw new UnauthorizedError('Failed to authenticate. Please check your credentials.');
79
+ }
80
+ if (response.status === 403) {
81
+ throw new ForbiddenError('Insufficient permissions to get issue links.');
82
+ }
83
+ if (response.status === 404) {
84
+ throw new NotFoundError(`Test case with key '${request.testCaseKey}'${request.version !== undefined && request.version > 0 ? ` version ${request.version}` : ''} not found.`);
85
+ }
86
+ throw new ServerError(`Failed to get test case issue links. Status: ${response.status}`, response.status, response.statusText);
87
+ }
88
+ const privateLinks = (await response.json());
89
+ // Transform to public API format
90
+ const issueLinks = privateLinks.map((link) => {
91
+ // Build self URL (approximate format based on public API pattern)
92
+ const self = `/testcases/${request.testCaseKey}/links/issues/${link.id}`;
93
+ return {
94
+ id: link.id,
95
+ self,
96
+ issueId: Number(link.issueId),
97
+ target: `/issues/${link.issueId}`,
98
+ type: link.type.systemKey,
99
+ };
100
+ });
101
+ return issueLinks;
102
+ }
103
+ catch (error) {
104
+ if (error instanceof BadRequestError ||
105
+ error instanceof UnauthorizedError ||
106
+ error instanceof ForbiddenError ||
107
+ error instanceof NotFoundError ||
108
+ error instanceof ServerError ||
109
+ error instanceof UnexpectedError) {
110
+ throw error;
111
+ }
112
+ throw new UnexpectedError('Unexpected error while getting test case issue links', error);
113
+ }
114
+ }
115
+ /**
116
+ * Get web links for a test case using private API (with version support)
117
+ *
118
+ * Retrieves all web links associated with a test case, optionally for a specific version.
119
+ * The response format matches the public API `getTestCaseLinks().webLinks` format.
120
+ *
121
+ * ⚠️ WARNING: This uses a private Zephyr API endpoint that is not officially supported.
122
+ * The endpoint may change or be removed at any time without notice.
123
+ *
124
+ * @param credentials - Private API credentials
125
+ * @param request - Get web links request
126
+ * @param request.testCaseKey - Test case key (e.g., 'PROJ-T1'). The numeric ID will be looked up automatically if Zephyr Connector is available.
127
+ * @param request.projectId - Jira project ID (numeric, not the project key)
128
+ * @param request.version - Optional version number (absolute, 1-based: 1 = first version ever created, 2 = second version, etc. The highest number is the current/latest version). If not provided, uses the latest version.
129
+ * @returns Array of web links matching the public API format
130
+ * @throws {BadRequestError} If the request is invalid
131
+ * @throws {UnauthorizedError} If authentication fails
132
+ * @throws {ForbiddenError} If the user doesn't have permission
133
+ * @throws {NotFoundError} If the test case is not found
134
+ * @throws {ServerError} If the server returns an error
135
+ * @throws {UnexpectedError} If test case ID cannot be looked up from key and Zephyr Connector is not available
136
+ */
137
+ async getTestCaseWebLinks(credentials, request) {
138
+ // Get test case ID from key if we have API connection
139
+ let testCaseId;
140
+ if (this.testCaseGroup) {
141
+ try {
142
+ // If version is specified, get the versioned test case; otherwise get the current version
143
+ const testCase = request.version !== undefined && request.version > 0
144
+ ? await this.testCaseGroup.getTestCaseVersion({
145
+ testCaseKey: request.testCaseKey,
146
+ version: request.version,
147
+ })
148
+ : await this.testCaseGroup.getTestCase({ testCaseKey: request.testCaseKey });
149
+ if (!testCase) {
150
+ throw new NotFoundError(`Test case with key '${request.testCaseKey}'${request.version !== undefined && request.version > 0 ? ` version ${request.version}` : ''} not found.`);
151
+ }
152
+ testCaseId = testCase.id;
153
+ }
154
+ catch (error) {
155
+ if (error instanceof NotFoundError) {
156
+ throw new NotFoundError(`Test case with key '${request.testCaseKey}'${request.version !== undefined && request.version > 0 ? ` version ${request.version}` : ''} not found.`);
157
+ }
158
+ throw new UnexpectedError(`Failed to look up test case ID from key '${request.testCaseKey}'. Ensure Zephyr Connector is configured.`, error);
159
+ }
160
+ }
161
+ else {
162
+ 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.');
163
+ }
164
+ // Get Context JWT
165
+ const contextJwt = await this.getContextJwt(credentials);
166
+ const fields = 'id,url,urlDescription,type(id,index,name,i18nKey,systemKey),testCaseId,testRunId,testPlanId';
167
+ const url = `${this.privateApiBaseUrl}/testcase/${testCaseId}/tracelinks/weblink?fields=${encodeURIComponent(fields)}`;
168
+ const headers = {
169
+ accept: 'application/json',
170
+ authorization: `JWT ${contextJwt}`,
171
+ 'jira-project-id': String(request.projectId),
172
+ };
173
+ try {
174
+ const response = await fetch(url, {
175
+ method: 'GET',
176
+ headers,
177
+ });
178
+ if (!response.ok) {
179
+ if (response.status === 400) {
180
+ throw new BadRequestError('Invalid request parameters for getting web links.');
181
+ }
182
+ if (response.status === 401) {
183
+ throw new UnauthorizedError('Failed to authenticate. Please check your credentials.');
184
+ }
185
+ if (response.status === 403) {
186
+ throw new ForbiddenError('Insufficient permissions to get web links.');
187
+ }
188
+ if (response.status === 404) {
189
+ throw new NotFoundError(`Test case with key '${request.testCaseKey}'${request.version !== undefined && request.version > 0 ? ` version ${request.version}` : ''} not found.`);
190
+ }
191
+ throw new ServerError(`Failed to get test case web links. Status: ${response.status}`, response.status, response.statusText);
192
+ }
193
+ const privateLinks = (await response.json());
194
+ // Transform to public API format
195
+ const webLinks = privateLinks.map((link) => {
196
+ // Build self URL (approximate format based on public API pattern)
197
+ const self = `/testcases/${request.testCaseKey}/links/weblinks/${link.id}`;
198
+ return {
199
+ id: link.id,
200
+ self,
201
+ url: link.url,
202
+ description: link.urlDescription || undefined,
203
+ type: link.type.systemKey,
204
+ };
205
+ });
206
+ return webLinks;
207
+ }
208
+ catch (error) {
209
+ if (error instanceof BadRequestError ||
210
+ error instanceof UnauthorizedError ||
211
+ error instanceof ForbiddenError ||
212
+ error instanceof NotFoundError ||
213
+ error instanceof ServerError ||
214
+ error instanceof UnexpectedError) {
215
+ throw error;
216
+ }
217
+ throw new UnexpectedError('Unexpected error while getting test case web links', error);
218
+ }
219
+ }
220
+ /**
221
+ * Get test script for a test case using private API (with version support)
222
+ *
223
+ * Retrieves the test script associated with a test case, optionally for a specific version.
224
+ * The response format matches the public API `getTestCaseTestScript()` format.
225
+ *
226
+ * ⚠️ WARNING: This uses a private Zephyr API endpoint that is not officially supported.
227
+ * The endpoint may change or be removed at any time without notice.
228
+ *
229
+ * @param credentials - Private API credentials
230
+ * @param request - Get test script request
231
+ * @param request.testCaseKey - Test case key (e.g., 'PROJ-T1')
232
+ * @param request.projectId - Jira project ID (numeric, not the project key)
233
+ * @param request.version - Optional version number (absolute, 1-based: 1 = first version ever created, 2 = second version, etc. The highest number is the current/latest version). If not provided, uses the latest version.
234
+ * @returns Test script matching the public API format, or null if no test script exists
235
+ * @throws {BadRequestError} If the request is invalid
236
+ * @throws {UnauthorizedError} If authentication fails
237
+ * @throws {ForbiddenError} If the user doesn't have permission
238
+ * @throws {NotFoundError} If the test case is not found
239
+ * @throws {ServerError} If the server returns an error
240
+ * @throws {UnexpectedError} If test case cannot be retrieved
241
+ */
242
+ async getTestCaseTestScript(credentials, request) {
243
+ // Get test case ID from key if we have API connection (for version support)
244
+ let testCaseId = request.testCaseKey;
245
+ if (this.testCaseGroup) {
246
+ try {
247
+ // If version is specified, get the versioned test case; otherwise get the current version
248
+ const testCase = request.version !== undefined && request.version > 0
249
+ ? await this.testCaseGroup.getTestCaseVersion({
250
+ testCaseKey: request.testCaseKey,
251
+ version: request.version,
252
+ })
253
+ : await this.testCaseGroup.getTestCase({ testCaseKey: request.testCaseKey });
254
+ if (!testCase) {
255
+ throw new NotFoundError(`Test case with key '${request.testCaseKey}'${request.version !== undefined && request.version > 0 ? ` version ${request.version}` : ''} not found.`);
256
+ }
257
+ // Use the ID for the private API call (works with both key and ID)
258
+ testCaseId = testCase.id;
259
+ }
260
+ catch (error) {
261
+ if (error instanceof NotFoundError) {
262
+ throw new NotFoundError(`Test case with key '${request.testCaseKey}'${request.version !== undefined && request.version > 0 ? ` version ${request.version}` : ''} not found.`);
263
+ }
264
+ throw new UnexpectedError(`Failed to look up test case ID from key '${request.testCaseKey}'. Ensure Zephyr Connector is configured.`, error);
265
+ }
266
+ }
267
+ // Get Context JWT
268
+ const contextJwt = await this.getContextJwt(credentials);
269
+ // Build fields query to get test script
270
+ 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))))';
271
+ const url = `${this.privateApiBaseUrl}/testcase/${testCaseId}?fields=${encodeURIComponent(fields)}`;
272
+ const headers = {
273
+ accept: 'application/json',
274
+ authorization: `JWT ${contextJwt}`,
275
+ 'jira-project-id': String(request.projectId),
276
+ };
277
+ try {
278
+ const response = await fetch(url, {
279
+ method: 'GET',
280
+ headers,
281
+ });
282
+ if (!response.ok) {
283
+ if (response.status === 400) {
284
+ throw new BadRequestError('Invalid request parameters for getting test script.');
285
+ }
286
+ if (response.status === 401) {
287
+ throw new UnauthorizedError('Failed to authenticate. Please check your credentials.');
288
+ }
289
+ if (response.status === 403) {
290
+ throw new ForbiddenError('Insufficient permissions to get test script.');
291
+ }
292
+ if (response.status === 404) {
293
+ throw new NotFoundError(`Test case with key '${request.testCaseKey}'${request.version !== undefined && request.version > 0 ? ` version ${request.version}` : ''} not found.`);
294
+ }
295
+ throw new ServerError(`Failed to get test case test script. Status: ${response.status}`, response.status, response.statusText);
296
+ }
297
+ const testCase = (await response.json());
298
+ // If no test script exists, return null
299
+ if (!testCase.testScript) {
300
+ return null;
301
+ }
302
+ // If it's a step-by-step script (has steps), it's not a plain/BDD script, return null
303
+ if (testCase.testScript.stepByStepScript) {
304
+ return null;
305
+ }
306
+ // If it has text, it's a plain text script
307
+ if (testCase.testScript.text) {
308
+ return {
309
+ id: testCase.testScript.id,
310
+ type: 'plain',
311
+ text: testCase.testScript.text,
312
+ };
313
+ }
314
+ // No script found
315
+ return null;
316
+ }
317
+ catch (error) {
318
+ if (error instanceof BadRequestError ||
319
+ error instanceof UnauthorizedError ||
320
+ error instanceof ForbiddenError ||
321
+ error instanceof NotFoundError ||
322
+ error instanceof ServerError ||
323
+ error instanceof UnexpectedError) {
324
+ throw error;
325
+ }
326
+ throw new UnexpectedError('Unexpected error while getting test case test script', error);
327
+ }
328
+ }
329
+ /**
330
+ * Get test steps for a test case using private API (with version support)
331
+ *
332
+ * Retrieves the test steps associated with a test case, optionally for a specific version.
333
+ * The response format matches the public API `getTestCaseTestSteps()` format.
334
+ *
335
+ * ⚠️ WARNING: This uses a private Zephyr API endpoint that is not officially supported.
336
+ * The endpoint may change or be removed at any time without notice.
337
+ *
338
+ * @param credentials - Private API credentials
339
+ * @param request - Get test steps request
340
+ * @param request.testCaseKey - Test case key (e.g., 'PROJ-T1')
341
+ * @param request.projectId - Jira project ID (numeric, not the project key)
342
+ * @param request.version - Optional version number (absolute, 1-based: 1 = first version ever created, 2 = second version, etc. The highest number is the current/latest version). If not provided, uses the latest version.
343
+ * @returns Test steps list matching the public API format
344
+ * @throws {BadRequestError} If the request is invalid
345
+ * @throws {UnauthorizedError} If authentication fails
346
+ * @throws {ForbiddenError} If the user doesn't have permission
347
+ * @throws {NotFoundError} If the test case is not found
348
+ * @throws {ServerError} If the server returns an error
349
+ * @throws {UnexpectedError} If test case cannot be retrieved
350
+ */
351
+ async getTestCaseTestSteps(credentials, request) {
352
+ // Get test case ID from key if we have API connection (for version support)
353
+ let testCaseId = request.testCaseKey;
354
+ if (this.testCaseGroup) {
355
+ try {
356
+ // If version is specified, get the versioned test case; otherwise get the current version
357
+ const testCase = request.version !== undefined && request.version > 0
358
+ ? await this.testCaseGroup.getTestCaseVersion({
359
+ testCaseKey: request.testCaseKey,
360
+ version: request.version,
361
+ })
362
+ : await this.testCaseGroup.getTestCase({ testCaseKey: request.testCaseKey });
363
+ if (!testCase) {
364
+ throw new NotFoundError(`Test case with key '${request.testCaseKey}'${request.version !== undefined && request.version > 0 ? ` version ${request.version}` : ''} not found.`);
365
+ }
366
+ // Use the ID for the private API call (works with both key and ID)
367
+ testCaseId = testCase.id;
368
+ }
369
+ catch (error) {
370
+ if (error instanceof NotFoundError) {
371
+ throw new NotFoundError(`Test case with key '${request.testCaseKey}'${request.version !== undefined && request.version > 0 ? ` version ${request.version}` : ''} not found.`);
372
+ }
373
+ throw new UnexpectedError(`Failed to look up test case ID from key '${request.testCaseKey}'. Ensure Zephyr Connector is configured.`, error);
374
+ }
375
+ }
376
+ // Get Context JWT
377
+ const contextJwt = await this.getContextJwt(credentials);
378
+ // Build fields query to get test steps
379
+ 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))))';
380
+ const url = `${this.privateApiBaseUrl}/testcase/${testCaseId}?fields=${encodeURIComponent(fields)}`;
381
+ const headers = {
382
+ accept: 'application/json',
383
+ authorization: `JWT ${contextJwt}`,
384
+ 'jira-project-id': String(request.projectId),
385
+ };
386
+ try {
387
+ const response = await fetch(url, {
388
+ method: 'GET',
389
+ headers,
390
+ });
391
+ if (!response.ok) {
392
+ if (response.status === 400) {
393
+ throw new BadRequestError('Invalid request parameters for getting test steps.');
394
+ }
395
+ if (response.status === 401) {
396
+ throw new UnauthorizedError('Failed to authenticate. Please check your credentials.');
397
+ }
398
+ if (response.status === 403) {
399
+ throw new ForbiddenError('Insufficient permissions to get test steps.');
400
+ }
401
+ if (response.status === 404) {
402
+ throw new NotFoundError(`Test case with key '${request.testCaseKey}'${request.version !== undefined && request.version > 0 ? ` version ${request.version}` : ''} not found.`);
403
+ }
404
+ throw new ServerError(`Failed to get test case test steps. Status: ${response.status}`, response.status, response.statusText);
405
+ }
406
+ const testCase = (await response.json());
407
+ // If no test script or no step-by-step script exists, return empty list
408
+ if (!testCase.testScript || !testCase.testScript.stepByStepScript || !testCase.testScript.stepByStepScript.steps) {
409
+ return {
410
+ values: [],
411
+ total: 0,
412
+ maxResults: 0,
413
+ startAt: 0,
414
+ isLast: true,
415
+ next: null,
416
+ };
417
+ }
418
+ const privateSteps = testCase.testScript.stepByStepScript.steps;
419
+ // Transform to public API format
420
+ const testSteps = privateSteps.map((step) => {
421
+ const inline = {
422
+ description: step.description || '',
423
+ testData: step.testData || undefined,
424
+ expectedResult: step.expectedResult || '',
425
+ reflectRef: step.reflectRef || undefined,
426
+ // Note: customFieldValues transformation would require more complex logic
427
+ // For now, we'll skip it as it's not in the public API TestStepInline type
428
+ };
429
+ return {
430
+ inline,
431
+ };
432
+ });
433
+ return {
434
+ values: testSteps,
435
+ total: testSteps.length,
436
+ maxResults: testSteps.length,
437
+ startAt: 0,
438
+ isLast: true,
439
+ next: null,
440
+ };
441
+ }
442
+ catch (error) {
443
+ if (error instanceof BadRequestError ||
444
+ error instanceof UnauthorizedError ||
445
+ error instanceof ForbiddenError ||
446
+ error instanceof NotFoundError ||
447
+ error instanceof ServerError ||
448
+ error instanceof UnexpectedError) {
449
+ throw error;
450
+ }
451
+ throw new UnexpectedError('Unexpected error while getting test case test steps', error);
452
+ }
453
+ }
454
+ /**
455
+ * Map LinkType to numeric typeId for private API
456
+ */
457
+ mapLinkTypeToTypeId(type) {
458
+ switch (type) {
459
+ case 'COVERAGE':
460
+ return 1;
461
+ case 'BLOCKS':
462
+ return 2;
463
+ case 'RELATED':
464
+ return 3;
465
+ default:
466
+ return 1; // Default to COVERAGE
467
+ }
468
+ }
469
+ /**
470
+ * Map numeric typeId to LinkType for private API
471
+ */
472
+ mapTypeIdToLinkType(typeId) {
473
+ switch (typeId) {
474
+ case 1:
475
+ return 'COVERAGE';
476
+ case 2:
477
+ return 'BLOCKS';
478
+ case 3:
479
+ return 'RELATED';
480
+ default:
481
+ return 'COVERAGE'; // Default to COVERAGE
482
+ }
483
+ }
484
+ /**
485
+ * Get issue links for a test execution step using private API
486
+ *
487
+ * Retrieves all issue links associated with a specific test execution step. This is useful for
488
+ * migration scenarios where you need to extract issue links from a source test execution and
489
+ * recreate them in a target instance.
490
+ *
491
+ * ⚠️ WARNING: This uses a private Zephyr API endpoint that is not officially supported.
492
+ * The endpoint may change or be removed at any time without notice.
493
+ *
494
+ * @param credentials - Private API credentials
495
+ * @param request - Get issue links request
496
+ * @param request.testExecutionKey - Test execution key (e.g., 'PROJ-E1')
497
+ * @param request.stepIndex - Zero-based index of the test execution step
498
+ * @param request.projectId - Jira project ID (numeric, not the project key)
499
+ * @returns List of issue links for the test execution step
500
+ * @throws {BadRequestError} If the request is invalid
501
+ * @throws {UnauthorizedError} If authentication fails
502
+ * @throws {ForbiddenError} If the user doesn't have permission
503
+ * @throws {NotFoundError} If the test execution or step is not found
504
+ * @throws {ServerError} If the server returns an error
505
+ * @throws {UnexpectedError} If an unexpected error occurs
506
+ */
507
+ async getTestExecutionStepIssueLinks(credentials, request) {
508
+ // Get Context JWT
509
+ const contextJwt = await this.getContextJwt(credentials);
510
+ // Fetch test execution with traceLinks in testScriptResults
511
+ const url = `${this.privateApiBaseUrl}/testresult/${request.testExecutionKey}?fields=testScriptResults(id,index,traceLinks(id,type(id,systemKey),issueId)),attachments&itemId=${request.testExecutionKey}`;
512
+ const headers = {
513
+ accept: 'application/json',
514
+ authorization: `JWT ${contextJwt}`,
515
+ 'jira-project-id': String(request.projectId),
516
+ };
517
+ try {
518
+ const response = await fetch(url, {
519
+ method: 'GET',
520
+ headers,
521
+ });
522
+ if (!response.ok) {
523
+ if (response.status === 404) {
524
+ throw new NotFoundError(`Test execution with key '${request.testExecutionKey}' not found.`);
525
+ }
526
+ if (response.status === 401) {
527
+ throw new UnauthorizedError('Failed to authenticate. Please check your credentials.');
528
+ }
529
+ if (response.status === 403) {
530
+ throw new ForbiddenError('Insufficient permissions to get test execution step issue links.');
531
+ }
532
+ throw new ServerError(`Failed to get test execution step issue links. Status: ${response.status}`, response.status, response.statusText);
533
+ }
534
+ const executionData = (await response.json());
535
+ const steps = executionData.testScriptResults || [];
536
+ if (request.stepIndex < 0 || request.stepIndex >= steps.length) {
537
+ throw new NotFoundError(`Test execution step at index ${request.stepIndex} not found in test execution '${request.testExecutionKey}'. Test execution has ${steps.length} step(s).`);
538
+ }
539
+ const step = steps[request.stepIndex];
540
+ const traceLinks = step.traceLinks || [];
541
+ // Transform private API response to migration-friendly format
542
+ return traceLinks.map((link) => ({
543
+ id: link.id,
544
+ issueId: link.issueId,
545
+ type: this.mapSystemKeyToLinkType(link.type.systemKey),
546
+ }));
547
+ }
548
+ catch (error) {
549
+ if (error instanceof NotFoundError ||
550
+ error instanceof UnauthorizedError ||
551
+ error instanceof ForbiddenError ||
552
+ error instanceof ServerError ||
553
+ error instanceof UnexpectedError) {
554
+ throw error;
555
+ }
556
+ throw new UnexpectedError('Unexpected error while getting test execution step issue links', error);
557
+ }
558
+ }
559
+ /**
560
+ * Map system key to LinkType
561
+ */
562
+ mapSystemKeyToLinkType(systemKey) {
563
+ switch (systemKey.toUpperCase()) {
564
+ case 'COVERAGE':
565
+ return 'COVERAGE';
566
+ case 'BLOCKS':
567
+ return 'BLOCKS';
568
+ case 'RELATED':
569
+ return 'RELATED';
570
+ default:
571
+ return 'COVERAGE'; // Default to COVERAGE
572
+ }
573
+ }
574
+ /**
575
+ * Create an issue link for a test execution step using private API
576
+ *
577
+ * Creates a link between a test execution step and a Jira issue. The link type can be COVERAGE, BLOCKS, or RELATED.
578
+ * This functionality is not available in the public API.
579
+ *
580
+ * ⚠️ WARNING: This uses a private Zephyr API endpoint that is not officially supported.
581
+ * The endpoint may change or be removed at any time without notice.
582
+ *
583
+ * @param credentials - Private API credentials
584
+ * @param request - Create issue link request
585
+ * @param request.testExecutionKey - Test execution key (e.g., 'PROJ-E1')
586
+ * @param request.stepIndex - Zero-based index of the test execution step
587
+ * @param request.issueId - Jira issue ID (numeric)
588
+ * @param request.type - Link type: 'COVERAGE', 'BLOCKS', or 'RELATED' (optional, defaults to 'COVERAGE')
589
+ * @param request.projectId - Jira project ID (numeric, not the project key)
590
+ * @returns Created link response with ID
591
+ * @throws {BadRequestError} If the request is invalid
592
+ * @throws {UnauthorizedError} If authentication fails
593
+ * @throws {ForbiddenError} If the user doesn't have permission
594
+ * @throws {NotFoundError} If the test execution or step is not found
595
+ * @throws {ServerError} If the server returns an error
596
+ * @throws {UnexpectedError} If test execution ID cannot be looked up
597
+ */
598
+ async createTestExecutionStepIssueLink(credentials, request) {
599
+ // Get Context JWT
600
+ const contextJwt = await this.getContextJwt(credentials);
601
+ // Step 1: Get test execution to find testResultId (numeric ID)
602
+ let testResultId;
603
+ if (this.testExecutionGroup) {
604
+ try {
605
+ const testExecution = await this.testExecutionGroup.getTestExecution({
606
+ testExecutionIdOrKey: request.testExecutionKey,
607
+ });
608
+ testResultId = testExecution.id;
609
+ }
610
+ catch (error) {
611
+ if (error instanceof NotFoundError) {
612
+ throw new NotFoundError(`Test execution with key '${request.testExecutionKey}' not found.`);
613
+ }
614
+ throw new UnexpectedError(`Failed to look up test execution ID from key '${request.testExecutionKey}'. Ensure Zephyr Connector is configured.`, error);
615
+ }
616
+ }
617
+ else {
618
+ 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.');
619
+ }
620
+ // Step 2: Get test execution steps to find testScriptResultId from stepIndex
621
+ const url = `${this.privateApiBaseUrl}/testresult/${request.testExecutionKey}?fields=testScriptResults(id,index),attachments&itemId=${request.testExecutionKey}`;
622
+ const headers = {
623
+ accept: 'application/json',
624
+ authorization: `JWT ${contextJwt}`,
625
+ 'jira-project-id': String(request.projectId),
626
+ };
627
+ let testScriptResultId;
628
+ try {
629
+ const response = await fetch(url, {
630
+ method: 'GET',
631
+ headers,
632
+ });
633
+ if (!response.ok) {
634
+ if (response.status === 404) {
635
+ throw new NotFoundError(`Test execution with key '${request.testExecutionKey}' not found.`);
636
+ }
637
+ throw new ServerError(`Failed to get test execution steps. Status: ${response.status}`, response.status, response.statusText);
638
+ }
639
+ const executionData = (await response.json());
640
+ const steps = executionData.testScriptResults || [];
641
+ if (request.stepIndex < 0 || request.stepIndex >= steps.length) {
642
+ throw new NotFoundError(`Test execution step at index ${request.stepIndex} not found in test execution '${request.testExecutionKey}'. Test execution has ${steps.length} step(s).`);
643
+ }
644
+ testScriptResultId = steps[request.stepIndex].id;
645
+ }
646
+ catch (error) {
647
+ if (error instanceof NotFoundError ||
648
+ error instanceof ServerError ||
649
+ error instanceof UnexpectedError) {
650
+ throw error;
651
+ }
652
+ throw new UnexpectedError('Unexpected error while getting test execution steps', error);
653
+ }
654
+ // Step 3: Create the issue link using bulk endpoint
655
+ const typeId = this.mapLinkTypeToTypeId(request.type);
656
+ const bulkUrl = `${this.privateApiBaseUrl}/tracelink/testresult/bulk/create`;
657
+ const bulkHeaders = {
658
+ 'Content-Type': 'application/json',
659
+ authorization: `JWT ${contextJwt}`,
660
+ 'jira-project-id': String(request.projectId),
661
+ };
662
+ const requestBody = [
663
+ {
664
+ testResultId,
665
+ issueId: String(request.issueId),
666
+ testScriptResultId,
667
+ typeId,
668
+ },
669
+ ];
670
+ try {
671
+ const response = await fetch(bulkUrl, {
672
+ method: 'POST',
673
+ headers: bulkHeaders,
674
+ body: JSON.stringify(requestBody),
675
+ });
676
+ if (!response.ok) {
677
+ if (response.status === 400) {
678
+ throw new BadRequestError('Invalid request parameters for creating test execution step issue link.');
679
+ }
680
+ if (response.status === 401) {
681
+ throw new UnauthorizedError('Failed to authenticate. Please check your credentials.');
682
+ }
683
+ if (response.status === 403) {
684
+ throw new ForbiddenError('Insufficient permissions to create test execution step issue link.');
685
+ }
686
+ if (response.status === 404) {
687
+ throw new NotFoundError(`Test execution '${request.testExecutionKey}' or step at index ${request.stepIndex} not found.`);
688
+ }
689
+ throw new ServerError(`Failed to create test execution step issue link. Status: ${response.status}`, response.status, response.statusText);
690
+ }
691
+ const results = (await response.json());
692
+ if (results.length === 0) {
693
+ throw new UnexpectedError('No link ID returned from API response.');
694
+ }
695
+ return results[0];
696
+ }
697
+ catch (error) {
698
+ if (error instanceof BadRequestError ||
699
+ error instanceof UnauthorizedError ||
700
+ error instanceof ForbiddenError ||
701
+ error instanceof NotFoundError ||
702
+ error instanceof ServerError ||
703
+ error instanceof UnexpectedError) {
704
+ throw error;
705
+ }
706
+ throw new UnexpectedError('Unexpected error while creating test execution step issue link', error);
707
+ }
708
+ }
709
+ }