//! 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" ); }