@dichovsky/testrail-api-client 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/dist/client.js ADDED
@@ -0,0 +1,1106 @@
1
+ import { TestRailClientCore } from './client-core.js';
2
+ import { TestRailValidationError } from './errors.js';
3
+ export { TestRailApiError, TestRailValidationError } from './errors.js';
4
+ /**
5
+ * TestRail API Client
6
+ *
7
+ * Type-safe client covering Projects, Suites, Sections, Cases, Plans, Runs,
8
+ * Tests, Results, Milestones, Users, Statuses, and Priorities.
9
+ * Extends {@link TestRailClientCore} for HTTP pipeline, caching, rate limiting, and retry.
10
+ */
11
+ export class TestRailClient extends TestRailClientCore {
12
+ // ── Projects ──────────────────────────────────────────────────────────────
13
+ /**
14
+ * Get a project by ID.
15
+ * @throws {TestRailValidationError} When projectId is invalid
16
+ * @throws {TestRailApiError} When the API request fails
17
+ */
18
+ async getProject(projectId) {
19
+ this.validateId(projectId, 'projectId');
20
+ return this.request('GET', `get_project/${projectId}`);
21
+ }
22
+ /**
23
+ * Get all projects.
24
+ * @throws {TestRailValidationError} When limit or offset is invalid
25
+ * @throws {TestRailApiError} When the API request fails
26
+ */
27
+ async getProjects(limit, offset) {
28
+ this.validatePaginationParams(limit, offset);
29
+ const endpoint = this.buildEndpoint('get_projects', { limit, offset });
30
+ const response = await this.request('GET', endpoint);
31
+ return response.projects ?? [];
32
+ }
33
+ /**
34
+ * Add a new project.
35
+ * @throws {TestRailApiError} When the API request fails
36
+ */
37
+ async addProject(payload) {
38
+ return this.request('POST', 'add_project', payload);
39
+ }
40
+ /**
41
+ * Update an existing project.
42
+ * @throws {TestRailValidationError} When projectId is invalid
43
+ * @throws {TestRailApiError} When the API request fails
44
+ */
45
+ async updateProject(projectId, payload) {
46
+ this.validateId(projectId, 'projectId');
47
+ return this.request('POST', `update_project/${projectId}`, payload);
48
+ }
49
+ /**
50
+ * Delete a project.
51
+ * @throws {TestRailValidationError} When projectId is invalid
52
+ * @throws {TestRailApiError} When the API request fails
53
+ */
54
+ async deleteProject(projectId) {
55
+ this.validateId(projectId, 'projectId');
56
+ await this.request('POST', `delete_project/${projectId}`);
57
+ }
58
+ // ── Suites ────────────────────────────────────────────────────────────────
59
+ /**
60
+ * Get a suite by ID.
61
+ * @throws {TestRailValidationError} When suiteId is invalid
62
+ * @throws {TestRailApiError} When the API request fails
63
+ */
64
+ async getSuite(suiteId) {
65
+ this.validateId(suiteId, 'suiteId');
66
+ return this.request('GET', `get_suite/${suiteId}`);
67
+ }
68
+ /**
69
+ * Get all suites for a project.
70
+ * @throws {TestRailValidationError} When projectId is invalid
71
+ * @throws {TestRailApiError} When the API request fails
72
+ */
73
+ async getSuites(projectId) {
74
+ this.validateId(projectId, 'projectId');
75
+ return this.request('GET', `get_suites/${projectId}`);
76
+ }
77
+ /**
78
+ * Add a suite to a project.
79
+ * @throws {TestRailValidationError} When projectId is invalid
80
+ * @throws {TestRailApiError} When the API request fails
81
+ */
82
+ async addSuite(projectId, payload) {
83
+ this.validateId(projectId, 'projectId');
84
+ return this.request('POST', `add_suite/${projectId}`, payload);
85
+ }
86
+ /**
87
+ * Update a suite.
88
+ * @throws {TestRailValidationError} When suiteId is invalid
89
+ * @throws {TestRailApiError} When the API request fails
90
+ */
91
+ async updateSuite(suiteId, payload) {
92
+ this.validateId(suiteId, 'suiteId');
93
+ return this.request('POST', `update_suite/${suiteId}`, payload);
94
+ }
95
+ /**
96
+ * Delete a suite.
97
+ * @throws {TestRailValidationError} When suiteId is invalid
98
+ * @throws {TestRailApiError} When the API request fails
99
+ */
100
+ async deleteSuite(suiteId) {
101
+ this.validateId(suiteId, 'suiteId');
102
+ await this.request('POST', `delete_suite/${suiteId}`);
103
+ }
104
+ // ── Sections ──────────────────────────────────────────────────────────────
105
+ /**
106
+ * Get a section by ID.
107
+ * @throws {TestRailValidationError} When sectionId is invalid
108
+ * @throws {TestRailApiError} When the API request fails
109
+ */
110
+ async getSection(sectionId) {
111
+ this.validateId(sectionId, 'sectionId');
112
+ return this.request('GET', `get_section/${sectionId}`);
113
+ }
114
+ /**
115
+ * Get all sections for a project, optionally filtered by suite.
116
+ * @param options.suiteId - Optional suite filter
117
+ * @param options.limit - Optional maximum number of results to return
118
+ * @param options.offset - Optional number of results to skip
119
+ * @throws {TestRailValidationError} When projectId or suiteId is invalid
120
+ * @throws {TestRailApiError} When the API request fails
121
+ */
122
+ async getSections(projectId, options) {
123
+ this.validateId(projectId, 'projectId');
124
+ const { suiteId, limit, offset } = options ?? {};
125
+ if (suiteId !== undefined) {
126
+ this.validateId(suiteId, 'suiteId');
127
+ }
128
+ this.validatePaginationParams(limit, offset);
129
+ const endpoint = this.buildEndpoint(`get_sections/${projectId}`, { suite_id: suiteId, limit, offset });
130
+ const response = await this.request('GET', endpoint);
131
+ return response.sections ?? [];
132
+ }
133
+ /**
134
+ * Add a new section to a project.
135
+ * @throws {TestRailValidationError} When projectId is invalid
136
+ * @throws {TestRailApiError} When the API request fails
137
+ */
138
+ async addSection(projectId, payload) {
139
+ this.validateId(projectId, 'projectId');
140
+ return this.request('POST', `add_section/${projectId}`, payload);
141
+ }
142
+ /**
143
+ * Update an existing section.
144
+ * @throws {TestRailValidationError} When sectionId is invalid
145
+ * @throws {TestRailApiError} When the API request fails
146
+ */
147
+ async updateSection(sectionId, payload) {
148
+ this.validateId(sectionId, 'sectionId');
149
+ return this.request('POST', `update_section/${sectionId}`, payload);
150
+ }
151
+ /**
152
+ * Delete a section.
153
+ * @throws {TestRailValidationError} When sectionId is invalid
154
+ * @throws {TestRailApiError} When the API request fails
155
+ */
156
+ async deleteSection(sectionId) {
157
+ this.validateId(sectionId, 'sectionId');
158
+ await this.request('POST', `delete_section/${sectionId}`);
159
+ }
160
+ // ── Cases ─────────────────────────────────────────────────────────────────
161
+ /**
162
+ * Get a case by ID.
163
+ * @throws {TestRailValidationError} When caseId is invalid
164
+ * @throws {TestRailApiError} When the API request fails
165
+ */
166
+ async getCase(caseId) {
167
+ this.validateId(caseId, 'caseId');
168
+ return this.request('GET', `get_case/${caseId}`);
169
+ }
170
+ /**
171
+ * Get all cases for a project with optional filters.
172
+ * @param options.suiteId - Return only cases in this suite
173
+ * @param options.sectionId - Return only cases in this section
174
+ * @param options.typeId - Return only cases of this type
175
+ * @param options.priorityId - Return only cases with this priority
176
+ * @param options.templateId - Return only cases using this template
177
+ * @param options.milestoneId - Return only cases linked to this milestone
178
+ * @param options.createdAfter - Return only cases created after this Unix timestamp
179
+ * @param options.createdBefore - Return only cases created before this Unix timestamp
180
+ * @param options.updatedAfter - Return only cases updated after this Unix timestamp
181
+ * @param options.updatedBefore - Return only cases updated before this Unix timestamp
182
+ * @param options.limit - Maximum number of cases to return
183
+ * @param options.offset - Pagination offset
184
+ * @throws {TestRailValidationError} When any provided ID is invalid
185
+ * @throws {TestRailApiError} When the API request fails
186
+ */
187
+ async getCases(projectId, options) {
188
+ this.validateId(projectId, 'projectId');
189
+ const { suiteId, sectionId, typeId, priorityId, templateId, milestoneId, createdAfter, createdBefore, updatedAfter, updatedBefore, limit, offset, } = options ?? {};
190
+ if (suiteId !== undefined)
191
+ this.validateId(suiteId, 'suiteId');
192
+ if (sectionId !== undefined)
193
+ this.validateId(sectionId, 'sectionId');
194
+ if (typeId !== undefined)
195
+ this.validateId(typeId, 'typeId');
196
+ if (priorityId !== undefined)
197
+ this.validateId(priorityId, 'priorityId');
198
+ if (templateId !== undefined)
199
+ this.validateId(templateId, 'templateId');
200
+ if (milestoneId !== undefined)
201
+ this.validateId(milestoneId, 'milestoneId');
202
+ this.validatePaginationParams(limit, offset);
203
+ const endpoint = this.buildEndpoint(`get_cases/${projectId}`, {
204
+ suite_id: suiteId,
205
+ section_id: sectionId,
206
+ type_id: typeId,
207
+ priority_id: priorityId,
208
+ template_id: templateId,
209
+ milestone_id: milestoneId,
210
+ created_after: createdAfter,
211
+ created_before: createdBefore,
212
+ updated_after: updatedAfter,
213
+ updated_before: updatedBefore,
214
+ limit,
215
+ offset,
216
+ });
217
+ const response = await this.request('GET', endpoint);
218
+ return response.cases ?? [];
219
+ }
220
+ /**
221
+ * Add a new case to a section.
222
+ * @throws {TestRailValidationError} When sectionId is invalid
223
+ * @throws {TestRailApiError} When the API request fails
224
+ */
225
+ async addCase(sectionId, payload) {
226
+ this.validateId(sectionId, 'sectionId');
227
+ return this.request('POST', `add_case/${sectionId}`, payload);
228
+ }
229
+ /**
230
+ * Update an existing case.
231
+ * @throws {TestRailValidationError} When caseId is invalid
232
+ * @throws {TestRailApiError} When the API request fails
233
+ */
234
+ async updateCase(caseId, payload) {
235
+ this.validateId(caseId, 'caseId');
236
+ return this.request('POST', `update_case/${caseId}`, payload);
237
+ }
238
+ /**
239
+ * Delete a case.
240
+ * @throws {TestRailValidationError} When caseId is invalid
241
+ * @throws {TestRailApiError} When the API request fails
242
+ */
243
+ async deleteCase(caseId) {
244
+ this.validateId(caseId, 'caseId');
245
+ await this.request('POST', `delete_case/${caseId}`);
246
+ }
247
+ // ── Plans ─────────────────────────────────────────────────────────────────
248
+ /**
249
+ * Get a plan by ID.
250
+ * @throws {TestRailValidationError} When planId is invalid
251
+ * @throws {TestRailApiError} When the API request fails
252
+ */
253
+ async getPlan(planId) {
254
+ this.validateId(planId, 'planId');
255
+ return this.request('GET', `get_plan/${planId}`);
256
+ }
257
+ /**
258
+ * Get all plans for a project with optional filters.
259
+ * @param projectId - The project ID
260
+ * @param options - Optional filter parameters (created_after, created_before, created_by,
261
+ * is_completed, milestone_id, limit, offset)
262
+ * @throws {TestRailValidationError} When projectId is invalid
263
+ * @throws {TestRailApiError} When the API request fails
264
+ */
265
+ async getPlans(projectId, options) {
266
+ this.validateId(projectId, 'projectId');
267
+ this.validatePaginationParams(options?.limit, options?.offset);
268
+ const endpoint = this.buildEndpoint(`get_plans/${projectId}`, {
269
+ created_after: options?.created_after,
270
+ created_before: options?.created_before,
271
+ created_by: this.serializeIdList(options?.created_by),
272
+ is_completed: options?.is_completed,
273
+ milestone_id: this.serializeIdList(options?.milestone_id),
274
+ limit: options?.limit,
275
+ offset: options?.offset,
276
+ });
277
+ const response = await this.request('GET', endpoint);
278
+ return response.plans ?? [];
279
+ }
280
+ /**
281
+ * Add a new plan to a project.
282
+ * @throws {TestRailValidationError} When projectId is invalid
283
+ * @throws {TestRailApiError} When the API request fails
284
+ */
285
+ async addPlan(projectId, payload) {
286
+ this.validateId(projectId, 'projectId');
287
+ return this.request('POST', `add_plan/${projectId}`, payload);
288
+ }
289
+ /**
290
+ * Update a plan.
291
+ * @throws {TestRailValidationError} When planId is invalid
292
+ * @throws {TestRailApiError} When the API request fails
293
+ */
294
+ async updatePlan(planId, payload) {
295
+ this.validateId(planId, 'planId');
296
+ return this.request('POST', `update_plan/${planId}`, payload);
297
+ }
298
+ /**
299
+ * Close a plan.
300
+ * @throws {TestRailValidationError} When planId is invalid
301
+ * @throws {TestRailApiError} When the API request fails
302
+ */
303
+ async closePlan(planId) {
304
+ this.validateId(planId, 'planId');
305
+ return this.request('POST', `close_plan/${planId}`);
306
+ }
307
+ /**
308
+ * Delete a plan.
309
+ * @throws {TestRailValidationError} When planId is invalid
310
+ * @throws {TestRailApiError} When the API request fails
311
+ */
312
+ async deletePlan(planId) {
313
+ this.validateId(planId, 'planId');
314
+ await this.request('POST', `delete_plan/${planId}`);
315
+ }
316
+ /**
317
+ * Add a plan entry (run) to a plan.
318
+ * @throws {TestRailValidationError} When planId is invalid
319
+ * @throws {TestRailApiError} When the API request fails
320
+ */
321
+ async addPlanEntry(planId, payload) {
322
+ this.validateId(planId, 'planId');
323
+ return this.request('POST', `add_plan_entry/${planId}`, payload);
324
+ }
325
+ /**
326
+ * Update an existing plan entry.
327
+ * @throws {TestRailValidationError} When planId is invalid or entryId is not a non-empty string
328
+ * @throws {TestRailApiError} When the API request fails
329
+ */
330
+ async updatePlanEntry(planId, entryId, payload) {
331
+ this.validateId(planId, 'planId');
332
+ this.validateEntryId(entryId);
333
+ return this.request('POST', `update_plan_entry/${planId}/${entryId}`, payload);
334
+ }
335
+ /**
336
+ * Delete a plan entry.
337
+ * @throws {TestRailValidationError} When planId is invalid or entryId is not a non-empty string
338
+ * @throws {TestRailApiError} When the API request fails
339
+ */
340
+ async deletePlanEntry(planId, entryId) {
341
+ this.validateId(planId, 'planId');
342
+ this.validateEntryId(entryId);
343
+ await this.request('POST', `delete_plan_entry/${planId}/${entryId}`);
344
+ }
345
+ // ── Runs ──────────────────────────────────────────────────────────────────
346
+ /**
347
+ * Get a run by ID.
348
+ * @throws {TestRailValidationError} When runId is invalid
349
+ * @throws {TestRailApiError} When the API request fails
350
+ */
351
+ async getRun(runId) {
352
+ this.validateId(runId, 'runId');
353
+ return this.request('GET', `get_run/${runId}`);
354
+ }
355
+ /**
356
+ * Get all runs for a project, with optional filters.
357
+ * @param projectId - The project ID
358
+ * @param options - Optional filters: createdAfter, createdBefore, createdBy, isCompleted,
359
+ * milestoneId, refsFilter, suiteId, limit, offset
360
+ * @throws {TestRailValidationError} When projectId or pagination params are invalid
361
+ * @throws {TestRailApiError} When the API request fails
362
+ */
363
+ async getRuns(projectId, options) {
364
+ this.validateId(projectId, 'projectId');
365
+ const { createdAfter, createdBefore, createdBy, isCompleted, milestoneId, refsFilter, suiteId, limit, offset } = options ?? {};
366
+ this.validatePaginationParams(limit, offset);
367
+ if (milestoneId !== undefined) {
368
+ this.validateId(milestoneId, 'milestoneId');
369
+ }
370
+ if (suiteId !== undefined) {
371
+ this.validateId(suiteId, 'suiteId');
372
+ }
373
+ if (createdBy !== undefined) {
374
+ createdBy.forEach((userId) => this.validateId(userId, 'createdBy'));
375
+ }
376
+ const createdByFilter = createdBy && createdBy.length > 0 ? createdBy.join(',') : undefined;
377
+ const endpoint = this.buildEndpoint(`get_runs/${projectId}`, {
378
+ created_after: createdAfter,
379
+ created_before: createdBefore,
380
+ created_by: createdByFilter,
381
+ is_completed: isCompleted !== undefined ? (isCompleted ? 1 : 0) : undefined,
382
+ milestone_id: milestoneId,
383
+ refs_filter: refsFilter,
384
+ suite_id: suiteId,
385
+ limit,
386
+ offset,
387
+ });
388
+ const response = await this.request('GET', endpoint);
389
+ return response.runs ?? [];
390
+ }
391
+ /**
392
+ * Add a new run to a project.
393
+ * @throws {TestRailValidationError} When projectId is invalid
394
+ * @throws {TestRailApiError} When the API request fails
395
+ */
396
+ async addRun(projectId, payload) {
397
+ this.validateId(projectId, 'projectId');
398
+ return this.request('POST', `add_run/${projectId}`, payload);
399
+ }
400
+ /**
401
+ * Update a run.
402
+ * @throws {TestRailValidationError} When runId is invalid
403
+ * @throws {TestRailApiError} When the API request fails
404
+ */
405
+ async updateRun(runId, payload) {
406
+ this.validateId(runId, 'runId');
407
+ return this.request('POST', `update_run/${runId}`, payload);
408
+ }
409
+ /**
410
+ * Close a run.
411
+ * @throws {TestRailValidationError} When runId is invalid
412
+ * @throws {TestRailApiError} When the API request fails
413
+ */
414
+ async closeRun(runId) {
415
+ this.validateId(runId, 'runId');
416
+ return this.request('POST', `close_run/${runId}`);
417
+ }
418
+ /**
419
+ * Delete a run.
420
+ * @throws {TestRailValidationError} When runId is invalid
421
+ * @throws {TestRailApiError} When the API request fails
422
+ */
423
+ async deleteRun(runId) {
424
+ this.validateId(runId, 'runId');
425
+ await this.request('POST', `delete_run/${runId}`);
426
+ }
427
+ // ── Tests ─────────────────────────────────────────────────────────────────
428
+ /**
429
+ * Get a test by ID.
430
+ * @throws {TestRailValidationError} When testId is invalid
431
+ * @throws {TestRailApiError} When the API request fails
432
+ */
433
+ async getTest(testId) {
434
+ this.validateId(testId, 'testId');
435
+ return this.request('GET', `get_test/${testId}`);
436
+ }
437
+ /**
438
+ * Get all tests for a run with optional filters.
439
+ * @param runId - The run ID
440
+ * @param options - Optional filter parameters (status_id, limit, offset)
441
+ * @throws {TestRailValidationError} When runId is invalid
442
+ * @throws {TestRailApiError} When the API request fails
443
+ */
444
+ async getTests(runId, options) {
445
+ this.validateId(runId, 'runId');
446
+ this.validatePaginationParams(options?.limit, options?.offset);
447
+ const endpoint = this.buildEndpoint(`get_tests/${runId}`, {
448
+ status_id: this.serializeIdList(options?.status_id),
449
+ limit: options?.limit,
450
+ offset: options?.offset,
451
+ });
452
+ const response = await this.request('GET', endpoint);
453
+ return response.tests ?? [];
454
+ }
455
+ // ── Results ───────────────────────────────────────────────────────────────
456
+ /**
457
+ * Get results for a test with optional filters.
458
+ * @param testId - The test ID
459
+ * @param options - Optional filter parameters (created_after, created_before, created_by,
460
+ * status_id, limit, offset)
461
+ * @throws {TestRailValidationError} When testId is invalid
462
+ * @throws {TestRailApiError} When the API request fails
463
+ */
464
+ async getResults(testId, options) {
465
+ this.validateId(testId, 'testId');
466
+ this.validatePaginationParams(options?.limit, options?.offset);
467
+ const endpoint = this.buildEndpoint(`get_results/${testId}`, {
468
+ created_after: options?.created_after,
469
+ created_before: options?.created_before,
470
+ created_by: this.serializeIdList(options?.created_by),
471
+ status_id: this.serializeIdList(options?.status_id),
472
+ limit: options?.limit,
473
+ offset: options?.offset,
474
+ });
475
+ const response = await this.request('GET', endpoint);
476
+ return response.results ?? [];
477
+ }
478
+ /**
479
+ * Get results for a specific case within a run with optional filters.
480
+ * @param runId - The run ID
481
+ * @param caseId - The case ID
482
+ * @param options - Optional filter parameters (created_after, created_before, created_by,
483
+ * status_id, limit, offset)
484
+ * @throws {TestRailValidationError} When runId or caseId is invalid
485
+ * @throws {TestRailApiError} When the API request fails
486
+ */
487
+ async getResultsForCase(runId, caseId, options) {
488
+ this.validateId(runId, 'runId');
489
+ this.validateId(caseId, 'caseId');
490
+ this.validatePaginationParams(options?.limit, options?.offset);
491
+ const endpoint = this.buildEndpoint(`get_results_for_case/${runId}/${caseId}`, {
492
+ created_after: options?.created_after,
493
+ created_before: options?.created_before,
494
+ created_by: this.serializeIdList(options?.created_by),
495
+ status_id: this.serializeIdList(options?.status_id),
496
+ limit: options?.limit,
497
+ offset: options?.offset,
498
+ });
499
+ const response = await this.request('GET', endpoint);
500
+ return response.results ?? [];
501
+ }
502
+ /**
503
+ * Get all results for a run with optional filters.
504
+ * @param runId - The run ID
505
+ * @param options - Optional filter parameters (created_after, created_before, created_by,
506
+ * status_id, limit, offset)
507
+ * @throws {TestRailValidationError} When runId is invalid
508
+ * @throws {TestRailApiError} When the API request fails
509
+ */
510
+ async getResultsForRun(runId, options) {
511
+ this.validateId(runId, 'runId');
512
+ this.validatePaginationParams(options?.limit, options?.offset);
513
+ const endpoint = this.buildEndpoint(`get_results_for_run/${runId}`, {
514
+ created_after: options?.created_after,
515
+ created_before: options?.created_before,
516
+ created_by: this.serializeIdList(options?.created_by),
517
+ status_id: this.serializeIdList(options?.status_id),
518
+ limit: options?.limit,
519
+ offset: options?.offset,
520
+ });
521
+ const response = await this.request('GET', endpoint);
522
+ return response.results ?? [];
523
+ }
524
+ /**
525
+ * Add a result for a test.
526
+ * @throws {TestRailValidationError} When testId is invalid
527
+ * @throws {TestRailApiError} When the API request fails
528
+ */
529
+ async addResult(testId, payload) {
530
+ this.validateId(testId, 'testId');
531
+ return this.request('POST', `add_result/${testId}`, payload);
532
+ }
533
+ /**
534
+ * Add a result for a specific case within a run.
535
+ * @throws {TestRailValidationError} When runId or caseId is invalid
536
+ * @throws {TestRailApiError} When the API request fails
537
+ */
538
+ async addResultForCase(runId, caseId, payload) {
539
+ this.validateId(runId, 'runId');
540
+ this.validateId(caseId, 'caseId');
541
+ return this.request('POST', `add_result_for_case/${runId}/${caseId}`, payload);
542
+ }
543
+ /**
544
+ * Add multiple results for cases in a run.
545
+ * @throws {TestRailValidationError} When runId is invalid
546
+ * @throws {TestRailApiError} When the API request fails
547
+ */
548
+ async addResultsForCases(runId, payload) {
549
+ this.validateId(runId, 'runId');
550
+ return this.request('POST', `add_results_for_cases/${runId}`, payload);
551
+ }
552
+ // ── Milestones ────────────────────────────────────────────────────────────
553
+ /**
554
+ * Get a milestone by ID.
555
+ * @throws {TestRailValidationError} When milestoneId is invalid
556
+ * @throws {TestRailApiError} When the API request fails
557
+ */
558
+ async getMilestone(milestoneId) {
559
+ this.validateId(milestoneId, 'milestoneId');
560
+ return this.request('GET', `get_milestone/${milestoneId}`);
561
+ }
562
+ /**
563
+ * Get all milestones for a project with optional filters.
564
+ * @param projectId - The project ID
565
+ * @param options - Optional filter parameters (is_completed, limit, offset)
566
+ * @throws {TestRailValidationError} When projectId is invalid
567
+ * @throws {TestRailApiError} When the API request fails
568
+ */
569
+ async getMilestones(projectId, options) {
570
+ this.validateId(projectId, 'projectId');
571
+ this.validatePaginationParams(options?.limit, options?.offset);
572
+ const endpoint = this.buildEndpoint(`get_milestones/${projectId}`, {
573
+ is_completed: options?.is_completed,
574
+ limit: options?.limit,
575
+ offset: options?.offset,
576
+ });
577
+ const response = await this.request('GET', endpoint);
578
+ return response.milestones ?? [];
579
+ }
580
+ /**
581
+ * Add a new milestone to a project.
582
+ * @throws {TestRailValidationError} When projectId is invalid
583
+ * @throws {TestRailApiError} When the API request fails
584
+ */
585
+ async addMilestone(projectId, payload) {
586
+ this.validateId(projectId, 'projectId');
587
+ return this.request('POST', `add_milestone/${projectId}`, payload);
588
+ }
589
+ /**
590
+ * Update an existing milestone.
591
+ * @throws {TestRailValidationError} When milestoneId is invalid
592
+ * @throws {TestRailApiError} When the API request fails
593
+ */
594
+ async updateMilestone(milestoneId, payload) {
595
+ this.validateId(milestoneId, 'milestoneId');
596
+ return this.request('POST', `update_milestone/${milestoneId}`, payload);
597
+ }
598
+ /**
599
+ * Delete a milestone.
600
+ * @throws {TestRailValidationError} When milestoneId is invalid
601
+ * @throws {TestRailApiError} When the API request fails
602
+ */
603
+ async deleteMilestone(milestoneId) {
604
+ this.validateId(milestoneId, 'milestoneId');
605
+ await this.request('POST', `delete_milestone/${milestoneId}`);
606
+ }
607
+ // ── Users ─────────────────────────────────────────────────────────────────
608
+ /**
609
+ * Get a user by ID.
610
+ * @throws {TestRailValidationError} When userId is invalid
611
+ * @throws {TestRailApiError} When the API request fails
612
+ */
613
+ async getUser(userId) {
614
+ this.validateId(userId, 'userId');
615
+ return this.request('GET', `get_user/${userId}`);
616
+ }
617
+ /**
618
+ * Get a user by email address.
619
+ * @throws {TestRailValidationError} When email format is invalid
620
+ * @throws {TestRailApiError} When the API request fails
621
+ */
622
+ async getUserByEmail(email) {
623
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
624
+ if (!emailRegex.test(email)) {
625
+ throw new TestRailValidationError('Invalid email format');
626
+ }
627
+ // buildEndpoint now encodes all values via encodeURIComponent internally.
628
+ return this.request('GET', this.buildEndpoint('get_user_by_email', { email }));
629
+ }
630
+ /**
631
+ * Get all users, optionally scoped to a project.
632
+ * @param limit - Maximum number of users to return
633
+ * @param offset - Number of users to skip
634
+ * @param projectId - When provided, returns only users with access to the specified project
635
+ * @throws {TestRailValidationError} When pagination or projectId is invalid
636
+ * @throws {TestRailApiError} When the API request fails
637
+ */
638
+ async getUsers(limit, offset, projectId) {
639
+ this.validatePaginationParams(limit, offset);
640
+ if (projectId !== undefined) {
641
+ this.validateId(projectId, 'projectId');
642
+ }
643
+ const endpoint = this.buildEndpoint(projectId !== undefined ? `get_users/${projectId}` : 'get_users', {
644
+ limit,
645
+ offset,
646
+ });
647
+ const response = await this.request('GET', endpoint);
648
+ return response.users ?? [];
649
+ }
650
+ /**
651
+ * Get the currently authenticated user.
652
+ * @throws {TestRailApiError} When the API request fails
653
+ */
654
+ async getCurrentUser() {
655
+ return this.request('GET', 'get_current_user');
656
+ }
657
+ // ── Statuses ──────────────────────────────────────────────────────────────
658
+ /**
659
+ * Get all test statuses.
660
+ * @throws {TestRailApiError} When the API request fails
661
+ */
662
+ async getStatuses() {
663
+ return this.request('GET', 'get_statuses');
664
+ }
665
+ // ── Priorities ────────────────────────────────────────────────────────────
666
+ /**
667
+ * Get all case priorities.
668
+ * @throws {TestRailApiError} When the API request fails
669
+ */
670
+ async getPriorities() {
671
+ return this.request('GET', 'get_priorities');
672
+ }
673
+ // ── Result Fields ─────────────────────────────────────────────────────────
674
+ /**
675
+ * Get all available custom result fields.
676
+ * @throws {TestRailApiError} When the API request fails
677
+ */
678
+ async getResultFields() {
679
+ return this.request('GET', 'get_result_fields');
680
+ }
681
+ // ── Case Fields & Types ───────────────────────────────────────────────────
682
+ /**
683
+ * Get all available custom case fields.
684
+ * @throws {TestRailApiError} When the API request fails
685
+ */
686
+ async getCaseFields() {
687
+ return this.request('GET', 'get_case_fields');
688
+ }
689
+ /**
690
+ * Get all available case types.
691
+ * @throws {TestRailApiError} When the API request fails
692
+ */
693
+ async getCaseTypes() {
694
+ return this.request('GET', 'get_case_types');
695
+ }
696
+ // ── Templates ─────────────────────────────────────────────────────────────
697
+ /**
698
+ * Get all available case templates for a project (requires TestRail 5.2+).
699
+ * @throws {TestRailValidationError} When projectId is invalid
700
+ * @throws {TestRailApiError} When the API request fails
701
+ */
702
+ async getTemplates(projectId) {
703
+ this.validateId(projectId, 'projectId');
704
+ return this.request('GET', `get_templates/${projectId}`);
705
+ }
706
+ // ── Configurations ────────────────────────────────────────────────────────
707
+ /**
708
+ * Get all configuration groups and their configurations for a project.
709
+ * @throws {TestRailValidationError} When projectId is invalid
710
+ * @throws {TestRailApiError} When the API request fails
711
+ */
712
+ async getConfigurations(projectId) {
713
+ this.validateId(projectId, 'projectId');
714
+ return this.request('GET', `get_configs/${projectId}`);
715
+ }
716
+ /**
717
+ * Add a new configuration group to a project.
718
+ * @throws {TestRailValidationError} When projectId is invalid
719
+ * @throws {TestRailApiError} When the API request fails
720
+ */
721
+ async addConfigurationGroup(projectId, payload) {
722
+ this.validateId(projectId, 'projectId');
723
+ return this.request('POST', `add_config_group/${projectId}`, payload);
724
+ }
725
+ /**
726
+ * Update an existing configuration group.
727
+ * @throws {TestRailValidationError} When configGroupId is invalid
728
+ * @throws {TestRailApiError} When the API request fails
729
+ */
730
+ async updateConfigurationGroup(configGroupId, payload) {
731
+ this.validateId(configGroupId, 'configGroupId');
732
+ return this.request('POST', `update_config_group/${configGroupId}`, payload);
733
+ }
734
+ /**
735
+ * Delete an existing configuration group and all its configurations.
736
+ * @throws {TestRailValidationError} When configGroupId is invalid
737
+ * @throws {TestRailApiError} When the API request fails
738
+ */
739
+ async deleteConfigurationGroup(configGroupId) {
740
+ this.validateId(configGroupId, 'configGroupId');
741
+ await this.request('POST', `delete_config_group/${configGroupId}`);
742
+ }
743
+ /**
744
+ * Add a new configuration to a configuration group.
745
+ * @throws {TestRailValidationError} When configGroupId is invalid
746
+ * @throws {TestRailApiError} When the API request fails
747
+ */
748
+ async addConfiguration(configGroupId, payload) {
749
+ this.validateId(configGroupId, 'configGroupId');
750
+ return this.request('POST', `add_config/${configGroupId}`, payload);
751
+ }
752
+ /**
753
+ * Update an existing configuration.
754
+ * @throws {TestRailValidationError} When configId is invalid
755
+ * @throws {TestRailApiError} When the API request fails
756
+ */
757
+ async updateConfiguration(configId, payload) {
758
+ this.validateId(configId, 'configId');
759
+ return this.request('POST', `update_config/${configId}`, payload);
760
+ }
761
+ /**
762
+ * Delete an existing configuration.
763
+ * @throws {TestRailValidationError} When configId is invalid
764
+ * @throws {TestRailApiError} When the API request fails
765
+ */
766
+ async deleteConfiguration(configId) {
767
+ this.validateId(configId, 'configId');
768
+ await this.request('POST', `delete_config/${configId}`);
769
+ }
770
+ // ── User Management (TASK-024, requires TestRail 7.3+) ────────────────────
771
+ /**
772
+ * Create a new TestRail user (requires TestRail 7.3+).
773
+ * @throws {TestRailApiError} When the API request fails
774
+ */
775
+ async addUser(payload) {
776
+ return this.request('POST', 'add_user', payload);
777
+ }
778
+ /**
779
+ * Update an existing TestRail user (requires TestRail 7.3+).
780
+ * @throws {TestRailValidationError} When userId is invalid
781
+ * @throws {TestRailApiError} When the API request fails
782
+ */
783
+ async updateUser(userId, payload) {
784
+ this.validateId(userId, 'userId');
785
+ return this.request('POST', `update_user/${userId}`, payload);
786
+ }
787
+ // ── Roles (TASK-025, requires TestRail 7.3+) ──────────────────────────────
788
+ /**
789
+ * Get all available user roles (requires TestRail 7.3+).
790
+ * @throws {TestRailApiError} When the API request fails
791
+ */
792
+ async getRoles() {
793
+ return this.request('GET', 'get_roles');
794
+ }
795
+ // ── Groups (TASK-026, requires TestRail 7.5+) ─────────────────────────────
796
+ /**
797
+ * Get a single group by ID (requires TestRail 7.5+).
798
+ * @throws {TestRailValidationError} When groupId is invalid
799
+ * @throws {TestRailApiError} When the API request fails
800
+ */
801
+ async getGroup(groupId) {
802
+ this.validateId(groupId, 'groupId');
803
+ return this.request('GET', `get_group/${groupId}`);
804
+ }
805
+ /**
806
+ * Get all groups (requires TestRail 7.5+).
807
+ * @throws {TestRailApiError} When the API request fails
808
+ */
809
+ async getGroups() {
810
+ return this.request('GET', 'get_groups');
811
+ }
812
+ /**
813
+ * Create a new group (requires TestRail 7.5+).
814
+ * @throws {TestRailApiError} When the API request fails
815
+ */
816
+ async addGroup(payload) {
817
+ return this.request('POST', 'add_group', payload);
818
+ }
819
+ /**
820
+ * Update an existing group (requires TestRail 7.5+).
821
+ * @throws {TestRailValidationError} When groupId is invalid
822
+ * @throws {TestRailApiError} When the API request fails
823
+ */
824
+ async updateGroup(groupId, payload) {
825
+ this.validateId(groupId, 'groupId');
826
+ return this.request('POST', `update_group/${groupId}`, payload);
827
+ }
828
+ /**
829
+ * Delete a group (requires TestRail 7.5+).
830
+ * @throws {TestRailValidationError} When groupId is invalid
831
+ * @throws {TestRailApiError} When the API request fails
832
+ */
833
+ async deleteGroup(groupId) {
834
+ this.validateId(groupId, 'groupId');
835
+ await this.request('POST', `delete_group/${groupId}`);
836
+ }
837
+ // ── Attachments (TASK-027) ────────────────────────────────────────────────
838
+ /**
839
+ * Get all attachments for a test case.
840
+ * @throws {TestRailValidationError} When caseId is invalid
841
+ * @throws {TestRailApiError} When the API request fails
842
+ */
843
+ async getAttachmentsForCase(caseId) {
844
+ this.validateId(caseId, 'caseId');
845
+ const response = await this.request('GET', `get_attachments_for_case/${caseId}`);
846
+ return response.attachments ?? [];
847
+ }
848
+ /**
849
+ * Get all attachments for a test run.
850
+ * @throws {TestRailValidationError} When runId is invalid
851
+ * @throws {TestRailApiError} When the API request fails
852
+ */
853
+ async getAttachmentsForRun(runId) {
854
+ this.validateId(runId, 'runId');
855
+ const response = await this.request('GET', `get_attachments_for_run/${runId}`);
856
+ return response.attachments ?? [];
857
+ }
858
+ /**
859
+ * Get all attachments for a test.
860
+ * @throws {TestRailValidationError} When testId is invalid
861
+ * @throws {TestRailApiError} When the API request fails
862
+ */
863
+ async getAttachmentsForTest(testId) {
864
+ this.validateId(testId, 'testId');
865
+ const response = await this.request('GET', `get_attachments_for_test/${testId}`);
866
+ return response.attachments ?? [];
867
+ }
868
+ /**
869
+ * Get all attachments for a test plan.
870
+ * @throws {TestRailValidationError} When planId is invalid
871
+ * @throws {TestRailApiError} When the API request fails
872
+ */
873
+ async getAttachmentsForPlan(planId) {
874
+ this.validateId(planId, 'planId');
875
+ const response = await this.request('GET', `get_attachments_for_plan/${planId}`);
876
+ return response.attachments ?? [];
877
+ }
878
+ /**
879
+ * Get all attachments for a specific plan entry.
880
+ * @throws {TestRailValidationError} When planId or entryId is invalid
881
+ * @throws {TestRailApiError} When the API request fails
882
+ */
883
+ async getAttachmentsForPlanEntry(planId, entryId) {
884
+ this.validateId(planId, 'planId');
885
+ this.validateId(entryId, 'entryId');
886
+ const response = await this.request('GET', `get_attachments_for_plan_entry/${planId}/${entryId}`);
887
+ return response.attachments ?? [];
888
+ }
889
+ /**
890
+ * Download the raw binary content of an attachment.
891
+ * @param attachmentId - The attachment ID (numeric)
892
+ * @throws {TestRailValidationError} When attachmentId is invalid
893
+ * @throws {TestRailApiError} When the API request fails
894
+ */
895
+ async getAttachment(attachmentId) {
896
+ this.validateId(attachmentId, 'attachmentId');
897
+ return this.requestBinary(`get_attachment/${attachmentId}`);
898
+ }
899
+ /**
900
+ * Upload a file attachment to a test case.
901
+ * @throws {TestRailValidationError} When caseId is invalid
902
+ * @throws {TestRailApiError} When the API request fails
903
+ */
904
+ async addAttachmentToCase(caseId, file, filename) {
905
+ this.validateId(caseId, 'caseId');
906
+ return this.requestMultipart(`add_attachment_to_case/${caseId}`, file, filename);
907
+ }
908
+ /**
909
+ * Upload a file attachment to a test result.
910
+ * @throws {TestRailValidationError} When resultId is invalid
911
+ * @throws {TestRailApiError} When the API request fails
912
+ */
913
+ async addAttachmentToResult(resultId, file, filename) {
914
+ this.validateId(resultId, 'resultId');
915
+ return this.requestMultipart(`add_attachment_to_result/${resultId}`, file, filename);
916
+ }
917
+ /**
918
+ * Upload a file attachment to a test run.
919
+ * @throws {TestRailValidationError} When runId is invalid
920
+ * @throws {TestRailApiError} When the API request fails
921
+ */
922
+ async addAttachmentToRun(runId, file, filename) {
923
+ this.validateId(runId, 'runId');
924
+ return this.requestMultipart(`add_attachment_to_run/${runId}`, file, filename);
925
+ }
926
+ /**
927
+ * Upload a file attachment to a test plan (requires TestRail 6.5.2+).
928
+ * @throws {TestRailValidationError} When planId is invalid
929
+ * @throws {TestRailApiError} When the API request fails
930
+ */
931
+ async addAttachmentToPlan(planId, file, filename) {
932
+ this.validateId(planId, 'planId');
933
+ return this.requestMultipart(`add_attachment_to_plan/${planId}`, file, filename);
934
+ }
935
+ /**
936
+ * Upload a file attachment to a specific plan entry (requires TestRail 6.5.2+).
937
+ * @throws {TestRailValidationError} When planId or entryId is invalid
938
+ * @throws {TestRailApiError} When the API request fails
939
+ */
940
+ async addAttachmentToPlanEntry(planId, entryId, file, filename) {
941
+ this.validateId(planId, 'planId');
942
+ this.validateId(entryId, 'entryId');
943
+ return this.requestMultipart(`add_attachment_to_plan_entry/${planId}/${entryId}`, file, filename);
944
+ }
945
+ /**
946
+ * Delete an attachment by ID.
947
+ * @throws {TestRailValidationError} When attachmentId is invalid
948
+ * @throws {TestRailApiError} When the API request fails
949
+ */
950
+ async deleteAttachment(attachmentId) {
951
+ this.validateId(attachmentId, 'attachmentId');
952
+ await this.request('POST', `delete_attachment/${attachmentId}`);
953
+ }
954
+ // ── Shared Steps (TASK-028, requires TestRail 7.0+) ───────────────────────
955
+ /**
956
+ * Get a single shared step by ID (requires TestRail 7.0+).
957
+ * @throws {TestRailValidationError} When sharedStepId is invalid
958
+ * @throws {TestRailApiError} When the API request fails
959
+ */
960
+ async getSharedStep(sharedStepId) {
961
+ this.validateId(sharedStepId, 'sharedStepId');
962
+ return this.request('GET', `get_shared_step/${sharedStepId}`);
963
+ }
964
+ /**
965
+ * Get all shared steps for a project (requires TestRail 7.0+).
966
+ * @throws {TestRailValidationError} When projectId is invalid
967
+ * @throws {TestRailApiError} When the API request fails
968
+ */
969
+ async getSharedSteps(projectId) {
970
+ this.validateId(projectId, 'projectId');
971
+ return this.request('GET', `get_shared_steps/${projectId}`);
972
+ }
973
+ /**
974
+ * Create a new shared step in a project (requires TestRail 7.0+).
975
+ * @throws {TestRailValidationError} When projectId is invalid
976
+ * @throws {TestRailApiError} When the API request fails
977
+ */
978
+ async addSharedStep(projectId, payload) {
979
+ this.validateId(projectId, 'projectId');
980
+ return this.request('POST', `add_shared_step/${projectId}`, payload);
981
+ }
982
+ /**
983
+ * Update an existing shared step (requires TestRail 7.0+).
984
+ * @throws {TestRailValidationError} When sharedStepId is invalid
985
+ * @throws {TestRailApiError} When the API request fails
986
+ */
987
+ async updateSharedStep(sharedStepId, payload) {
988
+ this.validateId(sharedStepId, 'sharedStepId');
989
+ return this.request('POST', `update_shared_step/${sharedStepId}`, payload);
990
+ }
991
+ /**
992
+ * Delete a shared step (requires TestRail 7.0+).
993
+ * @throws {TestRailValidationError} When sharedStepId is invalid
994
+ * @throws {TestRailApiError} When the API request fails
995
+ */
996
+ async deleteSharedStep(sharedStepId) {
997
+ this.validateId(sharedStepId, 'sharedStepId');
998
+ await this.request('POST', `delete_shared_step/${sharedStepId}`);
999
+ }
1000
+ // ── Variables (TASK-029) ──────────────────────────────────────────────────
1001
+ /**
1002
+ * Get all variables for a project.
1003
+ * @throws {TestRailValidationError} When projectId is invalid
1004
+ * @throws {TestRailApiError} When the API request fails
1005
+ */
1006
+ async getVariables(projectId) {
1007
+ this.validateId(projectId, 'projectId');
1008
+ return this.request('GET', `get_variables/${projectId}`);
1009
+ }
1010
+ /**
1011
+ * Create a new variable in a project.
1012
+ * @throws {TestRailValidationError} When projectId is invalid
1013
+ * @throws {TestRailApiError} When the API request fails
1014
+ */
1015
+ async addVariable(projectId, payload) {
1016
+ this.validateId(projectId, 'projectId');
1017
+ return this.request('POST', `add_variable/${projectId}`, payload);
1018
+ }
1019
+ /**
1020
+ * Update an existing variable.
1021
+ * @throws {TestRailValidationError} When variableId is invalid
1022
+ * @throws {TestRailApiError} When the API request fails
1023
+ */
1024
+ async updateVariable(variableId, payload) {
1025
+ this.validateId(variableId, 'variableId');
1026
+ return this.request('POST', `update_variable/${variableId}`, payload);
1027
+ }
1028
+ /**
1029
+ * Delete a variable.
1030
+ * @throws {TestRailValidationError} When variableId is invalid
1031
+ * @throws {TestRailApiError} When the API request fails
1032
+ */
1033
+ async deleteVariable(variableId) {
1034
+ this.validateId(variableId, 'variableId');
1035
+ await this.request('POST', `delete_variable/${variableId}`);
1036
+ }
1037
+ // ── Datasets (TASK-030) ───────────────────────────────────────────────────
1038
+ /**
1039
+ * Get a single dataset by ID.
1040
+ * @throws {TestRailValidationError} When datasetId is invalid
1041
+ * @throws {TestRailApiError} When the API request fails
1042
+ */
1043
+ async getDataset(datasetId) {
1044
+ this.validateId(datasetId, 'datasetId');
1045
+ return this.request('GET', `get_dataset/${datasetId}`);
1046
+ }
1047
+ /**
1048
+ * Get all datasets for a project.
1049
+ * @throws {TestRailValidationError} When projectId is invalid
1050
+ * @throws {TestRailApiError} When the API request fails
1051
+ */
1052
+ async getDatasets(projectId) {
1053
+ this.validateId(projectId, 'projectId');
1054
+ return this.request('GET', `get_datasets/${projectId}`);
1055
+ }
1056
+ /**
1057
+ * Create a new dataset in a project.
1058
+ * @throws {TestRailValidationError} When projectId is invalid
1059
+ * @throws {TestRailApiError} When the API request fails
1060
+ */
1061
+ async addDataset(projectId, payload) {
1062
+ this.validateId(projectId, 'projectId');
1063
+ return this.request('POST', `add_dataset/${projectId}`, payload);
1064
+ }
1065
+ /**
1066
+ * Update an existing dataset.
1067
+ * @throws {TestRailValidationError} When datasetId is invalid
1068
+ * @throws {TestRailApiError} When the API request fails
1069
+ */
1070
+ async updateDataset(datasetId, payload) {
1071
+ this.validateId(datasetId, 'datasetId');
1072
+ return this.request('POST', `update_dataset/${datasetId}`, payload);
1073
+ }
1074
+ /**
1075
+ * Delete a dataset.
1076
+ * @throws {TestRailValidationError} When datasetId is invalid
1077
+ * @throws {TestRailApiError} When the API request fails
1078
+ */
1079
+ async deleteDataset(datasetId) {
1080
+ this.validateId(datasetId, 'datasetId');
1081
+ await this.request('POST', `delete_dataset/${datasetId}`);
1082
+ }
1083
+ // ── Reports (TASK-031) ────────────────────────────────────────────────────
1084
+ /**
1085
+ * Get all available report templates for a project.
1086
+ * @throws {TestRailValidationError} When projectId is invalid
1087
+ * @throws {TestRailApiError} When the API request fails
1088
+ */
1089
+ async getReports(projectId) {
1090
+ this.validateId(projectId, 'projectId');
1091
+ return this.request('GET', `get_reports/${projectId}`);
1092
+ }
1093
+ /**
1094
+ * Execute a report template and return URLs to the generated output.
1095
+ * @throws {TestRailValidationError} When reportTemplateId is invalid
1096
+ * @throws {TestRailApiError} When the API request fails
1097
+ */
1098
+ async runReport(reportTemplateId) {
1099
+ this.validateId(reportTemplateId, 'reportTemplateId');
1100
+ return this.request('GET', `run_report/${reportTemplateId}`);
1101
+ }
1102
+ serializeIdList(ids) {
1103
+ return ids !== undefined && ids.length > 0 ? ids.join(',') : undefined;
1104
+ }
1105
+ }
1106
+ //# sourceMappingURL=client.js.map