Skip to content

Commit 394bcd6

Browse files
authored
[iOS SDK] Improved e2e tests stability (#2337)
* Improved code retrieving to make sure code is from correct email Increased retries to 5 Disable retry from command for e2e tests * Removed spacing * Added new lines in the script * Indentation * no message * Change to single quote * Trying non-boolean value * Changed parameter to retry_tests as - is not allowed * Temporary disable condition * Using different emails for ios and macos * Added back the condition * PR comments * PR Comments * Using numberOfRetries instead of hardcoded * PR comments * Remove condition * PR comments * PR comments * Added validation back * Added CIAM team as codeowners to azure_pipelines * Changed CodeOwners * Forcing refresh * Revert spacing
1 parent 78b9972 commit 394bcd6

File tree

9 files changed

+89
-52
lines changed

9 files changed

+89
-52
lines changed

CODEOWNERS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
# Unless a later match takes precedence, these users will be requested
33
# for review whenever someone opens a pull request.
44
* @AzureAD/AppleIdentityTeam
5-
# @AzureAD/AppleIdentityTeam and @AzureAD/MSAL-ObjC-CIAM will be the co-owners of MSAL.project and CHANGELOG.md files
5+
# @AzureAD/AppleIdentityTeam and @AzureAD/MSAL-ObjC-CIAM will be the co-owners of MSAL.project, CHANGELOG.md and all files under azure_pipelines
66
/MSAL/MSAL.xcodeproj/project.pbxproj @AzureAD/AppleIdentityTeam @AzureAD/MSAL-ObjC-CIAM
77
CHANGELOG.md @AzureAD/AppleIdentityTeam @AzureAD/MSAL-ObjC-CIAM
8+
/azure_pipelines/ @AzureAD/AppleIdentityTeam @AzureAD/MSAL-ObjC-CIAM
89
# @AzureAD/MSAL-ObjC-CIAM owns any files in the */native_auth
910
# directories, subdirectories and other files related to native auth.
1011
/MSAL/module.modulemap @AzureAD/MSAL-ObjC-CIAM

MSAL/test/integration/native_auth/end_to_end/MSALNativeAuthEndToEndBaseTestCase.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ class MSALNativeAuthEndToEndBaseTestCase: XCTestCase {
3737
static let signInEmailPasswordMFAUsernameKey = "sign_in_email_password_mfa_username"
3838
static let signInEmailPasswordMFANoDefaultAuthMethodUsernameKey = "sign_in_email_password_mfa_no_default_username"
3939
static let signInEmailCodeUsernameKey = "sign_in_email_code_username"
40+
#if !os(macOS)
4041
static let resetPasswordUsernameKey = "reset_password_username"
42+
#else
43+
static let resetPasswordUsernameKey = "reset_password_username_macos"
44+
#endif
4145
}
4246

4347
let correlationId = UUID()

MSAL/test/integration/native_auth/end_to_end/otp_code_retriever/MSALNativeAuthEmailCodeRetriever.swift

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ class MSALNativeAuthEmailCodeRetriever: XCTestCase {
2929
private let baseURLString = "https://www.1secmail.com/api/v1/?action="
3030
private let secondsToWait = 4.0
3131
private let numberOfRetry = 3
32+
private let dateFormatter: DateFormatter = {
33+
let dateFormatter = DateFormatter()
34+
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
35+
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
36+
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) //We ignore the timezone
37+
return dateFormatter
38+
}()
39+
private let maximumSecondsSinceEmailReceive = 5.0
3240

3341
func retrieveEmailOTPCode(email: String) async -> String? {
3442
let comps = email.components(separatedBy: "@")
@@ -76,15 +84,25 @@ class MSALNativeAuthEmailCodeRetriever: XCTestCase {
7684
}
7785
if dataDictionary.count > 0 {
7886
dataDictionary.sort(by: {($0["id"] as? Int ?? 0) > ($1["id"] as? Int ?? 0)})
79-
return dataDictionary.first?["id"] as? Int
80-
} else {
81-
// log only for the final retry
82-
if (retryCounter == 1) {
83-
print("Unexpected behaviour: no email received for the following local: \(local)")
87+
if let emailDateString = dataDictionary.first?["date"] as? String,
88+
let emailDate = dateFormatter.date(from: emailDateString) {
89+
let currentDate = Date()
90+
// Email should be newer than 5 seconds otherwise it could be from previous test
91+
// This retry will help with the delay in receiving the emails
92+
if currentDate.timeIntervalSince1970 - emailDate.timeIntervalSince1970 < maximumSecondsSinceEmailReceive {
93+
print ("Email is for current test, last receive date: \(emailDate) current date: \(currentDate)")
94+
return dataDictionary.first?["id"] as? Int
95+
} else {
96+
print ("Email is from previous tests, last receive date: \(emailDate) current date: \(currentDate)")
97+
}
8498
}
85-
// no emails found, retry
86-
return await retrieveLastMessage(local: local, domain: domain, retryCounter: retryCounter - 1)
8799
}
100+
// log only for the final retry
101+
if (retryCounter == 1) {
102+
print("Unexpected behaviour: no email received for the following local: \(local)")
103+
}
104+
// no emails found, retry
105+
return await retrieveLastMessage(local: local, domain: domain, retryCounter: retryCounter - 1)
88106
} catch {
89107
print(error)
90108
return nil

MSAL/test/integration/native_auth/end_to_end/reset_password/MSALNativeAuthResetPasswordEndToEndTests.swift

Lines changed: 34 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@
2424

2525
import Foundation
2626
import XCTest
27+
import MSAL
2728

2829
final class MSALNativeAuthResetPasswordEndToEndTests: MSALNativeAuthEndToEndBaseTestCase {
2930
// Hero Scenario 3.1.1. SSPR – without automatic sign in
31+
private let codeRetryCount = 3
32+
3033
func test_resetPassword_withoutAutomaticSignIn_succeeds() async throws {
3134
guard let sut = initialisePublicClientApplication(),
3235
let username = retrieveUsernameForResetPassword()
@@ -52,31 +55,16 @@ final class MSALNativeAuthResetPasswordEndToEndTests: MSALNativeAuthEndToEndBase
5255
XCTAssertNotNil(resetPasswordStartDelegate.codeLength)
5356

5457
// Now submit the code...
55-
56-
let passwordRequiredExp = expectation(description: "password required")
57-
let resetPasswordVerifyDelegate = ResetPasswordVerifyCodeDelegateSpy(expectation: passwordRequiredExp)
58-
59-
guard let code = await retrieveCodeFor(email: username) else {
60-
XCTFail("OTP code not retrieved from email")
61-
return
62-
}
63-
64-
resetPasswordStartDelegate.newState?.submitCode(code: code, delegate: resetPasswordVerifyDelegate)
65-
66-
await fulfillment(of: [passwordRequiredExp])
67-
XCTAssertTrue(resetPasswordVerifyDelegate.onPasswordRequiredCalled)
68-
69-
guard resetPasswordVerifyDelegate.onPasswordRequiredCalled else {
70-
XCTFail("onPasswordRequired not called")
71-
return
72-
}
58+
let newPasswordRequiredState = await retrieveAndSubmitCode(resetPasswordStartDelegate: resetPasswordStartDelegate,
59+
username: username,
60+
retries: codeRetryCount)
7361

7462
// Now submit the password...
7563
let resetPasswordCompletedExp = expectation(description: "reset password completed")
7664
let resetPasswordRequiredDelegate = ResetPasswordRequiredDelegateSpy(expectation: resetPasswordCompletedExp)
7765

7866
let uniquePassword = generateRandomPassword()
79-
resetPasswordVerifyDelegate.newPasswordRequiredState?.submitPassword(password: uniquePassword, delegate: resetPasswordRequiredDelegate)
67+
newPasswordRequiredState?.submitPassword(password: uniquePassword, delegate: resetPasswordRequiredDelegate)
8068

8169
await fulfillment(of: [resetPasswordCompletedExp])
8270
XCTAssertTrue(resetPasswordRequiredDelegate.onResetPasswordCompletedCalled)
@@ -108,31 +96,16 @@ final class MSALNativeAuthResetPasswordEndToEndTests: MSALNativeAuthEndToEndBase
10896
XCTAssertNotNil(resetPasswordStartDelegate.codeLength)
10997

11098
// Now submit the code...
111-
112-
let passwordRequiredExp = expectation(description: "password required")
113-
let resetPasswordVerifyDelegate = ResetPasswordVerifyCodeDelegateSpy(expectation: passwordRequiredExp)
114-
115-
guard let code = await retrieveCodeFor(email: username) else {
116-
XCTFail("OTP code not retrieved from email")
117-
return
118-
}
119-
120-
resetPasswordStartDelegate.newState?.submitCode(code: code, delegate: resetPasswordVerifyDelegate)
121-
122-
await fulfillment(of: [passwordRequiredExp])
123-
XCTAssertTrue(resetPasswordVerifyDelegate.onPasswordRequiredCalled)
124-
125-
guard resetPasswordVerifyDelegate.onPasswordRequiredCalled else {
126-
XCTFail("onPasswordRequired not called")
127-
return
128-
}
99+
let newPasswordRequiredState = await retrieveAndSubmitCode(resetPasswordStartDelegate: resetPasswordStartDelegate,
100+
username: username,
101+
retries: codeRetryCount)
129102

130103
// Now submit the password...
131104
let resetPasswordCompletedExp = expectation(description: "reset password completed")
132105
let resetPasswordRequiredDelegate = ResetPasswordRequiredDelegateSpy(expectation: resetPasswordCompletedExp)
133106

134107
let uniquePassword = generateRandomPassword()
135-
resetPasswordVerifyDelegate.newPasswordRequiredState?.submitPassword(password: uniquePassword, delegate: resetPasswordRequiredDelegate)
108+
newPasswordRequiredState?.submitPassword(password: uniquePassword, delegate: resetPasswordRequiredDelegate)
136109

137110
await fulfillment(of: [resetPasswordCompletedExp])
138111
XCTAssertTrue(resetPasswordRequiredDelegate.onResetPasswordCompletedCalled)
@@ -155,4 +128,27 @@ final class MSALNativeAuthResetPasswordEndToEndTests: MSALNativeAuthEndToEndBase
155128
XCTAssertNotNil(signInAfterResetPasswordDelegate.result?.idToken)
156129
XCTAssertNotNil(signInAfterResetPasswordDelegate.result?.account.accountClaims)
157130
}
131+
132+
// This method tries to fetch a code from 1secmail API and submit it
133+
private func retrieveAndSubmitCode(resetPasswordStartDelegate: ResetPasswordStartDelegateSpy, username: String, retries: Int) async -> ResetPasswordRequiredState? {
134+
let passwordRequiredExp = expectation(description: "password required")
135+
let resetPasswordVerifyDelegate = ResetPasswordVerifyCodeDelegateSpy(expectation: passwordRequiredExp)
136+
137+
guard let code = await retrieveCodeFor(email: username) else {
138+
XCTFail("OTP code not retrieved from email")
139+
return nil
140+
}
141+
142+
resetPasswordStartDelegate.newState?.submitCode(code: code, delegate: resetPasswordVerifyDelegate)
143+
144+
await fulfillment(of: [passwordRequiredExp])
145+
if resetPasswordVerifyDelegate.onResetPasswordVerifyCodeErrorCalled && resetPasswordVerifyDelegate.error?.isInvalidCode == true && retries > 0 {
146+
return await retrieveAndSubmitCode(resetPasswordStartDelegate: resetPasswordStartDelegate, username: username, retries: retries - 1)
147+
}
148+
guard resetPasswordVerifyDelegate.onPasswordRequiredCalled else {
149+
XCTFail("onPasswordRequired not called")
150+
return nil
151+
}
152+
return resetPasswordVerifyDelegate.newPasswordRequiredState
153+
}
158154
}

MSAL/test/testplan/MSAL Mac Native Auth E2E Tests.xctestplan

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
}
1010
],
1111
"defaultOptions" : {
12+
"maximumTestRepetitions" : 5,
1213
"testRepetitionMode" : "retryOnFailure"
1314
},
1415
"testTargets" : [

MSAL/test/testplan/MSAL iOS Native Auth E2E Tests.xctestplan

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
}
1010
],
1111
"defaultOptions" : {
12+
"maximumTestRepetitions" : 5,
1213
"testRepetitionMode" : "retryOnFailure"
1314
},
1415
"testTargets" : [

azure_pipelines/automation.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ jobs:
9393
full_path: 'build/Build/Products/MSAL iOS Native Auth E2E Tests_MSAL iOS Native Auth E2E Tests_iphonesimulator17.5-x86_64.xctestrun'
9494
destination: 'platform=iOS Simulator,name=iPhone 15,OS=17.5'
9595
sdk: 'iphonesimulator'
96+
retry_tests: false
9697

9798
- job: e2e_test_native_auth_mac
9899
displayName: 'Run MSAL E2E tests for macOS native auth'
@@ -108,6 +109,7 @@ jobs:
108109
full_path: 'build/Build/Products/MSAL Mac Native Auth E2E Tests_MSAL Mac Native Auth E2E Tests_macosx14.5-x86_64.xctestrun'
109110
destination: 'platform=macOS'
110111
sdk: 'macosx'
112+
retry_tests: false
111113

112114
- job: cocoapods_lib_lint
113115
displayName: Run Cocoapods lib lint

azure_pipelines/pr-validation.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ jobs:
168168
full_path: 'build/Build/Products/MSAL iOS Native Auth E2E Tests_MSAL iOS Native Auth E2E Tests_iphonesimulator17.5-x86_64.xctestrun'
169169
destination: 'platform=iOS Simulator,name=iPhone 15,OS=17.5'
170170
sdk: 'iphonesimulator'
171+
retry_tests: false
171172

172173
- job: e2e_test_native_auth_mac
173174
displayName: 'Run MSAL E2E tests for macOS native auth'
@@ -187,6 +188,7 @@ jobs:
187188
full_path: 'build/Build/Products/MSAL Mac Native Auth E2E Tests_MSAL Mac Native Auth E2E Tests_macosx14.5-x86_64.xctestrun'
188189
destination: 'platform=macOS'
189190
sdk: 'macosx'
191+
retry_tests: false
190192

191193
- job: 'Validate_SPM_Integration'
192194
displayName: Validate SPM Integration

azure_pipelines/templates/tests-with-conf-file.yml

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ parameters:
33
full_path: 'build/Build/Products/MSAL Test Automation (iOS)_iphonesimulator17.5-x86_64.xctestrun'
44
destination: 'platform=iOS Simulator,name=iPhone 15,OS=17.5'
55
sdk: 'iphonesimulator'
6+
retry_tests: true
67

78
steps:
89
- checkout: self
@@ -58,12 +59,23 @@ steps:
5859
targetType: 'inline'
5960
script: |
6061
ls build/Build/Products/
61-
xcodebuild test-without-building \
62-
-xctestrun '${{ parameters.full_path }}' \
63-
-destination '${{ parameters.destination }}' \
64-
-retry-tests-on-failure \
65-
-parallel-testing-enabled NO \
66-
-resultBundlePath '$(Agent.BuildDirectory)/s/test_output/report.xcresult'
62+
#Use retry-tests-on-failure only if tests don't already use "Maximum Test Repetitions"
63+
if ['${{ parameters.retry_tests }}' == true]; then
64+
xcodebuild test-without-building \
65+
-xctestrun '${{ parameters.full_path }}' \
66+
-destination '${{ parameters.destination }}' \
67+
-retry-tests-on-failure \
68+
-parallel-testing-enabled NO \
69+
-resultBundlePath '$(Agent.BuildDirectory)/s/test_output/report.xcresult'
70+
71+
else
72+
xcodebuild test-without-building \
73+
-xctestrun '${{ parameters.full_path }}' \
74+
-destination '${{ parameters.destination }}' \
75+
-parallel-testing-enabled NO \
76+
-resultBundlePath '$(Agent.BuildDirectory)/s/test_output/report.xcresult'
77+
78+
fi
6779
6880
# https://learn.microsoft.com/en-us/azure/devops/pipelines/artifacts/pipeline-artifacts?view=azure-devops&tabs=yaml#q-can-i-delete-pipeline-artifacts-when-re-running-failed-jobs
6981
- task: PublishPipelineArtifact@1

0 commit comments

Comments
 (0)