use std::time::Duration;
use serde_json::json;
use crate::{
ActionContract, BeamModule, BeamSet, ChildContract, Manifest, ManifestVersion, PackageContract,
PackageError, RetryContract, SignalContract, WorkerContract, content_hash_with_contract,
};
fn action(name: &str, field: &str) -> ActionContract {
ActionContract {
name: name.to_owned(),
input_schema: json!({
"required": [field],
"properties": { field: { "type": "string" } },
"type": "object"
}),
output_schema: json!({"type":"boolean"}),
node: Some("shell".to_owned()),
timeout: Some(Duration::from_secs(30)),
retry: Some(RetryContract::Backoff {
count: 2,
min: Duration::from_secs(1),
max: Duration::from_secs(8),
}),
advisory: false,
agent: false,
body: None,
}
}
fn contract(permuted: bool) -> PackageContract {
let mut workers = vec![
WorkerContract {
task_queue: "payments".to_owned(),
actions: vec![action("refund", "refund_id"), action("charge", "amount")],
},
WorkerContract {
task_queue: "mail".to_owned(),
actions: vec![action("send", "address")],
},
];
let mut children = vec![
ChildContract {
name: "receipt".to_owned(),
input_schema: json!({"type":"string"}),
output_schema: json!({"type":"boolean"}),
},
ChildContract {
name: "audit".to_owned(),
input_schema: json!({"type":"integer"}),
output_schema: json!({"type":"null"}),
},
];
let mut signals = vec![
SignalContract {
name: "cancel".to_owned(),
input_schema: json!({"type":"string"}),
},
SignalContract {
name: "approve".to_owned(),
input_schema: json!({"type":"boolean"}),
},
];
if permuted {
workers.reverse();
workers[1].actions.reverse();
children.reverse();
signals.reverse();
}
PackageContract {
input_schema: json!({"required":["order_id","account_id"],"type":"object"}),
output_schema: json!({"enum":["failed","paid"]}),
workers,
children,
signals,
additional_workflows: Vec::new(),
unscoped_activities: Vec::new(),
}
}
fn manifest() -> Manifest {
Manifest {
entry_module: "workflow/order".to_owned(),
entry_function: "run".to_owned(),
input_schema: json!({"type":"object"}),
output_schema: json!({"type":"object"}),
timeout: Some(Duration::from_secs(60)),
activities: Vec::new(),
version: ManifestVersion::new("unstamped"),
format_version: crate::CURRENT_FORMAT_VERSION,
additional_workflows: Vec::new(),
}
}
#[test]
fn v4_identity_is_declaration_order_independent() -> Result<(), PackageError> {
let beams = BeamSet::new(vec![BeamModule::new("workflow/order", vec![1, 2, 3])])?;
let first = contract(false);
let second = contract(true);
assert_eq!(first.canonical_bytes(), second.canonical_bytes());
assert_eq!(
content_hash_with_contract(&beams, &manifest(), &first),
content_hash_with_contract(&beams, &manifest(), &second),
);
Ok(())
}
#[test]
fn v4_identity_ignores_json_object_and_set_array_order() -> Result<(), Box<dyn std::error::Error>> {
let first: serde_json::Value = serde_json::from_str(
r#"{ "type": "object", "required": ["a", "b"], "properties": {"a":{"type":"string"},"b":{"type":"integer"}} }"#,
)?;
let second: serde_json::Value = serde_json::from_str(
r#"{"properties":{"b":{"type":"integer"},"a":{"type":"string"}},"required":["b","a"],"type":"object"}"#,
)?;
let mut left = contract(false);
let mut right = contract(false);
left.input_schema = first;
right.input_schema = second;
assert_eq!(left.canonical_bytes(), right.canonical_bytes());
Ok(())
}
const SUPERSEDED_V4_IDENTITY: &str =
"eef121798ea576fbf700b50d3fd79c3f7a979b5fcc7c85b8a02c1a71cfef1672";
#[test]
fn the_v5_domain_supersedes_every_v4_identity() -> Result<(), PackageError> {
let beams = BeamSet::new(vec![BeamModule::new("workflow/order", vec![1, 2, 3])])?;
let hash = content_hash_with_contract(&beams, &manifest(), &contract(false));
assert_ne!(
hash.to_string(),
SUPERSEDED_V4_IDENTITY,
"a `.v5` identity must never collide with the superseded `.v4` domain"
);
Ok(())
}
#[test]
fn a_declared_body_is_identity_bearing() -> Result<(), PackageError> {
let beams = BeamSet::new(vec![BeamModule::new("workflow/order", vec![1, 2, 3])])?;
let plain = contract(false);
let mut bodied = contract(false);
bodied.workers[0].actions[0].body = Some(crate::ActionBodyContract::Run {
command: "echo $amount".to_owned(),
});
assert_ne!(
plain.canonical_bytes(),
bodied.canonical_bytes(),
"a declared body is executable authority and must be committed to the record"
);
assert_ne!(
content_hash_with_contract(&beams, &manifest(), &plain),
content_hash_with_contract(&beams, &manifest(), &bodied),
"declaring a body changes what the package IS, so it must change identity"
);
Ok(())
}
#[test]
fn editing_a_declared_command_changes_identity() -> Result<(), PackageError> {
let beams = BeamSet::new(vec![BeamModule::new("workflow/order", vec![1, 2, 3])])?;
let mut first = contract(false);
first.workers[0].actions[0].body = Some(crate::ActionBodyContract::Run {
command: "echo safe".to_owned(),
});
let mut second = contract(false);
second.workers[0].actions[0].body = Some(crate::ActionBodyContract::Run {
command: "echo saf3".to_owned(),
});
assert_ne!(
content_hash_with_contract(&beams, &manifest(), &first),
content_hash_with_contract(&beams, &manifest(), &second),
"a rewritten command under an unchanged identity would execute unvouched"
);
Ok(())
}
#[test]
fn advisory_and_body_cannot_be_confused_in_the_record() -> Result<(), PackageError> {
let beams = BeamSet::new(vec![BeamModule::new("workflow/order", vec![1, 2, 3])])?;
let mut corners = Vec::new();
for advisory in [false, true] {
for body in [
None,
Some(crate::ActionBodyContract::Run {
command: "echo x".to_owned(),
}),
] {
let mut candidate = contract(false);
candidate.workers[0].actions[0].advisory = advisory;
candidate.workers[0].actions[0].body = body;
corners.push(content_hash_with_contract(&beams, &manifest(), &candidate));
}
}
for (left_index, left) in corners.iter().enumerate() {
for right in &corners[left_index + 1..] {
assert_ne!(
left, right,
"every advisory/body corner must have its own identity"
);
}
}
Ok(())
}
#[test]
fn advisory_is_identity_bearing() -> Result<(), PackageError> {
let beams = BeamSet::new(vec![BeamModule::new("workflow/order", vec![1, 2, 3])])?;
let plain = contract(false);
let mut advisory = contract(false);
advisory.workers[0].actions[0].advisory = true;
assert_ne!(
plain.canonical_bytes(),
advisory.canonical_bytes(),
"advisory must be committed to the canonical record"
);
assert_ne!(
content_hash_with_contract(&beams, &manifest(), &plain),
content_hash_with_contract(&beams, &manifest(), &advisory),
"flipping an action to advisory changes what the package promises, so it must \
change the package identity"
);
Ok(())
}
#[test]
fn advisory_identity_is_declaration_order_independent() -> Result<(), PackageError> {
let beams = BeamSet::new(vec![BeamModule::new("workflow/order", vec![1, 2, 3])])?;
let mut first = contract(false);
let mut second = contract(true);
mark_advisory(&mut first, "payments", "refund");
mark_advisory(&mut second, "payments", "refund");
assert_eq!(first.canonical_bytes(), second.canonical_bytes());
assert_eq!(
content_hash_with_contract(&beams, &manifest(), &first),
content_hash_with_contract(&beams, &manifest(), &second),
);
Ok(())
}
fn mark_advisory(contract: &mut PackageContract, queue: &str, action: &str) {
for worker in &mut contract.workers {
if worker.task_queue != queue {
continue;
}
for declared in &mut worker.actions {
if declared.name == action {
declared.advisory = true;
}
}
}
}