1+ using Microsoft . Azure . Commands . Profile . Models ;
2+ using Microsoft . Azure . Internal . Common ;
3+ using Microsoft . Rest . Azure ;
4+ using Moq ;
5+ using Newtonsoft . Json . Linq ;
6+ using System ;
7+ using System . Collections . Generic ;
8+ using System . Linq ;
9+ using System . Net ;
10+ using System . Threading ;
11+ using System . Threading . Tasks ;
12+ using Xunit ;
13+
14+ namespace Microsoft . Azure . Commands . Profile . Test . UnitTest
15+ {
16+ public class PSHttpResponseExtensionsTest
17+ {
18+ [ Fact ]
19+ public async Task PaginateAggregatesPagedResults ( )
20+ {
21+ // Arrange
22+ var responses = CreateFakePagedResponses ( 3 ) ;
23+
24+ var mockOps = new Mock < IAzureRestOperations > ( ) ;
25+ mockOps . SetupSequence ( o => o . BeginHttpGetMessagesAsyncWithFullResponse ( It . IsAny < string > ( ) , It . IsAny < string > ( ) ,
26+ It . IsAny < IDictionary < string , IList < string > > > ( ) , It . IsAny < CancellationToken > ( ) ) )
27+ . ReturnsAsync ( responses [ 1 ] )
28+ . ReturnsAsync ( responses [ 2 ] ) ;
29+
30+ var mockClient = new Mock < IAzureRestClient > ( ) ;
31+ mockClient . SetupGet ( c => c . Operations ) . Returns ( mockOps . Object ) ;
32+
33+ // Act
34+ var result = await PSHttpResponseExtensions . PaginateAsync ( responses [ 0 ] , mockClient . Object , "v1" ) ;
35+
36+ // Assert
37+ var json = JObject . Parse ( result . Content ) ;
38+ var actualItems = json [ "value" ] as JArray ;
39+ Assert . NotNull ( actualItems ) ;
40+ Assert . Equal ( responses . Count , actualItems . Count ) ;
41+
42+ var expectedIds = responses
43+ . Select ( r => JObject . Parse ( r . Body ) [ "value" ] ? [ 0 ] ? [ "id" ] ? . ToString ( ) )
44+ . ToList ( ) ;
45+
46+ // Assert each actual item matches expected
47+ for ( int i = 0 ; i < expectedIds . Count ; i ++ )
48+ {
49+ var actualId = actualItems [ i ] ? [ "id" ] ? . ToString ( ) ;
50+ Assert . Equal ( expectedIds [ i ] , actualId ) ;
51+ }
52+ }
53+
54+ [ Fact ]
55+ public async Task PaginateAHandles429WithRetry ( )
56+ {
57+ // First page (initial), second page (rate limited), third page (success after retry)
58+ var initialResponse = CreateFakePagedResponses ( 2 ) [ 0 ] ;
59+ var rateLimitedResponse = CreateFakePagedResponses ( 1 , ( HttpStatusCode ) 429 ) [ 0 ] ;
60+ var successResponse = CreateFakePagedResponses ( 1 , hasNext : false ) [ 0 ] ;
61+
62+
63+ var mockOps = new Mock < IAzureRestOperations > ( ) ;
64+ var callCount = 0 ;
65+ mockOps . Setup ( o => o . BeginHttpGetMessagesAsyncWithFullResponse ( It . IsAny < string > ( ) , It . IsAny < string > ( ) ,
66+ It . IsAny < IDictionary < string , IList < string > > > ( ) , It . IsAny < CancellationToken > ( ) ) )
67+ . ReturnsAsync ( ( ) =>
68+ {
69+ callCount ++ ;
70+ return callCount == 1 ? rateLimitedResponse : successResponse ;
71+ } ) ;
72+
73+ var mockClient = new Mock < IAzureRestClient > ( ) ;
74+ mockClient . SetupGet ( c => c . Operations ) . Returns ( mockOps . Object ) ;
75+
76+ var outputMessages = new List < string > ( ) ;
77+ Action < string > outputAction = msg => outputMessages . Add ( msg ) ;
78+
79+ // Act
80+ var result = await PSHttpResponseExtensions . PaginateAsync ( initialResponse , mockClient . Object , "v1" , "value" , "nextLink" ) ;
81+
82+ // Assert
83+ var json = JObject . Parse ( result . Content ) ;
84+ Assert . Equal ( 2 , ( ( JArray ) json [ "value" ] ) . Count ) ;
85+ }
86+
87+ [ Fact ]
88+ public async Task PaginateThrowsOnNonSuccessStatusCode ( )
89+ {
90+ // Arrange
91+ var initialResponse = CreateFakePagedResponses ( 2 ) [ 0 ] ;
92+ var errorResponse = CreateFakeResponse ( "{}" , HttpStatusCode . NotFound ) ;
93+
94+ var mockOps = new Mock < IAzureRestOperations > ( ) ;
95+ mockOps . Setup ( o => o . BeginHttpGetMessagesAsyncWithFullResponse ( It . IsAny < string > ( ) , It . IsAny < string > ( ) ,
96+ It . IsAny < IDictionary < string , IList < string > > > ( ) , It . IsAny < CancellationToken > ( ) ) )
97+ . ReturnsAsync ( errorResponse ) ;
98+
99+ var mockClient = new Mock < IAzureRestClient > ( ) ;
100+ mockClient . SetupGet ( c => c . Operations ) . Returns ( mockOps . Object ) ;
101+
102+ // Act & Assert
103+ await Assert . ThrowsAsync < Commands . Common . Exceptions . AzPSException > ( async ( ) =>
104+ {
105+ await PSHttpResponseExtensions . PaginateAsync ( initialResponse , mockClient . Object , "v1" ) ;
106+ } ) ;
107+ }
108+
109+ [ Fact ]
110+ public async Task PaginateThrowsOnInvalidJson ( )
111+ {
112+ // Arrange
113+ var invalidJson = "{ invalid json " ;
114+ var initialResponse = CreateFakeResponse ( invalidJson ) ;
115+
116+ var mockOps = new Mock < IAzureRestOperations > ( ) ;
117+ var mockClient = new Mock < IAzureRestClient > ( ) ;
118+ mockClient . SetupGet ( c => c . Operations ) . Returns ( mockOps . Object ) ;
119+
120+ // Act & Assert
121+ await Assert . ThrowsAsync < Commands . Common . Exceptions . AzPSException > ( async ( ) =>
122+ {
123+ await PSHttpResponseExtensions . PaginateAsync ( initialResponse , mockClient . Object , "v1" ) ;
124+ } ) ;
125+ }
126+
127+ [ Fact ]
128+ public async Task PaginateHandlesMissingNextLinkProperty ( )
129+ {
130+ // Arrange
131+ var initialResponse = CreateFakePagedResponses ( 1 ) [ 0 ] ;
132+ var responses = CreateFakePagedResponses ( 1 , hasNext : false ) ;
133+
134+ var mockOps = new Mock < IAzureRestOperations > ( ) ;
135+ var mockClient = new Mock < IAzureRestClient > ( ) ;
136+ mockClient . SetupGet ( c => c . Operations ) . Returns ( mockOps . Object ) ;
137+
138+ // Act
139+ var result = await PSHttpResponseExtensions . PaginateAsync ( initialResponse , mockClient . Object , "v1" ) ;
140+
141+ // Assert
142+ var json = JObject . Parse ( result . Content ) ;
143+ Assert . Single ( ( ( JArray ) json [ "value" ] ) ) ;
144+ }
145+
146+ [ Fact ]
147+ public async Task PaginateIgnoresUnknownItemName ( )
148+ {
149+ //Arrange
150+ var initialResponse = CreateFakePagedResponses ( 1 ) [ 0 ] ;
151+ var mockOps = new Mock < IAzureRestOperations > ( ) ;
152+ var mockClient = new Mock < IAzureRestClient > ( ) ;
153+ mockClient . SetupGet ( c => c . Operations ) . Returns ( mockOps . Object ) ;
154+
155+ // Act
156+ var result = await PSHttpResponseExtensions . PaginateAsync ( initialResponse , mockClient . Object , "v1" , pageableItemName : "unknownProperty" ) ;
157+
158+ // Assert not paginated
159+ var json = JObject . Parse ( result . Content ) ;
160+ Assert . True ( json [ "value" ] is JArray arr && arr . Count == 1 ) ;
161+ }
162+
163+ [ Fact ]
164+ public async Task PaginateRespectsMaxPageSizeLimit ( )
165+ {
166+ // Arrange
167+ var responses = CreateFakePagedResponses ( 4 ) ;
168+
169+ var mockOps = new Mock < IAzureRestOperations > ( ) ;
170+ mockOps . SetupSequence ( o => o . BeginHttpGetMessagesAsyncWithFullResponse ( It . IsAny < string > ( ) , It . IsAny < string > ( ) ,
171+ It . IsAny < IDictionary < string , IList < string > > > ( ) , It . IsAny < CancellationToken > ( ) ) )
172+ . ReturnsAsync ( responses [ 1 ] )
173+ . ReturnsAsync ( responses [ 2 ] )
174+ . ReturnsAsync ( responses [ 3 ] ) ;
175+
176+ var mockClient = new Mock < IAzureRestClient > ( ) ;
177+ mockClient . SetupGet ( c => c . Operations ) . Returns ( mockOps . Object ) ;
178+
179+ // Act
180+ var result = await PSHttpResponseExtensions . PaginateAsync ( responses [ 0 ] , mockClient . Object , "v1" , "value" , "nextLink" , maxPageSize : 2 ) ;
181+
182+ //Assert
183+ var json = JObject . Parse ( result . Content ) ;
184+ Assert . Equal ( 2 , ( ( JArray ) json [ "value" ] ) . Count ) ;
185+ mockOps . Verify ( o => o . BeginHttpGetMessagesAsyncWithFullResponse ( It . IsAny < string > ( ) , It . IsAny < string > ( ) ,
186+ It . IsAny < IDictionary < string , IList < string > > > ( ) , It . IsAny < CancellationToken > ( ) ) , Times . Once ( ) ) ;
187+ }
188+
189+ [ Fact ]
190+ public async Task PaginateIgnoresInvalidNextLink ( )
191+ {
192+ // Arrange
193+ var responseWithNullNextLink = CreateFakeResponse ( new JObject
194+ {
195+ [ "value" ] = new JArray ( new JObject { [ "id" ] = Guid . NewGuid ( ) . ToString ( ) } ) ,
196+ [ "nextLink" ] = "null"
197+ } . ToString ( ) ) ;
198+ var responseWithInvalidUrl = CreateFakeResponse ( new JObject
199+ {
200+ [ "value" ] = new JArray ( new JObject { [ "id" ] = Guid . NewGuid ( ) . ToString ( ) } ) ,
201+ [ "nextLink" ] = "not a url"
202+ } . ToString ( ) ) ;
203+
204+ var mockOps = new Mock < IAzureRestOperations > ( ) ;
205+ mockOps . Setup ( o => o . BeginHttpGetMessagesAsyncWithFullResponse ( It . IsAny < string > ( ) , It . IsAny < string > ( ) ,
206+ It . IsAny < IDictionary < string , IList < string > > > ( ) , It . IsAny < CancellationToken > ( ) ) )
207+ . Throws ( new Exception ( "Should not be called" ) ) ;
208+
209+ var mockClient = new Mock < IAzureRestClient > ( ) ;
210+ mockClient . SetupGet ( c => c . Operations ) . Returns ( mockOps . Object ) ;
211+
212+ // Act & Assert for "null" string
213+ var resultNull = await PSHttpResponseExtensions . PaginateAsync ( responseWithNullNextLink , mockClient . Object , "v1" ) ;
214+
215+ var jsonNull = JObject . Parse ( resultNull . Content ) ;
216+ Assert . Single ( ( JArray ) jsonNull [ "value" ] ) ;
217+
218+ // Act & Assert for invalid URL
219+ var resultInvalid = await PSHttpResponseExtensions . PaginateAsync ( responseWithInvalidUrl , mockClient . Object , "v1" ) ;
220+
221+ var jsonInvalid = JObject . Parse ( resultInvalid . Content ) ;
222+ Assert . Single ( ( JArray ) jsonInvalid [ "value" ] ) ;
223+ }
224+
225+ #region Test Data Helpers
226+ private static List < AzureOperationResponse < string > > CreateFakePagedResponses (
227+ int pages , HttpStatusCode statusCode = HttpStatusCode . OK , bool retry = false , bool hasNext = true ,
228+ string itemName = "value" )
229+ {
230+ var responses = new List < AzureOperationResponse < string > > ( ) ;
231+ for ( int i = 1 ; i <= pages ; i ++ )
232+ {
233+ var page = new JObject
234+ {
235+ [ itemName ] = new JArray ( new JObject { [ "id" ] = $ "page{ i } "} )
236+ } ;
237+
238+ if ( hasNext && i < pages ) //don't add nextLink for the last page
239+ {
240+ page [ "nextLink" ] = $ "https://example.com/api/resource/page{ i + 1 } ";
241+ }
242+
243+ responses . Add ( CreateFakeResponse ( page . ToString ( ) , statusCode , retry ) ) ;
244+ }
245+ return responses ;
246+ }
247+
248+ private static AzureOperationResponse < string > CreateFakeResponse ( string content ,
249+ HttpStatusCode statusCode = HttpStatusCode . OK , bool retry = false )
250+ {
251+ var response = new AzureOperationResponse < string >
252+ {
253+ Body = content ,
254+ Response = new System . Net . Http . HttpResponseMessage ( statusCode ) ,
255+ Request = new System . Net . Http . HttpRequestMessage
256+ {
257+ Method = new System . Net . Http . HttpMethod ( "GET" ) ,
258+ RequestUri = new Uri ( "https://example.com/api/resource" )
259+ }
260+ } ;
261+ response . Response . RequestMessage = response . Request ;
262+
263+ if ( retry )
264+ {
265+ response . Response . Headers . RetryAfter = new System . Net . Http . Headers . RetryConditionHeaderValue ( TimeSpan . FromSeconds ( 0 ) ) ;
266+ }
267+
268+ return response ;
269+ }
270+ #endregion
271+ }
272+ }
0 commit comments