commit 0790350ef9776eef27c35e730f79e0646ea2a573
parent 9cfb29d759aa4c284910d262c37433d582f474b3
Author: triesap <tyson@radroots.org>
Date: Mon, 27 Apr 2026 05:16:49 +0000
cli: prove myc signer binding readiness
- add deterministic fake myc session payloads
- cover unsupported and unavailable binding states
- cover ambiguous and ready authorized sessions
- serialize integration command execution for stability
Diffstat:
2 files changed, 201 insertions(+), 3 deletions(-)
diff --git a/tests/signer_runtime_modes.rs b/tests/signer_runtime_modes.rs
@@ -2,7 +2,8 @@ mod support;
use std::path::Path;
-use serde_json::Value;
+use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic};
+use serde_json::{Value, json};
use support::RadrootsCliSandbox;
#[test]
@@ -104,9 +105,10 @@ fn myc_signer_status_reports_unavailable_for_invalid_json() {
#[test]
fn myc_signer_status_reports_unconfigured_when_ready_without_binding() {
let sandbox = RadrootsCliSandbox::new();
- let myc = sandbox.write_fake_myc(
+ let myc = write_fake_myc_status(
+ &sandbox,
"myc-ready-no-binding",
- r#"printf '%s\n' '{"status_contract_version":1,"status":"ready","ready":true,"signer_backend":{"remote_session_count":0,"remote_sessions":[]}}'"#,
+ ready_myc_payload(Vec::new()),
);
configure_myc_mode(&sandbox, &myc);
@@ -122,6 +124,117 @@ fn myc_signer_status_reports_unconfigured_when_ready_without_binding() {
assert_contains(&value["result"]["binding"]["reason"], "signer.remote_nip46");
}
+#[cfg(unix)]
+#[test]
+fn myc_binding_reports_unsupported_target_kind() {
+ let sandbox = RadrootsCliSandbox::new();
+ let myc = write_fake_myc_status(
+ &sandbox,
+ "myc-ready-unsupported",
+ ready_myc_payload(Vec::new()),
+ );
+ configure_myc_mode_with_binding(&sandbox, &myc, "explicit_endpoint", "default", None, None);
+
+ let value = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
+
+ assert_eq!(value["result"]["mode"], "myc");
+ assert_eq!(value["result"]["state"], "unconfigured");
+ assert_eq!(value["result"]["binding"]["state"], "unsupported");
+ assert_contains(&value["result"]["binding"]["reason"], "target_kind");
+}
+
+#[cfg(unix)]
+#[test]
+fn myc_binding_reports_no_authorized_sessions() {
+ let sandbox = RadrootsCliSandbox::new();
+ let signer = identity_public(2);
+ let user = identity_public(3);
+ let payload = ready_myc_payload(vec![remote_session("session-ping", &signer, &user, "ping")]);
+ let myc = write_fake_myc_status(&sandbox, "myc-no-authorized", payload);
+ configure_myc_mode_with_binding(&sandbox, &myc, "managed_instance", "default", None, None);
+
+ let value = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
+
+ assert_eq!(value["result"]["mode"], "myc");
+ assert_eq!(value["result"]["state"], "unavailable");
+ assert_eq!(value["result"]["binding"]["state"], "unavailable");
+ assert_eq!(value["result"]["binding"]["matched_session_count"], 0);
+ assert_contains(
+ &value["result"]["binding"]["reason"],
+ "no authorized remote signer session",
+ );
+}
+
+#[cfg(unix)]
+#[test]
+fn myc_binding_reports_ambiguous_authorized_sessions() {
+ let sandbox = RadrootsCliSandbox::new();
+ let signer = identity_public(4);
+ let user_one = identity_public(5);
+ let user_two = identity_public(6);
+ let payload = ready_myc_payload(vec![
+ remote_session("session-one", &signer, &user_one, "sign_event"),
+ remote_session("session-two", &signer, &user_two, "sign_event"),
+ ]);
+ let myc = write_fake_myc_status(&sandbox, "myc-ambiguous", payload);
+ configure_myc_mode_with_binding(&sandbox, &myc, "managed_instance", "default", None, None);
+
+ let value = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
+
+ assert_eq!(value["result"]["mode"], "myc");
+ assert_eq!(value["result"]["state"], "unconfigured");
+ assert_eq!(value["result"]["binding"]["state"], "ambiguous");
+ assert_eq!(value["result"]["binding"]["matched_session_count"], 2);
+ assert_contains(
+ &value["result"]["binding"]["reason"],
+ "multiple authorized remote signer sessions",
+ );
+}
+
+#[cfg(unix)]
+#[test]
+fn myc_binding_reports_ready_for_one_authorized_session() {
+ let sandbox = RadrootsCliSandbox::new();
+ let signer = identity_public(7);
+ let user = identity_public(8);
+ let payload = ready_myc_payload(vec![remote_session(
+ "session-ready",
+ &signer,
+ &user,
+ "sign_event",
+ )]);
+ let myc = write_fake_myc_status(&sandbox, "myc-ready-bound", payload);
+ configure_myc_mode_with_binding(
+ &sandbox,
+ &myc,
+ "managed_instance",
+ "default",
+ Some(user.id.as_str()),
+ None,
+ );
+
+ let value = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
+
+ assert_eq!(value["result"]["mode"], "myc");
+ assert_eq!(value["result"]["state"], "ready");
+ assert_eq!(value["result"]["signer_account_id"], user.id.as_str());
+ assert_eq!(value["result"]["binding"]["state"], "ready");
+ assert_eq!(
+ value["result"]["binding"]["resolved_signer_session_id"],
+ "session-ready"
+ );
+ assert_eq!(value["result"]["binding"]["matched_session_count"], 1);
+ assert_eq!(
+ value["result"]["write_kinds"]
+ .as_array()
+ .expect("write kinds")
+ .iter()
+ .filter(|kind| kind["ready"] == true)
+ .count(),
+ 4
+ );
+}
+
fn configure_myc_mode(sandbox: &RadrootsCliSandbox, executable: &Path) {
sandbox.write_app_config(&format!(
"[signer]\nmode = \"myc\"\n\n[myc]\nexecutable = \"{}\"\n",
@@ -129,6 +242,86 @@ fn configure_myc_mode(sandbox: &RadrootsCliSandbox, executable: &Path) {
));
}
+fn configure_myc_mode_with_binding(
+ sandbox: &RadrootsCliSandbox,
+ executable: &Path,
+ target_kind: &str,
+ target: &str,
+ managed_account_ref: Option<&str>,
+ signer_session_ref: Option<&str>,
+) {
+ let mut raw = format!(
+ "[signer]\nmode = \"myc\"\n\n[myc]\nexecutable = \"{}\"\n\n[[capability_binding]]\ncapability = \"signer.remote_nip46\"\nprovider = \"myc\"\ntarget_kind = \"{}\"\ntarget = \"{}\"\n",
+ toml_string(executable.display().to_string().as_str()),
+ toml_string(target_kind),
+ toml_string(target)
+ );
+ if let Some(value) = managed_account_ref {
+ raw.push_str(&format!(
+ "managed_account_ref = \"{}\"\n",
+ toml_string(value)
+ ));
+ }
+ if let Some(value) = signer_session_ref {
+ raw.push_str(&format!(
+ "signer_session_ref = \"{}\"\n",
+ toml_string(value)
+ ));
+ }
+ sandbox.write_app_config(raw.as_str());
+}
+
+#[cfg(unix)]
+fn write_fake_myc_status(
+ sandbox: &RadrootsCliSandbox,
+ name: &str,
+ payload: Value,
+) -> std::path::PathBuf {
+ let raw = serde_json::to_string(&payload).expect("myc status payload");
+ sandbox.write_fake_myc(
+ name,
+ format!("printf '%s\\n' '{}'", shell_single_quoted(raw.as_str())).as_str(),
+ )
+}
+
+fn ready_myc_payload(remote_sessions: Vec<Value>) -> Value {
+ json!({
+ "status_contract_version": 1,
+ "status": "ready",
+ "ready": true,
+ "signer_backend": {
+ "remote_session_count": remote_sessions.len(),
+ "remote_sessions": remote_sessions
+ }
+ })
+}
+
+fn remote_session(
+ connection_id: &str,
+ signer_identity: &RadrootsIdentityPublic,
+ user_identity: &RadrootsIdentityPublic,
+ permissions: &str,
+) -> Value {
+ json!({
+ "connection_id": connection_id,
+ "signer_identity": signer_identity,
+ "user_identity": user_identity,
+ "relays": ["wss://relay.example.test"],
+ "permissions": permissions
+ })
+}
+
+fn identity_public(seed: u8) -> RadrootsIdentityPublic {
+ let secret = [seed; 32];
+ RadrootsIdentity::from_secret_key_bytes(&secret)
+ .expect("fixture identity")
+ .to_public()
+}
+
+fn shell_single_quoted(value: &str) -> String {
+ value.replace('\'', "'\"'\"'")
+}
+
fn toml_string(value: &str) -> String {
value.replace('\\', "\\\\").replace('"', "\\\"")
}
diff --git a/tests/support/mod.rs b/tests/support/mod.rs
@@ -3,6 +3,7 @@
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
+use std::sync::Mutex;
use assert_cmd::prelude::*;
use serde_json::Value;
@@ -11,6 +12,8 @@ use tempfile::TempDir;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
+static COMMAND_LOCK: Mutex<()> = Mutex::new(());
+
pub fn radroots() -> Command {
Command::cargo_bin("radroots").expect("binary")
}
@@ -47,6 +50,7 @@ impl RadrootsCliSandbox {
}
pub fn json_success(&self, args: &[&str]) -> Value {
+ let _guard = COMMAND_LOCK.lock().expect("cli command lock");
let output = self.command().args(args).output().expect("run command");
assert!(
output.status.success(),
@@ -58,6 +62,7 @@ impl RadrootsCliSandbox {
}
pub fn json_output(&self, args: &[&str]) -> (Output, Value) {
+ let _guard = COMMAND_LOCK.lock().expect("cli command lock");
let output = self.command().args(args).output().expect("run command");
let value = json_from_stdout(&output);
(output, value)