From 8e89f850e169a0f00d4d73d8ba926465e67fe668 Mon Sep 17 00:00:00 2001 From: akafredperry Date: Mon, 16 Mar 2026 15:50:20 +0000 Subject: [PATCH] test: some integration tests for the CLI code plus README enhancements --- at_client/README.md | 224 +++++++++++++++--- at_client/pom.xml | 7 + .../org/atsign/client/impl/AtClients.java | 4 + .../client/impl/AtCommandExecutors.java | 2 + .../atsign/client/impl/util/KeysUtils.java | 9 +- .../org/atsign/client/impl/cli/CliIT.java | 164 +++++++++++++ .../main/resources/simpleLogger.properties | 5 + 7 files changed, 371 insertions(+), 44 deletions(-) create mode 100644 at_client/src/test/java/org/atsign/client/impl/cli/CliIT.java create mode 100644 examples/src/main/resources/simpleLogger.properties diff --git a/at_client/README.md b/at_client/README.md index 639a2f6a..44b277e7 100644 --- a/at_client/README.md +++ b/at_client/README.md @@ -7,13 +7,14 @@ This module contains the AtSign Java SDK. If you are using maven, add the following to your pom.xml ```xml - - - org.atsign - at_client - 1.0.0 - - + + + + org.atsign + at_client + 1.0.0 + + ``` If you are using gradle, add the following to your build.gradle @@ -29,33 +30,29 @@ dependencies { The latest snapshot version can be added as a maven dependency like this... ```xml - - - Central Portal Snapshots - central-portal-snapshots - https://central.sonatype.com/repository/maven-snapshots - - false - - - true - - - - - - - org.atsign - at_client - 1.0.1-SNAPSHOT - - -``` - -## Logging -The SDK uses the slf4j-api. It is up to you to decide which slf4j binding to use. -See [slf4j providers](https://slf4j.org/manual.html#swapping). + + + Central Portal Snapshots + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots + + false + + + true + + + + + + + org.atsign + at_client + 1.0.1-SNAPSHOT + + +``` ## Dependencies @@ -67,19 +64,167 @@ The SDK currently depends on the following * Slf4j api (Simple logging facade that can be bound a variety of logging frameworks) -## Example Usage +## Logging -The command line classes under -[src/main/java/org/atsign/client/cli](src/main/java/org/atsign/client/impl/cli) -serve as simple examples of how to instantiate an AtClient instance and invoke its -interface. +The SDK uses the slf4j-api. It is up to you to decide which slf4j binding to use. +See [slf4j providers](https://slf4j.org/manual.html#swapping). + +If you are simply executing the example code below then you can just add slf4j-simple +as a dependency in your pom.xml. + +```xml + + org.slf4j + slf4j-simple + 2.0.13 + +``` + +## Getting Started + +The classes **org.atsign.client.impl.AtCommandExecutors** and +**org.atsign.client.impl.AtClients** +provide default implementations of the AtSign SDK interfaces ( +**org.atsign.client.api.AtCommandExecutor** and +**org.atsign.client.api.AtClient** respectively). + +An **org.atsign.client.api.AtCommandExecutor** represents something +that can send At Protocol commands to an At Server (including the root +/ directory sever). + +The package [commands](src/org/atsign/client/impl/commands) contains utility code +which implements the +[At Protocol Specification ](https://github.com/atsign-foundation/at_protocol/blob/trunk/specification/at_protocol_specification.md) +, for instance... + +* Authentication +* Notifications +* Verifying At Server responses +* Signing public key values +* End to end encryption / decryption for self keys and shared keys + +An **org.atsign.client.api.AtClient** provides a "map like" interface to +an At Server. Enabling you to put, get and delete the different types of +keys. By combining a default **org.atsign.client.api.AtCommandExecutor** and +the utility code in the **commands** package it provides the core capabilities +of the Java SDK. + +The following code illustrates putting some key values. + +```java +package org.example; + +import org.atsign.client.api.AtClient; +import org.atsign.client.api.AtSign; +import org.atsign.client.api.Keys; +import org.atsign.client.impl.AtClients; + +public class Main { + + public static void main(String[] args) throws Exception { + + AtSign atSign = AtSign.createAtSign("18prettyopera"); -See also the [examples](../examples) module. + try (AtClient client = AtClients.builder().atSign(atSign).build()) { + + Keys.PublicKey key = Keys.publicKeyBuilder() + .sharedBy(atSign) + .name("greeting") + .build(); + + client.put(key, "hello").get(); + + AtSign anotherAtSign = AtSign.createAtSign("41malakoff"); + + Keys.SharedKey anotherKey = Keys.sharedKeyBuilder() + .sharedBy(atSign) + .sharedWith(anotherAtSign) + .name("greeting") + .build(); + + client.put(anotherKey, "hola").get(); + + } + + } +} +``` +And this code illustrates creating getting the key values that were put in the +previous example code. + +```java +package org.example; + +import org.atsign.client.api.AtClient; +import org.atsign.client.api.AtSign; +import org.atsign.client.api.Keys; +import org.atsign.client.impl.AtClients; + +public class Main { + + public static void main(String[] args) throws Exception { + + AtSign atSign = AtSign.createAtSign("41malakoff"); + + try (AtClient client = AtClients.builder().atSign(atSign).build()) { + + AtSign anotherAtSign = AtSign.createAtSign("18prettyopera"); + + Keys.PublicKey key = Keys.publicKeyBuilder() + .sharedBy(anotherAtSign) + .name("greeting") + .build(); + + System.out.println(client.get(key).get()); + + Keys.SharedKey anotherKey = Keys.sharedKeyBuilder() + .sharedBy(anotherAtSign) + .sharedWith(atSign) + .name("greeting") + .build(); + + System.out.println(client.get(anotherKey).get()); + + } + + } +} +``` +The **AtClients.builder()** will create an **AtCommandExecutor** which will resolve the +endpoint for the atsign's at server from **root.atsign.org**. The **AtCommandExector** +will automatically retry connections and reconnect if it becomes disconnected. The +commands that are sent to the asign atserver will automatically timeout. The builder +will automatically load **AtKeys** which correspond to the atsign name and will assume +that these reside under ~/.atsign/keys. All of this behavior can be overridden with the +following builder fields. + +* **url** is used to set other root / directory endpoints, a proxy endpoint + or the explicit endpoint for the at server. +* **keys** is used to explicitly provide the **AtKeys** which will be used for + authentication, signing and encryption. +* **reconnect** is used to supply a **ReconnectStrategy** implementation that + controls if reconnect is supported at all, how many retries and reconnections + are supported, the pauses to apply between retries and when / if the at server + endpoint should be re-resolved. +* **timeoutMillis** is used to specify the command timeout +* **queueLimit** is used to specify how many commands can be queued. By default + the queue limit is zero. +* **awaitReadyMillis** is used to specify how long the builder blocks for the + **AtCommandExecutor** to become ready (to send commands). NOTE: This will NOT + trigger a build exception if the executor fails to become ready in that time, but + the first command that is sent might be queued. + +See the [examples](../examples) module. --- ## Command Line Utilities +The command line classes under +[src/main/java/org/atsign/client/cli](src/main/java/org/atsign/client/impl/cli) +serve as simple examples of how to instantiate an AtClient instance and invoke its +interface. + The following utilities provide a simple way to test behavior as-well as illustrating the fundamentals of using the SDK. @@ -144,6 +289,7 @@ mvn exec:java -Dexec.mainClass=org.atsign.client.cli.register.Register -Dexec.ar When using the SUPER_API Key to register an atsign, the following sequence of calls take place: + 1. User provides at_java/Register with the SUPER_API Key passed as an argument 2. at_java calls the AtSign Registrar API* Endpoint(get-atsign) with the SUPER_API Key provided diff --git a/at_client/pom.xml b/at_client/pom.xml index 897b3e3a..a9e4c642 100644 --- a/at_client/pom.xml +++ b/at_client/pom.xml @@ -219,6 +219,13 @@ test + + com.github.stefanbirkner + system-lambda + 1.2.1 + test + + diff --git a/at_client/src/main/java/org/atsign/client/impl/AtClients.java b/at_client/src/main/java/org/atsign/client/impl/AtClients.java index a7df018d..df86a059 100644 --- a/at_client/src/main/java/org/atsign/client/impl/AtClients.java +++ b/at_client/src/main/java/org/atsign/client/impl/AtClients.java @@ -37,6 +37,7 @@ public static AtClient createAtClient(String url, Long timeoutMillis, Long awaitReadyMillis, ReconnectStrategy reconnect, + Integer queueLimit, Boolean isVerbose) throws AtException { @@ -50,6 +51,7 @@ public static AtClient createAtClient(String url, .timeoutMillis(timeoutMillis) .awaitReadyMillis(awaitReadyMillis) .reconnect(reconnect) + .queueLimit(queueLimit) .isVerbose(isVerbose) .build(); @@ -76,6 +78,7 @@ public static AtClient createAtClient(String url, * .timeoutMillis() // timeout after which commands will complete exceptionally (optional) * .awaitReadyMillis() // how long to wait for executor to become ready during build() (optional) * .reconnect() // a ReconnectStrategy (optional) + * .queueLimit() // number of queued commands that are permitted (optional) * .isVerbose(...) // true or false (optional) * .build(); * } @@ -92,6 +95,7 @@ public static AtClient createAtClient(String url, * {@link AtCommandExecutors#DEFAULT_TIMEOUT_MILLIS}. * If reconnect is not set then the builder will default to a {@link SimpleReconnectStrategy} * with no limit to the retry attempts. + * If queueLimit is not set then the builder will default zero queued commands */ public static class AtClientBuilder { // required for javadoc diff --git a/at_client/src/main/java/org/atsign/client/impl/AtCommandExecutors.java b/at_client/src/main/java/org/atsign/client/impl/AtCommandExecutors.java index d560f253..625feab6 100644 --- a/at_client/src/main/java/org/atsign/client/impl/AtCommandExecutors.java +++ b/at_client/src/main/java/org/atsign/client/impl/AtCommandExecutors.java @@ -51,6 +51,7 @@ public static AtCommandExecutor createCommandExecutor(String url, Long timeoutMillis, Long awaitReadyMillis, ReconnectStrategy reconnect, + Integer queueLimit, Boolean isVerbose) throws AtException { @@ -64,6 +65,7 @@ public static AtCommandExecutor createCommandExecutor(String url, .timeoutMillis(defaultIfNotSet(timeoutMillis, DEFAULT_TIMEOUT_MILLIS)) .awaitReadyMillis(defaultIfNotSet(awaitReadyMillis, DEFAULT_TIMEOUT_MILLIS)) .reconnect(defaultIfNotSet(reconnect)) + .queueLimit(queueLimit) .onReady(createOnReady(atSign, keys)) .build(); } diff --git a/at_client/src/main/java/org/atsign/client/impl/util/KeysUtils.java b/at_client/src/main/java/org/atsign/client/impl/util/KeysUtils.java index 3bd620db..a9b6c040 100644 --- a/at_client/src/main/java/org/atsign/client/impl/util/KeysUtils.java +++ b/at_client/src/main/java/org/atsign/client/impl/util/KeysUtils.java @@ -122,13 +122,12 @@ private static File getKeysFileFallbackToLegacyLocation(AtSign atSign) throws At if (!file.exists()) { // if keys do not exist in root, check in keys sub-directory under current working directory - file = getKeysFile(atSign, legacyKeysFilesLocation); + File legacyFile = getKeysFile(atSign, legacyKeysFilesLocation); // if file does not exist under current working directory, we're done - can't find the keys file - if (!file.exists()) { - throw new AtClientConfigException("loadKeys: No file called " + atSign + keysFileSuffix - + " at " + expectedKeysFilesLocation + " or " + legacyKeysFilesLocation + - "\t Keys files are expected to be in ~/.atsign/keys/ (canonical location) or ./keys/ (legacy location)"); + if (!legacyFile.exists()) { + throw new AtClientConfigException("loadKeys: No file found at " + file + " or " + legacyFile); } + file = legacyFile; } return file; } diff --git a/at_client/src/test/java/org/atsign/client/impl/cli/CliIT.java b/at_client/src/test/java/org/atsign/client/impl/cli/CliIT.java new file mode 100644 index 00000000..b82cc981 --- /dev/null +++ b/at_client/src/test/java/org/atsign/client/impl/cli/CliIT.java @@ -0,0 +1,164 @@ +package org.atsign.client.impl.cli; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.security.SecureRandom; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.atsign.client.impl.util.KeysUtils; +import org.atsign.cucumber.helpers.Helpers; +import org.atsign.virtualenv.VirtualEnv; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.stefanbirkner.systemlambda.Statement; +import com.github.stefanbirkner.systemlambda.SystemLambda; + +class CliIT { + + public static final String VIRTUAL_ENV_ROOT = "vip.ve.atsign.zone:64"; + public static String AT_SIGN_KEYS_DIR; + public static String ATSIGN_KEYS_SUFFIX; + + @BeforeAll + public static void classSetup() { + if (!Helpers.isHostPortReachable(VIRTUAL_ENV_ROOT, SECONDS.toMillis(2))) { + VirtualEnv.setUp(); + } + AT_SIGN_KEYS_DIR = KeysUtils.expectedKeysFilesLocation; + ATSIGN_KEYS_SUFFIX = KeysUtils.keysFileSuffix; + KeysUtils.expectedKeysFilesLocation = "target/at_demo_data/lib/assets/atkeys"; + KeysUtils.keysFileSuffix = ".atKeys"; + } + + @AfterAll + public static void classTeardown() { + KeysUtils.expectedKeysFilesLocation = AT_SIGN_KEYS_DIR; + KeysUtils.keysFileSuffix = ATSIGN_KEYS_SUFFIX; + } + + @Test + public void testShareUsage() throws Exception { + int shareCode = runMainExpectSystemExit(() -> Share.main(new String[] {VIRTUAL_ENV_ROOT})); + assertThat(shareCode, not(equalTo(0))); + + int scanCode = runMainExpectSystemExit(() -> Scan.main(new String[] {VIRTUAL_ENV_ROOT})); + assertThat(scanCode, not(equalTo(0))); + + int getCode = runMainExpectSystemExit(() -> Get.main(new String[] {VIRTUAL_ENV_ROOT})); + assertThat(getCode, not(equalTo(0))); + + int deleteCode = runMainExpectSystemExit(() -> Delete.main(new String[] {VIRTUAL_ENV_ROOT})); + assertThat(deleteCode, not(equalTo(0))); + } + + @Test + public void testDumpKeys() throws Exception { + List stdout = runMain(() -> DumpKeys.main(new String[] {"colin"})); + findLines(stdout, + "\\s+key: aesPkamPublicKey", "\\s+value:\\s+\\S+", + "\\s+key: aesPkamPrivateKey", "\\s+value:\\s+\\S+"); + + } + + @Test + public void testShareScanGetDelete() throws Exception { + + String keyname = randomId(6); + + // create a new key value + + String[] shareArgs = {VIRTUAL_ENV_ROOT, "colin", "jeremy", keyname, "mary had a little lamb"}; + List sharedStdout = runMain(() -> Share.main(shareArgs)); + + assertThat(sharedStdout, is(empty())); + + // scan for new key value + + String[] scanArgs = {VIRTUAL_ENV_ROOT, "colin", keyname}; + List scanStdOut = runMain(() -> Scan.main(scanArgs), "l", "0", "q"); + + scanStdOut = findLines(scanStdOut, "Enter index you want to llookup.+"); + scanStdOut = findLines(scanStdOut, "\\s+0:\\s+.*" + keyname + ".*"); + findLines(scanStdOut, + "Full KeyName: @jeremy:" + keyname + "@colin", + "KeyName: " + keyname, + "SharedBy: @colin", + "SharedWith: @jeremy", + "KeyType: SharedKey"); + + // get the key value as the at sign which the key is shared with + + String[] getArgs = {VIRTUAL_ENV_ROOT, "jeremy", "colin", keyname}; + List getStdout = runMain(() -> Get.main(getArgs)); + findLines(getStdout, "get response : mary had a little lamb"); + + // delete the key value + + String[] deleteArgs = {VIRTUAL_ENV_ROOT, "colin", "jeremy", keyname}; + List deleteStdout = runMain(() -> Delete.main(deleteArgs)); + assertThat(deleteStdout, is(empty())); + + // verify it's gone + + List secondScanStdout = runMain(() -> Scan.main(scanArgs), "l", "q"); + assertThrows(AssertionError.class, () -> findLines(secondScanStdout, "\\s+0:\\s+.*" + keyname + ".*")); + } + + public static int runMainExpectSystemExit(Statement statement, String... stdin) throws Exception { + AtomicReference stdout = new AtomicReference<>(); + return SystemLambda.catchSystemExit(() -> stdout.set(SystemLambda.tapSystemErr(() -> { + SystemLambda.SystemInStub stub = SystemLambda.withTextFromSystemIn(String.join("\n", List.of(stdin))); + stub.execute(statement); + }))); + } + + public static List runMain(Statement statement, String... stdin) throws Exception { + AtomicReference stdout = new AtomicReference<>(); + stdout.set(SystemLambda.tapSystemOut(() -> { + SystemLambda.SystemInStub stub = SystemLambda.withTextFromSystemIn(String.join("\n", List.of(stdin))); + stub.execute(statement); + })); + return stdout.get().lines().collect(Collectors.toList()); + } + + public static List findLines(List lines, String... regexes) { + List result = lines; + for (String regex : regexes) { + result = findLine(result, Pattern.compile(regex).matcher("")); + } + return result; + } + + public static List findLine(List lines, Matcher matcher) { + for (int i = 0; i < lines.size(); i++) { + if (matcher.reset(lines.get(i)).matches()) { + return lines.subList(i, lines.size() - 1); + } + } + throw new AssertionError("unable to find line that matches : " + matcher.pattern().pattern() + " in\n" + + String.join("\n", lines)); + } + + private static final char[] ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz".toCharArray(); + + private static final SecureRandom RANDOM = new SecureRandom(); + + public static String randomId(int length) { + char[] result = new char[length]; + + for (int i = 0; i < length; i++) { + result[i] = ALPHABET[RANDOM.nextInt(ALPHABET.length)]; + } + + return new String(result); + } +} diff --git a/examples/src/main/resources/simpleLogger.properties b/examples/src/main/resources/simpleLogger.properties new file mode 100644 index 00000000..70f7fb47 --- /dev/null +++ b/examples/src/main/resources/simpleLogger.properties @@ -0,0 +1,5 @@ +org.slf4j.simpleLogger.defaultLogLevel=info +org.slf4j.simpleLogger.showDateTime=true +org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss.SSS + +#org.slf4j.simpleLogger.log.org.atsign.client=debug