Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ Pods/
Podfile.lock
*.xcworkspace/
samples/**/GoogleService-Info.plist
e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.xcresult/
Original file line number Diff line number Diff line change
Expand Up @@ -233,22 +233,15 @@ final class FirebaseSwiftUIExampleUITests: XCTestCase {
let emailField = app.textFields["email-field"]

XCTAssertTrue(emailField.waitForExistence(timeout: 2), "Email field should exist")
// Workaround for updating SecureFields with ConnectHardwareKeyboard enabled
UIPasteboard.general.string = email
emailField.press(forDuration: 1.2)
app.menuItems["Paste"].tap()
try pasteIntoField(emailField, text: email, app: app)

let passwordField = app.secureTextFields["password-field"]
XCTAssertTrue(passwordField.exists, "Password field should exist")
UIPasteboard.general.string = password
passwordField.press(forDuration: 1.2)
app.menuItems["Paste"].tap()
try pasteIntoField(passwordField, text: password, app: app)

let confirmPasswordField = app.secureTextFields["confirm-password-field"]
XCTAssertTrue(confirmPasswordField.exists, "Confirm password field should exist")
UIPasteboard.general.string = password
confirmPasswordField.press(forDuration: 1.2)
app.menuItems["Paste"].tap()
try pasteIntoField(confirmPasswordField, text: password, app: app)

// Create the user (sign up)
let signUpButton = app
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,23 +56,7 @@ final class MFAEnrollmentUITests: XCTestCase {
// Sign in first to access MFA management
try signInToApp(app: app, email: email)

// Check MFA management button exists
let mfaManagementButton = app.buttons["mfa-management-button"]
XCTAssertTrue(
mfaManagementButton.waitForExistence(timeout: 5),
"MFA management button should exist"
)
XCTAssertTrue(mfaManagementButton.isEnabled, "MFA management button should be enabled")

// Tap the button
mfaManagementButton.tap()

// Verify we navigated to MFA management view
let managementTitle = app.staticTexts["Two-Factor Authentication"]
XCTAssertTrue(
managementTitle.waitForExistence(timeout: 5),
"Should navigate to MFA management view"
)
try navigateToMFAManagement(app: app)
}

@MainActor
Expand All @@ -87,16 +71,16 @@ final class MFAEnrollmentUITests: XCTestCase {

// Sign in and navigate to MFA management
try signInToApp(app: app, email: email)
app.buttons["mfa-management-button"].tap()
try navigateToMFAManagement(app: app)

// Tap setup MFA button (for users with no enrolled factors)
let setupButton = app.buttons["setup-mfa-button"]
if setupButton.waitForExistence(timeout: 3) {
if setupButton.waitForExistence(timeout: 10) {
setupButton.tap()
} else {
// If factors are already enrolled, tap add another method
let addMethodButton = app.buttons["add-mfa-method-button"]
XCTAssertTrue(addMethodButton.waitForExistence(timeout: 3), "Add method button should exist")
XCTAssertTrue(addMethodButton.waitForExistence(timeout: 10), "Add method button should exist")
addMethodButton.tap()
}

Expand Down Expand Up @@ -448,27 +432,46 @@ final class MFAEnrollmentUITests: XCTestCase {
signedInText.waitForExistence(timeout: 30),
"SignedInView should be visible after login"
)
dismissAlert(app: app)
XCTAssertTrue(signedInText.exists, "SignedInView should be visible after login")
}

@MainActor
private func navigateToMFAManagement(app: XCUIApplication) throws {
dismissAlert(app: app)

let mfaManagementButton = app.buttons["mfa-management-button"]
XCTAssertTrue(
mfaManagementButton.waitForExistence(timeout: 10),
"MFA management button should exist"
)
XCTAssertTrue(mfaManagementButton.isEnabled, "MFA management button should be enabled")
mfaManagementButton.tap()

let managementTitle = app.staticTexts["Two-Factor Authentication"]
XCTAssertTrue(
managementTitle.waitForExistence(timeout: 10),
"Should navigate to MFA management view"
)
}

@MainActor
private func navigateToMFAEnrollment(app: XCUIApplication) throws {
// Navigate to MFA management
app.buttons["mfa-management-button"].tap()
try navigateToMFAManagement(app: app)

// Navigate to MFA enrollment
let setupButton = app.buttons["setup-mfa-button"]
if setupButton.waitForExistence(timeout: 3) {
if setupButton.waitForExistence(timeout: 10) {
setupButton.tap()
} else {
let addMethodButton = app.buttons["add-mfa-method-button"]
XCTAssertTrue(addMethodButton.waitForExistence(timeout: 3), "Add method button should exist")
XCTAssertTrue(addMethodButton.waitForExistence(timeout: 10), "Add method button should exist")
addMethodButton.tap()
}

// Verify we're in MFA enrollment view
let enrollmentTitle = app.staticTexts["Set Up Two-Factor Authentication"]
XCTAssertTrue(enrollmentTitle.waitForExistence(timeout: 5), "Should be in MFA enrollment view")
XCTAssertTrue(enrollmentTitle.waitForExistence(timeout: 10), "Should be in MFA enrollment view")
}
}

Expand All @@ -486,49 +489,51 @@ struct VerificationCode: Codable {
/// - Returns: The verification code as a String
/// - Throws: Error if unable to retrieve codes
private func getLastSmsCode(specificPhone: String? = nil) async throws -> String {
let getSmsCodesUrl =
"http://127.0.0.1:9099/emulator/v1/projects/flutterfire-e2e-tests/verificationCodes"
do {
for projectID in authEmulatorCandidateProjectIDs() {
let getSmsCodesUrl =
"http://127.0.0.1:9099/emulator/v1/projects/\(projectID)/verificationCodes"

guard let url = URL(string: getSmsCodesUrl) else {
throw NSError(
domain: "getLastSmsCode",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "Failed to create URL for SMS codes endpoint"]
)
}
guard let url = URL(string: getSmsCodesUrl) else {
continue
}

do {
let (data, _) = try await URLSession.shared.data(from: url)
guard let (data, _) = try? await URLSession.shared.data(from: url) else {
continue
}

let decoder = JSONDecoder()
let codesResponse = try decoder.decode(VerificationCodesResponse.self, from: data)
let decoder = JSONDecoder()
guard let codesResponse = try? decoder.decode(VerificationCodesResponse.self, from: data) else {
continue
}

guard let codes = codesResponse.verificationCodes, !codes.isEmpty else {
throw NSError(
domain: "getLastSmsCode",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "No SMS verification codes found in emulator"]
)
}
guard let codes = codesResponse.verificationCodes, !codes.isEmpty else {
continue
}

if let specificPhone = specificPhone {
// Search backwards through codes for the specific phone number
for code in codes.reversed() {
if code.phoneNumber == specificPhone {
return code.code
if let specificPhone = specificPhone {
// Search backwards through codes for the specific phone number
for code in codes.reversed() {
if code.phoneNumber == specificPhone {
return code.code
}
}
} else if let lastCode = codes.last {
return lastCode.code
}
throw NSError(
domain: "getLastSmsCode",
code: -1,
userInfo: [
NSLocalizedDescriptionKey: "No SMS verification code found for phone number: \(specificPhone)",
]
)
}

let description = if let specificPhone {
"No SMS verification code found for phone number: \(specificPhone)"
} else {
// Return the last code in the array
return codes.last!.code
"No SMS verification codes found in emulator"
}

throw NSError(
domain: "getLastSmsCode",
code: -1,
userInfo: [NSLocalizedDescriptionKey: description]
)
} catch let error as DecodingError {
throw NSError(
domain: "getLastSmsCode",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,63 +266,64 @@ final class MFAResolutionUITests: XCTestCase {
@MainActor
private func getSMSVerificationCode(for phoneNumber: String,
codeType: String = "enrollment") async -> String? {
let emulatorUrl =
"http://127.0.0.1:9099/emulator/v1/projects/flutterfire-e2e-tests/verificationCodes"

guard let url = URL(string: emulatorUrl) else {
return nil
}

do {
let (data, _) = try await URLSession.shared.data(from: url)
for projectID in authEmulatorCandidateProjectIDs() {
let emulatorUrl =
"http://127.0.0.1:9099/emulator/v1/projects/\(projectID)/verificationCodes"

guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let codes = json["verificationCodes"] as? [[String: Any]] else {
print("❌ Failed to parse verification codes")
return nil
}

// Filter codes by phone number and type, then get the most recent one
let matchingCodes = codes.filter { codeInfo in
guard let phone = codeInfo["phoneNumber"] as? String else {
print("❌ Code missing phoneNumber field")
return false
guard let url = URL(string: emulatorUrl) else {
continue
}

// The key difference between enrollment and verification codes:
// - Enrollment codes have full phone numbers (e.g., "+15551234567")
// - Verification codes have masked phone numbers (e.g., "+*******4567")
let isMasked = phone.contains("*")

// Match phone number
let phoneMatches: Bool
if isMasked {
// Extract last 4 digits from both numbers
let last4OfResponse = String(phone.suffix(4))
let last4OfTarget = String(phoneNumber.suffix(4))
phoneMatches = last4OfResponse == last4OfTarget
} else {
// Full phone number match
phoneMatches = phone == phoneNumber
}
let (data, _) = try await URLSession.shared.data(from: url)

guard phoneMatches else {
return false
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let codes = json["verificationCodes"] as? [[String: Any]] else {
continue
}

if codeType == "enrollment" {
// Enrollment codes have unmasked phone numbers
return !isMasked
} else { // "verification"
// Verification codes have masked phone numbers
return isMasked
// Filter codes by phone number and type, then get the most recent one
let matchingCodes = codes.filter { codeInfo in
guard let phone = codeInfo["phoneNumber"] as? String else {
print("❌ Code missing phoneNumber field")
return false
}

// The key difference between enrollment and verification codes:
// - Enrollment codes have full phone numbers (e.g., "+15551234567")
// - Verification codes have masked phone numbers (e.g., "+*******4567")
let isMasked = phone.contains("*")

// Match phone number
let phoneMatches: Bool
if isMasked {
// Extract last 4 digits from both numbers
let last4OfResponse = String(phone.suffix(4))
let last4OfTarget = String(phoneNumber.suffix(4))
phoneMatches = last4OfResponse == last4OfTarget
} else {
// Full phone number match
phoneMatches = phone == phoneNumber
}

guard phoneMatches else {
return false
}

if codeType == "enrollment" {
// Enrollment codes have unmasked phone numbers
return !isMasked
} else { // "verification"
// Verification codes have masked phone numbers
return isMasked
}
}
}

// Get the last matching code (most recent)
if let lastCode = matchingCodes.last,
let code = lastCode["code"] as? String {
return code
// Get the last matching code (most recent)
if let lastCode = matchingCodes.last,
let code = lastCode["code"] as? String {
return code
}
}

print("❌ No matching code found")
Expand Down
Loading
Loading