diff --git a/packages/test-helpers/package.json b/packages/test-helpers/package.json index ca51a79dc9..588bd421b6 100644 --- a/packages/test-helpers/package.json +++ b/packages/test-helpers/package.json @@ -18,6 +18,7 @@ "license": "MIT", "dependencies": { "@temporalio/common": "workspace:*", + "@temporalio/envconfig": "workspace:*", "@temporalio/proto": "workspace:*", "@temporalio/testing": "workspace:*", "@temporalio/worker": "workspace:*", diff --git a/packages/test-helpers/src/environment.ts b/packages/test-helpers/src/environment.ts index 2658a01b0b..2bc91327d8 100644 --- a/packages/test-helpers/src/environment.ts +++ b/packages/test-helpers/src/environment.ts @@ -1,10 +1,12 @@ import type { LocalTestWorkflowEnvironmentOptions } from '@temporalio/testing'; import { workflowInterceptorModules as defaultWorkflowInterceptorModules } from '@temporalio/testing'; +import { loadClientConnectConfig } from '@temporalio/envconfig'; import type { BundlerPlugin, WorkflowBundleWithSourceMap, BundleOptions } from '@temporalio/worker'; import { bundleWorkflowCode, DefaultLogger } from '@temporalio/worker'; import { defineSearchAttributeKey, SearchAttributeType } from '@temporalio/common/lib/search-attributes'; import { TestWorkflowEnvironment } from './wrappers'; import { baseBundlerIgnoreModules } from './bundler'; +import { isSet } from './flags'; export const defaultDynamicConfigOptions = [ 'system.enableActivityEagerExecution=true', @@ -76,14 +78,26 @@ export async function createLocalTestEnvironment( } /** - * Create a test workflow environment, using an existing server if TEMPORAL_SERVICE_ADDRESS is set, - * otherwise creating a local one. + * Create a test workflow environment. + * + * Uses envconfig for the test server connection when TEMPORAL_TEST_ENV_CONFIG_SERVER is truthy, uses + * TEMPORAL_SERVICE_ADDRESS as a legacy existing-server shortcut when set, otherwise creates a local environment. */ export async function createTestWorkflowEnvironment( opts?: LocalTestWorkflowEnvironmentOptions ): Promise { let env: TestWorkflowEnvironment; - if (process.env.TEMPORAL_SERVICE_ADDRESS) { + if (isSet(process.env.TEMPORAL_TEST_ENV_CONFIG_SERVER, false)) { + const { namespace, connectionOptions } = loadClientConnectConfig(); + const { address, apiKey, metadata, tls } = connectionOptions; + env = await TestWorkflowEnvironment.createFromExistingServer({ + address, + namespace, + connectionOptions: { apiKey, metadata, tls }, + client: opts?.client, + plugins: opts?.plugins, + }); + } else if (process.env.TEMPORAL_SERVICE_ADDRESS) { env = await TestWorkflowEnvironment.createFromExistingServer({ address: process.env.TEMPORAL_SERVICE_ADDRESS, plugins: opts?.plugins, diff --git a/packages/test-helpers/src/helpers.ts b/packages/test-helpers/src/helpers.ts index c704764944..d0dd80a104 100644 --- a/packages/test-helpers/src/helpers.ts +++ b/packages/test-helpers/src/helpers.ts @@ -73,6 +73,7 @@ export function helpers): Promise { return await Worker.create({ connection: env.nativeConnection, + namespace: env.namespace, workflowBundle, taskQueue, showStackTraceSources: true, diff --git a/packages/test-helpers/tsconfig.json b/packages/test-helpers/tsconfig.json index 9cb59c3d55..3f9d6e5a66 100644 --- a/packages/test-helpers/tsconfig.json +++ b/packages/test-helpers/tsconfig.json @@ -7,6 +7,7 @@ "references": [ { "path": "../client" }, { "path": "../common" }, + { "path": "../envconfig" }, { "path": "../proto" }, { "path": "../testing" }, { "path": "../worker" }, diff --git a/packages/test/src/helpers-integration.ts b/packages/test/src/helpers-integration.ts index cac93b103e..d4180121a0 100644 --- a/packages/test/src/helpers-integration.ts +++ b/packages/test/src/helpers-integration.ts @@ -171,7 +171,7 @@ export function helpers(t: ExecutionContext, env?: TestWorkflowEnvironm return { ...base, async createNativeConnection(opts?: Partial): Promise { - return await NativeConnection.connect({ address: testEnv.address, ...opts }); + return await NativeConnection.connect({ ...testEnv.connectionOptions, address: testEnv.address, ...opts }); }, async runReplayHistory( opts: Partial, diff --git a/packages/test/src/test-ephemeral-server.ts b/packages/test/src/test-ephemeral-server.ts index cdceabe726..d69038b219 100644 --- a/packages/test/src/test-ephemeral-server.ts +++ b/packages/test/src/test-ephemeral-server.ts @@ -1,11 +1,14 @@ import fs from 'fs/promises'; import { randomUUID } from 'crypto'; +import os from 'os'; +import path from 'path'; import type { ExecutionContext, TestFn } from 'ava'; import anyTest from 'ava'; import type { WorkflowBundle } from '@temporalio/worker'; import { bundleWorkflowCode } from '@temporalio/worker'; import { Connection } from '@temporalio/client'; import { TestWorkflowEnvironment as RealTestWorkflowEnvironment } from '@temporalio/testing'; +import { createTestWorkflowEnvironment } from '@temporalio/test-helpers'; import { Worker, TestWorkflowEnvironment, @@ -80,6 +83,50 @@ test('TestEnvironment sets up dev server and is able to run a single workflow', await runSimpleWorkflow(t, testEnv); }); +test.serial('Shared test harness supports envconfig and legacy existing servers', async (t) => { + const namespace = 'envconfig-test'; + const sourceEnv = await RealTestWorkflowEnvironment.createLocal({ server: { namespace } }); + const originalTemporalEnv = Object.fromEntries( + Object.entries(process.env).filter(([key, value]) => key.startsWith('TEMPORAL_') && value !== undefined) + ) as Record; + let envconfigEnv: TestWorkflowEnvironment | undefined; + let legacyEnv: TestWorkflowEnvironment | undefined; + + const setTemporalEnv = (values: Record) => { + for (const key of Object.keys(process.env)) { + if (key.startsWith('TEMPORAL_')) delete process.env[key]; + } + Object.assign(process.env, values); + }; + + try { + setTemporalEnv({ + TEMPORAL_TEST_ENV_CONFIG_SERVER: 'true', + TEMPORAL_CONFIG_FILE: path.join(os.tmpdir(), `missing-temporal-config-${randomUUID()}.toml`), + TEMPORAL_ADDRESS: sourceEnv.address, + TEMPORAL_NAMESPACE: namespace, + TEMPORAL_GRPC_META_TEST_HEADER: 'envconfig-test', + }); + + envconfigEnv = await createTestWorkflowEnvironment(); + t.is(envconfigEnv.address, sourceEnv.address); + t.is(envconfigEnv.namespace, namespace); + t.is(envconfigEnv.connectionOptions.metadata?.['test-header'], 'envconfig-test'); + await runSimpleWorkflow(t, envconfigEnv); + envconfigEnv = undefined; + + setTemporalEnv({ TEMPORAL_SERVICE_ADDRESS: sourceEnv.address }); + legacyEnv = await createTestWorkflowEnvironment(); + t.is(legacyEnv.address, sourceEnv.address); + await legacyEnv.connection.ensureConnected(); + } finally { + await envconfigEnv?.teardown(); + await legacyEnv?.teardown(); + setTemporalEnv(originalTemporalEnv); + await sourceEnv.teardown(); + } +}); + test.todo('TestEnvironment sets up test server with extra args'); test.todo('TestEnvironment sets up test server with specified port'); test.todo('TestEnvironment sets up test server with latest version'); diff --git a/packages/testing/src/testing-workflow-environment.ts b/packages/testing/src/testing-workflow-environment.ts index 14563422be..1d980d5509 100644 --- a/packages/testing/src/testing-workflow-environment.ts +++ b/packages/testing/src/testing-workflow-environment.ts @@ -33,14 +33,19 @@ export type TimeSkippingTestWorkflowEnvironmentOptions = { plugins?: (ClientPlugin | ConnectionPlugin | NativeConnectionPlugin)[]; }; +type ExistingServerConnectionOptions = Pick; + /** - * Options for {@link TestWorkflowEnvironment.createExistingServer} + * Options for {@link TestWorkflowEnvironment.createFromExistingServer} + * + * Accepts connection options that can be used for both the client and worker connections. */ export type ExistingServerTestWorkflowEnvironmentOptions = { /** If not set, defaults to localhost:7233 */ address?: string; /** If not set, defaults to default */ namespace?: string; + connectionOptions?: ExistingServerConnectionOptions; client?: ClientOptionsForTestEnv; plugins?: (ClientPlugin | ConnectionPlugin | NativeConnectionPlugin)[]; }; @@ -99,7 +104,11 @@ export class TestWorkflowEnvironment { /** * Address used when constructing `connection` and `nativeConnection` */ - public readonly address: string + public readonly address: string, + /** + * Connection options used when constructing `connection` and `nativeConnection`. + */ + public readonly connectionOptions: ExistingServerConnectionOptions ) { this.connection = connection; this.nativeConnection = nativeConnection; @@ -198,6 +207,7 @@ export class TestWorkflowEnvironment { static async createFromExistingServer( opts?: ExistingServerTestWorkflowEnvironmentOptions ): Promise { + const { apiKey, metadata, tls } = opts?.connectionOptions ?? {}; return await this.create({ server: { type: 'existing' }, client: opts?.client, @@ -205,6 +215,7 @@ export class TestWorkflowEnvironment { namespace: opts?.namespace ?? 'default', supportsTimeSkipping: false, address: opts?.address, + connectionOptions: { apiKey, metadata, tls }, }); } @@ -216,9 +227,10 @@ export class TestWorkflowEnvironment { supportsTimeSkipping: boolean; namespace?: string; address?: string; + connectionOptions?: ExistingServerConnectionOptions; } ): Promise { - const { supportsTimeSkipping, namespace, ...rest } = opts; + const { supportsTimeSkipping, namespace, connectionOptions, ...rest } = opts; const optsWithDefaults = addDefaults(filterNullAndUndefined(rest)); let address: string; @@ -243,12 +255,15 @@ export class TestWorkflowEnvironment { server = 'existing'; } + const connectionOptionsWithDefaults = { ...(connectionOptions ?? {}), address }; const nativeConnection = await NativeConnection.connect({ + ...connectionOptionsWithDefaults, address, plugins: opts.plugins, [InternalConnectionOptionsSymbol]: { supportsTestService: supportsTimeSkipping }, }); const connection = await Connection.connect({ + ...connectionOptionsWithDefaults, address, plugins: opts.plugins, [InternalConnectionOptionsSymbol]: { supportsTestService: supportsTimeSkipping }, @@ -262,7 +277,8 @@ export class TestWorkflowEnvironment { connection, nativeConnection, namespace, - address + address, + connectionOptionsWithDefaults ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ce2aa8e1b..c2f44ca121 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -810,6 +810,9 @@ importers: '@temporalio/common': specifier: workspace:* version: link:../common + '@temporalio/envconfig': + specifier: workspace:* + version: link:../envconfig '@temporalio/proto': specifier: workspace:* version: link:../proto