kalix.javasdk.testkit.KalixTestKit Maven / Gradle / Ivy
/*
* Copyright 2024 Lightbend Inc.
*
* 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 kalix.javasdk.testkit;
import akka.actor.ActorSystem;
import akka.annotation.InternalApi;
import akka.grpc.GrpcClientSettings;
import akka.http.javadsl.Http;
import akka.http.javadsl.model.HttpRequest;
import akka.pattern.Patterns;
import akka.stream.Materializer;
import akka.stream.SystemMaterializer;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import kalix.javasdk.Kalix;
import kalix.javasdk.KalixRunner;
import kalix.javasdk.Principal;
import kalix.javasdk.impl.GrpcClients;
import kalix.javasdk.impl.MessageCodec;
import kalix.javasdk.impl.ProxyInfoHolder;
import kalix.javasdk.testkit.EventingTestKit.IncomingMessages;
import kalix.javasdk.testkit.impl.KalixRuntimeContainer;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.ServerSocket;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import static kalix.javasdk.testkit.impl.KalixRuntimeContainer.DEFAULT_GOOGLE_PUBSUB_PORT;
import static kalix.javasdk.testkit.impl.KalixRuntimeContainer.DEFAULT_KAFKA_PORT;
import static kalix.javasdk.testkit.KalixTestKit.Settings.EventingSupport.GOOGLE_PUBSUB;
import static kalix.javasdk.testkit.KalixTestKit.Settings.EventingSupport.KAFKA;
import static kalix.javasdk.testkit.KalixTestKit.Settings.EventingSupport.TEST_BROKER;
/**
* Testkit for running Kalix services locally.
*
* Requires Docker for starting a local instance of the Kalix Runtime.
*
*
Create a KalixTestkit with an {@link Kalix} service descriptor, and then {@link #start} the
* testkit before testing the service with gRPC or HTTP clients. Call {@link #stop} after tests are
* complete.
*/
public class KalixTestKit {
public static class MockedEventing {
public static final String VALUE_ENTITY = "value-entity";
public static final String EVENT_SOURCED_ENTITY = "event-sourced-entity";
public static final String STREAM = "stream";
public static final String TOPIC = "topic";
private final Map> mockedIncomingEvents; //Subscriptions
private final Map> mockedOutgoingEvents; //Destination
private MockedEventing() {
this(new HashMap<>(), new HashMap<>());
}
private MockedEventing(Map> mockedIncomingEvents, Map> mockedOutgoingEvents) {
this.mockedIncomingEvents = mockedIncomingEvents;
this.mockedOutgoingEvents = mockedOutgoingEvents;
}
public static MockedEventing EMPTY = new MockedEventing();
public MockedEventing withValueEntityIncomingMessages(String typeId) {
Map> copy = new HashMap<>(mockedIncomingEvents);
copy.compute(VALUE_ENTITY, updateValues(typeId));
return new MockedEventing(copy, new HashMap<>(mockedOutgoingEvents));
}
public MockedEventing withEventSourcedIncomingMessages(String typeId) {
Map> copy = new HashMap<>(mockedIncomingEvents);
copy.compute(EVENT_SOURCED_ENTITY, updateValues(typeId));
return new MockedEventing(copy, new HashMap<>(mockedOutgoingEvents));
}
public MockedEventing withStreamIncomingMessages(String service, String streamId) {
Map> copy = new HashMap<>(mockedIncomingEvents);
copy.compute(STREAM, updateValues(service + "/" + streamId));
return new MockedEventing(copy, new HashMap<>(mockedOutgoingEvents));
}
public MockedEventing withTopicIncomingMessages(String topic) {
Map> copy = new HashMap<>(mockedIncomingEvents);
copy.compute(TOPIC, updateValues(topic));
return new MockedEventing(copy, new HashMap<>(mockedOutgoingEvents));
}
public MockedEventing withTopicOutgoingMessages(String topic) {
Map> copy = new HashMap<>(mockedOutgoingEvents);
copy.compute(TOPIC, updateValues(topic));
return new MockedEventing(new HashMap<>(mockedIncomingEvents), copy);
}
@NotNull
private BiFunction, Set> updateValues(String typeId) {
return (key, currentValues) -> {
if (currentValues == null) {
LinkedHashSet values = new LinkedHashSet<>(); //order is relevant only for tests
values.add(typeId);
return values;
} else {
currentValues.add(typeId);
return currentValues;
}
};
}
@Override
public String toString() {
return "MockedEventing{" +
"mockedIncomingEvents=" + mockedIncomingEvents +
", mockedOutgoingEvents=" + mockedOutgoingEvents +
'}';
}
public boolean hasIncomingConfig() {
return !mockedIncomingEvents.isEmpty();
}
public boolean hasConfig() {
return hasIncomingConfig() || hasOutgoingConfig();
}
public boolean hasOutgoingConfig() {
return !mockedOutgoingEvents.isEmpty();
}
public String toIncomingFlowConfig() {
return toConfig(mockedIncomingEvents);
}
public String toOutgoingFlowConfig() {
return toConfig(mockedOutgoingEvents);
}
private String toConfig(Map> configs) {
return configs.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.flatMap(entry -> {
String subscriptionType = entry.getKey();
return entry.getValue().stream().map(name -> subscriptionType + "," + name);
}).collect(Collectors.joining(";"));
}
boolean hasValueEntitySubscription(String typeId) {
return checkExistence(VALUE_ENTITY, typeId);
}
boolean hasEventSourcedEntitySubscription(String typeId) {
return checkExistence(EVENT_SOURCED_ENTITY, typeId);
}
boolean hasStreamSubscription(String service, String streamId) {
return checkExistence(STREAM, service + "/" + streamId);
}
boolean hasTopicSubscription(String topic) {
return checkExistence(TOPIC, topic);
}
boolean hasTopicDestination(String topic) {
Set values = mockedOutgoingEvents.get(TOPIC);
return values != null && values.contains(topic);
}
private boolean checkExistence(String type, String name) {
Set values = mockedIncomingEvents.get(type);
return values != null && values.contains(name);
}
}
/**
* Settings for KalixTestkit.
*/
public static class Settings {
/**
* Default stop timeout (10 seconds).
*/
public static Duration DEFAULT_STOP_TIMEOUT = Duration.ofSeconds(10);
/**
* Default settings for KalixTestkit.
*/
public static Settings DEFAULT = new Settings(DEFAULT_STOP_TIMEOUT);
/**
* Timeout setting for stopping the local Kalix test instance.
*/
public final Duration stopTimeout;
/**
* The name of this service when deployed.
*/
public final String serviceName;
/**
* Whether ACL checking is enabled.
*/
public final boolean aclEnabled;
/**
* Whether advanced View features are enabled.
*/
public final boolean advancedViews;
/**
* To override workflow tick interval for integration tests
*/
public final Optional workflowTickInterval;
/**
* Service port mappings from serviceName to host:port
*/
public final Map servicePortMappings;
public final EventingSupport eventingSupport;
public final MockedEventing mockedEventing;
/**
* Create new settings for KalixTestkit.
*
* @param stopTimeout timeout to use when waiting for Kalix to stop
* @deprecated Use Settings.DEFAULT.withStopTimeout() instead.
*/
@Deprecated
public Settings(final Duration stopTimeout) {
this(stopTimeout, "self", false, false, Optional.empty(), Collections.emptyMap(), TEST_BROKER, MockedEventing.EMPTY);
}
public enum EventingSupport {
/**
* This is the default type used and allows the testing eventing integrations without an external broker dependency
* running.
*/
TEST_BROKER,
/**
* Used if you want to use an external Google PubSub (or its Emulator) on your tests.
*
* Note: the Google PubSub broker instance needs to be started independently.
*/
GOOGLE_PUBSUB,
/**
* Used if you want to use an external Kafka broker on your tests.
*
* Note: the Kafka broker instance needs to be started independently.
*/
KAFKA
}
private Settings(
final Duration stopTimeout,
final String serviceName,
final boolean aclEnabled,
final boolean advancedViews,
final Optional workflowTickInterval,
final Map servicePortMappings,
final EventingSupport eventingSupport,
final MockedEventing mockedEventing) {
this.stopTimeout = stopTimeout;
this.serviceName = serviceName;
this.aclEnabled = aclEnabled;
this.advancedViews = advancedViews;
this.workflowTickInterval = workflowTickInterval;
this.servicePortMappings = servicePortMappings;
this.eventingSupport = eventingSupport;
this.mockedEventing = mockedEventing;
}
/**
* Set a custom stop timeout, for stopping the local Kalix test instance.
*
* @param stopTimeout timeout to use when waiting for Kalix to stop
* @return updated Settings
*/
public Settings withStopTimeout(final Duration stopTimeout) {
return new Settings(stopTimeout, serviceName, aclEnabled, advancedViews, workflowTickInterval, servicePortMappings, eventingSupport, mockedEventing);
}
/**
* Set the name of this service. This will be used by the service when making calls on other
* services run by the testkit to authenticate itself, allowing those services to apply ACLs
* based on that name.
*
* @param serviceName The name of this service.
* @return The updated settings.
*/
public Settings withServiceName(final String serviceName) {
return new Settings(stopTimeout, serviceName, aclEnabled, advancedViews, workflowTickInterval, servicePortMappings, eventingSupport, mockedEventing);
}
/**
* Disable ACL checking in this service.
*
* @return The updated settings.
*/
public Settings withAclDisabled() {
return new Settings(stopTimeout, serviceName, false, advancedViews, workflowTickInterval, servicePortMappings, eventingSupport, mockedEventing);
}
/**
* Enable ACL checking in this service.
*
* @return The updated settings.
*/
public Settings withAclEnabled() {
return new Settings(stopTimeout, serviceName, true, advancedViews, workflowTickInterval, servicePortMappings, eventingSupport, mockedEventing);
}
/**
* Enable advanced View features for this service.
*
* @return The updated settings.
*/
public Settings withAdvancedViews() {
return new Settings(stopTimeout, serviceName, aclEnabled, true, workflowTickInterval, servicePortMappings, eventingSupport, mockedEventing);
}
/**
* Overrides workflow tick interval
*
* @return The updated settings.
*/
public Settings withWorkflowTickInterval(Duration tickInterval) {
return new Settings(stopTimeout, serviceName, aclEnabled, true, Optional.of(tickInterval), servicePortMappings, eventingSupport, mockedEventing);
}
/**
* Mock the incoming messages flow from a ValueEntity.
*/
public Settings withValueEntityIncomingMessages(String typeId) {
return new Settings(stopTimeout, serviceName, aclEnabled, true, workflowTickInterval, servicePortMappings, eventingSupport,
mockedEventing.withValueEntityIncomingMessages(typeId));
}
/**
* Mock the incoming events flow from an EventSourcedEntity.
*/
public Settings withEventSourcedEntityIncomingMessages(String typeId) {
return new Settings(stopTimeout, serviceName, aclEnabled, true, workflowTickInterval, servicePortMappings, eventingSupport,
mockedEventing.withEventSourcedIncomingMessages(typeId));
}
/**
* Mock the incoming messages flow from a Stream (eventing.in.direct in case of protobuf SDKs).
*/
public Settings withStreamIncomingMessages(String service, String streamId) {
return new Settings(stopTimeout, serviceName, aclEnabled, true, workflowTickInterval, servicePortMappings, eventingSupport,
mockedEventing.withStreamIncomingMessages(service, streamId));
}
/**
* Mock the incoming events flow from a Topic.
*/
public Settings withTopicIncomingMessages(String topic) {
return new Settings(stopTimeout, serviceName, aclEnabled, true, workflowTickInterval, servicePortMappings, eventingSupport,
mockedEventing.withTopicIncomingMessages(topic));
}
/**
* Mock the outgoing events flow for a Topic.
*/
public Settings withTopicOutgoingMessages(String topic) {
return new Settings(stopTimeout, serviceName, aclEnabled, true, workflowTickInterval, servicePortMappings, eventingSupport,
mockedEventing.withTopicOutgoingMessages(topic));
}
/**
* Add a service port mapping from serviceName to host:port.
*
* @return The updated settings.
*/
public Settings withServicePortMapping(String serviceName, String host, int port) {
var updatedMappings = new HashMap<>(servicePortMappings);
updatedMappings.put(serviceName, host + ":" + port);
return new Settings(stopTimeout, serviceName, aclEnabled, advancedViews, workflowTickInterval, Map.copyOf(updatedMappings), eventingSupport, mockedEventing);
}
public Settings withEventingSupport(EventingSupport eventingSupport) {
return new Settings(stopTimeout, serviceName, aclEnabled, advancedViews, workflowTickInterval, servicePortMappings, eventingSupport, mockedEventing);
}
@Override
public String toString() {
var portMappingsRendered =
servicePortMappings.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.toList());
return "Settings(" +
"stopTimeout=" + stopTimeout +
", serviceName='" + serviceName + '\'' +
", aclEnabled=" + aclEnabled +
", advancedViews=" + advancedViews +
", workflowTickInterval=" + workflowTickInterval +
", servicePortMappings=[" + String.join(", ", portMappingsRendered) + "]" +
", eventingSupport=" + eventingSupport +
", mockedEventing=" + mockedEventing +
')';
}
}
private static final Logger log = LoggerFactory.getLogger(KalixTestKit.class);
private final Kalix kalix;
private final MessageCodec messageCodec;
private final EventingTestKit.MessageBuilder messageBuilder;
private final Settings settings;
private boolean started = false;
private String proxyHost;
private int proxyPort;
private Optional runtimeContainer = Optional.empty();
private KalixRunner runner;
private ActorSystem testSystem;
private EventingTestKit eventingTestKit;
/**
* Create a new testkit for a Kalix service descriptor.
*
* @param kalix Kalix service descriptor
*/
public KalixTestKit(final Kalix kalix) {
this(kalix, kalix.getMessageCodec(), Settings.DEFAULT);
}
/**
* Create a new testkit for a Kalix service descriptor with custom settings.
*
* @param kalix Kalix service descriptor
* @param settings custom testkit settings
*/
public KalixTestKit(final Kalix kalix, final Settings settings) {
this(kalix, kalix.getMessageCodec(), settings);
}
/**
* Create a new testkit for a Kalix service descriptor with custom settings.
*
* @param kalix Kalix service descriptor
* @param messageCodec message codec
* @param settings custom testkit settings
*/
public KalixTestKit(final Kalix kalix, final MessageCodec messageCodec, final Settings settings) {
this.kalix = kalix;
this.messageCodec = messageCodec;
this.messageBuilder = new EventingTestKit.MessageBuilder(messageCodec);
this.settings = settings;
}
/**
* Start this testkit with default configuration (loaded from {@code application.conf}).
*
* @return this KalixTestkit
*/
public KalixTestKit start() {
return start(ConfigFactory.load());
}
/**
* Start this testkit with custom configuration (overrides {@code application.conf}).
*
* @param config custom test configuration for the KalixRunner
* @return this KalixTestkit
*/
public KalixTestKit start(final Config config) {
if (started)
throw new IllegalStateException("KalixTestkit already started");
Boolean useTestContainers = Optional.ofNullable(System.getenv("KALIX_TESTKIT_USE_TEST_CONTAINERS")).map(Boolean::valueOf).orElse(true);
int port = userServicePort(useTestContainers);
Map conf = new HashMap<>();
conf.put("kalix.user-function-port", port);
// don't kill the test JVM when terminating the KalixRunner
conf.put("kalix.system.akka.coordinated-shutdown.exit-jvm", "off");
// integration tests runs the proxy with Testcontainers and therefore
// we shouldn't load DockerComposeUtils
conf.put("kalix.dev-mode.docker-compose-file", "none");
Config testConfig = ConfigFactory.parseMap(conf);
runner = kalix.createRunner(testConfig.withFallback(config));
runner.run();
testSystem = ActorSystem.create("KalixTestkit", ConfigFactory.parseString("akka.http.server.preview.enable-http2 = true"));
int eventingBackendPort = startEventingTestkit(useTestContainers);
runProxy(useTestContainers, port, eventingBackendPort);
started = true;
if (log.isDebugEnabled())
log.debug("TestKit using [{}:{}] for calls to proxy from service", proxyHost, proxyPort);
return this;
}
private int startEventingTestkit(Boolean useTestContainers) {
var port = eventingTestKitPort(useTestContainers);
if (settings.eventingSupport == TEST_BROKER || settings.mockedEventing.hasConfig()) {
log.info("Eventing TestKit booting up on port: " + port);
eventingTestKit = EventingTestKit.start(testSystem, "0.0.0.0", port, messageCodec);
}
return port;
}
private int eventingTestKitPort(Boolean useTestContainers) {
if (useTestContainers) {
return availableLocalPort();
} else {
return 8999;
}
}
private void runProxy(Boolean useTestContainers, int port, int grpcEventingBackendPort) {
if (useTestContainers) {
var runtimeContainer = new KalixRuntimeContainer(settings.eventingSupport, port, grpcEventingBackendPort);
this.runtimeContainer = Optional.of(runtimeContainer);
runtimeContainer.addEnv("SERVICE_NAME", settings.serviceName);
runtimeContainer.addEnv("ACL_ENABLED", Boolean.toString(settings.aclEnabled));
runtimeContainer.addEnv("VIEW_FEATURES_ALL", Boolean.toString(settings.advancedViews));
List javaOptions = new ArrayList<>();
javaOptions.add("-Dlogback.configurationFile=logback-dev-mode.xml");
//always passing grpc params, in the case of e.g. testing pubsub with mocked incoming messages
if (settings.mockedEventing.hasConfig()) {
javaOptions.add("-Dkalix.proxy.eventing.grpc-backend.host=host.testcontainers.internal");
javaOptions.add("-Dkalix.proxy.eventing.grpc-backend.port=" + grpcEventingBackendPort);
}
if (settings.eventingSupport == TEST_BROKER) {
javaOptions.add("-Dkalix.proxy.eventing.support=grpc-backend");
} else if (settings.eventingSupport == KAFKA) {
javaOptions.add("-Dkalix.proxy.eventing.support=kafka");
javaOptions.add("-Dkalix.proxy.eventing.kafka.bootstrap-servers=host.testcontainers.internal:" + DEFAULT_KAFKA_PORT);
} else if (settings.eventingSupport == GOOGLE_PUBSUB) {
javaOptions.add("-Dkalix.proxy.eventing.support=google-pubsub-emulator");
javaOptions.add("-Dkalix.proxy.eventing.google-pubsub-emulator-defaults.host=host.testcontainers.internal");
javaOptions.add("-Dkalix.proxy.eventing.google-pubsub-emulator-defaults.port=" + DEFAULT_GOOGLE_PUBSUB_PORT);
}
if (settings.mockedEventing.hasIncomingConfig()) {
javaOptions.add("-Dkalix.proxy.eventing.override.sources=" + settings.mockedEventing.toIncomingFlowConfig());
}
if (settings.mockedEventing.hasOutgoingConfig()) {
javaOptions.add("-Dkalix.proxy.eventing.override.destinations=" + settings.mockedEventing.toOutgoingFlowConfig());
}
settings.servicePortMappings.forEach((serviceName, hostPort) -> {
javaOptions.add("-Dkalix.dev-mode.service-port-mappings." + serviceName + "=" + hostPort);
});
log.debug("Running container with javaOptions=" + javaOptions);
runtimeContainer.addEnv("JAVA_TOOL_OPTIONS", String.join(" ", javaOptions));
settings.workflowTickInterval.ifPresent(tickInterval -> runtimeContainer.addEnv("WORKFLOW_TICK_INTERVAL", tickInterval.toMillis() + ".millis"));
runtimeContainer.start();
proxyPort = runtimeContainer.getProxyPort();
proxyHost = runtimeContainer.getHost();
} else {
proxyPort = 9000;
proxyHost = "localhost";
Http http = Http.get(testSystem);
log.info("Checking kalix-runtime status");
CompletionStage checkingProxyStatus = Patterns.retry(() -> http.singleRequest(HttpRequest.GET("http://localhost:8558/ready")).thenCompose(response -> {
int responseCode = response.status().intValue();
if (responseCode == 200) {
log.info("Kalix-runtime started");
return CompletableFuture.completedStage("Ok");
} else {
log.info("Waiting for kalix-runtime, current response code is {}", responseCode);
return CompletableFuture.failedFuture(new IllegalStateException("Kalix Runtime not started."));
}
}), 10, Duration.ofSeconds(1), testSystem);
try {
checkingProxyStatus.toCompletableFuture().get();
} catch (InterruptedException | ExecutionException e) {
log.error("Failed to connect to Kalix Runtime with:", e);
throw new RuntimeException(e);
}
}
// the proxy will announce its host and default port, but to communicate with it,
// we need to use the port and host that testcontainers will expose
// therefore, we set a port override in ProxyInfoHolder to allow for inter-component communication
ProxyInfoHolder holder = ProxyInfoHolder.get(runner.system());
holder.overridePort(proxyPort);
holder.overrideProxyHost(proxyHost);
holder.overrideTracingCollectorEndpoint(""); //emulating ProxyInfo with disabled tracing.
}
private int userServicePort(Boolean useTestContainers) {
if (useTestContainers) {
return availableLocalPort();
} else {
return KalixRuntimeContainer.DEFAULT_USER_SERVICE_PORT;
}
}
/**
* Get the host name/IP address where the Kalix service is available. This is relevant in certain
* Continuous Integration environments.
*
* @return Kalix host
*/
public String getHost() {
if (!started)
throw new IllegalStateException("Need to start KalixTestkit before accessing the host name");
return proxyHost;
}
/**
* Get the local port where the Kalix service is available.
*
* @return local Kalix port
*/
public int getPort() {
if (!started)
throw new IllegalStateException("Need to start KalixTestkit before accessing the port");
return proxyPort;
}
/**
* Get an Akka gRPC client for the given service name. The same client instance is shared for the
* test. The lifecycle of the client is managed by the SDK and it should not be stopped by user
* code.
*
* @param The "service" interface generated for the service by Akka gRPC
* @param clientClass The class of a gRPC service generated by Akka gRPC
*/
public T getGrpcClient(Class clientClass) {
return GrpcClients.get(getActorSystem()).getGrpcClient(clientClass, getHost(), getPort());
}
/**
* Get an Akka gRPC client for the given service name, authenticating using the given principal.
* The same client instance is shared for the test. The lifecycle of the client is managed by the
* SDK and it should not be stopped by user code.
*
* @param The "service" interface generated for the service by Akka gRPC
* @param clientClass The class of a gRPC service generated by Akka gRPC
* @param principal The principal to authenticate calls to the service as.
*/
public T getGrpcClientForPrincipal(Class clientClass, Principal principal) {
String serviceName = null;
if (principal == Principal.SELF) {
serviceName = settings.serviceName;
} else if (principal instanceof Principal.LocalService) {
serviceName = ((Principal.LocalService) principal).getName();
}
if (serviceName != null) {
return GrpcClients.get(getActorSystem())
.getGrpcClient(clientClass, getHost(), getPort(), serviceName);
} else {
return GrpcClients.get(getActorSystem()).getGrpcClient(clientClass, getHost(), getPort());
}
}
/**
* An Akka Stream materializer to use for running streams. Needed for example in a command handler
* which accepts streaming elements but returns a single async reply once all streamed elements
* has been consumed.
*/
public Materializer getMaterializer() {
return SystemMaterializer.get(getActorSystem()).materializer();
}
/**
* Get an {@link ActorSystem} for creating Akka HTTP clients.
*
* @return test actor system
*/
public ActorSystem getActorSystem() {
if (!started)
throw new IllegalStateException("Need to start KalixTestkit before accessing actor system");
return testSystem;
}
/**
* Get {@link GrpcClientSettings} for creating Akka gRPC clients.
*
* @return test gRPC client settings
* @deprecated Use getGrpcClient
instead.
*/
@Deprecated(since = "0.8.1", forRemoval = true)
public GrpcClientSettings getGrpcClientSettings() {
if (!started)
throw new IllegalStateException(
"Need to start KalixTestkit before accessing gRPC client settings");
return GrpcClientSettings.connectToServiceAt(getHost(), getPort(), testSystem).withTls(false);
}
/**
* Use getTopicIncomingMessages
or getTopicOutgoingMessages
instead.
*
* Get {@link EventingTestKit.Topic} for mocking interactions, avoiding the need for a real broker instance.
*
* @param topic the name of the topic configured in your service which you want to mock
* @return mocked topic to read/publish messages from/to
*/
@Deprecated
public EventingTestKit.Topic getTopic(String topic) {
if (!settings.eventingSupport.equals(TEST_BROKER)) {
throw new IllegalStateException("Currently configured eventing support is (" + settings.eventingSupport +
"). To use this API, configure it to be (" + TEST_BROKER + ")");
}
return eventingTestKit.getTopic(topic);
}
/**
* Get incoming messages for ValueEntity.
*
* @param typeId @TypeId or entity_type of the ValueEntity (depending on the used SDK)
*/
public IncomingMessages getValueEntityIncomingMessages(String typeId) {
if (!settings.mockedEventing.hasValueEntitySubscription(typeId)) {
throwMissingConfigurationException("ValueEntity " + typeId);
}
return eventingTestKit.getValueEntityIncomingMessages(typeId);
}
/**
* Get incoming messages for EventSourcedEntity.
*
* @param typeId @TypeId or entity_type of the EventSourcedEntity (depending on the used SDK)
*/
public IncomingMessages getEventSourcedEntityIncomingMessages(String typeId) {
if (!settings.mockedEventing.hasEventSourcedEntitySubscription(typeId)) {
throwMissingConfigurationException("EventSourcedEntity " + typeId);
}
return eventingTestKit.getEventSourcedEntityIncomingMessages(typeId);
}
/**
* Get incoming messages for Stream (eventing.in.direct in case of protobuf SDKs).
*
* @param service service name
* @param streamId service stream id
*/
public IncomingMessages getStreamIncomingMessages(String service, String streamId) {
if (!settings.mockedEventing.hasStreamSubscription(service, streamId)) {
throwMissingConfigurationException("Stream " + service + "/" + streamId);
}
return eventingTestKit.getStreamIncomingMessages(service, streamId);
}
/**
* Get incoming messages for Topic.
*
* @param topic topic name
*/
public IncomingMessages getTopicIncomingMessages(String topic) {
if (!settings.mockedEventing.hasTopicSubscription(topic)) {
throwMissingConfigurationException("Topic " + topic);
}
return eventingTestKit.getTopicIncomingMessages(topic);
}
/**
* Get mocked topic destination.
*
* @param topic topic name
*/
public EventingTestKit.OutgoingMessages getTopicOutgoingMessages(String topic) {
if (!settings.mockedEventing.hasTopicDestination(topic)) {
throwMissingConfigurationException("Topic " + topic);
}
return eventingTestKit.getTopicOutgoingMessages(topic);
}
private void throwMissingConfigurationException(String hint) {
throw new IllegalStateException("Currently configured mocked eventing is [" + settings.mockedEventing +
"]. To use the MockedEventing API, to configure mocking of " + hint);
}
/**
* Stop the testkit and local Kalix.
*/
public void stop() {
try {
runtimeContainer.ifPresent(container -> container.stop());
} catch (Exception e) {
log.error("KalixTestkit proxy container failed to stop", e);
}
try {
testSystem.terminate();
testSystem
.getWhenTerminated()
.toCompletableFuture()
.get(settings.stopTimeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (Exception e) {
log.error("KalixTestkit ActorSystem failed to terminate", e);
}
try {
runner
.terminate()
.toCompletableFuture()
.get(settings.stopTimeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (Exception e) {
log.error("KalixTestkit KalixRunner failed to terminate", e);
}
started = false;
}
/**
* Get an available local port for testing.
*
* @return available local port
*/
public static int availableLocalPort() {
try (ServerSocket socket = new ServerSocket(0)) {
return socket.getLocalPort();
} catch (IOException e) {
throw new RuntimeException("Couldn't get available local port", e);
}
}
/**
* INTERNAL API
*/
@InternalApi
public MessageCodec getMessageCodec() {
return messageCodec;
}
/**
* INTERNAL API
*/
@InternalApi
public KalixRunner getRunner() {
return runner;
}
/**
* Returns {@link kalix.javasdk.testkit.EventingTestKit.MessageBuilder} utility
* to create {@link kalix.javasdk.testkit.EventingTestKit.Message}s for the eventing testkit.
*/
public EventingTestKit.MessageBuilder getMessageBuilder() {
return messageBuilder;
}
}