Skip to content

Commit 70d4ab6

Browse files
authored
test: coverage for keycloak SSO login flows (WPB-27448) (#5102)
1 parent f386d50 commit 70d4ab6

6 files changed

Lines changed: 280 additions & 4 deletions

File tree

tests/testsCore/src/androidTest/kotlin/com/wire/android/tests/core/criticalFlows/SSODeviceBackup.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ class SSODeviceBackup : BaseUiTest() {
121121
pages.ssoPage.apply {
122122
waitUntilKeycloakPageLoaded()
123123
enterKeycloakEmail(member1.email ?: "")
124+
closeKeyboardIfOpened()
124125
enterKeycloakPassword(member1.password ?: "")
125126
closeKeyboardIfOpened()
126127
tapKeycloakSignIn()

tests/testsCore/src/androidTest/kotlin/com/wire/android/tests/core/e2eTests/ApplockTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ class ApplockTest : BaseUiTest() {
337337
}
338338

339339
@Suppress("CyclomaticComplexMethod", "LongMethod")
340+
@TestCaseId("TC-11587")
340341
@Category("regression", "RC", "applock", "security")
341342
@Tag(key = "feature", value = "appLockColdStart")
342343
@Test
@@ -360,6 +361,7 @@ class ApplockTest : BaseUiTest() {
360361
pages.loginPage.apply {
361362
clickStagingDeepLink()
362363
clickProceedButtonOnDeeplinkOverlay()
364+
clickContinueButtonOnBackendConfigSuccess()
363365
enterTeamOwnerLoggingEmail(teamOwner?.email ?: "")
364366
clickLoginButton()
365367
enterTeamOwnerLoggingPassword(teamOwner?.password ?: "")

tests/testsCore/src/androidTest/kotlin/com/wire/android/tests/core/e2eTests/SSOProvisioningTests.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ class SSOProvisioningTests : BaseUiTest() {
117117
pages.ssoPage.apply {
118118
waitUntilKeycloakPageLoaded()
119119
enterKeycloakEmail(member1.email.orEmpty())
120+
closeKeyboardIfOpened()
120121
enterKeycloakPassword(member1.password.orEmpty())
121122
closeKeyboardIfOpened()
122123
}
@@ -224,6 +225,7 @@ class SSOProvisioningTests : BaseUiTest() {
224225
pages.ssoPage.apply {
225226
waitUntilKeycloakPageLoaded()
226227
enterKeycloakEmail(member1.email.orEmpty())
228+
closeKeyboardIfOpened()
227229
enterKeycloakPassword(member1.password.orEmpty())
228230
closeKeyboardIfOpened()
229231
}
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2026 Wire Swiss GmbH
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see http://www.gnu.org/licenses/.
17+
*/
18+
package com.wire.android.tests.core.e2eTests
19+
20+
import SSOServiceHelper
21+
import androidx.test.ext.junit.runners.AndroidJUnit4
22+
import com.wire.android.tests.core.BaseUiTest
23+
import com.wire.android.tests.support.UiAutomatorSetup
24+
import com.wire.android.tests.support.tags.Category
25+
import com.wire.android.tests.support.tags.TestCaseId
26+
import keycloak.KeycloakApiClient
27+
import kotlinx.coroutines.runBlocking
28+
import org.junit.After
29+
import org.junit.Before
30+
import org.junit.Test
31+
import org.junit.runner.RunWith
32+
import uiautomatorutils.KeyboardUtils.closeKeyboardIfOpened
33+
import uiautomatorutils.UiWaitUtils
34+
import user.utils.ClientUser
35+
import kotlin.time.Duration.Companion.seconds
36+
37+
@RunWith(AndroidJUnit4::class)
38+
class SSOTests : BaseUiTest() {
39+
private lateinit var keycloakApiClient: KeycloakApiClient
40+
private lateinit var member1: ClientUser
41+
42+
@Before
43+
fun setUp() {
44+
initCommonTestHelpers()
45+
device = UiAutomatorSetup.start(UiAutomatorSetup.APP_ALPHA)
46+
SSOServiceHelper.initialize(clientUserManager)
47+
keycloakApiClient = KeycloakApiClient(backendClient)
48+
}
49+
50+
@After
51+
fun tearDown() {
52+
runCatching { keycloakApiClient.cleanUp() }
53+
}
54+
55+
@Suppress("LongMethod")
56+
@TestCaseId("TC-4547")
57+
@Category("regression", "RC", "login", "SSO")
58+
@Test
59+
fun givenSsoUserLogsInWithKeycloak_whenCompletingTheSsoFlow_thenUserReachesConversationList() {
60+
var ssoCode = ""
61+
62+
step("Given There is a team owner user1Name with SSO team SSO configured for keycloak") {
63+
runBlocking {
64+
SSOServiceHelper.createKeycloakSsoTeamOwner(
65+
context,
66+
"user1Name",
67+
"SSO",
68+
keycloakApiClient
69+
)
70+
}
71+
}
72+
73+
step("And User user1Name adds user user2Name to keycloak") {
74+
runBlocking {
75+
SSOServiceHelper.addKeycloakSsoUsers(
76+
"user1Name",
77+
"user2Name",
78+
keycloakApiClient
79+
)
80+
}
81+
}
82+
83+
step("And SSO user user2Name is me") {
84+
SSOServiceHelper.setCurrentSsoUser("user2Name")
85+
member1 = clientUserManager.findUserByNameOrNameAlias("user2Name")
86+
}
87+
88+
step("And I see email verification Welcome Page") {
89+
pages.registrationPage.assertEmailWelcomePage()
90+
}
91+
92+
step("And I open staging backend deep link and tap proceed button on custom backend alert") {
93+
pages.loginPage.apply {
94+
clickStagingDeepLink()
95+
clickProceedButtonOnDeeplinkOverlay()
96+
clickContinueButtonOnBackendConfigSuccess()
97+
}
98+
}
99+
100+
step("When I type the default SSO code on Login Tab") {
101+
ssoCode = SSOServiceHelper.getSsoCode()
102+
pages.loginPage.enterSSOCodeOnSSOLoginTab(ssoCode)
103+
}
104+
105+
step("And I tap next button to login and tap use without an account button if visible") {
106+
pages.loginPage.clickLoginButton()
107+
pages.chromePage.dismissFirstRunIfVisible()
108+
}
109+
110+
step("And I sign in with my credentials on Keycloak Page") {
111+
pages.ssoPage.apply {
112+
waitUntilKeycloakPageLoaded()
113+
enterKeycloakEmail(member1.email.orEmpty())
114+
closeKeyboardIfOpened()
115+
enterKeycloakPassword(member1.password.orEmpty())
116+
closeKeyboardIfOpened()
117+
}
118+
}
119+
120+
step("And I tap login button on Keycloak Page and wait until username setup page is visible") {
121+
pages.ssoPage.tapKeycloakSignIn()
122+
pages.registrationPage.assertEnterYourUserNameInfoText()
123+
}
124+
125+
step("And I submit my Username user2UniqueUsername on registration page") {
126+
pages.registrationPage.apply {
127+
setUserName(member1.uniqueUsername.orEmpty())
128+
clickConfirmButton()
129+
}
130+
}
131+
132+
step("And I wait until I am fully logged in") {
133+
pages.registrationPage.apply {
134+
waitUntilLoginFlowIsCompleted()
135+
clickAllowNotificationButton()
136+
}
137+
}
138+
139+
step("And I decline share data alert") {
140+
UiWaitUtils.waitFor(1.seconds)
141+
pages.registrationPage.clickDeclineShareDataAlert()
142+
}
143+
144+
step("Then I reach the conversation list") {
145+
pages.conversationListPage.assertConversationListVisible()
146+
}
147+
}
148+
149+
@Suppress("LongMethod")
150+
@TestCaseId("TC-4548")
151+
@Category("regression", "RC", "login", "SSO")
152+
@Test
153+
fun givenSsoUserEntersInvalidCodeAndCredentials_whenTryingToSignIn_thenTheRelevantErrorsAreShown() {
154+
var ssoCode = ""
155+
156+
step("Given There is a team owner user1Name with SSO team SSO configured for keycloak") {
157+
runBlocking {
158+
SSOServiceHelper.createKeycloakSsoTeamOwner(
159+
context,
160+
"user1Name",
161+
"SSO",
162+
keycloakApiClient
163+
)
164+
}
165+
}
166+
167+
step("And User user1Name adds user user2Name to keycloak") {
168+
runBlocking {
169+
SSOServiceHelper.addKeycloakSsoUsers(
170+
"user1Name",
171+
"user2Name",
172+
keycloakApiClient
173+
)
174+
}
175+
}
176+
177+
step("And SSO user user2Name is me") {
178+
SSOServiceHelper.setCurrentSsoUser("user2Name")
179+
member1 = clientUserManager.findUserByNameOrNameAlias("user2Name")
180+
}
181+
182+
step("And I see email verification Welcome Page") {
183+
pages.registrationPage.assertEmailWelcomePage()
184+
}
185+
186+
step("And I open staging backend deep link and tap proceed button on custom backend alert") {
187+
pages.loginPage.apply {
188+
clickStagingDeepLink()
189+
clickProceedButtonOnDeeplinkOverlay()
190+
clickContinueButtonOnBackendConfigSuccess()
191+
}
192+
}
193+
194+
step("When I type an invalid SSO code on Login Tab and tap next button to login") {
195+
pages.loginPage.apply {
196+
enterSSOCodeOnSSOLoginTab("wire-74b782bd-3bb0-4247-8aaf")
197+
clickLoginButton()
198+
}
199+
}
200+
201+
step("Then I see an error message underneath the SSO code input field") {
202+
pages.loginPage.assertSsoValidationErrorVisible("Please enter a valid email or SSO code")
203+
}
204+
205+
step("And I clear the SSO Code input field and type the default SSO code on Login Tab") {
206+
ssoCode = SSOServiceHelper.getSsoCode()
207+
pages.loginPage.apply {
208+
clearUserIdentifierInput()
209+
enterSSOCodeOnSSOLoginTab(ssoCode)
210+
}
211+
}
212+
213+
step("And I tap next button to login and tap use without an account button if visible") {
214+
pages.loginPage.clickLoginButton()
215+
pages.chromePage.dismissFirstRunIfVisible()
216+
}
217+
218+
step("When I sign in with invalid credentials on Keycloak Page") {
219+
pages.ssoPage.apply {
220+
waitUntilKeycloakPageLoaded()
221+
enterKeycloakEmail("smoketester+invalid@wire.com")
222+
closeKeyboardIfOpened()
223+
enterKeycloakPassword("thisIsAnInvalidPassword")
224+
closeKeyboardIfOpened()
225+
tapKeycloakSignIn()
226+
}
227+
}
228+
229+
step("Then I see an error message telling me that I am unable to sign in on Keycloak Page") {
230+
pages.ssoPage.assertKeycloakErrorVisible("Invalid username or password")
231+
}
232+
}
233+
}

tests/testsCore/src/androidTest/kotlin/com/wire/android/tests/core/pages/LoginPage.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,13 @@ data class LoginPage(private val device: UiDevice) {
147147
return this
148148
}
149149

150+
fun clearUserIdentifierInput(): LoginPage {
151+
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
152+
device.findObject(emailInputField).click()
153+
device.findObject(emailInputField).setText("")
154+
return this
155+
}
156+
150157
fun enterTeamOwnerLoggingPassword(password: String): LoginPage {
151158
enterPassword(password)
152159
return this
@@ -305,6 +312,15 @@ data class LoginPage(private val device: UiDevice) {
305312
return this
306313
}
307314

315+
fun assertSsoValidationErrorVisible(expectedMessage: String): LoginPage {
316+
val error = UiWaitUtils.waitElement(UiSelectorParams(text = expectedMessage))
317+
assertTrue(
318+
"Expected SSO validation error '$expectedMessage' to be visible",
319+
!error.visibleBounds.isEmpty
320+
)
321+
return this
322+
}
323+
308324
fun clickConfirmButtonOnUsernameSetupPage(): LoginPage {
309325
val confirmButton = UiWaitUtils.waitElement(confirmButtonSelector)
310326
confirmButton.click()

tests/testsCore/src/androidTest/kotlin/com/wire/android/tests/core/pages/SSOPage.kt

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package com.wire.android.tests.core.pages
2020
import androidx.test.uiautomator.By
2121
import androidx.test.uiautomator.UiDevice
2222
import androidx.test.uiautomator.UiObject2
23+
import org.junit.Assert.assertTrue
2324
import uiautomatorutils.UiSelectorParams
2425
import uiautomatorutils.UiWaitUtils
2526
import kotlin.time.Duration
@@ -51,17 +52,38 @@ data class SSOPage(private val device: UiDevice) {
5152
return this
5253
}
5354

55+
fun assertKeycloakErrorVisible(expectedMessage: String): SSOPage {
56+
val error = UiWaitUtils.waitElement(
57+
UiSelectorParams(textContains = expectedMessage),
58+
timeout = 15.seconds
59+
)
60+
assertTrue(
61+
"Expected Keycloak error '$expectedMessage' to be visible",
62+
!error.visibleBounds.isEmpty
63+
)
64+
return this
65+
}
66+
5467
fun waitUntilKeycloakPageLoaded(timeout: Duration = 20.seconds): SSOPage {
5568
UiWaitUtils.waitElement(keycloakUsernameLabel, timeout = timeout)
5669
return this
5770
}
5871

59-
private fun inputFieldBelow(label: UiSelectorParams, fieldName: String): UiObject2 {
60-
val labelElement = UiWaitUtils.waitElement(label, timeout = 15.seconds)
72+
private fun inputFieldBelow(
73+
label: UiSelectorParams,
74+
fieldName: String
75+
): UiObject2 {
76+
val labelBounds = UiWaitUtils.waitElement(label, timeout = 15.seconds).visibleBounds
77+
6178
return device.findObjects(By.clazz("android.widget.EditText"))
62-
.firstOrNull { editText ->
79+
.asSequence()
80+
.filter { editText ->
6381
!editText.visibleBounds.isEmpty &&
64-
editText.visibleBounds.top >= labelElement.visibleBounds.bottom
82+
editText.isEnabled &&
83+
editText.visibleBounds.top >= labelBounds.bottom
84+
}
85+
.minByOrNull { editText ->
86+
editText.visibleBounds.top - labelBounds.bottom
6587
}
6688
?: throw AssertionError("$fieldName was not visible.")
6789
}

0 commit comments

Comments
 (0)