Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-front/tests/one_sold_door.rs

//! **ONE shop-door client** (T16 / P4-REST, A28 M11): Gunnar's front calls the ONE monetize
//! through the shared client, `monetize-estate-side::sold_door`, as Holger's front does (P4a),
//! over `Fleet.Ask` (P3-FLEET). monetize H16 deleted the old `SoldEstate` door after its one
//! release of overlap: every call rides Fleet, signed as the estate's front, and a door that
//! names no product calls nothing. Over a real gRPC `Fleet` server in this process, every ask
//! verified under the estate's client key with monetize-embed's own verifier.

use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use monetize_estate_side::sold_door as shared;
use monetize_proto as pb;

const CLIENT_SEED: [u8; 32] = [0x11; 32];
const AT_MS: i64 = 1_761_400_000_000;

fn hex(b: &[u8]) -> String {
    b.iter().map(|x| format!("{x:02x}")).collect()
}

fn settings(addr: SocketAddr) -> toml::Table {
    let shop_key = hex(&ed25519_dalek::SigningKey::from_bytes(&[7; 32])
        .verifying_key()
        .to_bytes());
    format!("door = \"http://{addr}\"\ncompany = \"acme\"\nshop_pubkey = \"{shop_key}\"\n")
        .parse()
        .unwrap()
}

fn clock() -> Arc<dyn Fn() -> i64 + Send + Sync> {
    Arc::new(|| AT_MS)
}

/// ★ The bounds and the words are the shop's (`monetize-server::shop_cloud`, held equal in
/// `shop_pairing_contract.rs`) and the bench seam is refused by name as before.
#[test]
fn the_shared_clients_bounds_seam_and_settings_are_gunnars() {
    assert_eq!(
        shared::UPGRADE_WITHIN,
        Duration::from_secs(120 * 60),
        "the shop's whole keep boot"
    );
    assert_eq!(shared::READ_WITHIN, Duration::from_secs(30));
    // The one bound that moved (accepted, T16): Gunnar's front gave a Recharge 30 min, the shop's
    // own bound; the ONE client waits Holger's hour, so the front never gives up first.
    assert_eq!(shared::RECHARGE_WITHIN, Duration::from_secs(60 * 60));
    assert!(
        shared::RECHARGE_WITHIN >= Duration::from_secs(30 * 60),
        "never before the shop's 30 min"
    );
    let t = settings("127.0.0.1:1".parse().unwrap());
    let door = shared::ShopDoor::from_settings(&t, &hex(&CLIENT_SEED)).unwrap();
    assert_eq!(door.shop_host(), "127.0.0.1");
    let pem = "-----BEGIN CERTIFICATE-----\nBENCH\n-----END CERTIFICATE-----";
    let bench = door
        .clone()
        .with_seam(Some("10.0.2.2:35219"), Some(pem))
        .expect("a bench seam");
    assert_eq!(
        shared::dialled("https://monetize-test.ignalina.dk", bench.seam.as_ref()),
        "https://10.0.2.2:35219"
    );
    assert!(
        door.clone()
            .with_seam(Some("81.27.107.63:443"), Some(pem))
            .unwrap_err()
            .contains("fleet-seam-bench-only"),
        "a public address is no seam"
    );
    assert!(
        door.clone()
            .with_seam(None, Some(pem))
            .unwrap_err()
            .contains("fleet-seam-bench-only"),
        "a CA with no bench address"
    );
    // The one setting that moved (accepted, T16): Gunnar's front refused a door with no
    // `shop_pubkey`, though it verified nothing under it; the ONE client takes one, and a key
    // that is named must still be a key.
    let mut keyless = t.clone();
    keyless.remove("shop_pubkey");
    assert!(shared::ShopDoor::from_settings(&keyless, &hex(&CLIENT_SEED)).is_ok());
    let mut bad = t.clone();
    bad.insert("shop_pubkey".into(), toml::Value::String("zz".repeat(32)));
    assert!(shared::ShopDoor::from_settings(&bad, &hex(&CLIENT_SEED))
        .unwrap_err()
        .contains("shop_pubkey"));
}

// ── P3-FLEET: the calls ride Fleet (H16: no old door to fall back to) ───────────────────────

/// A ONE box after the fold: `Fleet` only (no `SoldEstate` mounted). Every front's ask is a
/// `FleetCall` of `role = front`, signed by the estate's client key, for product `gunnar`.
#[derive(Clone, Default)]
struct FleetOnly {
    asked: Arc<Mutex<Vec<String>>>,
}

#[tonic::async_trait]
impl pb::fleet_server::Fleet for FleetOnly {
    async fn report(
        &self,
        _: tonic::Request<pb::FleetSigned>,
    ) -> Result<tonic::Response<pb::FleetReportAnswer>, tonic::Status> {
        Err(tonic::Status::unimplemented("not this test's"))
    }
    async fn orders(
        &self,
        _: tonic::Request<pb::FleetSigned>,
    ) -> Result<tonic::Response<pb::FleetOrdersAnswer>, tonic::Status> {
        Err(tonic::Status::unimplemented("not this test's"))
    }
    async fn ask(
        &self,
        r: tonic::Request<pb::FleetSigned>,
    ) -> Result<tonic::Response<pb::FleetAskAnswer>, tonic::Status> {
        use prost::Message as _;
        let s = r.into_inner();
        let c = monetize_embed::fleet::FleetCall {
            product: s.product.clone(),
            instance: s.instance.clone(),
            role: s.role.clone(),
            call: s.call.clone(),
            body: s.body.clone(),
            at_unix_ms: s.at_unix_ms,
        };
        let client = monetize_embed::VerifyingKey::from_bytes(
            &ed25519_dalek::SigningKey::from_bytes(&CLIENT_SEED)
                .verifying_key()
                .to_bytes(),
        )
        .unwrap();
        monetize_embed::fleet::verify_fleet_call(&c, &s.signature, &client, s.at_unix_ms)
            .map_err(|e| tonic::Status::unauthenticated(format!("{e:?}")))?;
        if (s.product.as_str(), s.instance.as_str(), s.role.as_str()) != ("gunnar", "acme", "front")
        {
            return Err(tonic::Status::unauthenticated(format!(
                "not gunnar/acme's front: {}/{}/{}",
                s.product, s.instance, s.role
            )));
        }
        let ask: monetize_embed::fleet::Ask = serde_json::from_str(&s.body)
            .map_err(|e| tonic::Status::invalid_argument(e.to_string()))?;
        self.asked.lock().unwrap().push(ask.kind.clone());
        let answer = match ask.kind.as_str() {
            "update_take" => pb::EstateUpdateTakeResponse {
                open: true,
                release: "03".into(),
                role: "twin".into(),
                ..Default::default()
            }
            .encode_to_vec(),
            "recharge" => pb::EstateRechargeResponse {
                words: "the main laid again over Fleet".into(),
                ..Default::default()
            }
            .encode_to_vec(),
            other => {
                return Err(tonic::Status::invalid_argument(format!(
                    "no front ask {other}"
                )))
            }
        };
        Ok(tonic::Response::new(pb::FleetAskAnswer {
            answer,
            ..Default::default()
        }))
    }
}

fn serve_fleet(rt: &tokio::runtime::Runtime, fleet: FleetOnly) -> SocketAddr {
    let addr = std::net::TcpListener::bind("127.0.0.1:0")
        .unwrap()
        .local_addr()
        .unwrap();
    rt.spawn(
        tonic::transport::Server::builder()
            .add_service(pb::fleet_server::FleetServer::new(fleet))
            .serve(addr),
    );
    for _ in 0..200 {
        if std::net::TcpStream::connect(addr).is_ok() {
            return addr;
        }
        std::thread::sleep(Duration::from_millis(10));
    }
    panic!("the test Fleet did not listen at {addr}");
}

/// Gunnar's front's door as the feed raises it (no `product` in its `shop_door.toml`: a feed
/// from before the fold).
fn fed_door(addr: SocketAddr) -> shared::ShopDoor {
    let shop_key = hex(&ed25519_dalek::SigningKey::from_bytes(&[7; 32])
        .verifying_key()
        .to_bytes());
    let feed = std::collections::BTreeMap::from([
        (
            shared::FEED_SHOP_DOOR.to_string(),
            format!("door = \"http://{addr}\"\ncompany = \"acme\"\nshop_pubkey = \"{shop_key}\"\n"),
        ),
        (
            format!("secret.{}", shared::CLIENT_KEY_SECRET),
            hex(&CLIENT_SEED),
        ),
    ]);
    gunnar_front::recharge::shop_door(&feed)
        .expect("a sold front")
        .expect("its door")
}

/// ★ **Gunnar's front works against a ONE box that has only `Fleet`** (the fold, P3-FLEET):
/// its fed door rides `Fleet.Ask` as product `gunnar`, signed as the estate's front.
#[test]
fn gunnars_front_rides_fleet_on_a_fleet_only_one_box() {
    let rt = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(2)
        .enable_all()
        .build()
        .unwrap();
    let fleet = FleetOnly::default();
    let door = fed_door(serve_fleet(&rt, fleet.clone()));
    assert!(
        door.on_fleet(),
        "Gunnar's front names itself: its calls ride Fleet"
    );
    assert_eq!(door.fleet_product().as_deref(), Some("gunnar"));
    assert_eq!(
        door.update_take().expect("the take over Fleet").role,
        "twin"
    );
    assert_eq!(
        door.recharge_box("main", "seed-toml", false)
            .expect("a recharge over Fleet")
            .words,
        "the main laid again over Fleet"
    );
    assert_eq!(
        *fleet.asked.lock().unwrap(),
        vec!["update_take".to_string(), "recharge".to_string()]
    );
}

/// ★ **H16: there is no old door to fall back to.** Every one of the five calls rides `Fleet.Ask`
/// (the shared client from Gunnar's own settings, told its product), and a door told no product
/// calls nothing, by name — the ONE box serves no `SoldEstate` any more.
#[test]
fn every_call_rides_fleet_and_a_door_with_no_product_calls_nothing() {
    let rt = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(2)
        .enable_all()
        .build()
        .unwrap();
    let fleet = FleetOnly::default();
    let addr = serve_fleet(&rt, fleet.clone());
    let bare = shared::ShopDoor::from_settings(&settings(addr), &hex(&CLIENT_SEED))
        .expect("the door")
        .with_clock(clock());
    assert_eq!(
        bare.update_take().unwrap_err(),
        shared::NO_PRODUCT,
        "no product: nothing is called"
    );
    assert!(fleet.asked.lock().unwrap().is_empty());
    let door = bare.or_over_fleet("gunnar");
    assert_eq!(
        door.update_take().expect("the take over Fleet").role,
        "twin"
    );
    assert!(door
        .recharge_box("main", "seed-toml", false)
        .expect("a recharge over Fleet")
        .words
        .contains("over Fleet"));
    assert_eq!(
        *fleet.asked.lock().unwrap(),
        vec!["update_take".to_string(), "recharge".to_string()]
    );
}