diff --git a/backend/src/db/notifications.rs b/backend/src/db/notifications.rs index bb09a27..226e41a 100644 --- a/backend/src/db/notifications.rs +++ b/backend/src/db/notifications.rs @@ -1,6 +1,7 @@ use crate::db; use crate::db::error::DbError; -use log::debug; +use log::{debug, error}; +use rusqlite::params; use crate::model::notifications::Notification; @@ -28,6 +29,55 @@ pub fn list() -> Result, DbError> { Ok(notifications) } +pub fn insert(notification: Notification) -> Result { + let conn = db::get_db_connection(); + + match conn.execute( + "INSERT INTO notifications (created_on, notification_type, title, body, is_new) VALUES (?1, ?2, ?3, ?4, ?5)", + params![notification.created_on, notification.notification_type, notification.title, notification.body, notification.is_new]) { + Ok(_) => { + debug!("Notification inserted into database: {}", notification); + Ok(conn.last_insert_rowid()) + }, + Err(error) => { + error!("Error inserting notification ({notification}) into database: {error}"); + Err(DbError::from(error)) + }, + } +} + +pub fn read(id: i64) -> Option { + let conn = db::get_db_connection(); + + let result: Result = conn.query_one( + "SELECT id, created_on, notification_type, title, body, is_new FROM notifications WHERE id=?1", + params![id], + |row| { + Ok(Notification { + id: row.get(0)?, + created_on: row.get(1)?, + notification_type: row.get(2)?, + title: row.get(3)?, + body: row.get(4)?, + is_new: row.get(5)?, + }) + }, + ); + + match result { + Ok(value) => Some(value), + Err(error) => { + match error { + rusqlite::Error::QueryReturnedNoRows => { + debug!("No notification found for id={id}.") + } + _ => error!("Error reading notification from database: {error}"), + }; + None + } + } +} + #[cfg(test)] mod tests { use chrono::{TimeZone, Utc}; @@ -35,16 +85,184 @@ mod tests { use super::*; use crate::{model::notifications::NotificationType, tests_common}; + #[tokio::test] + async fn test_insert_default() { + tests_common::setup().await; + + // Insert notification and read with returned ID + let created_on = Utc::now(); + let inserted_id = insert(Notification::new( + created_on, + NotificationType::DeviceOnlineAfterTime, + "New notification title".to_string(), + "New notification body".to_string(), + true, + )) + .unwrap(); + + assert!( + inserted_id >= 0, + "Inserted notification id should be positive" + ); + + // Validate all fields + let inserted_notification = read(inserted_id).unwrap(); + assert_eq!( + inserted_notification.notification_type, + NotificationType::DeviceOnlineAfterTime, + "Wrong notification type (should be DeviceOnlineAfterTime)" + ); + assert_eq!( + inserted_notification.is_new, true, + "Notification should be new" + ); + assert_eq!( + inserted_notification.created_on, created_on, + "Wrong created_on (should be ${created_on})" + ); + assert_eq!( + inserted_notification.title, "New notification title", + "Wrong title (should be 'New notification title')" + ); + assert_eq!( + inserted_notification.body, "New notification body", + "Wrong body (should be 'New notification body')" + ); + + // Insert and validate notification without title nor body + let created_on = Utc::now(); + let inserted_id = insert(Notification::new( + created_on, + NotificationType::Other, + "".to_string(), + "".to_string(), + false, + )) + .unwrap(); + + assert!( + inserted_id >= 0, + "Inserted notification id should be positive" + ); + + // Validate all fields + let inserted_notification = read(inserted_id).unwrap(); + assert_eq!( + inserted_notification.notification_type, + NotificationType::Other, + "Wrong notification type (should be Other)" + ); + assert_eq!( + inserted_notification.is_new, false, + "Notification should not be new" + ); + assert_eq!( + inserted_notification.created_on, created_on, + "Wrong created_on (should be ${created_on})" + ); + assert_eq!( + inserted_notification.title, "", + "Wrong title (should be empty)" + ); + assert_eq!( + inserted_notification.body, "", + "Wrong body (should be empty)" + ); + } + + #[tokio::test] + async fn test_read_default() { + tests_common::setup().await; + + // Read existing notification + let notification_option = read(2); + + // Notification should not be empty + assert!( + notification_option.is_some(), + "Notification id=2 should be read" + ); + + // Validate all fields + let notification = notification_option.unwrap(); + assert_eq!(notification.id, 2, "Wrong notification id (should be 2)"); + assert_eq!( + notification.notification_type, + NotificationType::NewDeviceFound, + "Wrong notification type (should be NewDeviceFound)" + ); + assert_eq!(notification.is_new, false, "Notification should not be new"); + assert_eq!( + notification.created_on, + Utc.with_ymd_and_hms(2026, 1, 4, 8, 10, 13).unwrap(), + "Wrong created_on (should be 2026-01-04 08:10:13)" + ); + assert_eq!( + notification.title, "Read new device found", + "Wrong title (should be 'Read new device found')" + ); + assert_eq!( + notification.body, "Body read new device found", + "Wrong body (should be 'Body read new device found')" + ); + + // Read non-existant notification + assert!( + read(999999999).is_none(), + "Notification id=999999999 should not exist" + ); + + // Insert notification and read with returned ID + let created_on = Utc::now(); + let inserted_id = insert(Notification::new( + created_on, + NotificationType::DeviceOnlineAfterTime, + "New notification title".to_string(), + "New notification body".to_string(), + true, + )) + .unwrap(); + + // Validate all fields + let inserted_notification = read(inserted_id).unwrap(); + assert_eq!( + inserted_notification.notification_type, + NotificationType::DeviceOnlineAfterTime, + "Wrong notification type (should be DeviceOnlineAfterTime)" + ); + assert_eq!( + inserted_notification.is_new, true, + "Notification should be new" + ); + assert_eq!( + inserted_notification.created_on, created_on, + "Wrong created_on (should be ${created_on})" + ); + assert_eq!( + inserted_notification.title, "New notification title", + "Wrong title (should be 'New notification title')" + ); + assert_eq!( + inserted_notification.body, "New notification body", + "Wrong body (should be 'New notification body')" + ); + + // Read non-existant notification + assert!( + read(999999999).is_none(), + "Notification id=999999999 should not exist" + ); + } + #[tokio::test] async fn test_list_default() { tests_common::setup().await; let notifications = list().unwrap(); - // There should be 3 unread notifications - assert_eq!( - notifications.len(), - 3, - "There should be 3 notifications in the list" + // There should be at least 3 unread notifications + assert!( + notifications.len() >= 3, + "There should be at least 3 notifications in the list" ); // All notifications must be unread/new @@ -56,32 +274,32 @@ mod tests { ); } - // There should be only one of each type - assert_eq!( - 1, + // There should be at least one of each type + assert!( notifications .iter() .filter(|notification| notification.notification_type == NotificationType::Other) - .count(), - "There should be only one notification of type Other" + .count() + >= 1, + "There should be at least one notification of type Other" ); - assert_eq!( - 1, + assert!( notifications .iter() .filter(|notification| notification.notification_type == NotificationType::DeviceOnlineAfterTime) - .count(), - "There should be only one notification of type Other" + .count() + >= 1, + "There should be at least one notification of type DeviceOnlineAfterTime" ); - assert_eq!( - 1, + assert!( notifications .iter() .filter(|notification| notification.notification_type == NotificationType::NewDeviceFound) - .count(), - "There should be only one notification of type Other" + .count() + >= 1, + "There should be at least one notification of type Other" ); // Check date of notification 1 diff --git a/backend/src/model/notifications.rs b/backend/src/model/notifications.rs index 1cdb30d..8cf0f2e 100644 --- a/backend/src/model/notifications.rs +++ b/backend/src/model/notifications.rs @@ -14,6 +14,25 @@ pub struct Notification { pub is_new: bool, } +impl Notification { + pub fn new( + created_on: DateTime, + notification_type: NotificationType, + title: String, + body: String, + is_new: bool, + ) -> Self { + Self { + id: -1, + created_on: created_on, + notification_type: notification_type, + title: title, + body: body, + is_new: is_new, + } + } +} + impl fmt::Display for Notification { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( @@ -30,7 +49,7 @@ impl PartialEq for Notification { } } -#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum NotificationType { NewDeviceFound, DeviceOnlineAfterTime,