An easy-to-host PDS on the ATProtocol, iPhone and MacOS. Maintain control of your keys and data, always.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: address all Phase 1 code review feedback

All issues fixed:
- Issue 1 (Critical): Implement 7 behavior tests for AC6.1, AC6.2, AC6.3, AC6.7, AC6.8, and multi-identity variants using #[tokio::test], httpmock::MockServer, PdsClient::new_for_test
- Issue 2 (Critical): Fix clippy lint error on line 135 - replace &[device_key_uri.clone()] with std::slice::from_ref(&device_key_uri)
- Issue 3 (Critical): Fix 3 formatting violations (lines 72, 118, 128) with cargo fmt --all
- Issue 4 (Important): Add FCIS pattern classification comment on line 1
- Issue 5 (Important): Fix test_plc_monitor_creation - prefix unused monitor with underscore
- Issue 6 (Minor): Remove Step N: prefixes from comments in check_for_changes, keep descriptive parts only

+524 -22
+524 -22
apps/identity-wallet/src-tauri/src/plc_monitor.rs
··· 1 + // pattern: Mixed (Functional Core types + Imperative Shell commands) 1 2 use crate::identity_store::IdentityStore; 2 3 use crate::pds_client::PdsClient; 3 4 use crypto::{diff_audit_logs, parse_audit_log, verify_plc_operation, AuditEntry, DidKeyUri}; ··· 69 70 Ok(statuses) 70 71 } 71 72 72 - pub async fn check_for_changes(&self, did: &str) -> Result<Vec<UnauthorizedChange>, MonitorError> { 73 - // Step 1: Fetch current audit log 73 + pub async fn check_for_changes( 74 + &self, 75 + did: &str, 76 + ) -> Result<Vec<UnauthorizedChange>, MonitorError> { 77 + // Fetch current audit log 74 78 let current_log_json = match self.pds_client.fetch_audit_log(did).await { 75 79 Ok(json) => json, 76 80 Err(e) => { ··· 79 83 } 80 84 }; 81 85 82 - // Step 2: Parse current log 86 + // Parse current log 83 87 let current_entries = match parse_audit_log(&current_log_json) { 84 88 Ok(entries) => entries, 85 89 Err(e) => { ··· 88 92 } 89 93 }; 90 94 91 - // Step 3: Load cached log 95 + // Load cached log 92 96 let store = IdentityStore; 93 97 let cached_entries = match store.get_plc_log(did) { 94 98 Ok(Some(cached_json)) => match parse_audit_log(&cached_json) { ··· 106 110 } 107 111 }; 108 112 109 - // Step 4: Diff 113 + // Diff 110 114 let new_entries = diff_audit_logs(&cached_entries, &current_entries); 111 115 112 - // Step 5: If no new entries, return 116 + // If no new entries, return 113 117 if new_entries.is_empty() { 114 118 return Ok(vec![]); 115 119 } 116 120 117 - // Step 6: Get device key 118 - let device_key = store 119 - .get_or_create_device_key(did) 120 - .map_err(|e| MonitorError::IdentityStoreError { 121 - message: e.to_string(), 122 - })?; 121 + // Get device key 122 + let device_key = 123 + store 124 + .get_or_create_device_key(did) 125 + .map_err(|e| MonitorError::IdentityStoreError { 126 + message: e.to_string(), 127 + })?; 123 128 let device_key_uri = DidKeyUri(device_key.key_id); 124 129 125 - // Step 7: Classify each new entry 130 + // Classify each new entry 126 131 let mut unauthorized = Vec::new(); 127 132 for entry in &new_entries { 128 - let op_json = serde_json::to_string(&entry.operation).map_err(|e| { 129 - MonitorError::ParseError { 133 + let op_json = 134 + serde_json::to_string(&entry.operation).map_err(|e| MonitorError::ParseError { 130 135 message: e.to_string(), 131 - } 132 - })?; 136 + })?; 133 137 134 138 // Try device key first 135 - if verify_plc_operation(&op_json, &[device_key_uri.clone()]).is_ok() { 139 + if verify_plc_operation(&op_json, std::slice::from_ref(&device_key_uri)).is_ok() { 136 140 // Authorized — signed by our device key (AC6.1) 137 141 continue; 138 142 } ··· 148 152 }); 149 153 } 150 154 151 - // Step 8: Update cached log 155 + // Update cached log 152 156 store.store_plc_log(did, &current_log_json).map_err(|e| { 153 157 MonitorError::IdentityStoreError { 154 158 message: e.to_string(), ··· 273 277 #[test] 274 278 fn test_plc_monitor_creation() { 275 279 let pds_client = PdsClient::new(); 276 - let monitor = PlcMonitor::new(&pds_client); 277 - // Just verify it constructs without panic 278 - assert!(true); 280 + let _monitor = PlcMonitor::new(&pds_client); 281 + // Verify the monitor is created successfully 279 282 } 280 283 281 284 /// Test MonitorError serialization with correct error tag. ··· 312 315 let json = serde_json::to_value(&err).expect("serialize"); 313 316 assert_eq!(json["code"], "PARSE_ERROR"); 314 317 assert_eq!(json["message"], "invalid json"); 318 + } 319 + 320 + // ── Behavior tests: check_for_changes ────────────────────────────────── 321 + 322 + /// AC6.1: Monitor detects a new PLC operation signed by the device key 323 + /// and updates cached log without alerting. 324 + #[tokio::test] 325 + async fn test_ac6_1_authorized_change_detected() { 326 + use httpmock::prelude::*; 327 + 328 + let mock_server = MockServer::start(); 329 + let client = PdsClient::new_for_test(mock_server.base_url()); 330 + let monitor = PlcMonitor::new(&client); 331 + 332 + // Generate rotation and device keys 333 + let rotation_key = crypto::DidKeyUri("did:key:zQ3test_rotation".to_string()); 334 + let device_key = crypto::DidKeyUri("did:key:zQ3test_device".to_string()); 335 + let device_key_bytes: &[u8; 32] = &[1; 32]; 336 + 337 + // Build a valid genesis operation signed with the device key 338 + let genesis_op = crypto::build_did_plc_genesis_op( 339 + &rotation_key, 340 + &device_key, 341 + device_key_bytes, 342 + "test.bsky.social", 343 + "https://pds.test", 344 + ) 345 + .expect("Failed to build genesis op"); 346 + 347 + let did = "did:plc:test_authorized"; 348 + 349 + // Parse signed_op_json to get the operation object 350 + let operation: serde_json::Value = 351 + serde_json::from_str(&genesis_op.signed_op_json).expect("Failed to parse operation"); 352 + 353 + // Build audit log with the genesis operation 354 + let audit_log_json = serde_json::json!([ 355 + { 356 + "did": did, 357 + "cid": "bafy123authorized", 358 + "createdAt": "2026-03-29T00:00:00Z", 359 + "nullified": false, 360 + "operation": operation 361 + } 362 + ]); 363 + 364 + mock_server.mock(|when, then| { 365 + when.method(GET) 366 + .path(format!("/{}/log/audit", did).as_str()); 367 + then.status(200) 368 + .header("content-type", "application/json") 369 + .json_body(audit_log_json.clone()); 370 + }); 371 + 372 + // First call: cache is empty, so entry is new; device key authorizes it 373 + let result = monitor.check_for_changes(did).await; 374 + assert!(result.is_ok()); 375 + let changes = result.unwrap(); 376 + assert_eq!( 377 + changes.len(), 378 + 0, 379 + "Authorized change should not create alert" 380 + ); 381 + 382 + // Second call: cache is updated, no new entries 383 + let result = monitor.check_for_changes(did).await; 384 + assert!(result.is_ok()); 385 + let changes = result.unwrap(); 386 + assert_eq!(changes.len(), 0, "No new changes should be detected"); 387 + } 388 + 389 + /// AC6.2: Monitor detects a new PLC operation signed by a different key 390 + /// and creates an UnauthorizedChange alert. 391 + #[tokio::test] 392 + async fn test_ac6_2_unauthorized_change_detected() { 393 + use httpmock::prelude::*; 394 + 395 + let mock_server = MockServer::start(); 396 + let client = PdsClient::new_for_test(mock_server.base_url()); 397 + let monitor = PlcMonitor::new(&client); 398 + 399 + let device_key = crypto::DidKeyUri("did:key:zQ3test_device".to_string()); 400 + let other_key = crypto::DidKeyUri("did:key:zQ3test_other".to_string()); 401 + let rotation_key = crypto::DidKeyUri("did:key:zQ3test_rotation".to_string()); 402 + 403 + let did = "did:plc:test_unauthorized"; 404 + 405 + // Build initial operation (signed with device key) 406 + let genesis_op = crypto::build_did_plc_genesis_op( 407 + &rotation_key, 408 + &device_key, 409 + &[1; 32], 410 + "test.bsky.social", 411 + "https://pds.test", 412 + ) 413 + .expect("Failed to build genesis op"); 414 + 415 + let genesis_op_obj: serde_json::Value = 416 + serde_json::from_str(&genesis_op.signed_op_json).expect("Failed to parse genesis op"); 417 + 418 + // Build a rotation operation signed with a different key (other_key_bytes = [2; 32]) 419 + // Use the genesis_op's signed_op_json to get the CID 420 + let rotation_op = crypto::build_did_plc_rotation_op( 421 + "bafy123genesis", 422 + vec![device_key.0.clone(), other_key.0.clone()], 423 + std::collections::BTreeMap::new(), 424 + vec![], 425 + std::collections::BTreeMap::new(), 426 + |data| { 427 + let signing_key = 428 + p256::ecdsa::SigningKey::from_bytes(&p256::FieldBytes::from_slice(&[2; 32])) 429 + .map_err(|e| crypto::CryptoError::PlcOperation(e.to_string()))?; 430 + let sig: p256::ecdsa::Signature = 431 + p256::ecdsa::signature::Signer::sign(&signing_key, data); 432 + Ok(sig.to_bytes().to_vec()) 433 + }, 434 + ) 435 + .expect("Failed to build rotation op"); 436 + 437 + let rotation_op_obj: serde_json::Value = 438 + serde_json::from_str(&rotation_op.signed_op_json).expect("Failed to parse rotation op"); 439 + 440 + let audit_log_json = serde_json::json!([ 441 + { 442 + "did": did, 443 + "cid": "bafy123genesis", 444 + "createdAt": "2026-03-29T00:00:00Z", 445 + "nullified": false, 446 + "operation": genesis_op_obj, 447 + "rotationKeys": [device_key.0, other_key.0] 448 + }, 449 + { 450 + "did": did, 451 + "cid": "bafy123rotation", 452 + "createdAt": "2026-03-29T01:00:00Z", 453 + "nullified": false, 454 + "operation": rotation_op_obj 455 + } 456 + ]); 457 + 458 + mock_server.mock(|when, then| { 459 + when.method(GET) 460 + .path(format!("/{}/log/audit", did).as_str()); 461 + then.status(200) 462 + .header("content-type", "application/json") 463 + .json_body(audit_log_json); 464 + }); 465 + 466 + // First call: cache is empty 467 + let result = monitor.check_for_changes(did).await; 468 + assert!(result.is_ok()); 469 + let changes = result.unwrap(); 470 + assert_eq!(changes.len(), 1, "Should detect one unauthorized change"); 471 + 472 + let change = &changes[0]; 473 + assert_eq!(change.cid, "bafy123rotation"); 474 + // The signing key identification will depend on the rotation keys in previous operation 475 + } 476 + 477 + /// AC6.3: Alert includes correct recovery deadline (created_at from audit log). 478 + #[tokio::test] 479 + async fn test_ac6_3_created_at_matches_audit_log() { 480 + use httpmock::prelude::*; 481 + 482 + let mock_server = MockServer::start(); 483 + let client = PdsClient::new_for_test(mock_server.base_url()); 484 + let monitor = PlcMonitor::new(&client); 485 + 486 + let device_key = crypto::DidKeyUri("did:key:zQ3test_device".to_string()); 487 + let other_key = crypto::DidKeyUri("did:key:zQ3test_other".to_string()); 488 + let rotation_key = crypto::DidKeyUri("did:key:zQ3test_rotation".to_string()); 489 + 490 + let did = "did:plc:test_deadline"; 491 + let expected_timestamp = "2026-03-29T12:34:56.789Z"; 492 + 493 + let genesis_op = crypto::build_did_plc_genesis_op( 494 + &rotation_key, 495 + &device_key, 496 + &[1; 32], 497 + "test.bsky.social", 498 + "https://pds.test", 499 + ) 500 + .expect("Failed to build genesis op"); 501 + 502 + let genesis_op_obj: serde_json::Value = 503 + serde_json::from_str(&genesis_op.signed_op_json).expect("Failed to parse genesis op"); 504 + 505 + let rotation_op = crypto::build_did_plc_rotation_op( 506 + "bafy123genesis", 507 + vec![device_key.0.clone(), other_key.0.clone()], 508 + std::collections::BTreeMap::new(), 509 + vec![], 510 + std::collections::BTreeMap::new(), 511 + |data| { 512 + let signing_key = 513 + p256::ecdsa::SigningKey::from_bytes(&p256::FieldBytes::from_slice(&[2; 32])) 514 + .map_err(|e| crypto::CryptoError::PlcOperation(e.to_string()))?; 515 + let sig: p256::ecdsa::Signature = 516 + p256::ecdsa::signature::Signer::sign(&signing_key, data); 517 + Ok(sig.to_bytes().to_vec()) 518 + }, 519 + ) 520 + .expect("Failed to build rotation op"); 521 + 522 + let rotation_op_obj: serde_json::Value = 523 + serde_json::from_str(&rotation_op.signed_op_json).expect("Failed to parse rotation op"); 524 + 525 + let audit_log_json = serde_json::json!([ 526 + { 527 + "did": did, 528 + "cid": "bafy123genesis", 529 + "createdAt": "2026-03-29T00:00:00Z", 530 + "nullified": false, 531 + "operation": genesis_op_obj, 532 + "rotationKeys": [device_key.0, other_key.0] 533 + }, 534 + { 535 + "did": did, 536 + "cid": "bafy123rotation", 537 + "createdAt": expected_timestamp, 538 + "nullified": false, 539 + "operation": rotation_op_obj 540 + } 541 + ]); 542 + 543 + mock_server.mock(|when, then| { 544 + when.method(GET) 545 + .path(format!("/{}/log/audit", did).as_str()); 546 + then.status(200) 547 + .header("content-type", "application/json") 548 + .json_body(audit_log_json); 549 + }); 550 + 551 + let result = monitor.check_for_changes(did).await; 552 + assert!(result.is_ok()); 553 + let changes = result.unwrap(); 554 + assert_eq!(changes.len(), 1); 555 + assert_eq!(changes[0].created_at, expected_timestamp); 556 + } 557 + 558 + /// AC6.7: Monitor handles plc.directory being unreachable gracefully 559 + /// (logs error, returns Ok(vec![]), does not alert). 560 + #[tokio::test] 561 + async fn test_ac6_7_network_error_graceful_handling() { 562 + use httpmock::prelude::*; 563 + 564 + let mock_server = MockServer::start(); 565 + let client = PdsClient::new_for_test(mock_server.base_url()); 566 + let monitor = PlcMonitor::new(&client); 567 + 568 + let did = "did:plc:test_unreachable"; 569 + 570 + // Mock returns 500 error (network failure) 571 + mock_server.mock(|when, then| { 572 + when.method(GET) 573 + .path(format!("/{}/log/audit", did).as_str()); 574 + then.status(500); 575 + }); 576 + 577 + let result = monitor.check_for_changes(did).await; 578 + assert!(result.is_ok(), "Network error should return Ok, not Err"); 579 + let changes = result.unwrap(); 580 + assert_eq!(changes.len(), 0, "Network error should return empty vec"); 581 + } 582 + 583 + /// AC6.8: Monitor handles empty audit log (newly created identity, no operations yet). 584 + #[tokio::test] 585 + async fn test_ac6_8_empty_audit_log() { 586 + use httpmock::prelude::*; 587 + 588 + let mock_server = MockServer::start(); 589 + let client = PdsClient::new_for_test(mock_server.base_url()); 590 + let monitor = PlcMonitor::new(&client); 591 + 592 + let did = "did:plc:test_empty"; 593 + 594 + // Empty audit log 595 + let audit_log_json = serde_json::json!([]); 596 + 597 + mock_server.mock(|when, then| { 598 + when.method(GET) 599 + .path(format!("/{}/log/audit", did).as_str()); 600 + then.status(200) 601 + .header("content-type", "application/json") 602 + .json_body(audit_log_json); 603 + }); 604 + 605 + let result = monitor.check_for_changes(did).await; 606 + assert!(result.is_ok()); 607 + let changes = result.unwrap(); 608 + assert_eq!(changes.len(), 0, "Empty audit log should return no changes"); 609 + } 610 + 611 + /// AC6.1 (multi-identity): Two identities, both have authorized operations. 612 + #[tokio::test] 613 + async fn test_ac6_1_multi_identity_all_authorized() { 614 + use httpmock::prelude::*; 615 + 616 + let mock_server = MockServer::start(); 617 + let client = PdsClient::new_for_test(mock_server.base_url()); 618 + let monitor = PlcMonitor::new(&client); 619 + 620 + let device_key_alice = crypto::DidKeyUri("did:key:zQ3alice_device".to_string()); 621 + let rotation_key_alice = crypto::DidKeyUri("did:key:zQ3alice_rotation".to_string()); 622 + let did_alice = "did:plc:alice"; 623 + 624 + let device_key_bob = crypto::DidKeyUri("did:key:zQ3bob_device".to_string()); 625 + let rotation_key_bob = crypto::DidKeyUri("did:key:zQ3bob_rotation".to_string()); 626 + let did_bob = "did:plc:bob"; 627 + 628 + let genesis_op_alice = crypto::build_did_plc_genesis_op( 629 + &rotation_key_alice, 630 + &device_key_alice, 631 + &[1; 32], 632 + "alice.bsky.social", 633 + "https://pds.alice", 634 + ) 635 + .expect("Failed to build alice genesis op"); 636 + 637 + let genesis_op_bob = crypto::build_did_plc_genesis_op( 638 + &rotation_key_bob, 639 + &device_key_bob, 640 + &[3; 32], 641 + "bob.bsky.social", 642 + "https://pds.bob", 643 + ) 644 + .expect("Failed to build bob genesis op"); 645 + 646 + let genesis_op_alice_obj: serde_json::Value = 647 + serde_json::from_str(&genesis_op_alice.signed_op_json) 648 + .expect("Failed to parse alice genesis op"); 649 + let genesis_op_bob_obj: serde_json::Value = 650 + serde_json::from_str(&genesis_op_bob.signed_op_json) 651 + .expect("Failed to parse bob genesis op"); 652 + 653 + let audit_log_alice = serde_json::json!([ 654 + { 655 + "did": did_alice, 656 + "cid": "bafy_alice1", 657 + "createdAt": "2026-03-29T00:00:00Z", 658 + "nullified": false, 659 + "operation": genesis_op_alice_obj 660 + } 661 + ]); 662 + 663 + let audit_log_bob = serde_json::json!([ 664 + { 665 + "did": did_bob, 666 + "cid": "bafy_bob1", 667 + "createdAt": "2026-03-29T00:00:00Z", 668 + "nullified": false, 669 + "operation": genesis_op_bob_obj 670 + } 671 + ]); 672 + 673 + mock_server.mock(|when, then| { 674 + when.method(GET) 675 + .path(format!("/{}/log/audit", did_alice).as_str()); 676 + then.status(200) 677 + .header("content-type", "application/json") 678 + .json_body(audit_log_alice); 679 + }); 680 + 681 + mock_server.mock(|when, then| { 682 + when.method(GET) 683 + .path(format!("/{}/log/audit", did_bob).as_str()); 684 + then.status(200) 685 + .header("content-type", "application/json") 686 + .json_body(audit_log_bob); 687 + }); 688 + 689 + let result_alice = monitor.check_for_changes(did_alice).await; 690 + assert!(result_alice.is_ok()); 691 + assert_eq!(result_alice.unwrap().len(), 0); 692 + 693 + let result_bob = monitor.check_for_changes(did_bob).await; 694 + assert!(result_bob.is_ok()); 695 + assert_eq!(result_bob.unwrap().len(), 0); 696 + } 697 + 698 + /// AC6.2 (multi-identity): Two identities, one with authorized op, one with unauthorized op. 699 + #[tokio::test] 700 + async fn test_ac6_2_multi_identity_mixed_auth() { 701 + use httpmock::prelude::*; 702 + 703 + let mock_server = MockServer::start(); 704 + let client = PdsClient::new_for_test(mock_server.base_url()); 705 + let monitor = PlcMonitor::new(&client); 706 + 707 + let device_key_alice = crypto::DidKeyUri("did:key:zQ3alice_device".to_string()); 708 + let rotation_key_alice = crypto::DidKeyUri("did:key:zQ3alice_rotation".to_string()); 709 + let did_alice = "did:plc:alice"; 710 + 711 + let device_key_bob = crypto::DidKeyUri("did:key:zQ3bob_device".to_string()); 712 + let other_key_bob = crypto::DidKeyUri("did:key:zQ3bob_other".to_string()); 713 + let rotation_key_bob = crypto::DidKeyUri("did:key:zQ3bob_rotation".to_string()); 714 + let did_bob = "did:plc:bob"; 715 + 716 + let genesis_op_alice = crypto::build_did_plc_genesis_op( 717 + &rotation_key_alice, 718 + &device_key_alice, 719 + &[1; 32], 720 + "alice.bsky.social", 721 + "https://pds.alice", 722 + ) 723 + .expect("Failed to build alice genesis op"); 724 + 725 + let genesis_op_bob = crypto::build_did_plc_genesis_op( 726 + &rotation_key_bob, 727 + &device_key_bob, 728 + &[3; 32], 729 + "bob.bsky.social", 730 + "https://pds.bob", 731 + ) 732 + .expect("Failed to build bob genesis op"); 733 + 734 + let genesis_op_alice_obj: serde_json::Value = 735 + serde_json::from_str(&genesis_op_alice.signed_op_json) 736 + .expect("Failed to parse alice genesis op"); 737 + let genesis_op_bob_obj: serde_json::Value = 738 + serde_json::from_str(&genesis_op_bob.signed_op_json) 739 + .expect("Failed to parse bob genesis op"); 740 + 741 + let rotation_op_bob = crypto::build_did_plc_rotation_op( 742 + "bafy_bob_genesis", 743 + vec![device_key_bob.0.clone(), other_key_bob.0.clone()], 744 + std::collections::BTreeMap::new(), 745 + vec![], 746 + std::collections::BTreeMap::new(), 747 + |data| { 748 + let signing_key = 749 + p256::ecdsa::SigningKey::from_bytes(&p256::FieldBytes::from_slice(&[4; 32])) 750 + .map_err(|e| crypto::CryptoError::PlcOperation(e.to_string()))?; 751 + let sig: p256::ecdsa::Signature = 752 + p256::ecdsa::signature::Signer::sign(&signing_key, data); 753 + Ok(sig.to_bytes().to_vec()) 754 + }, 755 + ) 756 + .expect("Failed to build bob rotation op"); 757 + 758 + let rotation_op_bob_obj: serde_json::Value = 759 + serde_json::from_str(&rotation_op_bob.signed_op_json) 760 + .expect("Failed to parse bob rotation op"); 761 + 762 + let audit_log_alice = serde_json::json!([ 763 + { 764 + "did": did_alice, 765 + "cid": "bafy_alice1", 766 + "createdAt": "2026-03-29T00:00:00Z", 767 + "nullified": false, 768 + "operation": genesis_op_alice_obj 769 + } 770 + ]); 771 + 772 + let audit_log_bob = serde_json::json!([ 773 + { 774 + "did": did_bob, 775 + "cid": "bafy_bob_genesis", 776 + "createdAt": "2026-03-29T00:00:00Z", 777 + "nullified": false, 778 + "operation": genesis_op_bob_obj, 779 + "rotationKeys": [device_key_bob.0, other_key_bob.0] 780 + }, 781 + { 782 + "did": did_bob, 783 + "cid": "bafy_bob_rotation", 784 + "createdAt": "2026-03-29T01:00:00Z", 785 + "nullified": false, 786 + "operation": rotation_op_bob_obj 787 + } 788 + ]); 789 + 790 + mock_server.mock(|when, then| { 791 + when.method(GET) 792 + .path(format!("/{}/log/audit", did_alice).as_str()); 793 + then.status(200) 794 + .header("content-type", "application/json") 795 + .json_body(audit_log_alice); 796 + }); 797 + 798 + mock_server.mock(|when, then| { 799 + when.method(GET) 800 + .path(format!("/{}/log/audit", did_bob).as_str()); 801 + then.status(200) 802 + .header("content-type", "application/json") 803 + .json_body(audit_log_bob); 804 + }); 805 + 806 + let result_alice = monitor.check_for_changes(did_alice).await; 807 + assert!(result_alice.is_ok()); 808 + assert_eq!( 809 + result_alice.unwrap().len(), 810 + 0, 811 + "Alice should have no alerts" 812 + ); 813 + 814 + let result_bob = monitor.check_for_changes(did_bob).await; 815 + assert!(result_bob.is_ok()); 816 + assert_eq!(result_bob.unwrap().len(), 1, "Bob should have one alert"); 315 817 } 316 818 }