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/shop_pairing_contract.rs

//! ★ **The shop's side and the front's thin client speak the same words** (G11 2026-10-10;
//! K3 under ONE monetize, 2026-10-11).
//!
//! The installer a shop pairs a sold estate with is monetize's `monetize-shop-pairing` (G11),
//! which names no gunnar crate and keeps the few words of gunnar's contract it speaks as its
//! own constants. The front's side is now as thin: `shop_door` (the signed `SoldEstate`
//! client over monetize-proto) DUPLICATES the shop's call names and bundle words instead of
//! linking `monetize-server` (K3: the front embeds no monetize). So this test is the ONE
//! place both copies are held equal — monetize-server and monetize-shop-pairing are
//! dev-dependencies here, and nothing of them ships in any front build
//! (tests/one_monetize.rs, tests/one_front_pull.rs).

use std::collections::BTreeMap;

use gunnar_front::shop_door;
use gunnar_monetize_guest::lifecycle::doors;
use monetize_server::estate_order::LaidOrder;
use monetize_shop_pairing::contract as c;

/// Gunnar's contract, as the shop's pairing reads it off Gunnar's own facts (op 90).
fn gunnar() -> monetize_shop_pairing::ProductContract {
    monetize_shop_pairing::ProductContract::of(&gunnar_monetize_guest::lifecycle::facts())
        .expect("Gunnar's facts declare its contract")
}

fn order() -> LaidOrder {
    serde_json::from_value(serde_json::json!({
        "company": "acme", "apex": "test2.gunnar.rs", "domain": "",
        "names": ["acme.test2.gunnar.rs", "git.acme.test2.gunnar.rs"],
        "estate": {"reference": "estate/acme", "company": "acme", "zone": "se-sto1", "machines": [
            {"role": "front", "hostname": "f", "title": "F", "plan": "STARTER-4xCPU-8GB", "server": "s1", "public_ipv4": "198.51.100.1", "utility_ipv4": "10.6.0.1", "disks": [{"title": "sys", "id": "d1", "gib": 40, "tier": "standard", "address": "virtio:0"}]},
            {"role": "main", "hostname": "m", "title": "M", "plan": "STARTER-4xCPU-16GB", "server": "s2", "public_ipv4": "198.51.100.2", "utility_ipv4": "10.6.0.2", "disks": [{"title": "sys", "id": "d2", "gib": 50, "tier": "standard", "address": "virtio:0"}, {"title": "data", "id": "d4", "gib": 10, "tier": "maxiops", "address": "virtio:1"}]},
            {"role": "twin", "hostname": "t", "title": "T", "plan": "STARTER-1xCPU-2GB", "server": "s3", "public_ipv4": "198.51.100.3", "utility_ipv4": "10.6.0.3", "disks": [{"title": "sys", "id": "d3", "gib": 20, "tier": "standard", "address": "virtio:0"}]}
        ]},
        "first_login_ttl_secs": 604800
    }))
    .unwrap()
}

/// ★ Every word of gunnar's contract the shop's pairing keeps is gunnar's own.
#[test]
fn the_shop_pairings_contract_words_are_gunnars() {
    use gunnar_grpc::topology as t;
    assert_eq!(
        c::FEED_SHOP_DOOR,
        gunnar_front::recharge::FEED_SHOP_DOOR,
        "the shop door the front calls IN through (F4)"
    );
    assert_eq!(
        c::FEED_OPERATOR_WELCOME,
        gunnar_server::fed_principals::FEED_OPERATOR_WELCOME
    );
    assert_eq!(
        (c::WELCOME_REPO, doors::WELCOME_REPO),
        (
            gunnar_server::fed_principals::WELCOME_REPO,
            gunnar_server::fed_principals::WELCOME_REPO
        )
    );
    assert_eq!(
        c::FEED_BILLING_URL,
        gunnar_server::feed::FEED_BILLING_URL,
        "the sold site's pay page (UW-6)"
    );
    assert_eq!(
        [
            c::FEED_RECORD,
            c::RECORD_SCHEMA,
            c::FEED_VARIANT,
            c::FEED_VERSIONS,
            c::FEED_BOXES,
            c::FEED_BOX_PREFIX
        ],
        [
            t::FEED_RECORD,
            t::RECORD_SCHEMA,
            t::FEED_VARIANT,
            t::FEED_VERSIONS,
            t::FEED_BOXES,
            t::FEED_BOX_PREFIX
        ]
    );
}

/// ★ **The doors the shop's sale check reads are the console's own** (S4, 2026-10-11): before
/// a sale is captured the shop proves the sold front's PUBLIC `https://<host>` — the login page
/// (and the passkey sign-in only that page carries), the console's status and the first-login
/// door the ready mail opens, all reached with no session as the buyer reaches them; and the
/// repository list, which only the console's bearer reads, in the console's `Page` shape.
#[test]
fn the_sale_checks_doors_are_the_consoles_own() {
    use gunnar_ui_server::auth::middleware::is_public;
    for door in [
        doors::LOGIN_PATH,
        doors::AUTH_STATUS_PATH,
        doors::REGISTER_START_PATH,
    ] {
        assert!(
            is_public(door),
            "{door}: the buyer reaches it with no session"
        );
    }
    assert!(
        !is_public(doors::REPOS_PATH),
        "the repository list is read with the console's bearer"
    );
    let page = gunnar_ui_server::auth::routes::login_html();
    assert!(
        page.contains(doors::LOGIN_PAGE_MARK) && page.contains(doors::REGISTER_START_PATH),
        "the login page carries its passkey doors"
    );
    // Every route Gunnar's sale proof asks is one of these, asked as the console serves it.
    let proof = gunnar().proof;
    for check in &proof.checks {
        let path = check.ask.path.split('?').next().unwrap_or_default();
        assert!(
            is_public(path) != check.ask.bearer,
            "{path}: asked with the bearer exactly when only the bearer reads it"
        );
    }
    // A listed repository's `path` and `name`, as the console serializes its own `Repo`.
    fn _keys(r: &gunnar_ui_server::api::repos::Repo) -> (&str, &str) {
        (&r.path, &r.name)
    }
    let listed = serde_json::to_value(gunnar_ui_server::api::Page::of(
        vec![serde_json::json!({"path": "alice/welcome", "name": doors::WELCOME_REPO})],
        doors::REPOS_MAX,
    ))
    .unwrap();
    assert_eq!(
        (
            listed["items"][0]["path"].as_str(),
            listed["truncated"].as_bool()
        ),
        (Some("alice/welcome"), Some(false))
    );
    let all: Vec<u32> = (0..u32::try_from(doors::REPOS_MAX).unwrap() + 1).collect();
    assert_eq!(
        gunnar_ui_server::api::Page::of(all, doors::REPOS_MAX)
            .items
            .len(),
        doors::REPOS_MAX,
        "the most the console lists at once"
    );
}

/// ★ **The thin client's duplicated words are the shop's own** (K3): the signed `SoldEstate`
/// call names, the seed file names, the kept bundle's version and life, the client key's
/// secret name, and each verb's patience. A drift here is a refusal at the ONE monetize's
/// door — this test is why the duplication is safe.
#[test]
fn the_thin_shop_doors_words_are_the_shops_own() {
    use monetize_server::estate_order::sold;
    use monetize_server::shop_cloud;
    assert_eq!(shop_door::CALL_RECHARGE, sold::CALL_RECHARGE);
    assert_eq!(shop_door::CALL_UPGRADE, sold::CALL_UPGRADE);
    assert_eq!(shop_door::CALL_TAKE_MATERIAL, sold::CALL_TAKE_MATERIAL);
    assert_eq!(
        shop_door::CALL_UPDATE_TAKE,
        monetize_server::estate_order::update::CALL_UPDATE_TAKE
    );
    assert_eq!(
        shop_door::CALL_UPDATE_REPORT,
        monetize_server::estate_order::update::CALL_UPDATE_REPORT
    );
    assert_eq!(
        shop_door::SEED_FILE_PREFIX,
        monetize_server::estate_order::SEED_FILE_PREFIX
    );
    assert_eq!(shop_door::BUNDLE_V, monetize_shop_pairing::BUNDLE_V);
    assert_eq!(
        shop_door::MATERIAL_VALID,
        monetize_shop_pairing::MATERIAL_VALID
    );
    assert_eq!(shop_door::CLIENT_KEY_SECRET, shop_cloud::CLIENT_KEY_SECRET);
    // The front never gives up on a Recharge before the shop does (the ONE client's bound is
    // Holger's hour; the shop lays a lost box within its 30 min, T16 / P4-REST).
    assert!(shop_door::RECHARGE_WITHIN >= shop_cloud::RECHARGE_WITHIN);
    assert_eq!(shop_door::UPGRADE_WITHIN, shop_cloud::UPGRADE_WITHIN);
    assert_eq!(shop_door::READ_WITHIN, shop_cloud::READ_WITHIN);
}

/// ★ **The bundle the shop keeps for a sold estate is the one the front's `recharge` reads**:
/// minted by monetize-shop-pairing, parsed by `Kept`, and the front can be the estate's
/// installer from it.
#[test]
fn the_shops_kept_bundle_is_read_by_the_fronts_recharge() {
    let files: BTreeMap<String, String> =
        monetize_shop_pairing::mint_material_for(&gunnar(), "acme.test2.gunnar.rs")
            .unwrap()
            .into_iter()
            .collect();
    let plan = monetize_shop_pairing::plan_of_for(
        &gunnar(),
        &order(),
        BTreeMap::new(),
        BTreeMap::new(),
        &BTreeMap::new(),
        600,
    )
    .unwrap();
    // The very shape `monetize_shop_pairing::bundle` renders (it reads from a material dir on
    // disk; the shape is its contract, under the shop's own BUNDLE_V).
    let bytes = serde_json::to_vec(&serde_json::json!({"v": monetize_shop_pairing::BUNDLE_V, "files": files, "ledger": "ledger = \"\"\n", "plan": plan})).unwrap();
    let kept =
        gunnar_front::recharge::Kept::parse(&bytes).expect("the front reads the shop's bundle");
    assert_eq!(kept.plan, plan);
    assert!(
        kept.material().is_ok(),
        "the front can be the estate's installer from it"
    );
    assert!(
        kept.fresh_seed(gunnar_front::recharge::GUNNAR_SEED).is_ok(),
        "a fresh seed from the kept CA"
    );
}

/// ★ **The estate record a sold estate is fed is the one gunnar reads**, and exactly what
/// gunnar's own `Record::to_feed` writes for it.
#[test]
fn the_record_the_shop_feeds_is_the_one_gunnar_reads() {
    use gunnar_grpc::topology::{Record, RecordBox};
    let f = monetize_shop_pairing::shared_feed_for(
        &gunnar(),
        &order(),
        "acme-owner",
        "ssh-ed25519 AAAA op",
        "invite",
    )
    .unwrap();
    let b = |name: &str,
             host: &str,
             title: &str,
             public: &str,
             utility: &str,
             size: &str,
             sys: u64,
             data: &str| RecordBox {
        name: name.into(),
        laid_as: name.into(),
        hostname: host.into(),
        title: title.into(),
        public: public.into(),
        utility: utility.into(),
        size: size.into(),
        vcpu: 0,
        ram_mb: 0,
        system_disk_gb: sys,
        data: data.into(),
    };
    let want = Record {
        variant: gunnar().sold_variant,
        versions: Vec::new(),
        boxes: vec![
            b(
                "front",
                "f",
                "F",
                "198.51.100.1",
                "10.6.0.1",
                "STARTER-4xCPU-8GB",
                40,
                "",
            ),
            b(
                "main",
                "m",
                "M",
                "198.51.100.2",
                "10.6.0.2",
                "STARTER-4xCPU-16GB",
                50,
                "10 GB maxiops",
            ),
            b(
                "twin",
                "t",
                "T",
                "198.51.100.3",
                "10.6.0.3",
                "STARTER-1xCPU-2GB",
                20,
                "",
            ),
        ],
    };
    assert_eq!(
        Record::from_feed(&f),
        Some(want.clone()),
        "gunnar reads the record back"
    );
    let record: BTreeMap<String, String> = f
        .iter()
        .filter(|(k, _)| k.starts_with("estate."))
        .map(|(k, v)| (k.clone(), v.clone()))
        .collect();
    assert_eq!(
        record,
        want.to_feed(),
        "key for key what gunnar's own writer feeds"
    );
}

/// ★ **Each kind's seed is read by the box as its `user_data`** (gunnar-server `pairing`).
#[test]
fn every_seed_the_shop_mints_is_read_by_the_box() {
    let files =
        monetize_shop_pairing::mint_material_for(&gunnar(), "acme.test2.gunnar.rs").unwrap();
    for name in ["seed.front.toml", "seed.gunnar.toml"] {
        let seed = &files
            .iter()
            .find(|(k, _)| k == name)
            .unwrap_or_else(|| panic!("{name}"))
            .1;
        assert!(
            nordisk_pairing::seed::metadata::of_user_data(seed).is_ok(),
            "{name}: the box reads it as its user_data"
        );
    }
}

/// ★ **HOL-REPO-NAME: a shop-laid Gunnar front certifies `git.<site>` and sends it to the site**
/// (measured 2026-10-31: test2.gunnar.rs's certificate named test2 and www only, while
/// git.test2.gunnar.rs resolves to the front; the kit's gunnar.rs carries it, h-front-sans).
/// Gunnar's OWN facts (its guest's `redirect_names`) through the shop's front feed: the forge's
/// name is one of the front's served names (on its certificate) and a callback-only one (every
/// path there goes to the site), as edda's web reads the feed. No repository is routed.
#[test]
fn a_shop_laid_gunnar_front_certifies_git_of_its_site_as_a_redirect_only_name() {
    let dir = tempfile::tempdir().unwrap();
    for (n, v) in [("console-admin-token", "aa"), ("estate-client.key", "cc")] {
        std::fs::write(dir.path().join(n), v).unwrap();
    }
    let mut o = order();
    o.names = [
        "acme.test2.gunnar.rs",
        "www.acme.test2.gunnar.rs",
        "git.acme.test2.gunnar.rs",
    ]
    .map(String::from)
    .to_vec();
    let f =
        monetize_shop_pairing::front_feed_for(&gunnar(), &BTreeMap::new(), &o, dir.path()).unwrap();
    let web = gunnar_front::web::WebConfig::from("acme.test2.gunnar.rs", &f);
    assert_eq!(
        web.hosts,
        [
            "acme.test2.gunnar.rs",
            "www.acme.test2.gunnar.rs",
            "git.acme.test2.gunnar.rs"
        ],
        "every served name is on the certificate"
    );
    assert_eq!(
        web.callback_only,
        ["git.acme.test2.gunnar.rs"],
        "the forge's name answers no web face"
    );
    assert!(
        !f.contains_key("repo_hosts"),
        "Gunnar routes no repository through :443"
    );
}