diff --git a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/NomadLogoutReceiver.kt b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/NomadLogoutReceiver.kt index 106e6f9a83..766156851d 100644 --- a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/NomadLogoutReceiver.kt +++ b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/NomadLogoutReceiver.kt @@ -57,21 +57,34 @@ class NomadLogoutReceiver : CoroutineReceiver() { lateinit var nomadProfilesFeatureConfig: NomadProfilesFeatureConfig public override suspend fun receive(context: Context, intent: Intent) { - if (!nomadProfilesFeatureConfig.isEnabled()) return - if (intent.action != ACTION_LOGOUT) return + when { + intent.action != ACTION_LOGOUT -> { + appLogger.i("$TAG not a logout intent is passed ignore") + } + + !nomadProfilesFeatureConfig.isEnabled() -> { + appLogger.i("$TAG nomadProfilesFeatureConfig is not enabled ignoring") + } + + !coreLogic.getGlobalScope().isCurrentSessionNomadAccount() -> { + appLogger.i("$TAG Logout ignored: current session is not a nomad account") + } - appLogger.i("$TAG Received logout broadcast") + else -> { + appLogger.i("$TAG Received logout broadcast") - @Suppress("TooGenericExceptionCaught") - try { - performLogout() - CoroutineScope(Dispatchers.Default).launch { - AppBackgroundManager.moveAppToBackground() + @Suppress("TooGenericExceptionCaught") + try { + performLogout() + CoroutineScope(Dispatchers.Default).launch { + AppBackgroundManager.moveAppToBackground() + } + } catch (e: CancellationException) { + throw e + } catch (t: Exception) { + appLogger.e("$TAG Logout failed", t) + } } - } catch (e: CancellationException) { - throw e - } catch (t: Exception) { - appLogger.e("$TAG Logout failed", t) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt index 2c3734f67a..5cabac0f22 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -404,6 +404,11 @@ class WireActivityViewModel @Inject constructor( return@launch } + if (serverLinks?.isProductionApi() == true) { + appLogger.w("Nomad login ignored: resolved backend is Wire production") + return@launch + } + if (!isNomadProfilesFlowEnabled(serverLinks)) { return@launch } @@ -803,3 +808,7 @@ internal data class OnCustomBackendLogin( internal data class OnOpenUserProfile(val result: DeepLinkResult.OpenOtherUserProfile) : WireActivityViewAction internal data class OnSSOLogin(val result: DeepLinkResult.SSOLogin) : WireActivityViewAction internal data class ShowToast(val messageResId: Int) : WireActivityViewAction + +// TODO: replace with the kalium `ServerConfig.isProductionApi()` once it is made public and supports `Links` +internal fun ServerConfig.Links.isProductionApi(): Boolean = + ServerConfig.PRODUCTION.api.contains(java.net.URI(api).host ?: "") diff --git a/app/src/test/kotlin/com/wire/android/notification/broadcastreceivers/NomadLogoutReceiverTest.kt b/app/src/test/kotlin/com/wire/android/notification/broadcastreceivers/NomadLogoutReceiverTest.kt index 8ab0c7fc4f..b82d376827 100644 --- a/app/src/test/kotlin/com/wire/android/notification/broadcastreceivers/NomadLogoutReceiverTest.kt +++ b/app/src/test/kotlin/com/wire/android/notification/broadcastreceivers/NomadLogoutReceiverTest.kt @@ -34,6 +34,7 @@ import com.wire.kalium.logic.feature.auth.LogoutUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.CurrentSessionUseCase import com.wire.kalium.logic.feature.session.DeleteSessionUseCase +import com.wire.kalium.logic.feature.session.IsCurrentSessionNomadAccountUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify @@ -114,6 +115,26 @@ class NomadLogoutReceiverTest { verify(exactly = 0) { arrangement.context.startActivity(any()) } } + @Test + fun `when current session is not a nomad account then logout broadcast is ignored`() = runTest { + val userId = UserId("user", "domain") + val arrangement = Arrangement() + .withCurrentSession(CurrentSessionResult.Success(AccountInfo.Valid(userId))) + .withCurrentSessionNomadAccount(false) + .arrange() + + val intent = mockk { every { action } returns NomadLogoutReceiver.ACTION_LOGOUT } + arrangement.receiver.receive(arrangement.context, intent) + advanceUntilIdle() + + coVerify(exactly = 0) { + arrangement.currentSession() + arrangement.logoutUseCase(any(), any()) + arrangement.deleteSession(any()) + arrangement.accountSwitch(any()) + } + } + private class Arrangement { @MockK @@ -140,6 +161,9 @@ class NomadLogoutReceiverTest { @MockK lateinit var nomadProfilesFeatureConfig: NomadProfilesFeatureConfig + @MockK + lateinit var isCurrentSessionNomadAccount: IsCurrentSessionNomadAccountUseCase + val context = mockk(relaxed = true) val receiver = NomadLogoutReceiver() @@ -154,6 +178,8 @@ class NomadLogoutReceiverTest { coEvery { logoutUseCase(any(), any()) } returns Unit coEvery { deleteSession(any()) } returns DeleteSessionUseCase.Result.Success every { coreLogic.getGlobalScope().deleteSession } returns deleteSession + every { coreLogic.getGlobalScope().isCurrentSessionNomadAccount } returns isCurrentSessionNomadAccount + coEvery { isCurrentSessionNomadAccount() } returns true coEvery { accountSwitch(any()) } returns SwitchAccountResult.NoOtherAccountToSwitch every { coreLogic.getSessionScope(any()) } returns userSessionScope every { nomadProfilesFeatureConfig.isEnabled() } returns true @@ -175,5 +201,9 @@ class NomadLogoutReceiverTest { fun withNomadProfilesEnabled(enabled: Boolean) = apply { every { nomadProfilesFeatureConfig.isEnabled() } returns enabled } + + fun withCurrentSessionNomadAccount(isNomad: Boolean) = apply { + coEvery { isCurrentSessionNomadAccount() } returns isNomad + } } } diff --git a/app/src/test/kotlin/com/wire/android/ui/IsProductionApiTest.kt b/app/src/test/kotlin/com/wire/android/ui/IsProductionApiTest.kt new file mode 100644 index 0000000000..7f3ac892ab --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/IsProductionApiTest.kt @@ -0,0 +1,68 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui + +import com.wire.kalium.logic.configuration.server.ServerConfig +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class IsProductionApiTest { + + @Test + fun `given production API URL, then returns true`() { + val links = serverLinks(api = ServerConfig.PRODUCTION.api) + assertTrue(links.isProductionApi()) + } + + @Test + fun `given production API URL with trailing path, then returns true`() { + val links = serverLinks(api = "https://prod-nginz-https.wire.com/some/path") + assertTrue(links.isProductionApi()) + } + + @Test + fun `given custom on-premises API URL, then returns false`() { + val links = serverLinks(api = "https://custom-backend.example.com") + assertFalse(links.isProductionApi()) + } + + @Test + fun `given staging API URL, then returns false`() { + val links = serverLinks(api = ServerConfig.STAGING.api) + assertFalse(links.isProductionApi()) + } + + @Test + fun `given URL with production host as substring, then returns false`() { + val links = serverLinks(api = "https://not-prod-nginz-https.wire.com.evil.com") + assertFalse(links.isProductionApi()) + } + + private fun serverLinks(api: String) = ServerConfig.Links( + api = api, + accounts = "https://accounts.example.com", + webSocket = "https://ws.example.com", + blackList = "https://blacklist.example.com", + teams = "https://teams.example.com", + website = "https://example.com", + title = "test", + isOnPremises = false, + apiProxy = null + ) +} diff --git a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt index 880f8ab6ae..a05cd8e9f7 100644 --- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt @@ -82,6 +82,7 @@ import com.wire.kalium.logic.feature.client.IsProfileQRCodeEnabledUseCase import com.wire.kalium.logic.feature.client.NewClientResult import com.wire.kalium.logic.feature.client.ObserveNewClientsUseCase import com.wire.kalium.logic.feature.conversation.CheckConversationInviteCodeUseCase +import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.feature.server.GetServerConfigResult import com.wire.kalium.logic.feature.server.GetServerConfigUseCase import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase @@ -899,6 +900,27 @@ class WireActivityViewModelTest { } } + @Test + fun `given resolved backend is Wire production, when handling nomad intent, then login is ignored`() = runTest { + val (arrangement, viewModel) = Arrangement() + .withAutomatedLoginIntent( + ssoCode = "wire-b6261497-5b7d-4a57-8f4d-3a94e936b2c0", + backendConfig = "url" + ) + .withProductionServerConfig() + .arrange() + + viewModel.actions.test { + val handled = viewModel.handleIntentsThatAreNotDeepLinks(mockedIntent()) + advanceUntilIdle() + + assertTrue(handled) + assertFalse(arrangement.automatedLoginManager.pendingMoveToBackgroundAfterSync) + coVerify(exactly = 0) { arrangement.isNomadProfilesEnabledUseCase.invoke() } + expectNoEvents() + } + } + @Test fun `given nomad profiles disabled, when handling sharing intent, then import media screen is still shown`() = runTest { val (_, viewModel) = Arrangement() @@ -1298,6 +1320,10 @@ class WireActivityViewModelTest { coEvery { loginTypeSelector.canUseNewLogin(any()) } returns canUseNewLogin } + fun withProductionServerConfig(): Arrangement = apply { + coEvery { getServerConfigUseCase(any()) } returns GetServerConfigResult.Success(ServerConfig.PRODUCTION) + } + fun arrange() = this to viewModel }