Skip to content

Commit b1b4065

Browse files
authored
Merge branch 'master' into 1584-middleware
2 parents 9c0dd2f + 304fb2b commit b1b4065

File tree

16 files changed

+1303
-14
lines changed

16 files changed

+1303
-14
lines changed

.github/workflows/build.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ jobs:
3434
- name: Codecov
3535
uses: codecov/[email protected]
3636
- name: Upload test report for sdk
37-
uses: actions/upload-artifact@v5
37+
uses: actions/upload-artifact@v6
3838
with:
3939
name: test-dapr-java-sdk-jdk${{ env.JDK_VER }}
4040
path: sdk/target/jacoco-report/
4141
- name: Upload test report for sdk-actors
42-
uses: actions/upload-artifact@v5
42+
uses: actions/upload-artifact@v6
4343
with:
4444
name: report-dapr-java-sdk-actors-jdk${{ env.JDK_VER }}
4545
path: sdk-actors/target/jacoco-report/
@@ -83,7 +83,7 @@ jobs:
8383
run: docker kill durabletask-sidecar
8484

8585
- name: Upload Durable Task Sidecar Logs
86-
uses: actions/upload-artifact@v4
86+
uses: actions/upload-artifact@v6
8787
with:
8888
name: Durable Task Sidecar Logs
8989
path: durabletask-sidecar.log
@@ -200,13 +200,13 @@ jobs:
200200
run: PRODUCT_SPRING_BOOT_VERSION=${{ matrix.spring-boot-version }} ./mvnw -B -pl !durabletask-client -Pintegration-tests dependency:copy-dependencies verify
201201
- name: Upload failsafe test report for sdk-tests on failure
202202
if: ${{ failure() && steps.integration_tests.conclusion == 'failure' }}
203-
uses: actions/upload-artifact@v5
203+
uses: actions/upload-artifact@v6
204204
with:
205205
name: failsafe-report-sdk-tests-jdk${{ matrix.java }}-sb${{ matrix.spring-boot-version }}
206206
path: sdk-tests/target/failsafe-reports
207207
- name: Upload surefire test report for sdk-tests on failure
208208
if: ${{ failure() && steps.integration_tests.conclusion == 'failure' }}
209-
uses: actions/upload-artifact@v5
209+
uses: actions/upload-artifact@v6
210210
with:
211211
name: surefire-report-sdk-tests-jdk${{ matrix.java }}-sb${{ matrix.spring-boot-version }}
212212
path: sdk-tests/target/surefire-reports

sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/outbox/DaprPubSubOutboxIT.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@
2323
import io.dapr.testcontainers.DaprLogLevel;
2424
import org.assertj.core.api.Assertions;
2525
import org.awaitility.Awaitility;
26+
import org.junit.jupiter.api.BeforeAll;
2627
import org.junit.jupiter.api.BeforeEach;
28+
import org.junit.jupiter.api.Disabled;
2729
import org.junit.jupiter.api.Tag;
2830
import org.junit.jupiter.api.Test;
2931
import org.slf4j.Logger;
3032
import org.slf4j.LoggerFactory;
33+
import org.springframework.beans.factory.annotation.Autowired;
3134
import org.springframework.boot.test.context.SpringBootTest;
3235
import org.springframework.test.context.DynamicPropertyRegistry;
3336
import org.springframework.test.context.DynamicPropertySource;
@@ -44,6 +47,7 @@
4447

4548
import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
4649

50+
@Disabled("Unclear why this test is failing intermittently in CI")
4751
@SpringBootTest(
4852
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
4953
classes = {
@@ -81,6 +85,9 @@ public class DaprPubSubOutboxIT {
8185
.withAppChannelAddress("host.testcontainers.internal")
8286
.withAppPort(PORT);
8387

88+
@Autowired
89+
private ProductWebhookController productWebhookController;
90+
8491
/**
8592
* Expose the Dapr ports to the host.
8693
*
@@ -93,17 +100,18 @@ static void daprProperties(DynamicPropertyRegistry registry) {
93100
registry.add("server.port", () -> PORT);
94101
}
95102

96-
97-
@BeforeEach
98-
public void setUp() {
103+
@BeforeAll
104+
public static void beforeAll(){
99105
org.testcontainers.Testcontainers.exposeHostPorts(PORT);
100106
}
101107

108+
@BeforeEach
109+
public void beforeEach() {
110+
Wait.forLogMessage(APP_FOUND_MESSAGE_PATTERN, 1).waitUntilReady(DAPR_CONTAINER);
111+
}
102112

103113
@Test
104114
public void shouldPublishUsingOutbox() throws Exception {
105-
Wait.forLogMessage(APP_FOUND_MESSAGE_PATTERN, 1).waitUntilReady(DAPR_CONTAINER);
106-
107115
try (DaprClient client = DaprClientFactory.createDaprClientBuilder(DAPR_CONTAINER).build()) {
108116

109117
ExecuteStateTransactionRequest transactionRequest = new ExecuteStateTransactionRequest(STATE_STORE_NAME);
@@ -123,7 +131,7 @@ public void shouldPublishUsingOutbox() throws Exception {
123131

124132
Awaitility.await().atMost(Duration.ofSeconds(10))
125133
.ignoreExceptions()
126-
.untilAsserted(() -> Assertions.assertThat(ProductWebhookController.EVENT_LIST).isNotEmpty());
134+
.untilAsserted(() -> Assertions.assertThat(productWebhookController.getEventList()).isNotEmpty());
127135
}
128136
}
129137

sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/outbox/ProductWebhookController.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,17 @@
2626
@RequestMapping("/webhooks/products")
2727
public class ProductWebhookController {
2828

29-
public static final List<CloudEvent<Product>> EVENT_LIST = new CopyOnWriteArrayList<>();
29+
public final List<CloudEvent<Product>> events = new CopyOnWriteArrayList<>();
3030

3131
@PostMapping("/created")
3232
@Topic(name = "product.created", pubsubName = "pubsub")
33-
public void handleEvent(@RequestBody CloudEvent cloudEvent) {
33+
public void handleEvent(@RequestBody CloudEvent<Product> cloudEvent) {
3434
System.out.println("Received product.created event: " + cloudEvent.getData());
35-
EVENT_LIST.add(cloudEvent);
35+
36+
events.add(cloudEvent);
37+
}
38+
39+
public List<CloudEvent<Product>> getEventList() {
40+
return events;
3641
}
3742
}

testcontainers-dapr/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
<groupId>org.testcontainers</groupId>
3434
<artifactId>testcontainers</artifactId>
3535
</dependency>
36+
<dependency>
37+
<groupId>com.fasterxml.jackson.core</groupId>
38+
<artifactId>jackson-databind</artifactId>
39+
</dependency>
3640
</dependencies>
3741

3842
<build>
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.testcontainers.wait.strategy;
15+
16+
import com.fasterxml.jackson.databind.DeserializationFeature;
17+
import com.fasterxml.jackson.databind.ObjectMapper;
18+
import io.dapr.testcontainers.wait.strategy.metadata.Metadata;
19+
import org.testcontainers.containers.ContainerLaunchException;
20+
import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy;
21+
import org.testcontainers.shaded.org.awaitility.Awaitility;
22+
23+
import java.io.IOException;
24+
import java.net.HttpURLConnection;
25+
import java.net.URL;
26+
import java.time.Duration;
27+
import java.util.concurrent.TimeUnit;
28+
import java.util.function.Predicate;
29+
30+
/**
31+
* Base wait strategy for Dapr containers that polls the metadata endpoint.
32+
* Subclasses implement specific conditions to wait for.
33+
*/
34+
public abstract class AbstractDaprWaitStrategy extends AbstractWaitStrategy {
35+
36+
private static final int DAPR_HTTP_PORT = 3500;
37+
private static final String METADATA_ENDPOINT = "/v1.0/metadata";
38+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
39+
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
40+
41+
private Duration pollInterval = Duration.ofMillis(500);
42+
43+
/**
44+
* Sets the poll interval for checking the metadata endpoint.
45+
*
46+
* @param pollInterval the interval between polling attempts
47+
* @return this strategy for chaining
48+
*/
49+
public AbstractDaprWaitStrategy withPollInterval(Duration pollInterval) {
50+
this.pollInterval = pollInterval;
51+
return this;
52+
}
53+
54+
@Override
55+
protected void waitUntilReady() {
56+
String host = waitStrategyTarget.getHost();
57+
Integer port = waitStrategyTarget.getMappedPort(DAPR_HTTP_PORT);
58+
String metadataUrl = String.format("http://%s:%d%s", host, port, METADATA_ENDPOINT);
59+
60+
try {
61+
Awaitility.await()
62+
.atMost(startupTimeout.getSeconds(), TimeUnit.SECONDS)
63+
.pollInterval(pollInterval.toMillis(), TimeUnit.MILLISECONDS)
64+
.ignoreExceptions()
65+
.until(() -> checkCondition(metadataUrl));
66+
} catch (Exception e) {
67+
throw new ContainerLaunchException(
68+
String.format("Timed out waiting for Dapr condition: %s", getConditionDescription()), e);
69+
}
70+
}
71+
72+
/**
73+
* Checks if the wait condition is satisfied.
74+
*
75+
* @param metadataUrl the URL to the metadata endpoint
76+
* @return true if the condition is met
77+
* @throws IOException if there's an error fetching metadata
78+
*/
79+
protected boolean checkCondition(String metadataUrl) throws IOException {
80+
Metadata metadata = fetchMetadata(metadataUrl);
81+
return isConditionMet(metadata);
82+
}
83+
84+
/**
85+
* Fetches metadata from the Dapr sidecar.
86+
*
87+
* @param metadataUrl the URL to fetch metadata from
88+
* @return the parsed metadata
89+
* @throws IOException if there's an error fetching or parsing
90+
*/
91+
protected Metadata fetchMetadata(String metadataUrl) throws IOException {
92+
HttpURLConnection connection = (HttpURLConnection) new URL(metadataUrl).openConnection();
93+
connection.setRequestMethod("GET");
94+
connection.setConnectTimeout(1000);
95+
connection.setReadTimeout(1000);
96+
97+
try {
98+
int responseCode = connection.getResponseCode();
99+
if (responseCode != 200) {
100+
throw new IOException("Metadata endpoint returned status: " + responseCode);
101+
}
102+
return OBJECT_MAPPER.readValue(connection.getInputStream(), Metadata.class);
103+
} finally {
104+
connection.disconnect();
105+
}
106+
}
107+
108+
/**
109+
* Checks if the specific wait condition is met based on the metadata.
110+
*
111+
* @param metadata the current Dapr metadata
112+
* @return true if the condition is satisfied
113+
*/
114+
protected abstract boolean isConditionMet(Metadata metadata);
115+
116+
/**
117+
* Returns a description of what this strategy is waiting for.
118+
*
119+
* @return a human-readable description of the condition
120+
*/
121+
protected abstract String getConditionDescription();
122+
123+
/**
124+
* Creates a predicate-based wait strategy for custom conditions.
125+
*
126+
* @param predicate the predicate to test against metadata
127+
* @param description a description of what the predicate checks
128+
* @return a new wait strategy
129+
*/
130+
public static AbstractDaprWaitStrategy forCondition(Predicate<Metadata> predicate, String description) {
131+
return new AbstractDaprWaitStrategy() {
132+
@Override
133+
protected boolean isConditionMet(Metadata metadata) {
134+
return predicate.test(metadata);
135+
}
136+
137+
@Override
138+
protected String getConditionDescription() {
139+
return description;
140+
}
141+
};
142+
}
143+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.testcontainers.wait.strategy;
15+
16+
import io.dapr.testcontainers.wait.strategy.metadata.Actor;
17+
import io.dapr.testcontainers.wait.strategy.metadata.Metadata;
18+
19+
/**
20+
* Wait strategy that waits for actors to be registered with Dapr.
21+
*/
22+
public class ActorWaitStrategy extends AbstractDaprWaitStrategy {
23+
24+
private final String actorType;
25+
26+
/**
27+
* Creates a wait strategy that waits for any actor to be registered.
28+
*/
29+
public ActorWaitStrategy() {
30+
this.actorType = null;
31+
}
32+
33+
/**
34+
* Creates a wait strategy that waits for a specific actor type to be registered.
35+
*
36+
* @param actorType the actor type to wait for
37+
*/
38+
public ActorWaitStrategy(String actorType) {
39+
this.actorType = actorType;
40+
}
41+
42+
@Override
43+
protected boolean isConditionMet(Metadata metadata) {
44+
if (metadata == null) {
45+
return false;
46+
}
47+
if (actorType == null) {
48+
return !metadata.getActors().isEmpty();
49+
}
50+
return metadata.getActors().stream()
51+
.anyMatch(this::matchesActorType);
52+
}
53+
54+
private boolean matchesActorType(Actor actor) {
55+
if (actor == null || actorType == null) {
56+
return false;
57+
}
58+
return actorType.equals(actor.getType());
59+
}
60+
61+
@Override
62+
protected String getConditionDescription() {
63+
if (actorType != null) {
64+
return String.format("actor type '%s'", actorType);
65+
}
66+
return "any registered actors";
67+
}
68+
}

0 commit comments

Comments
 (0)