use {
super::*,
crate::utils::{Jwt, JwtIssuer, discover_all, sleep_s, timeout_s},
mosaik::{GroupKey, Network, NetworkId, primitives::TicketValidator},
};
#[tokio::test]
async fn jwt_authorized_peers_can_bond() -> anyhow::Result<()> {
const T: u64 = 8;
let network_id = NetworkId::random();
let key = GroupKey::from_secret("auth-group-secret");
let (n0, n1, n2, n3) = tokio::try_join!(
Network::new(network_id),
Network::new(network_id),
Network::new(network_id),
Network::new(network_id),
)?;
let jwt_issuer = JwtIssuer::default();
let jwt_validator =
Jwt::with_key(jwt_issuer.key()).allow_issuer(jwt_issuer.issuer());
let n0_ticket = jwt_issuer.make_valid_ticket(&n0.local().id());
let n1_ticket = jwt_issuer.make_valid_ticket(&n1.local().id());
let n2_ticket = jwt_issuer.make_expired_ticket(&n2.local().id());
n0.discovery().add_ticket(n0_ticket);
n1.discovery().add_ticket(n1_ticket);
n2.discovery().add_ticket(n2_ticket);
timeout_s(5, discover_all([&n0, &n1, &n2, &n3])).await??;
let g0 = n0
.groups()
.with_key(key)
.with_state_machine(Counter::default())
.require_ticket(jwt_validator.clone())
.join();
let g1 = n1
.groups()
.with_key(key)
.with_state_machine(Counter::default())
.require_ticket(jwt_validator.clone())
.join();
let g2 = n2
.groups()
.with_key(key)
.with_state_machine(Counter::default())
.require_ticket(jwt_validator.clone())
.join();
let g3 = n3
.groups()
.with_key(key)
.with_state_machine(Counter::default())
.require_ticket(jwt_validator.clone())
.join();
timeout_s(T, ensure_bonds_formed(&g0, &n0, &[&n1], "g0")).await?;
timeout_s(T, ensure_bonds_formed(&g1, &n1, &[&n0], "g1")).await?;
sleep_s(3).await;
let g0_bonds: Vec<_> = g0.bonds().iter().map(|b| *b.peer().id()).collect();
assert_eq!(g0_bonds.len(), 1, "g0 should have exactly one bond");
assert!(g0_bonds.contains(&n1.local().id()));
let g1_bonds: Vec<_> = g1.bonds().iter().map(|b| *b.peer().id()).collect();
assert_eq!(g1_bonds.len(), 1, "g1 should have exactly one bond");
assert!(g1_bonds.contains(&n0.local().id()));
assert_eq!(
g2.bonds().len(),
0,
"g2 should have no bonds (expired ticket)"
);
assert_eq!(g3.bonds().len(), 0, "g3 should have no bonds (no ticket)");
Ok(())
}
#[tokio::test]
async fn bond_terminated_on_jwt_ticket_revocation() -> anyhow::Result<()> {
const T: u64 = 8;
let network_id = NetworkId::random();
let key = GroupKey::from_secret("auth-revocation-test-secret");
let (n0, n1) =
tokio::try_join!(Network::new(network_id), Network::new(network_id))?;
let jwt_issuer = JwtIssuer::default();
let jwt_validator =
Jwt::with_key(jwt_issuer.key()).allow_issuer(jwt_issuer.issuer());
let n0_ticket = jwt_issuer.make_valid_ticket(&n0.local().id());
let n1_ticket = jwt_issuer.make_valid_ticket(&n1.local().id());
n0.discovery().add_ticket(n0_ticket);
n1.discovery().add_ticket(n1_ticket);
timeout_s(5, discover_all([&n0, &n1])).await??;
let g0 = n0
.groups()
.with_key(key)
.with_state_machine(Counter::default())
.require_ticket(jwt_validator.clone())
.join();
let g1 = n1
.groups()
.with_key(key)
.with_state_machine(Counter::default())
.require_ticket(jwt_validator.clone())
.join();
timeout_s(T, ensure_bonds_formed(&g0, &n0, &[&n1], "g0")).await?;
timeout_s(T, ensure_bonds_formed(&g1, &n1, &[&n0], "g1")).await?;
n1.discovery().remove_tickets_of(jwt_validator.class());
n0.discovery().feed(n1.discovery().me());
timeout_s(T, async {
loop {
if g0.bonds().is_empty() {
break;
}
g0.bonds().changed().await;
}
})
.await?;
assert_eq!(
g0.bonds().len(),
0,
"g0 bond should be gone after revocation"
);
Ok(())
}
#[tokio::test]
async fn group_key_derived_from_jwt_validator() -> anyhow::Result<()> {
const T: u64 = 8;
let network_id = NetworkId::random();
let issuer = JwtIssuer::default();
let jwt_validator = Jwt::with_key(issuer.key()).allow_issuer(issuer.issuer());
let key = GroupKey::from(&jwt_validator);
let key2 = GroupKey::from(&jwt_validator);
assert_eq!(
key, key2,
"same validator config must produce identical key"
);
let (n0, n1) =
tokio::try_join!(Network::new(network_id), Network::new(network_id))?;
let n0_ticket = issuer.make_valid_ticket(&n0.local().id());
let n1_ticket = issuer.make_valid_ticket(&n1.local().id());
n0.discovery().add_ticket(n0_ticket);
n1.discovery().add_ticket(n1_ticket);
timeout_s(5, discover_all([&n0, &n1])).await??;
let g0 = n0
.groups()
.with_key(key)
.with_state_machine(Counter::default())
.require_ticket(jwt_validator.clone())
.join();
let g1 = n1
.groups()
.with_key(key)
.with_state_machine(Counter::default())
.require_ticket(jwt_validator.clone())
.join();
assert_eq!(
g0.id(),
g1.id(),
"same key+auth must derive identical group id"
);
timeout_s(T, ensure_bonds_formed(&g0, &n0, &[&n1], "g0")).await?;
timeout_s(T, ensure_bonds_formed(&g1, &n1, &[&n0], "g1")).await?;
Ok(())
}
#[tokio::test]
async fn mismatched_jwt_auth_config_prevents_bonding() -> anyhow::Result<()> {
const T: u64 = 5;
let network_id = NetworkId::random();
let key = GroupKey::from_secret("auth-mismatch-test-secret");
let (n0, n1) =
tokio::try_join!(Network::new(network_id), Network::new(network_id))?;
timeout_s(T, discover_all([&n0, &n1])).await??;
let issuer = JwtIssuer::default();
let jwt_validator = Jwt::with_key(issuer.key()).allow_issuer(issuer.issuer());
let g0 = n0
.groups()
.with_key(key)
.with_state_machine(Counter::default())
.require_ticket(jwt_validator)
.join();
let g1 = n1
.groups()
.with_key(key)
.with_state_machine(Counter::default())
.join();
assert_ne!(
g0.id(),
g1.id(),
"groups with different auth configs should derive different IDs"
);
sleep_s(3).await;
assert_eq!(g0.bonds().len(), 0, "g0 should have no bonds");
assert_eq!(g1.bonds().len(), 0, "g1 should have no bonds");
Ok(())
}
#[tokio::test]
async fn multiple_ticket_validators() -> anyhow::Result<()> {
const T: u64 = 8;
let network_id = NetworkId::random();
let key = GroupKey::from_secret("multi-auth-group-secret");
let issuer_a = JwtIssuer::new("issuer-alpha", "secret-alpha");
let issuer_b = JwtIssuer::new("issuer-beta", "secret-beta");
let validator_a =
Jwt::with_key(issuer_a.key()).allow_issuer(issuer_a.issuer());
let validator_b =
Jwt::with_key(issuer_b.key()).allow_issuer(issuer_b.issuer());
let (n0, n1, n2, n3) = tokio::try_join!(
Network::new(network_id),
Network::new(network_id),
Network::new(network_id),
Network::new(network_id),
)?;
n0.discovery()
.add_ticket(issuer_a.make_valid_ticket(&n0.local().id()));
n0.discovery()
.add_ticket(issuer_b.make_valid_ticket(&n0.local().id()));
n1.discovery()
.add_ticket(issuer_a.make_valid_ticket(&n1.local().id()));
n1.discovery()
.add_ticket(issuer_b.make_valid_ticket(&n1.local().id()));
n2.discovery()
.add_ticket(issuer_a.make_valid_ticket(&n2.local().id()));
n3.discovery()
.add_ticket(issuer_b.make_valid_ticket(&n3.local().id()));
timeout_s(5, discover_all([&n0, &n1, &n2, &n3])).await??;
let g0 = n0
.groups()
.with_key(key)
.with_state_machine(Counter::default())
.require_ticket(validator_a.clone())
.require_ticket(validator_b.clone())
.join();
let g1 = n1
.groups()
.with_key(key)
.with_state_machine(Counter::default())
.require_ticket(validator_a.clone())
.require_ticket(validator_b.clone())
.join();
let g2 = n2
.groups()
.with_key(key)
.with_state_machine(Counter::default())
.require_ticket(validator_a.clone())
.require_ticket(validator_b.clone())
.join();
let g3 = n3
.groups()
.with_key(key)
.with_state_machine(Counter::default())
.require_ticket(validator_a.clone())
.require_ticket(validator_b.clone())
.join();
timeout_s(T, ensure_bonds_formed(&g0, &n0, &[&n1], "g0")).await?;
timeout_s(T, ensure_bonds_formed(&g1, &n1, &[&n0], "g1")).await?;
sleep_s(3).await;
let g0_bonds: Vec<_> = g0.bonds().iter().map(|b| *b.peer().id()).collect();
assert_eq!(g0_bonds.len(), 1, "g0 should have exactly one bond");
assert!(g0_bonds.contains(&n1.local().id()));
let g1_bonds: Vec<_> = g1.bonds().iter().map(|b| *b.peer().id()).collect();
assert_eq!(g1_bonds.len(), 1, "g1 should have exactly one bond");
assert!(g1_bonds.contains(&n0.local().id()));
assert_eq!(
g2.bonds().len(),
0,
"g2 should have no bonds (missing issuer_b ticket)"
);
assert_eq!(
g3.bonds().len(),
0,
"g3 should have no bonds (missing issuer_a ticket)"
);
Ok(())
}