diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index 7c8360900..de17a1260 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -178,6 +178,9 @@ 230967422711156A001B42D9 /* MSALTestsConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 230967402711156A001B42D9 /* MSALTestsConfig.m */; }; 230CA9A0303545F700E47BC0 /* MSALNativeAuthSignInUsernameAndPasswordV2EndToEndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230CA99F303545F700E47BC0 /* MSALNativeAuthSignInUsernameAndPasswordV2EndToEndTests.swift */; }; 230CA9A1303545F700E47BC0 /* MSALNativeAuthSignInUsernameAndPasswordV2EndToEndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230CA99F303545F700E47BC0 /* MSALNativeAuthSignInUsernameAndPasswordV2EndToEndTests.swift */; }; + AD2F6AA4F624BC2D74A4A3F7 /* MSALNativeAuthSignUpUsernameV2EndToEndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171B4750E8FCB45E4521B522 /* MSALNativeAuthSignUpUsernameV2EndToEndTests.swift */; }; + B02A2833685C0794B3E7430A /* MSALNativeAuthSignUpUsernameAndPasswordV2EndToEndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 247ABE3EFB4695D08EAF6992 /* MSALNativeAuthSignUpUsernameAndPasswordV2EndToEndTests.swift */; }; + B2C0F5A3BBEB14496E21116E /* SignUpDelegateSpiesV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A5F7F663C560D9402312960 /* SignUpDelegateSpiesV2.swift */; }; 231CE9DC1FEC682000E95D3E /* libIdentityTest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D6A206271FC50A4D00755A51 /* libIdentityTest.a */; }; 231CE9DE1FEC684C00E95D3E /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 231CE9DD1FEC684C00E95D3E /* Security.framework */; }; 231CE9DF1FEC7E8400E95D3E /* libIdentityTest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D6A206291FC50A4D00755A51 /* libIdentityTest.a */; }; @@ -673,6 +676,9 @@ A509294FE137EA2B29C6AE24 /* MSALNativeAuthV2ResponseParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseParserTests.swift */; }; A7F4C1013037A00100E2E002 /* MSALNativeAuthSignInUsernameV2EndToEndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F4C1013037A00100E2E001 /* MSALNativeAuthSignInUsernameV2EndToEndTests.swift */; }; A7F4C1013037A00100E2E003 /* MSALNativeAuthSignInUsernameV2EndToEndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F4C1013037A00100E2E001 /* MSALNativeAuthSignInUsernameV2EndToEndTests.swift */; }; + CB3D54C2BFDC5196DAE04F8E /* MSALNativeAuthSignUpUsernameV2EndToEndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171B4750E8FCB45E4521B522 /* MSALNativeAuthSignUpUsernameV2EndToEndTests.swift */; }; + F14D61CD4B45EE17057FF9B8 /* MSALNativeAuthSignUpUsernameAndPasswordV2EndToEndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 247ABE3EFB4695D08EAF6992 /* MSALNativeAuthSignUpUsernameAndPasswordV2EndToEndTests.swift */; }; + F144CB29F55DE2CEC93FB187 /* SignUpDelegateSpiesV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A5F7F663C560D9402312960 /* SignUpDelegateSpiesV2.swift */; }; A89E21F4CDFA919F513EA87E /* MSALNativeAuthV2ResponseParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseParserTests.swift */; }; A939579E9B632F2EFA0447E6 /* MSALNativeAuthFlowControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657374069BB444E4D7FF440C /* MSALNativeAuthFlowControllerMock.swift */; }; AA5AB06A9DD86202FD19BFC8 /* MSALNativeAuthV2RequestTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF017CDD211895E02588AA7E /* MSALNativeAuthV2RequestTarget.swift */; }; @@ -2262,6 +2268,7 @@ 2309673F2711156A001B42D9 /* MSALTestsConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALTestsConfig.h; sourceTree = ""; }; 230967402711156A001B42D9 /* MSALTestsConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALTestsConfig.m; sourceTree = ""; }; 230CA99F303545F700E47BC0 /* MSALNativeAuthSignInUsernameAndPasswordV2EndToEndTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignInUsernameAndPasswordV2EndToEndTests.swift; sourceTree = ""; }; + 247ABE3EFB4695D08EAF6992 /* MSALNativeAuthSignUpUsernameAndPasswordV2EndToEndTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignUpUsernameAndPasswordV2EndToEndTests.swift; sourceTree = ""; }; 231CE9DD1FEC684C00E95D3E /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.3.sdk/System/Library/Frameworks/Security.framework; sourceTree = DEVELOPER_DIR; }; 231CE9E01FECBD4600E95D3E /* unit-test-host.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "unit-test-host.entitlements"; sourceTree = ""; }; 2328073F28BC175C000306A9 /* MSALAccountEnumerationParameters+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALAccountEnumerationParameters+Private.h"; sourceTree = ""; }; @@ -2831,6 +2838,8 @@ D862978DEC304DD299125F27 /* MSALNativeAuthV2VerifyRequestBody.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2VerifyRequestBody.swift; sourceTree = ""; }; FE0A0B00000000000000F011 /* MSALNativeAuthV2SubmitAttributesRequestBody.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2SubmitAttributesRequestBody.swift; sourceTree = ""; }; DA3D00FD456DC3F9BF81C82E /* MSALNativeAuthResetPasswordV2EndToEndTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthResetPasswordV2EndToEndTests.swift; sourceTree = ""; }; + 171B4750E8FCB45E4521B522 /* MSALNativeAuthSignUpUsernameV2EndToEndTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignUpUsernameV2EndToEndTests.swift; sourceTree = ""; }; + 5A5F7F663C560D9402312960 /* SignUpDelegateSpiesV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpDelegateSpiesV2.swift; sourceTree = ""; }; DB4ABFF4EA9741E8B24C0066 /* MSALNativeAuthEmailOTPUserPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthEmailOTPUserPool.swift; sourceTree = ""; }; DE0347A72A41AD08003CB3B6 /* MSALNativeAuthUserAccountResultStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthUserAccountResultStub.swift; sourceTree = ""; }; DE0D656729BF72F6005798B1 /* MSALNativeAuthSignInInitiateRequestParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignInInitiateRequestParameters.swift; sourceTree = ""; }; @@ -5468,6 +5477,9 @@ E272C4EA2A4447520013B805 /* MSALNativeAuthSignUpUsernameEndToEndTests.swift */, E26E391A2A4C2BE200063C07 /* MSALNativeAuthSignUpUsernameAndPasswordEndToEndTests.swift */, E26E39232A4C2D7400063C07 /* SignUpDelegateSpies.swift */, + 171B4750E8FCB45E4521B522 /* MSALNativeAuthSignUpUsernameV2EndToEndTests.swift */, + 247ABE3EFB4695D08EAF6992 /* MSALNativeAuthSignUpUsernameAndPasswordV2EndToEndTests.swift */, + 5A5F7F663C560D9402312960 /* SignUpDelegateSpiesV2.swift */, ); path = sign_up; sourceTree = ""; @@ -7175,6 +7187,9 @@ E97A3F5320FF535A108B6879 /* MSALNativeAuthResetPasswordV2EndToEndTests.swift in Sources */, 281A0E0C2C21E1F000CB30CB /* MSALNativeAuthSignUpUsernameEndToEndTests.swift in Sources */, 281A0E152C21E1F500CB30CB /* SignUpDelegateSpies.swift in Sources */, + AD2F6AA4F624BC2D74A4A3F7 /* MSALNativeAuthSignUpUsernameV2EndToEndTests.swift in Sources */, + B02A2833685C0794B3E7430A /* MSALNativeAuthSignUpUsernameAndPasswordV2EndToEndTests.swift in Sources */, + B2C0F5A3BBEB14496E21116E /* SignUpDelegateSpiesV2.swift in Sources */, 28188F622C8F48BD00CFDD05 /* MSALNativeAuthSignInWithMFAEndToEndTests.swift in Sources */, 41FCDD339EA0CFFF4CA7D125 /* MSALNativeAuthSignInWithMFAV2EndToEndTests.swift in Sources */, DE20A8492DDE2CD200BC286C /* JITDelegateSpies.swift in Sources */, @@ -8492,6 +8507,9 @@ DE1BD1012C3C283C00B0888E /* MSALNativeAuthSignUpUsernameEndToEndTests.swift in Sources */, 28188F662C8F4C1100CFDD05 /* MFADelegateSpies.swift in Sources */, DE1BD1032C3C284100B0888E /* SignUpDelegateSpies.swift in Sources */, + CB3D54C2BFDC5196DAE04F8E /* MSALNativeAuthSignUpUsernameV2EndToEndTests.swift in Sources */, + F14D61CD4B45EE17057FF9B8 /* MSALNativeAuthSignUpUsernameAndPasswordV2EndToEndTests.swift in Sources */, + F144CB29F55DE2CEC93FB187 /* SignUpDelegateSpiesV2.swift in Sources */, DED1F09D2DD644FC009CB97A /* JITDelegateSpies.swift in Sources */, DE1BD1092C3C285D00B0888E /* MSALNativeAuthSignOutEndToEndTests.swift in Sources */, DE1BD1052C3C284700B0888E /* MSALNativeAuthSignInUsernameEndToEndTests.swift in Sources */, diff --git a/MSAL/test/integration/native_auth/end_to_end/sign_up/MSALNativeAuthSignUpUsernameAndPasswordV2EndToEndTests.swift b/MSAL/test/integration/native_auth/end_to_end/sign_up/MSALNativeAuthSignUpUsernameAndPasswordV2EndToEndTests.swift new file mode 100644 index 000000000..8c693478f --- /dev/null +++ b/MSAL/test/integration/native_auth/end_to_end/sign_up/MSALNativeAuthSignUpUsernameAndPasswordV2EndToEndTests.swift @@ -0,0 +1,709 @@ +// +// 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. + +import Foundation +import XCTest +import MSAL + +final class MSALNativeAuthSignUpUsernameAndPasswordV2EndToEndTests: MSALNativeAuthEndToEndBaseTestCase { + + override func setUpWithError() throws { + try super.setUpWithError() + throw XCTSkip("Sign Up V2 requires a test slice. Disable this test until api/test slice is ready.") + } + + // Hero Scenario 1.1.1. Sign up - with Email verification as LAST step (Email & Password) + @MainActor + func test_signUpWithPassword_withEmailVerificationLastStep_succeeds() async throws { + guard let sut = initialisePublicClientApplication( + clientIdType: .password, + customAuthorityURLFormat: .tenantSubdomainTenantId + ) else { + XCTFail("Missing information") + return + } + + let password = generateRandomPassword() + let username = await createEmailProviderAccount(password: password) + guard !username.isEmpty else { + return + } + + guard let codeRequiredState = try await startSignUpAndExpectCodeRequired( + application: sut, + username: username, + password: password + ) else { + return + } + + guard let code = await retrieveCodeFor(email: username, password: password) else { + XCTFail("OTP code could not be retrieved") + return + } + + let signInAfterSignUpRequiredExp = expectation(description: "sign in after sign up required") + let delegate = SignUpV2DelegateSpy(expectation: signInAfterSignUpRequiredExp) + delegate.reset(expectation: signInAfterSignUpRequiredExp) + codeRequiredState.submitCode(code, delegate: delegate) + + await fulfillment(of: [signInAfterSignUpRequiredExp]) + + guard delegate.onSignInAfterSignUpRequiredCalled, + let signInAfterSignUpState = delegate.signInAfterSignUpState + else { + XCTFail("onSignInAfterSignUpRequired not called") + return + } + + try await continueSignInAfterSignUp( + state: signInAfterSignUpState, + delegate: delegate, + username: username + ) + } + + // Use case 1.1.2. Sign up - with Email & Password, Resend email OOB + @MainActor + func test_signUpWithEmailPassword_resendEmail_success() async throws { + guard let sut = initialisePublicClientApplication( + clientIdType: .password, + customAuthorityURLFormat: .tenantSubdomainTenantId + ) else { + XCTFail("Missing information") + return + } + + let password = generateRandomPassword() + let username = await createEmailProviderAccount(password: password) + guard !username.isEmpty else { + return + } + + let codeRequiredExp = expectation(description: "code required") + let delegate = SignUpV2DelegateSpy(expectation: codeRequiredExp) + let parameters = signUpParameters(username: username, password: password) + + markEmailCheckpoint() + sut.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [codeRequiredExp]) + try skipIfEmailOTPThrottled(delegate.error) + + guard delegate.onCodeRequiredCalled, + let codeRequiredState = delegate.codeRequiredState + else { + XCTFail("OTP not sent") + return + } + checkCodeRequired(delegate) + + guard let code1 = await retrieveCodeFor(email: username, password: password) else { + XCTFail("OTP code could not be retrieved") + return + } + + let resendCodeRequiredExp = expectation(description: "code required again") + delegate.reset(expectation: resendCodeRequiredExp) + + markEmailCheckpoint() + codeRequiredState.resendCode(delegate: delegate) + + await fulfillment(of: [resendCodeRequiredExp]) + try skipIfEmailOTPThrottled(delegate.error) + + guard delegate.onCodeRequiredCalled else { + XCTFail("Resend code method should have been called") + return + } + + guard let code2 = await retrieveCodeFor(email: username, password: password) else { + XCTFail("OTP code could not be retrieved") + return + } + + 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) + @MainActor + func test_signUpWithPassword_withEmailVerificationAsLastStepAndCustomAttributes_succeeds() async throws { + guard let sut = initialisePublicClientApplication( + clientIdType: .passwordAndAttributes, + customAuthorityURLFormat: .tenantSubdomainTenantId + ) else { + XCTFail("Missing information") + return + } + + let password = generateRandomPassword() + let username = await createEmailProviderAccount(password: password) + guard !username.isEmpty else { + return + } + + let codeRequiredExp = expectation(description: "code required") + let delegate = SignUpV2DelegateSpy(expectation: codeRequiredExp) + let parameters = signUpParameters( + username: username, + password: password, + attributes: AttributesStub.allAttributes + ) + + markEmailCheckpoint() + sut.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [codeRequiredExp]) + try skipIfEmailOTPThrottled(delegate.error) + + guard delegate.onCodeRequiredCalled, + let codeRequiredState = delegate.codeRequiredState + else { + XCTFail("OTP not sent") + return + } + checkCodeRequired(delegate) + + guard let code = await retrieveCodeFor(email: username, password: password) else { + XCTFail("OTP code could not be retrieved") + return + } + + let signInAfterSignUpRequiredExp = expectation(description: "sign in after sign up required") + delegate.reset(expectation: signInAfterSignUpRequiredExp) + codeRequiredState.submitCode(code, delegate: delegate) + + await fulfillment(of: [signInAfterSignUpRequiredExp]) + + guard delegate.onSignInAfterSignUpRequiredCalled, + let signInAfterSignUpState = delegate.signInAfterSignUpState + else { + XCTFail("onSignInAfterSignUpRequired not called") + return + } + + try await continueSignInAfterSignUp( + state: signInAfterSignUpState, + delegate: delegate, + username: username + ) + } + + // Hero Scenario 1.1.4. Sign up - with Email verification as FIRST step (Email & Password) + @MainActor + func test_signUpWithPassword_withEmailVerificationAsFirstStepAndThenSetPassword_succeeds() async throws { + // NOTE: Sign Up V2 does not expose a post-OTP password step; the SDK can only submit + // password during the server-driven attribute collection stage, so this uses the closest + // supported flow with the password provided up front. + guard let sut = initialisePublicClientApplication( + clientIdType: .password, + customAuthorityURLFormat: .tenantSubdomainTenantId + ) else { + XCTFail("Missing information") + return + } + + let password = generateRandomPassword() + let username = await createEmailProviderAccount(password: password) + guard !username.isEmpty else { + return + } + + guard let codeRequiredState = try await startSignUpAndExpectCodeRequired( + application: sut, + username: username, + password: password + ) else { + return + } + + guard let code = await retrieveCodeFor(email: username, password: password) else { + XCTFail("OTP code could not be retrieved") + return + } + + let signInAfterSignUpRequiredExp = expectation(description: "sign in after sign up required") + let delegate = SignUpV2DelegateSpy(expectation: signInAfterSignUpRequiredExp) + delegate.reset(expectation: signInAfterSignUpRequiredExp) + codeRequiredState.submitCode(code, delegate: delegate) + + await fulfillment(of: [signInAfterSignUpRequiredExp]) + + guard delegate.onSignInAfterSignUpRequiredCalled, + let signInAfterSignUpState = delegate.signInAfterSignUpState + else { + XCTFail("onSignInAfterSignUpRequired not called") + return + } + + try await continueSignInAfterSignUp( + state: signInAfterSignUpState, + delegate: delegate, + username: username + ) + } + + // Use case 1.1.5. Sign up - with Email & Password, Verify email address using email OTP, resend OTP and then set password + @MainActor + func test_signUpWithEmailOTP_andSetPasswordAfterOTP_success() async throws { + // NOTE: Sign Up V2 does not expose a post-OTP password continuation, so this keeps the + // resend-OTP coverage and completes the closest supported variant with password supplied up front. + guard let sut = initialisePublicClientApplication( + clientIdType: .password, + customAuthorityURLFormat: .tenantSubdomainTenantId + ) else { + XCTFail("Missing information") + return + } + + let password = generateRandomPassword() + let username = await createEmailProviderAccount(password: password) + guard !username.isEmpty else { + return + } + + let codeRequiredExp = expectation(description: "code required") + let delegate = SignUpV2DelegateSpy(expectation: codeRequiredExp) + let parameters = signUpParameters(username: username, password: password) + + markEmailCheckpoint() + sut.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [codeRequiredExp]) + try skipIfEmailOTPThrottled(delegate.error) + + guard delegate.onCodeRequiredCalled, + let codeRequiredState = delegate.codeRequiredState + else { + XCTFail("OTP not sent") + return + } + checkCodeRequired(delegate) + + guard let initialCode = await retrieveCodeFor(email: username, password: password) else { + XCTFail("Initial OTP code could not be retrieved") + return + } + + let resendCodeRequiredExp = expectation(description: "code resend required") + delegate.reset(expectation: resendCodeRequiredExp) + + markEmailCheckpoint() + codeRequiredState.resendCode(delegate: delegate) + + await fulfillment(of: [resendCodeRequiredExp]) + try skipIfEmailOTPThrottled(delegate.error) + + guard delegate.onCodeRequiredCalled, + let resentCodeRequiredState = delegate.codeRequiredState + else { + XCTFail("Resend code method should have been called") + return + } + + guard let newCode = await retrieveCodeFor(email: username, password: password) else { + XCTFail("Resent OTP code could not be retrieved") + return + } + + XCTAssertNotEqual(initialCode, newCode, "Resent code should be different from the initial code") + + let signInAfterSignUpRequiredExp = expectation(description: "sign in after sign up required") + delegate.reset(expectation: signInAfterSignUpRequiredExp) + resentCodeRequiredState.submitCode(newCode, delegate: delegate) + + await fulfillment(of: [signInAfterSignUpRequiredExp]) + + XCTAssertTrue(delegate.onSignInAfterSignUpRequiredCalled, "Sign-up should complete successfully") + XCTAssertEqual(delegate.scenario, .signUp) + XCTAssertNotNil(delegate.signInAfterSignUpState) + } + + // Hero Scenario 1.1.6. Sign up - with Email verification as FIRST step & Custom Attribute (Email & Password) + @MainActor + func test_signUpWithPasswordWithEmailVerificationAsFirstStepAndCustomAttributes_succeeds() async throws { + // NOTE: Sign Up V2 does not expose a post-OTP password step. This preserves the first-step + // verification plus custom-attributes coverage by providing password up front and collecting + // only the custom attributes after OTP verification. + guard let sut = initialisePublicClientApplication( + clientIdType: .passwordAndAttributes, + customAuthorityURLFormat: .tenantSubdomainTenantId + ) else { + XCTFail("Missing information") + return + } + + let password = generateRandomPassword() + let username = await createEmailProviderAccount(password: password) + guard !username.isEmpty else { + return + } + + let codeRequiredExp = expectation(description: "code required") + let delegate = SignUpV2DelegateSpy(expectation: codeRequiredExp) + let parameters = signUpParameters(username: username, password: password) + + markEmailCheckpoint() + sut.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [codeRequiredExp]) + try skipIfEmailOTPThrottled(delegate.error) + + guard delegate.onCodeRequiredCalled, + let codeRequiredState = delegate.codeRequiredState + else { + XCTFail("OTP not sent") + return + } + checkCodeRequired(delegate) + + guard let code = await retrieveCodeFor(email: username, password: password) else { + XCTFail("OTP code could not be retrieved") + return + } + + let attributesRequiredExp = expectation(description: "attributes required") + delegate.reset(expectation: attributesRequiredExp) + codeRequiredState.submitCode(code, delegate: delegate) + + await fulfillment(of: [attributesRequiredExp]) + + guard delegate.onAttributesRequiredCalled, + let attributesRequiredState = delegate.attributesRequiredState + else { + XCTFail("onAttributesRequired not called") + return + } + + let signInAfterSignUpRequiredExp = expectation(description: "sign in after sign up required") + delegate.reset(expectation: signInAfterSignUpRequiredExp) + attributesRequiredState.submitAttributes(AttributesStub.allAttributes, delegate: delegate) + + await fulfillment(of: [signInAfterSignUpRequiredExp]) + + guard delegate.onSignInAfterSignUpRequiredCalled, + let signInAfterSignUpState = delegate.signInAfterSignUpState + else { + XCTFail("onSignInAfterSignUpRequired not called") + return + } + + try await continueSignInAfterSignUp( + state: signInAfterSignUpState, + delegate: delegate, + username: username + ) + } + + // Sign up - with Email verification as FIRST step & Custom Attributes over MULTIPLE screens (Email & Password) + @MainActor + func test_signUpWithPasswordWithEmailVerificationAsFirstStepAndCustomAttributesOverMultipleScreens_succeeds() async throws { + // NOTE: Sign Up V2 does not expose a post-OTP password step. This keeps the multi-screen + // custom-attributes coverage while providing password up front. + guard let sut = initialisePublicClientApplication( + clientIdType: .passwordAndAttributes, + customAuthorityURLFormat: .tenantSubdomainTenantId + ) else { + XCTFail("Missing information") + return + } + + let password = generateRandomPassword() + let username = await createEmailProviderAccount(password: password) + guard !username.isEmpty else { + return + } + + let codeRequiredExp = expectation(description: "code required") + let delegate = SignUpV2DelegateSpy(expectation: codeRequiredExp) + let parameters = signUpParameters(username: username, password: password) + + markEmailCheckpoint() + sut.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [codeRequiredExp]) + try skipIfEmailOTPThrottled(delegate.error) + + guard delegate.onCodeRequiredCalled, + let codeRequiredState = delegate.codeRequiredState + else { + XCTFail("OTP not sent") + return + } + checkCodeRequired(delegate) + + guard let code = await retrieveCodeFor(email: username, password: password) else { + XCTFail("OTP code could not be retrieved") + return + } + + let firstAttributesExp = expectation(description: "first attributes step") + delegate.reset(expectation: firstAttributesExp) + codeRequiredState.submitCode(code, delegate: delegate) + + await fulfillment(of: [firstAttributesExp]) + + guard delegate.onAttributesRequiredCalled, + let firstAttributesState = delegate.attributesRequiredState + else { + XCTFail("onAttributesRequired not called") + return + } + + let secondAttributesExp = expectation(description: "second attributes step") + delegate.reset(expectation: secondAttributesExp) + firstAttributesState.submitAttributes(AttributesStub.attribute1, delegate: delegate) + + await fulfillment(of: [secondAttributesExp]) + + let secondAttributesState = delegate.attributesRequiredState + let invalidAttributesState = delegate.attributesInvalidState + let finalSubmitExp = expectation(description: "final attributes submission") + delegate.reset(expectation: finalSubmitExp) + + if let secondAttributesState = secondAttributesState { + secondAttributesState.submitAttributes(AttributesStub.attribute2, delegate: delegate) + } else if let invalidAttributesState = invalidAttributesState { + invalidAttributesState.submitAttributes(AttributesStub.attribute2, delegate: delegate) + } else { + XCTFail("Expected another attributes continuation") + return + } + + await fulfillment(of: [finalSubmitExp]) + + guard delegate.onSignInAfterSignUpRequiredCalled, + let signInAfterSignUpState = delegate.signInAfterSignUpState + else { + XCTFail("onSignInAfterSignUpRequired not called") + return + } + + try await continueSignInAfterSignUp( + state: signInAfterSignUpState, + delegate: delegate, + username: username + ) + } + + // Sign up – without automatic sign in (Email & Password) + @MainActor + func test_signUpWithPasswordWithoutAutomaticSignIn() async throws { + guard let sut = initialisePublicClientApplication( + clientIdType: .password, + customAuthorityURLFormat: .tenantSubdomainTenantId + ) else { + XCTFail("Missing information") + return + } + + let password = generateRandomPassword() + let username = await createEmailProviderAccount(password: password) + guard !username.isEmpty else { + return + } + + let codeRequiredExp = expectation(description: "code required") + let delegate = SignUpV2DelegateSpy(expectation: codeRequiredExp) + let parameters = signUpParameters(username: username, password: password) + + markEmailCheckpoint() + sut.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [codeRequiredExp]) + try skipIfEmailOTPThrottled(delegate.error) + + guard delegate.onCodeRequiredCalled, + let codeRequiredState = delegate.codeRequiredState + else { + XCTFail("OTP not sent") + return + } + checkCodeRequired(delegate) + + guard let code = await retrieveCodeFor(email: username, password: password) else { + XCTFail("OTP code could not be retrieved") + return + } + + let signInAfterSignUpRequiredExp = expectation(description: "sign in after sign up required") + delegate.reset(expectation: signInAfterSignUpRequiredExp) + codeRequiredState.submitCode(code, delegate: delegate) + + await fulfillment(of: [signInAfterSignUpRequiredExp]) + + XCTAssertTrue(delegate.onSignInAfterSignUpRequiredCalled) + XCTAssertEqual(delegate.scenario, .signUp) + XCTAssertNotNil(delegate.signInAfterSignUpState) + XCTAssertFalse(delegate.onFlowCompletedCalled) + } + + // Use case 1.1.10. Sign up - with Email & Password, User already exists with given email as email-pw account + @MainActor + func test_signUpWithEmailPassword_andAgainSameEmail_fails() async throws { + guard let sut = initialisePublicClientApplication( + clientIdType: .password, + customAuthorityURLFormat: .tenantSubdomainTenantId + ), let username = retrieveUsernameForSignInUsernameAndPassword() else { + XCTFail("Missing information") + return + } + + let signUpFailureExp = expectation(description: "sign-up with existing email fails") + let delegate = SignUpV2DelegateSpy(expectation: signUpFailureExp) + let parameters = signUpParameters( + username: username, + password: generateRandomPassword() + ) + + sut.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [signUpFailureExp]) + + XCTAssertTrue(delegate.onFlowErrorCalled) + XCTAssertEqual(delegate.scenario, .signUp) + XCTAssertEqual(delegate.error?.isUserAlreadyExists, true) + } + + // Use case 1.1.11. Sign up - with Email & Password, User already exists with given email as social account + @MainActor + func test_signUpWithEmailPassword_socialAccount_fails() async throws { + throw XCTSkip("Skipping test as it requires a Social account, not present in MSIDLAB") + } + + // Use case 1.1.12. Sign up - with Email & Password, Developer makes a request with invalid format email address + @MainActor + func test_signUpWithEmailPassword_invalidEmail_fails() async throws { + guard let sut = initialisePublicClientApplication( + clientIdType: .password, + customAuthorityURLFormat: .tenantSubdomainTenantId + ) else { + XCTFail("Missing information") + return + } + + let signUpFailureExp = expectation(description: "sign-up with invalid format email fails") + let delegate = SignUpV2DelegateSpy(expectation: signUpFailureExp) + let parameters = signUpParameters(username: "invalid", password: generateRandomPassword()) + + sut.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [signUpFailureExp]) + + XCTAssertTrue(delegate.onFlowErrorCalled) + XCTAssertEqual(delegate.scenario, .signUp) + XCTAssertEqual(delegate.error?.isInvalidUsername, true) + } + + // 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 + @MainActor + func test_signUpWithEmailPassword_invalidPassword_fails() async throws { + guard let sut = initialisePublicClientApplication( + clientIdType: .password, + customAuthorityURLFormat: .tenantSubdomainTenantId + ) else { + XCTFail("Missing information") + return + } + + let signUpFailureExp = expectation(description: "sign-up with invalid password complexity fails") + let delegate = SignUpV2DelegateSpy(expectation: signUpFailureExp) + let parameters = signUpParameters(username: generateSignUpRandomEmail(), password: "invalid") + + sut.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [signUpFailureExp]) + + XCTAssertTrue(delegate.onFlowErrorCalled) + XCTAssertEqual(delegate.scenario, .signUp) + XCTAssertEqual(delegate.error?.isInvalidPassword, true) + } + + private func signUpParameters( + username: String, + password: String, + attributes: [String: Any]? = nil + ) -> MSALNativeAuthSignUpParametersV2 { + let parameters = MSALNativeAuthSignUpParametersV2(username: username) + parameters.password = password + parameters.attributes = attributes + parameters.correlationId = correlationId + return parameters + } + + @MainActor + private func startSignUpAndExpectCodeRequired( + application: MSALNativeAuthPublicClientApplication, + username: String, + password: String + ) async throws -> MSALNativeAuthCodeRequiredState? { + let codeRequiredExp = expectation(description: "code required") + let delegate = SignUpV2DelegateSpy(expectation: codeRequiredExp) + let parameters = signUpParameters(username: username, password: password) + + markEmailCheckpoint() + application.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [codeRequiredExp]) + try skipIfEmailOTPThrottled(delegate.error) + + guard delegate.onCodeRequiredCalled, + let codeRequiredState = delegate.codeRequiredState + else { + XCTFail("OTP not sent") + return nil + } + checkCodeRequired(delegate) + return codeRequiredState + } + + private func checkCodeRequired(_ delegate: SignUpV2DelegateSpy) { + XCTAssertEqual(delegate.scenario, .signUp) + XCTAssertEqual(delegate.channelTargetType?.isEmailType, true) + XCTAssertFalse(delegate.sentTo?.isEmpty ?? true) + XCTAssertGreaterThan(delegate.codeLength, 0) + } + + @MainActor + private func continueSignInAfterSignUp( + state: MSALNativeAuthSignInAfterSignUpState, + delegate: SignUpV2DelegateSpy, + username: String + ) async throws { + let flowCompletedExp = expectation(description: "sign in after sign up completed") + delegate.reset(expectation: flowCompletedExp) + + let parameters = MSALNativeAuthSignInAfterSignUpParameters() + state.signIn(parameters: parameters, delegate: delegate) + + await fulfillment(of: [flowCompletedExp]) + + XCTAssertTrue(delegate.onFlowCompletedCalled) + XCTAssertEqual(delegate.scenario, .signUp) + XCTAssertNotNil(delegate.result?.idToken) + XCTAssertNotNil(delegate.result?.account.accountClaims) + XCTAssertEqual(delegate.result?.account.username?.lowercased(), username.lowercased()) + } +} diff --git a/MSAL/test/integration/native_auth/end_to_end/sign_up/MSALNativeAuthSignUpUsernameV2EndToEndTests.swift b/MSAL/test/integration/native_auth/end_to_end/sign_up/MSALNativeAuthSignUpUsernameV2EndToEndTests.swift new file mode 100644 index 000000000..d20f3c992 --- /dev/null +++ b/MSAL/test/integration/native_auth/end_to_end/sign_up/MSALNativeAuthSignUpUsernameV2EndToEndTests.swift @@ -0,0 +1,551 @@ +// +// 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. + +import Foundation +import XCTest +import MSAL + +final class MSALNativeAuthSignUpUsernameV2EndToEndTests: MSALNativeAuthEndToEndBaseTestCase { + + override func setUpWithError() throws { + try super.setUpWithError() + throw XCTSkip("Sign Up V2 requires a test slice. Disable this test until api/test slice is ready.") + } + + // Hero Scenario 2.1.1. Sign up – with Email Verification (Email & Email OTP) + @MainActor + func test_signUpWithCode_withEmailVerification_succeeds() async throws { + guard let sut = initialisePublicClientApplication( + clientIdType: .code, + customAuthorityURLFormat: .tenantSubdomainTenantId + ) else { + XCTFail("Missing information") + return + } + + let password = generateRandomPassword() + let usernameOTP = await createEmailProviderAccount(password: password) + guard !usernameOTP.isEmpty else { + return + } + + let codeRequiredExp = expectation(description: "code required") + let delegate = SignUpV2DelegateSpy(expectation: codeRequiredExp) + let parameters = MSALNativeAuthSignUpParametersV2(username: usernameOTP) + parameters.correlationId = correlationId + + markEmailCheckpoint() + sut.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [codeRequiredExp]) + try skipIfEmailOTPThrottled(delegate.error) + + guard delegate.onCodeRequiredCalled, + let codeRequiredState = delegate.codeRequiredState + else { + XCTFail("OTP not sent") + return + } + checkCodeRequired(delegate) + + guard let code = await retrieveCodeFor(email: usernameOTP, password: password) else { + XCTFail("OTP code not retrieved from email") + return + } + + let signInAfterSignUpRequiredExp = expectation(description: "sign in after sign up required") + delegate.reset(expectation: signInAfterSignUpRequiredExp) + codeRequiredState.submitCode(code, delegate: delegate) + + await fulfillment(of: [signInAfterSignUpRequiredExp]) + + guard delegate.onSignInAfterSignUpRequiredCalled, + let signInAfterSignUpState = delegate.signInAfterSignUpState + else { + XCTFail("onSignInAfterSignUpRequired not called") + return + } + + try await continueSignInAfterSignUp( + state: signInAfterSignUpState, + delegate: delegate, + username: usernameOTP + ) + } + + // Hero Scenario 2.1.2. Sign up – with Email Verification as LAST step & Custom Attributes (Email & Email OTP) + @MainActor + func test_signUpWithCode_withEmailVerificationAsLastStepAndCustomAttributes_succeeds() async throws { + guard let sut = initialisePublicClientApplication( + clientIdType: .codeAndAttributes, + customAuthorityURLFormat: .tenantSubdomainTenantId + ) else { + XCTFail("Missing information") + return + } + + let password = generateRandomPassword() + let usernameOTP = await createEmailProviderAccount(password: password) + guard !usernameOTP.isEmpty else { + return + } + + let codeRequiredExp = expectation(description: "code required") + let delegate = SignUpV2DelegateSpy(expectation: codeRequiredExp) + let parameters = MSALNativeAuthSignUpParametersV2(username: usernameOTP) + parameters.attributes = AttributesStub.allAttributes + parameters.correlationId = correlationId + + markEmailCheckpoint() + sut.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [codeRequiredExp]) + try skipIfEmailOTPThrottled(delegate.error) + + guard delegate.onCodeRequiredCalled, + let codeRequiredState = delegate.codeRequiredState + else { + XCTFail("OTP not sent") + return + } + checkCodeRequired(delegate) + + guard let code = await retrieveCodeFor(email: usernameOTP, password: password) else { + XCTFail("OTP code not retrieved from email") + return + } + + let signInAfterSignUpRequiredExp = expectation(description: "sign in after sign up required") + delegate.reset(expectation: signInAfterSignUpRequiredExp) + codeRequiredState.submitCode(code, delegate: delegate) + + await fulfillment(of: [signInAfterSignUpRequiredExp]) + + guard delegate.onSignInAfterSignUpRequiredCalled, + let signInAfterSignUpState = delegate.signInAfterSignUpState + else { + XCTFail("onSignInAfterSignUpRequired not called") + return + } + + try await continueSignInAfterSignUp( + state: signInAfterSignUpState, + delegate: delegate, + username: usernameOTP + ) + } + + // Hero Scenario 2.1.3. Sign up – with Email Verification as FIRST step & Custom Attributes (Email & Email OTP) + @MainActor + func test_signUpWithCode_withEmailVerificationAsFirstStepAndCustomAttributes_succeeds() async throws { + guard let sut = initialisePublicClientApplication( + clientIdType: .codeAndAttributes, + customAuthorityURLFormat: .tenantSubdomainTenantId + ) else { + XCTFail("Missing information") + return + } + + let password = generateRandomPassword() + let usernameOTP = await createEmailProviderAccount(password: password) + guard !usernameOTP.isEmpty else { + return + } + + let codeRequiredExp = expectation(description: "code required") + let delegate = SignUpV2DelegateSpy(expectation: codeRequiredExp) + let parameters = MSALNativeAuthSignUpParametersV2(username: usernameOTP) + parameters.correlationId = correlationId + + markEmailCheckpoint() + sut.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [codeRequiredExp]) + try skipIfEmailOTPThrottled(delegate.error) + + guard delegate.onCodeRequiredCalled, + let codeRequiredState = delegate.codeRequiredState + else { + XCTFail("OTP not sent") + return + } + checkCodeRequired(delegate) + + guard let code = await retrieveCodeFor(email: usernameOTP, password: password) else { + XCTFail("OTP code not retrieved from email") + return + } + + let attributesRequiredExp = expectation(description: "attributes required") + delegate.reset(expectation: attributesRequiredExp) + codeRequiredState.submitCode(code, delegate: delegate) + + await fulfillment(of: [attributesRequiredExp]) + + guard delegate.onAttributesRequiredCalled, + let attributesRequiredState = delegate.attributesRequiredState + else { + XCTFail("onAttributesRequired not called") + return + } + XCTAssertEqual(delegate.scenario, .signUp) + XCTAssertFalse(attributesRequiredState.attributes.isEmpty) + + let signInAfterSignUpRequiredExp = expectation(description: "sign in after sign up required") + delegate.reset(expectation: signInAfterSignUpRequiredExp) + attributesRequiredState.submitAttributes(AttributesStub.allAttributes, delegate: delegate) + + await fulfillment(of: [signInAfterSignUpRequiredExp]) + + guard delegate.onSignInAfterSignUpRequiredCalled, + let signInAfterSignUpState = delegate.signInAfterSignUpState + else { + XCTFail("onSignInAfterSignUpRequired not called") + return + } + + try await continueSignInAfterSignUp( + state: signInAfterSignUpState, + delegate: delegate, + username: usernameOTP + ) + } + + // Hero Scenario 2.1.4. Sign up – with Email Verification as FIRST step & Custom Attributes over MULTIPLE screens (Email & Email OTP) + @MainActor + func test_signUpWithCode_withEmailVerificationAsFirstStepAndCustomAttributesOverMultipleScreens_succeeds() async throws { + guard let sut = initialisePublicClientApplication( + clientIdType: .codeAndAttributes, + customAuthorityURLFormat: .tenantSubdomainTenantId + ) else { + XCTFail("Missing information") + return + } + + let password = generateRandomPassword() + let usernameOTP = await createEmailProviderAccount(password: password) + guard !usernameOTP.isEmpty else { + return + } + + let codeRequiredExp = expectation(description: "code required") + let delegate = SignUpV2DelegateSpy(expectation: codeRequiredExp) + let parameters = MSALNativeAuthSignUpParametersV2(username: usernameOTP) + parameters.correlationId = correlationId + + markEmailCheckpoint() + sut.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [codeRequiredExp]) + try skipIfEmailOTPThrottled(delegate.error) + + guard delegate.onCodeRequiredCalled, + let codeRequiredState = delegate.codeRequiredState + else { + XCTFail("OTP not sent") + return + } + checkCodeRequired(delegate) + + guard let code = await retrieveCodeFor(email: usernameOTP, password: password) else { + XCTFail("OTP code not retrieved from email") + return + } + + let firstAttributesExp = expectation(description: "first attributes step") + delegate.reset(expectation: firstAttributesExp) + codeRequiredState.submitCode(code, delegate: delegate) + + await fulfillment(of: [firstAttributesExp]) + + guard delegate.onAttributesRequiredCalled, + let firstAttributesState = delegate.attributesRequiredState + else { + XCTFail("onAttributesRequired not called") + return + } + + let secondAttributesExp = expectation(description: "second attributes step") + delegate.reset(expectation: secondAttributesExp) + firstAttributesState.submitAttributes(AttributesStub.attribute1, delegate: delegate) + + await fulfillment(of: [secondAttributesExp]) + + let secondAttributesState = delegate.attributesRequiredState + let invalidAttributesState = delegate.attributesInvalidState + let finalSubmitExp = expectation(description: "final attributes submission") + delegate.reset(expectation: finalSubmitExp) + + if let secondAttributesState = secondAttributesState { + secondAttributesState.submitAttributes(AttributesStub.attribute2, delegate: delegate) + } else if let invalidAttributesState = invalidAttributesState { + invalidAttributesState.submitAttributes(AttributesStub.attribute2, delegate: delegate) + } else { + XCTFail("Expected another attributes continuation") + return + } + + await fulfillment(of: [finalSubmitExp]) + + guard delegate.onSignInAfterSignUpRequiredCalled, + let signInAfterSignUpState = delegate.signInAfterSignUpState + else { + XCTFail("onSignInAfterSignUpRequired not called") + return + } + + try await continueSignInAfterSignUp( + state: signInAfterSignUpState, + delegate: delegate, + username: usernameOTP + ) + } + + // use case 2.1.5. Sign up - with Email & OTP resend email OTP + @MainActor + func test_signUpWithEmailOTP_resendEmail_success() async throws { + guard let sut = initialisePublicClientApplication( + clientIdType: .code, + customAuthorityURLFormat: .tenantSubdomainTenantId + ) else { + XCTFail("Missing information") + return + } + + let password = generateRandomPassword() + let username = await createEmailProviderAccount(password: password) + guard !username.isEmpty else { + return + } + + let codeRequiredExp = expectation(description: "code required") + let delegate = SignUpV2DelegateSpy(expectation: codeRequiredExp) + let parameters = MSALNativeAuthSignUpParametersV2(username: username) + parameters.correlationId = correlationId + + markEmailCheckpoint() + sut.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [codeRequiredExp]) + try skipIfEmailOTPThrottled(delegate.error) + + guard delegate.onCodeRequiredCalled, + let codeRequiredState = delegate.codeRequiredState + else { + XCTFail("OTP not sent") + return + } + checkCodeRequired(delegate) + + guard let code1 = await retrieveCodeFor(email: username, password: password) else { + XCTFail("OTP code could not be retrieved") + return + } + + let resendCodeRequiredExp = expectation(description: "code required again") + delegate.reset(expectation: resendCodeRequiredExp) + + markEmailCheckpoint() + codeRequiredState.resendCode(delegate: delegate) + + await fulfillment(of: [resendCodeRequiredExp]) + try skipIfEmailOTPThrottled(delegate.error) + + guard delegate.onCodeRequiredCalled else { + XCTFail("Resend code method should have been called") + return + } + + guard let code2 = await retrieveCodeFor(email: username, password: password) else { + XCTFail("OTP code could not be retrieved") + return + } + + XCTAssertNotEqual(code1, code2, "Resent code should be different from the original code") + } + + // use case 2.1.6. Sign Up - with Email & OTP, User already exists with given email as email-otp account + @MainActor + func test_signUpWithEmailOTP_andExistingAccount() async throws { + guard let sut = initialisePublicClientApplication( + clientIdType: .code, + customAuthorityURLFormat: .tenantSubdomainTenantId + ), let username = retrieveUsernameForSignInCode() else { + XCTFail("Missing information") + return + } + + let signUpFailureExp = expectation(description: "sign-up with existing email fails") + let delegate = SignUpV2DelegateSpy(expectation: signUpFailureExp) + let parameters = MSALNativeAuthSignUpParametersV2(username: username) + parameters.correlationId = correlationId + + sut.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [signUpFailureExp]) + + XCTAssertTrue(delegate.onFlowErrorCalled) + XCTAssertEqual(delegate.scenario, .signUp) + XCTAssertEqual(delegate.error?.isUserAlreadyExists, true) + } + + // Use case 2.1.7. Sign up - with Email & Password, User already exists with given email as social account + @MainActor + func test_signUpWithEmailPassword_socialAccount_fails() async throws { + throw XCTSkip("Skipping test as it requires a Social account, not present in MSIDLAB") + } + + // Use case 2.1.8. Sign up - with Email & OTP, Developer makes a request with invalid format email address + @MainActor + func test_signUpWithEmailPassword_invalidEmailFormat_fails() async throws { + guard let sut = initialisePublicClientApplication( + clientIdType: .code, + customAuthorityURLFormat: .tenantSubdomainTenantId + ) else { + XCTFail("Missing information") + return + } + + let signUpFailureExp = expectation(description: "sign-up with invalid email format fails") + let delegate = SignUpV2DelegateSpy(expectation: signUpFailureExp) + let parameters = MSALNativeAuthSignUpParametersV2(username: "invalid") + parameters.correlationId = correlationId + + sut.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [signUpFailureExp]) + + XCTAssertTrue(delegate.onFlowErrorCalled) + XCTAssertEqual(delegate.scenario, .signUp) + XCTAssertEqual(delegate.error?.isInvalidUsername, true) + } + + // Hero Scenario 2.1.9. Sign up – without automatic sign in (Email & Email OTP) + @MainActor + func test_signUpWithoutAutomaticSignIn() async throws { + guard let sut = initialisePublicClientApplication( + clientIdType: .code, + customAuthorityURLFormat: .tenantSubdomainTenantId + ) else { + XCTFail("Missing information") + return + } + + let password = generateRandomPassword() + let usernameOTP = await createEmailProviderAccount(password: password) + guard !usernameOTP.isEmpty else { + return + } + + let codeRequiredExp = expectation(description: "code required") + let delegate = SignUpV2DelegateSpy(expectation: codeRequiredExp) + let parameters = MSALNativeAuthSignUpParametersV2(username: usernameOTP) + parameters.correlationId = correlationId + + markEmailCheckpoint() + sut.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [codeRequiredExp]) + try skipIfEmailOTPThrottled(delegate.error) + + guard delegate.onCodeRequiredCalled, + let codeRequiredState = delegate.codeRequiredState + else { + XCTFail("OTP not sent") + return + } + checkCodeRequired(delegate) + + guard let code = await retrieveCodeFor(email: usernameOTP, password: password) else { + XCTFail("OTP code not retrieved from email") + return + } + + let signInAfterSignUpRequiredExp = expectation(description: "sign in after sign up required") + delegate.reset(expectation: signInAfterSignUpRequiredExp) + codeRequiredState.submitCode(code, delegate: delegate) + + await fulfillment(of: [signInAfterSignUpRequiredExp]) + + XCTAssertTrue(delegate.onSignInAfterSignUpRequiredCalled) + XCTAssertEqual(delegate.scenario, .signUp) + XCTAssertNotNil(delegate.signInAfterSignUpState) + XCTAssertFalse(delegate.onFlowCompletedCalled) + } + + // Use case 2.1.10 Sign up - with Email & Password, Server requires password + // authentication, which is not supported by the developer (aka redirect flow) + @MainActor + func test_signUpWithEmailPassword_butChallengeTypeOOB_fails() async throws { + guard let sut = initialisePublicClientApplication( + clientIdType: .password, + challengeTypes: [.OOB], + customAuthorityURLFormat: .tenantSubdomainTenantId + ) else { + XCTFail("Missing information") + return + } + + let signUpFailureExp = expectation(description: "sign-up with invalid challenge type fails") + let delegate = SignUpV2DelegateSpy(expectation: signUpFailureExp) + let parameters = MSALNativeAuthSignUpParametersV2(username: generateSignUpRandomEmail()) + parameters.password = generateRandomPassword() + parameters.correlationId = correlationId + + sut.signUpV2(parameters: parameters, delegate: delegate) + + await fulfillment(of: [signUpFailureExp]) + + XCTAssertTrue(delegate.onFlowErrorCalled) + XCTAssertEqual(delegate.scenario, .signUp) + XCTAssertEqual(delegate.error?.isBrowserRequired, true) + } + + private func checkCodeRequired(_ delegate: SignUpV2DelegateSpy) { + XCTAssertEqual(delegate.scenario, .signUp) + XCTAssertEqual(delegate.channelTargetType?.isEmailType, true) + XCTAssertFalse(delegate.sentTo?.isEmpty ?? true) + XCTAssertGreaterThan(delegate.codeLength, 0) + } + + @MainActor + private func continueSignInAfterSignUp( + state: MSALNativeAuthSignInAfterSignUpState, + delegate: SignUpV2DelegateSpy, + username: String + ) async throws { + let flowCompletedExp = expectation(description: "sign in after sign up completed") + delegate.reset(expectation: flowCompletedExp) + + let parameters = MSALNativeAuthSignInAfterSignUpParameters() + state.signIn(parameters: parameters, delegate: delegate) + + await fulfillment(of: [flowCompletedExp]) + + XCTAssertTrue(delegate.onFlowCompletedCalled) + XCTAssertEqual(delegate.scenario, .signUp) + XCTAssertNotNil(delegate.result?.idToken) + XCTAssertEqual(delegate.result?.account.username?.lowercased(), username.lowercased()) + } +} diff --git a/MSAL/test/integration/native_auth/end_to_end/sign_up/SignUpDelegateSpiesV2.swift b/MSAL/test/integration/native_auth/end_to_end/sign_up/SignUpDelegateSpiesV2.swift new file mode 100644 index 000000000..bfe34ef9e --- /dev/null +++ b/MSAL/test/integration/native_auth/end_to_end/sign_up/SignUpDelegateSpiesV2.swift @@ -0,0 +1,140 @@ +// +// 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. + +import Foundation +import XCTest +import MSAL + +@MainActor +final class SignUpV2DelegateSpy: NSObject, + MSALNativeAuthCodeRequiredDelegate, + MSALNativeAuthAttributesRequiredDelegate, + MSALNativeAuthAttributesInvalidDelegate, + MSALNativeAuthSignInAfterSignUpRequiredDelegate { + + private var expectation: XCTestExpectation + + private(set) var onCodeRequiredCalled = false + private(set) var onAttributesRequiredCalled = false + private(set) var onAttributesInvalidCalled = false + private(set) var onSignInAfterSignUpRequiredCalled = false + private(set) var onFlowCompletedCalled = false + private(set) var onFlowErrorCalled = false + + private(set) var codeRequiredState: MSALNativeAuthCodeRequiredState? + private(set) var attributesRequiredState: MSALNativeAuthAttributesRequiredState? + private(set) var attributesInvalidState: MSALNativeAuthAttributesInvalidState? + private(set) var signInAfterSignUpState: MSALNativeAuthSignInAfterSignUpState? + private(set) var result: MSALNativeAuthUserAccountResult? + private(set) var error: MSALNativeAuthFlowError? + private(set) var scenario: MSALNativeAuthFlowScenario? + private(set) var sentTo: String? + private(set) var channelTargetType: MSALNativeAuthChannelType? + private(set) var codeLength = 0 + private(set) var requiredAttributes: [MSALNativeAuthRequiredAttribute] = [] + private(set) var invalidAttributeNames: [String] = [] + + init(expectation: XCTestExpectation) { + self.expectation = expectation + super.init() + } + + func reset(expectation: XCTestExpectation) { + self.expectation = expectation + onCodeRequiredCalled = false + onAttributesRequiredCalled = false + onAttributesInvalidCalled = false + onSignInAfterSignUpRequiredCalled = false + onFlowCompletedCalled = false + onFlowErrorCalled = false + codeRequiredState = nil + attributesRequiredState = nil + attributesInvalidState = nil + signInAfterSignUpState = nil + result = nil + error = nil + scenario = nil + sentTo = nil + channelTargetType = nil + codeLength = 0 + requiredAttributes = [] + invalidAttributeNames = [] + } + + func onCodeRequired(state: MSALNativeAuthCodeRequiredState, scenario: MSALNativeAuthFlowScenario) { + onCodeRequiredCalled = true + codeRequiredState = state + sentTo = state.sentTo + channelTargetType = state.channel + codeLength = state.codeLength + self.scenario = scenario + + expectation.fulfill() + } + + func onAttributesRequired(state: MSALNativeAuthAttributesRequiredState, scenario: MSALNativeAuthFlowScenario) { + onAttributesRequiredCalled = true + attributesRequiredState = state + requiredAttributes = state.attributes + self.scenario = scenario + + expectation.fulfill() + } + + func onAttributesInvalid(state: MSALNativeAuthAttributesInvalidState, scenario: MSALNativeAuthFlowScenario) { + onAttributesInvalidCalled = true + attributesInvalidState = state + invalidAttributeNames = state.attributeNames + self.scenario = scenario + + expectation.fulfill() + } + + func onSignInAfterSignUpRequired( + state: MSALNativeAuthSignInAfterSignUpState, + scenario: MSALNativeAuthFlowScenario + ) { + onSignInAfterSignUpRequiredCalled = true + signInAfterSignUpState = state + self.scenario = scenario + + expectation.fulfill() + } + + func onFlowCompleted(result: MSALNativeAuthUserAccountResult, scenario: MSALNativeAuthFlowScenario) { + onFlowCompletedCalled = true + self.result = result + self.scenario = scenario + + expectation.fulfill() + } + + func onFlowError(error: MSALNativeAuthFlowError, scenario: MSALNativeAuthFlowScenario) { + onFlowErrorCalled = true + self.error = error + self.scenario = scenario + + expectation.fulfill() + } +}