diff --git a/services/commtest/run-tests-ci.sh b/services/commtest/run-tests-ci.sh --- a/services/commtest/run-tests-ci.sh +++ b/services/commtest/run-tests-ci.sh @@ -57,6 +57,7 @@ run_test identity_one_time_key_tests run_test identity_prekey_tests run_test identity_tunnelbroker_tests +run_test identity_device_list_tests if [ $NUM_FAILURES -eq 0 ]; then echo "COMMTEST: ALL TESTS PASSED" diff --git a/services/commtest/tests/identity_device_list_tests.rs b/services/commtest/tests/identity_device_list_tests.rs new file mode 100644 --- /dev/null +++ b/services/commtest/tests/identity_device_list_tests.rs @@ -0,0 +1,125 @@ +use std::str::FromStr; + +use commtest::identity::device::{ + login_user_device, logout_user_device, register_user_device, DEVICE_TYPE, + PLACEHOLDER_CODE_VERSION, +}; +use commtest::service_addr; +use grpc_clients::identity::get_auth_client; +use grpc_clients::identity::protos::authenticated::GetDeviceListRequest; +use grpc_clients::identity::DeviceType; +use serde::Deserialize; + +// 1. register user with android device +// 2. register a web device +// 3. remove android device +// 4. register ios device +// 5. get device list - should have 4 updates: +// - [android] +// - [android, web] +// - [web] +// - [ios, web] - mobile should be first +#[tokio::test] +async fn test_device_list_updates() { + use commtest::identity::olm_account_infos::{ + DEFAULT_CLIENT_KEYS as DEVICE_KEYS_ANDROID, + MOCK_CLIENT_KEYS_1 as DEVICE_KEYS_WEB, + MOCK_CLIENT_KEYS_2 as DEVICE_KEYS_IOS, + }; + + // Create viewer (user that doesn't change devices) + let viewer = register_user_device(None, None).await; + let mut auth_client = get_auth_client( + &service_addr::IDENTITY_GRPC.to_string(), + viewer.user_id.clone(), + viewer.device_id, + viewer.access_token, + PLACEHOLDER_CODE_VERSION, + DEVICE_TYPE.to_string(), + ) + .await + .expect("Couldn't connect to identity service"); + + let android_device_id = + &DEVICE_KEYS_ANDROID.primary_identity_public_keys.ed25519; + let web_device_id = &DEVICE_KEYS_WEB.primary_identity_public_keys.ed25519; + let ios_device_id = &DEVICE_KEYS_IOS.primary_identity_public_keys.ed25519; + + // 1. Register user with primary Android device + let android = + register_user_device(Some(&DEVICE_KEYS_ANDROID), Some(DeviceType::Android)) + .await; + let user_id = android.user_id.clone(); + let username = android.username.clone(); + + // 2. Log in a web device + let _web = + login_user_device(&username, Some(&DEVICE_KEYS_WEB), Some(DeviceType::Web)) + .await; + + // 3. Remove android device + logout_user_device(android).await; + + // 4. Log in an iOS device + let _ios = + login_user_device(&username, Some(&DEVICE_KEYS_IOS), Some(DeviceType::Ios)) + .await; + + // Get device list updates for the user + let request = GetDeviceListRequest { + user_id, + since_timestamp: None, + }; + let response = auth_client + .get_device_list_for_user(request) + .await + .expect("Get device list request failed") + .into_inner(); + + let device_lists_response: Vec> = response + .device_list_updates + .into_iter() + .map(|update| { + let update: DeviceListUpdate = serde_json::from_str(&update) + .expect("Failed to parse device list update"); + let device_list: RawDeviceList = update + .raw_device_list + .parse() + .expect("Failed to parse raw device list"); + device_list.devices + }) + .collect(); + + let expected_device_list: Vec> = vec![ + vec![android_device_id.into()], + vec![android_device_id.into(), web_device_id.into()], + vec![web_device_id.into()], + vec![ios_device_id.into(), web_device_id.into()], + ]; + + assert_eq!(device_lists_response, expected_device_list); +} + +// See GetDeviceListResponse in identity_authenticated.proto +// for details on the response format. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +#[allow(unused)] +struct RawDeviceList { + devices: Vec, + timestamp: i64, +} +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct DeviceListUpdate { + raw_device_list: String, +} + +impl FromStr for RawDeviceList { + type Err = serde_json::Error; + fn from_str(s: &str) -> Result { + // The device list payload is sent as an escaped JSON payload. + // Escaped double quotes need to be trimmed before attempting to deserialize + serde_json::from_str(&s.replace(r#"\""#, r#"""#)) + } +}