@elisra-devops/docgen-data-provider 1.109.1 → 1.111.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/bin/helpers/tfs.js +9 -6
- package/bin/helpers/tfs.js.map +1 -1
- package/bin/modules/PipelinesDataProvider.d.ts +97 -2
- package/bin/modules/PipelinesDataProvider.js +281 -9
- package/bin/modules/PipelinesDataProvider.js.map +1 -1
- package/bin/modules/TicketsDataProvider.d.ts +2 -0
- package/bin/modules/TicketsDataProvider.js +74 -44
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/helpers/tfs.test.js +4 -3
- package/bin/tests/helpers/tfs.test.js.map +1 -1
- package/bin/tests/modules/pipelineDataProvider.test.js +336 -4
- package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -1
- package/bin/tests/modules/ticketsDataProvider.test.js +152 -34
- package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/helpers/tfs.ts +11 -6
- package/src/modules/PipelinesDataProvider.ts +400 -9
- package/src/modules/TicketsDataProvider.ts +96 -52
- package/src/tests/helpers/tfs.test.ts +4 -3
- package/src/tests/modules/pipelineDataProvider.test.ts +512 -5
- package/src/tests/modules/ticketsDataProvider.test.ts +177 -39
|
@@ -21,6 +21,7 @@ describe('TFSServices', () => {
|
|
|
21
21
|
|
|
22
22
|
beforeEach(() => {
|
|
23
23
|
jest.clearAllMocks();
|
|
24
|
+
mockAxiosInstance.request.mockReset();
|
|
24
25
|
// Mock Math.random to return a predictable value for tests with retry
|
|
25
26
|
Math.random = jest.fn().mockReturnValue(0.5);
|
|
26
27
|
});
|
|
@@ -191,7 +192,7 @@ describe('TFSServices', () => {
|
|
|
191
192
|
url: url.replace(/ /g, '%20'),
|
|
192
193
|
method: 'get',
|
|
193
194
|
auth: { username: '', password: pat },
|
|
194
|
-
timeout:
|
|
195
|
+
timeout: 30000,
|
|
195
196
|
})
|
|
196
197
|
);
|
|
197
198
|
});
|
|
@@ -228,7 +229,7 @@ describe('TFSServices', () => {
|
|
|
228
229
|
timeoutError.name = 'TimeoutError';
|
|
229
230
|
(timeoutError as any).code = 'ECONNABORTED';
|
|
230
231
|
|
|
231
|
-
//
|
|
232
|
+
// Timeout errors should get the standard retry budget.
|
|
232
233
|
mockAxiosInstance.request
|
|
233
234
|
.mockRejectedValueOnce(timeoutError)
|
|
234
235
|
.mockRejectedValueOnce(timeoutError)
|
|
@@ -290,7 +291,7 @@ describe('TFSServices', () => {
|
|
|
290
291
|
|
|
291
292
|
// Act & Assert
|
|
292
293
|
await expect(TFSServices.getItemContent(url, pat)).rejects.toThrow('ENOTFOUND');
|
|
293
|
-
expect(mockAxiosInstance.request).toHaveBeenCalledTimes(
|
|
294
|
+
expect(mockAxiosInstance.request).toHaveBeenCalledTimes(3); // Initial + 2 retries
|
|
294
295
|
});
|
|
295
296
|
|
|
296
297
|
it('should handle spaces in URL by replacing them with %20', async () => {
|
|
@@ -581,21 +581,312 @@ describe('PipelinesDataProvider', () => {
|
|
|
581
581
|
expect(result.value).toEqual([{ id: 1 }]);
|
|
582
582
|
});
|
|
583
583
|
|
|
584
|
-
it('should
|
|
584
|
+
it('should continue paging for reversed release ranges until the lower release id is loaded', async () => {
|
|
585
|
+
const projectName = 'project1';
|
|
586
|
+
const definitionId = '456';
|
|
587
|
+
|
|
588
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
589
|
+
.mockResolvedValueOnce({
|
|
590
|
+
data: { value: [{ id: 20 }, { id: 19 }] },
|
|
591
|
+
headers: { 'x-ms-continuationtoken': 'page-2' },
|
|
592
|
+
})
|
|
593
|
+
.mockResolvedValueOnce({
|
|
594
|
+
data: { value: [{ id: 14 }, { id: 13 }] },
|
|
595
|
+
headers: {},
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const result = await pipelinesDataProvider.GetAllReleaseHistory(projectName, definitionId, {
|
|
599
|
+
fromId: 20,
|
|
600
|
+
toId: 14,
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
expect(result.value.map((r: any) => r.id)).toEqual([20, 19, 14, 13]);
|
|
604
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledTimes(2);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('should throw when release history pagination fails', async () => {
|
|
585
608
|
// Arrange
|
|
586
609
|
const projectName = 'project1';
|
|
587
610
|
const definitionId = '456';
|
|
588
611
|
(TFSServices.getItemContentWithHeaders as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
|
|
589
612
|
|
|
590
|
-
// Act
|
|
591
|
-
const result = await pipelinesDataProvider.GetAllReleaseHistory(projectName, definitionId);
|
|
592
|
-
|
|
593
613
|
// Assert
|
|
594
|
-
expect(
|
|
614
|
+
await expect(pipelinesDataProvider.GetAllReleaseHistory(projectName, definitionId)).rejects.toThrow(
|
|
615
|
+
'API Error'
|
|
616
|
+
);
|
|
595
617
|
expect(logger.error).toHaveBeenCalled();
|
|
596
618
|
});
|
|
597
619
|
});
|
|
598
620
|
|
|
621
|
+
describe('findPreviousSuccessfulRelease', () => {
|
|
622
|
+
const releaseCandidate = (id: number, status = 'succeeded') => ({
|
|
623
|
+
id,
|
|
624
|
+
status: 'active',
|
|
625
|
+
environments: [{ status }],
|
|
626
|
+
releaseDefinition: { id: 456 },
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('should find previous successful release on a later Release API page', async () => {
|
|
630
|
+
const projectName = 'project1';
|
|
631
|
+
const definitionId = '456';
|
|
632
|
+
|
|
633
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
634
|
+
.mockResolvedValueOnce({
|
|
635
|
+
data: { value: [] },
|
|
636
|
+
headers: { 'x-ms-continuationtoken': 'next-page' },
|
|
637
|
+
})
|
|
638
|
+
.mockResolvedValueOnce({
|
|
639
|
+
data: { value: [releaseCandidate(80)] },
|
|
640
|
+
headers: {},
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
const result = await pipelinesDataProvider.findPreviousSuccessfulRelease(
|
|
644
|
+
projectName,
|
|
645
|
+
definitionId,
|
|
646
|
+
100
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
expect(result).toBe(80);
|
|
650
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledWith(
|
|
651
|
+
expect.stringContaining('continuationToken=next-page'),
|
|
652
|
+
mockToken,
|
|
653
|
+
'get',
|
|
654
|
+
null,
|
|
655
|
+
null
|
|
656
|
+
);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it('should query release history with expanded environments and ignore non-successful releases', async () => {
|
|
660
|
+
const projectName = 'project1';
|
|
661
|
+
const definitionId = '456';
|
|
662
|
+
|
|
663
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockResolvedValueOnce({
|
|
664
|
+
data: {
|
|
665
|
+
value: [
|
|
666
|
+
releaseCandidate(99, 'failed'),
|
|
667
|
+
releaseCandidate(98, 'rejected'),
|
|
668
|
+
releaseCandidate(97, 'succeeded'),
|
|
669
|
+
],
|
|
670
|
+
},
|
|
671
|
+
headers: {},
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
const result = await pipelinesDataProvider.findPreviousSuccessfulRelease(
|
|
675
|
+
projectName,
|
|
676
|
+
definitionId,
|
|
677
|
+
100
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
const url = (TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[0][0];
|
|
681
|
+
expect(url).toContain(`definitionId=${definitionId}`);
|
|
682
|
+
expect(url).toContain('queryOrder=descending');
|
|
683
|
+
expect(url).toContain('$top=200');
|
|
684
|
+
expect(url).toContain('$expand=environments');
|
|
685
|
+
expect(result).toBe(97);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it('should retry release discovery with api-version 6.0 when 7.1 is unsupported', async () => {
|
|
689
|
+
const unsupportedError: any = new Error('The requested resource does not support api-version 7.1');
|
|
690
|
+
unsupportedError.response = { status: 404 };
|
|
691
|
+
|
|
692
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
693
|
+
.mockRejectedValueOnce(unsupportedError)
|
|
694
|
+
.mockResolvedValueOnce({
|
|
695
|
+
data: { value: [releaseCandidate(90)] },
|
|
696
|
+
headers: {},
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
const result = await pipelinesDataProvider.findPreviousSuccessfulRelease('project1', '456', 100);
|
|
700
|
+
|
|
701
|
+
expect(result).toBe(90);
|
|
702
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[0][0]).toContain(
|
|
703
|
+
'api-version=7.1'
|
|
704
|
+
);
|
|
705
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[1][0]).toContain(
|
|
706
|
+
'api-version=6.0'
|
|
707
|
+
);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('should retry release discovery when unsupported api-version is reported in Axios response data', async () => {
|
|
711
|
+
const unsupportedError: any = new Error('Request failed with status code 404');
|
|
712
|
+
unsupportedError.response = {
|
|
713
|
+
status: 404,
|
|
714
|
+
data: { message: 'The requested resource does not support api-version 7.1' },
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
718
|
+
.mockRejectedValueOnce(unsupportedError)
|
|
719
|
+
.mockResolvedValueOnce({
|
|
720
|
+
data: { value: [releaseCandidate(89)] },
|
|
721
|
+
headers: {},
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
const result = await pipelinesDataProvider.findPreviousSuccessfulRelease('project1', '456', 100);
|
|
725
|
+
|
|
726
|
+
expect(result).toBe(89);
|
|
727
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[1][0]).toContain(
|
|
728
|
+
'api-version=6.0'
|
|
729
|
+
);
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it('should not retry api-version 6.0 for ordinary invalid release definition errors', async () => {
|
|
733
|
+
const invalidDefinitionError: any = new Error('Release definition 456 was not found');
|
|
734
|
+
invalidDefinitionError.response = { status: 404 };
|
|
735
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockRejectedValueOnce(invalidDefinitionError);
|
|
736
|
+
|
|
737
|
+
await expect(
|
|
738
|
+
pipelinesDataProvider.findPreviousSuccessfulRelease('project1', '456', 100)
|
|
739
|
+
).rejects.toThrow('Release definition 456 was not found');
|
|
740
|
+
|
|
741
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledTimes(1);
|
|
742
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[0][0]).toContain(
|
|
743
|
+
'api-version=7.1'
|
|
744
|
+
);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('should throw release discovery permission errors without retrying api-version 6.0', async () => {
|
|
748
|
+
const permissionError: any = new Error('Forbidden');
|
|
749
|
+
permissionError.response = { status: 403 };
|
|
750
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockRejectedValueOnce(permissionError);
|
|
751
|
+
|
|
752
|
+
await expect(
|
|
753
|
+
pipelinesDataProvider.findPreviousSuccessfulRelease('project1', '456', 100)
|
|
754
|
+
).rejects.toThrow('Forbidden');
|
|
755
|
+
|
|
756
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledTimes(1);
|
|
757
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[0][0]).toContain(
|
|
758
|
+
'api-version=7.1'
|
|
759
|
+
);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('should throw when a later release discovery page fails', async () => {
|
|
763
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
764
|
+
.mockResolvedValueOnce({
|
|
765
|
+
data: { value: [] },
|
|
766
|
+
headers: { 'x-ms-continuationtoken': 'next-page' },
|
|
767
|
+
})
|
|
768
|
+
.mockRejectedValueOnce(new Error('page failed'));
|
|
769
|
+
|
|
770
|
+
await expect(
|
|
771
|
+
pipelinesDataProvider.findPreviousSuccessfulRelease('project1', '456', 100)
|
|
772
|
+
).rejects.toThrow('page failed');
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it('should throw when previous release discovery exceeds the defensive page limit', async () => {
|
|
776
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockImplementation(() =>
|
|
777
|
+
Promise.resolve({
|
|
778
|
+
data: { value: [] },
|
|
779
|
+
headers: { 'x-ms-continuationtoken': 'next-page' },
|
|
780
|
+
})
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
await expect(
|
|
784
|
+
pipelinesDataProvider.findPreviousSuccessfulRelease('project1', '456', 100)
|
|
785
|
+
).rejects.toThrow('Release discovery exceeded 50 pages');
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
describe('findLatestSuccessfulRelease', () => {
|
|
790
|
+
const releaseCandidate = (id: number, status = 'succeeded') => ({
|
|
791
|
+
id,
|
|
792
|
+
status: 'active',
|
|
793
|
+
environments: [{ status }],
|
|
794
|
+
releaseDefinition: { id: 456 },
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('should find latest successful release on a later Release API page', async () => {
|
|
798
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
799
|
+
.mockResolvedValueOnce({
|
|
800
|
+
data: { value: [releaseCandidate(100, 'failed')] },
|
|
801
|
+
headers: { 'x-ms-continuationtoken': 'next-page' },
|
|
802
|
+
})
|
|
803
|
+
.mockResolvedValueOnce({
|
|
804
|
+
data: { value: [releaseCandidate(90)] },
|
|
805
|
+
headers: {},
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
const result = await pipelinesDataProvider.findLatestSuccessfulRelease('project1', '456');
|
|
809
|
+
|
|
810
|
+
expect(result).toBe(90);
|
|
811
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledWith(
|
|
812
|
+
expect.stringContaining('continuationToken=next-page'),
|
|
813
|
+
mockToken,
|
|
814
|
+
'get',
|
|
815
|
+
null,
|
|
816
|
+
null
|
|
817
|
+
);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('should retry latest release discovery with api-version 6.0 only for unsupported api-version errors', async () => {
|
|
821
|
+
const unsupportedError: any = new Error('The requested resource does not support api-version 7.1');
|
|
822
|
+
unsupportedError.response = { status: 404 };
|
|
823
|
+
|
|
824
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
825
|
+
.mockRejectedValueOnce(unsupportedError)
|
|
826
|
+
.mockResolvedValueOnce({
|
|
827
|
+
data: { value: [releaseCandidate(101)] },
|
|
828
|
+
headers: {},
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
const result = await pipelinesDataProvider.findLatestSuccessfulRelease('project1', '456');
|
|
832
|
+
|
|
833
|
+
expect(result).toBe(101);
|
|
834
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[0][0]).toContain(
|
|
835
|
+
'api-version=7.1'
|
|
836
|
+
);
|
|
837
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[1][0]).toContain(
|
|
838
|
+
'api-version=6.0'
|
|
839
|
+
);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it('should retry latest release discovery when unsupported api-version is reported in Axios response data', async () => {
|
|
843
|
+
const unsupportedError: any = new Error('Request failed with status code 404');
|
|
844
|
+
unsupportedError.response = {
|
|
845
|
+
status: 404,
|
|
846
|
+
data: { message: 'The requested resource does not support api-version 7.1' },
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
850
|
+
.mockRejectedValueOnce(unsupportedError)
|
|
851
|
+
.mockResolvedValueOnce({
|
|
852
|
+
data: { value: [releaseCandidate(102)] },
|
|
853
|
+
headers: {},
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
const result = await pipelinesDataProvider.findLatestSuccessfulRelease('project1', '456');
|
|
857
|
+
|
|
858
|
+
expect(result).toBe(102);
|
|
859
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[1][0]).toContain(
|
|
860
|
+
'api-version=6.0'
|
|
861
|
+
);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it('should not retry latest release discovery for ordinary 404 errors', async () => {
|
|
865
|
+
const invalidDefinitionError: any = new Error('Release definition 456 was not found');
|
|
866
|
+
invalidDefinitionError.response = { status: 404 };
|
|
867
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockRejectedValueOnce(invalidDefinitionError);
|
|
868
|
+
|
|
869
|
+
await expect(
|
|
870
|
+
pipelinesDataProvider.findLatestSuccessfulRelease('project1', '456')
|
|
871
|
+
).rejects.toThrow('Release definition 456 was not found');
|
|
872
|
+
|
|
873
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledTimes(1);
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
it('should throw when latest release discovery exceeds the defensive page limit', async () => {
|
|
877
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockImplementation(() =>
|
|
878
|
+
Promise.resolve({
|
|
879
|
+
data: { value: [] },
|
|
880
|
+
headers: { 'x-ms-continuationtoken': 'next-page' },
|
|
881
|
+
})
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
await expect(
|
|
885
|
+
pipelinesDataProvider.findLatestSuccessfulRelease('project1', '456')
|
|
886
|
+
).rejects.toThrow('Release discovery exceeded 50 pages');
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
|
|
599
890
|
describe('GetAllPipelines', () => {
|
|
600
891
|
it('should fetch all pipelines for a project', async () => {
|
|
601
892
|
// Arrange
|
|
@@ -877,12 +1168,40 @@ describe('PipelinesDataProvider', () => {
|
|
|
877
1168
|
});
|
|
878
1169
|
|
|
879
1170
|
describe('findPreviousPipeline', () => {
|
|
1171
|
+
const targetPipelineRun = {
|
|
1172
|
+
resources: {
|
|
1173
|
+
repositories: {
|
|
1174
|
+
'0': {
|
|
1175
|
+
self: {
|
|
1176
|
+
repository: { id: 'repo1' },
|
|
1177
|
+
version: 'target-sha',
|
|
1178
|
+
refName: 'refs/heads/main',
|
|
1179
|
+
},
|
|
1180
|
+
},
|
|
1181
|
+
},
|
|
1182
|
+
},
|
|
1183
|
+
} as unknown as PipelineRun;
|
|
1184
|
+
|
|
1185
|
+
const buildCandidate = (id: number, branchName: string, repoId = 'repo1') => ({
|
|
1186
|
+
id,
|
|
1187
|
+
status: 'completed',
|
|
1188
|
+
result: 'succeeded',
|
|
1189
|
+
sourceBranch: branchName,
|
|
1190
|
+
sourceVersion: `sha-${id}`,
|
|
1191
|
+
repository: { id: repoId },
|
|
1192
|
+
definition: { id: 123 },
|
|
1193
|
+
});
|
|
1194
|
+
|
|
880
1195
|
it('should return undefined when no pipeline runs exist', async () => {
|
|
881
1196
|
// Arrange
|
|
882
1197
|
const teamProject = 'project1';
|
|
883
1198
|
const pipelineId = '123';
|
|
884
1199
|
const toPipelineRunId = 100;
|
|
885
1200
|
const targetPipeline = {} as PipelineRun;
|
|
1201
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockResolvedValueOnce({
|
|
1202
|
+
data: { value: [] },
|
|
1203
|
+
headers: {},
|
|
1204
|
+
});
|
|
886
1205
|
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({});
|
|
887
1206
|
|
|
888
1207
|
// Act
|
|
@@ -908,6 +1227,15 @@ describe('PipelinesDataProvider', () => {
|
|
|
908
1227
|
},
|
|
909
1228
|
} as any;
|
|
910
1229
|
|
|
1230
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
1231
|
+
.mockResolvedValueOnce({
|
|
1232
|
+
data: { value: [] },
|
|
1233
|
+
headers: {},
|
|
1234
|
+
})
|
|
1235
|
+
.mockResolvedValueOnce({
|
|
1236
|
+
data: { value: [] },
|
|
1237
|
+
headers: {},
|
|
1238
|
+
});
|
|
911
1239
|
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({
|
|
912
1240
|
value: [
|
|
913
1241
|
{ id: 100, result: 'succeeded' },
|
|
@@ -938,6 +1266,10 @@ describe('PipelinesDataProvider', () => {
|
|
|
938
1266
|
const toPipelineRunId = 100;
|
|
939
1267
|
const targetPipeline = {} as PipelineRun;
|
|
940
1268
|
|
|
1269
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockResolvedValueOnce({
|
|
1270
|
+
data: { value: [] },
|
|
1271
|
+
headers: {},
|
|
1272
|
+
});
|
|
941
1273
|
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({
|
|
942
1274
|
value: [{ id: 99, result: 'succeeded' }],
|
|
943
1275
|
});
|
|
@@ -955,6 +1287,7 @@ describe('PipelinesDataProvider', () => {
|
|
|
955
1287
|
|
|
956
1288
|
expect(res).toBeUndefined();
|
|
957
1289
|
expect(detailsSpy).not.toHaveBeenCalled();
|
|
1290
|
+
expect(TFSServices.getItemContentWithHeaders).not.toHaveBeenCalled();
|
|
958
1291
|
});
|
|
959
1292
|
|
|
960
1293
|
it('should skip when pipeline details do not include repositories', async () => {
|
|
@@ -1018,6 +1351,15 @@ describe('PipelinesDataProvider', () => {
|
|
|
1018
1351
|
},
|
|
1019
1352
|
};
|
|
1020
1353
|
|
|
1354
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
1355
|
+
.mockResolvedValueOnce({
|
|
1356
|
+
data: { value: [] },
|
|
1357
|
+
headers: {},
|
|
1358
|
+
})
|
|
1359
|
+
.mockResolvedValueOnce({
|
|
1360
|
+
data: { value: [] },
|
|
1361
|
+
headers: {},
|
|
1362
|
+
});
|
|
1021
1363
|
(TFSServices.getItemContent as jest.Mock)
|
|
1022
1364
|
.mockResolvedValueOnce(mockRunHistory)
|
|
1023
1365
|
.mockResolvedValueOnce(mockPipelineDetails);
|
|
@@ -1034,6 +1376,171 @@ describe('PipelinesDataProvider', () => {
|
|
|
1034
1376
|
// Assert
|
|
1035
1377
|
expect(result).toBe(99);
|
|
1036
1378
|
});
|
|
1379
|
+
|
|
1380
|
+
it('should find previous successful build on a later Builds API page', async () => {
|
|
1381
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
1382
|
+
.mockResolvedValueOnce({
|
|
1383
|
+
data: { value: [] },
|
|
1384
|
+
headers: { 'x-ms-continuationtoken': 'next-page' },
|
|
1385
|
+
})
|
|
1386
|
+
.mockResolvedValueOnce({
|
|
1387
|
+
data: { value: [buildCandidate(80, 'refs/heads/main')] },
|
|
1388
|
+
headers: {},
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
const result = await pipelinesDataProvider.findPreviousPipeline(
|
|
1392
|
+
'project1',
|
|
1393
|
+
'123',
|
|
1394
|
+
100,
|
|
1395
|
+
targetPipelineRun,
|
|
1396
|
+
true
|
|
1397
|
+
);
|
|
1398
|
+
|
|
1399
|
+
expect(result).toBe(80);
|
|
1400
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledWith(
|
|
1401
|
+
expect.stringContaining('continuationToken=next-page'),
|
|
1402
|
+
mockToken,
|
|
1403
|
+
'get',
|
|
1404
|
+
null,
|
|
1405
|
+
null
|
|
1406
|
+
);
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
it('should prefer a same-branch successful build before trying cross-branch fallback', async () => {
|
|
1410
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockResolvedValueOnce({
|
|
1411
|
+
data: { value: [buildCandidate(90, 'refs/heads/main')] },
|
|
1412
|
+
headers: {},
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
const result = await pipelinesDataProvider.findPreviousPipeline(
|
|
1416
|
+
'project1',
|
|
1417
|
+
'123',
|
|
1418
|
+
100,
|
|
1419
|
+
targetPipelineRun,
|
|
1420
|
+
true
|
|
1421
|
+
);
|
|
1422
|
+
|
|
1423
|
+
expect(result).toBe(90);
|
|
1424
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledTimes(1);
|
|
1425
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[0][0]).toContain(
|
|
1426
|
+
'branchName=refs%2Fheads%2Fmain'
|
|
1427
|
+
);
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
it('should fall back to a different branch only when same-branch discovery fails', async () => {
|
|
1431
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
1432
|
+
.mockResolvedValueOnce({
|
|
1433
|
+
data: { value: [] },
|
|
1434
|
+
headers: {},
|
|
1435
|
+
})
|
|
1436
|
+
.mockResolvedValueOnce({
|
|
1437
|
+
data: { value: [buildCandidate(95, 'refs/heads/release')] },
|
|
1438
|
+
headers: {},
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
const result = await pipelinesDataProvider.findPreviousPipeline(
|
|
1442
|
+
'project1',
|
|
1443
|
+
'123',
|
|
1444
|
+
100,
|
|
1445
|
+
targetPipelineRun,
|
|
1446
|
+
true
|
|
1447
|
+
);
|
|
1448
|
+
|
|
1449
|
+
expect(result).toBe(95);
|
|
1450
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledTimes(2);
|
|
1451
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[0][0]).toContain(
|
|
1452
|
+
'branchName=refs%2Fheads%2Fmain'
|
|
1453
|
+
);
|
|
1454
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[1][0]).not.toContain(
|
|
1455
|
+
'branchName='
|
|
1456
|
+
);
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
it('should query completed successful builds and ignore non-previous candidates', async () => {
|
|
1460
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
1461
|
+
.mockResolvedValueOnce({
|
|
1462
|
+
data: { value: [buildCandidate(100, 'refs/heads/main'), buildCandidate(101, 'refs/heads/main')] },
|
|
1463
|
+
headers: {},
|
|
1464
|
+
})
|
|
1465
|
+
.mockResolvedValueOnce({
|
|
1466
|
+
data: { value: [] },
|
|
1467
|
+
headers: {},
|
|
1468
|
+
});
|
|
1469
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({});
|
|
1470
|
+
|
|
1471
|
+
const result = await pipelinesDataProvider.findPreviousPipeline(
|
|
1472
|
+
'project1',
|
|
1473
|
+
'123',
|
|
1474
|
+
100,
|
|
1475
|
+
targetPipelineRun,
|
|
1476
|
+
true
|
|
1477
|
+
);
|
|
1478
|
+
|
|
1479
|
+
const url = (TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[0][0];
|
|
1480
|
+
expect(url).toContain('definitions=123');
|
|
1481
|
+
expect(url).toContain('resultFilter=succeeded');
|
|
1482
|
+
expect(url).toContain('statusFilter=completed');
|
|
1483
|
+
expect(url).toContain('queryOrder=finishTimeDescending');
|
|
1484
|
+
expect(result).toBeUndefined();
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
it('should throw and not try cross-branch fallback when same-branch Builds API fails', async () => {
|
|
1488
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockRejectedValueOnce(new Error('same branch failed'));
|
|
1489
|
+
|
|
1490
|
+
await expect(
|
|
1491
|
+
pipelinesDataProvider.findPreviousPipeline('project1', '123', 100, targetPipelineRun, true)
|
|
1492
|
+
).rejects.toThrow('same branch failed');
|
|
1493
|
+
|
|
1494
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledTimes(1);
|
|
1495
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[0][0]).toContain(
|
|
1496
|
+
'branchName=refs%2Fheads%2Fmain'
|
|
1497
|
+
);
|
|
1498
|
+
expect(TFSServices.getItemContent).not.toHaveBeenCalled();
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
it('should throw when cross-branch Builds API fallback fails after same-branch no-match', async () => {
|
|
1502
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
1503
|
+
.mockResolvedValueOnce({
|
|
1504
|
+
data: { value: [] },
|
|
1505
|
+
headers: {},
|
|
1506
|
+
})
|
|
1507
|
+
.mockRejectedValueOnce(new Error('fallback failed'));
|
|
1508
|
+
|
|
1509
|
+
await expect(
|
|
1510
|
+
pipelinesDataProvider.findPreviousPipeline('project1', '123', 100, targetPipelineRun, true)
|
|
1511
|
+
).rejects.toThrow('fallback failed');
|
|
1512
|
+
|
|
1513
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledTimes(2);
|
|
1514
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[1][0]).not.toContain(
|
|
1515
|
+
'branchName='
|
|
1516
|
+
);
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
it('should throw when a later Builds API page fails', async () => {
|
|
1520
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
1521
|
+
.mockResolvedValueOnce({
|
|
1522
|
+
data: { value: [] },
|
|
1523
|
+
headers: { 'x-ms-continuationtoken': 'next-page' },
|
|
1524
|
+
})
|
|
1525
|
+
.mockRejectedValueOnce(new Error('build page failed'));
|
|
1526
|
+
|
|
1527
|
+
await expect(
|
|
1528
|
+
pipelinesDataProvider.findPreviousPipeline('project1', '123', 100, targetPipelineRun, true)
|
|
1529
|
+
).rejects.toThrow('build page failed');
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
it('should throw when previous build discovery exceeds the defensive page limit', async () => {
|
|
1533
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockImplementation(() =>
|
|
1534
|
+
Promise.resolve({
|
|
1535
|
+
data: { value: [] },
|
|
1536
|
+
headers: { 'x-ms-continuationtoken': 'next-page' },
|
|
1537
|
+
})
|
|
1538
|
+
);
|
|
1539
|
+
|
|
1540
|
+
await expect(
|
|
1541
|
+
pipelinesDataProvider.findPreviousPipeline('project1', '123', 100, targetPipelineRun, true)
|
|
1542
|
+
).rejects.toThrow('Pipeline discovery exceeded 50 pages');
|
|
1543
|
+
});
|
|
1037
1544
|
});
|
|
1038
1545
|
|
|
1039
1546
|
describe('private helper methods', () => {
|