diff --git a/backend/oott2.toml b/backend/oott2.toml deleted file mode 100644 index d3f1a77..0000000 --- a/backend/oott2.toml +++ /dev/null @@ -1,22 +0,0 @@ -[database] -path="./oott.db" - -[networking] -interface = "wlp3s0" # Network interface to use for scans - -[log] -level = "debug" # off, error, warn, info, debug, trace - -[timings] -wait_between_scans="30m" # Wait time between scans. This does not include the scan time -arp_sender_timeout="1m" # If the ARP sender process takes longer than this it will be stopped (for a class C network - 254 IPs - it should take less than a minute) -arp_scan_duration="2m" # How long to wait for response packets on each scan (5m to 10m is a good timeframe for a class B or C network) - -[notifications] -# method="pushover" # For now just pushover, you can set this to "none" to avoid sending notifications (it will just log) -method="none" -notify_when_not_seen_for="1w" # Send a notification if a device comes back online after not being seen for this timeframe - -[notifications.pushover] -token="a7vxi8b24ncdmuyt65x7zytkxaozi3" # Your pushover token goes here, just copy&paste from their website after creating the app -user_key="u872udy8xc8ceys9ee6iur229xs3fj" # User key goes here, this is the account wide code for pushover diff --git a/backend/src/db.rs b/backend/src/db.rs index 4cf1f37..8144d8a 100644 --- a/backend/src/db.rs +++ b/backend/src/db.rs @@ -1,4 +1,5 @@ pub mod devices; +pub mod error; pub mod notifications; use include_dir::{Dir, include_dir}; diff --git a/backend/src/db/error.rs b/backend/src/db/error.rs new file mode 100644 index 0000000..cf95f6b --- /dev/null +++ b/backend/src/db/error.rs @@ -0,0 +1,28 @@ +use std::{error, fmt}; + +#[derive(Debug)] +pub enum DbError { + Parse(rusqlite::Error), +} + +impl fmt::Display for DbError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + DbError::Parse(..) => write!(f, "Database access error"), + } + } +} + +impl error::Error for DbError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match *self { + DbError::Parse(ref e) => Some(e), + } + } +} + +impl From for DbError { + fn from(err: rusqlite::Error) -> DbError { + DbError::Parse(err) + } +} diff --git a/backend/src/db/notifications.rs b/backend/src/db/notifications.rs index 443e14e..20f40e5 100644 --- a/backend/src/db/notifications.rs +++ b/backend/src/db/notifications.rs @@ -1,32 +1,127 @@ -use chrono::Local; +use crate::db; +use crate::db::error::DbError; use log::debug; -use crate::model::{self, notifications::Notification}; +use crate::model::notifications::Notification; -pub fn list() -> Vec { +pub fn list() -> Result, DbError> { debug!("Listing notifications"); - let mut result = Vec::new(); - result.push(Notification { - id: 1, - created_on: Local::now().to_utc(), - is_new: true, - notification_type: model::notifications::NotificationType::NewDeviceFound, - title: "title".to_string(), - body: "body".to_string(), - }); + let conn = db::get_db_connection(); - result + let mut stmt = conn.prepare( + "SELECT id, created_on, notification_type, title, body FROM notifications WHERE is_new=1", + )?; + + let notifications: Vec = stmt + .query_map([], |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: true, + }) + })? + .collect::>()?; + + Ok(notifications) } #[cfg(test)] mod tests { + use chrono::TimeZone; + use super::*; - use crate::tests_common; + use crate::{model::notifications::NotificationType, tests_common}; #[tokio::test] - async fn list_default() { + async fn test_list_default() { tests_common::setup().await; - let notifications = list(); - assert_eq!(notifications.len(), 1); + let notifications = list().unwrap(); + + // There should be 3 unread notifications + assert_eq!( + notifications.len(), + 3, + "There should be 3 notifications in the list" + ); + + // All notifications must be unread/new + for notification in notifications.iter() { + assert!( + notification.is_new, + "Notification {} is not new and there should only be new notifications in the list", + notification.id + ); + } + + // There should be only one of each type + assert_eq!( + 1, + notifications + .iter() + .filter(|notification| notification.notification_type == NotificationType::Other) + .count(), + "There should be only one notification of type Other" + ); + assert_eq!( + 1, + notifications + .iter() + .filter(|notification| notification.notification_type + == NotificationType::DeviceOnlineAfterTime) + .count(), + "There should be only one notification of type Other" + ); + assert_eq!( + 1, + notifications + .iter() + .filter(|notification| notification.notification_type + == NotificationType::NewDeviceFound) + .count(), + "There should be only one notification of type Other" + ); + + // Check date of notification 1 + let notification1 = notifications + .iter() + .filter(|notification| notification.id == 1) + .next() + .unwrap(); + + // 2026-01-03 14:13:12 + assert_eq!( + notification1.created_on, + Local.with_ymd_and_hms(2026, 1, 3, 14, 13, 12).unwrap(), + "Incorrect created_on date/time for notification 1." + ); + + // Check title of notification 3 + let notification3 = notifications + .iter() + .filter(|notification| notification.id == 3) + .next() + .unwrap(); + + assert_eq!( + notification3.title, "Unread device online after time", + "Notification 3 title incorrect: {}", + notification3.title + ); + + // Check body of notification 5 + let notification5 = notifications + .iter() + .filter(|notification| notification.id == 5) + .next() + .unwrap(); + + assert_eq!( + notification5.body, "Body other", + "Notification 5 body incorrect: {}", + notification5.body + ); } } diff --git a/backend/src/model/notifications.rs b/backend/src/model/notifications.rs index 2a93c00..1cdb30d 100644 --- a/backend/src/model/notifications.rs +++ b/backend/src/model/notifications.rs @@ -5,7 +5,7 @@ use std::{error::Error, fmt, str::FromStr}; #[derive(Clone, Serialize, Deserialize)] pub struct Notification { - pub id: u64, + pub id: i64, #[serde(with = "ts_seconds")] pub created_on: DateTime, pub notification_type: NotificationType, @@ -24,7 +24,13 @@ impl fmt::Display for Notification { } } -#[derive(Clone, Serialize, Deserialize)] +impl PartialEq for Notification { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum NotificationType { NewDeviceFound, DeviceOnlineAfterTime, diff --git a/backend/tests/database_setup/01-notifications.sql b/backend/tests/database_setup/01-notifications.sql index 75feec7..a63e9d2 100644 --- a/backend/tests/database_setup/01-notifications.sql +++ b/backend/tests/database_setup/01-notifications.sql @@ -1 +1,6 @@ -INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (1, '2026-01-03 14:13:12', 'NewDeviceFound', 'Title new device found', 'Body new device found', 1); +INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (1, '2026-01-03 14:13:12', 'NewDeviceFound', 'Unread new device found', 'Body unread new device found', 1); +INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (2, '2026-01-04 08:10:13', 'NewDeviceFound', 'Read new device found', 'Body read new device found', 0); +INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (3, '2026-01-05 17:20:01', 'DeviceOnlineAfterTime', 'Unread device online after time', 'Body unread device online after time', 1); +INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (4, '2026-01-06 02:11:12', 'DeviceOnlineAfterTime', 'Read device online after time', 'Body read device online after time', 0); +INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (5, '2026-02-01 11:11:11', 'Other', 'Unread other', 'Body other', 1); +INSERT INTO NOTIFICATIONS (id, created_on, notification_type, title, body, is_new) VALUES (6, '2026-02-03 13:13:13', 'Other', 'Read other', 'Body other', 0);