diff --git a/.github/workflows/swiftui-auth.yml b/.github/workflows/swiftui-auth.yml index df3b364249..4b65be8fad 100644 --- a/.github/workflows/swiftui-auth.yml +++ b/.github/workflows/swiftui-auth.yml @@ -22,35 +22,76 @@ permissions: contents: read jobs: - swiftui-auth: + # Package Unit Tests (standalone, no emulator needed) + unit-tests: + name: Package Unit Tests runs-on: macos-15 - timeout-minutes: 60 + timeout-minutes: 20 steps: - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + + - name: Install xcpretty + run: gem install xcpretty + + - name: Select Xcode version + run: sudo xcode-select -switch /Applications/Xcode_16.4.app/Contents/Developer + + - name: Run FirebaseSwiftUI Package Unit Tests + run: | + set -o pipefail + xcodebuild test \ + -scheme FirebaseUI-Package \ + -destination 'platform=iOS Simulator,name=iPhone 16 Pro' \ + -enableCodeCoverage YES \ + -resultBundlePath FirebaseSwiftUIPackageTests.xcresult | tee FirebaseSwiftUIPackageTests.log | xcpretty --test --color --simple + + - name: Upload test logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: unit-tests-logs + path: FirebaseSwiftUIPackageTests.log + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: unit-tests-results + path: FirebaseSwiftUIPackageTests.xcresult + + # Integration Tests (requires emulator) + integration-tests: + name: Integration Tests + runs-on: macos-15 + timeout-minutes: 20 + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a name: Install Node.js 20 with: node-version: '20' + - uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b with: distribution: 'temurin' java-version: '17' + - name: Install Firebase - run: | - sudo npm i -g firebase-tools + run: sudo npm i -g firebase-tools + - name: Start Firebase Emulator run: | - sudo chown -R 501:20 "/Users/runner/.npm" && cd ./samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample && ./start-firebase-emulator.sh + sudo chown -R 501:20 "/Users/runner/.npm" + cd ./samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample + ./start-firebase-emulator.sh + - name: Install xcpretty run: gem install xcpretty + - name: Select Xcode version - run: | - sudo xcode-select -switch /Applications/Xcode_16.4.app/Contents/Developer - - name: Run FirebaseSwiftUI Package Unit Tests - run: | - set -o pipefail - xcodebuild test -scheme FirebaseUI-Package -destination 'platform=iOS Simulator,name=iPhone 16 Pro' -enableCodeCoverage YES -resultBundlePath FirebaseSwiftUIPackageTests.xcresult | tee FirebaseSwiftUIPackageTests.log | xcpretty --test --color --simple - # Build for integration tests (builds app + integration test bundle) + run: sudo xcode-select -switch /Applications/Xcode_16.4.app/Contents/Developer + - name: Build for Integration Tests run: | cd ./samples/swiftui/FirebaseSwiftUIExample @@ -59,7 +100,7 @@ jobs: -scheme FirebaseSwiftUIExampleTests \ -destination 'platform=iOS Simulator,name=iPhone 16 Pro' \ -enableCodeCoverage YES | xcpretty --color --simple - # Run integration tests + - name: Run Integration Tests run: | cd ./samples/swiftui/FirebaseSwiftUIExample @@ -69,7 +110,54 @@ jobs: -destination 'platform=iOS Simulator,name=iPhone 16 Pro' \ -enableCodeCoverage YES \ -resultBundlePath FirebaseSwiftUIExampleTests.xcresult | tee FirebaseSwiftUIExampleTests.log | xcpretty --test --color --simple - # Build for UI tests (reuses app build, builds UI test bundle) + + - name: Upload test logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: integration-tests-logs + path: samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests.log + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: integration-tests-results + path: samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests.xcresult + + # UI Tests (requires emulator) + ui-tests: + name: UI Tests + runs-on: macos-15 + timeout-minutes: 30 + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a + name: Install Node.js 20 + with: + node-version: '20' + + - uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b + with: + distribution: 'temurin' + java-version: '17' + + - name: Install Firebase + run: sudo npm i -g firebase-tools + + - name: Start Firebase Emulator + run: | + sudo chown -R 501:20 "/Users/runner/.npm" + cd ./samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample + ./start-firebase-emulator.sh + + - name: Install xcpretty + run: gem install xcpretty + + - name: Select Xcode version + run: sudo xcode-select -switch /Applications/Xcode_16.4.app/Contents/Developer + - name: Build for UI Tests run: | cd ./samples/swiftui/FirebaseSwiftUIExample @@ -78,8 +166,8 @@ jobs: -scheme FirebaseSwiftUIExampleUITests \ -destination 'platform=iOS Simulator,name=iPhone 16 Pro' \ -enableCodeCoverage YES | xcpretty --color --simple - # Run UI tests - - name: Run View UI Tests + + - name: Run UI Tests run: | cd ./samples/swiftui/FirebaseSwiftUIExample set -o pipefail @@ -90,30 +178,17 @@ jobs: -maximum-concurrent-test-simulator-destinations 2 \ -enableCodeCoverage YES \ -resultBundlePath FirebaseSwiftUIExampleUITests.xcresult | tee FirebaseSwiftUIExampleUITests.log | xcpretty --test --color --simple + - name: Upload test logs if: failure() uses: actions/upload-artifact@v4 with: - name: swiftui-auth-test-logs - path: | - FirebaseSwiftUIPackageTests.log - samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests.log - samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.log - - name: Upload FirebaseSwiftUIExampleUITests.xcresult bundle - if: failure() - uses: actions/upload-artifact@v4 - with: - name: FirebaseSwiftUIExampleUITests.xcresult - path: samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.xcresult - - name: Upload FirebaseSwiftUIExampleTests.xcresult bundle - if: failure() - uses: actions/upload-artifact@v4 - with: - name: FirebaseSwiftUIExampleTests.xcresult - path: samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests.xcresult - - name: Upload FirebaseSwiftUIPackageTests.xcresult bundle + name: ui-tests-logs + path: samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.log + + - name: Upload test results if: failure() uses: actions/upload-artifact@v4 with: - name: FirebaseSwiftUIPackageTests.xcresult - path: FirebaseSwiftUIPackageTests.xcresult \ No newline at end of file + name: ui-tests-results + path: samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.xcresult \ No newline at end of file diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index 43cb5fa238..360daa74d6 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -152,8 +152,8 @@ public final class AuthService { var emailSignInEnabled = false private var providers: [AuthProviderUI] = [] - public func registerProvider(provider: AuthProviderUI) { - providers.append(provider) + public func registerProvider(providerWithButton: AuthProviderUI) { + providers.append(providerWithButton) } public func renderButtons(spacing: CGFloat = 16) -> AnyView { diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift index 50f223a16b..9be336619a 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift @@ -38,9 +38,8 @@ extension SignedInView: View { .fontWeight(.bold) .padding() .accessibilityIdentifier("signed-in-text") - Text(authService.string.accountSettingsEmailLabel) - Text("\(authService.currentUser?.email ?? "Unknown")") - + Text("as:") + Text("\(authService.currentUser?.email ?? authService.currentUser?.displayName ?? "Unknown")") if authService.currentUser?.isEmailVerified == false { VerifyEmailView() } diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AuthService+Facebook.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AuthService+Facebook.swift index a37f1bdfed..9122e5b544 100644 --- a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AuthService+Facebook.swift +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AuthService+Facebook.swift @@ -25,7 +25,7 @@ public extension AuthService { @discardableResult func withFacebookSignIn(scopes scopes: [String]? = nil) -> AuthService { FacebookProviderAuthUI.configureProvider(scopes: scopes) - registerProvider(provider: FacebookProviderAuthUI.shared) + registerProvider(providerWithButton: FacebookProviderAuthUI.shared) return self } } diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift index eaa7606c27..c41bca579d 100644 --- a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift @@ -91,6 +91,7 @@ extension SignInWithFacebookButton: View { .background(Color.blue) .cornerRadius(8) } + .accessibilityIdentifier("sign-in-with-facebook-button") .alert(isPresented: $showCanceledAlert) { Alert( title: Text(authService.string.facebookLoginCancelledLabel), diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AuthService+Google.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AuthService+Google.swift index ddcc1a2223..e1c2d2930a 100644 --- a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AuthService+Google.swift +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AuthService+Google.swift @@ -25,7 +25,7 @@ public extension AuthService { @discardableResult func withGoogleSignIn(scopes scopes: [String]? = nil) -> AuthService { let clientID = auth.app?.options.clientID ?? "" - registerProvider(provider: GoogleProviderAuthUI(scopes: scopes, clientID: clientID)) + registerProvider(providerWithButton: GoogleProviderAuthUI(scopes: scopes, clientID: clientID)) return self } } diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift index 7cc5a9131f..02c270f732 100644 --- a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift @@ -42,6 +42,7 @@ extension SignInWithGoogleButton: View { try await authService.signIn(googleProvider) } } + .accessibilityIdentifier("sign-in-with-google-button") } } diff --git a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/AuthService+Phone.swift b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/AuthService+Phone.swift index 079381d7f6..b04384b49c 100644 --- a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/AuthService+Phone.swift +++ b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/AuthService+Phone.swift @@ -24,7 +24,7 @@ import FirebaseAuthSwiftUI public extension AuthService { @discardableResult func withPhoneSignIn() -> AuthService { - registerProvider(provider: PhoneAuthProviderAuthUI()) + registerProvider(providerWithButton: PhoneAuthProviderAuthUI()) return self } } diff --git a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthButtonView.swift b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthButtonView.swift index 528519809a..9ca1a5b41b 100644 --- a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthButtonView.swift +++ b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthButtonView.swift @@ -38,6 +38,7 @@ extension PhoneAuthButtonView: View { .background(Color.green.opacity(0.8)) // Light green .cornerRadius(8) } + .accessibilityIdentifier("sign-in-with-phone-button") } } diff --git a/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Resources/Media.xcassets/Contents.json b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Resources/Media.xcassets/Contents.json new file mode 100644 index 0000000000..6cc12269b3 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Resources/Media.xcassets/Contents.json @@ -0,0 +1,7 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} + diff --git a/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Resources/Media.xcassets/twitter_logo.imageset/Contents.json b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Resources/Media.xcassets/twitter_logo.imageset/Contents.json new file mode 100644 index 0000000000..bd4799c6c3 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Resources/Media.xcassets/twitter_logo.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "ic_twitter-white.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} + diff --git a/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Resources/Media.xcassets/twitter_logo.imageset/ic_twitter-white.png b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Resources/Media.xcassets/twitter_logo.imageset/ic_twitter-white.png new file mode 100644 index 0000000000..2609e58006 Binary files /dev/null and b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Resources/Media.xcassets/twitter_logo.imageset/ic_twitter-white.png differ diff --git a/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Services/AccountService+Twitter.swift b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Services/AccountService+Twitter.swift new file mode 100644 index 0000000000..734ddfcba3 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Services/AccountService+Twitter.swift @@ -0,0 +1,46 @@ +// +// AccountService+Twitter.swift +// FirebaseUI +// +// Created by Russell Wheatley on 10/10/2025. +// + +@preconcurrency import FirebaseAuth +import FirebaseAuthSwiftUI +import Observation + +protocol TwitterOperationReauthentication { + var twitterProvider: TwitterProviderSwift { get } +} + +extension TwitterOperationReauthentication { + @MainActor func reauthenticate() async throws -> AuthenticationToken { + guard let user = Auth.auth().currentUser else { + throw AuthServiceError.reauthenticationRequired("No user currently signed-in") + } + + do { + let credential = try await twitterProvider.createAuthCredential() + try await user.reauthenticate(with: credential) + + return .firebase("") + } catch { + throw AuthServiceError.signInFailed(underlying: error) + } + } +} + +@MainActor +class TwitterDeleteUserOperation: AuthenticatedOperation, + @preconcurrency TwitterOperationReauthentication { + let twitterProvider: TwitterProviderSwift + init(twitterProvider: TwitterProviderSwift) { + self.twitterProvider = twitterProvider + } + + func callAsFunction(on user: User) async throws { + try await callAsFunction(on: user) { + try await user.delete() + } + } +} diff --git a/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Services/AuthService+Twitter.swift b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Services/AuthService+Twitter.swift new file mode 100644 index 0000000000..8a75e10369 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Services/AuthService+Twitter.swift @@ -0,0 +1,31 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +// AuthService+Twitter.swift +// FirebaseUI +// +// Created by Russell Wheatley on 01/05/2025. +// + +import FirebaseAuthSwiftUI + +public extension AuthService { + @discardableResult + func withTwitterSignIn(_ provider: TwitterProviderSwift? = nil) -> AuthService { + registerProvider(providerWithButton: TwitterProviderAuthUI(provider: provider ?? + TwitterProviderSwift())) + return self + } +} diff --git a/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Services/TwitterProviderAuthUI.swift b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Services/TwitterProviderAuthUI.swift new file mode 100644 index 0000000000..627d8de1b9 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Services/TwitterProviderAuthUI.swift @@ -0,0 +1,64 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import FirebaseAuth +import FirebaseAuthSwiftUI +import FirebaseCore +import SwiftUI + +public class TwitterProviderSwift: AuthProviderSwift, DeleteUserSwift { + public let scopes: [String] + let providerId = "twitter.com" + + public init(scopes: [String] = ["user.readwrite"]) { + self.scopes = scopes + } + + @MainActor public func createAuthCredential() async throws -> AuthCredential { + let provider = OAuthProvider(providerID: providerId) + return try await withCheckedThrowingContinuation { continuation in + provider.getCredentialWith(nil) { credential, error in + if let error { + continuation + .resume(throwing: AuthServiceError.signInFailed(underlying: error)) + } else if let credential { + continuation.resume(returning: credential) + } else { + continuation + .resume(throwing: AuthServiceError + .invalidCredentials("Twitter did not provide a valid AuthCredential")) + } + } + } + } + + public func deleteUser(user: User) async throws { + let operation = TwitterDeleteUserOperation(twitterProvider: self) + try await operation(on: user) + } +} + +public class TwitterProviderAuthUI: AuthProviderUI { + public var provider: AuthProviderSwift + + public init(provider: AuthProviderSwift) { + self.provider = provider + } + + public let id: String = "twitter.com" + + @MainActor public func authButton() -> AnyView { + AnyView(SignInWithTwitterButton(provider: provider)) + } +} diff --git a/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Views/SignInWithTwitterButton.swift b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Views/SignInWithTwitterButton.swift new file mode 100644 index 0000000000..e9222fad55 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Views/SignInWithTwitterButton.swift @@ -0,0 +1,53 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import FirebaseAuthSwiftUI +import SwiftUI + +/// A button for signing in with Twitter/X +@MainActor +public struct SignInWithTwitterButton { + @Environment(AuthService.self) private var authService + let provider: AuthProviderSwift + public init(provider: AuthProviderSwift) { + self.provider = provider + } +} + +extension SignInWithTwitterButton: View { + public var body: some View { + Button(action: { + Task { + try await authService.signIn(provider) + } + }) { + HStack { + Image("twitter_logo", bundle: .module) + .resizable() + .renderingMode(.template) + .scaledToFit() + .frame(width: 24, height: 24) + .foregroundColor(.white) + Text("Sign in with X") + .fontWeight(.semibold) + .foregroundColor(.white) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background(Color.black) + .cornerRadius(8) + } + .accessibilityIdentifier("sign-in-with-twitter-button") + } +} diff --git a/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Tests/FirebaseTwitterSwiftUITests/FirebaseTwitterSwiftUITests.swift b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Tests/FirebaseTwitterSwiftUITests/FirebaseTwitterSwiftUITests.swift new file mode 100644 index 0000000000..f6bb2e3ec0 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Tests/FirebaseTwitterSwiftUITests/FirebaseTwitterSwiftUITests.swift @@ -0,0 +1,20 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@testable import FirebaseTwitterSwiftUI +import Testing + +@Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. +} diff --git a/Package.swift b/Package.swift index bc11bc3aac..d51a4f9ce4 100644 --- a/Package.swift +++ b/Package.swift @@ -78,6 +78,10 @@ let package = Package( name: "FirebasePhoneAuthSwiftUI", targets: ["FirebasePhoneAuthSwiftUI"] ), + .library( + name: "FirebaseTwitterSwiftUI", + targets: ["FirebaseTwitterSwiftUI"] + ), ], dependencies: [ .package( @@ -307,5 +311,20 @@ let package = Package( dependencies: ["FirebasePhoneAuthSwiftUI"], path: "FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Tests/" ), + .target( + name: "FirebaseTwitterSwiftUI", + dependencies: [ + "FirebaseAuthSwiftUI", + ], + path: "FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources", + resources: [ + .process("Resources") + ] + ), + .testTarget( + name: "FirebaseTwitterSwiftUITests", + dependencies: ["FirebaseTwitterSwiftUI"], + path: "FirebaseSwiftUI/FirebaseTwitterSwiftUI/Tests/" + ), ] ) diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj index 812f7b770c..12a8931628 100644 --- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 4600E5542DD777BE00EED5F3 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = 4600E5532DD777BE00EED5F3 /* FirebaseCore */; }; 4607CC9C2D9BFE29009EC3F5 /* FirebaseAuthSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4607CC9B2D9BFE29009EC3F5 /* FirebaseAuthSwiftUI */; }; 4607CC9E2D9BFE29009EC3F5 /* FirebaseGoogleSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4607CC9D2D9BFE29009EC3F5 /* FirebaseGoogleSwiftUI */; }; + 4681E0002E97F22B00387C88 /* FirebaseTwitterSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4681DFFF2E97F22B00387C88 /* FirebaseTwitterSwiftUI */; }; 46CB7B252D773F2100F1FD0A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 46CB7B242D773F2100F1FD0A /* GoogleService-Info.plist */; }; 46F89C392D64B04E000F8BC0 /* FirebaseAuthSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 46F89C382D64B04E000F8BC0 /* FirebaseAuthSwiftUI */; }; 46F89C4D2D64BB9B000F8BC0 /* FirebaseAuthSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 46F89C4C2D64BB9B000F8BC0 /* FirebaseAuthSwiftUI */; }; @@ -83,6 +84,7 @@ 46F89C4D2D64BB9B000F8BC0 /* FirebaseAuthSwiftUI in Frameworks */, 4607CC9E2D9BFE29009EC3F5 /* FirebaseGoogleSwiftUI in Frameworks */, 8D808CB92DB081F900D2293F /* FirebasePhoneAuthSwiftUI in Frameworks */, + 4681E0002E97F22B00387C88 /* FirebaseTwitterSwiftUI in Frameworks */, 4607CC9C2D9BFE29009EC3F5 /* FirebaseAuthSwiftUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -161,6 +163,7 @@ 4607CC9D2D9BFE29009EC3F5 /* FirebaseGoogleSwiftUI */, 8D808CB62DB0811900D2293F /* FirebaseFacebookSwiftUI */, 8D808CB82DB081F900D2293F /* FirebasePhoneAuthSwiftUI */, + 4681DFFF2E97F22B00387C88 /* FirebaseTwitterSwiftUI */, ); productName = FirebaseSwiftUIExample; productReference = 46F89C082D64A86C000F8BC0 /* FirebaseSwiftUIExample.app */; @@ -660,6 +663,11 @@ isa = XCSwiftPackageProductDependency; productName = FirebaseGoogleSwiftUI; }; + 4681DFFF2E97F22B00387C88 /* FirebaseTwitterSwiftUI */ = { + isa = XCSwiftPackageProductDependency; + package = 8D808CB52DB07EBD00D2293F /* XCLocalSwiftPackageReference "../../../../FirebaseUI-iOS" */; + productName = FirebaseTwitterSwiftUI; + }; 46F89C382D64B04E000F8BC0 /* FirebaseAuthSwiftUI */ = { isa = XCSwiftPackageProductDependency; productName = FirebaseAuthSwiftUI; diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift index adccdcbe4d..4c29bf0073 100644 --- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift @@ -24,14 +24,13 @@ import FirebaseAuthSwiftUI import FirebaseFacebookSwiftUI import FirebaseGoogleSwiftUI import FirebasePhoneAuthSwiftUI +import FirebaseTwitterSwiftUI import SwiftUI struct ContentView: View { let authService: AuthService init() { - Auth.auth().signInAnonymously() - let actionCodeSettings = ActionCodeSettings() actionCodeSettings.handleCodeInApp = true actionCodeSettings @@ -50,8 +49,10 @@ struct ContentView: View { ) .withGoogleSignIn() .withPhoneSignIn() + .withTwitterSignIn() .withFacebookSignIn() .withEmailSignIn() + } var body: some View { diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/TestView.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/TestView.swift index 79978406f4..5c074a2398 100644 --- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/TestView.swift +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/TestView.swift @@ -33,7 +33,7 @@ struct TestView: View { Auth.auth().useEmulator(withHost: "localhost", port: 9099) Auth.auth().settings?.isAppVerificationDisabledForTesting = true Task { - try await testCreateUser() + try signOut() } let isMfaEnabled = ProcessInfo.processInfo.arguments.contains("--mfa-enabled") @@ -56,6 +56,7 @@ struct TestView: View { ) .withGoogleSignIn() .withPhoneSignIn() + .withTwitterSignIn() .withFacebookSignIn() .withEmailSignIn() } diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/UITestUtils.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/UITestUtils.swift index 4eee5bb746..6d9586d248 100644 --- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/UITestUtils.swift +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/UITestUtils.swift @@ -8,123 +8,9 @@ import FirebaseAuth import SwiftUI // UI Test Runner keys -public let testRunner = CommandLine.arguments.contains("--auth-emulator") -let verifyEmail = CommandLine.arguments.contains("--verify-email") +public let testRunner = CommandLine.arguments.contains("--test-view-enabled") -public var testEmail: String? { - guard let emailIndex = CommandLine.arguments.firstIndex(of: "--create-user"), - CommandLine.arguments.indices.contains(emailIndex + 1) - else { return nil } - return CommandLine.arguments[emailIndex + 1] +func signOut() throws { + try Auth.auth().signOut() } -func testCreateUser() async throws { - if let email = testEmail { - let password = "123456" - let auth = Auth.auth() - let result = try await auth.createUser(withEmail: email, password: password) - if verifyEmail { - try await setEmailVerifiedInEmulator(for: result.user) - } - try auth.signOut() - } -} - -/// Marks the given Firebase `user` as email-verified **in the Auth emulator**. -/// Works in CI even if the email address doesn't exist. -/// - Parameters: -/// - user: The signed-in Firebase user you want to verify. -/// - projectID: Your emulator project ID (e.g. "demo-project" or whatever you're using locally). -/// - emulatorHost: Host:port for the Auth emulator. Defaults to localhost:9099. -func setEmailVerifiedInEmulator(for user: User, - projectID: String = "flutterfire-e2e-tests", - emulatorHost: String = "localhost:9099") async throws { - - guard let email = user.email else { - throw NSError(domain: "EmulatorError", code: 1, - userInfo: [ - NSLocalizedDescriptionKey: "User has no email; cannot look up OOB code in emulator", - ]) - } - - // 1) Trigger a verification email -> creates an OOB code in the emulator. - try await sendVerificationEmail(user) - - // 2) Read OOB codes from the emulator and find the VERIFY_EMAIL code for this user. - let base = "http://\(emulatorHost)" - let oobURL = URL(string: "\(base)/emulator/v1/projects/\(projectID)/oobCodes")! - - let (oobData, oobResp) = try await URLSession.shared.data(from: oobURL) - guard (oobResp as? HTTPURLResponse)?.statusCode == 200 else { - let body = String(data: oobData, encoding: .utf8) ?? "" - throw NSError(domain: "EmulatorError", code: 2, - userInfo: [ - NSLocalizedDescriptionKey: "Failed to fetch oobCodes. Response: \(body)", - ]) - } - - struct OobEnvelope: Decodable { let oobCodes: [OobItem] } - struct OobItem: Decodable { - let oobCode: String - let email: String - let requestType: String - let creationTime: String? // RFC3339/ISO8601; optional for safety - } - - let envelope = try JSONDecoder().decode(OobEnvelope.self, from: oobData) - - // Pick the most recent VERIFY_EMAIL code for this email (in case there are multiple). - let iso = ISO8601DateFormatter() - let codeItem = envelope.oobCodes - .filter { - $0.email.caseInsensitiveCompare(email) == .orderedSame && $0.requestType == "VERIFY_EMAIL" - } - .sorted { - let d0 = $0.creationTime.flatMap { iso.date(from: $0) } ?? .distantPast - let d1 = $1.creationTime.flatMap { iso.date(from: $0) } ?? .distantPast - return d0 > d1 - } - .first - - guard let oobCode = codeItem?.oobCode else { - throw NSError(domain: "EmulatorError", code: 3, - userInfo: [ - NSLocalizedDescriptionKey: "No VERIFY_EMAIL oobCode found for \(email) in emulator", - ]) - } - - // 3) Apply the OOB code via the emulator's identitytoolkit endpoint. - // Note: API key value does not matter when talking to the emulator. - var applyReq = URLRequest( - url: URL(string: "\(base)/identitytoolkit.googleapis.com/v1/accounts:update?key=anything")! - ) - applyReq.httpMethod = "POST" - applyReq.setValue("application/json", forHTTPHeaderField: "Content-Type") - applyReq.httpBody = try JSONSerialization.data(withJSONObject: ["oobCode": oobCode], options: []) - - let (applyData, applyResp) = try await URLSession.shared.data(for: applyReq) - guard let http = applyResp as? HTTPURLResponse, http.statusCode == 200 else { - let body = String(data: applyData, encoding: .utf8) ?? "" - throw NSError(domain: "EmulatorError", code: 4, - userInfo: [ - NSLocalizedDescriptionKey: "Applying oobCode failed. Status \((applyResp as? HTTPURLResponse)?.statusCode ?? -1). Body: \(body)", - ]) - } - - - // 4) Reload the user to reflect the new verification state. - try await user.reload() -} - -/// Small async helper to call FirebaseAuth's callback-based `sendEmailVerification` on iOS. -private func sendVerificationEmail(_ user: User) async throws { - try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in - user.sendEmailVerification { error in - if let error = error { - cont.resume(throwing: error) - } else { - cont.resume() - } - } - } -} diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift index 2f97542321..524d3511e4 100644 --- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift @@ -19,16 +19,8 @@ // Created by Russell Wheatley on 18/02/2025. // -import FirebaseAuth -import FirebaseCore import XCTest -func dismissAlert(app: XCUIApplication) { - if app.scrollViews.otherElements.buttons["Not Now"].waitForExistence(timeout: 2) { - app.scrollViews.otherElements.buttons["Not Now"].tap() - } -} - final class FirebaseSwiftUIExampleUITests: XCTestCase { override func setUpWithError() throws { continueAfterFailure = false @@ -52,12 +44,50 @@ final class FirebaseSwiftUIExampleUITests: XCTestCase { } @MainActor - func testSignInDisplaysSignedInView() throws { - let app = XCUIApplication() + func testSignInButtonsExist() throws { + let app = createTestApp() + app.launch() + + // Check for Twitter/X sign-in button + let twitterButton = app.buttons["sign-in-with-twitter-button"] + XCTAssertTrue( + twitterButton.waitForExistence(timeout: 5), + "Twitter/X sign-in button should exist" + ) + + // Check for Google sign-in button + let googleButton = app.buttons["sign-in-with-google-button"] + XCTAssertTrue( + googleButton.waitForExistence(timeout: 5), + "Google sign-in button should exist" + ) + + // Check for Facebook sign-in button + let facebookButton = app.buttons["sign-in-with-facebook-button"] + XCTAssertTrue( + facebookButton.waitForExistence(timeout: 5), + "Facebook sign-in button should exist" + ) + + // Check for Phone sign-in button + let phoneButton = app.buttons["sign-in-with-phone-button"] + XCTAssertTrue( + phoneButton.waitForExistence(timeout: 5), + "Phone sign-in button should exist" + ) + } + + @MainActor + func testSignInDisplaysSignedInView() async throws { let email = createEmail() - app.launchArguments.append("--auth-emulator") - app.launchArguments.append("--create-user") - app.launchArguments.append("\(email)") + let password = "123456" + + // Create user in test runner BEFORE launching app + // User will exist in emulator, but app starts unauthenticated + try await createTestUser(email: email, password: password) + + // Now launch the app - it connects to emulator but isn't signed in + let app = createTestApp() app.launch() let emailField = app.textFields["email-field"] @@ -68,7 +98,7 @@ final class FirebaseSwiftUIExampleUITests: XCTestCase { let passwordField = app.secureTextFields["password-field"] XCTAssertTrue(passwordField.exists, "Password field should exist") passwordField.tap() - passwordField.typeText("123456") + passwordField.typeText(password) let signInButton = app.buttons["sign-in-button"] XCTAssertTrue(signInButton.exists, "Sign-In button should exist") @@ -138,10 +168,9 @@ final class FirebaseSwiftUIExampleUITests: XCTestCase { @MainActor func testCreateUserDisplaysSignedInView() throws { - let app = XCUIApplication() let email = createEmail() let password = "qwerty321@" - app.launchArguments.append("--auth-emulator") + let app = createTestApp() app.launch() // Check the Views are updated diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAEnrolmentUITests.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAEnrolmentUITests.swift index 77da6b7116..924d6629c3 100644 --- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAEnrolmentUITests.swift +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAEnrolmentUITests.swift @@ -29,13 +29,13 @@ final class MFAEnrollmentUITests: XCTestCase { // MARK: - MFA Management Navigation Tests @MainActor - func testMFAManagementButtonExistsAndIsTappable() throws { - let app = XCUIApplication() - app.launchArguments.append("--auth-emulator") - app.launchArguments.append("--mfa-enabled") - app.launchArguments.append("--create-user") + func testMFAManagementButtonExistsAndIsTappable() async throws { let email = createEmail() - app.launchArguments.append("\(email)") + + // Create user in test runner before launching app + try await createTestUser(email: email) + + let app = createTestApp(mfaEnabled: true) app.launch() // Sign in first to access MFA management @@ -61,13 +61,13 @@ final class MFAEnrollmentUITests: XCTestCase { } @MainActor - func testMFAEnrollmentNavigationFromManagement() throws { - let app = XCUIApplication() - app.launchArguments.append("--auth-emulator") - app.launchArguments.append("--mfa-enabled") - app.launchArguments.append("--create-user") + func testMFAEnrollmentNavigationFromManagement() async throws { let email = createEmail() - app.launchArguments.append("\(email)") + + // Create user in test runner before launching app + try await createTestUser(email: email) + + let app = createTestApp(mfaEnabled: true) app.launch() // Sign in and navigate to MFA management @@ -96,13 +96,13 @@ final class MFAEnrollmentUITests: XCTestCase { // MARK: - MFA Enrollment Factor Selection Tests @MainActor - func testFactorTypePickerExistsAndWorks() throws { - let app = XCUIApplication() - app.launchArguments.append("--auth-emulator") - app.launchArguments.append("--mfa-enabled") - app.launchArguments.append("--create-user") + func testFactorTypePickerExistsAndWorks() async throws { let email = createEmail() - app.launchArguments.append("\(email)") + + // Create user in test runner before launching app + try await createTestUser(email: email) + + let app = createTestApp(mfaEnabled: true) app.launch() // Navigate to MFA enrollment @@ -125,13 +125,13 @@ final class MFAEnrollmentUITests: XCTestCase { } @MainActor - func testStartEnrollmentButtonExistsAndWorks() throws { - let app = XCUIApplication() - app.launchArguments.append("--auth-emulator") - app.launchArguments.append("--mfa-enabled") - app.launchArguments.append("--create-user") + func testStartEnrollmentButtonExistsAndWorks() async throws { let email = createEmail() - app.launchArguments.append("\(email)") + + // Create user in test runner before launching app + try await createTestUser(email: email) + + let app = createTestApp(mfaEnabled: true) app.launch() // Navigate to MFA enrollment @@ -164,14 +164,11 @@ final class MFAEnrollmentUITests: XCTestCase { @MainActor func testEndToEndSMSEnrollmentAndRemovalFlow() async throws { - // 1) Launch app with emulator and create a fresh user - let app = XCUIApplication() - app.launchArguments.append("--auth-emulator") - app.launchArguments.append("--mfa-enabled") - app.launchArguments.append("--verify-email") - app.launchArguments.append("--create-user") + // 1) Create user in test runner before launching app (with email verification) let email = createEmail() - app.launchArguments.append("\(email)") + try await createTestUser(email: email, verifyEmail: true) + + let app = createTestApp(mfaEnabled: true) app.launch() // 2) Sign in to reach SignedInView @@ -264,15 +261,13 @@ final class MFAEnrollmentUITests: XCTestCase { // MARK: - TOTP Enrollment Flow Tests @MainActor - func testTOTPEnrollmentFlowUI() throws { - let app = XCUIApplication() - - app.launchArguments.append("--auth-emulator") - app.launchArguments.append("--mfa-enabled") - app.launchArguments.append("--verify-email") - app.launchArguments.append("--create-user") + func testTOTPEnrollmentFlowUI() async throws { let email = createEmail() - app.launchArguments.append("\(email)") + + // Create user in test runner before launching app (with email verification) + try await createTestUser(email: email, verifyEmail: true) + + let app = createTestApp(mfaEnabled: true) app.launch() // Navigate to MFA enrollment and select TOTP @@ -325,13 +320,13 @@ final class MFAEnrollmentUITests: XCTestCase { // MARK: - Error Handling Tests @MainActor - func testErrorMessageDisplay() throws { - let app = XCUIApplication() - app.launchArguments.append("--auth-emulator") - app.launchArguments.append("--mfa-enabled") - app.launchArguments.append("--create-user") + func testErrorMessageDisplay() async throws { let email = createEmail() - app.launchArguments.append("\(email)") + + // Create user in test runner before launching app + try await createTestUser(email: email) + + let app = createTestApp(mfaEnabled: true) app.launch() // Navigate to MFA enrollment @@ -354,13 +349,13 @@ final class MFAEnrollmentUITests: XCTestCase { // MARK: - Navigation Tests @MainActor - func testBackButtonNavigation() throws { - let app = XCUIApplication() - app.launchArguments.append("--auth-emulator") - app.launchArguments.append("--mfa-enabled") - app.launchArguments.append("--create-user") + func testBackButtonNavigation() async throws { let email = createEmail() - app.launchArguments.append("\(email)") + + // Create user in test runner before launching app + try await createTestUser(email: email) + + let app = createTestApp(mfaEnabled: true) app.launch() // Navigate to MFA enrollment @@ -384,13 +379,13 @@ final class MFAEnrollmentUITests: XCTestCase { } @MainActor - func testBackButtonFromMFAManagement() throws { - let app = XCUIApplication() - app.launchArguments.append("--auth-emulator") - app.launchArguments.append("--mfa-enabled") - app.launchArguments.append("--create-user") + func testBackButtonFromMFAManagement() async throws { let email = createEmail() - app.launchArguments.append("\(email)") + + // Create user in test runner before launching app + try await createTestUser(email: email) + + let app = createTestApp(mfaEnabled: true) app.launch() // Sign in and navigate to MFA management diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAResolutionUITests.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAResolutionUITests.swift index 2a25bf9276..ef93e71984 100644 --- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAResolutionUITests.swift +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAResolutionUITests.swift @@ -32,9 +32,7 @@ final class MFAResolutionUITests: XCTestCase { @MainActor func testCompleteMFAResolutionFlowWithAPIEnrollment() async throws { - let app = XCUIApplication() - app.launchArguments.append("--auth-emulator") - app.launchArguments.append("--mfa-enabled") + let app = createTestApp(mfaEnabled: true) app.launch() let email = createEmail() @@ -117,12 +115,6 @@ final class MFAResolutionUITests: XCTestCase { // MARK: - Helper Methods - private func createEmail() -> String { - let before = UUID().uuidString.prefix(8) - let after = UUID().uuidString.prefix(6) - return "\(before)@\(after).com" - } - /// Programmatically enables SMS MFA for a user via the Auth emulator REST API /// - Parameters: /// - idToken: The user's Firebase ID token @@ -389,7 +381,7 @@ final class MFAResolutionUITests: XCTestCase { private func signUpUser(email: String, password: String = "12345678") async throws { // Create user via Auth Emulator REST API - let url = URL(string: "http://localhost:9099/identitytoolkit.googleapis.com/v1/accounts:signUp?key=fake-api-key")! + let url = URL(string: "http://127.0.0.1:9099/identitytoolkit.googleapis.com/v1/accounts:signUp?key=fake-api-key")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/TestUtils.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/TestUtils.swift index 2ac2bae387..7625f25623 100644 --- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/TestUtils.swift +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/TestUtils.swift @@ -1,16 +1,84 @@ import Foundation import XCTest +// MARK: - Email Generation + func createEmail() -> String { let before = UUID().uuidString.prefix(8) let after = UUID().uuidString.prefix(6) return "\(before)@\(after).com" } +// MARK: - App Configuration + +/// Creates and configures an XCUIApplication with default test launch arguments +func createTestApp(mfaEnabled: Bool = false) -> XCUIApplication { + let app = XCUIApplication() + app.launchArguments.append("--test-view-enabled") + if mfaEnabled { + app.launchArguments.append("--mfa-enabled") + } + return app +} + +// MARK: - Alert Handling + +func dismissAlert(app: XCUIApplication) { + if app.scrollViews.otherElements.buttons["Not Now"].waitForExistence(timeout: 2) { + app.scrollViews.otherElements.buttons["Not Now"].tap() + } +} + +// MARK: - User Creation + +/// Helper to create a test user in the emulator via REST API (avoids keychain issues) +func createTestUser(email: String, password: String = "123456", verifyEmail: Bool = false) async throws { + // Use Firebase Auth emulator REST API directly to avoid keychain access issues in UI tests + let signUpUrl = "http://127.0.0.1:9099/identitytoolkit.googleapis.com/v1/accounts:signUp?key=fake-api-key" + + guard let url = URL(string: signUpUrl) else { + throw NSError(domain: "TestError", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Invalid emulator URL"]) + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let body: [String: Any] = [ + "email": email, + "password": password, + "returnSecureToken": true + ] + + request.httpBody = try JSONSerialization.data(withJSONObject: body) + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse, + httpResponse.statusCode == 200 else { + let errorBody = String(data: data, encoding: .utf8) ?? "Unknown error" + throw NSError(domain: "TestError", code: 2, + userInfo: [NSLocalizedDescriptionKey: "Failed to create user: \(errorBody)"]) + } + + // If email verification is requested, verify the email + if verifyEmail { + // Parse the response to get the idToken + if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let idToken = json["idToken"] as? String { + try await verifyEmailInEmulator(email: email, idToken: idToken) + } + } +} + +// MARK: - Email Verification + +/// Verifies an email address in the emulator using the OOB code mechanism func verifyEmailInEmulator(email: String, idToken: String, projectID: String = "flutterfire-e2e-tests", - emulatorHost: String = "localhost:9099") async throws { + emulatorHost: String = "127.0.0.1:9099") async throws { let base = "http://\(emulatorHost)"