import XCTest /// Tests for the password reset flow against the local backend (DEBUG=true, code=123456). /// /// Test Plan IDs: AUTH-015, AUTH-016, AUTH-017 final class PasswordResetTests: BaseUITestCase { override var relaunchBetweenTests: Bool { true } private var testSession: TestSession? private var cleaner: TestDataCleaner? override func setUpWithError() throws { guard TestAccountAPIClient.isBackendReachable() else { throw XCTSkip("Local backend is not reachable at \(TestAccountAPIClient.baseURL)") } guard let session = TestAccountManager.createVerifiedAccount() else { throw XCTSkip("Could not create verified test account") } testSession = session cleaner = TestDataCleaner(token: session.token) // Force clean app launch — password reset flow leaves complex screen state app.terminate() try super.setUpWithError() } override func tearDownWithError() throws { cleaner?.cleanAll() try super.tearDownWithError() } // MARK: - AUTH-015: Verify reset code reaches new password screen func testAUTH015_VerifyResetCodeSuccessPath() throws { let session = try XCTUnwrap(testSession) // Navigate to forgot password let login = TestFlows.navigateToLoginFromOnboarding(app: app) login.tapForgotPassword() // Enter email and send code let forgotScreen = ForgotPasswordScreen(app: app) forgotScreen.waitForLoad() forgotScreen.enterEmail(session.user.email) forgotScreen.tapSendCode() // Enter the debug verification code let verifyScreen = VerifyResetCodeScreen(app: app) verifyScreen.waitForLoad() verifyScreen.enterCode(TestAccountAPIClient.debugVerificationCode) verifyScreen.tapVerify() // Should reach the new password screen let resetScreen = ResetPasswordScreen(app: app) try resetScreen.waitForLoad(timeout: loginTimeout) } // MARK: - AUTH-016: Full reset password cycle + login with new password func testAUTH016_ResetPasswordSuccess() throws { let session = try XCTUnwrap(testSession) let newPassword = "NewPass9876!" // Navigate to forgot password let login = TestFlows.navigateToLoginFromOnboarding(app: app) login.tapForgotPassword() // Complete the full reset flow via UI try TestFlows.completeForgotPasswordFlow( app: app, email: session.user.email, newPassword: newPassword ) // After reset, the app auto-logs in with the new password. // If auto-login succeeds → app goes directly to main tabs (sheet dismissed). // If auto-login fails → success message + "Return to Login" button appear. let tabBar = app.tabBars.firstMatch let returnButton = app.buttons[UITestID.PasswordReset.returnToLoginButton] let deadline = Date().addingTimeInterval(loginTimeout) var reachedPostReset = false while Date() < deadline { if tabBar.exists { // Auto-login succeeded — password reset worked! reachedPostReset = true break } if returnButton.exists { // Auto-login failed — manual login needed reachedPostReset = true returnButton.forceTap() break } RunLoop.current.run(until: Date().addingTimeInterval(0.5)) } XCTAssertTrue(reachedPostReset, "Expected main tabs (auto-login) or return button (manual login) after password reset") if tabBar.exists { // Already logged in via auto-login — test passed return } // Manual login path: return button was tapped, now on login screen let loginScreen = LoginScreenObject(app: app) loginScreen.waitForLoad(timeout: loginTimeout) loginScreen.enterUsername(session.username) loginScreen.enterPassword(newPassword) let loginButton = app.buttons[UITestID.Auth.loginButton] loginButton.waitUntilHittable(timeout: 10).tap() XCTAssertTrue(tabBar.waitForExistence(timeout: loginTimeout), "Should login successfully with new password") } // MARK: - AUTH-015 (alias): Verify reset code reaches the new password screen func test03_verifyResetCodeSuccess() throws { try XCTSkipIf(!TestAccountAPIClient.isBackendReachable(), "Backend not reachable") let session = try XCTUnwrap(testSession) // Navigate to forgot password let login = TestFlows.navigateToLoginFromOnboarding(app: app) login.tapForgotPassword() // Enter email and send the reset code let forgotScreen = ForgotPasswordScreen(app: app) forgotScreen.waitForLoad() forgotScreen.enterEmail(session.user.email) forgotScreen.tapSendCode() // Enter the debug verification code on the verify screen let verifyScreen = VerifyResetCodeScreen(app: app) verifyScreen.waitForLoad() verifyScreen.enterCode(TestAccountAPIClient.debugVerificationCode) verifyScreen.tapVerify() // The reset password screen should now appear let resetScreen = ResetPasswordScreen(app: app) try resetScreen.waitForLoad(timeout: loginTimeout) } // MARK: - AUTH-016 (alias): Full reset flow + login with new password func test04_resetPasswordSuccessAndLogin() throws { try XCTSkipIf(!TestAccountAPIClient.isBackendReachable(), "Backend not reachable") let session = try XCTUnwrap(testSession) let newPassword = "NewPass9876!" // Navigate to forgot password, then drive the complete 3-step reset flow let login = TestFlows.navigateToLoginFromOnboarding(app: app) login.tapForgotPassword() try TestFlows.completeForgotPasswordFlow( app: app, email: session.user.email, newPassword: newPassword ) // Wait for a success indication — either a success message or the return-to-login button let successText = app.staticTexts.containing( NSPredicate(format: "label CONTAINS[c] 'success' OR label CONTAINS[c] 'reset'") ).firstMatch let returnButton = app.buttons[UITestID.PasswordReset.returnToLoginButton] // After reset, the app auto-logs in with the new password. // If auto-login succeeds → app goes to main tabs. If fails → return button appears. let tabBar = app.tabBars.firstMatch let deadline = Date().addingTimeInterval(loginTimeout) var reachedPostReset = false while Date() < deadline { if tabBar.exists { reachedPostReset = true break } if returnButton.exists { reachedPostReset = true returnButton.forceTap() break } RunLoop.current.run(until: Date().addingTimeInterval(0.5)) } XCTAssertTrue(reachedPostReset, "Expected main tabs (auto-login) or return button after password reset") if tabBar.exists { return } // Manual login fallback let loginScreen = LoginScreenObject(app: app) loginScreen.waitForLoad(timeout: loginTimeout) loginScreen.enterUsername(session.username) loginScreen.enterPassword(newPassword) let loginButton = app.buttons[UITestID.Auth.loginButton] loginButton.waitUntilHittable(timeout: 10).tap() XCTAssertTrue(tabBar.waitForExistence(timeout: loginTimeout), "Should login successfully with new password") } // MARK: - AUTH-017: Mismatched passwords are blocked func testAUTH017_MismatchedPasswordBlocked() throws { let session = try XCTUnwrap(testSession) // Navigate to forgot password let login = TestFlows.navigateToLoginFromOnboarding(app: app) login.tapForgotPassword() // Get to the reset password screen let forgotScreen = ForgotPasswordScreen(app: app) forgotScreen.waitForLoad() forgotScreen.enterEmail(session.user.email) forgotScreen.tapSendCode() let verifyScreen = VerifyResetCodeScreen(app: app) verifyScreen.waitForLoad() verifyScreen.enterCode(TestAccountAPIClient.debugVerificationCode) verifyScreen.tapVerify() // Enter mismatched passwords let resetScreen = ResetPasswordScreen(app: app) try resetScreen.waitForLoad(timeout: loginTimeout) resetScreen.enterNewPassword("ValidPass123!") resetScreen.enterConfirmPassword("DifferentPass456!") // The reset button should be disabled when passwords don't match XCTAssertFalse(resetScreen.isResetButtonEnabled, "Reset button should be disabled when passwords don't match") } }