Skip to content

Commit c991a47

Browse files
committed
Add callHttp API with DurableHttp support, centralize test deps, and fix Windows build compatibility
1 parent ab01fb5 commit c991a47

File tree

23 files changed

+511
-73
lines changed

23 files changed

+511
-73
lines changed

azurefunctions/build.gradle

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ version = '1.7.0'
1010
archivesBaseName = 'durabletask-azure-functions'
1111

1212
def protocVersion = '3.25.8'
13+
// Java 11 is used to compile and run all tests. Set the JDK_11 env var to your
14+
// local JDK 11 home directory, e.g. C:/Program Files/Java/openjdk-11.0.12_7/
15+
// If unset, falls back to the current JDK running Gradle.
16+
def rawJdkPath = System.env.JDK_11 ?: System.getProperty("java.home")
17+
// Handle case where JDK_11 points to an executable (e.g., .../bin/javac.exe)
18+
// instead of the JDK home directory — walk up to the JDK root.
19+
def PATH_TO_TEST_JAVA_RUNTIME = rawJdkPath
20+
if (rawJdkPath != null) {
21+
def f = new File(rawJdkPath)
22+
if (f.isFile()) {
23+
PATH_TO_TEST_JAVA_RUNTIME = f.parentFile.parentFile.absolutePath
24+
}
25+
}
26+
// Append .exe on Windows when invoking javac/java directly
27+
def isWindows = System.getProperty("os.name").toLowerCase().contains("win")
28+
def exeSuffix = isWindows ? ".exe" : ""
1329

1430
repositories {
1531
maven {
@@ -22,11 +38,27 @@ dependencies {
2238
implementation group: 'com.microsoft.azure.functions', name: 'azure-functions-java-library', version: '3.2.3'
2339
implementation "com.google.protobuf:protobuf-java:${protocVersion}"
2440
compileOnly "com.microsoft.azure.functions:azure-functions-java-spi:1.1.0"
41+
42+
// Test dependencies
43+
testImplementation 'org.mockito:mockito-core:5.21.0'
44+
testImplementation 'org.mockito:mockito-junit-jupiter:5.21.0'
2545
}
2646

2747
sourceCompatibility = JavaVersion.VERSION_1_8
2848
targetCompatibility = JavaVersion.VERSION_1_8
2949

50+
compileTestJava {
51+
sourceCompatibility = JavaVersion.VERSION_11
52+
targetCompatibility = JavaVersion.VERSION_11
53+
options.fork = true
54+
options.forkOptions.executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/javac${exeSuffix}"
55+
}
56+
57+
test {
58+
useJUnitPlatform()
59+
executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/java${exeSuffix}"
60+
}
61+
3062
publishing {
3163
repositories {
3264
maven {

azurefunctions/src/test/java/com/microsoft/durabletask/azurefunctions/DurableHttpTest.java

Lines changed: 129 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
// Licensed under the MIT License.
33
package 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;
510
import com.microsoft.durabletask.Task;
611
import com.microsoft.durabletask.TaskOptions;
712
import com.microsoft.durabletask.TaskOrchestrationContext;
@@ -16,6 +21,7 @@
1621
import java.net.URI;
1722
import java.time.Duration;
1823
import java.util.Collections;
24+
import java.util.List;
1925
import java.util.Map;
2026

2127
import 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
}

azuremanaged/build.gradle

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,22 @@ version = '1.7.0'
2222
def grpcVersion = '1.78.0'
2323
def azureCoreVersion = '1.57.1'
2424
def azureIdentityVersion = '1.18.1'
25-
// When build on local, you need to set this value to your local jdk11 directory.
26-
// Java11 is used to compile and run all the tests.
27-
def PATH_TO_TEST_JAVA_RUNTIME = System.env.JDK_11 ?: System.getProperty("java.home")
25+
// Java 11 is used to compile and run all tests. Set the JDK_11 env var to your
26+
// local JDK 11 home directory, e.g. C:/Program Files/Java/openjdk-11.0.12_7/
27+
// If unset, falls back to the current JDK running Gradle.
28+
def rawJdkPath = System.env.JDK_11 ?: System.getProperty("java.home")
29+
// Handle case where JDK_11 points to an executable (e.g., .../bin/javac.exe)
30+
// instead of the JDK home directory — walk up to the JDK root.
31+
def PATH_TO_TEST_JAVA_RUNTIME = rawJdkPath
32+
if (rawJdkPath != null) {
33+
def f = new File(rawJdkPath)
34+
if (f.isFile()) {
35+
PATH_TO_TEST_JAVA_RUNTIME = f.parentFile.parentFile.absolutePath
36+
}
37+
}
38+
// Append .exe on Windows when invoking javac/java directly
39+
def isWindows = System.getProperty("os.name").toLowerCase().contains("win")
40+
def exeSuffix = isWindows ? ".exe" : ""
2841

2942
repositories {
3043
mavenCentral()
@@ -40,13 +53,9 @@ dependencies {
4053
implementation "com.azure:azure-identity:${azureIdentityVersion}"
4154

4255
// Test dependencies
43-
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.14.2'
44-
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.14.2'
4556
testImplementation 'org.mockito:mockito-core:5.21.0'
4657
testImplementation 'org.mockito:mockito-junit-jupiter:5.21.0'
4758
testImplementation "io.grpc:grpc-netty:${grpcVersion}"
48-
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.14.2'
49-
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.14.2'
5059
}
5160

5261
compileJava {
@@ -58,12 +67,13 @@ compileTestJava {
5867
sourceCompatibility = JavaVersion.VERSION_11
5968
targetCompatibility = JavaVersion.VERSION_11
6069
options.fork = true
61-
options.forkOptions.executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/javac"
70+
options.forkOptions.executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/javac${exeSuffix}"
6271
}
6372

6473
test {
6574
useJUnitPlatform()
6675
include '**/*Test.class'
76+
executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/java${exeSuffix}"
6777
}
6878

6979
publishing {

build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,12 @@ subprojects {
33
mavenLocal()
44
mavenCentral()
55
}
6+
7+
plugins.withType(JavaPlugin) {
8+
dependencies {
9+
testImplementation platform('org.junit:junit-bom:5.14.2')
10+
testImplementation 'org.junit.jupiter:junit-jupiter'
11+
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
12+
}
13+
}
614
}

0 commit comments

Comments
 (0)