diff --git a/services/identity/src/database/device_list.rs b/services/identity/src/database/device_list.rs --- a/services/identity/src/database/device_list.rs +++ b/services/identity/src/database/device_list.rs @@ -988,3 +988,185 @@ ) .consistent_read(true) } + +// Helper module for "migration" code into new device list schema. +// We can get rid of this when primary device takes over the responsibility +// of managing the device list. +mod migration { + use std::cmp::Ordering; + + use super::*; + + /// Returns reference to primary device (if any) from given list of devices + /// or None if there's no valid primary device. + fn determine_primary_device(devices: &[DeviceRow]) -> Option<&DeviceRow> { + // 1. Find mobile devices with valid token + // 2. Prioritize these with latest code version + // 3. If there's a tie, select the one with latest login time + + let mut mobile_devices = devices + .iter() + .filter(|device| { + device.device_type == DeviceType::Ios + || device.device_type == DeviceType::Android + }) + .collect::>(); + + mobile_devices.sort_by(|a, b| { + let code_version_cmp = b.code_version.cmp(&a.code_version); + if code_version_cmp == Ordering::Equal { + b.login_time.cmp(&a.login_time) + } else { + code_version_cmp + } + }); + mobile_devices.first().cloned() + } + + mod tests { + use super::*; + use chrono::Duration; + + #[test] + fn determine_primary_device_returns_none_for_empty_list() { + let devices = vec![]; + assert!(determine_primary_device(&devices).is_none()); + } + + #[test] + fn determine_primary_device_returns_none_for_web_only() { + let devices = + vec![create_test_device("web", DeviceType::Web, 0, Utc::now())]; + + assert!( + determine_primary_device(&devices).is_none(), + "Primary device should be None for web-only devices" + ); + } + + #[test] + fn determine_primary_device_prioritizes_mobile() { + let devices = vec![ + create_test_device("mobile", DeviceType::Android, 0, Utc::now()), + create_test_device("web", DeviceType::Web, 0, Utc::now()), + ]; + + let primary_device = determine_primary_device(&devices) + .expect("Primary device should be present"); + assert_eq!( + primary_device.device_id, "mobile", + "Primary device should be mobile" + ); + } + + #[test] + fn determine_primary_device_prioritizes_latest_code_version() { + let devices_with_latest_code_version = vec![ + create_test_device("mobile1", DeviceType::Android, 1, Utc::now()), + create_test_device("mobile2", DeviceType::Android, 2, Utc::now()), + create_test_device("web", DeviceType::Web, 0, Utc::now()), + ]; + + let primary_device = + determine_primary_device(&devices_with_latest_code_version) + .expect("Primary device should be present"); + + assert_eq!( + primary_device.device_id, "mobile2", + "Primary device should be mobile with latest code version" + ); + } + + #[test] + fn determine_primary_device_prioritizes_latest_login_time() { + let devices = vec![ + create_test_device("mobile1_today", DeviceType::Ios, 1, Utc::now()), + create_test_device( + "mobile2_yesterday", + DeviceType::Android, + 1, + Utc::now() - Duration::days(1), + ), + create_test_device("web", DeviceType::Web, 0, Utc::now()), + ]; + + let primary_device = determine_primary_device(&devices) + .expect("Primary device should be present"); + + assert_eq!( + primary_device.device_id, "mobile1_today", + "Primary device should be mobile with latest login time" + ); + } + + #[test] + fn determine_primary_device_keeps_deterministic_order() { + // Given two identical devices, the first one should be selected as primary + let today = Utc::now(); + let devices_with_latest_code_version = vec![ + create_test_device("mobile1", DeviceType::Android, 1, today), + create_test_device("mobile2", DeviceType::Android, 1, today), + ]; + + let primary_device = + determine_primary_device(&devices_with_latest_code_version) + .expect("Primary device should be present"); + + assert_eq!( + primary_device.device_id, "mobile1", + "Primary device selection should be deterministic" + ); + } + + #[test] + fn determine_primary_device_all_rules_together() { + use DeviceType::{Android, Ios, Web}; + let today = Utc::now(); + let yesterday = today - Duration::days(1); + + let devices = vec![ + create_test_device("mobile1_today", Android, 1, today), + create_test_device("mobile2_today", Android, 2, today), + create_test_device("mobile3_yesterday", Ios, 1, yesterday), + create_test_device("mobile4_yesterday", Ios, 2, yesterday), + create_test_device("web", Web, 5, today), + ]; + + let primary_device = determine_primary_device(&devices) + .expect("Primary device should be present"); + + assert_eq!( + primary_device.device_id, "mobile2_today", + "Primary device should be mobile with latest code version and login time" + ); + } + + fn create_test_device( + id: &str, + platform: DeviceType, + code_version: u64, + login_time: DateTime, + ) -> DeviceRow { + DeviceRow { + user_id: "test".into(), + device_id: id.into(), + device_type: platform, + device_key_info: IdentityKeyInfo { + key_payload: "".into(), + key_payload_signature: "".into(), + social_proof: None, + }, + content_prekey: PreKey { + pre_key: "".into(), + pre_key_signature: "".into(), + }, + notif_prekey: PreKey { + pre_key: "".into(), + pre_key_signature: "".into(), + }, + code_version, + login_time, + } + } + } +}