@rbaileysr/zephyr-managed-api 1.0.0
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 +618 -0
- package/dist/error-strategy.d.ts +69 -0
- package/dist/error-strategy.d.ts.map +1 -0
- package/dist/error-strategy.js +125 -0
- package/dist/groups/All.d.ts +90 -0
- package/dist/groups/All.d.ts.map +1 -0
- package/dist/groups/All.js +236 -0
- package/dist/groups/Automation.d.ts +75 -0
- package/dist/groups/Automation.d.ts.map +1 -0
- package/dist/groups/Automation.js +133 -0
- package/dist/groups/Environment.d.ts +73 -0
- package/dist/groups/Environment.d.ts.map +1 -0
- package/dist/groups/Environment.js +93 -0
- package/dist/groups/Folder.d.ts +55 -0
- package/dist/groups/Folder.d.ts.map +1 -0
- package/dist/groups/Folder.js +68 -0
- package/dist/groups/IssueLink.d.ts +59 -0
- package/dist/groups/IssueLink.d.ts.map +1 -0
- package/dist/groups/IssueLink.js +70 -0
- package/dist/groups/Link.d.ts +23 -0
- package/dist/groups/Link.d.ts.map +1 -0
- package/dist/groups/Link.js +34 -0
- package/dist/groups/Priority.d.ts +77 -0
- package/dist/groups/Priority.d.ts.map +1 -0
- package/dist/groups/Priority.js +97 -0
- package/dist/groups/Project.d.ts +36 -0
- package/dist/groups/Project.d.ts.map +1 -0
- package/dist/groups/Project.js +42 -0
- package/dist/groups/Status.d.ts +82 -0
- package/dist/groups/Status.d.ts.map +1 -0
- package/dist/groups/Status.js +102 -0
- package/dist/groups/TestCase.d.ts +254 -0
- package/dist/groups/TestCase.d.ts.map +1 -0
- package/dist/groups/TestCase.js +327 -0
- package/dist/groups/TestCycle.d.ts +127 -0
- package/dist/groups/TestCycle.d.ts.map +1 -0
- package/dist/groups/TestCycle.js +166 -0
- package/dist/groups/TestExecution.d.ts +176 -0
- package/dist/groups/TestExecution.d.ts.map +1 -0
- package/dist/groups/TestExecution.js +239 -0
- package/dist/groups/TestPlan.d.ts +103 -0
- package/dist/groups/TestPlan.d.ts.map +1 -0
- package/dist/groups/TestPlan.js +137 -0
- package/dist/index.d.ts +119 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +124 -0
- package/dist/types.d.ts +1353 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/utils-api-call.d.ts +22 -0
- package/dist/utils-api-call.d.ts.map +1 -0
- package/dist/utils-api-call.js +80 -0
- package/dist/utils.d.ts +144 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +432 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
# Zephyr Managed API
|
|
2
|
+
|
|
3
|
+
A comprehensive Managed API wrapper for Zephyr Cloud REST API v2, providing type-safe, hierarchical access to all Zephyr API endpoints.
|
|
4
|
+
|
|
5
|
+
> **⚠️ Important: ScriptRunner Connect Runtime Only**
|
|
6
|
+
>
|
|
7
|
+
> This package is specifically designed for **ScriptRunner Connect's custom runtime** and will **NOT work in standard Node.js projects**. It uses web standards-based APIs (fetch, etc.) and ES modules, making it compatible with ScriptRunner Connect's runtime environment.
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
This Managed API wrapper provides a clean, type-safe interface to interact with the Zephyr Cloud API. It follows the same hierarchical pattern as other ScriptRunner Connect Managed APIs, making it easy to use and consistent with the platform's conventions.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Type-Safe**: Full TypeScript support with IntelliSense
|
|
16
|
+
- **Hierarchical Structure**: Organized by resource type (TestCase, TestCycle, TestPlan, etc.)
|
|
17
|
+
- **Comprehensive Coverage**: Supports all Zephyr Cloud REST API v2 endpoints
|
|
18
|
+
- **Error Handling**: Built-in error parsing and handling using Commons Core error types
|
|
19
|
+
- **Pagination Support**: Both offset-based and cursor-based pagination
|
|
20
|
+
- **Region Support**: Configurable US and EU regions
|
|
21
|
+
- **Flexible Authentication**: Supports OAuth tokens and ScriptRunner Connect API Connections
|
|
22
|
+
- **Custom Fields**: Full support for dynamic custom fields
|
|
23
|
+
- **Web App Compatible**: Works seamlessly in ScriptRunner Connect web app
|
|
24
|
+
- **JSDoc Documentation**: All methods include descriptions and links to official API documentation
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
### NPM Package
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @rbaileysr/zephyr-managed-api
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Important:** After installing via NPM, you must also add the package through ScriptRunner Connect's Package Manager in the web UI. The package will then be available in your workspace.
|
|
35
|
+
|
|
36
|
+
### ScriptRunner Connect Workspace Setup
|
|
37
|
+
|
|
38
|
+
1. **Add Package via Web UI:**
|
|
39
|
+
- Go to Package Manager in ScriptRunner Connect web UI
|
|
40
|
+
- Click "Add Package"
|
|
41
|
+
- Enter: `@rbaileysr/zephyr-managed-api`
|
|
42
|
+
- Click Add/Save
|
|
43
|
+
|
|
44
|
+
2. **Sync Workspace Files:**
|
|
45
|
+
- Download latest workspace files from SFTP server
|
|
46
|
+
- Or use `SFTP: Sync Remote → Local` if using VS Code + SFTP
|
|
47
|
+
|
|
48
|
+
3. **Install Dependencies:**
|
|
49
|
+
```bash
|
|
50
|
+
npm install
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
4. **Use in Scripts:**
|
|
54
|
+
```typescript
|
|
55
|
+
import { createZephyrApi } from '@rbaileysr/zephyr-managed-api';
|
|
56
|
+
import ZephyrApiConnection from '../api/zephyr';
|
|
57
|
+
|
|
58
|
+
// Using API Connection (recommended for ScriptRunner Connect)
|
|
59
|
+
const Zephyr = createZephyrApi(ZephyrApiConnection, 'us');
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
### Basic Examples
|
|
65
|
+
|
|
66
|
+
#### Using API Connection (ScriptRunner Connect)
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { createZephyrApi } from '@rbaileysr/zephyr-managed-api';
|
|
70
|
+
import ZephyrApiConnection from '../api/zephyr';
|
|
71
|
+
|
|
72
|
+
const Zephyr = createZephyrApi(ZephyrApiConnection, 'us');
|
|
73
|
+
|
|
74
|
+
// Get a test case
|
|
75
|
+
const testCase = await Zephyr.TestCase.getTestCase({ testCaseKey: 'PROJ-T1' });
|
|
76
|
+
console.log(`Test case: ${testCase.name}`);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### Using OAuth Token
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { createZephyrApi } from '@rbaileysr/zephyr-managed-api';
|
|
83
|
+
|
|
84
|
+
const oauthToken = 'your-oauth-token-here';
|
|
85
|
+
const Zephyr = createZephyrApi(oauthToken, 'us');
|
|
86
|
+
|
|
87
|
+
// List test cases
|
|
88
|
+
const testCases = await Zephyr.TestCase.listTestCases({
|
|
89
|
+
projectKey: 'PROJ',
|
|
90
|
+
maxResults: 10,
|
|
91
|
+
startAt: 0
|
|
92
|
+
});
|
|
93
|
+
console.log(`Found ${testCases.values.length} test cases`);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Create a Test Case
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const testCase = await Zephyr.TestCase.createTestCase({
|
|
100
|
+
body: {
|
|
101
|
+
projectKey: 'PROJ',
|
|
102
|
+
name: 'Login Test Case',
|
|
103
|
+
objective: 'Verify user can login successfully',
|
|
104
|
+
precondition: 'User account exists',
|
|
105
|
+
priorityName: 'High',
|
|
106
|
+
statusName: 'Draft',
|
|
107
|
+
labels: ['automated', 'regression'],
|
|
108
|
+
customFields: {
|
|
109
|
+
'Build Number': '2024.1',
|
|
110
|
+
'Release Date': '2024-01-15'
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
console.log(`Created test case: ${testCase.key}`);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### Create a Test Cycle
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const testCycle = await Zephyr.TestCycle.createTestCycle({
|
|
121
|
+
body: {
|
|
122
|
+
projectKey: 'PROJ',
|
|
123
|
+
name: 'Sprint 1 Regression',
|
|
124
|
+
description: 'Regression tests for Sprint 1',
|
|
125
|
+
plannedStartDate: '2024-01-15T09:00:00Z',
|
|
126
|
+
plannedEndDate: '2024-01-22T17:00:00Z',
|
|
127
|
+
statusName: 'In Progress',
|
|
128
|
+
customFields: {
|
|
129
|
+
'Environment': 'Production'
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
console.log(`Created test cycle: ${testCycle.key}`);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### Create a Test Execution
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const testExecution = await Zephyr.TestExecution.createTestExecution({
|
|
140
|
+
body: {
|
|
141
|
+
projectKey: 'PROJ',
|
|
142
|
+
testCaseKey: 'PROJ-T1',
|
|
143
|
+
testCycleKey: 'PROJ-R1',
|
|
144
|
+
statusName: 'Pass',
|
|
145
|
+
environmentName: 'Chrome Latest',
|
|
146
|
+
executionTime: 120000, // 2 minutes in milliseconds
|
|
147
|
+
comment: 'Test passed successfully'
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
console.log(`Created test execution: ${testExecution.key || testExecution.id}`);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### Update a Test Case
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// First, get the test case
|
|
157
|
+
const testCase = await Zephyr.TestCase.getTestCase({ testCaseKey: 'PROJ-T1' });
|
|
158
|
+
|
|
159
|
+
// Update it
|
|
160
|
+
await Zephyr.TestCase.updateTestCase({
|
|
161
|
+
testCaseKey: 'PROJ-T1',
|
|
162
|
+
body: {
|
|
163
|
+
name: 'Updated Test Case Name',
|
|
164
|
+
objective: 'Updated objective',
|
|
165
|
+
labels: [...(testCase.labels || []), 'updated']
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
#### List with Filters and Pagination
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// Offset-based pagination
|
|
174
|
+
const testCases = await Zephyr.TestCase.listTestCases({
|
|
175
|
+
projectKey: 'PROJ',
|
|
176
|
+
folderId: 12345,
|
|
177
|
+
maxResults: 50,
|
|
178
|
+
startAt: 0
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Cursor-based pagination (for large datasets)
|
|
182
|
+
const testCasesCursor = await Zephyr.TestCase.listTestCasesCursorPaginated({
|
|
183
|
+
projectKey: 'PROJ',
|
|
184
|
+
limit: 100,
|
|
185
|
+
startAtId: 0
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### Create Links
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// Link test case to Jira issue
|
|
193
|
+
const issueLink = await Zephyr.TestCase.createTestCaseIssueLink({
|
|
194
|
+
testCaseKey: 'PROJ-T1',
|
|
195
|
+
body: {
|
|
196
|
+
issueId: 12345
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Link test case to web URL
|
|
201
|
+
const webLink = await Zephyr.TestCase.createTestCaseWebLink({
|
|
202
|
+
testCaseKey: 'PROJ-T1',
|
|
203
|
+
body: {
|
|
204
|
+
url: 'https://example.com/test-resource',
|
|
205
|
+
description: 'External test resource'
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## API Groups
|
|
211
|
+
|
|
212
|
+
The Managed API is organized into the following groups:
|
|
213
|
+
|
|
214
|
+
### Project
|
|
215
|
+
- `listProjects(options?)` - List all projects
|
|
216
|
+
- `getProject(options)` - Get a specific project
|
|
217
|
+
|
|
218
|
+
### Status
|
|
219
|
+
- `listStatuses(options)` - List all statuses
|
|
220
|
+
- `getStatus(options)` - Get a specific status
|
|
221
|
+
- `createStatus(request)` - Create a new status
|
|
222
|
+
- `updateStatus(request)` - Update a status
|
|
223
|
+
|
|
224
|
+
### Priority
|
|
225
|
+
- `listPriorities(options)` - List all priorities
|
|
226
|
+
- `getPriority(options)` - Get a specific priority
|
|
227
|
+
- `createPriority(request)` - Create a new priority
|
|
228
|
+
- `updatePriority(request)` - Update a priority
|
|
229
|
+
|
|
230
|
+
### Environment
|
|
231
|
+
- `listEnvironments(options)` - List all environments
|
|
232
|
+
- `getEnvironment(options)` - Get a specific environment
|
|
233
|
+
- `createEnvironment(request)` - Create a new environment
|
|
234
|
+
- `updateEnvironment(request)` - Update an environment
|
|
235
|
+
|
|
236
|
+
### Folder
|
|
237
|
+
- `listFolders(options)` - List all folders
|
|
238
|
+
- `getFolder(options)` - Get a specific folder
|
|
239
|
+
- `createFolder(request)` - Create a new folder
|
|
240
|
+
|
|
241
|
+
### TestCase
|
|
242
|
+
- `listTestCases(options?)` - List all test cases
|
|
243
|
+
- `listTestCasesCursorPaginated(options?)` - List test cases with cursor pagination
|
|
244
|
+
- `getTestCase(options)` - Get a specific test case
|
|
245
|
+
- `createTestCase(request)` - Create a new test case
|
|
246
|
+
- `updateTestCase(request)` - Update a test case
|
|
247
|
+
- `getTestCaseLinks(testCaseKey)` - Get links for a test case
|
|
248
|
+
- `createTestCaseIssueLink(request)` - Create issue link
|
|
249
|
+
- `createTestCaseWebLink(request)` - Create web link
|
|
250
|
+
- `listTestCaseVersions(options)` - List test case versions
|
|
251
|
+
- `getTestCaseVersion(options)` - Get a specific version
|
|
252
|
+
- `getTestCaseTestScript(testCaseKey)` - Get test script
|
|
253
|
+
- `createTestCaseTestScript(request)` - Create or update test script
|
|
254
|
+
- `getTestCaseTestSteps(testCaseKey, options?)` - Get test steps
|
|
255
|
+
- `createTestCaseTestSteps(request)` - Create test steps
|
|
256
|
+
|
|
257
|
+
### TestCycle
|
|
258
|
+
- `listTestCycles(options?)` - List all test cycles
|
|
259
|
+
- `getTestCycle(options)` - Get a specific test cycle
|
|
260
|
+
- `createTestCycle(request)` - Create a new test cycle
|
|
261
|
+
- `updateTestCycle(request)` - Update a test cycle
|
|
262
|
+
- `getTestCycleLinks(testCycleIdOrKey)` - Get links for a test cycle
|
|
263
|
+
- `createTestCycleIssueLink(request)` - Create issue link
|
|
264
|
+
- `createTestCycleWebLink(request)` - Create web link
|
|
265
|
+
|
|
266
|
+
### TestPlan
|
|
267
|
+
- `listTestPlans(options?)` - List all test plans
|
|
268
|
+
- `getTestPlan(options)` - Get a specific test plan
|
|
269
|
+
- `createTestPlan(request)` - Create a new test plan
|
|
270
|
+
- `createTestPlanIssueLink(request)` - Create issue link
|
|
271
|
+
- `createTestPlanWebLink(request)` - Create web link
|
|
272
|
+
- `createTestPlanTestCycleLink(request)` - Link test cycle to test plan
|
|
273
|
+
|
|
274
|
+
### TestExecution
|
|
275
|
+
- `listTestExecutions(options?)` - List all test executions
|
|
276
|
+
- `listTestExecutionsNextgen(options?)` - List test executions with cursor pagination
|
|
277
|
+
- `getTestExecution(options)` - Get a specific test execution
|
|
278
|
+
- `createTestExecution(request)` - Create a new test execution
|
|
279
|
+
- `updateTestExecution(request)` - Update a test execution
|
|
280
|
+
- `getTestExecutionTestSteps(options)` - Get test steps for execution
|
|
281
|
+
- `putTestExecutionTestSteps(request)` - Update test steps
|
|
282
|
+
- `syncTestExecutionScript(request)` - Sync with test case script
|
|
283
|
+
- `listTestExecutionLinks(testExecutionIdOrKey)` - Get links
|
|
284
|
+
- `createTestExecutionIssueLink(request)` - Create issue link
|
|
285
|
+
|
|
286
|
+
### Link
|
|
287
|
+
- `deleteLink(options)` - Delete a link (idempotent)
|
|
288
|
+
|
|
289
|
+
### IssueLink
|
|
290
|
+
- `getIssueLinkTestCases(options)` - Get test cases linked to issue
|
|
291
|
+
- `getIssueLinkTestCycles(options)` - Get test cycles linked to issue
|
|
292
|
+
- `getIssueLinkTestPlans(options)` - Get test plans linked to issue
|
|
293
|
+
- `getIssueLinkExecutions(options)` - Get test executions linked to issue
|
|
294
|
+
|
|
295
|
+
### Automation
|
|
296
|
+
- `createCustomExecutions(request)` - Upload custom format results
|
|
297
|
+
- `createCucumberExecutions(request)` - Upload Cucumber results
|
|
298
|
+
- `createJUnitExecutions(request)` - Upload JUnit XML results
|
|
299
|
+
- `retrieveBDDTestCases(options)` - Retrieve BDD feature files
|
|
300
|
+
|
|
301
|
+
## Authentication
|
|
302
|
+
|
|
303
|
+
### Using ScriptRunner Connect API Connection (Recommended)
|
|
304
|
+
|
|
305
|
+
When using in ScriptRunner Connect, configure an API Connection in the web UI and use it:
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
import { createZephyrApi } from '@rbaileysr/zephyr-managed-api';
|
|
309
|
+
import ZephyrApiConnection from '../api/zephyr';
|
|
310
|
+
|
|
311
|
+
const Zephyr = createZephyrApi(ZephyrApiConnection, 'us');
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Using OAuth Token
|
|
315
|
+
|
|
316
|
+
For local testing or when using OAuth tokens directly:
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
import { createZephyrApi } from '@rbaileysr/zephyr-managed-api';
|
|
320
|
+
|
|
321
|
+
const Zephyr = createZephyrApi('your-oauth-token', 'us');
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Region Support
|
|
325
|
+
|
|
326
|
+
The API supports both US and EU regions:
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
// US region (default)
|
|
330
|
+
const ZephyrUS = createZephyrApi(apiConnection, 'us');
|
|
331
|
+
|
|
332
|
+
// EU region
|
|
333
|
+
const ZephyrEU = createZephyrApi(apiConnection, 'eu');
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Custom Fields
|
|
337
|
+
|
|
338
|
+
Custom fields are supported as flexible key-value pairs:
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
const customFields = {
|
|
342
|
+
'Build Number': 20,
|
|
343
|
+
'Release Date': '2024-01-15',
|
|
344
|
+
'Pre-Condition(s)': 'User should have logged in.<br>User should have navigated to the panel.',
|
|
345
|
+
'Implemented': false,
|
|
346
|
+
'Category': ['Performance', 'Regression'],
|
|
347
|
+
'Tester': 'user-account-id-here'
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
await Zephyr.TestCase.createTestCase({
|
|
351
|
+
body: {
|
|
352
|
+
projectKey: 'PROJ',
|
|
353
|
+
name: 'Test Case',
|
|
354
|
+
customFields: customFields
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Note**:
|
|
360
|
+
- Multi-line text fields support HTML with `<br>` tags
|
|
361
|
+
- Dates should be in format `yyyy-MM-dd`
|
|
362
|
+
- Users should have values of Jira User Account IDs
|
|
363
|
+
- Custom field types are defined in your Zephyr project configuration
|
|
364
|
+
|
|
365
|
+
## Error Handling
|
|
366
|
+
|
|
367
|
+
The API uses Commons Core-compatible error types for consistent error handling. All API methods automatically retry on rate limiting (HTTP 429) with exponential backoff.
|
|
368
|
+
|
|
369
|
+
### Error Types
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
import {
|
|
373
|
+
BadRequestError,
|
|
374
|
+
UnauthorizedError,
|
|
375
|
+
ForbiddenError,
|
|
376
|
+
NotFoundError,
|
|
377
|
+
TooManyRequestsError,
|
|
378
|
+
ServerError,
|
|
379
|
+
HttpError,
|
|
380
|
+
UnexpectedError
|
|
381
|
+
} from '@rbaileysr/zephyr-managed-api';
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const testCase = await Zephyr.TestCase.getTestCase({ testCaseKey: 'INVALID-T1' });
|
|
385
|
+
} catch (error) {
|
|
386
|
+
if (error instanceof NotFoundError) {
|
|
387
|
+
console.log('Test case not found');
|
|
388
|
+
} else if (error instanceof BadRequestError) {
|
|
389
|
+
console.log('Invalid request');
|
|
390
|
+
} else if (error instanceof TooManyRequestsError) {
|
|
391
|
+
console.log('Rate limited - this should have been retried automatically');
|
|
392
|
+
} else if (error instanceof HttpError) {
|
|
393
|
+
console.log(`HTTP error: ${error.message}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Automatic Rate Limiting Handling
|
|
399
|
+
|
|
400
|
+
All API methods automatically handle rate limiting (HTTP 429) with:
|
|
401
|
+
|
|
402
|
+
- **Exponential Backoff**: Starts at 1 second, doubles with each retry
|
|
403
|
+
- **Retry-After Header Support**: Respects `Retry-After` header when present
|
|
404
|
+
- **Configurable Retries**: Defaults to 5 retries, max delay of 60 seconds
|
|
405
|
+
- **Transparent Operation**: Retries happen automatically - no code changes needed
|
|
406
|
+
|
|
407
|
+
**Default Retry Configuration:**
|
|
408
|
+
- Maximum retries: 5
|
|
409
|
+
- Initial delay: 1 second
|
|
410
|
+
- Maximum delay: 60 seconds
|
|
411
|
+
- Backoff multiplier: 2x
|
|
412
|
+
|
|
413
|
+
**Example:**
|
|
414
|
+
```typescript
|
|
415
|
+
// This call will automatically retry on 429 errors
|
|
416
|
+
// You don't need to handle rate limiting manually
|
|
417
|
+
const testCases = await Zephyr.TestCase.listTestCases({ projectKey: 'PROJ' });
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
If rate limiting occurs:
|
|
421
|
+
1. The API waits (using Retry-After header if present, otherwise exponential backoff)
|
|
422
|
+
2. Retries the request automatically
|
|
423
|
+
3. Repeats up to 5 times
|
|
424
|
+
4. Only throws `TooManyRequestsError` if all retries are exhausted
|
|
425
|
+
|
|
426
|
+
### Error Strategy Support
|
|
427
|
+
|
|
428
|
+
For advanced error handling, you can provide an `errorStrategy` parameter to customize how errors are handled:
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
import { retry, ErrorStrategyBuilder } from '@rbaileysr/zephyr-managed-api';
|
|
432
|
+
|
|
433
|
+
// Return null instead of throwing on 404
|
|
434
|
+
const testCase = await Zephyr.TestCase.getTestCase(
|
|
435
|
+
{ testCaseKey: 'PROJ-T1' },
|
|
436
|
+
{
|
|
437
|
+
handleHttp404Error: () => ({ type: 'return', value: null })
|
|
438
|
+
}
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
// Using ErrorStrategyBuilder for cleaner syntax
|
|
442
|
+
const errorStrategy = new ErrorStrategyBuilder<TestCase | null>()
|
|
443
|
+
.http404Error(() => ({ type: 'return', value: null }))
|
|
444
|
+
.retryOnRateLimiting(10) // Retry up to 10 times on rate limiting
|
|
445
|
+
.http500Error(() => retry(5000)) // Retry server errors with 5s delay
|
|
446
|
+
.build();
|
|
447
|
+
|
|
448
|
+
const testCase2 = await Zephyr.TestCase.getTestCase(
|
|
449
|
+
{ testCaseKey: 'PROJ-T2' },
|
|
450
|
+
errorStrategy
|
|
451
|
+
);
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
**Available Error Strategy Handlers:**
|
|
455
|
+
- `handleHttp400Error` - Handle 400 Bad Request
|
|
456
|
+
- `handleHttp401Error` - Handle 401 Unauthorized
|
|
457
|
+
- `handleHttp403Error` - Handle 403 Forbidden
|
|
458
|
+
- `handleHttp404Error` - Handle 404 Not Found
|
|
459
|
+
- `handleHttp429Error` - Handle 429 Too Many Requests (receives retryCount)
|
|
460
|
+
- `handleHttp500Error` - Handle 500+ Server Errors
|
|
461
|
+
- `handleHttpError` - Handle any HTTP error (fallback)
|
|
462
|
+
|
|
463
|
+
**Error Strategy Actions:**
|
|
464
|
+
- `retry(delay)` - Retry the request after the specified delay (milliseconds)
|
|
465
|
+
- `continuePropagation()` - Let the error propagate normally (default)
|
|
466
|
+
- `{ type: 'return', value: T }` - Return a specific value instead of throwing
|
|
467
|
+
|
|
468
|
+
### Error Type Reference
|
|
469
|
+
|
|
470
|
+
| Error Type | HTTP Status | Description |
|
|
471
|
+
|------------|-------------|-------------|
|
|
472
|
+
| `BadRequestError` | 400 | Invalid request parameters |
|
|
473
|
+
| `UnauthorizedError` | 401 | Authentication required or invalid |
|
|
474
|
+
| `ForbiddenError` | 403 | Insufficient permissions |
|
|
475
|
+
| `NotFoundError` | 404 | Resource not found |
|
|
476
|
+
| `TooManyRequestsError` | 429 | Rate limit exceeded (after retries) |
|
|
477
|
+
| `ServerError` | 500+ | Server-side error |
|
|
478
|
+
| `HttpError` | Other | Generic HTTP error |
|
|
479
|
+
| `UnexpectedError` | N/A | Network or parsing errors |
|
|
480
|
+
|
|
481
|
+
## Pagination
|
|
482
|
+
|
|
483
|
+
### Manual Pagination
|
|
484
|
+
|
|
485
|
+
#### Offset-Based Pagination
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
const testCases = await Zephyr.TestCase.listTestCases({
|
|
489
|
+
projectKey: 'PROJ',
|
|
490
|
+
maxResults: 50,
|
|
491
|
+
startAt: 0
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Check if more results available
|
|
495
|
+
if (!testCases.isLast) {
|
|
496
|
+
const nextPage = await Zephyr.TestCase.listTestCases({
|
|
497
|
+
projectKey: 'PROJ',
|
|
498
|
+
maxResults: 50,
|
|
499
|
+
startAt: testCases.startAt + testCases.maxResults
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
#### Cursor-Based Pagination
|
|
505
|
+
|
|
506
|
+
```typescript
|
|
507
|
+
const testCases = await Zephyr.TestCase.listTestCasesCursorPaginated({
|
|
508
|
+
projectKey: 'PROJ',
|
|
509
|
+
limit: 100,
|
|
510
|
+
startAtId: 0
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// Get next page
|
|
514
|
+
if (testCases.nextStartAtId !== null) {
|
|
515
|
+
const nextPage = await Zephyr.TestCase.listTestCasesCursorPaginated({
|
|
516
|
+
projectKey: 'PROJ',
|
|
517
|
+
limit: 100,
|
|
518
|
+
startAtId: testCases.nextStartAtId
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Automatic Pagination with `getAllPages()`
|
|
524
|
+
|
|
525
|
+
For convenience, use `getAllPages()` to automatically fetch all pages:
|
|
526
|
+
|
|
527
|
+
#### Offset-Based Pagination
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
import { getAllPages } from '@rbaileysr/zephyr-managed-api';
|
|
531
|
+
|
|
532
|
+
// Automatically fetches all pages
|
|
533
|
+
const allTestCases = await getAllPages(
|
|
534
|
+
(startAt, maxResults) => Zephyr.TestCase.listTestCases({
|
|
535
|
+
projectKey: 'PROJ',
|
|
536
|
+
startAt,
|
|
537
|
+
maxResults
|
|
538
|
+
}),
|
|
539
|
+
{ maxResults: 50 } // Optional: page size
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
// allTestCases is an array of all TestCase objects from all pages
|
|
543
|
+
console.log(`Found ${allTestCases.length} test cases`);
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
#### Cursor-Based Pagination
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
import { getAllPagesCursor } from '@rbaileysr/zephyr-managed-api';
|
|
550
|
+
|
|
551
|
+
// Automatically fetches all pages
|
|
552
|
+
const allTestCases = await getAllPagesCursor(
|
|
553
|
+
(startAtId, limit) => Zephyr.TestCase.listTestCasesCursorPaginated({
|
|
554
|
+
projectKey: 'PROJ',
|
|
555
|
+
startAtId,
|
|
556
|
+
limit
|
|
557
|
+
}),
|
|
558
|
+
{ limit: 100 } // Optional: page size
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
// allTestCases is an array of all TestCase objects from all pages
|
|
562
|
+
console.log(`Found ${allTestCases.length} test cases`);
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**Note**: `getAllPages()` and `getAllPagesCursor()` automatically handle pagination and will make multiple API calls as needed. Be mindful of rate limits when fetching large datasets.
|
|
566
|
+
|
|
567
|
+
## API Documentation
|
|
568
|
+
|
|
569
|
+
All API methods include comprehensive JSDoc comments with:
|
|
570
|
+
- Detailed descriptions of what each endpoint does
|
|
571
|
+
- Parameter documentation with types and requirements
|
|
572
|
+
- Return type documentation
|
|
573
|
+
- Links to official Zephyr API documentation
|
|
574
|
+
|
|
575
|
+
When using the package in ScriptRunner Connect, IntelliSense will display these descriptions and links in the code editor.
|
|
576
|
+
|
|
577
|
+
## Type Definitions
|
|
578
|
+
|
|
579
|
+
All TypeScript types are exported from the main module:
|
|
580
|
+
|
|
581
|
+
```typescript
|
|
582
|
+
import type {
|
|
583
|
+
TestCase,
|
|
584
|
+
TestCycle,
|
|
585
|
+
TestExecution,
|
|
586
|
+
TestPlan,
|
|
587
|
+
CustomFields,
|
|
588
|
+
ZephyrApiConnection,
|
|
589
|
+
ErrorStrategy,
|
|
590
|
+
ErrorStrategyBuilder
|
|
591
|
+
} from '@rbaileysr/zephyr-managed-api';
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
## All Group
|
|
595
|
+
|
|
596
|
+
For programmatic access to all methods, use the `All` group:
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
// Access all methods through the All group
|
|
600
|
+
const testCase = await Zephyr.All.getTestCase({ testCaseKey: 'PROJ-T1' });
|
|
601
|
+
const testCycle = await Zephyr.All.createTestCycle({ body: { ... } });
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
The `All` group provides a single point of access to all API methods across all resource groups, useful for dynamic method invocation or when you need to iterate over all available methods.
|
|
605
|
+
|
|
606
|
+
## License
|
|
607
|
+
|
|
608
|
+
MIT
|
|
609
|
+
|
|
610
|
+
## Support
|
|
611
|
+
|
|
612
|
+
For issues, questions, or contributions, please refer to the project repository on GitHub.
|
|
613
|
+
|
|
614
|
+
## Links
|
|
615
|
+
|
|
616
|
+
- **NPM Package**: https://www.npmjs.com/package/@rbaileysr/zephyr-managed-api
|
|
617
|
+
- **Zephyr API Documentation**: https://support.smartbear.com/zephyr-scale-cloud/api-docs/v2/
|
|
618
|
+
- **ScriptRunner Connect Documentation**: https://docs.adaptavist.com/src/latest/
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Strategy Types and Utilities
|
|
3
|
+
* Provides flexible error handling for API calls
|
|
4
|
+
*/
|
|
5
|
+
import type { ApiResponseType } from './utils';
|
|
6
|
+
/**
|
|
7
|
+
* Retry action - indicates the request should be retried after a delay
|
|
8
|
+
*/
|
|
9
|
+
export interface RetryAction {
|
|
10
|
+
type: 'retry';
|
|
11
|
+
delay: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Continue propagation - let the error propagate normally
|
|
15
|
+
*/
|
|
16
|
+
export interface ContinuePropagationAction {
|
|
17
|
+
type: 'continue';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Return value - return a specific value instead of throwing
|
|
21
|
+
*/
|
|
22
|
+
export interface ReturnValueAction<T> {
|
|
23
|
+
type: 'return';
|
|
24
|
+
value: T;
|
|
25
|
+
}
|
|
26
|
+
export type ErrorStrategyAction<T> = RetryAction | ContinuePropagationAction | ReturnValueAction<T>;
|
|
27
|
+
/**
|
|
28
|
+
* Create a retry action
|
|
29
|
+
*/
|
|
30
|
+
export declare function retry(delay: number): RetryAction;
|
|
31
|
+
/**
|
|
32
|
+
* Continue error propagation (default behavior)
|
|
33
|
+
*/
|
|
34
|
+
export declare function continuePropagation(): ContinuePropagationAction;
|
|
35
|
+
/**
|
|
36
|
+
* Error strategy handlers
|
|
37
|
+
*/
|
|
38
|
+
export interface ErrorStrategyHandlers<T> {
|
|
39
|
+
handleHttp400Error?: (response: ApiResponseType, errorData: unknown) => ErrorStrategyAction<T> | Promise<ErrorStrategyAction<T>>;
|
|
40
|
+
handleHttp401Error?: (response: ApiResponseType, errorData: unknown) => ErrorStrategyAction<T> | Promise<ErrorStrategyAction<T>>;
|
|
41
|
+
handleHttp403Error?: (response: ApiResponseType, errorData: unknown) => ErrorStrategyAction<T> | Promise<ErrorStrategyAction<T>>;
|
|
42
|
+
handleHttp404Error?: (response: ApiResponseType, errorData: unknown) => ErrorStrategyAction<T> | Promise<ErrorStrategyAction<T>>;
|
|
43
|
+
handleHttp429Error?: (response: ApiResponseType, retryCount: number) => ErrorStrategyAction<T> | Promise<ErrorStrategyAction<T>>;
|
|
44
|
+
handleHttp500Error?: (response: ApiResponseType, errorData: unknown) => ErrorStrategyAction<T> | Promise<ErrorStrategyAction<T>>;
|
|
45
|
+
handleHttpError?: (response: ApiResponseType, errorData: unknown) => ErrorStrategyAction<T> | Promise<ErrorStrategyAction<T>>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Error strategy builder pattern
|
|
49
|
+
*/
|
|
50
|
+
export declare class ErrorStrategyBuilder<T = unknown> {
|
|
51
|
+
private handlers;
|
|
52
|
+
http400Error(handler: (response: ApiResponseType, errorData: unknown) => ErrorStrategyAction<T> | Promise<ErrorStrategyAction<T>>): this;
|
|
53
|
+
http401Error(handler: (response: ApiResponseType, errorData: unknown) => ErrorStrategyAction<T> | Promise<ErrorStrategyAction<T>>): this;
|
|
54
|
+
http403Error(handler: (response: ApiResponseType, errorData: unknown) => ErrorStrategyAction<T> | Promise<ErrorStrategyAction<T>>): this;
|
|
55
|
+
http404Error(handler: (response: ApiResponseType, errorData: unknown) => ErrorStrategyAction<T> | Promise<ErrorStrategyAction<T>>): this;
|
|
56
|
+
http429Error(handler: (response: ApiResponseType, retryCount: number) => ErrorStrategyAction<T> | Promise<ErrorStrategyAction<T>>): this;
|
|
57
|
+
http500Error(handler: (response: ApiResponseType, errorData: unknown) => ErrorStrategyAction<T> | Promise<ErrorStrategyAction<T>>): this;
|
|
58
|
+
httpError(handler: (response: ApiResponseType, errorData: unknown) => ErrorStrategyAction<T> | Promise<ErrorStrategyAction<T>>): this;
|
|
59
|
+
/**
|
|
60
|
+
* Retry on rate limiting with specified max retries
|
|
61
|
+
*/
|
|
62
|
+
retryOnRateLimiting(maxRetries: number): this;
|
|
63
|
+
build(): ErrorStrategyHandlers<T>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Apply error strategy to a response
|
|
67
|
+
*/
|
|
68
|
+
export declare function applyErrorStrategy<T>(response: ApiResponseType, errorStrategy: ErrorStrategyHandlers<T> | undefined, retryCount?: number): Promise<ErrorStrategyAction<T>>;
|
|
69
|
+
//# sourceMappingURL=error-strategy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-strategy.d.ts","sourceRoot":"","sources":["../error-strategy.ts"],"names":[],"mappings":"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"}
|