test(java-cli): add in-process gRPC harness fixture

This commit is contained in:
Joseph Doherty
2026-06-16 16:55:48 -04:00
parent 4966ef3359
commit 70d2842c16
2 changed files with 219 additions and 0 deletions
@@ -6,6 +6,9 @@ dependencies {
implementation project(':zb-mom-ww-mxgateway-client')
implementation "com.google.protobuf:protobuf-java-util:${protobufVersion}"
implementation "info.picocli:picocli:${picocliVersion}"
testImplementation "io.grpc:grpc-inprocess:${grpcVersion}"
testImplementation "io.grpc:grpc-testing:${grpcVersion}"
}
application {
@@ -0,0 +1,216 @@
package com.zb.mom.ww.mxgateway.cli;
import com.zb.mom.ww.mxgateway.client.GalaxyRepositoryClient;
import com.zb.mom.ww.mxgateway.client.MxGatewayClient;
import com.zb.mom.ww.mxgateway.client.MxGatewayClientOptions;
import galaxy_repository.v1.GalaxyRepositoryGrpc;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest;
import io.grpc.ManagedChannel;
import io.grpc.Server;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.stub.StreamObserver;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import mxaccess_gateway.v1.MxAccessGatewayGrpc;
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
import mxaccess_gateway.v1.MxaccessGateway.SessionState;
/**
* Test fixture that stands up an in-process gRPC server hosting scripted fake
* {@code MxAccessGateway} and {@code GalaxyRepository} service implementations,
* so the real Java client types ({@link MxGatewayClient} /
* {@link GalaxyRepositoryClient}) can be driven over a real channel.
*
* <p>The real streaming wrappers ({@code MxEventStream} /
* {@code DeployEventStream}) have package-private constructors and
* {@link GalaxyRepositoryClient} is {@code final}, so the streaming and galaxy
* CLI commands cannot be exercised through the lightweight {@code FakeSession}
* seam. Driving the real client over an in-process channel against scripted
* services is the clean alternative; Tasks 5 and 6 add the CLI assertions on
* top of this fixture.
*
* <p>Scripted payloads are settable via constructor args or setters. Each
* instance uses a unique server name so harnesses do not collide. The
* {@code directExecutor()} wiring keeps all dispatch on the calling thread, so
* no background threads are leaked.
*/
final class InProcessGatewayHarness implements AutoCloseable {
private final String serverName;
private final Server server;
private final ManagedChannel channel;
private final FakeGatewayService fakeGateway;
private final FakeGalaxyService fakeGalaxy;
/** Starts a harness with empty scripted payloads; populate via setters. */
InProcessGatewayHarness() {
this(List.of(), List.of(), List.of());
}
/**
* Starts a harness with the supplied scripted payloads.
*
* @param scriptedEvents events {@code streamEvents} pushes before completing
* @param scriptedObjects objects {@code discoverHierarchy} returns (single page)
* @param scriptedDeployEvents events {@code watchDeployEvents} streams before completing
*/
InProcessGatewayHarness(
List<MxEvent> scriptedEvents,
List<GalaxyObject> scriptedObjects,
List<DeployEvent> scriptedDeployEvents) {
this.serverName = "mxgw-cli-harness-" + UUID.randomUUID();
this.fakeGateway = new FakeGatewayService(scriptedEvents);
this.fakeGalaxy = new FakeGalaxyService(scriptedObjects, scriptedDeployEvents);
try {
this.server = InProcessServerBuilder.forName(serverName)
.directExecutor()
.addService(fakeGateway)
.addService(fakeGalaxy)
.build()
.start();
} catch (IOException error) {
throw new IllegalStateException("failed to start in-process gateway harness", error);
}
this.channel = InProcessChannelBuilder.forName(serverName).directExecutor().build();
}
/** Replaces the scripted {@code streamEvents} payload. */
void setScriptedEvents(List<MxEvent> events) {
fakeGateway.scriptedEvents.clear();
fakeGateway.scriptedEvents.addAll(events);
}
/** Replaces the scripted {@code discoverHierarchy} payload. */
void setScriptedObjects(List<GalaxyObject> objects) {
fakeGalaxy.scriptedObjects.clear();
fakeGalaxy.scriptedObjects.addAll(objects);
}
/** Replaces the scripted {@code watchDeployEvents} payload. */
void setScriptedDeployEvents(List<DeployEvent> deployEvents) {
fakeGalaxy.scriptedDeployEvents.clear();
fakeGalaxy.scriptedDeployEvents.addAll(deployEvents);
}
/**
* Returns the in-process channel into the scripted services.
*
* @return the managed channel; lifecycle owned by the harness
*/
ManagedChannel channel() {
return channel;
}
/**
* Builds a real {@link MxGatewayClient} over the in-process channel.
*
* @return a client borrowing the harness channel
*/
MxGatewayClient gatewayClient() {
return new MxGatewayClient(channel, testOptions());
}
/**
* Builds a real {@link GalaxyRepositoryClient} over the in-process channel.
*
* @return a client borrowing the harness channel
*/
GalaxyRepositoryClient galaxyClient() {
return new GalaxyRepositoryClient(channel, testOptions());
}
private static MxGatewayClientOptions testOptions() {
return MxGatewayClientOptions.builder()
.endpoint("in-process")
.apiKey("mxgw_test_secret")
.plaintext(true)
.callTimeout(Duration.ofSeconds(5))
.build();
}
@Override
public void close() {
channel.shutdownNow();
server.shutdownNow();
}
private static ProtocolStatus ok() {
return ProtocolStatus.newBuilder()
.setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK)
.build();
}
/** Scripted fake of the {@code MxAccessGateway} service. */
private static final class FakeGatewayService extends MxAccessGatewayGrpc.MxAccessGatewayImplBase {
private final List<MxEvent> scriptedEvents = new CopyOnWriteArrayList<>();
FakeGatewayService(List<MxEvent> scriptedEvents) {
this.scriptedEvents.addAll(scriptedEvents);
}
@Override
public void streamEvents(
mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest request,
StreamObserver<MxEvent> responseObserver) {
for (MxEvent event : scriptedEvents) {
responseObserver.onNext(event);
}
responseObserver.onCompleted();
}
@Override
public void closeSession(
CloseSessionRequest request, StreamObserver<CloseSessionReply> responseObserver) {
responseObserver.onNext(CloseSessionReply.newBuilder()
.setSessionId(request.getSessionId())
.setFinalState(SessionState.SESSION_STATE_CLOSED)
.setProtocolStatus(ok())
.build());
responseObserver.onCompleted();
}
}
/** Scripted fake of the {@code GalaxyRepository} service. */
private static final class FakeGalaxyService extends GalaxyRepositoryGrpc.GalaxyRepositoryImplBase {
private final List<GalaxyObject> scriptedObjects = new CopyOnWriteArrayList<>();
private final List<DeployEvent> scriptedDeployEvents = new CopyOnWriteArrayList<>();
FakeGalaxyService(List<GalaxyObject> scriptedObjects, List<DeployEvent> scriptedDeployEvents) {
this.scriptedObjects.addAll(scriptedObjects);
this.scriptedDeployEvents.addAll(scriptedDeployEvents);
}
@Override
public void discoverHierarchy(
DiscoverHierarchyRequest request, StreamObserver<DiscoverHierarchyReply> responseObserver) {
List<GalaxyObject> snapshot = new ArrayList<>(scriptedObjects);
responseObserver.onNext(DiscoverHierarchyReply.newBuilder()
.setTotalObjectCount(snapshot.size())
.addAllObjects(snapshot)
.setNextPageToken("")
.build());
responseObserver.onCompleted();
}
@Override
public void watchDeployEvents(
WatchDeployEventsRequest request, StreamObserver<DeployEvent> responseObserver) {
for (DeployEvent event : scriptedDeployEvents) {
responseObserver.onNext(event);
}
responseObserver.onCompleted();
}
}
}