use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::Mutex;
use async_trait::async_trait;
use aion_proto::{WireError, WireErrorCode};
use aion_store::NamespaceOrigin;
use super::{
ForwardMintError, MintCredentials, MintForwarder, MintOwner, MintRoute, MintShardOwners,
NamespaceRouting, NamespaceShardResolver,
};
type TestResult = Result<(), Box<dyn std::error::Error>>;
const OWNER: &str = "127.0.0.1:50051";
struct FixedShards(HashMap<String, usize>);
impl NamespaceShardResolver for FixedShards {
fn shard_for_namespace(&self, name: &str) -> usize {
self.0.get(name).copied().unwrap_or(0)
}
}
struct FixedOwners {
shard: usize,
owner: MintOwner,
}
impl MintShardOwners for FixedOwners {
fn owner_of(&self, shard: usize) -> MintOwner {
if shard == self.shard {
self.owner.clone()
} else {
MintOwner::Local
}
}
}
struct ScriptedForwarder {
outcome: Mutex<Option<Result<(), ForwardMintError>>>,
seen: Mutex<Vec<(SocketAddr, Vec<String>, NamespaceOrigin)>>,
}
impl ScriptedForwarder {
fn new(outcome: Result<(), ForwardMintError>) -> Arc<Self> {
Arc::new(Self {
outcome: Mutex::new(Some(outcome)),
seen: Mutex::new(Vec::new()),
})
}
}
#[async_trait]
impl MintForwarder for ScriptedForwarder {
async fn forward_mint(
&self,
target: SocketAddr,
_credentials: &MintCredentials,
namespaces: &[String],
origin: NamespaceOrigin,
) -> Result<(), ForwardMintError> {
if let Ok(mut seen) = self.seen.lock() {
seen.push((target, namespaces.to_vec(), origin));
}
match self.outcome.lock() {
Ok(mut outcome) => outcome.take().unwrap_or(Ok(())),
Err(_) => Err(ForwardMintError::Unreachable("poisoned script".to_owned())),
}
}
}
fn routing(
shards: HashMap<String, usize>,
owners: FixedOwners,
forwarder: Arc<ScriptedForwarder>,
) -> NamespaceRouting {
NamespaceRouting::new(Arc::new(FixedShards(shards)), Arc::new(owners), forwarder)
}
fn shard_table(entries: &[(&str, usize)]) -> HashMap<String, usize> {
entries
.iter()
.map(|(name, shard)| ((*name).to_owned(), *shard))
.collect()
}
#[test]
fn locally_owned_registry_shard_routes_local() {
let routing = routing(
shard_table(&[("default", 1)]),
FixedOwners {
shard: 1,
owner: MintOwner::Local,
},
ScriptedForwarder::new(Ok(())),
);
assert_eq!(routing.route_for("default"), MintRoute::Local { shard: 1 });
}
#[test]
fn remote_forwardable_owner_routes_remote() -> TestResult {
let target: SocketAddr = OWNER.parse()?;
let routing = routing(
shard_table(&[("default", 1)]),
FixedOwners {
shard: 1,
owner: MintOwner::Remote(target),
},
ScriptedForwarder::new(Ok(())),
);
assert_eq!(
routing.route_for("default"),
MintRoute::Remote { shard: 1, target }
);
Ok(())
}
#[test]
fn unknown_owner_routes_unknown_so_the_fence_decides() {
let routing = routing(
shard_table(&[("default", 1)]),
FixedOwners {
shard: 1,
owner: MintOwner::Unknown,
},
ScriptedForwarder::new(Ok(())),
);
assert_eq!(
routing.route_for("default"),
MintRoute::Unknown { shard: 1 }
);
}
#[test]
fn namespaces_route_independently_by_their_own_shard() -> TestResult {
let target: SocketAddr = OWNER.parse()?;
let routing = routing(
shard_table(&[("default", 1), ("orders", 2)]),
FixedOwners {
shard: 1,
owner: MintOwner::Remote(target),
},
ScriptedForwarder::new(Ok(())),
);
assert_eq!(
routing.route_for("default"),
MintRoute::Remote { shard: 1, target }
);
assert_eq!(routing.route_for("orders"), MintRoute::Local { shard: 2 });
Ok(())
}
#[tokio::test]
async fn a_forwarded_mint_carries_the_namespace_and_origin() -> TestResult {
let target: SocketAddr = OWNER.parse()?;
let forwarder = ScriptedForwarder::new(Ok(()));
let routing = routing(
shard_table(&[("default", 1)]),
FixedOwners {
shard: 1,
owner: MintOwner::Remote(target),
},
Arc::clone(&forwarder),
);
routing
.forward("default", target, 1, NamespaceOrigin::StartMint)
.await?;
let seen = forwarder
.seen
.lock()
.map_err(|_| "forward log poisoned")?
.clone();
assert_eq!(
seen,
vec![(
target,
vec!["default".to_owned()],
NamespaceOrigin::StartMint
)]
);
Ok(())
}
#[tokio::test]
async fn an_owners_refusal_is_relayed_verbatim() -> TestResult {
let target: SocketAddr = OWNER.parse()?;
let refusal = WireError::namespace_denied("namespace tenant-x does not exist");
let routing = routing(
shard_table(&[("tenant-x", 1)]),
FixedOwners {
shard: 1,
owner: MintOwner::Remote(target),
},
ScriptedForwarder::new(Err(ForwardMintError::Refused(refusal))),
);
let error = routing
.forward("tenant-x", target, 1, NamespaceOrigin::StartMint)
.await
.err()
.ok_or("the owner's refusal must not be swallowed")?;
let wire = error.to_wire_error();
assert_eq!(wire.code, WireErrorCode::NamespaceDenied);
assert_eq!(wire.message, "namespace tenant-x does not exist");
Ok(())
}
#[tokio::test]
async fn an_unreachable_owner_surfaces_one_truthful_not_owner() -> TestResult {
let target: SocketAddr = OWNER.parse()?;
let routing = routing(
shard_table(&[("default", 1)]),
FixedOwners {
shard: 1,
owner: MintOwner::Remote(target),
},
ScriptedForwarder::new(Err(ForwardMintError::Unreachable(
"forward dial failed".to_owned(),
))),
);
let error = routing
.forward("default", target, 1, NamespaceOrigin::StartMint)
.await
.err()
.ok_or("an unreachable owner must not be reported as a mint")?;
let wire = error.to_wire_error();
assert_eq!(wire.code, WireErrorCode::NotOwner);
assert!(
wire.message.contains("namespace `default`") && wire.message.contains("shard 1"),
"the refusal must name the namespace and its shard: {}",
wire.message
);
Ok(())
}
#[test]
fn credentials_debug_redacts_values() -> TestResult {
let mut metadata = tonic::metadata::MetadataMap::new();
metadata.insert("authorization", "Bearer super-secret".parse()?);
let credentials = MintCredentials::from_grpc_metadata(&metadata);
let rendered = format!("{credentials:?}");
assert!(!rendered.contains("super-secret"), "rendered: {rendered}");
assert!(rendered.contains("entries: 1"), "rendered: {rendered}");
Ok(())
}