@elisra-devops/docgen-data-provider 1.63.12 → 1.67.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.
Files changed (94) hide show
  1. package/.github/workflows/ci.yml +26 -9
  2. package/.github/workflows/release.yml +9 -10
  3. package/bin/helpers/tfs.d.ts +3 -0
  4. package/bin/helpers/tfs.js +44 -7
  5. package/bin/helpers/tfs.js.map +1 -1
  6. package/bin/modules/GitDataProvider.d.ts +10 -0
  7. package/bin/modules/GitDataProvider.js +10 -0
  8. package/bin/modules/GitDataProvider.js.map +1 -1
  9. package/bin/modules/MangementDataProvider.js +7 -1
  10. package/bin/modules/MangementDataProvider.js.map +1 -1
  11. package/bin/modules/TestDataProvider.js +0 -1
  12. package/bin/modules/TestDataProvider.js.map +1 -1
  13. package/bin/modules/TicketsDataProvider.d.ts +63 -27
  14. package/bin/modules/TicketsDataProvider.js +226 -122
  15. package/bin/modules/TicketsDataProvider.js.map +1 -1
  16. package/bin/tests/helpers/helper.test.js +279 -0
  17. package/bin/tests/helpers/helper.test.js.map +1 -0
  18. package/bin/{helpers/test → tests/helpers}/tfs.test.js +312 -49
  19. package/bin/tests/helpers/tfs.test.js.map +1 -0
  20. package/bin/tests/index.test.js +25 -0
  21. package/bin/tests/index.test.js.map +1 -0
  22. package/bin/tests/models/tfs-data.test.js +160 -0
  23. package/bin/tests/models/tfs-data.test.js.map +1 -0
  24. package/bin/{modules/test → tests/modules}/JfrogDataProvider.test.js +9 -9
  25. package/bin/tests/modules/JfrogDataProvider.test.js.map +1 -0
  26. package/bin/tests/modules/ResultDataProvider.test.js +1942 -0
  27. package/bin/tests/modules/ResultDataProvider.test.js.map +1 -0
  28. package/bin/tests/modules/gitDataProvider.test.js +1888 -0
  29. package/bin/tests/modules/gitDataProvider.test.js.map +1 -0
  30. package/bin/{modules/test → tests/modules}/managmentDataProvider.test.js +39 -31
  31. package/bin/tests/modules/managmentDataProvider.test.js.map +1 -0
  32. package/bin/tests/modules/pipelineDataProvider.test.d.ts +1 -0
  33. package/bin/tests/modules/pipelineDataProvider.test.js +783 -0
  34. package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -0
  35. package/bin/tests/modules/testDataProvider.test.d.ts +1 -0
  36. package/bin/tests/modules/testDataProvider.test.js +717 -0
  37. package/bin/tests/modules/testDataProvider.test.js.map +1 -0
  38. package/bin/tests/modules/ticketsDataProvider.test.d.ts +1 -0
  39. package/bin/tests/modules/ticketsDataProvider.test.js +1681 -0
  40. package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -0
  41. package/bin/tests/utils/DataProviderUtils.test.d.ts +1 -0
  42. package/bin/tests/utils/DataProviderUtils.test.js +61 -0
  43. package/bin/tests/utils/DataProviderUtils.test.js.map +1 -0
  44. package/bin/tests/utils/testStepParserHelper.test.d.ts +1 -0
  45. package/bin/tests/utils/testStepParserHelper.test.js +359 -0
  46. package/bin/tests/utils/testStepParserHelper.test.js.map +1 -0
  47. package/package.json +10 -1
  48. package/src/helpers/tfs.ts +51 -7
  49. package/src/modules/GitDataProvider.ts +10 -0
  50. package/src/modules/MangementDataProvider.ts +6 -1
  51. package/src/modules/TestDataProvider.ts +0 -1
  52. package/src/modules/TicketsDataProvider.ts +311 -151
  53. package/src/tests/helpers/helper.test.ts +337 -0
  54. package/src/tests/helpers/tfs.test.ts +1092 -0
  55. package/src/tests/index.test.ts +28 -0
  56. package/src/tests/models/tfs-data.test.ts +203 -0
  57. package/src/tests/modules/JfrogDataProvider.test.ts +167 -0
  58. package/src/tests/modules/ResultDataProvider.test.ts +2571 -0
  59. package/src/tests/modules/gitDataProvider.test.ts +2628 -0
  60. package/src/{modules/test → tests/modules}/managmentDataProvider.test.ts +63 -32
  61. package/src/tests/modules/pipelineDataProvider.test.ts +1038 -0
  62. package/src/tests/modules/testDataProvider.test.ts +1046 -0
  63. package/src/tests/modules/ticketsDataProvider.test.ts +2204 -0
  64. package/src/tests/utils/DataProviderUtils.test.ts +76 -0
  65. package/src/tests/utils/testStepParserHelper.test.ts +437 -0
  66. package/tsconfig.json +1 -0
  67. package/bin/helpers/test/tfs.test.js.map +0 -1
  68. package/bin/modules/test/JfrogDataProvider.test.js.map +0 -1
  69. package/bin/modules/test/ResultDataProvider.test.js +0 -444
  70. package/bin/modules/test/ResultDataProvider.test.js.map +0 -1
  71. package/bin/modules/test/gitDataProvider.test.js +0 -433
  72. package/bin/modules/test/gitDataProvider.test.js.map +0 -1
  73. package/bin/modules/test/managmentDataProvider.test.js.map +0 -1
  74. package/bin/modules/test/pipelineDataProvider.test.js +0 -237
  75. package/bin/modules/test/pipelineDataProvider.test.js.map +0 -1
  76. package/bin/modules/test/testDataProvider.test.js +0 -234
  77. package/bin/modules/test/testDataProvider.test.js.map +0 -1
  78. package/bin/modules/test/ticketsDataProvider.test.js +0 -322
  79. package/bin/modules/test/ticketsDataProvider.test.js.map +0 -1
  80. package/src/helpers/test/tfs.test.ts +0 -748
  81. package/src/modules/test/JfrogDataProvider.test.ts +0 -171
  82. package/src/modules/test/ResultDataProvider.test.ts +0 -542
  83. package/src/modules/test/gitDataProvider.test.ts +0 -691
  84. package/src/modules/test/pipelineDataProvider.test.ts +0 -292
  85. package/src/modules/test/testDataProvider.test.ts +0 -318
  86. package/src/modules/test/ticketsDataProvider.test.ts +0 -434
  87. /package/bin/{helpers/test/tfs.test.d.ts → tests/helpers/helper.test.d.ts} +0 -0
  88. /package/bin/{modules/test/JfrogDataProvider.test.d.ts → tests/helpers/tfs.test.d.ts} +0 -0
  89. /package/bin/{modules/test/ResultDataProvider.test.d.ts → tests/index.test.d.ts} +0 -0
  90. /package/bin/{modules/test/gitDataProvider.test.d.ts → tests/models/tfs-data.test.d.ts} +0 -0
  91. /package/bin/{modules/test/managmentDataProvider.test.d.ts → tests/modules/JfrogDataProvider.test.d.ts} +0 -0
  92. /package/bin/{modules/test/pipelineDataProvider.test.d.ts → tests/modules/ResultDataProvider.test.d.ts} +0 -0
  93. /package/bin/{modules/test/testDataProvider.test.d.ts → tests/modules/gitDataProvider.test.d.ts} +0 -0
  94. /package/bin/{modules/test/ticketsDataProvider.test.d.ts → tests/modules/managmentDataProvider.test.d.ts} +0 -0
@@ -0,0 +1,1681 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tfs_1 = require("../../helpers/tfs");
4
+ const TicketsDataProvider_1 = require("../../modules/TicketsDataProvider");
5
+ const logger_1 = require("../../utils/logger");
6
+ const tfs_data_1 = require("../../models/tfs-data");
7
+ jest.mock('../../helpers/tfs');
8
+ jest.mock('../../utils/logger');
9
+ jest.mock('../../helpers/helper');
10
+ describe('TicketsDataProvider', () => {
11
+ let ticketsDataProvider;
12
+ const mockOrgUrl = 'https://dev.azure.com/organization/';
13
+ const mockToken = 'mock-token';
14
+ const mockProject = 'test-project';
15
+ beforeEach(() => {
16
+ jest.clearAllMocks();
17
+ ticketsDataProvider = new TicketsDataProvider_1.default(mockOrgUrl, mockToken);
18
+ });
19
+ describe('isLinkSideAllowedByTypeOrId', () => {
20
+ it('should accept when allowedTypes is empty and field is present', async () => {
21
+ const wiql = "SELECT * FROM WorkItemLinks WHERE Source.[System.WorkItemType] = 'Epic'";
22
+ const result = await ticketsDataProvider.isLinkSideAllowedByTypeOrId({}, wiql, 'Source', [], new Map());
23
+ expect(result).toBe(true);
24
+ });
25
+ it('should return false when allowedTypes is empty but field is not present', async () => {
26
+ const wiql = 'SELECT * FROM WorkItems';
27
+ const result = await ticketsDataProvider.isLinkSideAllowedByTypeOrId({}, wiql, 'Source', [], new Map());
28
+ expect(result).toBe(false);
29
+ });
30
+ it('should support equality operator and reject disallowed types', async () => {
31
+ const wiql = "SELECT * FROM WorkItemLinks WHERE Source.[System.WorkItemType] = 'Bug'";
32
+ const result = await ticketsDataProvider.isLinkSideAllowedByTypeOrId({}, wiql, 'Source', ['Epic', 'Feature'], new Map());
33
+ expect(result).toBe(false);
34
+ });
35
+ it('should support IN operator when all types are allowed', async () => {
36
+ const wiql = "SELECT * FROM WorkItemLinks WHERE Target.[System.WorkItemType] IN ('Requirement', 'Bug')";
37
+ const result = await ticketsDataProvider.isLinkSideAllowedByTypeOrId({}, wiql, 'Target', ['Requirement', 'Bug'], new Map());
38
+ expect(result).toBe(true);
39
+ });
40
+ it('should return false when no types are found in WIQL and no ids exist', async () => {
41
+ const wiql = "SELECT * FROM WorkItemLinks WHERE Target.[System.AreaPath] = 'A'";
42
+ const result = await ticketsDataProvider.isLinkSideAllowedByTypeOrId({}, wiql, 'Target', ['Bug'], new Map());
43
+ expect(result).toBe(false);
44
+ });
45
+ });
46
+ describe('findChildFolderByPossibleNames', () => {
47
+ it('should return null when parent is missing or possibleNames is empty', async () => {
48
+ await expect(ticketsDataProvider.findChildFolderByPossibleNames(null, ['a'])).resolves.toBeNull();
49
+ await expect(ticketsDataProvider.findChildFolderByPossibleNames({ hasChildren: false }, [])).resolves.toBeNull();
50
+ });
51
+ it('should prefer exact match over partial match and return the enriched folder', async () => {
52
+ const parent = {
53
+ id: 'p',
54
+ isFolder: true,
55
+ name: 'Parent',
56
+ hasChildren: true,
57
+ children: [
58
+ { id: 'c1', isFolder: true, name: 'Requirement - Test', hasChildren: false },
59
+ { id: 'c2', isFolder: true, name: 'requirement to test case', hasChildren: false },
60
+ ],
61
+ };
62
+ const ensureSpy = jest
63
+ .spyOn(ticketsDataProvider, 'ensureQueryChildren')
64
+ .mockImplementation(async (node) => node);
65
+ const result = await ticketsDataProvider.findChildFolderByPossibleNames(parent, [
66
+ 'requirement - test',
67
+ 'req',
68
+ ]);
69
+ expect(result).toBe(parent.children[0]);
70
+ expect(ensureSpy).toHaveBeenCalled();
71
+ });
72
+ it('should return first partial match when no exact match exists', async () => {
73
+ const parent = {
74
+ id: 'p',
75
+ isFolder: true,
76
+ name: 'Parent',
77
+ hasChildren: true,
78
+ children: [
79
+ { id: 'c1', isFolder: true, name: 'Some Req Folder', hasChildren: false },
80
+ { id: 'c2', isFolder: true, name: 'Other', hasChildren: false },
81
+ ],
82
+ };
83
+ jest
84
+ .spyOn(ticketsDataProvider, 'ensureQueryChildren')
85
+ .mockImplementation(async (node) => node);
86
+ const result = await ticketsDataProvider.findChildFolderByPossibleNames(parent, ['req']);
87
+ expect(result).toBe(parent.children[0]);
88
+ });
89
+ it('should BFS into nested folders and ignore non-folder children and visited duplicates', async () => {
90
+ const leaf = { id: 'leaf', isFolder: true, name: 'Deep Match', hasChildren: false, children: [] };
91
+ const parent = {
92
+ id: 'p',
93
+ isFolder: true,
94
+ name: 'Parent',
95
+ hasChildren: true,
96
+ children: [
97
+ { id: 'a', isFolder: true, name: 'A', hasChildren: true },
98
+ { id: 'b', isFolder: true, name: 'B', hasChildren: true },
99
+ ],
100
+ };
101
+ jest.spyOn(ticketsDataProvider, 'ensureQueryChildren').mockImplementation(async (node) => {
102
+ if ((node === null || node === void 0 ? void 0 : node.id) === 'a') {
103
+ return Object.assign(Object.assign({}, node), { children: [{ id: 'not-folder', isFolder: false, name: 'NF' }, leaf, leaf] });
104
+ }
105
+ if ((node === null || node === void 0 ? void 0 : node.id) === 'b') {
106
+ return Object.assign(Object.assign({}, node), { children: [leaf] });
107
+ }
108
+ return node;
109
+ });
110
+ const found = await ticketsDataProvider.findChildFolderByPossibleNames(parent, ['deep match']);
111
+ expect(found).toEqual(leaf);
112
+ });
113
+ });
114
+ describe('fetchWithAncestorFallback', () => {
115
+ it('should short-circuit when validator passes on the first candidate', async () => {
116
+ const root = { id: 'root', name: 'Root', hasChildren: false, children: [] };
117
+ const starting = { id: 'start', name: 'Start', hasChildren: false, children: [] };
118
+ jest.spyOn(ticketsDataProvider, 'ensureQueryChildren').mockImplementation(async (n) => n);
119
+ jest.spyOn(ticketsDataProvider, 'buildFallbackChain').mockResolvedValueOnce([starting, root]);
120
+ const fetcher = jest.fn().mockResolvedValueOnce({ ok: true });
121
+ const validator = jest.fn().mockReturnValueOnce(true);
122
+ const res = await ticketsDataProvider.fetchWithAncestorFallback(root, starting, fetcher, 'ctx', validator);
123
+ expect(res.usedFolder).toBe(starting);
124
+ expect(fetcher).toHaveBeenCalledTimes(1);
125
+ expect(validator).toHaveBeenCalledWith({ ok: true });
126
+ });
127
+ it('should fall back through all candidates and return last attempt when validator never passes', async () => {
128
+ const root = { id: 'root', name: 'Root', hasChildren: false, children: [] };
129
+ const a = { id: 'a', name: 'A', hasChildren: false, children: [] };
130
+ const b = { id: 'b', name: 'B', hasChildren: false, children: [] };
131
+ jest.spyOn(ticketsDataProvider, 'ensureQueryChildren').mockImplementation(async (n) => n);
132
+ jest.spyOn(ticketsDataProvider, 'buildFallbackChain').mockResolvedValueOnce([a, b, root]);
133
+ const fetcher = jest
134
+ .fn()
135
+ .mockResolvedValueOnce(null)
136
+ .mockResolvedValueOnce({})
137
+ .mockResolvedValueOnce({ last: true });
138
+ const validator = jest.fn().mockReturnValue(false);
139
+ const res = await ticketsDataProvider.fetchWithAncestorFallback(root, a, fetcher, 'ctx', validator);
140
+ expect(res.usedFolder).toBe(root);
141
+ expect(res.result).toEqual({ last: true });
142
+ expect(fetcher).toHaveBeenCalledTimes(3);
143
+ });
144
+ });
145
+ describe('FetchImageAsBase64', () => {
146
+ it('should fetch image as base64', async () => {
147
+ // Arrange
148
+ const mockUrl = 'https://example.com/image.jpg';
149
+ const mockBase64 = 'base64-encoded-image';
150
+ tfs_1.TFSServices.fetchAzureDevOpsImageAsBase64.mockResolvedValueOnce(mockBase64);
151
+ // Act
152
+ const result = await ticketsDataProvider.FetchImageAsBase64(mockUrl);
153
+ // Assert
154
+ expect(tfs_1.TFSServices.fetchAzureDevOpsImageAsBase64).toHaveBeenCalledWith(mockUrl, mockToken, 'get', null);
155
+ expect(result).toBe(mockBase64);
156
+ });
157
+ });
158
+ describe('GetWorkItem', () => {
159
+ it('should fetch work item with correct URL', async () => {
160
+ // Arrange
161
+ const mockId = '123';
162
+ const mockWorkItem = { id: 123, fields: { 'System.Title': 'Test Work Item' } };
163
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce(mockWorkItem);
164
+ // Act
165
+ const result = await ticketsDataProvider.GetWorkItem(mockProject, mockId);
166
+ // Assert
167
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledWith(`${mockOrgUrl}${mockProject}/_apis/wit/workitems/${mockId}?$expand=All`, mockToken);
168
+ expect(result).toEqual(mockWorkItem);
169
+ });
170
+ });
171
+ describe('GetLinksByIds', () => {
172
+ it('should retrieve links by ids', async () => {
173
+ // Arrange
174
+ const mockIds = [1, 2];
175
+ const mockWorkItems = [
176
+ { id: 1, fields: { 'System.Title': 'Item 1' } },
177
+ { id: 2, fields: { 'System.Title': 'Item 2' } },
178
+ ];
179
+ const mockLinksMap = new Map();
180
+ mockLinksMap.set('1', { id: '1', rels: ['3'] });
181
+ mockLinksMap.set('2', { id: '2', rels: [] });
182
+ const mockRelatedItems = [{ id: 3, fields: { 'System.Title': 'Related Item' } }];
183
+ const mockTraceItem = { id: '1', title: 'Item 1', url: 'url', customerId: 'customer', links: [] };
184
+ jest.spyOn(ticketsDataProvider, 'PopulateWorkItemsByIds').mockResolvedValueOnce(mockWorkItems);
185
+ jest.spyOn(ticketsDataProvider, 'GetRelationsIds').mockResolvedValueOnce(mockLinksMap);
186
+ jest
187
+ .spyOn(ticketsDataProvider, 'GetParentLink')
188
+ .mockResolvedValueOnce(mockTraceItem)
189
+ .mockResolvedValueOnce({ id: '2', title: 'Item 2', url: 'url', customerId: 'customer', links: [] });
190
+ jest.spyOn(ticketsDataProvider, 'PopulateWorkItemsByIds').mockResolvedValueOnce(mockRelatedItems);
191
+ jest.spyOn(ticketsDataProvider, 'GetLinks').mockResolvedValueOnce([]);
192
+ // Act
193
+ const result = await ticketsDataProvider.GetLinksByIds(mockProject, mockIds);
194
+ // Assert
195
+ expect(result.length).toBe(2);
196
+ expect(ticketsDataProvider.PopulateWorkItemsByIds).toHaveBeenCalledWith(mockIds, mockProject);
197
+ expect(ticketsDataProvider.GetRelationsIds).toHaveBeenCalledWith(mockWorkItems);
198
+ });
199
+ });
200
+ describe('GetSharedQueries', () => {
201
+ it('should fetch STD shared queries with correct URL', async () => {
202
+ // Arrange
203
+ const mockPath = '';
204
+ const mockDocType = 'STD';
205
+ const mockQueries = { name: 'Query 1' };
206
+ const mockBranchesResponse = {
207
+ reqToTest: { result: { reqTestTree: {} }, usedFolder: {} },
208
+ testToReq: { result: { testReqTree: {} }, usedFolder: {} },
209
+ mom: { result: { linkedMomTree: {} }, usedFolder: {} },
210
+ };
211
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce(mockQueries);
212
+ jest.spyOn(ticketsDataProvider, 'ensureQueryChildren').mockResolvedValueOnce(mockQueries);
213
+ jest
214
+ .spyOn(ticketsDataProvider, 'getDocTypeRoot')
215
+ .mockResolvedValueOnce({ root: mockQueries, found: true });
216
+ jest
217
+ .spyOn(ticketsDataProvider, 'fetchDocTypeBranches')
218
+ .mockResolvedValueOnce(mockBranchesResponse);
219
+ // Act
220
+ const result = await ticketsDataProvider.GetSharedQueries(mockProject, mockPath, mockDocType);
221
+ // Assert
222
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledWith(`${mockOrgUrl}${mockProject}/_apis/wit/queries/Shared%20Queries?$depth=2&$expand=all`, mockToken);
223
+ expect(result).toEqual({
224
+ reqTestQueries: { reqTestTree: {}, testReqTree: {} },
225
+ linkedMomQueries: { linkedMomTree: {} },
226
+ });
227
+ });
228
+ it('should fetch SVD shared queries and call fetchAnyQueries', async () => {
229
+ // Arrange
230
+ const mockPath = 'Custom Path';
231
+ const mockDocType = 'SVD';
232
+ const mockQueries = { name: 'Query 1' };
233
+ const mockBranchesResponse = {
234
+ systemOverview: { result: {}, usedFolder: {} },
235
+ knownBugs: { result: {}, usedFolder: {} },
236
+ };
237
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce(mockQueries);
238
+ jest.spyOn(ticketsDataProvider, 'ensureQueryChildren').mockResolvedValueOnce(mockQueries);
239
+ jest
240
+ .spyOn(ticketsDataProvider, 'getDocTypeRoot')
241
+ .mockResolvedValueOnce({ root: mockQueries, found: true });
242
+ jest
243
+ .spyOn(ticketsDataProvider, 'fetchDocTypeBranches')
244
+ .mockResolvedValueOnce(mockBranchesResponse);
245
+ // Act
246
+ const result = await ticketsDataProvider.GetSharedQueries(mockProject, mockPath, mockDocType);
247
+ // Assert
248
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledWith(`${mockOrgUrl}${mockProject}/_apis/wit/queries/${mockPath}?$depth=2&$expand=all`, mockToken);
249
+ expect(result).toEqual({
250
+ systemOverviewQueryTree: {},
251
+ knownBugsQueryTree: {},
252
+ });
253
+ });
254
+ it('should handle errors', async () => {
255
+ // Arrange
256
+ const mockPath = '';
257
+ const mockError = new Error('API error');
258
+ tfs_1.TFSServices.getItemContent.mockImplementationOnce(() => {
259
+ return Promise.reject(mockError);
260
+ });
261
+ // Act & Assert
262
+ await expect(ticketsDataProvider.GetSharedQueries(mockProject, mockPath)).rejects.toThrow('API error');
263
+ expect(logger_1.default.error).toHaveBeenCalled();
264
+ });
265
+ it('should fall back to reqToTestResult.testReqTree when testToReqResult is missing (std)', async () => {
266
+ const mockQueries = { name: 'QueriesRoot' };
267
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce(mockQueries);
268
+ jest.spyOn(ticketsDataProvider, 'ensureQueryChildren').mockResolvedValueOnce(mockQueries);
269
+ jest
270
+ .spyOn(ticketsDataProvider, 'getDocTypeRoot')
271
+ .mockResolvedValueOnce({ root: mockQueries, found: true });
272
+ jest.spyOn(ticketsDataProvider, 'fetchDocTypeBranches').mockResolvedValueOnce({
273
+ reqToTest: {
274
+ result: {
275
+ reqTestTree: { a: 1 },
276
+ testReqTree: { fallback: true },
277
+ },
278
+ usedFolder: { name: 'req-to-test' },
279
+ },
280
+ // missing testToReq
281
+ mom: { result: { linkedMomTree: null }, usedFolder: { name: 'mom' } },
282
+ });
283
+ const res = await ticketsDataProvider.GetSharedQueries(mockProject, '', 'std');
284
+ expect(res).toEqual({
285
+ reqTestQueries: { reqTestTree: { a: 1 }, testReqTree: { fallback: true } },
286
+ linkedMomQueries: { linkedMomTree: null },
287
+ });
288
+ });
289
+ it('should fall back to openPcrToTest.TestToOpenPcrTree when testToOpenPcr branch is missing (str)', async () => {
290
+ const mockQueries = { name: 'QueriesRoot' };
291
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce(mockQueries);
292
+ jest.spyOn(ticketsDataProvider, 'ensureQueryChildren').mockResolvedValueOnce(mockQueries);
293
+ jest
294
+ .spyOn(ticketsDataProvider, 'getDocTypeRoot')
295
+ .mockResolvedValueOnce({ root: mockQueries, found: true });
296
+ jest.spyOn(ticketsDataProvider, 'fetchDocTypeBranches').mockResolvedValueOnce({
297
+ reqToTest: { result: { reqTestTree: null }, usedFolder: { name: 'req-to-test' } },
298
+ testToReq: { result: { testReqTree: null }, usedFolder: { name: 'test-to-req' } },
299
+ openPcrToTest: {
300
+ result: {
301
+ OpenPcrToTestTree: { ok: true },
302
+ TestToOpenPcrTree: { fromOpenPcr: true },
303
+ },
304
+ usedFolder: { name: 'open-pcr-to-test' },
305
+ },
306
+ // missing testToOpenPcr
307
+ });
308
+ const res = await ticketsDataProvider.GetSharedQueries(mockProject, '', 'str');
309
+ expect(res).toEqual({
310
+ reqTestTrees: { reqTestTree: null, testReqTree: null },
311
+ openPcrTestTrees: {
312
+ OpenPcrToTestTree: { ok: true },
313
+ TestToOpenPcrTree: { fromOpenPcr: true },
314
+ },
315
+ });
316
+ });
317
+ it('should execute std validators via fetchDocTypeBranches and accept fallback when first folder yields no results', async () => {
318
+ const queries = {
319
+ id: 'root',
320
+ name: 'Root',
321
+ isFolder: true,
322
+ hasChildren: true,
323
+ children: [
324
+ { id: 'req', name: 'Requirement - Test', isFolder: true, hasChildren: false },
325
+ { id: 'test', name: 'Test - Requirement', isFolder: true, hasChildren: false },
326
+ { id: 'mom', name: 'MOM', isFolder: true, hasChildren: false },
327
+ ],
328
+ };
329
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce(queries);
330
+ jest.spyOn(ticketsDataProvider, 'ensureQueryChildren').mockImplementation(async (x) => x);
331
+ jest
332
+ .spyOn(ticketsDataProvider, 'getDocTypeRoot')
333
+ .mockResolvedValueOnce({ root: queries, found: true });
334
+ // Force two candidates so validators run with null and then with data
335
+ jest
336
+ .spyOn(ticketsDataProvider, 'buildFallbackChain')
337
+ .mockResolvedValueOnce([queries.children[0], queries])
338
+ .mockResolvedValueOnce([queries.children[1], queries])
339
+ .mockResolvedValueOnce([queries.children[2], queries]);
340
+ const fetchReqTestSpy = jest
341
+ .spyOn(ticketsDataProvider, 'fetchLinkedReqTestQueries')
342
+ .mockResolvedValueOnce(null)
343
+ .mockResolvedValueOnce({ reqTestTree: { isValidQuery: true } })
344
+ .mockResolvedValueOnce(null)
345
+ .mockResolvedValueOnce({ testReqTree: { isValidQuery: true } });
346
+ const fetchMomSpy = jest
347
+ .spyOn(ticketsDataProvider, 'fetchLinkedMomQueries')
348
+ .mockResolvedValueOnce(null)
349
+ .mockResolvedValueOnce({ linkedMomTree: { isValidQuery: true } });
350
+ const res = await ticketsDataProvider.GetSharedQueries(mockProject, '', 'std');
351
+ expect(fetchReqTestSpy).toHaveBeenCalled();
352
+ expect(fetchMomSpy).toHaveBeenCalled();
353
+ expect(res.reqTestQueries.reqTestTree).toEqual({ isValidQuery: true });
354
+ expect(res.reqTestQueries.testReqTree).toEqual({ isValidQuery: true });
355
+ expect(res.linkedMomQueries.linkedMomTree).toEqual({ isValidQuery: true });
356
+ });
357
+ });
358
+ describe('hasAnyQueryTree', () => {
359
+ it('should return true for objects with isValidQuery/wiql/queryType', () => {
360
+ expect(ticketsDataProvider.hasAnyQueryTree({ isValidQuery: true })).toBe(true);
361
+ expect(ticketsDataProvider.hasAnyQueryTree({ wiql: 'x' })).toBe(true);
362
+ expect(ticketsDataProvider.hasAnyQueryTree({ queryType: 'Flat' })).toBe(true);
363
+ });
364
+ it('should return true for roots/children containers and nested objects, otherwise false', () => {
365
+ expect(ticketsDataProvider.hasAnyQueryTree({ roots: [{}] })).toBe(true);
366
+ expect(ticketsDataProvider.hasAnyQueryTree({ children: [{}] })).toBe(true);
367
+ expect(ticketsDataProvider.hasAnyQueryTree({ a: { b: [{ wiql: 'y' }] } })).toBe(true);
368
+ expect(ticketsDataProvider.hasAnyQueryTree({ a: { b: [] } })).toBe(false);
369
+ });
370
+ });
371
+ describe('matchesAreaPathCondition', () => {
372
+ it('should require both source+target leaf matches when filters are provided', () => {
373
+ const wiql = "SELECT * FROM WorkItemLinks WHERE Source.[System.AreaPath] = 'Test CMMI\\System' AND Target.[System.AreaPath] = 'Test CMMI\\Software'";
374
+ const ok = ticketsDataProvider.matchesAreaPathCondition(wiql, 'system', 'software');
375
+ expect(ok).toBe(true);
376
+ const badTarget = ticketsDataProvider.matchesAreaPathCondition(wiql, 'system', 'nope');
377
+ expect(badTarget).toBe(false);
378
+ });
379
+ it('should return false when filter is provided but area paths are not present for that owner', () => {
380
+ const wiql = "SELECT * FROM WorkItemLinks WHERE Target.[System.AreaPath] = 'A\\B\\Leaf'";
381
+ const ok = ticketsDataProvider.matchesAreaPathCondition(wiql, 'leaf', 'leaf');
382
+ expect(ok).toBe(false);
383
+ });
384
+ });
385
+ describe('GetQueryResultsFromWiql', () => {
386
+ it('should handle OneHop query with table format', async () => {
387
+ // Arrange
388
+ const mockWiqlHref = 'https://example.com/wiql';
389
+ const mockTestCaseMap = new Map();
390
+ const mockQueryResult = {
391
+ queryType: tfs_data_1.QueryType.OneHop,
392
+ columns: [],
393
+ workItemRelations: [{ source: null, target: { id: 1, url: 'url' } }],
394
+ };
395
+ const mockTableResult = {
396
+ sourceTargetsMap: new Map(),
397
+ sortingSourceColumnsMap: new Map(),
398
+ sortingTargetsColumnsMap: new Map(),
399
+ };
400
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce(mockQueryResult);
401
+ jest
402
+ .spyOn(ticketsDataProvider, 'parseDirectLinkedQueryResultForTableFormat')
403
+ .mockResolvedValueOnce(mockTableResult);
404
+ // Act
405
+ const result = await ticketsDataProvider.GetQueryResultsFromWiql(mockWiqlHref, true, mockTestCaseMap);
406
+ // Assert
407
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledWith(mockWiqlHref, mockToken);
408
+ expect(result).toEqual(mockTableResult);
409
+ });
410
+ it('should throw error when wiqlHref is empty', async () => {
411
+ // Arrange
412
+ const mockTestCaseMap = new Map();
413
+ // Act & Assert
414
+ const result = await ticketsDataProvider.GetQueryResultsFromWiql('', false, mockTestCaseMap);
415
+ expect(logger_1.default.error).toHaveBeenCalled();
416
+ expect(result).toBeUndefined();
417
+ });
418
+ });
419
+ describe('parseDirectLinkedQueryResultForTableFormat', () => {
420
+ it('should throw when workItemRelations is an empty array', async () => {
421
+ await expect(ticketsDataProvider.parseDirectLinkedQueryResultForTableFormat({ columns: [], workItemRelations: [], queryType: tfs_data_1.QueryType.OneHop }, new Map())).rejects.toThrow('No related work items were found');
422
+ });
423
+ it('should build sourceTargetsMap, include root links, and dedupe testCase->related items', async () => {
424
+ const testCaseToRelatedWiMap = new Map();
425
+ const workItemRelations = [
426
+ { source: null, target: { id: 1 } },
427
+ { source: { id: 1 }, target: { id: 2 } },
428
+ { source: { id: 1 }, target: { id: 2 } },
429
+ ];
430
+ const fetchSpy = jest
431
+ .spyOn(ticketsDataProvider, 'fetchWIForQueryResult')
432
+ .mockImplementation(async (...args) => {
433
+ var _a, _b, _c;
434
+ const _rel = args[0];
435
+ const columnsToShowMap = args[1];
436
+ // Validate CustomerRequirementId -> Customer ID mapping is prepared
437
+ expect(columnsToShowMap.get('CustomerRequirementId')).toBe('Customer ID');
438
+ const id = (_b = (_a = _rel === null || _rel === void 0 ? void 0 : _rel.target) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : (_c = _rel === null || _rel === void 0 ? void 0 : _rel.source) === null || _c === void 0 ? void 0 : _c.id;
439
+ if (id === 1) {
440
+ return { id: 1, fields: { 'System.WorkItemType': 'Test Case' } };
441
+ }
442
+ if (id === 2) {
443
+ return { id: 2, fields: { 'System.WorkItemType': 'Bug' } };
444
+ }
445
+ return { id, fields: { 'System.WorkItemType': 'Task' } };
446
+ });
447
+ const res = await ticketsDataProvider.parseDirectLinkedQueryResultForTableFormat({
448
+ queryType: tfs_data_1.QueryType.OneHop,
449
+ columns: [{ referenceName: 'CustomerRequirementId', name: 'CustomerRequirementId' }],
450
+ workItemRelations,
451
+ }, testCaseToRelatedWiMap);
452
+ expect(fetchSpy).toHaveBeenCalled();
453
+ expect(res.sourceTargetsMap).toBeInstanceOf(Map);
454
+ const keys = Array.from(res.sourceTargetsMap.keys());
455
+ expect(keys.some((k) => k.id === 1)).toBe(true);
456
+ const relatedSet = testCaseToRelatedWiMap.get(1);
457
+ expect(relatedSet).toBeDefined();
458
+ expect(Array.from(relatedSet || [])).toHaveLength(1);
459
+ expect(Array.from(relatedSet || [])[0]).toEqual(expect.objectContaining({ id: 2 }));
460
+ });
461
+ it('should throw when relation.target is missing', async () => {
462
+ jest
463
+ .spyOn(ticketsDataProvider, 'fetchWIForQueryResult')
464
+ .mockResolvedValueOnce({ id: 1, fields: { 'System.WorkItemType': 'Test Case' } });
465
+ await expect(ticketsDataProvider.parseDirectLinkedQueryResultForTableFormat({
466
+ queryType: tfs_data_1.QueryType.OneHop,
467
+ columns: [],
468
+ workItemRelations: [{ source: { id: 1 }, target: null }],
469
+ }, new Map())).rejects.toThrow('Target relation is missing');
470
+ });
471
+ });
472
+ describe('isFlatQueryAllowedByTypeOrId', () => {
473
+ it('should accept when allowedTypes is empty and WIQL references [System.WorkItemType]', async () => {
474
+ const wiql = "SELECT * FROM WorkItems WHERE [System.WorkItemType] = 'Bug'";
475
+ const res = await ticketsDataProvider.isFlatQueryAllowedByTypeOrId({}, wiql, [], new Map());
476
+ expect(res).toBe(true);
477
+ });
478
+ it('should return false when allowedTypes is provided but no types are found and no ids exist', async () => {
479
+ const wiql = "SELECT * FROM WorkItems WHERE [System.Title] <> ''";
480
+ const res = await ticketsDataProvider.isFlatQueryAllowedByTypeOrId({}, wiql, ['Bug'], new Map());
481
+ expect(res).toBe(false);
482
+ });
483
+ it('should reject when WIQL contains a type outside allowedTypes', async () => {
484
+ const wiql = "SELECT * FROM WorkItems WHERE [System.WorkItemType] IN ('Bug','Task')";
485
+ const res = await ticketsDataProvider.isFlatQueryAllowedByTypeOrId({}, wiql, ['Bug'], new Map());
486
+ expect(res).toBe(false);
487
+ });
488
+ });
489
+ describe('matchesFlatAreaCondition', () => {
490
+ it('should return true when filter is empty', () => {
491
+ const wiql = 'SELECT * FROM WorkItems';
492
+ const res = ticketsDataProvider.matchesFlatAreaCondition(wiql, '');
493
+ expect(res).toBe(true);
494
+ });
495
+ it('should return false when no [System.AreaPath] is present in WIQL', () => {
496
+ const wiql = "SELECT * FROM WorkItems WHERE [System.Title] <> ''";
497
+ const res = ticketsDataProvider.matchesFlatAreaCondition(wiql, 'X');
498
+ expect(res).toBe(false);
499
+ });
500
+ it('should match by leaf segment of area path (case-insensitive)', () => {
501
+ const wiql = "SELECT * FROM WorkItems WHERE [System.AreaPath] = 'A\\B\\Leaf'";
502
+ const res = ticketsDataProvider.matchesFlatAreaCondition(wiql, 'leaf');
503
+ expect(res).toBe(true);
504
+ });
505
+ });
506
+ describe('filterFieldsByColumns', () => {
507
+ it('should keep fields included by columns map and always include System.WorkItemType and System.Title', () => {
508
+ const item = {
509
+ fields: {
510
+ 'System.Title': 'T',
511
+ 'System.WorkItemType': 'Bug',
512
+ 'Custom.Keep': 1,
513
+ 'Custom.Drop': 2,
514
+ },
515
+ };
516
+ const columnsToFilterMap = new Map([['Custom.Keep', 'Keep']]);
517
+ const resultedRefNameMap = new Map();
518
+ ticketsDataProvider.filterFieldsByColumns(item, columnsToFilterMap, resultedRefNameMap);
519
+ expect(Object.keys(item.fields).sort()).toEqual(['Custom.Keep', 'System.Title', 'System.WorkItemType']);
520
+ expect(resultedRefNameMap.get('Custom.Keep')).toBe('Keep');
521
+ expect(resultedRefNameMap.get('System.Title')).toBe('System.Title');
522
+ expect(resultedRefNameMap.get('System.WorkItemType')).toBe('System.WorkItemType');
523
+ });
524
+ it('should log and throw when item.fields is invalid', () => {
525
+ const item = { fields: undefined };
526
+ const columnsToFilterMap = new Map();
527
+ const resultedRefNameMap = new Map();
528
+ expect(() => ticketsDataProvider.filterFieldsByColumns(item, columnsToFilterMap, resultedRefNameMap)).toThrow();
529
+ expect(logger_1.default.error).toHaveBeenCalledWith(expect.stringContaining('Cannot filter columns'));
530
+ });
531
+ });
532
+ describe('GetWorkItemTypeList', () => {
533
+ it('should fetch work item types and attempt icon download with fallback accepts', async () => {
534
+ tfs_1.TFSServices.getItemContent.mockReset();
535
+ if (!tfs_1.TFSServices.fetchAzureDevOpsImageAsBase64) {
536
+ tfs_1.TFSServices.fetchAzureDevOpsImageAsBase64 = jest.fn();
537
+ }
538
+ tfs_1.TFSServices.fetchAzureDevOpsImageAsBase64.mockReset();
539
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce({
540
+ value: [
541
+ {
542
+ name: 'Bug',
543
+ referenceName: 'Microsoft.VSTS.WorkItemTypes.Bug',
544
+ color: 'ff0000',
545
+ icon: { id: 'i', url: 'http://example.com/icon' },
546
+ states: [],
547
+ },
548
+ ],
549
+ });
550
+ // First accept fails, second succeeds
551
+ tfs_1.TFSServices.fetchAzureDevOpsImageAsBase64
552
+ .mockRejectedValueOnce(new Error('svg fail'))
553
+ .mockResolvedValueOnce('data:image/png;base64,xxx');
554
+ const res = await ticketsDataProvider.GetWorkItemTypeList(mockProject);
555
+ expect(res).toHaveLength(1);
556
+ expect(res[0]).toEqual(expect.objectContaining({
557
+ name: 'Bug',
558
+ icon: expect.objectContaining({ dataUrl: 'data:image/png;base64,xxx' }),
559
+ }));
560
+ expect(logger_1.default.warn).toHaveBeenCalledWith(expect.stringContaining('Failed to download icon'));
561
+ });
562
+ });
563
+ describe('GetModeledQuery', () => {
564
+ it('should structure query list correctly', () => {
565
+ // Arrange
566
+ const mockQueryList = [
567
+ {
568
+ name: 'Query 1',
569
+ _links: { wiql: 'http://example.com/wiql1' },
570
+ id: 'q1',
571
+ },
572
+ {
573
+ name: 'Query 2',
574
+ _links: { wiql: null },
575
+ id: 'q2',
576
+ },
577
+ ];
578
+ // Act
579
+ const result = ticketsDataProvider.GetModeledQuery(mockQueryList);
580
+ // Assert
581
+ expect(result).toEqual([
582
+ { queryName: 'Query 1', wiql: 'http://example.com/wiql1', id: 'q1' },
583
+ { queryName: 'Query 2', wiql: null, id: 'q2' },
584
+ ]);
585
+ });
586
+ });
587
+ describe('PopulateWorkItemsByIds', () => {
588
+ it('should fetch work items in batches of 200', async () => {
589
+ // Arrange
590
+ const mockIds = Array.from({ length: 250 }, (_, i) => i + 1);
591
+ const mockResponse1 = { value: mockIds.slice(0, 200).map((id) => ({ id })) };
592
+ const mockResponse2 = { value: mockIds.slice(200).map((id) => ({ id })) };
593
+ tfs_1.TFSServices.getItemContent
594
+ .mockResolvedValueOnce(mockResponse1)
595
+ .mockResolvedValueOnce(mockResponse2);
596
+ // Act
597
+ const result = await ticketsDataProvider.PopulateWorkItemsByIds(mockIds, mockProject);
598
+ // Assert
599
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledTimes(2);
600
+ expect(result.length).toBe(250);
601
+ });
602
+ it('should handle errors and return empty array', async () => {
603
+ // Arrange
604
+ const mockIds = [1, 2, 3];
605
+ const mockError = new Error('API error');
606
+ tfs_1.TFSServices.getItemContent.mockRejectedValueOnce(mockError);
607
+ // Act
608
+ const result = await ticketsDataProvider.PopulateWorkItemsByIds(mockIds, mockProject);
609
+ // Assert
610
+ expect(result).toEqual([]);
611
+ expect(logger_1.default.error).toHaveBeenCalled();
612
+ });
613
+ });
614
+ describe('GetIterationsByTeamName', () => {
615
+ it('should fetch iterations with team name specified', async () => {
616
+ // Arrange
617
+ const mockTeamName = 'test-team';
618
+ const mockIterations = ['iteration1', 'iteration2'];
619
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce(mockIterations);
620
+ // Act
621
+ const result = await ticketsDataProvider.GetIterationsByTeamName(mockProject, mockTeamName);
622
+ // Assert
623
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledWith(`${mockOrgUrl}${mockProject}/${mockTeamName}/_apis/work/teamsettings/iterations`, mockToken, 'get');
624
+ expect(result).toEqual(mockIterations);
625
+ });
626
+ it('should fetch iterations without team name', async () => {
627
+ // Arrange
628
+ const mockTeamName = '';
629
+ const mockIterations = ['iteration1', 'iteration2'];
630
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce(mockIterations);
631
+ // Act
632
+ const result = await ticketsDataProvider.GetIterationsByTeamName(mockProject, mockTeamName);
633
+ // Assert
634
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledWith(`${mockOrgUrl}${mockProject}/_apis/work/teamsettings/iterations`, mockToken, 'get');
635
+ expect(result).toEqual(mockIterations);
636
+ });
637
+ });
638
+ describe('CreateNewWorkItem', () => {
639
+ it('should create work item with correct parameters', async () => {
640
+ // Arrange
641
+ const mockWiBody = [{ op: 'add', path: '/fields/System.Title', value: 'New Item' }];
642
+ const mockWiType = 'Bug';
643
+ const mockByPass = true;
644
+ const mockResponse = { id: 123, fields: { 'System.Title': 'New Item' } };
645
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce(mockResponse);
646
+ // Act
647
+ const result = await ticketsDataProvider.CreateNewWorkItem(mockProject, mockWiBody, mockWiType, mockByPass);
648
+ // Assert
649
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledWith(`${mockOrgUrl}${mockProject}/_apis/wit/workitems/$${mockWiType}?bypassRules=true`, mockToken, 'POST', mockWiBody, {
650
+ 'Content-Type': 'application/json-patch+json',
651
+ });
652
+ expect(result).toEqual(mockResponse);
653
+ });
654
+ });
655
+ describe('UpdateWorkItem', () => {
656
+ it('should update work item with correct parameters', async () => {
657
+ // Arrange
658
+ const mockWiBody = [{ op: 'add', path: '/fields/System.Title', value: 'Updated Item' }];
659
+ const mockWorkItemId = 123;
660
+ const mockByPass = true;
661
+ const mockResponse = { id: 123, fields: { 'System.Title': 'Updated Item' } };
662
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce(mockResponse);
663
+ // Act
664
+ const result = await ticketsDataProvider.UpdateWorkItem(mockProject, mockWiBody, mockWorkItemId, mockByPass);
665
+ // Assert
666
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledWith(`${mockOrgUrl}${mockProject}/_apis/wit/workitems/${mockWorkItemId}?bypassRules=true`, mockToken, 'patch', mockWiBody, {
667
+ 'Content-Type': 'application/json-patch+json',
668
+ });
669
+ expect(result).toEqual(mockResponse);
670
+ });
671
+ });
672
+ describe('GetWorkitemAttachments', () => {
673
+ it('should return attachments for work item', async () => {
674
+ // Arrange
675
+ const mockId = '123';
676
+ const mockWorkItem = {
677
+ relations: [
678
+ {
679
+ rel: 'AttachedFile',
680
+ url: 'https://example.com/attachment/1',
681
+ attributes: { name: 'file.txt' },
682
+ },
683
+ ],
684
+ };
685
+ // Mock the TFSServices.getItemContent directly to return our mock data
686
+ // This is what will be called by the new TicketsDataProvider instance
687
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce(mockWorkItem);
688
+ // Act
689
+ const result = await ticketsDataProvider.GetWorkitemAttachments(mockProject, mockId);
690
+ // Assert
691
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledWith(`${mockOrgUrl}${mockProject}/_apis/wit/workitems/${mockId}?$expand=All`, mockToken);
692
+ expect(result.length).toBe(1);
693
+ // Check that the downloadUrl was added correctly
694
+ expect(result[0]).toHaveProperty('downloadUrl', 'https://example.com/attachment/1/file.txt');
695
+ expect(result[0].rel).toBe('AttachedFile');
696
+ });
697
+ it('should return [] when relations are missing', async () => {
698
+ jest
699
+ .spyOn(ticketsDataProvider, 'GetWorkItem')
700
+ .mockResolvedValueOnce({ id: 1, relations: undefined });
701
+ const res = await ticketsDataProvider.GetWorkitemAttachments(mockProject, '1');
702
+ expect(res).toEqual([]);
703
+ });
704
+ it('should handle work item with no relations', async () => {
705
+ // Arrange
706
+ const mockId = '123';
707
+ const mockWorkItem = { relations: null };
708
+ jest.spyOn(ticketsDataProvider, 'GetWorkItem').mockResolvedValueOnce(mockWorkItem);
709
+ // Act
710
+ const result = await ticketsDataProvider.GetWorkitemAttachments(mockProject, mockId);
711
+ // Assert
712
+ expect(result).toEqual([]);
713
+ });
714
+ it('should log and return [] when GetWorkItem throws', async () => {
715
+ jest.spyOn(TicketsDataProvider_1.default.prototype, 'GetWorkItem').mockRejectedValueOnce(new Error('boom'));
716
+ const res = await ticketsDataProvider.GetWorkitemAttachments(mockProject, '1');
717
+ expect(res).toEqual([]);
718
+ expect(logger_1.default.error).toHaveBeenCalled();
719
+ });
720
+ it('should filter out non-attachment relations', async () => {
721
+ // Arrange
722
+ const mockId = '123';
723
+ const mockWorkItem = {
724
+ relations: [
725
+ {
726
+ rel: 'Parent',
727
+ url: 'https://example.com/parent/1',
728
+ attributes: { name: 'parent' },
729
+ },
730
+ {
731
+ rel: 'AttachedFile',
732
+ url: 'https://example.com/attachment/1',
733
+ attributes: { name: 'file.txt' },
734
+ },
735
+ ],
736
+ };
737
+ // Mock TFSServices.getItemContent directly instead of GetWorkItem
738
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce(mockWorkItem);
739
+ // Act
740
+ const result = await ticketsDataProvider.GetWorkitemAttachments(mockProject, mockId);
741
+ // Assert
742
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledWith(`${mockOrgUrl}${mockProject}/_apis/wit/workitems/${mockId}?$expand=All`, mockToken);
743
+ expect(result.length).toBe(1);
744
+ expect(result[0].rel).toBe('AttachedFile');
745
+ // Only the AttachedFile relation should be in the result
746
+ expect(result.some((item) => item.rel === 'Parent')).toBe(false);
747
+ });
748
+ });
749
+ describe('GetWorkItemByUrl', () => {
750
+ it('should fetch work item by URL', async () => {
751
+ // Arrange
752
+ const mockUrl = 'https://dev.azure.com/org/project/_apis/wit/workitems/123';
753
+ const mockWorkItem = { id: 123, fields: { 'System.Title': 'Test' } };
754
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce(mockWorkItem);
755
+ // Act
756
+ const result = await ticketsDataProvider.GetWorkItemByUrl(mockUrl);
757
+ // Assert
758
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledWith(mockUrl, mockToken);
759
+ expect(result).toEqual(mockWorkItem);
760
+ });
761
+ });
762
+ describe('GetParentLink', () => {
763
+ it('should return trace with work item info', async () => {
764
+ // Arrange
765
+ const mockWi = {
766
+ id: '123',
767
+ fields: {
768
+ 'System.Title': 'Test Work Item',
769
+ 'System.CustomerId': 'CUST-001',
770
+ },
771
+ };
772
+ // Act
773
+ const result = await ticketsDataProvider.GetParentLink(mockProject, mockWi);
774
+ // Assert
775
+ expect(result.id).toBe('123');
776
+ expect(result.title).toBe('Test Work Item');
777
+ expect(result.customerId).toBe('CUST-001');
778
+ expect(result.url).toContain(mockProject);
779
+ });
780
+ it('should handle work item without customerId', async () => {
781
+ // Arrange
782
+ const mockWi = {
783
+ id: '123',
784
+ fields: {
785
+ 'System.Title': 'Test Work Item',
786
+ },
787
+ };
788
+ // Act
789
+ const result = await ticketsDataProvider.GetParentLink(mockProject, mockWi);
790
+ // Assert
791
+ expect(result.id).toBe('123');
792
+ expect(result.customerId).toBeUndefined();
793
+ });
794
+ it('should handle null work item', async () => {
795
+ // Act
796
+ const result = await ticketsDataProvider.GetParentLink(mockProject, null);
797
+ // Assert
798
+ expect(result.id).toBeUndefined();
799
+ });
800
+ });
801
+ describe('GetRelationsIds', () => {
802
+ it('should return a map from work items', async () => {
803
+ // Arrange
804
+ const mockIds = [
805
+ {
806
+ id: 1,
807
+ relations: [{ rel: 'Parent', url: 'https://example.com/workitems/2' }],
808
+ },
809
+ ];
810
+ // Act
811
+ const result = await ticketsDataProvider.GetRelationsIds(mockIds);
812
+ // Assert - result is a Map
813
+ expect(result).toBeInstanceOf(Map);
814
+ });
815
+ it('should handle empty array', async () => {
816
+ // Arrange
817
+ const mockIds = [];
818
+ // Act
819
+ const result = await ticketsDataProvider.GetRelationsIds(mockIds);
820
+ // Assert
821
+ expect(result.size).toBe(0);
822
+ });
823
+ });
824
+ describe('GetLinks', () => {
825
+ it('should get links from work item relations', async () => {
826
+ // Arrange
827
+ const mockWi = {
828
+ relations: [{ rel: 'Parent', url: 'https://example.com/workitems/2' }],
829
+ };
830
+ const mockLinks = [
831
+ {
832
+ id: '2',
833
+ fields: {
834
+ 'System.Title': 'Parent Item',
835
+ 'System.Description': 'Parent Description',
836
+ },
837
+ },
838
+ ];
839
+ // Act
840
+ const result = await ticketsDataProvider.GetLinks(mockProject, mockWi, mockLinks);
841
+ // Assert
842
+ expect(result).toHaveLength(1);
843
+ expect(result[0].id).toBe('2');
844
+ expect(result[0].title).toBe('Parent Item');
845
+ expect(result[0].type).toBe('Parent');
846
+ });
847
+ });
848
+ describe('GetFieldsByType', () => {
849
+ it('should fetch fields for a work item type', async () => {
850
+ // Arrange
851
+ const mockItemType = 'Bug';
852
+ const mockResponse = {
853
+ value: [
854
+ { name: 'Priority', referenceName: 'Microsoft.VSTS.Common.Priority' },
855
+ { name: 'Severity', referenceName: 'Microsoft.VSTS.Common.Severity' },
856
+ { name: 'ID', referenceName: 'System.Id' },
857
+ { name: 'Title', referenceName: 'System.Title' },
858
+ ],
859
+ };
860
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce(mockResponse);
861
+ // Act
862
+ const result = await ticketsDataProvider.GetFieldsByType(mockProject, mockItemType);
863
+ // Assert
864
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledWith(`${mockOrgUrl}${mockProject}/_apis/wit/workitemtypes/${mockItemType}/fields`, mockToken);
865
+ // Should filter out ID and Title
866
+ expect(result).toHaveLength(2);
867
+ expect(result[0].text).toBe('Priority (Bug)');
868
+ expect(result[0].key).toBe('Microsoft.VSTS.Common.Priority');
869
+ });
870
+ it('should throw error when API fails', async () => {
871
+ // Arrange
872
+ const mockError = new Error('API Error');
873
+ tfs_1.TFSServices.getItemContent.mockRejectedValueOnce(mockError);
874
+ // Act & Assert
875
+ await expect(ticketsDataProvider.GetFieldsByType(mockProject, 'Bug')).rejects.toThrow();
876
+ expect(logger_1.default.error).toHaveBeenCalled();
877
+ });
878
+ });
879
+ describe('GetQueryResultsFromWiql - additional cases', () => {
880
+ it('should handle Tree query type', async () => {
881
+ // Arrange
882
+ const mockWiqlHref = 'https://example.com/wiql';
883
+ const mockTestCaseMap = new Map();
884
+ const mockQueryResult = {
885
+ queryType: 'tree',
886
+ workItemRelations: [{ source: null, target: { id: 1, url: 'https://example.com/wi/1' }, rel: null }],
887
+ };
888
+ const mockWiResponse = {
889
+ fields: { 'System.Title': 'Test', 'System.Description': 'Desc' },
890
+ _links: { html: { href: 'https://example.com' } },
891
+ };
892
+ tfs_1.TFSServices.getItemContent
893
+ .mockResolvedValueOnce(mockQueryResult)
894
+ .mockResolvedValueOnce(mockWiResponse);
895
+ // Act
896
+ const result = await ticketsDataProvider.GetQueryResultsFromWiql(mockWiqlHref, false, mockTestCaseMap);
897
+ // Assert
898
+ expect(result).toBeDefined();
899
+ });
900
+ it('should handle Flat query type with table format', async () => {
901
+ // Arrange
902
+ const mockWiqlHref = 'https://example.com/wiql';
903
+ const mockTestCaseMap = new Map();
904
+ const mockQueryResult = {
905
+ queryType: 'flat',
906
+ columns: [{ referenceName: 'System.Title', name: 'Title' }],
907
+ workItems: [{ id: 1, url: 'https://example.com/wi/1' }],
908
+ };
909
+ const mockWiResponse = {
910
+ id: 1,
911
+ fields: { 'System.Title': 'Test Item' },
912
+ };
913
+ tfs_1.TFSServices.getItemContent
914
+ .mockResolvedValueOnce(mockQueryResult)
915
+ .mockResolvedValueOnce(mockWiResponse);
916
+ // Act
917
+ const result = await ticketsDataProvider.GetQueryResultsFromWiql(mockWiqlHref, true, mockTestCaseMap);
918
+ // Assert
919
+ expect(result).toBeDefined();
920
+ });
921
+ it('should handle Flat query type without table format', async () => {
922
+ // Arrange
923
+ const mockWiqlHref = 'https://example.com/wiql';
924
+ const mockTestCaseMap = new Map();
925
+ const mockQueryResult = {
926
+ queryType: 'flat',
927
+ workItems: [{ id: 1, url: 'https://example.com/wi/1' }],
928
+ };
929
+ const mockWiResponse = {
930
+ fields: { 'System.Title': 'Test', 'System.Description': 'Desc' },
931
+ _links: { html: { href: 'https://example.com' } },
932
+ };
933
+ tfs_1.TFSServices.getItemContent
934
+ .mockResolvedValueOnce(mockQueryResult)
935
+ .mockResolvedValueOnce(mockWiResponse);
936
+ // Act
937
+ const result = await ticketsDataProvider.GetQueryResultsFromWiql(mockWiqlHref, false, mockTestCaseMap);
938
+ // Assert
939
+ expect(result).toBeDefined();
940
+ });
941
+ it('should return undefined when queryType is not supported', async () => {
942
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce({ queryType: 'Unknown' });
943
+ const res = await ticketsDataProvider.GetQueryResultsFromWiql('https://example.com/wiql', false, new Map());
944
+ expect(res).toBeUndefined();
945
+ });
946
+ });
947
+ describe('getRequirementTypeFieldRefs', () => {
948
+ it('should prioritize known ref and dedupe candidates', async () => {
949
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce({
950
+ value: [
951
+ { name: 'Requirement Type', referenceName: 'Microsoft.VSTS.CMMI.RequirementType' },
952
+ { name: 'Requirement_Type', referenceName: 'Custom.RequirementType' },
953
+ { name: 'Requirement Type', referenceName: 'Microsoft.VSTS.CMMI.RequirementType' },
954
+ { name: 'Other', referenceName: 'Custom.Other' },
955
+ ],
956
+ });
957
+ const res = await ticketsDataProvider.getRequirementTypeFieldRefs('project1');
958
+ expect(res[0]).toBe('Microsoft.VSTS.CMMI.RequirementType');
959
+ expect(res).toContain('Custom.RequirementType');
960
+ expect(res.filter((x) => x === 'Microsoft.VSTS.CMMI.RequirementType')).toHaveLength(1);
961
+ });
962
+ it('should fall back to known ref when API call throws', async () => {
963
+ tfs_1.TFSServices.getItemContent.mockRejectedValueOnce(new Error('boom'));
964
+ const res = await ticketsDataProvider.getRequirementTypeFieldRefs('project1');
965
+ expect(res).toEqual(['Microsoft.VSTS.CMMI.RequirementType']);
966
+ });
967
+ });
968
+ describe('findQueryFolderByName', () => {
969
+ it('should return null when input is invalid', async () => {
970
+ await expect(ticketsDataProvider.findQueryFolderByName(null, 'x')).resolves.toBeNull();
971
+ await expect(ticketsDataProvider.findQueryFolderByName({ id: 'r' }, '')).resolves.toBeNull();
972
+ });
973
+ it('should BFS and return a folder by name; using ensureQueryChildren when node hasChildren', async () => {
974
+ const root = {
975
+ id: 'root',
976
+ name: 'Root',
977
+ isFolder: true,
978
+ hasChildren: true,
979
+ children: [
980
+ { id: 'a', name: 'A', isFolder: true, hasChildren: true },
981
+ { id: 'b', name: 'B', isFolder: true, hasChildren: false, children: [] },
982
+ ],
983
+ };
984
+ jest.spyOn(ticketsDataProvider, 'ensureQueryChildren').mockImplementation(async (node) => {
985
+ if (node.id === 'a') {
986
+ node.children = [{ id: 'target', name: 'TargetFolder', isFolder: true, hasChildren: false }];
987
+ }
988
+ return node;
989
+ });
990
+ const found = await ticketsDataProvider.findQueryFolderByName(root, 'targetfolder');
991
+ expect(found).toEqual(expect.objectContaining({ id: 'target' }));
992
+ });
993
+ it('should return null when folder is not found', async () => {
994
+ const root = { id: 'root', name: 'Root', isFolder: true, hasChildren: false, children: [] };
995
+ const found = await ticketsDataProvider.findQueryFolderByName(root, 'missing');
996
+ expect(found).toBeNull();
997
+ });
998
+ });
999
+ describe('findChildFolderByName', () => {
1000
+ it('should return null when parent or childName is invalid', async () => {
1001
+ await expect(ticketsDataProvider.findChildFolderByName(null, 'x')).resolves.toBeNull();
1002
+ await expect(ticketsDataProvider.findChildFolderByName({ id: 1 }, '')).resolves.toBeNull();
1003
+ });
1004
+ it('should return null when parent has no children after enrichment', async () => {
1005
+ jest
1006
+ .spyOn(ticketsDataProvider, 'ensureQueryChildren')
1007
+ .mockResolvedValueOnce({ id: 'p', hasChildren: true, children: [] });
1008
+ const res = await ticketsDataProvider.findChildFolderByName({ id: 'p' }, 'child');
1009
+ expect(res).toBeNull();
1010
+ });
1011
+ it('should return matching child folder by case-insensitive exact name', async () => {
1012
+ const parentWithChildren = {
1013
+ id: 'p',
1014
+ hasChildren: true,
1015
+ children: [
1016
+ { id: 'c1', isFolder: true, name: 'Match', hasChildren: false },
1017
+ { id: 'c2', isFolder: true, name: 'Other', hasChildren: false },
1018
+ ],
1019
+ };
1020
+ jest.spyOn(ticketsDataProvider, 'ensureQueryChildren').mockResolvedValueOnce(parentWithChildren);
1021
+ const res = await ticketsDataProvider.findChildFolderByName({ id: 'p' }, 'match');
1022
+ expect(res).toEqual(parentWithChildren.children[0]);
1023
+ });
1024
+ });
1025
+ describe('buildFallbackChain + findPathToNode', () => {
1026
+ it('should use findPathToNode when startingFolder has id', async () => {
1027
+ const root = { id: 'root', name: 'Root', hasChildren: true, children: [] };
1028
+ const mid = { id: 'mid', name: 'Mid', hasChildren: true, children: [] };
1029
+ const start = { id: 'start', name: 'Start', hasChildren: false, children: [] };
1030
+ root.children = [mid];
1031
+ mid.children = [start];
1032
+ jest.spyOn(ticketsDataProvider, 'ensureQueryChildren').mockImplementation(async (n) => n);
1033
+ const path = await ticketsDataProvider.findPathToNode(root, 'start');
1034
+ expect(path === null || path === void 0 ? void 0 : path.map((n) => n.id)).toEqual(['root', 'mid', 'start']);
1035
+ const chain = await ticketsDataProvider.buildFallbackChain(root, start);
1036
+ expect(chain.map((n) => n.id)).toEqual(['start', 'mid', 'root']);
1037
+ });
1038
+ it('should fall back to startingFolder when findPathToNode returns null', async () => {
1039
+ const root = { id: 'root', name: 'Root', hasChildren: false, children: [] };
1040
+ const start = { id: 'start', name: 'Start', hasChildren: false, children: [] };
1041
+ jest.spyOn(ticketsDataProvider, 'findPathToNode').mockResolvedValueOnce(null);
1042
+ const chain = await ticketsDataProvider.buildFallbackChain(root, start);
1043
+ expect(chain.map((n) => n.id)).toEqual(['start', 'root']);
1044
+ });
1045
+ it('should handle startingFolder without id and always include rootQueries', async () => {
1046
+ const root = { id: 'root', name: 'Root', hasChildren: false, children: [] };
1047
+ const start = { name: 'StartNoId', hasChildren: false, children: [] };
1048
+ const chain = await ticketsDataProvider.buildFallbackChain(root, start);
1049
+ expect(chain).toHaveLength(2);
1050
+ expect(chain[0]).toBe(start);
1051
+ expect(chain[1]).toBe(root);
1052
+ });
1053
+ it('should return null from findPathToNode when a cycle is detected', async () => {
1054
+ const node = { id: 'x', name: 'X', hasChildren: true, children: [] };
1055
+ node.children = [node];
1056
+ jest.spyOn(ticketsDataProvider, 'ensureQueryChildren').mockImplementation(async (n) => n);
1057
+ const path = await ticketsDataProvider.findPathToNode(node, 'missing');
1058
+ expect(path).toBeNull();
1059
+ });
1060
+ });
1061
+ describe('structureFetchedQueries', () => {
1062
+ it('should skip excluded folders', async () => {
1063
+ const res = await ticketsDataProvider.structureFetchedQueries({ isFolder: true, name: 'SkipMe', hasChildren: true }, false, null, ['Epic'], ['Feature'], undefined, undefined, false, ['skipme'], false);
1064
+ expect(res).toEqual({ tree1: null, tree2: null });
1065
+ });
1066
+ it('should fetch children when hasChildren=true but children are missing', async () => {
1067
+ jest
1068
+ .spyOn(ticketsDataProvider, 'matchesSourceTargetConditionAsync')
1069
+ .mockResolvedValueOnce(true)
1070
+ .mockResolvedValueOnce(false);
1071
+ jest
1072
+ .spyOn(ticketsDataProvider, 'buildQueryNode')
1073
+ .mockImplementation((rq, parentId) => ({
1074
+ id: rq.id,
1075
+ pId: parentId,
1076
+ }));
1077
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce({
1078
+ id: 'root',
1079
+ name: 'Root',
1080
+ hasChildren: false,
1081
+ isFolder: false,
1082
+ queryType: 'oneHop',
1083
+ wiql: "SELECT * FROM WorkItemLinks WHERE Source.[System.WorkItemType] = 'Epic' AND Target.[System.WorkItemType] = 'Feature'",
1084
+ });
1085
+ const res = await ticketsDataProvider.structureFetchedQueries({ id: 'root', url: 'https://example.com/q', hasChildren: true, children: undefined }, false, null, ['Epic'], ['Feature']);
1086
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledWith('https://example.com/q?$depth=2&$expand=all', mockToken);
1087
+ expect(res.tree1).toEqual({ id: 'root', pId: 'root' });
1088
+ expect(res.tree2).toBeNull();
1089
+ });
1090
+ it('should build tree nodes for flat queries when includeFlatQueries is enabled (tree1 only)', async () => {
1091
+ jest
1092
+ .spyOn(ticketsDataProvider, 'buildQueryNode')
1093
+ .mockImplementation((rq, parentId) => ({ id: rq.id, pId: parentId }));
1094
+ const wiql = "SELECT * FROM WorkItems WHERE [System.WorkItemType] = 'Bug' AND [System.AreaPath] = 'A\\Leaf'";
1095
+ const res = await ticketsDataProvider.structureFetchedQueries({ id: 'q', isFolder: false, hasChildren: false, queryType: 'flat', wiql }, false, null, ['Bug'], ['Task'], 'leaf', 'missing', false, [], true);
1096
+ expect(res.tree1).toEqual({ id: 'q', pId: null });
1097
+ expect(res.tree2).toBeNull();
1098
+ });
1099
+ it('should build tree2 for reverse source/target in oneHop queries', async () => {
1100
+ jest
1101
+ .spyOn(ticketsDataProvider, 'matchesSourceTargetConditionAsync')
1102
+ .mockResolvedValueOnce(false)
1103
+ .mockResolvedValueOnce(true);
1104
+ jest
1105
+ .spyOn(ticketsDataProvider, 'buildQueryNode')
1106
+ .mockImplementation((rq, parentId) => ({ id: rq.id, pId: parentId }));
1107
+ const wiql = "SELECT * FROM WorkItemLinks WHERE Source.[System.WorkItemType] = 'Feature' AND Target.[System.WorkItemType] = 'Epic'";
1108
+ const res = await ticketsDataProvider.structureFetchedQueries({ id: 'q2', isFolder: false, hasChildren: false, queryType: 'oneHop', wiql }, false, null, ['Epic'], ['Feature']);
1109
+ expect(res.tree1).toBeNull();
1110
+ expect(res.tree2).toEqual({ id: 'q2', pId: null });
1111
+ });
1112
+ describe('ID-only work item type fallback', () => {
1113
+ const projectHref = `${mockOrgUrl}MyProject/_apis/wit/wiql/123`;
1114
+ const allowedTypes = ['epic', 'feature', 'requirement'];
1115
+ const makeLeafQuery = (overrides = {}) => (Object.assign({ id: 'q1', name: 'Query 1', isFolder: false, hasChildren: false, queryType: 'oneHop', wiql: '', _links: { wiql: { href: projectHref } } }, overrides));
1116
+ it('should include a tree query when Source has [System.Id] and no Source type filter, and the fetched work item type is allowed', async () => {
1117
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce({
1118
+ fields: { 'System.WorkItemType': 'Epic' },
1119
+ });
1120
+ const queryNode = makeLeafQuery({
1121
+ queryType: 'tree',
1122
+ wiql: `
1123
+ SELECT * FROM WorkItemLinks
1124
+ WHERE
1125
+ ([Source].[System.Id] = 123)
1126
+ AND ([Target].[System.WorkItemType] IN ('Requirement','Epic','Feature'))
1127
+ `,
1128
+ });
1129
+ const res = await ticketsDataProvider.structureFetchedQueries(queryNode, false, null, allowedTypes, [], undefined, undefined, true);
1130
+ expect(res.tree1).not.toBeNull();
1131
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledTimes(1);
1132
+ });
1133
+ it('should exclude a tree query when Source has [System.Id] and no Source type filter, but the fetched work item type is not allowed', async () => {
1134
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce({
1135
+ fields: { 'System.WorkItemType': 'Bug' },
1136
+ });
1137
+ const queryNode = makeLeafQuery({
1138
+ queryType: 'tree',
1139
+ wiql: `
1140
+ SELECT * FROM WorkItemLinks
1141
+ WHERE
1142
+ ([Source].[System.Id] = 123)
1143
+ AND ([Target].[System.WorkItemType] IN ('Requirement','Epic','Feature'))
1144
+ `,
1145
+ });
1146
+ const res = await ticketsDataProvider.structureFetchedQueries(queryNode, false, null, allowedTypes, [], undefined, undefined, true);
1147
+ expect(res.tree1).toBeNull();
1148
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledTimes(1);
1149
+ });
1150
+ it('should include a oneHop query when Source has [System.Id] and no Source type filter, and the fetched work item type is allowed', async () => {
1151
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce({
1152
+ fields: { 'System.WorkItemType': 'Requirement' },
1153
+ });
1154
+ const queryNode = makeLeafQuery({
1155
+ queryType: 'oneHop',
1156
+ wiql: `
1157
+ SELECT * FROM WorkItemLinks
1158
+ WHERE
1159
+ ([Source].[System.Id] = 123)
1160
+ AND ([Target].[System.WorkItemType] = 'Feature')
1161
+ `,
1162
+ });
1163
+ const res = await ticketsDataProvider.structureFetchedQueries(queryNode, false, null, allowedTypes, allowedTypes);
1164
+ expect(res.tree1).not.toBeNull();
1165
+ // reverse evaluation should reuse the cached lookup
1166
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledTimes(1);
1167
+ });
1168
+ it('should include a flat query when [System.Id] exists and no [System.WorkItemType] filter exists, and the fetched work item type is allowed', async () => {
1169
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce({
1170
+ fields: { 'System.WorkItemType': 'Feature' },
1171
+ });
1172
+ const queryNode = makeLeafQuery({
1173
+ queryType: 'flat',
1174
+ wiql: `
1175
+ SELECT * FROM WorkItems
1176
+ WHERE [System.Id] = 123
1177
+ `,
1178
+ });
1179
+ const res = await ticketsDataProvider.structureFetchedQueries(queryNode, false, null, allowedTypes, [], undefined, undefined, false, [], true);
1180
+ expect(res.tree1).not.toBeNull();
1181
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledTimes(1);
1182
+ });
1183
+ it('should not trigger the ID fallback when no [System.Id] filter exists', async () => {
1184
+ const queryNode = makeLeafQuery({
1185
+ queryType: 'tree',
1186
+ wiql: `
1187
+ SELECT * FROM WorkItemLinks
1188
+ WHERE
1189
+ ([Target].[System.WorkItemType] IN ('Requirement','Epic','Feature'))
1190
+ `,
1191
+ });
1192
+ const res = await ticketsDataProvider.structureFetchedQueries(queryNode, false, null, allowedTypes, [], undefined, undefined, true);
1193
+ expect(res.tree1).toBeNull();
1194
+ expect(tfs_1.TFSServices.getItemContent).not.toHaveBeenCalled();
1195
+ });
1196
+ });
1197
+ });
1198
+ describe('structureAllQueryPath', () => {
1199
+ it('should return sysOverview+knownBugs nodes for a flat Bug leaf query', async () => {
1200
+ const leaf = {
1201
+ id: 'q1',
1202
+ name: 'BugQuery',
1203
+ hasChildren: false,
1204
+ isFolder: false,
1205
+ queryType: 'flat',
1206
+ columns: [],
1207
+ wiql: "SELECT * FROM WorkItems WHERE [System.WorkItemType] = 'Bug'",
1208
+ _links: { wiql: { href: 'http://example.com/wiql' } },
1209
+ };
1210
+ const res = await ticketsDataProvider.structureAllQueryPath(leaf, 'p');
1211
+ expect(res.tree1).toEqual(expect.objectContaining({ id: 'q1', pId: 'p', isValidQuery: true }));
1212
+ expect(res.tree2).toEqual(expect.objectContaining({ id: 'q1', pId: 'p', isValidQuery: true }));
1213
+ });
1214
+ it('should return sysOverview only for a flat non-bug leaf query', async () => {
1215
+ const leaf = {
1216
+ id: 'q2',
1217
+ name: 'OtherQuery',
1218
+ hasChildren: false,
1219
+ isFolder: false,
1220
+ queryType: 'flat',
1221
+ columns: [],
1222
+ wiql: "SELECT * FROM WorkItems WHERE [System.WorkItemType] = 'Task'",
1223
+ _links: { wiql: { href: 'http://example.com/wiql' } },
1224
+ };
1225
+ const res = await ticketsDataProvider.structureAllQueryPath(leaf, 'p');
1226
+ expect(res.tree1).toEqual(expect.objectContaining({ id: 'q2', pId: 'p', isValidQuery: true }));
1227
+ expect(res.tree2).toBeNull();
1228
+ });
1229
+ it('should return null trees for a folder leaf', async () => {
1230
+ const leafFolder = { id: 'f', name: 'Folder', hasChildren: false, isFolder: true };
1231
+ const res = await ticketsDataProvider.structureAllQueryPath(leafFolder, 'p');
1232
+ expect(res).toEqual({ tree1: null, tree2: null });
1233
+ });
1234
+ it('should fetch query children from url when children are missing', async () => {
1235
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce({
1236
+ id: 'q3',
1237
+ name: 'Fetched',
1238
+ hasChildren: false,
1239
+ isFolder: false,
1240
+ queryType: 'flat',
1241
+ columns: [],
1242
+ wiql: "SELECT * FROM WorkItems WHERE [System.WorkItemType] = 'Bug'",
1243
+ _links: { wiql: { href: 'http://example.com/wiql' } },
1244
+ });
1245
+ const res = await ticketsDataProvider.structureAllQueryPath({
1246
+ id: 'q3',
1247
+ url: 'https://example.com/q3',
1248
+ name: 'NeedFetch',
1249
+ hasChildren: true,
1250
+ children: undefined,
1251
+ });
1252
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledWith('https://example.com/q3?$depth=2&$expand=all', mockToken);
1253
+ expect(res.tree1).toEqual(expect.objectContaining({ id: 'q3', pId: 'q3' }));
1254
+ });
1255
+ });
1256
+ describe('parseTreeQueryResult', () => {
1257
+ it('should build roots, skip non-hierarchy links, and dedupe children', async () => {
1258
+ const initSpy = jest
1259
+ .spyOn(ticketsDataProvider, 'initTreeQueryResultItem')
1260
+ .mockImplementation(async (item, allItems) => {
1261
+ allItems[item.id] = {
1262
+ id: item.id,
1263
+ title: `T${item.id}`,
1264
+ description: '',
1265
+ htmlUrl: `h${item.id}`,
1266
+ children: [],
1267
+ };
1268
+ });
1269
+ const workItemRelations = [
1270
+ { source: null, target: { id: 1, url: 'u1' }, rel: null },
1271
+ { source: null, target: { id: 1, url: 'u1' }, rel: null },
1272
+ {
1273
+ source: { id: 1, url: 'u1' },
1274
+ target: { id: 2, url: 'u2' },
1275
+ rel: 'System.LinkTypes.Hierarchy-Forward',
1276
+ },
1277
+ {
1278
+ source: { id: 1, url: 'u1' },
1279
+ target: { id: 2, url: 'u2' },
1280
+ rel: 'System.LinkTypes.Hierarchy-Forward',
1281
+ },
1282
+ {
1283
+ source: { id: 1, url: 'u1' },
1284
+ target: { id: 3, url: 'u3' },
1285
+ rel: 'System.LinkTypes.Related',
1286
+ },
1287
+ ];
1288
+ const res = await ticketsDataProvider.parseTreeQueryResult({ workItemRelations });
1289
+ expect(initSpy).toHaveBeenCalled();
1290
+ expect(res.roots).toHaveLength(1);
1291
+ expect(res.roots[0].id).toBe(1);
1292
+ expect(res.roots[0].children).toHaveLength(1);
1293
+ expect(res.roots[0].children[0].id).toBe(2);
1294
+ expect(Object.keys(res.allItems)).toEqual(expect.arrayContaining(['1', '2', '3']));
1295
+ });
1296
+ it('should warn and initialize missing parent/child during hierarchy link processing when init no-ops first pass', async () => {
1297
+ const warnSpy = jest.spyOn(logger_1.default, 'warn');
1298
+ const callCountById = new Map();
1299
+ jest
1300
+ .spyOn(ticketsDataProvider, 'initTreeQueryResultItem')
1301
+ .mockImplementation(async (item, allItems) => {
1302
+ const count = (callCountById.get(item.id) || 0) + 1;
1303
+ callCountById.set(item.id, count);
1304
+ // First time: do nothing (simulate unexpected missing init)
1305
+ if (count === 1)
1306
+ return;
1307
+ // Second time: actually initialize
1308
+ allItems[item.id] = {
1309
+ id: item.id,
1310
+ title: `T${item.id}`,
1311
+ description: '',
1312
+ htmlUrl: `h${item.id}`,
1313
+ children: [],
1314
+ };
1315
+ });
1316
+ const workItemRelations = [
1317
+ {
1318
+ source: { id: 10, url: 'u10' },
1319
+ target: { id: 11, url: 'u11' },
1320
+ rel: 'System.LinkTypes.Hierarchy-Forward',
1321
+ },
1322
+ ];
1323
+ const res = await ticketsDataProvider.parseTreeQueryResult({ workItemRelations });
1324
+ expect(warnSpy).toHaveBeenCalled();
1325
+ expect(res.roots).toHaveLength(0);
1326
+ expect(res.allItems[10]).toBeDefined();
1327
+ expect(res.allItems[11]).toBeDefined();
1328
+ expect(res.allItems[10].children).toHaveLength(1);
1329
+ expect(res.allItems[10].children[0].id).toBe(11);
1330
+ });
1331
+ });
1332
+ describe('flattenTreeToWorkItems', () => {
1333
+ it('should flatten a roots tree into a list of work items', () => {
1334
+ const roots = [
1335
+ {
1336
+ id: 1,
1337
+ title: 'R1',
1338
+ description: 'D1',
1339
+ htmlUrl: 'h1',
1340
+ children: [{ id: 2, title: 'R2', description: 'D2', htmlUrl: 'h2', children: [] }],
1341
+ },
1342
+ ];
1343
+ const res = ticketsDataProvider.flattenTreeToWorkItems(roots);
1344
+ expect(res).toHaveLength(2);
1345
+ expect(res[0]).toEqual(expect.objectContaining({ id: 1, url: 'h1' }));
1346
+ expect(res[1]).toEqual(expect.objectContaining({ id: 2, url: 'h2' }));
1347
+ });
1348
+ });
1349
+ describe('GetQueryResultsByWiqlHref', () => {
1350
+ it('should fetch and model query results', async () => {
1351
+ // Arrange
1352
+ const mockWiqlHref = 'https://example.com/wiql';
1353
+ const mockResults = {
1354
+ workItems: [{ id: 1, url: 'https://example.com/wi/1' }],
1355
+ };
1356
+ const mockWiResponse = {
1357
+ id: 1,
1358
+ fields: { 'System.Title': 'Test' },
1359
+ _links: { html: { href: 'https://example.com' } },
1360
+ };
1361
+ tfs_1.TFSServices.getItemContent
1362
+ .mockResolvedValueOnce(mockResults)
1363
+ .mockResolvedValueOnce(mockWiResponse);
1364
+ // Act
1365
+ const result = await ticketsDataProvider.GetQueryResultsByWiqlHref(mockWiqlHref, mockProject);
1366
+ // Assert
1367
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledWith(mockWiqlHref, mockToken);
1368
+ });
1369
+ });
1370
+ describe('GetWorkItemByUrl', () => {
1371
+ it('should fetch work item by URL', async () => {
1372
+ // Arrange
1373
+ const mockUrl = 'https://dev.azure.com/org/project/_apis/wit/workitems/123';
1374
+ // Act
1375
+ const result = await ticketsDataProvider.GetWorkItemByUrl(mockUrl);
1376
+ // Assert
1377
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalled();
1378
+ });
1379
+ });
1380
+ describe('GetSharedQueries - docType branches', () => {
1381
+ it('should handle STR docType', async () => {
1382
+ // Arrange
1383
+ const mockQueries = {
1384
+ children: [{ name: 'STR', isFolder: true, children: [] }],
1385
+ };
1386
+ tfs_1.TFSServices.getItemContent.mockResolvedValue(mockQueries);
1387
+ // Act
1388
+ const result = await ticketsDataProvider.GetSharedQueries(mockProject, '', 'str');
1389
+ // Assert
1390
+ expect(result).toBeDefined();
1391
+ });
1392
+ it('should handle test-reporter docType', async () => {
1393
+ // Arrange
1394
+ const mockQueries = {
1395
+ children: [{ name: 'Test Reporter', isFolder: true, children: [] }],
1396
+ };
1397
+ tfs_1.TFSServices.getItemContent.mockResolvedValue(mockQueries);
1398
+ // Act
1399
+ const result = await ticketsDataProvider.GetSharedQueries(mockProject, '', 'test-reporter');
1400
+ // Assert
1401
+ expect(result).toBeDefined();
1402
+ });
1403
+ it('should handle SRS docType', async () => {
1404
+ // Arrange
1405
+ const mockQueries = {
1406
+ children: [{ name: 'SRS', isFolder: true, children: [] }],
1407
+ };
1408
+ tfs_1.TFSServices.getItemContent.mockResolvedValue(mockQueries);
1409
+ // Act
1410
+ const result = await ticketsDataProvider.GetSharedQueries(mockProject, '', 'srs');
1411
+ // Assert
1412
+ expect(result).toBeDefined();
1413
+ });
1414
+ it('should handle unknown docType', async () => {
1415
+ // Arrange
1416
+ const mockQueries = { children: [] };
1417
+ tfs_1.TFSServices.getItemContent.mockResolvedValue(mockQueries);
1418
+ // Act
1419
+ const result = await ticketsDataProvider.GetSharedQueries(mockProject, '', 'unknown');
1420
+ // Assert
1421
+ expect(result).toBeUndefined();
1422
+ });
1423
+ it('should handle error in GetSharedQueries', async () => {
1424
+ // Arrange
1425
+ tfs_1.TFSServices.getItemContent.mockRejectedValueOnce(new Error('API Error'));
1426
+ // Act & Assert
1427
+ await expect(ticketsDataProvider.GetSharedQueries(mockProject, '', 'std')).rejects.toThrow('API Error');
1428
+ expect(logger_1.default.error).toHaveBeenCalled();
1429
+ });
1430
+ });
1431
+ describe('PopulateWorkItemsByIds', () => {
1432
+ it('should populate work items by IDs', async () => {
1433
+ // Arrange
1434
+ const mockIds = [1, 2, 3];
1435
+ const mockResponse = {
1436
+ value: [
1437
+ { id: 1, fields: { 'System.Title': 'Item 1' } },
1438
+ { id: 2, fields: { 'System.Title': 'Item 2' } },
1439
+ { id: 3, fields: { 'System.Title': 'Item 3' } },
1440
+ ],
1441
+ };
1442
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce(mockResponse);
1443
+ // Act
1444
+ const result = await ticketsDataProvider.PopulateWorkItemsByIds(mockIds, mockProject);
1445
+ // Assert
1446
+ expect(result).toEqual(mockResponse.value);
1447
+ });
1448
+ it('should handle empty IDs array', async () => {
1449
+ // Arrange
1450
+ const mockIds = [];
1451
+ // Act
1452
+ const result = await ticketsDataProvider.PopulateWorkItemsByIds(mockIds, mockProject);
1453
+ // Assert
1454
+ expect(result).toEqual([]);
1455
+ });
1456
+ });
1457
+ describe('GetRelationsIds - edge cases', () => {
1458
+ it('should return a Map from work items with relations', async () => {
1459
+ // Arrange
1460
+ const mockIds = [
1461
+ {
1462
+ id: 1,
1463
+ relations: [{ rel: 'Parent', url: 'https://example.com/workitems/2' }],
1464
+ },
1465
+ ];
1466
+ // Act
1467
+ const result = await ticketsDataProvider.GetRelationsIds(mockIds);
1468
+ // Assert
1469
+ expect(result).toBeInstanceOf(Map);
1470
+ });
1471
+ });
1472
+ describe('GetLinks - edge cases', () => {
1473
+ it('should handle empty relations', async () => {
1474
+ // Arrange
1475
+ const mockWi = { relations: [] };
1476
+ const mockLinks = [];
1477
+ // Act
1478
+ const result = await ticketsDataProvider.GetLinks(mockProject, mockWi, mockLinks);
1479
+ // Assert
1480
+ expect(result).toHaveLength(0);
1481
+ });
1482
+ });
1483
+ describe('GetSharedQueries - path variations', () => {
1484
+ it('should use path in URL when provided', async () => {
1485
+ // Arrange
1486
+ const mockPath = 'My Queries/Test';
1487
+ const mockQueries = { children: [] };
1488
+ tfs_1.TFSServices.getItemContent.mockResolvedValue(mockQueries);
1489
+ // Act
1490
+ await ticketsDataProvider.GetSharedQueries(mockProject, mockPath, '');
1491
+ // Assert
1492
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalledWith(expect.stringContaining(mockPath), mockToken);
1493
+ });
1494
+ });
1495
+ describe('GetQueryResultById', () => {
1496
+ it('should fetch query result by ID', async () => {
1497
+ // Arrange
1498
+ const mockQueryId = 'query-123';
1499
+ const mockQuery = {
1500
+ _links: { wiql: { href: 'https://example.com/wiql' } },
1501
+ };
1502
+ const mockWiqlResult = { workItems: [{ id: 1 }] };
1503
+ const mockWiResponse = {
1504
+ fields: { 'System.Title': 'Test' },
1505
+ _links: { html: { href: 'https://example.com' } },
1506
+ };
1507
+ tfs_1.TFSServices.getItemContent
1508
+ .mockResolvedValueOnce(mockQuery)
1509
+ .mockResolvedValueOnce(mockWiqlResult)
1510
+ .mockResolvedValueOnce(mockWiResponse);
1511
+ // Act
1512
+ const result = await ticketsDataProvider.GetQueryResultById(mockProject, mockQueryId);
1513
+ // Assert
1514
+ expect(tfs_1.TFSServices.getItemContent).toHaveBeenCalled();
1515
+ });
1516
+ });
1517
+ describe('GetModeledQueryResults', () => {
1518
+ it('should model Flat query results including System.Id and System.AssignedTo displayName', async () => {
1519
+ const results = {
1520
+ asOf: 'now',
1521
+ queryResultType: 'workItem',
1522
+ queryType: tfs_data_1.QueryType.Flat,
1523
+ workItems: [{ id: '1', url: 'https://example.com/wi/1' }],
1524
+ columns: [
1525
+ { name: 'ID', referenceName: 'System.Id', url: 'u1' },
1526
+ { name: 'Assigned To', referenceName: 'System.AssignedTo', url: 'u2' },
1527
+ { name: 'Custom', referenceName: 'Custom.Field', url: 'u3' },
1528
+ ],
1529
+ };
1530
+ jest.spyOn(TicketsDataProvider_1.default.prototype, 'GetWorkItem').mockResolvedValueOnce({
1531
+ id: 1,
1532
+ url: 'https://example.com/wi/1',
1533
+ fields: {
1534
+ 'System.AssignedTo': { displayName: 'Bob' },
1535
+ 'Custom.Field': 'X',
1536
+ 'System.Id': 1,
1537
+ },
1538
+ relations: [{ rel: 'AttachedFile', url: 'https://example.com/a', attributes: { name: 'a.txt' } }],
1539
+ });
1540
+ const modeled = await ticketsDataProvider.GetModeledQueryResults(results, mockProject);
1541
+ expect(modeled.queryType).toBe(tfs_data_1.QueryType.Flat);
1542
+ expect(modeled.workItems).toHaveLength(1);
1543
+ expect(modeled.workItems[0].attachments).toBeDefined();
1544
+ // System.Id branch
1545
+ expect(modeled.workItems[0].fields[0]).toEqual(expect.objectContaining({ name: 'ID', value: '1' }));
1546
+ // System.AssignedTo branch
1547
+ // Note: implementation does not set fields[i].name for AssignedTo branch
1548
+ expect(modeled.workItems[0].fields[1]).toEqual(expect.objectContaining({ value: 'Bob' }));
1549
+ // default field branch
1550
+ expect(modeled.workItems[0].fields[2]).toEqual(expect.objectContaining({ name: 'Custom', value: 'X' }));
1551
+ });
1552
+ it('should model non-Flat query results using workItemRelations and set Source when present', async () => {
1553
+ const results = {
1554
+ asOf: 'now',
1555
+ queryResultType: 'workItemLink',
1556
+ queryType: tfs_data_1.QueryType.OneHop,
1557
+ workItemRelations: [
1558
+ {
1559
+ source: { id: 10 },
1560
+ target: { id: 20 },
1561
+ },
1562
+ ],
1563
+ columns: [
1564
+ { name: 'Assigned To', referenceName: 'System.AssignedTo', url: 'u2' },
1565
+ { name: 'Title', referenceName: 'System.Title', url: 'u3' },
1566
+ ],
1567
+ };
1568
+ jest.spyOn(TicketsDataProvider_1.default.prototype, 'GetWorkItem').mockResolvedValueOnce({
1569
+ id: 20,
1570
+ url: 'https://example.com/wi/20',
1571
+ fields: {
1572
+ // Ensure AssignedTo branch falls through to the default path
1573
+ 'System.AssignedTo': null,
1574
+ 'System.Title': 'T20',
1575
+ },
1576
+ relations: null,
1577
+ });
1578
+ const modeled = await ticketsDataProvider.GetModeledQueryResults(results, mockProject);
1579
+ expect(modeled.queryType).toBe(tfs_data_1.QueryType.OneHop);
1580
+ expect(modeled.workItems).toHaveLength(1);
1581
+ expect(modeled.workItems[0].Source).toBe(10);
1582
+ expect(modeled.workItems[0].url).toBe('https://example.com/wi/20');
1583
+ });
1584
+ });
1585
+ describe('GetCategorizedRequirementsByType', () => {
1586
+ it('should categorize requirement items from queryResult.workItems and include priority=1 in precedence category', async () => {
1587
+ const wiqlHref = `${mockOrgUrl}project1/_apis/wit/wiql/123`;
1588
+ // Other tests in this file may set a persistent mockResolvedValue on getItemContent.
1589
+ // clearAllMocks() does not reset implementations, so ensure a clean slate.
1590
+ tfs_1.TFSServices.getItemContent.mockReset();
1591
+ tfs_1.TFSServices.getItemContent
1592
+ // query result
1593
+ .mockResolvedValueOnce({ workItems: [{ id: 10 }, { id: 11 }, { id: 12 }, { id: 13 }] })
1594
+ // getRequirementTypeFieldRefs(project)
1595
+ .mockResolvedValueOnce({
1596
+ value: [
1597
+ { name: 'Requirement Type', referenceName: 'Microsoft.VSTS.CMMI.RequirementType' },
1598
+ { name: 'Requirement_Type', referenceName: 'Custom.RequirementType' },
1599
+ ],
1600
+ })
1601
+ // WI 10 requirement security, priority=1
1602
+ .mockResolvedValueOnce({
1603
+ id: 10,
1604
+ fields: {
1605
+ 'System.WorkItemType': 'Requirement',
1606
+ 'System.Title': 'R10',
1607
+ 'System.Description': 'D10',
1608
+ 'Microsoft.VSTS.CMMI.RequirementType': 'Security',
1609
+ 'Microsoft.VSTS.Common.Priority': 1,
1610
+ },
1611
+ _links: { html: { href: 'h10' } },
1612
+ })
1613
+ // WI 11 requirement unknown type -> Other Requirements
1614
+ .mockResolvedValueOnce({
1615
+ id: 11,
1616
+ fields: {
1617
+ 'System.WorkItemType': 'Requirement',
1618
+ 'System.Title': 'R11',
1619
+ 'System.Description': 'D11',
1620
+ 'Microsoft.VSTS.CMMI.RequirementType': 'UnknownType',
1621
+ },
1622
+ _links: { html: { href: 'h11' } },
1623
+ })
1624
+ // WI 12 not a requirement -> skipped
1625
+ .mockResolvedValueOnce({
1626
+ id: 12,
1627
+ fields: {
1628
+ 'System.WorkItemType': 'Bug',
1629
+ 'System.Title': 'B12',
1630
+ },
1631
+ _links: { html: { href: 'h12' } },
1632
+ })
1633
+ // WI 13 throws -> warn branch
1634
+ .mockRejectedValueOnce(new Error('boom'));
1635
+ const res = await ticketsDataProvider.GetCategorizedRequirementsByType(wiqlHref);
1636
+ expect(res.totalCount).toBe(4);
1637
+ expect(res.categories['Security and Privacy Requirements']).toHaveLength(1);
1638
+ expect(res.categories['Precedence and Criticality of Requirements']).toHaveLength(1);
1639
+ expect(res.categories['Other Requirements']).toHaveLength(1);
1640
+ expect(logger_1.default.warn).toHaveBeenCalledWith(expect.stringContaining('Could not fetch work item 13'));
1641
+ });
1642
+ it('should extract IDs from workItemRelations for OneHop queries', async () => {
1643
+ const wiqlHref = `${mockOrgUrl}project1/_apis/wit/wiql/rel`;
1644
+ tfs_1.TFSServices.getItemContent.mockReset();
1645
+ tfs_1.TFSServices.getItemContent
1646
+ .mockResolvedValueOnce({
1647
+ workItemRelations: [
1648
+ { source: { id: 10 }, target: { id: 20 } },
1649
+ { source: { id: 10 }, target: { id: 21 } },
1650
+ ],
1651
+ })
1652
+ .mockResolvedValueOnce({ value: [] })
1653
+ .mockResolvedValueOnce({
1654
+ id: 10,
1655
+ fields: { 'System.WorkItemType': 'Requirement', 'System.Title': 'R10' },
1656
+ _links: { html: { href: 'h10' } },
1657
+ })
1658
+ .mockResolvedValueOnce({
1659
+ id: 20,
1660
+ fields: { 'System.WorkItemType': 'Bug', 'System.Title': 'B20' },
1661
+ _links: { html: { href: 'h20' } },
1662
+ })
1663
+ .mockResolvedValueOnce({
1664
+ id: 21,
1665
+ fields: { 'System.WorkItemType': 'Requirement', 'System.Title': 'R21' },
1666
+ _links: { html: { href: 'h21' } },
1667
+ });
1668
+ const res = await ticketsDataProvider.GetCategorizedRequirementsByType(wiqlHref);
1669
+ expect(res.totalCount).toBe(3);
1670
+ });
1671
+ it('should return empty when query result has no workItems and no workItemRelations', async () => {
1672
+ const wiqlHref = `${mockOrgUrl}project1/_apis/wit/wiql/empty`;
1673
+ tfs_1.TFSServices.getItemContent.mockReset();
1674
+ tfs_1.TFSServices.getItemContent.mockResolvedValueOnce({});
1675
+ const res = await ticketsDataProvider.GetCategorizedRequirementsByType(wiqlHref);
1676
+ expect(res).toEqual({ categories: {}, totalCount: 0 });
1677
+ expect(logger_1.default.warn).toHaveBeenCalledWith(expect.stringContaining('No work items found in query result'));
1678
+ });
1679
+ });
1680
+ });
1681
+ //# sourceMappingURL=ticketsDataProvider.test.js.map