@elisra-devops/docgen-data-provider 1.110.0 → 1.112.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/GitDataProvider.d.ts +2 -1
- package/bin/modules/GitDataProvider.js +3 -0
- package/bin/modules/GitDataProvider.js.map +1 -1
- package/bin/modules/PipelinesDataProvider.d.ts +105 -2
- package/bin/modules/PipelinesDataProvider.js +294 -9
- package/bin/modules/PipelinesDataProvider.js.map +1 -1
- package/bin/modules/TicketsDataProvider.js +2 -1
- 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/gitDataProvider.test.js +23 -0
- package/bin/tests/modules/gitDataProvider.test.js.map +1 -1
- package/bin/tests/modules/pipelineDataProvider.test.js +352 -4
- package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -1
- package/bin/tests/modules/ticketsDataProvider.test.js +2 -0
- 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/GitDataProvider.ts +4 -0
- package/src/modules/PipelinesDataProvider.ts +414 -9
- package/src/modules/TicketsDataProvider.ts +2 -1
- package/src/tests/helpers/tfs.test.ts +4 -3
- package/src/tests/modules/gitDataProvider.test.ts +31 -0
- package/src/tests/modules/pipelineDataProvider.test.ts +542 -5
- package/src/tests/modules/ticketsDataProvider.test.ts +5 -0
|
@@ -18,6 +18,36 @@ describe('PipelinesDataProvider', () => {
|
|
|
18
18
|
pipelinesDataProvider = new PipelinesDataProvider(mockOrgUrl, mockToken);
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
describe('GetBuildWorkItems', () => {
|
|
22
|
+
it('should return work item references associated with a single build', async () => {
|
|
23
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValue({
|
|
24
|
+
value: [{ id: '1', url: 'https://example.test/wi/1' }],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const result = await pipelinesDataProvider.GetBuildWorkItems('project1', 123);
|
|
28
|
+
|
|
29
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
30
|
+
'https://dev.azure.com/orgname/project1/_apis/build/builds/123/workitems?$top=2000&api-version=6.0',
|
|
31
|
+
mockToken,
|
|
32
|
+
'get'
|
|
33
|
+
);
|
|
34
|
+
expect(result).toEqual([{ id: '1', url: 'https://example.test/wi/1' }]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should encode project names in the build work items URL', async () => {
|
|
38
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValue({});
|
|
39
|
+
|
|
40
|
+
const result = await pipelinesDataProvider.GetBuildWorkItems('Project With Spaces', 123);
|
|
41
|
+
|
|
42
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
43
|
+
'https://dev.azure.com/orgname/Project%20With%20Spaces/_apis/build/builds/123/workitems?$top=2000&api-version=6.0',
|
|
44
|
+
mockToken,
|
|
45
|
+
'get'
|
|
46
|
+
);
|
|
47
|
+
expect(result).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
21
51
|
describe('isMatchingPipeline', () => {
|
|
22
52
|
// Create test method to access private method
|
|
23
53
|
const invokeIsMatchingPipeline = (
|
|
@@ -581,21 +611,312 @@ describe('PipelinesDataProvider', () => {
|
|
|
581
611
|
expect(result.value).toEqual([{ id: 1 }]);
|
|
582
612
|
});
|
|
583
613
|
|
|
584
|
-
it('should
|
|
614
|
+
it('should continue paging for reversed release ranges until the lower release id is loaded', async () => {
|
|
615
|
+
const projectName = 'project1';
|
|
616
|
+
const definitionId = '456';
|
|
617
|
+
|
|
618
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
619
|
+
.mockResolvedValueOnce({
|
|
620
|
+
data: { value: [{ id: 20 }, { id: 19 }] },
|
|
621
|
+
headers: { 'x-ms-continuationtoken': 'page-2' },
|
|
622
|
+
})
|
|
623
|
+
.mockResolvedValueOnce({
|
|
624
|
+
data: { value: [{ id: 14 }, { id: 13 }] },
|
|
625
|
+
headers: {},
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
const result = await pipelinesDataProvider.GetAllReleaseHistory(projectName, definitionId, {
|
|
629
|
+
fromId: 20,
|
|
630
|
+
toId: 14,
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
expect(result.value.map((r: any) => r.id)).toEqual([20, 19, 14, 13]);
|
|
634
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledTimes(2);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('should throw when release history pagination fails', async () => {
|
|
585
638
|
// Arrange
|
|
586
639
|
const projectName = 'project1';
|
|
587
640
|
const definitionId = '456';
|
|
588
641
|
(TFSServices.getItemContentWithHeaders as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
|
|
589
642
|
|
|
590
|
-
// Act
|
|
591
|
-
const result = await pipelinesDataProvider.GetAllReleaseHistory(projectName, definitionId);
|
|
592
|
-
|
|
593
643
|
// Assert
|
|
594
|
-
expect(
|
|
644
|
+
await expect(pipelinesDataProvider.GetAllReleaseHistory(projectName, definitionId)).rejects.toThrow(
|
|
645
|
+
'API Error'
|
|
646
|
+
);
|
|
595
647
|
expect(logger.error).toHaveBeenCalled();
|
|
596
648
|
});
|
|
597
649
|
});
|
|
598
650
|
|
|
651
|
+
describe('findPreviousSuccessfulRelease', () => {
|
|
652
|
+
const releaseCandidate = (id: number, status = 'succeeded') => ({
|
|
653
|
+
id,
|
|
654
|
+
status: 'active',
|
|
655
|
+
environments: [{ status }],
|
|
656
|
+
releaseDefinition: { id: 456 },
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it('should find previous successful release on a later Release API page', async () => {
|
|
660
|
+
const projectName = 'project1';
|
|
661
|
+
const definitionId = '456';
|
|
662
|
+
|
|
663
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
664
|
+
.mockResolvedValueOnce({
|
|
665
|
+
data: { value: [] },
|
|
666
|
+
headers: { 'x-ms-continuationtoken': 'next-page' },
|
|
667
|
+
})
|
|
668
|
+
.mockResolvedValueOnce({
|
|
669
|
+
data: { value: [releaseCandidate(80)] },
|
|
670
|
+
headers: {},
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
const result = await pipelinesDataProvider.findPreviousSuccessfulRelease(
|
|
674
|
+
projectName,
|
|
675
|
+
definitionId,
|
|
676
|
+
100
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
expect(result).toBe(80);
|
|
680
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledWith(
|
|
681
|
+
expect.stringContaining('continuationToken=next-page'),
|
|
682
|
+
mockToken,
|
|
683
|
+
'get',
|
|
684
|
+
null,
|
|
685
|
+
null
|
|
686
|
+
);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('should query release history with expanded environments and ignore non-successful releases', async () => {
|
|
690
|
+
const projectName = 'project1';
|
|
691
|
+
const definitionId = '456';
|
|
692
|
+
|
|
693
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockResolvedValueOnce({
|
|
694
|
+
data: {
|
|
695
|
+
value: [
|
|
696
|
+
releaseCandidate(99, 'failed'),
|
|
697
|
+
releaseCandidate(98, 'rejected'),
|
|
698
|
+
releaseCandidate(97, 'succeeded'),
|
|
699
|
+
],
|
|
700
|
+
},
|
|
701
|
+
headers: {},
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
const result = await pipelinesDataProvider.findPreviousSuccessfulRelease(
|
|
705
|
+
projectName,
|
|
706
|
+
definitionId,
|
|
707
|
+
100
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
const url = (TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[0][0];
|
|
711
|
+
expect(url).toContain(`definitionId=${definitionId}`);
|
|
712
|
+
expect(url).toContain('queryOrder=descending');
|
|
713
|
+
expect(url).toContain('$top=200');
|
|
714
|
+
expect(url).toContain('$expand=environments');
|
|
715
|
+
expect(result).toBe(97);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('should retry release discovery with api-version 6.0 when 7.1 is unsupported', async () => {
|
|
719
|
+
const unsupportedError: any = new Error('The requested resource does not support api-version 7.1');
|
|
720
|
+
unsupportedError.response = { status: 404 };
|
|
721
|
+
|
|
722
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
723
|
+
.mockRejectedValueOnce(unsupportedError)
|
|
724
|
+
.mockResolvedValueOnce({
|
|
725
|
+
data: { value: [releaseCandidate(90)] },
|
|
726
|
+
headers: {},
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
const result = await pipelinesDataProvider.findPreviousSuccessfulRelease('project1', '456', 100);
|
|
730
|
+
|
|
731
|
+
expect(result).toBe(90);
|
|
732
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[0][0]).toContain(
|
|
733
|
+
'api-version=7.1'
|
|
734
|
+
);
|
|
735
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[1][0]).toContain(
|
|
736
|
+
'api-version=6.0'
|
|
737
|
+
);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it('should retry release discovery when unsupported api-version is reported in Axios response data', async () => {
|
|
741
|
+
const unsupportedError: any = new Error('Request failed with status code 404');
|
|
742
|
+
unsupportedError.response = {
|
|
743
|
+
status: 404,
|
|
744
|
+
data: { message: 'The requested resource does not support api-version 7.1' },
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
748
|
+
.mockRejectedValueOnce(unsupportedError)
|
|
749
|
+
.mockResolvedValueOnce({
|
|
750
|
+
data: { value: [releaseCandidate(89)] },
|
|
751
|
+
headers: {},
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
const result = await pipelinesDataProvider.findPreviousSuccessfulRelease('project1', '456', 100);
|
|
755
|
+
|
|
756
|
+
expect(result).toBe(89);
|
|
757
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[1][0]).toContain(
|
|
758
|
+
'api-version=6.0'
|
|
759
|
+
);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('should not retry api-version 6.0 for ordinary invalid release definition errors', async () => {
|
|
763
|
+
const invalidDefinitionError: any = new Error('Release definition 456 was not found');
|
|
764
|
+
invalidDefinitionError.response = { status: 404 };
|
|
765
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockRejectedValueOnce(invalidDefinitionError);
|
|
766
|
+
|
|
767
|
+
await expect(
|
|
768
|
+
pipelinesDataProvider.findPreviousSuccessfulRelease('project1', '456', 100)
|
|
769
|
+
).rejects.toThrow('Release definition 456 was not found');
|
|
770
|
+
|
|
771
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledTimes(1);
|
|
772
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[0][0]).toContain(
|
|
773
|
+
'api-version=7.1'
|
|
774
|
+
);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
it('should throw release discovery permission errors without retrying api-version 6.0', async () => {
|
|
778
|
+
const permissionError: any = new Error('Forbidden');
|
|
779
|
+
permissionError.response = { status: 403 };
|
|
780
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockRejectedValueOnce(permissionError);
|
|
781
|
+
|
|
782
|
+
await expect(
|
|
783
|
+
pipelinesDataProvider.findPreviousSuccessfulRelease('project1', '456', 100)
|
|
784
|
+
).rejects.toThrow('Forbidden');
|
|
785
|
+
|
|
786
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledTimes(1);
|
|
787
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[0][0]).toContain(
|
|
788
|
+
'api-version=7.1'
|
|
789
|
+
);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it('should throw when a later release discovery page fails', async () => {
|
|
793
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
794
|
+
.mockResolvedValueOnce({
|
|
795
|
+
data: { value: [] },
|
|
796
|
+
headers: { 'x-ms-continuationtoken': 'next-page' },
|
|
797
|
+
})
|
|
798
|
+
.mockRejectedValueOnce(new Error('page failed'));
|
|
799
|
+
|
|
800
|
+
await expect(
|
|
801
|
+
pipelinesDataProvider.findPreviousSuccessfulRelease('project1', '456', 100)
|
|
802
|
+
).rejects.toThrow('page failed');
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it('should throw when previous release discovery exceeds the defensive page limit', async () => {
|
|
806
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockImplementation(() =>
|
|
807
|
+
Promise.resolve({
|
|
808
|
+
data: { value: [] },
|
|
809
|
+
headers: { 'x-ms-continuationtoken': 'next-page' },
|
|
810
|
+
})
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
await expect(
|
|
814
|
+
pipelinesDataProvider.findPreviousSuccessfulRelease('project1', '456', 100)
|
|
815
|
+
).rejects.toThrow('Release discovery exceeded 50 pages');
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
describe('findLatestSuccessfulRelease', () => {
|
|
820
|
+
const releaseCandidate = (id: number, status = 'succeeded') => ({
|
|
821
|
+
id,
|
|
822
|
+
status: 'active',
|
|
823
|
+
environments: [{ status }],
|
|
824
|
+
releaseDefinition: { id: 456 },
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
it('should find latest successful release on a later Release API page', async () => {
|
|
828
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
829
|
+
.mockResolvedValueOnce({
|
|
830
|
+
data: { value: [releaseCandidate(100, 'failed')] },
|
|
831
|
+
headers: { 'x-ms-continuationtoken': 'next-page' },
|
|
832
|
+
})
|
|
833
|
+
.mockResolvedValueOnce({
|
|
834
|
+
data: { value: [releaseCandidate(90)] },
|
|
835
|
+
headers: {},
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
const result = await pipelinesDataProvider.findLatestSuccessfulRelease('project1', '456');
|
|
839
|
+
|
|
840
|
+
expect(result).toBe(90);
|
|
841
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledWith(
|
|
842
|
+
expect.stringContaining('continuationToken=next-page'),
|
|
843
|
+
mockToken,
|
|
844
|
+
'get',
|
|
845
|
+
null,
|
|
846
|
+
null
|
|
847
|
+
);
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it('should retry latest release discovery with api-version 6.0 only for unsupported api-version errors', async () => {
|
|
851
|
+
const unsupportedError: any = new Error('The requested resource does not support api-version 7.1');
|
|
852
|
+
unsupportedError.response = { status: 404 };
|
|
853
|
+
|
|
854
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
855
|
+
.mockRejectedValueOnce(unsupportedError)
|
|
856
|
+
.mockResolvedValueOnce({
|
|
857
|
+
data: { value: [releaseCandidate(101)] },
|
|
858
|
+
headers: {},
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
const result = await pipelinesDataProvider.findLatestSuccessfulRelease('project1', '456');
|
|
862
|
+
|
|
863
|
+
expect(result).toBe(101);
|
|
864
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[0][0]).toContain(
|
|
865
|
+
'api-version=7.1'
|
|
866
|
+
);
|
|
867
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[1][0]).toContain(
|
|
868
|
+
'api-version=6.0'
|
|
869
|
+
);
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
it('should retry latest release discovery when unsupported api-version is reported in Axios response data', async () => {
|
|
873
|
+
const unsupportedError: any = new Error('Request failed with status code 404');
|
|
874
|
+
unsupportedError.response = {
|
|
875
|
+
status: 404,
|
|
876
|
+
data: { message: 'The requested resource does not support api-version 7.1' },
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
880
|
+
.mockRejectedValueOnce(unsupportedError)
|
|
881
|
+
.mockResolvedValueOnce({
|
|
882
|
+
data: { value: [releaseCandidate(102)] },
|
|
883
|
+
headers: {},
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
const result = await pipelinesDataProvider.findLatestSuccessfulRelease('project1', '456');
|
|
887
|
+
|
|
888
|
+
expect(result).toBe(102);
|
|
889
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[1][0]).toContain(
|
|
890
|
+
'api-version=6.0'
|
|
891
|
+
);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
it('should not retry latest release discovery for ordinary 404 errors', async () => {
|
|
895
|
+
const invalidDefinitionError: any = new Error('Release definition 456 was not found');
|
|
896
|
+
invalidDefinitionError.response = { status: 404 };
|
|
897
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockRejectedValueOnce(invalidDefinitionError);
|
|
898
|
+
|
|
899
|
+
await expect(
|
|
900
|
+
pipelinesDataProvider.findLatestSuccessfulRelease('project1', '456')
|
|
901
|
+
).rejects.toThrow('Release definition 456 was not found');
|
|
902
|
+
|
|
903
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledTimes(1);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
it('should throw when latest release discovery exceeds the defensive page limit', async () => {
|
|
907
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockImplementation(() =>
|
|
908
|
+
Promise.resolve({
|
|
909
|
+
data: { value: [] },
|
|
910
|
+
headers: { 'x-ms-continuationtoken': 'next-page' },
|
|
911
|
+
})
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
await expect(
|
|
915
|
+
pipelinesDataProvider.findLatestSuccessfulRelease('project1', '456')
|
|
916
|
+
).rejects.toThrow('Release discovery exceeded 50 pages');
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
|
|
599
920
|
describe('GetAllPipelines', () => {
|
|
600
921
|
it('should fetch all pipelines for a project', async () => {
|
|
601
922
|
// Arrange
|
|
@@ -877,12 +1198,40 @@ describe('PipelinesDataProvider', () => {
|
|
|
877
1198
|
});
|
|
878
1199
|
|
|
879
1200
|
describe('findPreviousPipeline', () => {
|
|
1201
|
+
const targetPipelineRun = {
|
|
1202
|
+
resources: {
|
|
1203
|
+
repositories: {
|
|
1204
|
+
'0': {
|
|
1205
|
+
self: {
|
|
1206
|
+
repository: { id: 'repo1' },
|
|
1207
|
+
version: 'target-sha',
|
|
1208
|
+
refName: 'refs/heads/main',
|
|
1209
|
+
},
|
|
1210
|
+
},
|
|
1211
|
+
},
|
|
1212
|
+
},
|
|
1213
|
+
} as unknown as PipelineRun;
|
|
1214
|
+
|
|
1215
|
+
const buildCandidate = (id: number, branchName: string, repoId = 'repo1') => ({
|
|
1216
|
+
id,
|
|
1217
|
+
status: 'completed',
|
|
1218
|
+
result: 'succeeded',
|
|
1219
|
+
sourceBranch: branchName,
|
|
1220
|
+
sourceVersion: `sha-${id}`,
|
|
1221
|
+
repository: { id: repoId },
|
|
1222
|
+
definition: { id: 123 },
|
|
1223
|
+
});
|
|
1224
|
+
|
|
880
1225
|
it('should return undefined when no pipeline runs exist', async () => {
|
|
881
1226
|
// Arrange
|
|
882
1227
|
const teamProject = 'project1';
|
|
883
1228
|
const pipelineId = '123';
|
|
884
1229
|
const toPipelineRunId = 100;
|
|
885
1230
|
const targetPipeline = {} as PipelineRun;
|
|
1231
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockResolvedValueOnce({
|
|
1232
|
+
data: { value: [] },
|
|
1233
|
+
headers: {},
|
|
1234
|
+
});
|
|
886
1235
|
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({});
|
|
887
1236
|
|
|
888
1237
|
// Act
|
|
@@ -908,6 +1257,15 @@ describe('PipelinesDataProvider', () => {
|
|
|
908
1257
|
},
|
|
909
1258
|
} as any;
|
|
910
1259
|
|
|
1260
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
1261
|
+
.mockResolvedValueOnce({
|
|
1262
|
+
data: { value: [] },
|
|
1263
|
+
headers: {},
|
|
1264
|
+
})
|
|
1265
|
+
.mockResolvedValueOnce({
|
|
1266
|
+
data: { value: [] },
|
|
1267
|
+
headers: {},
|
|
1268
|
+
});
|
|
911
1269
|
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({
|
|
912
1270
|
value: [
|
|
913
1271
|
{ id: 100, result: 'succeeded' },
|
|
@@ -938,6 +1296,10 @@ describe('PipelinesDataProvider', () => {
|
|
|
938
1296
|
const toPipelineRunId = 100;
|
|
939
1297
|
const targetPipeline = {} as PipelineRun;
|
|
940
1298
|
|
|
1299
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockResolvedValueOnce({
|
|
1300
|
+
data: { value: [] },
|
|
1301
|
+
headers: {},
|
|
1302
|
+
});
|
|
941
1303
|
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({
|
|
942
1304
|
value: [{ id: 99, result: 'succeeded' }],
|
|
943
1305
|
});
|
|
@@ -955,6 +1317,7 @@ describe('PipelinesDataProvider', () => {
|
|
|
955
1317
|
|
|
956
1318
|
expect(res).toBeUndefined();
|
|
957
1319
|
expect(detailsSpy).not.toHaveBeenCalled();
|
|
1320
|
+
expect(TFSServices.getItemContentWithHeaders).not.toHaveBeenCalled();
|
|
958
1321
|
});
|
|
959
1322
|
|
|
960
1323
|
it('should skip when pipeline details do not include repositories', async () => {
|
|
@@ -1018,6 +1381,15 @@ describe('PipelinesDataProvider', () => {
|
|
|
1018
1381
|
},
|
|
1019
1382
|
};
|
|
1020
1383
|
|
|
1384
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
1385
|
+
.mockResolvedValueOnce({
|
|
1386
|
+
data: { value: [] },
|
|
1387
|
+
headers: {},
|
|
1388
|
+
})
|
|
1389
|
+
.mockResolvedValueOnce({
|
|
1390
|
+
data: { value: [] },
|
|
1391
|
+
headers: {},
|
|
1392
|
+
});
|
|
1021
1393
|
(TFSServices.getItemContent as jest.Mock)
|
|
1022
1394
|
.mockResolvedValueOnce(mockRunHistory)
|
|
1023
1395
|
.mockResolvedValueOnce(mockPipelineDetails);
|
|
@@ -1034,6 +1406,171 @@ describe('PipelinesDataProvider', () => {
|
|
|
1034
1406
|
// Assert
|
|
1035
1407
|
expect(result).toBe(99);
|
|
1036
1408
|
});
|
|
1409
|
+
|
|
1410
|
+
it('should find previous successful build on a later Builds API page', async () => {
|
|
1411
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
1412
|
+
.mockResolvedValueOnce({
|
|
1413
|
+
data: { value: [] },
|
|
1414
|
+
headers: { 'x-ms-continuationtoken': 'next-page' },
|
|
1415
|
+
})
|
|
1416
|
+
.mockResolvedValueOnce({
|
|
1417
|
+
data: { value: [buildCandidate(80, 'refs/heads/main')] },
|
|
1418
|
+
headers: {},
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
const result = await pipelinesDataProvider.findPreviousPipeline(
|
|
1422
|
+
'project1',
|
|
1423
|
+
'123',
|
|
1424
|
+
100,
|
|
1425
|
+
targetPipelineRun,
|
|
1426
|
+
true
|
|
1427
|
+
);
|
|
1428
|
+
|
|
1429
|
+
expect(result).toBe(80);
|
|
1430
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledWith(
|
|
1431
|
+
expect.stringContaining('continuationToken=next-page'),
|
|
1432
|
+
mockToken,
|
|
1433
|
+
'get',
|
|
1434
|
+
null,
|
|
1435
|
+
null
|
|
1436
|
+
);
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
it('should prefer a same-branch successful build before trying cross-branch fallback', async () => {
|
|
1440
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockResolvedValueOnce({
|
|
1441
|
+
data: { value: [buildCandidate(90, 'refs/heads/main')] },
|
|
1442
|
+
headers: {},
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
const result = await pipelinesDataProvider.findPreviousPipeline(
|
|
1446
|
+
'project1',
|
|
1447
|
+
'123',
|
|
1448
|
+
100,
|
|
1449
|
+
targetPipelineRun,
|
|
1450
|
+
true
|
|
1451
|
+
);
|
|
1452
|
+
|
|
1453
|
+
expect(result).toBe(90);
|
|
1454
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledTimes(1);
|
|
1455
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[0][0]).toContain(
|
|
1456
|
+
'branchName=refs%2Fheads%2Fmain'
|
|
1457
|
+
);
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
it('should fall back to a different branch only when same-branch discovery fails', async () => {
|
|
1461
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
1462
|
+
.mockResolvedValueOnce({
|
|
1463
|
+
data: { value: [] },
|
|
1464
|
+
headers: {},
|
|
1465
|
+
})
|
|
1466
|
+
.mockResolvedValueOnce({
|
|
1467
|
+
data: { value: [buildCandidate(95, 'refs/heads/release')] },
|
|
1468
|
+
headers: {},
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
const result = await pipelinesDataProvider.findPreviousPipeline(
|
|
1472
|
+
'project1',
|
|
1473
|
+
'123',
|
|
1474
|
+
100,
|
|
1475
|
+
targetPipelineRun,
|
|
1476
|
+
true
|
|
1477
|
+
);
|
|
1478
|
+
|
|
1479
|
+
expect(result).toBe(95);
|
|
1480
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledTimes(2);
|
|
1481
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[0][0]).toContain(
|
|
1482
|
+
'branchName=refs%2Fheads%2Fmain'
|
|
1483
|
+
);
|
|
1484
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[1][0]).not.toContain(
|
|
1485
|
+
'branchName='
|
|
1486
|
+
);
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
it('should query completed successful builds and ignore non-previous candidates', async () => {
|
|
1490
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
1491
|
+
.mockResolvedValueOnce({
|
|
1492
|
+
data: { value: [buildCandidate(100, 'refs/heads/main'), buildCandidate(101, 'refs/heads/main')] },
|
|
1493
|
+
headers: {},
|
|
1494
|
+
})
|
|
1495
|
+
.mockResolvedValueOnce({
|
|
1496
|
+
data: { value: [] },
|
|
1497
|
+
headers: {},
|
|
1498
|
+
});
|
|
1499
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({});
|
|
1500
|
+
|
|
1501
|
+
const result = await pipelinesDataProvider.findPreviousPipeline(
|
|
1502
|
+
'project1',
|
|
1503
|
+
'123',
|
|
1504
|
+
100,
|
|
1505
|
+
targetPipelineRun,
|
|
1506
|
+
true
|
|
1507
|
+
);
|
|
1508
|
+
|
|
1509
|
+
const url = (TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[0][0];
|
|
1510
|
+
expect(url).toContain('definitions=123');
|
|
1511
|
+
expect(url).toContain('resultFilter=succeeded');
|
|
1512
|
+
expect(url).toContain('statusFilter=completed');
|
|
1513
|
+
expect(url).toContain('queryOrder=finishTimeDescending');
|
|
1514
|
+
expect(result).toBeUndefined();
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
it('should throw and not try cross-branch fallback when same-branch Builds API fails', async () => {
|
|
1518
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockRejectedValueOnce(new Error('same branch failed'));
|
|
1519
|
+
|
|
1520
|
+
await expect(
|
|
1521
|
+
pipelinesDataProvider.findPreviousPipeline('project1', '123', 100, targetPipelineRun, true)
|
|
1522
|
+
).rejects.toThrow('same branch failed');
|
|
1523
|
+
|
|
1524
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledTimes(1);
|
|
1525
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[0][0]).toContain(
|
|
1526
|
+
'branchName=refs%2Fheads%2Fmain'
|
|
1527
|
+
);
|
|
1528
|
+
expect(TFSServices.getItemContent).not.toHaveBeenCalled();
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
it('should throw when cross-branch Builds API fallback fails after same-branch no-match', async () => {
|
|
1532
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
1533
|
+
.mockResolvedValueOnce({
|
|
1534
|
+
data: { value: [] },
|
|
1535
|
+
headers: {},
|
|
1536
|
+
})
|
|
1537
|
+
.mockRejectedValueOnce(new Error('fallback failed'));
|
|
1538
|
+
|
|
1539
|
+
await expect(
|
|
1540
|
+
pipelinesDataProvider.findPreviousPipeline('project1', '123', 100, targetPipelineRun, true)
|
|
1541
|
+
).rejects.toThrow('fallback failed');
|
|
1542
|
+
|
|
1543
|
+
expect(TFSServices.getItemContentWithHeaders).toHaveBeenCalledTimes(2);
|
|
1544
|
+
expect((TFSServices.getItemContentWithHeaders as jest.Mock).mock.calls[1][0]).not.toContain(
|
|
1545
|
+
'branchName='
|
|
1546
|
+
);
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
it('should throw when a later Builds API page fails', async () => {
|
|
1550
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock)
|
|
1551
|
+
.mockResolvedValueOnce({
|
|
1552
|
+
data: { value: [] },
|
|
1553
|
+
headers: { 'x-ms-continuationtoken': 'next-page' },
|
|
1554
|
+
})
|
|
1555
|
+
.mockRejectedValueOnce(new Error('build page failed'));
|
|
1556
|
+
|
|
1557
|
+
await expect(
|
|
1558
|
+
pipelinesDataProvider.findPreviousPipeline('project1', '123', 100, targetPipelineRun, true)
|
|
1559
|
+
).rejects.toThrow('build page failed');
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
it('should throw when previous build discovery exceeds the defensive page limit', async () => {
|
|
1563
|
+
(TFSServices.getItemContentWithHeaders as jest.Mock).mockImplementation(() =>
|
|
1564
|
+
Promise.resolve({
|
|
1565
|
+
data: { value: [] },
|
|
1566
|
+
headers: { 'x-ms-continuationtoken': 'next-page' },
|
|
1567
|
+
})
|
|
1568
|
+
);
|
|
1569
|
+
|
|
1570
|
+
await expect(
|
|
1571
|
+
pipelinesDataProvider.findPreviousPipeline('project1', '123', 100, targetPipelineRun, true)
|
|
1572
|
+
).rejects.toThrow('Pipeline discovery exceeded 50 pages');
|
|
1573
|
+
});
|
|
1037
1574
|
});
|
|
1038
1575
|
|
|
1039
1576
|
describe('private helper methods', () => {
|
|
@@ -1029,6 +1029,11 @@ describe('TicketsDataProvider', () => {
|
|
|
1029
1029
|
icon: expect.objectContaining({ dataUrl: 'data:image/png;base64,xxx' }),
|
|
1030
1030
|
}),
|
|
1031
1031
|
);
|
|
1032
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
1033
|
+
expect.stringContaining('/_apis/wit/workitemtypes?api-version=5.1'),
|
|
1034
|
+
expect.any(String),
|
|
1035
|
+
);
|
|
1036
|
+
expect((TFSServices.getItemContent as jest.Mock).mock.calls[0][0]).not.toContain('api-version=5.1:');
|
|
1032
1037
|
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Failed to download icon'));
|
|
1033
1038
|
});
|
|
1034
1039
|
});
|