/ core / src / storage / contacts.rs
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  }