From 53769e5cd19a31ea8196209a6b6ee6cdd9ed303b Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 11 Aug 2026 12:42:05 -0400 Subject: [PATCH 1/4] Add envconfig support to test harness --- .github/workflows/ci.yml | 9 +- CONTRIBUTING.md | 18 +++ temporal-sdk/build.gradle | 3 +- temporal-testing/build.gradle | 2 + .../docker/RegisterTestNamespace.java | 5 +- .../ExternalServiceTestConfigurator.java | 141 +++++++++++++++--- .../ExternalServiceTestConfiguratorTest.java | 122 +++++++++++++++ 7 files changed, 275 insertions(+), 25 deletions(-) create mode 100644 temporal-testing/src/test/java/io/temporal/testing/internal/ExternalServiceTestConfiguratorTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c147f7e9a..cacd39c0e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,10 +139,17 @@ jobs: if: ${{ github.event.pull_request.head.repo.full_name == '' || github.event.pull_request.head.repo.full_name == 'temporalio/sdk-java' }} env: USER: unittest + TEMPORAL_TEST_ENV_CONFIG_SERVER: "true" + TEMPORAL_ADDRESS: sdk-ci.a2dd6.tmprl.cloud:7233 + TEMPORAL_NAMESPACE: sdk-ci.a2dd6 + TEMPORAL_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} TEMPORAL_CLIENT_CLOUD_NAMESPACE: sdk-ci.a2dd6 TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} TEMPORAL_CLIENT_CLOUD_API_VERSION: 2024-05-13-00 - run: ./gradlew --no-daemon :temporal-sdk:test --tests '*CloudOperationsClientTest' + run: | + ./gradlew --no-daemon :temporal-sdk:test \ + --tests '*CloudOperationsClientTest' \ + --tests 'io.temporal.client.functional.SignalTest.signalCompletedWorkflow' - name: Publish Test Report uses: mikepenz/action-junit-report@bccf2e31636835cf0874589931c4116687171386 # v6 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d5b8881133..1dc11851ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,6 +94,24 @@ Normal Gradle test filtering works, so a single dev-server-backed test can be ru Java 11 must be available to Gradle for these commands. +To run an SDK test against an externally managed server using the standard Temporal client +environment configuration, set `TEMPORAL_TEST_ENV_CONFIG_SERVER`. For example, the following runs +one Cloud-safe workflow test: + +```bash +TEMPORAL_TEST_ENV_CONFIG_SERVER=true \ +TEMPORAL_ADDRESS=your-namespace.tmprl.cloud:7233 \ +TEMPORAL_NAMESPACE=your-namespace \ +TEMPORAL_API_KEY=your-api-key \ +./gradlew :temporal-sdk:test \ + --tests 'io.temporal.client.functional.SignalTest.signalCompletedWorkflow' +``` + +The harness also supports the standard `TEMPORAL_CONFIG_FILE` and `TEMPORAL_PROFILE` variables. +Values from `TEMPORAL_ADDRESS`, `TEMPORAL_NAMESPACE`, `TEMPORAL_API_KEY`, `TEMPORAL_TLS_*`, and +`TEMPORAL_GRPC_META_*` override the selected profile. Envconfig mode connects to an existing server +and namespace; it does not create or register either one. + ## Things to Avoid Avoid changes that make review harder without improving the contribution: diff --git a/temporal-sdk/build.gradle b/temporal-sdk/build.gradle index d7b090e5b5..594a7660e0 100644 --- a/temporal-sdk/build.gradle +++ b/temporal-sdk/build.gradle @@ -25,6 +25,7 @@ dependencies { } testImplementation project(':temporal-testing') + testRuntimeOnly project(':temporal-envconfig') testImplementation "junit:junit:${junitVersion}" testImplementation "org.mockito:mockito-core:${mockitoVersion}" testImplementation 'pl.pragmatists:JUnitParams:1.1.1' @@ -287,4 +288,4 @@ testing { tasks.named('check') { dependsOn(testing.suites.jackson3Tests) dependsOn(testing.suites.virtualThreadTests) -} \ No newline at end of file +} diff --git a/temporal-testing/build.gradle b/temporal-testing/build.gradle index f9ca013456..10a7ddc6a1 100644 --- a/temporal-testing/build.gradle +++ b/temporal-testing/build.gradle @@ -16,6 +16,7 @@ java { dependencies { api project(':temporal-sdk') api project(':temporal-test-server') + compileOnly project(':temporal-envconfig') implementation 'org.apache.commons:commons-compress:1.28.0' @@ -32,6 +33,7 @@ dependencies { junit5Api 'org.junit.jupiter:junit-jupiter-api' testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter' + testRuntimeOnly project(':temporal-envconfig') testRuntimeOnly group: 'ch.qos.logback', name: 'logback-classic', version: "${logbackVersion}" } diff --git a/temporal-testing/src/main/java/io/temporal/internal/docker/RegisterTestNamespace.java b/temporal-testing/src/main/java/io/temporal/internal/docker/RegisterTestNamespace.java index c840adf55c..b43a9ca3cf 100644 --- a/temporal-testing/src/main/java/io/temporal/internal/docker/RegisterTestNamespace.java +++ b/temporal-testing/src/main/java/io/temporal/internal/docker/RegisterTestNamespace.java @@ -7,6 +7,7 @@ import io.temporal.api.workflowservice.v1.ListNamespacesRequest; import io.temporal.api.workflowservice.v1.ListNamespacesResponse; import io.temporal.api.workflowservice.v1.RegisterNamespaceRequest; +import io.temporal.internal.common.env.EnvironmentVariableUtils; import io.temporal.serviceclient.WorkflowServiceStubs; import io.temporal.serviceclient.WorkflowServiceStubsOptions; @@ -15,10 +16,12 @@ public class RegisterTestNamespace { public static final String NAMESPACE = "UnitTest"; private static final boolean useExternalService = Boolean.parseBoolean(System.getenv("USE_EXTERNAL_SERVICE")); + private static final boolean useEnvConfig = + EnvironmentVariableUtils.readBooleanFlag("TEMPORAL_TEST_ENV_CONFIG_SERVER"); private static final String serviceAddress = System.getenv("TEMPORAL_SERVICE_ADDRESS"); public static void main(String[] args) throws InterruptedException { - if (!useExternalService) { + if (!useExternalService || useEnvConfig) { return; } diff --git a/temporal-testing/src/main/java/io/temporal/testing/internal/ExternalServiceTestConfigurator.java b/temporal-testing/src/main/java/io/temporal/testing/internal/ExternalServiceTestConfigurator.java index 6c68d5f2b5..a722f5041e 100644 --- a/temporal-testing/src/main/java/io/temporal/testing/internal/ExternalServiceTestConfigurator.java +++ b/temporal-testing/src/main/java/io/temporal/testing/internal/ExternalServiceTestConfigurator.java @@ -1,43 +1,69 @@ package io.temporal.testing.internal; -import io.temporal.internal.common.env.EnvironmentVariableUtils; +import io.temporal.envconfig.ClientConfigProfile; +import io.temporal.envconfig.LoadClientConfigProfileOptions; import io.temporal.testing.TestEnvironmentOptions; import io.temporal.testing.TestWorkflowRule; import io.temporal.testing.internal.devserver.SdkJavaTestServerProfile; +import java.io.IOException; +import java.util.Map; import javax.annotation.Nonnull; public class ExternalServiceTestConfigurator { - private static boolean USE_EXTERNAL_SERVICE = - EnvironmentVariableUtils.readBooleanFlag("USE_EXTERNAL_SERVICE"); - private static String TEMPORAL_SERVICE_ADDRESS = - EnvironmentVariableUtils.readString("TEMPORAL_SERVICE_ADDRESS"); - private static boolean USE_VIRTUAL_THREADS = - EnvironmentVariableUtils.readBooleanFlag("USE_VIRTUAL_THREADS"); + static final String TEMPORAL_TEST_ENV_CONFIG_SERVER = "TEMPORAL_TEST_ENV_CONFIG_SERVER"; + private static final String USE_EXTERNAL_SERVICE = "USE_EXTERNAL_SERVICE"; + private static final String TEMPORAL_SERVICE_ADDRESS = "TEMPORAL_SERVICE_ADDRESS"; + private static final String USE_VIRTUAL_THREADS = "USE_VIRTUAL_THREADS"; public static boolean isUseExternalService() { - return USE_EXTERNAL_SERVICE || SdkJavaTestServerProfile.isActive(); + return isUseExternalService(System.getenv()) || SdkJavaTestServerProfile.isActive(); } public static boolean isUseVirtualThreads() { - return USE_VIRTUAL_THREADS; + return readBooleanFlag(System.getenv(), USE_VIRTUAL_THREADS); } public static String getTemporalServiceAddress() { - if (SdkJavaTestServerProfile.isActive()) { - return SdkJavaTestServerProfile.getTarget(); + Map environment = System.getenv(); + if (readBooleanFlag(environment, TEMPORAL_TEST_ENV_CONFIG_SERVER)) { + return getTemporalServiceAddress(environment); } - return USE_EXTERNAL_SERVICE - ? (TEMPORAL_SERVICE_ADDRESS != null ? TEMPORAL_SERVICE_ADDRESS : "127.0.0.1:7233") - : null; + String devServerTarget = SdkJavaTestServerProfile.getTarget(); + if (devServerTarget != null) { + return devServerTarget; + } + return getTemporalServiceAddress(environment); } public static TestWorkflowRule.Builder configure( @Nonnull TestWorkflowRule.Builder testWorkflowRule) { - if (isUseExternalService()) { + return configure(testWorkflowRule, System.getenv(), SdkJavaTestServerProfile.getTarget()); + } + + static TestWorkflowRule.Builder configure( + TestWorkflowRule.Builder testWorkflowRule, Map environment) { + return configure(testWorkflowRule, environment, null); + } + + static TestWorkflowRule.Builder configure( + TestWorkflowRule.Builder testWorkflowRule, + Map environment, + String devServerTarget) { + ClientConfigProfile profile = loadEnvConfigProfile(environment); + if (profile != null) { + testWorkflowRule.setUseExternalService(true); + testWorkflowRule.setTarget(profile.getAddress()); + testWorkflowRule.setNamespace(profile.getNamespace()); + testWorkflowRule.setWorkflowServiceStubsOptions(profile.toWorkflowServiceStubsOptions()); + testWorkflowRule.setWorkflowClientOptions(profile.toWorkflowClientOptions()); + } else if (devServerTarget != null) { + testWorkflowRule.setUseExternalService(true); + testWorkflowRule.setTarget(devServerTarget); + } else if (readBooleanFlag(environment, USE_EXTERNAL_SERVICE)) { testWorkflowRule.setUseExternalService(true); - String target = getTemporalServiceAddress(); - if (target != null) { - testWorkflowRule.setTarget(target); + String serviceAddress = environment.get(TEMPORAL_SERVICE_ADDRESS); + if (serviceAddress != null) { + testWorkflowRule.setTarget(serviceAddress); } } return testWorkflowRule; @@ -45,11 +71,33 @@ public static TestWorkflowRule.Builder configure( public static TestEnvironmentOptions.Builder configure( @Nonnull TestEnvironmentOptions.Builder testEnvironmentOptions) { - if (isUseExternalService()) { + return configure(testEnvironmentOptions, System.getenv(), SdkJavaTestServerProfile.getTarget()); + } + + static TestEnvironmentOptions.Builder configure( + TestEnvironmentOptions.Builder testEnvironmentOptions, Map environment) { + return configure(testEnvironmentOptions, environment, null); + } + + static TestEnvironmentOptions.Builder configure( + TestEnvironmentOptions.Builder testEnvironmentOptions, + Map environment, + String devServerTarget) { + ClientConfigProfile profile = loadEnvConfigProfile(environment); + if (profile != null) { testEnvironmentOptions.setUseExternalService(true); - String target = getTemporalServiceAddress(); - if (target != null) { - testEnvironmentOptions.setTarget(target); + testEnvironmentOptions.setTarget(profile.getAddress()); + testEnvironmentOptions.setWorkflowServiceStubsOptions( + profile.toWorkflowServiceStubsOptions()); + testEnvironmentOptions.setWorkflowClientOptions(profile.toWorkflowClientOptions()); + } else if (devServerTarget != null) { + testEnvironmentOptions.setUseExternalService(true); + testEnvironmentOptions.setTarget(devServerTarget); + } else if (readBooleanFlag(environment, USE_EXTERNAL_SERVICE)) { + testEnvironmentOptions.setUseExternalService(true); + String serviceAddress = environment.get(TEMPORAL_SERVICE_ADDRESS); + if (serviceAddress != null) { + testEnvironmentOptions.setTarget(serviceAddress); } } return testEnvironmentOptions; @@ -58,4 +106,53 @@ public static TestEnvironmentOptions.Builder configure( public static TestEnvironmentOptions.Builder configuredTestEnvironmentOptions() { return configure(TestEnvironmentOptions.newBuilder()); } + + static boolean isUseExternalService(Map environment) { + return readBooleanFlag(environment, TEMPORAL_TEST_ENV_CONFIG_SERVER) + || readBooleanFlag(environment, USE_EXTERNAL_SERVICE); + } + + static String getTemporalServiceAddress(Map environment) { + ClientConfigProfile profile = loadEnvConfigProfile(environment); + if (profile != null) { + return profile.getAddress(); + } + return readBooleanFlag(environment, USE_EXTERNAL_SERVICE) + ? (environment.get(TEMPORAL_SERVICE_ADDRESS) != null + ? environment.get(TEMPORAL_SERVICE_ADDRESS) + : "127.0.0.1:7233") + : null; + } + + private static ClientConfigProfile loadEnvConfigProfile(Map environment) { + if (!readBooleanFlag(environment, TEMPORAL_TEST_ENV_CONFIG_SERVER)) { + return null; + } + + ClientConfigProfile profile; + try { + profile = + ClientConfigProfile.load( + LoadClientConfigProfileOptions.newBuilder().setEnvOverrides(environment).build()); + } catch (IOException e) { + throw new IllegalStateException( + "Unable to load client configuration for the Temporal test harness.", e); + } + if (profile.getAddress() == null || profile.getAddress().isEmpty()) { + throw new IllegalStateException("Envconfig test harness requires a Temporal server address."); + } + if (profile.getNamespace() == null || profile.getNamespace().isEmpty()) { + throw new IllegalStateException("Envconfig test harness requires a Temporal namespace."); + } + return profile; + } + + private static boolean readBooleanFlag(Map environment, String variableName) { + String value = environment.get(variableName); + if (value == null) { + return false; + } + value = value.trim(); + return !Boolean.FALSE.toString().equalsIgnoreCase(value) && !"0".equals(value); + } } diff --git a/temporal-testing/src/test/java/io/temporal/testing/internal/ExternalServiceTestConfiguratorTest.java b/temporal-testing/src/test/java/io/temporal/testing/internal/ExternalServiceTestConfiguratorTest.java new file mode 100644 index 0000000000..d02c28479f --- /dev/null +++ b/temporal-testing/src/test/java/io/temporal/testing/internal/ExternalServiceTestConfiguratorTest.java @@ -0,0 +1,122 @@ +package io.temporal.testing.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.grpc.Metadata; +import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import io.temporal.testing.TestEnvironmentOptions; +import io.temporal.testing.TestWorkflowRule; +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +public class ExternalServiceTestConfiguratorTest { + + @Test + public void configureTestWorkflowRuleFromEnvConfig() { + Map environment = newEnvConfigEnvironment(); + environment.put("USE_EXTERNAL_SERVICE", "true"); + environment.put("TEMPORAL_SERVICE_ADDRESS", "legacy-address:7233"); + + TestWorkflowRule rule = + ExternalServiceTestConfigurator.configure(TestWorkflowRule.newBuilder(), environment) + .build(); + try { + assertTrue(rule.isUseExternalService()); + assertEquals( + "envconfig-address:7233", rule.getWorkflowServiceStubs().getOptions().getTarget()); + assertEquals("envconfig-namespace", rule.getWorkflowClient().getOptions().getNamespace()); + + WorkflowServiceStubsOptions stubsOptions = rule.getWorkflowServiceStubs().getOptions(); + assertTrue(stubsOptions.getEnableHttps()); + Metadata metadata = new Metadata(); + stubsOptions + .getGrpcMetadataProviders() + .forEach(provider -> metadata.merge(provider.getMetadata())); + assertEquals( + "metadata-value", + metadata.get(Metadata.Key.of("test-header", Metadata.ASCII_STRING_MARSHALLER))); + assertEquals( + "Bearer api-key", + metadata.get(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER))); + } finally { + rule.getTestEnvironment().close(); + } + } + + @Test + public void preserveLegacyAndLocalModes() { + Map environment = new HashMap<>(); + TestEnvironmentOptions localOptions = + ExternalServiceTestConfigurator.configure(TestEnvironmentOptions.newBuilder(), environment) + .build(); + assertFalse(localOptions.isUseExternalService()); + + environment.put("USE_EXTERNAL_SERVICE", "true"); + environment.put("TEMPORAL_SERVICE_ADDRESS", "legacy-address:7233"); + TestEnvironmentOptions externalOptions = + ExternalServiceTestConfigurator.configure(TestEnvironmentOptions.newBuilder(), environment) + .build(); + assertTrue(externalOptions.isUseExternalService()); + assertEquals("legacy-address:7233", externalOptions.getTarget()); + } + + @Test + public void preserveDevServerProfilePrecedence() { + Map environment = new HashMap<>(); + environment.put("USE_EXTERNAL_SERVICE", "true"); + environment.put("TEMPORAL_SERVICE_ADDRESS", "legacy-address:7233"); + + TestEnvironmentOptions devServerOptions = + ExternalServiceTestConfigurator.configure( + TestEnvironmentOptions.newBuilder(), environment, "dev-server-address:7233") + .build(); + assertTrue(devServerOptions.isUseExternalService()); + assertEquals("dev-server-address:7233", devServerOptions.getTarget()); + + TestEnvironmentOptions envConfigOptions = + ExternalServiceTestConfigurator.configure( + TestEnvironmentOptions.newBuilder(), + newEnvConfigEnvironment(), + "dev-server-address:7233") + .build(); + assertEquals("envconfig-address:7233", envConfigOptions.getTarget()); + assertEquals("envconfig-namespace", envConfigOptions.getWorkflowClientOptions().getNamespace()); + } + + @Test + public void requireAddressAndNamespaceInEnvConfigMode() { + Map environment = new HashMap<>(); + environment.put(ExternalServiceTestConfigurator.TEMPORAL_TEST_ENV_CONFIG_SERVER, "true"); + environment.put("TEMPORAL_CONFIG_FILE", nonExistentConfigFile()); + environment.put("TEMPORAL_ADDRESS", "envconfig-address:7233"); + + IllegalStateException exception = + assertThrows( + IllegalStateException.class, + () -> + ExternalServiceTestConfigurator.configure( + TestEnvironmentOptions.newBuilder(), environment)); + assertEquals("Envconfig test harness requires a Temporal namespace.", exception.getMessage()); + } + + private static Map newEnvConfigEnvironment() { + Map environment = new HashMap<>(); + environment.put(ExternalServiceTestConfigurator.TEMPORAL_TEST_ENV_CONFIG_SERVER, "true"); + environment.put("TEMPORAL_CONFIG_FILE", nonExistentConfigFile()); + environment.put("TEMPORAL_ADDRESS", "envconfig-address:7233"); + environment.put("TEMPORAL_NAMESPACE", "envconfig-namespace"); + environment.put("TEMPORAL_API_KEY", "api-key"); + environment.put("TEMPORAL_GRPC_META_TEST_HEADER", "metadata-value"); + return environment; + } + + private static String nonExistentConfigFile() { + return new File("build/non-existent-envconfig-" + UUID.randomUUID()).getAbsolutePath(); + } +} From 4fd3de42d20f1de47f0a5989578c7f02cb4dfaa8 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Mon, 24 Aug 2026 14:39:07 -0400 Subject: [PATCH 2/4] Keep envconfig harness PR focused --- .github/workflows/ci.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cacd39c0e9..1c147f7e9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,17 +139,10 @@ jobs: if: ${{ github.event.pull_request.head.repo.full_name == '' || github.event.pull_request.head.repo.full_name == 'temporalio/sdk-java' }} env: USER: unittest - TEMPORAL_TEST_ENV_CONFIG_SERVER: "true" - TEMPORAL_ADDRESS: sdk-ci.a2dd6.tmprl.cloud:7233 - TEMPORAL_NAMESPACE: sdk-ci.a2dd6 - TEMPORAL_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} TEMPORAL_CLIENT_CLOUD_NAMESPACE: sdk-ci.a2dd6 TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} TEMPORAL_CLIENT_CLOUD_API_VERSION: 2024-05-13-00 - run: | - ./gradlew --no-daemon :temporal-sdk:test \ - --tests '*CloudOperationsClientTest' \ - --tests 'io.temporal.client.functional.SignalTest.signalCompletedWorkflow' + run: ./gradlew --no-daemon :temporal-sdk:test --tests '*CloudOperationsClientTest' - name: Publish Test Report uses: mikepenz/action-junit-report@bccf2e31636835cf0874589931c4116687171386 # v6 From 2743f3cbd1e4ead5c3e5dbe5bc4b5433f02e7420 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Mon, 24 Aug 2026 15:19:58 -0400 Subject: [PATCH 3/4] Preserve test options with envconfig --- .../ExternalServiceTestConfigurator.java | 104 +++++++++++++++++- .../testing/internal/SDKTestWorkflowRule.java | 33 +++++- .../ExternalServiceTestConfiguratorTest.java | 92 +++++++++++++++- 3 files changed, 221 insertions(+), 8 deletions(-) diff --git a/temporal-testing/src/main/java/io/temporal/testing/internal/ExternalServiceTestConfigurator.java b/temporal-testing/src/main/java/io/temporal/testing/internal/ExternalServiceTestConfigurator.java index a722f5041e..15982b8bec 100644 --- a/temporal-testing/src/main/java/io/temporal/testing/internal/ExternalServiceTestConfigurator.java +++ b/temporal-testing/src/main/java/io/temporal/testing/internal/ExternalServiceTestConfigurator.java @@ -1,11 +1,19 @@ package io.temporal.testing.internal; +import io.grpc.Metadata; +import io.temporal.client.ActivityClientOptions; +import io.temporal.client.WorkflowClientOptions; import io.temporal.envconfig.ClientConfigProfile; import io.temporal.envconfig.LoadClientConfigProfileOptions; +import io.temporal.serviceclient.GrpcMetadataProvider; +import io.temporal.serviceclient.WorkflowServiceStubsOptions; import io.temporal.testing.TestEnvironmentOptions; import io.temporal.testing.TestWorkflowRule; import io.temporal.testing.internal.devserver.SdkJavaTestServerProfile; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; import javax.annotation.Nonnull; @@ -49,13 +57,30 @@ static TestWorkflowRule.Builder configure( TestWorkflowRule.Builder testWorkflowRule, Map environment, String devServerTarget) { + return configure(testWorkflowRule, environment, devServerTarget, true); + } + + static TestWorkflowRule.Builder configureConnection( + TestWorkflowRule.Builder testWorkflowRule, + Map environment, + String devServerTarget) { + return configure(testWorkflowRule, environment, devServerTarget, false); + } + + private static TestWorkflowRule.Builder configure( + TestWorkflowRule.Builder testWorkflowRule, + Map environment, + String devServerTarget, + boolean configureOptions) { ClientConfigProfile profile = loadEnvConfigProfile(environment); if (profile != null) { testWorkflowRule.setUseExternalService(true); testWorkflowRule.setTarget(profile.getAddress()); testWorkflowRule.setNamespace(profile.getNamespace()); - testWorkflowRule.setWorkflowServiceStubsOptions(profile.toWorkflowServiceStubsOptions()); - testWorkflowRule.setWorkflowClientOptions(profile.toWorkflowClientOptions()); + if (configureOptions) { + testWorkflowRule.setWorkflowServiceStubsOptions(profile.toWorkflowServiceStubsOptions()); + testWorkflowRule.setWorkflowClientOptions(profile.toWorkflowClientOptions()); + } } else if (devServerTarget != null) { testWorkflowRule.setUseExternalService(true); testWorkflowRule.setTarget(devServerTarget); @@ -107,6 +132,81 @@ public static TestEnvironmentOptions.Builder configuredTestEnvironmentOptions() return configure(TestEnvironmentOptions.newBuilder()); } + static WorkflowServiceStubsOptions configure( + WorkflowServiceStubsOptions workflowServiceStubsOptions, Map environment) { + ClientConfigProfile profile = loadEnvConfigProfile(environment); + if (profile == null) { + return workflowServiceStubsOptions; + } + + WorkflowServiceStubsOptions profileOptions = profile.toWorkflowServiceStubsOptions(); + GrpcMetadataProvider metadataProvider = + mergeMetadata( + profileOptions.getHeaders(), + profileOptions.getGrpcMetadataProviders(), + workflowServiceStubsOptions.getHeaders(), + workflowServiceStubsOptions.getGrpcMetadataProviders()); + return WorkflowServiceStubsOptions.newBuilder(workflowServiceStubsOptions) + .setChannel(null) + .setTarget(profileOptions.getTarget()) + .setEnableHttps(profileOptions.getEnableHttps()) + .setSslContext(profileOptions.getSslContext()) + .setChannelInitializer(profileOptions.getChannelInitializer()) + .setHeaders(new Metadata()) + .setGrpcMetadataProviders(Collections.singletonList(metadataProvider)) + .build(); + } + + private static GrpcMetadataProvider mergeMetadata( + Metadata profileHeaders, + Iterable profileProviders, + Metadata testHeaders, + Iterable testProviders) { + List profileProviderList = new ArrayList<>(); + profileProviders.forEach(profileProviderList::add); + return () -> { + Metadata metadata = new Metadata(); + if (testHeaders != null) { + metadata.merge(testHeaders); + } + testProviders.forEach(provider -> metadata.merge(provider.getMetadata())); + Metadata profileMetadata = new Metadata(); + if (profileHeaders != null) { + profileMetadata.merge(profileHeaders); + } + profileProviderList.forEach(provider -> profileMetadata.merge(provider.getMetadata())); + for (String keyName : profileMetadata.keys()) { + if (keyName.endsWith("-bin")) { + metadata.discardAll(Metadata.Key.of(keyName, Metadata.BINARY_BYTE_MARSHALLER)); + } else { + metadata.discardAll(Metadata.Key.of(keyName, Metadata.ASCII_STRING_MARSHALLER)); + } + } + metadata.merge(profileMetadata); + return metadata; + }; + } + + static WorkflowClientOptions configure( + WorkflowClientOptions workflowClientOptions, Map environment) { + ClientConfigProfile profile = loadEnvConfigProfile(environment); + return profile == null + ? workflowClientOptions + : WorkflowClientOptions.newBuilder(workflowClientOptions) + .setNamespace(profile.getNamespace()) + .build(); + } + + static ActivityClientOptions configure( + ActivityClientOptions activityClientOptions, Map environment) { + ClientConfigProfile profile = loadEnvConfigProfile(environment); + return profile == null + ? activityClientOptions + : ActivityClientOptions.newBuilder(activityClientOptions) + .setNamespace(profile.getNamespace()) + .build(); + } + static boolean isUseExternalService(Map environment) { return readBooleanFlag(environment, TEMPORAL_TEST_ENV_CONFIG_SERVER) || readBooleanFlag(environment, USE_EXTERNAL_SERVICE); diff --git a/temporal-testing/src/main/java/io/temporal/testing/internal/SDKTestWorkflowRule.java b/temporal-testing/src/main/java/io/temporal/testing/internal/SDKTestWorkflowRule.java index 9c81461212..c6109c5ec5 100644 --- a/temporal-testing/src/main/java/io/temporal/testing/internal/SDKTestWorkflowRule.java +++ b/temporal-testing/src/main/java/io/temporal/testing/internal/SDKTestWorkflowRule.java @@ -31,6 +31,7 @@ import io.temporal.serviceclient.WorkflowServiceStubsOptions; import io.temporal.testing.TestWorkflowEnvironment; import io.temporal.testing.TestWorkflowRule; +import io.temporal.testing.internal.devserver.SdkJavaTestServerProfile; import io.temporal.worker.*; import io.temporal.workflow.Functions; import java.io.File; @@ -40,6 +41,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.concurrent.*; import javax.annotation.Nonnull; @@ -91,7 +93,9 @@ private SDKTestWorkflowRule(SDKTestWorkflowRule.Builder builder) { : null; testWorkflowRule = - ExternalServiceTestConfigurator.configure(builder.testWorkflowRuleBuilder).build(); + ExternalServiceTestConfigurator.configureConnection( + builder.testWorkflowRuleBuilder, builder.environment, builder.devServerTarget) + .build(); } public static Builder newBuilder() { @@ -104,24 +108,43 @@ public static class Builder { private boolean workerFactoryOptionsAreSet = false; private boolean workerOptionsAreSet = false; private final TestWorkflowRule.Builder testWorkflowRuleBuilder; + private final Map environment; + @Nullable private final String devServerTarget; public Builder() { - testWorkflowRuleBuilder = TestWorkflowRule.newBuilder(); + this(System.getenv(), SdkJavaTestServerProfile.getTarget()); + } + + Builder(Map environment, @Nullable String devServerTarget) { + this.environment = environment; + this.devServerTarget = devServerTarget; + testWorkflowRuleBuilder = + ExternalServiceTestConfigurator.configure( + TestWorkflowRule.newBuilder(), environment, devServerTarget); } public Builder setWorkflowServiceStubsOptions( WorkflowServiceStubsOptions workflowServiceStubsOptions) { - testWorkflowRuleBuilder.setWorkflowServiceStubsOptions(workflowServiceStubsOptions); + if (workflowServiceStubsOptions != null) { + testWorkflowRuleBuilder.setWorkflowServiceStubsOptions( + ExternalServiceTestConfigurator.configure(workflowServiceStubsOptions, environment)); + } return this; } public Builder setWorkflowClientOptions(WorkflowClientOptions workflowClientOptions) { - testWorkflowRuleBuilder.setWorkflowClientOptions(workflowClientOptions); + if (workflowClientOptions != null) { + testWorkflowRuleBuilder.setWorkflowClientOptions( + ExternalServiceTestConfigurator.configure(workflowClientOptions, environment)); + } return this; } public Builder setActivityClientOptions(ActivityClientOptions activityClientOptions) { - testWorkflowRuleBuilder.setActivityClientOptions(activityClientOptions); + if (activityClientOptions != null) { + testWorkflowRuleBuilder.setActivityClientOptions( + ExternalServiceTestConfigurator.configure(activityClientOptions, environment)); + } return this; } diff --git a/temporal-testing/src/test/java/io/temporal/testing/internal/ExternalServiceTestConfiguratorTest.java b/temporal-testing/src/test/java/io/temporal/testing/internal/ExternalServiceTestConfiguratorTest.java index d02c28479f..0432605634 100644 --- a/temporal-testing/src/test/java/io/temporal/testing/internal/ExternalServiceTestConfiguratorTest.java +++ b/temporal-testing/src/test/java/io/temporal/testing/internal/ExternalServiceTestConfiguratorTest.java @@ -6,10 +6,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import io.grpc.Metadata; +import io.temporal.client.WorkflowClientOptions; import io.temporal.serviceclient.WorkflowServiceStubsOptions; import io.temporal.testing.TestEnvironmentOptions; import io.temporal.testing.TestWorkflowRule; import java.io.File; +import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -27,7 +29,6 @@ public void configureTestWorkflowRuleFromEnvConfig() { ExternalServiceTestConfigurator.configure(TestWorkflowRule.newBuilder(), environment) .build(); try { - assertTrue(rule.isUseExternalService()); assertEquals( "envconfig-address:7233", rule.getWorkflowServiceStubs().getOptions().getTarget()); assertEquals("envconfig-namespace", rule.getWorkflowClient().getOptions().getNamespace()); @@ -89,6 +90,95 @@ public void preserveDevServerProfilePrecedence() { assertEquals("envconfig-namespace", envConfigOptions.getWorkflowClientOptions().getNamespace()); } + @Test + public void preserveTestOptionsWhenApplyingEnvConfigConnection() { + Map environment = newEnvConfigEnvironment(); + Metadata.Key customHeader = + Metadata.Key.of("custom-option-header", Metadata.ASCII_STRING_MARSHALLER); + Metadata.Key authorizationHeader = + Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER); + Metadata.Key profileHeader = + Metadata.Key.of("test-header", Metadata.ASCII_STRING_MARSHALLER); + Metadata.Key fixedHeader = + Metadata.Key.of("custom-fixed-header", Metadata.ASCII_STRING_MARSHALLER); + Metadata fixedHeaders = new Metadata(); + fixedHeaders.put(fixedHeader, "custom-fixed-value"); + fixedHeaders.put(authorizationHeader, "Bearer stale-fixed-api-key"); + fixedHeaders.put(profileHeader, "stale-fixed-metadata-value"); + WorkflowServiceStubsOptions serviceOptions = + WorkflowServiceStubsOptions.newBuilder() + .setRpcTimeout(Duration.ofSeconds(17)) + .setHeaders(fixedHeaders) + .addGrpcMetadataProvider( + () -> { + Metadata metadata = new Metadata(); + metadata.put(customHeader, "custom-option-value"); + metadata.put(authorizationHeader, "Bearer stale-api-key"); + metadata.put(profileHeader, "stale-metadata-value"); + return metadata; + }) + .build(); + + WorkflowServiceStubsOptions configuredServiceOptions = + ExternalServiceTestConfigurator.configure(serviceOptions, environment); + assertEquals("envconfig-address:7233", configuredServiceOptions.getTarget()); + assertEquals(Duration.ofSeconds(17), configuredServiceOptions.getRpcTimeout()); + assertTrue(configuredServiceOptions.getEnableHttps()); + Metadata metadata = new Metadata(); + configuredServiceOptions + .getGrpcMetadataProviders() + .forEach(provider -> metadata.merge(provider.getMetadata())); + assertEquals("custom-option-value", metadata.get(customHeader)); + assertEquals("custom-fixed-value", metadata.get(fixedHeader)); + assertEquals("metadata-value", metadata.get(profileHeader)); + assertEquals("Bearer api-key", metadata.get(authorizationHeader)); + int authorizationValues = 0; + for (String ignored : metadata.getAll(authorizationHeader)) { + authorizationValues++; + } + assertEquals(1, authorizationValues); + + WorkflowClientOptions clientOptions = + WorkflowClientOptions.newBuilder() + .setNamespace("test-option-namespace") + .setIdentity("test-option-identity") + .build(); + WorkflowClientOptions configuredClientOptions = + ExternalServiceTestConfigurator.configure(clientOptions, environment); + assertEquals("envconfig-namespace", configuredClientOptions.getNamespace()); + assertEquals("test-option-identity", configuredClientOptions.getIdentity()); + } + + @Test + public void sdkRuleReappliesEnvConfigConnectionAfterTestCustomization() { + Map environment = newEnvConfigEnvironment(); + SDKTestWorkflowRule rule = + new SDKTestWorkflowRule.Builder(environment, null) + .setUseExternalService(false) + .setTarget("test-option-address:7233") + .setNamespace("test-option-namespace") + .setWorkflowServiceStubsOptions( + WorkflowServiceStubsOptions.newBuilder() + .setRpcTimeout(Duration.ofSeconds(17)) + .build()) + .setWorkflowClientOptions( + WorkflowClientOptions.newBuilder() + .setNamespace("test-option-namespace") + .setIdentity("test-option-identity") + .build()) + .build(); + try { + assertEquals( + "envconfig-address:7233", rule.getWorkflowServiceStubs().getOptions().getTarget()); + assertEquals( + Duration.ofSeconds(17), rule.getWorkflowServiceStubs().getOptions().getRpcTimeout()); + assertEquals("envconfig-namespace", rule.getWorkflowClient().getOptions().getNamespace()); + assertEquals("test-option-identity", rule.getWorkflowClient().getOptions().getIdentity()); + } finally { + rule.getTestEnvironment().close(); + } + } + @Test public void requireAddressAndNamespaceInEnvConfigMode() { Map environment = new HashMap<>(); From fdf3b7645679680ceb48a80f147ba48c801d200c Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Mon, 24 Aug 2026 17:23:00 -0400 Subject: [PATCH 4/4] Handle envconfig profiles without metadata --- .../internal/ExternalServiceTestConfigurator.java | 8 ++++++-- .../ExternalServiceTestConfiguratorTest.java | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/temporal-testing/src/main/java/io/temporal/testing/internal/ExternalServiceTestConfigurator.java b/temporal-testing/src/main/java/io/temporal/testing/internal/ExternalServiceTestConfigurator.java index 15982b8bec..95c213f219 100644 --- a/temporal-testing/src/main/java/io/temporal/testing/internal/ExternalServiceTestConfigurator.java +++ b/temporal-testing/src/main/java/io/temporal/testing/internal/ExternalServiceTestConfigurator.java @@ -163,13 +163,17 @@ private static GrpcMetadataProvider mergeMetadata( Metadata testHeaders, Iterable testProviders) { List profileProviderList = new ArrayList<>(); - profileProviders.forEach(profileProviderList::add); + if (profileProviders != null) { + profileProviders.forEach(profileProviderList::add); + } return () -> { Metadata metadata = new Metadata(); if (testHeaders != null) { metadata.merge(testHeaders); } - testProviders.forEach(provider -> metadata.merge(provider.getMetadata())); + if (testProviders != null) { + testProviders.forEach(provider -> metadata.merge(provider.getMetadata())); + } Metadata profileMetadata = new Metadata(); if (profileHeaders != null) { profileMetadata.merge(profileHeaders); diff --git a/temporal-testing/src/test/java/io/temporal/testing/internal/ExternalServiceTestConfiguratorTest.java b/temporal-testing/src/test/java/io/temporal/testing/internal/ExternalServiceTestConfiguratorTest.java index 0432605634..cd7a72114e 100644 --- a/temporal-testing/src/test/java/io/temporal/testing/internal/ExternalServiceTestConfiguratorTest.java +++ b/temporal-testing/src/test/java/io/temporal/testing/internal/ExternalServiceTestConfiguratorTest.java @@ -149,6 +149,21 @@ public void preserveTestOptionsWhenApplyingEnvConfigConnection() { assertEquals("test-option-identity", configuredClientOptions.getIdentity()); } + @Test + public void preserveTestOptionsWhenEnvConfigHasNoMetadataProviders() { + Map environment = newEnvConfigEnvironment(); + environment.remove("TEMPORAL_API_KEY"); + environment.remove("TEMPORAL_GRPC_META_TEST_HEADER"); + WorkflowServiceStubsOptions serviceOptions = + WorkflowServiceStubsOptions.newBuilder().setRpcTimeout(Duration.ofSeconds(17)).build(); + + WorkflowServiceStubsOptions configuredServiceOptions = + ExternalServiceTestConfigurator.configure(serviceOptions, environment); + + assertEquals("envconfig-address:7233", configuredServiceOptions.getTarget()); + assertEquals(Duration.ofSeconds(17), configuredServiceOptions.getRpcTimeout()); + } + @Test public void sdkRuleReappliesEnvConfigConnectionAfterTestCustomization() { Map environment = newEnvConfigEnvironment();