v0.1.15 - Rename binaries to nsigner/nsigner_client, integrate client into release pipeline, update role+path authorization model with wizard presets and wildcard path support

This commit is contained in:
Laan Tungir
2026-08-05 18:59:47 -04:00
parent 821245ac1d
commit e65ed5c5d6
28 changed files with 5310 additions and 730 deletions
File diff suppressed because it is too large Load Diff
+821
View File
@@ -0,0 +1,821 @@
#!/bin/bash
#
# test_n_signer_client.sh — Integration test suite for n_signer_client CLI.
#
# Spawns a dedicated nsigner server with a known test mnemonic, runs the
# full verb surface through build/n_signer_client, and tears down.
#
# Usage:
# make test-n-signer-client
# # or directly:
# bash tests/test_n_signer_client.sh
#
# Prerequisites:
# - make dev clients (or at least build/nsigner and build/nsigner_client)
# - jq (optional, falls back to python3/grep)
#
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
BUILD_DIR="$PROJECT_DIR/build"
CLIENT="$BUILD_DIR/nsigner_client"
SERVER="$BUILD_DIR/nsigner"
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
MNEMONIC="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
SOCKET_NAME="nsigner_test_client_$$"
SERVER_PID=""
PASS_COUNT=0
FAIL_COUNT=0
SKIP_COUNT=0
# Test event JSON (kind 1, deterministic created_at)
EVENT_JSON='{"kind":1,"content":"hello from test","tags":[],"created_at":1700000000}'
# A known secp256k1 public key for NIP-04/44 tests (64 hex chars).
# We'll derive this from the signer at runtime, but we also need a peer key.
# Use a well-known test vector pubkey (any valid 64-hex secp256k1 pubkey).
# This is the pubkey for "abandon..." mnemonic, role=main path=m/44'/1237'/0'/0/0,
# but we'll discover it dynamically. For peer operations we can use the same
# pubkey (encrypt to self).
PEER_PUBKEY="" # filled at runtime
# ---------------------------------------------------------------------------
# Tool detection
# ---------------------------------------------------------------------------
HAS_JQ=0
HAS_PYTHON=0
if command -v jq &>/dev/null; then
HAS_JQ=1
elif command -v python3 &>/dev/null; then
HAS_PYTHON=1
fi
json_get() {
# Usage: json_get <key> <json-string>
# Returns the string value of <key> from the JSON.
local key="$1"
local json="$2"
if [ "$HAS_JQ" -eq 1 ]; then
echo "$json" | jq -r ".$key // empty"
elif [ "$HAS_PYTHON" -eq 1 ]; then
python3 -c "import sys,json; d=json.loads('$json'); print(d.get('$key',''))"
else
# Fallback: grep for "key":"value" pattern
echo "$json" | grep -o "\"$key\":\"[^\"]*\"" | sed "s/\"$key\":\"//;s/\"//" | head -1
fi
}
json_has_key() {
local key="$1"
local json="$2"
if [ "$HAS_JQ" -eq 1 ]; then
echo "$json" | jq -e ". | has(\"$key\")" &>/dev/null
elif [ "$HAS_PYTHON" -eq 1 ]; then
python3 -c "import sys,json; d=json.loads('$json'); sys.exit(0 if '$key' in d else 1)"
else
echo "$json" | grep -q "\"$key\""
fi
}
# ---------------------------------------------------------------------------
# Test harness
# ---------------------------------------------------------------------------
print_result() {
local name="$1"
local status="$2"
local detail="${3:-}"
if [ "$status" = "PASS" ]; then
echo " PASS $name"
elif [ "$status" = "SKIP" ]; then
echo " SKIP $name${detail:+: $detail}"
else
echo " FAIL $name${detail:+: $detail}"
fi
}
pass() {
local name="$1"
PASS_COUNT=$((PASS_COUNT + 1))
print_result "$name" "PASS"
}
fail() {
local name="$1"
local detail="${2:-}"
FAIL_COUNT=$((FAIL_COUNT + 1))
print_result "$name" "FAIL" "$detail"
}
skip() {
local name="$1"
local reason="${2:-}"
SKIP_COUNT=$((SKIP_COUNT + 1))
print_result "$name" "SKIP" "$reason"
}
# Run a command, check exit code, and optionally grep stdout.
# Usage: check_test <test-name> <expected-exit> [<grep-pattern>...]
check_test() {
local name="$1"
local expected_exit="$2"
shift 2
local patterns=("$@")
# Build the command from remaining args (everything after patterns)
# We need to be careful: the caller passes the command as the last arguments
# But we already consumed name and expected_exit. The remaining args are
# patterns + command. We need to separate them.
# Actually, let's use a different approach: capture patterns and command separately.
# We'll use a sentinel approach: patterns end before '--'
# But that's awkward. Let's just use a simpler helper.
# For now, we'll use a simpler inline approach in each test.
:
}
# ---------------------------------------------------------------------------
# Server management
# ---------------------------------------------------------------------------
start_server() {
echo "Starting nsigner server (socket: @$SOCKET_NAME)..."
# Build the server if not present
if [ ! -x "$SERVER" ]; then
echo "Building nsigner server..."
(cd "$PROJECT_DIR" && make dev) || {
echo "ERROR: failed to build nsigner server"
exit 1
}
fi
# Build the client if not present
if [ ! -x "$CLIENT" ]; then
echo "Building n_signer_client..."
(cd "$PROJECT_DIR" && make clients) || {
echo "ERROR: failed to build n_signer_client"
exit 1
}
fi
# Start the server with --mnemonic-stdin and --allow-all
# Set test env vars so non-interactive prompts auto-allow
# Note: export is needed so the backgrounded server process inherits it
export NSIGNER_TEST_NONINTERACTIVE_PROMPT=allow
echo "$MNEMONIC" | "$SERVER" \
--socket-name "$SOCKET_NAME" \
--allow-all \
--listen unix \
--mnemonic-stdin &
SERVER_PID=$!
# Wait for the server to be ready by polling /proc/net/unix
local max_attempts=50
local attempt=0
while [ $attempt -lt $max_attempts ]; do
if grep -q "$SOCKET_NAME" /proc/net/unix 2>/dev/null; then
echo "Server ready (PID $SERVER_PID, socket @$SOCKET_NAME)"
return 0
fi
sleep 0.1
attempt=$((attempt + 1))
done
# Fallback: try connecting with the client
if $CLIENT --socket-name "$SOCKET_NAME" --timeout 2000 get-info &>/dev/null; then
echo "Server ready (PID $SERVER_PID, socket @$SOCKET_NAME)"
return 0
fi
echo "ERROR: server did not become ready within ${max_attempts} attempts"
kill "$SERVER_PID" 2>/dev/null
SERVER_PID=""
return 1
}
stop_server() {
if [ -n "$SERVER_PID" ]; then
echo "Stopping server (PID $SERVER_PID)..."
kill "$SERVER_PID" 2>/dev/null
wait "$SERVER_PID" 2>/dev/null || true
SERVER_PID=""
fi
}
cleanup() {
stop_server
}
# ---------------------------------------------------------------------------
# Test functions
# ---------------------------------------------------------------------------
test_get_info() {
echo ""
echo "=== Basic connectivity ==="
# get-info
local name="get-info returns server metadata"
local output
output=$($CLIENT --socket-name "$SOCKET_NAME" get-info 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
# The server returns the result as a JSON string with escaped quotes.
# Grep for field names without surrounding quotes to handle both cases.
if printf '%s\n' "$output" | grep -q 'name' && \
printf '%s\n' "$output" | grep -q 'verbs' && \
printf '%s\n' "$output" | grep -q 'algorithms'; then
pass "$name"
else
fail "$name" "missing expected fields: $output"
fi
}
test_get_public_key_nostr() {
local name="get-public-key (nostr, default role) returns 64 hex chars"
local output
output=$($CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" get-public-key 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
# Should be 64 hex characters
if printf '%s\n' "$output" | grep -qE '^[0-9a-f]{64}$'; then
pass "$name"
PEER_PUBKEY="$output"
else
fail "$name" "expected 64 hex chars, got: $output"
fi
name="get-public-key --role main --path \"m/44'/1237'/0'/0/0\" returns 64 hex chars"
output=$($CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" get-public-key 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
if printf '%s\n' "$output" | grep -qE '^[0-9a-f]{64}$'; then
pass "$name"
else
fail "$name" "expected 64 hex chars, got: $output"
fi
name="get-public-key --role main --path \"m/44'/1237'/0'/0/0\" --format structured returns JSON"
output=$($CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" --format structured get-public-key 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
if printf '%s\n' "$output" | grep -q '"algorithm"' && \
printf '%s\n' "$output" | grep -q '"public_key"'; then
pass "$name"
else
fail "$name" "expected structured JSON, got: $output"
fi
}
test_sign_event() {
echo ""
echo "=== Sign event ==="
local name="sign-event from stdin (pipe)"
local output
output=$(echo "$EVENT_JSON" | $CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" sign-event 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
if printf '%s\n' "$output" | grep -q '"id"' && \
printf '%s\n' "$output" | grep -q '"pubkey"' && \
printf '%s\n' "$output" | grep -q '"sig"'; then
pass "$name"
else
fail "$name" "expected signed event JSON, got: $output"
return
fi
# Verify pubkey matches get-public-key output
local signed_pubkey
signed_pubkey=$(echo "$output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('pubkey',''))" 2>/dev/null)
local expected_pubkey
expected_pubkey=$($CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" get-public-key 2>/dev/null)
if [ "$signed_pubkey" = "$expected_pubkey" ]; then
pass "sign-event pubkey matches get-public-key"
else
fail "sign-event pubkey matches get-public-key" "expected $expected_pubkey, got $signed_pubkey"
fi
# Verify sig is 128 hex chars (schnorr signature)
local sig
sig=$(echo "$output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('sig',''))" 2>/dev/null)
if echo "$sig" | grep -qE '^[0-9a-f]{128}$'; then
pass "sign-event sig is 128 hex chars"
else
fail "sign-event sig is 128 hex chars" "got length ${#sig}: $sig"
fi
# sign-event from argv
name="sign-event from argv"
output=$($CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" sign-event "$EVENT_JSON" 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
if printf '%s\n' "$output" | grep -q '"id"' && \
printf '%s\n' "$output" | grep -q '"pubkey"' && \
printf '%s\n' "$output" | grep -q '"sig"'; then
pass "$name"
else
fail "$name" "expected signed event JSON, got: $output"
fi
}
test_mine_event() {
echo ""
echo "=== Mine event ==="
local name="mine-event with difficulty 4"
local output
# Use a short timeout to avoid hanging
output=$(echo "$EVENT_JSON" | $CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" --difficulty 4 mine-event 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
# The mine-event result wraps the signed event in an "event" field as a JSON string.
# Check for the wrapper fields and also verify the inner event has id/pubkey/sig.
if printf '%s\n' "$output" | grep -q '"event"' && \
printf '%s\n' "$output" | grep -q '"achieved_difficulty"' && \
printf '%s\n' "$output" | grep -q '"target_reached"'; then
pass "$name"
else
fail "$name" "expected mined event JSON with event/achieved_difficulty/target_reached, got: $output"
fi
}
test_nip04_roundtrip() {
echo ""
echo "=== NIP-04 encrypt/decrypt round-trip ==="
local plaintext="hello_nip04_test"
local name="nip04-encrypt returns ciphertext"
local ciphertext
ciphertext=$($CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" nip04-encrypt "$PEER_PUBKEY" "$plaintext" 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
if [ -n "$ciphertext" ]; then
pass "$name"
else
fail "$name" "empty ciphertext"
return
fi
name="nip04-decrypt recovers plaintext"
local decrypted
decrypted=$(echo "$ciphertext" | $CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" nip04-decrypt "$PEER_PUBKEY" 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
if [ "$decrypted" = "$plaintext" ]; then
pass "$name"
else
fail "$name" "expected '$plaintext', got '$decrypted'"
fi
}
test_nip44_roundtrip() {
echo ""
echo "=== NIP-44 encrypt/decrypt round-trip ==="
local plaintext="hello_nip44_test"
local name="nip44-encrypt returns ciphertext"
local ciphertext
ciphertext=$($CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" nip44-encrypt "$PEER_PUBKEY" "$plaintext" 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
if [ -n "$ciphertext" ]; then
pass "$name"
else
fail "$name" "empty ciphertext"
return
fi
name="nip44-decrypt recovers plaintext"
local decrypted
decrypted=$(echo "$ciphertext" | $CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" nip44-decrypt "$PEER_PUBKEY" 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
if [ "$decrypted" = "$plaintext" ]; then
pass "$name"
else
fail "$name" "expected '$plaintext', got '$decrypted'"
fi
}
test_algorithm_verbs() {
echo ""
echo "=== Algorithm-based verbs ==="
local name="get-public-key --algorithm secp256k1 --index 0"
local output
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm secp256k1 --index 0 get-public-key 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
if printf '%s\n' "$output" | grep -q '"algorithm":"secp256k1"' && \
printf '%s\n' "$output" | grep -q '"public_key"'; then
pass "$name"
else
fail "$name" "got: $output"
fi
name="get-public-key --algorithm ed25519 --index 0"
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ed25519 --index 0 get-public-key 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
if printf '%s\n' "$output" | grep -q '"algorithm":"ed25519"'; then
pass "$name"
else
fail "$name" "got: $output"
fi
name="sign --algorithm ed25519 --index 0 68656c6c6f"
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ed25519 --index 0 sign "68656c6c6f" 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
if printf '%s\n' "$output" | grep -q '"signature"'; then
pass "$name"
else
fail "$name" "got: $output"
return
fi
# Extract the signature for verify test
local ed_sig
ed_sig=$(echo "$output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('signature',''))" 2>/dev/null)
name="verify --algorithm ed25519 --index 0 68656c6c6f (valid)"
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ed25519 --index 0 verify "68656c6c6f" "$ed_sig" 2>/dev/null) || {
local rc=$?
if [ $rc -eq 1 ]; then
fail "$name" "signature reported as invalid"
else
fail "$name" "exit code $rc"
fi
return
}
if printf '%s\n' "$output" | grep -q "valid"; then
pass "$name"
else
fail "$name" "expected 'valid', got: $output"
fi
name="verify --algorithm ed25519 --index 0 68656c6c6f (invalid sig)"
local wrong_sig="abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01"
set +e
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ed25519 --index 0 verify "68656c6c6f" "$wrong_sig" 2>/dev/null)
local rc=$?
set -e
if [ $rc -eq 1 ] && printf '%s\n' "$output" | grep -q "invalid"; then
pass "$name"
else
fail "$name" "expected exit 1 + 'invalid', got exit $rc: $output"
fi
name="derive --algorithm secp256k1 --index 0 'test-data'"
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm secp256k1 --index 0 derive "test-data" 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
if printf '%s\n' "$output" | grep -q '"digest"'; then
local digest
digest=$(printf '%s\n' "$output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('digest',''))" 2>/dev/null)
if printf '%s\n' "$digest" | grep -qE '^[0-9a-f]{64}$'; then
pass "$name"
else
fail "$name" "digest not 64 hex chars: $digest"
fi
else
fail "$name" "no digest field: $output"
fi
name="derive-shared-secret --algorithm x25519 --index 0"
# Use the nostr pubkey as the peer (it's a valid secp256k1 point, which x25519 can work with)
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm x25519 --index 0 derive-shared-secret "$PEER_PUBKEY" 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
# The server returns structured JSON for derive-shared-secret
if printf '%s\n' "$output" | grep -q '"shared_secret"' && \
printf '%s\n' "$output" | grep -q '"algorithm"'; then
local ss
ss=$(printf '%s\n' "$output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('shared_secret',''))" 2>/dev/null)
if printf '%s\n' "$ss" | grep -qE '^[0-9a-f]{64}$'; then
pass "$name"
else
fail "$name" "shared_secret not 64 hex chars: $ss"
fi
else
fail "$name" "expected structured JSON with shared_secret, got: $output"
fi
}
test_ml_kem_roundtrip() {
echo ""
echo "=== ML-KEM-768 encapsulate/decapsulate round-trip ==="
local name="get-public-key --algorithm ml-kem-768 --index 0"
local output
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ml-kem-768 --index 0 get-public-key 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
if printf '%s\n' "$output" | grep -q '"algorithm":"ml-kem-768"' && \
printf '%s\n' "$output" | grep -q '"public_key"'; then
pass "$name"
else
fail "$name" "got: $output"
return
fi
# Extract the ML-KEM public key
local mlkem_pubkey
mlkem_pubkey=$(echo "$output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('public_key',''))" 2>/dev/null)
if [ -z "$mlkem_pubkey" ]; then
fail "extract ml-kem-768 pubkey" "empty"
return
fi
name="encapsulate --algorithm ml-kem-768 with peer pubkey"
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ml-kem-768 encapsulate "$mlkem_pubkey" 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
if printf '%s\n' "$output" | grep -q '"ciphertext"' && \
printf '%s\n' "$output" | grep -q '"shared_secret"'; then
pass "$name"
else
fail "$name" "got: $output"
return
fi
# Extract ciphertext and shared_secret from encapsulate
local ct enc_ss
ct=$(echo "$output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('ciphertext',''))" 2>/dev/null)
enc_ss=$(echo "$output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('shared_secret',''))" 2>/dev/null)
name="decapsulate --algorithm ml-kem-768 --index 0 with ciphertext"
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ml-kem-768 --index 0 decapsulate "$ct" 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
if printf '%s\n' "$output" | grep -q '"shared_secret"'; then
pass "$name"
else
fail "$name" "got: $output"
return
fi
# Verify shared secrets match
local dec_ss
dec_ss=$(echo "$output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('shared_secret',''))" 2>/dev/null)
if [ "$enc_ss" = "$dec_ss" ]; then
pass "ML-KEM-768 encapsulate/decapsulate shared secrets match"
else
fail "ML-KEM-768 encapsulate/decapsulate shared secrets match" "enc=$enc_ss dec=$dec_ss"
fi
}
test_otp_encrypt_decrypt() {
echo ""
echo "=== OTP encrypt/decrypt ==="
local name="encrypt --algorithm otp (base64 plaintext)"
local plaintext_b64="SGVsbG8gT1RQIQ==" # "Hello OTP!" in base64
local rc=0
local stdout_file
local stderr_file
stdout_file=$(mktemp /tmp/otp_stdout_XXXXXX)
stderr_file=$(mktemp /tmp/otp_stderr_XXXXXX)
# Run the command, capturing stdout and stderr separately
set +e
"$CLIENT" --socket-name "$SOCKET_NAME" --algorithm otp encrypt "$plaintext_b64" >"$stdout_file" 2>"$stderr_file"
rc=$?
set -e
if [ $rc -ne 0 ]; then
local stderr_text
stderr_text=$(cat "$stderr_file")
rm -f "$stdout_file" "$stderr_file"
# Check if the error is about missing OTP pad
if printf '%s\n' "$stderr_text" | grep -qi "otp_pad\|pad_not_bound\|no pad\|not available\|not supported\|not configured"; then
skip "$name" "OTP pad not available on server"
return
fi
fail "$name" "exit code $rc stderr: $stderr_text"
return
fi
local ciphertext
ciphertext=$(cat "$stdout_file")
rm -f "$stdout_file" "$stderr_file"
if [ -n "$ciphertext" ]; then
pass "$name"
else
fail "$name" "empty output"
return
fi
name="decrypt --algorithm otp"
stdout_file=$(mktemp /tmp/otp_stdout_XXXXXX)
stderr_file=$(mktemp /tmp/otp_stderr_XXXXXX)
set +e
"$CLIENT" --socket-name "$SOCKET_NAME" --algorithm otp decrypt "$ciphertext" >"$stdout_file" 2>"$stderr_file"
rc=$?
set -e
local decrypted
decrypted=$(cat "$stdout_file")
rm -f "$stdout_file" "$stderr_file"
if [ $rc -eq 0 ] && [ -n "$decrypted" ]; then
pass "$name"
else
fail "$name" "exit code $rc output: $decrypted"
fi
}
test_call_verb() {
echo ""
echo "=== Generic call verb ==="
local name="call get_info via stdin"
local output
output=$(echo '[]' | $CLIENT --socket-name "$SOCKET_NAME" call get_info 2>/dev/null) || {
fail "$name" "exit code $?"
return
}
if printf '%s\n' "$output" | grep -q 'name' && \
printf '%s\n' "$output" | grep -q 'verbs'; then
pass "$name"
else
fail "$name" "got: $output"
fi
}
test_error_cases() {
echo ""
echo "=== Error cases ==="
local name="No socket found (bogus socket name)"
local rc=0
local stderr_file
stderr_file=$(mktemp /tmp/err_stderr_XXXXXX)
set +e
$CLIENT --socket-name "nonexistent_socket_$$" --timeout 1000 get-info 2>"$stderr_file" >/dev/null
rc=$?
set -e
local stderr_text
stderr_text=$(cat "$stderr_file")
rm -f "$stderr_file"
if [ $rc -ne 0 ] && [ -n "$stderr_text" ]; then
pass "$name"
else
fail "$name" "expected non-zero exit + stderr, got exit $rc stderr: $stderr_text"
fi
name="--index 5 without --algorithm"
stderr_file=$(mktemp /tmp/err_stderr_XXXXXX)
set +e
$CLIENT --socket-name "$SOCKET_NAME" --index 5 get-public-key 2>"$stderr_file" >/dev/null
rc=$?
set -e
stderr_text=$(cat "$stderr_file")
rm -f "$stderr_file"
if [ $rc -ne 0 ] && printf '%s\n' "$stderr_text" | grep -qi "index.*only valid\|--index"; then
pass "$name"
else
fail "$name" "expected error about --index, got exit $rc: $stderr_text"
fi
name="Unknown verb"
stderr_file=$(mktemp /tmp/err_stderr_XXXXXX)
set +e
$CLIENT --socket-name "$SOCKET_NAME" nonexistent-verb 2>"$stderr_file" >/dev/null
rc=$?
set -e
stderr_text=$(cat "$stderr_file")
rm -f "$stderr_file"
if [ $rc -ne 0 ] && printf '%s\n' "$stderr_text" | grep -qi "unknown verb"; then
pass "$name"
else
fail "$name" "expected unknown verb error, got exit $rc: $stderr_text"
fi
name="verify with malformed signature (exit 2)"
stderr_file=$(mktemp /tmp/err_stderr_XXXXXX)
set +e
$CLIENT --socket-name "$SOCKET_NAME" --algorithm ed25519 --index 0 verify "68656c6c6f" "nothex" 2>"$stderr_file"
rc=$?
set -e
stderr_text=$(cat "$stderr_file")
rm -f "$stderr_file"
# Should be exit 2 (error), not exit 1 (invalid)
if [ $rc -eq 2 ]; then
pass "$name"
else
fail "$name" "expected exit 2 (error), got exit $rc: $stderr_text"
fi
}
test_auto_discovery() {
echo ""
echo "=== Auto-discovery ==="
local name="Auto-discover socket (only test signer running)"
# This is best-effort: if only our test signer is running, it should work.
# If other signers are running, skip.
local rc=0
local stdout_file
local stderr_file
stdout_file=$(mktemp /tmp/auto_stdout_XXXXXX)
stderr_file=$(mktemp /tmp/auto_stderr_XXXXXX)
set +e
$CLIENT get-info >"$stdout_file" 2>"$stderr_file"
rc=$?
set -e
local output
output=$(cat "$stdout_file")
local stderr_text
stderr_text=$(cat "$stderr_file")
rm -f "$stdout_file" "$stderr_file"
if [ $rc -eq 0 ]; then
if printf '%s\n' "$output" | grep -q 'name'; then
pass "$name"
else
fail "$name" "got output but missing 'name' field: $output"
fi
else
if printf '%s\n' "$stderr_text" | grep -qi "multiple"; then
skip "$name" "multiple signer sockets found"
else
skip "$name" "auto-discovery failed: $stderr_text"
fi
fi
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
trap cleanup EXIT INT TERM
echo "============================================"
echo " n_signer_client Integration Test Suite"
echo "============================================"
echo ""
# Start the server
start_server || {
echo "FATAL: could not start nsigner server"
exit 1
}
# Run tests
test_get_info
test_get_public_key_nostr
test_sign_event
test_mine_event
test_nip04_roundtrip
test_nip44_roundtrip
test_algorithm_verbs
test_ml_kem_roundtrip
test_otp_encrypt_decrypt
test_call_verb
test_error_cases
test_auto_discovery
# Summary
echo ""
echo "============================================"
echo " Results"
echo "============================================"
echo " PASS: $PASS_COUNT"
echo " FAIL: $FAIL_COUNT"
echo " SKIP: $SKIP_COUNT"
echo " TOTAL: $((PASS_COUNT + FAIL_COUNT + SKIP_COUNT))"
echo "============================================"
if [ "$FAIL_COUNT" -gt 0 ]; then
exit 1
fi
exit 0