use std::sync::Arc;
use super::tests::{FakeLys, scratch, template};
use super::{Lifecycle, LifecycleError, ServiceState, digest};
fn placed_kinds(lys: &FakeLys) -> Vec<(String, String, String)> {
lys.placed.lock().map(|p| p.clone()).unwrap_or_default()
}
fn records(lifecycle: &Lifecycle) -> Result<Vec<super::ServiceRecord>, LifecycleError> {
Ok(lifecycle.lock()?.records.values().cloned().collect())
}
fn operation(kind: &str, id: &str) -> String {
format!("op-{}", digest(&["liminal-place/v1", kind, id]))
}
#[test]
fn access014_a_record_created_without_a_placer_is_placed_at_catch_up_once()
-> Result<(), LifecycleError> {
let dir = scratch("access014-catch-up");
let open = Lifecycle::open(
&[template("true")],
dir.clone(),
Lifecycle::OPEN_NETWORK.to_owned(),
None,
)?;
let record = open.create("echo", "one", "open:hermes", "op-1", 10)?;
assert!(!record.placed, "no placer, so not placed");
assert!(open.catch_up().is_empty(), "no placer places nothing");
drop(open);
let lys = Arc::new(FakeLys::default());
let gated = Lifecycle::open(
&[template("true")],
dir.clone(),
"net".to_owned(),
Some(Arc::clone(&lys) as Arc<dyn super::Placer>),
)?;
assert_eq!(gated.catch_up(), Vec::<(String, String)>::new());
assert_eq!(
placed_kinds(&lys),
vec![
(
operation("liminal.service", &record.id),
"liminal.service".to_owned(),
record.id.clone()
),
(
operation("liminal.subject", "echo.one"),
"liminal.subject".to_owned(),
"echo.one".to_owned()
),
],
"the service, then its subject, with the create digests"
);
assert_eq!(gated.catch_up(), Vec::<(String, String)>::new());
assert_eq!(
placed_kinds(&lys).len(),
2,
"a second catch-up places nothing"
);
drop(gated);
let reopened = Lifecycle::open(
&[template("true")],
dir,
"net".to_owned(),
Some(lys.clone()),
)?;
let kept = records(&reopened)?;
assert_eq!(kept.len(), 1);
assert!(kept[0].placed, "the mark survives a reopen");
assert_eq!(reopened.catch_up(), Vec::<(String, String)>::new());
assert_eq!(placed_kinds(&lys).len(), 2);
Ok(())
}
#[test]
fn access014_a_refused_child_leaves_the_record_unplaced_and_is_named() -> Result<(), LifecycleError>
{
let dir = scratch("access014-refused");
let open = Lifecycle::open(
&[template("true")],
dir.clone(),
Lifecycle::OPEN_NETWORK.to_owned(),
None,
)?;
let record = open.create("echo", "one", "open:hermes", "op-1", 10)?;
drop(open);
let lys = Arc::new(FakeLys {
refuse: Some("liminal.subject"),
..FakeLys::default()
});
let gated = Lifecycle::open(
&[template("true")],
dir,
"net".to_owned(),
Some(lys.clone()),
)?;
assert_eq!(
gated.catch_up(),
vec![(
record.id,
"placement_refused: WithheldFromAgents".to_owned()
)]
);
assert_eq!(
placed_kinds(&lys).len(),
1,
"only the service child was placed"
);
assert!(!records(&gated)?[0].placed);
Ok(())
}
#[test]
fn access014_a_record_placed_at_create_is_marked_and_a_retired_one_is_skipped()
-> Result<(), LifecycleError> {
let dir = scratch("access014-marked");
std::fs::create_dir_all(&dir).map_err(|error| LifecycleError::Store(error.to_string()))?;
let legacy = serde_json::json!({
"id": "svc-legacy", "name": "old", "template": "echo", "subjects": ["echo.old"],
"purpose": null, "contracts": {}, "builder": null, "builder_grants": null,
"builder_bound": false, "approved_by": [], "responsible": null,
"plan": {"steps": [], "current": null, "revision": 0, "transitions": []},
"approvals": [], "state": "retired", "created_by": "agent-z", "transitions": []
});
std::fs::write(
dir.join("svc-legacy.json"),
serde_json::to_vec(&legacy).map_err(|error| LifecycleError::Store(error.to_string()))?,
)
.map_err(|error| LifecycleError::Store(error.to_string()))?;
let lys = Arc::new(FakeLys::default());
let gated = Lifecycle::open(
&[template("true")],
dir,
"net".to_owned(),
Some(lys.clone()),
)?;
let record = gated.create("echo", "one", "agent-a", "op-1", 10)?;
assert!(record.placed, "placed through the placer at create");
assert_eq!(placed_kinds(&lys).len(), 2);
let legacy = records(&gated)?
.into_iter()
.find(|record| record.id == "svc-legacy")
.ok_or_else(|| LifecycleError::UnknownService("svc-legacy".into()))?;
assert!(!legacy.placed, "a snapshot without the member reads false");
assert_eq!(legacy.state, ServiceState::Retired);
assert_eq!(gated.catch_up(), Vec::<(String, String)>::new());
assert_eq!(
placed_kinds(&lys).len(),
2,
"the retired record is skipped, the marked one too"
);
Ok(())
}