use std::sync::{Arc, Mutex};
use hostbat::{
GoldenVector, HostBuilder, HostControlError, HostModule, SchemaDescriptor, SchemaId,
SchemaRole, SchemaVersion,
};
use syncbat::{
Ctx, EffectClass, Handler, HandlerError, HandlerResult, OperationDescriptor,
OperationEffectRow, RuntimeError,
};
const HOST_CONTROL: &str = "ctrl.alpha";
fn canonical_bytes(value: &str) -> Vec<u8> {
batpak::canonical::to_bytes(&value).expect("canonical fixture encodes")
}
fn schema_with_role(id: &str, role: SchemaRole, bytes: &[u8]) -> SchemaDescriptor {
SchemaDescriptor::new(
SchemaId::new(id).expect("id"),
SchemaVersion(1),
role,
vec![GoldenVector::new("c", bytes.to_vec())],
)
.expect("descriptor")
}
fn control_descriptor() -> OperationDescriptor {
OperationDescriptor::new(
"host.reboot",
EffectClass::Control,
"schema.in.v1",
"schema.out.v1",
"receipt.v1",
)
.with_effect_row(OperationEffectRow::new().uses_host_control(HOST_CONTROL))
}
struct RebootHandler;
impl Handler for RebootHandler {
fn handle(&mut self, input: &[u8], cx: &mut Ctx<'_>) -> HandlerResult {
cx.host_control_handle()
.use_host_control(HOST_CONTROL)
.map_err(|error| HandlerError::failed(error.message().to_owned()))?;
Ok(input.to_vec())
}
}
fn control_module() -> HostModule {
HostModule::builder("mod.control", 1)
.operation(control_descriptor(), RebootHandler)
.expect("operation")
.schema(schema_with_role(
"schema.in.v1",
SchemaRole::OperationInput,
&canonical_bytes("default-in"),
))
.expect("input schema")
.schema(schema_with_role(
"schema.out.v1",
SchemaRole::OperationOutput,
&canonical_bytes("default-out"),
))
.expect("output schema")
.schema(schema_with_role(
"receipt.v1",
SchemaRole::ReceiptPayload,
&canonical_bytes("default-receipt"),
))
.expect("receipt schema")
.build()
.expect("module")
}
#[test]
fn host_control_op_performs_through_bound_controller() {
let observed: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&observed);
let mut host = HostBuilder::new()
.mount(control_module())
.expect("mount")
.host_control(move |control: &str| -> Result<(), HostControlError> {
sink.lock().expect("observed lock").push(control.to_owned());
Ok(())
})
.build()
.expect("build");
let payload = canonical_bytes("go");
let result = host.invoke("host.reboot", payload.clone()).expect("invoke");
assert_eq!(result.output(), payload.as_slice());
assert_eq!(
observed.lock().expect("observed lock").clone(),
vec![HOST_CONTROL.to_owned()],
"the controller must be performed with the operation's declared control-id",
);
}
#[test]
fn host_control_op_without_controller_fails_closed() -> Result<(), Box<dyn std::error::Error>> {
let mut host = HostBuilder::new()
.mount(control_module())
.expect("mount")
.build()
.expect("build");
let err = match host.invoke("host.reboot", canonical_bytes("go")) {
Ok(_) => {
return Err(std::io::Error::other(
"PROPERTY: a Control op without a bound controller must fail closed",
)
.into())
}
Err(error) => error,
};
assert!(matches!(
err,
RuntimeError::Handler { ref name, .. } if name == "host.reboot"
));
Ok(())
}
#[test]
fn host_control_op_with_rejecting_controller_fails_closed() -> Result<(), Box<dyn std::error::Error>>
{
let observed: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&observed);
let mut host = HostBuilder::new()
.mount(control_module())
.expect("mount")
.host_control(move |control: &str| -> Result<(), HostControlError> {
sink.lock().expect("observed lock").push(control.to_owned());
Err(HostControlError::new("reboot refused"))
})
.build()
.expect("build");
let err = match host.invoke("host.reboot", canonical_bytes("go")) {
Ok(_) => {
return Err(std::io::Error::other(
"PROPERTY: a rejecting controller must fail the handler closed",
)
.into())
}
Err(error) => error,
};
assert!(matches!(
err,
RuntimeError::Handler { ref name, .. } if name == "host.reboot"
));
assert_eq!(
observed.lock().expect("observed lock").clone(),
vec![HOST_CONTROL.to_owned()],
);
Ok(())
}