Skip to content

Commit 179f5ba

Browse files
test: test legacy fetchSignInWithEmail flow
1 parent 90ecadf commit 179f5ba

File tree

7 files changed

+368
-1
lines changed

7 files changed

+368
-1
lines changed

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Strings/Localizable.xcstrings

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47531,6 +47531,66 @@
4753147531
}
4753247532
}
4753347533
},
47534+
"Use a previous sign-in method" : {
47535+
"comment" : "Legacy sign-in recovery sheet title.",
47536+
"extractionState" : "manual",
47537+
"localizations" : {
47538+
"en" : {
47539+
"stringUnit" : {
47540+
"state" : "translated",
47541+
"value" : "Use a previous sign-in method"
47542+
}
47543+
}
47544+
}
47545+
},
47546+
"You previously signed in with one of these methods for %@." : {
47547+
"comment" : "Legacy sign-in recovery sheet message.",
47548+
"extractionState" : "manual",
47549+
"localizations" : {
47550+
"en" : {
47551+
"stringUnit" : {
47552+
"state" : "translated",
47553+
"value" : "You previously signed in with one of these methods for %@."
47554+
}
47555+
}
47556+
}
47557+
},
47558+
"Some previous sign-in methods are not enabled in this app." : {
47559+
"comment" : "Legacy sign-in recovery helper copy when some providers are unavailable.",
47560+
"extractionState" : "manual",
47561+
"localizations" : {
47562+
"en" : {
47563+
"stringUnit" : {
47564+
"state" : "translated",
47565+
"value" : "Some previous sign-in methods are not enabled in this app."
47566+
}
47567+
}
47568+
}
47569+
},
47570+
"Continue with email and password" : {
47571+
"comment" : "Legacy sign-in recovery action for email/password.",
47572+
"extractionState" : "manual",
47573+
"localizations" : {
47574+
"en" : {
47575+
"stringUnit" : {
47576+
"state" : "translated",
47577+
"value" : "Continue with email and password"
47578+
}
47579+
}
47580+
}
47581+
},
47582+
"Continue with email link" : {
47583+
"comment" : "Legacy sign-in recovery action for email link.",
47584+
"extractionState" : "manual",
47585+
"localizations" : {
47586+
"en" : {
47587+
"stringUnit" : {
47588+
"state" : "translated",
47589+
"value" : "Continue with email link"
47590+
}
47591+
}
47592+
}
47593+
},
4753447594
"Your password has been successfully updated." : {
4753547595
"localizations" : {
4753647596
"bg" : {

e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExample/TestView.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ struct TestView: View {
4242
}
4343

4444
let isMfaEnabled = ProcessInfo.processInfo.arguments.contains("--mfa-enabled")
45+
let legacyFetchSignInEnabled = ProcessInfo.processInfo.arguments.contains(
46+
"--legacy-fetch-sign-in-enabled"
47+
)
48+
let legacyRecoveryPreviewEnabled = ProcessInfo.processInfo.arguments.contains(
49+
"--legacy-sign-in-recovery-preview"
50+
)
4551

4652
let actionCodeSettings = ActionCodeSettings()
4753
actionCodeSettings.handleCodeInApp = true
@@ -50,6 +56,7 @@ struct TestView: View {
5056
actionCodeSettings.linkDomain = "flutterfire-e2e-tests.firebaseapp.com"
5157
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
5258
let configuration = AuthConfiguration(
59+
legacyFetchSignInWithEmail: legacyFetchSignInEnabled,
5360
tosUrl: URL(string: "https://example.com/tos"),
5461
privacyPolicyUrl: URL(string: "https://example.com/privacy"),
5562
emailLinkSignInActionCodeSettings: actionCodeSettings,
@@ -76,6 +83,21 @@ struct TestView: View {
7683
.withEmailSignIn()
7784
.withEmailLinkSignIn()
7885
}
86+
if legacyRecoveryPreviewEnabled {
87+
authService.legacySignInRecovery = LegacySignInRecoveryContext(
88+
email: "legacy@example.com",
89+
options: [
90+
LegacySignInOption(
91+
id: EmailAuthProviderID,
92+
displayName: authService.string.legacyEmailPasswordOptionLabel
93+
),
94+
LegacySignInOption(
95+
id: "emailLink",
96+
displayName: authService.string.legacyEmailLinkOptionLabel
97+
),
98+
]
99+
)
100+
}
79101
authService.isPresented = true
80102
}
81103

e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests/FirebaseSwiftUIExampleTests.swift

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,26 @@ struct FirebaseSwiftUIExampleTests {
3838
return AuthService(configuration: resolvedConfiguration)
3939
}
4040

41+
@MainActor
42+
func prepareLegacyRecoveryService(legacyFetchSignInWithEmail: Bool,
43+
includeEmailLinkProvider: Bool) async throws -> AuthService {
44+
configureFirebaseIfNeeded()
45+
try await isEmulatorRunning()
46+
47+
let service = AuthService(
48+
configuration: AuthConfiguration(
49+
legacyFetchSignInWithEmail: legacyFetchSignInWithEmail
50+
)
51+
)
52+
.withEmailSignIn()
53+
54+
if includeEmailLinkProvider {
55+
_ = service.withEmailLinkSignIn()
56+
}
57+
58+
return service
59+
}
60+
4161
@Test
4262
@MainActor
4363
func defaultAuthConfigurationInjection() async throws {
@@ -49,6 +69,7 @@ struct FirebaseSwiftUIExampleTests {
4969
#expect(actual.shouldHideCancelButton == false)
5070
#expect(actual.interactiveDismissEnabled == true)
5171
#expect(actual.shouldAutoUpgradeAnonymousUsers == false)
72+
#expect(actual.legacyFetchSignInWithEmail == false)
5273
#expect(actual.customStringsBundle == nil)
5374
#expect(actual.tosUrl == nil)
5475
#expect(actual.privacyPolicyUrl == nil)
@@ -73,6 +94,7 @@ struct FirebaseSwiftUIExampleTests {
7394
shouldHideCancelButton: true,
7495
interactiveDismissEnabled: false,
7596
shouldAutoUpgradeAnonymousUsers: true,
97+
legacyFetchSignInWithEmail: true,
7698
customStringsBundle: .main,
7799
tosUrl: URL(string: "https://example.com/tos"),
78100
privacyPolicyUrl: URL(string: "https://example.com/privacy"),
@@ -86,6 +108,7 @@ struct FirebaseSwiftUIExampleTests {
86108
#expect(actual.shouldHideCancelButton == true)
87109
#expect(actual.interactiveDismissEnabled == false)
88110
#expect(actual.shouldAutoUpgradeAnonymousUsers == true)
111+
#expect(actual.legacyFetchSignInWithEmail == true)
89112
#expect(actual.customStringsBundle === Bundle.main)
90113
#expect(actual.tosUrl == URL(string: "https://example.com/tos"))
91114
#expect(actual.privacyPolicyUrl == URL(string: "https://example.com/privacy"))
@@ -95,6 +118,88 @@ struct FirebaseSwiftUIExampleTests {
95118
#expect(actual.verifyEmailActionCodeSettings?.url == verifySettings.url)
96119
}
97120

121+
@Test
122+
@MainActor
123+
func legacySignInRecoveryResolvesEmailLinkProvider() async throws {
124+
let email = createEmail()
125+
try await clearAuthEmulatorState()
126+
try await createEmailLinkOnlyUser(email: email)
127+
128+
let service = try await prepareLegacyRecoveryService(
129+
legacyFetchSignInWithEmail: true,
130+
includeEmailLinkProvider: true
131+
)
132+
133+
do {
134+
try await service.signIn(email: email, password: kPassword)
135+
Issue.record("Expected email/password sign-in to fail for an email-link account")
136+
} catch {
137+
if case .legacySignInRecoveryPresented = error as? AuthServiceError {
138+
// Expected path.
139+
} else {
140+
Issue.record("Expected legacy recovery error, got: \(error)")
141+
}
142+
}
143+
144+
let recovery = service.legacySignInRecovery
145+
#expect(recovery?.email == email)
146+
#expect(recovery?.options.contains(where: { $0.id == "emailLink" }) == true)
147+
}
148+
149+
@Test
150+
@MainActor
151+
func legacySignInRecoveryFallsBackWhenProviderNotEnabled() async throws {
152+
let email = createEmail()
153+
try await clearAuthEmulatorState()
154+
try await createEmailLinkOnlyUser(email: email)
155+
156+
let service = try await prepareLegacyRecoveryService(
157+
legacyFetchSignInWithEmail: true,
158+
includeEmailLinkProvider: false
159+
)
160+
161+
do {
162+
try await service.signIn(email: email, password: kPassword)
163+
Issue.record("Expected email/password sign-in to fail for an email-link account")
164+
} catch {
165+
#expect((error as? AuthServiceError) == nil || {
166+
if case .legacySignInRecoveryPresented = error as? AuthServiceError {
167+
return false
168+
}
169+
return true
170+
}())
171+
}
172+
173+
#expect(service.legacySignInRecovery == nil)
174+
}
175+
176+
@Test
177+
@MainActor
178+
func legacySignInRecoveryDisabledPreservesExistingFailure() async throws {
179+
let email = createEmail()
180+
try await clearAuthEmulatorState()
181+
try await createEmailLinkOnlyUser(email: email)
182+
183+
let service = try await prepareLegacyRecoveryService(
184+
legacyFetchSignInWithEmail: false,
185+
includeEmailLinkProvider: true
186+
)
187+
188+
do {
189+
try await service.signIn(email: email, password: kPassword)
190+
Issue.record("Expected email/password sign-in to fail for an email-link account")
191+
} catch {
192+
#expect((error as? AuthServiceError) == nil || {
193+
if case .legacySignInRecoveryPresented = error as? AuthServiceError {
194+
return false
195+
}
196+
return true
197+
}())
198+
}
199+
200+
#expect(service.legacySignInRecovery == nil)
201+
}
202+
98203
@Test
99204
@MainActor
100205
func createEmailPasswordUser() async throws {

e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests/TestHarness.swift

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import FirebaseAuth
9+
import FirebaseAuthSwiftUI
910
import FirebaseCore
1011

1112
@MainActor
@@ -80,3 +81,90 @@ func waitForStateChange(timeout: TimeInterval = 10.0,
8081
enum TestError: Error {
8182
case timeout(String)
8283
}
84+
85+
@MainActor
86+
func makeEmailLinkActionCodeSettings() -> ActionCodeSettings {
87+
let actionCodeSettings = ActionCodeSettings()
88+
actionCodeSettings.handleCodeInApp = true
89+
actionCodeSettings.url = URL(string: "https://flutterfire-e2e-tests.firebaseapp.com")
90+
actionCodeSettings.linkDomain = "flutterfire-e2e-tests.firebaseapp.com"
91+
actionCodeSettings.setIOSBundleID("io.flutter.plugins.firebase.auth.example")
92+
return actionCodeSettings
93+
}
94+
95+
@MainActor
96+
func createEmailLinkOnlyUser(email: String) async throws {
97+
configureFirebaseIfNeeded()
98+
try await isEmulatorRunning()
99+
100+
let service = AuthService(
101+
configuration: AuthConfiguration(
102+
emailLinkSignInActionCodeSettings: makeEmailLinkActionCodeSettings()
103+
)
104+
)
105+
.withEmailLinkSignIn()
106+
107+
service.emailLink = email
108+
try await service.sendEmailSignInLink(email: email)
109+
let signInLink = try await fetchEmailSignInLinkFromEmulator(email: email)
110+
try await service.handleSignInLink(url: signInLink)
111+
try await service.signOut()
112+
}
113+
114+
@MainActor
115+
func fetchEmailSignInLinkFromEmulator(email: String,
116+
projectID: String = "flutterfire-e2e-tests",
117+
emulatorHost: String = "127.0.0.1:9099") async throws -> URL {
118+
struct OobEnvelope: Decodable { let oobCodes: [OobItem] }
119+
struct OobItem: Decodable {
120+
let email: String
121+
let oobLink: String?
122+
let requestType: String
123+
let creationTime: String?
124+
}
125+
126+
let oobURL = URL(string: "http://\(emulatorHost)/emulator/v1/projects/\(projectID)/oobCodes")!
127+
let iso = ISO8601DateFormatter()
128+
129+
var attempts = 0
130+
let maxAttempts = 5
131+
132+
while attempts < maxAttempts {
133+
let (oobData, oobResponse) = try await URLSession.shared.data(from: oobURL)
134+
guard (oobResponse as? HTTPURLResponse)?.statusCode == 200 else {
135+
throw NSError(
136+
domain: "EmulatorError",
137+
code: 10,
138+
userInfo: [NSLocalizedDescriptionKey: "Failed to fetch OOB codes for email sign-in"]
139+
)
140+
}
141+
142+
let envelope = try JSONDecoder().decode(OobEnvelope.self, from: oobData)
143+
let oobLink = envelope.oobCodes
144+
.filter {
145+
$0.email.caseInsensitiveCompare(email) == .orderedSame && $0.requestType == "EMAIL_SIGNIN"
146+
}
147+
.sorted {
148+
let lhs = $0.creationTime.flatMap { iso.date(from: $0) } ?? .distantPast
149+
let rhs = $1.creationTime.flatMap { iso.date(from: $0) } ?? .distantPast
150+
return lhs > rhs
151+
}
152+
.compactMap(\.oobLink)
153+
.first
154+
155+
if let oobLink,
156+
let encodedLink = oobLink.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
157+
let wrappedLink = URL(string: "https://example.com/?link=\(encodedLink)") {
158+
return wrappedLink
159+
}
160+
161+
attempts += 1
162+
try await Task.sleep(nanoseconds: 500_000_000)
163+
}
164+
165+
throw NSError(
166+
domain: "EmulatorError",
167+
code: 11,
168+
userInfo: [NSLocalizedDescriptionKey: "No EMAIL_SIGNIN OOB link found for \(email)"]
169+
)
170+
}

0 commit comments

Comments
 (0)