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

nordisk/gunnar

crates/gunnar-server/tests/add_principal.rs

//! **`add_principal`** (5.4, ONE monetize, G1 pull): monetize never dials an estate, so a
//! paid signup's account leaves monetize as a signed order the main pulls. The main makes
//! the principal and its namespace (the `Access.AddPrincipal` path) and acks monetize's
//! contract (monetize-product `pull::order::ADD_PRINCIPAL`):
//!
//! * ok, the detail the namespace made — also on a repeat for the same `signup` (monetize
//!   asks again when it lost the ack), across a restart;
//! * not ok, the detail starting `taken` — the name is somebody else's;
//! * not ok, anything else — final, in words (a box that does not serve, an `org`, a
//!   reserved name).
//!
//! Driven through `gunnar_repair::exec::execute`, the one dispatch a pulled order takes,
//! on the server's own [`ServerEstate`].
#![cfg(feature = "replication")]

use std::sync::Arc;

use gunnar_core::ssh_key::PrivateKey;
use gunnar_core::{Principal, PrincipalKind};
use gunnar_repair::exec::execute;
use gunnar_repair::{Order, OrderAck};
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::repair::{Entitlements, ServerEstate};
use gunnar_server::stores::ServerStores;

struct Rig {
    dir: tempfile::TempDir,
    stores: Arc<ServerStores>,
}

impl Rig {
    /// A main that serves: alice (a person), and a namespace `team` nobody's principal.
    fn serving() -> Rig {
        let r = Rig::fresh();
        r.stores
            .auth()
            .add_principal(Principal {
                id: "alice".into(),
                name: "alice".into(),
                kind: PrincipalKind::User,
                created: 0,
            })
            .expect("alice");
        for ns in ["alice", "team"] {
            r.stores.catalog().create_namespace(ns).expect("namespace");
        }
        r
    }

    /// A re-laid main nobody restored: it does not serve.
    fn fresh() -> Rig {
        let dir = tempfile::tempdir().expect("tempdir");
        let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
        Rig { dir, stores }
    }

    /// The same box after a restart: its stores reopened from disk.
    fn restarted(self) -> Rig {
        let Rig { dir, stores } = self;
        drop(stores);
        let stores = Arc::new(ServerStores::open(dir.path()).expect("stores again"));
        Rig { dir, stores }
    }

    fn estate(&self) -> ServerEstate {
        let pem = gunnar_core::mint_ed25519().expect("mint");
        let key = PrivateKey::from_openssh(std::str::from_utf8(&pem).unwrap()).expect("parse");
        let plane = CoreControlPlane::new(Arc::clone(&self.stores));
        ServerEstate {
            key,
            own: "127.0.0.1:1".into(),
            entitlements: Entitlements::of(Arc::clone(&self.stores)).with_control(Arc::new(plane)),
        }
    }

    async fn add(&self, seq: u64, username: &str, kind: &str, signup: &str) -> OrderAck {
        let order = Order::AddPrincipal {
            username: username.into(),
            principal_kind: kind.into(),
            signup: signup.into(),
        };
        execute(&self.estate(), seq, &order, &|| false).await
    }

    fn has(&self, name: &str) -> (bool, bool) {
        (
            self.stores.auth().principal(name).expect("auth").is_some(),
            self.stores
                .catalog()
                .namespace(name)
                .expect("catalog")
                .is_some(),
        )
    }
}

#[tokio::test]
async fn a_paid_signup_gets_its_person_and_namespace_and_the_ack_names_the_namespace() {
    let r = Rig::serving();
    let a = r.add(1, "bob", "person", "stripe:cs_1").await;
    assert!(a.ok, "{a:?}");
    assert_eq!((a.seq, a.detail.as_str()), (1, "bob"));
    assert_eq!(r.has("bob"), (true, true));
    let bob = r.stores.auth().principal("bob").unwrap().unwrap();
    assert_eq!(bob.kind, PrincipalKind::User, "a person");
}

#[tokio::test]
async fn the_same_signup_asked_again_is_ok_also_after_a_restart() {
    let r = Rig::serving();
    assert!(r.add(1, "bob", "person", "stripe:cs_1").await.ok);
    let a = r.add(2, "bob", "person", "stripe:cs_1").await;
    assert!(a.ok, "monetize lost the ack and asks again: {a:?}");
    assert_eq!(a.detail, "bob");

    let r = r.restarted();
    let a = r.add(3, "bob", "person", "stripe:cs_1").await;
    assert!(a.ok, "the signup's record outlives a restart (it is in the control store, which the twin follows): {a:?}");
    assert_eq!(
        r.stores
            .auth()
            .principals()
            .unwrap()
            .iter()
            .filter(|p| p.id == "bob")
            .count(),
        1
    );
}

#[tokio::test]
async fn a_name_that_is_somebody_elses_is_taken() {
    let r = Rig::serving();
    assert!(r.add(1, "bob", "person", "stripe:cs_1").await.ok);
    for (seq, name, signup) in [
        (2, "bob", "stripe:cs_2"),
        (3, "alice", "stripe:cs_3"),
        (4, "team", "stripe:cs_4"),
    ] {
        let a = r.add(seq, name, "person", signup).await;
        assert!(!a.ok, "{name}: {a:?}");
        assert!(
            a.detail.starts_with("taken"),
            "{name}: monetize reads `taken` and the buyer picks another: {}",
            a.detail
        );
    }
    assert_eq!(
        r.has("team"),
        (false, true),
        "a taken name is left as it was"
    );
}

#[tokio::test]
async fn an_org_and_a_reserved_name_are_refused_finally_and_not_as_taken() {
    let r = Rig::serving();
    let a = r.add(1, "acme", "org", "stripe:cs_1").await;
    assert!(
        !a.ok && a.detail.starts_with("bad_order:") && a.detail.contains("org"),
        "{a:?}"
    );
    let a = r.add(2, "console-x", "person", "stripe:cs_2").await;
    assert!(
        !a.ok && !a.detail.starts_with("taken") && a.detail.contains("reserved"),
        "{a:?}"
    );
    assert_eq!(
        (r.has("acme"), r.has("console-x")),
        ((false, false), (false, false))
    );
}

#[tokio::test]
async fn a_box_that_does_not_serve_makes_no_account_and_says_so() {
    let r = Rig::fresh();
    let a = r.add(1, "bob", "person", "stripe:cs_1").await;
    assert!(!a.ok && a.detail.starts_with("not_serving:"), "{a:?}");
    assert_eq!(r.has("bob"), (false, false));
}

#[tokio::test]
async fn a_box_without_its_control_plane_refuses_by_name() {
    let r = Rig::serving();
    let estate = ServerEstate {
        entitlements: Entitlements::of(Arc::clone(&r.stores)),
        ..r.estate()
    };
    let a = execute(
        &estate,
        1,
        &Order::AddPrincipal {
            username: "bob".into(),
            principal_kind: "person".into(),
            signup: "s".into(),
        },
        &|| false,
    )
    .await;
    assert!(
        !a.ok && a.detail.starts_with("bad_order:") && a.detail.contains("account"),
        "{a:?}"
    );
    assert_eq!(r.has("bob"), (false, false));
}