Skip to main content

kcode_k1_ktool_set_launch_node/
lib.rs

1#![doc = include_str!("../Documentation.md")]
2
3use std::sync::Arc;
4
5use kcode_k1_access_launch_nodes::{
6    AccessContext, AccessId, AccessPolicy, Authority, K1AccessLaunchNodes, ProfileId, TargetId,
7    TargetName, TxId,
8};
9use serde::Deserialize;
10
11const INVALID: &str = "invalid SetLaunchNode arguments";
12
13#[derive(Clone)]
14pub struct SetLaunchNodeKtool {
15    launch_nodes: Arc<K1AccessLaunchNodes>,
16}
17
18impl SetLaunchNodeKtool {
19    pub fn new(launch_nodes: Arc<K1AccessLaunchNodes>) -> Self {
20        Self { launch_nodes }
21    }
22
23    pub fn launch(
24        &self,
25        context: &AccessContext,
26        profile_id: ProfileId,
27        policy: AccessPolicy,
28        arguments: &str,
29    ) -> Result<String, String> {
30        let parsed = parse_arguments(arguments)?;
31        let target_name = TargetName::new(parsed.target).map_err(|_| invalid())?;
32        let node = parsed
33            .node_id
34            .parse::<TxId>()
35            .map(AccessId::new)
36            .map_err(|_| invalid())?;
37        let target = TargetId::new(Authority::User(context.user()), target_name);
38        self.launch_nodes
39            .set(context, profile_id, policy, target, node)?;
40        Ok("success".to_owned())
41    }
42}
43
44#[derive(Deserialize)]
45#[serde(deny_unknown_fields)]
46struct Arguments {
47    target: String,
48    node_id: String,
49}
50
51fn parse_arguments(arguments: &str) -> Result<Arguments, String> {
52    serde_json::from_str(arguments).map_err(|_| invalid())
53}
54
55fn invalid() -> String {
56    INVALID.to_owned()
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use kcode_k1_access::{
63        FilteredAuthorities, K1Access, ModelId, SubsystemId, Target, UserId, ViewerSubject,
64    };
65    use kcode_k1_groups::K1Groups;
66    use kcode_k1_launch_nodes::LaunchNodes;
67    use kcode_k1_peering::K1Peering;
68    use kcode_k1_txn_ordering::K1TxnOrdering;
69
70    fn tx(value: u8) -> TxId {
71        TxId::from_bytes([value; 12])
72    }
73
74    #[test]
75    fn strict_arguments_reject_every_malformed_shape() {
76        let valid = r#"{"target":"Harness","node_id":"000000000000000000000001"}"#;
77        assert!(parse_arguments(valid).is_ok());
78        for arguments in [
79            "",
80            "{}",
81            "[]",
82            r#"{"target":"Harness"}"#,
83            r#"{"node_id":"000000000000000000000001"}"#,
84            r#"{"target":"Harness","node_id":"000000000000000000000001","extra":true}"#,
85        ] {
86            assert_eq!(parse_arguments(arguments).err(), Some(INVALID.to_owned()));
87        }
88    }
89
90    #[test]
91    fn real_stack_create_and_update_use_the_context_user() {
92        let root = tempfile::tempdir().unwrap();
93        let ordering = Arc::new(K1TxnOrdering::open(&root.path().join("ordering")).unwrap());
94        let peering =
95            Arc::new(K1Peering::open(&root.path().join("peering"), ordering.clone()).unwrap());
96        let groups = Arc::new(
97            K1Groups::open(
98                &root.path().join("groups"),
99                ordering.clone(),
100                peering.clone(),
101            )
102            .unwrap(),
103        );
104        let access = Arc::new(
105            K1Access::open(
106                &root.path().join("access"),
107                ordering.clone(),
108                peering.clone(),
109                groups.clone(),
110            )
111            .unwrap(),
112        );
113        let bindings = Arc::new(
114            LaunchNodes::open(&root.path().join("launch-nodes"), ordering, peering).unwrap(),
115        );
116        let facade = Arc::new(K1AccessLaunchNodes::open(access.clone(), groups, bindings).unwrap());
117        let tool = SetLaunchNodeKtool::new(facade.clone());
118
119        let user = UserId::from_tx_id(tx(1));
120        let model = ModelId::from_bytes([2; 32]);
121        let context =
122            AccessContext::new(user, model, FilteredAuthorities::new(Vec::new()).unwrap()).unwrap();
123        let profile = ProfileId::new(tx(3));
124        let policy = AccessPolicy::new(
125            Authority::User(user),
126            Vec::new(),
127            vec![ViewerSubject::Model(model)],
128        )
129        .unwrap();
130        let subsystem = SubsystemId::from_str("k1-kmap").unwrap();
131        let first = access
132            .create(
133                &context,
134                Target::new(subsystem, vec![4; 12]),
135                profile,
136                policy.clone(),
137            )
138            .unwrap()
139            .access_id();
140        let second = access
141            .create(
142                &context,
143                Target::new(subsystem, vec![5; 12]),
144                profile,
145                policy.clone(),
146            )
147            .unwrap()
148            .access_id();
149        let target = TargetId::new(
150            Authority::User(user),
151            TargetName::new("Harness".to_owned()).unwrap(),
152        );
153
154        let first_arguments = format!(r#"{{"target":"Harness","node_id":"{}"}}"#, first.txid());
155        assert_eq!(
156            tool.launch(&context, profile, policy.clone(), &first_arguments),
157            Ok("success".to_owned())
158        );
159        assert_eq!(facade.lookup(&context, &target), Ok(first));
160
161        let second_arguments = format!(r#"{{"target":"Harness","node_id":"{}"}}"#, second.txid());
162        assert_eq!(
163            tool.launch(&context, profile, policy, &second_arguments),
164            Ok("success".to_owned())
165        );
166        assert_eq!(facade.lookup(&context, &target), Ok(second));
167    }
168
169    #[test]
170    fn launch_rejects_invalid_target_and_node_before_mutation() {
171        let invalid_target = Arguments {
172            target: " ".to_owned(),
173            node_id: "000000000000000000000001".to_owned(),
174        };
175        assert!(TargetName::new(invalid_target.target).is_err());
176        assert!("00000000000000000000000A".parse::<TxId>().is_err());
177    }
178
179    #[test]
180    fn complete_package_stays_below_the_managed_limit() {
181        let files = [
182            include_str!("../Cargo.toml"),
183            include_str!("../Documentation.md"),
184            include_str!("lib.rs"),
185        ];
186        let count = files
187            .iter()
188            .flat_map(|file| file.lines())
189            .filter(|line| !line.trim().is_empty())
190            .count();
191        assert!(count < 500, "complete package has {count} nonblank lines");
192    }
193}