Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a4b718a
Re-enable Native Auth SSPR tests with Mail.tm
djanardhan98 Aug 12, 2026
4ad73aa
Update Common after dev rebase
djanardhan98 Aug 12, 2026
e9d6059
Document Native Auth E2E CI design
djanardhan98 Aug 12, 2026
dd6b5ce
Correct Native Auth E2E task design
djanardhan98 Aug 12, 2026
ce92902
Run SSPR E2E tests in PR validation
djanardhan98 Aug 13, 2026
a6aba48
Delete docs/superpowers/specs/2026-08-12-native-auth-e2e-ci-design.md
djanardhan98 Aug 14, 2026
a90fc17
Run all Native Auth E2E tests in PR validation
djanardhan98 Aug 14, 2026
f56e844
Update pr-msal.yml
djanardhan98 Aug 14, 2026
e94d321
Fix AB ID extraction in PR workflow
Copilot Aug 14, 2026
3c4a8e3
Restore retries for SSPR E2E tests
Copilot Aug 14, 2026
e1124b5
Pass email password through pipeline env
Copilot Aug 14, 2026
97dcc4b
Restore email password Gradle property
Copilot Aug 14, 2026
f259295
Skip rate-limited SSPR OTP E2E scenarios
Copilot Aug 14, 2026
97f1df6
Retry rate-limited Native Auth E2E tests
Copilot Aug 14, 2026
41b12b8
Preserve Native Auth retry failure cause
Copilot Aug 14, 2026
9686b50
Update Common Mail.tm integration
djanardhan98 Aug 14, 2026
e356e84
Update Common runtime email credential integration
djanardhan98 Aug 17, 2026
a599345
Fix Native Auth email service initialization
djanardhan98 Aug 17, 2026
4bd47e5
Revert PR AB ID workflow change
djanardhan98 Aug 18, 2026
62183f7
Update Common to latest dev
djanardhan98 Aug 24, 2026
6bbd3b1
Merge dev into Native Auth SSPR changes
djanardhan98 Aug 24, 2026
a1be871
Address review feedback on Native Auth E2E tests
djanardhan98 Aug 25, 2026
03d18c5
Stop retrying non-throttle Native Auth failures
djanardhan98 Aug 26, 2026
e05aa50
Merge dev and update Common
Copilot Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions azure-pipelines/pull-request-validation/pr-msal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,59 @@ stages:
tasks: msal:jacocoTestReport -PcodeCoverageEnabled=true -ProbolectricSdkVersion=${{variables.robolectricSdkVersion}} -PmockApiUrl=$(MOCK_API_URL) -PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING)
javaHomeSelection: $(BuildParameters.javaHomeSelection)
jdkVersion: 1.17
- bash: |
set -euo pipefail
SKIP_LABEL="skip-native-auth-e2e-tests"
# AGENT_TEMPDIRECTORY is a Windows path on the windows-2022 pool; normalise the
# separators so curl/grep never see a backslash path under Git Bash/MSYS.
TEMP_DIR="${AGENT_TEMPDIRECTORY:-.}"
LABELS_FILE="${TEMP_DIR//\\//}/native-auth-pr-labels.json"

if [ -z "${PR_NUMBER:-}" ]; then
echo "No PR number available (not a PR build). Skipping Native Auth E2E tests."
echo "##vso[task.setvariable variable=SKIP_NATIVE_AUTH_E2E]true"
exit 0
fi

LABELS_URL="https://api.github.com/repos/${REPO}/issues/${PR_NUMBER}/labels"
echo "Fetching PR labels from ${LABELS_URL}"
# Capture the body and the HTTP status separately, so that a failed API call is
# distinguishable from a successful call that simply returned no matching label.
HTTP_STATUS=$(curl -sS -o "$LABELS_FILE" -w "%{http_code}" "$LABELS_URL" || echo "000")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This label check hits api.github.com unauthenticated. Unauth github API is 60/hr per IP, and on the shared windows-2022 pool that egress IP is shared across a ton of jobs, so on a busy day this'll come back 403 rate-limited. The fail-safe right below then treats any non-200 as skip=true, so the native auth e2e tests we're re-enabling would just silently stop running with a green pipeline and nobody notices. kinda defeats the point of making them a PR gate.


if [ "$HTTP_STATUS" != "200" ]; then
echo "GitHub labels API returned HTTP ${HTTP_STATUS}. Skipping Native Auth E2E tests as a preventive measure (fail-safe to skipping, never to blocking)."
echo "##vso[task.setvariable variable=SKIP_NATIVE_AUTH_E2E]true"
exit 0
fi

# jq is not reliably available on the windows-2022 pool, so match on the raw JSON.
if grep -q "\"name\"[[:space:]]*:[[:space:]]*\"${SKIP_LABEL}\"" "$LABELS_FILE"; then
echo "Label '${SKIP_LABEL}' is present on PR #${PR_NUMBER}. Skipping Native Auth E2E tests."
echo "##vso[task.setvariable variable=SKIP_NATIVE_AUTH_E2E]true"
else
echo "Label '${SKIP_LABEL}' is not present on PR #${PR_NUMBER}. Running Native Auth E2E tests."
echo "##vso[task.setvariable variable=SKIP_NATIVE_AUTH_E2E]false"
fi
displayName: Check for skip-native-auth-e2e-tests label
env:
PR_NUMBER: $(System.PullRequest.PullRequestNumber)
REPO: $(Build.Repository.Name)
- task: Gradle@2
displayName: Run Native Auth E2E Tests
# Mirrors iOS (microsoft-authentication-library-for-objc) pr-validation.yml, which guards its
# native auth E2E jobs with the same opt-out label and a 30 minute timeout.
condition: and(succeeded(), ne(variables['SKIP_NATIVE_AUTH_E2E'], 'true'))
timeoutInMinutes: 30
inputs:
tasks: msal:testLocalDebugUnitTest -Plabtest -ProbolectricSdkVersion=${{variables.robolectricSdkVersion}} -PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING) --tests com.microsoft.identity.client.e2e.tests.network.nativeauth.*

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we gating every PR on these now? They hit mail.tm + a live tenant, and these are the same tests that were iGnOrE (@ignore)'d for OTP flakiness. I feel like unrelated PRs will start failing validation whenever mail.tm has a hiccup or the tenant throttles OTPs. Could these run on a nightly/scheduled pipeline instead of the per-PR gate?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point — a mail.tm hiccup shouldn't fail a PR that never touched Native Auth.

I went with an escape hatch rather than nightly: a skip-native-auth-e2e-tests label drops the E2E task (same thing iOS does in pr-validation.yml), plus a 30 min timeout. The label check fail-safes to skipping, so if the labels API is down it can't block anything either.

Keeping them on the gate is what makes a regression attributable to the PR that caused it. The label gives us the unblock without giving up that attribution.

Let me know if you think that's a reasonable solution

javaHomeSelection: $(BuildParameters.javaHomeSelection)
jdkVersion: 1.17
publishJUnitResults: true
testResultsFiles: '**/build/test-results/testLocalDebugUnitTest/TEST-*.xml'
testRunTitle: Native Auth E2E Tests (PR Branch)
env:
EMAIL_PROVIDER_PASSWORD: $(EMAIL_PROVIDER_PASSWORD)
Comment thread
djanardhan98 marked this conversation as resolved.
- publish: $(Build.SourcesDirectory)/msal/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml
artifact: jacocoReport
displayName: 'Publish JaCoCo Report Artifact (PR Branch)'
Expand Down
2 changes: 1 addition & 1 deletion common
Submodule common updated 16 files
+1 −0 changelog.txt
+10 −0 common4j/src/main/com/microsoft/identity/common/java/AuthenticationConstants.java
+84 −0 common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/BrokerIpcTelemetry.kt
+155 −0 common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventCollector.kt
+61 −0 common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/EventTag.kt
+42 −0 common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/ExecutionEvent.kt
+35 −0 common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/IBrokerIpcTelemetryProvider.kt
+49 −0 common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/PerformanceRecord.kt
+47 −0 common4j/src/main/com/microsoft/identity/common/java/broker/telemetry/TelemetryHelper.kt
+5 −0 common4j/src/main/com/microsoft/identity/common/java/commands/parameters/CommandParameters.java
+21 −1 common4j/src/main/com/microsoft/identity/common/java/exception/BaseException.java
+4 −1 .../main/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectory.java
+22 −1 common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java
+252 −0 common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/BrokerIpcTelemetrySerializationTest.kt
+236 −0 common4j/src/test/com/microsoft/identity/common/java/broker/telemetry/EventCollectorTest.kt
+36 −0 ...est/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectoryTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ import com.google.gson.reflect.TypeToken
import com.microsoft.identity.client.PublicClientApplication
import com.microsoft.identity.client.e2e.shadows.ShadowAndroidSdkStorageEncryptionManager
import com.microsoft.identity.client.e2e.tests.IPublicClientApplicationTest
import com.microsoft.identity.client.e2e.utils.NativeAuthEmailOTPErrorClassifier
import com.microsoft.identity.client.exception.MsalException
import com.microsoft.identity.common.internal.controllers.CommandDispatcherHelper
import com.microsoft.identity.common.java.nativeauth.BuildValues
import com.microsoft.identity.internal.testutils.TestUtils
import com.microsoft.identity.internal.testutils.nativeauth.ConfigType
import com.microsoft.identity.internal.testutils.nativeauth.api.TemporaryEmailService
import com.microsoft.identity.internal.testutils.nativeauth.api.models.NativeAuthTestConfig
import com.microsoft.identity.labapi.utilities.BuildConfig
import com.microsoft.identity.labapi.utilities.authentication.LabApiAuthenticationClient
Expand Down Expand Up @@ -69,13 +71,31 @@ abstract class NativeAuthPublicClientApplicationAbstractTest : IPublicClientAppl
const val INVALID_PASSWORD = "password"
const val INCORRECT_CODE = "00000000"

/**
* Retry budget for OTP-throttled (AADSTS701014) auth flows.
*
* Each retry re-runs the whole flow (new temporary inbox + signup + OTP request), so
* attempts are expensive in wall-clock time on top of the backoff itself. With 3 retries
* and the delay capped at [MAX_RETRY_DELAY_MILLIS], a persistently throttled test sleeps at
* most 5s + 10s + 20s = 35s across 4 attempts.
*
* iOS keeps 5 attempts only because XCTest's plan-level `maximumTestRepetitions` /
* `retryOnFailure` costs no extra wall-clock on success, and because its throttle path
* raises `XCTSkip` (via `skipIfEmailOTPThrottled`) instead of retrying into the throttle.
* This hand-rolled loop has neither property, hence the tighter budget.
*/
const val MAX_THROTTLE_RETRIES = 3
private const val RETRY_BASE_DELAY_MILLIS = 5_000L
private const val MAX_RETRY_DELAY_MILLIS = 20_000L

private val labApiAuthenticationClient: LabApiAuthenticationClient =
LabApiAuthenticationClient(BuildConfig.LAB_CLIENT_SECRET)
val labClient: LabClient = LabClient(labApiAuthenticationClient)
}

private lateinit var context: Context
private lateinit var activity: Activity
protected val tempEmailApi = TemporaryEmailService()

// Remove default Coroutine test timeout of 10 seconds.
private val testDispatcher = StandardTestDispatcher()
Expand Down Expand Up @@ -150,29 +170,38 @@ abstract class NativeAuthPublicClientApplicationAbstractTest : IPublicClientAppl
}

fun <T> retryOperation(
maxRetries: Int = 5,
maxRetries: Int = MAX_THROTTLE_RETRIES,
authFlow: () -> T
) {
var retryCount = 0
var shouldRetry = true

while (shouldRetry) {
while (true) {
try {
authFlow()
shouldRetry = false // authFlow() has succeeded, so we don't need to retry.
} catch (e: Exception) {
//1secmail occasionally has a delay for emails to arrive / return from the API, or throws an internal server error, which causes tests to fail
//In this case, retry the test
if (retryCount >= maxRetries) {
Assert.fail(e.message)
shouldRetry = false
} else {
retryCount++
return
} catch (e: AssertionError) {
// Classification (typed errorCodes first, assertion message as unconditional
// fallback) lives in the classifier so it is unit-testable without standing up
// this Robolectric-backed abstract test.
if (!NativeAuthEmailOTPErrorClassifier.isThrottleError(e)) {
throw e
}
retryOrFail(e, retryCount++, maxRetries)
}
}
}

private fun retryOrFail(error: Throwable, retryCount: Int, maxRetries: Int) {
if (retryCount >= maxRetries) {
throw AssertionError(error.message).apply { initCause(error) }
}

// Avoid repeatedly requesting OTPs while the Native Auth test tenant is throttling them.
Thread.sleep(
minOf(RETRY_BASE_DELAY_MILLIS * (1L shl retryCount), MAX_RETRY_DELAY_MILLIS)
)
}

private fun readConfigFile(filePath: String): String {
val sb = StringBuilder()
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft Corporation.
// All rights reserved.
//
// This code is licensed under the MIT License.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files(the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package com.microsoft.identity.client.e2e.tests.network.nativeauth

import org.junit.Assert.assertEquals
import org.junit.Assert.assertSame
import org.junit.Assert.assertThrows
import org.junit.Test

class NativeAuthPublicClientApplicationAbstractTestTest :
NativeAuthPublicClientApplicationAbstractTest() {

@Test
fun retryOperationDoesNotRetryNonThrottleExceptions() {
val expected = IllegalStateException("Non-throttle failure")
var attempts = 0

val actual = assertThrows(IllegalStateException::class.java) {
retryOperation(maxRetries = 1) {
attempts++
throw expected
Comment on lines +38 to +41

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IllegalStateException isn't an AssertionError, so it never reaches the catch (e: AssertionError) in retryOperation. it just falls straight through here, so this is only really checking that a stray RuntimeException isn't swallowed, not the throttle gating.

the branch I'd want pinned is a non-701014 AssertionError getting rethrown with no retry (the isThrottleError false -> throw e path). can we add that case? attempts should still be 1 and the original error should come back out.

}
}

assertSame(expected, actual)
assertEquals(1, attempts)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ package com.microsoft.identity.client.e2e.tests.network.nativeauth

import com.microsoft.identity.client.e2e.utils.assertResult
import com.microsoft.identity.internal.testutils.nativeauth.ConfigType
import com.microsoft.identity.internal.testutils.nativeauth.api.TemporaryEmailService
import com.microsoft.identity.internal.testutils.nativeauth.api.models.NativeAuthTestConfig
import com.microsoft.identity.nativeauth.INativeAuthPublicClientApplication
import com.microsoft.identity.nativeauth.parameters.NativeAuthResetPasswordParameters
Expand All @@ -42,20 +41,18 @@ import org.junit.Test

class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() {

private val tempEmailApi = TemporaryEmailService()

lateinit var application: INativeAuthPublicClientApplication
lateinit var config: NativeAuthTestConfig.Config

private val defaultConfigType = ConfigType.SSPR
private val defaultChallengeTypes = listOf("password", "oob")
private val defaultCapabilities = listOf("mfa_required", "registration_required")
private val invalidPasswordTooShort = "a"

/**
* Verify email with email OTP first and then reset password.
* (hero scenario 8 & 17, use case 3.1.1)
*/
@Ignore("Retrieving OTP code failure")
@Test
fun testSSPRSuccess() {
config = getConfig(defaultConfigType)
Expand All @@ -67,6 +64,7 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() {
runBlocking {
val user = config.email
val param = NativeAuthResetPasswordParameters(username = user)
tempEmailApi.markCheckpoint(user)
result = application.resetPassword(param)
assertResult<ResetPasswordStartResult.CodeRequired>(result)

Expand All @@ -85,7 +83,6 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() {
* New password being set doesn’t meet password complexity requirements set on portal
* (use case 3.1.3)
*/
@Ignore("Retrieving OTP code failure.")
@Test
fun testErrorInvalidPasswordFormat() {
config = getConfig(defaultConfigType)
Expand All @@ -97,17 +94,22 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() {
runBlocking {
val user = config.email
val param = NativeAuthResetPasswordParameters(username = user)
tempEmailApi.markCheckpoint(user)
result = application.resetPassword(param)
assertResult<ResetPasswordStartResult.CodeRequired>(result)

val otp = tempEmailApi.retrieveCodeFromInbox(user)
val submitCodeResult = (result as ResetPasswordStartResult.CodeRequired).nextState.submitCode(otp)
assertResult<ResetPasswordSubmitCodeResult.PasswordRequired>(submitCodeResult)

val password = INVALID_PASSWORD
val password = invalidPasswordTooShort
val submitPasswordResult = (submitCodeResult as ResetPasswordSubmitCodeResult.PasswordRequired).nextState.submitPassword(password.toCharArray())
Assert.assertTrue(submitPasswordResult is ResetPasswordSubmitPasswordError)
Assert.assertTrue((submitPasswordResult as ResetPasswordSubmitPasswordError).isInvalidPassword())
val error = submitPasswordResult as ResetPasswordSubmitPasswordError
Assert.assertTrue(
"Expected invalid password error, but received errorType=${error.errorType}, error=${error.error}, subError=${error.subError}",
error.isInvalidPassword()
)
}
}
}
Expand All @@ -116,7 +118,6 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() {
* Resend Code.
* (use case 3.1.4)
*/
@Ignore("Retrieving OTP code failure.")
@Test
fun testResendCode() {
config = getConfig(defaultConfigType)
Expand All @@ -128,11 +129,13 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() {
runBlocking {
val user = config.email
val param = NativeAuthResetPasswordParameters(username = user)
tempEmailApi.markCheckpoint(user)
result = application.resetPassword(param)
assertResult<ResetPasswordStartResult.CodeRequired>(result)

val otp1 = tempEmailApi.retrieveCodeFromInbox(user)
val codeRequiredState = (result as ResetPasswordStartResult.CodeRequired).nextState
tempEmailApi.markCheckpoint(user)
val resendCodeResult = codeRequiredState.resendCode()
assertResult<ResetPasswordResendCodeResult.Success>(resendCodeResult)

Expand All @@ -159,7 +162,7 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() {
application = setupPCA(config, defaultChallengeTypes, defaultCapabilities)

runBlocking {
val username = tempEmailApi.generateRandomEmailAddressLocally()
val username = tempEmailApi.generateRandomUnregisteredEmailAddress()
val param = NativeAuthResetPasswordParameters(username = username)
val result = application.resetPassword(param)
Assert.assertTrue(result is ResetPasswordError)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ package com.microsoft.identity.client.e2e.tests.network.nativeauth

import com.microsoft.identity.client.e2e.utils.assertResult
import com.microsoft.identity.internal.testutils.nativeauth.ConfigType
import com.microsoft.identity.internal.testutils.nativeauth.api.TemporaryEmailService
import com.microsoft.identity.internal.testutils.nativeauth.api.models.NativeAuthTestConfig
import com.microsoft.identity.nativeauth.INativeAuthPublicClientApplication
import com.microsoft.identity.nativeauth.parameters.NativeAuthSignInParameters
Expand All @@ -40,8 +39,6 @@ import org.junit.Test

class SignInEmailOTPTest : NativeAuthPublicClientApplicationAbstractTest() {

private val tempEmailApi = TemporaryEmailService()

lateinit var application: INativeAuthPublicClientApplication
lateinit var config: NativeAuthTestConfig.Config

Expand All @@ -63,6 +60,7 @@ class SignInEmailOTPTest : NativeAuthPublicClientApplicationAbstractTest() {
runBlocking {
val user = config.email
val param = NativeAuthSignInParameters(username = user)
tempEmailApi.markCheckpoint(user)
val signInResult = application.signIn(param)
assertResult<SignInResult.CodeRequired>(signInResult)
val otp = tempEmailApi.retrieveCodeFromInbox(user)
Expand All @@ -83,7 +81,7 @@ class SignInEmailOTPTest : NativeAuthPublicClientApplicationAbstractTest() {

retryOperation {
runBlocking {
val username = tempEmailApi.generateRandomEmailAddressLocally()
val username = tempEmailApi.generateRandomUnregisteredEmailAddress()
val param = NativeAuthSignInParameters(username = username)
val signInResult = application.signIn(param)
Assert.assertTrue(signInResult is SignInError)
Expand Down Expand Up @@ -146,11 +144,13 @@ class SignInEmailOTPTest : NativeAuthPublicClientApplicationAbstractTest() {
runBlocking {
val user = config.email
val param = NativeAuthSignInParameters(username = user)
tempEmailApi.markCheckpoint(user)
val signInResult = application.signIn(param)
assertResult<SignInResult.CodeRequired>(signInResult)

val otp1 = tempEmailApi.retrieveCodeFromInbox(user)
val codeRequiredState = (signInResult as SignInResult.CodeRequired).nextState
tempEmailApi.markCheckpoint(user)
val resendCodeResult = codeRequiredState.resendCode()
assertResult<SignInResendCodeResult.Success>(resendCodeResult)

Expand Down
Loading
Loading