From 64e2e985e97813ff2ee33050e07fea630ea44ccf Mon Sep 17 00:00:00 2001 From: Andre Rosado Date: Tue, 21 Apr 2026 13:09:05 +0100 Subject: [PATCH] Removed unnecessary api call. Refactored methods to use existing currentDeviceId. Better string naming Added more tests --- .../data/auth/repository/AuthRepository.kt | 6 - .../auth/repository/AuthRepositoryImpl.kt | 15 +- .../data/auth/repository/model/DeviceInfo.kt | 4 +- .../auth/repository/model/GetDeviceResult.kt | 16 -- .../util/DeviceResponseJsonExtensions.kt | 3 +- .../AccountSecurityViewModel.kt | 2 +- .../managedevices/ManageDevicesScreen.kt | 2 +- .../managedevices/ManageDevicesViewModel.kt | 60 +++---- .../util/DeviceTypeExtensions.kt | 56 +++--- .../auth/repository/AuthRepositoryTest.kt | 45 +---- .../AccountSecurityViewModelTest.kt | 20 +++ .../managedevices/ManageDevicesScreenTest.kt | 1 - .../ManageDevicesViewModelTest.kt | 56 +++--- .../util/DeviceLastActivityExtensionsTest.kt | 107 ++++++++++++ .../util/DeviceTypeExtensionsTest.kt | 159 +++++++++++++++--- .../network/api/AuthenticatedDevicesApi.kt | 6 - .../network/model/DeviceResponseJson.kt | 2 +- .../network/service/DevicesService.kt | 6 - .../network/service/DevicesServiceImpl.kt | 6 - .../network/service/DevicesServiceTest.kt | 54 ++++++ .../components/debug/FeatureFlagListItems.kt | 2 +- ui/src/main/res/values/strings.xml | 10 +- .../main/res/values/strings_non_localized.xml | 1 + 23 files changed, 404 insertions(+), 235 deletions(-) delete mode 100644 app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/model/GetDeviceResult.kt create mode 100644 app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/util/DeviceLastActivityExtensionsTest.kt diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt index 3da6da9e17..51ad10afbb 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt @@ -11,7 +11,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.AuthState import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult -import com.x8bit.bitwarden.data.auth.repository.model.GetDeviceResult import com.x8bit.bitwarden.data.auth.repository.model.GetDevicesResult import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult import com.x8bit.bitwarden.data.auth.repository.model.LeaveOrganizationResult @@ -361,11 +360,6 @@ interface AuthRepository : */ suspend fun getDevices(): GetDevicesResult - /** - * Retrieves the device matching this app's unique identifier. - */ - suspend fun getDeviceByIdentifier(): GetDeviceResult - /** * Get a [Boolean] indicating whether this is a known device. */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index e2a34d5426..819b6455bc 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -70,7 +70,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.AuthState import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult -import com.x8bit.bitwarden.data.auth.repository.model.GetDeviceResult import com.x8bit.bitwarden.data.auth.repository.model.GetDevicesResult import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult import com.x8bit.bitwarden.data.auth.repository.model.LeaveOrganizationResult @@ -1278,24 +1277,12 @@ class AuthRepositoryImpl( onSuccess = { response -> GetDevicesResult.Success( devices = response.devices.map { json -> - json.toDeviceInfo() + json.toDeviceInfo(currentDeviceIdentifier = authDiskSource.uniqueAppId) }, ) }, ) - override suspend fun getDeviceByIdentifier(): GetDeviceResult = - devicesService - .getDeviceByIdentifier(authDiskSource.uniqueAppId) - .fold( - onFailure = { GetDeviceResult.Error }, - onSuccess = { json -> - GetDeviceResult.Success( - device = json.toDeviceInfo(), - ) - }, - ) - override suspend fun getIsKnownDevice(emailAddress: String): KnownDeviceResult = devicesService .getIsKnownDevice( diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/model/DeviceInfo.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/model/DeviceInfo.kt index f19a1b5473..490f9750e5 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/model/DeviceInfo.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/model/DeviceInfo.kt @@ -9,12 +9,13 @@ import java.time.Instant * * @property id The unique identifier of the device. * @property name The name of the device. - * @property identifier The unique identifier of the device. + * @property identifier The unique device install identifier of the device. * @property type The type of the device. * @property isTrusted Whether this device is trusted. * @property creationDate The date and time on which this device was created. * @property lastActivityDate The date and time of the device's last activity, if available. * @property pendingAuthRequest The pending auth request for this device, if any. + * @property isCurrentDevice If this is the current device being used. */ @Parcelize data class DeviceInfo( @@ -26,4 +27,5 @@ data class DeviceInfo( val creationDate: Instant, val lastActivityDate: Instant?, val pendingAuthRequest: DevicePendingAuthRequest?, + val isCurrentDevice: Boolean, ) : Parcelable diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/model/GetDeviceResult.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/model/GetDeviceResult.kt deleted file mode 100644 index 44ec984aa7..0000000000 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/model/GetDeviceResult.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.x8bit.bitwarden.data.auth.repository.model - -/** - * Models result of retrieving the device matching this app's unique identifier. - */ -sealed class GetDeviceResult { - /** - * Contains the [DeviceInfo] for the current device. - */ - data class Success(val device: DeviceInfo) : GetDeviceResult() - - /** - * There was an error retrieving the device. - */ - data object Error : GetDeviceResult() -} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/DeviceResponseJsonExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/DeviceResponseJsonExtensions.kt index 2bf86345a2..49149e9fd5 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/DeviceResponseJsonExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/DeviceResponseJsonExtensions.kt @@ -7,7 +7,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.DevicePendingAuthRequest /** * Maps the given [DeviceResponseJson] to a [DeviceInfo]. */ -fun DeviceResponseJson.toDeviceInfo(): DeviceInfo = +fun DeviceResponseJson.toDeviceInfo(currentDeviceIdentifier: String): DeviceInfo = DeviceInfo( id = id, name = name, @@ -22,4 +22,5 @@ fun DeviceResponseJson.toDeviceInfo(): DeviceInfo = creationDate = it.creationDate, ) }, + isCurrentDevice = identifier == currentDeviceIdentifier, ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt index 8a48011436..18fd84c8fe 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt @@ -4,6 +4,7 @@ import android.os.Build import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.bitwarden.core.data.manager.model.FlagKey import com.bitwarden.core.util.isBuildVersionAtLeast import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault import com.bitwarden.network.model.PolicyTypeJson @@ -18,7 +19,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult import com.x8bit.bitwarden.data.auth.repository.util.policyInformation -import com.bitwarden.core.data.manager.model.FlagKey import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/ManageDevicesScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/ManageDevicesScreen.kt index 1ed1df92cc..3dfce965e6 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/ManageDevicesScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/ManageDevicesScreen.kt @@ -224,7 +224,7 @@ private fun ManageDevicesContent( dividerPadding = 0.dp, ), modifier = Modifier - .testTag("LoginRequestCell") + .testTag("CurrentItemCell") .fillMaxWidth() .standardHorizontalMargin(), ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/ManageDevicesViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/ManageDevicesViewModel.kt index 72c528d8be..0d2050115c 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/ManageDevicesViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/ManageDevicesViewModel.kt @@ -20,17 +20,13 @@ import com.x8bit.bitwarden.data.auth.manager.model.AuthRequest import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestsUpdatesResult import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.DeviceInfo -import com.x8bit.bitwarden.data.auth.repository.model.GetDeviceResult import com.x8bit.bitwarden.data.auth.repository.model.GetDevicesResult import com.x8bit.bitwarden.data.platform.repository.SettingsRepository -import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.managedevices.util.toLastActivityLabel import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.managedevices.util.readableDeviceTypeName -import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager +import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.managedevices.util.toLastActivityLabel import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -51,7 +47,6 @@ private const val KEY_STATE = "state" class ManageDevicesViewModel @Inject constructor( private val clock: Clock, private val authRepository: AuthRepository, - private val resourceManager: ResourceManager, snackbarRelayManager: SnackbarRelayManager, settingsRepository: SettingsRepository, buildInfoManager: BuildInfoManager, @@ -60,7 +55,6 @@ class ManageDevicesViewModel @Inject constructor( initialState = savedStateHandle[KEY_STATE] ?: ManageDevicesState( authRequests = emptyList(), devices = emptyList(), - currentDeviceId = null, viewState = ManageDevicesState.ViewState.Loading, isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value, isRefreshing = false, @@ -71,7 +65,6 @@ class ManageDevicesViewModel @Inject constructor( ), ) { private var authJob: Job = Job().apply { complete() } - private var devicesJob: Job = Job().apply { complete() } init { updateAuthRequestList() @@ -112,19 +105,20 @@ class ManageDevicesViewModel @Inject constructor( private fun handleOnLifecycleResumed() { updateAuthRequestList() - fetchAllDevices() } private fun handleRefreshPull() { + val shouldRefetchDevices = !state.devicesLoaded mutableStateFlow.update { it.copy( isRefreshing = true, - devicesLoaded = false, authRequestsLoaded = false, ) } updateAuthRequestList() - fetchAllDevices() + if (shouldRefetchDevices) { + fetchAllDevices() + } } private fun handlePendingRequestRowClicked( @@ -143,8 +137,8 @@ class ManageDevicesViewModel @Inject constructor( handleSnackbarDataReceive(action) } - is ManageDevicesAction.Internal.AllDevicesResultReceive -> { - handleAllDevicesResultReceived(action) + is ManageDevicesAction.Internal.GetDevicesResultReceive -> { + handleGetDevicesResultReceived(action) } is ManageDevicesAction.Internal.AuthRequestsResultReceive -> { @@ -177,18 +171,12 @@ class ManageDevicesViewModel @Inject constructor( } private fun fetchAllDevices() { - devicesJob.cancel() - devicesJob = viewModelScope.launch { - coroutineScope { - val devicesDeferred = async { authRepository.getDevices() } - val currentDeviceDeferred = async { authRepository.getDeviceByIdentifier() } - sendAction( - ManageDevicesAction.Internal.AllDevicesResultReceive( - devicesResult = devicesDeferred.await(), - currentDeviceResult = currentDeviceDeferred.await(), - ), - ) - } + viewModelScope.launch { + sendAction( + ManageDevicesAction.Internal.GetDevicesResultReceive( + devicesResult = authRepository.getDevices(), + ), + ) } } @@ -213,8 +201,8 @@ class ManageDevicesViewModel @Inject constructor( } } - private fun handleAllDevicesResultReceived( - action: ManageDevicesAction.Internal.AllDevicesResultReceive, + private fun handleGetDevicesResultReceived( + action: ManageDevicesAction.Internal.GetDevicesResultReceive, ) { val devicesResult = action.devicesResult as? GetDevicesResult.Success ?: run { @@ -223,18 +211,10 @@ class ManageDevicesViewModel @Inject constructor( } return } - val currentDeviceResult = action.currentDeviceResult as? GetDeviceResult.Success - ?: run { - mutableStateFlow.update { - it.copy(viewState = ManageDevicesState.ViewState.Error, isRefreshing = false) - } - return - } mutableStateFlow.update { it.copy( devices = devicesResult.devices, - currentDeviceId = currentDeviceResult.device.id, devicesLoaded = true, isRefreshing = if (state.authRequestsLoaded) false else it.isRefreshing, ) @@ -251,7 +231,7 @@ class ManageDevicesViewModel @Inject constructor( compareBy { device -> val matchingRequest = device.pendingAuthRequest?.let { authRequestMap[it.id] } when { - device.id == state.currentDeviceId -> 0 + device.isCurrentDevice -> 0 matchingRequest != null -> 1 else -> 2 } @@ -262,7 +242,7 @@ class ManageDevicesViewModel @Inject constructor( .map { device -> val matchingRequest = device.pendingAuthRequest?.let { authRequestMap[it.id] } val status = when { - device.id == state.currentDeviceId -> DeviceSessionStatus.Current + device.isCurrentDevice -> DeviceSessionStatus.Current matchingRequest != null -> DeviceSessionStatus.Pending else -> DeviceSessionStatus.None } @@ -296,7 +276,6 @@ class ManageDevicesViewModel @Inject constructor( data class ManageDevicesState( val authRequests: List, val devices: List, - val currentDeviceId: String?, val viewState: ViewState, private val isPullToRefreshSettingEnabled: Boolean, val isRefreshing: Boolean, @@ -451,11 +430,10 @@ sealed class ManageDevicesAction { ) : Internal() /** - * Indicates that the combined result of fetching all devices has been received. + * Indicates that the get devices has been received. */ - data class AllDevicesResultReceive( + data class GetDevicesResultReceive( val devicesResult: GetDevicesResult, - val currentDeviceResult: GetDeviceResult, ) : Internal() /** diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/util/DeviceTypeExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/util/DeviceTypeExtensions.kt index d697a6edd2..b983b6aba6 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/util/DeviceTypeExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/util/DeviceTypeExtensions.kt @@ -4,7 +4,6 @@ import androidx.annotation.StringRes import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText -import com.bitwarden.ui.util.concat private data class DeviceTypeEntry(@StringRes val categoryResId: Int, val platform: String) @@ -16,41 +15,38 @@ private data class DeviceTypeEntry(@StringRes val categoryResId: Int, val platfo val Int.readableDeviceTypeName: Text get() { val entry: DeviceTypeEntry = when (this) { - 0 -> DeviceTypeEntry(BitwardenString.mobile, "Android") - 1 -> DeviceTypeEntry(BitwardenString.mobile, "iOS") - 2 -> DeviceTypeEntry(BitwardenString.extension, "Chrome") - 3 -> DeviceTypeEntry(BitwardenString.extension, "Firefox") - 4 -> DeviceTypeEntry(BitwardenString.extension, "Opera") - 5 -> DeviceTypeEntry(BitwardenString.extension, "Edge") - 6 -> DeviceTypeEntry(BitwardenString.desktop, "Windows") - 7 -> DeviceTypeEntry(BitwardenString.desktop, "MacOS") - 8 -> DeviceTypeEntry(BitwardenString.desktop, "Linux") - 9 -> DeviceTypeEntry(BitwardenString.web, "Chrome") - 10 -> DeviceTypeEntry(BitwardenString.web, "Firefox") - 11 -> DeviceTypeEntry(BitwardenString.web, "Opera") - 12 -> DeviceTypeEntry(BitwardenString.web, "Edge") - 13 -> DeviceTypeEntry(BitwardenString.web, "IE") - 14 -> DeviceTypeEntry(BitwardenString.web, "Unknown") - 15 -> DeviceTypeEntry(BitwardenString.mobile, "Amazon") - 16 -> DeviceTypeEntry(BitwardenString.desktop, "Windows UWP") - 17 -> DeviceTypeEntry(BitwardenString.web, "Safari") - 18 -> DeviceTypeEntry(BitwardenString.web, "Vivaldi") - 19 -> DeviceTypeEntry(BitwardenString.extension, "Vivaldi") - 20 -> DeviceTypeEntry(BitwardenString.extension, "Safari") + 0 -> DeviceTypeEntry(BitwardenString.mobile_platform, "Android") + 1 -> DeviceTypeEntry(BitwardenString.mobile_platform, "iOS") + 2 -> DeviceTypeEntry(BitwardenString.extension_platform, "Chrome") + 3 -> DeviceTypeEntry(BitwardenString.extension_platform, "Firefox") + 4 -> DeviceTypeEntry(BitwardenString.extension_platform, "Opera") + 5 -> DeviceTypeEntry(BitwardenString.extension_platform, "Edge") + 6 -> DeviceTypeEntry(BitwardenString.desktop_platform, "Windows") + 7 -> DeviceTypeEntry(BitwardenString.desktop_platform, "MacOS") + 8 -> DeviceTypeEntry(BitwardenString.desktop_platform, "Linux") + 9 -> DeviceTypeEntry(BitwardenString.web_platform, "Chrome") + 10 -> DeviceTypeEntry(BitwardenString.web_platform, "Firefox") + 11 -> DeviceTypeEntry(BitwardenString.web_platform, "Opera") + 12 -> DeviceTypeEntry(BitwardenString.web_platform, "Edge") + 13 -> DeviceTypeEntry(BitwardenString.web_platform, "IE") + 14 -> DeviceTypeEntry(BitwardenString.web_platform, "Unknown") + 15 -> DeviceTypeEntry(BitwardenString.mobile_platform, "Amazon") + 16 -> DeviceTypeEntry(BitwardenString.desktop_platform, "Windows UWP") + 17 -> DeviceTypeEntry(BitwardenString.web_platform, "Safari") + 18 -> DeviceTypeEntry(BitwardenString.web_platform, "Vivaldi") + 19 -> DeviceTypeEntry(BitwardenString.extension_platform, "Vivaldi") + 20 -> DeviceTypeEntry(BitwardenString.extension_platform, "Safari") 21 -> DeviceTypeEntry(BitwardenString.sdk, "") 22 -> DeviceTypeEntry(BitwardenString.server, "") - 23 -> DeviceTypeEntry(BitwardenString.cli, "Windows") - 24 -> DeviceTypeEntry(BitwardenString.cli, "MacOs") - 25 -> DeviceTypeEntry(BitwardenString.cli, "Linux") - 26 -> DeviceTypeEntry(BitwardenString.extension, "DuckDuckGo") + 23 -> DeviceTypeEntry(BitwardenString.cli_platform, "Windows") + 24 -> DeviceTypeEntry(BitwardenString.cli_platform, "MacOS") + 25 -> DeviceTypeEntry(BitwardenString.cli_platform, "Linux") + 26 -> DeviceTypeEntry(BitwardenString.extension_platform, "DuckDuckGo") else -> return BitwardenString.unknown_device.asText() } return if (entry.platform.isNotEmpty()) { - entry.categoryResId.asText() - .concat( - " - ${entry.platform}".asText(), - ) + entry.categoryResId.asText(entry.platform) } else { entry.categoryResId.asText() } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index f55d80faf2..70e6f186e4 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -98,7 +98,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.AuthState import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult -import com.x8bit.bitwarden.data.auth.repository.model.GetDeviceResult import com.x8bit.bitwarden.data.auth.repository.model.GetDevicesResult import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult import com.x8bit.bitwarden.data.auth.repository.model.LeaveOrganizationResult @@ -6682,46 +6681,10 @@ class AuthRepositoryTest { coVerify(exactly = 1) { devicesService.getDevices() } assertTrue(result is GetDevicesResult.Success) assertEquals(1, (result as GetDevicesResult.Success).devices.size) - assertEquals("deviceId", result.devices.first().id) - } - - @Test - fun `getDeviceByIdentifier should return Error when service returns failure`() = runTest { - val error = Throwable("Fail!") - coEvery { - devicesService.getDeviceByIdentifier(UNIQUE_APP_ID) - } returns error.asFailure() - - val result = repository.getDeviceByIdentifier() - - coVerify(exactly = 1) { devicesService.getDeviceByIdentifier(UNIQUE_APP_ID) } - assertEquals(GetDeviceResult.Error, result) - } - - @Test - fun `getDeviceByIdentifier should return Success when service returns success`() = runTest { - val deviceJson = DeviceResponseJson( - id = "deviceId", - name = "Test Device", - identifier = UNIQUE_APP_ID, - type = 0, - creationDate = Instant.parse("2023-10-27T12:00:00Z"), - lastActivityDate = null, - isTrusted = false, - encryptedUserKey = null, - encryptedPublicKey = null, - devicePendingAuthRequest = null, - ) - coEvery { - devicesService.getDeviceByIdentifier(UNIQUE_APP_ID) - } returns deviceJson.asSuccess() - - val result = repository.getDeviceByIdentifier() - - coVerify(exactly = 1) { devicesService.getDeviceByIdentifier(UNIQUE_APP_ID) } - assertTrue(result is GetDeviceResult.Success) - assertEquals("deviceId", (result as GetDeviceResult.Success).device.id) - assertEquals(UNIQUE_APP_ID, result.device.identifier) + val device = result.devices.first() + assertEquals("deviceId", device.id) + // identifier "deviceIdentifier" != uniqueAppId "testUniqueAppId", so not current device + assertFalse(device.isCurrentDevice) } @Test diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt index c7dc6cb2d6..1a1ee95434 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt @@ -886,6 +886,26 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { ) } + @Test + fun `on ManageDevicesClick should emit NavigateToManageDevices`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(AccountSecurityAction.ManageDevicesClick) + assertEquals(AccountSecurityEvent.NavigateToManageDevices, awaitItem()) + } + } + + @Test + fun `when ManageDevices flag updates, should update isManageDevicesEnabled state`() = runTest { + val viewModel = createViewModel() + mutableManageDevicesFlagFlow.emit(true) + val expectedState = DEFAULT_STATE.copy(isManageDevicesEnabled = true) + assertEquals( + viewModel.stateFlow.value, + expectedState, + ) + } + @Suppress("LongParameterList") private fun createViewModel( initialState: AccountSecurityState? = DEFAULT_STATE, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/ManageDevicesScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/ManageDevicesScreenTest.kt index 8e737dee74..0fc5e0fad8 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/ManageDevicesScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/ManageDevicesScreenTest.kt @@ -236,7 +236,6 @@ private val DEFAULT_DEVICE_ITEM = ManageDevicesState.ViewState.Content.DeviceIte private val DEFAULT_STATE = ManageDevicesState( authRequests = emptyList(), devices = emptyList(), - currentDeviceId = null, viewState = ManageDevicesState.ViewState.Loading, isPullToRefreshSettingEnabled = false, isRefreshing = false, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/ManageDevicesViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/ManageDevicesViewModelTest.kt index 7d71b1e53b..a5d75e2c63 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/ManageDevicesViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/ManageDevicesViewModelTest.kt @@ -15,10 +15,8 @@ import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestsUpdatesResult import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.DeviceInfo import com.x8bit.bitwarden.data.auth.repository.model.DevicePendingAuthRequest -import com.x8bit.bitwarden.data.auth.repository.model.GetDeviceResult import com.x8bit.bitwarden.data.auth.repository.model.GetDevicesResult import com.x8bit.bitwarden.data.platform.repository.SettingsRepository -import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay import io.mockk.coEvery import io.mockk.coVerify @@ -52,10 +50,6 @@ class ManageDevicesViewModelTest : BaseViewModelTest() { private val authRepository = mockk { every { getAuthRequestsWithUpdates() } returns mutableAuthRequestsWithUpdatesFlow coEvery { getDevices() } returns GetDevicesResult.Success(emptyList()) - coEvery { getDeviceByIdentifier() } returns GetDeviceResult.Success(DEFAULT_DEVICE) - } - private val resourceManager = mockk { - every { getString(any()) } returns "Mock" } private val mutablePullToRefreshStateFlow = MutableStateFlow(false) private val settingsRepository = mockk { @@ -97,7 +91,6 @@ class ManageDevicesViewModelTest : BaseViewModelTest() { coVerify { authRepository.getAuthRequestsWithUpdates() authRepository.getDevices() - authRepository.getDeviceByIdentifier() } } @@ -126,31 +119,44 @@ class ManageDevicesViewModelTest : BaseViewModelTest() { } @Test - fun `LifecycleResume should re-fetch devices and auth requests`() = runTest { + fun `LifecycleResume should re-fetch auth requests only`() = runTest { val viewModel = createViewModel() viewModel.trySendAction(ManageDevicesAction.LifecycleResume) // getAuthRequestsWithUpdates called twice: once on init, once on resume verify(exactly = 2) { authRepository.getAuthRequestsWithUpdates() } - coVerify(exactly = 2) { authRepository.getDevices() } - coVerify(exactly = 2) { authRepository.getDeviceByIdentifier() } + coVerify(exactly = 1) { authRepository.getDevices() } } @Test - fun `RefreshPull should reset loaded state, set isRefreshing, and re-fetch data`() = runTest { + fun `RefreshPull when devices loaded should re-fetch auth requests only`() = runTest { val viewModel = createViewModel() viewModel.stateFlow.test { - // Skip initial states from init skipItems(1) viewModel.trySendAction(ManageDevicesAction.RefreshPull) - // After refresh, isRefreshing should be true transiently and devices reloaded - coVerify(exactly = 2) { authRepository.getDevices() } + coVerify(exactly = 1) { authRepository.getDevices() } verify(exactly = 2) { authRepository.getAuthRequestsWithUpdates() } cancelAndIgnoreRemainingEvents() } } + @Test + fun `RefreshPull when devices failed should re-fetch both devices and auth requests`() = + runTest { + coEvery { authRepository.getDevices() } returns GetDevicesResult.Error + val viewModel = createViewModel() + viewModel.stateFlow.test { + skipItems(1) + + viewModel.trySendAction(ManageDevicesAction.RefreshPull) + + coVerify(exactly = 2) { authRepository.getDevices() } + verify(exactly = 2) { authRepository.getAuthRequestsWithUpdates() } + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `PendingRequestRowClick should emit NavigateToLoginApproval`() = runTest { val fingerprint = "mock-fingerprint" @@ -165,7 +171,7 @@ class ManageDevicesViewModelTest : BaseViewModelTest() { } @Test - fun `AllDevicesResultReceive with device error should show error state`() { + fun `when getDevices returns error should show error state`() { coEvery { authRepository.getDevices() } returns GetDevicesResult.Error val viewModel = createViewModel() assertEquals( @@ -174,16 +180,6 @@ class ManageDevicesViewModelTest : BaseViewModelTest() { ) } - @Test - fun `AllDevicesResultReceive with current device error should show error state`() { - coEvery { authRepository.getDeviceByIdentifier() } returns GetDeviceResult.Error - val viewModel = createViewModel() - assertEquals( - ManageDevicesState.ViewState.Error, - viewModel.stateFlow.value.viewState, - ) - } - @Test fun `AuthRequestsResultReceive with error should use empty auth request list`() { val viewModel = createViewModel() @@ -229,7 +225,6 @@ class ManageDevicesViewModelTest : BaseViewModelTest() { @Test fun `content state should sort devices with current first, pending second, others last`() { - val currentDeviceId = DEFAULT_DEVICE.id val pendingRequest = DevicePendingAuthRequest( id = "auth-req-1", creationDate = fixedClock.instant(), @@ -247,7 +242,7 @@ class ManageDevicesViewModelTest : BaseViewModelTest() { originUrl = "www.bitwarden.com", fingerprint = "fingerprint-phrase", ) - val currentDevice = DEFAULT_DEVICE + val currentDevice = DEFAULT_DEVICE.copy(isCurrentDevice = true) val pendingDevice = DEFAULT_DEVICE.copy( id = "device-pending", pendingAuthRequest = pendingRequest, @@ -257,9 +252,6 @@ class ManageDevicesViewModelTest : BaseViewModelTest() { coEvery { authRepository.getDevices() } returns GetDevicesResult.Success( devices = listOf(otherDevice, pendingDevice, currentDevice), ) - coEvery { authRepository.getDeviceByIdentifier() } returns GetDeviceResult.Success( - currentDevice, - ) val viewModel = createViewModel() mutableAuthRequestsWithUpdatesFlow.tryEmit( @@ -270,14 +262,13 @@ class ManageDevicesViewModelTest : BaseViewModelTest() { assertEquals(DeviceSessionStatus.Current, content.items[0].status) assertEquals(DeviceSessionStatus.Pending, content.items[1].status) assertEquals(DeviceSessionStatus.None, content.items[2].status) - assertEquals(currentDeviceId, content.items[0].id) + assertEquals(currentDevice.id, content.items[0].id) assertEquals(pendingDevice.id, content.items[1].id) } private fun createViewModel(state: ManageDevicesState? = null) = ManageDevicesViewModel( clock = fixedClock, authRepository = authRepository, - resourceManager = resourceManager, snackbarRelayManager = snackbarRelayManager, settingsRepository = settingsRepository, buildInfoManager = buildInfoManager, @@ -294,4 +285,5 @@ private val DEFAULT_DEVICE = DeviceInfo( creationDate = Instant.parse("2023-10-27T12:00:00Z"), lastActivityDate = Instant.parse("2023-10-27T12:00:00Z"), pendingAuthRequest = null, + isCurrentDevice = false, ) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/util/DeviceLastActivityExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/util/DeviceLastActivityExtensionsTest.kt new file mode 100644 index 0000000000..97f04715aa --- /dev/null +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/util/DeviceLastActivityExtensionsTest.kt @@ -0,0 +1,107 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.managedevices.util + +import com.bitwarden.ui.platform.resource.BitwardenString +import com.bitwarden.ui.util.asText +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import java.time.Clock +import java.time.Instant +import java.time.ZoneOffset + +class DeviceLastActivityExtensionsTest { + + private val fixedClock = Clock.fixed( + Instant.parse("2024-03-15T12:00:00Z"), + ZoneOffset.UTC, + ) + + @Test + fun `null activity date returns null`() { + assertNull((null as Instant?).toLastActivityLabel(fixedClock)) + } + + @Test + fun `same day returns today`() { + val activityDate = Instant.parse("2024-03-15T00:00:00Z") + assertEquals(BitwardenString.today.asText(), activityDate.toLastActivityLabel(fixedClock)) + } + + @Test + fun `future date returns today`() { + val activityDate = Instant.parse("2024-03-16T00:00:00Z") + assertEquals(BitwardenString.today.asText(), activityDate.toLastActivityLabel(fixedClock)) + } + + @Test + fun `1 day ago returns past seven days`() { + val activityDate = Instant.parse("2024-03-14T00:00:00Z") + assertEquals( + BitwardenString.past_seven_days.asText(), + activityDate.toLastActivityLabel(fixedClock), + ) + } + + @Test + fun `6 days ago returns past seven days`() { + val activityDate = Instant.parse("2024-03-09T00:00:00Z") + assertEquals( + BitwardenString.past_seven_days.asText(), + activityDate.toLastActivityLabel(fixedClock), + ) + } + + @Test + fun `7 days ago returns past fourteen days`() { + val activityDate = Instant.parse("2024-03-08T00:00:00Z") + assertEquals( + BitwardenString.past_fourteen_days.asText(), + activityDate.toLastActivityLabel(fixedClock), + ) + } + + @Test + fun `13 days ago returns past fourteen days`() { + val activityDate = Instant.parse("2024-03-02T00:00:00Z") + assertEquals( + BitwardenString.past_fourteen_days.asText(), + activityDate.toLastActivityLabel(fixedClock), + ) + } + + @Test + fun `14 days ago returns past thirty days`() { + val activityDate = Instant.parse("2024-03-01T00:00:00Z") + assertEquals( + BitwardenString.past_thirty_days.asText(), + activityDate.toLastActivityLabel(fixedClock), + ) + } + + @Test + fun `29 days ago returns past thirty days`() { + val activityDate = Instant.parse("2024-02-15T00:00:00Z") + assertEquals( + BitwardenString.past_thirty_days.asText(), + activityDate.toLastActivityLabel(fixedClock), + ) + } + + @Test + fun `30 days ago returns over thirty days ago`() { + val activityDate = Instant.parse("2024-02-14T00:00:00Z") + assertEquals( + BitwardenString.over_thirty_days_ago.asText(), + activityDate.toLastActivityLabel(fixedClock), + ) + } + + @Test + fun `31 days ago returns over thirty days ago`() { + val activityDate = Instant.parse("2024-02-13T00:00:00Z") + assertEquals( + BitwardenString.over_thirty_days_ago.asText(), + activityDate.toLastActivityLabel(fixedClock), + ) + } +} diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/util/DeviceTypeExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/util/DeviceTypeExtensionsTest.kt index 25d68f8b25..6e732e3f1e 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/util/DeviceTypeExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/managedevices/util/DeviceTypeExtensionsTest.kt @@ -10,108 +10,217 @@ import org.junit.jupiter.api.Test class DeviceTypeExtensionsTest { private val resources = mockk { - every { getText(BitwardenString.mobile) } returns "Mobile" - every { getText(BitwardenString.extension) } returns "Extension" - every { getText(BitwardenString.desktop) } returns "Desktop" - every { getText(BitwardenString.web) } returns "Web" + // Categories with platform: getString(resId, platform) + every { getString(BitwardenString.mobile_platform, "Android") } returns "Mobile - Android" + every { getString(BitwardenString.mobile_platform, "iOS") } returns "Mobile - iOS" + every { getString(BitwardenString.mobile_platform, "Amazon") } returns "Mobile - Amazon" + every { + getString( + BitwardenString.extension_platform, + "Chrome", + ) + } returns "Extension - Chrome" + every { + getString( + BitwardenString.extension_platform, + "Firefox", + ) + } returns "Extension - Firefox" + every { getString(BitwardenString.extension_platform, "Opera") } returns "Extension - Opera" + every { getString(BitwardenString.extension_platform, "Edge") } returns "Extension - Edge" + every { + getString( + BitwardenString.extension_platform, + "Vivaldi", + ) + } returns "Extension - Vivaldi" + every { + getString( + BitwardenString.extension_platform, + "Safari", + ) + } returns "Extension - Safari" + every { + getString( + BitwardenString.extension_platform, + "DuckDuckGo", + ) + } returns "Extension - DuckDuckGo" + every { getString(BitwardenString.desktop_platform, "Windows") } returns "Desktop - Windows" + every { getString(BitwardenString.desktop_platform, "MacOS") } returns "Desktop - MacOS" + every { getString(BitwardenString.desktop_platform, "Linux") } returns "Desktop - Linux" + every { + getString( + BitwardenString.desktop_platform, + "Windows UWP", + ) + } returns "Desktop - Windows UWP" + every { getString(BitwardenString.web_platform, "Chrome") } returns "Web - Chrome" + every { getString(BitwardenString.web_platform, "Firefox") } returns "Web - Firefox" + every { getString(BitwardenString.web_platform, "Opera") } returns "Web - Opera" + every { getString(BitwardenString.web_platform, "Edge") } returns "Web - Edge" + every { getString(BitwardenString.web_platform, "IE") } returns "Web - IE" + every { getString(BitwardenString.web_platform, "Unknown") } returns "Web - Unknown" + every { getString(BitwardenString.web_platform, "Safari") } returns "Web - Safari" + every { getString(BitwardenString.web_platform, "Vivaldi") } returns "Web - Vivaldi" + every { getString(BitwardenString.cli_platform, "Windows") } returns "CLI - Windows" + every { getString(BitwardenString.cli_platform, "MacOS") } returns "CLI - MacOS" + every { getString(BitwardenString.cli_platform, "Linux") } returns "CLI - Linux" + // Categories without platform: getText(resId) every { getText(BitwardenString.sdk) } returns "SDK" every { getText(BitwardenString.server) } returns "Server" - every { getText(BitwardenString.cli) } returns "CLI" every { getText(BitwardenString.unknown_device) } returns "Unknown device" } @Test fun `type 0 should return Mobile - Android`() { - assertEquals("Mobile - Android", 0.readableDeviceTypeName.toString(resources)) + assertEquals( + "Mobile - Android", + 0.readableDeviceTypeName.toString(resources), + ) } @Test fun `type 1 should return Mobile - iOS`() { - assertEquals("Mobile - iOS", 1.readableDeviceTypeName.toString(resources)) + assertEquals( + "Mobile - iOS", + 1.readableDeviceTypeName.toString(resources), + ) } @Test fun `type 2 should return Extension - Chrome`() { - assertEquals("Extension - Chrome", 2.readableDeviceTypeName.toString(resources)) + assertEquals( + "Extension - Chrome", + 2.readableDeviceTypeName.toString(resources), + ) } @Test fun `type 3 should return Extension - Firefox`() { - assertEquals("Extension - Firefox", 3.readableDeviceTypeName.toString(resources)) + assertEquals( + "Extension - Firefox", + 3.readableDeviceTypeName.toString(resources), + ) } @Test fun `type 5 should return Extension - Edge`() { - assertEquals("Extension - Edge", 5.readableDeviceTypeName.toString(resources)) + assertEquals( + "Extension - Edge", + 5.readableDeviceTypeName.toString(resources), + ) } @Test fun `type 6 should return Desktop - Windows`() { - assertEquals("Desktop - Windows", 6.readableDeviceTypeName.toString(resources)) + assertEquals( + "Desktop - Windows", + 6.readableDeviceTypeName.toString(resources), + ) } @Test fun `type 7 should return Desktop - MacOS`() { - assertEquals("Desktop - MacOS", 7.readableDeviceTypeName.toString(resources)) + assertEquals( + "Desktop - MacOS", + 7.readableDeviceTypeName.toString(resources), + ) } @Test fun `type 9 should return Web - Chrome`() { - assertEquals("Web - Chrome", 9.readableDeviceTypeName.toString(resources)) + assertEquals( + "Web - Chrome", + 9.readableDeviceTypeName.toString(resources), + ) } @Test fun `type 15 should return Mobile - Amazon`() { - assertEquals("Mobile - Amazon", 15.readableDeviceTypeName.toString(resources)) + assertEquals( + "Mobile - Amazon", + 15.readableDeviceTypeName.toString(resources), + ) } @Test fun `type 16 should return Desktop - Windows UWP`() { - assertEquals("Desktop - Windows UWP", 16.readableDeviceTypeName.toString(resources)) + assertEquals( + "Desktop - Windows UWP", + 16.readableDeviceTypeName.toString(resources), + ) } @Test fun `type 20 should return Extension - Safari`() { - assertEquals("Extension - Safari", 20.readableDeviceTypeName.toString(resources)) + assertEquals( + "Extension - Safari", + 20.readableDeviceTypeName.toString(resources), + ) } @Test fun `type 21 should return SDK category only with no platform suffix`() { - assertEquals("SDK", 21.readableDeviceTypeName.toString(resources)) + assertEquals( + "SDK", + 21.readableDeviceTypeName.toString(resources), + ) } @Test fun `type 22 should return Server category only with no platform suffix`() { - assertEquals("Server", 22.readableDeviceTypeName.toString(resources)) + assertEquals( + "Server", + 22.readableDeviceTypeName.toString(resources), + ) } @Test fun `type 23 should return CLI - Windows`() { - assertEquals("CLI - Windows", 23.readableDeviceTypeName.toString(resources)) + assertEquals( + "CLI - Windows", + 23.readableDeviceTypeName.toString(resources), + ) } @Test - fun `type 24 should return CLI - MacOs`() { - assertEquals("CLI - MacOs", 24.readableDeviceTypeName.toString(resources)) + fun `type 24 should return CLI - MacOS`() { + assertEquals( + "CLI - MacOS", + 24.readableDeviceTypeName.toString(resources), + ) } @Test fun `type 25 should return CLI - Linux`() { - assertEquals("CLI - Linux", 25.readableDeviceTypeName.toString(resources)) + assertEquals( + "CLI - Linux", + 25.readableDeviceTypeName.toString(resources), + ) } @Test fun `type 26 should return Extension - DuckDuckGo`() { - assertEquals("Extension - DuckDuckGo", 26.readableDeviceTypeName.toString(resources)) + assertEquals( + "Extension - DuckDuckGo", + 26.readableDeviceTypeName.toString(resources), + ) } @Test fun `unknown type should return unknown device string`() { - assertEquals("Unknown device", 999.readableDeviceTypeName.toString(resources)) + assertEquals( + "Unknown device", + 999.readableDeviceTypeName.toString(resources), + ) } @Test fun `negative type should return unknown device string`() { - assertEquals("Unknown device", (-1).readableDeviceTypeName.toString(resources)) + assertEquals( + "Unknown device", + (-1).readableDeviceTypeName.toString(resources), + ) } } diff --git a/network/src/main/kotlin/com/bitwarden/network/api/AuthenticatedDevicesApi.kt b/network/src/main/kotlin/com/bitwarden/network/api/AuthenticatedDevicesApi.kt index d5c66fe403..b43de63fb5 100644 --- a/network/src/main/kotlin/com/bitwarden/network/api/AuthenticatedDevicesApi.kt +++ b/network/src/main/kotlin/com/bitwarden/network/api/AuthenticatedDevicesApi.kt @@ -1,7 +1,6 @@ package com.bitwarden.network.api import androidx.annotation.Keep -import com.bitwarden.network.model.DeviceResponseJson import com.bitwarden.network.model.DevicesResponseJson import com.bitwarden.network.model.NetworkResult import com.bitwarden.network.model.TrustedDeviceKeysRequestJson @@ -19,11 +18,6 @@ internal interface AuthenticatedDevicesApi { @GET("/devices") suspend fun getDevices(): NetworkResult - @GET("/devices/identifier/{deviceIdentifier}") - suspend fun getDeviceByIdentifier( - @Path(value = "deviceIdentifier") deviceIdentifier: String, - ): NetworkResult - @PUT("/devices/{appId}/keys") suspend fun updateTrustedDeviceKeys( @Path(value = "appId") appId: String, diff --git a/network/src/main/kotlin/com/bitwarden/network/model/DeviceResponseJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/DeviceResponseJson.kt index a4a153de8d..9a2cafc813 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/DeviceResponseJson.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/DeviceResponseJson.kt @@ -10,7 +10,7 @@ import java.time.Instant * * @property id The unique identifier of the device. * @property name The name of the device. - * @property identifier The unique identifier of the device. + * @property identifier The unique install identifier of the device. * @property type The type of the device. * @property creationDate The date and time on which this device was created. * @property isTrusted Whether this device is trusted. diff --git a/network/src/main/kotlin/com/bitwarden/network/service/DevicesService.kt b/network/src/main/kotlin/com/bitwarden/network/service/DevicesService.kt index 24d6ea22e7..21bbe53c81 100644 --- a/network/src/main/kotlin/com/bitwarden/network/service/DevicesService.kt +++ b/network/src/main/kotlin/com/bitwarden/network/service/DevicesService.kt @@ -1,6 +1,5 @@ package com.bitwarden.network.service -import com.bitwarden.network.model.DeviceResponseJson import com.bitwarden.network.model.DevicesResponseJson import com.bitwarden.network.model.TrustedDeviceKeysResponseJson @@ -13,11 +12,6 @@ interface DevicesService { */ suspend fun getDevices(): Result - /** - * Get a device by its client-generated identifier. - */ - suspend fun getDeviceByIdentifier(deviceIdentifier: String): Result - /** * Check whether this device is known (and thus whether Login with Device is available). */ diff --git a/network/src/main/kotlin/com/bitwarden/network/service/DevicesServiceImpl.kt b/network/src/main/kotlin/com/bitwarden/network/service/DevicesServiceImpl.kt index 7474f386c2..1a5793274b 100644 --- a/network/src/main/kotlin/com/bitwarden/network/service/DevicesServiceImpl.kt +++ b/network/src/main/kotlin/com/bitwarden/network/service/DevicesServiceImpl.kt @@ -2,7 +2,6 @@ package com.bitwarden.network.service import com.bitwarden.network.api.AuthenticatedDevicesApi import com.bitwarden.network.api.UnauthenticatedDevicesApi -import com.bitwarden.network.model.DeviceResponseJson import com.bitwarden.network.model.DevicesResponseJson import com.bitwarden.network.model.TrustedDeviceKeysRequestJson import com.bitwarden.network.model.TrustedDeviceKeysResponseJson @@ -19,11 +18,6 @@ internal class DevicesServiceImpl( override suspend fun getDevices(): Result = authenticatedDevicesApi.getDevices().toResult() - override suspend fun getDeviceByIdentifier( - deviceIdentifier: String, - ): Result = - authenticatedDevicesApi.getDeviceByIdentifier(deviceIdentifier).toResult() - override suspend fun getIsKnownDevice( emailAddress: String, deviceId: String, diff --git a/network/src/test/kotlin/com/bitwarden/network/service/DevicesServiceTest.kt b/network/src/test/kotlin/com/bitwarden/network/service/DevicesServiceTest.kt index 4115f0d769..81a0f5e360 100644 --- a/network/src/test/kotlin/com/bitwarden/network/service/DevicesServiceTest.kt +++ b/network/src/test/kotlin/com/bitwarden/network/service/DevicesServiceTest.kt @@ -4,6 +4,8 @@ import com.bitwarden.core.data.util.asSuccess import com.bitwarden.network.api.AuthenticatedDevicesApi import com.bitwarden.network.api.UnauthenticatedDevicesApi import com.bitwarden.network.base.BaseServiceTest +import com.bitwarden.network.model.DeviceResponseJson +import com.bitwarden.network.model.DevicesResponseJson import com.bitwarden.network.model.TrustedDeviceKeysResponseJson import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -22,6 +24,22 @@ class DevicesServiceTest : BaseServiceTest() { unauthenticatedDevicesApi = unauthenticatedDevicesApi, ) + @Test + fun `getDevices when request response is Failure should return Failure`() = runTest { + val response = MockResponse().setResponseCode(400) + server.enqueue(response) + val actual = service.getDevices() + assertTrue(actual.isFailure) + } + + @Test + fun `getDevices when request response is Success should return Success`() = runTest { + val response = MockResponse().setBody(GET_DEVICES_RESPONSE_JSON).setResponseCode(200) + server.enqueue(response) + val actual = service.getDevices() + assertEquals(GET_DEVICES_RESPONSE.asSuccess(), actual) + } + @Test fun `getIsKnownDevice when request response is Failure should return Failure`() = runTest { val response = MockResponse().setResponseCode(400) @@ -65,6 +83,42 @@ class DevicesServiceTest : BaseServiceTest() { } } +private val GET_DEVICES_RESPONSE: DevicesResponseJson = DevicesResponseJson( + devices = listOf( + DeviceResponseJson( + id = "0d31b6fb-d282-43c7-b614-b13e0129dbd7", + name = "Pixel 8", + identifier = "ea7c0a13-5ce4-4f96-8e17-4fc7fa54f464", + type = 0, + creationDate = Instant.parse("2024-03-25T18:04:28.23Z"), + lastActivityDate = Instant.parse("2024-03-26T10:00:00.00Z"), + isTrusted = true, + encryptedUserKey = null, + encryptedPublicKey = null, + devicePendingAuthRequest = null, + ), + ), +) + +private const val GET_DEVICES_RESPONSE_JSON: String = """ +{ + "data": [ + { + "id": "0d31b6fb-d282-43c7-b614-b13e0129dbd7", + "name": "Pixel 8", + "identifier": "ea7c0a13-5ce4-4f96-8e17-4fc7fa54f464", + "type": 0, + "creationDate": "2024-03-25T18:04:28.23Z", + "lastActivityDate": "2024-03-26T10:00:00.00Z", + "isTrusted": true, + "encryptedUserKey": null, + "encryptedPublicKey": null, + "devicePendingAuthRequest": null + } + ] +} +""" + private val TRUST_DEVICE_RESPONSE: TrustedDeviceKeysResponseJson = TrustedDeviceKeysResponseJson( id = "0d31b6fb-d282-43c7-b614-b13e0129dbd7", diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt index ae5cc25683..c1dbe5ffd2 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt @@ -93,7 +93,7 @@ private fun FlagKey.getDisplayLabel(): String = when (this) { FlagKey.CardScanner -> stringResource(BitwardenString.scan_card) FlagKey.SendEmailVerification -> stringResource(BitwardenString.send_email_verification) FlagKey.MobilePremiumUpgrade -> stringResource(BitwardenString.mobile_premium_upgrade) - FlagKey.ManageDevices -> stringResource(BitwardenString.manage_devices) + FlagKey.ManageDevices -> stringResource(BitwardenString.manage_devices_flag) FlagKey.AttachmentUpdates -> stringResource(BitwardenString.attachment_updates) FlagKey.V2EncryptionJitPassword -> stringResource(BitwardenString.v2_encryption_jit_password) FlagKey.V2EncryptionKeyConnector -> stringResource(BitwardenString.v2_encryption_key_connector) diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index f6ae0baf79..92389acb1f 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -1233,11 +1233,11 @@ Do you want to switch to this account? External link %1$s, External link Manage devices - Mobile - Extension - Web - Desktop - CLI + Mobile - %1$s + Extension - %1$s + Web - %1$s + Desktop - %1$s + CLI - %1$s SDK Server Unknown device diff --git a/ui/src/main/res/values/strings_non_localized.xml b/ui/src/main/res/values/strings_non_localized.xml index 1daa5e7891..3be6418093 100644 --- a/ui/src/main/res/values/strings_non_localized.xml +++ b/ui/src/main/res/values/strings_non_localized.xml @@ -53,6 +53,7 @@ V2 Encryption - Key Connector V2 Encryption - JIT Password V2 Encryption - Password + Manage devices