22// Licensed under the MIT License.
33package com .microsoft .durabletask .azurefunctions ;
44
5+ import com .microsoft .durabletask .DurableHttp ;
6+ import com .microsoft .durabletask .DurableHttpRequest ;
7+ import com .microsoft .durabletask .DurableHttpResponse ;
8+ import com .microsoft .durabletask .ManagedIdentityOptions ;
9+ import com .microsoft .durabletask .ManagedIdentityTokenSource ;
510import com .microsoft .durabletask .Task ;
611import com .microsoft .durabletask .TaskOptions ;
712import com .microsoft .durabletask .TaskOrchestrationContext ;
1621import java .net .URI ;
1722import java .time .Duration ;
1823import java .util .Collections ;
24+ import java .util .List ;
1925import java .util .Map ;
2026
2127import static org .junit .jupiter .api .Assertions .*;
@@ -65,71 +71,84 @@ void callHttp_withOptions_nullRequest_throws() {
6571
6672 // ---- Delegation tests ----
6773
74+ @ SuppressWarnings ("unchecked" )
6875 @ Test
69- @ DisplayName ("callHttp(ctx, request): delegates to callActivity with BuiltIn::HttpActivity " )
76+ @ DisplayName ("callHttp(ctx, request): delegates to callActivity with wrapped list input " )
7077 void callHttp_delegatesToCallActivity () {
7178 DurableHttpRequest request = new DurableHttpRequest ("GET" , URI .create ("https://example.com" ));
72- when (ctx .callActivity (eq ("BuiltIn::HttpActivity" ), eq (request ), eq (DurableHttpResponse .class )))
79+ ArgumentCaptor <List <DurableHttpRequest >> captor = ArgumentCaptor .forClass (List .class );
80+ when (ctx .callActivity (eq ("BuiltIn::HttpActivity" ), captor .capture (), eq (DurableHttpResponse .class )))
7381 .thenReturn (mockTask );
7482
7583 Task <DurableHttpResponse > result = DurableHttp .callHttp (ctx , request );
7684
7785 assertSame (mockTask , result );
78- verify (ctx ).callActivity ("BuiltIn::HttpActivity" , request , DurableHttpResponse .class );
86+ List <DurableHttpRequest > captured = captor .getValue ();
87+ assertEquals (1 , captured .size ());
88+ assertSame (request , captured .get (0 ));
7989 }
8090
91+ @ SuppressWarnings ("unchecked" )
8192 @ Test
8293 @ DisplayName ("callHttp(ctx, request, options): delegates with TaskOptions when options is non-null" )
8394 void callHttp_withOptions_delegatesWithTaskOptions () {
8495 DurableHttpRequest request = new DurableHttpRequest ("POST" , URI .create ("https://example.com/api" ));
8596 TaskOptions options = new TaskOptions (new RetryPolicy (3 , Duration .ofSeconds (1 )));
86- when (ctx .callActivity (eq ("BuiltIn::HttpActivity" ), eq (request ), eq (options ), eq (DurableHttpResponse .class )))
97+ ArgumentCaptor <List <DurableHttpRequest >> captor = ArgumentCaptor .forClass (List .class );
98+ when (ctx .callActivity (eq ("BuiltIn::HttpActivity" ), captor .capture (), eq (options ), eq (DurableHttpResponse .class )))
8799 .thenReturn (mockTask );
88100
89101 Task <DurableHttpResponse > result = DurableHttp .callHttp (ctx , request , options );
90102
91103 assertSame (mockTask , result );
92- verify (ctx ).callActivity ("BuiltIn::HttpActivity" , request , options , DurableHttpResponse .class );
104+ List <DurableHttpRequest > captured = captor .getValue ();
105+ assertEquals (1 , captured .size ());
106+ assertSame (request , captured .get (0 ));
93107 }
94108
109+ @ SuppressWarnings ("unchecked" )
95110 @ Test
96111 @ DisplayName ("callHttp(ctx, request, null options): delegates without TaskOptions" )
97112 void callHttp_withNullOptions_delegatesWithoutTaskOptions () {
98113 DurableHttpRequest request = new DurableHttpRequest ("GET" , URI .create ("https://example.com" ));
99- when (ctx .callActivity (eq ("BuiltIn::HttpActivity" ), eq (request ), eq (DurableHttpResponse .class )))
114+ ArgumentCaptor <List <DurableHttpRequest >> captor = ArgumentCaptor .forClass (List .class );
115+ when (ctx .callActivity (eq ("BuiltIn::HttpActivity" ), captor .capture (), eq (DurableHttpResponse .class )))
100116 .thenReturn (mockTask );
101117
102118 Task <DurableHttpResponse > result = DurableHttp .callHttp (ctx , request , null );
103119
104120 assertSame (mockTask , result );
105- verify (ctx ).callActivity ("BuiltIn::HttpActivity" , request , DurableHttpResponse .class );
121+ assertEquals (1 , captor .getValue ().size ());
122+ assertSame (request , captor .getValue ().get (0 ));
106123 verify (ctx , never ()).callActivity (anyString (), any (), any (TaskOptions .class ), any ());
107124 }
108125
109126 // ---- Convenience method tests ----
110127
128+ @ SuppressWarnings ("unchecked" )
111129 @ Test
112130 @ DisplayName ("callHttp(ctx, method, uri): creates DurableHttpRequest and delegates" )
113131 void callHttp_convenience_methodUri () {
114- ArgumentCaptor <DurableHttpRequest > captor = ArgumentCaptor .forClass (DurableHttpRequest .class );
132+ ArgumentCaptor <List < DurableHttpRequest >> captor = ArgumentCaptor .forClass (List .class );
115133 when (ctx .callActivity (eq ("BuiltIn::HttpActivity" ), captor .capture (), eq (DurableHttpResponse .class )))
116134 .thenReturn (mockTask );
117135
118136 URI uri = URI .create ("https://httpbin.org/get" );
119137 Task <DurableHttpResponse > result = DurableHttp .callHttp (ctx , "GET" , uri );
120138
121139 assertSame (mockTask , result );
122- DurableHttpRequest captured = captor .getValue ();
140+ DurableHttpRequest captured = captor .getValue (). get ( 0 ) ;
123141 assertEquals ("GET" , captured .getMethod ());
124142 assertEquals (uri , captured .getUri ());
125143 assertNull (captured .getHeaders ());
126144 assertNull (captured .getContent ());
127145 }
128146
147+ @ SuppressWarnings ("unchecked" )
129148 @ Test
130149 @ DisplayName ("callHttp(ctx, method, uri, headers, content): creates DurableHttpRequest and delegates" )
131150 void callHttp_convenience_headersContent () {
132- ArgumentCaptor <DurableHttpRequest > captor = ArgumentCaptor .forClass (DurableHttpRequest .class );
151+ ArgumentCaptor <List < DurableHttpRequest >> captor = ArgumentCaptor .forClass (List .class );
133152 when (ctx .callActivity (eq ("BuiltIn::HttpActivity" ), captor .capture (), eq (DurableHttpResponse .class )))
134153 .thenReturn (mockTask );
135154
@@ -140,25 +159,26 @@ void callHttp_convenience_headersContent() {
140159 Task <DurableHttpResponse > result = DurableHttp .callHttp (ctx , "POST" , uri , headers , content );
141160
142161 assertSame (mockTask , result );
143- DurableHttpRequest captured = captor .getValue ();
162+ DurableHttpRequest captured = captor .getValue (). get ( 0 ) ;
144163 assertEquals ("POST" , captured .getMethod ());
145164 assertEquals (uri , captured .getUri ());
146165 assertEquals (headers , captured .getHeaders ());
147166 assertEquals (content , captured .getContent ());
148167 }
149168
169+ @ SuppressWarnings ("unchecked" )
150170 @ Test
151171 @ DisplayName ("callHttp(ctx, method, uri, null, null): null headers and content are allowed" )
152172 void callHttp_convenience_nullHeadersContent () {
153- ArgumentCaptor <DurableHttpRequest > captor = ArgumentCaptor .forClass (DurableHttpRequest .class );
173+ ArgumentCaptor <List < DurableHttpRequest >> captor = ArgumentCaptor .forClass (List .class );
154174 when (ctx .callActivity (eq ("BuiltIn::HttpActivity" ), captor .capture (), eq (DurableHttpResponse .class )))
155175 .thenReturn (mockTask );
156176
157177 Task <DurableHttpResponse > result = DurableHttp .callHttp (
158178 ctx , "DELETE" , URI .create ("https://example.com/resource" ), null , null );
159179
160180 assertSame (mockTask , result );
161- DurableHttpRequest captured = captor .getValue ();
181+ DurableHttpRequest captured = captor .getValue (). get ( 0 ) ;
162182 assertEquals ("DELETE" , captured .getMethod ());
163183 assertNull (captured .getHeaders ());
164184 assertNull (captured .getContent ());
@@ -171,4 +191,100 @@ void callHttp_convenience_nullHeadersContent() {
171191 void builtInActivityName () {
172192 assertEquals ("BuiltIn::HttpActivity" , DurableHttp .BUILT_IN_HTTP_ACTIVITY_NAME );
173193 }
194+
195+ // ---- TokenSource / Managed Identity delegation tests ----
196+
197+ @ SuppressWarnings ("unchecked" )
198+ @ Test
199+ @ DisplayName ("callHttp: request with ManagedIdentityTokenSource passes token source to activity" )
200+ void callHttp_withManagedIdentityTokenSource_passesThrough () {
201+ ManagedIdentityTokenSource tokenSource = new ManagedIdentityTokenSource (
202+ "https://management.core.windows.net/.default" );
203+ DurableHttpRequest request = new DurableHttpRequest ("GET" ,
204+ URI .create ("https://example.com" ), null , null , tokenSource );
205+
206+ ArgumentCaptor <List <DurableHttpRequest >> captor = ArgumentCaptor .forClass (List .class );
207+ when (ctx .callActivity (eq ("BuiltIn::HttpActivity" ), captor .capture (), eq (DurableHttpResponse .class )))
208+ .thenReturn (mockTask );
209+
210+ Task <DurableHttpResponse > result = DurableHttp .callHttp (ctx , request );
211+
212+ assertSame (mockTask , result );
213+ DurableHttpRequest captured = captor .getValue ().get (0 );
214+ assertNotNull (captured .getTokenSource ());
215+ assertInstanceOf (ManagedIdentityTokenSource .class , captured .getTokenSource ());
216+ ManagedIdentityTokenSource capturedToken = (ManagedIdentityTokenSource ) captured .getTokenSource ();
217+ assertEquals ("https://management.core.windows.net/.default" , capturedToken .getResource ());
218+ assertEquals ("AzureManagedIdentity" , capturedToken .getKind ());
219+ assertNull (capturedToken .getOptions ());
220+ }
221+
222+ @ SuppressWarnings ("unchecked" )
223+ @ Test
224+ @ DisplayName ("callHttp: request with ManagedIdentityTokenSource and options passes both through" )
225+ void callHttp_withManagedIdentityAndOptions_passesThrough () {
226+ ManagedIdentityOptions options = new ManagedIdentityOptions (
227+ URI .create ("https://login.microsoftonline.com/" ), "tenant_id" );
228+ ManagedIdentityTokenSource tokenSource = new ManagedIdentityTokenSource (
229+ "https://graph.microsoft.com/.default" , options );
230+ DurableHttpRequest request = new DurableHttpRequest ("GET" ,
231+ URI .create ("https://example.com" ), null , null , tokenSource );
232+
233+ ArgumentCaptor <List <DurableHttpRequest >> captor = ArgumentCaptor .forClass (List .class );
234+ when (ctx .callActivity (eq ("BuiltIn::HttpActivity" ), captor .capture (), eq (DurableHttpResponse .class )))
235+ .thenReturn (mockTask );
236+
237+ Task <DurableHttpResponse > result = DurableHttp .callHttp (ctx , request );
238+
239+ assertSame (mockTask , result );
240+ DurableHttpRequest captured = captor .getValue ().get (0 );
241+ assertNotNull (captured .getTokenSource ());
242+ ManagedIdentityTokenSource capturedToken = (ManagedIdentityTokenSource ) captured .getTokenSource ();
243+ assertEquals ("https://graph.microsoft.com/.default" , capturedToken .getResource ());
244+ assertNotNull (capturedToken .getOptions ());
245+ assertEquals (URI .create ("https://login.microsoftonline.com/" ),
246+ capturedToken .getOptions ().getAuthorityHost ());
247+ assertEquals ("tenant_id" , capturedToken .getOptions ().getTenantId ());
248+ }
249+
250+ @ SuppressWarnings ("unchecked" )
251+ @ Test
252+ @ DisplayName ("callHttp: request with ManagedIdentityTokenSource + TaskOptions passes both through" )
253+ void callHttp_withManagedIdentityAndTaskOptions_passesThrough () {
254+ ManagedIdentityTokenSource tokenSource = new ManagedIdentityTokenSource (
255+ "https://vault.azure.net/.default" );
256+ DurableHttpRequest request = new DurableHttpRequest ("GET" ,
257+ URI .create ("https://myvault.vault.azure.net/secrets/mysecret" ),
258+ null , null , tokenSource );
259+ TaskOptions taskOptions = new TaskOptions (new RetryPolicy (3 , Duration .ofSeconds (1 )));
260+
261+ ArgumentCaptor <List <DurableHttpRequest >> captor = ArgumentCaptor .forClass (List .class );
262+ when (ctx .callActivity (eq ("BuiltIn::HttpActivity" ), captor .capture (),
263+ eq (taskOptions ), eq (DurableHttpResponse .class )))
264+ .thenReturn (mockTask );
265+
266+ Task <DurableHttpResponse > result = DurableHttp .callHttp (ctx , request , taskOptions );
267+
268+ assertSame (mockTask , result );
269+ DurableHttpRequest captured = captor .getValue ().get (0 );
270+ assertNotNull (captured .getTokenSource ());
271+ assertInstanceOf (ManagedIdentityTokenSource .class , captured .getTokenSource ());
272+ assertEquals ("https://vault.azure.net/.default" ,
273+ ((ManagedIdentityTokenSource ) captured .getTokenSource ()).getResource ());
274+ }
275+
276+ @ SuppressWarnings ("unchecked" )
277+ @ Test
278+ @ DisplayName ("callHttp: request without tokenSource passes null tokenSource" )
279+ void callHttp_withoutTokenSource_passesNullTokenSource () {
280+ DurableHttpRequest request = new DurableHttpRequest ("GET" , URI .create ("https://example.com" ));
281+
282+ ArgumentCaptor <List <DurableHttpRequest >> captor = ArgumentCaptor .forClass (List .class );
283+ when (ctx .callActivity (eq ("BuiltIn::HttpActivity" ), captor .capture (), eq (DurableHttpResponse .class )))
284+ .thenReturn (mockTask );
285+
286+ DurableHttp .callHttp (ctx , request );
287+
288+ assertNull (captor .getValue ().get (0 ).getTokenSource ());
289+ }
174290}
0 commit comments