Skip to content

Commit 07c20de

Browse files
test: bifurcate test setup with content view + test helpers
1 parent 7b74a8f commit 07c20de

File tree

3 files changed

+179
-11
lines changed

3 files changed

+179
-11
lines changed

samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExampleApp.swift

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@ class AppDelegate: NSObject, UIApplicationDelegate {
3030
UIApplication.LaunchOptionsKey: Any
3131
]?) -> Bool {
3232
FirebaseApp.configure()
33-
if uiAuthEmulator {
34-
Auth.auth().useEmulator(withHost: "localhost", port: 9099)
35-
}
3633

3734
ApplicationDelegate.shared.application(
3835
application,
@@ -58,6 +55,8 @@ class AppDelegate: NSObject, UIApplicationDelegate {
5855
func application(_ app: UIApplication,
5956
open url: URL,
6057
options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
58+
if Auth.auth().canHandle(url) { return true }
59+
6160
if ApplicationDelegate.shared.application(
6261
app,
6362
open: url,
@@ -76,15 +75,13 @@ class AppDelegate: NSObject, UIApplicationDelegate {
7675
struct FirebaseSwiftUIExampleApp: App {
7776
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
7877

79-
init() {
80-
Task {
81-
try await testCreateUser()
82-
}
83-
}
78+
init() {}
8479

8580
var body: some Scene {
8681
WindowGroup {
87-
NavigationView {
82+
if testRunner {
83+
TestView()
84+
} else {
8885
ContentView()
8986
}
9087
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//
16+
// ContentView.swift
17+
// FirebaseSwiftUIExample
18+
//
19+
// Created by Russell Wheatley on 23/04/2025.
20+
//
21+
22+
import FirebaseAuth
23+
import FirebaseAuthSwiftUI
24+
import FirebaseFacebookSwiftUI
25+
import FirebaseGoogleSwiftUI
26+
import FirebasePhoneAuthSwiftUI
27+
import SwiftUI
28+
29+
struct TestView: View {
30+
let authService: AuthService
31+
32+
init() {
33+
Auth.auth().useEmulator(withHost: "localhost", port: 9099)
34+
Auth.auth().settings?.isAppVerificationDisabledForTesting = true
35+
Task {
36+
try await testCreateUser()
37+
}
38+
39+
let isMfaEnabled = ProcessInfo.processInfo.arguments.contains("--mfa-enabled")
40+
41+
let actionCodeSettings = ActionCodeSettings()
42+
actionCodeSettings.handleCodeInApp = true
43+
actionCodeSettings
44+
.url = URL(string: "https://flutterfire-e2e-tests.firebaseapp.com")
45+
actionCodeSettings.linkDomain = "flutterfire-e2e-tests.firebaseapp.com"
46+
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
47+
let configuration = AuthConfiguration(
48+
tosUrl: URL(string: "https://example.com/tos"),
49+
privacyPolicyUrl: URL(string: "https://example.com/privacy"),
50+
emailLinkSignInActionCodeSettings: actionCodeSettings,
51+
mfaEnabled: isMfaEnabled
52+
)
53+
54+
authService = AuthService(
55+
configuration: configuration
56+
)
57+
.withGoogleSignIn()
58+
.withPhoneSignIn()
59+
.withFacebookSignIn()
60+
.withEmailSignIn()
61+
}
62+
63+
var body: some View {
64+
AuthPickerView().environment(authService)
65+
}
66+
}

samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/UITestUtils.swift

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import FirebaseAuth
88
import SwiftUI
99

1010
// UI Test Runner keys
11-
public let uiAuthEmulator = CommandLine.arguments.contains("--auth-emulator")
11+
public let testRunner = CommandLine.arguments.contains("--auth-emulator")
12+
let verifyEmail = CommandLine.arguments.contains("--verify-email")
1213

1314
public var testEmail: String? {
1415
guard let emailIndex = CommandLine.arguments.firstIndex(of: "--create-user"),
@@ -21,7 +22,111 @@ func testCreateUser() async throws {
2122
if let email = testEmail {
2223
let password = "123456"
2324
let auth = Auth.auth()
24-
try await auth.createUser(withEmail: email, password: password)
25+
let result = try await auth.createUser(withEmail: email, password: password)
26+
if verifyEmail {
27+
try await setEmailVerifiedInEmulator(for: result.user)
28+
}
2529
try auth.signOut()
2630
}
2731
}
32+
33+
/// Marks the given Firebase `user` as email-verified **in the Auth emulator**.
34+
/// Works in CI even if the email address doesn't exist.
35+
/// - Parameters:
36+
/// - user: The signed-in Firebase user you want to verify.
37+
/// - projectID: Your emulator project ID (e.g. "demo-project" or whatever you're using locally).
38+
/// - emulatorHost: Host:port for the Auth emulator. Defaults to localhost:9099.
39+
func setEmailVerifiedInEmulator(for user: User,
40+
projectID: String = "flutterfire-e2e-tests",
41+
emulatorHost: String = "localhost:9099") async throws {
42+
43+
guard let email = user.email else {
44+
throw NSError(domain: "EmulatorError", code: 1,
45+
userInfo: [
46+
NSLocalizedDescriptionKey: "User has no email; cannot look up OOB code in emulator",
47+
])
48+
}
49+
50+
// 1) Trigger a verification email -> creates an OOB code in the emulator.
51+
try await sendVerificationEmail(user)
52+
53+
// 2) Read OOB codes from the emulator and find the VERIFY_EMAIL code for this user.
54+
let base = "http://\(emulatorHost)"
55+
let oobURL = URL(string: "\(base)/emulator/v1/projects/\(projectID)/oobCodes")!
56+
57+
let (oobData, oobResp) = try await URLSession.shared.data(from: oobURL)
58+
guard (oobResp as? HTTPURLResponse)?.statusCode == 200 else {
59+
let body = String(data: oobData, encoding: .utf8) ?? ""
60+
throw NSError(domain: "EmulatorError", code: 2,
61+
userInfo: [
62+
NSLocalizedDescriptionKey: "Failed to fetch oobCodes. Response: \(body)",
63+
])
64+
}
65+
66+
struct OobEnvelope: Decodable { let oobCodes: [OobItem] }
67+
struct OobItem: Decodable {
68+
let oobCode: String
69+
let email: String
70+
let requestType: String
71+
let creationTime: String? // RFC3339/ISO8601; optional for safety
72+
}
73+
74+
let envelope = try JSONDecoder().decode(OobEnvelope.self, from: oobData)
75+
76+
// Pick the most recent VERIFY_EMAIL code for this email (in case there are multiple).
77+
let iso = ISO8601DateFormatter()
78+
let codeItem = envelope.oobCodes
79+
.filter {
80+
$0.email.caseInsensitiveCompare(email) == .orderedSame && $0.requestType == "VERIFY_EMAIL"
81+
}
82+
.sorted {
83+
let d0 = $0.creationTime.flatMap { iso.date(from: $0) } ?? .distantPast
84+
let d1 = $1.creationTime.flatMap { iso.date(from: $0) } ?? .distantPast
85+
return d0 > d1
86+
}
87+
.first
88+
89+
guard let oobCode = codeItem?.oobCode else {
90+
throw NSError(domain: "EmulatorError", code: 3,
91+
userInfo: [
92+
NSLocalizedDescriptionKey: "No VERIFY_EMAIL oobCode found for \(email) in emulator",
93+
])
94+
}
95+
96+
// 3) Apply the OOB code via the emulator's identitytoolkit endpoint.
97+
// Note: API key value does not matter when talking to the emulator.
98+
var applyReq = URLRequest(
99+
url: URL(string: "\(base)/identitytoolkit.googleapis.com/v1/accounts:update?key=anything")!
100+
)
101+
applyReq.httpMethod = "POST"
102+
applyReq.setValue("application/json", forHTTPHeaderField: "Content-Type")
103+
applyReq.httpBody = try JSONSerialization.data(withJSONObject: ["oobCode": oobCode], options: [])
104+
105+
let (applyData, applyResp) = try await URLSession.shared.data(for: applyReq)
106+
guard let http = applyResp as? HTTPURLResponse, http.statusCode == 200 else {
107+
let body = String(data: applyData, encoding: .utf8) ?? ""
108+
throw NSError(domain: "EmulatorError", code: 4,
109+
userInfo: [
110+
NSLocalizedDescriptionKey: "Applying oobCode failed. Status \((applyResp as? HTTPURLResponse)?.statusCode ?? -1). Body: \(body)",
111+
])
112+
}
113+
114+
log("Applied oobCode successfully; reloading user...")
115+
116+
// 4) Reload the user to reflect the new verification state.
117+
try await user.reload()
118+
log("User reloaded. emailVerified after reload: \(user.isEmailVerified)")
119+
}
120+
121+
/// Small async helper to call FirebaseAuth's callback-based `sendEmailVerification` on iOS.
122+
private func sendVerificationEmail(_ user: User) async throws {
123+
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
124+
user.sendEmailVerification { error in
125+
if let error = error {
126+
cont.resume(throwing: error)
127+
} else {
128+
cont.resume()
129+
}
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)