use crate::{ database::{Database, DatabaseAccess}, repository::Repository, models::{ conversation::{Conversation, ConversationBuilder}, participant::Participant, message::Message, }, settings::Settings, }; // Helper function to compare participants ignoring database IDs fn participants_equal_ignoring_id(a: &Participant, b: &Participant) -> bool { match (a, b) { (Participant::Me, Participant::Me) => true, (Participant::Remote { display_name: name_a, .. }, Participant::Remote { display_name: name_b, .. }) => name_a == name_b, _ => false } } fn participants_vec_equal_ignoring_id(a: &[Participant], b: &[Participant]) -> bool { if a.len() != b.len() { return false; } a.iter().zip(b.iter()).all(|(a, b)| participants_equal_ignoring_id(a, b)) } #[tokio::test] async fn test_database_init() { let _ = Database::new_in_memory().unwrap(); } #[tokio::test] async fn test_add_conversation() { let mut db = Database::new_in_memory().unwrap(); db.with_repository(|repository| { let guid = "test"; let test_conversation = Conversation::builder() .guid(guid) .unread_count(2) .display_name("Test Conversation") .build(); repository.insert_conversation(test_conversation.clone()).unwrap(); // Try to fetch with id now let conversation = repository.get_conversation_by_guid(guid).unwrap().unwrap(); assert_eq!(conversation.guid, "test"); // Modify the conversation and update it let modified_conversation = test_conversation.into_builder() .display_name("Modified Conversation") .build(); repository.insert_conversation(modified_conversation.clone()).unwrap(); // Make sure we still only have one conversation. let all_conversations = repository.all_conversations().unwrap(); assert_eq!(all_conversations.len(), 1); // And make sure the display name was updated let conversation = repository.get_conversation_by_guid(guid).unwrap().unwrap(); assert_eq!(conversation.display_name.unwrap(), "Modified Conversation"); }).await; } #[tokio::test] async fn test_conversation_participants() { let mut db = Database::new_in_memory().unwrap(); db.with_repository(|repository| { let participants: Vec = vec!["one".into(), "two".into()]; let guid = uuid::Uuid::new_v4().to_string(); let conversation = ConversationBuilder::new() .guid(&guid) .display_name("Test") .participants(participants.clone()) .build(); repository.insert_conversation(conversation).unwrap(); let read_conversation = repository.get_conversation_by_guid(&guid).unwrap().unwrap(); let read_participants = read_conversation.participants; assert!(participants_vec_equal_ignoring_id(&participants, &read_participants)); // Try making another conversation with the same participants let conversation = ConversationBuilder::new() .display_name("A Different Test") .participants(participants.clone()) .build(); repository.insert_conversation(conversation).unwrap(); let read_conversation = repository.get_conversation_by_guid(&guid).unwrap().unwrap(); let read_participants: Vec = read_conversation.participants; assert!(participants_vec_equal_ignoring_id(&participants, &read_participants)); }).await; } #[tokio::test] async fn test_all_conversations_with_participants() { let mut db = Database::new_in_memory().unwrap(); db.with_repository(|repository| { // Create two conversations with different participants let participants1: Vec = vec!["one".into(), "two".into()]; let participants2: Vec = vec!["three".into(), "four".into()]; let guid1 = uuid::Uuid::new_v4().to_string(); let conversation1 = ConversationBuilder::new() .guid(&guid1) .display_name("Test 1") .participants(participants1.clone()) .build(); let guid2 = uuid::Uuid::new_v4().to_string(); let conversation2 = ConversationBuilder::new() .guid(&guid2) .display_name("Test 2") .participants(participants2.clone()) .build(); // Insert both conversations repository.insert_conversation(conversation1).unwrap(); repository.insert_conversation(conversation2).unwrap(); // Get all conversations and verify the results let all_conversations = repository.all_conversations().unwrap(); assert_eq!(all_conversations.len(), 2); // Find and verify each conversation's participants let conv1 = all_conversations.iter().find(|c| c.guid == guid1).unwrap(); let conv2 = all_conversations.iter().find(|c| c.guid == guid2).unwrap(); assert!(participants_vec_equal_ignoring_id(&conv1.participants, &participants1)); assert!(participants_vec_equal_ignoring_id(&conv2.participants, &participants2)); }).await; } #[tokio::test] async fn test_messages() { let mut db = Database::new_in_memory().unwrap(); db.with_repository(|repository| { // First create a conversation with participants let participants = vec!["Alice".into(), "Bob".into()]; let conversation = ConversationBuilder::new() .display_name("Test Chat") .participants(participants) .build(); let conversation_id = conversation.guid.clone(); repository.insert_conversation(conversation).unwrap(); // Create and insert a message from Me let message1 = Message::builder() .text("Hello everyone!".to_string()) .build(); // Create and insert a message from a remote participant let message2 = Message::builder() .text("Hi there!".to_string()) .sender("Alice".into()) .build(); // Insert both messages repository.insert_message(&conversation_id, message1.clone()).unwrap(); repository.insert_message(&conversation_id, message2.clone()).unwrap(); // Retrieve messages let messages = repository.get_messages_for_conversation(&conversation_id).unwrap(); assert_eq!(messages.len(), 2); // Verify first message (from Me) let retrieved_message1 = messages.iter().find(|m| m.id == message1.id).unwrap(); assert_eq!(retrieved_message1.text, "Hello everyone!"); assert!(matches!(retrieved_message1.sender, Participant::Me)); // Verify second message (from Alice) let retrieved_message2 = messages.iter().find(|m| m.id == message2.id).unwrap(); assert_eq!(retrieved_message2.text, "Hi there!"); if let Participant::Remote { display_name, .. } = &retrieved_message2.sender { assert_eq!(display_name, "Alice"); } else { panic!("Expected Remote participant. Got: {:?}", retrieved_message2.sender); } }).await; } #[tokio::test] async fn test_message_ordering() { let mut db = Database::new_in_memory().unwrap(); db.with_repository(|repository| { // Create a conversation let conversation = ConversationBuilder::new() .display_name("Test Chat") .build(); let conversation_id = conversation.guid.clone(); repository.insert_conversation(conversation).unwrap(); // Create messages with specific timestamps let now = chrono::Utc::now().naive_utc(); let message1 = Message::builder() .text("First message".to_string()) .date(now) .build(); let message2 = Message::builder() .text("Second message".to_string()) .date(now + chrono::Duration::minutes(1)) .build(); let message3 = Message::builder() .text("Third message".to_string()) .date(now + chrono::Duration::minutes(2)) .build(); // Insert messages repository.insert_message(&conversation_id, message1).unwrap(); repository.insert_message(&conversation_id, message2).unwrap(); repository.insert_message(&conversation_id, message3).unwrap(); // Retrieve messages and verify order let messages = repository.get_messages_for_conversation(&conversation_id).unwrap(); assert_eq!(messages.len(), 3); // Messages should be ordered by date for i in 1..messages.len() { assert!(messages[i].date > messages[i-1].date); } }).await; } #[tokio::test] async fn test_insert_messages_batch() { let mut db = Database::new_in_memory().unwrap(); db.with_repository(|repository| { // Create a conversation with two remote participants let participants: Vec = vec!["Alice".into(), "Bob".into()]; let conversation = ConversationBuilder::new() .display_name("Batch Chat") .participants(participants.clone()) .build(); let conversation_id = conversation.guid.clone(); repository.insert_conversation(conversation).unwrap(); // Prepare a batch of messages with increasing timestamps let now = chrono::Utc::now().naive_utc(); let message1 = Message::builder() .text("Hi".to_string()) .date(now) .build(); let message2 = Message::builder() .text("Hello".to_string()) .sender("Alice".into()) .date(now + chrono::Duration::seconds(1)) .build(); let message3 = Message::builder() .text("How are you?".to_string()) .sender("Bob".into()) .date(now + chrono::Duration::seconds(2)) .build(); let message4 = Message::builder() .text("Great!".to_string()) .date(now + chrono::Duration::seconds(3)) .build(); let original_messages = vec![ message1.clone(), message2.clone(), message3.clone(), message4.clone(), ]; // Batch insert the messages repository .insert_messages(&conversation_id, original_messages.clone()) .unwrap(); // Retrieve messages and verify let retrieved_messages = repository.get_messages_for_conversation(&conversation_id).unwrap(); assert_eq!(retrieved_messages.len(), original_messages.len()); // Ensure ordering by date for i in 1..retrieved_messages.len() { assert!(retrieved_messages[i].date > retrieved_messages[i - 1].date); } // Verify that all messages are present with correct content and sender for original in &original_messages { let retrieved = retrieved_messages .iter() .find(|m| m.id == original.id) .expect("Message not found"); assert_eq!(retrieved.text, original.text); match (&original.sender, &retrieved.sender) { (Participant::Me, Participant::Me) => {} ( Participant::Remote { display_name: o_name, .. }, Participant::Remote { display_name: r_name, .. }, ) => assert_eq!(o_name, r_name), _ => panic!( "Sender mismatch: original {:?}, retrieved {:?}", original.sender, retrieved.sender ), } } }) .await; } #[tokio::test] async fn test_settings() { let mut db = Database::new_in_memory().unwrap(); db.with_settings(|settings| { settings.put("test", &"test".to_string()).unwrap(); assert_eq!(settings.get::("test").unwrap().unwrap(), "test"); settings.del("test").unwrap(); assert!(settings.get::("test").unwrap().is_none()); let keys = settings.list_keys().unwrap(); assert_eq!(keys.len(), 0); // Try encoding a struct #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq)] struct TestStruct { name: String, age: u32, } let test_struct = TestStruct { name: "James".to_string(), age: 35, }; settings.put("test_struct", &test_struct).unwrap(); assert_eq!(settings.get::("test_struct").unwrap().unwrap(), test_struct); // Test with an option settings.put("test_struct_option", &Option::::None).unwrap(); assert!(settings.get::>("test_struct_option").unwrap().unwrap().is_none()); settings.put("test_struct_option", &Option::::Some("test".to_string())).unwrap(); assert_eq!(settings.get::>("test_struct_option").unwrap().unwrap(), Some("test".to_string())); }).await; }