From e77688b3f4ce8287b775ba28ecbcab93be2a6f08 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 27 Jul 2022 15:31:23 +0100 Subject: [PATCH 1/9] adding splash action tests --- .../onboarding/OnboardingViewModelTest.kt | 39 +++++++++++++++++++ .../app/test/fakes/FakeVectorFeatures.kt | 4 ++ 2 files changed, 43 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index a9bbb3eb07..23c25354fa 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -67,9 +67,11 @@ private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canChangeDisplay private val ANY_CONTINUING_REGISTRATION_RESULT = RegistrationActionHandler.Result.NextStage(Stage.Dummy(mandatory = true)) private val A_DIRECT_LOGIN = OnboardingAction.AuthenticateAction.LoginDirect("@a-user:id.org", "a-password", "a-device-name") private const val A_HOMESERVER_URL = "https://edited-homeserver.org" +private val A_DEFAULT_HOMESERVER_URL = "${R.string.matrix_org_server_url.toTestString()}/" private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance) private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password, userFacingUrl = A_HOMESERVER_URL) private val SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES = SelectedHomeserverState(isLogoutDevicesSupported = true) +private val DEFAULT_SELECTED_HOMESERVER_STATE = SELECTED_HOMESERVER_STATE.copy(userFacingUrl = A_DEFAULT_HOMESERVER_URL) private const val AN_EMAIL = "hello@example.com" private const val A_PASSWORD = "a-password" private const val A_USERNAME = "hello-world" @@ -102,6 +104,43 @@ class OnboardingViewModelTest { viewModelWith(initialState) } + @Test + fun `given usecase screen enabled, when handling sign up splash action, then emits OpenUseCaseSelection`() = runTest { + val test = viewModel.test() + fakeVectorFeatures.givenOnboardingUseCaseEnabled() + + viewModel.handle(OnboardingAction.SplashAction.OnGetStarted(OnboardingFlow.SignUp)) + + test + .assertStatesChanges( + initialState, + { copy(onboardingFlow = OnboardingFlow.SignUp) } + ) + .assertEvents(OnboardingViewEvents.OpenUseCaseSelection) + .finish() + } + + @Test + fun `given combined login enabled, when handling sign in splash action, then emits OpenCombinedLogin with default homeserver`() = runTest { + val test = viewModel.test() + fakeVectorFeatures.givenCombinedLoginEnabled() + givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE) + + viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(OnboardingFlow.SignIn)) + + test + .assertStatesChanges( + initialState, + { copy(onboardingFlow = OnboardingFlow.SignIn) }, + { copy(isLoading = true) }, + { copy(selectedHomeserver = DEFAULT_SELECTED_HOMESERVER_STATE) }, + { copy(signMode = SignMode.SignIn) }, + { copy(isLoading = false) } + ) + .assertEvents(OnboardingViewEvents.OpenCombinedLogin) + .finish() + } + @Test fun `given registration started with currentThreePid, when handling InitWith, then emits restored session OnSendEmailSuccess`() = runTest { val test = viewModel.test() diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt index 18d7355aeb..4e6b4fc3df 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt @@ -35,6 +35,10 @@ class FakeVectorFeatures : VectorFeatures by spyk() { every { isOnboardingCombinedLoginEnabled() } returns true } + fun givenOnboardingUseCaseEnabled() { + every { isOnboardingUseCaseEnabled() } returns true + } + fun givenCombinedLoginDisabled() { every { isOnboardingCombinedLoginEnabled() } returns false } From 87a0957d9a62cfaa276b8579e9ffbe9f15bdd366 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 27 Jul 2022 15:57:42 +0100 Subject: [PATCH 2/9] adding test around login with token --- .../onboarding/OnboardingViewModelTest.kt | 20 +++++++++++++++++++ .../vector/app/test/fakes/FakeLoginWizard.kt | 6 ++++++ 2 files changed, 26 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index 23c25354fa..a932f187f0 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -76,6 +76,7 @@ private const val AN_EMAIL = "hello@example.com" private const val A_PASSWORD = "a-password" private const val A_USERNAME = "hello-world" private const val A_MATRIX_ID = "@$A_USERNAME:matrix.org" +private const val A_LOGIN_TOKEN = "a-login-token" class OnboardingViewModelTest { @@ -141,6 +142,25 @@ class OnboardingViewModelTest { .finish() } + @Test + fun `given can successfully login in with token, when logging in with token, then emits AccountSignedIn`() = runTest { + val test = viewModel.test() + fakeAuthenticationService.givenLoginWizard(fakeLoginWizard) + fakeLoginWizard.givenLoginWithTokenResult(A_LOGIN_TOKEN, fakeSession) + givenInitialisesSession(fakeSession) + + viewModel.handle(OnboardingAction.LoginWithToken(A_LOGIN_TOKEN)) + + test + .assertStatesChanges( + initialState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvents(OnboardingViewEvents.OnAccountSignedIn) + .finish() + } + @Test fun `given registration started with currentThreePid, when handling InitWith, then emits restored session OnSendEmailSuccess`() = runTest { val test = viewModel.test() diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeLoginWizard.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeLoginWizard.kt index 07577ed61c..e255fa7deb 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeLoginWizard.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeLoginWizard.kt @@ -16,10 +16,12 @@ package im.vector.app.test.fakes +import io.mockk.coEvery import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.mockk import org.matrix.android.sdk.api.auth.login.LoginWizard +import org.matrix.android.sdk.api.session.Session class FakeLoginWizard : LoginWizard by mockk() { @@ -27,6 +29,10 @@ class FakeLoginWizard : LoginWizard by mockk() { coJustRun { resetPassword(email) } } + fun givenLoginWithTokenResult(token: String, result: Session) { + coEvery { loginWithToken(token) } returns result + } + fun givenConfirmResetPasswordSuccess(password: String) { coJustRun { resetPasswordMailConfirmed(password) } } From a32a78751a30e28e61e5c4f133c9e95fef480bd6 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 27 Jul 2022 16:25:22 +0100 Subject: [PATCH 3/9] adding test around login with username and password --- .../onboarding/OnboardingViewModelTest.kt | 20 +++++++++++++++++++ .../vector/app/test/fakes/FakeLoginWizard.kt | 4 ++++ 2 files changed, 24 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index a932f187f0..d6cfcac96f 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -75,6 +75,7 @@ private val DEFAULT_SELECTED_HOMESERVER_STATE = SELECTED_HOMESERVER_STATE.copy(u private const val AN_EMAIL = "hello@example.com" private const val A_PASSWORD = "a-password" private const val A_USERNAME = "hello-world" +private const val A_DEVICE_NAME = "a-device-name" private const val A_MATRIX_ID = "@$A_USERNAME:matrix.org" private const val A_LOGIN_TOKEN = "a-login-token" @@ -161,6 +162,25 @@ class OnboardingViewModelTest { .finish() } + @Test + fun `given can login with username and password, when logging in, then emits AccountSignedIn`() = runTest { + val test = viewModel.test() + fakeAuthenticationService.givenLoginWizard(fakeLoginWizard) + fakeLoginWizard.givenLoginSuccess(A_USERNAME, A_PASSWORD, A_DEVICE_NAME, fakeSession) + givenInitialisesSession(fakeSession) + + viewModel.handle(OnboardingAction.AuthenticateAction.Login(A_USERNAME, A_PASSWORD, A_DEVICE_NAME)) + + test + .assertStatesChanges( + initialState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvents(OnboardingViewEvents.OnAccountSignedIn) + .finish() + } + @Test fun `given registration started with currentThreePid, when handling InitWith, then emits restored session OnSendEmailSuccess`() = runTest { val test = viewModel.test() diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeLoginWizard.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeLoginWizard.kt index e255fa7deb..ba702a324c 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeLoginWizard.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeLoginWizard.kt @@ -33,6 +33,10 @@ class FakeLoginWizard : LoginWizard by mockk() { coEvery { loginWithToken(token) } returns result } + fun givenLoginSuccess(username: String, password: String, deviceName: String, result: Session) { + coEvery { login(username, password, deviceName) } returns result + } + fun givenConfirmResetPasswordSuccess(password: String) { coJustRun { resetPasswordMailConfirmed(password) } } From c4831510ac3975ffd419897a54306c9182604e65 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 27 Jul 2022 17:17:36 +0100 Subject: [PATCH 4/9] adding tests around the state resetting --- .../onboarding/OnboardingViewModelTest.kt | 104 ++++++++++++++++++ .../onboarding/RegistrationStateFixture.kt | 26 +++++ .../test/fakes/FakeAuthenticationService.kt | 9 ++ 3 files changed, 139 insertions(+) create mode 100644 vector/src/test/java/im/vector/app/features/onboarding/RegistrationStateFixture.kt diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index d6cfcac96f..1bc1b5bbf4 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -22,7 +22,9 @@ import im.vector.app.R import im.vector.app.features.login.LoginConfig import im.vector.app.features.login.LoginMode import im.vector.app.features.login.ReAuthHelper +import im.vector.app.features.login.ServerType import im.vector.app.features.login.SignMode +import im.vector.app.features.onboarding.RegistrationStateFixture.aRegistrationState import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeAnalyticsTracker @@ -78,6 +80,7 @@ private const val A_USERNAME = "hello-world" private const val A_DEVICE_NAME = "a-device-name" private const val A_MATRIX_ID = "@$A_USERNAME:matrix.org" private const val A_LOGIN_TOKEN = "a-login-token" +private val A_REGISTRATION_STATE = aRegistrationState(email = AN_EMAIL) class OnboardingViewModelTest { @@ -782,6 +785,107 @@ class OnboardingViewModelTest { .finish() } + @Test + fun `given homeserver state, when resetting homeserver url, then resets auth service and state`() = runTest { + viewModelWith(initialState.copy(isLoading = true, selectedHomeserver = SELECTED_HOMESERVER_STATE)) + val test = viewModel.test() + fakeAuthenticationService.expectReset() + + viewModel.handle(OnboardingAction.ResetHomeServerUrl) + + test + .assertStatesChanges( + initialState, + { copy(isLoading = false, selectedHomeserver = SelectedHomeserverState()) }, + ) + .assertNoEvents() + .finish() + fakeAuthenticationService.verifyReset() + } + + @Test + fun `given server type, when resetting homeserver type, then resets state`() = runTest { + viewModelWith(initialState.copy(serverType = ServerType.EMS)) + val test = viewModel.test() + + viewModel.handle(OnboardingAction.ResetHomeServerType) + + test + .assertStatesChanges( + initialState, + { copy(serverType = ServerType.Unknown) }, + ) + .assertNoEvents() + .finish() + } + + @Test + fun `given sign mode, when resetting sign mode, then resets state`() = runTest { + viewModelWith(initialState.copy(isLoading = true, signMode = SignMode.SignIn)) + val test = viewModel.test() + + viewModel.handle(OnboardingAction.ResetSignMode) + + test + .assertStatesChanges( + initialState, + { copy(isLoading = false, signMode = SignMode.Unknown) }, + ) + .assertNoEvents() + .finish() + } + + @Test + fun `given registration state, when resetting authentication attempt, then cancels pending logic or registration and resets state`() = runTest { + viewModelWith(initialState.copy(isLoading = true, registrationState = A_REGISTRATION_STATE)) + val test = viewModel.test() + fakeAuthenticationService.expectedCancelsPendingLogin() + + viewModel.handle(OnboardingAction.ResetAuthenticationAttempt) + + test + .assertStatesChanges( + initialState, + { copy(isLoading = false, registrationState = RegistrationState()) }, + ) + .assertNoEvents() + .finish() + fakeAuthenticationService.verifyCancelsPendingLogin() + } + + + @Test + fun `given reset state, when resetting reset state, then resets state`() = runTest { + viewModelWith(initialState.copy(isLoading = true, resetState = ResetState(AN_EMAIL))) + val test = viewModel.test() + + viewModel.handle(OnboardingAction.ResetResetPassword) + + test + .assertStatesChanges( + initialState, + { copy(isLoading = false, resetState = ResetState()) }, + ) + .assertNoEvents() + .finish() + } + + @Test + fun `given registration state, when resetting user name, then resets state`() = runTest { + viewModelWith(initialState.copy(registrationState = A_REGISTRATION_STATE)) + val test = viewModel.test() + + viewModel.handle(OnboardingAction.ResetSelectedRegistrationUserName) + + test + .assertStatesChanges( + initialState, + { copy(registrationState = RegistrationState()) }, + ) + .assertNoEvents() + .finish() + } + private fun viewModelWith(state: OnboardingViewState) { OnboardingViewModel( state, diff --git a/vector/src/test/java/im/vector/app/features/onboarding/RegistrationStateFixture.kt b/vector/src/test/java/im/vector/app/features/onboarding/RegistrationStateFixture.kt new file mode 100644 index 0000000000..bb7711a5bc --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/onboarding/RegistrationStateFixture.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.onboarding + +object RegistrationStateFixture { + + fun aRegistrationState( + email: String? = null, + isUserNameAvailable: Boolean = false, + selectedMatrixId: String? = null, + ) = RegistrationState(email, isUserNameAvailable, selectedMatrixId) +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt index cc606497f5..0075fb29b1 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt @@ -18,6 +18,7 @@ package im.vector.app.test.fakes import io.mockk.coEvery import io.mockk.coJustRun +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import org.matrix.android.sdk.api.auth.AuthenticationService @@ -68,4 +69,12 @@ class FakeAuthenticationService : AuthenticationService by mockk() { fun givenDirectAuthenticationThrows(config: HomeServerConnectionConfig, matrixId: String, password: String, deviceName: String, cause: Throwable) { coEvery { directAuthentication(config, matrixId, password, deviceName) } throws cause } + + fun verifyReset() { + coVerify { reset() } + } + + fun verifyCancelsPendingLogin() { + coVerify { cancelPendingLoginOrRegistration() } + } } From bb9459fcabfaaea285d2f6ccc89f388c34eb8309 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 28 Jul 2022 11:41:40 +0100 Subject: [PATCH 5/9] adding test around sso url fetching --- .../onboarding/OnboardingViewModelTest.kt | 26 ++++++++++++++++++- .../test/fakes/FakeAuthenticationService.kt | 4 +++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index 1bc1b5bbf4..3785729230 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -49,10 +49,12 @@ import im.vector.app.test.fixtures.aBuildMeta import im.vector.app.test.fixtures.aHomeServerCapabilities import im.vector.app.test.test import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo import org.junit.Before import org.junit.Rule import org.junit.Test import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.Session @@ -81,6 +83,10 @@ private const val A_DEVICE_NAME = "a-device-name" private const val A_MATRIX_ID = "@$A_USERNAME:matrix.org" private const val A_LOGIN_TOKEN = "a-login-token" private val A_REGISTRATION_STATE = aRegistrationState(email = AN_EMAIL) +private const val A_SSO_URL = "https://a-sso.url" +private const val A_REDIRECT_URI = "https://a-redirect.uri" +private const val A_DEVICE_ID = "a-device-id" +private val SSO_REGISTRATION_DESCRIPTION = AuthenticationDescription.Register(AuthenticationDescription.AuthenticationType.SSO) class OnboardingViewModelTest { @@ -853,7 +859,6 @@ class OnboardingViewModelTest { fakeAuthenticationService.verifyCancelsPendingLogin() } - @Test fun `given reset state, when resetting reset state, then resets state`() = runTest { viewModelWith(initialState.copy(isLoading = true, resetState = ResetState(AN_EMAIL))) @@ -862,6 +867,7 @@ class OnboardingViewModelTest { viewModel.handle(OnboardingAction.ResetResetPassword) test + .assertStatesChanges( initialState, { copy(isLoading = false, resetState = ResetState()) }, @@ -886,6 +892,24 @@ class OnboardingViewModelTest { .finish() } + @Test + fun `given returns Sso url, when fetching Sso url, then updates authentication state and returns supplied Sso url`() = runTest { + val test = viewModel.test() + val provider = SsoIdentityProvider(id = "provider_id", null, null, null) + fakeAuthenticationService.givenSsoUrl(A_REDIRECT_URI, A_DEVICE_ID, provider.id, result = A_SSO_URL) + + val result = viewModel.fetchSsoUrl(A_REDIRECT_URI, A_DEVICE_ID, provider) + + result shouldBeEqualTo A_SSO_URL + test + .assertStatesChanges( + initialState, + { copy(selectedAuthenticationState = SelectedAuthenticationState(SSO_REGISTRATION_DESCRIPTION)) } + ) + .finish() + + } + private fun viewModelWith(state: OnboardingViewState) { OnboardingViewModel( state, diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt index 0075fb29b1..af53913169 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt @@ -77,4 +77,8 @@ class FakeAuthenticationService : AuthenticationService by mockk() { fun verifyCancelsPendingLogin() { coVerify { cancelPendingLoginOrRegistration() } } + + fun givenSsoUrl(redirectUri: String, deviceId: String, providerId: String, result: String) { + coEvery { getSsoUrl(redirectUri, deviceId, providerId) } returns result + } } From d3046248781da45e61d19b64f83b7034e6c75fa1 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 28 Jul 2022 11:49:50 +0100 Subject: [PATCH 6/9] removing extra blank lines --- .../vector/app/features/onboarding/OnboardingViewModelTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index 3785729230..495553c04d 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -867,7 +867,6 @@ class OnboardingViewModelTest { viewModel.handle(OnboardingAction.ResetResetPassword) test - .assertStatesChanges( initialState, { copy(isLoading = false, resetState = ResetState()) }, @@ -907,7 +906,6 @@ class OnboardingViewModelTest { { copy(selectedAuthenticationState = SelectedAuthenticationState(SSO_REGISTRATION_DESCRIPTION)) } ) .finish() - } private fun viewModelWith(state: OnboardingViewState) { From 5864ce43488c928e5ec8905c3eb4c6a2b048bb7d Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 5 Aug 2022 13:08:21 +0100 Subject: [PATCH 7/9] adding rule to force a new session to be started for instrumentation tests --- .../java/im/vector/app/CantVerifyTest.kt | 5 ++- .../im/vector/app/ClearCurrentSessionRule.kt | 40 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 vector/src/androidTest/java/im/vector/app/ClearCurrentSessionRule.kt diff --git a/vector/src/androidTest/java/im/vector/app/CantVerifyTest.kt b/vector/src/androidTest/java/im/vector/app/CantVerifyTest.kt index ba844e56b7..e6b17c1e9e 100644 --- a/vector/src/androidTest/java/im/vector/app/CantVerifyTest.kt +++ b/vector/src/androidTest/java/im/vector/app/CantVerifyTest.kt @@ -27,6 +27,7 @@ import im.vector.app.features.MainActivity import im.vector.app.ui.robot.ElementRobot import org.junit.Rule import org.junit.Test +import org.junit.rules.RuleChain import org.junit.runner.RunWith import java.util.UUID @@ -35,7 +36,9 @@ import java.util.UUID class CantVerifyTest : VerificationTestBase() { @get:Rule - val activityRule = ActivityScenarioRule(MainActivity::class.java) + val testRule = RuleChain + .outerRule(ActivityScenarioRule(MainActivity::class.java)) + .around(ClearCurrentSessionRule()) private val elementRobot = ElementRobot() var userName: String = "loginTest_${UUID.randomUUID()}" diff --git a/vector/src/androidTest/java/im/vector/app/ClearCurrentSessionRule.kt b/vector/src/androidTest/java/im/vector/app/ClearCurrentSessionRule.kt new file mode 100644 index 0000000000..7ba0b63799 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/ClearCurrentSessionRule.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app + +import androidx.datastore.preferences.preferencesDataStoreFile +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.runBlocking +import org.junit.rules.TestWatcher +import org.junit.runner.Description +import org.junit.runners.model.Statement + +class ClearCurrentSessionRule : TestWatcher() { + override fun apply(base: Statement, description: Description): Statement { + val context = InstrumentationRegistry.getInstrumentation().targetContext + runBlocking { + runCatching { + val holder = (context.applicationContext as VectorApplication).activeSessionHolder + holder.getSafeActiveSession()?.signOutService()?.signOut(true) + context.preferencesDataStoreFile(name = "vector_analytics").delete() + (context.applicationContext as VectorApplication).vectorPreferences.clearPreferences() + holder.clearActiveSession() + } + } + return super.apply(base, description) + } +} From 2683e9209b3d3aaf737795ee4818b9e76443b2cc Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 5 Aug 2022 14:24:53 +0100 Subject: [PATCH 8/9] resetting the analytics datastore via reflection --- vector/build.gradle | 1 + .../im/vector/app/ClearCurrentSessionRule.kt | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/vector/build.gradle b/vector/build.gradle index dc0a2da35d..0edaf5424e 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -598,4 +598,5 @@ dependencies { androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator debugImplementation libs.androidx.fragmentTesting + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.10" } diff --git a/vector/src/androidTest/java/im/vector/app/ClearCurrentSessionRule.kt b/vector/src/androidTest/java/im/vector/app/ClearCurrentSessionRule.kt index 7ba0b63799..68db0a9509 100644 --- a/vector/src/androidTest/java/im/vector/app/ClearCurrentSessionRule.kt +++ b/vector/src/androidTest/java/im/vector/app/ClearCurrentSessionRule.kt @@ -16,21 +16,26 @@ package im.vector.app -import androidx.datastore.preferences.preferencesDataStoreFile +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit import androidx.test.platform.app.InstrumentationRegistry +import im.vector.app.features.analytics.store.AnalyticsStore import kotlinx.coroutines.runBlocking import org.junit.rules.TestWatcher import org.junit.runner.Description import org.junit.runners.model.Statement +import kotlin.reflect.KClass class ClearCurrentSessionRule : TestWatcher() { override fun apply(base: Statement, description: Description): Statement { val context = InstrumentationRegistry.getInstrumentation().targetContext runBlocking { + reflectAnalyticDatastore(context).edit { it.clear() } runCatching { val holder = (context.applicationContext as VectorApplication).activeSessionHolder holder.getSafeActiveSession()?.signOutService()?.signOut(true) - context.preferencesDataStoreFile(name = "vector_analytics").delete() (context.applicationContext as VectorApplication).vectorPreferences.clearPreferences() holder.clearActiveSession() } @@ -38,3 +43,12 @@ class ClearCurrentSessionRule : TestWatcher() { return super.apply(base, description) } } + +private fun KClass<*>.asTopLevel() = Class.forName("${qualifiedName}Kt") + +@Suppress("UNCHECKED_CAST") +private fun reflectAnalyticDatastore(context: Context): DataStore { + val klass = AnalyticsStore::class.asTopLevel() + val method = klass.getMethod("access\$getDataStore", Context::class.java) + return method.invoke(klass, context) as DataStore +} From 20b3dbc6e6e239104fd5bf5931f4ee663155f776 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 5 Aug 2022 17:53:36 +0100 Subject: [PATCH 9/9] documenting the rule and reflection helper --- .../java/im/vector/app/ClearCurrentSessionRule.kt | 9 +++++++++ .../app/features/analytics/store/AnalyticsStore.kt | 3 +++ 2 files changed, 12 insertions(+) diff --git a/vector/src/androidTest/java/im/vector/app/ClearCurrentSessionRule.kt b/vector/src/androidTest/java/im/vector/app/ClearCurrentSessionRule.kt index 68db0a9509..735e96c1e0 100644 --- a/vector/src/androidTest/java/im/vector/app/ClearCurrentSessionRule.kt +++ b/vector/src/androidTest/java/im/vector/app/ClearCurrentSessionRule.kt @@ -28,6 +28,11 @@ import org.junit.runner.Description import org.junit.runners.model.Statement import kotlin.reflect.KClass +/** + * A TestRule to reset and clear the current Session. + * If a Session is active it will be signed out and cleared from the ActiveSessionHolder. + * The VectorPreferences and AnalyticsDatastore are also cleared in an attempt to recreate a fresh base. + */ class ClearCurrentSessionRule : TestWatcher() { override fun apply(base: Statement, description: Description): Statement { val context = InstrumentationRegistry.getInstrumentation().targetContext @@ -46,6 +51,10 @@ class ClearCurrentSessionRule : TestWatcher() { private fun KClass<*>.asTopLevel() = Class.forName("${qualifiedName}Kt") +/** + * Fetches the top level, private [Context.dataStore] extension property from [im.vector.app.features.analytics.store.AnalyticsStore] + * via reflection to avoid exposing property to all callers. + */ @Suppress("UNCHECKED_CAST") private fun reflectAnalyticDatastore(context: Context): DataStore { val klass = AnalyticsStore::class.asTopLevel() diff --git a/vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.kt b/vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.kt index 823d6285ed..fe1e8ad9ce 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.kt @@ -29,6 +29,9 @@ import kotlinx.coroutines.flow.map import org.matrix.android.sdk.api.extensions.orFalse import javax.inject.Inject +/** + * Also accessed via reflection by the instrumentation tests @see [im.vector.app.ClearCurrentSessionRule]. + */ private val Context.dataStore: DataStore by preferencesDataStore(name = "vector_analytics") /**