diff --git a/at_client/src/main/java/org/atsign/client/api/AtCommandExecutorContext.java b/at_client/src/main/java/org/atsign/client/api/AtCommandExecutorContext.java
index 3db7cfa9..987d28d5 100644
--- a/at_client/src/main/java/org/atsign/client/api/AtCommandExecutorContext.java
+++ b/at_client/src/main/java/org/atsign/client/api/AtCommandExecutorContext.java
@@ -2,42 +2,87 @@
import lombok.*;
+import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.unmodifiableMap;
+import static org.atsign.client.impl.common.Preconditions.checkNotNull;
+
/**
* The identity a connection authenticates as (its {@code atSign}, {@code keys} and {@code config})
* together with the single-use challenge from the {@code from:} that is issued as the first command
* once the connection is ready.
*
*
- * The context is created by the builder (see
- * {@code AtCommandExecutors#createCommandExecutor}) and closed over by the {@code onReady}
- * consumers it wires, so the {@code from:} sender can retain the challenge and the authentication
- * that follows on the same connection can reuse it rather than issuing a second {@code from:}.
- * The command executor itself is pure transport and knows nothing about this context.
+ * There is one context per connection. {@code AtClients#createAtClient} creates it and passes it to
+ * both the executor and the client; {@code AtCommandExecutors#createCommandExecutor} creates one
+ * itself when no caller supplies one. The {@code onReady} consumers hold a reference to it, so the
+ * {@code from:} sender can retain the challenge and the authentication that follows on the same
+ * connection can reuse it instead of issuing a second {@code from:}. The command executor itself
+ * only sends and receives, and never sees the context.
*
*
* The identity fields are fixed for the life of the context. The challenge is per-connection state:
* {@link #setChallenge(String) retained} when the initial {@code from:} completes and
* {@link #consumeChallenge() consumed} at most once (the server's {@code from:} challenge is
- * single-use). On reconnect the ready sequence re-runs, so a fresh challenge overwrites any
- * previous
- * one before it is consumed.
+ * single-use). On reconnect the ready sequence re-runs, so a fresh challenge replaces any
+ * previous one before it is consumed.
*/
@Value
public class AtCommandExecutorContext {
+ /**
+ * The atSign the connection authenticates as. Never {@code null}: a connection is built either from
+ * a context or from an atSign, and both establish an identity.
+ */
AtSign atSign;
+ /**
+ * The keys the connection authenticates with, or {@code null} for a connection that is not
+ * PKAM-authenticated — one that only issues {@code from:}, or one authenticating with CRAM before
+ * any keys exist.
+ */
AtKeys keys;
+ /**
+ * The client config sent with {@code from:}. Never {@code null} and never modifiable; empty
+ * when the connection has none.
+ */
Map config;
+ /**
+ * Holds the {@code from:} challenge for this connection.
+ */
@Getter(AccessLevel.NONE)
@EqualsAndHashCode.Exclude
@ToString.Exclude
- AtomicReference challenge = new AtomicReference<>();
+ AtomicReference challenge;
+
+ /**
+ * A context with no client config, for a connection whose {@code from:} carries no
+ * {@code clientConfig} segment.
+ *
+ * @param atSign the atSign the connection authenticates as
+ * @param keys the keys the connection authenticates with, or null if it is not PKAM-authenticated
+ */
+ public AtCommandExecutorContext(AtSign atSign, AtKeys keys) {
+ this(atSign, keys, null);
+ }
+
+ /**
+ * @param atSign the atSign the connection authenticates as
+ * @param keys the keys the connection authenticates with, or null if it is not PKAM-authenticated
+ * @param config the client config to send with {@code from:}. Copied, so a later change to the
+ * caller's map cannot alter what this connection sends; null is stored as an empty map
+ */
+ public AtCommandExecutorContext(AtSign atSign, AtKeys keys, Map config) {
+ this.atSign = checkNotNull(atSign, "atSign not set");
+ this.keys = keys;
+ this.config = config != null ? unmodifiableMap(new LinkedHashMap<>(config)) : emptyMap();
+ this.challenge = new AtomicReference<>();
+ }
/**
* Retains the challenge returned by the initial {@code from:}, so the authentication that follows
diff --git a/at_client/src/main/java/org/atsign/client/impl/AtClientImpl.java b/at_client/src/main/java/org/atsign/client/impl/AtClientImpl.java
index 7968dc88..87beaf29 100644
--- a/at_client/src/main/java/org/atsign/client/impl/AtClientImpl.java
+++ b/at_client/src/main/java/org/atsign/client/impl/AtClientImpl.java
@@ -35,8 +35,7 @@
*
*
* AtClientImplBuilder builder = AtClientImpl.builder()
- * .atSign(...)
- * .keys(...)
+ * .context(...)
* .executor(...)
* .eventBus(...);
*
@@ -50,10 +49,8 @@
@Slf4j
public class AtClientImpl implements AtClient {
- private final AtSign atSign;
- private final AtKeys keys;
- private MonitorOptions monitorOptions;
- private final Map config;
+ private final AtCommandExecutorContext context;
+ private final MonitorOptions monitorOptions;
private final AtCommandExecutor executor;
private final AtEventBus eventBus;
private final AtomicBoolean isMonitoring = new AtomicBoolean();
@@ -61,7 +58,7 @@ public class AtClientImpl implements AtClient {
@Override
public AtSign getAtSign() {
- return atSign;
+ return context.getAtSign();
}
@Override
@@ -70,23 +67,20 @@ public AtCommandExecutor getCommandExecutor() {
}
@Builder
- public AtClientImpl(AtSign atSign,
- AtKeys keys,
+ public AtClientImpl(AtCommandExecutorContext context,
boolean withMonitoring,
MonitorOptions monitorOptions,
- Map config,
AtCommandExecutor executor,
AtEventBus eventBus) {
- this.atSign = checkNotNull(atSign, "atSign not set");
- this.keys = checkNotNull(keys, "keys not set");
+ this.context = checkNotNull(context, "context not set");
+ checkNotNull(context.getKeys(), "keys not set");
+ checkNotNull(context.getKeys().getEncryptPrivateKey(), "keys have not been fully enrolled");
this.monitorOptions = monitorOptions != null ? monitorOptions : MonitorOptions.builder().build();
- this.config = config;
this.executor = checkNotNull(executor, "executor not set");
this.eventBus = checkNotNull(eventBus, "eventBus not set");
this.eventBus.addEventListener(this::handleEvent, EnumSet.allOf(AtEventType.class));
this.isMonitoring.set(withMonitoring);
- this.eventBusBridge = new Notifications.EventBusBridge(eventBus, atSign, this.monitorOptions);
- checkNotNull(keys.getEncryptPrivateKey(), "keys have not been fully enrolled");
+ this.eventBusBridge = new Notifications.EventBusBridge(eventBus, context.getAtSign(), this.monitorOptions);
}
/**
@@ -96,13 +90,16 @@ public AtClientImpl(AtSign atSign,
*
*
* AtClientImpl.builder()
- * .atSign(...) // the AtSign that this client will authenticate as
- * .keys(...) // the AtKeys that this client will use
+ * .context(...) // the connection context: atSign, keys and client config
* .executor() // the AtCommandExecutor this client will use
* .eventBus() // the AtEventBus this client will publish to
* .build();
* }
*
+ *
+ * NOTE the context must be the same instance the {@link AtCommandExecutor} was built with
+ * (see {@link AtClients#createAtClient}), so that the {@code from:} challenge and the client config
+ * are shared by the connection's authentication and by every command the client issues.
*/
public static class AtClientImplBuilder {
// required for javadoc
@@ -116,13 +113,13 @@ public void close() throws Exception {
@Override
public void startMonitor() {
isMonitoring.compareAndSet(false, true);
- executor.onReady(Notifications.monitor(atSign, monitorOptions, keys, config, eventBusBridge));
+ executor.onReady(Notifications.monitor(context, monitorOptions, eventBusBridge));
}
@Override
public void stopMonitor() {
isMonitoring.compareAndSet(true, false);
- executor.onReady(AuthenticationCommands.pkamAuthenticator(atSign, keys, config));
+ executor.onReady(AuthenticationCommands.pkamAuthenticator(context));
}
@Override
@@ -147,17 +144,17 @@ public int publishEvent(AtEventType eventType, Map eventData) {
@Override
public String get(SharedKey sharedKey) throws AtException {
- return SharedKeyCommands.get(executor, atSign, keys, sharedKey);
+ return SharedKeyCommands.get(executor, context, sharedKey);
}
@Override
public byte[] getBinary(SharedKey sharedKey) throws AtException {
- return Base2e15Utils.decode(SharedKeyCommands.get(executor, atSign, keys, sharedKey, true));
+ return Base2e15Utils.decode(SharedKeyCommands.get(executor, context, sharedKey, true));
}
@Override
public void put(SharedKey sharedKey, String value) throws AtException {
- SharedKeyCommands.put(executor, atSign, keys, sharedKey, value);
+ SharedKeyCommands.put(executor, context, sharedKey, value);
}
@Override
@@ -167,17 +164,17 @@ public void delete(SharedKey sharedKey) throws AtException {
@Override
public String get(SelfKey selfKey) throws AtException {
- return SelfKeyCommands.get(executor, atSign, keys, selfKey);
+ return SelfKeyCommands.get(executor, context, selfKey);
}
@Override
public byte[] getBinary(SelfKey selfKey) throws AtException {
- return Base2e15Utils.decode(SelfKeyCommands.get(executor, atSign, keys, selfKey, true));
+ return Base2e15Utils.decode(SelfKeyCommands.get(executor, context, selfKey, true));
}
@Override
public void put(SelfKey selfKey, String value) throws AtException {
- SelfKeyCommands.put(executor, atSign, keys, selfKey, value);
+ SelfKeyCommands.put(executor, context, selfKey, value);
}
@Override
@@ -192,7 +189,7 @@ public String get(PublicKey publicKey) throws AtException {
@Override
public String get(PublicKey publicKey, GetRequestOptions options) throws AtException {
- return PublicKeyCommands.get(executor, atSign, publicKey, options);
+ return PublicKeyCommands.get(executor, context, publicKey, options);
}
@Override
@@ -202,12 +199,12 @@ public byte[] getBinary(PublicKey publicKey) throws AtException {
@Override
public byte[] getBinary(PublicKey publicKey, GetRequestOptions options) throws AtException {
- return Base2e15Utils.decode(PublicKeyCommands.get(executor, atSign, publicKey, true, options));
+ return Base2e15Utils.decode(PublicKeyCommands.get(executor, context, publicKey, true, options));
}
@Override
public void put(PublicKey publicKey, String value) throws AtException {
- PublicKeyCommands.put(executor, atSign, keys, publicKey, value);
+ PublicKeyCommands.put(executor, context, publicKey, value);
}
@Override
@@ -262,6 +259,7 @@ private void onSharedKeyNotification(Map eventData) throws AtDec
// If we also got a value, we can decrypt it and add it to our keys map
// Note: a value isn't supplied when the ttr on the shared key was set to 0
if (eventData.get("value") != null) {
+ AtKeys keys = context.getKeys();
String keyName = (String) eventData.get("key");
String value = (String) eventData.get("value");
String decrypted = rsaDecryptFromBase64(value, keys.getEncryptPrivateKey());
@@ -272,6 +270,7 @@ private void onSharedKeyNotification(Map eventData) throws AtDec
private void onUpdateNotification(Map eventData) throws AtException {
// Let's see if we can decrypt it on the fly
if (eventData.get("value") != null) {
+ AtKeys keys = context.getKeys();
String encryptedValue = (String) eventData.get("value");
Map metadata = (Map) eventData.get("metadata");
String ivNonce = (String) metadata.get("ivNonce");
@@ -282,7 +281,7 @@ private void onUpdateNotification(Map eventData) throws AtExcept
} else {
String key = (String) eventData.get("key");
SharedKey sk = org.atsign.client.api.Keys.sharedKeyBuilder().rawKey(key).build();
- encryptKeySharedByOther = SharedKeyCommands.lookupEncryptKeySharedByOther(executor, keys, sk);
+ encryptKeySharedByOther = SharedKeyCommands.lookupEncryptKeySharedByOther(executor, context, sk);
}
String decryptedValue = aesDecryptFromBase64(encryptedValue, encryptKeySharedByOther, ivNonce);
HashMap newEventData = new HashMap<>(eventData);
@@ -292,8 +291,8 @@ private void onUpdateNotification(Map eventData) throws AtExcept
}
/**
- * A runnable command which returns a String value but can throw {@link AtException}s or execution
- * exceptions
+ * A runnable command returning a String; may throw {@link AtException}s or
+ * execution exceptions.
*/
public interface AtCommandThatReturnsString {
String run() throws AtException, ExecutionException, InterruptedException;
@@ -310,9 +309,8 @@ public static CompletableFuture wrapAsync(AtCommandThatReturnsString com
}
/**
- * A runnable command which returns a byte array value but can throw {@link AtException}s or
- * execution
- * exceptions
+ * A runnable command returning a byte array; may throw {@link AtException}s or
+ * execution exceptions.
*/
public interface AtCommandThatReturnsByteArray {
byte[] run() throws AtException, ExecutionException, InterruptedException;
@@ -329,8 +327,8 @@ public static CompletableFuture wrapAsync(AtCommandThatReturnsByteArray
}
/**
- * A runnable command which does NOT return a value but can throw {@link AtException}s or execution
- * exceptions
+ * A runnable command returning no value; may throw {@link AtException}s or
+ * execution exceptions.
*/
public interface AtCommandThatReturnsVoid {
void run() throws AtException, ExecutionException, InterruptedException;
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 203c2b39..5b61359d 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
@@ -8,6 +8,7 @@
import org.atsign.client.api.AtClient;
import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.api.AtCommandExecutorContext;
import org.atsign.client.api.AtKeys;
import org.atsign.client.api.AtSign;
import org.atsign.client.impl.commands.MonitorOptions;
@@ -63,18 +64,18 @@ public static AtClient createAtClient(String url,
monitorOptions = MonitorOptions.builder().build();
}
+ AtCommandExecutorContext context = createContext(atSign, keys, config);
+
Consumer onReady = null;
if (withMonitoring) {
Notifications.EventBusBridge eventBusBridge = new Notifications.EventBusBridge(eventBus, atSign, monitorOptions);
- onReady = Notifications.monitor(atSign, monitorOptions, keys, config, eventBusBridge);
+ onReady = createMonitoringOnReady(context, monitorOptions, eventBusBridge);
}
AtCommandExecutor executor = AtCommandExecutors.builder()
.url(url)
- .atSign(atSign)
- .keys(keys)
+ .context(context)
.onReady(onReady)
- .config(config)
.timeoutMillis(timeoutMillis)
.awaitReadyMillis(awaitReadyMillis)
.reconnect(reconnect)
@@ -83,16 +84,41 @@ public static AtClient createAtClient(String url,
.build();
return AtClientImpl.builder()
- .atSign(atSign)
- .keys(keys)
+ .context(context)
.withMonitoring(withMonitoring)
.monitorOptions(monitorOptions)
- .config(config)
.executor(executor)
.eventBus(eventBus)
.build();
}
+ /**
+ * The one context for the whole connection. The executor's {@code onReady} sequence and every
+ * command the client issues both read from it, so they share one {@code from:} challenge and one
+ * client config.
+ *
+ *
+ * The config is enriched here — {@code clientId} plus the {@code client-config.properties} entries
+ * — and enriched exactly once, because a second call would mint a second {@code clientId}.
+ *
+ *
+ * Package-private so a test can drive it together with {@link #createMonitoringOnReady}.
+ */
+ static AtCommandExecutorContext createContext(AtSign atSign, AtKeys keys, Map config) {
+ return new AtCommandExecutorContext(atSign, keys, AtCommandExecutors.createClientConfig(config));
+ }
+
+ /**
+ * The {@code onReady} sequence for a client built with monitoring: authenticate with PKAM using the
+ * connection's context, then send the {@code monitor} command. Package-private so that it can be
+ * driven with a stubbed {@link AtCommandExecutor} in tests.
+ */
+ static Consumer createMonitoringOnReady(AtCommandExecutorContext context,
+ MonitorOptions monitorOptions,
+ Consumer consumer) {
+ return Notifications.monitor(context, monitorOptions, consumer);
+ }
+
private static AtKeys loadKeys(String path, AtSign atSign) throws AtClientConfigException {
if (path == null) {
return KeysUtils.loadKeys(atSign);
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 70773ada..e7dfeb66 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
@@ -22,7 +22,7 @@
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
-import static org.atsign.client.impl.common.Preconditions.checkNotNull;
+import static org.atsign.client.impl.common.Preconditions.*;
/**
* Utility methods / builders for instantiating {@link AtCommandExecutor} implementations
@@ -42,10 +42,9 @@
*
* NOTE: If the url is prefixed with proxy (e.g. proxy:host:port) then the builder
* will automatically attempt to connect to an At Server at host:port.
- * NOTE: If an atSign is provided then the builder issues {@code from:@atSign} as the first
- * command once connected (so proxies / gateways can route the connection); if keys are also
- * provided it then authenticates the {@link AtCommandExecutor} with PKAM, reusing that
- * {@code from:}'s challenge.
+ * NOTE: The builder issues {@code from:@atSign} as the first command once connected (so
+ * proxies / gateways can route the connection); if keys are also provided it then authenticates the
+ * {@link AtCommandExecutor} with PKAM, reusing that {@code from:}'s challenge.
* NOTE: If reconnect is not set then the builder will default to a
* {@link SimpleReconnectStrategy}
*/
@@ -58,6 +57,7 @@ public class AtCommandExecutors {
public static AtCommandExecutor createCommandExecutor(String url,
AtSign atSign,
AtKeys keys,
+ AtCommandExecutorContext context,
Consumer onReady,
Map config,
Long timeoutMillis,
@@ -67,22 +67,16 @@ public static AtCommandExecutor createCommandExecutor(String url,
Boolean isVerbose)
throws AtException {
- if (AtEndpointSuppliers.isProxyUrl(url)) {
- checkNotNull(atSign, "atSign not set");
- }
-
- // the context is closed over by the onReady consumers the builder wires (see createOnReady); the
- // command executor itself stays pure transport and knows nothing about it
- AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, keys, createClientConfig(config));
+ AtCommandExecutorContext executorContext = resolveContext(context, atSign, keys, config);
return NettyAtCommandExecutor.builder()
- .endpoint(AtEndpointSuppliers.builder().url(url).atSign(atSign).build())
+ .endpoint(AtEndpointSuppliers.builder().url(url).atSign(executorContext.getAtSign()).build())
.isVerbose(isVerbose)
.timeoutMillis(defaultIfNotSet(timeoutMillis, DEFAULT_TIMEOUT_MILLIS))
.awaitReadyMillis(defaultIfNotSet(awaitReadyMillis, DEFAULT_TIMEOUT_MILLIS))
.reconnect(defaultIfNotSet(reconnect, SimpleReconnectStrategy.builder().build()))
.queueLimit(queueLimit)
- .onReady(defaultIfNotSet(onReady, createOnReady(context)))
+ .onReady(defaultIfNotSet(onReady, createOnReady(executorContext)))
.build();
}
@@ -94,8 +88,9 @@ public static AtCommandExecutor createCommandExecutor(String url,
*
* AtCommandExecutors.builder()
* .url(...) // the url for the root server or proxy (optional)
- * .atSign(...) // the AtSign that this client will authenticate as (optional)
+ * .atSign(...) // the AtSign that this client will authenticate as (unless context is set)
* .keys(...) // the AtKeys that this client will use (optional)
+ * .context(...) // the connection context to share with the caller (instead of the above)
* .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)
@@ -112,6 +107,11 @@ public static AtCommandExecutor createCommandExecutor(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 context is not set then the builder creates one from atSign, keys and
+ * config, and atSign is then required. Set context when the caller needs the
+ * same context for its own commands, so that the {@code from:} challenge and the client config are
+ * shared rather than duplicated; it carries the whole identity, so it cannot be combined with
+ * atSign, keys or config.
*/
public static class AtCommandExecutorBuilder {
// required for javadoc
@@ -135,16 +135,30 @@ public static Map createClientConfig(Map config)
}
/**
- * The default protocol for a newly-ready connection. A connection with no atSign (e.g. one talking
- * to the atDirectory / root server) sends nothing. Every connection that has an atSign issues
- * {@code from:@atSign} first so that proxies / gateways can route it; if keys are also present it
- * then authenticates with PKAM, reusing the challenge from that initial {@code from:}.
+ * The context the {@code onReady} consumers read the connection's identity from (see
+ * {@link #createOnReady}). The executor itself only sends and receives, and never sees it.
+ *
+ *
+ * A caller that needs the same context for its own commands passes one in, so that the connection
+ * has a single challenge and a single {@code clientId} rather than one of each per holder.
*/
- private static Consumer createOnReady(AtCommandExecutorContext context) {
- if (context.getAtSign() == null) {
- return c -> {
- };
+ private static AtCommandExecutorContext resolveContext(AtCommandExecutorContext context,
+ AtSign atSign,
+ AtKeys keys,
+ Map config) {
+ if (context != null) {
+ checkAllNull("both context and one or more of atSign,keys and config set", atSign, keys, config);
+ return context;
}
+ return new AtCommandExecutorContext(atSign, keys, createClientConfig(config));
+ }
+
+ /**
+ * The default protocol for a newly-ready connection: issue {@code from:@atSign} first so that
+ * proxies / gateways can route it, then, if keys are present, authenticate with PKAM reusing the
+ * challenge from that initial {@code from:}.
+ */
+ private static Consumer createOnReady(AtCommandExecutorContext context) {
Consumer onReady = AuthenticationCommands.sendFrom(context);
if (context.getKeys() != null) {
onReady = onReady.andThen(AuthenticationCommands.pkamAuthenticator(context));
diff --git a/at_client/src/main/java/org/atsign/client/impl/AtEndpointSuppliers.java b/at_client/src/main/java/org/atsign/client/impl/AtEndpointSuppliers.java
index f6ce24d8..35e105df 100644
--- a/at_client/src/main/java/org/atsign/client/impl/AtEndpointSuppliers.java
+++ b/at_client/src/main/java/org/atsign/client/impl/AtEndpointSuppliers.java
@@ -91,8 +91,4 @@ public static AtEndpointSupplier createEndpointSupplier(String url, AtSign atSig
public static class AtEndpointSuppliersBuilder {
// required for javadoc
}
-
- public static boolean isProxyUrl(String s) {
- return s != null && PATTERN_PROXY_URL.matcher(s).matches();
- }
}
diff --git a/at_client/src/main/java/org/atsign/client/impl/cli/AbstractCli.java b/at_client/src/main/java/org/atsign/client/impl/cli/AbstractCli.java
index 73a85e85..78f810ee 100644
--- a/at_client/src/main/java/org/atsign/client/impl/cli/AbstractCli.java
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/AbstractCli.java
@@ -113,7 +113,7 @@ protected AtCommandExecutor createAuthenticatedConnection(String rootUrl, AtSign
protected AtCommandExecutor createConnectionSendingFrom(AtCommandExecutorContext context) throws AtException {
return AtCommandExecutors.builder()
.url(rootUrl)
- .atSign(context.getAtSign())
+ .context(context)
.onReady(AuthenticationCommands.sendFrom(context))
.reconnect(ReconnectStrategy.NONE)
.isVerbose(verbose)
diff --git a/at_client/src/main/java/org/atsign/client/impl/cli/Activate.java b/at_client/src/main/java/org/atsign/client/impl/cli/Activate.java
index e7685bd7..4c3793b6 100644
--- a/at_client/src/main/java/org/atsign/client/impl/cli/Activate.java
+++ b/at_client/src/main/java/org/atsign/client/impl/cli/Activate.java
@@ -28,8 +28,8 @@
import picocli.CommandLine.Parameters;
/**
- * Utility (and CommandLineInterface) for onboarding and enrolling atSigns and AtSign application
- * devices
+ * Utility (and CommandLineInterface) for onboarding and enrolling atSigns and AtSign
+ * application devices.
*/
@Command(
mixinStandardHelpOptions = true)
@@ -37,7 +37,7 @@ public class Activate extends AbstractCli implements Callable
enum Action {
onboard, enroll, otp, list, approve, deny, revoke, unrevoke
- };
+ }
public static final String DEFAULT_FIRST_APP = "firstApp";
public static final String DEFAULT_FIRST_DEVICE = "firstDevice";
@@ -218,7 +218,7 @@ public void approve() throws Exception {
public void approve(EnrollmentId enrollmentId) throws Exception {
try (AtCommandExecutor executor = createAuthenticatedConnection(rootUrl, atSign, connectionRetries)) {
- EnrollCommands.approve(executor, getKeys(), enrollmentId);
+ EnrollCommands.approve(executor, newConnectionContext(getKeys()), enrollmentId);
}
}
@@ -286,7 +286,7 @@ public EnrollmentId enroll(AtCommandExecutor executor) throws Exception {
checkNotExists(file);
}
AtKeys keys = generateAtKeys(false);
- keys = EnrollCommands.enroll(executor, atSign, keys, otp, appName, deviceName, namespaces);
+ keys = EnrollCommands.enroll(executor, newConnectionContext(keys), otp, appName, deviceName, namespaces);
KeysUtils.saveKeys(keys, keysFile);
return keys.getEnrollmentId();
}
@@ -301,7 +301,7 @@ public void complete() throws Exception {
public void complete(AtCommandExecutor executor) throws Exception {
AtKeys keys = KeysUtils.loadKeys(keysFile);
- keys = EnrollCommands.complete(executor, atSign, keys);
+ keys = EnrollCommands.complete(executor, newConnectionContext(keys));
KeysUtils.saveKeys(keys, keysFile);
}
@@ -316,19 +316,24 @@ public void complete(AtCommandExecutor executor, int retries, long sleepDuration
throws Exception {
Exception exception;
int remainingRetries = retries;
+ AtKeys keys = KeysUtils.loadKeys(keysFile);
+ AtCommandExecutorContext context = newConnectionContext(keys);
do {
Thread.sleep(sleepUnit.toMillis(sleepDuration));
try {
- complete(executor);
+ keys = EnrollCommands.complete(executor, context);
+ saveKeys(keys, keysFile);
return;
- } catch (AtUnauthenticatedException e) {
- exception = e.getMessage().contains("is pending") ? null : e;
} catch (Exception e) {
exception = e;
}
- } while (exception == null && remainingRetries-- > 0);
+ } while (isPendingApproval(exception) && remainingRetries-- > 0);
+
+ throw exception;
+ }
- throw exception != null ? exception : new IllegalArgumentException();
+ private static boolean isPendingApproval(Exception e) {
+ return e instanceof AtUnauthenticatedException && e.getMessage().contains("is pending");
}
protected static AtKeys generateAtKeys(boolean generateEncryptionKeyPair) throws AtEncryptionException {
diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/AuthenticationCommands.java b/at_client/src/main/java/org/atsign/client/impl/commands/AuthenticationCommands.java
index f4ac9b54..6e5d1336 100644
--- a/at_client/src/main/java/org/atsign/client/impl/commands/AuthenticationCommands.java
+++ b/at_client/src/main/java/org/atsign/client/impl/commands/AuthenticationCommands.java
@@ -9,7 +9,6 @@
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
-import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
@@ -18,7 +17,6 @@
import org.atsign.client.api.AtCommandExecutorContext;
import org.atsign.client.impl.util.EncryptionUtils;
import org.atsign.client.impl.exceptions.AtException;
-import org.atsign.client.api.AtSign;
import org.atsign.client.impl.exceptions.AtEncryptionException;
import org.atsign.client.impl.exceptions.AtUnauthenticatedException;
@@ -58,70 +56,33 @@ public static Consumer sendFrom(AtCommandExecutorContext cont
* @return an onReady consumer that performs PKAM authentication
*/
public static Consumer pkamAuthenticator(AtCommandExecutorContext context) {
- return throwOnReadyException(executor -> authenticateWithPkam(executor, context.getAtSign(),
- context.getKeys(), context.getConfig(),
- context.consumeChallenge()));
- }
-
- public static Consumer pkamAuthenticator(AtSign atSign, AtKeys keys, Map config) {
- return throwOnReadyException(executor -> authenticateWithPkam(executor, atSign, keys, config));
- }
-
- /**
- * Implements the protocol workflow / sequence for PKAM authentication.
- *
- * @param executor The executor with which to send the commands.
- * @param atSign The asign to authenticate.
- * @param keys The keys to use to authenticate.
- * @throws AtException If authentication fails.
- */
- public static void authenticateWithPkam(AtCommandExecutor executor, AtSign atSign, AtKeys keys)
- throws AtException {
- authenticateWithPkam(executor, atSign, keys, null);
+ return throwOnReadyException(executor -> authenticateWithPkam(executor, context));
}
/**
- * Implements the protocol workflow / sequence for PKAM authentication.
+ * Implements the protocol workflow / sequence for PKAM authentication, using the identity in the
+ * given {@code context}. Reuses the challenge from an initial {@code from:} (as issued by
+ * {@link #sendFrom(AtCommandExecutorContext)}) when the context holds one, and otherwise issues its
+ * own {@code from:}.
*
* @param executor The executor with which to send the commands.
- * @param atSign The asign to authenticate.
- * @param keys The keys to use to authenticate.
- * @param config The map of configuration values to send in the from command.
+ * @param context The connection context; supplies the atSign, keys and client config, and holds
+ * the {@code from:} challenge.
* @throws AtException If authentication fails.
*/
- public static void authenticateWithPkam(AtCommandExecutor executor,
- AtSign atSign,
- AtKeys keys,
- Map config)
- throws AtException {
- authenticateWithPkam(executor, atSign, keys, config, null);
- }
-
- /**
- * Implements the protocol workflow / sequence for PKAM authentication, reusing an already-issued
- * {@code from:} challenge when one is supplied.
- *
- * @param executor The executor with which to send the commands.
- * @param atSign The asign to authenticate.
- * @param keys The keys to use to authenticate.
- * @param config The map of configuration values to send in the from command.
- * @param reusableChallenge The challenge from an initial {@code from:} to reuse, or {@code null} to
- * issue a fresh {@code from:}.
- * @throws AtException If authentication fails.
- */
- private static void authenticateWithPkam(AtCommandExecutor executor,
- AtSign atSign,
- AtKeys keys,
- Map config,
- String reusableChallenge)
+ public static void authenticateWithPkam(AtCommandExecutor executor, AtCommandExecutorContext context)
throws AtException {
+ AtKeys keys = context.getKeys();
try {
// reuse the challenge from the initial from: if one was issued on this connection, otherwise
// send a from command and expect to receive a challenge
- String challenge = reusableChallenge;
+ String challenge = context.consumeChallenge();
if (challenge == null) {
- String fromCommand = CommandBuilders.fromCommandBuilder().atSign(atSign).config(config).build();
+ String fromCommand = CommandBuilders.fromCommandBuilder()
+ .atSign(context.getAtSign())
+ .config(context.getConfig())
+ .build();
String fromResponse = executor.sendSync(fromCommand);
challenge = matchDataStringNoWhitespace(throwExceptionIfError(fromResponse));
}
diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/EnrollCommands.java b/at_client/src/main/java/org/atsign/client/impl/commands/EnrollCommands.java
index 963eaa88..df079330 100644
--- a/at_client/src/main/java/org/atsign/client/impl/commands/EnrollCommands.java
+++ b/at_client/src/main/java/org/atsign/client/impl/commands/EnrollCommands.java
@@ -84,9 +84,11 @@ public static AtKeys onboard(AtCommandExecutor executor,
.build();
// authenticate with PKAM — issues its own from: (the connection's challenge was single-use and
- // is spent by CRAM above; PKAM also authenticates with the freshly-enrolled keys, not the
- // connection identity)
- AuthenticationCommands.authenticateWithPkam(executor, atSign, keys);
+ // is spent by CRAM above). The keys now carry the enrollment id, so this needs a context built
+ // over them rather than the connection's; the client config is carried across so the from: it
+ // sends still identifies the client
+ AtCommandExecutorContext enrolled = new AtCommandExecutorContext(atSign, keys, context.getConfig());
+ AuthenticationCommands.authenticateWithPkam(executor, enrolled);
// explicitly store the public encryption key in the atserver
String updateCommand = CommandBuilders.updateCommandBuilder()
@@ -143,14 +145,14 @@ public static String otp(AtCommandExecutor executor) throws AtException {
/**
* Performs the enrollment request commands for a new application / device set of keys.
* NOTE The result of this command will be a pending request that must be approved
- * ({@link #approve(AtCommandExecutor, AtKeys, EnrollmentId)}) using keys that have access
- * to the manage namespace (the original keys from onboard) and then completed
- * ({@link #complete(AtCommandExecutor, AtSign, AtKeys)}).
+ * ({@link #approve(AtCommandExecutor, AtCommandExecutorContext, EnrollmentId)}) using keys that
+ * have access to the manage namespace (the original keys from onboard) and then completed
+ * ({@link #complete(AtCommandExecutor, AtCommandExecutorContext)}).
*
* @param executor An executor to an atserver command interface.
- * @param atSign The AtSign that corresponds to the executor.
- * @param keys The {@link AtKeys} for the {@link AtSign}, this should have the APKAM keys populated.
- * The rest of the fields will be set once enrollment is complated.
+ * @param context The connection context; supplies the atSign and the {@link AtKeys} being
+ * enrolled, which should have the APKAM keys populated. The rest of the fields will be set
+ * once enrollment is completed.
* @param otp A one time password.
* @param appName The app name for this enrollment.
* @param deviceName The device name for this enrollment.
@@ -160,14 +162,15 @@ public static String otp(AtCommandExecutor executor) throws AtException {
* @throws AtException If any of the commands fail.
*/
public static AtKeys enroll(AtCommandExecutor executor,
- AtSign atSign,
- AtKeys keys,
+ AtCommandExecutorContext context,
String otp,
String appName,
String deviceName,
Map namespaces)
throws Exception {
+ AtSign atSign = context.getAtSign();
+ AtKeys keys = context.getKeys();
checkNotNull(keys.getApkamPublicKey(), "apkam public key not set");
checkNotNull(keys.getApkamSymmetricKey(), "apkam symmetric key not set");
@@ -206,18 +209,20 @@ public static AtKeys enroll(AtCommandExecutor executor,
* has been approved.
*
* @param executor An executor to an atserver command interface.
- * @param atSign The AtSign that corresponds to the executor.
- * @param keys The {@link AtKeys} for the {@link AtSign}, this should have the APKAM keys populated.
- * The rest of the fields will be set once enrollment is completed.
- * @return A new copy of the {@link AtKeys} that has the private encrypt key and self encrypt key
- * set.
+ * @param context The connection context; supplies the atSign and the {@link AtKeys} being
+ * enrolled, which should have the APKAM keys populated. The rest of the fields will be set
+ * once enrollment is completed.
+ * @return A new copy of the {@link AtKeys} with the private encrypt key and self encrypt key set.
* These need to be persisted by the caller.
* @throws AtException If any of the commands fail.
*/
- public static AtKeys complete(AtCommandExecutor executor, AtSign atSign, AtKeys keys) throws AtException {
+ public static AtKeys complete(AtCommandExecutor executor, AtCommandExecutorContext context) throws AtException {
+
+ AtSign atSign = context.getAtSign();
+ AtKeys keys = context.getKeys();
// attempt to authenticate with PKAM, this will succeed once the enroll request is approved
- AuthenticationCommands.authenticateWithPkam(executor, atSign, keys);
+ AuthenticationCommands.authenticateWithPkam(executor, context);
// Use the keys:get command to obtain the private encryption key and self encryption key
String selfEncryptKey = keysGetSelfEncryptKey(executor, atSign, keys);
@@ -263,12 +268,16 @@ public static List list(AtCommandExecutor executor, String status)
* Performs the enrollment approve commands for a new application / device set of keys.
*
* @param executor An executor to an atserver command interface.
- * @param keys The {@link AtKeys} for the {@link AtSign} that have the authority to manage
- * enrollment requests. Typically the first set of keys from the onboard.
+ * @param context The connection context; supplies the {@link AtKeys} that have the authority to
+ * manage enrollment requests. Typically the first set of keys from the onboard.
* @param enrollmentId The {@link EnrollmentId} to approve.
* @throws AtException If any of the commands fail.
*/
- public static void approve(AtCommandExecutor executor, AtKeys keys, EnrollmentId enrollmentId) throws AtException {
+ public static void approve(AtCommandExecutor executor,
+ AtCommandExecutorContext context,
+ EnrollmentId enrollmentId)
+ throws AtException {
+ AtKeys keys = context.getKeys();
try {
// fetch the request and decrypt the apkam symmetric key that will be used encrypt the shared keys
@@ -321,8 +330,7 @@ public static void deny(AtCommandExecutor executor, EnrollmentId enrollmentId) t
}
/**
- * Performs the enrollment revoke commands for a previously approved application / device set of
- * keys.
+ * Performs the enrollment revoke commands for a previously approved application / device key set.
*
* @param executor An executor to an atserver command interface.
* @param enrollmentId The {@link EnrollmentId} to revoke.
@@ -333,8 +341,7 @@ public static void revoke(AtCommandExecutor executor, EnrollmentId enrollmentId)
}
/**
- * Performs the enrollment revoke commands for a previously revoked application / device set of
- * keys.
+ * Performs the enrollment unrevoke commands for a previously revoked application / device key set.
*
* @param executor An executor to an atserver command interface.
* @param enrollmentId The {@link EnrollmentId} to unrevoke.
diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/Notifications.java b/at_client/src/main/java/org/atsign/client/impl/commands/Notifications.java
index e2e8dba1..de7beee6 100644
--- a/at_client/src/main/java/org/atsign/client/impl/commands/Notifications.java
+++ b/at_client/src/main/java/org/atsign/client/impl/commands/Notifications.java
@@ -12,8 +12,8 @@
import java.util.regex.Pattern;
import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.api.AtCommandExecutorContext;
import org.atsign.client.api.AtEvents;
-import org.atsign.client.api.AtKeys;
import org.atsign.client.api.AtSign;
import org.atsign.client.impl.exceptions.AtException;
@@ -31,66 +31,41 @@ public class Notifications {
/**
* Creates a {@link Consumer} that can be passed to {@link AtCommandExecutor#onReady(Consumer)} to
- * request notifications. NOTE monitoring is contingent on authentication to this will
- * authenticate
- * with pkam prior to sending the monitor command.
+ * request notifications. NOTE monitoring is contingent on authentication so this will
+ * authenticate with pkam prior to sending the monitor command.
*
- * @param atSign The {@link AtSign} to authenticate.
+ * @param context The connection context; supplies the atSign / keys / client config and the
+ * {@code from:} challenge.
* @param options optional arguments that influence the server behavior.
- * @param keys The {@link AtKeys} to authenticate with.
* @param consumer A consumer that will be invoked with each notification.
* @return A consumer that can be provided as OnReady argument.
*/
- public static Consumer monitor(AtSign atSign,
+ public static Consumer monitor(AtCommandExecutorContext context,
MonitorOptions options,
- AtKeys keys,
- Map config,
Consumer consumer) {
- return throwOnReadyException(executor -> monitor(executor, atSign, options, keys, config, consumer));
+ return throwOnReadyException(executor -> monitor(executor, context, options, consumer));
}
/**
* Sends the commands to perform PKAM authentication followed by monitor command.
*
* @param executor The {@link AtCommandExecutor} to use.
- * @param atSign The {@link AtSign} to authenticate.
+ * @param context The connection context; supplies the atSign / keys / client config and the
+ * {@code from:} challenge.
* @param options optional arguments that influence the server behavior.
- * @param keys The {@link AtKeys} to authenticate with.
* @param consumer A consumer that will be invoked with each notification.
* @throws AtException If any of the commands fail.
*/
public static void monitor(AtCommandExecutor executor,
- AtSign atSign,
+ AtCommandExecutorContext context,
MonitorOptions options,
- AtKeys keys,
- Consumer consumer)
- throws AtException {
- monitor(executor, atSign, options, keys, null, consumer);
- }
-
- /**
- * Sends the commands to perform PKAM authentication followed by monitor command.
- *
- * @param executor The {@link AtCommandExecutor} to use.
- * @param atSign The {@link AtSign} to authenticate.
- * @param options optional arguments that influence the server behavior.
- * @param keys The {@link AtKeys} to authenticate with.
- * @param config The map of configuration values to send with the from command.
- * @param consumer A consumer that will be invoked with each notification.
- * notifications.
- * @throws AtException If any of the commands fail.
- */
- public static void monitor(AtCommandExecutor executor,
- AtSign atSign,
- MonitorOptions options,
- AtKeys keys,
- Map config,
Consumer consumer)
throws AtException {
try {
- // authenticate
- authenticateWithPkam(executor, atSign, keys, config);
+ // authenticate; the context supplies the client config and any challenge already retained by
+ // the initial from: on this connection
+ authenticateWithPkam(executor, context);
// send monitor command
String command = CommandBuilders.monitorCommandBuilder().options(options).build();
diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/PublicKeyCommands.java b/at_client/src/main/java/org/atsign/client/impl/commands/PublicKeyCommands.java
index 6ce4df93..f1ed4e25 100644
--- a/at_client/src/main/java/org/atsign/client/impl/commands/PublicKeyCommands.java
+++ b/at_client/src/main/java/org/atsign/client/impl/commands/PublicKeyCommands.java
@@ -11,6 +11,7 @@
import org.atsign.client.api.AtClient.GetRequestOptions;
import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.api.AtCommandExecutorContext;
import org.atsign.client.api.AtKeys;
import org.atsign.client.api.AtSign;
import org.atsign.client.api.Keys.PublicKey;
@@ -27,34 +28,38 @@ public class PublicKeyCommands {
* Get the String value associated with a public key.
*
* @param executor The {@link AtCommandExecutor} to use.
- * @param atSign The AtSign that corresponds to the executor.
+ * @param context The connection context; supplies the atSign.
* @param key The {@link PublicKey}
* @param options If set then can be used to bypass caches.
* @return The associated value.
* @throws AtException If any of the commands fail or the key does not exist.
*/
- public static String get(AtCommandExecutor executor, AtSign atSign, PublicKey key, GetRequestOptions options)
+ public static String get(AtCommandExecutor executor,
+ AtCommandExecutorContext context,
+ PublicKey key,
+ GetRequestOptions options)
throws AtException {
- return get(executor, atSign, key, false, options);
+ return get(executor, context, key, false, options);
}
/**
* Get the String value associated with a public key.
*
* @param executor The {@link AtCommandExecutor} to use.
- * @param atSign The AtSign that corresponds to the executor.
+ * @param context The connection context; supplies the atSign.
* @param key The {@link PublicKey}
+ * @param expectBinary If true then metadata will be checked
* @param options If set then can be used to bypass caches.
* @return The associated value.
* @throws AtException If any of the commands fail or the key does not exist.
*/
public static String get(AtCommandExecutor executor,
- AtSign atSign,
+ AtCommandExecutorContext context,
PublicKey key,
boolean expectBinary,
GetRequestOptions options)
throws AtException {
- if (atSign.equals(key.sharedBy())) {
+ if (context.getAtSign().equals(key.sharedBy())) {
return getSharedByMe(executor, key, expectBinary);
} else {
return getSharedByOther(executor, key, expectBinary, options);
@@ -154,13 +159,15 @@ public static String getSharedByOther(AtCommandExecutor executor,
* metadata using the AtSign's Private Encryption Key.
*
* @param executor The {@link AtCommandExecutor} to use.
- * @param atSign The AtSign that corresponds to the executor.
+ * @param context The connection context; supplies the atSign and keys.
* @param key The {@link PublicKey}
* @param value The associated value.
* @throws AtException If any of the commands fail or the key does not exist.
*/
- public static void put(AtCommandExecutor executor, AtSign atSign, AtKeys keys, PublicKey key, String value)
+ public static void put(AtCommandExecutor executor, AtCommandExecutorContext context, PublicKey key, String value)
throws AtException {
+ AtSign atSign = context.getAtSign();
+ AtKeys keys = context.getKeys();
checkAtSignCanPut(atSign, key);
try {
diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/SelfKeyCommands.java b/at_client/src/main/java/org/atsign/client/impl/commands/SelfKeyCommands.java
index bf967a27..7a0cf1bc 100644
--- a/at_client/src/main/java/org/atsign/client/impl/commands/SelfKeyCommands.java
+++ b/at_client/src/main/java/org/atsign/client/impl/commands/SelfKeyCommands.java
@@ -25,13 +25,14 @@ public class SelfKeyCommands {
* Self Encryption Key.
*
* @param executor The {@link AtCommandExecutor} to use.
- * @param atSign The AtSign that corresponds to the executor.
+ * @param context The connection context; supplies the atSign and keys.
* @param key The {@link Keys.SelfKey}
* @return The associated value.
* @throws AtException If any of the commands fail or the key does not exist.
*/
- public static String get(AtCommandExecutor executor, AtSign atSign, AtKeys keys, SelfKey key) throws AtException {
- return get(executor, atSign, keys, key, false);
+ public static String get(AtCommandExecutor executor, AtCommandExecutorContext context, SelfKey key)
+ throws AtException {
+ return get(executor, context, key, false);
}
/**
@@ -39,18 +40,19 @@ public static String get(AtCommandExecutor executor, AtSign atSign, AtKeys keys,
* Self Encryption Key.
*
* @param executor The {@link AtCommandExecutor} to use.
- * @param atSign The AtSign that corresponds to the executor.
+ * @param context The connection context; supplies the atSign and keys.
* @param key The {@link Keys.SelfKey}
* @param expectBinary If true then metadata will be checked
* @return The associated value.
* @throws AtException If any of the commands fail or the key does not exist.
*/
public static String get(AtCommandExecutor executor,
- AtSign atSign,
- AtKeys keys,
+ AtCommandExecutorContext context,
SelfKey key,
boolean expectBinary)
throws AtException {
+ AtSign atSign = context.getAtSign();
+ AtKeys keys = context.getKeys();
checkAtSignCanGet(atSign, key);
try {
@@ -79,17 +81,18 @@ public static String get(AtCommandExecutor executor,
/**
* Set a String value to be associated with a self key. The value will be encrypted with the
- * AtSign's
- * Self Encryption Key.
+ * AtSign's Self Encryption Key.
*
* @param executor The {@link AtCommandExecutor} to use.
- * @param atSign The AtSign that corresponds to the executor.
+ * @param context The connection context; supplies the atSign and keys.
* @param key The {@link Keys.SelfKey}
* @param value The associated value.
* @throws AtException If any of the commands fail or the key does not exist.
*/
- public static void put(AtCommandExecutor executor, AtSign atSign, AtKeys keys, SelfKey key, String value)
+ public static void put(AtCommandExecutor executor, AtCommandExecutorContext context, SelfKey key, String value)
throws AtException {
+ AtSign atSign = context.getAtSign();
+ AtKeys keys = context.getKeys();
checkAtSignCanPut(atSign, key);
try {
diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/SharedKeyCommands.java b/at_client/src/main/java/org/atsign/client/impl/commands/SharedKeyCommands.java
index 6e3116cc..d83163f3 100644
--- a/at_client/src/main/java/org/atsign/client/impl/commands/SharedKeyCommands.java
+++ b/at_client/src/main/java/org/atsign/client/impl/commands/SharedKeyCommands.java
@@ -28,14 +28,14 @@ public class SharedKeyCommands {
* key for the sharedBy-sharedWith relationship.
*
* @param executor The {@link AtCommandExecutor} to use.
- * @param atSign The AtSign that corresponds to the executor.
+ * @param context The connection context; supplies the atSign and keys.
* @param key The {@link Keys.SharedKey}
* @return The associated value.
* @throws AtException If any of the commands fail or the key does not exist.
*/
-
- public static String get(AtCommandExecutor executor, AtSign atSign, AtKeys keys, SharedKey key) throws AtException {
- return get(executor, atSign, keys, key, false);
+ public static String get(AtCommandExecutor executor, AtCommandExecutorContext context, SharedKey key)
+ throws AtException {
+ return get(executor, context, key, false);
}
/**
@@ -43,46 +43,47 @@ public static String get(AtCommandExecutor executor, AtSign atSign, AtKeys keys,
* key for the sharedBy-sharedWith relationship.
*
* @param executor The {@link AtCommandExecutor} to use.
- * @param atSign The AtSign that corresponds to the executor.
+ * @param context The connection context; supplies the atSign and keys.
* @param key The {@link Keys.SharedKey}
* @param expectedBinary If true then lookup metadata will be checked
* @return The associated value.
* @throws AtException If any of the commands fail or the key does not exist.
*/
-
- public static String get(AtCommandExecutor executor, AtSign atSign, AtKeys keys, SharedKey key,
+ public static String get(AtCommandExecutor executor, AtCommandExecutorContext context, SharedKey key,
boolean expectedBinary)
throws AtException {
+ AtSign atSign = context.getAtSign();
checkAtSignCanGet(atSign, key);
if (key.sharedBy().equals(atSign)) {
- return getSharedByMe(executor, keys, key, expectedBinary);
+ return getSharedByMe(executor, context, key, expectedBinary);
} else if (key.sharedWith().equals(atSign)) {
- return getSharedByOther(executor, keys, key, expectedBinary);
+ return getSharedByOther(executor, context, key, expectedBinary);
} else {
throw new IllegalArgumentException("the client atsign is neither the sharedBy or sharedWith");
}
}
/**
- * Set a String value to be associated with a shared key. The value will be decrypted with a
- * specific
- * key for the sharedBy-sharedWith relationship.
+ * Set a String value to be associated with a shared key. The value will be encrypted with a key
+ * specific to the sharedBy-sharedWith relationship.
*
* @param executor The {@link AtCommandExecutor} to use.
- * @param atSign The AtSign that corresponds to the executor.
+ * @param context The connection context; supplies the atSign and keys.
* @param key The {@link Keys.SharedKey}
* @param value The associated value.
* @throws AtException If any of the commands fail or the key does not exist.
*/
- public static void put(AtCommandExecutor executor, AtSign atSign, AtKeys keys, SharedKey key, String value)
+ public static void put(AtCommandExecutor executor, AtCommandExecutorContext context, SharedKey key, String value)
throws AtException {
+ AtSign atSign = context.getAtSign();
+ AtKeys keys = context.getKeys();
checkAtSignCanPut(atSign, key);
try {
// get or create key for sharedBy - sharedWith
- String aesKey = lookupEncryptKeySharedByMe(executor, keys, key);
+ String aesKey = lookupEncryptKeySharedByMe(executor, context, key);
if (aesKey == null) {
- aesKey = createEncryptKey(executor, keys, key);
+ aesKey = createEncryptKey(executor, context, key);
}
// encrypt the value
@@ -102,7 +103,10 @@ public static void put(AtCommandExecutor executor, AtSign atSign, AtKeys keys, S
}
}
- private static String getSharedByMe(AtCommandExecutor executor, AtKeys keys, SharedKey key, boolean expectBinary)
+ private static String getSharedByMe(AtCommandExecutor executor,
+ AtCommandExecutorContext context,
+ SharedKey key,
+ boolean expectBinary)
throws AtException {
try {
@@ -115,7 +119,7 @@ private static String getSharedByMe(AtCommandExecutor executor, AtKeys keys, Sha
}
// get the encryption key that was previously created by "me"
- String aesKey = checkNotNull(lookupEncryptKeySharedByMe(executor, keys, key), key + " not found");
+ String aesKey = checkNotNull(lookupEncryptKeySharedByMe(executor, context, key), key + " not found");
// return decrypted value
return aesDecryptFromBase64(llookupResponse.data, aesKey, llookupResponse.metaData.ivNonce());
@@ -125,7 +129,10 @@ private static String getSharedByMe(AtCommandExecutor executor, AtKeys keys, Sha
}
}
- private static String getSharedByOther(AtCommandExecutor executor, AtKeys keys, SharedKey key, boolean expectBinary)
+ private static String getSharedByOther(AtCommandExecutor executor,
+ AtCommandExecutorContext context,
+ SharedKey key,
+ boolean expectBinary)
throws AtException {
try {
@@ -140,9 +147,9 @@ private static String getSharedByOther(AtCommandExecutor executor, AtKeys keys,
// get the encryption key that was created by the "other"
String sharedEncryptionKey;
if (lookupResponse.metaData.sharedKeyEnc() != null) {
- sharedEncryptionKey = extractEncryptKeySharedByOther(lookupResponse, keys);
+ sharedEncryptionKey = extractEncryptKeySharedByOther(lookupResponse, context.getKeys());
} else {
- sharedEncryptionKey = lookupEncryptKeySharedByOther(executor, keys, key);
+ sharedEncryptionKey = lookupEncryptKeySharedByOther(executor, context, key);
}
// return decrypted value
@@ -172,13 +179,16 @@ private static String extractEncryptKeySharedByOther(LookupResponse lookupRespon
* AtSign. This will automatically decrypt the value with the AtKeys Private Encryption Key.
*
* @param executor The {@link AtCommandExecutor} to use.
- * @param keys The {@link AtKeys} for the {@link AtSign} that is the sharedBy in the relationship.
+ * @param context The connection context; supplies the keys of the sharedBy {@link AtSign}.
* @param key The {@link Keys.SharedKey}
* @return The symmetric encryption key (in base64).
* @throws AtException If any of the commands fail or the key does not exist.
*/
- public static String lookupEncryptKeySharedByMe(AtCommandExecutor executor, AtKeys keys, SharedKey key)
+ public static String lookupEncryptKeySharedByMe(AtCommandExecutor executor,
+ AtCommandExecutorContext context,
+ SharedKey key)
throws AtException {
+ AtKeys keys = context.getKeys();
try {
String keyName = AtKeyNames.toSharedByMeKeyName(key.sharedWith());
@@ -217,13 +227,16 @@ public static String lookupEncryptKeySharedByMe(AtCommandExecutor executor, AtKe
* AtSign. This will automatically decrypt the value with the AtKeys Private Encryption Key.
*
* @param executor The {@link AtCommandExecutor} to use.
- * @param keys The {@link AtKeys} for the {@link AtSign} that is the sharedWith in the relationship.
+ * @param context The connection context; supplies the keys of the sharedWith {@link AtSign}.
* @param key The {@link Keys.SharedKey}
* @return The symmetric encryption key (in base64).
* @throws AtException If any of the commands fail or the key does not exist.
*/
- public static String lookupEncryptKeySharedByOther(AtCommandExecutor executor, AtKeys keys, SharedKey key)
+ public static String lookupEncryptKeySharedByOther(AtCommandExecutor executor,
+ AtCommandExecutorContext context,
+ SharedKey key)
throws AtException {
+ AtKeys keys = context.getKeys();
try {
// check in Keys cache
@@ -254,7 +267,11 @@ public static String lookupEncryptKeySharedByOther(AtCommandExecutor executor, A
}
}
- private static String createEncryptKey(AtCommandExecutor executor, AtKeys keys, SharedKey key) throws AtException {
+ private static String createEncryptKey(AtCommandExecutor executor,
+ AtCommandExecutorContext context,
+ SharedKey key)
+ throws AtException {
+ AtKeys keys = context.getKeys();
try {
// generate a new encrypt key
diff --git a/at_client/src/test/java/org/atsign/client/api/AtCommandExecutorContextTest.java b/at_client/src/test/java/org/atsign/client/api/AtCommandExecutorContextTest.java
new file mode 100644
index 00000000..f32f8b12
--- /dev/null
+++ b/at_client/src/test/java/org/atsign/client/api/AtCommandExecutorContextTest.java
@@ -0,0 +1,74 @@
+package org.atsign.client.api;
+
+import static org.atsign.client.api.AtSign.createAtSign;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.anEmptyMap;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+class AtCommandExecutorContextTest {
+
+ @Test
+ void testConfigIsAnEmptyMapWhenNotSupplied() {
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null);
+
+ assertThat(context.getConfig(), anEmptyMap());
+ }
+
+ @Test
+ void testConfigIsAnEmptyMapWhenNull() {
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null);
+
+ assertThat(context.getConfig(), anEmptyMap());
+ }
+
+ @Test
+ void testConfigCannotBeModified() {
+ Map config = new HashMap<>();
+ config.put("clientId", "abc");
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null, config);
+
+ assertThrows(UnsupportedOperationException.class, () -> context.getConfig().put("clientId", "hijacked"));
+ }
+
+ @Test
+ void testConfigIsCopiedSoLaterCallerChangesDoNotLeakIn() {
+ Map config = new HashMap<>();
+ config.put("clientId", "abc");
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null, config);
+
+ config.put("clientId", "changed");
+ config.put("added", "later");
+
+ // what the connection sends is fixed at construction
+ assertThat(context.getConfig(), hasEntry("clientId", (Object) "abc"));
+ assertThat(context.getConfig(), not(hasEntry("added", (Object) "later")));
+ }
+
+ @Test
+ void testAnAtSignIsRequired() {
+ // a context is a connection's identity, and every connection issues from:@atSign
+ IllegalArgumentException ex =
+ assertThrows(IllegalArgumentException.class, () -> new AtCommandExecutorContext(null, null));
+
+ assertThat(ex.getMessage(), equalTo("atSign not set"));
+ }
+
+ @Test
+ void testAContextHoldsAChallengeAtMostOnce() {
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null);
+
+ context.setChallenge("challenge");
+
+ assertThat(context.consumeChallenge(), equalTo("challenge"));
+ assertThat(context.consumeChallenge(), nullValue());
+ }
+}
diff --git a/at_client/src/test/java/org/atsign/client/impl/AtClientImplTest.java b/at_client/src/test/java/org/atsign/client/impl/AtClientImplTest.java
index a645768d..e5b8bb58 100644
--- a/at_client/src/test/java/org/atsign/client/impl/AtClientImplTest.java
+++ b/at_client/src/test/java/org/atsign/client/impl/AtClientImplTest.java
@@ -29,6 +29,7 @@ class AtClientImplTest {
private AtCommandExecutor executor;
private AtClientImpl client;
private AtSign atSign;
+ private AtCommandExecutorContext context;
@BeforeEach
void setUp() throws Exception {
@@ -40,9 +41,9 @@ void setUp() throws Exception {
.encryptKeyPair(generateRSAKeyPair())
.selfEncryptKey(generateAESKeyBase64())
.build();
+ context = new AtCommandExecutorContext(atSign, keys);
client = AtClientImpl.builder()
- .atSign(atSign)
- .keys(keys)
+ .context(context)
.executor(executor)
.eventBus(bus)
.build();
@@ -54,8 +55,7 @@ void tearDown() {}
@Test
void testSetAtSignReturnsConstructorArg() {
AtClientImpl client = AtClientImpl.builder()
- .atSign(createAtSign("test"))
- .keys(keys)
+ .context(context)
.executor(executor)
.eventBus(bus)
.build();
@@ -65,8 +65,7 @@ void testSetAtSignReturnsConstructorArg() {
@Test
void testGetCommandExecutorReturnsConstructorArg() {
AtClientImpl client = AtClientImpl.builder()
- .atSign(createAtSign("test"))
- .keys(keys)
+ .context(context)
.executor(executor)
.eventBus(bus)
.build();
@@ -85,13 +84,14 @@ void testStartMonitor() throws Exception {
// stub the executor so that onReady consumer successfully authenticates
executor = TestExecutorBuilder.builder()
- .stub("from:@alice", "data:challenge")
+ .stub("from:@alice:clientConfig:.*12345.*", "data:challenge")
.stub("pkam:[^{].+", "data:success")
.build();
+ Map config = Collections.singletonMap("clientId", "12345");
+ AtCommandExecutorContext contextAlice = new AtCommandExecutorContext(createAtSign("alice"), keys, config);
AtClientImpl client = AtClientImpl.builder()
- .atSign(createAtSign("alice"))
- .keys(keys)
+ .context(contextAlice)
.executor(executor)
.eventBus(bus)
.build();
@@ -127,13 +127,14 @@ void testStartMonitor() throws Exception {
void testStopMonitor() throws Exception {
// stub the executor so that onReady consumer successfully authenticates
executor = TestExecutorBuilder.builder()
- .stub("from:@alice", "data:challenge")
+ .stub("from:@alice:clientConfig:.*12345.*", "data:challenge")
.stub("pkam:[^{].+", "data:success")
.build();
+ Map config = Collections.singletonMap("clientId", "12345");
+ AtCommandExecutorContext contextAlice = new AtCommandExecutorContext(createAtSign("alice"), keys, config);
AtClientImpl client = AtClientImpl.builder()
- .atSign(createAtSign("alice"))
- .keys(keys)
+ .context(contextAlice)
.executor(executor)
.eventBus(bus)
.build();
@@ -193,7 +194,11 @@ void testGetSelfKey() throws Exception {
.stubExecutionException("llookup:all:key3@test")
.build();
- AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build();
+ AtClientImpl client = AtClientImpl.builder()
+ .context(context)
+ .executor(executor)
+ .eventBus(bus)
+ .build();
Keys.SelfKey key1 = Keys.selfKeyBuilder().sharedBy(atSign).name("key1").build();
assertThat(client.get(key1), equalTo("hello me"));
@@ -219,7 +224,11 @@ void testGetSelfKeyBinary() throws Exception {
.stubExecutionException("llookup:all:key3@test")
.build();
- AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build();
+ AtClientImpl client = AtClientImpl.builder()
+ .context(context)
+ .executor(executor)
+ .eventBus(bus)
+ .build();
Keys.SelfKey key1 = Keys.selfKeyBuilder().sharedBy(atSign).name("key1").build();
assertThat(client.getBinary(key1), equalTo(bytes));
@@ -243,7 +252,11 @@ void testPutSelfKey() throws Exception {
.stubExecutionException("update:dataSignature:.+:key3@test .+")
.build();
- AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build();
+ AtClientImpl client = AtClientImpl.builder()
+ .context(context)
+ .executor(executor)
+ .eventBus(bus)
+ .build();
Keys.SelfKey key1 = Keys.selfKeyBuilder().sharedBy(atSign).name("key1").build();
client.put(key1, "hello world");
@@ -265,7 +278,11 @@ void testPutSelfKeyBytes() throws Exception {
.stubExecutionException("update:.+:key3@test .+")
.build();
- AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build();
+ AtClientImpl client = AtClientImpl.builder()
+ .context(context)
+ .executor(executor)
+ .eventBus(bus)
+ .build();
Keys.SelfKey key1 = Keys.selfKeyBuilder().sharedBy(atSign).name("key1").build();
client.put(key1, bytes);
@@ -284,7 +301,11 @@ void testDeleteSelfKey() throws Exception {
.stubExecutionException("delete:key3@test")
.build();
- AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build();
+ AtClientImpl client = AtClientImpl.builder()
+ .context(context)
+ .executor(executor)
+ .eventBus(bus)
+ .build();
Keys.SelfKey key1 = Keys.selfKeyBuilder().sharedBy(atSign).name("key1").build();
client.delete(key1);
@@ -304,7 +325,11 @@ void testGetPublicKey() throws Exception {
.stubLookupResponse("plookup:all:key4@another", "key4@test", "greetings from another world")
.build();
- AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build();
+ AtClientImpl client = AtClientImpl.builder()
+ .context(context)
+ .executor(executor)
+ .eventBus(bus)
+ .build();
Keys.PublicKey key1 = Keys.publicKeyBuilder().sharedBy(atSign).name("key1").build();
assertThat(client.get(key1), equalTo("hello world"));
@@ -334,7 +359,11 @@ void testGetPublicKeyBinary() throws Exception {
.stubLookupResponse("llookup:all:public:key5@test", "key5@test", Base2e15Utils.encode(bytes1))
.build();
- AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build();
+ AtClientImpl client = AtClientImpl.builder()
+ .context(context)
+ .executor(executor)
+ .eventBus(bus)
+ .build();
Keys.PublicKey key1 = Keys.publicKeyBuilder().sharedBy(atSign).name("key1").build();
assertThat(client.getBinary(key1), equalTo(bytes1));
@@ -360,7 +389,11 @@ void testPutPublicKey() throws Exception {
.stubExecutionException("update:dataSignature:.+:key3@test .+")
.build();
- AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build();
+ AtClientImpl client = AtClientImpl.builder()
+ .context(context)
+ .executor(executor)
+ .eventBus(bus)
+ .build();
Keys.PublicKey key1 = Keys.publicKeyBuilder().sharedBy(atSign).name("key1").build();
client.put(key1, "hello world");
@@ -383,7 +416,11 @@ void testPutPublicKeyBytes() throws Exception {
.stubExecutionException("update:dataSignature:.+:key3@test .+")
.build();
- AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build();
+ AtClientImpl client = AtClientImpl.builder()
+ .context(context)
+ .executor(executor)
+ .eventBus(bus)
+ .build();
Keys.PublicKey key1 = Keys.publicKeyBuilder().sharedBy(atSign).name("key1").build();
client.put(key1, bytes);
@@ -402,7 +439,11 @@ void testDeletePublicKey() throws Exception {
.stubExecutionException("delete:public:key3@test")
.build();
- AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build();
+ AtClientImpl client = AtClientImpl.builder()
+ .context(context)
+ .executor(executor)
+ .eventBus(bus)
+ .build();
Keys.PublicKey key1 = Keys.publicKeyBuilder().sharedBy(atSign).name("key1").build();
client.delete(key1);
@@ -432,7 +473,11 @@ void testGetSharedKey() throws Exception {
"ivNonce", iv, "sharedKeyEnc", sharedKeyEnc)
.build();
- AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build();
+ AtClientImpl client = AtClientImpl.builder()
+ .context(context)
+ .executor(executor)
+ .eventBus(bus)
+ .build();
AtSign atSign2 = createAtSign("another");
Keys.SharedKey key1 = Keys.sharedKeyBuilder().sharedBy(atSign).sharedWith(atSign2).name("key1").build();
@@ -472,7 +517,11 @@ void testGetSharedKeyBinary() throws Exception {
.stubLookupResponse("llookup:all:@another:key5@test", "key5@test", encrypted1, "ivNonce", iv)
.build();
- AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build();
+ AtClientImpl client = AtClientImpl.builder()
+ .context(context)
+ .executor(executor)
+ .eventBus(bus)
+ .build();
AtSign atSign2 = createAtSign("another");
Keys.SharedKey key1 = Keys.sharedKeyBuilder().sharedBy(atSign).sharedWith(atSign2).name("key1").build();
@@ -501,7 +550,11 @@ void testPutSharedKey() throws Exception {
.stubExecutionException("update:.+:key3@test .+")
.build();
- AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build();
+ AtClientImpl client = AtClientImpl.builder()
+ .context(context)
+ .executor(executor)
+ .eventBus(bus)
+ .build();
AtSign atSign2 = createAtSign("another");
Keys.SharedKey key1 = Keys.sharedKeyBuilder().sharedBy(atSign).sharedWith(atSign2).name("key1").build();
@@ -527,7 +580,11 @@ void testPutSharedKeyBytes() throws Exception {
.stubExecutionException("update:.+:key3@test .+")
.build();
- AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build();
+ AtClientImpl client = AtClientImpl.builder()
+ .context(context)
+ .executor(executor)
+ .eventBus(bus)
+ .build();
AtSign atSign2 = createAtSign("another");
Keys.SharedKey key1 = Keys.sharedKeyBuilder().sharedBy(atSign).sharedWith(atSign2).name("key1").build();
@@ -548,7 +605,11 @@ void testDeleteSharedKey() throws Exception {
.stubExecutionException("delete:@another:key3@test")
.build();
- AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build();
+ AtClientImpl client = AtClientImpl.builder()
+ .context(context)
+ .executor(executor)
+ .eventBus(bus)
+ .build();
AtSign atSign2 = createAtSign("another");
Keys.SharedKey key1 = Keys.sharedKeyBuilder().sharedBy(atSign).sharedWith(atSign2).name("key1").build();
diff --git a/at_client/src/test/java/org/atsign/client/impl/AtClientsTest.java b/at_client/src/test/java/org/atsign/client/impl/AtClientsTest.java
new file mode 100644
index 00000000..4cf16d0e
--- /dev/null
+++ b/at_client/src/test/java/org/atsign/client/impl/AtClientsTest.java
@@ -0,0 +1,118 @@
+package org.atsign.client.impl;
+
+import static org.atsign.client.api.AtSign.createAtSign;
+import static org.atsign.client.impl.util.EncryptionUtils.generateRSAKeyPair;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.hasKey;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.matches;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.util.Collections;
+import java.util.Map;
+
+import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.api.AtCommandExecutorContext;
+import org.atsign.client.api.AtKeys;
+import org.atsign.client.impl.commands.MonitorOptions;
+import org.atsign.client.impl.commands.TestExecutorBuilder;
+import org.atsign.client.impl.common.ReconnectStrategy;
+import org.atsign.client.impl.exceptions.AtException;
+import org.junit.jupiter.api.Test;
+
+class AtClientsTest {
+
+ private static final String CLIENT_CONFIG_WITH_CLIENT_ID = "from:@alice:clientConfig:.*\"clientId\".*";
+
+ private AtKeys keys() throws Exception {
+ return AtKeys.builder().apkamKeyPair(generateRSAKeyPair()).build();
+ }
+
+ @Test
+ void testCreateContextEnrichesTheConfigWithTheClientIdentity() {
+ AtCommandExecutorContext context = AtClients.createContext(createAtSign("@alice"), null, null);
+
+ // clientId plus the client-config.properties entries, even though the caller passed no
+ // config at all — this is what identifies the client to the At Server on the from:
+ assertThat(context.getConfig(), hasKey("clientId"));
+ assertThat(context.getConfig(), hasEntry("platform", "Java"));
+ assertThat(context.getConfig(), hasKey("version"));
+ }
+
+ @Test
+ void testCreateContextKeepsCallerSuppliedConfigEntries() {
+ Map config = Collections.singletonMap("myOwnKey", "myOwnValue");
+
+ AtCommandExecutorContext context = AtClients.createContext(createAtSign("@alice"), null, config);
+
+ assertThat(context.getConfig(), hasKey("clientId"));
+ assertThat(context.getConfig(), hasKey("myOwnKey"));
+ }
+
+ @Test
+ void testMonitoringOnReadyIdentifiesTheClientInItsFromCommand() throws Exception {
+ // the stub only answers a from: that carries a clientConfig, so a bare from: fails the run
+ AtCommandExecutor executor = TestExecutorBuilder.builder()
+ .stub("from:@alice:clientConfig:.+", "data:challenge")
+ .stub("pkam:[^{].+", "data:success")
+ .build();
+ AtCommandExecutorContext context = AtClients.createContext(createAtSign("@alice"), keys(), null);
+
+ AtClients.createMonitoringOnReady(context, MonitorOptions.builder().build(), s -> {
+ }).accept(executor);
+
+ verify(executor, times(1)).sendSync(matches(CLIENT_CONFIG_WITH_CLIENT_ID));
+ verify(executor, times(1)).sendSync(matches("pkam:.*"));
+ }
+
+ @Test
+ void testMonitoringOnReadyReusesAnAlreadyRetainedChallenge() throws Exception {
+ AtCommandExecutor executor = TestExecutorBuilder.builder()
+ .stub("pkam:[^{].+", "data:success")
+ .build();
+ AtCommandExecutorContext context = AtClients.createContext(createAtSign("@alice"), keys(), null);
+ context.setChallenge("challenge");
+
+ AtClients.createMonitoringOnReady(context, MonitorOptions.builder().build(), s -> {
+ }).accept(executor);
+
+ // no from: at all — the challenge the connection already holds is used instead, which is only
+ // possible because the monitoring path and the executor share one context
+ verify(executor, times(0)).sendSync(matches("from:.*"));
+ verify(executor, times(1)).sendSync(matches("pkam:.*"));
+ assertThat(context.consumeChallenge(), nullValue());
+ }
+
+ /**
+ * A proxy url so that the builder connects directly rather than resolving through the root server,
+ * pointing at a port nothing is listening on so that the build fails promptly and locally once it
+ * gets as far as connecting.
+ */
+ private String unservedProxyUrl() throws IOException {
+ try (ServerSocket socket = new ServerSocket(0)) {
+ return "proxy:localhost:" + socket.getLocalPort();
+ }
+ }
+
+ @Test
+ void testCreateAtClientHandsTheExecutorBuilderTheSharedContextAndNothingElse() throws Exception {
+ String url = unservedProxyUrl();
+ AtKeys keys = keys();
+
+ // the executor builder rejects a context combined with a loose atSign / keys / config, so
+ // an AtException (rather than an IllegalArgumentException) is what says this passes the
+ // connection's identity once, as the shared context
+ assertThrows(AtException.class, () -> AtClients.builder()
+ .atSign(createAtSign("@alice"))
+ .keys(keys)
+ .url(url)
+ .reconnect(ReconnectStrategy.NONE)
+ .awaitReadyMillis(500L)
+ .build());
+ }
+}
diff --git a/at_client/src/test/java/org/atsign/client/impl/AtCommandExecutorsTest.java b/at_client/src/test/java/org/atsign/client/impl/AtCommandExecutorsTest.java
new file mode 100644
index 00000000..5546e636
--- /dev/null
+++ b/at_client/src/test/java/org/atsign/client/impl/AtCommandExecutorsTest.java
@@ -0,0 +1,124 @@
+package org.atsign.client.impl;
+
+import static org.atsign.client.api.AtSign.createAtSign;
+import static org.atsign.client.impl.util.EncryptionUtils.generateRSAKeyPair;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.util.Collections;
+
+import org.atsign.client.api.AtCommandExecutorContext;
+import org.atsign.client.api.AtKeys;
+import org.atsign.client.impl.common.ReconnectStrategy;
+import org.atsign.client.impl.exceptions.AtException;
+import org.junit.jupiter.api.Test;
+
+class AtCommandExecutorsTest {
+
+ private static final String CONTEXT_WITH_LOOSE_ARGS =
+ "both context and one or more of atSign,keys and config set";
+ private static final String ATSIGN_NOT_SET = "atSign not set";
+
+ private AtCommandExecutorContext aliceContext() {
+ return AtClients.createContext(createAtSign("@alice"), null, null);
+ }
+
+ private AtKeys keys() throws Exception {
+ return AtKeys.builder().apkamKeyPair(generateRSAKeyPair()).build();
+ }
+
+ /**
+ * A proxy url so that the builder connects directly rather than resolving through the root server,
+ * pointing at a port nothing is listening on so that a build which gets as far as connecting fails
+ * promptly and locally.
+ */
+ private String unservedProxyUrl() throws IOException {
+ try (ServerSocket socket = new ServerSocket(0)) {
+ return "proxy:localhost:" + socket.getLocalPort();
+ }
+ }
+
+ @Test
+ void testContextIsRejectedAlongsideAnAtSign() throws Exception {
+ String url = unservedProxyUrl();
+ AtCommandExecutorContext context = aliceContext();
+
+ // the context is the connection's whole identity: a second, disagreeing atSign would connect
+ // to bob's atServer and then authenticate as alice
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
+ () -> AtCommandExecutors.builder().url(url).context(context)
+ .atSign(createAtSign("@bob")).build());
+
+ assertEquals(CONTEXT_WITH_LOOSE_ARGS, ex.getMessage());
+ }
+
+ @Test
+ void testContextIsRejectedAlongsideKeys() throws Exception {
+ String url = unservedProxyUrl();
+ AtCommandExecutorContext context = aliceContext();
+ AtKeys keys = keys();
+
+ // the context carries the keys the onReady sequence authenticates with, so loose keys would
+ // be silently discarded rather than used
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
+ () -> AtCommandExecutors.builder().url(url).context(context).keys(keys)
+ .build());
+
+ assertEquals(CONTEXT_WITH_LOOSE_ARGS, ex.getMessage());
+ }
+
+ @Test
+ void testContextIsRejectedAlongsideConfig() throws Exception {
+ String url = unservedProxyUrl();
+ AtCommandExecutorContext context = aliceContext();
+
+ // likewise the context's config is the one the from: carries, so a loose config would be lost
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
+ () -> AtCommandExecutors.builder()
+ .url(url)
+ .context(context)
+ .config(Collections.singletonMap("myOwnKey", "myOwnValue"))
+ .build());
+
+ assertEquals(CONTEXT_WITH_LOOSE_ARGS, ex.getMessage());
+ }
+
+ @Test
+ void testAnAtSignIsRequiredWhenNoContextIsSupplied() throws Exception {
+ String url = unservedProxyUrl();
+
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
+ () -> AtCommandExecutors.builder().url(url).build());
+
+ assertEquals(ATSIGN_NOT_SET, ex.getMessage());
+ }
+
+ @Test
+ void testKeysAreRejectedWithoutAnAtSign() throws Exception {
+ String url = unservedProxyUrl();
+ AtKeys keys = keys();
+
+ // there is no anonymous connection to fall back to, so keys alone are not a configuration
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
+ () -> AtCommandExecutors.builder().url(url).keys(keys).build());
+
+ assertEquals(ATSIGN_NOT_SET, ex.getMessage());
+ }
+
+ @Test
+ void testAContextAloneIsASufficientConfiguration() throws Exception {
+ String url = unservedProxyUrl();
+ AtCommandExecutorContext context = aliceContext();
+
+ // an AtException (rather than an IllegalArgumentException) means the identity and the endpoint
+ // were both satisfied from the context alone, and only the connection failed
+ assertThrows(AtException.class, () -> AtCommandExecutors.builder()
+ .url(url)
+ .context(context)
+ .reconnect(ReconnectStrategy.NONE)
+ .awaitReadyMillis(500L)
+ .build());
+ }
+}
diff --git a/at_client/src/test/java/org/atsign/client/impl/cli/AbstractCliTest.java b/at_client/src/test/java/org/atsign/client/impl/cli/AbstractCliTest.java
new file mode 100644
index 00000000..4f440f22
--- /dev/null
+++ b/at_client/src/test/java/org/atsign/client/impl/cli/AbstractCliTest.java
@@ -0,0 +1,48 @@
+package org.atsign.client.impl.cli;
+
+import static org.atsign.client.api.AtSign.createAtSign;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+
+import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.api.AtCommandExecutorContext;
+import org.atsign.client.impl.exceptions.AtException;
+import org.junit.jupiter.api.Test;
+
+class AbstractCliTest {
+
+ private static class TestCli extends AbstractCli {
+ @Override
+ protected TestCli self() {
+ return this;
+ }
+
+ AtCommandExecutor connectSendingFrom(AtCommandExecutorContext context) throws AtException {
+ return createConnectionSendingFrom(context);
+ }
+ }
+
+ /**
+ * A proxy url so that the connection is attempted directly rather than resolved through the root
+ * server, pointing at a port nothing is listening on so that it fails promptly and locally.
+ */
+ private String unservedProxyUrl() throws IOException {
+ try (ServerSocket socket = new ServerSocket(0)) {
+ return "proxy:localhost:" + socket.getLocalPort();
+ }
+ }
+
+ @Test
+ void testCreateConnectionSendingFromPassesOnlyTheSharedContext() throws Exception {
+ TestCli cli = new TestCli().setAtSign(createAtSign("@alice"));
+ cli.setRootUrl(unservedProxyUrl());
+ AtCommandExecutorContext context = cli.newConnectionContext(null);
+
+ // the executor builder rejects a context combined with a loose atSign / keys / config, so an
+ // AtException (rather than an IllegalArgumentException) is what says the onboarding flow's
+ // context reaches the connection as the one identity it is built from
+ assertThrows(AtException.class, () -> cli.connectSendingFrom(context));
+ }
+}
diff --git a/at_client/src/test/java/org/atsign/client/impl/cli/ActivateTest.java b/at_client/src/test/java/org/atsign/client/impl/cli/ActivateTest.java
new file mode 100644
index 00000000..d583d89f
--- /dev/null
+++ b/at_client/src/test/java/org/atsign/client/impl/cli/ActivateTest.java
@@ -0,0 +1,110 @@
+package org.atsign.client.impl.cli;
+
+import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.api.AtKeys;
+import org.atsign.client.impl.commands.TestExecutorBuilder;
+import org.atsign.client.impl.exceptions.AtUnauthenticatedException;
+import org.atsign.client.impl.util.KeysUtils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.stream.Collectors;
+
+import static java.lang.String.format;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.atsign.client.api.AtSign.createAtSign;
+import static org.atsign.client.impl.commands.EnrollCommandsTest.*;
+import static org.atsign.client.impl.common.EnrollmentId.createEnrollmentId;
+import static org.atsign.client.impl.util.EncryptionUtils.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class ActivateTest {
+
+ @TempDir
+ File keysDir;
+ private AtKeys keys;
+ private File keysFile;
+ private String selfKey;
+ private String privateKey;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ keys = AtKeys.builder()
+ .apkamKeyPair(generateRSAKeyPair())
+ .apkamSymmetricKey(generateAESKeyBase64())
+ .selfEncryptKey(generateAESKeyBase64())
+ .enrollmentId(createEnrollmentId("12345"))
+ .build();
+ keysFile = new File(keysDir, "@alice_key.atKeys");
+ KeysUtils.saveKeys(keys, keysFile);
+
+ selfKey = aesEncryptToBase64(AES_KEY, keys.getApkamSymmetricKey(), IV);
+ privateKey = aesEncryptToBase64(RSA_KEY, keys.getApkamSymmetricKey(), IV);
+ }
+
+ @Test
+ void testCompleteRetryEventuallySucceeds() throws Exception {
+
+ List commands = new ArrayList<>();
+ AtCommandExecutor executor = TestExecutorBuilder.builder()
+ .stub("from:@alice:clientConfig:.+", "data:challenge")
+ .stub("pkam:[^{].+", createPkamResponse(4))
+ .stub("keys:get:keyName:12345.default_self_enc_key.__manage@alice", createGetKeyResponse(selfKey))
+ .stub("keys:get:keyName:12345.default_enc_private_key.__manage@alice", createGetKeyResponse(privateKey))
+ .record(commands::add)
+ .build();
+
+ Activate activate = new Activate().setAtSign(createAtSign("@alice"));
+ activate.setKeysFile(keysFile.getPath());
+
+ activate.complete(executor, 3, 0, MILLISECONDS);
+
+ List fromCommands = commands.stream().filter(x -> x.startsWith("from:")).collect(Collectors.toList());
+ assertThat(fromCommands.size(), equalTo(4));
+ assertThat(fromCommands.stream().distinct().count(), equalTo(1L));
+
+ AtKeys newKeys = KeysUtils.loadKeys(keysFile);
+ assertThat(newKeys.getEncryptPrivateKey(), notNullValue());
+ }
+
+ @Test
+ void testCompleteRetryEventuallyGivesUp() throws Exception {
+
+ AtCommandExecutor executor = TestExecutorBuilder.builder()
+ .stub("from:@alice:clientConfig:.+", "data:challenge")
+ .stub("pkam:[^{].+", createPkamResponse(4))
+ .stub("keys:get:keyName:12345.default_self_enc_key.__manage@alice", createGetKeyResponse(selfKey))
+ .stub("keys:get:keyName:12345.default_enc_private_key.__manage@alice", createGetKeyResponse(privateKey))
+ .build();
+
+ Activate activate = new Activate().setAtSign(createAtSign("@alice"));
+ activate.setKeysFile(keysFile.getPath());
+
+ assertThrows(AtUnauthenticatedException.class, () -> activate.complete(executor, 2, 0, MILLISECONDS));
+ }
+
+ private static Function createPkamResponse(int after) {
+ AtomicInteger invocations = new AtomicInteger();
+ return x -> {
+ if (invocations.incrementAndGet() == after) {
+ return "data:success";
+ } else {
+ return "error:AT0401:enrollment_id:12345 is pending";
+ }
+ };
+ }
+
+ private static Function createGetKeyResponse(String key) {
+ return x -> format("data:{\"value\":\"%s\",\"iv\":\"%s\"}", key, IV);
+ }
+}
diff --git a/at_client/src/test/java/org/atsign/client/impl/commands/AuthenticationCommandsTest.java b/at_client/src/test/java/org/atsign/client/impl/commands/AuthenticationCommandsTest.java
index 65c11cb4..0ace6c5c 100644
--- a/at_client/src/test/java/org/atsign/client/impl/commands/AuthenticationCommandsTest.java
+++ b/at_client/src/test/java/org/atsign/client/impl/commands/AuthenticationCommandsTest.java
@@ -9,6 +9,7 @@
import java.util.Collections;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
import static org.atsign.client.api.AtSign.createAtSign;
import static org.atsign.client.impl.common.EnrollmentId.createEnrollmentId;
@@ -29,9 +30,8 @@ public void testAuthenticateWithCramDoesNotThrowException() throws Exception {
.stub("cram:7e91508d5.+", "data:success")
.build();
- AuthenticationCommands.authenticateWithCram(executor,
- new AtCommandExecutorContext(createAtSign("@alice"), null, null),
- "secret");
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null);
+ AuthenticationCommands.authenticateWithCram(executor, context, "secret");
}
@Test
@@ -41,13 +41,9 @@ public void testAuthenticateWithCramFailThrowsExpectedException() throws Excepti
.stub("cram:.+", "error:AT0401:deliberate")
.build();
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null);
Exception ex = assertThrows(AtUnauthenticatedException.class,
- () -> AuthenticationCommands.authenticateWithCram(
- executor,
- new AtCommandExecutorContext(
- createAtSign("@alice"), null,
- null),
- "secret"));
+ () -> AuthenticationCommands.authenticateWithCram(executor, context, "secret"));
assertThat(ex.getMessage(), containsString("deliberate"));
}
@@ -59,22 +55,24 @@ public void testAuthenticateWithPkamDoesNotThrowException() throws Exception {
.stub("pkam:[^{].+", "data:success")
.build();
- AuthenticationCommands.authenticateWithPkam(executor, createAtSign("@alice"), keys);
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys);
+ AuthenticationCommands.authenticateWithPkam(executor, context);
}
@Test
- public void testAuthenticateWithApkamWithEnrollmentId() throws Exception {
+ public void testAuthenticateWithApkamWithEnrollmentIdDoesNotThrowException() throws Exception {
AtKeys keys = AtKeys.builder().apkamKeyPair(generateRSAKeyPair()).enrollmentId(createEnrollmentId("12345")).build();
AtCommandExecutor executor = TestExecutorBuilder.builder()
.stub("from:@alice", "data:challenge")
.stub("pkam:signingAlgo:rsa2048:hashingAlgo:sha256:enrollmentId:12345:.+", "data:success")
.build();
- AuthenticationCommands.authenticateWithPkam(executor, createAtSign("@alice"), keys);
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys);
+ AuthenticationCommands.authenticateWithPkam(executor, context);
}
@Test
- public void testAuthenticateWithApkamWithConfig() throws Exception {
+ public void testAuthenticateWithApkamWithConfigDoesNotThrowException() throws Exception {
AtKeys keys = AtKeys.builder().apkamKeyPair(generateRSAKeyPair()).enrollmentId(createEnrollmentId("12345")).build();
AtCommandExecutor executor = TestExecutorBuilder.builder()
.stub("from:@alice:clientConfig:.+", "data:challenge")
@@ -82,7 +80,8 @@ public void testAuthenticateWithApkamWithConfig() throws Exception {
.build();
Map config = Collections.singletonMap("clientVersion", "1.2.3");
- AuthenticationCommands.authenticateWithPkam(executor, createAtSign("@alice"), keys, config);
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys, config);
+ AuthenticationCommands.authenticateWithPkam(executor, context);
}
@Test
@@ -92,10 +91,10 @@ public void testAuthenticateWithApkamFailThrowsExpectedException() throws Except
.stub("from:@alice", "data:challenge")
.stub("pkam:.+", "error:AT0401:deliberate")
.build();
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys);
Exception ex = assertThrows(AtUnauthenticatedException.class,
- () -> AuthenticationCommands.authenticateWithPkam(executor, createAtSign("@alice"),
- keys));
+ () -> AuthenticationCommands.authenticateWithPkam(executor, context));
assertThat(ex.getMessage(), containsString("deliberate"));
}
@@ -106,10 +105,10 @@ public void testPkamAuthenticatorThrowsOnReadyException() throws Exception {
.stub("from:@alice", "data:challenge")
.stub("pkam:.+", "error:AT0401:deliberate")
.build();
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys);
Exception ex = assertThrows(Exception.class,
- () -> AuthenticationCommands.pkamAuthenticator(createAtSign("@alice"), keys, null)
- .accept(executor));
+ () -> AuthenticationCommands.pkamAuthenticator(context).accept(executor));
assertThat(ex, instanceOf(AtOnReadyException.class));
assertThat(ex.getMessage(), containsString("deliberate"));
}
@@ -119,7 +118,7 @@ public void testSendFromIssuesFromAndRetainsChallenge() throws Exception {
AtCommandExecutor executor = TestExecutorBuilder.builder()
.stub("from:@alice", "data:challenge")
.build();
- AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null, null);
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null);
AuthenticationCommands.sendFrom(context).accept(executor);
@@ -134,7 +133,7 @@ public void testPkamAuthenticatorReusesTheInitialFromChallenge() throws Exceptio
.stub("from:@alice", "data:challenge")
.stub("pkam:[^{].+", "data:success")
.build();
- AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys, null);
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys);
// the from: sender runs first (as wired by createOnReady), then PKAM reuses its challenge
AuthenticationCommands.sendFrom(context).accept(executor);
@@ -146,6 +145,25 @@ public void testPkamAuthenticatorReusesTheInitialFromChallenge() throws Exceptio
assertThat(context.consumeChallenge(), is(nullValue()));
}
+ @Test
+ public void testAuthenticateWithCramReusesTheInitialFromChallenge() throws Exception {
+ AtCommandExecutor executor = TestExecutorBuilder.builder()
+ .stub("from:@alice", "data:challenge")
+ .stub("cram:7e91508d5.+", "data:success")
+ .build();
+
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null);
+
+ // the from: sender runs first (as wired by createOnReady), then CRAM reuses its challenge
+ AuthenticationCommands.sendFrom(context).accept(executor);
+ AuthenticationCommands.authenticateWithCram(executor, context, "secret");
+
+ // exactly one from: for the whole connection — CRAM did not issue a second one
+ verify(executor, times(1)).sendSync(matches("from:.*"));
+ verify(executor, times(1)).sendSync(matches("cram:.*"));
+ assertThat(context.consumeChallenge(), is(nullValue()));
+ }
+
@Test
public void testPkamAuthenticatorIssuesItsOwnFromWhenNoChallengeRetained() throws Exception {
AtKeys keys = AtKeys.builder().apkamKeyPair(generateRSAKeyPair()).build();
@@ -153,7 +171,7 @@ public void testPkamAuthenticatorIssuesItsOwnFromWhenNoChallengeRetained() throw
.stub("from:@alice", "data:challenge")
.stub("pkam:[^{].+", "data:success")
.build();
- AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys, null);
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys);
// no prior sendFrom: the authenticator must issue its own from: to obtain a challenge
AuthenticationCommands.pkamAuthenticator(context).accept(executor);
@@ -164,7 +182,7 @@ public void testPkamAuthenticatorIssuesItsOwnFromWhenNoChallengeRetained() throw
@Test
public void testRetainedChallengeIsConsumedAtMostOnce() {
- AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null, null);
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null);
context.setChallenge("challenge");
// single-use: the first consumer gets it, a second (e.g. a further auth on the same connection)
@@ -172,4 +190,18 @@ public void testRetainedChallengeIsConsumedAtMostOnce() {
assertThat(context.consumeChallenge(), is("challenge"));
assertThat(context.consumeChallenge(), is(nullValue()));
}
+
+ @Test
+ public void testFromReplacesUnconsumedChallenge() throws Exception {
+ AtomicInteger invocations = new AtomicInteger();
+ AtCommandExecutor executor = TestExecutorBuilder.builder()
+ .stub("from:@alice", m -> "data:challenge" + invocations.incrementAndGet())
+ .build();
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null);
+
+ AuthenticationCommands.sendFrom(context).accept(executor);
+ AuthenticationCommands.sendFrom(context).accept(executor);
+
+ assertThat(context.consumeChallenge(), equalTo("challenge2"));
+ }
}
diff --git a/at_client/src/test/java/org/atsign/client/impl/commands/EnrollCommandsTest.java b/at_client/src/test/java/org/atsign/client/impl/commands/EnrollCommandsTest.java
index f3f4a8d6..92a5ed83 100644
--- a/at_client/src/test/java/org/atsign/client/impl/commands/EnrollCommandsTest.java
+++ b/at_client/src/test/java/org/atsign/client/impl/commands/EnrollCommandsTest.java
@@ -66,9 +66,10 @@ public void testOnboardThrowsExceptionIfSigningPublicKeyIsMissing() throws Excep
.stub("scan", "data:[\"signing_publickey@gary\"]")
.build();
+ AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, keys);
+
Exception ex = assertThrows(Exception.class,
- () -> EnrollCommands.onboard(executor, new AtCommandExecutorContext(atSign, keys, null),
- "secret", "app", "device", false));
+ () -> EnrollCommands.onboard(executor, context, "secret", "app", "device", false));
assertThat(ex.getMessage(), containsString("not connected to the atsign's at server"));
}
@@ -107,8 +108,9 @@ public void testOnboard() throws Exception {
.stub("update:public:publickey@alice .+", "data:1")
.build();
- AtKeys newKeys = EnrollCommands.onboard(executor, new AtCommandExecutorContext(atSign, keys, null), "secret",
- "app", "device", false);
+ AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, keys);
+
+ AtKeys newKeys = EnrollCommands.onboard(executor, context, "secret", "app", "device", false);
assertThat(newKeys, is(not(sameInstance(keys))));
assertThat(newKeys.getEnrollmentId(), equalTo(createEnrollmentId("904dcbf7")));
@@ -123,7 +125,7 @@ public void testOnboardReusesTheConnectionFromChallengeForCram() throws Exceptio
.build();
// simulate the connection having issued from: on connect (sendFrom) and retained the challenge
- AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, keys, null);
+ AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, keys);
context.setChallenge("challenge");
AtCommandExecutor executor = TestExecutorBuilder.builder()
@@ -157,7 +159,9 @@ public void testEnroll() throws Exception {
.stub("enroll:request\\{.+}", "data:{\"enrollmentId\":\"759acb09\",\"status\":\"pending\"}")
.build();
- AtKeys newKeys = EnrollCommands.enroll(executor, atSign, keys, "OTP123", "app", "device", singletonMap("ns", "rw"));
+ AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, keys);
+
+ AtKeys newKeys = EnrollCommands.enroll(executor, context, "OTP123", "app", "device", singletonMap("ns", "rw"));
assertThat(newKeys, is(not(sameInstance(keys))));
assertThat(newKeys.getEnrollmentId(), equalTo(createEnrollmentId("759acb09")));
@@ -186,7 +190,9 @@ public void testComplete() throws Exception {
.stub("keys:get:keyName:12345.default_enc_private_key.__manage@alice", privateEncryptKeysGetResponse)
.build();
- AtKeys newKeys = EnrollCommands.complete(executor, atSign, keys);
+ AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, keys);
+
+ AtKeys newKeys = EnrollCommands.complete(executor, context);
assertThat(newKeys, is(not(sameInstance(keys))));
assertThat(newKeys.getEncryptPrivateKey(), notNullValue());
@@ -210,7 +216,10 @@ public void testApprove() throws Exception {
"data:{\"status\":\"approved\",\"enrollmentId\":\"12345\"}")
.build();
- EnrollCommands.approve(executor, keys, createEnrollmentId("12345"));
+ // approve reads only the keys from the context, but a context always carries an atSign
+ AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys);
+
+ EnrollCommands.approve(executor, context, createEnrollmentId("12345"));
}
@Test
diff --git a/at_client/src/test/java/org/atsign/client/impl/commands/NotificationsTest.java b/at_client/src/test/java/org/atsign/client/impl/commands/NotificationsTest.java
index 53bf64f2..26db1fde 100644
--- a/at_client/src/test/java/org/atsign/client/impl/commands/NotificationsTest.java
+++ b/at_client/src/test/java/org/atsign/client/impl/commands/NotificationsTest.java
@@ -13,6 +13,7 @@
import java.util.function.Consumer;
import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.api.AtCommandExecutorContext;
import org.atsign.client.api.AtEvents;
import org.atsign.client.api.AtKeys;
import org.atsign.client.api.AtSign;
@@ -29,11 +30,12 @@ class NotificationsTest {
void testMonitorSendExpectedCommands() throws Exception {
AtSign atSign = createAtSign("colin");
AtKeys keys = AtKeys.builder().apkamKeyPair(generateRSAKeyPair()).build();
+ AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, keys);
AtCommandExecutor executor = mock(AtCommandExecutor.class);
stubAuthentication(executor, atSign);
Consumer consumer = mock(Consumer.class);
- Notifications.monitor(executor, atSign, null, keys, consumer);
+ Notifications.monitor(executor, context, null, consumer);
verify(executor).sendSync(eq("monitor"), eq(consumer));
}
@@ -42,6 +44,7 @@ void testMonitorSendExpectedCommands() throws Exception {
void testMonitorWrapsConsumer() throws Exception {
AtSign atSign = createAtSign("colin");
AtKeys keys = AtKeys.builder().apkamKeyPair(generateRSAKeyPair()).build();
+ AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, keys);
AtCommandExecutor executor = mock(AtCommandExecutor.class);
stubAuthentication(executor, atSign);
Consumer consumer = mock(Consumer.class);
@@ -49,8 +52,8 @@ void testMonitorWrapsConsumer() throws Exception {
throw new AtTimeoutException("deliberate");
}).when(executor).sendSync(eq("monitor"), Mockito.any(Consumer.class));
- Exception ex =
- assertThrows(Exception.class, () -> Notifications.monitor(atSign, null, keys, null, consumer).accept(executor));
+ Exception ex = assertThrows(Exception.class,
+ () -> Notifications.monitor(context, null, consumer).accept(executor));
assertThat(ex, instanceOf(AtOnReadyException.class));
}
diff --git a/at_client/src/test/java/org/atsign/client/impl/commands/PublicKeyCommandsTest.java b/at_client/src/test/java/org/atsign/client/impl/commands/PublicKeyCommandsTest.java
index f7c4bd95..9b105646 100644
--- a/at_client/src/test/java/org/atsign/client/impl/commands/PublicKeyCommandsTest.java
+++ b/at_client/src/test/java/org/atsign/client/impl/commands/PublicKeyCommandsTest.java
@@ -10,6 +10,7 @@
import org.atsign.client.api.AtClient.GetRequestOptions;
import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.api.AtCommandExecutorContext;
import org.atsign.client.api.AtKeys;
import org.atsign.client.api.AtSign;
import org.atsign.client.api.Keys;
@@ -25,6 +26,10 @@ class PublicKeyCommandsTest {
private AtKeys keys;
private AtSign atSign;
+ // the key is shared by @gary, so @gary reads it with llookup and anyone else with plookup
+ private AtCommandExecutorContext contextGary;
+ private AtCommandExecutorContext contextColin;
+
@BeforeEach
public void setup() throws Exception {
keys = AtKeys.builder()
@@ -35,6 +40,8 @@ public void setup() throws Exception {
.sharedBy(atSign)
.name("test")
.build();
+ contextGary = new AtCommandExecutorContext(atSign, keys);
+ contextColin = new AtCommandExecutorContext(createAtSign("colin"), null);
}
@Test
@@ -43,7 +50,7 @@ void testGetSharedByMe() throws Exception {
.stubLookupResponse("llookup:all:public:test@gary", "public:test@gary", "hello world")
.build();
- String actual = PublicKeyCommands.get(executor, createAtSign("gary"), key, null);
+ String actual = PublicKeyCommands.get(executor, contextGary, key, null);
assertThat(actual, equalTo("hello world"));
}
@@ -54,7 +61,7 @@ void testGetCachedSharedByMe() throws Exception {
.stubLookupResponse("llookup:all:public:test@gary", "cached:public:test@gary", "hello world")
.build();
- PublicKeyCommands.get(executor, createAtSign("gary"), key, null);
+ PublicKeyCommands.get(executor, contextGary, key, null);
assertThat(key.metadata().isCached(), is(true));
}
@@ -65,7 +72,7 @@ void testGetSharedByMeNoSuchKey() throws Exception {
.stub("llookup:all:public:test@gary", "error:AT0015:deliberate")
.build();
- assertThrows(AtKeyNotFoundException.class, () -> PublicKeyCommands.get(executor, createAtSign("gary"), key, null));
+ assertThrows(AtKeyNotFoundException.class, () -> PublicKeyCommands.get(executor, contextGary, key, null));
}
@Test
@@ -74,7 +81,7 @@ void testGetSharedByMeExecutionException() throws Exception {
.stub("llookup:all:public:test@gary", new ExecutionException("deliberate", null))
.build();
- assertThrows(RuntimeException.class, () -> PublicKeyCommands.get(executor, createAtSign("gary"), key, null));
+ assertThrows(RuntimeException.class, () -> PublicKeyCommands.get(executor, contextGary, key, null));
}
@Test
@@ -83,7 +90,7 @@ void testGetSharedByOther() throws Exception {
.stubLookupResponse("plookup:all:test@gary", "public:test@gary", "hello world")
.build();
- String actual = PublicKeyCommands.get(executor, createAtSign("colin"), key, null);
+ String actual = PublicKeyCommands.get(executor, contextColin, key, null);
assertThat(actual, equalTo("hello world"));
}
@@ -94,7 +101,7 @@ void testGetCachedSharedByOther() throws Exception {
.stubLookupResponse("plookup:all:test@gary", "cached:public:test@gary", "hello world")
.build();
- PublicKeyCommands.get(executor, createAtSign("colin"), key, null);
+ PublicKeyCommands.get(executor, contextColin, key, null);
assertThat(key.metadata().isCached(), is(true));
}
@@ -106,20 +113,18 @@ void testGetSharedByOtherBypassCache() throws Exception {
.build();
GetRequestOptions options = GetRequestOptions.builder().bypassCache(true).build();
- String actual = PublicKeyCommands.get(executor, createAtSign("colin"), key, options);
+ String actual = PublicKeyCommands.get(executor, contextColin, key, options);
assertThat(actual, equalTo("hello world"));
}
-
-
@Test
void testGetSharedByOtherNoSuchKey() throws Exception {
AtCommandExecutor executor = TestExecutorBuilder.builder()
.stub("plookup:all:test@gary", "error:AT0015:deliberate")
.build();
- assertThrows(AtKeyNotFoundException.class, () -> PublicKeyCommands.get(executor, createAtSign("colin"), key, null));
+ assertThrows(AtKeyNotFoundException.class, () -> PublicKeyCommands.get(executor, contextColin, key, null));
}
@Test
@@ -128,7 +133,7 @@ void testGetSharedByOtherExecutionException() throws Exception {
.stubExecutionException("plookup:all:test@gary")
.build();
- assertThrows(RuntimeException.class, () -> PublicKeyCommands.get(executor, createAtSign("colin"), key, null));
+ assertThrows(RuntimeException.class, () -> PublicKeyCommands.get(executor, contextColin, key, null));
}
@Test
@@ -137,7 +142,7 @@ void testPutSendsExpectedCommands() throws Exception {
.stub("update:dataSignature:.+:isEncrypted:false:public:test@gary hello world", "data:123")
.build();
- PublicKeyCommands.put(executor, createAtSign("gary"), keys, key, "hello world");
+ PublicKeyCommands.put(executor, contextGary, key, "hello world");
}
@Test
@@ -146,12 +151,8 @@ void testPutThrowExceptionIfCommandFails() throws Exception {
.stub("update:dataSignature:.+:isEncrypted:false:public:test@gary hello world", "error:AT0001:deliberate")
.build();
- AtKeys keys = AtKeys.builder()
- .encryptKeyPair(EncryptionUtils.generateRSAKeyPair())
- .build();
-
assertThrows(AtServerRuntimeException.class,
- () -> PublicKeyCommands.put(executor, createAtSign("gary"), keys, key, "hello world"));
+ () -> PublicKeyCommands.put(executor, contextGary, key, "hello world"));
}
@Test
@@ -160,11 +161,7 @@ void testPutExecutionException() throws Exception {
.stubExecutionException("update:dataSignature:.+:isEncrypted:false:public:test@gary hello world")
.build();
- AtKeys keys = AtKeys.builder()
- .encryptKeyPair(EncryptionUtils.generateRSAKeyPair())
- .build();
-
assertThrows(RuntimeException.class,
- () -> PublicKeyCommands.put(executor, createAtSign("gary"), keys, key, "hello world"));
+ () -> PublicKeyCommands.put(executor, contextGary, key, "hello world"));
}
}
diff --git a/at_client/src/test/java/org/atsign/client/impl/commands/SelfKeyCommandsTest.java b/at_client/src/test/java/org/atsign/client/impl/commands/SelfKeyCommandsTest.java
index 7d9fb61c..c5ebb611 100644
--- a/at_client/src/test/java/org/atsign/client/impl/commands/SelfKeyCommandsTest.java
+++ b/at_client/src/test/java/org/atsign/client/impl/commands/SelfKeyCommandsTest.java
@@ -10,6 +10,7 @@
import org.atsign.client.api.AtKeys;
import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.api.AtCommandExecutorContext;
import org.atsign.client.api.AtSign;
import org.atsign.client.api.Keys;
import org.atsign.client.impl.exceptions.AtServerRuntimeException;
@@ -22,6 +23,7 @@ class SelfKeyCommandsTest {
private AtKeys keys;
private AtSign atSign;
+ private AtCommandExecutorContext context;
@BeforeEach
public void setup() throws Exception {
@@ -30,6 +32,7 @@ public void setup() throws Exception {
.encryptKeyPair(generateRSAKeyPair())
.build();
atSign = createAtSign("gary");
+ context = new AtCommandExecutorContext(atSign, keys);
key = Keys.selfKeyBuilder()
.sharedBy(atSign)
.name("test")
@@ -45,7 +48,7 @@ void testGet() throws Exception {
.stubLookupResponse("llookup:all:test@gary", "test@gary", encrypted, "ivNonce", iv)
.build();
- String actual = SelfKeyCommands.get(executor, atSign, keys, key);
+ String actual = SelfKeyCommands.get(executor, context, key);
assertThat(actual, equalTo("hello me"));
}
@@ -56,7 +59,7 @@ void testGetException() throws Exception {
.stub("llookup:all:test@gary", "error:AT0001:deliberate")
.build();
- assertThrows(AtServerRuntimeException.class, () -> SelfKeyCommands.get(executor, atSign, keys, key));
+ assertThrows(AtServerRuntimeException.class, () -> SelfKeyCommands.get(executor, context, key));
}
@Test
@@ -65,7 +68,7 @@ void testGetExecutionException() throws Exception {
.stubExecutionException("llookup:all:test@gary")
.build();
- assertThrows(RuntimeException.class, () -> SelfKeyCommands.get(executor, atSign, keys, key));
+ assertThrows(RuntimeException.class, () -> SelfKeyCommands.get(executor, context, key));
}
@Test
@@ -74,7 +77,7 @@ void testPut() throws Exception {
.stub("update:dataSignature:.+:isEncrypted:true:ivNonce:.+:test@gary .+", "data:123")
.build();
- SelfKeyCommands.put(executor, atSign, keys, key, "hello world");
+ SelfKeyCommands.put(executor, context, key, "hello world");
verify(executor).sendSync(argThat(s -> !s.contains("hello world")));
}
@@ -84,7 +87,8 @@ void testPutAtException() throws Exception {
.stub("update:dataSignature:.+:isEncrypted:true:ivNonce:.+:test@gary .+", "error:AT0001:deliberate")
.build();
- assertThrows(AtServerRuntimeException.class, () -> SelfKeyCommands.put(executor, atSign, keys, key, "hello world"));
+ assertThrows(AtServerRuntimeException.class,
+ () -> SelfKeyCommands.put(executor, context, key, "hello world"));
}
@Test
@@ -93,7 +97,8 @@ void testPutExecutionException() throws Exception {
.stubExecutionException("update:dataSignature:.+:isEncrypted:true:ivNonce:.+:test@gary .+")
.build();
- assertThrows(RuntimeException.class, () -> SelfKeyCommands.put(executor, atSign, keys, key, "hello world"));
+ assertThrows(RuntimeException.class,
+ () -> SelfKeyCommands.put(executor, context, key, "hello world"));
}
}
diff --git a/at_client/src/test/java/org/atsign/client/impl/commands/SharedKeyCommandsTest.java b/at_client/src/test/java/org/atsign/client/impl/commands/SharedKeyCommandsTest.java
index 0c85db67..f62a6410 100644
--- a/at_client/src/test/java/org/atsign/client/impl/commands/SharedKeyCommandsTest.java
+++ b/at_client/src/test/java/org/atsign/client/impl/commands/SharedKeyCommandsTest.java
@@ -10,6 +10,7 @@
import static org.mockito.Mockito.verify;
import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.api.AtCommandExecutorContext;
import org.atsign.client.api.AtKeys;
import org.atsign.client.api.Keys;
import org.atsign.client.api.Metadata;
@@ -25,6 +26,10 @@ class SharedKeyCommandsTest {
private AtKeys keys;
+ // the key is shared by @gary with @colin, so these are the two perspectives it can be read from
+ private AtCommandExecutorContext contextGary;
+ private AtCommandExecutorContext contextColin;
+
@BeforeEach
public void setup() throws Exception {
keys = AtKeys.builder()
@@ -36,6 +41,8 @@ public void setup() throws Exception {
.sharedWith(createAtSign("colin"))
.name("test")
.build();
+ contextGary = new AtCommandExecutorContext(createAtSign("gary"), keys);
+ contextColin = new AtCommandExecutorContext(createAtSign("colin"), keys);
}
@Test
@@ -48,7 +55,7 @@ void testGetSharedByMe() throws Exception {
.stubLookupResponse("llookup:all:@colin:test@gary", "@colin:test@gary", encrypted, "ivNonce", iv)
.build();
- String actual = SharedKeyCommands.get(executor, createAtSign("gary"), keys, key);
+ String actual = SharedKeyCommands.get(executor, contextGary, key);
assertThat(actual, equalTo("hello colin"));
}
@@ -57,9 +64,10 @@ void testGetSharedByMe() throws Exception {
void testGetNotSharedByMeOrWithMeThrowsException() throws Exception {
AtCommandExecutor executor = TestExecutorBuilder.builder()
.build();
+ AtCommandExecutorContext contextAlice = new AtCommandExecutorContext(createAtSign("alice"), keys);
RuntimeException ex =
- assertThrows(RuntimeException.class, () -> SharedKeyCommands.get(executor, createAtSign("alice"), keys, key));
+ assertThrows(RuntimeException.class, () -> SharedKeyCommands.get(executor, contextAlice, key));
assertThat(ex.getMessage(), containsString("@alice is neither the sharedBy or sharedWith of @colin:test@gary"));
}
@@ -73,7 +81,7 @@ void testGetSharedByMeCachedKey() throws Exception {
.stubLookupResponse("llookup:all:@colin:test@gary", "@colin:test@gary", encrypted, "ivNonce", iv)
.build();
- String actual = SharedKeyCommands.get(executor, createAtSign("gary"), keys, key);
+ String actual = SharedKeyCommands.get(executor, contextGary, key);
assertThat(actual, equalTo("hello colin"));
}
@@ -86,8 +94,7 @@ void testGetSharedByMeServerException() throws Exception {
.stub("llookup:all:@colin:test@gary", "error:AT0001:deliberate")
.build();
- assertThrows(AtServerRuntimeException.class,
- () -> SharedKeyCommands.get(executor, createAtSign("gary"), keys, key));
+ assertThrows(AtServerRuntimeException.class, () -> SharedKeyCommands.get(executor, contextGary, key));
}
@Test
@@ -98,7 +105,7 @@ void testGetSharedByMeExecutionException() throws Exception {
.stubExecutionException("llookup:all:@colin:test@gary")
.build();
- assertThrows(RuntimeException.class, () -> SharedKeyCommands.get(executor, createAtSign("gary"), keys, key));
+ assertThrows(RuntimeException.class, () -> SharedKeyCommands.get(executor, contextGary, key));
}
@Test
@@ -116,7 +123,7 @@ void getGetSharedByOther() throws Exception {
"ivNonce", iv, "sharedKeyEnc", sharedKeyEnc, "pubKeyHash", hash)
.build();
- String actual = SharedKeyCommands.get(executor, createAtSign("colin"), keys, key);
+ String actual = SharedKeyCommands.get(executor, contextColin, key);
assertThat(actual, equalTo("hello colin"));
}
@@ -132,7 +139,7 @@ void getGetSharedByOtherBackwardCompatibilityCase() throws Exception {
.stub("lookup:shared_key@gary", "data:" + sharedKeyEnc)
.build();
- String actual = SharedKeyCommands.get(executor, createAtSign("colin"), keys, key);
+ String actual = SharedKeyCommands.get(executor, contextColin, key);
assertThat(actual, equalTo("hello colin"));
}
@@ -152,8 +159,7 @@ void getGetSharedByOtherThrowsExceptionForPubKeyHashMismatch() throws Exception
"ivNonce", iv, "sharedKeyEnc", sharedKeyEnc, "pubKeyHash", hash)
.build();
- assertThrows(AtPublicKeyChangeException.class,
- () -> SharedKeyCommands.get(executor, createAtSign("colin"), keys, key));
+ assertThrows(AtPublicKeyChangeException.class, () -> SharedKeyCommands.get(executor, contextColin, key));
}
@Test
@@ -164,7 +170,7 @@ void testPutWhenSharedKeyAlreadyExists() throws Exception {
.stub("update:isEncrypted:true:ivNonce:.+:@colin:test@gary .+", "data:123")
.build();
- SharedKeyCommands.put(executor, createAtSign("gary"), keys, key, "hello colin");
+ SharedKeyCommands.put(executor, contextGary, key, "hello colin");
verify(executor).sendSync(argThat(s -> s.contains("update:") && !s.contains("hello colin")));
}
@@ -178,7 +184,7 @@ void testPutWhenSharedKeyDoesNotAlreadyExists() throws Exception {
.stub("update:isEncrypted:true:sharedKeyEnc:.+:ivNonce:.+:@colin:test@gary .+", "data:3")
.build();
- SharedKeyCommands.put(executor, createAtSign("gary"), keys, key, "hello colin");
+ SharedKeyCommands.put(executor, contextGary, key, "hello colin");
verify(executor).sendSync(argThat(s -> s.contains("update:") && s.contains(":pubKeyHash:")));
verify(executor).sendSync(argThat(s -> s.contains("update:") && s.contains(":pubKeyCS:")));
diff --git a/at_client/src/test/java/org/atsign/client/impl/commands/TestExecutorBuilder.java b/at_client/src/test/java/org/atsign/client/impl/commands/TestExecutorBuilder.java
index 0f224e69..ddea674d 100644
--- a/at_client/src/test/java/org/atsign/client/impl/commands/TestExecutorBuilder.java
+++ b/at_client/src/test/java/org/atsign/client/impl/commands/TestExecutorBuilder.java
@@ -8,6 +8,7 @@
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -19,12 +20,19 @@
public class TestExecutorBuilder {
- private Map mapping = new LinkedHashMap<>();
+ private final Map mapping = new LinkedHashMap<>();
+ private Consumer commandConsumer = c -> {
+ };
public static TestExecutorBuilder builder() {
return new TestExecutorBuilder();
}
+ public TestExecutorBuilder record(Consumer consumer) {
+ this.commandConsumer = consumer;
+ return this;
+ }
+
public TestExecutorBuilder stub(String command, String response) {
return stub(Pattern.compile(command), response);
}
@@ -67,6 +75,7 @@ public AtCommandExecutor build() throws ExecutionException, InterruptedException
for (Map.Entry entry : mapping.entrySet()) {
Matcher matcher = entry.getKey().matcher(command);
if (matcher.matches()) {
+ commandConsumer.accept(command);
if (entry.getValue() instanceof Throwable) {
throw (Throwable) entry.getValue();
} else if (entry.getValue() instanceof Function) {
diff --git a/at_client/src/test/java/org/atsign/cucumber/steps/ActivateSteps.java b/at_client/src/test/java/org/atsign/cucumber/steps/ActivateSteps.java
index b667e47d..7b0328cd 100644
--- a/at_client/src/test/java/org/atsign/cucumber/steps/ActivateSteps.java
+++ b/at_client/src/test/java/org/atsign/cucumber/steps/ActivateSteps.java
@@ -15,6 +15,7 @@
import org.atsign.client.impl.cli.Activate;
import org.atsign.client.api.AtCommandExecutor;
+import org.atsign.client.api.AtCommandExecutorContext;
import org.atsign.client.impl.commands.EnrollCommands;
import org.atsign.client.impl.commands.KeyCommands;
import org.atsign.client.impl.common.EnrollmentId;
@@ -233,7 +234,8 @@ public void run() {
try (AtCommandExecutor executor = createConnection(rootUrl, atSign, 0)) {
- authenticateWithPkam(executor, atSign, KeysUtils.loadKeys(keysFile));
+ AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, KeysUtils.loadKeys(keysFile));
+ authenticateWithPkam(executor, context);
// delete keys that have been created
matchDataJsonListOfStrings(executor.sendSync("scan")).stream()