Added list devices to database handler

This commit is contained in:
rzuasti
2026-02-25 12:54:39 -05:00
parent be38d2b15f
commit f45a22a892
2 changed files with 360 additions and 94 deletions
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
rm -f oott.db
cargo test -- --show-output
+357 -94
View File
@@ -1,11 +1,95 @@
use chrono::{DateTime, Utc};
use log::{debug, error};
use rusqlite::params;
use rusqlite::{ToSql, params, params_from_iter};
use crate::{
db::{self, error::DbError},
model::devices::Device,
};
pub fn list_devices(
is_registered: Option<bool>,
last_seen_from: Option<DateTime<Utc>>,
last_seen_to: Option<DateTime<Utc>>,
owner: Option<String>,
device_type: Option<String>,
vendor: Option<String>,
) -> Result<Vec<Device>, DbError> {
debug!("Listing devices");
let conn = db::get_db_connection();
// Prepare SQL and parameters
let mut sql_statement = "SELECT mac_address, ipv4_address, vendor, last_seen, is_registered, owner, device_type FROM devices WHERE 1=1 ".to_string();
let mut params: Vec<rusqlite::types::Value> = Vec::new();
if is_registered.is_some() {
debug!("Adding filter is_registered={}", is_registered.unwrap());
sql_statement.push_str("AND is_registered=? ");
params.push(is_registered.unwrap().into());
};
if last_seen_from.is_some() {
debug!(
"Adding filter last_seen>={}",
last_seen_from.unwrap().format("%Y-%m-%d %H:%M:%S")
);
sql_statement.push_str("AND last_seen>=? ");
params.push(
last_seen_from
.unwrap()
.format("%Y-%m-%d %H:%M:%S")
.to_string()
.into(),
);
};
if last_seen_to.is_some() {
debug!(
"Adding filter last_seen<={}",
last_seen_to.unwrap().format("%Y-%m-%d %H:%M:%S")
);
sql_statement.push_str("AND last_seen<=? ");
params.push(
last_seen_to
.unwrap()
.format("%Y-%m-%d %H:%M:%S")
.to_string()
.into(),
);
};
if owner.is_some() {
debug!("Adding filter owner={}", owner.clone().unwrap());
sql_statement.push_str("AND owner=? ");
params.push(owner.unwrap().into());
};
if device_type.is_some() {
debug!("Adding filter device_type={}", device_type.clone().unwrap());
sql_statement.push_str("AND device_type=? ");
params.push(device_type.unwrap().into());
}
if vendor.is_some() {
debug!("Adding filter vendor={}", vendor.clone().unwrap());
sql_statement.push_str("AND vendor=? ");
params.push(vendor.unwrap().into());
}
let mut stmt = conn.prepare(sql_statement.as_str())?;
let devices: Vec<Device> = stmt
.query_map(params_from_iter(params.iter()), |row| {
Ok(Device {
mac_address: row.get(0)?,
ipv4_address: row.get(1)?,
vendor: row.get(2)?,
last_seen: row.get(3)?,
is_registered: row.get(4)?,
owner: row.get(5)?,
device_type: row.get(6)?,
})
})?
.collect::<Result<_, _>>()?;
Ok(devices)
}
// Read device from its MAC address
pub fn read(mac_address: String) -> Option<Device> {
let conn = db::get_db_connection();
@@ -84,11 +168,218 @@ pub fn update(device: Device) -> Result<(), DbError> {
#[cfg(test)]
mod tests {
use chrono::{TimeZone, Utc};
use chrono::{DateTime, TimeZone, Utc};
use super::*;
use crate::tests_common;
#[tokio::test]
async fn test_list() {
tests_common::setup().await;
// List all devices
let devices: Vec<Device> = list_devices(None, None, None, None, None, None).unwrap();
assert!(devices.len() >= 3, "There should be at least 3 devices");
// Validate 1 device data
let device = devices
.iter()
.filter(|item| item.mac_address == "bb:bb:bb:bb:bb:bb")
.next()
.unwrap();
validate_device(
device.clone(),
"bb:bb:bb:bb:bb:bb".to_string(),
"Phone".to_string(),
"192.168.0.2".to_string(),
true,
Utc.with_ymd_and_hms(2026, 2, 3, 13, 14, 15).unwrap(),
"John".to_string(),
"Vendor 2".to_string(),
);
// List registered devices
let devices: Vec<Device> = list_devices(Some(true), None, None, None, None, None).unwrap();
assert!(
devices.len() >= 2,
"There should be at least 2 registered devices"
);
// Device aa:aa:aa:aa:aa:aa should not be present
assert!(
devices
.iter()
.filter(|item| item.mac_address == "aa:aa:aa:aa:aa:aa")
.next()
.is_none(),
"Device aa:aa:aa:aa:aa:aa should not be present"
);
// All devices should be registered
assert!(
devices
.iter()
.filter(|item| !item.is_registered)
.next()
.is_none(),
"There should not be any non-registered devices"
);
// Validate 1 device
let device = devices
.iter()
.filter(|item| item.mac_address == "bb:bb:bb:bb:bb:bb")
.next()
.unwrap();
validate_device(
device.clone(),
"bb:bb:bb:bb:bb:bb".to_string(),
"Phone".to_string(),
"192.168.0.2".to_string(),
true,
Utc.with_ymd_and_hms(2026, 2, 3, 13, 14, 15).unwrap(),
"John".to_string(),
"Vendor 2".to_string(),
);
// List devices seen in a time period - 2026-02-03 13:14:15
let devices = list_devices(
None,
Some(Utc.with_ymd_and_hms(2026, 2, 2, 13, 13, 13).unwrap()),
Some(Utc.with_ymd_and_hms(2026, 2, 4, 11, 11, 11).unwrap()),
None,
None,
None,
)
.unwrap();
assert!(devices.len() >= 1, "There should be at least one device");
assert!(
devices
.iter()
.filter(|item| item.mac_address == "bb:bb:bb:bb:bb:bb")
.next()
.is_some(),
"Device bb:bb:bb:bb:bb:bb should be present"
);
}
#[tokio::test]
async fn test_update() {
tests_common::setup().await;
// Insert a device, update it and validate it
let last_seen = Utc::now();
insert(Device::new(
"uu:tt:tt:tt:tt:aa".to_string(),
"192.168.200.1".to_string(),
"Test vendor".to_string(),
last_seen,
))
.unwrap();
let mut device = read("uu:tt:tt:tt:tt:aa".to_string()).unwrap();
device.is_registered = true;
device.owner = "Grace".to_string();
device.device_type = "Phone".to_string();
update(device).unwrap();
let device = read("uu:tt:tt:tt:tt:aa".to_string()).unwrap();
validate_device(
device,
"uu:tt:tt:tt:tt:aa".to_string(),
"Phone".to_string(),
"192.168.200.1".to_string(),
true,
last_seen,
"Grace".to_string(),
"Test vendor".to_string(),
);
// Insert a device, change its MAC address and validate it hasnt changed (it cannot)
let last_seen = Utc::now();
insert(Device::new(
"uu:tt:tt:tt:tt:bb".to_string(),
"192.168.200.3".to_string(),
"Test vendor".to_string(),
last_seen,
))
.unwrap();
let mut device = read("uu:tt:tt:tt:tt:bb".to_string()).unwrap();
device.mac_address = "uu:tt:tt:tt:tt:cc".to_string();
update(device).unwrap();
assert!(
read("uu:tt:tt:tt:tt:cc".to_string()).is_none(),
"Device should not exist"
);
assert!(
read("uu:tt:tt:tt:tt:bb".to_string()).is_some(),
"Device should exist"
)
}
#[tokio::test]
async fn test_insert() {
tests_common::setup().await;
// Insert constructed device and validate it
let last_seen = Utc::now();
insert(Device::new(
"tt:tt:tt:tt:tt:aa".to_string(),
"192.168.100.1".to_string(),
"Test vendor".to_string(),
last_seen,
))
.unwrap();
let device = read("tt:tt:tt:tt:tt:aa".to_string()).unwrap();
validate_device(
device,
"tt:tt:tt:tt:tt:aa".to_string(),
"".to_string(),
"192.168.100.1".to_string(),
false,
last_seen,
"".to_string(),
"Test vendor".to_string(),
);
// Insert complete device and validate it
let last_seen = Utc::now();
insert(Device {
mac_address: "tt:tt:tt:tt:tt:bb".to_string(),
device_type: "Server".to_string(),
ipv4_address: "192.168.100.2".to_string(),
is_registered: true,
last_seen: last_seen,
owner: "Carl".to_string(),
vendor: "Vendor X".to_string(),
})
.unwrap();
let device = read("tt:tt:tt:tt:tt:bb".to_string()).unwrap();
validate_device(
device,
"tt:tt:tt:tt:tt:bb".to_string(),
"Server".to_string(),
"192.168.100.2".to_string(),
true,
last_seen,
"Carl".to_string(),
"Vendor X".to_string(),
);
}
#[tokio::test]
async fn test_read() {
tests_common::setup().await;
@@ -101,116 +392,88 @@ mod tests {
// Read all testing devices and validate their values
// 'aa:aa:aa:aa:aa:aa', '192.168.0.1', 'Vendor 1', '2026-01-01 11:11:11', 0, '', ''
let device1 = read("aa:aa:aa:aa:aa:aa".to_string()).unwrap();
assert_eq!(
device1.mac_address, "aa:aa:aa:aa:aa:aa",
"Invalid MAC address for device aa:aa:aa:aa:aa:aa"
);
assert_eq!(
device1.device_type, "",
"Device type should be empty for device {}",
device1.mac_address
);
assert_eq!(
device1.ipv4_address, "192.168.0.1",
"Invalid IP address for device {}",
device1.mac_address
);
assert!(
!device1.is_registered,
"Device {} should not be registered",
device1.mac_address
);
assert_eq!(
device1.last_seen,
validate_device(
device1,
"aa:aa:aa:aa:aa:aa".to_string(),
"".to_string(),
"192.168.0.1".to_string(),
false,
Utc.with_ymd_and_hms(2026, 1, 1, 11, 11, 11).unwrap(),
"Invalid last_seen for device {}",
device1.mac_address
);
assert_eq!(
device1.owner, "",
"Invalid owner for device {}",
device1.mac_address
);
assert_eq!(
device1.vendor, "Vendor 1",
"Invalid vendor for device {}",
device1.mac_address
"".to_string(),
"Vendor 1".to_string(),
);
// 'bb:bb:bb:bb:bb:bb', '192.168.0.2', 'Vendor 2', '2026-02-03 13:14:15', 1, 'John', 'Phone'
let device2 = read("bb:bb:bb:bb:bb:bb".to_string()).unwrap();
assert_eq!(
device2.mac_address, "bb:bb:bb:bb:bb:bb",
"Invalid MAC address for device bb:bb:bb:bb:bb:bb"
);
assert_eq!(
device2.device_type, "Phone",
"Device type should be 'Phone' for device {}",
device2.mac_address
);
assert_eq!(
device2.ipv4_address, "192.168.0.2",
"Invalid IP address for device {}",
device2.mac_address
);
assert!(
device2.is_registered,
"Device {} should not be registered",
device2.mac_address
);
assert_eq!(
device2.last_seen,
validate_device(
device2,
"bb:bb:bb:bb:bb:bb".to_string(),
"Phone".to_string(),
"192.168.0.2".to_string(),
true,
Utc.with_ymd_and_hms(2026, 2, 3, 13, 14, 15).unwrap(),
"Invalid last_seen for device {}",
device2.mac_address
);
assert_eq!(
device2.owner, "John",
"Invalid owner for device {}",
device2.mac_address
);
assert_eq!(
device2.vendor, "Vendor 2",
"Invalid vendor for device {}",
device2.mac_address
"John".to_string(),
"Vendor 2".to_string(),
);
// 'cc:cc:cc:cc:cc:cc', '192.168.0.3', 'Vendor 3', '2026-02-17 20:11:00', 1, 'Sarah', 'Laptop'
let device3 = read("cc:cc:cc:cc:cc:cc".to_string()).unwrap();
assert_eq!(
device3.mac_address, "cc:cc:cc:cc:cc:cc",
"Invalid MAC address for device cc:cc:cc:cc:cc:cc"
);
assert_eq!(
device3.device_type, "Laptop",
"Device type should be 'Laptop' for device {}",
device3.mac_address
);
assert_eq!(
device3.ipv4_address, "192.168.0.3",
"Invalid IP address for device {}",
device3.mac_address
);
assert!(
device3.is_registered,
"Device {} should not be registered",
device3.mac_address
);
assert_eq!(
device3.last_seen,
validate_device(
device3,
"cc:cc:cc:cc:cc:cc".to_string(),
"Laptop".to_string(),
"192.168.0.3".to_string(),
true,
Utc.with_ymd_and_hms(2026, 2, 17, 20, 11, 00).unwrap(),
"Sarah".to_string(),
"Vendor 3".to_string(),
);
}
fn validate_device(
device: Device,
mac_address: String,
device_type: String,
ipv4_address: String,
is_registered: bool,
last_seen: DateTime<Utc>,
owner: String,
vendor: String,
) {
assert_eq!(
device.mac_address, mac_address,
"Invalid MAC address for device {}",
device.mac_address
);
assert_eq!(
device.device_type, device_type,
"Device type should be empty for device {}",
device.mac_address
);
assert_eq!(
device.ipv4_address, ipv4_address,
"Invalid IP address for device {}",
device.mac_address
);
assert_eq!(
device.is_registered, is_registered,
"Invalid registration status for device {}",
device.mac_address
);
assert_eq!(
device.last_seen, last_seen,
"Invalid last_seen for device {}",
device3.mac_address
device.mac_address
);
assert_eq!(
device3.owner, "Sarah",
device.owner, owner,
"Invalid owner for device {}",
device3.mac_address
device.mac_address
);
assert_eq!(
device3.vendor, "Vendor 3",
device.vendor, vendor,
"Invalid vendor for device {}",
device3.mac_address
device.mac_address
);
}
}