diff --git a/Sources/SpeziFirebaseAccount/FirebaseAccountService.swift b/Sources/SpeziFirebaseAccount/FirebaseAccountService.swift index 0f34a15..d7f2e58 100644 --- a/Sources/SpeziFirebaseAccount/FirebaseAccountService.swift +++ b/Sources/SpeziFirebaseAccount/FirebaseAccountService.swift @@ -126,10 +126,10 @@ public final class FirebaseAccountService: AccountService { // swiftlint:disable @MainActor private var authStateDidChangeListenerHandle: AuthStateDidChangeListenerHandle? @MainActor private var lastNonce: String? - private var isConfiguring = false private var shouldQueue = false private var queuedUpdates: [UserUpdate] = [] private var actionSemaphore = AsyncSemaphore() + private var skipNextStateChange = false private var unsupportedKeys: AccountKeyCollection { @@ -179,13 +179,12 @@ public final class FirebaseAccountService: AccountService { // swiftlint:disable Auth.auth().useEmulator(withHost: emulatorSettings.host, port: emulatorSettings.port) } - isConfiguring = true - defer { - isConfiguring = false - } + checkForInitialUserAccount() // get notified about changes of the User reference authStateDidChangeListenerHandle = Auth.auth().addStateDidChangeListener { [weak self] auth, user in + // We could safely assume main actor isolation here, see + // https://firebase.google.com/docs/reference/swift/firebaseauth/api/reference/Classes/Auth#/c:@M@FirebaseAuth@objc(cs)FIRAuth(im)addAuthStateDidChangeListener: self?.handleStateDidChange(auth: auth, user: user) } @@ -528,32 +527,30 @@ public final class FirebaseAccountService: AccountService { // swiftlint:disable extension FirebaseAccountService { @MainActor - private func handleStateDidChange(auth: Auth, user: User?) { - if isConfiguring { - // We can safely assume main actor isolation, see - // https://firebase.google.com/docs/reference/swift/firebaseauth/api/reference/Classes/Auth#/c:@M@FirebaseAuth@objc(cs)FIRAuth(im)addAuthStateDidChangeListener: - let skip = MainActor.assumeIsolated { - // Ensure that there are details associated as soon as possible. - // Mark them as incomplete if we know there might be account details that are stored externally, - // we update the details later anyways, even if we might be wrong. - if let user { - var details = buildUser(user, isNewUser: false) - details.isIncomplete = !self.unsupportedKeys.isEmpty - - logger.debug("Supply initial user details of associated Firebase account.") - account.supplyUserDetails(details) - return !details.isIncomplete - } else { - account.removeUserDetails() - return true - } - } - - guard !skip else { - return // don't spin of the task below if we know it wouldn't change anything. - } + private func checkForInitialUserAccount() { + guard let user = Auth.auth().currentUser else { + skipNextStateChange = true + return } + // Ensure that there are details associated as soon as possible. + // Mark them as incomplete if we know there might be account details that are stored externally, + // we update the details later anyways, even if we might be wrong. + + var details = buildUser(user, isNewUser: false) + details.isIncomplete = !self.unsupportedKeys.isEmpty + + logger.debug("Supply initial user details of associated Firebase account.") + account.supplyUserDetails(details) + skipNextStateChange = !details.isIncomplete + } + + @MainActor + private func handleStateDidChange(auth: Auth, user: User?) { + if skipNextStateChange { + skipNextStateChange = false + return + } Task { do { diff --git a/Tests/UITests/TestApp/Shared/TestAppDelegate.swift b/Tests/UITests/TestApp/Shared/TestAppDelegate.swift index 0f64fc7..df4ba4d 100644 --- a/Tests/UITests/TestApp/Shared/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/Shared/TestAppDelegate.swift @@ -18,6 +18,29 @@ import SwiftUI class TestAppDelegate: SpeziAppDelegate { + private class Logout: Module { + @Application(\.logger) + private var logger + + @Dependency(Account.self) + private var account + @Dependency(FirebaseAccountService.self) + private var service + + func configure() { + if account.signedIn { + Task { [logger, service] in + do { + logger.info("Performing initial logout!") + try await service.logout() + } catch { + logger.error("Failed initial logout") + } + } + } + } + } + override var configuration: Configuration { Configuration { let configuration: AccountValueConfiguration = FeatureFlags.accountStorageTests @@ -46,6 +69,8 @@ class TestAppDelegate: SpeziAppDelegate { AccountConfiguration(service: service, configuration: configuration) } + Logout() + Firestore(settings: .emulator) FirebaseStorageConfiguration(emulatorSettings: (host: "localhost", port: 9199)) } diff --git a/Tests/UITests/TestAppUITests/FirebaseAccountStorageTests.swift b/Tests/UITests/TestAppUITests/FirebaseAccountStorageTests.swift index ec09f9b..331d829 100644 --- a/Tests/UITests/TestAppUITests/FirebaseAccountStorageTests.swift +++ b/Tests/UITests/TestAppUITests/FirebaseAccountStorageTests.swift @@ -31,11 +31,6 @@ final class FirebaseAccountStorageTests: XCTestCase { XCTAssert(app.buttons["FirebaseAccount"].waitForExistence(timeout: 2.0)) app.buttons["FirebaseAccount"].tap() - if app.buttons["Logout"].waitForExistence(timeout: 2.0) && app.buttons["Logout"].isHittable { - app.buttons["Logout"].tap() - } - - try app.signup( username: "test@username1.edu", password: "TestPassword1", diff --git a/Tests/UITests/TestAppUITests/FirebaseAccountTests.swift b/Tests/UITests/TestAppUITests/FirebaseAccountTests.swift index 5a2f423..10ff52b 100644 --- a/Tests/UITests/TestAppUITests/FirebaseAccountTests.swift +++ b/Tests/UITests/TestAppUITests/FirebaseAccountTests.swift @@ -37,14 +37,10 @@ final class FirebaseAccountTests: XCTestCase { // swiftlint:disable:this type_bo var accounts = try await FirebaseClient.getAllAccounts() XCTAssert(accounts.isEmpty) - - if app.buttons["Logout"].waitForExistence(timeout: 5.0) && app.buttons["Logout"].isHittable { - app.buttons["Logout"].tap() - } try app.signup(username: "test@username1.edu", password: "TestPassword1", givenName: "Test1", familyName: "Username1") - XCTAssert(app.buttons["Logout"].waitForExistence(timeout: 10.0)) + XCTAssert(app.buttons["Logout"].waitForExistence(timeout: 2.0)) app.buttons["Logout"].tap() try app.signup(username: "test@username2.edu", password: "TestPassword2", givenName: "Test2", familyName: "Username2") @@ -60,7 +56,7 @@ final class FirebaseAccountTests: XCTestCase { // swiftlint:disable:this type_bo ] ) - XCTAssert(app.buttons["Logout"].waitForExistence(timeout: 10.0)) + XCTAssert(app.buttons["Logout"].waitForExistence(timeout: 2.0)) app.buttons["Logout"].tap() } @@ -83,23 +79,19 @@ final class FirebaseAccountTests: XCTestCase { // swiftlint:disable:this type_bo app.launchArguments = ["--firebaseAccount"] app.launch() - XCTAssert(app.buttons["FirebaseAccount"].waitForExistence(timeout: 10.0)) + XCTAssert(app.buttons["FirebaseAccount"].waitForExistence(timeout: 2.0)) app.buttons["FirebaseAccount"].tap() - - if app.buttons["Logout"].waitForExistence(timeout: 3.0) && app.buttons["Logout"].isHittable { - app.buttons["Logout"].tap() - } try app.login(username: "test@username1.edu", password: "TestPassword1") - XCTAssert(app.staticTexts["test@username1.edu"].waitForExistence(timeout: 10.0)) - - XCTAssert(app.buttons["Logout"].waitForExistence(timeout: 10.0)) + XCTAssert(app.staticTexts["test@username1.edu"].waitForExistence(timeout: 2.0)) + + XCTAssert(app.buttons["Logout"].exists) app.buttons["Logout"].tap() try app.login(username: "test@username2.edu", password: "TestPassword2") - XCTAssert(app.staticTexts["test@username2.edu"].waitForExistence(timeout: 10.0)) - - XCTAssert(app.buttons["Logout"].waitForExistence(timeout: 10.0)) + XCTAssert(app.staticTexts["test@username2.edu"].waitForExistence(timeout: 2.0)) + + XCTAssert(app.buttons["Logout"].exists) app.buttons["Logout"].tap() } @@ -117,10 +109,6 @@ final class FirebaseAccountTests: XCTestCase { // swiftlint:disable:this type_bo XCTAssert(app.buttons["FirebaseAccount"].waitForExistence(timeout: 10.0)) app.buttons["FirebaseAccount"].tap() - if app.buttons["Logout"].waitForExistence(timeout: 5.0) && app.buttons["Logout"].isHittable { - app.buttons["Logout"].tap() - } - try app.login(username: "test@username.edu", password: "TestPassword") XCTAssert(app.staticTexts["test@username.edu"].waitForExistence(timeout: 10.0)) @@ -139,9 +127,7 @@ final class FirebaseAccountTests: XCTestCase { // swiftlint:disable:this type_bo let accounts2 = try await FirebaseClient.getAllAccounts() XCTAssertEqual( accounts2.sorted(by: { $0.email < $1.email }), - [ - FirestoreAccount(email: "test@username.edu", displayName: "Test Username") - ] + [FirestoreAccount(email: "test@username.edu", displayName: "Test Username")] ) } @@ -161,10 +147,6 @@ final class FirebaseAccountTests: XCTestCase { // swiftlint:disable:this type_bo XCTAssert(app.buttons["FirebaseAccount"].waitForExistence(timeout: 10.0)) app.buttons["FirebaseAccount"].tap() - if app.buttons["Logout"].waitForExistence(timeout: 5.0) && app.buttons["Logout"].isHittable { - app.buttons["Logout"].tap() - } - try app.login(username: "test@username.edu", password: "TestPassword") XCTAssert(app.staticTexts["test@username.edu"].waitForExistence(timeout: 10.0)) @@ -212,10 +194,6 @@ final class FirebaseAccountTests: XCTestCase { // swiftlint:disable:this type_bo XCTAssert(app.buttons["FirebaseAccount"].waitForExistence(timeout: 2.0)) app.buttons["FirebaseAccount"].tap() - if app.buttons["Logout"].waitForExistence(timeout: 2.0) && app.buttons["Logout"].isHittable { - app.buttons["Logout"].tap() - } - try app.login(username: "test@username.edu", password: "TestPassword") XCTAssert(app.staticTexts["test@username.edu"].waitForExistence(timeout: 10.0)) @@ -233,8 +211,8 @@ final class FirebaseAccountTests: XCTestCase { // swiftlint:disable:this type_bo try app.textFields["enter last name"].enter(value: "Test1") app.buttons["Done"].tap() - sleep(3) - XCTAssertTrue(app.staticTexts["Username Test1"].waitForExistence(timeout: 5.0)) + XCTAssertTrue(app.navigationBars.staticTexts["Name, E-Mail Address"].waitForExistence(timeout: 4.0)) + XCTAssertTrue(app.staticTexts["Name, Username Test1"].exists) // CHANGE EMAIL ADDRESS app.buttons["E-Mail Address, test@username.edu"].tap() @@ -251,8 +229,8 @@ final class FirebaseAccountTests: XCTestCase { // swiftlint:disable:this type_bo XCTAssertTrue(app.alerts["Authentication Required"].buttons["Login"].waitForExistence(timeout: 0.5)) app.alerts["Authentication Required"].buttons["Login"].tap() - sleep(3) - XCTAssertTrue(app.staticTexts["test@username.de"].waitForExistence(timeout: 5.0)) + XCTAssertTrue(app.navigationBars.staticTexts["Name, E-Mail Address"].waitForExistence(timeout: 4.0)) + XCTAssertTrue(app.staticTexts["E-Mail Address, test@username.de"].exists) let newAccounts = try await FirebaseClient.getAllAccounts() @@ -270,25 +248,25 @@ final class FirebaseAccountTests: XCTestCase { // swiftlint:disable:this type_bo app.launchArguments = ["--firebaseAccount"] app.launch() - XCTAssert(app.buttons["FirebaseAccount"].waitForExistence(timeout: 10.0)) + XCTAssert(app.buttons["FirebaseAccount"].waitForExistence(timeout: 2.0)) app.buttons["FirebaseAccount"].tap() - if app.buttons["Logout"].waitForExistence(timeout: 5.0) && app.buttons["Logout"].isHittable { - app.buttons["Logout"].tap() - } - try app.login(username: "test@username.edu", password: "TestPassword") - XCTAssert(app.staticTexts["test@username.edu"].waitForExistence(timeout: 10.0)) + XCTAssert(app.staticTexts["test@username.edu"].waitForExistence(timeout: 2.0)) + XCTAssertTrue(app.buttons["Account Overview"].exists) app.buttons["Account Overview"].tap() - XCTAssertTrue(app.staticTexts["test@username.edu"].waitForExistence(timeout: 5.0)) + XCTAssertTrue(app.staticTexts["test@username.edu"].waitForExistence(timeout: 2.0)) + XCTAssertTrue(app.buttons["Sign-In & Security"].exists) app.buttons["Sign-In & Security"].tap() - XCTAssertTrue(app.navigationBars.staticTexts["Sign-In & Security"].waitForExistence(timeout: 10.0)) + XCTAssertTrue(app.navigationBars.staticTexts["Sign-In & Security"].waitForExistence(timeout: 2.0)) + XCTAssertTrue(app.buttons["Change Password"].exists) app.buttons["Change Password"].tap() - XCTAssertTrue(app.navigationBars.staticTexts["Change Password"].waitForExistence(timeout: 10.0)) - sleep(2) + + + XCTAssertTrue(app.navigationBars.staticTexts["Change Password"].waitForExistence(timeout: 2.0)) try app.secureTextFields["enter password"].enter(value: "1234567890") app.dismissKeyboard() @@ -312,11 +290,13 @@ final class FirebaseAccountTests: XCTestCase { // swiftlint:disable:this type_bo XCTAssertTrue(app.alerts["Authentication Required"].buttons["Login"].waitForExistence(timeout: 0.5)) app.alerts["Authentication Required"].buttons["Login"].tap() - sleep(1) + XCTAssertTrue(app.navigationBars.buttons["Account Overview"].waitForExistence(timeout: 2.0)) app.navigationBars.buttons["Account Overview"].tap() // back button - sleep(1) - app.buttons["Close"].tap() - sleep(1) + + XCTAssertTrue(app.navigationBars.buttons["Close"].waitForExistence(timeout: 2.0)) + app.navigationBars.buttons["Close"].tap() + + XCTAssertTrue(app.buttons["Logout"].waitForExistence(timeout: 2.0)) app.buttons["Logout"].tap() // we tap the custom button to be lest dependent on the other tests and not deal with the alert try app.login(username: "test@username.edu", password: "1234567890", close: false) @@ -351,11 +331,13 @@ final class FirebaseAccountTests: XCTestCase { // swiftlint:disable:this type_bo XCTAssertTrue(app.alerts["Authentication Required"].buttons["Cancel"].waitForExistence(timeout: 0.5)) app.alerts["Authentication Required"].buttons["Cancel"].tap() - sleep(1) + XCTAssertTrue(app.navigationBars.buttons["Account Overview"].waitForExistence(timeout: 2.0)) app.navigationBars.buttons["Account Overview"].tap() // back button - sleep(1) - app.buttons["Close"].tap() - sleep(1) + + XCTAssertTrue(app.navigationBars.buttons["Close"].waitForExistence(timeout: 2.0)) + app.navigationBars.buttons["Close"].tap() + + XCTAssertTrue(app.buttons["Logout"].waitForExistence(timeout: 2.0)) app.buttons["Logout"].tap() // we tap the custom button to be lest dependent on the other tests and not deal with the alert try app.login(username: "test@username.edu", password: "TestPassword", close: false) // login with previous password! @@ -371,10 +353,6 @@ final class FirebaseAccountTests: XCTestCase { // swiftlint:disable:this type_bo XCTAssert(app.buttons["FirebaseAccount"].waitForExistence(timeout: 10.0)) app.buttons["FirebaseAccount"].tap() - if app.buttons["Logout"].waitForExistence(timeout: 5.0) && app.buttons["Logout"].isHittable { - app.buttons["Logout"].tap() - } - app.buttons["Account Setup"].tap() XCTAssertTrue(app.buttons["Forgot Password?"].waitForExistence(timeout: 2.0)) @@ -403,18 +381,18 @@ final class FirebaseAccountTests: XCTestCase { // swiftlint:disable:this type_bo app.launchArguments = ["--firebaseAccount"] app.launch() - XCTAssert(app.buttons["FirebaseAccount"].waitForExistence(timeout: 10.0)) + XCTAssert(app.buttons["FirebaseAccount"].waitForExistence(timeout: 2.0)) app.buttons["FirebaseAccount"].tap() - if app.buttons["Logout"].waitForExistence(timeout: 5.0) && app.buttons["Logout"].isHittable { - app.buttons["Logout"].tap() - } - try app.login(username: "unknown@example.de", password: "HelloWorld", close: false) - XCTAssertTrue(app.alerts["Invalid Credentials"].waitForExistence(timeout: 6.0)) - app.alerts["Invalid Credentials"].scrollViews.otherElements.buttons["OK"].tap() + XCTAssertTrue(app.alerts["Invalid Credentials"].waitForExistence(timeout: 3.0)) + XCTAssertTrue(app.alerts["Invalid Credentials"].scrollViews.buttons["OK"].exists) + app.alerts["Invalid Credentials"].scrollViews.buttons["OK"].tap() + + XCTAssertTrue(app.buttons["Close"].exists) app.buttons["Close"].tap() - sleep(2) + + XCTAssertTrue(app.buttons["Account Setup"].waitForExistence(timeout: 2.0)) // signing in with unknown credentials or credentials with a incorrect password are two different errors // that should, nonetheless, be treated equally in UI. @@ -432,10 +410,6 @@ final class FirebaseAccountTests: XCTestCase { // swiftlint:disable:this type_bo XCTAssert(app.buttons["FirebaseAccount"].waitForExistence(timeout: 10.0)) app.buttons["FirebaseAccount"].tap() - if app.buttons["Logout"].waitForExistence(timeout: 3.0) && app.buttons["Logout"].isHittable { - app.buttons["Logout"].tap() - } - app.buttons["Account Setup"].tap() addUIInterruptionMonitor(withDescription: "Apple Sign In") { element in @@ -461,10 +435,6 @@ final class FirebaseAccountTests: XCTestCase { // swiftlint:disable:this type_bo XCTAssert(app.buttons["FirebaseAccount"].waitForExistence(timeout: 2.0)) app.buttons["FirebaseAccount"].tap() - if app.buttons["Logout"].waitForExistence(timeout: 2.0) && app.buttons["Logout"].isHittable { - app.buttons["Logout"].tap() - } - XCTAssertTrue(app.buttons["Account Setup"].exists) app.buttons["Account Setup"].tap() @@ -493,13 +463,12 @@ extension XCUIApplication { try textFields["E-Mail Address"].enter(value: username) try secureTextFields["Password"].enter(value: password) - - swipeUp() scrollViews.buttons["Login"].tap() + if close { - sleep(3) // TODO: remove all sleeps! + XCTAssertTrue(staticTexts[username].waitForExistence(timeout: 5.0)) self.buttons["Close"].tap() } } @@ -529,4 +498,3 @@ extension XCUIApplication { buttons["Close"].tap() } } -// swiftlint:disable:this file_length