kcode-k1-ktool-set-launch-node 0.1.1

Standalone SetLaunchNode Ktool operation for K1
Documentation
#![doc = include_str!("../Documentation.md")]

use std::sync::Arc;

use kcode_k1_access_launch_nodes::{
    AccessContext, AccessId, AccessPolicy, Authority, K1AccessLaunchNodes, ProfileId, TargetId,
    TargetName, TxId,
};
use serde::Deserialize;

const INVALID: &str = "invalid SetLaunchNode arguments";

#[derive(Clone)]
pub struct SetLaunchNodeKtool {
    launch_nodes: Arc<K1AccessLaunchNodes>,
}

impl SetLaunchNodeKtool {
    pub fn new(launch_nodes: Arc<K1AccessLaunchNodes>) -> Self {
        Self { launch_nodes }
    }

    pub fn launch(
        &self,
        context: &AccessContext,
        profile_id: ProfileId,
        policy: AccessPolicy,
        arguments: &str,
    ) -> Result<String, String> {
        let parsed = parse_arguments(arguments)?;
        let target_name = TargetName::new(parsed.target).map_err(|_| invalid())?;
        let node = parsed
            .node_id
            .parse::<TxId>()
            .map(AccessId::new)
            .map_err(|_| invalid())?;
        let target = TargetId::new(Authority::User(context.user()), target_name);
        self.launch_nodes
            .set(context, profile_id, policy, target, node)?;
        Ok("success".to_owned())
    }
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Arguments {
    target: String,
    node_id: String,
}

fn parse_arguments(arguments: &str) -> Result<Arguments, String> {
    serde_json::from_str(arguments).map_err(|_| invalid())
}

fn invalid() -> String {
    INVALID.to_owned()
}

#[cfg(test)]
mod tests {
    use super::*;
    use kcode_k1_access::{
        FilteredAuthorities, K1Access, ModelId, SubsystemId, Target, UserId, ViewerSubject,
    };
    use kcode_k1_groups::K1Groups;
    use kcode_k1_launch_nodes::LaunchNodes;
    use kcode_k1_peering::K1Peering;
    use kcode_k1_txn_ordering::K1TxnOrdering;

    fn tx(value: u8) -> TxId {
        TxId::from_bytes([value; 12])
    }

    #[test]
    fn strict_arguments_reject_every_malformed_shape() {
        let valid = r#"{"target":"Harness","node_id":"000000000000000000000001"}"#;
        assert!(parse_arguments(valid).is_ok());
        for arguments in [
            "",
            "{}",
            "[]",
            r#"{"target":"Harness"}"#,
            r#"{"node_id":"000000000000000000000001"}"#,
            r#"{"target":"Harness","node_id":"000000000000000000000001","extra":true}"#,
        ] {
            assert_eq!(parse_arguments(arguments).err(), Some(INVALID.to_owned()));
        }
    }

    #[test]
    fn real_stack_create_and_update_use_the_context_user() {
        let root = tempfile::tempdir().unwrap();
        let ordering = Arc::new(K1TxnOrdering::open(&root.path().join("ordering")).unwrap());
        let peering =
            Arc::new(K1Peering::open(&root.path().join("peering"), ordering.clone()).unwrap());
        let groups = Arc::new(
            K1Groups::open(
                &root.path().join("groups"),
                ordering.clone(),
                peering.clone(),
            )
            .unwrap(),
        );
        let access = Arc::new(
            K1Access::open(
                &root.path().join("access"),
                ordering.clone(),
                peering.clone(),
                groups.clone(),
            )
            .unwrap(),
        );
        let bindings = Arc::new(
            LaunchNodes::open(&root.path().join("launch-nodes"), ordering, peering).unwrap(),
        );
        let facade = Arc::new(K1AccessLaunchNodes::open(access.clone(), groups, bindings).unwrap());
        let tool = SetLaunchNodeKtool::new(facade.clone());

        let user = UserId::from_tx_id(tx(1));
        let model = ModelId::from_bytes([2; 32]);
        let context =
            AccessContext::new(user, model, FilteredAuthorities::new(Vec::new()).unwrap()).unwrap();
        let profile = ProfileId::new(tx(3));
        let policy = AccessPolicy::new(
            Authority::User(user),
            Vec::new(),
            vec![ViewerSubject::Model(model)],
        )
        .unwrap();
        let subsystem = SubsystemId::from_str("k1-kmap").unwrap();
        let first = access
            .create(
                &context,
                Target::new(subsystem, vec![4; 12]),
                profile,
                policy.clone(),
            )
            .unwrap()
            .access_id();
        let second = access
            .create(
                &context,
                Target::new(subsystem, vec![5; 12]),
                profile,
                policy.clone(),
            )
            .unwrap()
            .access_id();
        let target = TargetId::new(
            Authority::User(user),
            TargetName::new("Harness".to_owned()).unwrap(),
        );

        let first_arguments = format!(r#"{{"target":"Harness","node_id":"{}"}}"#, first.txid());
        assert_eq!(
            tool.launch(&context, profile, policy.clone(), &first_arguments),
            Ok("success".to_owned())
        );
        assert_eq!(facade.lookup(&context, &target), Ok(first));

        let second_arguments = format!(r#"{{"target":"Harness","node_id":"{}"}}"#, second.txid());
        assert_eq!(
            tool.launch(&context, profile, policy, &second_arguments),
            Ok("success".to_owned())
        );
        assert_eq!(facade.lookup(&context, &target), Ok(second));
    }

    #[test]
    fn launch_rejects_invalid_target_and_node_before_mutation() {
        let invalid_target = Arguments {
            target: " ".to_owned(),
            node_id: "000000000000000000000001".to_owned(),
        };
        assert!(TargetName::new(invalid_target.target).is_err());
        assert!("00000000000000000000000A".parse::<TxId>().is_err());
    }

    #[test]
    fn complete_package_stays_below_the_managed_limit() {
        let files = [
            include_str!("../Cargo.toml"),
            include_str!("../Documentation.md"),
            include_str!("lib.rs"),
        ];
        let count = files
            .iter()
            .flat_map(|file| file.lines())
            .filter(|line| !line.trim().is_empty())
            .count();
        assert!(count < 500, "complete package has {count} nonblank lines");
    }
}