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