#![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;
const GIB: u64 = 1 << 30;
struct Rig {
_dir: tempfile::TempDir,
stores: Arc<ServerStores>,
_identity: Arc<gunnar_grpc::identity::GunnarIdentity>,
}
impl Rig {
fn serving() -> Rig {
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
stores
.auth()
.add_principal(Principal {
id: "acme".into(),
name: "acme".into(),
kind: PrincipalKind::User,
created: 0,
})
.expect("acme");
stores
.catalog()
.create_namespace("acme")
.expect("namespace");
let control: Arc<dyn gunnar_grpc::ControlPlane> = Arc::new(
gunnar_server::control_plane::CoreControlPlane::new(Arc::clone(&stores)),
);
let identity = gunnar_server::identity::build(
dir.path(),
control,
&gunnar_grpc::ServerConfig::default(),
false,
);
stores.set_identity(&identity);
Rig {
_dir: dir,
stores,
_identity: identity,
}
}
fn entitlements(&self) -> Entitlements {
Entitlements::of(Arc::clone(&self.stores))
.with_control(Arc::new(CoreControlPlane::new(Arc::clone(&self.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");
ServerEstate {
key,
own: "127.0.0.1:1".into(),
entitlements: self.entitlements(),
}
}
async fn run(&self, seq: u64, order: Order) -> OrderAck {
execute(&self.estate(), seq, &order, &|| false).await
}
}
fn add_users(ask: serde_json::Value, order: &str) -> Order {
Order::AddUsers {
tenant: "acme".into(),
ask,
order: order.into(),
}
}
#[tokio::test]
async fn a_paid_named_user_is_granted_on_the_account_and_the_ack_is_the_answer() {
let r = Rig::serving();
let a = r
.run(
1,
add_users(
serde_json::json!({"count": 1, "room_bytes": 0, "name": "eve"}),
"gunnar/acme/users-1",
),
)
.await;
assert!(a.ok, "{a:?}");
let answer: serde_json::Value =
serde_json::from_str(&a.detail).expect("the UsersAnswer as JSON");
assert_eq!(answer["principal"], "eve", "{answer}");
assert_eq!(
answer["room_bytes"],
10 * GIB,
"the product's default room per user: {answer}"
);
assert_eq!(answer["unchanged"], false, "{answer}");
assert!(
answer["invite_secret"]
.as_str()
.is_some_and(|s| s.len() >= 32),
"a new named user's invite rides the first answer: {answer}"
);
assert!(
r.stores.auth().principal("eve").unwrap().is_some(),
"eve is made"
);
let again = r
.run(
2,
add_users(
serde_json::json!({"count": 1, "room_bytes": 0, "name": "eve"}),
"gunnar/acme/users-1",
),
)
.await;
assert!(again.ok, "monetize lost the ack and asks again: {again:?}");
let again: serde_json::Value = serde_json::from_str(&again.detail).unwrap();
assert_eq!(
again["unchanged"], true,
"the retry answers the first grant: {again}"
);
assert_eq!(
again["invite_secret"].as_str().unwrap_or_default(),
"",
"no second invite: {again}"
);
}
#[tokio::test]
async fn seats_are_granted_unnamed_and_a_bad_ask_is_refused_finally() {
let r = Rig::serving();
let a = r
.run(
1,
add_users(
serde_json::json!({"count": 3, "room_bytes": 0}),
"gunnar/acme/seats-1",
),
)
.await;
assert!(a.ok, "{a:?}");
let answer: serde_json::Value = serde_json::from_str(&a.detail).unwrap();
assert_eq!(answer["seats"], 3, "{answer}");
let a = r
.run(
2,
add_users(
serde_json::json!({"count": 1, "room_bytes": 0, "name": "console-x"}),
"gunnar/acme/users-2",
),
)
.await;
assert!(
!a.ok && a.detail.contains("console-x"),
"a reserved name is refused, final: {a:?}"
);
let a = r
.run(
3,
add_users(
serde_json::json!({"count": 1, "room_bytes": GIB, "name": "zed"}),
"gunnar/acme/users-3",
),
)
.await;
assert!(!a.ok, "below the room's floor: {a:?}");
}
#[tokio::test]
async fn room_sold_past_the_set_is_kept_and_said_to_the_operator() {
let r = Rig::serving();
let a = r
.run(
1,
Order::GrowRoom {
need_bytes: 30 * GIB,
reason: "acme: needs 30 GiB past the 10 the main has".into(),
},
)
.await;
assert!(a.ok, "{a:?}");
assert!(
a.detail.contains("30.0 GiB") || a.detail.contains("32212254720"),
"{}",
a.detail
);
let alarms = gunnar_server::feed::alarms();
assert!(
alarms
.iter()
.any(|al| al.event == "grow.room_wanted" && al.detail.contains("acme")),
"the operator is told on Admin.Health: {alarms:?}"
);
}
#[tokio::test]
async fn the_mains_report_carries_each_accounts_room_for_add_users() {
let r = Rig::serving();
let text = r
.entitlements()
.product(1_900_000_000_000)
.expect("a serving main reports its product");
let report: serde_json::Value = serde_json::from_str(&text).unwrap();
let acme = &report["accounts"]["acme"];
assert_eq!(
acme["room_bytes"],
10 * GIB,
"the room one user holds: {report}"
);
assert_eq!(acme["seats"], 0, "{report}");
}