Skip to content

Commit 7efca20

Browse files
authored
Merge pull request #265 from FusionAuth/miker/issue-264-sample-application-view
Update the Sample Application View to match the UI in iOS SDK.
2 parents 5fcdbf0 + 0acdd57 commit 7efca20

File tree

15 files changed

+455
-313
lines changed

15 files changed

+455
-313
lines changed

.github/workflows/mobsf.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
- name: Run mobsfscan
5050
uses: MobSF/mobsfscan@3d87bc570c4614d705547bddb521395663dba353 # 0.4.5
5151
with:
52-
args: . --sarif --output mobsf.sarif.json || true
52+
args: . --config .mobsf.yml --sarif --output mobsf.sarif.json || true
5353

5454
# Uploads Sarif Report to GitHub
5555
- name: Upload mobsfscan report

.mobsf.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
ignore-paths:
2+
- app/src
3+
4+
ignore-rules:
5+
- android_logging

SECURITY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ For Responsible Disclosure Program, Discovering Security Vulnerabilities
77

88
| SDK Version | Tested FusionAuth Version | Tested Android API Level | Supported |
99
|---------------|---------------------------|--------------------------|--------------------|
10+
| \>= 1.0.0 | 1.57 - 1.60 | 31 - 36 | :white_check_mark: |
1011
| \>= 0.2.0 | 1.51 - 1.56 | 29 - 35 | :white_check_mark: |
1112
| 0.1.6 | 1.47 - 1.51 | 29 - 34 | :x: |
1213
| 0.1.4 - 0.1.5 | 1.47 - 1.50 | 29 - 34 | :x: |

app/src/androidTest/java/io/fusionauth/sdk/FullEnd2EndTest.kt

Lines changed: 95 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -38,201 +38,142 @@ internal class FullEnd2EndTest {
3838
@get:Rule
3939
val repeatRule = RepeatRule()
4040

41+
private lateinit var device: UiDevice
42+
4143
@Before
4244
fun setUp() {
4345
logger.info("Setting up test")
44-
4546
Intents.init()
47+
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
4648

4749
val automation = InstrumentationRegistry.getInstrumentation().uiAutomation
4850
val info = automation.serviceInfo
4951
info.flags = info.flags or AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
5052
automation.serviceInfo = info
5153
}
5254

53-
/**
54-
* Executes an end-to-end test for the login functionality.
55-
*
56-
* It performs the following steps:
57-
*
58-
* 1. Clicks the login button.
59-
* 2. Wait for the login form to appear.
60-
* 3. Sets the username and password on the login form.
61-
* 4. Submits the form by pressing the enter key.
62-
* 5. Wait for the token activity to be displayed.
63-
* 6. Checks the refresh token functionality.
64-
* 7. Checks if the token was refreshed.
65-
* 8. Clicks the sign-out button.
66-
* 9. Wait for the login activity to be displayed.
67-
*
68-
* This test is repeated twice to ensure logout was successful and the login form is displayed again.
69-
*/
7055
@Test
7156
@Repeat(2)
7257
fun e2eTest() {
73-
logger.info("Click login button")
74-
onView(withId(R.id.start_auth)).perform(click())
75-
logger.info("Login button clicked")
76-
77-
logger.info("Waiting for login form to appear")
78-
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
79-
80-
handleFALoginForm(device, USERNAME, PASSWORD)
81-
82-
// Check that the token activity is displayed
83-
device.wait(Until.findObject(By.res("io.fusionauth.app:id/sign_out")), TIMEOUT_MILLIS)
84-
onView(withId(R.id.sign_out)).check(matches(isDisplayed()))
85-
86-
logger.info("Token activity displayed")
87-
88-
// Check refresh token functionality
89-
val expirationTime = runBlocking { AuthorizationManager.getAccessTokenExpirationTime()!! }
90-
logger.info("Check refresh token")
91-
onView(withId(R.id.refresh_token))
92-
.check(matches(isDisplayed()))
93-
.perform(click())
94-
95-
val newExpirationTime = runBlocking { AuthorizationManager.getAccessTokenExpirationTime()!! }
96-
97-
logger.info("Token was refreshed (${expirationTime} to ${newExpirationTime})")
98-
check(newExpirationTime > expirationTime) { "Token was not refreshed" }
99-
100-
Thread.sleep(1000)
101-
102-
// Click the sign-out button
103-
logger.info("Click sign out button")
104-
onView(withId(R.id.sign_out)).perform(click())
105-
106-
// Check that the login activity is displayed
107-
logger.info("Check that the login activity is displayed")
108-
device.wait(Until.findObject(By.res("io.fusionauth.app:id/start_auth")), TIMEOUT_MILLIS)
109-
onView(withId(R.id.start_auth)).check(matches(isDisplayed()))
110-
111-
logger.info("Click login button for second user login")
112-
onView(withId(R.id.start_auth)).perform(click())
113-
logger.info("Login button clicked")
114-
115-
logger.info("Waiting for login form to appear")
116-
117-
handleFALoginForm(device, USERNAME2, PASSWORD2)
118-
119-
// Check that the token activity is displayed
120-
device.wait(Until.findObject(By.res("io.fusionauth.app:id/sign_out")), TIMEOUT_MILLIS)
121-
onView(withId(R.id.sign_out)).check(matches(isDisplayed()))
122-
123-
logger.info("Token activity displayed for second user")
58+
login(USERNAME, PASSWORD)
59+
performAndVerifyTokenRefresh()
60+
logout()
61+
login(USERNAME2, PASSWORD2)
62+
logout()
63+
}
12464

125-
// Click the sign-out button
126-
logger.info("Click sign out button for second user")
127-
onView(withId(R.id.sign_out)).perform(click())
65+
@Test
66+
fun e2eTestSwitchFromPrimaryToAlternative() {
67+
login(USERNAME, PASSWORD)
68+
switchToAlternative()
69+
login(USERNAME_RESET_CONFIGURATION, PASSWORD_RESET_CONFIGURATION)
70+
logout()
71+
loginSessionExists(USERNAME)
72+
logout()
73+
}
12874

129-
// Check that the login activity is displayed
130-
logger.info("Check that the login activity is displayed")
131-
device.wait(Until.findObject(By.res("io.fusionauth.app:id/start_auth")), TIMEOUT_MILLIS)
132-
onView(withId(R.id.start_auth)).check(matches(isDisplayed()))
75+
@Test
76+
fun e2eTestSwitchFromAlternativeToPrimary() {
77+
login(USERNAME, PASSWORD)
78+
switchToAlternative()
79+
login(USERNAME_RESET_CONFIGURATION, PASSWORD_RESET_CONFIGURATION)
80+
switchToPrimary()
81+
loginSessionExists(USERNAME)
82+
switchToAlternative()
83+
loginSessionExists(USERNAME_RESET_CONFIGURATION)
84+
logout()
85+
loginSessionExists(USERNAME)
86+
logout()
13387
}
13488

135-
/**
136-
* Executes an end-to-end test for the login functionality where the configuration is reset to login
137-
* to a different tenant. It performs the following steps:
138-
* 1. Clicks the login button.
139-
* 2. Waits for the login form to appear.
140-
* 3. Sets the username and password on the login form.
141-
* 4. Submits the form by pressing the enter key.
142-
* 5. Waits for the token activity to be displayed.
143-
* 6. Checks the reset configuration functionality.
144-
* 7. Waits for the login activity to be displayed.
145-
* 8. Clicks the login button.
146-
* 9. Waits for the login form to appear.
147-
* 10. Sets the username and password on the login form.
148-
* 11. Submits the form by pressing the enter key.
149-
* 12. Checks the refresh token functionality.
150-
* 13. Checks if the token was refreshed.
151-
* 14. Clicks the sign-out button.
152-
* 15. Waits for the login activity to be displayed.
153-
*/
15489
@Test
155-
fun e2eTestResetConfiguration() {
156-
logger.info("Click login button")
157-
onView(withId(R.id.start_auth)).perform(click())
158-
logger.info("Login button clicked")
90+
fun e2eTestCancelConfigurationSwitch() {
91+
login(USERNAME, PASSWORD)
15992

160-
logger.info("Waiting for login form to appear")
161-
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
93+
val expirationTimeBefore = runBlocking { AuthorizationManager.getAccessTokenExpirationTime()!! }
16294

163-
handleFALoginForm(device, USERNAME, PASSWORD)
95+
logger.info("Click reset configuration")
96+
onView(withId(R.id.reset_configuration)).perform(click())
97+
logger.info("Click cancel button on dialog")
98+
onView(withId(R.id.cancel_button)).perform(click())
16499

165-
// Check that the token activity is displayed
166-
device.wait(Until.findObject(By.res("io.fusionauth.app:id/sign_out")), TIMEOUT_MILLIS)
167-
onView(withId(R.id.sign_out)).check(matches(isDisplayed()))
100+
verifyOnTokenActivity()
168101

169-
logger.info("Token activity displayed")
102+
logger.info("Click refresh token to confirm session is active")
103+
onView(withId(R.id.refresh_token)).perform(click())
104+
val expirationTimeAfter = runBlocking { AuthorizationManager.getAccessTokenExpirationTime()!! }
105+
check(expirationTimeAfter > expirationTimeBefore) { "Token was not refreshed after canceling config switch" }
170106

171-
// Check reset configuration functionality
172-
logger.info("Check reset configuration")
173-
onView(withId(R.id.reset_configuration))
174-
.check(matches(isDisplayed()))
175-
.perform(click())
107+
logout()
108+
}
176109

177-
// Check that the login activity is displayed
178-
logger.info("Check that the login activity is displayed")
179-
device.wait(Until.findObject(By.res("io.fusionauth.app:id/start_auth")), TIMEOUT_MILLIS)
180-
onView(withId(R.id.start_auth)).check(matches(isDisplayed()))
110+
// Helper Functions
181111

182-
logger.info("Click login button for user login")
112+
private fun login(username: String, password: String) {
113+
logger.info("--> login(username: $username)")
183114
onView(withId(R.id.start_auth)).perform(click())
184-
logger.info("Login button clicked")
185-
186-
logger.info("Waiting for login form to appear")
187-
188-
handleFALoginForm(device, USERNAME_RESET_CONFIGURATION, PASSWORD_RESET_CONFIGURATION)
189-
190-
// Check that the token activity is displayed
191-
device.wait(Until.findObject(By.res("io.fusionauth.app:id/sign_out")), TIMEOUT_MILLIS)
192-
onView(withId(R.id.sign_out)).check(matches(isDisplayed()))
115+
handleFALoginForm(username, password)
116+
verifyOnTokenActivity()
117+
logger.info("<-- login")
118+
}
193119

194-
logger.info("Token activity displayed for user in reset configuration tenant")
120+
private fun loginSessionExists(username: String) {
121+
logger.info("--> login(username: $username)")
122+
onView(withId(R.id.start_auth)).perform(click())
123+
verifyOnTokenActivity()
124+
logger.info("<-- login")
125+
}
126+
private fun logout() {
127+
logger.info("--> logout()")
128+
onView(withId(R.id.sign_out)).perform(click())
129+
verifyOnLoginActivity()
130+
logger.info("<-- logout()")
131+
}
195132

196-
// Check refresh token functionality
133+
private fun performAndVerifyTokenRefresh() {
134+
logger.info("--> performAndVerifyTokenRefresh()")
197135
val expirationTime = runBlocking { AuthorizationManager.getAccessTokenExpirationTime()!! }
198-
logger.info("Check refresh token")
199136
onView(withId(R.id.refresh_token))
200137
.check(matches(isDisplayed()))
201138
.perform(click())
202-
203139
val newExpirationTime = runBlocking { AuthorizationManager.getAccessTokenExpirationTime()!! }
204-
205140
logger.info("Token was refreshed (${expirationTime} to ${newExpirationTime})")
206141
check(newExpirationTime > expirationTime) { "Token was not refreshed" }
142+
Thread.sleep(1000) // Wait a bit for UI to settle
143+
logger.info("<-- performAndVerifyTokenRefresh()")
144+
}
207145

208-
Thread.sleep(1000)
146+
private fun switchToAlternative() {
147+
logger.info("--> switchToAlternative()")
148+
onView(withId(R.id.reset_configuration)).perform(click())
149+
onView(withId(R.id.switch_to_alternative_button)).perform(click())
150+
verifyOnLoginActivity()
151+
logger.info("<-- switchToAlternative()")
152+
}
209153

210-
// Click the sign-out button
211-
logger.info("Click sign out button for user")
212-
onView(withId(R.id.sign_out)).perform(click())
154+
private fun switchToPrimary() {
155+
logger.info("--> switchToPrimary()")
156+
onView(withId(R.id.reset_configuration)).perform(click())
157+
onView(withId(R.id.switch_to_primary_button)).perform(click())
158+
verifyOnLoginActivity()
159+
logger.info("<-- switchToPrimary()")
160+
}
161+
162+
private fun verifyOnTokenActivity() {
163+
device.wait(Until.findObject(By.res("io.fusionauth.app:id/sign_out")), TIMEOUT_MILLIS)
164+
onView(withId(R.id.sign_out)).check(matches(isDisplayed()))
165+
logger.info("Verified on TokenActivity")
166+
}
213167

214-
// Check that the login activity is displayed
215-
logger.info("Check that the login activity is displayed")
168+
private fun verifyOnLoginActivity() {
216169
device.wait(Until.findObject(By.res("io.fusionauth.app:id/start_auth")), TIMEOUT_MILLIS)
217170
onView(withId(R.id.start_auth)).check(matches(isDisplayed()))
171+
logger.info("Verified on LoginActivity")
218172
}
219173

220-
/**
221-
* Sets the username and password on the login form.
222-
*
223-
* @param device The UiDevice used to interact with the UI.
224-
* @param username The username to set on the login form.
225-
* @param password The password to set on the login form.
226-
*/
227-
private fun handleFALoginForm(
228-
device: UiDevice,
229-
username: String,
230-
password: String
231-
) {
232-
device.wait(
233-
Until.findObject(By.clazz("android.webkit.WebView")),
234-
TIMEOUT_MILLIS
235-
)
174+
private fun handleFALoginForm(username: String, password: String) {
175+
device.wait(Until.findObject(By.clazz("android.webkit.WebView")),
176+
TIMEOUT_MILLIS)
236177

237178
val textFields = device.findObjects(By.clazz("android.widget.EditText"))
238179

@@ -246,25 +187,16 @@ internal class FullEnd2EndTest {
246187
val passwordInputObject = textFields[1]
247188
passwordInputObject.setText(password)
248189

249-
// Submit the form by pressing the enter key
250190
logger.info("Submit form by pressing enter key")
251191
passwordInputObject.click()
252192
device.pressEnter()
253193
}
254194

255-
/**
256-
* Closes the keyboard if it is open on the screen.
257-
*
258-
* When the (automated test) device has a small vertical resolution, the keyboard may be open and cover the login
259-
* form, thus preventing the UISelector from targeting the form fields.
260-
*
261-
* @throws IllegalStateException if the keyboard cannot be closed.
262-
*/
263195
private fun closeKeyboardIfOpen() {
264196
val automation = InstrumentationRegistry.getInstrumentation().uiAutomation
265197
for (window in automation.windows) {
266198
if (window.type == AccessibilityWindowInfo.TYPE_INPUT_METHOD) {
267-
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).pressBack()
199+
device.pressBack()
268200
return
269201
}
270202
}
@@ -273,7 +205,7 @@ internal class FullEnd2EndTest {
273205
@After
274206
fun tearDown() {
275207
logger.info("Tearing down test")
276-
208+
runBlocking { AuthorizationManager.clearState() }
277209
Intents.release()
278210
}
279211

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.fusionauth.sdk
2+
3+
import io.fusionauth.mobilesdk.AuthorizationConfiguration
4+
5+
object AppConfigurations {
6+
val PRIMARY_CONFIG = AuthorizationConfiguration(
7+
fusionAuthUrl = "http://10.0.2.2:9011",
8+
tenant = "d7d09513-a3f5-401c-9685-34ab6c552453",
9+
clientId = "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e",
10+
allowUnsecureConnection = true,
11+
additionalScopes = setOf("email", "profile")
12+
)
13+
14+
val ALTERNATIVE_CONFIG = AuthorizationConfiguration(
15+
fusionAuthUrl = "http://10.0.2.2:9011",
16+
clientId = "2d491002-1b1b-4a59-be2a-1d570c834c7a",
17+
allowUnsecureConnection = true,
18+
additionalScopes = setOf("email", "profile"),
19+
tenant = "a3138f90-16b5-444f-b5f6-0ca64bc30ca7"
20+
)
21+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.fusionauth.sdk
2+
3+
import io.fusionauth.mobilesdk.AuthorizationConfiguration
4+
5+
class ConfigurationManager {
6+
7+
fun getPrimaryConfig(): AuthorizationConfiguration {
8+
return AppConfigurations.PRIMARY_CONFIG
9+
}
10+
11+
fun getAlternativeConfig(): AuthorizationConfiguration {
12+
return AppConfigurations.ALTERNATIVE_CONFIG
13+
}
14+
15+
fun isPrimaryConfig(config: AuthorizationConfiguration): Boolean {
16+
return config.clientId == AppConfigurations.PRIMARY_CONFIG.clientId
17+
}
18+
}

0 commit comments

Comments
 (0)