Fix "device changed" notification showing stored devices as unregistered

record_sighting only carried the stored name and IP onto the reconciled
sighting, leaving is_registered, owner, and device_type at their bare
defaults. The change handed to the notification layer therefore rendered a
registered, typed device as "Not registered" with type "-". Carry those
fields over too, mirroring how db::devices::seen() preserves them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-06-15 13:46:30 -04:00
co-authored by Claude Opus 4.8
parent 8b2c8e7285
commit 916b081225
+62 -1
View File
@@ -15,7 +15,10 @@ use crate::notifications;
/// When the device is already known, the sighting is reconciled with the stored record:
/// - a previously stored hostname is kept rather than overwritten by this sighting's name;
/// - a known IP address is kept when this sighting carries none (an empty string), so a DHCP
/// DISCOVER (which has no assigned IP) never clobbers a good address.
/// DISCOVER (which has no assigned IP) never clobbers a good address;
/// - registration, owner, and a non-empty stored vendor/device_type are carried over, so the
/// change passed to the notification layer describes the device as it is stored rather than as
/// this bare sighting saw it.
///
/// Errors are logged and swallowed (an empty list is returned): a scan/listen loop must never stop
/// because a single sighting failed to persist.
@@ -29,6 +32,19 @@ pub fn record_sighting(mut device: Device, scanner: DeviceEventScanner) -> Vec<D
if device.ipv4_address.is_empty() {
device.ipv4_address = recorded.ipv4_address.clone();
}
// Carry forward the fields a sighting can never establish or improve, mirroring how
// db::devices::seen() preserves them: registration and owner are only ever set via the
// UI, and a non-empty stored vendor/device_type wins over what this sighting deduced.
// Without this, the reconciled device (used for notifications) would report a
// registered, typed device as "Not registered" with type "-".
device.is_registered = recorded.is_registered;
device.owner = recorded.owner.clone();
if device.vendor.is_empty() {
device.vendor = recorded.vendor.clone();
}
if !recorded.device_type.is_empty() {
device.device_type = recorded.device_type.clone();
}
if let Err(err) = db::devices::seen(
device.mac_address.clone(),
device.ipv4_address.clone(),
@@ -87,6 +103,51 @@ mod tests {
assert_eq!(stored.name, Some("printer".to_string()));
}
#[tokio::test]
async fn changed_notification_reflects_stored_registration_and_type() {
tests_common::setup().await;
let mac = "de:ad:be:ef:00:03".to_string();
// A device is discovered, then registered with an owner and a device type.
let first = Device::new(
mac.clone(),
"192.168.9.3".to_string(),
"Acme".to_string(),
Utc::now(),
);
record_sighting(first, DeviceEventScanner::Arp);
db::devices::register(
mac.clone(),
"Alice".to_string(),
"Smartphone".to_string(),
None,
)
.expect("register should succeed");
// It is later seen at a different IP by a scanner that carries no vendor or type.
let moved = Device::new(
mac.clone(),
"192.168.9.99".to_string(),
String::new(),
Utc::now(),
);
let changes = record_sighting(moved, DeviceEventScanner::Arp);
let new = changes
.iter()
.find_map(|change| match change {
DeviceChange::Changed { new, .. } => Some(new),
_ => None,
})
.expect("an IP change should be reported");
// The change must describe the device as it is stored, not as the bare sighting saw it.
assert!(new.is_registered);
assert_eq!(new.owner, "Alice");
assert_eq!(new.device_type, "Smartphone");
assert_eq!(new.vendor, "Acme");
}
#[tokio::test]
async fn known_device_keeps_stored_name_and_ip() {
tests_common::setup().await;