Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ FROM eclipse-temurin:21.0.9_10-jre-noble AS smoketest-harness
COPY --from=builder /app/server/setup/server-lib /opt/engine/server-lib
COPY --from=builder /app/server/setup/extensions /opt/engine/extensions
COPY --from=builder /app/server/setup/conf /opt/engine/conf
# The launcher resolves cli-lib/ relative to its working directory, so the CLI has
# to keep the layout it has in the distribution.
COPY --from=builder /app/server/setup/cli-lib /opt/engine/cli-lib
COPY --from=builder /app/server/setup/mirth-cli-launcher.jar /opt/engine/mirth-cli-launcher.jar
COPY --from=builder /app/smoketest/build/install/smoketest-harness /harness

ENTRYPOINT ["/bin/bash", "/harness/run-harness.sh"]
Expand Down
1 change: 1 addition & 0 deletions ci/run-harness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ java \
-Doie.baseUrl="$OIE_BASE_URL" \
-Doie.configuration="$OIE_CONFIGURATION" \
-Doie.password="$OIE_PASSWORD" \
-Doie.cliHome="$ENGINE_HOME" \
${OIE_DB_DRIVER:+-Doie.db.driver="$OIE_DB_DRIVER"} \
${OIE_DB_URL:+-Doie.db.url="$OIE_DB_URL"} \
${OIE_DB_USERNAME:+-Doie.db.username="$OIE_DB_USERNAME"} \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,19 @@ private void runShell(String server, String user, String password, String script
runConsole();
}
client.logout();
client.close();
out.println("Disconnected from server.");
} catch (ClientException ce) {
ce.printStackTrace();
} catch (IOException ioe) {
error("Could not load script file.", ioe);
} catch (URISyntaxException e) {
error("Invalid server address.", e);
} finally {
// The client's connection monitor is a non-daemon thread, so an unclosed
// client keeps the JVM alive instead of letting it exit.
if (client != null) {
client.close();
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Mitch Gaffigan

package org.openintegrationengine.smoketest;

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
* Runs the packaged command-line client ({@code mirth-cli-launcher.jar}) as a child
* process.
*
* <p>The launcher resolves {@code cli-lib/} and {@code ./extensions} relative to its
* working directory and reads its log configuration from {@code conf/} via its manifest
* {@code Class-Path}, so it only runs from a staged distribution.
*/
final class CommandLineClient {

/** Distribution root; ci/run-harness.sh points this at the tree in the harness image. */
private static final Path HOME = Path.of(System.getProperty("oie.cliHome", "/opt/engine"));

private static final long TIMEOUT_SECONDS = HarnessConfig.TIMEOUT.toSeconds();

private CommandLineClient() {
}

/** Runs the CLI against the server under test, feeding it {@code statements} as a script. */
static Result runScript(String... statements) throws Exception {
return runScriptAgainst(HarnessConfig.BASE_URL, HarnessConfig.USERNAME, HarnessConfig.PASSWORD,
statements);
}

/** As {@link #runScript}, against an address and credentials of the caller's choosing. */
static Result runScriptAgainst(String address, String user, String password, String... statements)
throws Exception {
Path script = Files.createTempFile("oie-cli-", ".script");
try {
Files.writeString(script, String.join("\n", statements) + "\n", StandardCharsets.UTF_8);
return run("-a", address, "-u", user, "-p", password, "-s", script.toString());
} finally {
Files.deleteIfExists(script);
}
}

/** Runs the CLI with exactly {@code args}. */
static Result run(String... args) throws Exception {
Path launcher = HOME.resolve("mirth-cli-launcher.jar");
if (!Files.isRegularFile(launcher) || !Files.isRegularFile(HOME.resolve("cli-lib/mirth-cli.jar"))) {
throw new AssertionError("No command-line client staged at " + HOME
+ "; the harness image is built to carry one (see the Dockerfile"
+ " smoketest-harness target) and ci/run-harness.sh sets oie.cliHome.");
}

List<String> command = new ArrayList<>();
command.add(Path.of(System.getProperty("java.home"), "bin", "java").toString());
command.add("-jar");
command.add(launcher.getFileName().toString());
command.addAll(Arrays.asList(args));

// A file rather than a pipe: reading a pipe would block forever on a CLI that
// never exits, so the timeout below could never fire.
Path outputFile = Files.createTempFile("oie-cli-", ".out");
try {
Process process = new ProcessBuilder(command)
.directory(HOME.toFile())
.redirectErrorStream(true)
.redirectOutput(outputFile.toFile())
.start();

// Without -s the CLI reads stdin; close it so such a run cannot wait forever.
process.getOutputStream().close();

boolean exited;
try {
exited = process.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS);
} finally {
process.destroyForcibly();
}

String output = Files.readString(outputFile, StandardCharsets.UTF_8);
if (!exited) {
throw new AssertionError("The CLI did not exit within " + TIMEOUT_SECONDS + "s: "
+ String.join(" ", command) + "\n--- output ---\n" + output + "--- end output ---");
}
return new Result(command, process.exitValue(), output);
} finally {
Files.deleteIfExists(outputFile);
}
}

record Result(List<String> command, int exitCode, String output) {

@Override
public String toString() {
return "exit=" + exitCode + " from " + String.join(" ", command)
+ "\n--- output ---\n" + output + "--- end output ---";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Mitch Gaffigan

package org.openintegrationengine.smoketest;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/**
* Smoke tests for the packaged command-line client, run as a child process against the
* live server.
*
* <p>Assertions are on output rather than on the exit code: the CLI exits 0 whether or
* not it could log in or run the statements it was given.
*/
@DisplayName("Command-line client")
class CommandLineInterfaceTest {

@Test
@DisplayName("prints usage for -h without contacting a server")
void printsUsageForHelp() throws Exception {
CommandLineClient.Result result = CommandLineClient.run("-h");

assertEquals(0, result.exitCode(), () -> "-h should exit 0, got " + result);
assertTrue(result.output().contains("usage: Shell"), () -> "no usage text in " + result);

for (String option : List.of("-a <address>", "-u <user>", "-p <password>",
"-s <script>", "-c <config file>")) {
assertTrue(result.output().contains(option),
() -> "usage does not offer " + option + " in " + result);
}
}

@Test
@DisplayName("logs in to the live server and reports its version")
void logsInAndReportsServerVersion() throws Exception {
// Read over the API, so a match proves the CLI reached this server rather than
// echoing back the address it was given.
String version = SharedServer.get().version();

CommandLineClient.Result result = CommandLineClient.runScript("status");

assertTrue(result.output().contains("Server @ " + HarnessConfig.BASE_URL + " (" + version + ")"),
() -> "the CLI did not report a connection to " + HarnessConfig.BASE_URL
+ " running " + version + ": " + result);
// Printed only after a successful logout.
assertTrue(result.output().contains("Disconnected from server."),
() -> "the CLI did not disconnect cleanly: " + result);
}

@Test
@DisplayName("exits instead of hanging when the server is unreachable")
void exitsWhenServerIsUnreachable() throws Exception {
// The client's connection monitor is a non-daemon thread: a CLI that leaves it
// running never exits, and this fails on the timeout rather than an assertion.
CommandLineClient.Result result = CommandLineClient.runScriptAgainst(
"https://127.0.0.1:1", HarnessConfig.USERNAME, HarnessConfig.PASSWORD, "status");

assertFalse(result.output().contains("Server @ "),
() -> "the CLI reported a connection it could not have made: " + result);
assertTrue(result.output().contains("ClientException"),
() -> "the CLI did not report why it could not connect: " + result);
}

@Test
@DisplayName("runs read-only commands against the live server")
void runsReadOnlyCommands() throws Exception {
CommandLineClient.Result result =
CommandLineClient.runScript("status", "channel list", "user list");

assertTrue(result.output().contains("Server @ " + HarnessConfig.BASE_URL),
() -> "the CLI never reported a connection: " + result);

assertTrue(result.output().contains("Status\t\tName"),
() -> "no status listing: " + result);
assertTrue(result.output().contains("Enabled\t\tName"),
() -> "no channel listing: " + result);
assertTrue(result.output().contains("User Name"),
() -> "no user listing: " + result);
assertTrue(result.output().contains(HarnessConfig.USERNAME),
() -> "user listing omits " + HarnessConfig.USERNAME + ": " + result);

// A failed statement prints "Error: ..." and still exits 0.
assertFalse(result.output().contains("Error:"),
() -> "a statement failed: " + result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ private void awaitStarted(String channelId, String label) throws Exception {
+ HarnessConfig.TIMEOUT.toSeconds() + "s; last state was " + lastState);
}

/** The server's reported version, as the CLI echoes it in its connect banner. */
String version() throws ClientException {
return client.getVersion();
}

/** Submits a source payload and returns the new message id. */
long submitMessage(String channelId, String rawData, Map<String, Object> sourceMap) throws ClientException {
RawMessage rawMessage = new RawMessage(rawData, null, sourceMap);
Expand Down
Loading