xray-mcp 1.2.3 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,955 @@
1
+ import axios, { AxiosInstance } from 'axios';
2
+
3
+ export interface XrayConfig {
4
+ clientId: string;
5
+ clientSecret: string;
6
+ baseUrl?: string;
7
+ }
8
+
9
+ export interface XrayAuthResponse {
10
+ access_token: string;
11
+ expires_in: number;
12
+ }
13
+
14
+ export interface TestCase {
15
+ id?: string;
16
+ key?: string;
17
+ summary: string;
18
+ description?: string;
19
+ testType?: 'Manual' | 'Cucumber' | 'Generic';
20
+ projectKey: string;
21
+ labels?: string[];
22
+ components?: string[];
23
+ priority?: string;
24
+ status?: string;
25
+ }
26
+
27
+ export interface TestCaseResponse {
28
+ id: string;
29
+ key: string;
30
+ self: string;
31
+ }
32
+
33
+ export interface TestExecution {
34
+ summary: string;
35
+ projectKey: string;
36
+ testIssueIds?: string[];
37
+ testEnvironments?: string[];
38
+ description?: string;
39
+ }
40
+
41
+ export interface TestExecutionResponse {
42
+ issueId: string;
43
+ key: string;
44
+ testRuns?: TestRun[];
45
+ }
46
+
47
+ export interface TestRun {
48
+ id: string;
49
+ status: {
50
+ name: string;
51
+ description?: string;
52
+ };
53
+ test: {
54
+ issueId: string;
55
+ jira: any;
56
+ };
57
+ startedOn?: string;
58
+ finishedOn?: string;
59
+ executedBy?: string;
60
+ }
61
+
62
+ export type TestRunStatus = 'TODO' | 'EXECUTING' | 'PASS' | 'FAIL' | 'ABORTED' | 'PASSED' | 'FAILED';
63
+
64
+ export interface TestPlan {
65
+ summary: string;
66
+ projectKey: string;
67
+ testIssueIds?: string[];
68
+ description?: string;
69
+ }
70
+
71
+ export interface TestPlanResponse {
72
+ issueId: string;
73
+ key: string;
74
+ tests?: any[];
75
+ }
76
+
77
+ export interface TestSet {
78
+ summary: string;
79
+ projectKey: string;
80
+ testIssueIds?: string[];
81
+ description?: string;
82
+ }
83
+
84
+ export interface TestSetResponse {
85
+ issueId: string;
86
+ key: string;
87
+ tests?: any[];
88
+ }
89
+
90
+ export class XrayClient {
91
+ private config: XrayConfig;
92
+ private client: AxiosInstance;
93
+ private graphqlClient: AxiosInstance;
94
+ private accessToken: string | null = null;
95
+ private tokenExpiry: number = 0;
96
+
97
+ constructor(config: XrayConfig) {
98
+ this.config = {
99
+ ...config,
100
+ baseUrl: config.baseUrl || 'https://xray.cloud.getxray.app/api/v2'
101
+ };
102
+
103
+ this.client = axios.create({
104
+ baseURL: this.config.baseUrl,
105
+ headers: {
106
+ 'Content-Type': 'application/json'
107
+ }
108
+ });
109
+
110
+ this.graphqlClient = axios.create({
111
+ baseURL: this.config.baseUrl,
112
+ headers: {
113
+ 'Content-Type': 'application/json'
114
+ }
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Authenticate with Xray Cloud API and get access token
120
+ */
121
+ private async authenticate(): Promise<string> {
122
+ // Check if we have a valid token
123
+ if (this.accessToken && Date.now() < this.tokenExpiry) {
124
+ return this.accessToken;
125
+ }
126
+
127
+ try {
128
+ const response = await axios.post<string>(
129
+ 'https://xray.cloud.getxray.app/api/v1/authenticate',
130
+ {
131
+ client_id: this.config.clientId,
132
+ client_secret: this.config.clientSecret
133
+ },
134
+ {
135
+ headers: {
136
+ 'Content-Type': 'application/json'
137
+ }
138
+ }
139
+ );
140
+
141
+ this.accessToken = response.data;
142
+ // Set expiry to 23 hours from now (tokens are valid for 24 hours)
143
+ this.tokenExpiry = Date.now() + (23 * 60 * 60 * 1000);
144
+
145
+ return this.accessToken;
146
+ } catch (error) {
147
+ if (axios.isAxiosError(error)) {
148
+ throw new Error(`Authentication failed: ${error.response?.data || error.message}`);
149
+ }
150
+ throw error;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Make an authenticated request to the Xray API
156
+ */
157
+ private async request<T>(method: string, url: string, data?: any): Promise<T> {
158
+ const token = await this.authenticate();
159
+
160
+ try {
161
+ const response = await this.client.request<T>({
162
+ method,
163
+ url,
164
+ data,
165
+ headers: {
166
+ Authorization: `Bearer ${token}`
167
+ }
168
+ });
169
+
170
+ return response.data;
171
+ } catch (error) {
172
+ if (axios.isAxiosError(error)) {
173
+ throw new Error(`API request failed: ${error.response?.data?.error || error.message}`);
174
+ }
175
+ throw error;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Make a GraphQL query to Xray API
181
+ */
182
+ private async graphqlRequest<T>(query: string, variables?: any): Promise<T> {
183
+ const token = await this.authenticate();
184
+
185
+ try {
186
+ const response = await this.graphqlClient.post<{ data: T; errors?: any[] }>(
187
+ '/graphql',
188
+ {
189
+ query,
190
+ variables
191
+ },
192
+ {
193
+ headers: {
194
+ Authorization: `Bearer ${token}`
195
+ }
196
+ }
197
+ );
198
+
199
+ if (response.data.errors && response.data.errors.length > 0) {
200
+ throw new Error(`GraphQL errors: ${JSON.stringify(response.data.errors)}`);
201
+ }
202
+
203
+ return response.data.data;
204
+ } catch (error) {
205
+ if (axios.isAxiosError(error)) {
206
+ const errorDetails = error.response?.data
207
+ ? JSON.stringify(error.response.data)
208
+ : error.message;
209
+ throw new Error(`GraphQL request failed (${error.response?.status}): ${errorDetails}`);
210
+ }
211
+ throw error;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Create a new test case using GraphQL mutation
217
+ */
218
+ async createTestCase(testCase: TestCase): Promise<TestCaseResponse> {
219
+ const mutation = `
220
+ mutation CreateTest($jira: JSON!, $testType: UpdateTestTypeInput, $unstructured: String) {
221
+ createTest(jira: $jira, testType: $testType, unstructured: $unstructured) {
222
+ test {
223
+ issueId
224
+ jira(fields: ["key"])
225
+ }
226
+ warnings
227
+ }
228
+ }
229
+ `;
230
+
231
+ const jiraFields: any = {
232
+ fields: {
233
+ project: {
234
+ key: testCase.projectKey
235
+ },
236
+ summary: testCase.summary,
237
+ issuetype: {
238
+ name: 'Test'
239
+ }
240
+ }
241
+ };
242
+
243
+ if (testCase.description) {
244
+ jiraFields.fields.description = testCase.description;
245
+ }
246
+
247
+ if (testCase.labels && testCase.labels.length > 0) {
248
+ jiraFields.fields.labels = testCase.labels;
249
+ }
250
+
251
+ if (testCase.priority) {
252
+ jiraFields.fields.priority = { name: testCase.priority };
253
+ }
254
+
255
+ const variables: any = {
256
+ jira: jiraFields,
257
+ unstructured: testCase.description || ''
258
+ };
259
+
260
+ if (testCase.testType) {
261
+ variables.testType = {
262
+ name: testCase.testType
263
+ };
264
+ }
265
+
266
+ const result = await this.graphqlRequest<{ createTest: any }>(mutation, variables);
267
+
268
+ return {
269
+ id: result.createTest.test.issueId,
270
+ key: result.createTest.test.jira.key,
271
+ self: `https://your-jira-instance.atlassian.net/browse/${result.createTest.test.jira.key}`
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Get a test case by key using GraphQL
277
+ */
278
+ async getTestCase(testKey: string): Promise<any> {
279
+ const query = `
280
+ query GetTest($jql: String!, $limit: Int!) {
281
+ getTests(jql: $jql, limit: $limit) {
282
+ total
283
+ results {
284
+ issueId
285
+ projectId
286
+ jira(fields: ["key", "summary", "description", "priority", "status", "labels"])
287
+ testType {
288
+ name
289
+ kind
290
+ }
291
+ steps {
292
+ id
293
+ action
294
+ data
295
+ result
296
+ }
297
+ gherkin
298
+ unstructured
299
+ }
300
+ }
301
+ }
302
+ `;
303
+
304
+ const variables = {
305
+ jql: `key = '${testKey}'`,
306
+ limit: 1
307
+ };
308
+
309
+ const result = await this.graphqlRequest<{ getTests: any }>(query, variables);
310
+
311
+ if (result.getTests.total === 0) {
312
+ throw new Error(`Test case ${testKey} not found`);
313
+ }
314
+
315
+ return result.getTests.results[0];
316
+ }
317
+
318
+ /**
319
+ * Update a test case
320
+ * Note: Xray GraphQL API has specific mutations for test definition updates
321
+ * (updateUnstructuredTestDefinition, updateGherkinTestDefinition, etc.)
322
+ * For general Jira field updates (summary, description, labels), use Jira REST API directly
323
+ */
324
+ async updateTestCase(testKey: string, updates: Partial<TestCase>): Promise<void> {
325
+ throw new Error(
326
+ 'Direct test case update is not supported via Xray GraphQL API. ' +
327
+ 'Use Jira REST API to update standard fields (summary, description, labels, priority). ' +
328
+ 'Use specific Xray mutations for test definition updates: ' +
329
+ 'updateUnstructuredTestDefinition, updateGherkinTestDefinition, updateTestType, etc.'
330
+ );
331
+ }
332
+
333
+ /**
334
+ * Delete a test case using GraphQL mutation
335
+ * First retrieves the issueId from the test key, then deletes it
336
+ */
337
+ async deleteTestCase(testKey: string): Promise<void> {
338
+ // First, get the issueId from the test key
339
+ const test = await this.getTestCase(testKey);
340
+
341
+ const mutation = `
342
+ mutation DeleteTest($issueId: String!) {
343
+ deleteTest(issueId: $issueId)
344
+ }
345
+ `;
346
+
347
+ const variables = {
348
+ issueId: test.issueId
349
+ };
350
+
351
+ await this.graphqlRequest<{ deleteTest: string }>(mutation, variables);
352
+ }
353
+
354
+ /**
355
+ * Get test cases for a project using GraphQL
356
+ */
357
+ async getTestCasesByProject(projectKey: string, maxResults: number = 50): Promise<any> {
358
+ const jql = `project = '${projectKey}'`;
359
+ return this.searchTestCases(jql, maxResults);
360
+ }
361
+
362
+ /**
363
+ * Search test cases using JQL and GraphQL
364
+ */
365
+ async searchTestCases(jql: string, maxResults: number = 50): Promise<any> {
366
+ const query = `
367
+ query SearchTests($jql: String!, $limit: Int!) {
368
+ getTests(jql: $jql, limit: $limit) {
369
+ total
370
+ start
371
+ limit
372
+ results {
373
+ issueId
374
+ projectId
375
+ jira(fields: ["key", "summary", "description", "priority", "status", "labels"])
376
+ testType {
377
+ name
378
+ kind
379
+ }
380
+ }
381
+ }
382
+ }
383
+ `;
384
+
385
+ const variables = {
386
+ jql,
387
+ limit: maxResults
388
+ };
389
+
390
+ const result = await this.graphqlRequest<{ getTests: any }>(query, variables);
391
+ return result.getTests;
392
+ }
393
+
394
+ // ========================================
395
+ // Test Execution Methods
396
+ // ========================================
397
+
398
+ /**
399
+ * Create a new test execution using GraphQL mutation
400
+ */
401
+ async createTestExecution(testExecution: TestExecution): Promise<TestExecutionResponse> {
402
+ const mutation = `
403
+ mutation CreateTestExecution($jira: JSON!, $testIssueIds: [String], $testEnvironments: [String]) {
404
+ createTestExecution(jira: $jira, testIssueIds: $testIssueIds, testEnvironments: $testEnvironments) {
405
+ testExecution {
406
+ issueId
407
+ jira(fields: ["key", "summary"])
408
+ testRuns(limit: 100) {
409
+ results {
410
+ id
411
+ status {
412
+ name
413
+ description
414
+ }
415
+ test {
416
+ issueId
417
+ jira(fields: ["key", "summary"])
418
+ }
419
+ }
420
+ }
421
+ }
422
+ warnings
423
+ }
424
+ }
425
+ `;
426
+
427
+ const jiraFields: any = {
428
+ fields: {
429
+ project: {
430
+ key: testExecution.projectKey
431
+ },
432
+ summary: testExecution.summary,
433
+ issuetype: {
434
+ name: 'Test Execution'
435
+ }
436
+ }
437
+ };
438
+
439
+ if (testExecution.description) {
440
+ jiraFields.fields.description = testExecution.description;
441
+ }
442
+
443
+ const variables: any = {
444
+ jira: jiraFields
445
+ };
446
+
447
+ if (testExecution.testIssueIds && testExecution.testIssueIds.length > 0) {
448
+ variables.testIssueIds = testExecution.testIssueIds;
449
+ }
450
+
451
+ if (testExecution.testEnvironments && testExecution.testEnvironments.length > 0) {
452
+ variables.testEnvironments = testExecution.testEnvironments;
453
+ }
454
+
455
+ const result = await this.graphqlRequest<{ createTestExecution: any }>(mutation, variables);
456
+
457
+ return {
458
+ issueId: result.createTestExecution.testExecution.issueId,
459
+ key: result.createTestExecution.testExecution.jira.key,
460
+ testRuns: result.createTestExecution.testExecution.testRuns.results
461
+ };
462
+ }
463
+
464
+ /**
465
+ * Get a test execution by key using GraphQL
466
+ */
467
+ async getTestExecution(testExecutionKey: string): Promise<any> {
468
+ const query = `
469
+ query GetTestExecution($jql: String!, $limit: Int!) {
470
+ getTestExecutions(jql: $jql, limit: $limit) {
471
+ total
472
+ results {
473
+ issueId
474
+ projectId
475
+ jira(fields: ["key", "summary", "description", "status"])
476
+ testRuns(limit: 100) {
477
+ results {
478
+ id
479
+ status {
480
+ name
481
+ description
482
+ }
483
+ test {
484
+ issueId
485
+ jira(fields: ["key", "summary"])
486
+ }
487
+ startedOn
488
+ finishedOn
489
+ executedBy
490
+ }
491
+ }
492
+ }
493
+ }
494
+ }
495
+ `;
496
+
497
+ const variables = {
498
+ jql: `key = '${testExecutionKey}'`,
499
+ limit: 1
500
+ };
501
+
502
+ const result = await this.graphqlRequest<{ getTestExecutions: any }>(query, variables);
503
+
504
+ if (result.getTestExecutions.total === 0) {
505
+ throw new Error(`Test execution ${testExecutionKey} not found`);
506
+ }
507
+
508
+ return result.getTestExecutions.results[0];
509
+ }
510
+
511
+ /**
512
+ * Get test executions for a project using GraphQL
513
+ */
514
+ async getTestExecutionsByProject(projectKey: string, maxResults: number = 50): Promise<any> {
515
+ const jql = `project = '${projectKey}'`;
516
+ return this.searchTestExecutions(jql, maxResults);
517
+ }
518
+
519
+ /**
520
+ * Search test executions using JQL and GraphQL
521
+ */
522
+ async searchTestExecutions(jql: string, maxResults: number = 50): Promise<any> {
523
+ const query = `
524
+ query SearchTestExecutions($jql: String!, $limit: Int!) {
525
+ getTestExecutions(jql: $jql, limit: $limit) {
526
+ total
527
+ start
528
+ limit
529
+ results {
530
+ issueId
531
+ projectId
532
+ jira(fields: ["key", "summary", "description", "status", "created", "updated"])
533
+ testRuns(limit: 100) {
534
+ total
535
+ results {
536
+ id
537
+ status {
538
+ name
539
+ description
540
+ }
541
+ test {
542
+ issueId
543
+ jira(fields: ["key", "summary"])
544
+ }
545
+ }
546
+ }
547
+ }
548
+ }
549
+ }
550
+ `;
551
+
552
+ const variables = {
553
+ jql,
554
+ limit: maxResults
555
+ };
556
+
557
+ const result = await this.graphqlRequest<{ getTestExecutions: any }>(query, variables);
558
+ return result.getTestExecutions;
559
+ }
560
+
561
+ /**
562
+ * Update the status of a test run using GraphQL mutation
563
+ */
564
+ async updateTestRunStatus(testRunId: string, status: TestRunStatus): Promise<string> {
565
+ const mutation = `
566
+ mutation UpdateTestRunStatus($id: String!, $status: String!) {
567
+ updateTestRunStatus(id: $id, status: $status)
568
+ }
569
+ `;
570
+
571
+ const variables = {
572
+ id: testRunId,
573
+ status
574
+ };
575
+
576
+ const result = await this.graphqlRequest<{ updateTestRunStatus: string }>(mutation, variables);
577
+ return result.updateTestRunStatus;
578
+ }
579
+
580
+ // ========================================
581
+ // Test Plan Methods
582
+ // ========================================
583
+
584
+ /**
585
+ * Create a new test plan using GraphQL mutation
586
+ */
587
+ async createTestPlan(testPlan: TestPlan): Promise<TestPlanResponse> {
588
+ const mutation = `
589
+ mutation CreateTestPlan($jira: JSON!, $testIssueIds: [String]) {
590
+ createTestPlan(jira: $jira, testIssueIds: $testIssueIds) {
591
+ testPlan {
592
+ issueId
593
+ jira(fields: ["key", "summary"])
594
+ tests(limit: 100) {
595
+ results {
596
+ issueId
597
+ jira(fields: ["key", "summary"])
598
+ }
599
+ }
600
+ }
601
+ warnings
602
+ }
603
+ }
604
+ `;
605
+
606
+ const jiraFields: any = {
607
+ fields: {
608
+ project: {
609
+ key: testPlan.projectKey
610
+ },
611
+ summary: testPlan.summary,
612
+ issuetype: {
613
+ name: 'Test Plan'
614
+ }
615
+ }
616
+ };
617
+
618
+ if (testPlan.description) {
619
+ jiraFields.fields.description = testPlan.description;
620
+ }
621
+
622
+ const variables: any = {
623
+ jira: jiraFields
624
+ };
625
+
626
+ if (testPlan.testIssueIds && testPlan.testIssueIds.length > 0) {
627
+ variables.testIssueIds = testPlan.testIssueIds;
628
+ }
629
+
630
+ const result = await this.graphqlRequest<{ createTestPlan: any }>(mutation, variables);
631
+
632
+ return {
633
+ issueId: result.createTestPlan.testPlan.issueId,
634
+ key: result.createTestPlan.testPlan.jira.key,
635
+ tests: result.createTestPlan.testPlan.tests?.results || []
636
+ };
637
+ }
638
+
639
+ /**
640
+ * Get a test plan by key using GraphQL
641
+ */
642
+ async getTestPlan(testPlanKey: string): Promise<any> {
643
+ const query = `
644
+ query GetTestPlan($jql: String!, $limit: Int!) {
645
+ getTestPlans(jql: $jql, limit: $limit) {
646
+ total
647
+ results {
648
+ issueId
649
+ projectId
650
+ jira(fields: ["key", "summary", "description", "status"])
651
+ tests(limit: 100) {
652
+ total
653
+ results {
654
+ issueId
655
+ jira(fields: ["key", "summary", "status"])
656
+ testType {
657
+ name
658
+ kind
659
+ }
660
+ }
661
+ }
662
+ }
663
+ }
664
+ }
665
+ `;
666
+
667
+ const variables = {
668
+ jql: `key = '${testPlanKey}'`,
669
+ limit: 1
670
+ };
671
+
672
+ const result = await this.graphqlRequest<{ getTestPlans: any }>(query, variables);
673
+
674
+ if (result.getTestPlans.total === 0) {
675
+ throw new Error(`Test plan ${testPlanKey} not found`);
676
+ }
677
+
678
+ return result.getTestPlans.results[0];
679
+ }
680
+
681
+ /**
682
+ * Search test plans using JQL and GraphQL
683
+ */
684
+ async searchTestPlans(jql: string, maxResults: number = 50): Promise<any> {
685
+ const query = `
686
+ query SearchTestPlans($jql: String!, $limit: Int!) {
687
+ getTestPlans(jql: $jql, limit: $limit) {
688
+ total
689
+ start
690
+ limit
691
+ results {
692
+ issueId
693
+ projectId
694
+ jira(fields: ["key", "summary", "description", "status", "created", "updated"])
695
+ tests(limit: 10) {
696
+ total
697
+ results {
698
+ issueId
699
+ jira(fields: ["key", "summary"])
700
+ }
701
+ }
702
+ }
703
+ }
704
+ }
705
+ `;
706
+
707
+ const variables = {
708
+ jql,
709
+ limit: maxResults
710
+ };
711
+
712
+ const result = await this.graphqlRequest<{ getTestPlans: any }>(query, variables);
713
+ return result.getTestPlans;
714
+ }
715
+
716
+ /**
717
+ * Get test plans for a project using GraphQL
718
+ */
719
+ async getTestPlansByProject(projectKey: string, maxResults: number = 50): Promise<any> {
720
+ const jql = `project = '${projectKey}'`;
721
+ return this.searchTestPlans(jql, maxResults);
722
+ }
723
+
724
+ /**
725
+ * Add tests to a test plan using GraphQL mutation
726
+ */
727
+ async addTestsToTestPlan(testPlanIssueId: string, testIssueIds: string[]): Promise<any> {
728
+ const mutation = `
729
+ mutation AddTestsToTestPlan($issueId: String!, $testIssueIds: [String]!) {
730
+ addTestsToTestPlan(issueId: $issueId, testIssueIds: $testIssueIds) {
731
+ addedTests
732
+ warning
733
+ }
734
+ }
735
+ `;
736
+
737
+ const variables = {
738
+ issueId: testPlanIssueId,
739
+ testIssueIds
740
+ };
741
+
742
+ const result = await this.graphqlRequest<{ addTestsToTestPlan: any }>(mutation, variables);
743
+ return result.addTestsToTestPlan;
744
+ }
745
+
746
+ /**
747
+ * Remove tests from a test plan using GraphQL mutation
748
+ */
749
+ async removeTestsFromTestPlan(testPlanIssueId: string, testIssueIds: string[]): Promise<any> {
750
+ const mutation = `
751
+ mutation RemoveTestsFromTestPlan($issueId: String!, $testIssueIds: [String]!) {
752
+ removeTestsFromTestPlan(issueId: $issueId, testIssueIds: $testIssueIds) {
753
+ removedTests
754
+ warning
755
+ }
756
+ }
757
+ `;
758
+
759
+ const variables = {
760
+ issueId: testPlanIssueId,
761
+ testIssueIds
762
+ };
763
+
764
+ const result = await this.graphqlRequest<{ removeTestsFromTestPlan: any }>(mutation, variables);
765
+ return result.removeTestsFromTestPlan;
766
+ }
767
+
768
+ // ========================================
769
+ // Test Set Methods
770
+ // ========================================
771
+
772
+ /**
773
+ * Create a new test set using GraphQL mutation
774
+ */
775
+ async createTestSet(testSet: TestSet): Promise<TestSetResponse> {
776
+ const mutation = `
777
+ mutation CreateTestSet($jira: JSON!, $testIssueIds: [String]) {
778
+ createTestSet(jira: $jira, testIssueIds: $testIssueIds) {
779
+ testSet {
780
+ issueId
781
+ jira(fields: ["key", "summary"])
782
+ tests(limit: 100) {
783
+ results {
784
+ issueId
785
+ jira(fields: ["key", "summary"])
786
+ }
787
+ }
788
+ }
789
+ warnings
790
+ }
791
+ }
792
+ `;
793
+
794
+ const jiraFields: any = {
795
+ fields: {
796
+ project: {
797
+ key: testSet.projectKey
798
+ },
799
+ summary: testSet.summary,
800
+ issuetype: {
801
+ name: 'Test Set'
802
+ }
803
+ }
804
+ };
805
+
806
+ if (testSet.description) {
807
+ jiraFields.fields.description = testSet.description;
808
+ }
809
+
810
+ const variables: any = {
811
+ jira: jiraFields
812
+ };
813
+
814
+ if (testSet.testIssueIds && testSet.testIssueIds.length > 0) {
815
+ variables.testIssueIds = testSet.testIssueIds;
816
+ }
817
+
818
+ const result = await this.graphqlRequest<{ createTestSet: any }>(mutation, variables);
819
+
820
+ return {
821
+ issueId: result.createTestSet.testSet.issueId,
822
+ key: result.createTestSet.testSet.jira.key,
823
+ tests: result.createTestSet.testSet.tests?.results || []
824
+ };
825
+ }
826
+
827
+ /**
828
+ * Get a test set by key using GraphQL
829
+ */
830
+ async getTestSet(testSetKey: string): Promise<any> {
831
+ const query = `
832
+ query GetTestSet($jql: String!, $limit: Int!) {
833
+ getTestSets(jql: $jql, limit: $limit) {
834
+ total
835
+ results {
836
+ issueId
837
+ projectId
838
+ jira(fields: ["key", "summary", "description", "status"])
839
+ tests(limit: 100) {
840
+ total
841
+ results {
842
+ issueId
843
+ jira(fields: ["key", "summary", "status"])
844
+ testType {
845
+ name
846
+ kind
847
+ }
848
+ }
849
+ }
850
+ }
851
+ }
852
+ }
853
+ `;
854
+
855
+ const variables = {
856
+ jql: `key = '${testSetKey}'`,
857
+ limit: 1
858
+ };
859
+
860
+ const result = await this.graphqlRequest<{ getTestSets: any }>(query, variables);
861
+
862
+ if (result.getTestSets.total === 0) {
863
+ throw new Error(`Test set ${testSetKey} not found`);
864
+ }
865
+
866
+ return result.getTestSets.results[0];
867
+ }
868
+
869
+ /**
870
+ * Search test sets using JQL and GraphQL
871
+ */
872
+ async searchTestSets(jql: string, maxResults: number = 50): Promise<any> {
873
+ const query = `
874
+ query SearchTestSets($jql: String!, $limit: Int!) {
875
+ getTestSets(jql: $jql, limit: $limit) {
876
+ total
877
+ start
878
+ limit
879
+ results {
880
+ issueId
881
+ projectId
882
+ jira(fields: ["key", "summary", "description", "status", "created", "updated"])
883
+ tests(limit: 10) {
884
+ total
885
+ results {
886
+ issueId
887
+ jira(fields: ["key", "summary"])
888
+ }
889
+ }
890
+ }
891
+ }
892
+ }
893
+ `;
894
+
895
+ const variables = {
896
+ jql,
897
+ limit: maxResults
898
+ };
899
+
900
+ const result = await this.graphqlRequest<{ getTestSets: any }>(query, variables);
901
+ return result.getTestSets;
902
+ }
903
+
904
+ /**
905
+ * Get test sets for a project using GraphQL
906
+ */
907
+ async getTestSetsByProject(projectKey: string, maxResults: number = 50): Promise<any> {
908
+ const jql = `project = '${projectKey}'`;
909
+ return this.searchTestSets(jql, maxResults);
910
+ }
911
+
912
+ /**
913
+ * Add tests to a test set using GraphQL mutation
914
+ */
915
+ async addTestsToTestSet(testSetIssueId: string, testIssueIds: string[]): Promise<any> {
916
+ const mutation = `
917
+ mutation AddTestsToTestSet($issueId: String!, $testIssueIds: [String]!) {
918
+ addTestsToTestSet(issueId: $issueId, testIssueIds: $testIssueIds) {
919
+ addedTests
920
+ warning
921
+ }
922
+ }
923
+ `;
924
+
925
+ const variables = {
926
+ issueId: testSetIssueId,
927
+ testIssueIds
928
+ };
929
+
930
+ const result = await this.graphqlRequest<{ addTestsToTestSet: any }>(mutation, variables);
931
+ return result.addTestsToTestSet;
932
+ }
933
+
934
+ /**
935
+ * Remove tests from a test set using GraphQL mutation
936
+ */
937
+ async removeTestsFromTestSet(testSetIssueId: string, testIssueIds: string[]): Promise<any> {
938
+ const mutation = `
939
+ mutation RemoveTestsFromTestSet($issueId: String!, $testIssueIds: [String]!) {
940
+ removeTestsFromTestSet(issueId: $issueId, testIssueIds: $testIssueIds) {
941
+ removedTests
942
+ warning
943
+ }
944
+ }
945
+ `;
946
+
947
+ const variables = {
948
+ issueId: testSetIssueId,
949
+ testIssueIds
950
+ };
951
+
952
+ const result = await this.graphqlRequest<{ removeTestsFromTestSet: any }>(mutation, variables);
953
+ return result.removeTestsFromTestSet;
954
+ }
955
+ }