@elisra-devops/docgen-data-provider 1.63.13 → 1.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +26 -9
- package/.github/workflows/release.yml +9 -10
- package/README.md +50 -24
- package/bin/helpers/tfs.d.ts +3 -0
- package/bin/helpers/tfs.js +44 -7
- package/bin/helpers/tfs.js.map +1 -1
- package/bin/modules/GitDataProvider.d.ts +10 -0
- package/bin/modules/GitDataProvider.js +10 -0
- package/bin/modules/GitDataProvider.js.map +1 -1
- package/bin/modules/TestDataProvider.js +0 -1
- package/bin/modules/TestDataProvider.js.map +1 -1
- package/bin/modules/TicketsDataProvider.d.ts +63 -24
- package/bin/modules/TicketsDataProvider.js +216 -114
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/helpers/helper.test.js +279 -0
- package/bin/tests/helpers/helper.test.js.map +1 -0
- package/bin/{helpers/test → tests/helpers}/tfs.test.js +312 -49
- package/bin/tests/helpers/tfs.test.js.map +1 -0
- package/bin/tests/index.test.js +25 -0
- package/bin/tests/index.test.js.map +1 -0
- package/bin/tests/models/tfs-data.test.js +160 -0
- package/bin/tests/models/tfs-data.test.js.map +1 -0
- package/bin/{modules/test → tests/modules}/JfrogDataProvider.test.js +9 -9
- package/bin/tests/modules/JfrogDataProvider.test.js.map +1 -0
- package/bin/tests/modules/ResultDataProvider.test.js +1942 -0
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -0
- package/bin/tests/modules/gitDataProvider.test.js +1888 -0
- package/bin/tests/modules/gitDataProvider.test.js.map +1 -0
- package/bin/{modules/test → tests/modules}/managmentDataProvider.test.js +13 -1
- package/bin/tests/modules/managmentDataProvider.test.js.map +1 -0
- package/bin/tests/modules/pipelineDataProvider.test.d.ts +1 -0
- package/bin/tests/modules/pipelineDataProvider.test.js +783 -0
- package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -0
- package/bin/tests/modules/testDataProvider.test.d.ts +1 -0
- package/bin/tests/modules/testDataProvider.test.js +717 -0
- package/bin/tests/modules/testDataProvider.test.js.map +1 -0
- package/bin/tests/modules/ticketsDataProvider.test.d.ts +1 -0
- package/bin/tests/modules/ticketsDataProvider.test.js +1681 -0
- package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -0
- package/bin/tests/utils/DataProviderUtils.test.d.ts +1 -0
- package/bin/tests/utils/DataProviderUtils.test.js +61 -0
- package/bin/tests/utils/DataProviderUtils.test.js.map +1 -0
- package/bin/tests/utils/testStepParserHelper.test.d.ts +1 -0
- package/bin/tests/utils/testStepParserHelper.test.js +359 -0
- package/bin/tests/utils/testStepParserHelper.test.js.map +1 -0
- package/package.json +9 -1
- package/src/helpers/tfs.ts +51 -7
- package/src/modules/GitDataProvider.ts +10 -0
- package/src/modules/TestDataProvider.ts +0 -1
- package/src/modules/TicketsDataProvider.ts +298 -141
- package/src/tests/helpers/helper.test.ts +337 -0
- package/src/tests/helpers/tfs.test.ts +1092 -0
- package/src/tests/index.test.ts +28 -0
- package/src/tests/models/tfs-data.test.ts +203 -0
- package/src/tests/modules/JfrogDataProvider.test.ts +167 -0
- package/src/tests/modules/ResultDataProvider.test.ts +2571 -0
- package/src/tests/modules/gitDataProvider.test.ts +2628 -0
- package/src/{modules/test → tests/modules}/managmentDataProvider.test.ts +33 -1
- package/src/tests/modules/pipelineDataProvider.test.ts +1038 -0
- package/src/tests/modules/testDataProvider.test.ts +1046 -0
- package/src/tests/modules/ticketsDataProvider.test.ts +2204 -0
- package/src/tests/utils/DataProviderUtils.test.ts +76 -0
- package/src/tests/utils/testStepParserHelper.test.ts +437 -0
- package/tsconfig.json +1 -0
- package/bin/helpers/test/tfs.test.js.map +0 -1
- package/bin/modules/test/JfrogDataProvider.test.js.map +0 -1
- package/bin/modules/test/ResultDataProvider.test.js +0 -444
- package/bin/modules/test/ResultDataProvider.test.js.map +0 -1
- package/bin/modules/test/gitDataProvider.test.js +0 -428
- package/bin/modules/test/gitDataProvider.test.js.map +0 -1
- package/bin/modules/test/managmentDataProvider.test.js.map +0 -1
- package/bin/modules/test/pipelineDataProvider.test.js +0 -237
- package/bin/modules/test/pipelineDataProvider.test.js.map +0 -1
- package/bin/modules/test/testDataProvider.test.js +0 -234
- package/bin/modules/test/testDataProvider.test.js.map +0 -1
- package/bin/modules/test/ticketsDataProvider.test.js +0 -348
- package/bin/modules/test/ticketsDataProvider.test.js.map +0 -1
- package/src/helpers/test/tfs.test.ts +0 -748
- package/src/modules/test/JfrogDataProvider.test.ts +0 -171
- package/src/modules/test/ResultDataProvider.test.ts +0 -542
- package/src/modules/test/gitDataProvider.test.ts +0 -645
- package/src/modules/test/pipelineDataProvider.test.ts +0 -292
- package/src/modules/test/testDataProvider.test.ts +0 -318
- package/src/modules/test/ticketsDataProvider.test.ts +0 -462
- /package/bin/{helpers/test/tfs.test.d.ts → tests/helpers/helper.test.d.ts} +0 -0
- /package/bin/{modules/test/JfrogDataProvider.test.d.ts → tests/helpers/tfs.test.d.ts} +0 -0
- /package/bin/{modules/test/ResultDataProvider.test.d.ts → tests/index.test.d.ts} +0 -0
- /package/bin/{modules/test/gitDataProvider.test.d.ts → tests/models/tfs-data.test.d.ts} +0 -0
- /package/bin/{modules/test/managmentDataProvider.test.d.ts → tests/modules/JfrogDataProvider.test.d.ts} +0 -0
- /package/bin/{modules/test/pipelineDataProvider.test.d.ts → tests/modules/ResultDataProvider.test.d.ts} +0 -0
- /package/bin/{modules/test/testDataProvider.test.d.ts → tests/modules/gitDataProvider.test.d.ts} +0 -0
- /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
|