Files
sausage/host/tests/cred_replay.rs
T

416 lines
11 KiB
Rust

//! v2 keyed replay compares intent before current item validation or edit planning.
use sausage::cred::{
edits::*,
items::*,
requests::{GrantRequest, Overrides},
types::FieldType,
*,
};
fn fixture() -> (tempfile::TempDir, Cred, Caller, Item) {
let dir = tempfile::tempdir().unwrap();
let cred = Cred::open(dir.path()).unwrap();
let caller = Caller {
pid: 1,
program: "/bin/tool".into(),
conversations: vec!["root".into()],
};
let item = cred
.create_item(
Item {
id: String::new(),
name: "item".into(),
version: 0,
free_use: false,
rules: vec![],
validate_js: None,
transform_js: String::new(),
fields: vec![Field {
field_id: String::new(),
name: "token".into(),
public: false,
value: None,
needs_value: true,
field_type: FieldType::Text,
key_type: None,
}],
},
&caller,
cred.unlocked().unwrap(),
)
.unwrap();
(dir, cred, caller, item)
}
#[test]
fn approved_operations_and_deletes_replay() {
let (_dir, cred, caller, item) = fixture();
let request = OperationRequest {
item: Some(item.id.clone()),
base_version: Some(item.version),
change: vec![Operation::SetName {
name: "renamed".into(),
}],
};
let id = cred
.file_operation_request(&caller, "edit", request.clone())
.unwrap();
cred.approve(
&id,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
cred.unlocked().unwrap(),
)
.unwrap();
assert_eq!(
cred.file_operation_request(&caller, "edit", request)
.unwrap(),
id
);
let current = cred.item_get(&item.id).unwrap();
let delete = Delete {
item: item.id,
base_version: current.version,
};
let id = cred
.file_delete_request(&caller, "delete", delete.clone())
.unwrap();
cred.approve(
&id,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
cred.unlocked().unwrap(),
)
.unwrap();
assert_eq!(
cred.file_delete_request(&caller, "delete", delete).unwrap(),
id
);
}
#[test]
fn approved_replacements_and_creations_replay() {
let (_dir, cred, caller, item) = fixture();
let mut config = item.clone();
config.name = "renamed".into();
let edit = Edit {
item: item.id,
base_version: item.version,
config: config.clone(),
};
let id = cred
.file_edit_request(&caller, "replace", edit.clone())
.unwrap();
cred.approve(
&id,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
cred.unlocked().unwrap(),
)
.unwrap();
assert_eq!(
cred.file_edit_request(&caller, "replace", edit).unwrap(),
id
);
config.name = "new".into();
let request = OperationRequest {
item: None,
base_version: None,
change: vec![Operation::CreateItem { config }],
};
let id = cred
.file_operation_request(&caller, "create", request.clone())
.unwrap();
cred.approve(
&id,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
cred.unlocked().unwrap(),
)
.unwrap();
assert_eq!(
cred.file_operation_request(&caller, "create", request)
.unwrap(),
id
);
assert_eq!(cred.item_list().unwrap().len(), 2);
}
#[test]
fn grant_defaults_normalize_and_pending_requests_replay_after_deletion() {
let (_dir, cred, caller, item) = fixture();
let payload = GrantRequest {
items: vec![item.id.clone()],
duration_secs: None,
max_uses: None,
validator: Some(String::new()),
};
let id = cred.file_request(&caller, "grant", payload).unwrap();
let payload = GrantRequest {
items: vec![item.id.clone()],
duration_secs: Some(3600),
max_uses: None,
validator: None,
};
assert_eq!(
cred.file_request(&caller, "grant", payload.clone())
.unwrap(),
id
);
let delete = cred
.file_delete_request(
&caller,
"delete",
Delete {
item: item.id,
base_version: item.version,
},
)
.unwrap();
cred.approve(
&delete,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
cred.unlocked().unwrap(),
)
.unwrap();
assert_eq!(cred.file_request(&caller, "grant", payload).unwrap(), id);
assert!(
cred.approve(
&id,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
cred.unlocked().unwrap()
)
.is_err()
);
assert_eq!(
cred.request_list()
.unwrap()
.iter()
.find(|r| r.id == id)
.unwrap()
.state,
"pending"
);
}
#[test]
fn values_are_checked_at_each_public_operation() {
let (_dir, cred, caller, item) = fixture();
let mut config = item.clone();
config.fields.push(Field {
field_id: String::new(),
name: "public".into(),
public: true,
value: Some("old".into()),
needs_value: false,
field_type: FieldType::Text,
key_type: None,
});
let id = cred
.file_edit_request(
&caller,
"setup",
Edit {
item: item.id.clone(),
base_version: item.version,
config,
},
)
.unwrap();
cred.approve(
&id,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
cred.unlocked().unwrap(),
)
.unwrap();
let current = cred.item_get(&item.id).unwrap();
let field = current
.fields
.iter()
.find(|f| f.name == "public")
.unwrap()
.field_id
.clone();
let mut request = OperationRequest {
item: Some(item.id.clone()),
base_version: Some(current.version),
change: vec![
Operation::SetFieldValue {
field: field.clone(),
value: "bad\nvalue".into(),
},
Operation::SetFieldType {
field,
field_type: FieldType::Note,
key_type: None,
},
],
};
assert_eq!(
cred.file_operation_request(&caller, "invalid", request.clone())
.err()
.unwrap()
.0,
"invalid_value"
);
assert_eq!(cred.item_get(&item.id).unwrap().version, current.version);
request.change.reverse();
let id = cred
.file_operation_request(&caller, "ordered", request)
.unwrap();
cred.approve(
&id,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
cred.unlocked().unwrap(),
)
.unwrap();
assert_eq!(
cred.item_get(&item.id).unwrap().version,
current.version + 1
);
}
#[test]
fn secret_retyping_checks_each_operation_only_on_approval() {
let (_dir, cred, caller, item) = fixture();
let field = &item.fields[0].field_id;
cred.set_field_input(
&item.id,
field,
&FieldInput {
value: sausage::cred::items::FieldValue::Plain("VALUE_SENTINEL\nTAIL_SENTINEL"),
field_type: Some(FieldType::Note),
key_type: None,
},
&caller,
cred.unlocked().unwrap(),
)
.unwrap();
let current = cred.item_get(&item.id).unwrap();
let request = OperationRequest {
item: Some(item.id.clone()),
base_version: Some(current.version),
change: vec![
Operation::SetFieldType {
field: field.clone(),
field_type: FieldType::Text,
key_type: None,
},
Operation::SetFieldType {
field: field.clone(),
field_type: FieldType::Note,
key_type: None,
},
],
};
let id = cred
.file_operation_request(&caller, "types", request)
.unwrap();
assert!(
!cred.request_list().unwrap()[0]
.payload
.to_string()
.contains("VALUE_SENTINEL")
);
assert_eq!(
cred.approve(
&id,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
cred.unlocked().unwrap()
)
.err()
.unwrap()
.0,
"invalid_value"
);
assert_eq!(cred.item_get(&item.id).unwrap().version, current.version);
assert_eq!(cred.request_list().unwrap()[0].state, "pending");
let history = cred.item_versions(&item.id).unwrap().len();
cred.set_field(
&item.id,
field,
"single-line",
&caller,
cred.unlocked().unwrap(),
)
.unwrap();
let next = cred.item_get(&item.id).unwrap();
assert_eq!(next.version, current.version + 1);
assert_eq!(cred.item_versions(&item.id).unwrap().len(), history + 1);
cred.resolve_stale(&id, next.version, &caller, cred.unlocked().unwrap())
.unwrap();
cred.approve(
&id,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
cred.unlocked().unwrap(),
)
.unwrap();
}
#[test]
fn stale_rebases_keep_original_keyed_intent() {
let (_dir, cred, caller, item) = fixture();
let request = OperationRequest {
item: Some(item.id.clone()),
base_version: Some(item.version),
change: vec![Operation::SetName {
name: "renamed".into(),
}],
};
let id = cred
.file_operation_request(&caller, "rebase", request.clone())
.unwrap();
cred.set_field(
&item.id,
&item.fields[0].field_id,
"opaque",
&caller,
cred.unlocked().unwrap(),
)
.unwrap();
let current = cred.item_get(&item.id).unwrap();
cred.resolve_stale(&id, current.version, &caller, cred.unlocked().unwrap())
.unwrap();
assert_eq!(
cred.file_operation_request(&caller, "rebase", request.clone())
.unwrap(),
id
);
cred.approve(
&id,
sausage::cred::webauthn::Decision::Approve,
Overrides::default(),
cred.unlocked().unwrap(),
)
.unwrap();
assert_eq!(
cred.file_operation_request(&caller, "rebase", request)
.unwrap(),
id
);
assert!(!cred.item_get(&item.id).unwrap().fields[0].needs_value);
}
#[test]
fn operation_order_is_part_of_key_identity() {
let (_dir, cred, caller, item) = fixture();
let mut request = OperationRequest {
item: Some(item.id),
base_version: Some(item.version),
change: vec![
Operation::SetName {
name: "renamed".into(),
},
Operation::SetFreeUse { enabled: true },
],
};
cred.file_operation_request(&caller, "ordered", request.clone())
.unwrap();
request.change.reverse();
assert_eq!(
cred.file_operation_request(&caller, "ordered", request)
.err()
.unwrap()
.0,
"key_conflict"
);
}