liminal-server 0.27.1

Standalone server for the liminal messaging bus
Documentation
//! ACCESS-014 R1: a record created before its exchange had a placer is placed
//! after listening, once, and marked; refusals are answered by name.
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()))?;
    // A record written before this change: no `placed` member, retired.
    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(())
}