use std::os::unix::fs::PermissionsExt;
use std::sync::Arc;
use liminal::channel::ChannelSupervisor;
use serde_json::json;
use super::{DeclaredActs, NEEDS_A_NAME, OpenSiteEffects};
use crate::agents::config::AgentsConfig;
use crate::agents::open_session::OpenSession;
use crate::server::connection::services::SiteDraftHolder;
use crate::services::lifecycle::plan::StepStatus;
use crate::services::lifecycle::site::test_support::{
Issuer, TestResult, limits, open_store, spec,
};
use crate::services::lifecycle::site::{SiteChange, SiteError};
use crate::services::lifecycle::templates::TemplatePolicy;
use crate::services::lifecycle::{Lifecycle, ServiceState};
type Failure = Box<dyn std::error::Error>;
struct Open {
directory: tempfile::TempDir,
root: tempfile::TempDir,
lifecycle: Arc<Lifecycle>,
agents: Arc<crate::agents::runtime::Agents>,
scheduler: ChannelSupervisor,
}
impl Open {
fn new() -> Result<Self, Failure> {
let directory = tempfile::tempdir()?;
let root = tempfile::tempdir()?;
let program = root.path().join("echo");
std::fs::write(&program, b"#!/bin/sh\nexit 0\n")?;
std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o700))?;
let store = open_store(&directory.path().join("store"))?;
let scheduler = ChannelSupervisor::new()?;
let agents = crate::agents::runtime::Agents::open(
store.clone(),
AgentsConfig::default(),
Lifecycle::OPEN_NETWORK.into(),
scheduler.clone(),
"fixture",
None,
None,
)?;
let lifecycle = Lifecycle::open_bounded(
&[],
directory.path().join("services"),
Lifecycle::OPEN_NETWORK.into(),
None,
&limits()?,
)?;
lifecycle.configure_sites(
limits()?,
store,
Some(policy(root.path())?),
Arc::new(OpenSiteEffects::new(Arc::clone(&agents))),
retained(&scheduler),
)?;
Ok(Self {
directory,
root,
lifecycle,
agents,
scheduler,
})
}
fn act(&self, holder: &str, operation: &str, target: &str, change: SiteChange) -> Answer {
DeclaredActs.hold(
&self.lifecycle,
&OpenSession::new(holder.into()),
operation,
target,
change,
10,
)
}
}
type Answer = Result<serde_json::Value, SiteError>;
fn policy(root: &std::path::Path) -> Result<TemplatePolicy, serde_json::Error> {
serde_json::from_value(json!({"roots":[root],"command_bytes":1_048_576,"records":8,
"arguments":32,"environment_names":16,"text_bytes":1024,"stop_timeout_ms":2000}))
}
fn retained(scheduler: &ChannelSupervisor) -> Arc<crate::agents::retained::Retained> {
crate::agents::retained::Retained::new(
AgentsConfig {
max_event_bytes: 65536,
..AgentsConfig::default()
},
scheduler.clone(),
"fixture",
)
}
fn executed(answer: &serde_json::Value, operation: &str) {
assert_eq!(
answer,
&json!({"state":"executed","grant":"open","approved_by":"open:tom",
"responsible":"open:tom","operation":operation})
);
}
#[test]
fn access015_an_open_holder_walks_create_site_through_complete_as_its_own_act() -> TestResult {
let open = Open::new()?;
executed(
&open.act("tom", "op-create", "echo", SiteChange::CreateSite(spec()))?,
"op-create",
);
assert!(
open.agents
.registry()
.place_own("builder", "open:builder", 1)?
);
let bind = SiteChange::BindBuilder {
agent: "builder".into(),
};
executed(&open.act("tom", "op-bind", "echo", bind)?, "op-bind");
open.lifecycle
.plan_set("echo", "builder", &["build the echo".into()], 20)?;
open.lifecycle
.plan_step("echo", "builder", 1, StepStatus::Done, None, 21)?;
let proposal = open.lifecycle.propose_template(
"echo",
&open.root.path().join("echo"),
Vec::new(),
Vec::new(),
1000,
)?;
let name = proposal.name.clone();
let template = SiteChange::CreateTemplate(proposal);
executed(
&open.act("tom", "op-template", &name, template)?,
"op-template",
);
executed(
&open.act("tom", "op-complete", "echo", SiteChange::Complete)?,
"op-complete",
);
let record = open.lifecycle.record("echo")?;
assert_eq!(record.approved_by, vec!["open:tom".to_owned()]);
assert_eq!(record.responsible.as_deref(), Some("open:tom"));
assert_eq!(record.created_by, "open:tom");
assert!(!record.placed);
assert_eq!(record.builder.as_deref(), Some("builder"));
let grants = record.builder_grants.as_ref().ok_or("no builder grants")?;
assert_eq!(
(grants.plan.as_str(), grants.serve.as_str()),
("open", "open")
);
assert_eq!(record.template.as_deref(), Some(name.as_str()));
assert_eq!(record.state, ServiceState::Asleep);
assert_eq!(record.approvals[0].draft, "open:tom:op-create");
let Open {
directory,
root,
lifecycle,
agents,
scheduler,
} = open;
drop(lifecycle);
drop(agents);
let reopened = Lifecycle::open_bounded(
&[],
directory.path().join("services"),
Lifecycle::OPEN_NETWORK.into(),
None,
&limits()?,
)?;
reopened.configure_sites(
limits()?,
open_store(&directory.path().join("store"))?,
Some(policy(root.path())?),
Arc::new(Issuer::default()),
retained(&scheduler),
)?;
assert_eq!(reopened.record("echo")?, record);
scheduler.shutdown();
Ok(())
}
#[test]
fn access015_the_anonymous_seat_acts_for_no_one_and_a_builder_needs_a_station() -> TestResult {
let open = Open::new()?;
let refused = open
.act("anonymous", "op-1", "echo", SiteChange::CreateSite(spec()))
.err()
.ok_or("the anonymous seat created a site")?;
assert_eq!(refused.to_string(), NEEDS_A_NAME);
assert!(open.lifecycle.record("echo").is_err());
open.act("tom", "op-2", "echo", SiteChange::CreateSite(spec()))?;
let bind = SiteChange::BindBuilder {
agent: "stranger".into(),
};
let refused = open
.act("tom", "op-3", "echo", bind)
.err()
.ok_or("a builder with no station was bound")?;
assert_eq!(
refused.to_string(),
"builder stranger has no station on this exchange; its seat listens to its inbox \
once to place it"
);
assert!(!open.lifecycle.record("echo")?.builder_bound);
open.scheduler.shutdown();
Ok(())
}
fn registration() -> liminal::protocol::WorkerRegistration {
liminal::protocol::WorkerRegistration {
namespaces: vec!["echo.request".into()],
task_queue: "echo.request".into(),
node: None,
activity_types: vec!["echo.request".into()],
identity: "builder".into(),
activities: vec![liminal::protocol::WorkerActivityDescriptor {
name: "echo.request".into(),
input_schema_json: "{\"type\":\"object\"}".into(),
output_schema_json: "{\"type\":\"object\"}".into(),
}],
}
}
#[test]
fn access015_an_open_builder_registers_its_trial_and_the_answerable_holder_calls_it() -> TestResult
{
let open = Open::new()?;
open.act("tom", "op-1", "echo", SiteChange::CreateSite(spec()))?;
open.agents
.registry()
.place_own("builder", "open:builder", 1)?;
let bind = SiteChange::BindBuilder {
agent: "builder".into(),
};
open.act("tom", "op-2", "echo", bind)?;
let other = OpenSession::new("other".into());
let refused = open
.lifecycle
.prepare_trial(7, Some(&other), ®istration(), None, 30)
.err()
.ok_or("another open seat registered the trial")?;
assert_eq!(
refused.to_string(),
"pass holder other is not the builder of echo"
);
let builder = OpenSession::new("builder".into());
let permit = open
.lifecycle
.prepare_trial(8, Some(&builder), ®istration(), None, 30)?;
assert!(permit.is_some());
let tom = OpenSession::new("tom".into());
assert!(
open.lifecycle
.admit_trial_call(Some(&tom), "echo.request", 30)?
);
let refused = open
.lifecycle
.admit_trial_call(Some(&other), "echo.request", 30)
.err()
.ok_or("a holder not answerable called the trial")?;
assert_eq!(refused.to_string(), "service echo is a trial");
drop(permit);
open.scheduler.shutdown();
Ok(())
}
#[test]
fn access015_only_a_named_open_seat_or_an_agent_builds_and_answers_as_records_name_it() {
use super::{answerable, builds};
let named = OpenSession::new("tom".into());
let anonymous = OpenSession::new("anonymous".into());
assert!(builds(&named) && !builds(&anonymous));
assert_eq!(answerable(&named).as_deref(), Some("open:tom"));
assert_eq!(answerable(&anonymous), None);
}
#[test]
fn access015_a_gated_installation_with_no_placer_still_refuses_to_create_a_site() -> TestResult {
let directory = tempfile::tempdir()?;
let scheduler = ChannelSupervisor::new()?;
let lifecycle = Lifecycle::open_bounded(
&[],
directory.path().join("services"),
"net".into(),
None,
&limits()?,
)?;
lifecycle.configure_sites(
limits()?,
open_store(&directory.path().join("store"))?,
None,
Arc::new(Issuer::default()),
retained(&scheduler),
)?;
let act = crate::services::lifecycle::site::SiteAct {
target: "echo".into(),
holder: "person-owner".into(),
responsible: "person-owner".into(),
change: SiteChange::CreateSite(spec()),
};
let refused = lifecycle
.apply_outright(&act, "grant-1", "op-1", "words", 10)
.err()
.ok_or("a gated site was created with no placer")?;
assert_eq!(
refused.to_string(),
crate::services::lifecycle::LifecycleError::NoPlacer.to_string()
);
assert!(lifecycle.record("echo").is_err());
scheduler.shutdown();
Ok(())
}