use std::{ptr, time::Duration};
use anyhow::{bail, Context, Result};
use tracing::{debug, info};
use super::common::{sleep, DeviceCtx, DevicesCtx, SLEEP_INTERVAL};
#[cfg(feature = "preview")]
use crate::config::HelloSubscriptionConfig;
use crate::{
client::{ChanOp, Permission},
config::{CreateTeamConfig, SyncPeerConfig},
text, AddTeamConfig, AddTeamQuicSyncConfig, CreateTeamQuicSyncConfig, Rank,
};
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_get_keybundle_device_id() -> Result<()> {
let devices = DevicesCtx::new("test_get_keybundle_device_id").await?;
assert_eq!(
devices.owner.client.get_device_id().await?,
devices.owner.id
);
assert_eq!(
devices.owner.client.get_public_key_bundle().await?,
devices.owner.pk
);
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_client_rand() -> Result<()> {
let devices = DevicesCtx::new("test_get_keybundle_device_id").await?;
let mut buf1 = vec![0u8; 100];
devices.owner.client.rand(&mut buf1).await;
let mut buf2 = vec![0u8; 100];
devices.owner.client.rand(&mut buf2).await;
assert_ne!(buf1, buf2);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_trace_id_round_trip() -> Result<()> {
let work_dir = tempfile::tempdir()?;
let owner = DeviceCtx::new("trace-roundtrip", "owner", work_dir.path().join("owner")).await?;
let (client_trace_id, daemon_trace_id) = owner.client.test_trace_id().await?;
info!("client trace_id: {client_trace_id}");
info!("daemon trace_id: {daemon_trace_id}");
assert_eq!(client_trace_id, daemon_trace_id);
assert_ne!(client_trace_id, "00");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_sync_now() -> Result<()> {
let mut devices = DevicesCtx::new("test_sync_now").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices
.setup_default_roles(team_id)
.await
.context("unable to setup default roles")?;
let owner_addr = devices.owner.aranya_local_addr().await?;
let owner = devices.owner.client.team(team_id);
let admin = devices.admin.client.team(team_id);
let admin_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.admin().id)
.await?;
owner
.add_device(
devices.admin.pk.clone(),
None,
Rank::new(admin_role_rank.value().saturating_sub(1)),
)
.await
.context("owner unable to add admin to team")?;
let operator_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.operator().id)
.await?;
owner
.add_device(
devices.operator.pk.clone(),
None,
Rank::new(operator_role_rank.value().saturating_sub(1)),
)
.await
.context("owner unable to add operator to team")?;
owner
.device(devices.admin.id)
.assign_role(roles.admin().id)
.await
.context("owner unable to assign admin role")?;
let err = admin
.device(devices.operator.id)
.assign_role(roles.operator().id)
.await
.expect_err("admin has not synced yet, role assignment should fail");
assert!(matches!(err, crate::Error::Aranya(_)), "{err:?}");
admin
.sync_now(owner_addr, None)
.await
.context("admin unable to sync with owner")?;
let member_role_rank = admin.rank(roles.member().id).await?;
let label_rank = Rank::new(member_role_rank.value().saturating_sub(1));
admin
.create_label(text!("test_label"), label_rank)
.await
.context("admin unable to create label after sync")?;
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_add_remove_sync_peers() -> Result<()> {
let mut devices = DevicesCtx::new("test_add_remove_sync_peers").await?;
let team_id = devices
.create_and_add_team()
.await
.expect("expected to create team");
let roles = devices.setup_default_roles(team_id).await?;
devices
.add_all_sync_peers(team_id)
.await
.context("unable to add all sync peers")?;
devices.add_all_device_roles(team_id, &roles).await?;
let owner_team = devices.owner.client.team(team_id);
let member_role_rank = owner_team.rank(roles.member().id).await?;
let label_rank = Rank::new(member_role_rank.value().saturating_sub(1));
owner_team.create_label(text!("label1"), label_rank).await?;
sleep(SLEEP_INTERVAL).await;
let owner_team_labels = owner_team.labels().await?;
assert_eq!(owner_team_labels.iter().count(), 1);
let membera_team = devices.membera.client.team(team_id);
let membera_team_labels = membera_team.labels().await?;
assert_eq!(membera_team_labels.iter().count(), 1);
let memberb_team = devices.memberb.client.team(team_id);
let memberb_team_labels = memberb_team.labels().await?;
assert_eq!(memberb_team_labels.iter().count(), 1);
for device in devices.devices() {
for peer in devices.devices() {
if ptr::eq(device, peer) {
continue;
}
device
.client
.team(team_id)
.remove_sync_peer(peer.aranya_local_addr().await?)
.await?;
}
}
let owner_addr = devices.owner.aranya_local_addr().await?;
membera_team.remove_sync_peer(owner_addr).await?;
let membera_addr = devices.membera.aranya_local_addr().await?;
owner_team.remove_sync_peer(membera_addr).await?;
owner_team.create_label(text!("label2"), label_rank).await?;
sleep(SLEEP_INTERVAL).await;
let labels_after_removing_sync_peer = membera_team.labels().await?;
assert_eq!(labels_after_removing_sync_peer.iter().count(), 1);
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_role_create_assign_revoke() -> Result<()> {
let devices = DevicesCtx::new("test_role_create_assign_revoke").await?;
let owner_addr = devices.owner.aranya_local_addr().await?;
let owner = devices
.owner
.client
.create_team({
CreateTeamConfig::builder()
.quic_sync(CreateTeamQuicSyncConfig::builder().build()?)
.build()?
})
.await
.expect("expected to create team");
let team_id = owner.team_id();
info!(?team_id);
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let admin_team = devices.admin.client.team(team_id);
let roles_on_team = owner_team.roles().await?;
assert_eq!(roles_on_team.iter().count(), 4);
roles_on_team
.iter()
.find(|r| r.name == "admin")
.ok_or_else(|| anyhow::anyhow!("no admin role"))?;
let member_role_rank = owner_team.rank(roles.member().id).await?;
let label_rank = Rank::new(member_role_rank.value().saturating_sub(1));
admin_team
.create_label(text!("label1"), label_rank)
.await
.expect_err("expected label creation to fail");
info!("adding admin to team");
let admin_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.admin().id)
.await?;
owner
.add_device(
devices.admin.pk.clone(),
None,
Rank::new(admin_role_rank.value().saturating_sub(1)),
)
.await?;
let admin_seed = owner
.encrypt_psk_seed_for_peer(devices.admin.pk.encryption())
.await?;
devices
.admin
.client
.add_team({
AddTeamConfig::builder()
.team_id(team_id)
.quic_sync(
AddTeamQuicSyncConfig::builder()
.wrapped_seed(&admin_seed)?
.build()?,
)
.build()?
})
.await?;
admin_team
.sync_now(owner_addr, None)
.await
.context("admin unable to sync with owner")?;
admin_team
.create_label(text!("label1"), label_rank)
.await
.expect_err("expected label creation to fail");
owner
.device(devices.admin.id)
.assign_role(roles.admin().id)
.await?;
admin_team
.sync_now(owner_addr, None)
.await
.context("admin unable to sync with owner")?;
let admin_role = admin_team
.device(devices.admin.id)
.role()
.await?
.expect("expected admin device to have role assigned to it");
assert_eq!(admin_role.name, text!("admin"));
let label1 = admin_team
.create_label(text!("label1"), label_rank)
.await
.expect("expected admin to create label");
assert_eq!(admin_team.labels().await?.iter().count(), 1);
admin_team.label(label1).await?;
owner
.device(devices.admin.id)
.revoke_role(roles.admin().id)
.await?;
admin_team
.sync_now(owner_addr, None)
.await
.context("admin unable to sync with owner")?;
if admin_team.device(devices.admin.id).role().await?.is_some() {
bail!("did not expect role to be assigned to admin device");
}
admin_team
.create_label(text!("label1"), label_rank)
.await
.expect_err("expected label creation to fail");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_role_change() -> Result<()> {
let mut devices = DevicesCtx::new("test_role_change").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices
.setup_default_roles(team_id)
.await
.context("unable to setup default roles")?;
devices.add_all_device_roles(team_id, &roles).await?;
let owner = devices.owner.client.team(team_id);
owner
.device(devices.membera.id)
.change_role(roles.member().id, roles.operator().id)
.await
.expect("expected to change role from member to operator");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_add_devices() -> Result<()> {
let mut team = DevicesCtx::new("test_add_devices").await?;
let team_id = team
.create_and_add_team()
.await
.expect("expected to create team");
info!(?team_id);
let owner = team.owner.client.team(team_id);
let admin = team.admin.client.team(team_id);
let operator = team.operator.client.team(team_id);
let roles = team
.setup_default_roles(team_id)
.await
.context("unable to setup default roles")?;
let admin_role_rank = team
.owner
.client
.team(team_id)
.rank(roles.admin().id)
.await?;
owner
.add_device(
team.admin.pk.clone(),
Some(roles.admin().id),
Rank::new(admin_role_rank.value().saturating_sub(1)),
)
.await
.context("owner should be able to add admin to team")?;
admin
.sync_now(team.owner.aranya_local_addr().await?, None)
.await
.context("admin unable to sync with owner")?;
let operator_role_rank = team
.owner
.client
.team(team_id)
.rank(roles.operator().id)
.await?;
admin
.add_device(
team.operator.pk.clone(),
None,
Rank::new(operator_role_rank.value().saturating_sub(1)),
)
.await
.context("admin should be able to add operator to team")?;
owner
.sync_now(team.admin.aranya_local_addr().await?, None)
.await
.context("owner unable to sync with admin")?;
owner
.device(team.operator.id)
.assign_role(roles.operator().id)
.await
.context("owner should be able to assign operator role")?;
operator
.sync_now(team.owner.aranya_local_addr().await?, None)
.await
.context("operator unable to sync with owner")?;
let member_role_rank = team
.owner
.client
.team(team_id)
.rank(roles.member().id)
.await?;
for (name, kb, device_id) in [
("membera", team.membera.pk.clone(), team.membera.id),
("memberb", team.memberb.pk.clone(), team.memberb.id),
] {
admin
.add_device(
kb,
None,
Rank::new(member_role_rank.value().saturating_sub(1)),
)
.await
.with_context(|| format!("admin should be able to add `{name}` to team"))?;
operator
.sync_now(team.admin.aranya_local_addr().await?, None)
.await
.context("operator unable to sync with admin")?;
operator
.device(device_id)
.assign_role(roles.member().id)
.await
.with_context(|| {
format!("operator should be able to assign member role to `{name}`")
})?;
}
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_add_device_with_initial_role_requires_outranking() -> Result<()> {
let mut devices =
DevicesCtx::new("test_add_device_with_initial_role_requires_outranking").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices
.setup_default_roles(team_id)
.await
.context("unable to setup default roles")?;
let owner_team = devices.owner.client.team(team_id);
let admin_team = devices.admin.client.team(team_id);
owner_team
.add_perm_to_role(roles.admin().id, Permission::AssignRole)
.await
.context("owner should be able to add AssignRole to admin")?;
let admin_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.admin().id)
.await?;
let member_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.member().id)
.await?;
owner_team
.add_device(
devices.admin.pk.clone(),
Some(roles.admin().id),
Rank::new(admin_role_rank.value().saturating_sub(1)),
)
.await
.context("owner should be able to add admin to team")?;
admin_team
.sync_now(devices.owner.aranya_local_addr().await?, None)
.await
.context("admin unable to sync with owner")?;
admin_team
.add_device(
devices.membera.pk.clone(),
Some(roles.member().id),
Rank::new(member_role_rank.value().saturating_sub(1)),
)
.await
.context("admin should be able to add device with member role (admin outranks member)")?;
match admin_team
.add_device(
devices.memberb.pk.clone(),
Some(roles.owner().id),
Rank::new(member_role_rank.value().saturating_sub(1)),
)
.await
{
Ok(_) => {
bail!("expected add_device with owner role to fail (admin does not outrank owner)")
}
Err(crate::Error::Aranya(_)) => {}
Err(err) => bail!("unexpected error: {err:?}"),
}
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_remove_devices() -> Result<()> {
let mut devices = DevicesCtx::new("test_remove_devices").await?;
let team_id = devices
.create_and_add_team()
.await
.expect("expected to create team");
info!(?team_id);
let roles = devices
.setup_default_roles(team_id)
.await
.context("unable to setup default roles")?;
devices.add_all_device_roles(team_id, &roles).await?;
let owner = devices.owner.client.team(team_id);
assert_eq!(owner.devices().await?.iter().count(), 5);
owner.device(devices.membera.id).remove_from_team().await?;
assert_eq!(owner.devices().await?.iter().count(), 4);
owner.device(devices.memberb.id).remove_from_team().await?;
assert_eq!(owner.devices().await?.iter().count(), 3);
owner.device(devices.operator.id).remove_from_team().await?;
assert_eq!(owner.devices().await?.iter().count(), 2);
owner.device(devices.admin.id).remove_from_team().await?;
assert_eq!(owner.devices().await?.iter().count(), 1);
owner
.device(devices.owner.id)
.remove_from_team()
.await
.expect_err("owner should not be able to remove itself from team");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_query_functions() -> Result<()> {
let mut devices = DevicesCtx::new("test_query_functions").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices
.setup_default_roles(team_id)
.await
.context("unable to setup default roles")?;
devices.add_all_device_roles(team_id, &roles).await?;
let owner_team = devices.owner.client.team(team_id);
let member_role_rank = owner_team.rank(roles.member().id).await?;
let label_rank = Rank::new(member_role_rank.value().saturating_sub(1));
let label_id = owner_team.create_label(text!("label1"), label_rank).await?;
let op = ChanOp::SendRecv;
owner_team
.device(devices.membera.id)
.assign_label(label_id, op)
.await?;
owner_team
.device(devices.memberb.id)
.assign_label(label_id, op)
.await?;
let owner_addr = devices.owner.aranya_local_addr().await?;
devices
.membera
.client
.team(team_id)
.sync_now(owner_addr, None)
.await?;
devices
.memberb
.client
.team(team_id)
.sync_now(owner_addr, None)
.await?;
let memberb = devices.membera.client.team(team_id);
let devices_query = memberb.devices().await?;
assert_eq!(devices_query.iter().count(), 5);
debug!(
"membera devices on team: {:?}",
devices_query.iter().count()
);
let dev_role = memberb.device(devices.membera.id).role().await?;
assert_eq!(dev_role.as_ref().map(|r| r.id), Some(roles.member().id));
debug!("membera role: {:?}", dev_role);
let keybundle = memberb
.device(devices.membera.id)
.public_key_bundle()
.await?;
debug!("membera keybundle: {:?}", keybundle);
let membera_labels = memberb
.device(devices.membera.id)
.label_assignments()
.await?;
assert_eq!(membera_labels.iter().count(), 1);
let team_labels = owner_team.labels().await?;
assert_eq!(team_labels.iter().count(), 1);
owner_team.label(label_id).await?;
assert_eq!(
roles.member().id,
memberb
.device(devices.memberb.id)
.role()
.await?
.expect("expected role")
.id
);
let membera_team = devices.membera.client.team(team_id);
assert_eq!(membera_team.roles().await?.iter().count(), 4);
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_add_team() -> Result<()> {
let devices = DevicesCtx::new("test_add_team").await?;
let owner_addr = devices.owner.aranya_local_addr().await?;
let owner = devices
.owner
.client
.create_team({
CreateTeamConfig::builder()
.quic_sync(CreateTeamQuicSyncConfig::builder().build()?)
.build()?
})
.await
.expect("expected to create team");
let team_id = owner.team_id();
info!(?team_id);
let roles = devices.setup_default_roles(team_id).await?;
let member_role_rank = owner.rank(roles.member().id).await?;
let label_rank = Rank::new(member_role_rank.value().saturating_sub(1));
info!("adding admin to team");
let admin_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.admin().id)
.await?;
owner
.add_device(
devices.admin.pk.clone(),
None,
Rank::new(admin_role_rank.value().saturating_sub(1)),
)
.await?;
info!("adding operator to team");
let operator_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.operator().id)
.await?;
owner
.add_device(
devices.operator.pk.clone(),
None,
Rank::new(operator_role_rank.value().saturating_sub(1)),
)
.await?;
owner
.device(devices.admin.id)
.assign_role(roles.admin().id)
.await?;
{
let admin = devices.admin.client.team(team_id);
let err = admin
.sync_now(owner_addr, None)
.await
.expect_err("syncing should fail before add_team()");
assert!(matches!(err, crate::Error::Aranya(_)), "{err:?}");
let err = admin
.device(devices.operator.id)
.assign_role(roles.operator().id)
.await
.expect_err("role assignment should fail before add_team()");
assert!(matches!(err, crate::Error::Aranya(_)), "{err:?}");
}
let admin_seed = owner
.encrypt_psk_seed_for_peer(devices.admin.pk.encryption())
.await?;
devices
.admin
.client
.add_team({
AddTeamConfig::builder()
.team_id(team_id)
.quic_sync(
AddTeamQuicSyncConfig::builder()
.wrapped_seed(&admin_seed)?
.build()?,
)
.build()?
})
.await?;
{
let admin = devices.admin.client.team(team_id);
admin.sync_now(owner_addr, None).await?;
admin
.create_label(text!("test_label"), label_rank)
.await
.context("Creating a label should not fail here!")?;
}
return Ok(());
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_remove_team() -> Result<()> {
let mut devices = DevicesCtx::new("test_remove_team").await?;
let team_id = devices
.create_and_add_team()
.await
.expect("expected to create team");
info!(?team_id);
let owner = devices.owner.client.team(team_id);
let roles = devices.setup_default_roles(team_id).await?;
let member_role_rank = owner.rank(roles.member().id).await?;
let label_rank = Rank::new(member_role_rank.value().saturating_sub(1));
{
let admin = devices.admin.client.team(team_id);
info!("adding operator to team");
let operator_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.operator().id)
.await?;
owner
.add_device(
devices.operator.pk.clone(),
None,
Rank::new(operator_role_rank.value().saturating_sub(1)),
)
.await?;
let admin_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.admin().id)
.await?;
owner
.add_device(
devices.admin.pk.clone(),
None,
Rank::new(admin_role_rank.value().saturating_sub(1)),
)
.await?;
owner
.device(devices.admin.id)
.assign_role(roles.admin().id)
.await?;
admin
.sync_now(devices.owner.aranya_local_addr().await?, None)
.await?;
admin.create_label(text!("test_label"), label_rank).await?;
}
devices.admin.client.remove_team(team_id).await?;
{
let admin = devices.admin.client.team(team_id);
match admin.create_label(text!("test_label2"), label_rank).await {
Ok(_) => bail!("Expected label creation to fail"),
Err(crate::Error::Aranya(_)) => {}
Err(_) => bail!("Unexpected error"),
}
}
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_multi_team_sync() -> Result<()> {
let devices = DevicesCtx::new("test_multi_team_sync").await?;
let owner_addr = devices.owner.aranya_local_addr().await?;
let team1 = devices
.owner
.client
.create_team({
CreateTeamConfig::builder()
.quic_sync(CreateTeamQuicSyncConfig::builder().build()?)
.build()?
})
.await
.expect("expected to create team1");
let team_id1 = team1.team_id();
info!(?team_id1);
let team2 = devices
.owner
.client
.create_team({
CreateTeamConfig::builder()
.quic_sync(CreateTeamQuicSyncConfig::builder().build()?)
.build()?
})
.await
.expect("expected to create team2");
let team_id2 = team2.team_id();
info!(?team_id2);
let roles1 = devices.setup_default_roles(team_id1).await?;
let roles2 = devices.setup_default_roles(team_id2).await?;
info!("adding admin to team1");
let admin_role_rank_t1 = devices
.owner
.client
.team(team_id1)
.rank(roles1.admin().id)
.await?;
let operator_role_rank_t1 = devices
.owner
.client
.team(team_id1)
.rank(roles1.operator().id)
.await?;
team1
.add_device(
devices.admin.pk.clone(),
None,
Rank::new(admin_role_rank_t1.value().saturating_sub(1)),
)
.await?;
info!("adding operator to team1");
team1
.add_device(
devices.operator.pk.clone(),
None,
Rank::new(operator_role_rank_t1.value().saturating_sub(1)),
)
.await?;
team1
.device(devices.admin.id)
.assign_role(roles1.admin().id)
.await?;
info!("adding admin to team2");
let admin_role_rank_t2 = devices
.owner
.client
.team(team_id2)
.rank(roles2.admin().id)
.await?;
let operator_role_rank_t2 = devices
.owner
.client
.team(team_id2)
.rank(roles2.operator().id)
.await?;
team2
.add_device(
devices.admin.pk.clone(),
None,
Rank::new(admin_role_rank_t2.value().saturating_sub(1)),
)
.await?;
info!("adding operator to team2");
team2
.add_device(
devices.operator.pk.clone(),
None,
Rank::new(operator_role_rank_t2.value().saturating_sub(1)),
)
.await?;
team2
.device(devices.admin.id)
.assign_role(roles2.admin().id)
.await?;
{
let admin = devices.admin.client.team(team_id1);
let err = admin
.sync_now(owner_addr, None)
.await
.expect_err("syncing team1 should fail before add_team()");
assert!(matches!(err, crate::Error::Aranya(_)), "{err:?}");
let err = admin
.device(devices.operator.id)
.assign_role(roles1.operator().id)
.await
.expect_err("role assignment on team1 should fail before add_team()");
assert!(matches!(err, crate::Error::Aranya(_)), "{err:?}");
}
let admin_seed1 = team1
.encrypt_psk_seed_for_peer(devices.admin.pk.encryption())
.await?;
devices
.admin
.client
.add_team({
AddTeamConfig::builder()
.team_id(team_id1)
.quic_sync(
AddTeamQuicSyncConfig::builder()
.wrapped_seed(&admin_seed1)?
.build()?,
)
.build()?
})
.await?;
let admin1 = devices.admin.client.team(team_id1);
admin1.sync_now(owner_addr, None).await?;
let member_role_rank_t1 = admin1.rank(roles1.member().id).await?;
let label_rank_t1 = Rank::new(member_role_rank_t1.value().saturating_sub(1));
admin1
.create_label(text!("team1_label"), label_rank_t1)
.await
.context("Creating a label should not fail here!")?;
{
let admin = devices.admin.client.team(team_id2);
let err = admin
.sync_now(owner_addr, None)
.await
.expect_err("syncing team2 should fail before add_team()");
assert!(matches!(err, crate::Error::Aranya(_)), "{err:?}");
let err = admin
.device(devices.operator.id)
.assign_role(roles2.operator().id)
.await
.expect_err("role assignment on team2 should fail before add_team()");
assert!(matches!(err, crate::Error::Aranya(_)), "{err:?}");
}
let admin_seed2 = team2
.encrypt_psk_seed_for_peer(devices.admin.pk.encryption())
.await?;
devices
.admin
.client
.add_team({
AddTeamConfig::builder()
.team_id(team_id2)
.quic_sync(
AddTeamQuicSyncConfig::builder()
.wrapped_seed(&admin_seed2)?
.build()?,
)
.build()?
})
.await?;
let admin2 = devices.admin.client.team(team_id2);
admin2.sync_now(owner_addr, None).await?;
let member_role_rank_t2 = admin2.rank(roles2.member().id).await?;
let label_rank_t2 = Rank::new(member_role_rank_t2.value().saturating_sub(1));
admin2
.create_label(text!("team2_label"), label_rank_t2)
.await
.context("Creating a label should not fail here!")?;
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
#[cfg(feature = "preview")]
async fn test_hello_subscription() -> Result<()> {
let mut devices = DevicesCtx::new("test_hello_subscription").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
devices.add_all_device_roles(team_id, &roles).await?;
let admin_addr = devices.admin.aranya_local_addr().await?;
let membera_team = devices.membera.client.team(team_id);
let admin_team = devices.admin.client.team(team_id);
let sync_config = SyncPeerConfig::builder()
.sync_now(false)
.sync_on_hello(true)
.build()?;
membera_team.add_sync_peer(admin_addr, sync_config).await?;
info!("membera added admin as sync peer with sync_on_hello=true");
membera_team
.sync_hello_subscribe(
admin_addr,
HelloSubscriptionConfig::builder()
.periodic_interval(Duration::from_secs(60))
.build()?,
)
.await?;
info!("membera subscribed to hello notifications from admin");
info!("verifying initial state - membera should not see any labels created by admin");
let initial_labels = membera_team.labels().await?;
let initial_label_count = initial_labels.iter().count();
info!(
"initial label count as seen by membera: {}",
initial_label_count
);
info!("admin creating a test label");
let member_role_rank = admin_team.rank(roles.member().id).await?;
let label_rank = Rank::new(member_role_rank.value().saturating_sub(1));
let test_label = admin_team
.create_label(
aranya_daemon_api::text!("sync_hello_test_label"),
label_rank,
)
.await?;
info!("admin created test label: {:?}", test_label);
info!("waiting for hello message and automatic sync...");
let poll_start = std::time::Instant::now();
let poll_timeout = Duration::from_millis(10_000);
let poll_interval = Duration::from_millis(100);
let final_labels = loop {
let current_labels = membera_team.labels().await?;
let current_count = current_labels.iter().count();
if current_count > initial_label_count {
info!(
"sync detected - label count increased from {} to {} after {:?}",
initial_label_count,
current_count,
poll_start.elapsed()
);
break current_labels;
}
if poll_start.elapsed() >= poll_timeout {
bail!(
"Sync on hello failed: timeout after {:?} - expected label count to increase from {} but it remained at {}",
poll_timeout,
initial_label_count,
current_count
);
}
sleep(poll_interval).await;
};
let label_exists = final_labels
.iter()
.any(|label| label.name.as_str() == "sync_hello_test_label");
if !label_exists {
bail!("Sync on hello failed: the test label created by admin is not visible to membera");
}
info!("sync_on_hello test succeeded - membera automatically synced after receiving hello notification");
info!("testing basic subscription functionality");
let owner_addr = devices.owner.aranya_local_addr().await?;
let operator_team = devices.operator.client.team(team_id);
admin_team
.sync_hello_subscribe(owner_addr, HelloSubscriptionConfig::default())
.await?;
info!("admin subscribed to hello notifications from owner");
operator_team
.sync_hello_subscribe(owner_addr, HelloSubscriptionConfig::default())
.await?;
operator_team
.sync_hello_subscribe(admin_addr, HelloSubscriptionConfig::default())
.await?;
info!("operator subscribed to both owner and admin");
admin_team.sync_hello_unsubscribe(owner_addr).await?;
operator_team.sync_hello_unsubscribe(owner_addr).await?;
operator_team.sync_hello_unsubscribe(admin_addr).await?;
membera_team.sync_hello_unsubscribe(admin_addr).await?;
info!("all devices unsubscribed");
admin_team
.sync_hello_subscribe(owner_addr, HelloSubscriptionConfig::default())
.await?;
admin_team.sync_hello_unsubscribe(owner_addr).await?;
info!("tested immediate subscribe/unsubscribe");
let memberb_addr = devices.memberb.aranya_local_addr().await?;
admin_team.sync_hello_unsubscribe(memberb_addr).await?;
info!("tested unsubscribing from non-subscribed peer");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
#[cfg(feature = "preview")]
async fn test_hello_subscription_schedule_delay() -> Result<()> {
let mut devices = DevicesCtx::new("test_hello_subscription_schedule_delay").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
devices.add_all_device_roles(team_id, &roles).await?;
let admin_addr = devices.admin.aranya_local_addr().await?;
let membera_team = devices.membera.client.team(team_id);
let admin_team = devices.admin.client.team(team_id);
let sync_config = SyncPeerConfig::builder()
.sync_now(false)
.sync_on_hello(true)
.build()?;
membera_team.add_sync_peer(admin_addr, sync_config).await?;
info!("membera added admin as sync peer with sync_on_hello=true");
info!("Phase 1: Testing with high schedule_delay (60s) and high graph_change_debounce (60s)");
membera_team
.sync_hello_subscribe(
admin_addr,
HelloSubscriptionConfig::builder()
.graph_change_debounce(Duration::from_secs(60))
.periodic_interval(Duration::from_secs(60))
.build()?,
)
.await?;
info!("membera subscribed to hello notifications from admin with high schedule_delay");
info!("verifying initial state - membera should not see any labels created by admin");
let initial_labels = membera_team.labels().await?;
let initial_label_count = initial_labels.iter().count();
info!(
"initial label count as seen by membera: {}",
initial_label_count
);
info!("admin creating first test label");
let member_role_rank = admin_team.rank(roles.member().id).await?;
let label_rank = Rank::new(member_role_rank.value().saturating_sub(1));
let test_label_1 = admin_team
.create_label(
aranya_daemon_api::text!("schedule_test_label_1"),
label_rank,
)
.await?;
info!("admin created first test label: {:?}", test_label_1);
info!("waiting for first label to be seen via graph change notification...");
let poll_start = std::time::Instant::now();
let poll_timeout = Duration::from_secs(10);
let poll_interval = Duration::from_millis(100);
let first_label_seen = loop {
let current_labels = membera_team.labels().await?;
let current_count = current_labels.iter().count();
if current_count > initial_label_count {
let label_exists = current_labels
.iter()
.any(|label| label.name.as_str() == "schedule_test_label_1");
if label_exists {
info!(
"first label seen after {:?} - label count increased from {} to {}",
poll_start.elapsed(),
initial_label_count,
current_count
);
break true;
}
}
if poll_start.elapsed() >= poll_timeout {
bail!(
"First label not seen: timeout after {:?} - expected label count to increase from {} but it remained at {}",
poll_timeout,
initial_label_count,
current_count
);
}
sleep(poll_interval).await;
};
assert!(first_label_seen, "First label should have been seen");
info!("admin creating second test label");
let test_label_2 = admin_team
.create_label(
aranya_daemon_api::text!("schedule_test_label_2"),
label_rank,
)
.await?;
info!("admin created second test label: {:?}", test_label_2);
info!("waiting briefly to confirm second label is not seen (rate-limited)...");
sleep(Duration::from_secs(2)).await;
let current_labels = membera_team.labels().await?;
let current_count = current_labels.iter().count();
let second_label_exists = current_labels
.iter()
.any(|label| label.name.as_str() == "schedule_test_label_2");
if second_label_exists {
bail!(
"Second label should not have been seen yet (rate-limited) - found {} labels, expected {}",
current_count,
initial_label_count + 1
);
}
info!("confirmed second label not seen - rate limiting working");
info!("Phase 2: Testing with low schedule_delay (10ms)");
membera_team.sync_hello_unsubscribe(admin_addr).await?;
info!("membera unsubscribed from admin");
membera_team
.sync_hello_subscribe(
admin_addr,
HelloSubscriptionConfig::builder()
.graph_change_debounce(Duration::from_secs(60))
.periodic_interval(Duration::from_millis(10))
.build()?,
)
.await?;
info!("membera subscribed to hello notifications from admin with low periodic_interval");
info!("waiting for both labels to be seen via scheduled periodic send...");
let poll_start = std::time::Instant::now();
let poll_timeout = Duration::from_secs(10);
let poll_interval = Duration::from_millis(100);
let both_labels_seen = loop {
let current_labels = membera_team.labels().await?;
let current_count = current_labels.iter().count();
if current_count >= initial_label_count + 2 {
let label1_exists = current_labels
.iter()
.any(|label| label.name.as_str() == "schedule_test_label_1");
let label2_exists = current_labels
.iter()
.any(|label| label.name.as_str() == "schedule_test_label_2");
if label1_exists && label2_exists {
info!(
"both labels seen after {:?} - label count: {} (expected at least {})",
poll_start.elapsed(),
current_count,
initial_label_count + 2
);
break true;
}
}
if poll_start.elapsed() >= poll_timeout {
let current_labels = membera_team.labels().await?;
let label1_exists = current_labels
.iter()
.any(|label| label.name.as_str() == "schedule_test_label_1");
let label2_exists = current_labels
.iter()
.any(|label| label.name.as_str() == "schedule_test_label_2");
bail!(
"Both labels not seen: timeout after {:?} - label1: {}, label2: {}, count: {} (expected at least {})",
poll_timeout,
label1_exists,
label2_exists,
current_labels.iter().count(),
initial_label_count + 2
);
}
sleep(poll_interval).await;
};
assert!(both_labels_seen, "Both labels should have been seen");
membera_team.sync_hello_unsubscribe(admin_addr).await?;
info!("cleanup: unsubscribed membera from admin");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_setup_default_roles_single_use() -> Result<()> {
let mut devices = DevicesCtx::new("test_setup_default_roles_single_use").await?;
let team_id = devices.create_and_add_team().await?;
devices
.setup_default_roles(team_id)
.await
.context("unable to setup default roles")?;
let owner_team = devices.owner.client.team(team_id);
match owner_team.setup_default_roles().await {
Ok(_) => bail!("expected replayed setup_default_roles to fail"),
Err(crate::Error::Aranya(_)) => {}
Err(err) => bail!("unexpected error re-running setup_default_roles: {err:?}"),
}
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_create_role() -> Result<()> {
let mut devices = DevicesCtx::new("test_create_role").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let member_role_rank = owner_team.rank(roles.member().id).await?;
let expected_rank = Rank::new(member_role_rank.value().saturating_sub(1));
let test_role = owner_team
.create_role(text!("test_role"), expected_rank)
.await
.expect("expected to create role");
let rank = owner_team.rank(test_role.id).await?;
assert_eq!(rank, expected_rank);
owner_team
.roles()
.await?
.into_iter()
.find(|r| r.name == "test_role")
.expect("no test role found");
let admin_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.admin().id)
.await?;
owner_team
.add_device(
devices.admin.pk.clone(),
Some(roles.admin().id),
Rank::new(admin_role_rank.value().saturating_sub(1)),
)
.await?;
let admin_team = devices.admin.client.team(team_id);
let owner_addr = devices.owner.aranya_local_addr().await?;
admin_team.sync_now(owner_addr, None).await?;
let test_role2 = admin_team
.roles()
.await?
.into_iter()
.find(|r| r.name == "test_role")
.expect("no test role found");
assert_eq!(test_role.id, test_role2.id);
assert_eq!(test_role.name, test_role2.name);
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_add_perm_to_created_role() -> Result<()> {
let mut devices = DevicesCtx::new("test_add_perm_to_created_role").await?;
let team_id = devices.create_and_add_team().await?;
let owner_team = devices.owner.client.team(team_id);
let owner_addr = devices.owner.aranya_local_addr().await?;
let owner_device_rank = owner_team.rank(devices.owner.id).await?;
let custom_role_rank = owner_device_rank.value() / 2;
let admin_role = owner_team
.create_role(text!("admin"), custom_role_rank.into())
.await?;
owner_team
.add_perm_to_role(admin_role.id, Permission::AddDevice)
.await
.expect("expected to assign AddDevice to admin");
owner_team
.add_device(
devices.admin.pk,
Some(admin_role.id),
custom_role_rank.saturating_sub(1).into(),
)
.await
.expect("expected to add admin with role");
let admin_team = devices.admin.client.team(team_id);
admin_team.sync_now(owner_addr, None).await?;
admin_team
.add_device(
devices.operator.pk,
None,
custom_role_rank.saturating_sub(100).into(),
)
.await
.expect("admin should be able to add operator");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_privilege_escalation_rejected() -> Result<()> {
let team_name = "test_privilege_escalation_rejected";
let mut devices = DevicesCtx::new(team_name).await?;
let team_id = devices.create_and_add_team().await?;
let owner_team = devices.owner.client.team(team_id);
let roles = devices.setup_default_roles(team_id).await?;
let member_role_rank = owner_team.rank(roles.member().id).await?;
let malicious_role_rank = Rank::new(member_role_rank.value().saturating_sub(1));
let malicious_device_rank = Rank::new(malicious_role_rank.value().saturating_sub(1));
let target_role_rank = Rank::new(malicious_device_rank.value().saturating_sub(1));
let work_dir = tempfile::tempdir()?;
let work_dir_path = work_dir.path();
let device = DeviceCtx::new(team_name, "malicious", work_dir_path.join("malicious")).await?;
owner_team
.add_device(device.pk.clone(), None, malicious_device_rank)
.await?;
let device_seed = owner_team
.encrypt_psk_seed_for_peer(device.pk.encryption())
.await?;
device
.client
.add_team({
AddTeamConfig::builder()
.team_id(team_id)
.quic_sync(
AddTeamQuicSyncConfig::builder()
.wrapped_seed(&device_seed)?
.build()?,
)
.build()?
})
.await?;
let role = owner_team
.create_role(text!("malicious_role"), malicious_role_rank)
.await
.expect("expected to create malicious role");
owner_team
.add_perm_to_role(role.id, Permission::CreateRole)
.await?;
owner_team.device(device.id).assign_role(role.id).await?;
let device_team = device.client.team(team_id);
let owner_addr = devices.owner.aranya_local_addr().await?;
device_team.sync_now(owner_addr, None).await?;
let target_role = device_team
.create_role(text!("target_role"), target_role_rank)
.await
.expect("unable to create target role");
device_team
.add_perm_to_role(target_role.id, Permission::CanUseAfc)
.await
.expect_err("expected privilege escalation attempt to fail");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_remove_perm_from_default_role() -> Result<()> {
let mut devices = DevicesCtx::new("test_add_perm_to_created_role").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let owner_addr = devices.owner.aranya_local_addr().await?;
let admin_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.admin().id)
.await?;
owner_team
.add_device(
devices.admin.pk,
Some(roles.admin().id),
Rank::new(admin_role_rank.value().saturating_sub(1)),
)
.await
.expect("expected to add admin with role");
owner_team
.remove_perm_from_role(roles.admin().id, Permission::AddDevice)
.await
.expect("expected to remove AddDevice from admin");
let admin_team = devices.admin.client.team(team_id);
admin_team.sync_now(owner_addr, None).await?;
let operator_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.operator().id)
.await?;
admin_team
.add_device(
devices.operator.pk,
None,
Rank::new(operator_role_rank.value().saturating_sub(1)),
)
.await
.expect_err("admin should not be able to add operator");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_delete_role() -> Result<()> {
let mut devices = DevicesCtx::new("test_delete_role").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
owner_team
.delete_role(roles.member().id)
.await
.expect("expected to delete role");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_assign_role_self_rejected() -> Result<()> {
let mut devices = DevicesCtx::new("test_assign_role_self_rejected").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let err = owner_team
.device(devices.owner.id)
.assign_role(roles.owner().id)
.await
.expect_err("assigning role to self should fail");
assert!(matches!(err, crate::Error::Aranya(_)), "{err:?}");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_owner_cannot_revoke_owner_role() -> Result<()> {
let mut devices = DevicesCtx::new("test_owner_cannot_revoke_owner_role").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let err = owner_team
.device(devices.owner.id)
.revoke_role(roles.owner().id)
.await
.expect_err("sole owner cannot revoke its own role");
assert!(matches!(err, crate::Error::Aranya(_)), "{err:?}");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_cannot_assign_role_twice() -> Result<()> {
let mut devices = DevicesCtx::new("test_cannot_assign_role_twice").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
devices.add_all_device_roles(team_id, &roles).await?;
let owner_team = devices.owner.client.team(team_id);
match owner_team
.device(devices.membera.id)
.assign_role(roles.operator().id)
.await
{
Ok(_) => bail!("Expected role assignment to fail"),
Err(crate::Error::Aranya(_)) => {}
Err(_) => bail!("Unexpected error"),
}
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_delete_label_requires_permission() -> Result<()> {
let mut devices = DevicesCtx::new("test_delete_label_requires_permission").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
devices.add_all_device_roles(team_id, &roles).await?;
let owner_team = devices.owner.client.team(team_id);
let operator_team = devices.operator.client.team(team_id);
let member_role_rank = owner_team.rank(roles.member().id).await?;
let label_rank = Rank::new(member_role_rank.value().saturating_sub(1));
let label = owner_team
.create_label(text!("delete-label-guard"), label_rank)
.await?;
operator_team
.sync_now(devices.owner.aranya_local_addr().await?, None)
.await
.context("operator unable to sync owner state")?;
let err = operator_team
.delete_label(label)
.await
.expect_err("delete_label without permission should fail");
assert!(matches!(err, crate::Error::Aranya(_)), "{err:?}");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_assign_label_to_device_self_rejected() -> Result<()> {
let mut devices = DevicesCtx::new("test_assign_label_to_device_self_rejected").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_id = devices.owner.id;
let owner_team = devices.owner.client.team(team_id);
let member_role_rank = owner_team.rank(roles.member().id).await?;
let label_rank = Rank::new(member_role_rank.value().saturating_sub(1));
let label = owner_team
.create_label(text!("device-self-label"), label_rank)
.await?;
let err = owner_team
.device(owner_id)
.assign_label(label, ChanOp::SendRecv)
.await
.expect_err("assigning label to self should fail");
assert!(matches!(err, crate::Error::Aranya(_)), "{err:?}");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_admin_cannot_remove_last_owner() -> Result<()> {
let mut devices = DevicesCtx::new("test_admin_cannot_remove_last_owner").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
devices.add_all_device_roles(team_id, &roles).await?;
let admin_team = devices.admin.client.team(team_id);
let err = admin_team
.device(devices.owner.id)
.remove_from_team()
.await
.expect_err("removing the final owner should fail");
assert!(matches!(err, crate::Error::Aranya(_)), "{err:?}");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_create_label() -> Result<()> {
let mut devices = DevicesCtx::new("test_create_label").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let member_role_rank = owner_team.rank(roles.member().id).await?;
let expected_rank = Rank::new(member_role_rank.value().saturating_sub(1));
let label_id = owner_team
.create_label(text!("ranked_label"), expected_rank)
.await?;
let rank = owner_team.rank(label_id).await?;
assert_eq!(rank, expected_rank);
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_change_rank() -> Result<()> {
let mut devices = DevicesCtx::new("test_change_rank").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let member_role_rank = owner_team.rank(roles.member().id).await?;
let initial_rank = Rank::new(member_role_rank.value().saturating_sub(2));
let updated_rank = Rank::new(initial_rank.value().saturating_add(1));
let label = owner_team
.create_label(text!("mutable_label"), initial_rank)
.await?;
owner_team
.change_rank(label, initial_rank, updated_rank)
.await?;
let new_rank = owner_team.rank(label).await?;
assert_eq!(new_rank, updated_rank);
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_change_rank_requires_sufficient_author_rank() -> Result<()> {
let mut devices = DevicesCtx::new("test_change_rank_requires_sufficient_author_rank").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let operator_team = devices.operator.client.team(team_id);
let operator_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.operator().id)
.await?;
let member_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.member().id)
.await?;
let high_label_rank = Rank::new(operator_role_rank.value().saturating_add(50));
let high_label = owner_team
.create_label(text!("high_label"), high_label_rank)
.await?;
owner_team
.add_device(
devices.operator.pk.clone(),
Some(roles.operator().id),
Rank::new(operator_role_rank.value().saturating_sub(1)),
)
.await?;
owner_team
.add_perm_to_role(roles.operator().id, Permission::ChangeRank)
.await?;
let owner_addr = devices.owner.aranya_local_addr().await?;
operator_team.sync_now(owner_addr, None).await?;
match operator_team
.change_rank(high_label, high_label_rank, member_role_rank)
.await
{
Ok(_) => bail!("expected change_rank to fail when author rank < object rank"),
Err(crate::Error::Aranya(_)) => {}
Err(err) => bail!("unexpected change_rank error: {err:?}"),
}
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_create_role_rank_too_high_rejected() -> Result<()> {
let mut devices = DevicesCtx::new("test_create_role_rank_too_high_rejected").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let admin_team = devices.admin.client.team(team_id);
let admin_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.admin().id)
.await?;
owner_team
.add_device(
devices.admin.pk.clone(),
Some(roles.admin().id),
Rank::new(admin_role_rank.value().saturating_sub(1)),
)
.await?;
let owner_addr = devices.owner.aranya_local_addr().await?;
admin_team.sync_now(owner_addr, None).await?;
match admin_team
.create_role(
text!("too_high"),
Rank::new(admin_role_rank.value().saturating_add(100)),
)
.await
{
Ok(_) => bail!("expected create_role to fail when rank > author rank"),
Err(crate::Error::Aranya(_)) => {}
Err(err) => bail!("unexpected create_role error: {err:?}"),
}
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_add_device_rank_higher_than_role_rejected() -> Result<()> {
let mut devices = DevicesCtx::new("test_add_device_rank_higher_than_role_rejected").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let operator_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.operator().id)
.await?;
match owner_team
.add_device(
devices.admin.pk.clone(),
Some(roles.member().id),
operator_role_rank,
)
.await
{
Ok(_) => bail!("expected add_device to fail when device rank > role rank"),
Err(crate::Error::Aranya(_)) => {}
Err(err) => bail!("unexpected add_device error: {err:?}"),
}
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_change_rank_above_role_rank_rejected() -> Result<()> {
let mut devices = DevicesCtx::new("test_change_rank_above_role_rank_rejected").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let member_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.member().id)
.await?;
let operator_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.operator().id)
.await?;
let device_rank = Rank::new(member_role_rank.value().saturating_sub(1));
owner_team
.add_device(
devices.admin.pk.clone(),
Some(roles.member().id),
device_rank,
)
.await?;
match owner_team
.change_rank(devices.admin.id, device_rank, operator_role_rank)
.await
{
Ok(_) => bail!("expected change_rank to fail when new rank > role rank"),
Err(crate::Error::Aranya(_)) => {}
Err(err) => bail!("unexpected change_rank error: {err:?}"),
}
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_change_role_rank_rejected() -> Result<()> {
let mut devices = DevicesCtx::new("test_change_role_rank_rejected").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let member_role_rank = owner_team.rank(roles.member().id).await?;
let role_rank = Rank::new(member_role_rank.value().saturating_sub(2));
let role = owner_team
.create_role(text!("immutable_role"), role_rank)
.await?;
let attempted_rank = Rank::new(role_rank.value().saturating_add(1));
match owner_team
.change_rank(role.id, role_rank, attempted_rank)
.await
{
Ok(_) => bail!("expected change_rank to fail for roles"),
Err(crate::Error::Aranya(_)) => {}
Err(err) => bail!("unexpected change_rank error: {err:?}"),
}
let current_role_rank = owner_team.rank(role.id).await?;
assert_eq!(
current_role_rank, role_rank,
"role rank should not have changed"
);
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_role_rank_migration_pattern() -> Result<()> {
let mut devices = DevicesCtx::new("test_role_rank_migration_pattern").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let member_role_rank = owner_team.rank(roles.member().id).await?;
let old_role_rank = member_role_rank.value().saturating_sub(2);
let old_role = owner_team
.create_role(text!("old_role"), old_role_rank.into())
.await?;
owner_team
.add_perm_to_role(old_role.id, Permission::CreateLabel)
.await?;
owner_team
.add_perm_to_role(old_role.id, Permission::DeleteLabel)
.await?;
let device_rank = old_role_rank.saturating_sub(1);
owner_team
.add_device(
devices.admin.pk.clone(),
Some(old_role.id),
device_rank.into(),
)
.await?;
let assigned = owner_team.device(devices.admin.id).role().await?;
assert_eq!(assigned.map(|r| r.id), Some(old_role.id));
let new_role_rank = old_role_rank.saturating_add(1);
let new_role = owner_team
.create_role(text!("new_role"), new_role_rank.into())
.await?;
let old_perms = owner_team.role_perm(old_role.id).await?;
assert_eq!(old_perms.len(), 2, "old role should have 2 permissions");
for &perm in &old_perms {
owner_team.add_perm_to_role(new_role.id, perm).await?;
}
let mut new_perms = owner_team.role_perm(new_role.id).await?;
let mut old_sorted = old_perms.clone();
old_sorted.sort_by_key(|p| format!("{p:?}"));
new_perms.sort_by_key(|p| format!("{p:?}"));
assert_eq!(
old_sorted, new_perms,
"new role should have the same permissions as old role"
);
owner_team
.device(devices.admin.id)
.change_role(old_role.id, new_role.id)
.await
.context("should be able to change to higher-ranked role")?;
let assigned = owner_team.device(devices.admin.id).role().await?;
assert_eq!(assigned.map(|r| r.id), Some(new_role.id));
owner_team
.delete_role(old_role.id)
.await
.context("should be able to delete unused role")?;
let all_roles = owner_team.roles().await?;
assert!(
!all_roles.iter().any(|r| r.id == old_role.id),
"old role should be deleted"
);
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_insufficient_rank_cannot_operate_on_objects() -> Result<()> {
let mut devices = DevicesCtx::new("test_insufficient_rank_cannot_operate_on_objects").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let operator_team = devices.operator.client.team(team_id);
let admin_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.admin().id)
.await?;
let operator_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.operator().id)
.await?;
owner_team
.add_device(
devices.admin.pk.clone(),
Some(roles.admin().id),
Rank::new(admin_role_rank.value().saturating_sub(1)),
)
.await?;
owner_team
.add_device(
devices.operator.pk.clone(),
Some(roles.operator().id),
Rank::new(operator_role_rank.value().saturating_sub(1)),
)
.await?;
let high_label_rank = Rank::new(operator_role_rank.value().saturating_add(100));
let high_label = owner_team
.create_label(text!("high_label"), high_label_rank)
.await?;
let owner_addr = devices.owner.aranya_local_addr().await?;
operator_team.sync_now(owner_addr, None).await?;
match operator_team
.device(devices.admin.id)
.remove_from_team()
.await
{
Ok(_) => bail!("expected removing higher-ranked device to fail"),
Err(crate::Error::Aranya(_)) => {}
Err(err) => bail!("unexpected remove_device error: {err:?}"),
}
match operator_team.delete_role(roles.admin().id).await {
Ok(_) => bail!("expected deleting higher-ranked role to fail"),
Err(crate::Error::Aranya(_)) => {}
Err(err) => bail!("unexpected delete_role error: {err:?}"),
}
match operator_team.delete_label(high_label).await {
Ok(_) => bail!("expected deleting higher-ranked label to fail"),
Err(crate::Error::Aranya(_)) => {}
Err(err) => bail!("unexpected delete_label error: {err:?}"),
}
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_change_rank_new_rank_above_author_rejected() -> Result<()> {
let mut devices = DevicesCtx::new("test_change_rank_new_rank_above_author_rejected").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let operator_team = devices.operator.client.team(team_id);
let member_role_rank = owner_team.rank(roles.member().id).await?;
let low_label_rank = Rank::new(member_role_rank.value().saturating_sub(1));
let label = owner_team
.create_label(text!("low_label"), low_label_rank)
.await?;
let operator_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.operator().id)
.await?;
let admin_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.admin().id)
.await?;
owner_team
.add_device(
devices.operator.pk.clone(),
Some(roles.operator().id),
Rank::new(operator_role_rank.value().saturating_sub(1)),
)
.await?;
owner_team
.add_perm_to_role(roles.operator().id, Permission::ChangeRank)
.await?;
let owner_addr = devices.owner.aranya_local_addr().await?;
operator_team.sync_now(owner_addr, None).await?;
match operator_team
.change_rank(label, low_label_rank, admin_role_rank)
.await
{
Ok(_) => bail!("expected change_rank to fail when new_rank > author_rank"),
Err(crate::Error::Aranya(_)) => {}
Err(err) => bail!("unexpected change_rank error: {err:?}"),
}
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_change_rank_stale_old_rank_rejected() -> Result<()> {
let mut devices = DevicesCtx::new("test_change_rank_stale_old_rank_rejected").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let member_role_rank = owner_team.rank(roles.member().id).await?;
let initial_rank = Rank::new(member_role_rank.value().saturating_sub(3));
let updated_rank = Rank::new(initial_rank.value().saturating_add(1));
let stale_attempt_rank = Rank::new(updated_rank.value().saturating_add(1));
let label = owner_team
.create_label(text!("versioned_label"), initial_rank)
.await?;
owner_team
.change_rank(label, initial_rank, updated_rank)
.await?;
match owner_team
.change_rank(label, initial_rank, stale_attempt_rank)
.await
{
Ok(_) => bail!("expected change_rank to fail with stale old_rank"),
Err(crate::Error::Aranya(_)) => {}
Err(err) => bail!("unexpected change_rank error: {err:?}"),
}
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_change_rank_self_demotion() -> Result<()> {
let mut devices = DevicesCtx::new("test_change_rank_self_demotion").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let admin_team = devices.admin.client.team(team_id);
let admin_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.admin().id)
.await?;
let admin_rank = Rank::new(admin_role_rank.value().saturating_sub(1));
let member_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.member().id)
.await?;
let demoted_rank = Rank::new(member_role_rank.value().saturating_sub(1));
owner_team
.add_device(devices.admin.pk.clone(), Some(roles.admin().id), admin_rank)
.await?;
let owner_addr = devices.owner.aranya_local_addr().await?;
admin_team.sync_now(owner_addr, None).await?;
admin_team
.change_rank(devices.admin.id, admin_rank, demoted_rank)
.await
.context("device should be able to demote its own rank")?;
owner_team
.sync_now(devices.admin.aranya_local_addr().await?, None)
.await?;
let new_rank = owner_team.rank(devices.admin.id).await?;
assert_eq!(new_rank, demoted_rank);
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_change_rank_self_promotion_rejected() -> Result<()> {
let mut devices = DevicesCtx::new("test_change_rank_self_promotion_rejected").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let admin_team = devices.admin.client.team(team_id);
let admin_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.admin().id)
.await?;
let admin_rank = Rank::new(admin_role_rank.value().saturating_sub(1));
let promoted_rank = Rank::new(admin_rank.value().saturating_add(50));
owner_team
.add_device(devices.admin.pk.clone(), Some(roles.admin().id), admin_rank)
.await?;
let owner_addr = devices.owner.aranya_local_addr().await?;
admin_team.sync_now(owner_addr, None).await?;
match admin_team
.change_rank(devices.admin.id, admin_rank, promoted_rank)
.await
{
Ok(_) => bail!("expected self-promotion to fail"),
Err(crate::Error::Aranya(_)) => {}
Err(err) => bail!("unexpected change_rank error: {err:?}"),
}
let current_rank = admin_team.rank(devices.admin.id).await?;
assert_eq!(current_rank, admin_rank, "rank should not have changed");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_equal_rank_cannot_operate() -> Result<()> {
let mut devices = DevicesCtx::new("test_equal_rank_cannot_operate").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let admin_team = devices.admin.client.team(team_id);
let operator_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.operator().id)
.await?;
let equal_rank = Rank::new(operator_role_rank.value().saturating_sub(1));
owner_team
.add_device(devices.admin.pk.clone(), Some(roles.admin().id), equal_rank)
.await?;
owner_team
.add_device(
devices.operator.pk.clone(),
Some(roles.operator().id),
equal_rank,
)
.await?;
let owner_addr = devices.owner.aranya_local_addr().await?;
admin_team.sync_now(owner_addr, None).await?;
match admin_team
.device(devices.operator.id)
.remove_from_team()
.await
{
Ok(_) => bail!("expected removing equal-ranked device to fail"),
Err(crate::Error::Aranya(_)) => {}
Err(err) => bail!("unexpected remove_device error: {err:?}"),
}
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_assign_role_requires_outranking_both_role_and_device() -> Result<()> {
let mut devices = DevicesCtx::new("test_assign_role_requires_outranking_both").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let admin_team = devices.admin.client.team(team_id);
let owner_addr = devices.owner.aranya_local_addr().await?;
owner_team
.add_perm_to_role(roles.admin().id, Permission::AssignRole)
.await?;
let member_role_rank = owner_team.rank(roles.member().id).await?;
let high_role_rank = member_role_rank.value().saturating_sub(1);
let high_device_rank = high_role_rank.saturating_sub(1);
let admin_rank = high_device_rank.saturating_sub(1);
let low_role_rank = admin_rank.saturating_sub(1);
let low_device_rank = low_role_rank.saturating_sub(1);
let low_role = owner_team
.create_role(text!("low_role"), low_role_rank.into())
.await?;
let high_role = owner_team
.create_role(text!("high_role"), high_role_rank.into())
.await?;
owner_team
.add_device(
devices.admin.pk.clone(),
Some(roles.admin().id),
admin_rank.into(),
)
.await?;
owner_team
.add_device(devices.membera.pk.clone(), None, low_device_rank.into())
.await?;
owner_team
.add_device(devices.memberb.pk.clone(), None, high_device_rank.into())
.await?;
admin_team.sync_now(owner_addr, None).await?;
let result = admin_team
.device(devices.membera.id)
.assign_role(high_role.id)
.await;
assert!(
matches!(result, Err(crate::Error::Aranya(_))),
"Case 1: expected assign_role to fail when author doesn't outrank the role, got {result:?}"
);
let result = admin_team
.device(devices.memberb.id)
.assign_role(low_role.id)
.await;
assert!(
matches!(result, Err(crate::Error::Aranya(_))),
"Case 2: expected assign_role to fail when author doesn't outrank the device, got {result:?}"
);
admin_team
.device(devices.membera.id)
.assign_role(low_role.id)
.await
.context("Case 3: assign_role should succeed when author outranks both role and device")?;
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_create_label_rank_too_high_rejected() -> Result<()> {
let mut devices = DevicesCtx::new("test_create_label_rank_too_high_rejected").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let admin_team = devices.admin.client.team(team_id);
let admin_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.admin().id)
.await?;
owner_team
.add_device(
devices.admin.pk.clone(),
Some(roles.admin().id),
Rank::new(admin_role_rank.value().saturating_sub(1)),
)
.await?;
let owner_addr = devices.owner.aranya_local_addr().await?;
admin_team.sync_now(owner_addr, None).await?;
match admin_team
.create_label(
text!("too_high_label"),
Rank::new(admin_role_rank.value().saturating_add(100)),
)
.await
{
Ok(_) => bail!("expected create_label to fail when rank > author rank"),
Err(crate::Error::Aranya(_)) => {}
Err(err) => bail!("unexpected create_label error: {err:?}"),
}
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_perm_change_requires_outranking_role() -> Result<()> {
let mut devices = DevicesCtx::new("test_perm_change_requires_outranking_role").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let operator_team = devices.operator.client.team(team_id);
let operator_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.operator().id)
.await?;
owner_team
.add_device(
devices.operator.pk.clone(),
Some(roles.operator().id),
Rank::new(operator_role_rank.value().saturating_sub(1)),
)
.await?;
owner_team
.add_perm_to_role(roles.operator().id, Permission::ChangeRolePerms)
.await?;
let owner_addr = devices.owner.aranya_local_addr().await?;
operator_team.sync_now(owner_addr, None).await?;
match operator_team
.add_perm_to_role(roles.admin().id, Permission::CreateRole)
.await
{
Ok(_) => bail!("expected add_perm_to_role to fail when author doesn't outrank role"),
Err(crate::Error::Aranya(_)) => {}
Err(err) => bail!("unexpected add_perm_to_role error: {err:?}"),
}
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_assign_role_where_role_rank_equals_device_rank_allowed() -> Result<()> {
let mut devices =
DevicesCtx::new("test_assign_role_where_role_rank_equals_device_rank_allowed").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let member_role_rank = owner_team.rank(roles.member().id).await?;
let role_rank = member_role_rank.value().saturating_sub(1);
let custom_role = owner_team
.create_role(text!("equal_rank_role"), role_rank.into())
.await?;
owner_team
.add_device(devices.admin.pk.clone(), None, role_rank.into())
.await?;
owner_team
.device(devices.admin.id)
.assign_role(custom_role.id)
.await
.context("assigning role with equal rank should succeed")?;
let assigned_role = owner_team.device(devices.admin.id).role().await?;
assert_eq!(
assigned_role.map(|r| r.id),
Some(custom_role.id),
"role should be assigned"
);
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_change_rank_device_to_exact_role_rank_allowed() -> Result<()> {
let mut devices = DevicesCtx::new("test_change_rank_device_to_exact_role_rank_allowed").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let member_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.member().id)
.await?;
let device_rank = Rank::new(member_role_rank.value().saturating_sub(100));
owner_team
.add_device(
devices.admin.pk.clone(),
Some(roles.member().id),
device_rank,
)
.await?;
owner_team
.change_rank(devices.admin.id, device_rank, member_role_rank)
.await
.context("changing device rank to equal role rank should succeed")?;
let new_rank = owner_team.rank(devices.admin.id).await?;
assert_eq!(new_rank, member_role_rank);
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_has_permission_but_insufficient_rank() -> Result<()> {
let mut devices = DevicesCtx::new("test_has_permission_but_insufficient_rank").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let operator_team = devices.operator.client.team(team_id);
let operator_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.operator().id)
.await?;
let high_label_rank = Rank::new(operator_role_rank.value().saturating_add(100));
let high_label = owner_team
.create_label(text!("high_label"), high_label_rank)
.await?;
owner_team
.add_device(
devices.operator.pk.clone(),
Some(roles.operator().id),
Rank::new(operator_role_rank.value().saturating_sub(1)),
)
.await?;
owner_team
.add_perm_to_role(roles.operator().id, Permission::ChangeRank)
.await?;
let owner_addr = devices.owner.aranya_local_addr().await?;
operator_team.sync_now(owner_addr, None).await?;
let member_role_rank = owner_team.rank(roles.member().id).await?;
let low_rank = Rank::new(member_role_rank.value().saturating_sub(1));
match operator_team
.change_rank(high_label, high_label_rank, low_rank)
.await
{
Ok(_) => {
bail!("expected change_rank to fail despite having permission (insufficient rank)")
}
Err(crate::Error::Aranya(_)) => {}
Err(err) => bail!("unexpected change_rank error: {err:?}"),
}
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_outranks_but_missing_permission() -> Result<()> {
let mut devices = DevicesCtx::new("test_outranks_but_missing_permission").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices.setup_default_roles(team_id).await?;
let owner_team = devices.owner.client.team(team_id);
let admin_team = devices.admin.client.team(team_id);
let member_role_rank = owner_team.rank(roles.member().id).await?;
let low_label_rank = Rank::new(member_role_rank.value().saturating_sub(1));
let low_label = owner_team
.create_label(text!("low_label"), low_label_rank)
.await?;
let admin_role_rank = devices
.owner
.client
.team(team_id)
.rank(roles.admin().id)
.await?;
owner_team
.add_device(
devices.admin.pk.clone(),
Some(roles.admin().id),
Rank::new(admin_role_rank.value().saturating_sub(1)),
)
.await?;
owner_team
.remove_perm_from_role(roles.admin().id, Permission::ChangeRank)
.await?;
let owner_addr = devices.owner.aranya_local_addr().await?;
admin_team.sync_now(owner_addr, None).await?;
let attempted_rank = Rank::new(low_label_rank.value().saturating_add(1));
match admin_team
.change_rank(low_label, low_label_rank, attempted_rank)
.await
{
Ok(_) => {
bail!("expected change_rank to fail despite outranking target (missing permission)")
}
Err(crate::Error::Aranya(_)) => {}
Err(err) => bail!("unexpected change_rank error: {err:?}"),
}
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_duplicate_label_assignment_rejected() -> Result<()> {
let mut devices = DevicesCtx::new("test_duplicate_label_assignment_rejected").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices
.setup_default_roles(team_id)
.await
.context("unable to setup default roles")?;
devices.add_all_device_roles(team_id, &roles).await?;
let owner_team = devices.owner.client.team(team_id);
let member_role_rank = owner_team.rank(roles.member().id).await?;
let label_rank = Rank::new(member_role_rank.value().saturating_sub(1));
let label = owner_team.create_label(text!("label"), label_rank).await?;
owner_team
.device(devices.membera.id)
.assign_label(label, ChanOp::SendRecv)
.await
.expect("first label assignment should succeed");
let err = owner_team
.device(devices.membera.id)
.assign_label(label, ChanOp::SendRecv)
.await
.expect_err("duplicate label assignment should fail");
assert!(matches!(err, crate::Error::Aranya(_)), "{err:?}");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_device_generation_counter_label_reassignment() -> Result<()> {
let mut devices = DevicesCtx::new("test_device_generation_counter_label_reassignment").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices
.setup_default_roles(team_id)
.await
.context("unable to setup default roles")?;
devices.add_all_device_roles(team_id, &roles).await?;
let owner_team = devices.owner.client.team(team_id);
let member_role_rank = owner_team.rank(roles.member().id).await?;
let label_rank = Rank::new(member_role_rank.value().saturating_sub(1));
let label = owner_team.create_label(text!("label"), label_rank).await?;
let gen = owner_team
.device(devices.membera.id)
.generation()
.await
.expect("generation should succeed");
assert_eq!(gen, Some(0), "initial generation should be 0");
owner_team
.device(devices.membera.id)
.assign_label(label, ChanOp::SendRecv)
.await?;
let labels = owner_team
.device(devices.membera.id)
.label_assignments()
.await?;
assert_eq!(labels.iter().count(), 1, "label should be assigned");
owner_team
.device(devices.membera.id)
.remove_from_team()
.await?;
let gen = owner_team
.device(devices.membera.id)
.generation()
.await
.expect("generation should succeed");
assert_eq!(gen, Some(1), "generation should be 1 after removal");
let device_rank = Rank::new(member_role_rank.value().saturating_sub(1));
owner_team
.add_device(
devices.membera.pk.clone(),
Some(roles.member().id),
device_rank,
)
.await?;
let gen = owner_team
.device(devices.membera.id)
.generation()
.await
.expect("generation should succeed");
assert_eq!(gen, Some(1), "generation should still be 1 after re-add");
let labels = owner_team
.device(devices.membera.id)
.label_assignments()
.await?;
assert_eq!(
labels.iter().count(),
0,
"stale label should not be visible"
);
owner_team
.device(devices.membera.id)
.assign_label(label, ChanOp::SendRecv)
.await?;
let gen = owner_team
.device(devices.membera.id)
.generation()
.await
.expect("generation should succeed");
assert_eq!(
gen,
Some(1),
"generation should still be 1 after reassignment"
);
let labels = owner_team
.device(devices.membera.id)
.label_assignments()
.await?;
assert_eq!(
labels.iter().count(),
1,
"reassigned label should be visible"
);
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_add_device_to_team_twice_rejected() -> Result<()> {
let mut devices = DevicesCtx::new("test_add_device_to_team_twice_rejected").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices
.setup_default_roles(team_id)
.await
.context("unable to setup default roles")?;
devices.add_all_device_roles(team_id, &roles).await?;
let owner_team = devices.owner.client.team(team_id);
let member_role_rank = owner_team.rank(roles.member().id).await?;
let device_rank = Rank::new(member_role_rank.value().saturating_sub(1));
let err = owner_team
.add_device(
devices.membera.pk.clone(),
Some(roles.member().id),
device_rank,
)
.await
.expect_err("adding same device twice should fail");
assert!(matches!(err, crate::Error::Aranya(_)), "{err:?}");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_revoke_stale_label_assignment_rejected() -> Result<()> {
let mut devices = DevicesCtx::new("test_revoke_stale_label_assignment_rejected").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices
.setup_default_roles(team_id)
.await
.context("unable to setup default roles")?;
devices.add_all_device_roles(team_id, &roles).await?;
let owner_team = devices.owner.client.team(team_id);
let member_role_rank = owner_team.rank(roles.member().id).await?;
let label_rank = Rank::new(member_role_rank.value().saturating_sub(1));
let label = owner_team.create_label(text!("label"), label_rank).await?;
let gen = owner_team
.device(devices.membera.id)
.generation()
.await
.expect("generation should succeed");
assert_eq!(gen, Some(0), "initial generation should be 0");
owner_team
.device(devices.membera.id)
.assign_label(label, ChanOp::SendRecv)
.await
.expect("label assignment should succeed");
owner_team
.device(devices.membera.id)
.remove_from_team()
.await
.expect("remove_from_team should succeed");
let gen = owner_team
.device(devices.membera.id)
.generation()
.await
.expect("generation should succeed");
assert_eq!(gen, Some(1), "generation should be 1 after removal");
let err = owner_team
.device(devices.membera.id)
.revoke_label(label)
.await
.expect_err("revoking label from removed device should fail");
assert!(matches!(err, crate::Error::Aranya(_)), "{err:?}");
let device_rank = Rank::new(member_role_rank.value().saturating_sub(1));
owner_team
.add_device(
devices.membera.pk.clone(),
Some(roles.member().id),
device_rank,
)
.await
.expect("re-adding device should succeed");
let gen = owner_team
.device(devices.membera.id)
.generation()
.await
.expect("generation should succeed");
assert_eq!(gen, Some(1), "generation should still be 1 after re-add");
let err = owner_team
.device(devices.membera.id)
.revoke_label(label)
.await
.expect_err("revoking stale label assignment should fail");
assert!(matches!(err, crate::Error::Aranya(_)), "{err:?}");
Ok(())
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn test_concurrent_label_assign_and_device_removal() -> Result<()> {
let mut devices = DevicesCtx::new("test_concurrent_label_assign_and_device_removal").await?;
let team_id = devices.create_and_add_team().await?;
let roles = devices
.setup_default_roles(team_id)
.await
.context("unable to setup default roles")?;
devices.add_all_device_roles(team_id, &roles).await?;
let owner_team = devices.owner.client.team(team_id);
let member_role_rank = owner_team.rank(roles.member().id).await?;
let label_rank = Rank::new(member_role_rank.value().saturating_sub(1));
let label = owner_team.create_label(text!("label"), label_rank).await?;
let owner_addr = devices.owner.aranya_local_addr().await?;
let admin_team = devices.admin.client.team(team_id);
admin_team.sync_now(owner_addr, None).await?;
let gen = owner_team.device(devices.membera.id).generation().await?;
assert_eq!(gen, Some(0), "initial generation should be 0 on owner");
let gen = admin_team.device(devices.membera.id).generation().await?;
assert_eq!(gen, Some(0), "initial generation should be 0 on admin");
owner_team
.device(devices.membera.id)
.assign_label(label, ChanOp::SendRecv)
.await
.expect("label assignment should succeed locally");
let labels = owner_team
.device(devices.membera.id)
.label_assignments()
.await?;
assert_eq!(
labels.iter().count(),
1,
"label should be assigned on owner before sync"
);
let gen = owner_team.device(devices.membera.id).generation().await?;
assert_eq!(
gen,
Some(0),
"generation should still be 0 on owner before sync"
);
admin_team
.device(devices.membera.id)
.remove_from_team()
.await
.expect("remove should succeed locally");
let gen = admin_team.device(devices.membera.id).generation().await?;
assert_eq!(
gen,
Some(1),
"generation should be 1 on admin after removal"
);
let gen = owner_team.device(devices.membera.id).generation().await?;
assert_eq!(
gen,
Some(0),
"generation should still be 0 on owner before sync"
);
admin_team.sync_now(owner_addr, None).await?;
owner_team
.sync_now(devices.admin.aranya_local_addr().await?, None)
.await?;
let gen = owner_team.device(devices.membera.id).generation().await?;
assert_eq!(gen, Some(1), "generation should be 1 on owner after sync");
let device_rank = Rank::new(member_role_rank.value().saturating_sub(1));
owner_team
.add_device(
devices.membera.pk.clone(),
Some(roles.member().id),
device_rank,
)
.await
.context("re-adding device should succeed")?;
let gen = owner_team.device(devices.membera.id).generation().await?;
assert_eq!(gen, Some(1), "generation should still be 1 after re-add");
let labels = owner_team
.device(devices.membera.id)
.label_assignments()
.await?;
assert_eq!(
labels.iter().count(),
0,
"label should not be assigned after concurrent removal invalidated the assignment"
);
admin_team.sync_now(owner_addr, None).await?;
let labels = admin_team
.device(devices.membera.id)
.label_assignments()
.await?;
assert_eq!(
labels.iter().count(),
0,
"label should not be assigned on admin after sync either"
);
Ok(())
}