Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IOS Sign Up E2E Test #2422

Merged
merged 43 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2565b80
1.1.10 & 1.1.12
Yuki-YuXin Dec 11, 2024
3247d2a
2.1.11
Yuki-YuXin Dec 11, 2024
fd3a63a
1.1.13
Yuki-YuXin Dec 11, 2024
a469024
1.1.2
Yuki-YuXin Dec 11, 2024
9974b64
1.1.5
Yuki-YuXin Dec 11, 2024
578c479
2.1.10
Yuki-YuXin Dec 11, 2024
24e6838
2.1.10 move to the right place
Yuki-YuXin Dec 11, 2024
2bf3f9e
2.1.5
Yuki-YuXin Dec 11, 2024
e90dadc
2.1.6
Yuki-YuXin Dec 11, 2024
94b550b
2.1.7&2.1.8
Yuki-YuXin Dec 12, 2024
db431c4
pass 2.1.11
Yuki-YuXin Dec 12, 2024
9cd2c77
1.1.10 pass
Yuki-YuXin Dec 12, 2024
b197a17
1.1.11 pass
Yuki-YuXin Dec 12, 2024
95bfcaf
1.1.12 pass
Yuki-YuXin Dec 12, 2024
b7e826d
1.1.13 pass
Yuki-YuXin Dec 12, 2024
dc67b95
1.1.2 pass
Yuki-YuXin Dec 12, 2024
91e59f0
1.1.5 pass
Yuki-YuXin Dec 12, 2024
c2ff7f7
2.1.10 pass + use correct error message
Yuki-YuXin Dec 12, 2024
273af9b
2.1.5 pass
Yuki-YuXin Dec 12, 2024
e163297
2.1.6 pass
Yuki-YuXin Dec 12, 2024
4ab385a
2.1.7 pass
Yuki-YuXin Dec 12, 2024
08ba0c8
2.1.8 pass
Yuki-YuXin Dec 12, 2024
61569bb
remove correlation id one because it's out of scope
Yuki-YuXin Dec 12, 2024
faa59c5
Verify Custom URL Domain - Sign In
Yuki-YuXin Dec 12, 2024
886d1fe
add signInCustomDomain2InSuccess and skip
Yuki-YuXin Dec 12, 2024
6a82162
tenant_id + replicate name
Yuki-YuXin Dec 13, 2024
f4b68df
trigger pipeline
Yuki-YuXin Dec 16, 2024
d1ce495
wrong key
Yuki-YuXin Dec 16, 2024
cc3864d
Merge branch 'dev' of https://github.com/AzureAD/microsoft-authentica…
Yuki-YuXin Dec 17, 2024
14531e1
Address comments
Yuki-YuXin Dec 17, 2024
4463c60
Revert "Address comments"
Yuki-YuXin Dec 17, 2024
9b56bc2
Address comments
Yuki-YuXin Dec 17, 2024
3812849
Revert "Address comments"
Yuki-YuXin Dec 18, 2024
cefac96
Revert "Revert "Address comments""
Yuki-YuXin Dec 18, 2024
99d3b11
Use OTP instead of password to customURL
Yuki-YuXin Dec 18, 2024
7fe3bc8
Address comments
Yuki-YuXin Dec 19, 2024
1422d14
Pass the local test
Yuki-YuXin Dec 19, 2024
d239b4c
Skipping failing E2E tests for macOS due to Keychain access required …
spetrescu84 Dec 20, 2024
4f65bff
Revert "Skipping failing E2E tests for macOS due to Keychain access r…
spetrescu84 Dec 20, 2024
1c93d83
Switch PR validation to proper macOS version
spetrescu84 Dec 20, 2024
96c3908
Revert "Switch PR validation to proper macOS version"
spetrescu84 Dec 20, 2024
08ef259
Merge branch 'dev' of https://github.com/AzureAD/microsoft-authentica…
Yuki-YuXin Dec 25, 2024
6704e4c
Merge branch 'dev' of https://github.com/AzureAD/microsoft-authentica…
Yuki-YuXin Jan 3, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ class MSALNativeAuthEndToEndBaseTestCase: XCTestCase {
static let signInEmailPasswordMFAUsernameKey = "sign_in_email_password_mfa_username"

Choose a reason for hiding this comment

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

This pull request does not update CHANGELOG.md.

Please consider if this change would be noticeable to a partner or user and either update CHANGELOG.md or resolve this conversation.

static let signInEmailPasswordMFANoDefaultAuthMethodUsernameKey = "sign_in_email_password_mfa_no_default_username"
static let signInEmailCodeUsernameKey = "sign_in_email_code_username"
static let customDomainFormat = [
"https://<tenantName>.ciamlogin.com/<tenantName>.onmicrosoft.com",
"https://<tenantName>.ciamlogin.com/<tenantId>",
"https://<tenantName>.ciamlogin.com/",
]
#if !os(macOS)
static let resetPasswordUsernameKey = "reset_password_username"
#else
Expand Down Expand Up @@ -71,14 +76,46 @@ class MSALNativeAuthEndToEndBaseTestCase: XCTestCase {

func initialisePublicClientApplication(
clientIdType: ClientIdType = .password,
challengeTypes: MSALNativeAuthChallengeTypes = [.OOB, .password]
challengeTypes: MSALNativeAuthChallengeTypes = [.OOB, .password],
customSubdomainFormat: Int? = nil
Copy link
Contributor

@nilo-ms nilo-ms Dec 17, 2024

Choose a reason for hiding this comment

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

Using an Int in this case create the following issues:

  • Int, if not validated, can led to "arrray index out of bound error"
  • The caller of the method will need to check all the formats available before passing the correct index
  • Changing the order of the format in the array can led to use the unexpected custom domain.

Did you take in consideration using an enum instead?
I suggest using an enum like this one:

enum AuthorityURLFormat {
    case tenantSubdomainShortVersion
    case tenantSubdomainLongVersion
    case tenantSubdomainTenantId
}

So, the firm of the method can be changed to:
customAuthorityURLFormat: AuthorityURLFormat? = nil

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea. Done.

) -> MSALNativeAuthPublicClientApplication? {
let clientIdKey = getClientIdKey(type: clientIdType)
guard let clientId = MSALNativeAuthEndToEndBaseTestCase.nativeAuthConfFileContent?[clientIdKey] as? String, let tenantSubdomain = MSALNativeAuthEndToEndBaseTestCase.nativeAuthConfFileContent?[Constants.tenantSubdomainKey] as? String else {
XCTFail("ClientId or tenantSubdomain not found in conf.json")
guard let clientId = MSALNativeAuthEndToEndBaseTestCase.nativeAuthConfFileContent?[clientIdKey] as? String else {
XCTFail("ClientId not found in conf.json")
return nil
}
return try? MSALNativeAuthPublicClientApplication(clientId: clientId, tenantSubdomain: tenantSubdomain, challengeTypes: challengeTypes)

guard let tenantSubdomain = MSALNativeAuthEndToEndBaseTestCase.nativeAuthConfFileContent?[Constants.tenantSubdomainKey] as? String else {
XCTFail("TenantName not found in conf.json")
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you call it "tenant subdomain"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

return nil
}


if customSubdomainFormat != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

if you use if let you can automatically unwrap the optional value :)

Suggested change
if customSubdomainFormat != nil {
if let customSubdomainFormat = customSubdomainFormat {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

let customSubdomain = getCustomTenantSubdomain(
tenantName: tenantSubdomain,
tenantId: "", // TODO: Upload the tenant id into the config
format: customSubdomainFormat!
)

let authority = try? MSALCIAMAuthority(
url: URL(string: customSubdomain)!,
validateFormat: false
)

let configuration = MSALPublicClientApplicationConfig(
clientId: clientId,
redirectUri: nil,
authority: authority
)

return try? MSALNativeAuthPublicClientApplication(
configuration: configuration,
challengeTypes: challengeTypes
)
} else {
return try? MSALNativeAuthPublicClientApplication(clientId: clientId, tenantSubdomain: tenantSubdomain, challengeTypes: challengeTypes)
}
}

func generateSignUpRandomEmail() -> String {
Expand Down Expand Up @@ -129,4 +166,10 @@ class MSALNativeAuthEndToEndBaseTestCase: XCTestCase {
return Constants.clientIdEmailCodeAttributesKey
}
}

private func getCustomTenantSubdomain(tenantName: String?, tenantId: String?, format: Int) -> String {
return Constants.customDomainFormat[format]
.replacingOccurrences(of: "<tenantName>", with: tenantName!)
Copy link
Contributor

Choose a reason for hiding this comment

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

Misspelling the string to replace can led to unexpected result. Did you consider using String template instead?
More about it here:
https://developer.apple.com/documentation/swift/string/init(format:_:)
Moreover, tenantSubdomain and tenantId should not be nil at this point.
You can use String template like this:
let stringresult = String(format: "https://%@.ciamlogin.com/", tenantSubdomain)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea. Done.

.replacingOccurrences(of: "<tenantId>", with: tenantId!)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,58 @@ final class MSALNativeAuthSignInUsernameAndPasswordEndToEndTests: MSALNativeAuth
XCTAssertTrue(signInPasswordRequiredDelegateSpy.onSignInPasswordRequiredErrorCalled)
XCTAssertEqual(signInPasswordRequiredDelegateSpy.error?.isInvalidPassword, true)
}

// Sign In - Verify Custom URL Domain - "https://<tenantName>.ciamlogin.com/<tenantName>.onmicrosoft.com"
func test_signInCustomDomain1InSuccess() async throws {
guard let sut = initialisePublicClientApplication(customSubdomainFormat: 0) else {
XCTFail("Failed to initialise auth client")
return
}

guard let username = retrieveUsernameForSignInUsernameAndPassword(),
let password = await retrievePasswordForSignInUsername() else {
XCTFail("Missing username or password")
return
}

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

// Perform the sign-in asynchronously
Task {
await sut.signIn(username: username, password: password, correlationId: correlationId, delegate: signInDelegateSpy)
await fulfillment(of: [signInExpectation])

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

// Sign In - Verify Custom URL Domain - "https://<tenantName>.ciamlogin.com/"
func test_signInCustomDomain3InSuccess() async throws {
guard let sut = initialisePublicClientApplication(customSubdomainFormat: 2) else {
XCTFail("Failed to initialise auth client")
return
}

guard let username = retrieveUsernameForSignInUsernameAndPassword(),
let password = await retrievePasswordForSignInUsername() else {
XCTFail("Missing username or password")
return
}

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

// Perform the sign-in asynchronously
Task {
await sut.signIn(username: username, password: password, correlationId: correlationId, delegate: signInDelegateSpy)
await fulfillment(of: [signInExpectation])

XCTAssertTrue(signInDelegateSpy.onSignInCompletedCalled)
XCTAssertNotNil(signInDelegateSpy.result?.idToken)
XCTAssertEqual(signInDelegateSpy.result?.account.username, username)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,58 @@ final class MSALNativeAuthSignUpUsernameAndPasswordEndToEndTests: MSALNativeAuth
await fulfillment(of: [signInExp])
checkSignInAfterSignUpDelegate(signInAfterSignUpDelegate, expectedUsername: username)
}

// Use case 1.1.2. Sign up - with Email & Password, Resend email OOB
func test_signUpWithEmailPassword_resendEmail_success() async throws {
guard let sut = initialisePublicClientApplication() else {
XCTFail("Missing information")
return
}

let username = generateSignUpRandomEmail()
let password = generateRandomPassword()

let codeRequiredExp = expectation(description: "code required")
let signUpStartDelegate = SignUpPasswordStartDelegateSpy(expectation: codeRequiredExp)

sut.signUp(
username: username,
password: password,
correlationId: correlationId,
delegate: signUpStartDelegate
)

await fulfillment(of: [codeRequiredExp])
checkSignUpStartDelegate(signUpStartDelegate)

// 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 signUpResendCodeDelegate = SignUpResendCodeDelegateSpy(expectation: resendCodeRequiredExp)

// Call resend code method
signUpStartDelegate.newState?.resendCode(delegate: signUpResendCodeDelegate)

await fulfillment(of: [resendCodeRequiredExp])

// Verify that resend code method was called
XCTAssertTrue(signUpResendCodeDelegate.onSignUpResendCodeCodeRequiredCalled,
"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")
}

// Hero Scenario 1.1.3. Sign up - with Email verification as LAST step & Custom Attributes (Email & Password)
func test_signUpWithPassword_withEmailVerificationAsLastStepAndCustomAttributes_succeeds() async throws {
Expand Down Expand Up @@ -218,6 +270,72 @@ final class MSALNativeAuthSignUpUsernameAndPasswordEndToEndTests: MSALNativeAuth
await fulfillment(of: [signInExp])
checkSignInAfterSignUpDelegate(signInAfterSignUpDelegate, expectedUsername: username)
}

// Use case 1.1.5. Sign up - with Email & Password, Verify email address using email OTP, resend OTP and then set password
func test_signUpWithEmailOTP_andSetPasswordAfterOTP_success() async throws {
guard let sut = initialisePublicClientApplication() else {
XCTFail("Missing information")
return
}

let username = generateSignUpRandomEmail()
let password = generateRandomPassword()

let codeRequiredExp = expectation(description: "code required")
let signUpStartDelegate = SignUpPasswordStartDelegateSpy(expectation: codeRequiredExp)

sut.signUp(
username: username,
password: password,
correlationId: correlationId,
delegate: signUpStartDelegate
)

await fulfillment(of: [codeRequiredExp])
checkSignUpStartDelegate(signUpStartDelegate)

guard signUpStartDelegate.onSignUpCodeRequiredCalled else {
XCTFail("onSignUpCodeRequired not called")
return
}

// First attempt to get code
guard let initialCode = await retrieveCodeFor(email: username) else {
XCTFail("Initial OTP code could not be retrieved")
return
}

// Resend code expectation
let resendCodeRequiredExp = expectation(description: "code resend required")
let signUpResendCodeDelegate = SignUpResendCodeDelegateSpy(expectation: resendCodeRequiredExp)

// Call resend code method
signUpStartDelegate.newState?.resendCode(delegate: signUpResendCodeDelegate)

await fulfillment(of: [resendCodeRequiredExp])

// Verify resend code was triggered
XCTAssertTrue(signUpResendCodeDelegate.onSignUpResendCodeCodeRequiredCalled,
"Resend code method should have been called")

// Get new code after resend
guard let newCode = await retrieveCodeFor(email: username) else {
XCTFail("Resent OTP code could not be retrieved")
return
}

// Verify that the new code is different from the initial code
XCTAssertNotEqual(initialCode, newCode, "Resent code should be different from the initial code")

// Complete sign up with the new code
let signUpCompleteExp = expectation(description: "sign-up complete")
let signUpVerifyCodeDelegate = SignUpVerifyCodeDelegateSpy(expectation: signUpCompleteExp)

signUpStartDelegate.newState?.submitCode(code: newCode, delegate: signUpVerifyCodeDelegate)

await fulfillment(of: [signUpCompleteExp])
XCTAssertTrue(signUpVerifyCodeDelegate.onSignUpCompletedCalled, "Sign-up should be completed successfully")
}

// Hero Scenario 1.1.6. Sign up - with Email verification as FIRST step & Custom Attribute (Email & Password)
func test_signUpWithPasswordWithEmailVerificationAsFirstStepAndCustomAttributes_succeeds() async throws {
Expand Down Expand Up @@ -464,7 +582,116 @@ final class MSALNativeAuthSignUpUsernameAndPasswordEndToEndTests: MSALNativeAuth
await fulfillment(of: [signUpCompleteExp])
XCTAssertTrue(signUpVerifyCodeDelegate.onSignUpCompletedCalled)
}


// Use case 1.1.10. Sign up - with Email & Password, User already exists with given email as email-pw account
func test_signUpWithEmailPassword_andAgainSameEmail_fails() async throws {
guard let sut = initialisePublicClientApplication(clientIdType: .password), let username = retrieveUsernameForSignInUsernameAndPassword() else {
XCTFail("Missing information")
return
}

let password = generateRandomPassword()

let signUpFailureExp = expectation(description: "sign-up with existing email fails")
let signUpStartDelegate = SignUpPasswordStartDelegateSpy(expectation: signUpFailureExp)

sut.signUp(
username: username,
password: password,
correlationId: correlationId,
delegate: signUpStartDelegate
)

await fulfillment(of: [signUpFailureExp])

// Verify error condition
XCTAssertTrue(signUpStartDelegate.onSignUpPasswordErrorCalled)
XCTAssertEqual(signUpStartDelegate.error?.isUserAlreadyExists, true)
}

// Use case 1.1.11. Sign up - with Email & Password, User already exists with given email as social account
func test_signUpWithEmailPassword_socialAccount_fails() 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 = "social_account"
let password = generateRandomPassword()

let signUpFailureExp = expectation(description: "sign-up with social account email fails")
let signUpStartDelegate = SignUpPasswordStartDelegateSpy(expectation: signUpFailureExp)

sut.signUp(
username: username,
password: password,
correlationId: correlationId,
delegate: signUpStartDelegate
)

await fulfillment(of: [signUpFailureExp])

// Verify error condition
XCTAssertTrue(signUpStartDelegate.onSignUpPasswordErrorCalled)
XCTAssertEqual(signUpStartDelegate.error!.isInvalidUsername, true)
Copy link
Contributor

@nilo-ms nilo-ms Dec 19, 2024

Choose a reason for hiding this comment

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

Try to never use ! to force unwrap an optional. This can cause a crash and stop the execution of all tests

Suggested change
XCTAssertEqual(signUpStartDelegate.error!.isInvalidUsername, true)
XCTAssertEqual(signUpStartDelegate.error?.isInvalidUsername, true)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. Done and modify and check all the sign up cases. error!.isBrowserRequired needs to use ! instead of ?. The others use ?.

}

// Use case 1.1.12. Sign up - with Email & Password, Developer makes a request with invalid format email address
func test_signUpWithEmailPassword_invalidEmail_fails() async throws {
guard let sut = initialisePublicClientApplication(clientIdType: .password) else {
XCTFail("Missing information")
return
}

let username = "invalid"
let password = generateRandomPassword()

let signUpFailureExp = expectation(description: "sign-up with invalid format email fails")
let signUpStartDelegate = SignUpPasswordStartDelegateSpy(expectation: signUpFailureExp)

sut.signUp(
username: username,
password: password,
correlationId: correlationId,
delegate: signUpStartDelegate
)

await fulfillment(of: [signUpFailureExp])

// Verify error condition
XCTAssertTrue(signUpStartDelegate.onSignUpPasswordErrorCalled)
XCTAssertEqual(signUpStartDelegate.error!.isInvalidUsername, true)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
XCTAssertEqual(signUpStartDelegate.error!.isInvalidUsername, true)
XCTAssertEqual(signUpStartDelegate.error?.isInvalidUsername, true)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

}

// Use case 1.1.13. Sign up - with Email & Password, Developer makes a request with password that does not match password complexity requirements set on portal
func test_signUpWithEmailPassword_invalidPassword_fails() async throws {
guard let sut = initialisePublicClientApplication(clientIdType: .password) else {
XCTFail("Missing information")
return
}

let username = generateSignUpRandomEmail()
let password = "invalid"

let signUpFailureExp = expectation(description: "sign-up with invalid password complexity fails")
let signUpStartDelegate = SignUpPasswordStartDelegateSpy(expectation: signUpFailureExp)

sut.signUp(
username: username,
password: password,
correlationId: correlationId,
delegate: signUpStartDelegate
)

await fulfillment(of: [signUpFailureExp])

// Verify error condition
XCTAssertTrue(signUpStartDelegate.onSignUpPasswordErrorCalled)
XCTAssertEqual(signUpStartDelegate.error!.isInvalidPassword, true)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
XCTAssertEqual(signUpStartDelegate.error!.isInvalidPassword, true)
XCTAssertEqual(signUpStartDelegate.error?.isInvalidPassword, true)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

}

private func checkSignUpStartDelegate(_ delegate: SignUpPasswordStartDelegateSpy) {
XCTAssertTrue(delegate.onSignUpCodeRequiredCalled)
XCTAssertEqual(delegate.channelTargetType?.isEmailType, true)
Expand Down
Loading
Loading