signer_runtime_modes.rs (84034B)
1 mod support; 2 3 use std::fs; 4 use std::path::Path; 5 6 use serde_json::Value; 7 use support::{ 8 RadrootsCliSandbox, assert_contains, assert_no_daemon_runtime_reference, 9 assert_no_removed_command_reference, create_listing_draft, identity_public, identity_secret, 10 json_from_stdout, make_listing_publishable, seed_orderable_listing, shell_single_quoted, 11 toml_string, write_public_identity_profile, write_secret_identity_profile, 12 }; 13 14 const LISTING_ADDR: &str = 15 "30402:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg"; 16 17 #[test] 18 fn local_signer_status_reports_unconfigured_without_account() { 19 let sandbox = RadrootsCliSandbox::new(); 20 21 let value = sandbox.json_success(&["--format", "json", "signer", "status", "get"]); 22 23 assert_eq!(value["schema_version"], "radroots.cli.output.v1"); 24 assert_eq!(value["operation_id"], "signer.status.get"); 25 assert_eq!(value["kind"], "signer.status.get"); 26 assert_eq!(value["result"]["mode"], "local"); 27 assert_eq!(value["result"]["state"], "unconfigured"); 28 assert_eq!( 29 value["result"]["signer_account_id"], 30 serde_json::Value::Null 31 ); 32 assert_eq!(value["result"]["binding"]["state"], "disabled"); 33 assert_eq!(value["result"]["local"], serde_json::Value::Null); 34 assert_eq!(value["errors"].as_array().expect("errors").len(), 0); 35 } 36 37 #[test] 38 fn local_signer_status_reports_ready_after_account_create() { 39 let sandbox = RadrootsCliSandbox::new(); 40 41 let created = sandbox.json_success(&["--format", "json", "account", "create"]); 42 assert_eq!(created["operation_id"], "account.create"); 43 assert_eq!(created["result"]["state"], "created"); 44 assert_eq!(created["result"]["account"]["signer"], "local"); 45 assert_eq!(created["result"]["account"]["custody"], "secret_backed"); 46 assert_eq!(created["result"]["account"]["write_capable"], true); 47 assert_eq!(created["result"]["account"]["is_default"], true); 48 let account_id = created["result"]["account"]["id"] 49 .as_str() 50 .expect("created account id"); 51 52 let status = sandbox.json_success(&["--format", "json", "signer", "status", "get"]); 53 54 assert_eq!(status["operation_id"], "signer.status.get"); 55 assert_eq!(status["result"]["mode"], "local"); 56 assert_eq!(status["result"]["state"], "ready"); 57 assert_eq!(status["result"]["signer_account_id"], account_id); 58 assert_eq!(status["result"]["local"]["account_id"], account_id); 59 assert_eq!(status["result"]["local"]["availability"], "secret_backed"); 60 assert_eq!(status["result"]["local"]["secret_backed"], true); 61 assert_eq!(status["result"]["local"]["backend"], "encrypted_file"); 62 assert_eq!(status["result"]["local"]["used_fallback"], false); 63 assert_eq!(status["result"]["binding"]["state"], "disabled"); 64 } 65 66 #[test] 67 fn local_account_selection_and_invocation_override_resolve_signer_actor() { 68 let sandbox = RadrootsCliSandbox::new(); 69 70 let first = sandbox.json_success(&["--format", "json", "account", "create"]); 71 let second = sandbox.json_success(&["--format", "json", "account", "create"]); 72 let first_account_id = first["result"]["account"]["id"] 73 .as_str() 74 .expect("first account id"); 75 let second_account_id = second["result"]["account"]["id"] 76 .as_str() 77 .expect("second account id"); 78 assert_eq!(first["result"]["account"]["is_default"], true); 79 assert_eq!(second["result"]["account"]["is_default"], false); 80 81 let default_status = sandbox.json_success(&["--format", "json", "signer", "status", "get"]); 82 assert_eq!(default_status["result"]["state"], "ready"); 83 assert_eq!( 84 default_status["result"]["signer_account_id"], 85 first_account_id 86 ); 87 assert_eq!( 88 default_status["result"]["account_resolution"]["source"], 89 "default_account" 90 ); 91 92 let override_status = sandbox.json_success(&[ 93 "--format", 94 "json", 95 "--account-id", 96 second_account_id, 97 "signer", 98 "status", 99 "get", 100 ]); 101 assert_eq!(override_status["actor"]["account_id"], second_account_id); 102 assert_eq!(override_status["actor"]["role"], "account"); 103 assert_eq!( 104 override_status["result"]["signer_account_id"], 105 second_account_id 106 ); 107 assert_eq!( 108 override_status["result"]["account_resolution"]["source"], 109 "invocation_override" 110 ); 111 assert_eq!( 112 override_status["result"]["account_resolution"]["default_account"]["id"], 113 first_account_id 114 ); 115 116 let selected = sandbox.json_success(&[ 117 "--format", 118 "json", 119 "account", 120 "selection", 121 "update", 122 second_account_id, 123 ]); 124 assert_eq!(selected["operation_id"], "account.selection.update"); 125 assert_eq!(selected["result"]["state"], "default"); 126 assert_eq!(selected["result"]["account"]["id"], second_account_id); 127 128 let selected_status = sandbox.json_success(&["--format", "json", "signer", "status", "get"]); 129 assert_eq!( 130 selected_status["result"]["signer_account_id"], 131 second_account_id 132 ); 133 assert_eq!( 134 selected_status["result"]["account_resolution"]["source"], 135 "default_account" 136 ); 137 138 let selected_get = 139 sandbox.json_success(&["--format", "json", "account", "get", first_account_id]); 140 assert_eq!(selected_get["operation_id"], "account.get"); 141 assert_eq!( 142 selected_get["result"]["account_resolution"]["source"], 143 "invocation_override" 144 ); 145 assert_eq!( 146 selected_get["result"]["account_resolution"]["resolved_account"]["id"], 147 first_account_id 148 ); 149 } 150 151 #[test] 152 fn account_import_dry_run_validates_profile_without_mutating_store() { 153 let sandbox = RadrootsCliSandbox::new(); 154 let public_identity = identity_public(21); 155 let public_identity_file = 156 write_public_identity_profile(&sandbox, "dry-run-import", &public_identity); 157 158 let value = sandbox.json_success(&[ 159 "--format", 160 "json", 161 "--dry-run", 162 "account", 163 "import", 164 "--default", 165 public_identity_file.to_string_lossy().as_ref(), 166 ]); 167 168 assert_eq!(value["operation_id"], "account.import"); 169 assert_eq!(value["dry_run"], true); 170 assert_eq!(value["result"]["state"], "dry_run"); 171 assert_eq!( 172 value["result"]["account"]["id"], 173 public_identity.id.as_str() 174 ); 175 assert_eq!(value["result"]["account"]["signer"], "watch_only"); 176 assert_eq!(value["result"]["account"]["custody"], "watch_only"); 177 assert_eq!(value["result"]["account"]["write_capable"], false); 178 assert_eq!(value["result"]["account"]["is_default"], true); 179 180 let list = sandbox.json_success(&["--format", "json", "account", "list"]); 181 assert_eq!(list["result"]["count"], 0); 182 } 183 184 #[test] 185 fn account_import_dry_run_validates_missing_profile_file() { 186 let sandbox = RadrootsCliSandbox::new(); 187 let missing = sandbox.root().join("missing-account.json"); 188 189 let (output, value) = sandbox.json_output(&[ 190 "--format", 191 "json", 192 "--dry-run", 193 "account", 194 "import", 195 missing.to_string_lossy().as_ref(), 196 ]); 197 198 assert!(!output.status.success()); 199 assert_eq!(value["operation_id"], "account.import"); 200 assert_eq!(value["errors"][0]["code"], "not_found"); 201 assert_eq!(value["errors"][0]["exit_code"], 4); 202 } 203 204 #[test] 205 fn account_attach_secret_dry_run_validates_without_mutating_store() { 206 let sandbox = RadrootsCliSandbox::new(); 207 let default_account = sandbox.json_success(&["--format", "json", "account", "create"]); 208 let default_account_id = default_account["result"]["account"]["id"] 209 .as_str() 210 .expect("default account id"); 211 let identity = identity_secret(31); 212 let public_identity = identity.to_public(); 213 let public_identity_file = 214 write_public_identity_profile(&sandbox, "attach-dry-public", &public_identity); 215 let secret_identity_file = 216 write_secret_identity_profile(&sandbox, "attach-dry-secret", &identity); 217 let imported = sandbox.json_success(&[ 218 "--format", 219 "json", 220 "--approval-token", 221 "approve", 222 "account", 223 "import", 224 public_identity_file.to_string_lossy().as_ref(), 225 ]); 226 let watch_account_id = imported["result"]["account"]["id"] 227 .as_str() 228 .expect("watch account id"); 229 assert_eq!(imported["result"]["account"]["signer"], "watch_only"); 230 assert_eq!(imported["result"]["account"]["custody"], "watch_only"); 231 assert_eq!(imported["result"]["account"]["write_capable"], false); 232 assert_eq!(imported["result"]["account"]["is_default"], false); 233 234 let value = sandbox.json_success(&[ 235 "--format", 236 "json", 237 "--dry-run", 238 "account", 239 "attach-secret", 240 watch_account_id, 241 secret_identity_file.to_string_lossy().as_ref(), 242 "--default", 243 ]); 244 245 assert_eq!(value["operation_id"], "account.attach_secret"); 246 assert_eq!(value["dry_run"], true); 247 assert_eq!(value["result"]["state"], "dry_run"); 248 assert_eq!(value["result"]["default"], true); 249 assert_eq!(value["result"]["account"]["id"], watch_account_id); 250 assert_eq!(value["result"]["account"]["signer"], "local"); 251 assert_eq!(value["result"]["account"]["custody"], "secret_backed"); 252 assert_eq!(value["result"]["account"]["write_capable"], true); 253 assert_eq!(value["result"]["account"]["is_default"], true); 254 255 let watch_get = sandbox.json_success(&["--format", "json", "account", "get", watch_account_id]); 256 assert_eq!( 257 watch_get["result"]["account_resolution"]["resolved_account"]["signer"], 258 "watch_only" 259 ); 260 assert_eq!( 261 watch_get["result"]["account_resolution"]["resolved_account"]["custody"], 262 "watch_only" 263 ); 264 assert_eq!( 265 watch_get["result"]["account_resolution"]["resolved_account"]["write_capable"], 266 false 267 ); 268 let selected = sandbox.json_success(&["--format", "json", "account", "selection", "get"]); 269 assert_eq!( 270 selected["result"]["account_resolution"]["resolved_account"]["id"], 271 default_account_id 272 ); 273 } 274 275 #[test] 276 fn account_attach_secret_attaches_matching_secret_and_can_make_default() { 277 let sandbox = RadrootsCliSandbox::new(); 278 sandbox.json_success(&["--format", "json", "account", "create"]); 279 let identity = identity_secret(32); 280 let public_identity = identity.to_public(); 281 let public_identity_file = 282 write_public_identity_profile(&sandbox, "attach-public", &public_identity); 283 let secret_identity_file = write_secret_identity_profile(&sandbox, "attach-secret", &identity); 284 let imported = sandbox.json_success(&[ 285 "--format", 286 "json", 287 "--approval-token", 288 "approve", 289 "account", 290 "import", 291 public_identity_file.to_string_lossy().as_ref(), 292 ]); 293 let watch_account_id = imported["result"]["account"]["id"] 294 .as_str() 295 .expect("watch account id"); 296 297 let attached = sandbox.json_success(&[ 298 "--format", 299 "json", 300 "--approval-token", 301 "approve", 302 "account", 303 "attach-secret", 304 watch_account_id, 305 secret_identity_file.to_string_lossy().as_ref(), 306 "--default", 307 ]); 308 309 assert_eq!(attached["operation_id"], "account.attach_secret"); 310 assert_eq!(attached["result"]["state"], "secret_attached"); 311 assert_eq!(attached["result"]["account"]["id"], watch_account_id); 312 assert_eq!(attached["result"]["account"]["signer"], "local"); 313 assert_eq!(attached["result"]["account"]["custody"], "secret_backed"); 314 assert_eq!(attached["result"]["account"]["write_capable"], true); 315 assert_eq!(attached["result"]["account"]["is_default"], true); 316 317 let status = sandbox.json_success(&["--format", "json", "signer", "status", "get"]); 318 assert_eq!(status["result"]["state"], "ready"); 319 assert_eq!(status["result"]["signer_account_id"], watch_account_id); 320 assert_eq!(status["result"]["local"]["availability"], "secret_backed"); 321 } 322 323 #[test] 324 fn account_attach_secret_requires_approval_before_writing_secret() { 325 let sandbox = RadrootsCliSandbox::new(); 326 let identity = identity_secret(33); 327 let public_identity = identity.to_public(); 328 let public_identity_file = 329 write_public_identity_profile(&sandbox, "attach-approval-public", &public_identity); 330 let secret_identity_file = 331 write_secret_identity_profile(&sandbox, "attach-approval-secret", &identity); 332 let imported = sandbox.json_success(&[ 333 "--format", 334 "json", 335 "--approval-token", 336 "approve", 337 "account", 338 "import", 339 "--default", 340 public_identity_file.to_string_lossy().as_ref(), 341 ]); 342 let account_id = imported["result"]["account"]["id"] 343 .as_str() 344 .expect("account id"); 345 346 let (output, value) = sandbox.json_output(&[ 347 "--format", 348 "json", 349 "account", 350 "attach-secret", 351 account_id, 352 secret_identity_file.to_string_lossy().as_ref(), 353 ]); 354 355 assert!(!output.status.success()); 356 assert_eq!(value["operation_id"], "account.attach_secret"); 357 assert_eq!(value["errors"][0]["code"], "approval_required"); 358 assert_eq!(value["errors"][0]["exit_code"], 6); 359 let get = sandbox.json_success(&["--format", "json", "account", "get", account_id]); 360 assert_eq!( 361 get["result"]["account_resolution"]["resolved_account"]["signer"], 362 "watch_only" 363 ); 364 assert_eq!( 365 get["result"]["account_resolution"]["resolved_account"]["custody"], 366 "watch_only" 367 ); 368 assert_eq!( 369 get["result"]["account_resolution"]["resolved_account"]["write_capable"], 370 false 371 ); 372 } 373 374 #[test] 375 fn account_attach_secret_reports_structured_validation_failures() { 376 let sandbox = RadrootsCliSandbox::new(); 377 let matching_identity = identity_secret(34); 378 let public_identity = matching_identity.to_public(); 379 let public_identity_file = 380 write_public_identity_profile(&sandbox, "attach-fail-public", &public_identity); 381 let secret_identity_file = 382 write_secret_identity_profile(&sandbox, "attach-fail-secret", &matching_identity); 383 let imported = sandbox.json_success(&[ 384 "--format", 385 "json", 386 "--approval-token", 387 "approve", 388 "account", 389 "import", 390 public_identity_file.to_string_lossy().as_ref(), 391 ]); 392 let account_id = imported["result"]["account"]["id"] 393 .as_str() 394 .expect("account id"); 395 396 let (missing_input_output, missing_input) = 397 sandbox.json_output(&["--format", "json", "--dry-run", "account", "attach-secret"]); 398 assert!(!missing_input_output.status.success()); 399 assert_eq!(missing_input["operation_id"], "account.attach_secret"); 400 assert_eq!(missing_input["errors"][0]["code"], "invalid_input"); 401 402 let (missing_account_output, missing_account) = sandbox.json_output(&[ 403 "--format", 404 "json", 405 "--dry-run", 406 "account", 407 "attach-secret", 408 "missing-account", 409 secret_identity_file.to_string_lossy().as_ref(), 410 ]); 411 assert!(!missing_account_output.status.success()); 412 assert_eq!(missing_account["errors"][0]["code"], "account_unresolved"); 413 assert_eq!(missing_account["errors"][0]["exit_code"], 5); 414 415 let mismatched_identity = identity_secret(35); 416 let mismatched_identity_file = 417 write_secret_identity_profile(&sandbox, "attach-mismatch-secret", &mismatched_identity); 418 let (mismatch_output, mismatch) = sandbox.json_output(&[ 419 "--format", 420 "json", 421 "--dry-run", 422 "account", 423 "attach-secret", 424 account_id, 425 mismatched_identity_file.to_string_lossy().as_ref(), 426 ]); 427 assert!(!mismatch_output.status.success()); 428 assert_eq!(mismatch["errors"][0]["code"], "account_mismatch"); 429 assert_eq!(mismatch["errors"][0]["exit_code"], 5); 430 431 let invalid_identity_file = sandbox.root().join("attach-invalid-secret.json"); 432 fs::write(&invalid_identity_file, "{ invalid json").expect("write invalid identity"); 433 let (invalid_output, invalid) = sandbox.json_output(&[ 434 "--format", 435 "json", 436 "--dry-run", 437 "account", 438 "attach-secret", 439 account_id, 440 invalid_identity_file.to_string_lossy().as_ref(), 441 ]); 442 assert!(!invalid_output.status.success()); 443 assert_eq!(invalid["errors"][0]["code"], "validation_failed"); 444 assert_eq!(invalid["errors"][0]["exit_code"], 10); 445 446 let mut unavailable_command = sandbox.command(); 447 unavailable_command 448 .env("RADROOTS_CLI_ACCOUNT_SECRET_BACKEND", "host_vault") 449 .env("RADROOTS_CLI_ACCOUNT_SECRET_FALLBACK", "none") 450 .env("RADROOTS_CLI_ACCOUNT_HOST_VAULT_AVAILABLE", "false") 451 .args([ 452 "--format", 453 "json", 454 "--dry-run", 455 "account", 456 "attach-secret", 457 account_id, 458 secret_identity_file.to_string_lossy().as_ref(), 459 ]); 460 let unavailable_output = unavailable_command 461 .output() 462 .expect("run unavailable backend"); 463 let unavailable = json_from_stdout(&unavailable_output); 464 assert!(!unavailable_output.status.success()); 465 assert_eq!(unavailable["errors"][0]["code"], "operation_unavailable"); 466 assert_eq!(unavailable["errors"][0]["exit_code"], 3); 467 } 468 469 #[test] 470 fn account_remove_dry_run_validates_selector_without_mutating_store() { 471 let sandbox = RadrootsCliSandbox::new(); 472 let created = sandbox.json_success(&["--format", "json", "account", "create"]); 473 let account_id = created["result"]["account"]["id"] 474 .as_str() 475 .expect("account id"); 476 477 let value = sandbox.json_success(&[ 478 "--format", 479 "json", 480 "--dry-run", 481 "account", 482 "remove", 483 account_id, 484 ]); 485 486 assert_eq!(value["operation_id"], "account.remove"); 487 assert_eq!(value["result"]["state"], "dry_run"); 488 assert_eq!(value["result"]["removed_account"]["id"], account_id); 489 assert_eq!(value["result"]["default_would_clear"], true); 490 assert_eq!(value["result"]["remaining_account_count"], 0); 491 492 let get = sandbox.json_success(&["--format", "json", "account", "get", account_id]); 493 assert_eq!(get["result"]["state"], "ready"); 494 assert_eq!( 495 get["result"]["account_resolution"]["resolved_account"]["id"], 496 account_id 497 ); 498 } 499 500 #[test] 501 fn account_selection_update_dry_run_validates_selector_without_mutating_selection() { 502 let sandbox = RadrootsCliSandbox::new(); 503 let first = sandbox.json_success(&["--format", "json", "account", "create"]); 504 let second = sandbox.json_success(&["--format", "json", "account", "create"]); 505 let first_account_id = first["result"]["account"]["id"] 506 .as_str() 507 .expect("first account id"); 508 let second_account_id = second["result"]["account"]["id"] 509 .as_str() 510 .expect("second account id"); 511 512 let value = sandbox.json_success(&[ 513 "--format", 514 "json", 515 "--dry-run", 516 "account", 517 "selection", 518 "update", 519 second_account_id, 520 ]); 521 522 assert_eq!(value["operation_id"], "account.selection.update"); 523 assert_eq!(value["result"]["state"], "dry_run"); 524 assert_eq!(value["result"]["account"]["id"], second_account_id); 525 526 let selected = sandbox.json_success(&["--format", "json", "account", "selection", "get"]); 527 assert_eq!( 528 selected["result"]["account_resolution"]["resolved_account"]["id"], 529 first_account_id 530 ); 531 } 532 533 #[test] 534 fn account_selection_update_dry_run_rejects_missing_selector() { 535 let sandbox = RadrootsCliSandbox::new(); 536 let (output, value) = sandbox.json_output(&[ 537 "--format", 538 "json", 539 "--dry-run", 540 "account", 541 "selection", 542 "update", 543 "missing-account", 544 ]); 545 546 assert!(!output.status.success()); 547 assert_eq!(value["operation_id"], "account.selection.update"); 548 assert_eq!(value["errors"][0]["code"], "account_unresolved"); 549 assert_eq!(value["errors"][0]["exit_code"], 5); 550 } 551 552 #[test] 553 fn unresolved_account_override_returns_account_failure() { 554 let sandbox = RadrootsCliSandbox::new(); 555 let (output, value) = sandbox.json_output(&[ 556 "--format", 557 "json", 558 "--account-id", 559 "missing-account", 560 "account", 561 "get", 562 ]); 563 564 assert!(!output.status.success()); 565 assert_eq!(value["operation_id"], "account.get"); 566 assert_eq!(value["result"], serde_json::Value::Null); 567 assert_eq!(value["errors"][0]["code"], "account_unresolved"); 568 assert_eq!(value["errors"][0]["exit_code"], 5); 569 assert_eq!(value["errors"][0]["detail"]["class"], "account"); 570 assert_contains(&value["errors"][0]["message"], "account selector"); 571 } 572 573 #[test] 574 fn watch_only_import_reports_unconfigured_local_signer() { 575 let sandbox = RadrootsCliSandbox::new(); 576 let public_identity = identity_public(11); 577 let public_identity_file = 578 write_public_identity_profile(&sandbox, "watch-only", &public_identity); 579 580 let imported = sandbox.json_success(&[ 581 "--format", 582 "json", 583 "--approval-token", 584 "approve", 585 "account", 586 "import", 587 "--default", 588 public_identity_file.to_string_lossy().as_ref(), 589 ]); 590 591 assert_eq!(imported["operation_id"], "account.import"); 592 assert_eq!(imported["result"]["state"], "imported"); 593 assert_eq!( 594 imported["result"]["account"]["id"], 595 public_identity.id.as_str() 596 ); 597 assert_eq!(imported["result"]["account"]["signer"], "watch_only"); 598 assert_eq!(imported["result"]["account"]["custody"], "watch_only"); 599 assert_eq!(imported["result"]["account"]["write_capable"], false); 600 assert_eq!(imported["result"]["account"]["is_default"], true); 601 602 let status = sandbox.json_success(&["--format", "json", "signer", "status", "get"]); 603 604 assert_eq!(status["result"]["mode"], "local"); 605 assert_eq!(status["result"]["state"], "unconfigured"); 606 assert_eq!( 607 status["result"]["signer_account_id"], 608 public_identity.id.as_str() 609 ); 610 assert_eq!( 611 status["result"]["account_resolution"]["source"], 612 "default_account" 613 ); 614 assert_eq!( 615 status["result"]["account_resolution"]["resolved_account"]["signer"], 616 "watch_only" 617 ); 618 assert_eq!( 619 status["result"]["account_resolution"]["resolved_account"]["custody"], 620 "watch_only" 621 ); 622 assert_eq!( 623 status["result"]["account_resolution"]["resolved_account"]["write_capable"], 624 false 625 ); 626 assert_eq!( 627 status["result"]["local"]["account_id"], 628 public_identity.id.as_str() 629 ); 630 assert_eq!(status["result"]["local"]["availability"], "public_only"); 631 assert_eq!(status["result"]["local"]["secret_backed"], false); 632 assert_contains(&status["result"]["reason"], "not secret-backed"); 633 assert!( 634 status["result"]["write_kinds"] 635 .as_array() 636 .expect("write kinds") 637 .iter() 638 .all(|kind| kind["ready"] == false) 639 ); 640 } 641 642 #[test] 643 fn myc_signer_status_reports_missing_binding() { 644 let sandbox = RadrootsCliSandbox::new(); 645 let missing_myc = sandbox.root().join("bin/missing-myc"); 646 configure_myc_mode(&sandbox, &missing_myc); 647 648 let (output, value) = sandbox.json_output(&["--format", "json", "signer", "status", "get"]); 649 650 assert!(output.status.success()); 651 assert_eq!(value["operation_id"], "signer.status.get"); 652 assert!(value["errors"].as_array().expect("errors").is_empty()); 653 assert_eq!(value["result"]["mode"], "myc"); 654 assert_eq!(value["result"]["state"], "unconfigured"); 655 assert_eq!(value["result"]["binding"]["state"], "unconfigured"); 656 assert_eq!(value["result"]["myc"]["state"], "unconfigured"); 657 assert_eq!(value["result"]["myc"]["ready"], false); 658 assert_contains( 659 &value["result"]["reason"], 660 "signer.remote_nip46 binding is missing", 661 ); 662 assert_no_removed_command_reference(&value, &["signer", "status", "get"]); 663 } 664 665 #[test] 666 fn myc_signer_status_fails_closed_when_managed_account_is_unresolved() { 667 let sandbox = RadrootsCliSandbox::new(); 668 let missing_myc = sandbox.root().join("bin/missing-myc"); 669 let remote_signer = identity_public(91); 670 sandbox.write_app_config(&format!( 671 r#"[signer] 672 backend = "myc" 673 674 [myc] 675 executable = "{}" 676 677 [[capability_binding]] 678 capability = "signer.remote_nip46" 679 provider = "myc" 680 target_kind = "explicit_endpoint" 681 target = "bunker://{}?relay=wss%3A%2F%2Frelay.example" 682 managed_account_ref = "acct_missing" 683 signer_session_ref = "session_missing" 684 "#, 685 toml_string(missing_myc.display().to_string().as_str()), 686 remote_signer.public_key_hex, 687 )); 688 689 let value = sandbox.json_success(&["--format", "json", "signer", "status", "get"]); 690 691 assert_eq!(value["result"]["mode"], "myc"); 692 assert_eq!(value["result"]["state"], "unconfigured"); 693 assert_eq!(value["result"]["binding"]["state"], "unconfigured"); 694 assert_eq!(value["result"]["myc"]["ready"], false); 695 assert_contains( 696 &value["result"]["reason"], 697 "managed_account_ref `acct_missing` cannot be evaluated", 698 ); 699 assert!( 700 value["result"]["write_kinds"] 701 .as_array() 702 .expect("write kinds") 703 .iter() 704 .all(|kind| kind["ready"] == false) 705 ); 706 } 707 708 #[cfg(unix)] 709 #[test] 710 fn myc_signer_status_does_not_invoke_configured_executable() { 711 let sandbox = RadrootsCliSandbox::new(); 712 let invoked = sandbox.root().join("myc-invoked.txt"); 713 let myc = sandbox.write_fake_myc( 714 "myc-deferred", 715 format!( 716 "printf invoked > '{}'", 717 shell_single_quoted(invoked.to_string_lossy().as_ref()) 718 ) 719 .as_str(), 720 ); 721 configure_myc_mode(&sandbox, &myc); 722 723 let (output, value) = sandbox.json_output(&["--format", "json", "signer", "status", "get"]); 724 725 assert!(output.status.success()); 726 assert_eq!(value["operation_id"], "signer.status.get"); 727 assert!(value["errors"].as_array().expect("errors").is_empty()); 728 assert_eq!(value["result"]["state"], "unconfigured"); 729 assert_eq!(value["result"]["myc"]["ready"], false); 730 assert!(!invoked.exists(), "target CLI must not execute MYC"); 731 } 732 733 #[test] 734 fn myc_mode_allows_read_inspection_commands() { 735 let sandbox = RadrootsCliSandbox::new(); 736 let missing_myc = sandbox.root().join("bin/missing-myc"); 737 configure_myc_mode(&sandbox, &missing_myc); 738 739 for args in [ 740 &["--format", "json", "workspace", "get"][..], 741 &["--format", "json", "config", "get"][..], 742 &["--format", "json", "account", "list"][..], 743 &["--format", "json", "relay", "list"][..], 744 ] { 745 let (output, value) = sandbox.json_output(args); 746 747 assert!( 748 output.status.success(), 749 "`{args:?}` should remain observable under MYC mode: {value:?}" 750 ); 751 assert_eq!(value["errors"].as_array().expect("errors").len(), 0); 752 } 753 } 754 755 #[test] 756 fn local_listing_publish_fails_without_local_account_authority() { 757 let sandbox = RadrootsCliSandbox::new(); 758 let account = sandbox.json_success(&["--format", "json", "account", "create"]); 759 let account_id = account["result"]["account"]["id"] 760 .as_str() 761 .expect("account id"); 762 let listing_file = create_listing_draft(&sandbox, "local-no-account"); 763 make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw"); 764 sandbox.json_success(&[ 765 "--format", 766 "json", 767 "--approval-token", 768 "approve", 769 "account", 770 "remove", 771 account_id, 772 ]); 773 774 let (output, value) = sandbox.json_output(&[ 775 "--format", 776 "json", 777 "--relay", 778 "ws://127.0.0.1:9", 779 "--approval-token", 780 "approve", 781 "listing", 782 "publish", 783 listing_file.to_string_lossy().as_ref(), 784 ]); 785 786 assert!(!output.status.success()); 787 assert_eq!(value["operation_id"], "listing.publish"); 788 assert_eq!(value["result"], serde_json::Value::Null); 789 assert_eq!(value["errors"][0]["code"], "account_unresolved"); 790 assert_eq!(value["errors"][0]["exit_code"], 5); 791 assert_eq!(value["errors"][0]["detail"]["class"], "account"); 792 assert_contains( 793 &value["errors"][0]["message"], 794 "listing-bound seller account", 795 ); 796 } 797 798 #[test] 799 fn local_listing_publish_dry_run_validates_local_account_authority() { 800 let sandbox = RadrootsCliSandbox::new(); 801 let account = sandbox.json_success(&["--format", "json", "account", "create"]); 802 let account_id = account["result"]["account"]["id"] 803 .as_str() 804 .expect("account id"); 805 let listing_file = create_listing_draft(&sandbox, "local-dry-run-no-account"); 806 make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw"); 807 sandbox.json_success(&[ 808 "--format", 809 "json", 810 "--approval-token", 811 "approve", 812 "account", 813 "remove", 814 account_id, 815 ]); 816 817 let (output, value) = sandbox.json_output(&[ 818 "--format", 819 "json", 820 "--dry-run", 821 "listing", 822 "publish", 823 listing_file.to_string_lossy().as_ref(), 824 ]); 825 826 assert!(!output.status.success()); 827 assert_eq!(value["operation_id"], "listing.publish"); 828 assert_eq!(value["result"], serde_json::Value::Null); 829 assert_eq!(value["errors"][0]["code"], "account_unresolved"); 830 assert_eq!(value["errors"][0]["detail"]["class"], "account"); 831 assert_no_removed_command_reference(&value, &["listing", "publish", "--dry-run"]); 832 } 833 834 #[test] 835 fn local_listing_update_dry_run_validates_local_account_authority() { 836 let sandbox = RadrootsCliSandbox::new(); 837 let account = sandbox.json_success(&["--format", "json", "account", "create"]); 838 let account_id = account["result"]["account"]["id"] 839 .as_str() 840 .expect("account id"); 841 let listing_file = create_listing_draft(&sandbox, "local-update-dry-run-no-account"); 842 make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw"); 843 sandbox.json_success(&[ 844 "--format", 845 "json", 846 "--approval-token", 847 "approve", 848 "account", 849 "remove", 850 account_id, 851 ]); 852 853 let (output, value) = sandbox.json_output(&[ 854 "--format", 855 "json", 856 "--dry-run", 857 "listing", 858 "update", 859 listing_file.to_string_lossy().as_ref(), 860 ]); 861 862 assert!(!output.status.success()); 863 assert_eq!(value["operation_id"], "listing.update"); 864 assert_eq!(value["result"], serde_json::Value::Null); 865 assert_eq!(value["errors"][0]["code"], "account_unresolved"); 866 assert_eq!(value["errors"][0]["detail"]["class"], "account"); 867 assert_no_removed_command_reference(&value, &["listing", "update", "--dry-run"]); 868 } 869 870 #[test] 871 fn local_listing_update_dry_run_rejects_mismatched_local_account() { 872 let sandbox = RadrootsCliSandbox::new(); 873 let first = sandbox.json_success(&["--format", "json", "account", "create"]); 874 let listing_file = create_listing_draft(&sandbox, "local-update-dry-run-mismatch"); 875 make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw"); 876 let second = sandbox.json_success(&["--format", "json", "account", "create"]); 877 let second_account_id = second["result"]["account"]["id"] 878 .as_str() 879 .expect("second account id"); 880 881 let (output, value) = sandbox.json_output(&[ 882 "--format", 883 "json", 884 "--account-id", 885 second_account_id, 886 "--dry-run", 887 "listing", 888 "update", 889 listing_file.to_string_lossy().as_ref(), 890 ]); 891 892 assert_ne!( 893 first["result"]["account"]["id"], 894 second["result"]["account"]["id"] 895 ); 896 assert!(!output.status.success()); 897 assert_eq!(value["operation_id"], "listing.update"); 898 assert_eq!(value["errors"][0]["code"], "account_mismatch"); 899 assert_eq!(value["errors"][0]["detail"]["class"], "account"); 900 } 901 902 #[test] 903 fn local_listing_publish_fails_without_configured_relay() { 904 let sandbox = RadrootsCliSandbox::new(); 905 sandbox.json_success(&["--format", "json", "account", "create"]); 906 let listing_file = create_listing_draft(&sandbox, "local-unavailable"); 907 make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw"); 908 909 let (output, value) = sandbox.json_output(&[ 910 "--format", 911 "json", 912 "--approval-token", 913 "approve", 914 "listing", 915 "publish", 916 listing_file.to_string_lossy().as_ref(), 917 ]); 918 919 assert!(!output.status.success()); 920 assert_eq!(value["operation_id"], "listing.publish"); 921 assert_eq!(value["result"], serde_json::Value::Null); 922 assert_eq!(value["errors"][0]["code"], "empty_target_relays"); 923 assert_eq!(value["errors"][0]["detail"]["class"], "configuration"); 924 assert_contains(&value["errors"][0]["message"], "sdk empty target relays"); 925 assert_no_removed_command_reference(&value, &["listing", "publish"]); 926 assert_no_daemon_runtime_reference(&value, &["listing", "publish"]); 927 } 928 929 #[test] 930 fn local_listing_publish_dry_run_does_not_sign_matching_listing() { 931 let sandbox = RadrootsCliSandbox::new(); 932 sandbox.json_success(&["--format", "json", "account", "create"]); 933 let listing_file = create_listing_draft(&sandbox, "local-dry-run"); 934 make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw"); 935 936 let value = sandbox.json_success(&[ 937 "--format", 938 "json", 939 "--dry-run", 940 "listing", 941 "publish", 942 listing_file.to_string_lossy().as_ref(), 943 ]); 944 945 assert_eq!(value["operation_id"], "listing.publish"); 946 assert_eq!(value["dry_run"], true); 947 assert_eq!(value["result"]["state"], "dry_run"); 948 assert_eq!(value["result"]["dry_run"], true); 949 assert_eq!( 950 value["result"]["event_id"] 951 .as_str() 952 .expect("dry-run event id") 953 .len(), 954 64 955 ); 956 assert!( 957 !sandbox.root().join("data/apps/cli/replica/sdk").exists(), 958 "dry-run must not materialize durable SDK storage" 959 ); 960 assert_no_removed_command_reference(&value, &["listing", "publish", "--dry-run"]); 961 assert_no_daemon_runtime_reference(&value, &["listing", "publish", "--dry-run"]); 962 } 963 964 #[test] 965 fn local_listing_archive_dry_run_validates_local_account_authority() { 966 let sandbox = RadrootsCliSandbox::new(); 967 sandbox.json_success(&["--format", "json", "account", "create"]); 968 let listing_file = create_listing_draft(&sandbox, "local-archive-mismatch"); 969 make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw"); 970 let second = sandbox.json_success(&["--format", "json", "account", "create"]); 971 let second_account_id = second["result"]["account"]["id"] 972 .as_str() 973 .expect("second account id"); 974 975 let (output, value) = sandbox.json_output(&[ 976 "--format", 977 "json", 978 "--account-id", 979 second_account_id, 980 "--dry-run", 981 "listing", 982 "archive", 983 listing_file.to_string_lossy().as_ref(), 984 ]); 985 986 assert!(!output.status.success()); 987 assert_eq!(value["operation_id"], "listing.archive"); 988 assert_eq!(value["errors"][0]["code"], "account_mismatch"); 989 assert_eq!(value["errors"][0]["detail"]["class"], "account"); 990 } 991 992 #[test] 993 fn local_listing_publish_fails_when_selected_account_does_not_match_seller() { 994 let sandbox = RadrootsCliSandbox::new(); 995 let first = sandbox.json_success(&["--format", "json", "account", "create"]); 996 let first_account_id = first["result"]["account"]["id"] 997 .as_str() 998 .expect("first account id"); 999 let listing_file = create_listing_draft(&sandbox, "local-mismatch"); 1000 make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw"); 1001 let second = sandbox.json_success(&["--format", "json", "account", "create"]); 1002 let second_account_id = second["result"]["account"]["id"] 1003 .as_str() 1004 .expect("second account id"); 1005 assert_ne!(first_account_id, second_account_id); 1006 1007 let (output, value) = sandbox.json_output(&[ 1008 "--format", 1009 "json", 1010 "--relay", 1011 "ws://127.0.0.1:9", 1012 "--account-id", 1013 second_account_id, 1014 "--approval-token", 1015 "approve", 1016 "listing", 1017 "publish", 1018 listing_file.to_string_lossy().as_ref(), 1019 ]); 1020 1021 assert!(!output.status.success()); 1022 assert_eq!(value["operation_id"], "listing.publish"); 1023 assert_eq!(value["result"], serde_json::Value::Null); 1024 assert_eq!(value["errors"][0]["code"], "account_mismatch"); 1025 assert_eq!(value["errors"][0]["exit_code"], 5); 1026 assert_eq!(value["errors"][0]["detail"]["class"], "account"); 1027 assert_contains( 1028 &value["errors"][0]["message"], 1029 "listing draft is bound to seller account", 1030 ); 1031 assert_no_removed_command_reference(&value, &["listing", "publish", "account mismatch"]); 1032 } 1033 1034 #[test] 1035 fn local_farm_publish_dry_run_validates_secret_backed_account() { 1036 let sandbox = RadrootsCliSandbox::new(); 1037 sandbox.json_success(&["--format", "json", "account", "create"]); 1038 sandbox.json_success(&[ 1039 "--format", 1040 "json", 1041 "farm", 1042 "create", 1043 "--name", 1044 "Green Farm", 1045 "--location", 1046 "farmstand", 1047 "--country", 1048 "US", 1049 "--delivery-method", 1050 "pickup", 1051 ]); 1052 1053 let value = sandbox.json_success(&[ 1054 "--format", 1055 "json", 1056 "--relay", 1057 "ws://127.0.0.1:9", 1058 "--dry-run", 1059 "farm", 1060 "publish", 1061 ]); 1062 1063 assert_eq!(value["operation_id"], "farm.publish"); 1064 assert_eq!(value["dry_run"], true); 1065 assert_eq!(value["result"]["state"], "dry_run"); 1066 assert_eq!(value["result"]["dry_run"], true); 1067 assert_no_daemon_runtime_reference(&value, &["farm", "publish", "--dry-run"]); 1068 } 1069 1070 #[test] 1071 fn local_farm_publish_dry_run_fails_without_configured_relay() { 1072 let sandbox = RadrootsCliSandbox::new(); 1073 sandbox.json_success(&["--format", "json", "account", "create"]); 1074 sandbox.json_success(&[ 1075 "--format", 1076 "json", 1077 "farm", 1078 "create", 1079 "--name", 1080 "Green Farm", 1081 "--location", 1082 "farmstand", 1083 "--country", 1084 "US", 1085 "--delivery-method", 1086 "pickup", 1087 ]); 1088 1089 let (output, value) = 1090 sandbox.json_output(&["--format", "json", "--dry-run", "farm", "publish"]); 1091 1092 assert!(!output.status.success()); 1093 assert_eq!(value["operation_id"], "farm.publish"); 1094 assert_eq!(value["dry_run"], true); 1095 assert_eq!(value["result"], serde_json::Value::Null); 1096 assert_eq!(value["errors"][0]["code"], "network_unavailable"); 1097 assert_eq!(value["errors"][0]["detail"]["class"], "network"); 1098 assert_contains( 1099 &value["errors"][0]["message"], 1100 "requires at least one configured relay", 1101 ); 1102 assert_no_removed_command_reference(&value, &["farm", "publish", "--dry-run"]); 1103 assert_no_daemon_runtime_reference(&value, &["farm", "publish", "--dry-run"]); 1104 } 1105 1106 #[test] 1107 fn local_farm_publish_fails_without_configured_relay() { 1108 let sandbox = RadrootsCliSandbox::new(); 1109 sandbox.json_success(&["--format", "json", "account", "create"]); 1110 sandbox.json_success(&[ 1111 "--format", 1112 "json", 1113 "farm", 1114 "create", 1115 "--name", 1116 "Green Farm", 1117 "--location", 1118 "farmstand", 1119 "--country", 1120 "US", 1121 "--delivery-method", 1122 "pickup", 1123 ]); 1124 1125 let (output, value) = sandbox.json_output(&[ 1126 "--format", 1127 "json", 1128 "--approval-token", 1129 "approve", 1130 "farm", 1131 "publish", 1132 ]); 1133 1134 assert!(!output.status.success()); 1135 assert_eq!(value["operation_id"], "farm.publish"); 1136 assert_eq!(value["result"], serde_json::Value::Null); 1137 assert_eq!(value["errors"][0]["code"], "network_unavailable"); 1138 assert_eq!(value["errors"][0]["detail"]["class"], "network"); 1139 assert_contains( 1140 &value["errors"][0]["message"], 1141 "requires at least one configured relay", 1142 ); 1143 assert_no_removed_command_reference(&value, &["farm", "publish"]); 1144 assert_no_daemon_runtime_reference(&value, &["farm", "publish"]); 1145 } 1146 1147 #[test] 1148 fn farm_setup_actions_offer_publish_only_when_relay_publish_executable() { 1149 let sandbox = RadrootsCliSandbox::new(); 1150 sandbox.json_success(&["--format", "json", "account", "create"]); 1151 1152 let unconfigured = sandbox.json_success(&[ 1153 "--format", 1154 "json", 1155 "farm", 1156 "create", 1157 "--name", 1158 "Green Farm", 1159 "--location", 1160 "farmstand", 1161 "--country", 1162 "US", 1163 "--delivery-method", 1164 "pickup", 1165 ]); 1166 1167 assert_action_present(&unconfigured, "radroots farm readiness check"); 1168 assert_action_absent(&unconfigured, "radroots farm publish"); 1169 1170 let configured = sandbox.json_success(&[ 1171 "--format", 1172 "json", 1173 "--relay", 1174 "ws://127.0.0.1:9", 1175 "farm", 1176 "profile", 1177 "update", 1178 "--field", 1179 "name", 1180 "--value", 1181 "Green Farm Updated", 1182 ]); 1183 1184 assert_action_present(&configured, "radroots farm readiness check"); 1185 assert_action_present(&configured, "radroots farm publish"); 1186 } 1187 1188 #[test] 1189 fn farm_setup_actions_withhold_publish_for_watch_only_account() { 1190 let sandbox = RadrootsCliSandbox::new(); 1191 let public_identity = identity_public(51); 1192 let public_identity_file = 1193 write_public_identity_profile(&sandbox, "farm-watch-only", &public_identity); 1194 sandbox.json_success(&[ 1195 "--format", 1196 "json", 1197 "--approval-token", 1198 "approve", 1199 "account", 1200 "import", 1201 "--default", 1202 public_identity_file.to_string_lossy().as_ref(), 1203 ]); 1204 1205 let created = sandbox.json_success(&[ 1206 "--format", 1207 "json", 1208 "--relay", 1209 "ws://127.0.0.1:9", 1210 "farm", 1211 "create", 1212 "--name", 1213 "Watch Farm", 1214 "--location", 1215 "farmstand", 1216 "--country", 1217 "US", 1218 "--delivery-method", 1219 "pickup", 1220 ]); 1221 1222 assert_action_present(&created, "radroots farm readiness check"); 1223 assert_action_absent(&created, "radroots farm publish"); 1224 1225 let updated = sandbox.json_success(&[ 1226 "--format", 1227 "json", 1228 "--relay", 1229 "ws://127.0.0.1:9", 1230 "farm", 1231 "profile", 1232 "update", 1233 "--field", 1234 "name", 1235 "--value", 1236 "Watch Farm Updated", 1237 ]); 1238 1239 assert_action_present(&updated, "radroots farm readiness check"); 1240 assert_action_absent(&updated, "radroots farm publish"); 1241 } 1242 1243 #[test] 1244 fn local_farm_publish_reports_sdk_push_failure_without_profile_publish() { 1245 let sandbox = RadrootsCliSandbox::new(); 1246 sandbox.json_success(&["--format", "json", "account", "create"]); 1247 sandbox.json_success(&[ 1248 "--format", 1249 "json", 1250 "farm", 1251 "create", 1252 "--name", 1253 "Green Farm", 1254 "--location", 1255 "farmstand", 1256 "--country", 1257 "US", 1258 "--delivery-method", 1259 "pickup", 1260 ]); 1261 let relay_url = "ws://127.0.0.1:9"; 1262 1263 let (output, value) = sandbox.json_output(&[ 1264 "--format", 1265 "json", 1266 "--relay", 1267 relay_url, 1268 "--approval-token", 1269 "approve", 1270 "--idempotency-key", 1271 "farm_partial", 1272 "farm", 1273 "publish", 1274 ]); 1275 1276 assert!(!output.status.success()); 1277 assert_eq!(value["operation_id"], "farm.publish"); 1278 assert_eq!(value["result"], serde_json::Value::Null); 1279 assert_eq!(value["errors"][0]["code"], "network_unavailable"); 1280 assert_eq!(value["errors"][0]["detail"]["class"], "network"); 1281 assert_contains( 1282 &value["errors"][0]["message"], 1283 "SDK relay publish did not reach accepted quorum", 1284 ); 1285 let detail = &value["errors"][0]["detail"]; 1286 assert_eq!(detail["source"], "SDK farm publish ยท configured signer"); 1287 assert_eq!(detail["state"], "unavailable"); 1288 assert_eq!(detail["profile"]["state"], "not_submitted"); 1289 assert_eq!(detail["farm"]["state"], "unavailable"); 1290 assert_eq!(detail["profile"]["event_id"], serde_json::Value::Null); 1291 assert_eq!( 1292 detail["farm"]["event_id"] 1293 .as_str() 1294 .expect("sdk farm event id") 1295 .len(), 1296 64 1297 ); 1298 assert_eq!(detail["profile"]["idempotency_key"], "farm_partial:profile"); 1299 assert_eq!(detail["farm"]["idempotency_key"], "farm_partial:farm"); 1300 assert_eq!(detail["actions"][0], "radroots sync push"); 1301 assert_eq!(detail["farm"]["target_relays"][0], relay_url); 1302 assert_relay_url(&detail["farm"]["failed_relays"][0]["relay"], relay_url); 1303 assert_no_removed_command_reference(&value, &["farm", "publish"]); 1304 assert_no_daemon_runtime_reference(&value, &["farm", "publish"]); 1305 1306 let persisted = sandbox.json_success(&["--format", "json", "farm", "get"]); 1307 assert_eq!( 1308 persisted["result"]["document"]["publication"]["profile_state"], 1309 "not_published" 1310 ); 1311 assert_eq!( 1312 persisted["result"]["document"]["publication"]["farm_state"], 1313 "not_published" 1314 ); 1315 assert_eq!( 1316 persisted["result"]["document"]["publication"]["profile_event_id"], 1317 serde_json::Value::Null 1318 ); 1319 assert_eq!( 1320 persisted["result"]["document"]["publication"]["farm_event_id"], 1321 serde_json::Value::Null 1322 ); 1323 } 1324 1325 #[test] 1326 fn local_farm_publish_does_not_persist_publication_until_sdk_push_publishes() { 1327 let sandbox = RadrootsCliSandbox::new(); 1328 sandbox.json_success(&["--format", "json", "account", "create"]); 1329 sandbox.json_success(&[ 1330 "--format", 1331 "json", 1332 "farm", 1333 "create", 1334 "--name", 1335 "Green Farm", 1336 "--location", 1337 "farmstand", 1338 "--country", 1339 "US", 1340 "--delivery-method", 1341 "pickup", 1342 ]); 1343 let relay_url = "ws://127.0.0.1:9"; 1344 1345 let (output, value) = sandbox.json_output(&[ 1346 "--format", 1347 "json", 1348 "--relay", 1349 relay_url, 1350 "--approval-token", 1351 "approve", 1352 "--idempotency-key", 1353 "farm_success", 1354 "farm", 1355 "publish", 1356 ]); 1357 1358 assert!(!output.status.success()); 1359 assert_eq!(value["operation_id"], "farm.publish"); 1360 assert_eq!(value["result"], serde_json::Value::Null); 1361 assert_eq!(value["errors"][0]["code"], "network_unavailable"); 1362 assert_eq!(value["errors"][0]["detail"]["class"], "network"); 1363 let detail = &value["errors"][0]["detail"]; 1364 assert_eq!(detail["source"], "SDK farm publish ยท configured signer"); 1365 assert_eq!(detail["profile"]["state"], "not_submitted"); 1366 assert_eq!(detail["farm"]["state"], "unavailable"); 1367 assert_eq!(detail["profile"]["event_id"], serde_json::Value::Null); 1368 assert_eq!( 1369 detail["farm"]["event_id"] 1370 .as_str() 1371 .expect("sdk farm event id") 1372 .len(), 1373 64 1374 ); 1375 assert_eq!(detail["profile"]["idempotency_key"], "farm_success:profile"); 1376 assert_eq!(detail["farm"]["idempotency_key"], "farm_success:farm"); 1377 assert_no_removed_command_reference(&value, &["farm", "publish"]); 1378 assert_no_daemon_runtime_reference(&value, &["farm", "publish"]); 1379 1380 let persisted = sandbox.json_success(&["--format", "json", "farm", "get"]); 1381 assert_eq!( 1382 persisted["result"]["document"]["publication"]["profile_state"], 1383 "not_published" 1384 ); 1385 assert_eq!( 1386 persisted["result"]["document"]["publication"]["farm_state"], 1387 "not_published" 1388 ); 1389 assert_eq!( 1390 persisted["result"]["document"]["publication"]["profile_event_id"], 1391 serde_json::Value::Null 1392 ); 1393 assert_eq!( 1394 persisted["result"]["document"]["publication"]["farm_event_id"], 1395 serde_json::Value::Null 1396 ); 1397 } 1398 1399 #[test] 1400 fn farm_rebind_is_explicit_and_publish_defaults_ignore_ambient_selection() { 1401 let sandbox = RadrootsCliSandbox::new(); 1402 let first = sandbox.json_success(&["--format", "json", "account", "create"]); 1403 let first_account_id = first["result"]["account"]["id"] 1404 .as_str() 1405 .expect("first account id"); 1406 let farm = sandbox.json_success(&[ 1407 "--format", 1408 "json", 1409 "farm", 1410 "create", 1411 "--name", 1412 "Green Farm", 1413 "--location", 1414 "farmstand", 1415 "--country", 1416 "US", 1417 "--delivery-method", 1418 "pickup", 1419 ]); 1420 let farm_path = farm["result"]["config"]["path"] 1421 .as_str() 1422 .expect("farm path"); 1423 let farm_d_tag = farm["result"]["config"]["farm_d_tag"] 1424 .as_str() 1425 .expect("farm d tag"); 1426 let first_pubkey = farm["result"]["config"]["seller_pubkey"] 1427 .as_str() 1428 .expect("first pubkey"); 1429 assert_eq!( 1430 farm["result"]["config"]["seller_account_id"], 1431 first_account_id 1432 ); 1433 assert_eq!(farm["result"]["config"]["seller_pubkey"], first_pubkey); 1434 assert_eq!( 1435 farm["result"]["config"]["seller_actor_source"], 1436 "farm_config" 1437 ); 1438 assert!( 1439 farm["result"]["config"] 1440 .get("selected_account_id") 1441 .is_none() 1442 ); 1443 1444 let published = sandbox.json_success(&["--format", "json", "farm", "get"]); 1445 assert_eq!( 1446 published["result"]["document"]["publication"]["profile_state"], 1447 "not_published" 1448 ); 1449 assert_eq!( 1450 published["result"]["document"]["publication"]["farm_state"], 1451 "not_published" 1452 ); 1453 1454 let same_seller_dry_run = sandbox.json_success(&[ 1455 "--format", 1456 "json", 1457 "--dry-run", 1458 "farm", 1459 "rebind", 1460 first_account_id, 1461 ]); 1462 assert_eq!(same_seller_dry_run["operation_id"], "farm.rebind"); 1463 assert_eq!( 1464 same_seller_dry_run["result"]["publication_state_action"], 1465 "preserved" 1466 ); 1467 1468 let same_seller_live = sandbox.json_success(&[ 1469 "--format", 1470 "json", 1471 "--approval-token", 1472 "approve", 1473 "farm", 1474 "rebind", 1475 first_account_id, 1476 ]); 1477 assert_eq!(same_seller_live["operation_id"], "farm.rebind"); 1478 assert_eq!( 1479 same_seller_live["result"]["publication_state_action"], 1480 "preserved" 1481 ); 1482 let same_seller_get = sandbox.json_success(&["--format", "json", "farm", "get"]); 1483 assert_eq!( 1484 same_seller_get["result"]["document"]["publication"]["profile_state"], 1485 "not_published" 1486 ); 1487 assert_eq!( 1488 same_seller_get["result"]["document"]["publication"]["farm_state"], 1489 "not_published" 1490 ); 1491 1492 let second = sandbox.json_success(&["--format", "json", "account", "create"]); 1493 let second_account_id = second["result"]["account"]["id"] 1494 .as_str() 1495 .expect("second account id"); 1496 assert_ne!(first_account_id, second_account_id); 1497 sandbox.json_success(&[ 1498 "--format", 1499 "json", 1500 "account", 1501 "selection", 1502 "update", 1503 second_account_id, 1504 ]); 1505 1506 let (retarget_output, retarget) = sandbox.json_output(&[ 1507 "--format", 1508 "json", 1509 "farm", 1510 "create", 1511 "--name", 1512 "Green Farm Retarget", 1513 "--location", 1514 "farmstand", 1515 "--country", 1516 "US", 1517 "--delivery-method", 1518 "pickup", 1519 ]); 1520 assert!(!retarget_output.status.success()); 1521 assert_eq!(retarget["operation_id"], "farm.create"); 1522 assert_eq!(retarget["errors"][0]["code"], "account_mismatch"); 1523 assert_contains(&retarget["errors"][0]["message"], "farm-bound seller"); 1524 assert_eq!( 1525 retarget["errors"][0]["detail"]["seller_actor_source"], 1526 "farm_config" 1527 ); 1528 assert_eq!( 1529 retarget["errors"][0]["detail"]["farm_bound_seller_account_id"], 1530 first_account_id 1531 ); 1532 assert_eq!( 1533 retarget["errors"][0]["detail"]["attempted_seller_account_id"], 1534 second_account_id 1535 ); 1536 assert_next_action_present( 1537 &retarget, 1538 format!("radroots farm rebind {second_account_id}").as_str(), 1539 ); 1540 1541 let (missing_rebind_output, missing_rebind) = sandbox.json_output(&[ 1542 "--format", 1543 "json", 1544 "--dry-run", 1545 "farm", 1546 "rebind", 1547 "acct_missing", 1548 ]); 1549 assert!(!missing_rebind_output.status.success()); 1550 assert_eq!(missing_rebind["operation_id"], "farm.rebind"); 1551 assert_eq!(missing_rebind["errors"][0]["code"], "account_unresolved"); 1552 assert_eq!( 1553 missing_rebind["errors"][0]["detail"]["seller_actor_source"], 1554 "farm_config" 1555 ); 1556 assert_eq!( 1557 missing_rebind["errors"][0]["detail"]["selector"], 1558 "acct_missing" 1559 ); 1560 assert_next_action_present(&missing_rebind, "radroots account import <path>"); 1561 assert_next_action_present(&missing_rebind, "radroots account create"); 1562 1563 let publish_dry_run = sandbox.json_success(&[ 1564 "--format", 1565 "json", 1566 "--relay", 1567 "ws://127.0.0.1:9", 1568 "--dry-run", 1569 "farm", 1570 "publish", 1571 ]); 1572 assert_eq!(publish_dry_run["operation_id"], "farm.publish"); 1573 assert_eq!(publish_dry_run["result"]["state"], "dry_run"); 1574 assert_eq!( 1575 publish_dry_run["result"]["seller_account_id"], 1576 first_account_id 1577 ); 1578 assert_eq!(publish_dry_run["result"]["seller_pubkey"], first_pubkey); 1579 assert!( 1580 publish_dry_run["result"] 1581 .get("selected_account_id") 1582 .is_none() 1583 ); 1584 1585 let listing_path = sandbox.root().join("drift-listing.toml"); 1586 let listing = sandbox.json_success(&[ 1587 "--format", 1588 "json", 1589 "listing", 1590 "create", 1591 "--output", 1592 listing_path.to_string_lossy().as_ref(), 1593 "--key", 1594 "drift-eggs", 1595 "--title", 1596 "Eggs", 1597 "--category", 1598 "eggs", 1599 "--summary", 1600 "Fresh eggs", 1601 "--bin-id", 1602 "bin-1", 1603 "--quantity-amount", 1604 "1", 1605 "--quantity-unit", 1606 "each", 1607 "--price-amount", 1608 "6", 1609 "--price-currency", 1610 "USD", 1611 "--price-per-amount", 1612 "1", 1613 "--price-per-unit", 1614 "each", 1615 "--available", 1616 "10", 1617 ]); 1618 assert_eq!(listing["operation_id"], "listing.create"); 1619 assert_eq!(listing["result"]["seller_pubkey"], first_pubkey); 1620 assert_eq!(listing["result"]["farm_d_tag"], farm_d_tag); 1621 1622 let farm_before_dry_run = fs::read_to_string(farm_path).expect("farm before dry-run rebind"); 1623 let dry_rebind = sandbox.json_success(&[ 1624 "--format", 1625 "json", 1626 "--dry-run", 1627 "farm", 1628 "rebind", 1629 second_account_id, 1630 ]); 1631 assert_eq!(dry_rebind["operation_id"], "farm.rebind"); 1632 assert_eq!(dry_rebind["result"]["state"], "dry_run"); 1633 assert_eq!( 1634 dry_rebind["result"]["from_seller_account_id"], 1635 first_account_id 1636 ); 1637 assert_eq!(dry_rebind["result"]["from_seller_pubkey"], first_pubkey); 1638 assert_eq!( 1639 dry_rebind["result"]["to_seller_account_id"], 1640 second_account_id 1641 ); 1642 let second_pubkey = dry_rebind["result"]["to_seller_pubkey"] 1643 .as_str() 1644 .expect("second pubkey"); 1645 assert_eq!(dry_rebind["result"]["to_seller_pubkey"], second_pubkey); 1646 assert_eq!(dry_rebind["result"]["seller_pubkey_changed"], true); 1647 assert_eq!(dry_rebind["result"]["publication_state_action"], "cleared"); 1648 assert_eq!( 1649 fs::read_to_string(farm_path).expect("farm after dry-run rebind"), 1650 farm_before_dry_run 1651 ); 1652 1653 let (unapproved_output, unapproved) = 1654 sandbox.json_output(&["--format", "json", "farm", "rebind", second_account_id]); 1655 assert!(!unapproved_output.status.success()); 1656 assert_eq!(unapproved["operation_id"], "farm.rebind"); 1657 assert_eq!(unapproved["errors"][0]["code"], "approval_required"); 1658 1659 let rebound = sandbox.json_success(&[ 1660 "--format", 1661 "json", 1662 "--approval-token", 1663 "approve", 1664 "farm", 1665 "rebind", 1666 second_account_id, 1667 ]); 1668 assert_eq!(rebound["operation_id"], "farm.rebind"); 1669 assert_eq!(rebound["result"]["state"], "rebound"); 1670 assert_eq!( 1671 rebound["result"]["config"]["seller_account_id"], 1672 second_account_id 1673 ); 1674 assert_eq!(rebound["result"]["config"]["seller_pubkey"], second_pubkey); 1675 assert_eq!(rebound["result"]["config"]["farm_d_tag"], farm_d_tag); 1676 assert_eq!(rebound["result"]["config"]["name"], "Green Farm"); 1677 assert_eq!(rebound["result"]["config"]["location_primary"], "farmstand"); 1678 assert_eq!(rebound["result"]["config"]["delivery_method"], "pickup"); 1679 assert_eq!(rebound["result"]["publication_state_action"], "cleared"); 1680 1681 let rebound_get = sandbox.json_success(&["--format", "json", "farm", "get"]); 1682 assert_eq!( 1683 rebound_get["result"]["document"]["selection"]["seller_account_id"], 1684 second_account_id 1685 ); 1686 assert_eq!( 1687 rebound_get["result"]["document"]["publication"]["profile_state"], 1688 "not_published" 1689 ); 1690 assert_eq!( 1691 rebound_get["result"]["document"]["publication"]["farm_state"], 1692 "not_published" 1693 ); 1694 } 1695 1696 #[test] 1697 fn missing_farm_bound_seller_blocks_listing_create_and_guides_setup_repair() { 1698 let sandbox = RadrootsCliSandbox::new(); 1699 let first = sandbox.json_success(&["--format", "json", "account", "create"]); 1700 let first_account_id = first["result"]["account"]["id"] 1701 .as_str() 1702 .expect("first account id"); 1703 sandbox.json_success(&[ 1704 "--format", 1705 "json", 1706 "farm", 1707 "create", 1708 "--name", 1709 "Missing Seller Farm", 1710 "--location", 1711 "farmstand", 1712 "--country", 1713 "US", 1714 "--delivery-method", 1715 "pickup", 1716 ]); 1717 let second = sandbox.json_success(&["--format", "json", "account", "create"]); 1718 let second_account_id = second["result"]["account"]["id"] 1719 .as_str() 1720 .expect("second account id"); 1721 sandbox.json_success(&[ 1722 "--format", 1723 "json", 1724 "account", 1725 "selection", 1726 "update", 1727 second_account_id, 1728 ]); 1729 sandbox.json_success(&[ 1730 "--format", 1731 "json", 1732 "--approval-token", 1733 "approve", 1734 "account", 1735 "remove", 1736 first_account_id, 1737 ]); 1738 1739 let updated = sandbox.json_success(&[ 1740 "--format", 1741 "json", 1742 "farm", 1743 "profile", 1744 "update", 1745 "--field", 1746 "name", 1747 "--value", 1748 "Missing Seller Farm Updated", 1749 ]); 1750 assert_eq!(updated["operation_id"], "farm.profile.update"); 1751 assert_contains(&updated["result"]["reason"], "farm-bound seller account"); 1752 assert_action_present(&updated, "radroots account import <path>"); 1753 assert_action_present(&updated, "radroots farm rebind <selector>"); 1754 1755 let listing_path = sandbox.root().join("missing-seller-listing.toml"); 1756 let (listing_output, listing) = sandbox.json_output(&[ 1757 "--format", 1758 "json", 1759 "listing", 1760 "create", 1761 "--output", 1762 listing_path.to_string_lossy().as_ref(), 1763 "--key", 1764 "missing-seller-eggs", 1765 "--title", 1766 "Missing Seller Eggs", 1767 "--category", 1768 "eggs", 1769 "--summary", 1770 "Fresh eggs", 1771 "--bin-id", 1772 "bin-1", 1773 "--quantity-amount", 1774 "1", 1775 "--quantity-unit", 1776 "each", 1777 "--price-amount", 1778 "6", 1779 "--price-currency", 1780 "USD", 1781 "--price-per-amount", 1782 "1", 1783 "--price-per-unit", 1784 "each", 1785 "--available", 1786 "10", 1787 ]); 1788 assert!(!listing_output.status.success()); 1789 assert_eq!(listing["operation_id"], "listing.create"); 1790 assert_eq!(listing["errors"][0]["code"], "account_unresolved"); 1791 assert_contains( 1792 &listing["errors"][0]["message"], 1793 "farm-bound seller account", 1794 ); 1795 assert_eq!( 1796 listing["errors"][0]["detail"]["seller_actor_source"], 1797 "farm_config" 1798 ); 1799 assert_eq!( 1800 listing["errors"][0]["detail"]["farm_bound_seller_account_id"], 1801 first_account_id 1802 ); 1803 assert_next_action_present(&listing, "radroots account import <path>"); 1804 assert_next_action_present(&listing, "radroots farm rebind <selector>"); 1805 assert!(!listing_path.exists()); 1806 } 1807 1808 #[test] 1809 fn farm_rebind_allows_watch_only_target_and_attach_secret_recovers_publish() { 1810 let sandbox = RadrootsCliSandbox::new(); 1811 sandbox.json_success(&["--format", "json", "account", "create"]); 1812 sandbox.json_success(&[ 1813 "--format", 1814 "json", 1815 "farm", 1816 "create", 1817 "--name", 1818 "Watch Rebind Farm", 1819 "--location", 1820 "farmstand", 1821 "--country", 1822 "US", 1823 "--delivery-method", 1824 "pickup", 1825 ]); 1826 let watch_identity = identity_secret(56); 1827 let watch_public = watch_identity.to_public(); 1828 let public_identity_file = 1829 write_public_identity_profile(&sandbox, "watch-rebind-public", &watch_public); 1830 let secret_identity_file = 1831 write_secret_identity_profile(&sandbox, "watch-rebind-secret", &watch_identity); 1832 let imported = sandbox.json_success(&[ 1833 "--format", 1834 "json", 1835 "--approval-token", 1836 "approve", 1837 "account", 1838 "import", 1839 public_identity_file.to_string_lossy().as_ref(), 1840 ]); 1841 let watch_account_id = imported["result"]["account"]["id"] 1842 .as_str() 1843 .expect("watch account id"); 1844 assert_eq!(imported["result"]["account"]["custody"], "watch_only"); 1845 1846 let rebound = sandbox.json_success(&[ 1847 "--format", 1848 "json", 1849 "--approval-token", 1850 "approve", 1851 "farm", 1852 "rebind", 1853 watch_account_id, 1854 ]); 1855 assert_eq!(rebound["operation_id"], "farm.rebind"); 1856 assert_eq!( 1857 rebound["result"]["config"]["seller_account_id"], 1858 watch_account_id 1859 ); 1860 1861 let readiness = sandbox.json_success(&[ 1862 "--format", 1863 "json", 1864 "--relay", 1865 "ws://127.0.0.1:9", 1866 "farm", 1867 "readiness", 1868 "check", 1869 ]); 1870 assert_eq!(readiness["operation_id"], "farm.readiness.check"); 1871 assert_eq!(readiness["result"]["publish_state"], "unconfigured"); 1872 assert_eq!( 1873 readiness["result"]["missing"][0], 1874 "Write-capable farm-bound seller account" 1875 ); 1876 assert_action_present( 1877 &readiness, 1878 format!("radroots account attach-secret {watch_account_id} <path>").as_str(), 1879 ); 1880 1881 let (publish_output, publish) = sandbox.json_output(&[ 1882 "--format", 1883 "json", 1884 "--relay", 1885 "ws://127.0.0.1:9", 1886 "--dry-run", 1887 "farm", 1888 "publish", 1889 ]); 1890 assert!(!publish_output.status.success()); 1891 assert_eq!(publish["operation_id"], "farm.publish"); 1892 assert_eq!(publish["errors"][0]["code"], "account_watch_only"); 1893 1894 sandbox.json_success(&[ 1895 "--format", 1896 "json", 1897 "--approval-token", 1898 "approve", 1899 "account", 1900 "attach-secret", 1901 watch_account_id, 1902 secret_identity_file.to_string_lossy().as_ref(), 1903 ]); 1904 let recovered = sandbox.json_success(&[ 1905 "--format", 1906 "json", 1907 "--relay", 1908 "ws://127.0.0.1:9", 1909 "--dry-run", 1910 "farm", 1911 "publish", 1912 ]); 1913 assert_eq!(recovered["operation_id"], "farm.publish"); 1914 assert_eq!(recovered["result"]["state"], "dry_run"); 1915 assert_eq!(recovered["result"]["seller_account_id"], watch_account_id); 1916 assert_eq!( 1917 recovered["result"]["seller_pubkey"], 1918 watch_public.public_key_hex 1919 ); 1920 } 1921 1922 #[test] 1923 fn local_seller_publish_commands_attempt_configured_relay() { 1924 let sandbox = RadrootsCliSandbox::new(); 1925 sandbox.json_success(&["--format", "json", "account", "create"]); 1926 let farm = sandbox.json_success(&[ 1927 "--format", 1928 "json", 1929 "farm", 1930 "create", 1931 "--name", 1932 "Green Farm", 1933 "--location", 1934 "farmstand", 1935 "--country", 1936 "US", 1937 "--delivery-method", 1938 "pickup", 1939 ]); 1940 let farm_d_tag = farm["result"]["config"]["farm_d_tag"] 1941 .as_str() 1942 .expect("farm d tag"); 1943 let relay = "ws://127.0.0.1:9"; 1944 1945 let (farm_output, farm_value) = sandbox.json_output(&[ 1946 "--format", 1947 "json", 1948 "--relay", 1949 relay, 1950 "--approval-token", 1951 "approve", 1952 "farm", 1953 "publish", 1954 ]); 1955 assert!(!farm_output.status.success()); 1956 assert_eq!(farm_value["operation_id"], "farm.publish"); 1957 assert_eq!(farm_value["result"], serde_json::Value::Null); 1958 assert_eq!(farm_value["errors"][0]["code"], "network_unavailable"); 1959 assert_eq!(farm_value["errors"][0]["detail"]["class"], "network"); 1960 assert_contains( 1961 &farm_value["errors"][0]["message"], 1962 "SDK relay publish did not reach accepted quorum", 1963 ); 1964 assert_eq!( 1965 farm_value["errors"][0]["detail"]["source"], 1966 "SDK farm publish ยท configured signer" 1967 ); 1968 assert_eq!( 1969 farm_value["errors"][0]["detail"]["farm"]["target_relays"][0], 1970 relay 1971 ); 1972 assert_eq!( 1973 farm_value["errors"][0]["detail"]["farm"]["failed_relays"][0]["relay"], 1974 relay 1975 ); 1976 assert_no_removed_command_reference(&farm_value, &["farm", "publish"]); 1977 assert_no_daemon_runtime_reference(&farm_value, &["farm", "publish"]); 1978 1979 let listing_file = create_listing_draft(&sandbox, "direct-relay-attempt"); 1980 make_listing_publishable(&listing_file, farm_d_tag); 1981 let listing_file_arg = listing_file.to_string_lossy(); 1982 1983 let (publish_output, publish_value) = sandbox.json_output(&[ 1984 "--format", 1985 "json", 1986 "--relay", 1987 relay, 1988 "--approval-token", 1989 "approve", 1990 "listing", 1991 "publish", 1992 listing_file_arg.as_ref(), 1993 ]); 1994 assert!(!publish_output.status.success()); 1995 assert_eq!(publish_value["operation_id"], "listing.publish"); 1996 assert_eq!(publish_value["result"], serde_json::Value::Null); 1997 assert_eq!(publish_value["errors"][0]["code"], "network_unavailable"); 1998 assert_eq!(publish_value["errors"][0]["detail"]["class"], "network"); 1999 assert_contains( 2000 &publish_value["errors"][0]["message"], 2001 "SDK relay publish did not reach accepted quorum", 2002 ); 2003 assert_no_removed_command_reference(&publish_value, &["listing", "publish"]); 2004 assert_no_daemon_runtime_reference(&publish_value, &["listing", "publish"]); 2005 assert_eq!( 2006 publish_value["errors"][0]["detail"]["target_relays"][0], 2007 relay 2008 ); 2009 assert_eq!( 2010 publish_value["errors"][0]["detail"]["connected_relays"] 2011 .as_array() 2012 .expect("connected relays") 2013 .len(), 2014 1 2015 ); 2016 assert_eq!( 2017 publish_value["errors"][0]["detail"]["failed_relays"] 2018 .as_array() 2019 .expect("failed relays") 2020 .len(), 2021 1 2022 ); 2023 2024 let (archive_output, archive_value) = sandbox.json_output(&[ 2025 "--format", 2026 "json", 2027 "--relay", 2028 relay, 2029 "--approval-token", 2030 "approve", 2031 "listing", 2032 "archive", 2033 listing_file_arg.as_ref(), 2034 ]); 2035 assert!(!archive_output.status.success()); 2036 assert_eq!(archive_value["operation_id"], "listing.archive"); 2037 assert_eq!(archive_value["result"], serde_json::Value::Null); 2038 assert_eq!(archive_value["errors"][0]["code"], "network_unavailable"); 2039 assert_eq!(archive_value["errors"][0]["detail"]["class"], "network"); 2040 assert_contains( 2041 &archive_value["errors"][0]["message"], 2042 "SDK relay publish did not reach accepted quorum", 2043 ); 2044 assert_no_removed_command_reference(&archive_value, &["listing", "archive"]); 2045 assert_no_daemon_runtime_reference(&archive_value, &["listing", "archive"]); 2046 assert_eq!( 2047 archive_value["errors"][0]["detail"]["target_relays"][0], 2048 relay 2049 ); 2050 assert_eq!( 2051 archive_value["errors"][0]["detail"]["connected_relays"] 2052 .as_array() 2053 .expect("connected relays") 2054 .len(), 2055 1 2056 ); 2057 assert_eq!( 2058 archive_value["errors"][0]["detail"]["failed_relays"] 2059 .as_array() 2060 .expect("failed relays") 2061 .len(), 2062 1 2063 ); 2064 2065 seed_orderable_listing(&sandbox, LISTING_ADDR); 2066 sandbox.json_success(&["--format", "json", "basket", "create", "direct_order"]); 2067 sandbox.json_success(&[ 2068 "--format", 2069 "json", 2070 "basket", 2071 "item", 2072 "add", 2073 "direct_order", 2074 "--listing-addr", 2075 LISTING_ADDR, 2076 "--bin-id", 2077 "bin-1", 2078 "--quantity", 2079 "1", 2080 ]); 2081 let quote = sandbox.json_success(&[ 2082 "--format", 2083 "json", 2084 "basket", 2085 "quote", 2086 "create", 2087 "direct_order", 2088 ]); 2089 let order_id = quote["result"]["quote"]["order_id"] 2090 .as_str() 2091 .expect("order id"); 2092 let (order_output, order_value) = sandbox.json_output(&[ 2093 "--format", 2094 "json", 2095 "--relay", 2096 relay, 2097 "--approval-token", 2098 "approve", 2099 "order", 2100 "submit", 2101 order_id, 2102 ]); 2103 assert!(!order_output.status.success()); 2104 assert_eq!(order_value["operation_id"], "order.submit"); 2105 assert_eq!(order_value["result"], serde_json::Value::Null); 2106 assert_eq!(order_value["errors"][0]["code"], "operation_unavailable"); 2107 assert_eq!( 2108 order_value["errors"][0]["detail"]["issues"][0]["field"], 2109 "order.listing_addr" 2110 ); 2111 assert_contains( 2112 &order_value["errors"][0]["detail"]["issues"][0]["message"], 2113 "local market freshness", 2114 ); 2115 assert_no_removed_command_reference(&order_value, &["order", "submit"]); 2116 assert_no_daemon_runtime_reference(&order_value, &["order", "submit"]); 2117 } 2118 2119 #[test] 2120 fn local_order_event_list_attempts_configured_direct_relay() { 2121 let sandbox = RadrootsCliSandbox::new(); 2122 sandbox.json_success(&["--format", "json", "account", "create"]); 2123 let relay = "ws://127.0.0.1:9"; 2124 2125 let (output, value) = sandbox.json_output(&[ 2126 "--format", "json", "--relay", relay, "order", "event", "list", 2127 ]); 2128 2129 assert!(!output.status.success()); 2130 assert_direct_relay_connection_failure(&value, "order.event.list", &["order", "event", "list"]); 2131 assert_eq!(value["errors"][0]["detail"]["state"], "unavailable"); 2132 assert_eq!(value["errors"][0]["detail"]["target_relays"][0], relay); 2133 assert_eq!( 2134 value["errors"][0]["detail"]["connected_relays"] 2135 .as_array() 2136 .expect("connected relays") 2137 .len(), 2138 0 2139 ); 2140 assert_eq!( 2141 value["errors"][0]["detail"]["failed_relays"] 2142 .as_array() 2143 .expect("failed relays") 2144 .len(), 2145 1 2146 ); 2147 assert_contains( 2148 &value["errors"][0]["detail"]["failed_relays"][0]["relay"], 2149 "127.0.0.1:9", 2150 ); 2151 assert_eq!(value["errors"][0]["detail"]["fetched_count"], 0); 2152 assert_eq!(value["errors"][0]["detail"]["decoded_count"], 0); 2153 assert_eq!(value["errors"][0]["detail"]["skipped_count"], 0); 2154 } 2155 2156 #[test] 2157 fn local_order_failure_envelopes_are_structured_and_actionable() { 2158 let sandbox = RadrootsCliSandbox::new(); 2159 let watch_args = ["--format", "json", "order", "event", "watch", "ord_missing"]; 2160 let (watch_output, watch) = sandbox.json_output(&watch_args); 2161 assert!(!watch_output.status.success()); 2162 assert_eq!(watch["operation_id"], "order.event.watch"); 2163 assert_eq!(watch["result"], Value::Null); 2164 assert_eq!(watch["errors"][0]["code"], "not_implemented"); 2165 assert_eq!(watch["errors"][0]["detail"]["state"], "not_implemented"); 2166 assert_eq!(watch["errors"][0]["detail"]["order_id"], "ord_missing"); 2167 assert_eq!( 2168 watch["next_actions"][0]["command"], 2169 "radroots order status get ord_missing" 2170 ); 2171 assert_no_daemon_runtime_reference(&watch, &watch_args); 2172 2173 let submit_args = [ 2174 "--format", 2175 "json", 2176 "--publish-transport", 2177 "direct_nostr_relay", 2178 "--dry-run", 2179 "order", 2180 "submit", 2181 "ord_missing", 2182 ]; 2183 let (submit_output, submit) = sandbox.json_output(&submit_args); 2184 assert!(!submit_output.status.success()); 2185 assert_eq!(submit["errors"][0]["code"], "not_found"); 2186 assert_eq!(submit["errors"][0]["detail"]["state"], "missing"); 2187 assert_eq!(submit["errors"][0]["detail"]["order_id"], "ord_missing"); 2188 assert_eq!(submit["next_actions"][0]["command"], "radroots order list"); 2189 assert_eq!( 2190 submit["next_actions"][1]["command"], 2191 "radroots basket create" 2192 ); 2193 assert_no_daemon_runtime_reference(&submit, &submit_args); 2194 2195 let status_args = ["--format", "json", "order", "status", "get", "ord_missing"]; 2196 let status = sandbox.json_success(&status_args); 2197 assert_eq!(status["operation_id"], "order.status.get"); 2198 assert_eq!(status["result"]["state"], "missing"); 2199 assert_eq!(status["result"]["source"], "SDK local order projection"); 2200 assert_eq!( 2201 status["result"]["actor_context_source"], 2202 "sdk_local_projection" 2203 ); 2204 assert_eq!(status["result"]["order_id"], "ord_missing"); 2205 assert_eq!(status["result"]["fetched_count"], 0); 2206 assert_eq!(status["result"]["decoded_count"], 0); 2207 assert_eq!( 2208 status["result"]["reason"], 2209 "no local SDK order events matched `ord_missing`" 2210 ); 2211 assert_no_daemon_runtime_reference(&status, &status_args); 2212 2213 let event_list_no_relay_args = ["--format", "json", "order", "event", "list"]; 2214 let (event_list_no_relay_output, event_list_no_relay) = 2215 sandbox.json_output(&event_list_no_relay_args); 2216 assert!(!event_list_no_relay_output.status.success()); 2217 assert_eq!( 2218 event_list_no_relay["errors"][0]["code"], 2219 "operation_unavailable" 2220 ); 2221 assert_eq!( 2222 event_list_no_relay["errors"][0]["detail"]["state"], 2223 "unconfigured" 2224 ); 2225 assert_eq!( 2226 event_list_no_relay["next_actions"][0]["command"], 2227 "radroots --relay wss://relay.example.com order event list" 2228 ); 2229 assert_no_daemon_runtime_reference(&event_list_no_relay, &event_list_no_relay_args); 2230 2231 let event_list_no_account_args = [ 2232 "--format", 2233 "json", 2234 "--relay", 2235 "ws://127.0.0.1:9", 2236 "order", 2237 "event", 2238 "list", 2239 ]; 2240 let (event_list_no_account_output, event_list_no_account) = 2241 sandbox.json_output(&event_list_no_account_args); 2242 assert!(!event_list_no_account_output.status.success()); 2243 assert_eq!( 2244 event_list_no_account["errors"][0]["code"], 2245 "operation_unavailable" 2246 ); 2247 assert_eq!( 2248 event_list_no_account["errors"][0]["detail"]["state"], 2249 "unconfigured" 2250 ); 2251 assert_eq!( 2252 event_list_no_account["next_actions"][0]["command"], 2253 "radroots account create" 2254 ); 2255 assert_no_daemon_runtime_reference(&event_list_no_account, &event_list_no_account_args); 2256 2257 let accept_args = [ 2258 "--format", 2259 "json", 2260 "--publish-transport", 2261 "direct_nostr_relay", 2262 "--dry-run", 2263 "order", 2264 "accept", 2265 "ord_missing", 2266 ]; 2267 let (accept_output, accept) = sandbox.json_output(&accept_args); 2268 assert!(!accept_output.status.success()); 2269 assert_eq!(accept["errors"][0]["code"], "operation_unavailable"); 2270 assert_eq!(accept["errors"][0]["detail"]["state"], "unconfigured"); 2271 assert_eq!(accept["errors"][0]["detail"]["order_id"], "ord_missing"); 2272 assert_eq!(accept["errors"][0]["detail"]["decision"], "accepted"); 2273 assert_no_daemon_runtime_reference(&accept, &accept_args); 2274 2275 let decline_args = [ 2276 "--format", 2277 "json", 2278 "--publish-transport", 2279 "direct_nostr_relay", 2280 "--dry-run", 2281 "order", 2282 "decline", 2283 "ord_missing", 2284 "--reason", 2285 "not available", 2286 ]; 2287 let (decline_output, decline) = sandbox.json_output(&decline_args); 2288 assert!(!decline_output.status.success()); 2289 assert_eq!(decline["errors"][0]["code"], "operation_unavailable"); 2290 assert_eq!(decline["errors"][0]["detail"]["state"], "unconfigured"); 2291 assert_eq!(decline["errors"][0]["detail"]["order_id"], "ord_missing"); 2292 assert_eq!(decline["errors"][0]["detail"]["decision"], "declined"); 2293 assert_no_daemon_runtime_reference(&decline, &decline_args); 2294 } 2295 2296 #[test] 2297 fn watch_only_farm_publish_dry_run_fails_as_account_watch_only() { 2298 let sandbox = RadrootsCliSandbox::new(); 2299 let public_identity = identity_public(13); 2300 let public_identity_file = 2301 write_public_identity_profile(&sandbox, "watch-only-farm", &public_identity); 2302 sandbox.json_success(&[ 2303 "--format", 2304 "json", 2305 "--approval-token", 2306 "approve", 2307 "account", 2308 "import", 2309 "--default", 2310 public_identity_file.to_string_lossy().as_ref(), 2311 ]); 2312 sandbox.json_success(&[ 2313 "--format", 2314 "json", 2315 "farm", 2316 "create", 2317 "--name", 2318 "Green Farm", 2319 "--location", 2320 "farmstand", 2321 "--country", 2322 "US", 2323 "--delivery-method", 2324 "pickup", 2325 ]); 2326 2327 let (output, value) = sandbox.json_output(&[ 2328 "--format", 2329 "json", 2330 "--relay", 2331 "ws://127.0.0.1:9", 2332 "--dry-run", 2333 "farm", 2334 "publish", 2335 ]); 2336 2337 assert!(!output.status.success()); 2338 assert_eq!(value["operation_id"], "farm.publish"); 2339 assert_eq!(value["errors"][0]["code"], "account_watch_only"); 2340 assert_eq!(value["errors"][0]["detail"]["class"], "account"); 2341 } 2342 2343 #[test] 2344 fn watch_only_listing_publish_fails_as_account_watch_only() { 2345 let sandbox = RadrootsCliSandbox::new(); 2346 let public_identity = identity_public(12); 2347 let public_identity_file = 2348 write_public_identity_profile(&sandbox, "watch-only-publish", &public_identity); 2349 sandbox.json_success(&[ 2350 "--format", 2351 "json", 2352 "--approval-token", 2353 "approve", 2354 "account", 2355 "import", 2356 "--default", 2357 public_identity_file.to_string_lossy().as_ref(), 2358 ]); 2359 let listing_file = create_listing_draft(&sandbox, "watch-only-publish"); 2360 make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw"); 2361 2362 let (output, value) = sandbox.json_output(&[ 2363 "--format", 2364 "json", 2365 "--relay", 2366 "ws://127.0.0.1:9", 2367 "--approval-token", 2368 "approve", 2369 "listing", 2370 "publish", 2371 listing_file.to_string_lossy().as_ref(), 2372 ]); 2373 2374 assert!(!output.status.success()); 2375 assert_eq!(value["operation_id"], "listing.publish"); 2376 assert_eq!(value["result"], serde_json::Value::Null); 2377 assert_eq!(value["errors"][0]["code"], "account_watch_only"); 2378 assert_eq!(value["errors"][0]["exit_code"], 7); 2379 assert_eq!(value["errors"][0]["detail"]["class"], "account"); 2380 assert_contains(&value["errors"][0]["message"], "resolved account"); 2381 assert_contains(&value["errors"][0]["message"], "watch_only"); 2382 } 2383 2384 #[test] 2385 fn watch_only_listing_update_dry_run_fails_as_account_watch_only() { 2386 let sandbox = RadrootsCliSandbox::new(); 2387 let public_identity = identity_public(13); 2388 let public_identity_file = 2389 write_public_identity_profile(&sandbox, "watch-only-update", &public_identity); 2390 sandbox.json_success(&[ 2391 "--format", 2392 "json", 2393 "--approval-token", 2394 "approve", 2395 "account", 2396 "import", 2397 "--default", 2398 public_identity_file.to_string_lossy().as_ref(), 2399 ]); 2400 let listing_file = create_listing_draft(&sandbox, "watch-only-update"); 2401 make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw"); 2402 2403 let (output, value) = sandbox.json_output(&[ 2404 "--format", 2405 "json", 2406 "--dry-run", 2407 "listing", 2408 "update", 2409 listing_file.to_string_lossy().as_ref(), 2410 ]); 2411 2412 assert!(!output.status.success()); 2413 assert_eq!(value["operation_id"], "listing.update"); 2414 assert_eq!(value["result"], serde_json::Value::Null); 2415 assert_eq!(value["errors"][0]["code"], "account_watch_only"); 2416 assert_eq!(value["errors"][0]["exit_code"], 7); 2417 assert_eq!(value["errors"][0]["detail"]["class"], "account"); 2418 assert_contains(&value["errors"][0]["message"], "watch_only"); 2419 } 2420 2421 #[cfg(unix)] 2422 #[test] 2423 fn myc_listing_publish_does_not_fallback_to_local_account() { 2424 let sandbox = RadrootsCliSandbox::new(); 2425 sandbox.json_success(&["--format", "json", "account", "create"]); 2426 let listing_file = create_listing_draft(&sandbox, "myc-no-binding"); 2427 make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw"); 2428 let invoked = sandbox.root().join("myc-listing-invoked.txt"); 2429 let myc = sandbox.write_fake_myc( 2430 "myc-listing-deferred", 2431 format!( 2432 "printf invoked > '{}'", 2433 shell_single_quoted(invoked.to_string_lossy().as_ref()) 2434 ) 2435 .as_str(), 2436 ); 2437 configure_myc_mode(&sandbox, &myc); 2438 2439 let (output, value) = sandbox.json_output(&[ 2440 "--format", 2441 "json", 2442 "--approval-token", 2443 "approve", 2444 "listing", 2445 "publish", 2446 listing_file.to_string_lossy().as_ref(), 2447 ]); 2448 2449 assert!(!output.status.success()); 2450 assert_eq!(value["operation_id"], "listing.publish"); 2451 assert_eq!(value["result"], serde_json::Value::Null); 2452 assert_eq!(value["errors"][0]["code"], "signer_unconfigured"); 2453 assert_eq!(value["errors"][0]["exit_code"], 7); 2454 assert_eq!(value["errors"][0]["detail"]["class"], "signer"); 2455 assert_contains( 2456 &value["errors"][0]["message"], 2457 "signer.remote_nip46 binding is missing", 2458 ); 2459 assert!(!invoked.exists(), "target CLI must not execute MYC"); 2460 } 2461 2462 fn configure_myc_mode(sandbox: &RadrootsCliSandbox, executable: &Path) { 2463 sandbox.write_app_config(&format!( 2464 "[signer]\nbackend = \"myc\"\n\n[myc]\nexecutable = \"{}\"\n", 2465 toml_string(executable.display().to_string().as_str()) 2466 )); 2467 } 2468 2469 fn assert_direct_relay_connection_failure( 2470 value: &serde_json::Value, 2471 operation_id: &str, 2472 args: &[&str], 2473 ) { 2474 assert_eq!(value["operation_id"], operation_id); 2475 assert_eq!(value["result"], serde_json::Value::Null); 2476 assert_eq!(value["errors"][0]["code"], "network_unavailable"); 2477 assert_ne!(value["errors"][0]["code"], "operation_unavailable"); 2478 assert_eq!(value["errors"][0]["detail"]["class"], "network"); 2479 assert_contains( 2480 &value["errors"][0]["message"], 2481 "direct relay connection failed", 2482 ); 2483 assert_no_removed_command_reference(value, args); 2484 assert_no_daemon_runtime_reference(value, args); 2485 } 2486 2487 fn assert_relay_url(value: &Value, relay_url: &str) { 2488 let actual = value.as_str().expect("relay url"); 2489 assert!( 2490 actual == relay_url || actual == format!("{relay_url}/"), 2491 "expected relay url `{actual}` to match `{relay_url}`" 2492 ); 2493 } 2494 2495 fn assert_action_present(value: &Value, action: &str) { 2496 assert!( 2497 action_list(value).iter().any(|entry| *entry == action), 2498 "expected action `{action}` in `{}`", 2499 value["result"]["actions"] 2500 ); 2501 } 2502 2503 fn assert_next_action_present(value: &Value, action: &str) { 2504 assert!( 2505 next_action_commands(value) 2506 .iter() 2507 .any(|entry| *entry == action), 2508 "expected next action `{action}` in `{}`", 2509 value["next_actions"] 2510 ); 2511 } 2512 2513 fn assert_action_absent(value: &Value, action: &str) { 2514 assert!( 2515 action_list(value).iter().all(|entry| *entry != action), 2516 "did not expect action `{action}` in `{}`", 2517 value["result"]["actions"] 2518 ); 2519 } 2520 2521 fn action_list(value: &Value) -> Vec<&str> { 2522 value["result"]["actions"] 2523 .as_array() 2524 .expect("actions") 2525 .iter() 2526 .map(|entry| entry.as_str().expect("action")) 2527 .collect() 2528 } 2529 2530 fn next_action_commands(value: &Value) -> Vec<&str> { 2531 value["next_actions"] 2532 .as_array() 2533 .expect("next actions") 2534 .iter() 2535 .filter_map(|entry| entry["command"].as_str()) 2536 .collect() 2537 }