diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b25c89e78b..8f201bfa33 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -187,7 +187,7 @@ jobs: ./dist/linux_amd64/release/placement & - name: Spin local environment run: | - docker compose -f ./sdk-tests/deploy/local-test.yml up -d mongo kafka + docker compose -f ./sdk-tests/deploy/local-test.yml up -d kafka docker ps - name: Install local ToxiProxy to simulate connectivity issues to Dapr sidecar run: | diff --git a/pom.xml b/pom.xml index 322bf1320a..dc3eaf2022 100644 --- a/pom.xml +++ b/pom.xml @@ -73,6 +73,7 @@ 1.7.0 3.5.12 2.2.4 + 5.1.0 2.0.9 3.11.2 2.1.0 @@ -406,6 +407,12 @@ + + redis.clients + jedis + ${jedis.version} + test + io.github.microcks microcks-testcontainers diff --git a/sdk-tests/components/secretstore.yaml b/sdk-tests/components/secretstore.yaml index c6e4fb4e55..87782ad94f 100644 --- a/sdk-tests/components/secretstore.yaml +++ b/sdk-tests/components/secretstore.yaml @@ -7,7 +7,7 @@ spec: version: v1 metadata: - name: secretsFile - value: "./components/secret.json" + value: "./secrets/secret.json" - name: nestedSeparator value: ":" - name: multiValued diff --git a/sdk-tests/deploy/local-test.yml b/sdk-tests/deploy/local-test.yml index f920f6acc4..7160ac25b6 100644 --- a/sdk-tests/deploy/local-test.yml +++ b/sdk-tests/deploy/local-test.yml @@ -21,8 +21,3 @@ services: KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - - mongo: - image: mongo - ports: - - "27017:27017" diff --git a/sdk-tests/pom.xml b/sdk-tests/pom.xml index 47f4be822a..9d68559aec 100644 --- a/sdk-tests/pom.xml +++ b/sdk-tests/pom.xml @@ -152,6 +152,11 @@ org.springframework.data spring-data-keyvalue + + redis.clients + jedis + test + org.wiremock wiremock-standalone diff --git a/sdk-tests/components/secret.json b/sdk-tests/secrets/secret.json similarity index 100% rename from sdk-tests/components/secret.json rename to sdk-tests/secrets/secret.json diff --git a/sdk-tests/src/test/java/io/dapr/it/AppRun.java b/sdk-tests/src/test/java/io/dapr/it/AppRun.java index 4ad886b841..d1be44c89d 100644 --- a/sdk-tests/src/test/java/io/dapr/it/AppRun.java +++ b/sdk-tests/src/test/java/io/dapr/it/AppRun.java @@ -50,6 +50,31 @@ public class AppRun implements Stoppable { this.maxWaitMilliseconds = maxWaitMilliseconds; } + /** + * Overload used by {@link io.dapr.it.containers.BaseContainerIT} when the Dapr + * sidecar runs in a Testcontainer rather than via {@code dapr run}. The + * {@code DAPR_HTTP_PORT} / {@code DAPR_GRPC_PORT} env vars on the spawned + * app process point at the explicit override values (typically the + * DaprContainer's mapped host ports) instead of {@code ports.getHttpPort() / + * ports.getGrpcPort()}. + */ + public AppRun(DaprPorts ports, + String successMessage, + Class serviceClass, + int maxWaitMilliseconds, + int daprHttpPortOverride, + int daprGrpcPortOverride) { + this.command = new Command( + successMessage, + buildCommand(serviceClass, ports), + new HashMap<>() {{ + put("DAPR_HTTP_PORT", Integer.toString(daprHttpPortOverride)); + put("DAPR_GRPC_PORT", Integer.toString(daprGrpcPortOverride)); + }}); + this.ports = ports; + this.maxWaitMilliseconds = maxWaitMilliseconds; + } + public void start() throws InterruptedException, IOException { long start = System.currentTimeMillis(); // First, try to stop previous run (if left running). diff --git a/sdk-tests/src/test/java/io/dapr/it/AppRunOverrideTest.java b/sdk-tests/src/test/java/io/dapr/it/AppRunOverrideTest.java new file mode 100644 index 0000000000..f5e4a44424 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/AppRunOverrideTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.it; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AppRunOverrideTest { + + /** + * Verifies that when we construct AppRun with explicit Dapr port overrides, + * the DAPR_HTTP_PORT / DAPR_GRPC_PORT env vars on the spawned command point + * at the override values, not at the DaprPorts-allocated ones. + */ + @Test + void daprPortOverridesAreUsedInEnv() throws Exception { + DaprPorts ports = DaprPorts.build(true, true, true); + AppRun app = new AppRun(ports, "ready", Object.class, 1000, 12345, 67890); + + Field commandField = AppRun.class.getDeclaredField("command"); + commandField.setAccessible(true); + Command command = (Command) commandField.get(app); + + Field envField = Command.class.getDeclaredField("env"); + envField.setAccessible(true); + @SuppressWarnings("unchecked") + Map env = (Map) envField.get(command); + + assertEquals("12345", env.get("DAPR_HTTP_PORT")); + assertEquals("67890", env.get("DAPR_GRPC_PORT")); + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/DaprRun.java b/sdk-tests/src/test/java/io/dapr/it/DaprRun.java index 966d4f08dd..e29c5f1347 100644 --- a/sdk-tests/src/test/java/io/dapr/it/DaprRun.java +++ b/sdk-tests/src/test/java/io/dapr/it/DaprRun.java @@ -172,10 +172,13 @@ public void stop() throws InterruptedException, IOException { System.out.println("Stopping dapr application ..."); try { this.stopCommand.run(); - System.out.println("Dapr application stopped."); } catch (RuntimeException e) { - System.out.println("Could not stop app " + this.appName + ": " + e.getMessage()); + if (e.getMessage() != null && e.getMessage().contains("Could not find success criteria")) { + System.out.println("App " + this.appName + " already stopped or not found (ignored)."); + } else { + System.out.println("Could not stop app " + this.appName + ": " + e.getMessage()); + } } } @@ -219,8 +222,7 @@ public void waitForAppHealth(int maxWaitMilliseconds) throws InterruptedExceptio while (System.currentTimeMillis() <= maxWait) { try { stub.healthCheck(Empty.getDefaultInstance()); - // artursouza: workaround due to race condition with runtime's probe on app's health. - Thread.sleep(5000); + Thread.sleep(2000); return; } catch (Exception e) { Thread.sleep(1000); @@ -232,10 +234,10 @@ public void waitForAppHealth(int maxWaitMilliseconds) throws InterruptedExceptio channel.shutdown(); } } else { - Duration waitDuration = Duration.ofMillis(maxWaitMilliseconds); + long maxWait = System.currentTimeMillis() + maxWaitMilliseconds; HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) - .connectTimeout(waitDuration) + .connectTimeout(Duration.ofSeconds(5)) .build(); String url = "http://127.0.0.1:" + this.getAppPort() + "/health"; HttpRequest request = HttpRequest.newBuilder() @@ -243,18 +245,20 @@ public void waitForAppHealth(int maxWaitMilliseconds) throws InterruptedExceptio .uri(URI.create(url)) .build(); - try { - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() != 200) { - throw new RuntimeException("error: HTTP service is not healthy."); + while (System.currentTimeMillis() <= maxWait) { + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + Thread.sleep(2000); + return; + } + } catch (IOException e) { + // not ready yet } - } catch (IOException e) { - throw new RuntimeException("exception: HTTP service is not healthy."); + Thread.sleep(1000); } - // artursouza: workaround due to race condition with runtime's probe on app's health. - Thread.sleep(5000); + throw new RuntimeException("timeout: HTTP service is not healthy."); } } diff --git a/sdk-tests/src/test/java/io/dapr/it/actors/ActivationDeactivationIT.java b/sdk-tests/src/test/java/io/dapr/it/actors/ActivationDeactivationIT.java index 369d02945e..e698da7b10 100644 --- a/sdk-tests/src/test/java/io/dapr/it/actors/ActivationDeactivationIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/actors/ActivationDeactivationIT.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Dapr Authors + * Copyright 2025 The Dapr Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -14,10 +14,14 @@ package io.dapr.it.actors; import io.dapr.actors.ActorId; +import io.dapr.actors.client.ActorClient; import io.dapr.actors.client.ActorProxyBuilder; -import io.dapr.it.BaseIT; +import io.dapr.it.AppRun; import io.dapr.it.actors.services.springboot.DemoActor; import io.dapr.it.actors.services.springboot.DemoActorService; +import io.dapr.it.containers.BaseContainerIT; +import io.dapr.testcontainers.DaprContainer; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,24 +34,38 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -public class ActivationDeactivationIT extends BaseIT { +public class ActivationDeactivationIT extends BaseContainerIT { private static Logger logger = LoggerFactory.getLogger(ActivationDeactivationIT.class); - @Test - public void activateInvokeDeactivate() throws Exception { - // The call below will fail if service cannot start successfully. - var run = startDaprApp( - ActivationDeactivationIT.class.getSimpleName(), - DemoActorService.SUCCESS_MESSAGE, + private static DaprContainer dapr; + private static AppRun app; + private static ActorClient actorClient; + + @BeforeAll + public static void start() throws Exception { + var pair = startAppAndAttach( + "activation-deactivation-it", DemoActorService.class, - true, - 60000); + AppRun.AppProtocol.HTTP, + appPort -> { + DaprContainer d = daprBuilder("activation-deactivation-it") + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(redisStateStore(STATE_STORE_NAME)); + return d; + }); + dapr = pair.dapr(); + app = pair.app(); + actorClient = newActorClient(dapr); + waitForActorsReady(dapr); + } + @Test + public void activateInvokeDeactivate() throws Exception { final AtomicInteger atomicInteger = new AtomicInteger(1); logger.debug("Creating proxy builder"); - ActorProxyBuilder proxyBuilder - = new ActorProxyBuilder(DemoActor.class, deferClose(run.newActorClient())); + ActorProxyBuilder proxyBuilder = new ActorProxyBuilder(DemoActor.class, actorClient); logger.debug("Creating actorId"); ActorId actorId1 = new ActorId(Integer.toString(atomicInteger.getAndIncrement())); logger.debug("Building proxy"); @@ -63,7 +81,7 @@ public void activateInvokeDeactivate() throws Exception { logger.debug("Retrieving active Actors"); List activeActors = proxy.retrieveActiveActors(); logger.debug("Active actors: [" + activeActors.toString() + "]"); - assertTrue(activeActors.contains(actorId1.toString()),"Expecting actorId:[" + actorId1.toString() + "]"); + assertTrue(activeActors.contains(actorId1.toString()), "Expecting actorId:[" + actorId1.toString() + "]"); ActorId actorId2 = new ActorId(Integer.toString(atomicInteger.getAndIncrement())); DemoActor proxy2 = proxyBuilder.build(actorId2); diff --git a/sdk-tests/src/test/java/io/dapr/it/actors/ActorExceptionIT.java b/sdk-tests/src/test/java/io/dapr/it/actors/ActorExceptionIT.java index 64d0f3ae8b..6fb8474093 100644 --- a/sdk-tests/src/test/java/io/dapr/it/actors/ActorExceptionIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/actors/ActorExceptionIT.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Dapr Authors + * Copyright 2025 The Dapr Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -14,12 +14,13 @@ package io.dapr.it.actors; import io.dapr.actors.ActorId; +import io.dapr.actors.client.ActorClient; import io.dapr.actors.client.ActorProxyBuilder; -import io.dapr.it.BaseIT; -import io.dapr.it.DaprRun; +import io.dapr.it.AppRun; import io.dapr.it.actors.app.MyActor; import io.dapr.it.actors.app.MyActorService; -import org.junit.jupiter.api.Assertions; +import io.dapr.it.containers.BaseContainerIT; +import io.dapr.testcontainers.DaprContainer; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -30,35 +31,44 @@ import static io.dapr.it.Retry.callWithRetry; import static io.dapr.it.TestUtils.assertThrowsDaprExceptionSubstring; - -public class ActorExceptionIT extends BaseIT { +public class ActorExceptionIT extends BaseContainerIT { private static Logger logger = LoggerFactory.getLogger(ActorExceptionIT.class); - private static DaprRun run; + private static DaprContainer dapr; + private static AppRun app; + private static ActorClient actorClient; @BeforeAll public static void start() throws Exception { - // The call below will fail if service cannot start successfully. - run = startDaprApp( - ActorExceptionIT.class.getSimpleName(), - MyActorService.SUCCESS_MESSAGE, + var pair = startAppAndAttach( + "actor-exception-it", MyActorService.class, - true, - 60000); + AppRun.AppProtocol.HTTP, + appPort -> { + DaprContainer d = daprBuilder("actor-exception-it") + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(redisStateStore(STATE_STORE_NAME)); + return d; + }); + dapr = pair.dapr(); + app = pair.app(); + actorClient = newActorClient(dapr); + waitForActorsReady(dapr); } @Test public void exceptionTest() throws Exception { ActorProxyBuilder proxyBuilder = - new ActorProxyBuilder("MyActorTest", MyActor.class, deferClose(run.newActorClient())); + new ActorProxyBuilder("MyActorTest", MyActor.class, actorClient); MyActor proxy = proxyBuilder.build(new ActorId("1")); callWithRetry(() -> { assertThrowsDaprExceptionSubstring( "INTERNAL", "INTERNAL: error invoke actor method: error from actor service", - () -> proxy.throwException()); + () -> proxy.throwException()); }, 10000); } @@ -66,8 +76,9 @@ public void exceptionTest() throws Exception { public void exceptionDueToMetadataTest() throws Exception { // Setting this HTTP header via actor metadata will cause the Actor HTTP server to error. Map metadata = Map.of("Content-Length", "9999"); + ActorClient metadataClient = newActorClient(dapr, metadata); ActorProxyBuilder proxyBuilderMetadataOverride = - new ActorProxyBuilder("MyActorTest", MyActor.class, deferClose(run.newActorClient(metadata))); + new ActorProxyBuilder("MyActorTest", MyActor.class, metadataClient); MyActor proxyWithMetadata = proxyBuilderMetadataOverride.build(new ActorId("2")); callWithRetry(() -> { diff --git a/sdk-tests/src/test/java/io/dapr/it/actors/ActorMethodNameIT.java b/sdk-tests/src/test/java/io/dapr/it/actors/ActorMethodNameIT.java index bf9a2eb749..f23878c3b9 100644 --- a/sdk-tests/src/test/java/io/dapr/it/actors/ActorMethodNameIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/actors/ActorMethodNameIT.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Dapr Authors + * Copyright 2025 The Dapr Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -14,11 +14,15 @@ package io.dapr.it.actors; import io.dapr.actors.ActorId; +import io.dapr.actors.client.ActorClient; import io.dapr.actors.client.ActorProxy; import io.dapr.actors.client.ActorProxyBuilder; -import io.dapr.it.BaseIT; +import io.dapr.it.AppRun; import io.dapr.it.actors.app.MyActor; import io.dapr.it.actors.app.MyActorService; +import io.dapr.it.containers.BaseContainerIT; +import io.dapr.testcontainers.DaprContainer; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,23 +30,38 @@ import static io.dapr.it.Retry.callWithRetry; import static org.junit.jupiter.api.Assertions.assertTrue; -public class ActorMethodNameIT extends BaseIT { +public class ActorMethodNameIT extends BaseContainerIT { private static Logger logger = LoggerFactory.getLogger(ActorMethodNameIT.class); - @Test - public void actorMethodNameChange() throws Exception { - // The call below will fail if service cannot start successfully. - var run = startDaprApp( - ActorMethodNameIT.class.getSimpleName(), - MyActorService.SUCCESS_MESSAGE, + private static DaprContainer dapr; + private static AppRun app; + private static ActorClient actorClient; + + @BeforeAll + public static void start() throws Exception { + var pair = startAppAndAttach( + "actor-method-name-it", MyActorService.class, - true, - 60000); + AppRun.AppProtocol.HTTP, + appPort -> { + DaprContainer d = daprBuilder("actor-method-name-it") + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(redisStateStore(STATE_STORE_NAME)); + return d; + }); + dapr = pair.dapr(); + app = pair.app(); + actorClient = newActorClient(dapr); + waitForActorsReady(dapr); + } + @Test + public void actorMethodNameChange() throws Exception { logger.debug("Creating proxy builder"); ActorProxyBuilder proxyBuilder = - new ActorProxyBuilder("MyActorTest", MyActor.class, deferClose(run.newActorClient())); + new ActorProxyBuilder("MyActorTest", MyActor.class, actorClient); logger.debug("Creating actorId"); ActorId actorId1 = new ActorId("1"); logger.debug("Building proxy"); @@ -57,7 +76,7 @@ public void actorMethodNameChange() throws Exception { logger.debug("Creating proxy builder 2"); ActorProxyBuilder proxyBuilder2 = - new ActorProxyBuilder("MyActorTest", ActorProxy.class, deferClose(run.newActorClient())); + new ActorProxyBuilder("MyActorTest", ActorProxy.class, actorClient); logger.debug("Building proxy 2"); ActorProxy proxy2 = proxyBuilder2.build(actorId1); @@ -67,6 +86,5 @@ public void actorMethodNameChange() throws Exception { logger.debug("asserting true response 2: [" + response + "]"); assertTrue(response); }, 60000); - } } diff --git a/sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java b/sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java index c388a906a6..4ea6a759fd 100644 --- a/sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/actors/ActorReminderRecoveryIT.java @@ -129,24 +129,19 @@ public void reminderRecoveryTest( ) throws Exception { setup(actorType); - logger.debug("Pausing 3 seconds to let gRPC connection get ready"); - Thread.sleep(3000); - logger.debug("Invoking actor method 'startReminder' which will register a reminder"); proxy.invokeMethod("setReminderData", reminderDataParam).block(); proxy.invokeMethod("startReminder", reminderName).block(); - logger.debug("Pausing 7 seconds to allow reminder to fire"); - Thread.sleep(7000); - + logger.debug("Waiting for reminder to fire at least 3 times"); final List logs = new ArrayList<>(); callWithRetry(() -> { logs.clear(); logs.addAll(fetchMethodCallLogs(proxy)); validateMethodCalls(logs, METHOD_NAME, 3); validateMessageContent(logs, METHOD_NAME, expectedReminderStateText); - }, 5000); + }, 30000); // Restarts runtime only. logger.info("Stopping Dapr sidecar"); diff --git a/sdk-tests/src/test/java/io/dapr/it/actors/ActorTimerRecoveryIT.java b/sdk-tests/src/test/java/io/dapr/it/actors/ActorTimerRecoveryIT.java index 809cd21a90..21910376fa 100644 --- a/sdk-tests/src/test/java/io/dapr/it/actors/ActorTimerRecoveryIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/actors/ActorTimerRecoveryIT.java @@ -54,7 +54,6 @@ public void timerRecoveryTest() throws Exception { true, 60000); - Thread.sleep(3000); String actorType="MyActorTest"; logger.debug("Creating proxy builder"); @@ -68,16 +67,14 @@ public void timerRecoveryTest() throws Exception { logger.debug("Invoking actor method 'startTimer' which will register a timer"); proxy.invokeMethod("startTimer", "myTimer").block(); - logger.debug("Pausing 7 seconds to allow timer to fire"); - Thread.sleep(7000); - + logger.debug("Waiting for timer to fire at least 3 times"); final List logs = new ArrayList<>(); callWithRetry(() -> { logs.clear(); logs.addAll(fetchMethodCallLogs(proxy)); validateMethodCalls(logs, METHOD_NAME, 3); validateMessageContent(logs, METHOD_NAME, "ping!"); - }, 5000); + }, 30000); // Restarts app only. runs.left.stop(); @@ -91,7 +88,7 @@ public void timerRecoveryTest() throws Exception { newLogs.clear(); newLogs.addAll(fetchMethodCallLogs(proxy)); validateMethodCalls(newLogs, METHOD_NAME, 3); - }, 10000); + }, 30000); // Check that the restart actually happened by confirming the old logs are not in the new logs. for (MethodEntryTracker oldLog: logs) { diff --git a/sdk-tests/src/test/java/io/dapr/it/actors/ActorTurnBasedConcurrencyIT.java b/sdk-tests/src/test/java/io/dapr/it/actors/ActorTurnBasedConcurrencyIT.java index dd021d98b9..cc96b902a4 100644 --- a/sdk-tests/src/test/java/io/dapr/it/actors/ActorTurnBasedConcurrencyIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/actors/ActorTurnBasedConcurrencyIT.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Dapr Authors + * Copyright 2025 The Dapr Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -14,17 +14,20 @@ package io.dapr.it.actors; import io.dapr.actors.ActorId; +import io.dapr.actors.client.ActorClient; import io.dapr.actors.client.ActorProxy; import io.dapr.actors.client.ActorProxyBuilder; import io.dapr.actors.runtime.DaprClientHttpUtils; -import io.dapr.config.Properties; -import io.dapr.it.BaseIT; +import io.dapr.it.AppRun; import io.dapr.it.actors.app.MyActorService; +import io.dapr.it.containers.BaseContainerIT; +import io.dapr.testcontainers.DaprContainer; import io.dapr.utils.Version; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,20 +42,39 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -public class ActorTurnBasedConcurrencyIT extends BaseIT { +public class ActorTurnBasedConcurrencyIT extends BaseContainerIT { private static final Logger logger = LoggerFactory.getLogger(ActorTurnBasedConcurrencyIT.class); private static final String TIMER_METHOD_NAME = "clock"; - private static final String REMINDER_METHOD_NAME = "receiveReminder"; - private static final String ACTOR_TYPE = "MyActorTest"; - private static final String REMINDER_NAME = UUID.randomUUID().toString(); - private static final String ACTOR_ID = "1"; + private static DaprContainer dapr; + private static AppRun app; + private static ActorClient actorClient; + + @BeforeAll + public static void start() throws Exception { + var pair = startAppAndAttach( + "actor-concurrency-it", + MyActorService.class, + AppRun.AppProtocol.HTTP, + appPort -> { + DaprContainer d = daprBuilder("actor-concurrency-it") + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal") + .withComponent(redisStateStore(STATE_STORE_NAME)); + return d; + }); + dapr = pair.dapr(); + app = pair.app(); + actorClient = newActorClient(dapr); + waitForActorsReady(dapr); + } + @AfterEach public void cleanUpTestCase() { // Delete the reminder in case the test failed, otherwise it may interfere with future tests since it is persisted. @@ -80,19 +102,12 @@ public void cleanUpTestCase() { public void invokeOneActorMethodReminderAndTimer() throws Exception { System.out.println("Starting test 'actorTest1'"); - var run = startDaprApp( - ActorTurnBasedConcurrencyIT.class.getSimpleName(), - MyActorService.SUCCESS_MESSAGE, - MyActorService.class, - true, - 60000); - Thread.sleep(5000); String actorType="MyActorTest"; logger.debug("Creating proxy builder"); ActorProxyBuilder proxyBuilder = - new ActorProxyBuilder(actorType, ActorProxy.class, deferClose(run.newActorClient())); + new ActorProxyBuilder(actorType, ActorProxy.class, actorClient); logger.debug("Creating actorId"); ActorId actorId1 = new ActorId(ACTOR_ID); logger.debug("Building proxy"); @@ -157,7 +172,6 @@ public void invokeOneActorMethodReminderAndTimer() throws Exception { validateEventNotObserved(logs, "stopTimer", TIMER_METHOD_NAME); validateEventNotObserved(logs, "stopReminder", REMINDER_METHOD_NAME); validateMethodCalls(logs, "say", expectedSayMethodInvocations.get()); - } /** @@ -230,12 +244,7 @@ void validateEventNotObserved(List logs, String startingPoin } private static ManagedChannel buildManagedChannel() { - int port = Properties.GRPC_PORT.get(); - if (port <= 0) { - throw new IllegalStateException("Invalid port."); - } - - return ManagedChannelBuilder.forAddress(Properties.SIDECAR_IP.get(), port) + return ManagedChannelBuilder.forAddress("127.0.0.1", dapr.getGrpcPort()) .usePlaintext() .userAgent(Version.getSdkVersion()) .build(); diff --git a/sdk-tests/src/test/java/io/dapr/it/api/ApiIT.java b/sdk-tests/src/test/java/io/dapr/it/api/ApiIT.java index 8b37c5ad34..24d7bae1ee 100644 --- a/sdk-tests/src/test/java/io/dapr/it/api/ApiIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/api/ApiIT.java @@ -1,30 +1,57 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + package io.dapr.it.api; import io.dapr.client.DaprClient; -import io.dapr.client.DaprClientBuilder; -import io.dapr.it.BaseIT; -import io.dapr.it.DaprRun; +import io.dapr.it.containers.BaseContainerIT; +import io.dapr.testcontainers.DaprContainer; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class ApiIT extends BaseIT { +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class ApiIT extends BaseContainerIT { private static final Logger logger = LoggerFactory.getLogger(ApiIT.class); - private static final int DEFAULT_TIMEOUT = 60000; + private static final long SHUTDOWN_TIMEOUT_MS = 60_000; + private static final long SIDECAR_WARMUP_MS = 3_000; + + private static DaprContainer dapr; + + @BeforeAll + public static void init() throws Exception { + dapr = daprBuilder("api-it"); + dapr.start(); + deferStop(dapr); + } @Test public void testShutdownAPI() throws Exception { - DaprRun run = startDaprApp(this.getClass().getSimpleName(), DEFAULT_TIMEOUT); - // TODO(artursouza): change this to wait for the sidecar to be healthy (new method needed in DaprClient). - Thread.sleep(3000); - try (DaprClient client = run.newDaprClientBuilder().build()) { + Thread.sleep(SIDECAR_WARMUP_MS); + try (DaprClient client = newDaprClient(dapr)) { logger.info("Sending shutdown request."); client.shutdown().block(); logger.info("Ensuring dapr has stopped."); - run.checkRunState(DEFAULT_TIMEOUT, false); + long deadline = System.currentTimeMillis() + SHUTDOWN_TIMEOUT_MS; + while (dapr.isRunning() && System.currentTimeMillis() < deadline) { + Thread.sleep(100); + } + assertFalse(dapr.isRunning(), "Dapr container should have exited after client.shutdown()"); } } } diff --git a/sdk-tests/src/test/java/io/dapr/it/configuration/ConfigurationClientIT.java b/sdk-tests/src/test/java/io/dapr/it/configuration/ConfigurationClientIT.java index adbe4ee1c9..44f0f3b254 100644 --- a/sdk-tests/src/test/java/io/dapr/it/configuration/ConfigurationClientIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/configuration/ConfigurationClientIT.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Dapr Authors + * Copyright 2025 The Dapr Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -14,20 +14,20 @@ package io.dapr.it.configuration; import io.dapr.client.DaprClient; -import io.dapr.client.DaprClientBuilder; import io.dapr.client.domain.ConfigurationItem; import io.dapr.client.domain.SubscribeConfigurationResponse; import io.dapr.client.domain.UnsubscribeConfigurationResponse; -import io.dapr.it.BaseIT; -import io.dapr.it.DaprRun; +import io.dapr.it.containers.BaseContainerIT; +import io.dapr.it.containers.SharedTestInfra; +import io.dapr.testcontainers.DaprContainer; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.Disposable; import reactor.core.publisher.Flux; +import redis.clients.jedis.Jedis; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -37,38 +37,40 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -public class ConfigurationClientIT extends BaseIT { +public class ConfigurationClientIT extends BaseContainerIT { private static final String CONFIG_STORE_NAME = "redisconfigstore"; - private static DaprRun daprRun; - + private static DaprContainer dapr; private static DaprClient daprClient; + private static Jedis jedis; private static String key = "myconfig1"; - private static List keys = new ArrayList<>(Arrays.asList("myconfig1", "myconfig2", "myconfig3")); - private static String[] insertCmd = new String[] { - "docker", "exec", "dapr_redis", "redis-cli", - "MSET", - "myconfigkey1", "myconfigvalue1||1", - "myconfigkey2", "myconfigvalue2||1", - "myconfigkey3", "myconfigvalue3||1" - }; - - private static String[] updateCmd = new String[] { - "docker", "exec", "dapr_redis", "redis-cli", - "MSET", - "myconfigkey1", "update_myconfigvalue1||2", - "myconfigkey2", "update_myconfigvalue2||2", - "myconfigkey3", "update_myconfigvalue3||2" - }; + private static final Map INITIAL_VALUES = Map.of( + "myconfigkey1", "myconfigvalue1||1", + "myconfigkey2", "myconfigvalue2||1", + "myconfigkey3", "myconfigvalue3||1" + ); + + private static final Map UPDATED_VALUES = Map.of( + "myconfigkey1", "update_myconfigvalue1||2", + "myconfigkey2", "update_myconfigvalue2||2", + "myconfigkey3", "update_myconfigvalue3||2" + ); @BeforeAll public static void init() throws Exception { - daprRun = startDaprApp(ConfigurationClientIT.class.getSimpleName(), 5000); - daprClient = daprRun.newDaprClientBuilder().build(); + dapr = daprBuilder("config-it") + .withComponent(redisConfigStore(CONFIG_STORE_NAME)); + dapr.start(); + deferStop(dapr); + + jedis = new Jedis(SharedTestInfra.redis().getHost(), SharedTestInfra.redis().getMappedPort(6379)); + deferClose(jedis); + + daprClient = newDaprClient(dapr); daprClient.waitForSidecar(10000).block(); } @@ -79,7 +81,7 @@ public static void tearDown() throws Exception { @BeforeEach public void setupConfigStore() { - executeDockerCommand(insertCmd); + seedRedis(INITIAL_VALUES); } @Test @@ -115,17 +117,13 @@ public void subscribeConfiguration() { Thread subscribeThread = new Thread(subscribeTask); subscribeThread.start(); try { - // To ensure that subscribeThread gets scheduled Thread.sleep(0); } catch (InterruptedException e) { e.printStackTrace(); } - Runnable updateKeys = () -> { - executeDockerCommand(updateCmd); - }; + Runnable updateKeys = () -> seedRedis(UPDATED_VALUES); new Thread(updateKeys).start(); try { - // To ensure main thread does not die before outFlux subscribe gets called Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); @@ -149,24 +147,17 @@ public void unsubscribeConfigurationItems() { }; new Thread(subscribeTask).start(); - // To ensure that subscribeThread gets scheduled inducingSleepTime(0); Runnable updateKeys = () -> { int i = 1; while (i <= 5) { - String[] command = new String[] { - "docker", "exec", "dapr_redis", "redis-cli", - "SET", - "myconfigkey1", "update_myconfigvalue" + i + "||2" - }; - executeDockerCommand(command); + jedis.set("myconfigkey1", "update_myconfigvalue" + i + "||2"); i++; } }; new Thread(updateKeys).start(); - // To ensure key starts getting updated inducingSleepTime(1000); UnsubscribeConfigurationResponse res = daprClient.unsubscribeConfiguration( @@ -177,12 +168,10 @@ public void unsubscribeConfigurationItems() { assertTrue(res != null); assertTrue(res.getIsUnsubscribed()); int listSize = updatedValues.size(); - // To ensure main thread does not die inducingSleepTime(1000); new Thread(updateKeys).start(); - // To ensure main thread does not die inducingSleepTime(2000); assertTrue(updatedValues.size() == listSize); } @@ -195,19 +184,13 @@ private static void inducingSleepTime(int timeInMillis) { } } - private static void executeDockerCommand(String[] command) { - ProcessBuilder processBuilder = new ProcessBuilder(command); - Process process = null; - try { - process = processBuilder.start(); - process.waitFor(); - if (process.exitValue() != 0) { - throw new RuntimeException("Not zero exit code for Redis command: " + process.exitValue()); - } - } catch (IOException e) { - e.printStackTrace(); - } catch (InterruptedException e) { - e.printStackTrace(); + private static void seedRedis(Map kvs) { + String[] flat = new String[kvs.size() * 2]; + int i = 0; + for (Map.Entry entry : kvs.entrySet()) { + flat[i++] = entry.getKey(); + flat[i++] = entry.getValue(); } + jedis.mset(flat); } } diff --git a/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java b/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java new file mode 100644 index 0000000000..e6ec233c40 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerIT.java @@ -0,0 +1,310 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.it.containers; + +import io.dapr.actors.client.ActorClient; +import io.dapr.client.DaprClient; +import io.dapr.client.DaprClientBuilder; +import io.dapr.config.Properties; +import io.dapr.config.Property; +import io.dapr.it.AppRun; +import io.dapr.it.DaprPorts; +import io.dapr.it.Stoppable; +import io.dapr.testcontainers.Component; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.testcontainers.DaprLogLevel; +import io.dapr.testcontainers.wait.strategy.DaprWait; +import org.junit.jupiter.api.AfterAll; +import org.testcontainers.Testcontainers; + +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; + +/** + * Base class for sdk-tests integration tests that run Dapr inside a + * Testcontainer rather than via the local {@code dapr run} CLI. + * + *

Each subclass owns its own {@code private static DaprContainer dapr} + * (and optionally {@code AppRun app}) field. This class holds no + * Dapr/App fields itself — it only provides helpers and {@code @AfterAll} + * cleanup hooks. + * + *

Lifecycle (per IT class): + *

    + *
  1. {@code @BeforeAll}: call {@link #startAppAndAttach} (if needed), then build + * the DaprContainer via {@link #daprBuilder}, start it, and call + * {@link #deferStop(org.testcontainers.containers.GenericContainer)}.
  2. + *
  3. {@code @AfterAll}: inherited cleanup drains deferStop (LIFO) then + * deferClose.
  4. + *
+ */ +public abstract class BaseContainerIT { + + /** Pinned Dapr runtime image. Matches the testcontainers-dapr library default. */ + protected static final String DAPR_IMAGE = io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG; + + protected static final String STATE_STORE_NAME = "statestore"; + protected static final String PUBSUB_NAME = "messagebus"; + protected static final String CONFIG_STORE_NAME = "redisconfigstore"; + protected static final String MONGO_QUERY_STATE_STORE_NAME = "mongo-statestore"; + + // JUnit Jupiter runs @BeforeAll/@AfterAll single-threaded per class, so no synchronization needed. + private static final Deque TO_BE_STOPPED = new LinkedList<>(); + private static final Deque TO_BE_CLOSED = new LinkedList<>(); + + // ---------- DaprContainer builder ---------- + + /** + * Returns a pre-configured {@link DaprContainer} wired into the shared + * Network and Redis. Callers add components and (optionally) an app port + * before calling {@code .start()}. + */ + protected static DaprContainer daprBuilder(String appName) { + SharedTestInfra.redis(); // ensure Redis is up before DaprContainer needs it + return new DaprContainer(DAPR_IMAGE) + .withAppName(appName) + .withNetwork(SharedTestInfra.network()) + .withDaprLogLevel(DaprLogLevel.DEBUG) + // Stream daprd logs to stdout so CI surfaces app-discovery and component-load + // errors. Without this, the container's stdout is consumed by Testcontainers + // and we have no insight when actor registration or component init fails. + .withLogConsumer(frame -> System.out.print("[daprd] " + frame.getUtf8String())) + // Reuses the placement sidecar container within this JVM (Testcontainers manages it); + // orthogonal to SharedTestInfra's Redis `withReuse(true)`. + .withReusablePlacement(true); + } + + // ---------- App lifecycle ---------- + + /** Pair returned by {@link #startAppAndAttach}. */ + public record DaprAndApp(DaprContainer dapr, AppRun app) {} + + /** + * Two-phase startup for ITs that need an app callback. Allocates the app + * port, exposes it to Testcontainers, starts the AppRun subprocess so it + * has bound the host port, then lets the caller build the + * DaprContainer. Binds daprd to the pre-allocated host ports so the + * already-running app's {@code DAPR_HTTP_PORT} / {@code DAPR_GRPC_PORT} + * env vars (used by callbacks like {@code registerActorTimer}) point at + * a reachable daprd. Returns both. Both are registered for + * {@code @AfterAll} cleanup via {@link #deferStop}. + * + *

Order matters: starting daprd before the app causes daprd's + * application-channel probe to succeed against the Testcontainers SSH + * bridge before the JVM has actually bound the host port. Daprd then + * fetches {@code /dapr/config}, gets nothing, reports actor types {@code []} + * to placement, and never re-queries — so actor ITs hang at + * {@code waitForActorsReady}. Starting the app first avoids the race. + * + * @param appName used both as the Dapr app id and the AppRun name + * @param serviceClass the class whose {@code main(String[])} the subprocess runs + * @param protocol reserved for future use; AppRun currently ignores it + * @param daprFactory given the allocated app port, returns an UNSTARTED + * DaprContainer (factory body builds the container, + * calls {@code .withAppPort(appPort) + * .withAppChannelAddress("host.testcontainers.internal")} + * and other configuration, then returns it WITHOUT + * calling {@code .start()} — BaseContainerIT pins the + * daprd HTTP/gRPC host ports and starts the container). + */ + protected static DaprAndApp startAppAndAttach( + String appName, + Class serviceClass, + AppRun.AppProtocol protocol, + java.util.function.IntFunction daprFactory) throws Exception { + DaprPorts ports = DaprPorts.build(true, true, true); + int appPort = ports.getAppPort(); + int daprHttpPort = ports.getHttpPort(); + int daprGrpcPort = ports.getGrpcPort(); + + // Wire the SSH bridge before either side starts so daprd can resolve + // host.testcontainers.internal:appPort the moment its container boots. + Testcontainers.exposeHostPorts(appPort); + + // Start the app subprocess BEFORE daprd. AppRun.start() blocks on + // assertListeningOnPort, so by the time it returns the JVM has bound + // appPort and /dapr/config will respond with the registered actor types. + // DAPR_HTTP_PORT/DAPR_GRPC_PORT point at the pre-allocated host ports + // we will pin daprd to below; this is what app-side callbacks like + // registerActorTimer use to dial back to the sidecar. + AppRun app = new AppRun( + ports, + // Empty success-message: the legacy "dapr initialized. Status: Running" string is + // emitted by daprd's stdout, which used to be merged into the subprocess output by + // the dapr CLI but is now isolated in the Docker container. Pass "" so Command.run() + // returns on Maven's first stdout line; AppRun.start() then waits for the app to + // actually bind its port via assertListeningOnPort, which is the real readiness + // signal in the containerized world. + "", + serviceClass, + 60_000, + daprHttpPort, + daprGrpcPort); + app.start(); + deferStop(app); + + DaprContainer dapr = daprFactory.apply(appPort); + // Pin daprd's host ports so they match the values the AppRun's env was + // already given. Must be done before .start(). + dapr.setPortBindings(java.util.List.of( + daprHttpPort + ":3500", + daprGrpcPort + ":50001" + )); + dapr.start(); + deferStop(dapr); + + // Daprd's HTTP healthz/outbound (the wait strategy on DaprContainer) returns 2xx as + // soon as outbound connections are ready, but its gRPC server can be a beat behind. + // Tests that use the gRPC channel (method-invoke gRPC, tracing) hit "error reading + // server preface: EOF" if they call too soon. Prove the gRPC channel is responsive + // by issuing a waitForSidecar against a fresh DaprClient before returning. + try (DaprClient client = newDaprClient(dapr)) { + client.waitForSidecar(30_000).block(); + } + return new DaprAndApp(dapr, app); + } + + /** + * Polls daprd's metadata endpoint until at least one actor is registered. Call from + * {@code @BeforeAll} of actor ITs after {@link #startAppAndAttach} returns: the app + * subprocess takes a moment to register its actor types with daprd, and tests will + * fail with "did not find address for actor" if invoked too early. + */ + protected static void waitForActorsReady(DaprContainer dapr) { + DaprWait.forActors().waitUntilReady(dapr); + } + + // ---------- DaprClient / ActorClient factories ---------- + + protected static DaprClient newDaprClient(DaprContainer dapr) { + return newDaprClientBuilder(dapr).build(); + } + + protected static DaprClientBuilder newDaprClientBuilder(DaprContainer dapr) { + return new DaprClientBuilder().withPropertyOverrides(daprOverrides(dapr)); + } + + protected static ActorClient newActorClient(DaprContainer dapr) { + ActorClient client = new ActorClient(new Properties(daprOverrides(dapr)), null); + deferClose(client); + return client; + } + + /** + * ActorClient overload that injects HTTP headers (metadata) on actor calls. + * Used by ITs that need to override request-level headers like Content-Length. + */ + protected static ActorClient newActorClient(DaprContainer dapr, Map metadata) { + ActorClient client = new ActorClient(new Properties(daprOverrides(dapr)), metadata, null); + deferClose(client); + return client; + } + + private static Map, String> daprOverrides(DaprContainer dapr) { + Map, String> overrides = new HashMap<>(); + overrides.put(Properties.HTTP_ENDPOINT, "http://127.0.0.1:" + dapr.getHttpPort()); + overrides.put(Properties.GRPC_ENDPOINT, "127.0.0.1:" + dapr.getGrpcPort()); + overrides.put(Properties.HTTP_PORT, String.valueOf(dapr.getHttpPort())); + overrides.put(Properties.GRPC_PORT, String.valueOf(dapr.getGrpcPort())); + return overrides; + } + + // ---------- Component helpers (Redis) ---------- + + protected static Component redisStateStore(String name) { + return new Component(name, "state.redis", "v1", Map.of( + "redisHost", SharedTestInfra.redisInternalHost(), + "redisPassword", "", + "actorStateStore", "true" + )); + } + + protected static Component redisPubSub(String name) { + return new Component(name, "pubsub.redis", "v1", Map.of( + "redisHost", SharedTestInfra.redisInternalHost(), + "redisPassword", "", + "processingTimeout", "100ms", + "redeliverInterval", "100ms" + )); + } + + protected static Component redisConfigStore(String name) { + return new Component(name, "configuration.redis", "v1", Map.of( + "redisHost", SharedTestInfra.redisInternalHost(), + "redisPassword", "" + )); + } + + /** + * Mongo-backed state store with query API support. Lazily starts the + * shared Mongo container before returning the component. Used by + * {@code AbstractStateClientIT#saveAndQueryAndDeleteState}, which exercises + * the Dapr preview Query State API — Redis doesn't support that API, so a + * separate store is required. + */ + protected static Component mongoStateStore(String name) { + SharedTestInfra.mongo(); // ensure Mongo is up before DaprContainer needs it + return new Component(name, "state.mongodb", "v1", Map.of( + "host", SharedTestInfra.mongoInternalHost(), + "databaseName", "local", + "collectionName", "testCollection" + )); + } + + // ---------- Cleanup ---------- + + protected static T deferClose(T object) { + TO_BE_CLOSED.push(object); + return object; + } + + /** + * Defer-stop a plain {@link Stoppable} (e.g., {@link AppRun}). + * Use the {@link #deferStop(org.testcontainers.containers.GenericContainer) GenericContainer overload} + * for Testcontainers — they aren't {@code Stoppable}. + */ + protected static void deferStop(Stoppable stoppable) { + TO_BE_STOPPED.push(stoppable); + } + + /** + * Adapter so a Testcontainer can be registered alongside AppRuns in the + * stop queue. + */ + protected static void deferStop(org.testcontainers.containers.GenericContainer container) { + TO_BE_STOPPED.push(() -> container.stop()); + } + + @AfterAll + protected static void cleanUp() throws Exception { + while (!TO_BE_STOPPED.isEmpty()) { + try { + TO_BE_STOPPED.pop().stop(); + } catch (Exception e) { + // best-effort + e.printStackTrace(); + } + } + while (!TO_BE_CLOSED.isEmpty()) { + try { + TO_BE_CLOSED.pop().close(); + } catch (Exception e) { + // best-effort + e.printStackTrace(); + } + } + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerITSmokeTest.java b/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerITSmokeTest.java new file mode 100644 index 0000000000..a779fd7788 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/containers/BaseContainerITSmokeTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.it.containers; + +import io.dapr.client.DaprClient; +import io.dapr.testcontainers.DaprContainer; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Minimal smoke test that exercises BaseContainerIT's helpers end-to-end. + * Boots a no-app DaprContainer with no components and verifies that we can + * build a DaprClient against it and invoke a metadata call. + */ +class BaseContainerITSmokeTest extends BaseContainerIT { + + private static DaprContainer dapr; + + @BeforeAll + static void init() { + dapr = daprBuilder("smoke-test"); + dapr.start(); + deferStop(dapr); + } + + @Test + void canBuildAndUseDaprClient() throws Exception { + try (DaprClient client = newDaprClient(dapr)) { + // waitForSidecar is a cheap healthcheck — it's fine if it returns immediately. + client.waitForSidecar(5000).block(); + assertNotNull(client); + } + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/containers/SharedTestInfra.java b/sdk-tests/src/test/java/io/dapr/it/containers/SharedTestInfra.java new file mode 100644 index 0000000000..e778d1f7d9 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/containers/SharedTestInfra.java @@ -0,0 +1,94 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.it.containers; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.utility.DockerImageName; + +/** + * JVM-singleton holder for backing service containers shared across all + * migrated integration tests. Containers are started lazily on first access + * and reused for the lifetime of the JVM. With {@code withReuse(true)}, dev + * machines that opt in via ~/.testcontainers.properties also reuse across + * JVM runs. + */ +public final class SharedTestInfra { + + private static final String REDIS_NETWORK_ALIAS = "redis"; + private static final String ZIPKIN_NETWORK_ALIAS = "zipkin"; + private static final String MONGO_NETWORK_ALIAS = "mongo"; + + private static volatile Network network; + private static volatile GenericContainer redis; + private static volatile GenericContainer zipkin; + private static volatile GenericContainer mongo; + + private SharedTestInfra() {} + + public static synchronized Network network() { + if (network == null) { + network = Network.newNetwork(); + } + return network; + } + + public static synchronized GenericContainer redis() { + if (redis == null) { + redis = new GenericContainer<>(DockerImageName.parse("redis:7-alpine")) + .withNetwork(network()) + .withNetworkAliases(REDIS_NETWORK_ALIAS) + .withExposedPorts(6379) + .withReuse(true); + redis.start(); + } + return redis; + } + + public static String redisInternalHost() { + return REDIS_NETWORK_ALIAS + ":6379"; + } + + public static synchronized GenericContainer zipkin() { + if (zipkin == null) { + zipkin = new GenericContainer<>(DockerImageName.parse("openzipkin/zipkin:3.4")) + .withNetwork(network()) + .withNetworkAliases(ZIPKIN_NETWORK_ALIAS) + .withExposedPorts(9411) + .withReuse(true); + zipkin.start(); + } + return zipkin; + } + + public static String zipkinInternalEndpoint() { + return "http://" + ZIPKIN_NETWORK_ALIAS + ":9411/api/v2/spans"; + } + + public static synchronized GenericContainer mongo() { + if (mongo == null) { + mongo = new GenericContainer<>(DockerImageName.parse("mongo:7")) + .withNetwork(network()) + .withNetworkAliases(MONGO_NETWORK_ALIAS) + .withExposedPorts(27017) + .withReuse(true); + mongo.start(); + } + return mongo; + } + + public static String mongoInternalHost() { + return MONGO_NETWORK_ALIAS + ":27017"; + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/containers/SharedTestInfraTest.java b/sdk-tests/src/test/java/io/dapr/it/containers/SharedTestInfraTest.java new file mode 100644 index 0000000000..909b6662e1 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/containers/SharedTestInfraTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.it.containers; + +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SharedTestInfraTest { + + @Test + void networkIsSingleton() { + Network n1 = SharedTestInfra.network(); + Network n2 = SharedTestInfra.network(); + assertSame(n1, n2); + } + + @Test + void redisStartsAndIsReachable() { + GenericContainer redis = SharedTestInfra.redis(); + assertTrue(redis.isRunning()); + assertNotNull(redis.getMappedPort(6379)); + assertEquals("redis", redis.getNetworkAliases().get(0)); + } + + @Test + void redisInternalHostFormat() { + SharedTestInfra.redis(); // ensure started + assertEquals("redis:6379", SharedTestInfra.redisInternalHost()); + } + + @Test + void zipkinStartsAndIsReachable() { + GenericContainer z = SharedTestInfra.zipkin(); + assertTrue(z.isRunning()); + assertNotNull(z.getMappedPort(9411)); + assertEquals("zipkin", z.getNetworkAliases().get(0)); + } + + @Test + void zipkinInternalEndpointFormat() { + SharedTestInfra.zipkin(); // ensure started + assertEquals("http://zipkin:9411/api/v2/spans", SharedTestInfra.zipkinInternalEndpoint()); + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/methodinvoke/grpc/MethodInvokeIT.java b/sdk-tests/src/test/java/io/dapr/it/methodinvoke/grpc/MethodInvokeIT.java index ea94d2136e..f1be9f354c 100644 --- a/sdk-tests/src/test/java/io/dapr/it/methodinvoke/grpc/MethodInvokeIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/methodinvoke/grpc/MethodInvokeIT.java @@ -1,15 +1,28 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + package io.dapr.it.methodinvoke.grpc; import io.dapr.client.DaprClient; -import io.dapr.client.DaprClientBuilder; import io.dapr.client.resiliency.ResiliencyOptions; import io.dapr.it.AppRun; -import io.dapr.it.BaseIT; -import io.dapr.it.DaprRun; import io.dapr.it.MethodInvokeServiceGrpc; +import io.dapr.it.containers.BaseContainerIT; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.testcontainers.DaprProtocol; import io.grpc.Status; import io.grpc.StatusRuntimeException; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.time.Duration; @@ -23,38 +36,41 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -public class MethodInvokeIT extends BaseIT { +public class MethodInvokeIT extends BaseContainerIT { - //Number of messages to be sent: 10 + private static final String APP_NAME = "methodinvoke-grpc-it"; private static final int NUM_MESSAGES = 10; private static final int TIMEOUT_MS = 100; private static final ResiliencyOptions RESILIENCY_OPTIONS = new ResiliencyOptions() .setTimeout(Duration.ofMillis(TIMEOUT_MS)); - /** - * Run of a Dapr application. - */ - private DaprRun daprRun = null; - - @BeforeEach - public void init() throws Exception { - daprRun = startDaprApp( - MethodInvokeIT.class.getSimpleName() + "grpc", - MethodInvokeService.SUCCESS_MESSAGE, - MethodInvokeService.class, - AppRun.AppProtocol.GRPC, // appProtocol - 60000); - daprRun.waitForAppHealth(40000); + private static DaprContainer dapr; + private static AppRun app; + + @BeforeAll + public static void init() throws Exception { + var pair = startAppAndAttach( + APP_NAME, + MethodInvokeService.class, + AppRun.AppProtocol.GRPC, + appPort -> { + DaprContainer d = daprBuilder(APP_NAME) + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal") + .withAppProtocol(DaprProtocol.GRPC); + return d; + }); + dapr = pair.dapr(); + app = pair.app(); } @Test public void testInvoke() throws Exception { - try (DaprClient client = daprRun.newDaprClientBuilder().build()) { + try (DaprClient client = newDaprClient(dapr)) { client.waitForSidecar(10000).block(); - daprRun.waitForAppHealth(10000); MethodInvokeServiceGrpc.MethodInvokeServiceBlockingStub stub = createGrpcStub(client); - + for (int i = 0; i < NUM_MESSAGES; i++) { String message = String.format("This is message #%d", i); PostMessageRequest req = PostMessageRequest.newBuilder().setId(i).setMessage(message).build(); @@ -81,9 +97,8 @@ public void testInvoke() throws Exception { @Test public void testInvokeTimeout() throws Exception { - try (DaprClient client = daprRun.newDaprClientBuilder().withResiliencyOptions(RESILIENCY_OPTIONS).build()) { + try (DaprClient client = newDaprClientBuilder(dapr).withResiliencyOptions(RESILIENCY_OPTIONS).build()) { client.waitForSidecar(10000).block(); - daprRun.waitForAppHealth(10000); MethodInvokeServiceGrpc.MethodInvokeServiceBlockingStub stub = createGrpcStub(client); long started = System.currentTimeMillis(); @@ -99,9 +114,8 @@ public void testInvokeTimeout() throws Exception { @Test public void testInvokeException() throws Exception { - try (DaprClient client = daprRun.newDaprClientBuilder().build()) { + try (DaprClient client = newDaprClient(dapr)) { client.waitForSidecar(10000).block(); - daprRun.waitForAppHealth(10000); MethodInvokeServiceGrpc.MethodInvokeServiceBlockingStub stub = createGrpcStub(client); @@ -118,7 +132,7 @@ public void testInvokeException() throws Exception { } } - private MethodInvokeServiceGrpc.MethodInvokeServiceBlockingStub createGrpcStub(DaprClient client) { - return client.newGrpcStub(daprRun.getAppName(), MethodInvokeServiceGrpc::newBlockingStub); + private static MethodInvokeServiceGrpc.MethodInvokeServiceBlockingStub createGrpcStub(DaprClient client) { + return client.newGrpcStub(APP_NAME, MethodInvokeServiceGrpc::newBlockingStub); } } diff --git a/sdk-tests/src/test/java/io/dapr/it/methodinvoke/http/MethodInvokeIT.java b/sdk-tests/src/test/java/io/dapr/it/methodinvoke/http/MethodInvokeIT.java index 3c1ee01b51..6a64665b9e 100644 --- a/sdk-tests/src/test/java/io/dapr/it/methodinvoke/http/MethodInvokeIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/methodinvoke/http/MethodInvokeIT.java @@ -1,3 +1,16 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + package io.dapr.it.methodinvoke.http; import com.fasterxml.jackson.databind.JsonNode; @@ -5,10 +18,11 @@ import io.dapr.client.DaprHttp; import io.dapr.client.domain.HttpExtension; import io.dapr.exceptions.DaprException; -import io.dapr.it.BaseIT; -import io.dapr.it.DaprRun; +import io.dapr.it.AppRun; import io.dapr.it.MethodInvokeServiceProtos; -import org.junit.jupiter.api.BeforeEach; +import io.dapr.it.containers.BaseContainerIT; +import io.dapr.testcontainers.DaprContainer; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.time.Duration; @@ -24,76 +38,74 @@ import static org.junit.jupiter.api.Assertions.assertTrue; @SuppressWarnings("deprecation") -public class MethodInvokeIT extends BaseIT { +public class MethodInvokeIT extends BaseContainerIT { - //Number of messages to be sent: 10 + private static final String APP_NAME = "methodinvoke-http-it"; private static final int NUM_MESSAGES = 10; - /** - * Run of a Dapr application. - */ - private DaprRun daprRun = null; - - @BeforeEach - public void init() throws Exception { - daprRun = startDaprApp( - MethodInvokeIT.class.getSimpleName() + "http", - MethodInvokeService.SUCCESS_MESSAGE, - MethodInvokeService.class, - true, - 30000); - daprRun.waitForAppHealth(20000); + private static DaprContainer dapr; + private static AppRun app; + + @BeforeAll + public static void init() throws Exception { + var pair = startAppAndAttach( + APP_NAME, + MethodInvokeService.class, + AppRun.AppProtocol.HTTP, + appPort -> { + DaprContainer d = daprBuilder(APP_NAME) + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal"); + return d; + }); + dapr = pair.dapr(); + app = pair.app(); } @Test public void testInvoke() throws Exception { - - // At this point, it is guaranteed that the service above is running and all ports being listened to. - - try (DaprClient client = daprRun.newDaprClientBuilder().build()) { + try (DaprClient client = newDaprClient(dapr)) { client.waitForSidecar(10000).block(); for (int i = 0; i < NUM_MESSAGES; i++) { String message = String.format("This is message #%d", i); - //Publishing messages - client.invokeMethod(daprRun.getAppName(), "messages", message.getBytes(), HttpExtension.POST).block(); + client.invokeMethod(APP_NAME, "messages", message.getBytes(), HttpExtension.POST).block(); System.out.println("Invoke method messages : " + message); } - Map messages = client.invokeMethod(daprRun.getAppName(), "messages", null, + Map messages = client.invokeMethod(APP_NAME, "messages", null, HttpExtension.GET, Map.class).block(); assertEquals(10, messages.size()); - client.invokeMethod(daprRun.getAppName(), "messages/1", null, HttpExtension.DELETE).block(); + client.invokeMethod(APP_NAME, "messages/1", null, HttpExtension.DELETE).block(); - messages = client.invokeMethod(daprRun.getAppName(), "messages", null, HttpExtension.GET, Map.class).block(); + messages = client.invokeMethod(APP_NAME, "messages", null, HttpExtension.GET, Map.class).block(); assertEquals(9, messages.size()); - client.invokeMethod(daprRun.getAppName(), "messages/2", "updated message".getBytes(), HttpExtension.PUT).block(); - messages = client.invokeMethod(daprRun.getAppName(), "messages", null, HttpExtension.GET, Map.class).block(); + client.invokeMethod(APP_NAME, "messages/2", "updated message".getBytes(), HttpExtension.PUT).block(); + messages = client.invokeMethod(APP_NAME, "messages", null, HttpExtension.GET, Map.class).block(); assertEquals("updated message", messages.get("2")); } } @Test public void testInvokeWithObjects() throws Exception { - try (DaprClient client = daprRun.newDaprClientBuilder().build()) { + try (DaprClient client = newDaprClient(dapr)) { client.waitForSidecar(10000).block(); for (int i = 0; i < NUM_MESSAGES; i++) { Person person = new Person(); person.setName(String.format("Name %d", i)); person.setLastName(String.format("Last Name %d", i)); person.setBirthDate(new Date()); - //Publishing messages - client.invokeMethod(daprRun.getAppName(), "persons", person, HttpExtension.POST).block(); + client.invokeMethod(APP_NAME, "persons", person, HttpExtension.POST).block(); System.out.println("Invoke method persons with parameter : " + person); } - List persons = Arrays.asList(client.invokeMethod(daprRun.getAppName(), "persons", null, HttpExtension.GET, Person[].class).block()); + List persons = Arrays.asList(client.invokeMethod(APP_NAME, "persons", null, HttpExtension.GET, Person[].class).block()); assertEquals(10, persons.size()); - client.invokeMethod(daprRun.getAppName(), "persons/1", null, HttpExtension.DELETE).block(); + client.invokeMethod(APP_NAME, "persons/1", null, HttpExtension.DELETE).block(); - persons = Arrays.asList(client.invokeMethod(daprRun.getAppName(), "persons", null, HttpExtension.GET, Person[].class).block()); + persons = Arrays.asList(client.invokeMethod(APP_NAME, "persons", null, HttpExtension.GET, Person[].class).block()); assertEquals(9, persons.size()); Person person = new Person(); @@ -101,9 +113,9 @@ public void testInvokeWithObjects() throws Exception { person.setLastName("Smith"); person.setBirthDate(Calendar.getInstance().getTime()); - client.invokeMethod(daprRun.getAppName(), "persons/2", person, HttpExtension.PUT).block(); + client.invokeMethod(APP_NAME, "persons/2", person, HttpExtension.PUT).block(); - persons = Arrays.asList(client.invokeMethod(daprRun.getAppName(), "persons", null, HttpExtension.GET, Person[].class).block()); + persons = Arrays.asList(client.invokeMethod(APP_NAME, "persons", null, HttpExtension.GET, Person[].class).block()); Person resultPerson = persons.get(1); assertEquals("John", resultPerson.getName()); assertEquals("Smith", resultPerson.getLastName()); @@ -112,11 +124,11 @@ public void testInvokeWithObjects() throws Exception { @Test public void testInvokeTimeout() throws Exception { - try (DaprClient client = daprRun.newDaprClientBuilder().build()) { + try (DaprClient client = newDaprClient(dapr)) { client.waitForSidecar(10000).block(); long started = System.currentTimeMillis(); String message = assertThrows(IllegalStateException.class, () -> { - client.invokeMethod(daprRun.getAppName(), "sleep", 1, HttpExtension.POST) + client.invokeMethod(APP_NAME, "sleep", 1, HttpExtension.POST) .block(Duration.ofMillis(10)); }).getMessage(); @@ -129,11 +141,11 @@ public void testInvokeTimeout() throws Exception { @Test public void testInvokeException() throws Exception { - try (DaprClient client = daprRun.newDaprClientBuilder().build()) { + try (DaprClient client = newDaprClient(dapr)) { client.waitForSidecar(10000).block(); MethodInvokeServiceProtos.SleepRequest req = MethodInvokeServiceProtos.SleepRequest.newBuilder().setSeconds(-9).build(); DaprException exception = assertThrows(DaprException.class, () -> - client.invokeMethod(daprRun.getAppName(), "sleep", -9, HttpExtension.POST).block()); + client.invokeMethod(APP_NAME, "sleep", -9, HttpExtension.POST).block()); // TODO(artursouza): change this to INTERNAL once runtime is fixed. assertEquals("UNKNOWN", exception.getErrorCode()); @@ -145,14 +157,14 @@ public void testInvokeException() throws Exception { @Test public void testInvokeQueryParamEncoding() throws Exception { - try (DaprClient client = daprRun.newDaprClientBuilder().build()) { + try (DaprClient client = newDaprClient(dapr)) { client.waitForSidecar(10000).block(); String uri = "abc/pqr"; Map> queryParams = Map.of("uri", List.of(uri)); HttpExtension httpExtension = new HttpExtension(DaprHttp.HttpMethods.GET, queryParams, Map.of()); JsonNode result = client.invokeMethod( - daprRun.getAppName(), + APP_NAME, "/query", null, httpExtension, diff --git a/sdk-tests/src/test/java/io/dapr/it/pubsub/http/PubSubIT.java b/sdk-tests/src/test/java/io/dapr/it/pubsub/http/PubSubIT.java deleted file mode 100644 index 377d51e765..0000000000 --- a/sdk-tests/src/test/java/io/dapr/it/pubsub/http/PubSubIT.java +++ /dev/null @@ -1,723 +0,0 @@ -/* - * Copyright 2021 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and -limitations under the License. -*/ - -package io.dapr.it.pubsub.http; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.dapr.client.DaprClient; -import io.dapr.client.DaprClientBuilder; -import io.dapr.client.domain.BulkPublishEntry; -import io.dapr.client.domain.BulkPublishRequest; -import io.dapr.client.domain.BulkPublishResponse; -import io.dapr.client.domain.BulkSubscribeAppResponse; -import io.dapr.client.domain.BulkSubscribeAppResponseEntry; -import io.dapr.client.domain.BulkSubscribeAppResponseStatus; -import io.dapr.client.domain.CloudEvent; -import io.dapr.client.domain.HttpExtension; -import io.dapr.client.domain.Metadata; -import io.dapr.client.domain.PublishEventRequest; -import io.dapr.it.BaseIT; -import io.dapr.it.DaprRun; -import io.dapr.serializer.DaprObjectSerializer; -import io.dapr.utils.TypeRef; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Random; -import java.util.Set; - -import static io.dapr.it.Retry.callWithRetry; -import static io.dapr.it.TestUtils.assertThrowsDaprException; -import static io.dapr.it.TestUtils.assertThrowsDaprExceptionWithReason; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - - -public class PubSubIT extends BaseIT { - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - private static final TypeRef> CLOUD_EVENT_LIST_TYPE_REF = new TypeRef<>() {}; - private static final TypeRef>> CLOUD_EVENT_LONG_LIST_TYPE_REF = new TypeRef<>() {}; - private static final TypeRef>> CLOUD_EVENT_MYOBJECT_LIST_TYPE_REF = new TypeRef<>() {}; - - //Number of messages to be sent: 10 - private static final int NUM_MESSAGES = 10; - - private static final String PUBSUB_NAME = "messagebus"; - //The title of the topic to be used for publishing - private static final String TOPIC_NAME = "testingtopic"; - - private static final String TOPIC_BULK = "testingbulktopic"; - private static final String TYPED_TOPIC_NAME = "typedtestingtopic"; - private static final String ANOTHER_TOPIC_NAME = "anothertopic"; - // Topic used for TTL test - private static final String TTL_TOPIC_NAME = "ttltopic"; - // Topic to test binary data - private static final String BINARY_TOPIC_NAME = "binarytopic"; - - private static final String LONG_TOPIC_NAME = "testinglongvalues"; - // Topic to test bulk subscribe. - private static final String BULK_SUB_TOPIC_NAME = "topicBulkSub"; - - private final List runs = new ArrayList<>(); - - private DaprRun closeLater(DaprRun run) { - this.runs.add(run); - return run; - } - - @AfterEach - public void tearDown() throws Exception { - for (DaprRun run : runs) { - run.stop(); - } - } - - @Test - public void publishPubSubNotFound() throws Exception { - DaprRun daprRun = closeLater(startDaprApp( - this.getClass().getSimpleName(), - 60000)); - - try (DaprClient client = daprRun.newDaprClientBuilder().build()) { - assertThrowsDaprExceptionWithReason( - "INVALID_ARGUMENT", - "INVALID_ARGUMENT: pubsub unknown pubsub is not found", - "DAPR_PUBSUB_NOT_FOUND", - () -> client.publishEvent("unknown pubsub", "mytopic", "payload").block()); - } - } - - @Test - public void testBulkPublishPubSubNotFound() throws Exception { - DaprRun daprRun = closeLater(startDaprApp( - this.getClass().getSimpleName(), - 60000)); - - try (DaprClient client = daprRun.newDaprClientBuilder().build()) { - assertThrowsDaprException( - "INVALID_ARGUMENT", - "INVALID_ARGUMENT: pubsub unknown pubsub is not found", - () -> client.publishEvents("unknown pubsub", "mytopic","text/plain", "message").block()); - } - } - - @Test - public void testBulkPublish() throws Exception { - final DaprRun daprRun = closeLater(startDaprApp( - this.getClass().getSimpleName(), - SubscriberService.SUCCESS_MESSAGE, - SubscriberService.class, - true, - 60000)); - DaprObjectSerializer serializer = new DaprObjectSerializer() { - @Override - public byte[] serialize(Object o) throws JsonProcessingException { - return OBJECT_MAPPER.writeValueAsBytes(o); - } - - @Override - public T deserialize(byte[] data, TypeRef type) throws IOException { - return (T) OBJECT_MAPPER.readValue(data, OBJECT_MAPPER.constructType(type.getType())); - } - - @Override - public String getContentType() { - return "application/json"; - } - }; - try (DaprClient client = daprRun.newDaprClientBuilder().withObjectSerializer(serializer).build()) { - // Only for the gRPC test - // Send a multiple messages on one topic in messagebus pubsub via publishEvents API. - List messages = new ArrayList<>(); - for (int i = 0; i < NUM_MESSAGES; i++) { - messages.add(String.format("This is message #%d on topic %s", i, TOPIC_BULK)); - } - //Publishing 10 messages - BulkPublishResponse response = client.publishEvents(PUBSUB_NAME, TOPIC_BULK, "", messages).block(); - System.out.println(String.format("Published %d messages to topic '%s' pubsub_name '%s'", - NUM_MESSAGES, TOPIC_BULK, PUBSUB_NAME)); - assertNotNull(response, "expected not null bulk publish response"); - assertEquals( 0, response.getFailedEntries().size(), "expected no failures in the response"); - - //Publishing an object. - MyObject object = new MyObject(); - object.setId("123"); - response = client.publishEvents(PUBSUB_NAME, TOPIC_BULK, - "application/json", Collections.singletonList(object)).block(); - System.out.println("Published one object."); - assertNotNull(response, "expected not null bulk publish response"); - assertEquals(0, response.getFailedEntries().size(), "expected no failures in the response"); - - //Publishing a single byte: Example of non-string based content published - client.publishEvents(PUBSUB_NAME, TOPIC_BULK, "", - Collections.singletonList(new byte[]{1})).block(); - System.out.println("Published one byte."); - - assertNotNull(response, "expected not null bulk publish response"); - assertEquals(0, response.getFailedEntries().size(), "expected no failures in the response"); - - CloudEvent cloudEvent = new CloudEvent(); - cloudEvent.setId("1234"); - cloudEvent.setData("message from cloudevent"); - cloudEvent.setSource("test"); - cloudEvent.setSpecversion("1"); - cloudEvent.setType("myevent"); - cloudEvent.setDatacontenttype("text/plain"); - BulkPublishRequest req = new BulkPublishRequest<>(PUBSUB_NAME, TOPIC_BULK, - Collections.singletonList( - new BulkPublishEntry<>("1", cloudEvent, "application/cloudevents+json", null) - )); - - //Publishing a cloud event. - client.publishEvents(req).block(); - assertNotNull(response, "expected not null bulk publish response"); - assertEquals(0, response.getFailedEntries().size(), "expected no failures in the response"); - - System.out.println("Published one cloud event."); - - // Introduce sleep - Thread.sleep(10000); - - // Check messagebus subscription for topic testingbulktopic since it is populated only by publishEvents API call - callWithRetry(() -> { - System.out.println("Checking results for topic " + TOPIC_BULK + " in pubsub " + PUBSUB_NAME); - // Validate text payload. - final List cloudEventMessages = client.invokeMethod( - daprRun.getAppName(), - "messages/redis/testingbulktopic", - null, - HttpExtension.GET, - CLOUD_EVENT_LIST_TYPE_REF).block(); - assertEquals(13, cloudEventMessages.size(), "expected 13 messages to be received on subscribe"); - for (int i = 0; i < NUM_MESSAGES; i++) { - final int messageId = i; - assertTrue(cloudEventMessages - .stream() - .filter(m -> m.getData() != null) - .map(m -> m.getData()) - .filter(m -> m.equals(String.format("This is message #%d on topic %s", messageId, TOPIC_BULK))) - .count() == 1, "expected data content to match"); - } - - // Validate object payload. - assertTrue(cloudEventMessages - .stream() - .filter(m -> m.getData() != null) - .filter(m -> m.getData() instanceof LinkedHashMap) - .map(m -> (LinkedHashMap) m.getData()) - .filter(m -> "123".equals(m.get("id"))) - .count() == 1, "expected data content 123 to match"); - - // Validate byte payload. - assertTrue(cloudEventMessages - .stream() - .filter(m -> m.getData() != null) - .map(m -> m.getData()) - .filter(m -> "AQ==".equals(m)) - .count() == 1, "expected bin data to match"); - - // Validate cloudevent payload. - assertTrue( cloudEventMessages - .stream() - .filter(m -> m.getData() != null) - .map(m -> m.getData()) - .filter(m -> "message from cloudevent".equals(m)) - .count() == 1, "expected data to match"); - }, 2000); - } - - } - - @Test - public void testPubSub() throws Exception { - final DaprRun daprRun = closeLater(startDaprApp( - this.getClass().getSimpleName(), - SubscriberService.SUCCESS_MESSAGE, - SubscriberService.class, - true, - 60000)); - - DaprObjectSerializer serializer = new DaprObjectSerializer() { - @Override - public byte[] serialize(Object o) throws JsonProcessingException { - return OBJECT_MAPPER.writeValueAsBytes(o); - } - - @Override - public T deserialize(byte[] data, TypeRef type) throws IOException { - return (T) OBJECT_MAPPER.readValue(data, OBJECT_MAPPER.constructType(type.getType())); - } - - @Override - public String getContentType() { - return "application/json"; - } - }; - - // Send a batch of messages on one topic - try (DaprClient client = daprRun.newDaprClientBuilder().withObjectSerializer(serializer).build()) { - for (int i = 0; i < NUM_MESSAGES; i++) { - String message = String.format("This is message #%d on topic %s", i, TOPIC_NAME); - //Publishing messages - client.publishEvent(PUBSUB_NAME, TOPIC_NAME, message).block(); - System.out.println(String.format("Published message: '%s' to topic '%s' pubsub_name '%s'", message, TOPIC_NAME, PUBSUB_NAME)); - } - - // Send a batch of different messages on the other. - for (int i = 0; i < NUM_MESSAGES; i++) { - String message = String.format("This is message #%d on topic %s", i, ANOTHER_TOPIC_NAME); - //Publishing messages - client.publishEvent(PUBSUB_NAME, ANOTHER_TOPIC_NAME, message).block(); - System.out.println(String.format("Published message: '%s' to topic '%s' pubsub_name '%s'", message, ANOTHER_TOPIC_NAME, PUBSUB_NAME)); - } - - //Publishing an object. - MyObject object = new MyObject(); - object.setId("123"); - client.publishEvent(PUBSUB_NAME, TOPIC_NAME, object).block(); - System.out.println("Published one object."); - - client.publishEvent(PUBSUB_NAME, TYPED_TOPIC_NAME, object).block(); - System.out.println("Published another object."); - - //Publishing a single byte: Example of non-string based content published - client.publishEvent( - PUBSUB_NAME, - TOPIC_NAME, - new byte[]{1}).block(); - System.out.println("Published one byte."); - - CloudEvent cloudEvent = new CloudEvent(); - cloudEvent.setId("1234"); - cloudEvent.setData("message from cloudevent"); - cloudEvent.setSource("test"); - cloudEvent.setSpecversion("1"); - cloudEvent.setType("myevent"); - cloudEvent.setDatacontenttype("text/plain"); - - //Publishing a cloud event. - client.publishEvent(new PublishEventRequest(PUBSUB_NAME, TOPIC_NAME, cloudEvent) - .setContentType("application/cloudevents+json")).block(); - System.out.println("Published one cloud event."); - - { - CloudEvent cloudEventV2 = new CloudEvent(); - cloudEventV2.setId("2222"); - cloudEventV2.setData("message from cloudevent v2"); - cloudEventV2.setSource("test"); - cloudEventV2.setSpecversion("1"); - cloudEventV2.setType("myevent.v2"); - cloudEventV2.setDatacontenttype("text/plain"); - client.publishEvent( - new PublishEventRequest(PUBSUB_NAME, TOPIC_NAME, cloudEventV2) - .setContentType("application/cloudevents+json")).block(); - System.out.println("Published one cloud event for v2."); - } - - { - CloudEvent cloudEventV3 = new CloudEvent(); - cloudEventV3.setId("3333"); - cloudEventV3.setData("message from cloudevent v3"); - cloudEventV3.setSource("test"); - cloudEventV3.setSpecversion("1"); - cloudEventV3.setType("myevent.v3"); - cloudEventV3.setDatacontenttype("text/plain"); - client.publishEvent( - new PublishEventRequest(PUBSUB_NAME, TOPIC_NAME, cloudEventV3) - .setContentType("application/cloudevents+json")).block(); - System.out.println("Published one cloud event for v3."); - } - - Thread.sleep(2000); - - callWithRetry(() -> { - System.out.println("Checking results for topic " + TOPIC_NAME); - // Validate text payload. - final List messages = client.invokeMethod( - daprRun.getAppName(), - "messages/testingtopic", - null, - HttpExtension.GET, - CLOUD_EVENT_LIST_TYPE_REF).block(); - assertEquals(13, messages.size()); - for (int i = 0; i < NUM_MESSAGES; i++) { - final int messageId = i; - assertTrue(messages - .stream() - .filter(m -> m.getData() != null) - .map(m -> m.getData()) - .filter(m -> m.equals(String.format("This is message #%d on topic %s", messageId, TOPIC_NAME))) - .count() == 1); - } - - // Validate object payload. - assertTrue(messages - .stream() - .filter(m -> m.getData() != null) - .filter(m -> m.getData() instanceof LinkedHashMap) - .map(m -> (LinkedHashMap)m.getData()) - .filter(m -> "123".equals(m.get("id"))) - .count() == 1); - - // Validate byte payload. - assertTrue(messages - .stream() - .filter(m -> m.getData() != null) - .map(m -> m.getData()) - .filter(m -> "AQ==".equals(m)) - .count() == 1); - - // Validate cloudevent payload. - assertTrue(messages - .stream() - .filter(m -> m.getData() != null) - .map(m -> m.getData()) - .filter(m -> "message from cloudevent".equals(m)) - .count() == 1); - }, 2000); - - callWithRetry(() -> { - System.out.println("Checking results for topic " + TOPIC_NAME + " V2"); - // Validate text payload. - final List messages = client.invokeMethod( - daprRun.getAppName(), - "messages/testingtopicV2", - null, - HttpExtension.GET, - CLOUD_EVENT_LIST_TYPE_REF).block(); - assertEquals(1, messages.size()); - }, 2000); - - callWithRetry(() -> { - System.out.println("Checking results for topic " + TOPIC_NAME + " V3"); - // Validate text payload. - final List messages = client.invokeMethod( - daprRun.getAppName(), - "messages/testingtopicV3", - null, - HttpExtension.GET, - CLOUD_EVENT_LIST_TYPE_REF).block(); - assertEquals(1, messages.size()); - }, 2000); - - callWithRetry(() -> { - System.out.println("Checking results for topic " + TYPED_TOPIC_NAME); - // Validate object payload. - final List> messages = client.invokeMethod( - daprRun.getAppName(), - "messages/typedtestingtopic", - null, - HttpExtension.GET, - CLOUD_EVENT_MYOBJECT_LIST_TYPE_REF).block(); - - assertTrue(messages - .stream() - .filter(m -> m.getData() != null) - .filter(m -> m.getData() instanceof MyObject) - .map(m -> (MyObject)m.getData()) - .filter(m -> "123".equals(m.getId())) - .count() == 1); - }, 2000); - - callWithRetry(() -> { - System.out.println("Checking results for topic " + ANOTHER_TOPIC_NAME); - final List messages = client.invokeMethod( - daprRun.getAppName(), - "messages/anothertopic", - null, - HttpExtension.GET, - CLOUD_EVENT_LIST_TYPE_REF).block(); - assertEquals(10, messages.size()); - - for (int i = 0; i < NUM_MESSAGES; i++) { - final int messageId = i; - assertTrue(messages - .stream() - .filter(m -> m.getData() != null) - .map(m -> m.getData()) - .filter(m -> m.equals(String.format("This is message #%d on topic %s", messageId, ANOTHER_TOPIC_NAME))) - .count() == 1); - } - }, 2000); - } - } - - @Test - public void testPubSubBinary() throws Exception { - final DaprRun daprRun = closeLater(startDaprApp( - this.getClass().getSimpleName(), - SubscriberService.SUCCESS_MESSAGE, - SubscriberService.class, - true, - 60000)); - - DaprObjectSerializer serializer = new DaprObjectSerializer() { - @Override - public byte[] serialize(Object o) { - return (byte[])o; - } - - @Override - public T deserialize(byte[] data, TypeRef type) { - return (T) data; - } - - @Override - public String getContentType() { - return "application/octet-stream"; - } - }; - try (DaprClient client = daprRun.newDaprClientBuilder().withObjectSerializer(serializer).build()) { - client.publishEvent( - PUBSUB_NAME, - BINARY_TOPIC_NAME, - new byte[]{1}).block(); - System.out.println("Published one byte."); - } - - Thread.sleep(3000); - - try (DaprClient client = daprRun.newDaprClientBuilder().build()) { - callWithRetry(() -> { - System.out.println("Checking results for topic " + BINARY_TOPIC_NAME); - final List messages = client.invokeMethod( - daprRun.getAppName(), - "messages/binarytopic", - null, - HttpExtension.GET, CLOUD_EVENT_LIST_TYPE_REF).block(); - assertEquals(1, messages.size()); - assertNull(messages.get(0).getData()); - assertArrayEquals(new byte[]{1}, messages.get(0).getBinaryData()); - }, 2000); - } - } - - @Test - public void testPubSubTTLMetadata() throws Exception { - DaprRun daprRun = closeLater(startDaprApp( - this.getClass().getSimpleName(), - 60000)); - - // Send a batch of messages on one topic, all to be expired in 1 second. - try (DaprClient client = daprRun.newDaprClientBuilder().build()) { - for (int i = 0; i < NUM_MESSAGES; i++) { - String message = String.format("This is message #%d on topic %s", i, TTL_TOPIC_NAME); - //Publishing messages - client.publishEvent( - PUBSUB_NAME, - TTL_TOPIC_NAME, - message, - Map.of(Metadata.TTL_IN_SECONDS, "1")).block(); - System.out.println(String.format("Published message: '%s' to topic '%s' pubsub_name '%s'", message, TOPIC_NAME, PUBSUB_NAME)); - } - } - - daprRun.stop(); - - // Sleeps for two seconds to let them expire. - Thread.sleep(2000); - - daprRun = closeLater(startDaprApp( - this.getClass().getSimpleName(), - SubscriberService.SUCCESS_MESSAGE, - SubscriberService.class, - true, - 60000)); - - // Sleeps for five seconds to give subscriber a chance to receive messages. - Thread.sleep(5000); - - final String appId = daprRun.getAppName(); - try (DaprClient client = daprRun.newDaprClientBuilder().build()) { - callWithRetry(() -> { - System.out.println("Checking results for topic " + TTL_TOPIC_NAME); - final List messages = client.invokeMethod(appId, "messages/" + TTL_TOPIC_NAME, null, HttpExtension.GET, List.class).block(); - assertEquals(0, messages.size()); - }, 2000); - } - - daprRun.stop(); - } - - @Test - public void testPubSubBulkSubscribe() throws Exception { - DaprRun daprRun = closeLater(startDaprApp( - this.getClass().getSimpleName(), - SubscriberService.SUCCESS_MESSAGE, - SubscriberService.class, - true, - 60000)); - - // Send a batch of messages on one topic. - try (DaprClient client = daprRun.newDaprClientBuilder().build()) { - for (int i = 0; i < NUM_MESSAGES; i++) { - String message = String.format("This is message #%d on topic %s", i, BULK_SUB_TOPIC_NAME); - // Publishing messages - client.publishEvent(PUBSUB_NAME, BULK_SUB_TOPIC_NAME, message).block(); - System.out.printf("Published message: '%s' to topic '%s' pubSub_name '%s'\n", - message, BULK_SUB_TOPIC_NAME, PUBSUB_NAME); - } - } - - // Sleeps for five seconds to give subscriber a chance to receive messages. - Thread.sleep(5000); - - final String appId = daprRun.getAppName(); - try (DaprClient client = daprRun.newDaprClientBuilder().build()) { - callWithRetry(() -> { - System.out.println("Checking results for topic " + BULK_SUB_TOPIC_NAME); - - @SuppressWarnings("unchecked") - Class> clazz = (Class) List.class; - - final List messages = client.invokeMethod( - appId, - "messages/" + BULK_SUB_TOPIC_NAME, - null, - HttpExtension.GET, - clazz).block(); - - assertNotNull(messages); - BulkSubscribeAppResponse response = OBJECT_MAPPER.convertValue(messages.get(0), BulkSubscribeAppResponse.class); - - // There should be a single bulk response. - assertEquals(1, messages.size()); - - // The bulk response should contain NUM_MESSAGES entries. - assertEquals(NUM_MESSAGES, response.getStatuses().size()); - - // All the entries should be SUCCESS. - for (BulkSubscribeAppResponseEntry entry : response.getStatuses()) { - assertEquals(entry.getStatus(), BulkSubscribeAppResponseStatus.SUCCESS); - } - }, 2000); - } - - daprRun.stop(); - } - - @Test - public void testLongValues() throws Exception { - final DaprRun daprRun = closeLater(startDaprApp( - this.getClass().getSimpleName(), - SubscriberService.SUCCESS_MESSAGE, - SubscriberService.class, - true, - 60000)); - - Random random = new Random(590518626939830271L); - Set values = new HashSet<>(); - values.add(new ConvertToLong().setVal(590518626939830271L)); - ConvertToLong val; - for (int i = 0; i < NUM_MESSAGES - 1; i++) { - do { - val = new ConvertToLong().setVal(random.nextLong()); - } while (values.contains(val)); - values.add(val); - } - Iterator valuesIt = values.iterator(); - try (DaprClient client = daprRun.newDaprClientBuilder().build()) { - for (int i = 0; i < NUM_MESSAGES; i++) { - ConvertToLong value = valuesIt.next(); - System.out.println("The long value sent " + value.getValue()); - //Publishing messages - client.publishEvent( - PUBSUB_NAME, - LONG_TOPIC_NAME, - value, - Map.of(Metadata.TTL_IN_SECONDS, "30")).block(); - - try { - Thread.sleep((long) (1000 * Math.random())); - } catch (InterruptedException e) { - e.printStackTrace(); - Thread.currentThread().interrupt(); - return; - } - } - } - - Set actual = new HashSet<>(); - try (DaprClient client = daprRun.newDaprClientBuilder().build()) { - callWithRetry(() -> { - System.out.println("Checking results for topic " + LONG_TOPIC_NAME); - final List> messages = client.invokeMethod( - daprRun.getAppName(), - "messages/testinglongvalues", - null, - HttpExtension.GET, CLOUD_EVENT_LONG_LIST_TYPE_REF).block(); - assertNotNull(messages); - for (CloudEvent message : messages) { - actual.add(message.getData()); - } - Assertions.assertEquals(values, actual); - }, 2000); - } - } - - public static class MyObject { - private String id; - - public String getId() { - return this.id; - } - - public void setId(String id) { - this.id = id; - } - } - - public static class ConvertToLong { - private Long value; - - public ConvertToLong setVal(Long value) { - this.value = value; - return this; - } - - public Long getValue() { - return value; - } - - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ConvertToLong that = (ConvertToLong) o; - return Objects.equals(value, that.value); - } - - @Override - public int hashCode() { - return Objects.hash(value); - } - } - -} diff --git a/sdk-tests/src/test/java/io/dapr/it/pubsub/http/SubscriberController.java b/sdk-tests/src/test/java/io/dapr/it/pubsub/http/SubscriberController.java deleted file mode 100644 index 9fc5df3ee2..0000000000 --- a/sdk-tests/src/test/java/io/dapr/it/pubsub/http/SubscriberController.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright 2021 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and -limitations under the License. -*/ - -package io.dapr.it.pubsub.http; - -import io.dapr.Rule; -import io.dapr.Topic; -import io.dapr.client.domain.BulkSubscribeAppResponse; -import io.dapr.client.domain.BulkSubscribeAppResponseEntry; -import io.dapr.client.domain.BulkSubscribeAppResponseStatus; -import io.dapr.client.domain.BulkSubscribeMessage; -import io.dapr.client.domain.BulkSubscribeMessageEntry; -import io.dapr.client.domain.CloudEvent; -import io.dapr.springboot.annotations.BulkSubscribe; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; -import reactor.core.publisher.Mono; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.BiFunction; - -/** - * SpringBoot Controller to handle input binding. - */ -@RestController -public class SubscriberController { - - private final Map>> messagesByTopic = Collections.synchronizedMap(new HashMap<>()); - - @GetMapping(path = "/messages/{topic}") - public List> getMessagesByTopic(@PathVariable("topic") String topic) { - return messagesByTopic.getOrDefault(topic, Collections.emptyList()); - } - - private static final List messagesReceivedBulkPublishTopic = new ArrayList(); - private static final List messagesReceivedTestingTopic = new ArrayList(); - private static final List messagesReceivedTestingTopicV2 = new ArrayList(); - private static final List messagesReceivedTestingTopicV3 = new ArrayList(); - private static final List responsesReceivedTestingTopicBulkSub = new ArrayList<>(); - - @GetMapping(path = "/messages/redis/testingbulktopic") - public List getMessagesReceivedBulkTopic() { - return messagesReceivedBulkPublishTopic; - } - - - - @GetMapping(path = "/messages/testingtopic") - public List getMessagesReceivedTestingTopic() { - return messagesReceivedTestingTopic; - } - - @GetMapping(path = "/messages/testingtopicV2") - public List getMessagesReceivedTestingTopicV2() { - return messagesReceivedTestingTopicV2; - } - - @GetMapping(path = "/messages/testingtopicV3") - public List getMessagesReceivedTestingTopicV3() { - return messagesReceivedTestingTopicV3; - } - - @GetMapping(path = "/messages/topicBulkSub") - public List getMessagesReceivedTestingTopicBulkSub() { - return responsesReceivedTestingTopicBulkSub; - } - - @Topic(name = "testingtopic", pubsubName = "messagebus") - @PostMapping("/route1") - public Mono handleMessage(@RequestBody(required = false) CloudEvent envelope) { - return Mono.fromRunnable(() -> { - try { - String message = envelope.getData() == null ? "" : envelope.getData().toString(); - String contentType = envelope.getDatacontenttype() == null ? "" : envelope.getDatacontenttype(); - System.out.println("Testing topic Subscriber got message: " + message + "; Content-type: " + contentType); - messagesReceivedTestingTopic.add(envelope); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } - - @Topic(name = "testingbulktopic", pubsubName = "messagebus") - @PostMapping("/route1_redis") - public Mono handleBulkTopicMessage(@RequestBody(required = false) CloudEvent envelope) { - return Mono.fromRunnable(() -> { - try { - String message = envelope.getData() == null ? "" : envelope.getData().toString(); - String contentType = envelope.getDatacontenttype() == null ? "" : envelope.getDatacontenttype(); - System.out.println("Testing bulk publish topic Subscriber got message: " + message + "; Content-type: " + contentType); - messagesReceivedBulkPublishTopic.add(envelope); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } - - @Topic(name = "testingtopic", pubsubName = "messagebus", - rule = @Rule(match = "event.type == 'myevent.v2'", priority = 2)) - @PostMapping(path = "/route1_v2") - public Mono handleMessageV2(@RequestBody(required = false) CloudEvent envelope) { - return Mono.fromRunnable(() -> { - try { - String message = envelope.getData() == null ? "" : envelope.getData().toString(); - String contentType = envelope.getDatacontenttype() == null ? "" : envelope.getDatacontenttype(); - System.out.println("Testing topic Subscriber got message: " + message + "; Content-type: " + contentType); - messagesReceivedTestingTopicV2.add(envelope); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } - - @Topic(name = "testingtopic", pubsubName = "messagebus", - rule = @Rule(match = "event.type == 'myevent.v3'", priority = 1)) - @PostMapping(path = "/route1_v3") - public Mono handleMessageV3(@RequestBody(required = false) CloudEvent envelope) { - return Mono.fromRunnable(() -> { - try { - String message = envelope.getData() == null ? "" : envelope.getData().toString(); - String contentType = envelope.getDatacontenttype() == null ? "" : envelope.getDatacontenttype(); - System.out.println("Testing topic Subscriber got message: " + message + "; Content-type: " + contentType); - messagesReceivedTestingTopicV3.add(envelope); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } - - @Topic(name = "typedtestingtopic", pubsubName = "messagebus") - @PostMapping(path = "/route1b") - public Mono handleMessageTyped(@RequestBody(required = false) CloudEvent envelope) { - return Mono.fromRunnable(() -> { - try { - String id = envelope.getData() == null ? "" : envelope.getData().getId(); - String contentType = envelope.getDatacontenttype() == null ? "" : envelope.getDatacontenttype(); - System.out.println("Testing typed topic Subscriber got message with ID: " + id + "; Content-type: " + contentType); - messagesByTopic.compute("typedtestingtopic", merge(envelope)); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } - - @Topic(name = "binarytopic", pubsubName = "messagebus") - @PostMapping(path = "/route2") - public Mono handleBinaryMessage(@RequestBody(required = false) CloudEvent envelope) { - return Mono.fromRunnable(() -> { - try { - String message = envelope.getData() == null ? "" : envelope.getData().toString(); - String contentType = envelope.getDatacontenttype() == null ? "" : envelope.getDatacontenttype(); - System.out.println("Binary topic Subscriber got message: " + message + "; Content-type: " + contentType); - messagesByTopic.compute("binarytopic", merge(envelope)); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } - - @Topic(name = "#{'another'.concat('topic')}", pubsubName = "${pubsubName:messagebus}") - @PostMapping(path = "/route3") - public Mono handleMessageAnotherTopic(@RequestBody(required = false) CloudEvent envelope) { - return Mono.fromRunnable(() -> { - try { - String message = envelope.getData() == null ? "" : envelope.getData().toString(); - System.out.println("Another topic Subscriber got message: " + message); - messagesByTopic.compute("anothertopic", merge(envelope)); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } - - @Topic(name = "ttltopic", pubsubName = "messagebus") - @PostMapping(path = "/route4") - public Mono handleMessageTTLTopic(@RequestBody(required = false) CloudEvent envelope) { - return Mono.fromRunnable(() -> { - try { - String message = envelope.getData() == null ? "" : envelope.getData().toString(); - System.out.println("TTL topic Subscriber got message: " + message); - messagesByTopic.compute("ttltopic", merge(envelope)); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } - - @Topic(name = "testinglongvalues", pubsubName = "messagebus") - @PostMapping(path = "/testinglongvalues") - public Mono handleMessageLongValues(@RequestBody(required = false) CloudEvent cloudEvent) { - return Mono.fromRunnable(() -> { - try { - Long message = cloudEvent.getData().getValue(); - System.out.println("Subscriber got: " + message); - messagesByTopic.compute("testinglongvalues", merge(cloudEvent)); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } - - /** - * Receive messages using the bulk subscribe API. - * The maxBulkSubCount and maxBulkSubAwaitDurationMs are adjusted to ensure - * that all the test messages arrive in a single batch. - * - * @param bulkMessage incoming bulk of messages from the message bus. - * @return status for each message received. - */ - @BulkSubscribe(maxMessagesCount = 100, maxAwaitDurationMs = 5000) - @Topic(name = "topicBulkSub", pubsubName = "messagebus") - @PostMapping(path = "/routeBulkSub") - public Mono handleMessageBulk( - @RequestBody(required = false) BulkSubscribeMessage> bulkMessage) { - return Mono.fromCallable(() -> { - if (bulkMessage.getEntries().size() == 0) { - BulkSubscribeAppResponse response = new BulkSubscribeAppResponse(new ArrayList<>()); - responsesReceivedTestingTopicBulkSub.add(response); - return response; - } - - List entries = new ArrayList<>(); - for (BulkSubscribeMessageEntry entry: bulkMessage.getEntries()) { - try { - System.out.printf("Bulk Subscriber got entry ID: %s\n", entry.getEntryId()); - entries.add(new BulkSubscribeAppResponseEntry(entry.getEntryId(), BulkSubscribeAppResponseStatus.SUCCESS)); - } catch (Exception e) { - entries.add(new BulkSubscribeAppResponseEntry(entry.getEntryId(), BulkSubscribeAppResponseStatus.RETRY)); - } - } - BulkSubscribeAppResponse response = new BulkSubscribeAppResponse(entries); - responsesReceivedTestingTopicBulkSub.add(response); - return response; - }); - } - - private BiFunction>, List>> merge(final CloudEvent item) { - return (key, value) -> { - final List> list = value == null ? new ArrayList<>() : value; - list.add(item); - return list; - }; - } - - @GetMapping(path = "/health") - public void health() { - } -} diff --git a/sdk-tests/src/test/java/io/dapr/it/pubsub/http/SubscriberService.java b/sdk-tests/src/test/java/io/dapr/it/pubsub/http/SubscriberService.java deleted file mode 100644 index 8667b2956e..0000000000 --- a/sdk-tests/src/test/java/io/dapr/it/pubsub/http/SubscriberService.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2021 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and -limitations under the License. -*/ - -package io.dapr.it.pubsub.http; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - - -/** - * Service for subscriber. - */ -@SpringBootApplication -public class SubscriberService { - - public static final String SUCCESS_MESSAGE = "Completed initialization in"; - - public static void main(String[] args) throws Exception { - int port = Integer.parseInt(args[0]); - - System.out.printf("Service starting on port %d ...\n", port); - - // Start Dapr's callback endpoint. - start(port); - } - - /** - * Starts Dapr's callback in a given port. - * - * @param port Port to listen to. - */ - private static void start(int port) { - SpringApplication app = new SpringApplication(SubscriberService.class); - app.run(String.format("--server.port=%d", port)); - } - -} \ No newline at end of file diff --git a/sdk-tests/src/test/java/io/dapr/it/pubsub/stream/PubSubStreamIT.java b/sdk-tests/src/test/java/io/dapr/it/pubsub/stream/PubSubStreamIT.java deleted file mode 100644 index 9b0b78ef2f..0000000000 --- a/sdk-tests/src/test/java/io/dapr/it/pubsub/stream/PubSubStreamIT.java +++ /dev/null @@ -1,339 +0,0 @@ -/* - * Copyright 2021 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and -limitations under the License. -*/ - -package io.dapr.it.pubsub.stream; - -import io.dapr.client.DaprClient; -import io.dapr.client.DaprPreviewClient; -import io.dapr.client.SubscriptionListener; -import io.dapr.client.domain.CloudEvent; -import io.dapr.it.BaseIT; -import io.dapr.it.DaprRun; -import io.dapr.utils.TypeRef; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Set; -import java.util.UUID; - -import static io.dapr.it.Retry.callWithRetry; -import static org.junit.jupiter.api.Assertions.assertEquals; - - -public class PubSubStreamIT extends BaseIT { - - // Must be a large enough number, so we validate that we get more than the initial batch - // sent by the runtime. When this was first added, the batch size in runtime was set to 10. - private static final int NUM_MESSAGES = 100; - private static final String TOPIC_NAME = "stream-topic"; - private static final String TOPIC_NAME_FLUX = "stream-topic-flux"; - private static final String TOPIC_NAME_CLOUDEVENT = "stream-topic-cloudevent"; - private static final String TOPIC_NAME_RAWPAYLOAD = "stream-topic-rawpayload"; - private static final String TOPIC_NAME_DLQ = "stream-topic-dlq"; - private static final String TOPIC_NAME_DLQ_DEADLETTER = "stream-topic-dlq-deadletter"; - private static final String PUBSUB_NAME = "messagebus"; - - private final List runs = new ArrayList<>(); - - private DaprRun closeLater(DaprRun run) { - this.runs.add(run); - return run; - } - - @AfterEach - public void tearDown() throws Exception { - for (DaprRun run : runs) { - run.stop(); - } - } - - @Test - public void testPubSub() throws Exception { - final DaprRun daprRun = closeLater(startDaprApp( - this.getClass().getSimpleName(), - 60000)); - - var runId = UUID.randomUUID().toString(); - try (DaprClient client = daprRun.newDaprClient(); - DaprPreviewClient previewClient = daprRun.newDaprPreviewClient()) { - for (int i = 0; i < NUM_MESSAGES; i++) { - String message = String.format("This is message #%d on topic %s for run %s", i, TOPIC_NAME, runId); - //Publishing messages - client.publishEvent(PUBSUB_NAME, TOPIC_NAME, message).block(); - System.out.println( - String.format("Published message: '%s' to topic '%s' pubsub_name '%s'", message, TOPIC_NAME, PUBSUB_NAME)); - } - - System.out.println("Starting subscription for " + TOPIC_NAME); - - Set messages = Collections.synchronizedSet(new HashSet<>()); - Set errors = Collections.synchronizedSet(new HashSet<>()); - - var random = new Random(37); // predictable random. - var listener = new SubscriptionListener() { - @Override - public Mono onEvent(CloudEvent event) { - return Mono.fromCallable(() -> { - // Useful to avoid false negatives running locally multiple times. - if (event.getData().contains(runId)) { - // 5% failure rate. - var decision = random.nextInt(100); - if (decision < 5) { - if (decision % 2 == 0) { - throw new RuntimeException("artificial exception on message " + event.getId()); - } - return Status.RETRY; - } - - messages.add(event.getId()); - return Status.SUCCESS; - } - - return Status.DROP; - }); - } - - @Override - public void onError(RuntimeException exception) { - errors.add(exception.getMessage()); - } - - }; - try(var subscription = previewClient.subscribeToEvents(PUBSUB_NAME, TOPIC_NAME, listener, TypeRef.STRING)) { - callWithRetry(() -> { - var messageCount = messages.size(); - System.out.println( - String.format("Got %d messages out of %d for topic %s.", messageCount, NUM_MESSAGES, TOPIC_NAME)); - assertEquals(NUM_MESSAGES, messages.size()); - assertEquals(4, errors.size()); - }, 120000); // Time for runtime to retry messages. - - subscription.close(); - subscription.awaitTermination(); - } - } - } - - @Test - public void testPubSubFlux() throws Exception { - final DaprRun daprRun = closeLater(startDaprApp( - this.getClass().getSimpleName() + "-flux", - 60000)); - - var runId = UUID.randomUUID().toString(); - try (DaprClient client = daprRun.newDaprClient(); - DaprPreviewClient previewClient = daprRun.newDaprPreviewClient()) { - - // Publish messages - for (int i = 0; i < NUM_MESSAGES; i++) { - String message = String.format("Flux message #%d for run %s", i, runId); - client.publishEvent(PUBSUB_NAME, TOPIC_NAME_FLUX, message).block(); - System.out.println( - String.format("Published flux message: '%s' to topic '%s'", message, TOPIC_NAME_FLUX)); - } - - System.out.println("Starting Flux subscription for " + TOPIC_NAME_FLUX); - - Set messages = Collections.synchronizedSet(new HashSet<>()); - - // subscribeToTopic returns Flux directly (raw data) - var disposable = previewClient.subscribeToTopic(PUBSUB_NAME, TOPIC_NAME_FLUX, TypeRef.STRING) - .doOnNext(rawMessage -> { - // rawMessage is String directly - if (rawMessage.contains(runId)) { - messages.add(rawMessage); - System.out.println("Received raw message: " + rawMessage); - } - }) - .subscribe(); - - callWithRetry(() -> { - var messageCount = messages.size(); - System.out.println( - String.format("Got %d flux messages out of %d for topic %s.", messageCount, NUM_MESSAGES, TOPIC_NAME_FLUX)); - assertEquals(NUM_MESSAGES, messages.size()); - }, 60000); - - disposable.dispose(); - } - } - - @Test - public void testPubSubCloudEvent() throws Exception { - final DaprRun daprRun = closeLater(startDaprApp( - this.getClass().getSimpleName() + "-cloudevent", - 60000)); - - var runId = UUID.randomUUID().toString(); - try (DaprClient client = daprRun.newDaprClient(); - DaprPreviewClient previewClient = daprRun.newDaprPreviewClient()) { - - // Publish messages - for (int i = 0; i < NUM_MESSAGES; i++) { - String message = String.format("CloudEvent message #%d for run %s", i, runId); - client.publishEvent(PUBSUB_NAME, TOPIC_NAME_CLOUDEVENT, message).block(); - System.out.println( - String.format("Published CloudEvent message: '%s' to topic '%s'", message, TOPIC_NAME_CLOUDEVENT)); - } - - System.out.println("Starting CloudEvent subscription for " + TOPIC_NAME_CLOUDEVENT); - - Set messageIds = Collections.synchronizedSet(new HashSet<>()); - - // Use TypeRef> to receive full CloudEvent with metadata - var disposable = previewClient.subscribeToTopic(PUBSUB_NAME, TOPIC_NAME_CLOUDEVENT, new TypeRef>(){}) - .doOnNext(cloudEvent -> { - if (cloudEvent.getData() != null && cloudEvent.getData().contains(runId)) { - messageIds.add(cloudEvent.getId()); - System.out.println("Received CloudEvent with ID: " + cloudEvent.getId() - + ", topic: " + cloudEvent.getTopic() - + ", data: " + cloudEvent.getData()); - } - }) - .subscribe(); - - callWithRetry(() -> { - var messageCount = messageIds.size(); - System.out.println( - String.format("Got %d CloudEvent messages out of %d for topic %s.", messageCount, NUM_MESSAGES, TOPIC_NAME_CLOUDEVENT)); - assertEquals(NUM_MESSAGES, messageIds.size()); - }, 60000); - - disposable.dispose(); - } - } - - @Test - public void testPubSubRawPayload() throws Exception { - final DaprRun daprRun = closeLater(startDaprApp( - this.getClass().getSimpleName() + "-rawpayload", - 60000)); - - var runId = UUID.randomUUID().toString(); - try (DaprClient client = daprRun.newDaprClient(); - DaprPreviewClient previewClient = daprRun.newDaprPreviewClient()) { - - // Publish messages with rawPayload metadata - for (int i = 0; i < NUM_MESSAGES; i++) { - String message = String.format("RawPayload message #%d for run %s", i, runId); - client.publishEvent(PUBSUB_NAME, TOPIC_NAME_RAWPAYLOAD, message, Map.of("rawPayload", "true")).block(); - System.out.println( - String.format("Published raw payload message: '%s' to topic '%s'", message, TOPIC_NAME_RAWPAYLOAD)); - } - - System.out.println("Starting raw payload subscription for " + TOPIC_NAME_RAWPAYLOAD); - - Set messages = Collections.synchronizedSet(new HashSet<>()); - Map metadata = Map.of("rawPayload", "true"); - - // Use subscribeToTopic with rawPayload metadata - var disposable = previewClient.subscribeToTopic(PUBSUB_NAME, TOPIC_NAME_RAWPAYLOAD, TypeRef.STRING, metadata) - .doOnNext(rawMessage -> { - if (rawMessage.contains(runId)) { - messages.add(rawMessage); - System.out.println("Received raw payload message: " + rawMessage); - } - }) - .subscribe(); - - callWithRetry(() -> { - var messageCount = messages.size(); - System.out.println( - String.format("Got %d raw payload messages out of %d for topic %s.", messageCount, NUM_MESSAGES, TOPIC_NAME_RAWPAYLOAD)); - assertEquals(NUM_MESSAGES, messages.size()); - }, 60000); - - disposable.dispose(); - } - } - - @Test - public void testPubSubDeadLetterTopic() throws Exception { - final DaprRun daprRun = closeLater(startDaprApp( - this.getClass().getSimpleName() + "-dlq", - 60000)); - - var runId = UUID.randomUUID().toString(); - try (DaprClient client = daprRun.newDaprClient(); - DaprPreviewClient previewClient = daprRun.newDaprPreviewClient()) { - - // Subscribe to the dead-letter topic first so we don't miss any messages. - Set deadLetterMessageIds = Collections.synchronizedSet(new HashSet<>()); - var deadLetterListener = new SubscriptionListener() { - @Override - public Mono onEvent(CloudEvent event) { - if (event.getData() != null && event.getData().contains(runId)) { - deadLetterMessageIds.add(event.getId()); - System.out.println("Received dead-letter message ID: " + event.getId()); - } - return Mono.just(Status.SUCCESS); - } - - @Override - public void onError(RuntimeException exception) { - System.err.println("Dead-letter subscription error: " + exception.getMessage()); - } - }; - - // Subscribe to the main topic with a listener that always DROPs, which should - // forward the messages to the dead-letter topic. - var mainListener = new SubscriptionListener() { - @Override - public Mono onEvent(CloudEvent event) { - if (event.getData() != null && event.getData().contains(runId)) { - System.out.println("Dropping message ID: " + event.getId()); - return Mono.just(Status.DROP); - } - return Mono.just(Status.DROP); - } - - @Override - public void onError(RuntimeException exception) { - System.err.println("Main subscription error: " + exception.getMessage()); - } - }; - - try (var deadLetterSubscription = previewClient.subscribeToEvents( - PUBSUB_NAME, TOPIC_NAME_DLQ_DEADLETTER, deadLetterListener, TypeRef.STRING); - var mainSubscription = previewClient.subscribeToEvents( - PUBSUB_NAME, TOPIC_NAME_DLQ, TOPIC_NAME_DLQ_DEADLETTER, mainListener, TypeRef.STRING)) { - - // Publish messages to the main topic. - for (int i = 0; i < NUM_MESSAGES; i++) { - String message = String.format("DLQ message #%d for run %s", i, runId); - client.publishEvent(PUBSUB_NAME, TOPIC_NAME_DLQ, message).block(); - } - - callWithRetry(() -> { - var count = deadLetterMessageIds.size(); - System.out.println( - String.format("Got %d dead-letter messages out of %d for topic %s.", - count, NUM_MESSAGES, TOPIC_NAME_DLQ_DEADLETTER)); - assertEquals(NUM_MESSAGES, deadLetterMessageIds.size()); - }, 120000); - - mainSubscription.close(); - mainSubscription.awaitTermination(); - deadLetterSubscription.close(); - deadLetterSubscription.awaitTermination(); - } - } - } -} diff --git a/sdk-tests/src/test/java/io/dapr/it/resiliency/SdkResiliencyIT.java b/sdk-tests/src/test/java/io/dapr/it/resiliency/SdkResiliencyIT.java index 05182f8d6c..d7f6eea96b 100644 --- a/sdk-tests/src/test/java/io/dapr/it/resiliency/SdkResiliencyIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/resiliency/SdkResiliencyIT.java @@ -14,7 +14,8 @@ package io.dapr.it.resiliency; import com.github.tomakehurst.wiremock.client.WireMock; -import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import eu.rekawek.toxiproxy.Proxy; import eu.rekawek.toxiproxy.ToxiproxyClient; import eu.rekawek.toxiproxy.model.ToxicDirection; @@ -35,6 +36,7 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Tags; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.slf4j.LoggerFactory; import org.testcontainers.containers.Network; import org.testcontainers.toxiproxy.ToxiproxyContainer; @@ -49,7 +51,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.any; -import static com.github.tomakehurst.wiremock.client.WireMock.configureFor; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; @@ -57,32 +58,25 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; import static com.github.tomakehurst.wiremock.client.WireMock.verify; -import static io.dapr.it.resiliency.SdkResiliencyIT.WIREMOCK_PORT; import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG; import static io.dapr.it.testcontainers.ContainerConstants.TOXI_PROXY_IMAGE_TAG; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @Testcontainers -@WireMockTest(httpPort = WIREMOCK_PORT) @Tags({@Tag("testcontainers"), @Tag("resiliency")}) public class SdkResiliencyIT { - public static final int WIREMOCK_PORT = 8888; private static final Network NETWORK = Network.newNetwork(); private static final String STATE_STORE_NAME = "kvstore"; private static final int INFINITE_RETRY = -1; - @Container - private static final DaprContainer DAPR_CONTAINER = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) - .withAppName("dapr-app") - .withAppPort(WIREMOCK_PORT) - .withDaprLogLevel(DaprLogLevel.DEBUG) - .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("dapr-logs"))) - .withAppHealthCheckPath("/actuator/health") - .withAppChannelAddress("host.testcontainers.internal") - .withNetworkAliases("dapr") - .withNetwork(NETWORK); + @RegisterExtension + static WireMockExtension wireMock = WireMockExtension.newInstance() + .options(WireMockConfiguration.wireMockConfig().dynamicPort()) + .build(); + + private static DaprContainer daprContainer; @Container private static final ToxiproxyContainer TOXIPROXY = new ToxiproxyContainer(TOXI_PROXY_IMAGE_TAG) @@ -91,41 +85,54 @@ public class SdkResiliencyIT { private static Proxy proxy; private void configStub() { - stubFor(any(urlMatching("/actuator/health")) + wireMock.stubFor(any(urlMatching("/actuator/health")) .willReturn(aResponse().withBody("[]").withStatus(200))); - stubFor(any(urlMatching("/dapr/subscribe")) + wireMock.stubFor(any(urlMatching("/dapr/subscribe")) .willReturn(aResponse().withBody("[]").withStatus(200))); - stubFor(get(urlMatching("/dapr/config")) + wireMock.stubFor(get(urlMatching("/dapr/config")) .willReturn(aResponse().withBody("[]").withStatus(200))); - // create a stub for simulating dapr sidecar with timeout of 1000 ms - stubFor(post(urlEqualTo("/dapr.proto.runtime.v1.Dapr/SaveState")) + wireMock.stubFor(post(urlEqualTo("/dapr.proto.runtime.v1.Dapr/SaveState")) .willReturn(aResponse().withStatus(204).withFixedDelay(1000))); - stubFor(any(urlMatching("/([a-z1-9]*)")) + wireMock.stubFor(any(urlMatching("/([a-z1-9]*)")) .willReturn(aResponse().withBody("[]").withStatus(200))); - configureFor("localhost", WIREMOCK_PORT); + WireMock.configureFor("localhost", wireMock.getPort()); } @BeforeAll static void configure() throws IOException { + int wmPort = wireMock.getPort(); + org.testcontainers.Testcontainers.exposeHostPorts(wmPort); + + daprContainer = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) + .withAppName("dapr-app") + .withAppPort(wmPort) + .withDaprLogLevel(DaprLogLevel.DEBUG) + .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("dapr-logs"))) + .withAppHealthCheckPath("/actuator/health") + .withAppChannelAddress("host.testcontainers.internal") + .withNetworkAliases("dapr") + .withNetwork(NETWORK); + daprContainer.start(); + ToxiproxyClient toxiproxyClient = new ToxiproxyClient(TOXIPROXY.getHost(), TOXIPROXY.getControlPort()); - proxy = - toxiproxyClient.createProxy("dapr", "0.0.0.0:8666", "dapr:3500"); + proxy = toxiproxyClient.createProxy("dapr", "0.0.0.0:8666", "dapr:3500"); } @AfterAll static void afterAll() { - WireMock.shutdownServer(); + if (daprContainer != null) { + daprContainer.stop(); + } } @BeforeEach public void beforeEach() { configStub(); - org.testcontainers.Testcontainers.exposeHostPorts(WIREMOCK_PORT); } @Test @@ -189,10 +196,11 @@ public void shouldFailDueToLatencyExceedingConfigurationWithInfiniteRetry() thro @Test @DisplayName("should fail due to latency exceeding configuration with once retry") public void shouldFailDueToLatencyExceedingConfigurationWithOnceRetry() throws Exception { + int wmPort = wireMock.getPort(); DaprClient client = - new DaprClientBuilder().withPropertyOverride(Properties.HTTP_ENDPOINT, "http://localhost:" + WIREMOCK_PORT) - .withPropertyOverride(Properties.GRPC_ENDPOINT, "http://localhost:" + WIREMOCK_PORT) + new DaprClientBuilder().withPropertyOverride(Properties.HTTP_ENDPOINT, "http://localhost:" + wmPort) + .withPropertyOverride(Properties.GRPC_ENDPOINT, "http://localhost:" + wmPort) .withResiliencyOptions(new ResiliencyOptions().setTimeout(Duration.ofMillis(900)) .setMaxRetries(1)) .build(); @@ -202,7 +210,7 @@ public void shouldFailDueToLatencyExceedingConfigurationWithOnceRetry() throws E } catch (Exception ignored) { } - verify(2, postRequestedFor(urlEqualTo("/dapr.proto.runtime.v1.Dapr/SaveState"))); + wireMock.verify(2, postRequestedFor(urlEqualTo("/dapr.proto.runtime.v1.Dapr/SaveState"))); client.close(); } diff --git a/sdk-tests/src/test/java/io/dapr/it/secrets/SecretsClientIT.java b/sdk-tests/src/test/java/io/dapr/it/secrets/SecretsClientIT.java index 23f05957ba..202d7b9499 100644 --- a/sdk-tests/src/test/java/io/dapr/it/secrets/SecretsClientIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/secrets/SecretsClientIT.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Dapr Authors + * Copyright 2025 The Dapr Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -15,19 +15,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.dapr.client.DaprClient; -import io.dapr.client.DaprClientBuilder; -import io.dapr.it.BaseIT; -import io.dapr.it.DaprRun; -import org.apache.commons.io.IOUtils; +import io.dapr.it.containers.BaseContainerIT; +import io.dapr.testcontainers.Component; +import io.dapr.testcontainers.DaprContainer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.testcontainers.images.builder.Transferable; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -35,57 +31,44 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -/** - * Test Secrets Store APIs using local file. - * - * 1. create secret file locally: - */ -public class SecretsClientIT extends BaseIT { +public class SecretsClientIT extends BaseContainerIT { - /** - * JSON Serializer to print output. - */ private static final ObjectMapper JSON_SERIALIZER = new ObjectMapper(); - private static final String SECRETS_STORE_NAME = "localSecretStore"; - - private static final String LOCAL_SECRET_FILE_PATH = "./components/secret.json"; - + private static final String CONTAINER_SECRET_PATH = "/dapr-secret.json"; private static final String KEY1 = UUID.randomUUID().toString(); - private static final String KYE2 = UUID.randomUUID().toString(); - private static DaprRun daprRun; - - + private static DaprContainer dapr; private DaprClient daprClient; - private static File localSecretFile; - @BeforeAll public static void init() throws Exception { - - localSecretFile = new File(LOCAL_SECRET_FILE_PATH); - boolean existed = localSecretFile.exists(); - assertTrue(existed); - initSecretFile(); - - daprRun = startDaprApp(SecretsClientIT.class.getSimpleName(), 5000); + byte[] secretJson = JSON_SERIALIZER.writeValueAsBytes(buildSecretPayload()); + + dapr = daprBuilder("secrets-it") + .withComponent(new Component(SECRETS_STORE_NAME, "secretstores.local.file", "v1", Map.of( + "secretsFile", CONTAINER_SECRET_PATH, + "nestedSeparator", ":", + "multiValued", "true" + ))) + .withCopyToContainer(Transferable.of(secretJson), CONTAINER_SECRET_PATH); + dapr.start(); + deferStop(dapr); } @BeforeEach public void setup() { - this.daprClient = daprRun.newDaprClientBuilder().build(); + this.daprClient = newDaprClient(dapr); } @AfterEach public void tearDown() throws Exception { daprClient.close(); - clearSecretFile(); } @Test - public void getSecret() throws Exception { + public void getSecret() { Map data = daprClient.getSecret(SECRETS_STORE_NAME, KEY1).block(); assertEquals(2, data.size()); assertEquals("The Metrics IV", data.get("title")); @@ -93,9 +76,8 @@ public void getSecret() throws Exception { } @Test - public void getBulkSecret() throws Exception { + public void getBulkSecret() { Map> data = daprClient.getBulkSecret(SECRETS_STORE_NAME).block(); - // There can be other keys from other runs or test cases, so we are good with at least two. assertTrue(data.size() >= 2); assertEquals(2, data.get(KEY1).size()); assertEquals("The Metrics IV", data.get(KEY1).get("title")); @@ -114,26 +96,10 @@ public void getSecretStoreNotFound() { assertThrows(RuntimeException.class, () -> daprClient.getSecret("unknownStore", "unknownKey").block()); } - private static void initSecretFile() throws Exception { - Map key2 = new HashMap(){{ - put("name", "Jon Doe"); - }}; - Map key1 = new HashMap(){{ - put("title", "The Metrics IV"); - put("year", "2020"); - }}; - Map> secret = new HashMap<>(){{ - put(KEY1, key1); - put(KYE2, key2); - }}; - try (FileOutputStream fos = new FileOutputStream(localSecretFile)) { - JSON_SERIALIZER.writeValue(fos, secret); - } - } - - private static void clearSecretFile() throws IOException { - try (FileOutputStream fos = new FileOutputStream(localSecretFile)) { - IOUtils.write("{}", fos); - } + private static Map> buildSecretPayload() { + return Map.of( + KEY1, Map.of("title", "The Metrics IV", "year", "2020"), + KYE2, Map.of("name", "Jon Doe") + ); } } diff --git a/sdk-tests/src/test/java/io/dapr/it/state/AbstractStateClientIT.java b/sdk-tests/src/test/java/io/dapr/it/state/AbstractStateClientIT.java index 255c310517..4cc9db29c9 100644 --- a/sdk-tests/src/test/java/io/dapr/it/state/AbstractStateClientIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/state/AbstractStateClientIT.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Dapr Authors + * Copyright 2025 The Dapr Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -27,7 +27,7 @@ import io.dapr.client.domain.query.Sorting; import io.dapr.client.domain.query.filters.EqFilter; import io.dapr.exceptions.DaprException; -import io.dapr.it.BaseIT; +import io.dapr.it.containers.BaseContainerIT; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -50,8 +50,9 @@ /** * Common test cases for Dapr client (GRPC and HTTP). */ -public abstract class AbstractStateClientIT extends BaseIT { +public abstract class AbstractStateClientIT extends BaseContainerIT { private static final Logger logger = Logger.getLogger(AbstractStateClientIT.class.getName()); + private static final String QUERY_STATE_STORE = MONGO_QUERY_STATE_STORE_NAME; @Test public void saveAndGetState() { diff --git a/sdk-tests/src/test/java/io/dapr/it/state/GRPCStateClientIT.java b/sdk-tests/src/test/java/io/dapr/it/state/GRPCStateClientIT.java index 5254e0b06a..900ea7af1d 100644 --- a/sdk-tests/src/test/java/io/dapr/it/state/GRPCStateClientIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/state/GRPCStateClientIT.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Dapr Authors + * Copyright 2025 The Dapr Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -13,10 +13,13 @@ package io.dapr.it.state; +import com.google.protobuf.ByteString; import io.dapr.client.DaprClient; -import io.dapr.client.DaprClientBuilder; import io.dapr.client.domain.State; -import io.dapr.it.DaprRun; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.v1.CommonProtos; +import io.dapr.v1.DaprGrpc; +import io.dapr.v1.DaprStateProtos; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -24,27 +27,31 @@ import java.util.Collections; import static io.dapr.it.TestUtils.assertThrowsDaprException; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * Test State GRPC DAPR capabilities using a DAPR instance with an empty service running */ public class GRPCStateClientIT extends AbstractStateClientIT { - private static DaprRun daprRun; - + private static DaprContainer dapr; private static DaprClient daprClient; @BeforeAll - public static void init() throws Exception { - daprRun = startDaprApp(GRPCStateClientIT.class.getSimpleName(), 5000); - daprClient = daprRun.newDaprClientBuilder().build(); + public static void init() { + dapr = daprBuilder("grpc-state-it") + .withComponent(redisStateStore(STATE_STORE_NAME)) + .withComponent(mongoStateStore(MONGO_QUERY_STATE_STORE_NAME)); + dapr.start(); + deferStop(dapr); + daprClient = newDaprClient(dapr); } @AfterAll public static void tearDown() throws Exception { daprClient.close(); } - + @Override protected DaprClient buildDaprClient() { return daprClient; @@ -81,4 +88,45 @@ public void getStatesStoreNotFound() { byte[].class).block()); } + /** + * Exercises {@link DaprClient#newGrpcStub(String, java.util.function.Function)} — + * the public API for obtaining a raw {@code DaprGrpc.DaprBlockingStub} routed + * through the SDK's managed channel. Ports the only test from the legacy + * {@code HelloWorldClientIT}, which previously exercised this API end-to-end + * via {@code dapr run}. Uses the raw stub for save/get/delete to avoid the + * SDK's default JSON serialization wrapping the value in quotes. + */ + @Test + public void rawGrpcStubGetAndDeleteState() { + final String key = "newGrpcStubKey"; + final String value = "Hello World"; + + DaprGrpc.DaprBlockingStub stub = buildDaprClient().newGrpcStub("n/a", DaprGrpc::newBlockingStub); + + stub.saveState(DaprStateProtos.SaveStateRequest.newBuilder() + .setStoreName(STATE_STORE_NAME) + .addStates(CommonProtos.StateItem.newBuilder() + .setKey(key) + .setValue(ByteString.copyFromUtf8(value)) + .build()) + .build()); + + DaprStateProtos.GetStateResponse before = stub.getState(DaprStateProtos.GetStateRequest.newBuilder() + .setStoreName(STATE_STORE_NAME) + .setKey(key) + .build()); + assertEquals(value, before.getData().toStringUtf8()); + + stub.deleteState(DaprStateProtos.DeleteStateRequest.newBuilder() + .setStoreName(STATE_STORE_NAME) + .setKey(key) + .build()); + + DaprStateProtos.GetStateResponse after = stub.getState(DaprStateProtos.GetStateRequest.newBuilder() + .setStoreName(STATE_STORE_NAME) + .setKey(key) + .build()); + assertEquals("", after.getData().toStringUtf8()); + } + } diff --git a/sdk-tests/src/test/java/io/dapr/it/state/HelloWorldClientIT.java b/sdk-tests/src/test/java/io/dapr/it/state/HelloWorldClientIT.java deleted file mode 100644 index bdd25ae780..0000000000 --- a/sdk-tests/src/test/java/io/dapr/it/state/HelloWorldClientIT.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2021 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and -limitations under the License. -*/ - -package io.dapr.it.state; - -import io.dapr.it.BaseIT; -import io.dapr.it.DaprRun; -import io.dapr.v1.DaprGrpc; -import io.dapr.v1.DaprStateProtos; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class HelloWorldClientIT extends BaseIT { - - @Test - public void testHelloWorldState() throws Exception { - DaprRun daprRun = startDaprApp( - HelloWorldClientIT.class.getSimpleName(), - HelloWorldGrpcStateService.SUCCESS_MESSAGE, - HelloWorldGrpcStateService.class, - false, - 2000 - ); - try (var client = daprRun.newDaprClientBuilder().build()) { - var stub = client.newGrpcStub("n/a", DaprGrpc::newBlockingStub); - - String key = "mykey"; - { - DaprStateProtos.GetStateRequest req = DaprStateProtos.GetStateRequest - .newBuilder() - .setStoreName(STATE_STORE_NAME) - .setKey(key) - .build(); - DaprStateProtos.GetStateResponse response = stub.getState(req); - String value = response.getData().toStringUtf8(); - System.out.println("Got: " + value); - Assertions.assertEquals("Hello World", value); - } - - // Then, delete it. - { - DaprStateProtos.DeleteStateRequest req = DaprStateProtos.DeleteStateRequest - .newBuilder() - .setStoreName(STATE_STORE_NAME) - .setKey(key) - .build(); - stub.deleteState(req); - System.out.println("Deleted!"); - } - - { - DaprStateProtos.GetStateRequest req = DaprStateProtos.GetStateRequest - .newBuilder() - .setStoreName(STATE_STORE_NAME) - .setKey(key) - .build(); - DaprStateProtos.GetStateResponse response = stub.getState(req); - String value = response.getData().toStringUtf8(); - System.out.println("Got: " + value); - Assertions.assertEquals("", value); - } - } - } -} diff --git a/sdk-tests/src/test/java/io/dapr/it/state/HelloWorldGrpcStateService.java b/sdk-tests/src/test/java/io/dapr/it/state/HelloWorldGrpcStateService.java deleted file mode 100644 index abab918be7..0000000000 --- a/sdk-tests/src/test/java/io/dapr/it/state/HelloWorldGrpcStateService.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2021 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and -limitations under the License. -*/ - -package io.dapr.it.state; - -import com.google.protobuf.ByteString; -import io.dapr.client.DaprClientBuilder; -import io.dapr.config.Properties; -import io.dapr.internal.grpc.DaprClientGrpcInterceptors; -import io.dapr.v1.CommonProtos.StateItem; -import io.dapr.v1.DaprGrpc; -import io.dapr.v1.DaprGrpc.DaprBlockingStub; -import io.dapr.v1.DaprStateProtos.SaveStateRequest; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; - - -/** - * Simple example. - * To run manually, from repo root: - * 1. mvn clean install - * 2. dapr run --resources-path ./components --dapr-grpc-port 50001 -- mvn exec:java -Dexec.mainClass=io.dapr.it.state.HelloWorldGrpcStateService -Dexec.classpathScope="test" -pl=sdk - */ -public class HelloWorldGrpcStateService { - - public static final String SUCCESS_MESSAGE = "Hello from " + HelloWorldGrpcStateService.class.getSimpleName(); - - public static void main(String[] args) { - String grpcPort = System.getenv("DAPR_GRPC_PORT"); - - // If port string is not valid, it will throw an exception. - int grpcPortInt = Integer.parseInt(grpcPort); - ManagedChannel channel = ManagedChannelBuilder.forAddress( - Properties.SIDECAR_IP.get(), grpcPortInt).usePlaintext().build(); - DaprClientGrpcInterceptors interceptors = new DaprClientGrpcInterceptors( - Properties.API_TOKEN.get(), null); - DaprBlockingStub client = interceptors.intercept(DaprGrpc.newBlockingStub(channel)); - - String key = "mykey"; - // First, write key-value pair. - - String value = "Hello World"; - StateItem req = StateItem - .newBuilder() - .setKey(key) - .setValue(ByteString.copyFromUtf8(value)) - .build(); - SaveStateRequest state = SaveStateRequest.newBuilder() - .setStoreName("statestore") - .addStates(req) - .build(); - client.saveState(state); - System.out.println("Saved!"); - channel.shutdown(); - - System.out.println(SUCCESS_MESSAGE); - } -} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/jobs/DaprJobsIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/jobs/DaprJobsIT.java index 2694e32e5d..79a1d3485a 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/jobs/DaprJobsIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/jobs/DaprJobsIT.java @@ -25,6 +25,7 @@ import io.dapr.it.testcontainers.DaprClientConfiguration; import io.dapr.testcontainers.DaprContainer; import io.dapr.testcontainers.DaprLogLevel; +import org.awaitility.Awaitility; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -43,9 +44,11 @@ import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.Random; +import java.util.UUID; import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; @SpringBootTest( webEnvironment = WebEnvironment.RANDOM_PORT, @@ -93,63 +96,77 @@ public void setUp(){ @Test public void testJobScheduleCreationWithDueTime() { + String jobName = "Job-" + UUID.randomUUID().toString().substring(0, 8); DateTimeFormatter iso8601Formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") .withZone(ZoneOffset.UTC); - Instant currentTime = Instant.now(); - daprClient.scheduleJob(new ScheduleJobRequest("Job", currentTime).setOverwrite(true)).block(); + Instant currentTime = Instant.now().plus(10, ChronoUnit.MINUTES); + daprClient.scheduleJob(new ScheduleJobRequest(jobName, currentTime)).block(); - GetJobResponse getJobResponse = - daprClient.getJob(new GetJobRequest("Job")).block(); + GetJobResponse getJobResponse = Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofMillis(300)) + .ignoreExceptions() + .until(() -> daprClient.getJob(new GetJobRequest(jobName)).block(), r -> r != null); - daprClient.deleteJob(new DeleteJobRequest("Job")).block(); + daprClient.deleteJob(new DeleteJobRequest(jobName)).block(); + assertNotNull(getJobResponse); assertEquals(iso8601Formatter.format(currentTime), getJobResponse.getDueTime().toString()); - assertEquals("Job", getJobResponse.getName()); + assertEquals(jobName, getJobResponse.getName()); } @Test public void testJobScheduleCreationWithSchedule() { + String jobName = "Job-" + UUID.randomUUID().toString().substring(0, 8); DateTimeFormatter iso8601Formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") .withZone(ZoneOffset.UTC); - Instant currentTime = Instant.now(); - daprClient.scheduleJob(new ScheduleJobRequest("Job", JobSchedule.hourly()) - .setDueTime(currentTime).setOverwrite(true)).block(); + Instant currentTime = Instant.now().plus(10, ChronoUnit.MINUTES); + daprClient.scheduleJob(new ScheduleJobRequest(jobName, JobSchedule.hourly()) + .setDueTime(currentTime)).block(); - GetJobResponse getJobResponse = - daprClient.getJob(new GetJobRequest("Job")).block(); + GetJobResponse getJobResponse = Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofMillis(300)) + .ignoreExceptions() + .until(() -> daprClient.getJob(new GetJobRequest(jobName)).block(), r -> r != null); - daprClient.deleteJob(new DeleteJobRequest("Job")).block(); + daprClient.deleteJob(new DeleteJobRequest(jobName)).block(); + assertNotNull(getJobResponse); assertEquals(iso8601Formatter.format(currentTime), getJobResponse.getDueTime().toString()); assertEquals(JobSchedule.hourly().getExpression(), getJobResponse.getSchedule().getExpression()); - assertEquals("Job", getJobResponse.getName()); + assertEquals(jobName, getJobResponse.getName()); } @Test public void testJobScheduleCreationWithAllParameters() { - Instant currentTime = Instant.now(); + String jobName = "Job-" + UUID.randomUUID().toString().substring(0, 8); + Instant currentTime = Instant.now().plus(10, ChronoUnit.MINUTES); DateTimeFormatter iso8601Formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") .withZone(ZoneOffset.UTC); String cronExpression = "2 * 3 * * FRI"; - daprClient.scheduleJob(new ScheduleJobRequest("Job", currentTime) + daprClient.scheduleJob(new ScheduleJobRequest(jobName, currentTime) .setTtl(currentTime.plus(2, ChronoUnit.HOURS)) .setData("Job data".getBytes()) .setRepeat(3) - .setOverwrite(true) .setSchedule(JobSchedule.fromString(cronExpression))).block(); - GetJobResponse getJobResponse = - daprClient.getJob(new GetJobRequest("Job")).block(); + GetJobResponse getJobResponse = Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofMillis(300)) + .ignoreExceptions() + .until(() -> daprClient.getJob(new GetJobRequest(jobName)).block(), r -> r != null); - daprClient.deleteJob(new DeleteJobRequest("Job")).block(); + daprClient.deleteJob(new DeleteJobRequest(jobName)).block(); + assertNotNull(getJobResponse); assertEquals(iso8601Formatter.format(currentTime), getJobResponse.getDueTime().toString()); assertEquals("2 * 3 * * FRI", getJobResponse.getSchedule().getExpression()); - assertEquals("Job", getJobResponse.getName()); + assertEquals(jobName, getJobResponse.getName()); assertEquals(Integer.valueOf(3), getJobResponse.getRepeats()); assertEquals("Job data", new String(getJobResponse.getData())); assertEquals(iso8601Formatter.format(currentTime.plus(2, ChronoUnit.HOURS)), @@ -158,36 +175,38 @@ public void testJobScheduleCreationWithAllParameters() { @Test public void testJobScheduleCreationWithDropFailurePolicy() { - Instant currentTime = Instant.now(); - DateTimeFormatter iso8601Formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - .withZone(ZoneOffset.UTC); + String jobName = "Job-" + UUID.randomUUID().toString().substring(0, 8); + Instant currentTime = Instant.now().plus(10, ChronoUnit.MINUTES); String cronExpression = "2 * 3 * * FRI"; - daprClient.scheduleJob(new ScheduleJobRequest("Job", currentTime) + daprClient.scheduleJob(new ScheduleJobRequest(jobName, currentTime) .setTtl(currentTime.plus(2, ChronoUnit.HOURS)) .setData("Job data".getBytes()) .setRepeat(3) - .setFailurePolicy(new DropFailurePolicy()) + .setFailurePolicy(new DropFailurePolicy()) .setSchedule(JobSchedule.fromString(cronExpression))).block(); - GetJobResponse getJobResponse = - daprClient.getJob(new GetJobRequest("Job")).block(); + GetJobResponse getJobResponse = Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofMillis(300)) + .ignoreExceptions() + .until(() -> daprClient.getJob(new GetJobRequest(jobName)).block(), r -> r != null); - daprClient.deleteJob(new DeleteJobRequest("Job")).block(); + daprClient.deleteJob(new DeleteJobRequest(jobName)).block(); + assertNotNull(getJobResponse); assertEquals(FailurePolicyType.DROP, getJobResponse.getFailurePolicy().getFailurePolicyType()); } @Test public void testJobScheduleCreationWithConstantFailurePolicy() { - Instant currentTime = Instant.now(); - DateTimeFormatter iso8601Formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - .withZone(ZoneOffset.UTC); + String jobName = "Job-" + UUID.randomUUID().toString().substring(0, 8); + Instant currentTime = Instant.now().plus(10, ChronoUnit.MINUTES); String cronExpression = "2 * 3 * * FRI"; - daprClient.scheduleJob(new ScheduleJobRequest("Job", currentTime) + daprClient.scheduleJob(new ScheduleJobRequest(jobName, currentTime) .setTtl(currentTime.plus(2, ChronoUnit.HOURS)) .setData("Job data".getBytes()) .setRepeat(3) @@ -195,11 +214,15 @@ public void testJobScheduleCreationWithConstantFailurePolicy() { .setDurationBetweenRetries(Duration.of(10, ChronoUnit.SECONDS))) .setSchedule(JobSchedule.fromString(cronExpression))).block(); - GetJobResponse getJobResponse = - daprClient.getJob(new GetJobRequest("Job")).block(); + GetJobResponse getJobResponse = Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofMillis(300)) + .ignoreExceptions() + .until(() -> daprClient.getJob(new GetJobRequest(jobName)).block(), r -> r != null); - daprClient.deleteJob(new DeleteJobRequest("Job")).block(); + daprClient.deleteJob(new DeleteJobRequest(jobName)).block(); + assertNotNull(getJobResponse); ConstantFailurePolicy jobFailurePolicyConstant = (ConstantFailurePolicy) getJobResponse.getFailurePolicy(); assertEquals(FailurePolicyType.CONSTANT, getJobResponse.getFailurePolicy().getFailurePolicyType()); assertEquals(3, (int)jobFailurePolicyConstant.getMaxRetries()); @@ -209,17 +232,23 @@ public void testJobScheduleCreationWithConstantFailurePolicy() { @Test public void testDeleteJobRequest() { - Instant currentTime = Instant.now(); + String jobName = "Job-" + UUID.randomUUID().toString().substring(0, 8); + Instant currentTime = Instant.now().plus(10, ChronoUnit.MINUTES); String cronExpression = "2 * 3 * * FRI"; - daprClient.scheduleJob(new ScheduleJobRequest("Job", currentTime) + daprClient.scheduleJob(new ScheduleJobRequest(jobName, currentTime) .setTtl(currentTime.plus(2, ChronoUnit.HOURS)) .setData("Job data".getBytes()) .setRepeat(3) - .setOverwrite(true) .setSchedule(JobSchedule.fromString(cronExpression))).block(); - daprClient.deleteJob(new DeleteJobRequest("Job")).block(); + Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofMillis(300)) + .ignoreExceptions() + .until(() -> daprClient.getJob(new GetJobRequest(jobName)).block(), r -> r != null); + + daprClient.deleteJob(new DeleteJobRequest(jobName)).block(); } } diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/http/ConvertToLong.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/http/ConvertToLong.java new file mode 100644 index 0000000000..56e10b7880 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/http/ConvertToLong.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ +package io.dapr.it.testcontainers.pubsub.http; + +import java.util.Objects; + +public class ConvertToLong { + private Long value; + + public ConvertToLong setVal(Long value) { + this.value = value; + return this; + } + + public Long getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConvertToLong that = (ConvertToLong) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/http/DaprPubSubIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/http/DaprPubSubIT.java index eaa0f0c99f..832dff08fe 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/http/DaprPubSubIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/http/DaprPubSubIT.java @@ -22,11 +22,13 @@ import io.dapr.client.domain.BulkPublishEntry; import io.dapr.client.domain.BulkPublishRequest; import io.dapr.client.domain.BulkPublishResponse; +import io.dapr.client.domain.BulkSubscribeAppResponse; +import io.dapr.client.domain.BulkSubscribeAppResponseEntry; +import io.dapr.client.domain.BulkSubscribeAppResponseStatus; import io.dapr.client.domain.CloudEvent; import io.dapr.client.domain.HttpExtension; import io.dapr.client.domain.Metadata; import io.dapr.client.domain.PublishEventRequest; -import io.dapr.it.pubsub.http.PubSubIT; import io.dapr.it.testcontainers.DaprClientFactory; import io.dapr.serializer.CustomizableObjectSerializer; import io.dapr.serializer.DaprObjectSerializer; @@ -96,16 +98,17 @@ public class DaprPubSubIT { private static final String BINARY_TOPIC_NAME = "binarytopic"; private static final String TTL_TOPIC_NAME = "ttltopic"; private static final String LONG_TOPIC_NAME = "testinglongvalues"; + private static final String BULK_SUB_TOPIC_NAME = "topicBulkSub"; private static final int NUM_MESSAGES = 10; // typeRefs private static final TypeRef> CLOUD_EVENT_LIST_TYPE_REF = new TypeRef<>() { }; - private static final TypeRef>> CLOUD_EVENT_LONG_LIST_TYPE_REF = + private static final TypeRef>> CLOUD_EVENT_LONG_LIST_TYPE_REF = new TypeRef<>() { }; - private static final TypeRef>> CLOUD_EVENT_MYOBJECT_LIST_TYPE_REF = + private static final TypeRef>> CLOUD_EVENT_MYOBJECT_LIST_TYPE_REF = new TypeRef<>() { }; @@ -199,7 +202,7 @@ public void testPubSub() throws Exception { sendBulkMessagesAsText(client, ANOTHER_TOPIC_NAME); //Publishing an object. - PubSubIT.MyObject object = new PubSubIT.MyObject(); + MyObject object = new MyObject(); object.setId("123"); client.publishEvent(PUBSUB_NAME, TOPIC_NAME, object).block(); LOG.info("Published one object."); @@ -321,7 +324,7 @@ public void testPubSub() throws Exception { callWithRetry(() -> { LOG.info("Checking results for topic " + TYPED_TOPIC_NAME); - List> messages = client.invokeMethod( + List> messages = client.invokeMethod( PUBSUB_APP_ID, "messages/typedtestingtopic", null, @@ -332,8 +335,8 @@ public void testPubSub() throws Exception { assertThat(messages) .extracting(CloudEvent::getData) .filteredOn(Objects::nonNull) - .filteredOn(PubSubIT.MyObject.class::isInstance) - .map(PubSubIT.MyObject::getId) + .filteredOn(MyObject.class::isInstance) + .map(MyObject::getId) .contains("123"); }, 2000); @@ -408,9 +411,9 @@ private static void sendBulkMessagesAsText(DaprClient client, String topicName) } private void publishMyObjectAsserting(DaprClient client) { - PubSubIT.MyObject object = new PubSubIT.MyObject(); + MyObject object = new MyObject(); object.setId("123"); - BulkPublishResponse response = client.publishEvents( + BulkPublishResponse response = client.publishEvents( PUBSUB_NAME, TOPIC_BULK, "application/json", @@ -537,24 +540,74 @@ public void testPubSubTTLMetadata() throws Exception { } } + @Test + @DisplayName("Should deliver published messages via the @BulkSubscribe handler with all SUCCESS") + public void testPubSubBulkSubscribe() throws Exception { + // Send a batch of messages on the bulk-subscribe topic. + try (DaprClient client = DaprClientFactory.createDaprClientBuilder(DAPR_CONTAINER).build()) { + for (int i = 0; i < NUM_MESSAGES; i++) { + String message = String.format("This is message #%d on topic %s", i, BULK_SUB_TOPIC_NAME); + client.publishEvent(PUBSUB_NAME, BULK_SUB_TOPIC_NAME, message).block(); + LOG.info("Published message: '{}' to topic '{}' pubsub_name '{}'", message, BULK_SUB_TOPIC_NAME, PUBSUB_NAME); + } + } + + // Give the subscriber a chance to receive the messages. + Thread.sleep(5000); + + try (DaprClient client = DaprClientFactory.createDaprClientBuilder(DAPR_CONTAINER).build()) { + callWithRetry(() -> { + LOG.info("Checking results for topic " + BULK_SUB_TOPIC_NAME); + + // The subscriber returns BulkSubscribeAppResponse objects, but the controller's + // generic List response type erases at runtime, so Jackson deserializes each + // element to LinkedHashMap. Take the list as List and convertValue each item. + final List messages = client.invokeMethod( + PUBSUB_APP_ID, + "messages/" + BULK_SUB_TOPIC_NAME, + null, + HttpExtension.GET, + List.class).block(); + + assertNotNull(messages); + + // Bulk-subscribe batching is timing-dependent (depends on publish rate vs the + // controller's maxAwaitDurationMs window). With pubsub.in-memory and synchronous + // publishes the runtime may deliver each message in its own batch. Assert on the + // contract that matters: every published message reached the bulk endpoint with + // SUCCESS, regardless of how the runtime batched them. + int totalEntries = 0; + for (Object rawResponse : messages) { + BulkSubscribeAppResponse response = OBJECT_MAPPER.convertValue( + rawResponse, BulkSubscribeAppResponse.class); + for (BulkSubscribeAppResponseEntry entry : response.getStatuses()) { + assertThat(entry.getStatus()).isEqualTo(BulkSubscribeAppResponseStatus.SUCCESS); + totalEntries++; + } + } + assertThat(totalEntries).isEqualTo(NUM_MESSAGES); + }, 2000); + } + } + @Test @DisplayName("Should publish long values") public void testLongValues() throws Exception { Random random = new Random(590518626939830271L); - Set values = new HashSet<>(); - values.add(new PubSubIT.ConvertToLong().setVal(590518626939830271L)); - PubSubIT.ConvertToLong val; + Set values = new HashSet<>(); + values.add(new ConvertToLong().setVal(590518626939830271L)); + ConvertToLong val; for (int i = 0; i < NUM_MESSAGES - 1; i++) { do { - val = new PubSubIT.ConvertToLong().setVal(random.nextLong()); + val = new ConvertToLong().setVal(random.nextLong()); } while (values.contains(val)); values.add(val); } - Iterator valuesIt = values.iterator(); + Iterator valuesIt = values.iterator(); try (DaprClient client = DaprClientFactory.createDaprClientBuilder(DAPR_CONTAINER).build()) { for (int i = 0; i < NUM_MESSAGES; i++) { - PubSubIT.ConvertToLong value = valuesIt.next(); + ConvertToLong value = valuesIt.next(); LOG.info("The long value sent " + value.getValue()); //Publishing messages client.publishEvent( @@ -573,17 +626,17 @@ public void testLongValues() throws Exception { } } - Set actual = new HashSet<>(); + Set actual = new HashSet<>(); try (DaprClient client = DaprClientFactory.createDaprClientBuilder(DAPR_CONTAINER).build()) { callWithRetry(() -> { LOG.info("Checking results for topic " + LONG_TOPIC_NAME); - final List> messages = client.invokeMethod( + final List> messages = client.invokeMethod( PUBSUB_APP_ID, "messages/testinglongvalues", null, HttpExtension.GET, CLOUD_EVENT_LONG_LIST_TYPE_REF).block(); assertNotNull(messages); - for (CloudEvent message : messages) { + for (CloudEvent message : messages) { actual.add(message.getData()); } assertThat(values).containsAll(actual); diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/http/MyObject.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/http/MyObject.java new file mode 100644 index 0000000000..019c537727 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/http/MyObject.java @@ -0,0 +1,25 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ +package io.dapr.it.testcontainers.pubsub.http; + +public class MyObject { + private String id; + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/http/SubscriberController.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/http/SubscriberController.java index 30e9204018..1428d85788 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/http/SubscriberController.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/http/SubscriberController.java @@ -21,7 +21,6 @@ import io.dapr.client.domain.BulkSubscribeMessage; import io.dapr.client.domain.BulkSubscribeMessageEntry; import io.dapr.client.domain.CloudEvent; -import io.dapr.it.pubsub.http.PubSubIT; import io.dapr.springboot.annotations.BulkSubscribe; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -151,7 +150,7 @@ public Mono handleMessageV3(@RequestBody(required = false) CloudEvent enve @Topic(name = "typedtestingtopic", pubsubName = "pubsub") @PostMapping(path = "/route1b") - public Mono handleMessageTyped(@RequestBody(required = false) CloudEvent envelope) { + public Mono handleMessageTyped(@RequestBody(required = false) CloudEvent envelope) { return Mono.fromRunnable(() -> { try { String id = envelope.getData() == null ? "" : envelope.getData().getId(); @@ -208,7 +207,7 @@ public Mono handleMessageTTLTopic(@RequestBody(required = false) CloudEven @Topic(name = "testinglongvalues", pubsubName = "pubsub") @PostMapping(path = "/testinglongvalues") - public Mono handleMessageLongValues(@RequestBody(required = false) CloudEvent cloudEvent) { + public Mono handleMessageLongValues(@RequestBody(required = false) CloudEvent cloudEvent) { return Mono.fromRunnable(() -> { try { Long message = cloudEvent.getData().getValue(); diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/outbox/DaprPubSubOutboxIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/outbox/DaprPubSubOutboxIT.java index d4139bcf91..b053b5c070 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/outbox/DaprPubSubOutboxIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/outbox/DaprPubSubOutboxIT.java @@ -24,6 +24,7 @@ import io.dapr.testcontainers.wait.strategy.DaprWait; import org.assertj.core.api.Assertions; import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -36,7 +37,6 @@ import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.containers.Network; -import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import java.time.Duration; @@ -47,7 +47,7 @@ import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG; -@Disabled("Unclear why this test is failing intermittently in CI") +@Disabled("Outbox event delivery via in-memory pubsub is unreliable — suspected Dapr runtime issue. See #1603") @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = { @@ -70,7 +70,6 @@ public class DaprPubSubOutboxIT { private static final String TOPIC_PRODUCT_CREATED = "product.created"; private static final String STATE_STORE_NAME = "kvstore"; - @Container private static final DaprContainer DAPR_CONTAINER = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) .withAppName(PUBSUB_APP_ID) .withNetwork(DAPR_NETWORK) @@ -87,21 +86,20 @@ public class DaprPubSubOutboxIT { @Autowired private ProductWebhookController productWebhookController; - /** - * Expose the Dapr ports to the host. - * - * @param registry the dynamic property registry - */ @DynamicPropertySource static void daprProperties(DynamicPropertyRegistry registry) { - registry.add("dapr.http.endpoint", DAPR_CONTAINER::getHttpEndpoint); - registry.add("dapr.grpc.endpoint", DAPR_CONTAINER::getGrpcEndpoint); registry.add("server.port", () -> PORT); } @BeforeAll - public static void beforeAll(){ + public static void beforeAll() { org.testcontainers.Testcontainers.exposeHostPorts(PORT); + DAPR_CONTAINER.start(); + } + + @AfterAll + public static void afterAll() { + DAPR_CONTAINER.stop(); } @BeforeEach @@ -128,7 +126,8 @@ public void shouldPublishUsingOutbox() throws Exception { client.executeStateTransaction(transactionRequest).block(); - Awaitility.await().atMost(Duration.ofSeconds(10)) + Awaitility.await().atMost(Duration.ofSeconds(60)) + .pollInterval(Duration.ofMillis(500)) .ignoreExceptions() .untilAsserted(() -> Assertions.assertThat(productWebhookController.getEventList()).isNotEmpty()); } diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/stream/DaprPubSubStreamIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/stream/DaprPubSubStreamIT.java new file mode 100644 index 0000000000..c10aef36c9 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/pubsub/stream/DaprPubSubStreamIT.java @@ -0,0 +1,295 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ +package io.dapr.it.testcontainers.pubsub.stream; + +import io.dapr.client.DaprClient; +import io.dapr.client.DaprPreviewClient; +import io.dapr.client.SubscriptionListener; +import io.dapr.client.domain.CloudEvent; +import io.dapr.it.testcontainers.DaprClientFactory; +import io.dapr.testcontainers.Component; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.utils.TypeRef; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import reactor.core.publisher.Mono; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static io.dapr.it.Retry.callWithRetry; +import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Testcontainers +@Tag("testcontainers") +public class DaprPubSubStreamIT { + + private static final int NUM_MESSAGES = 100; + private static final String TOPIC_NAME = "stream-topic"; + private static final String TOPIC_NAME_FLUX = "stream-topic-flux"; + private static final String TOPIC_NAME_CLOUDEVENT = "stream-topic-cloudevent"; + private static final String TOPIC_NAME_RAWPAYLOAD = "stream-topic-rawpayload"; + private static final String TOPIC_NAME_DLQ = "stream-topic-dlq"; + private static final String TOPIC_NAME_DLQ_DEADLETTER = "stream-topic-dlq-deadletter"; + private static final String PUBSUB_NAME = "pubsub"; + + @Container + private static final DaprContainer DAPR_CONTAINER = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) + .withAppName("pubsub-stream-app") + .withComponent(new Component(PUBSUB_NAME, "pubsub.in-memory", "v1", Collections.emptyMap())); + + private void waitForSubscription(DaprClient client, String topic, CountDownLatch latch) throws InterruptedException { + callWithRetry(() -> { + client.publishEvent(PUBSUB_NAME, topic, "probe").block(); + try { + assertTrue(latch.await(500, TimeUnit.MILLISECONDS), "Subscription not ready for " + topic); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + }, 60000); + } + + @Test + public void testPubSub() throws Exception { + var runId = UUID.randomUUID().toString(); + try (DaprClient client = DaprClientFactory.createDaprClientBuilder(DAPR_CONTAINER).build(); + DaprPreviewClient previewClient = DaprClientFactory.createDaprClientBuilder(DAPR_CONTAINER) + .buildPreviewClient()) { + + Set received = Collections.synchronizedSet(new HashSet<>()); + CountDownLatch ready = new CountDownLatch(1); + + var listener = new SubscriptionListener() { + @Override + public Mono onEvent(CloudEvent event) { + return Mono.fromCallable(() -> { + ready.countDown(); + if (event.getData().contains(runId)) { + received.add(event.getId()); + return Status.SUCCESS; + } + return Status.DROP; + }); + } + + @Override + public void onError(RuntimeException exception) { + } + }; + + try (var subscription = previewClient.subscribeToEvents(PUBSUB_NAME, TOPIC_NAME, listener, TypeRef.STRING)) { + waitForSubscription(client, TOPIC_NAME, ready); + + for (int i = 0; i < NUM_MESSAGES; i++) { + String message = String.format("This is message #%d on topic %s for run %s", i, TOPIC_NAME, runId); + client.publishEvent(PUBSUB_NAME, TOPIC_NAME, message).block(); + } + + callWithRetry(() -> { + assertEquals(NUM_MESSAGES, received.size(), + String.format("Got %d/%d messages for topic %s", received.size(), NUM_MESSAGES, TOPIC_NAME)); + }, 120000); + } + } + } + + @Test + public void testPubSubFlux() throws Exception { + var runId = UUID.randomUUID().toString(); + try (DaprClient client = DaprClientFactory.createDaprClientBuilder(DAPR_CONTAINER).build(); + DaprPreviewClient previewClient = DaprClientFactory.createDaprClientBuilder(DAPR_CONTAINER) + .buildPreviewClient()) { + + Set received = Collections.synchronizedSet(new HashSet<>()); + CountDownLatch ready = new CountDownLatch(1); + + var disposable = previewClient.subscribeToTopic(PUBSUB_NAME, TOPIC_NAME_FLUX, TypeRef.STRING) + .doOnNext(rawMessage -> { + ready.countDown(); + if (rawMessage.contains(runId)) { + received.add(rawMessage); + } + }) + .subscribe(); + + waitForSubscription(client, TOPIC_NAME_FLUX, ready); + + for (int i = 0; i < NUM_MESSAGES; i++) { + String message = String.format("Flux message #%d for run %s", i, runId); + client.publishEvent(PUBSUB_NAME, TOPIC_NAME_FLUX, message).block(); + } + + callWithRetry(() -> { + assertEquals(NUM_MESSAGES, received.size(), + String.format("Got %d/%d flux messages for topic %s", received.size(), NUM_MESSAGES, TOPIC_NAME_FLUX)); + }, 60000); + + disposable.dispose(); + } + } + + @Test + public void testPubSubCloudEvent() throws Exception { + var runId = UUID.randomUUID().toString(); + try (DaprClient client = DaprClientFactory.createDaprClientBuilder(DAPR_CONTAINER).build(); + DaprPreviewClient previewClient = DaprClientFactory.createDaprClientBuilder(DAPR_CONTAINER) + .buildPreviewClient()) { + + Set received = Collections.synchronizedSet(new HashSet<>()); + CountDownLatch ready = new CountDownLatch(1); + + var disposable = previewClient.subscribeToTopic( + PUBSUB_NAME, TOPIC_NAME_CLOUDEVENT, new TypeRef>() {}) + .doOnNext(cloudEvent -> { + ready.countDown(); + if (cloudEvent.getData() != null && cloudEvent.getData().contains(runId)) { + received.add(cloudEvent.getId()); + } + }) + .subscribe(); + + waitForSubscription(client, TOPIC_NAME_CLOUDEVENT, ready); + + for (int i = 0; i < NUM_MESSAGES; i++) { + String message = String.format("CloudEvent message #%d for run %s", i, runId); + client.publishEvent(PUBSUB_NAME, TOPIC_NAME_CLOUDEVENT, message).block(); + } + + callWithRetry(() -> { + assertEquals(NUM_MESSAGES, received.size(), + String.format("Got %d/%d CloudEvent messages for topic %s", + received.size(), NUM_MESSAGES, TOPIC_NAME_CLOUDEVENT)); + }, 60000); + + disposable.dispose(); + } + } + + @Disabled("Streaming subscription with rawPayload metadata not supported by pubsub.in-memory") + @Test + public void testPubSubRawPayload() throws Exception { + var runId = UUID.randomUUID().toString(); + try (DaprClient client = DaprClientFactory.createDaprClientBuilder(DAPR_CONTAINER).build(); + DaprPreviewClient previewClient = DaprClientFactory.createDaprClientBuilder(DAPR_CONTAINER) + .buildPreviewClient()) { + + Set received = Collections.synchronizedSet(new HashSet<>()); + Map metadata = Map.of("rawPayload", "true"); + CountDownLatch ready = new CountDownLatch(1); + + var disposable = previewClient.subscribeToTopic(PUBSUB_NAME, TOPIC_NAME_RAWPAYLOAD, TypeRef.STRING, metadata) + .doOnNext(rawMessage -> { + ready.countDown(); + if (rawMessage.contains(runId)) { + received.add(rawMessage); + } + }) + .subscribe(); + + waitForSubscription(client, TOPIC_NAME_RAWPAYLOAD, ready); + + for (int i = 0; i < NUM_MESSAGES; i++) { + String message = String.format("RawPayload message #%d for run %s", i, runId); + client.publishEvent(PUBSUB_NAME, TOPIC_NAME_RAWPAYLOAD, message, Map.of("rawPayload", "true")).block(); + } + + callWithRetry(() -> { + assertEquals(NUM_MESSAGES, received.size(), + String.format("Got %d/%d raw payload messages for topic %s", + received.size(), NUM_MESSAGES, TOPIC_NAME_RAWPAYLOAD)); + }, 60000); + + disposable.dispose(); + } + } + + /** + * Streaming subscription should forward DROPped messages to the configured + * dead-letter topic. Ports the {@code testPubSubDeadLetterTopic} test from + * the legacy {@code PubSubStreamIT} into the Testcontainers harness. + */ + @Test + public void testPubSubDeadLetterTopic() throws Exception { + var runId = UUID.randomUUID().toString(); + try (DaprClient client = DaprClientFactory.createDaprClientBuilder(DAPR_CONTAINER).build(); + DaprPreviewClient previewClient = DaprClientFactory.createDaprClientBuilder(DAPR_CONTAINER) + .buildPreviewClient()) { + + Set deadLetterMessageIds = Collections.synchronizedSet(new HashSet<>()); + CountDownLatch deadLetterReady = new CountDownLatch(1); + CountDownLatch mainReady = new CountDownLatch(1); + + // Subscribe to the dead-letter topic first so we don't miss any messages. + var deadLetterListener = new SubscriptionListener() { + @Override + public Mono onEvent(CloudEvent event) { + deadLetterReady.countDown(); + if (event.getData() != null && event.getData().contains(runId)) { + deadLetterMessageIds.add(event.getId()); + } + return Mono.just(Status.SUCCESS); + } + + @Override + public void onError(RuntimeException exception) { + } + }; + + // Always-DROP listener on the main topic; daprd should forward each dropped + // message to the dead-letter topic. + var mainListener = new SubscriptionListener() { + @Override + public Mono onEvent(CloudEvent event) { + mainReady.countDown(); + return Mono.just(Status.DROP); + } + + @Override + public void onError(RuntimeException exception) { + } + }; + + try (var deadLetterSubscription = previewClient.subscribeToEvents( + PUBSUB_NAME, TOPIC_NAME_DLQ_DEADLETTER, deadLetterListener, TypeRef.STRING); + var mainSubscription = previewClient.subscribeToEvents( + PUBSUB_NAME, TOPIC_NAME_DLQ, TOPIC_NAME_DLQ_DEADLETTER, mainListener, TypeRef.STRING)) { + + waitForSubscription(client, TOPIC_NAME_DLQ_DEADLETTER, deadLetterReady); + waitForSubscription(client, TOPIC_NAME_DLQ, mainReady); + + for (int i = 0; i < NUM_MESSAGES; i++) { + String message = String.format("DLQ message #%d for run %s", i, runId); + client.publishEvent(PUBSUB_NAME, TOPIC_NAME_DLQ, message).block(); + } + + callWithRetry(() -> { + assertEquals(NUM_MESSAGES, deadLetterMessageIds.size(), + String.format("Got %d/%d dead-letter messages on topic %s", + deadLetterMessageIds.size(), NUM_MESSAGES, TOPIC_NAME_DLQ_DEADLETTER)); + }, 120000); + } + } + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/secrets/DaprSecretsIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/secrets/DaprSecretsIT.java new file mode 100644 index 0000000000..3f71938286 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/secrets/DaprSecretsIT.java @@ -0,0 +1,148 @@ +/* + * Copyright 2024 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.dapr.it.testcontainers.secrets; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dapr.client.DaprClient; +import io.dapr.client.DaprClientBuilder; +import io.dapr.config.Properties; +import io.dapr.testcontainers.Component; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.testcontainers.DaprLogLevel; +import io.dapr.testcontainers.MetadataEntry; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.Network; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Integration tests for the Dapr Secrets API using testcontainers. + */ +@Disabled("Needs investigation: DaprContainer file mounting with secretstores.local.file") +@Testcontainers +@Tag("testcontainers") +public class DaprSecretsIT { + + private static final String SECRETS_STORE_NAME = "localSecretStore"; + private static final String CONTAINER_SECRETS_PATH = "/tmp/secrets.json"; + private static final ObjectMapper JSON_SERIALIZER = new ObjectMapper(); + + private static final String KEY1 = "movie"; + private static final String KEY2 = "person"; + + private static final Network DAPR_NETWORK = Network.newNetwork(); + + private static DaprClient daprClient; + + private static final String SECRETS_JSON = createSecretsJson(); + + @Container + private static final DaprContainer DAPR_CONTAINER = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) + .withAppName("secrets-test-app") + .withNetwork(DAPR_NETWORK) + .withDaprLogLevel(DaprLogLevel.DEBUG) + .withComponent(new Component( + SECRETS_STORE_NAME, + "secretstores.local.file", + "v1", + List.of(new MetadataEntry("secretsFile", CONTAINER_SECRETS_PATH)) + )) + .withCopyToContainer(Transferable.of(SECRETS_JSON), CONTAINER_SECRETS_PATH) + .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())); + + private static String createSecretsJson() { + try { + Map secrets = new HashMap<>(); + Map movieSecret = new HashMap<>(); + movieSecret.put("title", "The Metrics IV"); + movieSecret.put("year", "2020"); + secrets.put(KEY1, movieSecret); + + Map personSecret = new HashMap<>(); + personSecret.put("name", "Jon Doe"); + secrets.put(KEY2, personSecret); + + return JSON_SERIALIZER.writeValueAsString(secrets); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @BeforeAll + static void setUp() { + daprClient = new DaprClientBuilder() + .withPropertyOverride(Properties.HTTP_ENDPOINT, DAPR_CONTAINER.getHttpEndpoint()) + .withPropertyOverride(Properties.GRPC_ENDPOINT, DAPR_CONTAINER.getGrpcEndpoint()) + .build(); + } + + @AfterAll + static void tearDown() throws Exception { + if (daprClient != null) { + daprClient.close(); + } + } + + @Test + public void testGetSecret() { + Map data = daprClient.getSecret(SECRETS_STORE_NAME, KEY1).block(); + + assertNotNull(data); + assertEquals(2, data.size()); + assertEquals("The Metrics IV", data.get("title")); + assertEquals("2020", data.get("year")); + } + + @Test + public void testGetBulkSecret() { + Map> data = daprClient.getBulkSecret(SECRETS_STORE_NAME).block(); + + assertNotNull(data); + assertTrue(data.size() >= 2); + assertEquals(2, data.get(KEY1).size()); + assertEquals("The Metrics IV", data.get(KEY1).get("title")); + assertEquals("2020", data.get(KEY1).get("year")); + assertEquals(1, data.get(KEY2).size()); + assertEquals("Jon Doe", data.get(KEY2).get("name")); + } + + @Test + public void testGetSecretKeyNotFound() { + assertThrows(RuntimeException.class, () -> + daprClient.getSecret(SECRETS_STORE_NAME, "unknownKey").block() + ); + } + + @Test + public void testGetSecretStoreNotFound() { + assertThrows(RuntimeException.class, () -> + daprClient.getSecret("unknownStore", "unknownKey").block() + ); + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/tracing/OpenTelemetry.java b/sdk-tests/src/test/java/io/dapr/it/tracing/OpenTelemetry.java index 08a6ea88f7..649a18f9f9 100644 --- a/sdk-tests/src/test/java/io/dapr/it/tracing/OpenTelemetry.java +++ b/sdk-tests/src/test/java/io/dapr/it/tracing/OpenTelemetry.java @@ -32,6 +32,27 @@ public class OpenTelemetry { private static final String ENDPOINT_V2_SPANS = "/api/v2/spans"; + /** + * Creates an opentelemetry instance using an explicit Zipkin endpoint URL. + * Skips the local Zipkin readiness probe — callers (e.g., Testcontainers-backed ITs) + * are responsible for ensuring the Zipkin endpoint is reachable before invocation. + * @param serviceName Name of the service in Zipkin (informational; not consumed here). + * @param zipkinEndpointUrl Full Zipkin spans endpoint URL (e.g., http://host:port/api/v2/spans). + * @return OpenTelemetry. + */ + public static io.opentelemetry.api.OpenTelemetry createOpenTelemetry(String serviceName, String zipkinEndpointUrl) { + ZipkinSpanExporter zipkinExporter = ZipkinSpanExporter.builder().setEndpoint(zipkinEndpointUrl).build(); + + SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(zipkinExporter)) + .build(); + + return OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) + .build(); + } + /** * Creates an opentelemetry instance. * @param serviceName Name of the service in Zipkin diff --git a/sdk-tests/src/test/java/io/dapr/it/tracing/Validation.java b/sdk-tests/src/test/java/io/dapr/it/tracing/Validation.java index 0f0adf3fed..52884f72bf 100644 --- a/sdk-tests/src/test/java/io/dapr/it/tracing/Validation.java +++ b/sdk-tests/src/test/java/io/dapr/it/tracing/Validation.java @@ -49,6 +49,27 @@ public final class Validation { public static final String JSONPATH_SLEEP_SPAN_ID = "$..[?(@.parentId=='%s' && @.duration > 1000000 && @.name=='%s')]['id']"; + public static void validate(String spanName, String sleepSpanName, String zipkinTracesUrl) throws Exception { + // Must wait for some time to make sure Zipkin receives all spans. + Thread.sleep(10000); + + HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(URI.create(zipkinTracesUrl)) + .build(); + + HttpResponse response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + DocumentContext documentContext = JsonPath.parse(response.body()); + String mainSpanId = readOne(documentContext, String.format(JSONPATH_MAIN_SPAN_ID, spanName)).toString(); + + assertNotNull(mainSpanId); + + String sleepSpanId = readOne(documentContext, String.format(JSONPATH_SLEEP_SPAN_ID, mainSpanId, sleepSpanName)) + .toString(); + + assertNotNull(sleepSpanId); + } + public static void validate(String spanName, String sleepSpanName) throws Exception { // Must wait for some time to make sure Zipkin receives all spans. Thread.sleep(10000); diff --git a/sdk-tests/src/test/java/io/dapr/it/tracing/grpc/TracingIT.java b/sdk-tests/src/test/java/io/dapr/it/tracing/grpc/TracingIT.java index 43f982eed4..523bfa762b 100644 --- a/sdk-tests/src/test/java/io/dapr/it/tracing/grpc/TracingIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/tracing/grpc/TracingIT.java @@ -1,18 +1,35 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + package io.dapr.it.tracing.grpc; import io.dapr.client.DaprClient; -import io.dapr.client.DaprClientBuilder; import io.dapr.client.domain.HttpExtension; import io.dapr.it.AppRun; -import io.dapr.it.BaseIT; -import io.dapr.it.DaprRun; +import io.dapr.it.containers.BaseContainerIT; +import io.dapr.it.containers.SharedTestInfra; import io.dapr.it.tracing.Validation; +import io.dapr.testcontainers.Configuration; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.testcontainers.DaprProtocol; +import io.dapr.testcontainers.TracingConfigurationSettings; +import io.dapr.testcontainers.ZipkinTracingConfigurationSettings; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Scope; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.util.UUID; @@ -22,37 +39,60 @@ import static io.dapr.it.tracing.OpenTelemetry.getReactorContext; @SuppressWarnings("deprecation") -public class TracingIT extends BaseIT { +public class TracingIT extends BaseContainerIT { + + private static final String APP_NAME = "tracing-grpc-it"; - /** - * Run of a Dapr application. - */ - private DaprRun daprRun = null; + private static DaprContainer dapr; + private static AppRun app; + private static String zipkinHostUrl; + private static String zipkinTracesUrl; - @BeforeEach - public void setup() throws Exception { - daprRun = startDaprApp( - TracingIT.class.getSimpleName() + "grpc", - Service.SUCCESS_MESSAGE, - Service.class, - AppRun.AppProtocol.GRPC, // appProtocol - 60000); + @BeforeAll + public static void setup() throws Exception { + SharedTestInfra.zipkin(); + String zipkinHost = SharedTestInfra.zipkin().getHost(); + int zipkinPort = SharedTestInfra.zipkin().getMappedPort(9411); + zipkinHostUrl = "http://" + zipkinHost + ":" + zipkinPort + "/api/v2/spans"; + zipkinTracesUrl = "http://" + zipkinHost + ":" + zipkinPort + "/api/v2/traces?limit=100"; - daprRun.waitForAppHealth(10000); + var pair = startAppAndAttach( + APP_NAME, + Service.class, + AppRun.AppProtocol.GRPC, + appPort -> { + DaprContainer d = daprBuilder(APP_NAME) + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal") + .withAppProtocol(DaprProtocol.GRPC) + .withConfiguration(new Configuration( + "tracing", + new TracingConfigurationSettings( + "1", + true, + null, + new ZipkinTracingConfigurationSettings(SharedTestInfra.zipkinInternalEndpoint()) + ), + null + )); + return d; + }); + dapr = pair.dapr(); + app = pair.app(); } @Test public void testInvoke() throws Exception { - OpenTelemetry openTelemetry = createOpenTelemetry("service over grpc"); + OpenTelemetry openTelemetry = createOpenTelemetry("service over grpc", zipkinHostUrl); Tracer tracer = openTelemetry.getTracer("grpc integration test tracer"); String spanName = UUID.randomUUID().toString(); Span span = tracer.spanBuilder(spanName).setSpanKind(SpanKind.CLIENT).startSpan(); - try (DaprClient client = daprRun.newDaprClientBuilder().build()) { + try (DaprClient client = newDaprClient(dapr)) { client.waitForSidecar(10000).block(); try (Scope scope = span.makeCurrent()) { SleepRequest req = SleepRequest.newBuilder().setSeconds(1).build(); - client.invokeMethod(daprRun.getAppName(), "sleepOverGRPC", req.toByteArray(), HttpExtension.POST) + client.invokeMethod(APP_NAME, "sleepOverGRPC", req.toByteArray(), HttpExtension.POST) .contextWrite(getReactorContext(openTelemetry)) .block(); } @@ -60,6 +100,6 @@ public void testInvoke() throws Exception { span.end(); - Validation.validate(spanName, "calllocal/tracingitgrpc-service/sleepovergrpc"); + Validation.validate(spanName, "calllocal/" + APP_NAME + "/sleepovergrpc", zipkinTracesUrl); } } diff --git a/sdk-tests/src/test/java/io/dapr/it/tracing/http/TracingIT.java b/sdk-tests/src/test/java/io/dapr/it/tracing/http/TracingIT.java index b133ce7213..ec1eb77e79 100644 --- a/sdk-tests/src/test/java/io/dapr/it/tracing/http/TracingIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/tracing/http/TracingIT.java @@ -1,17 +1,34 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + package io.dapr.it.tracing.http; import io.dapr.client.DaprClient; -import io.dapr.client.DaprClientBuilder; import io.dapr.client.domain.HttpExtension; -import io.dapr.it.BaseIT; -import io.dapr.it.DaprRun; +import io.dapr.it.AppRun; +import io.dapr.it.containers.BaseContainerIT; +import io.dapr.it.containers.SharedTestInfra; import io.dapr.it.tracing.Validation; +import io.dapr.testcontainers.Configuration; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.testcontainers.TracingConfigurationSettings; +import io.dapr.testcontainers.ZipkinTracingConfigurationSettings; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Scope; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.util.UUID; @@ -20,21 +37,46 @@ import static io.dapr.it.tracing.OpenTelemetry.getReactorContext; @SuppressWarnings("deprecation") -public class TracingIT extends BaseIT { +public class TracingIT extends BaseContainerIT { + + private static final String APP_NAME = "tracing-http-it"; - /** - * Run of a Dapr application. - */ - private DaprRun daprRun = null; + private static DaprContainer dapr; + private static AppRun app; + private static String zipkinHostUrl; + private static String zipkinTracesUrl; - @BeforeEach - public void setup() throws Exception { - daprRun = startDaprApp( - TracingIT.class.getSimpleName() + "http", - Service.SUCCESS_MESSAGE, - Service.class, - true, - 30000); + @BeforeAll + public static void setup() throws Exception { + // Start Zipkin first so we can wire its endpoint into both Dapr and the test's OpenTelemetry SDK. + SharedTestInfra.zipkin(); + String zipkinHost = SharedTestInfra.zipkin().getHost(); + int zipkinPort = SharedTestInfra.zipkin().getMappedPort(9411); + zipkinHostUrl = "http://" + zipkinHost + ":" + zipkinPort + "/api/v2/spans"; + zipkinTracesUrl = "http://" + zipkinHost + ":" + zipkinPort + "/api/v2/traces?limit=100"; + + var pair = startAppAndAttach( + APP_NAME, + Service.class, + AppRun.AppProtocol.HTTP, + appPort -> { + DaprContainer d = daprBuilder(APP_NAME) + .withAppPort(appPort) + .withAppChannelAddress("host.testcontainers.internal") + .withConfiguration(new Configuration( + "tracing", + new TracingConfigurationSettings( + "1", + true, + null, + new ZipkinTracingConfigurationSettings(SharedTestInfra.zipkinInternalEndpoint()) + ), + null + )); + return d; + }); + dapr = pair.dapr(); + app = pair.app(); // Wait since service might be ready even after port is available. Thread.sleep(2000); @@ -42,15 +84,15 @@ public void setup() throws Exception { @Test public void testInvoke() throws Exception { - OpenTelemetry openTelemetry = createOpenTelemetry(OpenTelemetryConfig.SERVICE_NAME); + OpenTelemetry openTelemetry = createOpenTelemetry(OpenTelemetryConfig.SERVICE_NAME, zipkinHostUrl); Tracer tracer = openTelemetry.getTracer(OpenTelemetryConfig.TRACER_NAME); String spanName = UUID.randomUUID().toString(); Span span = tracer.spanBuilder(spanName).setSpanKind(SpanKind.CLIENT).startSpan(); - try (DaprClient client = daprRun.newDaprClientBuilder().build()) { + try (DaprClient client = newDaprClient(dapr)) { client.waitForSidecar(10000).block(); try (Scope scope = span.makeCurrent()) { - client.invokeMethod(daprRun.getAppName(), "sleep", 1, HttpExtension.POST) + client.invokeMethod(APP_NAME, "sleep", 1, HttpExtension.POST) .contextWrite(getReactorContext(openTelemetry)) .block(); } @@ -58,7 +100,6 @@ public void testInvoke() throws Exception { span.end(); - Validation.validate(spanName, "calllocal/tracingithttp-service/sleep"); + Validation.validate(spanName, "calllocal/" + APP_NAME + "/sleep", zipkinTracesUrl); } - } diff --git a/sdk/src/main/java/io/dapr/client/Subscription.java b/sdk/src/main/java/io/dapr/client/Subscription.java index 2f85128474..f42c04de81 100644 --- a/sdk/src/main/java/io/dapr/client/Subscription.java +++ b/sdk/src/main/java/io/dapr/client/Subscription.java @@ -88,6 +88,7 @@ public class Subscription implements Closeable { }); this.receiver = new Thread(() -> { + long backoffMs = 1000L; while (running.get()) { var stream = asyncStub.subscribeTopicEventsAlpha1(new StreamObserver<>() { @Override @@ -124,6 +125,7 @@ public void onNext(DaprPubsubProtos.SubscribeTopicEventsResponseAlpha1 topicEven @Override public void onError(Throwable throwable) { listener.onError(DaprException.propagate(throwable)); + receiverStateChange.release(); } @Override @@ -142,6 +144,17 @@ public void onCompleted() { Thread.currentThread().interrupt(); running.set(false); } + + if (running.get()) { + try { + Thread.sleep(backoffMs); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + running.set(false); + } + // Double the backoff for the next reconnect, capped at 30s. + backoffMs = Math.min(backoffMs * 2, 30_000L); + } } }); } @@ -186,6 +199,10 @@ void start() { public void close() { running.set(false); receiverStateChange.release(); + // Interrupt both threads so that any in-flight Thread.sleep (e.g., the + // receiver's reconnect backoff, up to 30s) returns immediately instead + // of blocking shutdown. + this.receiver.interrupt(); this.acker.interrupt(); } diff --git a/spring-boot-4-sdk-tests/src/test/java/io/dapr/it/springboot4/testcontainers/jobs/DaprJobsIT.java b/spring-boot-4-sdk-tests/src/test/java/io/dapr/it/springboot4/testcontainers/jobs/DaprJobsIT.java index 686c7eb01f..92f2d09aa5 100644 --- a/spring-boot-4-sdk-tests/src/test/java/io/dapr/it/springboot4/testcontainers/jobs/DaprJobsIT.java +++ b/spring-boot-4-sdk-tests/src/test/java/io/dapr/it/springboot4/testcontainers/jobs/DaprJobsIT.java @@ -25,6 +25,7 @@ import io.dapr.it.springboot4.testcontainers.DaprClientConfiguration; import io.dapr.testcontainers.DaprContainer; import io.dapr.testcontainers.DaprLogLevel; +import org.awaitility.Awaitility; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -43,9 +44,11 @@ import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.Random; +import java.util.UUID; import static io.dapr.it.springboot4.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; @SpringBootTest( webEnvironment = WebEnvironment.RANDOM_PORT, @@ -93,63 +96,77 @@ public void setUp(){ @Test public void testJobScheduleCreationWithDueTime() { + String jobName = "Job-" + UUID.randomUUID().toString().substring(0, 8); DateTimeFormatter iso8601Formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") .withZone(ZoneOffset.UTC); - Instant currentTime = Instant.now(); - daprClient.scheduleJob(new ScheduleJobRequest("Job", currentTime).setOverwrite(true)).block(); + Instant currentTime = Instant.now().plus(10, ChronoUnit.MINUTES); + daprClient.scheduleJob(new ScheduleJobRequest(jobName, currentTime)).block(); - GetJobResponse getJobResponse = - daprClient.getJob(new GetJobRequest("Job")).block(); + GetJobResponse getJobResponse = Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofMillis(300)) + .ignoreExceptions() + .until(() -> daprClient.getJob(new GetJobRequest(jobName)).block(), r -> r != null); - daprClient.deleteJob(new DeleteJobRequest("Job")).block(); + daprClient.deleteJob(new DeleteJobRequest(jobName)).block(); + assertNotNull(getJobResponse); assertEquals(iso8601Formatter.format(currentTime), getJobResponse.getDueTime().toString()); - assertEquals("Job", getJobResponse.getName()); + assertEquals(jobName, getJobResponse.getName()); } @Test public void testJobScheduleCreationWithSchedule() { + String jobName = "Job-" + UUID.randomUUID().toString().substring(0, 8); DateTimeFormatter iso8601Formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") .withZone(ZoneOffset.UTC); - Instant currentTime = Instant.now(); - daprClient.scheduleJob(new ScheduleJobRequest("Job", JobSchedule.hourly()) - .setDueTime(currentTime).setOverwrite(true)).block(); + Instant currentTime = Instant.now().plus(10, ChronoUnit.MINUTES); + daprClient.scheduleJob(new ScheduleJobRequest(jobName, JobSchedule.hourly()) + .setDueTime(currentTime)).block(); - GetJobResponse getJobResponse = - daprClient.getJob(new GetJobRequest("Job")).block(); + GetJobResponse getJobResponse = Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofMillis(300)) + .ignoreExceptions() + .until(() -> daprClient.getJob(new GetJobRequest(jobName)).block(), r -> r != null); - daprClient.deleteJob(new DeleteJobRequest("Job")).block(); + daprClient.deleteJob(new DeleteJobRequest(jobName)).block(); + assertNotNull(getJobResponse); assertEquals(iso8601Formatter.format(currentTime), getJobResponse.getDueTime().toString()); assertEquals(JobSchedule.hourly().getExpression(), getJobResponse.getSchedule().getExpression()); - assertEquals("Job", getJobResponse.getName()); + assertEquals(jobName, getJobResponse.getName()); } @Test public void testJobScheduleCreationWithAllParameters() { - Instant currentTime = Instant.now(); + String jobName = "Job-" + UUID.randomUUID().toString().substring(0, 8); + Instant currentTime = Instant.now().plus(10, ChronoUnit.MINUTES); DateTimeFormatter iso8601Formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") .withZone(ZoneOffset.UTC); String cronExpression = "2 * 3 * * FRI"; - daprClient.scheduleJob(new ScheduleJobRequest("Job", currentTime) + daprClient.scheduleJob(new ScheduleJobRequest(jobName, currentTime) .setTtl(currentTime.plus(2, ChronoUnit.HOURS)) .setData("Job data".getBytes()) .setRepeat(3) - .setOverwrite(true) .setSchedule(JobSchedule.fromString(cronExpression))).block(); - GetJobResponse getJobResponse = - daprClient.getJob(new GetJobRequest("Job")).block(); + GetJobResponse getJobResponse = Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofMillis(300)) + .ignoreExceptions() + .until(() -> daprClient.getJob(new GetJobRequest(jobName)).block(), r -> r != null); - daprClient.deleteJob(new DeleteJobRequest("Job")).block(); + daprClient.deleteJob(new DeleteJobRequest(jobName)).block(); + assertNotNull(getJobResponse); assertEquals(iso8601Formatter.format(currentTime), getJobResponse.getDueTime().toString()); assertEquals("2 * 3 * * FRI", getJobResponse.getSchedule().getExpression()); - assertEquals("Job", getJobResponse.getName()); + assertEquals(jobName, getJobResponse.getName()); assertEquals(Integer.valueOf(3), getJobResponse.getRepeats()); assertEquals("Job data", new String(getJobResponse.getData())); assertEquals(iso8601Formatter.format(currentTime.plus(2, ChronoUnit.HOURS)), @@ -158,36 +175,38 @@ public void testJobScheduleCreationWithAllParameters() { @Test public void testJobScheduleCreationWithDropFailurePolicy() { - Instant currentTime = Instant.now(); - DateTimeFormatter iso8601Formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - .withZone(ZoneOffset.UTC); + String jobName = "Job-" + UUID.randomUUID().toString().substring(0, 8); + Instant currentTime = Instant.now().plus(10, ChronoUnit.MINUTES); String cronExpression = "2 * 3 * * FRI"; - daprClient.scheduleJob(new ScheduleJobRequest("Job", currentTime) + daprClient.scheduleJob(new ScheduleJobRequest(jobName, currentTime) .setTtl(currentTime.plus(2, ChronoUnit.HOURS)) .setData("Job data".getBytes()) .setRepeat(3) - .setFailurePolicy(new DropFailurePolicy()) + .setFailurePolicy(new DropFailurePolicy()) .setSchedule(JobSchedule.fromString(cronExpression))).block(); - GetJobResponse getJobResponse = - daprClient.getJob(new GetJobRequest("Job")).block(); + GetJobResponse getJobResponse = Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofMillis(300)) + .ignoreExceptions() + .until(() -> daprClient.getJob(new GetJobRequest(jobName)).block(), r -> r != null); - daprClient.deleteJob(new DeleteJobRequest("Job")).block(); + daprClient.deleteJob(new DeleteJobRequest(jobName)).block(); + assertNotNull(getJobResponse); assertEquals(FailurePolicyType.DROP, getJobResponse.getFailurePolicy().getFailurePolicyType()); } @Test public void testJobScheduleCreationWithConstantFailurePolicy() { - Instant currentTime = Instant.now(); - DateTimeFormatter iso8601Formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - .withZone(ZoneOffset.UTC); + String jobName = "Job-" + UUID.randomUUID().toString().substring(0, 8); + Instant currentTime = Instant.now().plus(10, ChronoUnit.MINUTES); String cronExpression = "2 * 3 * * FRI"; - daprClient.scheduleJob(new ScheduleJobRequest("Job", currentTime) + daprClient.scheduleJob(new ScheduleJobRequest(jobName, currentTime) .setTtl(currentTime.plus(2, ChronoUnit.HOURS)) .setData("Job data".getBytes()) .setRepeat(3) @@ -195,11 +214,15 @@ public void testJobScheduleCreationWithConstantFailurePolicy() { .setDurationBetweenRetries(Duration.of(10, ChronoUnit.SECONDS))) .setSchedule(JobSchedule.fromString(cronExpression))).block(); - GetJobResponse getJobResponse = - daprClient.getJob(new GetJobRequest("Job")).block(); + GetJobResponse getJobResponse = Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofMillis(300)) + .ignoreExceptions() + .until(() -> daprClient.getJob(new GetJobRequest(jobName)).block(), r -> r != null); - daprClient.deleteJob(new DeleteJobRequest("Job")).block(); + daprClient.deleteJob(new DeleteJobRequest(jobName)).block(); + assertNotNull(getJobResponse); ConstantFailurePolicy jobFailurePolicyConstant = (ConstantFailurePolicy) getJobResponse.getFailurePolicy(); assertEquals(FailurePolicyType.CONSTANT, getJobResponse.getFailurePolicy().getFailurePolicyType()); assertEquals(3, (int)jobFailurePolicyConstant.getMaxRetries()); @@ -209,17 +232,23 @@ public void testJobScheduleCreationWithConstantFailurePolicy() { @Test public void testDeleteJobRequest() { - Instant currentTime = Instant.now(); + String jobName = "Job-" + UUID.randomUUID().toString().substring(0, 8); + Instant currentTime = Instant.now().plus(10, ChronoUnit.MINUTES); String cronExpression = "2 * 3 * * FRI"; - daprClient.scheduleJob(new ScheduleJobRequest("Job", currentTime) + daprClient.scheduleJob(new ScheduleJobRequest(jobName, currentTime) .setTtl(currentTime.plus(2, ChronoUnit.HOURS)) .setData("Job data".getBytes()) .setRepeat(3) - .setOverwrite(true) .setSchedule(JobSchedule.fromString(cronExpression))).block(); - daprClient.deleteJob(new DeleteJobRequest("Job")).block(); + Awaitility.await() + .atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofMillis(300)) + .ignoreExceptions() + .until(() -> daprClient.getJob(new GetJobRequest(jobName)).block(), r -> r != null); + + daprClient.deleteJob(new DeleteJobRequest(jobName)).block(); } }