@elisra-devops/docgen-data-provider 1.104.0 → 1.105.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.
@@ -199,12 +199,25 @@ export default class TicketsDataProvider {
199
199
  const queriesWithChildren = await this.ensureQueryChildren(queries);
200
200
 
201
201
  switch (normalizedDocType) {
202
- case 'std': {
203
- const { root: stdRoot, found: stdRootFound } = await this.getDocTypeRoot(
204
- queriesWithChildren,
205
- 'std'
202
+ case 'std':
203
+ case 'stp': {
204
+ const rootCandidates =
205
+ normalizedDocType === 'stp' ? (['stp', 'std'] as const) : (['std'] as const);
206
+ let stdRoot = queriesWithChildren;
207
+ let stdRootFound = false;
208
+ for (const candidate of rootCandidates) {
209
+ const lookup = await this.getDocTypeRoot(queriesWithChildren, candidate);
210
+ if (lookup.found) {
211
+ stdRoot = lookup.root;
212
+ stdRootFound = true;
213
+ break;
214
+ }
215
+ }
216
+ logger.debug(
217
+ `[GetSharedQueries][${normalizedDocType}] using ${
218
+ stdRootFound ? 'dedicated folder' : 'root queries'
219
+ }`
206
220
  );
207
- logger.debug(`[GetSharedQueries][std] using ${stdRootFound ? 'dedicated folder' : 'root queries'}`);
208
221
  // Each branch describes the dedicated folder names, the fetch routine, and how to validate results.
209
222
  const stdBranches = await this.fetchDocTypeBranches(queriesWithChildren, stdRoot, [
210
223
  {
@@ -13,13 +13,14 @@ describe('Helper', () => {
13
13
  describe('suiteData class', () => {
14
14
  it('should create suiteData with correct properties', () => {
15
15
  // Act
16
- const suite = new suiteData('Test Suite', '123', '456', 2);
16
+ const suite = new suiteData('Test Suite', '123', '456', 2, 'Suite description');
17
17
 
18
18
  // Assert
19
19
  expect(suite.name).toBe('Test Suite');
20
20
  expect(suite.id).toBe('123');
21
21
  expect(suite.parent).toBe('456');
22
22
  expect(suite.level).toBe(2);
23
+ expect(suite.description).toBe('Suite description');
23
24
  expect(suite.url).toBeUndefined();
24
25
  });
25
26
  });
@@ -204,8 +204,8 @@ describe('TestDataProvider', () => {
204
204
  // Arrange
205
205
  const mockData = {
206
206
  testSuites: [
207
- { id: '123', name: 'Test Suite 1' },
208
- { id: '456', name: 'Test Suite 2' },
207
+ { id: '123', name: 'Test Suite 1', suiteDescription: 'Legacy Desc 1' },
208
+ { id: '456', name: 'Test Suite 2', description: 'Legacy Desc 2' },
209
209
  ],
210
210
  };
211
211
  (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockData);
@@ -214,19 +214,110 @@ describe('TestDataProvider', () => {
214
214
  const result = await testDataProvider.GetTestSuitesForPlan(mockProject, mockPlanId);
215
215
 
216
216
  // Assert
217
- expect(result).toEqual(mockData);
217
+ expect(result.testSuites).toEqual(
218
+ expect.arrayContaining([
219
+ expect.objectContaining({
220
+ id: '123',
221
+ title: 'Test Suite 1',
222
+ parentSuiteId: 0,
223
+ description: 'Legacy Desc 1',
224
+ }),
225
+ expect.objectContaining({
226
+ id: '456',
227
+ title: 'Test Suite 2',
228
+ parentSuiteId: 0,
229
+ description: 'Legacy Desc 2',
230
+ }),
231
+ ])
232
+ );
218
233
  expect(TFSServices.getItemContent).toHaveBeenCalledWith(
219
234
  `${mockOrgUrl.replace(/\/+$/, '')}/${mockProject}/_api/_testManagement/GetTestSuitesForPlan?__v=5&planId=${mockPlanId}`,
220
235
  mockToken
221
236
  );
222
237
  });
223
238
 
239
+ it('should enrich missing suite descriptions via work items batch for PAT token', async () => {
240
+ const suitesPayload = {
241
+ testSuites: [
242
+ { id: 11, name: 'Suite A' },
243
+ { id: 22, name: 'Suite B', suiteDescription: 'From legacy payload' },
244
+ ],
245
+ };
246
+ const workItemsPayload = {
247
+ value: [{ id: 11, fields: { 'System.Description': '<div>WI Desc A</div>' } }],
248
+ };
249
+ (TFSServices.getItemContent as jest.Mock).mockImplementation((url: string) => {
250
+ if (url.includes('/_apis/wit/workitemsbatch')) {
251
+ return Promise.resolve(workItemsPayload);
252
+ }
253
+ return Promise.resolve(suitesPayload);
254
+ });
255
+
256
+ const result = await testDataProvider.GetTestSuitesForPlan(mockProject, mockPlanId);
257
+
258
+ expect(TFSServices.getItemContent).toHaveBeenNthCalledWith(
259
+ 1,
260
+ `${mockOrgUrl.replace(/\/+$/, '')}/${mockProject}/_api/_testManagement/GetTestSuitesForPlan?__v=5&planId=${mockPlanId}`,
261
+ mockToken
262
+ );
263
+ expect(TFSServices.getItemContent).toHaveBeenNthCalledWith(
264
+ 2,
265
+ `${mockOrgUrl.replace(/\/+$/, '')}/${mockProject}/_apis/wit/workitemsbatch?api-version=7.1`,
266
+ mockToken,
267
+ 'post',
268
+ {
269
+ ids: [11],
270
+ fields: ['System.Description'],
271
+ errorPolicy: 'Omit',
272
+ },
273
+ { 'Content-Type': 'application/json' }
274
+ );
275
+ expect(result.testSuites).toEqual(
276
+ expect.arrayContaining([
277
+ expect.objectContaining({ id: 11, description: '<div>WI Desc A</div>' }),
278
+ expect.objectContaining({ id: 22, description: 'From legacy payload' }),
279
+ ])
280
+ );
281
+ });
282
+
283
+ it('should chunk work items batch requests when suite IDs exceed batch limit', async () => {
284
+ const suitesPayload = {
285
+ testSuites: Array.from({ length: 205 }, (_, i) => ({
286
+ id: i + 1,
287
+ name: `Suite ${i + 1}`,
288
+ })),
289
+ };
290
+ (TFSServices.getItemContent as jest.Mock).mockImplementation((url: string, _token: string, method?: string, data?: any) => {
291
+ if (url.includes('/_apis/wit/workitemsbatch') && method === 'post') {
292
+ const ids = Array.isArray(data?.ids) ? data.ids : [];
293
+ return Promise.resolve({
294
+ value: ids.map((id: number) => ({
295
+ id,
296
+ fields: { 'System.Description': `Desc ${id}` },
297
+ })),
298
+ });
299
+ }
300
+ return Promise.resolve(suitesPayload);
301
+ });
302
+
303
+ const result = await testDataProvider.GetTestSuitesForPlan(mockProject, mockPlanId);
304
+
305
+ expect((TFSServices.getItemContent as jest.Mock).mock.calls).toHaveLength(3);
306
+ expect((TFSServices.getItemContent as jest.Mock).mock.calls[1][2]).toBe('post');
307
+ expect((TFSServices.getItemContent as jest.Mock).mock.calls[1][3].ids).toHaveLength(200);
308
+ expect((TFSServices.getItemContent as jest.Mock).mock.calls[2][2]).toBe('post');
309
+ expect((TFSServices.getItemContent as jest.Mock).mock.calls[2][3].ids).toHaveLength(5);
310
+
311
+ expect(result.testSuites.find((s: any) => s.id === 1)?.description).toBe('Desc 1');
312
+ expect(result.testSuites.find((s: any) => s.id === 205)?.description).toBe('Desc 205');
313
+ });
314
+
224
315
  it('should use testplan suites endpoint for bearer token and normalize response', async () => {
225
316
  const bearerProvider = new TestDataProvider(mockOrgUrl, mockBearerToken);
226
317
  const mockData = {
227
318
  value: [
228
- { id: '123', name: 'Suite 1' },
229
- { id: '456', name: 'Suite 2', parentSuite: { id: '123' } },
319
+ { id: '123', name: 'Suite 1', description: 'Desc 1' },
320
+ { id: '456', name: 'Suite 2', parentSuite: { id: '123' }, suiteDescription: 'Desc 2' },
230
321
  ],
231
322
  count: 2,
232
323
  };
@@ -240,8 +331,58 @@ describe('TestDataProvider', () => {
240
331
  );
241
332
  expect(result.testSuites).toEqual(
242
333
  expect.arrayContaining([
243
- expect.objectContaining({ id: '123', title: 'Suite 1', parentSuiteId: 0 }),
244
- expect.objectContaining({ id: '456', title: 'Suite 2', parentSuiteId: '123' }),
334
+ expect.objectContaining({ id: '123', title: 'Suite 1', parentSuiteId: 0, description: 'Desc 1' }),
335
+ expect.objectContaining({
336
+ id: '456',
337
+ title: 'Suite 2',
338
+ parentSuiteId: '123',
339
+ description: 'Desc 2',
340
+ }),
341
+ ])
342
+ );
343
+ });
344
+
345
+ it('should enrich missing suite descriptions via work items batch for bearer token', async () => {
346
+ const bearerProvider = new TestDataProvider(mockOrgUrl, mockBearerToken);
347
+ const suitesPayload = {
348
+ value: [
349
+ { id: 101, name: 'Suite A' },
350
+ { id: 202, name: 'Suite B', description: 'Native Desc B' },
351
+ ],
352
+ };
353
+ const workItemsPayload = {
354
+ value: [{ id: 101, fields: { 'System.Description': 'Enriched Desc A' } }],
355
+ };
356
+ (TFSServices.getItemContent as jest.Mock).mockImplementation((url: string) => {
357
+ if (url.includes('/_apis/wit/workitemsbatch')) {
358
+ return Promise.resolve(workItemsPayload);
359
+ }
360
+ return Promise.resolve(suitesPayload);
361
+ });
362
+
363
+ const result = await bearerProvider.GetTestSuitesForPlan(mockProject, mockPlanId);
364
+
365
+ expect(TFSServices.getItemContent).toHaveBeenNthCalledWith(
366
+ 1,
367
+ `${mockOrgUrl.replace(/\/+$/, '')}/${mockProject}/_apis/testplan/Plans/${mockPlanId}/suites?includeChildren=true&api-version=7.0`,
368
+ mockBearerToken
369
+ );
370
+ expect(TFSServices.getItemContent).toHaveBeenNthCalledWith(
371
+ 2,
372
+ `${mockOrgUrl.replace(/\/+$/, '')}/${mockProject}/_apis/wit/workitemsbatch?api-version=7.1`,
373
+ mockBearerToken,
374
+ 'post',
375
+ {
376
+ ids: [101],
377
+ fields: ['System.Description'],
378
+ errorPolicy: 'Omit',
379
+ },
380
+ { 'Content-Type': 'application/json' }
381
+ );
382
+ expect(result.testSuites).toEqual(
383
+ expect.arrayContaining([
384
+ expect.objectContaining({ id: 101, description: 'Enriched Desc A' }),
385
+ expect.objectContaining({ id: 202, description: 'Native Desc B' }),
245
386
  ])
246
387
  );
247
388
  });
@@ -268,7 +409,14 @@ describe('TestDataProvider', () => {
268
409
  mockPlanId,
269
410
  mockOrgUrl,
270
411
  mockProject,
271
- mockTestSuites.testSuites,
412
+ expect.arrayContaining([
413
+ expect.objectContaining({
414
+ id: '123',
415
+ title: 'Test Suite 1',
416
+ parentSuiteId: 0,
417
+ description: '',
418
+ }),
419
+ ]),
272
420
  mockSuiteId,
273
421
  true
274
422
  );
@@ -278,7 +426,7 @@ describe('TestDataProvider', () => {
278
426
  it('should use bearer suites payload when token is bearer', async () => {
279
427
  const bearerProvider = new TestDataProvider(mockOrgUrl, mockBearerToken);
280
428
  const mockTestSuites = {
281
- value: [{ id: '123', name: 'Suite 1', parentSuite: { id: 0 } }],
429
+ value: [{ id: '123', name: 'Suite 1', parentSuite: { id: 0 }, description: 'Suite Desc' }],
282
430
  };
283
431
  const mockSuiteData = [new suiteData('Suite 1', '123', '456', 1)];
284
432
  (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockTestSuites);
@@ -1046,6 +1194,74 @@ describe('TestDataProvider', () => {
1046
1194
  );
1047
1195
  });
1048
1196
 
1197
+ it('should populate testPhase from Custom.TestPhase', async () => {
1198
+ const suite = { id: '1', name: 'Suite 1' } as any;
1199
+ const testCases = {
1200
+ count: 1,
1201
+ value: [{ testCase: { id: 123, url: 'https://example.com/testcase/123' } }],
1202
+ };
1203
+
1204
+ jest.spyOn(testDataProvider as any, 'fetchWithCache').mockResolvedValueOnce({
1205
+ id: 123,
1206
+ fields: {
1207
+ 'System.Title': 'TC 123',
1208
+ 'System.AreaPath': 'A',
1209
+ 'System.Description': 'D',
1210
+ 'Custom.TestPhase': 'SIT',
1211
+ 'Microsoft.VSTS.TCM.Steps': null,
1212
+ },
1213
+ relations: [],
1214
+ });
1215
+
1216
+ const res = await testDataProvider.StructureTestCase(
1217
+ mockProject,
1218
+ testCases as any,
1219
+ suite,
1220
+ false,
1221
+ false,
1222
+ false,
1223
+ new Map<string, string[]>(),
1224
+ new Map<string, string[]>()
1225
+ );
1226
+
1227
+ expect(res).toHaveLength(1);
1228
+ expect(res[0].testPhase).toBe('SIT');
1229
+ });
1230
+
1231
+ it('should extract testPhase from fallback key containing testphase', async () => {
1232
+ const suite = { id: '1', name: 'Suite 1' } as any;
1233
+ const testCases = {
1234
+ count: 1,
1235
+ value: [{ testCase: { id: 123, url: 'https://example.com/testcase/123' } }],
1236
+ };
1237
+
1238
+ jest.spyOn(testDataProvider as any, 'fetchWithCache').mockResolvedValueOnce({
1239
+ id: 123,
1240
+ fields: {
1241
+ 'System.Title': 'TC 123',
1242
+ 'System.AreaPath': 'A',
1243
+ 'System.Description': 'D',
1244
+ 'Custom.MyTestPhaseField': { displayName: 'Regression' },
1245
+ 'Microsoft.VSTS.TCM.Steps': null,
1246
+ },
1247
+ relations: [],
1248
+ });
1249
+
1250
+ const res = await testDataProvider.StructureTestCase(
1251
+ mockProject,
1252
+ testCases as any,
1253
+ suite,
1254
+ false,
1255
+ false,
1256
+ false,
1257
+ new Map<string, string[]>(),
1258
+ new Map<string, string[]>()
1259
+ );
1260
+
1261
+ expect(res).toHaveLength(1);
1262
+ expect(res[0].testPhase).toBe('Regression');
1263
+ });
1264
+
1049
1265
  it('should return empty list when test case fetch fails', async () => {
1050
1266
  const suite = { id: '1', name: 'Suite 1' } as any;
1051
1267
  const testCases = {
@@ -1848,6 +1848,81 @@ describe('TicketsDataProvider', () => {
1848
1848
  });
1849
1849
 
1850
1850
  describe('GetSharedQueries - docType branches', () => {
1851
+ it('should handle STP docType', async () => {
1852
+ // Arrange
1853
+ const mockQueries = {
1854
+ children: [{ name: 'STP', isFolder: true, children: [] }],
1855
+ };
1856
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValue(mockQueries);
1857
+
1858
+ // Act
1859
+ const result = await ticketsDataProvider.GetSharedQueries(mockProject, '', 'stp');
1860
+
1861
+ // Assert
1862
+ expect(result).toBeDefined();
1863
+ });
1864
+
1865
+ it('should fallback to STD root when STP root is missing', async () => {
1866
+ const rootQueries = { id: 'root', children: [] };
1867
+ const stdRoot = { id: 'std-root', children: [] };
1868
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValue(rootQueries);
1869
+
1870
+ const getDocTypeRootSpy = jest
1871
+ .spyOn(ticketsDataProvider as any, 'getDocTypeRoot')
1872
+ .mockImplementation(async (...args: any[]) => {
1873
+ const docTypeName = String(args[1] || '');
1874
+ if (docTypeName === 'stp') return { root: rootQueries, found: false };
1875
+ if (docTypeName === 'std') return { root: stdRoot, found: true };
1876
+ return { root: rootQueries, found: false };
1877
+ });
1878
+
1879
+ const fetchDocTypeBranchesSpy = jest
1880
+ .spyOn(ticketsDataProvider as any, 'fetchDocTypeBranches')
1881
+ .mockResolvedValue({
1882
+ reqToTest: { result: { reqTestTree: { id: 'req-tree' }, testReqTree: { id: 'fallback-tree' } } },
1883
+ testToReq: { result: { testReqTree: { id: 'test-tree' } } },
1884
+ mom: { result: { linkedMomTree: { id: 'mom-tree' } } },
1885
+ });
1886
+
1887
+ const result = await ticketsDataProvider.GetSharedQueries(mockProject, '', 'stp');
1888
+
1889
+ expect(getDocTypeRootSpy).toHaveBeenNthCalledWith(1, rootQueries, 'stp');
1890
+ expect(getDocTypeRootSpy).toHaveBeenNthCalledWith(2, rootQueries, 'std');
1891
+ expect(fetchDocTypeBranchesSpy).toHaveBeenCalledWith(rootQueries, stdRoot, expect.any(Array));
1892
+ expect(result.reqTestQueries.reqTestTree).toEqual({ id: 'req-tree' });
1893
+ expect(result.reqTestQueries.testReqTree).toEqual({ id: 'test-tree' });
1894
+ expect(result.linkedMomQueries.linkedMomTree).toEqual({ id: 'mom-tree' });
1895
+ });
1896
+
1897
+ it('should prefer STP root over STD when both exist', async () => {
1898
+ const rootQueries = { id: 'root', children: [] };
1899
+ const stpRoot = { id: 'stp-root', children: [] };
1900
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValue(rootQueries);
1901
+
1902
+ const getDocTypeRootSpy = jest
1903
+ .spyOn(ticketsDataProvider as any, 'getDocTypeRoot')
1904
+ .mockImplementation(async (...args: any[]) => {
1905
+ const docTypeName = String(args[1] || '');
1906
+ if (docTypeName === 'stp') return { root: stpRoot, found: true };
1907
+ if (docTypeName === 'std') return { root: { id: 'std-root', children: [] }, found: true };
1908
+ return { root: rootQueries, found: false };
1909
+ });
1910
+
1911
+ const fetchDocTypeBranchesSpy = jest
1912
+ .spyOn(ticketsDataProvider as any, 'fetchDocTypeBranches')
1913
+ .mockResolvedValue({
1914
+ reqToTest: { result: { reqTestTree: { id: 'req-tree' }, testReqTree: null } },
1915
+ testToReq: { result: { testReqTree: { id: 'test-tree' } } },
1916
+ mom: { result: { linkedMomTree: null } },
1917
+ });
1918
+
1919
+ await ticketsDataProvider.GetSharedQueries(mockProject, '', 'stp');
1920
+
1921
+ expect(getDocTypeRootSpy).toHaveBeenCalledTimes(1);
1922
+ expect(getDocTypeRootSpy).toHaveBeenCalledWith(rootQueries, 'stp');
1923
+ expect(fetchDocTypeBranchesSpy).toHaveBeenCalledWith(rootQueries, stpRoot, expect.any(Array));
1924
+ });
1925
+
1851
1926
  it('should handle STR docType', async () => {
1852
1927
  // Arrange
1853
1928
  const mockQueries = {