#[path = "test_support/state_guard.rs"]
mod state_guard;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use aion::EngineBuilder;
use aion_proto::generated::{self, workflow_service_server::WorkflowServiceServer};
use aion_server::api::grpc::workflow_service;
use aion_server::config::{AutoCreate, NamespaceConfig, NamespaceMode};
use aion_server::namespace::{
ForwardMintError, GrpcMintForwarder, MintCredentials, MintForwarder, encode_mint_origin,
};
use aion_server::routing::{FORWARD_HOPS_METADATA, GrpcRequestForwarder, RequestForwarder};
use aion_server::{NamespaceResolver, ServerState};
use aion_store::{EventStore, InMemoryStore, NamespaceOrigin, NamespaceStore};
use state_guard::StateUnderTest;
use tokio::net::TcpListener;
use tonic::transport::Server;
type TestError = Box<dyn std::error::Error>;
const NAMESPACE: &str = "tenant-a";
const SUBJECT: &str = "alice";
#[derive(Clone, Default)]
struct Received {
subject: Option<String>,
hops: Option<String>,
}
struct Owner {
server: StateUnderTest,
namespaces: Arc<InMemoryStore>,
received: Arc<Mutex<Received>>,
}
async fn owner(policy: AutoCreate) -> Result<Owner, TestError> {
let backing = Arc::new(InMemoryStore::default());
let store: Arc<dyn EventStore> = Arc::clone(&backing) as Arc<dyn EventStore>;
let engine = Arc::new(
EngineBuilder::new()
.stop_drain_timeout(std::time::Duration::from_secs(5))
.store_arc(store)
.in_memory_visibility()
.scheduler_threads(1)
.query_timeout(Duration::from_secs(5))
.build()
.await?,
);
let resolver = NamespaceResolver::from_config(
NamespaceConfig {
mode: NamespaceMode::SharedEngine,
},
engine,
);
Ok(Owner {
server: StateUnderTest::new(ServerState::from_parts_with_namespace_store(
resolver,
test_runtime(policy),
backing.clone(),
)),
namespaces: backing,
received: Arc::new(Mutex::new(Received::default())),
})
}
fn test_runtime(auto_create: AutoCreate) -> aion_server::config::RuntimeConfig {
use aion_server::config::{
AuthConfig, AuthoringConfig, DeployConfig, DevConfig, ListenConfig, MetricsConfig,
OpsConsoleAssetSource, OpsConsoleConfig, OutboxConfig, RuntimeConfig, WebSocketConfig,
WorkerConfig,
};
RuntimeConfig {
listen: ListenConfig {
grpc: std::net::SocketAddr::from(([127, 0, 0, 1], 0)),
http: std::net::SocketAddr::from(([127, 0, 0, 1], 0)),
},
tls: None,
auth: AuthConfig {
enabled: false,
jwks_url: None,
jwks_refresh_seconds: 300,
},
ops_console: OpsConsoleConfig {
source: OpsConsoleAssetSource::Embedded,
},
namespace: NamespaceConfig {
mode: NamespaceMode::SharedEngine,
},
worker: WorkerConfig {
heartbeat_window: Duration::from_secs(30),
..Default::default()
},
websocket: WebSocketConfig {
outbound_buffer_bound: 32,
event_broadcast_capacity: Some(64),
cluster_broadcast_capacity: Some(64),
},
workflow_packages: Vec::new(),
deploy: DeployConfig::default(),
authoring: AuthoringConfig::default(),
dev: DevConfig::default(),
outbox: OutboxConfig::default(),
observability: aion_server::config::ObservabilityConfig::with_flush_policy(64, 0),
mcp: aion_server::config::ResolvedMcpConfig::default(),
assistant: aion_server::config::ResolvedAssistantConfig::default(),
scheduler_threads: 1,
stop_drain_timeout: Some(std::time::Duration::from_secs(5)),
jit_threshold: None,
query_timeout: Some(Duration::from_secs(5)),
workloop_sweep_interval: Some(std::time::Duration::from_millis(50)),
default_namespace: "default".to_owned(),
auto_create,
max_in_flight_activities: aion_server::config::DEFAULT_MAX_IN_FLIGHT_ACTIVITIES,
drain_timeout: Duration::from_secs(30),
metrics: MetricsConfig { enabled: true },
owned_shards: Vec::new(),
cors_allowed_origins: Vec::new(),
}
}
async fn spawn_owner(
service: WorkflowServiceServer<impl generated::workflow_service_server::WorkflowService>,
received: Arc<Mutex<Received>>,
) -> Result<(std::net::SocketAddr, tokio::sync::oneshot::Sender<()>), TestError> {
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
let (tx, rx) = tokio::sync::oneshot::channel();
let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener);
let intercepted = tonic::service::interceptor::InterceptedService::new(
service,
move |request: tonic::Request<()>| {
let metadata = request.metadata();
let read = |key: &str| {
metadata
.get(key)
.and_then(|value| value.to_str().ok())
.map(str::to_owned)
};
if let Ok(mut seen) = received.lock() {
*seen = Received {
subject: read("x-aion-subject"),
hops: read(FORWARD_HOPS_METADATA),
};
}
Ok(request)
},
);
tokio::spawn(async move {
let _ = Server::builder()
.add_service(intercepted)
.serve_with_incoming_shutdown(incoming, async {
let _ = rx.await;
})
.await;
});
tokio::time::sleep(Duration::from_millis(50)).await;
Ok((addr, tx))
}
fn caller_credentials() -> Result<MintCredentials, TestError> {
let mut metadata = tonic::metadata::MetadataMap::new();
metadata.insert("x-aion-subject", SUBJECT.parse()?);
metadata.insert("x-aion-namespaces", NAMESPACE.parse()?);
Ok(MintCredentials::from_grpc_metadata(&metadata))
}
fn mint_forwarder() -> GrpcMintForwarder {
let transport: Arc<dyn RequestForwarder> = Arc::new(GrpcRequestForwarder::new());
GrpcMintForwarder::new(transport)
}
#[tokio::test]
async fn forwarded_mint_lands_on_the_owner_with_metadata_and_hop_stamp() -> Result<(), TestError> {
let owner = owner(AutoCreate::Open).await?;
let registry = Arc::clone(&owner.namespaces);
let received = Arc::clone(&owner.received);
let (addr, shutdown) = spawn_owner(
workflow_service(owner.server.state.clone()),
Arc::clone(&received),
)
.await?;
let result = mint_forwarder()
.forward_mint(
addr,
&caller_credentials()?,
&[NAMESPACE.to_owned()],
NamespaceOrigin::StartMint,
)
.await;
let _ = shutdown.send(());
result.map_err(|error| format!("the forwarded mint was refused: {error:?}"))?;
let record = registry
.get_namespace(NAMESPACE)
.await?
.ok_or("the owner must hold the record it was asked to mint")?;
assert_eq!(record.name, NAMESPACE);
assert_eq!(
record.origin,
NamespaceOrigin::StartMint,
"the initiator's provenance travels with the mint"
);
let seen = received
.lock()
.map_err(|_| "the interceptor's record is poisoned")?
.clone();
assert_eq!(
seen.subject.as_deref(),
Some(SUBJECT),
"the caller's metadata must reach the owner verbatim, so it authorizes \
the ORIGINAL caller and not an anonymous peer"
);
assert_eq!(
seen.hops.as_deref(),
Some("1"),
"the forward-hop counter must be stamped, so the cluster's existing loop \
prevention covers a forwarded mint too"
);
owner.server.shutdown()?;
Ok(())
}
#[tokio::test]
async fn a_closed_policy_refuses_a_forwarded_mint_exactly_as_a_local_one() -> Result<(), TestError>
{
let owner = owner(AutoCreate::Closed).await?;
let registry = Arc::clone(&owner.namespaces);
let local = owner
.server
.state
.namespace_minter()
.mint_or_gate(&[NAMESPACE.to_owned()], NamespaceOrigin::StartMint)
.await
.err()
.ok_or("a closed policy must refuse an unknown namespace locally")?
.to_wire_error();
let received = Arc::clone(&owner.received);
let (addr, shutdown) =
spawn_owner(workflow_service(owner.server.state.clone()), received).await?;
let forwarded = mint_forwarder()
.forward_mint(
addr,
&caller_credentials()?,
&[NAMESPACE.to_owned()],
NamespaceOrigin::StartMint,
)
.await
.err()
.ok_or("a closed policy must refuse a FORWARDED mint too")?;
let _ = shutdown.send(());
let ForwardMintError::Refused(wire) = forwarded else {
return Err(format!("expected the owner's own typed refusal, got {forwarded:?}").into());
};
assert_eq!(
wire.code, local.code,
"the forwarded refusal must carry the SAME typed code as the local one"
);
assert_eq!(
wire.message, local.message,
"and the same message: a forwarded mint is exactly as privileged as a local one"
);
assert!(
registry.get_namespace(NAMESPACE).await?.is_none(),
"a refused mint must create nothing"
);
owner.server.shutdown()?;
Ok(())
}
#[tokio::test]
async fn an_empty_namespace_set_is_refused() -> Result<(), TestError> {
let owner = owner(AutoCreate::Open).await?;
let received = Arc::clone(&owner.received);
let (addr, shutdown) =
spawn_owner(workflow_service(owner.server.state.clone()), received).await?;
let forwarder: Arc<dyn RequestForwarder> = Arc::new(GrpcRequestForwarder::new());
let status = forwarder
.forward(
addr,
caller_credentials()?.to_grpc_metadata(),
aion_server::routing::ForwardRequest::MintNamespace(generated::MintNamespaceRequest {
namespaces: Vec::new(),
origin: encode_mint_origin(NamespaceOrigin::StartMint),
}),
)
.await
.err()
.ok_or("an empty mint set must be refused")?;
let _ = shutdown.send(());
assert_eq!(status.code(), tonic::Code::InvalidArgument);
owner.server.shutdown()?;
Ok(())
}
#[tokio::test]
async fn an_unspecified_origin_is_refused() -> Result<(), TestError> {
let owner = owner(AutoCreate::Open).await?;
let registry = Arc::clone(&owner.namespaces);
let received = Arc::clone(&owner.received);
let (addr, shutdown) =
spawn_owner(workflow_service(owner.server.state.clone()), received).await?;
let forwarder: Arc<dyn RequestForwarder> = Arc::new(GrpcRequestForwarder::new());
let status = forwarder
.forward(
addr,
caller_credentials()?.to_grpc_metadata(),
aion_server::routing::ForwardRequest::MintNamespace(generated::MintNamespaceRequest {
namespaces: vec![NAMESPACE.to_owned()],
origin: 0,
}),
)
.await
.err()
.ok_or("an unspecified mint origin must be refused")?;
let _ = shutdown.send(());
assert_eq!(status.code(), tonic::Code::InvalidArgument);
assert!(
registry.get_namespace(NAMESPACE).await?.is_none(),
"a refused mint must create nothing"
);
owner.server.shutdown()?;
Ok(())
}
#[tokio::test]
async fn measure_the_cost_of_a_forwarded_mint_against_a_local_one() -> Result<(), TestError> {
const MINTS: u32 = 20;
let owner = owner(AutoCreate::Open).await?;
let registry = Arc::clone(&owner.namespaces);
let local_minter = owner.server.state.namespace_minter();
let began = std::time::Instant::now();
for index in 0..MINTS {
local_minter
.mint_or_gate(&[format!("local-{index}")], NamespaceOrigin::StartMint)
.await?;
}
let local_elapsed = began.elapsed();
let received = Arc::clone(&owner.received);
let (addr, shutdown) =
spawn_owner(workflow_service(owner.server.state.clone()), received).await?;
let forwarder = mint_forwarder();
let began = std::time::Instant::now();
for index in 0..MINTS {
forwarder
.forward_mint(
addr,
&caller_credentials()?,
&[format!("forwarded-{index}")],
NamespaceOrigin::StartMint,
)
.await
.map_err(|error| format!("forwarded mint {index} failed: {error:?}"))?;
}
let forwarded_elapsed = began.elapsed();
let _ = shutdown.send(());
println!(
"MINT COST over {MINTS} mints: local total {local_elapsed:?} (mean {:?}); \
forwarded total {forwarded_elapsed:?} (mean {:?}), fresh dial per forward",
local_elapsed / MINTS,
forwarded_elapsed / MINTS,
);
assert!(registry.get_namespace("local-0").await?.is_some());
assert!(registry.get_namespace("forwarded-0").await?.is_some());
owner.server.shutdown()?;
Ok(())
}