app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

local_relay_e2e.rs (5424B)


      1 use std::time::Duration;
      2 
      3 use harvestcircle_application::{
      4     Clock, DurableRequestId, InMemorySecretStore, ProfileLoadState, ProfileRepository,
      5     RelayConfiguration, RelayConnectionState, SecretStore, SessionState,
      6 };
      7 use harvestcircle_domain::{SecretKeyInput, UnixTimestamp};
      8 use harvestcircle_nostr::SdkNostrClient;
      9 use harvestcircle_runtime::PersistentAppCore;
     10 use nostr::{EventBuilder, Keys, Metadata};
     11 use nostr_relay_builder::MockRelay;
     12 use nostr_sdk::Client;
     13 use radroots_runtime_paths::{
     14     InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver,
     15     RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, ServiceId,
     16 };
     17 use radroots_service_sqlite::MigrationBuildIdentity;
     18 use radroots_transport_nostr::{RelayAccess, RelayEndpoint, RelayUrlPolicy};
     19 
     20 const SECRET_HEX: &str = "7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7";
     21 
     22 struct FixedClock;
     23 
     24 impl Clock for FixedClock {
     25     fn now(&self) -> UnixTimestamp {
     26         UnixTimestamp::from_seconds(100).expect("fixed timestamp")
     27     }
     28 }
     29 
     30 #[tokio::test]
     31 async fn local_relay_e2e_imports_activates_refreshes_and_caches_profile() {
     32     let directory = tempfile::tempdir().expect("temporary directory");
     33     let context = RuntimeContext::resolve(
     34         &RadrootsPathResolver::new(
     35             RadrootsPlatform::current(),
     36             RadrootsHostEnvironment::default(),
     37         ),
     38         RuntimeContextBootstrap::new(
     39             RadrootsPathProfile::RepoLocal,
     40             Some(directory.path().canonicalize().expect("canonical root")),
     41             RuntimeContextSource::BootstrapCli,
     42             RuntimeContextSource::SafeDefault,
     43         )
     44         .expect("bootstrap"),
     45         ServiceId::new("harvestcircle").expect("service"),
     46         InstanceId::new("desktop").expect("instance"),
     47     )
     48     .expect("context");
     49     std::fs::create_dir_all(directory.path().join("data")).expect("state root");
     50     let build = MigrationBuildIdentity::new(
     51         "0.1.0-alpha",
     52         "1111111111111111111111111111111111111111",
     53         "2222222222222222222222222222222222222222",
     54         "1.97.1",
     55         "test",
     56         "test",
     57         1,
     58         1,
     59         1,
     60         1,
     61         1,
     62     )
     63     .expect("build");
     64     let local_relay = MockRelay::run().await.expect("local relay");
     65     let relay_url = local_relay.url().await;
     66     let keys = Keys::parse(SECRET_HEX).expect("known secret key");
     67     let publisher = Client::new(keys);
     68     publisher
     69         .add_relay(relay_url.clone())
     70         .await
     71         .expect("publisher relay");
     72     publisher.connect().await;
     73     publisher.wait_for_connection(Duration::from_secs(2)).await;
     74     publisher
     75         .send_event_builder(EventBuilder::metadata(
     76             &Metadata::new()
     77                 .name("farmer")
     78                 .display_name("Farm Identity")
     79                 .about("Local food profile"),
     80         ))
     81         .await
     82         .expect("publish profile");
     83 
     84     let relay = RelayEndpoint::new(
     85         relay_url.as_str(),
     86         RelayUrlPolicy::Local,
     87         RelayAccess::ReadWrite,
     88     )
     89     .expect("relay endpoint");
     90     let adapter = PersistentAppCore::open(
     91         &context,
     92         RelayConfiguration::new(vec![relay]).expect("relay configuration"),
     93         100_000,
     94         100,
     95         &build,
     96     )
     97     .await
     98     .expect("persistent adapter");
     99     let secrets = InMemorySecretStore::default();
    100     adapter
    101         .bootstrap(&secrets, &FixedClock)
    102         .await
    103         .expect("bootstrap");
    104     let imported = adapter
    105         .import_secret_key_durable(
    106             &DurableRequestId::parse("01890f3e-7b1c-7000-8000-000000000201").expect("request"),
    107             adapter.core().snapshot().revision().value(),
    108             SecretKeyInput::parse(SECRET_HEX.to_owned()).expect("secret input"),
    109             &secrets,
    110             &FixedClock,
    111         )
    112         .await
    113         .expect("import identity");
    114     let public_key = imported.identity().public_key();
    115     assert!(
    116         secrets
    117             .contains(public_key)
    118             .await
    119             .expect("credential exists")
    120     );
    121     adapter
    122         .activate_identity(public_key, &secrets, &FixedClock)
    123         .await
    124         .expect("activate identity");
    125 
    126     let refreshed = adapter
    127         .core()
    128         .refresh_active_profile(
    129             adapter.database(),
    130             &SdkNostrClient::new(Duration::from_secs(2)),
    131             &FixedClock,
    132             std::time::Instant::now() + Duration::from_secs(2),
    133         )
    134         .await
    135         .expect("refresh profile");
    136 
    137     assert_eq!(refreshed.session(), SessionState::Active);
    138     let active = refreshed.active_identity().expect("active identity");
    139     assert_eq!(active.relay_state(), RelayConnectionState::Connected);
    140     assert_eq!(active.profile_state(), ProfileLoadState::Fresh);
    141     assert_eq!(
    142         active.profile().and_then(|profile| profile.display_name()),
    143         Some("Farm Identity")
    144     );
    145     let cached = adapter
    146         .database()
    147         .load_profile(public_key)
    148         .await
    149         .expect("load cache")
    150         .expect("cached profile");
    151     assert_eq!(
    152         cached.candidate().metadata().preferred_name(),
    153         Some("Farm Identity")
    154     );
    155     let public_debug = format!("{refreshed:?}");
    156     assert!(!public_debug.contains(SECRET_HEX));
    157     assert!(!public_debug.contains("nsec1"));
    158     adapter.close().await.expect("close");
    159     publisher.shutdown().await;
    160     local_relay.shutdown();
    161 }