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