Skip to content

Commit

Permalink
Merge pull request #2493 from AzureAD/yuki/e2e-batch-3
Browse files Browse the repository at this point in the history
Native Auth E2E Test Batch 3
  • Loading branch information
Yuki-YuXin authored Feb 21, 2025
2 parents 1bfd768 + 0fb20f0 commit b216b25
Show file tree
Hide file tree
Showing 6 changed files with 579 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,38 @@ final class MSALNativeAuthUserAccountEndToEndTests: MSALNativeAuthEndToEndPasswo
XCTAssertTrue(credentialsDelegateSpy.onAccessTokenRetrieveErrorCalled)
XCTAssertTrue(credentialsDelegateSpy.error!.errorDescription!.contains("Send an interactive authorization request for this user and resource."))
}

// Sign in with username and password with extra scopes to get access token and validate the scopes
func test_signInWithExtraScopes() async throws {
#if os(macOS)
throw XCTSkip("Bundle id for macOS is not added to the client id, test is not needed on both iOS and macOS")
#endif
guard let sut = initialisePublicClientApplication(), let username = retrieveUsernameForSignInUsernameAndPassword(), let password = await retrievePasswordForSignInUsername() else {
XCTFail("Missing information")
return
}

let signInExpectation = expectation(description: "signing in")
let signInDelegateSpy = SignInPasswordStartDelegateSpy(expectation: signInExpectation)

sut.signIn(username: username, password: password, scopes: ["User.Read"], correlationId: correlationId, delegate: signInDelegateSpy)

await fulfillment(of: [signInExpectation])

XCTAssertTrue(signInDelegateSpy.onSignInCompletedCalled)
XCTAssertNotNil(signInDelegateSpy.result?.idToken)
XCTAssertEqual(signInDelegateSpy.result?.account.username, username)

let getAccessTokenExpectation = expectation(description: "getting access token")
let credentialsDelegateSpy = CredentialsDelegateSpy(expectation: getAccessTokenExpectation)

signInDelegateSpy.result?.getAccessToken(scopes: ["User.Read"], delegate: credentialsDelegateSpy)

await fulfillment(of: [getAccessTokenExpectation])

XCTAssertTrue(credentialsDelegateSpy.onAccessTokenRetrieveCompletedCalled)
XCTAssertNotNil(credentialsDelegateSpy.result?.accessToken)
XCTAssertNotNil(credentialsDelegateSpy.result?.scopes)
XCTAssertTrue(credentialsDelegateSpy.result!.scopes.contains("User.Read"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ final class MSALNativeAuthResetPasswordEndToEndTests: MSALNativeAuthEndToEndBase
private let codeRetryCount = 3

func test_resetPassword_withoutAutomaticSignIn_succeeds() async throws {
throw XCTSkip("1secmail service is down. Ignoring test for now.")
throw XCTSkip("Retrieving OTP failure")

guard let sut = initialisePublicClientApplication(),
let username = retrieveUsernameForResetPassword()
Expand Down Expand Up @@ -71,10 +71,207 @@ final class MSALNativeAuthResetPasswordEndToEndTests: MSALNativeAuthEndToEndBase
await fulfillment(of: [resetPasswordCompletedExp])
XCTAssertTrue(resetPasswordRequiredDelegate.onResetPasswordCompletedCalled)
}

// User Case 3.1.3. SSPR – New password being set doesn’t meet password complexity requirements set on portal
func test_resetPassword_passwordComplexity_error() async throws {
throw XCTSkip("Retrieving OTP failure")

guard let sut = initialisePublicClientApplication(),
let username = retrieveUsernameForResetPassword()
else {
XCTFail("Missing information")
return
}
let codeRequiredExp = expectation(description: "code required")
let resetPasswordStartDelegate = ResetPasswordStartDelegateSpy(expectation: codeRequiredExp)

sut.resetPassword(username: username, delegate: resetPasswordStartDelegate)

await fulfillment(of: [codeRequiredExp])
XCTAssertTrue(resetPasswordStartDelegate.onResetPasswordCodeRequiredCalled)

guard resetPasswordStartDelegate.onResetPasswordCodeRequiredCalled else {
XCTFail("onResetPasswordCodeRequired not called")
return
}

XCTAssertEqual(resetPasswordStartDelegate.channelTargetType?.isEmailType, true)
XCTAssertFalse(resetPasswordStartDelegate.sentTo?.isEmpty ?? true)
XCTAssertNotNil(resetPasswordStartDelegate.codeLength)

// Now submit the code...
let newPasswordRequiredState = await retrieveAndSubmitCode(resetPasswordStartDelegate: resetPasswordStartDelegate,
username: username,
retries: codeRetryCount)

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

let uniquePassword = "INVALID_PASSWORD"
newPasswordRequiredState?.submitPassword(password: uniquePassword, delegate: resetPasswordRequiredDelegate)

await fulfillment(of: [resetPasswordCompletedExp])
XCTAssertTrue(resetPasswordRequiredDelegate.onResetPasswordRequiredErrorCalled)
XCTAssertEqual(resetPasswordRequiredDelegate.error?.isInvalidPassword, true)
}

// User Case 3.1.4 SSPR - Resend email OTP
func test_resetPassword_resendCode_succeeds() async throws {
throw XCTSkip("Retrieving OTP failure")

guard let sut = initialisePublicClientApplication(),
let username = retrieveUsernameForResetPassword()
else {
XCTFail("Missing information")
return
}
let codeRequiredExp = expectation(description: "code required")
let resetPasswordStartDelegate = ResetPasswordStartDelegateSpy(expectation: codeRequiredExp)

sut.resetPassword(username: username, delegate: resetPasswordStartDelegate)

await fulfillment(of: [codeRequiredExp])
XCTAssertTrue(resetPasswordStartDelegate.onResetPasswordCodeRequiredCalled)

guard resetPasswordStartDelegate.onResetPasswordCodeRequiredCalled else {
XCTFail("onResetPasswordCodeRequired not called")
return
}

// Now get code1...
guard let code1 = await retrieveCodeFor(email: username) else {
XCTFail("OTP code could not be retrieved")
return
}

// Resend code
let resendCodeRequiredExp = expectation(description: "code required again")
let resetPasswordResendCodeDelegate = ResetPasswordResendCodeDelegateSpy(expectation: resendCodeRequiredExp)

// Call resend code method
resetPasswordStartDelegate.newState?.resendCode(delegate: resetPasswordResendCodeDelegate)

await fulfillment(of: [resendCodeRequiredExp])

// Verify that resend code method was called
XCTAssertTrue(resetPasswordResendCodeDelegate.onResetPasswordResendCodeCodeRequiredCalled,
"Resend code method should have been called")

// Now get code2...
guard let code2 = await retrieveCodeFor(email: username) else {
XCTFail("OTP code could not be retrieved")
return
}

// Verify that the codes are different
XCTAssertNotEqual(code1, code2, "Resent code should be different from the original code")

// Now submit the code...
let newPasswordRequiredState = await retrieveAndSubmitCode(resetPasswordStartDelegate: resetPasswordStartDelegate,
username: username,
retries: codeRetryCount)

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

let uniquePassword = generateRandomPassword()
newPasswordRequiredState?.submitPassword(password: uniquePassword, delegate: resetPasswordRequiredDelegate)

await fulfillment(of: [resetPasswordCompletedExp])
XCTAssertTrue(resetPasswordRequiredDelegate.onResetPasswordCompletedCalled)
}

// User Case 3.1.5 SSPR - Email is not found in records
func test_resetPassword_emailNotFound_error() async throws {
guard let sut = initialisePublicClientApplication() else {
XCTFail("Missing information")
return
}

let resetPasswordFailureExp = expectation(description: "reset password user not found")
let resetPasswordStartDelegate = ResetPasswordStartDelegateSpy(expectation: resetPasswordFailureExp)

let unknownUsername = UUID().uuidString + "@contoso.com"

sut.resetPassword(username: unknownUsername, delegate: resetPasswordStartDelegate)

await fulfillment(of: [resetPasswordFailureExp])

// Verify error condition
XCTAssertTrue(resetPasswordStartDelegate.onResetPasswordErrorCalled)
XCTAssertEqual(resetPasswordStartDelegate.error?.isUserNotFound, true)
}

// User Case 3.1.6 SSPR - When SSPR requires a challenge type not supported by the client, redirect to web-fallback
func test_resetPassword_webfallback_error() async throws {
guard let sut = initialisePublicClientApplication(challengeTypes: [.password]),
let username = retrieveUsernameForResetPassword()
else {
XCTFail("Missing information")
return
}

let resetPasswordFailureExp = expectation(description: "reset password web-fallback")
let resetPasswordStartDelegate = ResetPasswordStartDelegateSpy(expectation: resetPasswordFailureExp)

sut.resetPassword(username: username, delegate: resetPasswordStartDelegate)

await fulfillment(of: [resetPasswordFailureExp])

// Verify error condition
XCTAssertTrue(resetPasswordStartDelegate.onResetPasswordErrorCalled)
XCTAssertEqual(resetPasswordStartDelegate.error?.isBrowserRequired, true)
}

// User Case 3.1.8 SSPR – Email exists but not linked to any password
func test_resetPassword_accoutWithoutPassword_error() async throws {
guard let sut = initialisePublicClientApplication(),
let username = retrieveUsernameForSignInCode()
else {
XCTFail("Missing information")
return
}

let resetPasswordFailureExp = expectation(description: "does not support password")
let resetPasswordStartDelegate = ResetPasswordStartDelegateSpy(expectation: resetPasswordFailureExp)

sut.resetPassword(username: username, delegate: resetPasswordStartDelegate)

await fulfillment(of: [resetPasswordFailureExp])

// Verify error condition
XCTAssertTrue(resetPasswordStartDelegate.onResetPasswordErrorCalled)
XCTAssertTrue(resetPasswordStartDelegate.error?.errorDescription?.contains("The tenant or user does not support native credential recovery.") ?? false)
}

// User Case 3.1.9 - Email exists but signup method was OTP, social, etc.
func test_resetPassword_socialAccount_error() async throws {
throw XCTSkip("Skipping test as it requires a Social account, not present in MSIDLAB")

guard let sut = initialisePublicClientApplication() else {
XCTFail("Missing information")
return
}

let username = "invalid" // TODO: use social account instead

let resetPasswordFailureExp = expectation(description: "reset password user not found")
let resetPasswordStartDelegate = ResetPasswordStartDelegateSpy(expectation: resetPasswordFailureExp)

sut.resetPassword(username: username, delegate: resetPasswordStartDelegate)

await fulfillment(of: [resetPasswordFailureExp])

// Verify error condition
XCTAssertTrue(resetPasswordStartDelegate.onResetPasswordErrorCalled)
XCTAssertEqual(resetPasswordStartDelegate.error?.isInvalidUsername, true)
}

// SSPR - with automatic sign in
func test_resetPassword_withAutomaticSignIn_succeeds() async throws {
throw XCTSkip("1secmail service is down. Ignoring test for now.")
throw XCTSkip("Retrieving OTP failure")

guard let sut = initialisePublicClientApplication(),
let username = retrieveUsernameForResetPassword()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,36 @@ class ResetPasswordRequiredDelegateSpy: ResetPasswordRequiredDelegate {
}
}

class ResetPasswordResendCodeDelegateSpy: ResetPasswordResendCodeDelegate {
private let expectation: XCTestExpectation
private(set) var onResetPasswordResendCodeErrorCalled = false
private(set) var error: ResendCodeError?
private(set) var onResetPasswordResendCodeCodeRequiredCalled = false
private(set) var resetPasswordCodeRequiredState: ResetPasswordCodeRequiredState?
private(set) var sentTo: String?
private(set) var channelTargetType: MSALNativeAuthChannelType?
private(set) var codeLength: Int?

init(expectation: XCTestExpectation) {
self.expectation = expectation
}

func onResetPasswordResendCodeError(error: MSAL.ResendCodeError, newState: MSAL.ResetPasswordCodeRequiredState?) {
onResetPasswordResendCodeErrorCalled = true
self.error = error
}

func onResetPasswordResendCodeCodeRequired(newState: ResetPasswordCodeRequiredState, sentTo: String, channelTargetType: MSALNativeAuthChannelType, codeLength: Int) {
onResetPasswordResendCodeCodeRequiredCalled = true
resetPasswordCodeRequiredState = newState
self.sentTo = sentTo
self.channelTargetType = channelTargetType
self.codeLength = codeLength

expectation.fulfill()
}
}

class SignInAfterResetPasswordDelegateSpy: SignInAfterResetPasswordDelegate {
private let expectation: XCTestExpectation
private(set) var onSignInAfterResetPasswordErrorCalled = false
Expand Down
Loading

0 comments on commit b216b25

Please sign in to comment.