commit 804c37619c7bae69474c03803ee5adc5da61202c
parent 3fd36213bb5863d026b35b914e51a309dd821037
Author: triesap <tyson@radroots.org>
Date: Thu, 26 Mar 2026 17:00:30 +0000
tests: add sqlite persistence parity coverage
Diffstat:
| M | tests/nip46_e2e.rs | | | 143 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- |
| A | tests/persistence_cli.rs | | | 129 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 270 insertions(+), 2 deletions(-)
diff --git a/tests/nip46_e2e.rs b/tests/nip46_e2e.rs
@@ -8,8 +8,9 @@ use myc::control;
use myc::{
MycConfig, MycConnectionApproval, MycDiscoveryContext, MycDiscoveryLiveStatus,
MycDiscoveryRelayFetchStatus, MycDiscoveryRepairOutcome, MycOperationAuditKind,
- MycOperationAuditOutcome, MycOperationAuditRecord, MycRuntime, MycTransportDeliveryPolicy,
- diff_live_nip89, fetch_live_nip89, publish_nip89_event, refresh_nip89,
+ MycOperationAuditOutcome, MycOperationAuditRecord, MycRuntime, MycRuntimeAuditBackend,
+ MycSignerStateBackend, MycTransportDeliveryPolicy, diff_live_nip89, fetch_live_nip89,
+ publish_nip89_event, refresh_nip89,
};
use nostr::filter::MatchEventOptions;
use nostr::nips::nip44;
@@ -1397,6 +1398,144 @@ async fn live_listener_consumes_connect_secret_only_after_successful_publish() -
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn live_listener_works_with_sqlite_signer_state_and_runtime_audit() -> TestResult<()> {
+ let relay = TestRelay::spawn().await?;
+ let test_runtime = MycTestRuntime::new_with_transport_config(
+ &[relay.url()],
+ MycConnectionApproval::NotRequired,
+ |config| {
+ config.persistence.signer_state_backend = MycSignerStateBackend::Sqlite;
+ config.persistence.runtime_audit_backend = MycRuntimeAuditBackend::Sqlite;
+ },
+ );
+ let runtime = test_runtime.runtime.clone();
+ let signer_public_key = runtime.signer_identity().public_key();
+ let client_identity =
+ identity("5353535353535353535353535353535353535353535353535353535353535353");
+ let base_created_at = Timestamp::now().as_secs();
+
+ assert_eq!(
+ runtime
+ .paths()
+ .signer_state_path
+ .file_name()
+ .and_then(|name| name.to_str()),
+ Some("signer-state.sqlite")
+ );
+ assert_eq!(
+ runtime
+ .paths()
+ .runtime_audit_path
+ .file_name()
+ .and_then(|name| name.to_str()),
+ Some("operations.sqlite")
+ );
+
+ relay
+ .queue_publish_outcomes(signer_public_key, &[false, true])
+ .await;
+
+ let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
+ let service_runtime = runtime.clone();
+ let listener_task = tokio::spawn(async move {
+ service_runtime
+ .run_until(async {
+ let _ = shutdown_rx.await;
+ })
+ .await
+ });
+
+ relay.wait_for_subscription_count(1).await?;
+
+ let request_one = build_request_event(
+ &client_identity,
+ signer_public_key,
+ connect_request_message("sqlite-connect-1", signer_public_key, "sqlite-secret"),
+ base_created_at,
+ );
+ publish_event(relay.url(), &request_one).await?;
+ wait_for_connection_count(&runtime, 1).await?;
+ sleep(Duration::from_millis(100)).await;
+
+ assert!(
+ relay
+ .published_events_by_author(signer_public_key)
+ .await
+ .is_empty()
+ );
+ let initial_connection = runtime
+ .signer_manager()?
+ .list_connections()?
+ .into_iter()
+ .next()
+ .expect("stored connection");
+ assert!(!initial_connection.connect_secret_is_consumed());
+
+ let request_two = build_request_event(
+ &client_identity,
+ signer_public_key,
+ connect_request_message("sqlite-connect-2", signer_public_key, "sqlite-secret"),
+ base_created_at + 1,
+ );
+ publish_event(relay.url(), &request_two).await?;
+
+ let response_events = relay
+ .wait_for_published_events_by_author(signer_public_key, 1)
+ .await?;
+ let response = decrypt_response(&client_identity, signer_public_key, &response_events[0]);
+ assert_eq!(response.id, "sqlite-connect-2");
+ assert_eq!(
+ response.result,
+ Some(serde_json::Value::String("sqlite-secret".to_owned()))
+ );
+
+ wait_for_connect_secret_consumed(&runtime).await?;
+ let consumed_connection = runtime
+ .signer_manager()?
+ .list_connections()?
+ .into_iter()
+ .next()
+ .expect("stored connection");
+ assert!(consumed_connection.connect_secret_is_consumed());
+ let operation_audit = runtime.operation_audit_store().list_all()?;
+ assert_eq!(operation_audit.len(), 2);
+ assert_eq!(
+ operation_audit[0].outcome,
+ MycOperationAuditOutcome::Rejected
+ );
+ assert_eq!(
+ operation_audit[1].outcome,
+ MycOperationAuditOutcome::Succeeded
+ );
+
+ let restarted_runtime = MycRuntime::bootstrap(runtime.config().clone())?;
+ assert_eq!(
+ restarted_runtime
+ .signer_manager()?
+ .list_connections()?
+ .len(),
+ 1
+ );
+ assert_eq!(
+ restarted_runtime.operation_audit_store().list_all()?.len(),
+ 2
+ );
+ assert!(
+ restarted_runtime
+ .signer_manager()?
+ .list_connections()?
+ .into_iter()
+ .next()
+ .expect("persisted connection")
+ .connect_secret_is_consumed()
+ );
+
+ let _ = shutdown_tx.send(());
+ listener_task.await??;
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn trusted_client_reauths_after_authorized_ttl() -> TestResult<()> {
let relay = TestRelay::spawn().await?;
let client_identity =
diff --git a/tests/persistence_cli.rs b/tests/persistence_cli.rs
@@ -0,0 +1,129 @@
+use std::path::Path;
+use std::process::Command;
+
+use myc::{
+ MycConfig, MycOperationAuditKind, MycOperationAuditOutcome, MycOperationAuditRecord,
+ MycRuntime, MycRuntimeAuditBackend, MycSignerStateBackend,
+};
+use nostr::PublicKey;
+use radroots_identity::RadrootsIdentity;
+use radroots_nostr_signer::prelude::RadrootsNostrSignerConnectionDraft;
+use serde_json::Value;
+
+fn write_identity(path: &Path, secret_key: &str) {
+ RadrootsIdentity::from_secret_key_str(secret_key)
+ .expect("identity")
+ .save_json(path)
+ .expect("save identity");
+}
+
+fn bootstrap_populated_json_runtime(temp: &tempfile::TempDir) -> (MycConfig, MycConfig) {
+ let mut json_config = MycConfig::default();
+ json_config.paths.state_dir = temp.path().join("state");
+ json_config.paths.signer_identity_path = temp.path().join("signer.json");
+ json_config.paths.user_identity_path = temp.path().join("user.json");
+
+ write_identity(
+ &json_config.paths.signer_identity_path,
+ "1111111111111111111111111111111111111111111111111111111111111111",
+ );
+ write_identity(
+ &json_config.paths.user_identity_path,
+ "2222222222222222222222222222222222222222222222222222222222222222",
+ );
+
+ let runtime = MycRuntime::bootstrap(json_config.clone()).expect("json runtime");
+ let manager = runtime.signer_manager().expect("manager");
+ let connection = manager
+ .register_connection(RadrootsNostrSignerConnectionDraft::new(
+ PublicKey::from_hex("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")
+ .expect("pubkey"),
+ runtime.user_public_identity(),
+ ))
+ .expect("register connection");
+ runtime.record_operation_audit(&MycOperationAuditRecord::new(
+ MycOperationAuditKind::ListenerResponsePublish,
+ MycOperationAuditOutcome::Succeeded,
+ Some(&connection.connection_id),
+ Some("request-1"),
+ 1,
+ 1,
+ "publish succeeded",
+ ));
+
+ let mut sqlite_config = json_config.clone();
+ sqlite_config.persistence.signer_state_backend = MycSignerStateBackend::Sqlite;
+ sqlite_config.persistence.runtime_audit_backend = MycRuntimeAuditBackend::Sqlite;
+
+ (json_config, sqlite_config)
+}
+
+#[test]
+fn persistence_import_json_to_sqlite_cli_migrates_state_and_rejects_rerun() {
+ let temp = tempfile::tempdir().expect("tempdir");
+ let (_json_config, sqlite_config) = bootstrap_populated_json_runtime(&temp);
+ let env_path = temp.path().join("myc-sqlite.env");
+ std::fs::write(
+ &env_path,
+ sqlite_config.to_env_string().expect("render sqlite env"),
+ )
+ .expect("write env");
+
+ let output = Command::new(env!("CARGO_BIN_EXE_myc"))
+ .arg("--env-file")
+ .arg(&env_path)
+ .arg("persistence")
+ .arg("import-json-to-sqlite")
+ .output()
+ .expect("run import");
+
+ assert!(output.status.success(), "{:?}", output);
+
+ let parsed: Value = serde_json::from_slice(&output.stdout).expect("import json");
+ assert_eq!(parsed["signer_state"]["connection_count"], 1);
+ assert_eq!(parsed["signer_state"]["request_audit_count"], 0);
+ assert_eq!(parsed["runtime_audit"]["record_count"], 1);
+ assert!(
+ parsed["signer_state"]["destination_path"]
+ .as_str()
+ .expect("sqlite signer destination")
+ .ends_with("signer-state.sqlite")
+ );
+ assert!(
+ parsed["runtime_audit"]["destination_path"]
+ .as_str()
+ .expect("sqlite audit destination")
+ .ends_with("operations.sqlite")
+ );
+
+ let sqlite_runtime = MycRuntime::bootstrap(sqlite_config.clone()).expect("sqlite runtime");
+ assert_eq!(
+ sqlite_runtime
+ .signer_manager()
+ .expect("sqlite manager")
+ .list_connections()
+ .expect("sqlite connections")
+ .len(),
+ 1
+ );
+ assert_eq!(
+ sqlite_runtime
+ .operation_audit_store()
+ .list_all()
+ .expect("sqlite audit records")
+ .len(),
+ 1
+ );
+
+ let rerun = Command::new(env!("CARGO_BIN_EXE_myc"))
+ .arg("--env-file")
+ .arg(&env_path)
+ .arg("persistence")
+ .arg("import-json-to-sqlite")
+ .output()
+ .expect("rerun import");
+
+ assert!(!rerun.status.success(), "{:?}", rerun);
+ let stderr = String::from_utf8(rerun.stderr).expect("rerun stderr");
+ assert!(stderr.contains("sqlite signer-state destination"));
+}