contacts.rs
1 //! Contact storage and management. 2 //! 3 //! This module provides CRUD operations for contacts. Contacts are 4 //! identified by a random 128-bit ID and store the contact's public 5 //! keys for encryption and verification. 6 //! 7 //! # Contact States 8 //! 9 //! - `Pending`: Contact added but not yet verified 10 //! - `Active`: Contact is active and can exchange messages 11 //! - `Blocked`: Contact is blocked (messages ignored) 12 //! - `Revoked`: Contact was revoked (key compromised) 13 14 use chrono::{DateTime, TimeZone, Utc}; 15 use rusqlite::params; 16 use serde::{Deserialize, Serialize}; 17 18 use crate::crypto::keys::ContactPublicKeys; 19 use crate::crypto::signing::blake2b_256; 20 use crate::error::{DeadDropError, Result}; 21 use crate::protocol::messages::{generate_contact_id, ContactId}; 22 use crate::storage::Database; 23 24 /// State of a contact in the contact list. 25 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] 26 #[repr(u8)] 27 pub enum ContactState { 28 /// Contact added but not yet fully verified. 29 Pending = 0, 30 /// Contact is active and can exchange messages. 31 Active = 1, 32 /// Contact is blocked (incoming messages ignored). 33 Blocked = 2, 34 /// Contact's key was revoked (compromised). 35 Revoked = 3, 36 } 37 38 impl ContactState { 39 /// Convert from database integer. 40 pub fn from_i32(value: i32) -> Option<Self> { 41 match value { 42 0 => Some(ContactState::Pending), 43 1 => Some(ContactState::Active), 44 2 => Some(ContactState::Blocked), 45 3 => Some(ContactState::Revoked), 46 _ => None, 47 } 48 } 49 50 /// Convert to database integer. 51 pub fn to_i32(self) -> i32 { 52 self as i32 53 } 54 55 /// Check if the contact can receive messages. 56 pub fn can_send_messages(&self) -> bool { 57 matches!(self, ContactState::Active) 58 } 59 60 /// Check if incoming messages should be accepted. 61 pub fn can_receive_messages(&self) -> bool { 62 matches!(self, ContactState::Active | ContactState::Pending) 63 } 64 } 65 66 /// A contact in the contact list. 67 #[derive(Debug, Clone)] 68 pub struct Contact { 69 /// Unique contact identifier (random 128 bits). 70 pub contact_id: ContactId, 71 /// User-assigned nickname (optional). 72 pub nickname: Option<String>, 73 /// Contact's Ed25519 identity public key. 74 pub identity_public: [u8; 32], 75 /// Contact's X25519 exchange public key. 76 pub exchange_public: [u8; 32], 77 /// Current contact state. 78 pub state: ContactState, 79 /// When the contact was added. 80 pub created_at: DateTime<Utc>, 81 /// When the contact was last seen nearby. 82 pub last_seen: Option<DateTime<Utc>>, 83 /// When the last message exchange occurred. 84 pub last_exchange: Option<DateTime<Utc>>, 85 /// Disappearing message duration in seconds (0 = off). 86 pub disappearing_duration: u64, 87 /// Whether notifications are muted for this contact. 88 pub is_muted: bool, 89 } 90 91 impl Contact { 92 /// Create a new contact from public keys. 93 /// 94 /// Generates a new random contact ID and sets the state to Pending. 95 pub fn new(identity_public: [u8; 32], exchange_public: [u8; 32]) -> Self { 96 Self { 97 contact_id: generate_contact_id(), 98 nickname: None, 99 identity_public, 100 exchange_public, 101 state: ContactState::Pending, 102 created_at: Utc::now(), 103 last_seen: None, 104 last_exchange: None, 105 disappearing_duration: 0, 106 is_muted: false, 107 } 108 } 109 110 /// Create a new contact with a nickname. 111 pub fn with_nickname( 112 identity_public: [u8; 32], 113 exchange_public: [u8; 32], 114 nickname: String, 115 ) -> Self { 116 let mut contact = Self::new(identity_public, exchange_public); 117 contact.nickname = Some(nickname); 118 contact 119 } 120 121 /// Get the contact's public keys. 122 pub fn public_keys(&self) -> ContactPublicKeys { 123 ContactPublicKeys::new(self.identity_public, self.exchange_public) 124 } 125 126 /// Compute the contact's fingerprint. 127 /// 128 /// Based on the identity public key. 129 pub fn fingerprint(&self) -> [u8; 32] { 130 blake2b_256(&self.identity_public) 131 } 132 133 /// Format the fingerprint as a human-readable string. 134 pub fn fingerprint_string(&self) -> String { 135 let fp = self.fingerprint(); 136 let hex = hex::encode_upper(fp); 137 138 hex.chars() 139 .collect::<Vec<_>>() 140 .chunks(4) 141 .map(|c| c.iter().collect::<String>()) 142 .collect::<Vec<_>>() 143 .join(" ") 144 } 145 146 /// Get display name (nickname or truncated fingerprint). 147 pub fn display_name(&self) -> String { 148 match &self.nickname { 149 Some(name) => name.clone(), 150 None => { 151 // Use first 8 characters of fingerprint 152 let fp = hex::encode_upper(&self.fingerprint()[..4]); 153 format!("Contact {}", fp) 154 } 155 } 156 } 157 } 158 159 impl Database { 160 /// Add a new contact. 161 /// 162 /// # Arguments 163 /// 164 /// * `contact` - The contact to add 165 /// 166 /// # Errors 167 /// 168 /// - `AlreadyExists` if a contact with the same ID or public key exists 169 /// - `Database` if storage fails 170 pub fn add_contact(&self, contact: &Contact) -> Result<()> { 171 // Check for existing contact with same public key 172 if self.get_contact_by_identity_key(&contact.identity_public)?.is_some() { 173 return Err(DeadDropError::AlreadyExists( 174 "Contact with this identity key already exists".to_string() 175 )); 176 } 177 178 self.connection().execute( 179 "INSERT INTO contacts ( 180 contact_id, nickname, identity_public, exchange_public, 181 state, created_at, last_seen, last_exchange 182 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", 183 params![ 184 contact.contact_id.as_slice(), 185 contact.nickname, 186 contact.identity_public.as_slice(), 187 contact.exchange_public.as_slice(), 188 contact.state.to_i32(), 189 contact.created_at.timestamp(), 190 contact.last_seen.map(|t| t.timestamp()), 191 contact.last_exchange.map(|t| t.timestamp()), 192 ], 193 )?; 194 195 Ok(()) 196 } 197 198 /// Get a contact by ID. 199 /// 200 /// # Arguments 201 /// 202 /// * `contact_id` - The contact's unique identifier 203 /// 204 /// # Returns 205 /// 206 /// The contact if found, `None` otherwise. 207 pub fn get_contact(&self, contact_id: &ContactId) -> Result<Option<Contact>> { 208 let result = self.connection().query_row( 209 "SELECT contact_id, nickname, identity_public, exchange_public, 210 state, created_at, last_seen, last_exchange, disappearing_duration, is_muted 211 FROM contacts WHERE contact_id = ?", 212 [contact_id.as_slice()], 213 |row| Self::contact_from_row(row), 214 ); 215 216 match result { 217 Ok(contact) => Ok(Some(contact)), 218 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), 219 Err(e) => Err(DeadDropError::Database(e.to_string())), 220 } 221 } 222 223 /// Get a contact by identity public key. 224 /// 225 /// Useful for looking up contacts during message verification. 226 pub fn get_contact_by_identity_key(&self, identity_public: &[u8; 32]) -> Result<Option<Contact>> { 227 let result = self.connection().query_row( 228 "SELECT contact_id, nickname, identity_public, exchange_public, 229 state, created_at, last_seen, last_exchange, disappearing_duration, is_muted 230 FROM contacts WHERE identity_public = ?", 231 [identity_public.as_slice()], 232 |row| Self::contact_from_row(row), 233 ); 234 235 match result { 236 Ok(contact) => Ok(Some(contact)), 237 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), 238 Err(e) => Err(DeadDropError::Database(e.to_string())), 239 } 240 } 241 242 /// Get a contact by exchange public key. 243 /// 244 /// Useful for looking up contacts when receiving DHT messages, 245 /// where we only have the sender's exchange key. 246 pub fn get_contact_by_exchange_key(&self, exchange_public: &[u8; 32]) -> Result<Option<Contact>> { 247 let result = self.connection().query_row( 248 "SELECT contact_id, nickname, identity_public, exchange_public, 249 state, created_at, last_seen, last_exchange, disappearing_duration, is_muted 250 FROM contacts WHERE exchange_public = ?", 251 [exchange_public.as_slice()], 252 |row| Self::contact_from_row(row), 253 ); 254 255 match result { 256 Ok(contact) => Ok(Some(contact)), 257 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), 258 Err(e) => Err(DeadDropError::Database(e.to_string())), 259 } 260 } 261 262 /// List all contacts. 263 /// 264 /// Returns contacts ordered by last seen (most recent first), 265 /// then by creation date. 266 pub fn list_contacts(&self) -> Result<Vec<Contact>> { 267 let mut stmt = self.connection().prepare( 268 "SELECT contact_id, nickname, identity_public, exchange_public, 269 state, created_at, last_seen, last_exchange, disappearing_duration, is_muted 270 FROM contacts 271 ORDER BY COALESCE(last_seen, 0) DESC, created_at DESC", 272 )?; 273 274 let contacts = stmt 275 .query_map([], |row| Self::contact_from_row(row))? 276 .collect::<std::result::Result<Vec<_>, _>>()?; 277 278 Ok(contacts) 279 } 280 281 /// List contacts by state. 282 pub fn list_contacts_by_state(&self, state: ContactState) -> Result<Vec<Contact>> { 283 let mut stmt = self.connection().prepare( 284 "SELECT contact_id, nickname, identity_public, exchange_public, 285 state, created_at, last_seen, last_exchange, disappearing_duration, is_muted 286 FROM contacts WHERE state = ? 287 ORDER BY COALESCE(last_seen, 0) DESC, created_at DESC", 288 )?; 289 290 let contacts = stmt 291 .query_map([state.to_i32()], |row| Self::contact_from_row(row))? 292 .collect::<std::result::Result<Vec<_>, _>>()?; 293 294 Ok(contacts) 295 } 296 297 /// Update a contact's state. 298 pub fn update_contact_state(&self, contact_id: &ContactId, state: ContactState) -> Result<()> { 299 let rows = self.connection().execute( 300 "UPDATE contacts SET state = ? WHERE contact_id = ?", 301 params![state.to_i32(), contact_id.as_slice()], 302 )?; 303 304 if rows == 0 { 305 return Err(DeadDropError::NotFound("Contact not found".to_string())); 306 } 307 308 Ok(()) 309 } 310 311 /// Update a contact's nickname. 312 pub fn update_contact_nickname(&self, contact_id: &ContactId, nickname: Option<&str>) -> Result<()> { 313 let rows = self.connection().execute( 314 "UPDATE contacts SET nickname = ? WHERE contact_id = ?", 315 params![nickname, contact_id.as_slice()], 316 )?; 317 318 if rows == 0 { 319 return Err(DeadDropError::NotFound("Contact not found".to_string())); 320 } 321 322 Ok(()) 323 } 324 325 /// Update the last seen timestamp. 326 pub fn update_contact_last_seen(&self, contact_id: &ContactId, time: DateTime<Utc>) -> Result<()> { 327 let rows = self.connection().execute( 328 "UPDATE contacts SET last_seen = ? WHERE contact_id = ?", 329 params![time.timestamp(), contact_id.as_slice()], 330 )?; 331 332 if rows == 0 { 333 return Err(DeadDropError::NotFound("Contact not found".to_string())); 334 } 335 336 Ok(()) 337 } 338 339 /// Update the last exchange timestamp. 340 pub fn update_contact_last_exchange(&self, contact_id: &ContactId, time: DateTime<Utc>) -> Result<()> { 341 let rows = self.connection().execute( 342 "UPDATE contacts SET last_exchange = ? WHERE contact_id = ?", 343 params![time.timestamp(), contact_id.as_slice()], 344 )?; 345 346 if rows == 0 { 347 return Err(DeadDropError::NotFound("Contact not found".to_string())); 348 } 349 350 Ok(()) 351 } 352 353 /// Update a contact's public keys. 354 /// 355 /// This is used when a contact's identity has changed (e.g., app reinstall) 356 /// and the user has verified the new identity via QR code. 357 pub fn update_contact_keys( 358 &self, 359 contact_id: &ContactId, 360 identity_public: &[u8; 32], 361 exchange_public: &[u8; 32], 362 ) -> Result<()> { 363 let rows = self.connection().execute( 364 "UPDATE contacts SET identity_public = ?, exchange_public = ? WHERE contact_id = ?", 365 params![ 366 identity_public.as_slice(), 367 exchange_public.as_slice(), 368 contact_id.as_slice() 369 ], 370 )?; 371 372 if rows == 0 { 373 return Err(DeadDropError::NotFound("Contact not found".to_string())); 374 } 375 376 Ok(()) 377 } 378 379 /// Delete a contact and all associated messages. 380 pub fn delete_contact(&self, contact_id: &ContactId) -> Result<()> { 381 let rows = self.connection().execute( 382 "DELETE FROM contacts WHERE contact_id = ?", 383 [contact_id.as_slice()], 384 )?; 385 386 if rows == 0 { 387 return Err(DeadDropError::NotFound("Contact not found".to_string())); 388 } 389 390 Ok(()) 391 } 392 393 /// Count contacts by state. 394 pub fn count_contacts(&self) -> Result<ContactCounts> { 395 let total: u32 = self.connection().query_row( 396 "SELECT COUNT(*) FROM contacts", 397 [], 398 |row| row.get(0), 399 )?; 400 401 let active: u32 = self.connection().query_row( 402 "SELECT COUNT(*) FROM contacts WHERE state = ?", 403 [ContactState::Active.to_i32()], 404 |row| row.get(0), 405 )?; 406 407 let pending: u32 = self.connection().query_row( 408 "SELECT COUNT(*) FROM contacts WHERE state = ?", 409 [ContactState::Pending.to_i32()], 410 |row| row.get(0), 411 )?; 412 413 let blocked: u32 = self.connection().query_row( 414 "SELECT COUNT(*) FROM contacts WHERE state = ?", 415 [ContactState::Blocked.to_i32()], 416 |row| row.get(0), 417 )?; 418 419 Ok(ContactCounts { 420 total, 421 active, 422 pending, 423 blocked, 424 }) 425 } 426 427 /// Set the disappearing message duration for a contact. 428 pub fn set_disappearing_duration( 429 &self, 430 contact_id: &ContactId, 431 duration_secs: u64, 432 ) -> Result<()> { 433 let rows = self.connection().execute( 434 "UPDATE contacts SET disappearing_duration = ? WHERE contact_id = ?", 435 params![duration_secs as i64, contact_id.as_slice()], 436 )?; 437 438 if rows == 0 { 439 return Err(DeadDropError::NotFound("Contact not found".to_string())); 440 } 441 442 Ok(()) 443 } 444 445 /// Get the disappearing message duration for a contact. 446 pub fn get_disappearing_duration(&self, contact_id: &ContactId) -> Result<u64> { 447 let duration: i64 = self.connection().query_row( 448 "SELECT COALESCE(disappearing_duration, 0) FROM contacts WHERE contact_id = ?", 449 [contact_id.as_slice()], 450 |row| row.get(0), 451 ).map_err(|e| match e { 452 rusqlite::Error::QueryReturnedNoRows => { 453 DeadDropError::NotFound("Contact not found".to_string()) 454 } 455 _ => DeadDropError::Database(e.to_string()), 456 })?; 457 458 Ok(duration as u64) 459 } 460 461 /// Set the muted state for a contact. 462 pub fn set_contact_muted(&self, contact_id: &ContactId, muted: bool) -> Result<()> { 463 let rows = self.connection().execute( 464 "UPDATE contacts SET is_muted = ? WHERE contact_id = ?", 465 params![muted as i32, contact_id.as_slice()], 466 )?; 467 468 if rows == 0 { 469 return Err(DeadDropError::NotFound("Contact not found".to_string())); 470 } 471 472 Ok(()) 473 } 474 475 /// Get the muted state for a contact. 476 pub fn get_contact_muted(&self, contact_id: &ContactId) -> Result<bool> { 477 let muted: i32 = self.connection().query_row( 478 "SELECT COALESCE(is_muted, 0) FROM contacts WHERE contact_id = ?", 479 [contact_id.as_slice()], 480 |row| row.get(0), 481 ).map_err(|e| match e { 482 rusqlite::Error::QueryReturnedNoRows => { 483 DeadDropError::NotFound("Contact not found".to_string()) 484 } 485 _ => DeadDropError::Database(e.to_string()), 486 })?; 487 488 Ok(muted != 0) 489 } 490 491 /// Cache a contact's onion address for instant Tor P2P reconnect. 492 pub fn set_contact_onion_address( 493 &self, 494 contact_id: &ContactId, 495 onion_address: &str, 496 ) -> Result<()> { 497 self.connection().execute( 498 "UPDATE contacts SET onion_address = ? WHERE contact_id = ?", 499 rusqlite::params![onion_address, contact_id.as_slice()], 500 )?; 501 Ok(()) 502 } 503 504 /// Get all contacts with cached onion addresses. 505 /// 506 /// Returns (contact_id, onion_address) pairs for all contacts that have 507 /// a cached onion address. 508 pub fn get_cached_onion_addresses(&self) -> Result<Vec<(ContactId, String)>> { 509 let mut stmt = self.connection().prepare( 510 "SELECT contact_id, onion_address FROM contacts WHERE onion_address IS NOT NULL", 511 )?; 512 513 let results = stmt 514 .query_map([], |row| { 515 let id_vec: Vec<u8> = row.get(0)?; 516 let addr: String = row.get(1)?; 517 let id: ContactId = id_vec.try_into().map_err(|_| { 518 rusqlite::Error::InvalidColumnType( 519 0, 520 "contact_id".to_string(), 521 rusqlite::types::Type::Blob, 522 ) 523 })?; 524 Ok((id, addr)) 525 })? 526 .collect::<std::result::Result<Vec<_>, _>>()?; 527 528 Ok(results) 529 } 530 531 /// Cache a contact's iroh endpoint ID for instant reconnect. 532 pub fn set_contact_iroh_endpoint_id( 533 &self, 534 contact_id: &ContactId, 535 endpoint_id: &str, 536 ) -> Result<()> { 537 self.connection().execute( 538 "UPDATE contacts SET iroh_endpoint_id = ? WHERE contact_id = ?", 539 rusqlite::params![endpoint_id, contact_id.as_slice()], 540 )?; 541 Ok(()) 542 } 543 544 /// Get all contacts with cached iroh endpoint IDs. 545 /// 546 /// Returns (contact_id, endpoint_id) pairs for all contacts that have 547 /// a cached iroh endpoint ID. 548 pub fn get_cached_iroh_endpoint_ids(&self) -> Result<Vec<(ContactId, String)>> { 549 let mut stmt = self.connection().prepare( 550 "SELECT contact_id, iroh_endpoint_id FROM contacts WHERE iroh_endpoint_id IS NOT NULL", 551 )?; 552 553 let results = stmt 554 .query_map([], |row| { 555 let id_vec: Vec<u8> = row.get(0)?; 556 let endpoint_id: String = row.get(1)?; 557 let id: ContactId = id_vec.try_into().map_err(|_| { 558 rusqlite::Error::InvalidColumnType( 559 0, 560 "contact_id".to_string(), 561 rusqlite::types::Type::Blob, 562 ) 563 })?; 564 Ok((id, endpoint_id)) 565 })? 566 .collect::<std::result::Result<Vec<_>, _>>()?; 567 568 Ok(results) 569 } 570 571 /// Helper function to construct a Contact from a database row. 572 fn contact_from_row(row: &rusqlite::Row) -> rusqlite::Result<Contact> { 573 let contact_id_vec: Vec<u8> = row.get(0)?; 574 let contact_id: ContactId = contact_id_vec 575 .try_into() 576 .map_err(|_| rusqlite::Error::InvalidColumnType(0, "contact_id".to_string(), rusqlite::types::Type::Blob))?; 577 578 let nickname: Option<String> = row.get(1)?; 579 580 let identity_public_vec: Vec<u8> = row.get(2)?; 581 let identity_public: [u8; 32] = identity_public_vec 582 .try_into() 583 .map_err(|_| rusqlite::Error::InvalidColumnType(2, "identity_public".to_string(), rusqlite::types::Type::Blob))?; 584 585 let exchange_public_vec: Vec<u8> = row.get(3)?; 586 let exchange_public: [u8; 32] = exchange_public_vec 587 .try_into() 588 .map_err(|_| rusqlite::Error::InvalidColumnType(3, "exchange_public".to_string(), rusqlite::types::Type::Blob))?; 589 590 let state_int: i32 = row.get(4)?; 591 let state = ContactState::from_i32(state_int) 592 .ok_or_else(|| rusqlite::Error::InvalidColumnType(4, "state".to_string(), rusqlite::types::Type::Integer))?; 593 594 let created_at_ts: i64 = row.get(5)?; 595 let created_at = Utc.timestamp_opt(created_at_ts, 0) 596 .single() 597 .unwrap_or_else(Utc::now); 598 599 let last_seen_ts: Option<i64> = row.get(6)?; 600 let last_seen = last_seen_ts.and_then(|ts| Utc.timestamp_opt(ts, 0).single()); 601 602 let last_exchange_ts: Option<i64> = row.get(7)?; 603 let last_exchange = last_exchange_ts.and_then(|ts| Utc.timestamp_opt(ts, 0).single()); 604 605 let disappearing_duration: i64 = row.get::<_, Option<i64>>(8)?.unwrap_or(0); 606 let is_muted: bool = row.get::<_, Option<i32>>(9)?.unwrap_or(0) != 0; 607 608 Ok(Contact { 609 contact_id, 610 nickname, 611 identity_public, 612 exchange_public, 613 state, 614 created_at, 615 last_seen, 616 last_exchange, 617 disappearing_duration: disappearing_duration as u64, 618 is_muted, 619 }) 620 } 621 } 622 623 /// Contact count statistics. 624 #[derive(Debug, Clone)] 625 pub struct ContactCounts { 626 /// Total number of contacts. 627 pub total: u32, 628 /// Number of active contacts. 629 pub active: u32, 630 /// Number of pending contacts. 631 pub pending: u32, 632 /// Number of blocked contacts. 633 pub blocked: u32, 634 } 635 636 #[cfg(test)] 637 mod tests { 638 use super::*; 639 640 fn test_db() -> Database { 641 Database::open_in_memory(b"test_passphrase").unwrap() 642 } 643 644 fn random_keys() -> ([u8; 32], [u8; 32]) { 645 use crate::crypto::keys::{IdentityKeyPair, ExchangeKeyPair}; 646 let identity = IdentityKeyPair::generate(); 647 let exchange = ExchangeKeyPair::generate(); 648 (identity.public_bytes(), exchange.public_bytes()) 649 } 650 651 #[test] 652 fn test_contact_state_round_trip() { 653 for state in [ContactState::Pending, ContactState::Active, ContactState::Blocked, ContactState::Revoked] { 654 let int_val = state.to_i32(); 655 let recovered = ContactState::from_i32(int_val).unwrap(); 656 assert_eq!(state, recovered); 657 } 658 } 659 660 #[test] 661 fn test_contact_state_permissions() { 662 assert!(!ContactState::Pending.can_send_messages()); 663 assert!(ContactState::Pending.can_receive_messages()); 664 665 assert!(ContactState::Active.can_send_messages()); 666 assert!(ContactState::Active.can_receive_messages()); 667 668 assert!(!ContactState::Blocked.can_send_messages()); 669 assert!(!ContactState::Blocked.can_receive_messages()); 670 671 assert!(!ContactState::Revoked.can_send_messages()); 672 assert!(!ContactState::Revoked.can_receive_messages()); 673 } 674 675 #[test] 676 fn test_add_contact() { 677 let db = test_db(); 678 let (identity_pub, exchange_pub) = random_keys(); 679 680 let contact = Contact::new(identity_pub, exchange_pub); 681 db.add_contact(&contact).unwrap(); 682 683 // Verify contact exists 684 let retrieved = db.get_contact(&contact.contact_id).unwrap().unwrap(); 685 assert_eq!(retrieved.identity_public, identity_pub); 686 assert_eq!(retrieved.exchange_public, exchange_pub); 687 assert_eq!(retrieved.state, ContactState::Pending); 688 } 689 690 #[test] 691 fn test_add_contact_with_nickname() { 692 let db = test_db(); 693 let (identity_pub, exchange_pub) = random_keys(); 694 695 let contact = Contact::with_nickname(identity_pub, exchange_pub, "Alice".to_string()); 696 db.add_contact(&contact).unwrap(); 697 698 let retrieved = db.get_contact(&contact.contact_id).unwrap().unwrap(); 699 assert_eq!(retrieved.nickname, Some("Alice".to_string())); 700 assert_eq!(retrieved.display_name(), "Alice"); 701 } 702 703 #[test] 704 fn test_add_duplicate_contact() { 705 let db = test_db(); 706 let (identity_pub, exchange_pub) = random_keys(); 707 708 let contact1 = Contact::new(identity_pub, exchange_pub); 709 db.add_contact(&contact1).unwrap(); 710 711 // Try to add another contact with same identity key 712 let contact2 = Contact::new(identity_pub, exchange_pub); 713 let result = db.add_contact(&contact2); 714 715 assert!(result.is_err()); 716 assert!(matches!(result.unwrap_err(), DeadDropError::AlreadyExists(_))); 717 } 718 719 #[test] 720 fn test_get_contact_not_found() { 721 let db = test_db(); 722 let fake_id: ContactId = [0u8; 16]; 723 724 let result = db.get_contact(&fake_id).unwrap(); 725 assert!(result.is_none()); 726 } 727 728 #[test] 729 fn test_get_contact_by_identity_key() { 730 let db = test_db(); 731 let (identity_pub, exchange_pub) = random_keys(); 732 733 let contact = Contact::with_nickname(identity_pub, exchange_pub, "Bob".to_string()); 734 db.add_contact(&contact).unwrap(); 735 736 let retrieved = db.get_contact_by_identity_key(&identity_pub).unwrap().unwrap(); 737 assert_eq!(retrieved.contact_id, contact.contact_id); 738 assert_eq!(retrieved.nickname, Some("Bob".to_string())); 739 } 740 741 #[test] 742 fn test_list_contacts() { 743 let db = test_db(); 744 745 // Add multiple contacts 746 for i in 0..3 { 747 let (identity_pub, exchange_pub) = random_keys(); 748 let contact = Contact::with_nickname(identity_pub, exchange_pub, format!("Contact {}", i)); 749 db.add_contact(&contact).unwrap(); 750 } 751 752 let contacts = db.list_contacts().unwrap(); 753 assert_eq!(contacts.len(), 3); 754 } 755 756 #[test] 757 fn test_list_contacts_by_state() { 758 let db = test_db(); 759 760 // Add contacts in different states 761 let (id1, ex1) = random_keys(); 762 let mut c1 = Contact::new(id1, ex1); 763 c1.state = ContactState::Active; 764 db.add_contact(&c1).unwrap(); 765 db.update_contact_state(&c1.contact_id, ContactState::Active).unwrap(); 766 767 let (id2, ex2) = random_keys(); 768 let c2 = Contact::new(id2, ex2); // Pending by default 769 db.add_contact(&c2).unwrap(); 770 771 let active = db.list_contacts_by_state(ContactState::Active).unwrap(); 772 assert_eq!(active.len(), 1); 773 774 let pending = db.list_contacts_by_state(ContactState::Pending).unwrap(); 775 assert_eq!(pending.len(), 1); 776 } 777 778 #[test] 779 fn test_update_contact_state() { 780 let db = test_db(); 781 let (identity_pub, exchange_pub) = random_keys(); 782 783 let contact = Contact::new(identity_pub, exchange_pub); 784 db.add_contact(&contact).unwrap(); 785 786 // Update to active 787 db.update_contact_state(&contact.contact_id, ContactState::Active).unwrap(); 788 789 let retrieved = db.get_contact(&contact.contact_id).unwrap().unwrap(); 790 assert_eq!(retrieved.state, ContactState::Active); 791 } 792 793 #[test] 794 fn test_update_contact_nickname() { 795 let db = test_db(); 796 let (identity_pub, exchange_pub) = random_keys(); 797 798 let contact = Contact::new(identity_pub, exchange_pub); 799 db.add_contact(&contact).unwrap(); 800 801 // Set nickname 802 db.update_contact_nickname(&contact.contact_id, Some("Charlie")).unwrap(); 803 let retrieved = db.get_contact(&contact.contact_id).unwrap().unwrap(); 804 assert_eq!(retrieved.nickname, Some("Charlie".to_string())); 805 806 // Clear nickname 807 db.update_contact_nickname(&contact.contact_id, None).unwrap(); 808 let retrieved = db.get_contact(&contact.contact_id).unwrap().unwrap(); 809 assert_eq!(retrieved.nickname, None); 810 } 811 812 #[test] 813 fn test_update_contact_timestamps() { 814 let db = test_db(); 815 let (identity_pub, exchange_pub) = random_keys(); 816 817 let contact = Contact::new(identity_pub, exchange_pub); 818 db.add_contact(&contact).unwrap(); 819 820 let now = Utc::now(); 821 822 // Update last_seen 823 db.update_contact_last_seen(&contact.contact_id, now).unwrap(); 824 let retrieved = db.get_contact(&contact.contact_id).unwrap().unwrap(); 825 assert!(retrieved.last_seen.is_some()); 826 827 // Update last_exchange 828 db.update_contact_last_exchange(&contact.contact_id, now).unwrap(); 829 let retrieved = db.get_contact(&contact.contact_id).unwrap().unwrap(); 830 assert!(retrieved.last_exchange.is_some()); 831 } 832 833 #[test] 834 fn test_delete_contact() { 835 let db = test_db(); 836 let (identity_pub, exchange_pub) = random_keys(); 837 838 let contact = Contact::new(identity_pub, exchange_pub); 839 let contact_id = contact.contact_id; 840 db.add_contact(&contact).unwrap(); 841 842 // Delete 843 db.delete_contact(&contact_id).unwrap(); 844 845 // Verify gone 846 let result = db.get_contact(&contact_id).unwrap(); 847 assert!(result.is_none()); 848 } 849 850 #[test] 851 fn test_delete_contact_not_found() { 852 let db = test_db(); 853 let fake_id: ContactId = [0u8; 16]; 854 855 let result = db.delete_contact(&fake_id); 856 assert!(result.is_err()); 857 assert!(matches!(result.unwrap_err(), DeadDropError::NotFound(_))); 858 } 859 860 #[test] 861 fn test_count_contacts() { 862 let db = test_db(); 863 864 // Add contacts in different states 865 for i in 0..3 { 866 let (identity_pub, exchange_pub) = random_keys(); 867 let contact = Contact::new(identity_pub, exchange_pub); 868 db.add_contact(&contact).unwrap(); 869 870 if i == 0 { 871 db.update_contact_state(&contact.contact_id, ContactState::Active).unwrap(); 872 } else if i == 1 { 873 db.update_contact_state(&contact.contact_id, ContactState::Blocked).unwrap(); 874 } 875 // i == 2 stays Pending 876 } 877 878 let counts = db.count_contacts().unwrap(); 879 assert_eq!(counts.total, 3); 880 assert_eq!(counts.active, 1); 881 assert_eq!(counts.pending, 1); 882 assert_eq!(counts.blocked, 1); 883 } 884 885 #[test] 886 fn test_contact_fingerprint() { 887 let (identity_pub, exchange_pub) = random_keys(); 888 let contact = Contact::new(identity_pub, exchange_pub); 889 890 let fp = contact.fingerprint(); 891 assert_eq!(fp.len(), 32); 892 893 let fp_string = contact.fingerprint_string(); 894 assert!(fp_string.len() > 0); 895 assert!(fp_string.contains(' ')); 896 } 897 898 #[test] 899 fn test_contact_display_name() { 900 let (identity_pub, exchange_pub) = random_keys(); 901 902 // Without nickname 903 let contact = Contact::new(identity_pub, exchange_pub); 904 let name = contact.display_name(); 905 assert!(name.starts_with("Contact ")); 906 907 // With nickname 908 let contact = Contact::with_nickname(identity_pub, exchange_pub, "Alice".to_string()); 909 assert_eq!(contact.display_name(), "Alice"); 910 } 911 }