nvpn 4.1.13

CLI and daemon for Nostr VPN private mesh networks
#[cfg(feature = "paid-exit")]
#[test]
fn paid_exit_offer_follows_listener_and_upstream_readiness_without_relays() {
    use nostr_vpn_core::config::NostrPubsubMode;
    use nostr_vpn_core::paid_routes::{PAID_ROUTE_OFFER_KIND, PAID_ROUTE_OFFER_TTL_SECS};
    use crate::session_runtime::daemon_vpn_paid_exit::{
        PaidExitOfferPublication, PaidExitOfferPublisher,
    };

    let nonce = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .expect("clock")
        .as_nanos();
    let directory = std::env::temp_dir().join(format!("nvpn-paid-exit-p2p-{nonce}"));
    std::fs::create_dir_all(&directory).expect("create test directory");
    let config_path = directory.join("config.toml");
    let mut app = AppConfig::generated();
    app.nostr.relays.clear();
    app.nostr.pubsub.mode = NostrPubsubMode::Client;
    app.paid_exit.enabled = true;
    app.paid_exit.pricing.price_msat_per_gb = 100;
    app.paid_exit.channel.accepted_mints = vec!["https://mint.example".to_string()];
    app.paid_exit.normalize();
    let now_unix = PAID_ROUTE_OFFER_TTL_SECS + 1_000;
    let keys = app.nostr_keys().expect("seller keys");
    let stale_signed_at = now_unix - PAID_ROUTE_OFFER_TTL_SECS;
    let stale_offer = signed_paid_exit_offer_from_config(
        "internet-exit",
        &keys,
        &app.paid_exit,
        None,
        stale_signed_at,
    )
    .expect("sign stale offer")
    .offer()
    .expect("decode stale offer");
    let stale = SignedPaidRouteOffer::sign_expiring_at(
        stale_offer,
        &keys,
        stale_signed_at,
        now_unix + 100,
    )
    .expect("sign stale offer with misleading long expiration");
    assert!(!stale.is_live_at(now_unix));
    persist_paid_exit_offer_snapshot(
        &paid_route_store_file_path(&config_path),
        &stale,
        &[],
        &stale.offer().expect("stale offer"),
        stale_signed_at,
    )
    .expect("store stale offer");

    let mut publisher = PaidExitOfferPublisher::load(&app, &config_path, now_unix);
    assert_eq!(
        publisher
            .reconcile(&app, &config_path, now_unix, false, false)
            .expect("keep unready seller hidden"),
        PaidExitOfferPublication::None
    );
    assert!(!crate::control_pubsub_runtime::control_pubsub_outbox_directory(&config_path).exists());

    assert_eq!(
        publisher
            .reconcile(&app, &config_path, now_unix, true, false)
            .expect("publish ready seller"),
        PaidExitOfferPublication::Published
    );
    assert_eq!(
        publisher
            .reconcile(&app, &config_path, now_unix, false, false)
            .expect("withdraw on listener or upstream loss"),
        PaidExitOfferPublication::Withdrawn(1)
    );
    assert_eq!(
        publisher
            .reconcile(&app, &config_path, now_unix + 1, true, false)
            .expect("republish after readiness returns"),
        PaidExitOfferPublication::Published
    );
    app.paid_exit.channel.accepted_mints.clear();
    let outbox = crate::control_pubsub_runtime::control_pubsub_outbox_directory(&config_path);
    let saved_outbox = directory.join("saved-control-pubsub-outbox");
    std::fs::rename(&outbox, &saved_outbox).expect("move offer outbox");
    std::fs::write(&outbox, b"temporarily blocked").expect("block offer outbox path");
    assert!(
        publisher
            .reconcile(&app, &config_path, now_unix + 1, true, false)
            .expect_err("transient outbox error")
            .to_string()
            .contains("failed to create")
    );
    std::fs::remove_file(&outbox).expect("unblock offer outbox path");
    std::fs::rename(&saved_outbox, &outbox).expect("restore offer outbox");
    assert_eq!(
        publisher
            .reconcile(&app, &config_path, now_unix + 1, true, false)
            .expect("withdraw offer after terms become invalid"),
        PaidExitOfferPublication::Withdrawn(1)
    );
    app.paid_exit.channel.accepted_mints = vec!["https://mint.example".to_string()];
    assert_eq!(
        publisher
            .reconcile(&app, &config_path, now_unix + 1, true, false)
            .expect("republish after valid terms return"),
        PaidExitOfferPublication::Published
    );
    app.paid_exit.enabled = false;
    assert_eq!(
        publisher
            .reconcile(&app, &config_path, now_unix + 1, true, false)
            .expect("withdraw when seller is disabled"),
        PaidExitOfferPublication::Withdrawn(1)
    );

    let mut events = std::fs::read_dir(
        crate::control_pubsub_runtime::control_pubsub_outbox_directory(&config_path),
    )
    .expect("read offer outbox")
    .map(|entry| {
        serde_json::from_slice::<nostr_sdk::prelude::Event>(
            &std::fs::read(entry.expect("outbox entry").path()).expect("read queued offer"),
        )
        .expect("decode queued offer")
    })
    .collect::<Vec<_>>();
    events.sort_by_key(|event| event.created_at);
    assert!(
        events
            .iter()
            .all(|event| u16::from(event.kind) == PAID_ROUTE_OFFER_KIND)
    );
    assert_eq!(
        events
            .iter()
            .map(|event| (
                event.created_at.as_secs(),
                event.tags.expiration().expect("offer expiry").as_secs()
                    - event.created_at.as_secs(),
            ))
            .collect::<Vec<_>>(),
        vec![
            (now_unix, PAID_ROUTE_OFFER_TTL_SECS),
            (now_unix + 1, 0),
            (now_unix + 2, PAID_ROUTE_OFFER_TTL_SECS),
            (now_unix + 3, 0),
            (now_unix + 4, PAID_ROUTE_OFFER_TTL_SECS),
            (now_unix + 5, 0),
        ]
    );

    let _ = std::fs::remove_dir_all(directory);
}