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 { private var testSession: TestSession? override func setUpWithError() throws { guard TestAccountAPIClient.isBackendReachable() else { throw XCTSkip("Local backend is not reachable at \(TestAccountAPIClient.baseURL)") } // Create a verified account via API so we have real credentials for reset guard let session = TestAccountManager.createVerifiedAccount() else { throw XCTSkip("Could not create verified test account") } testSession = session try super.setUpWithError() } // 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) resetScreen.waitForLoad(timeout: longTimeout) } // 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 TestFlows.completeForgotPasswordFlow( app: app, email: session.user.email, newPassword: newPassword ) // Wait for success indication - either success message or return to login let successText = app.staticTexts.containing( NSPredicate(format: "label CONTAINS[c] 'success' OR label CONTAINS[c] 'reset'") ).firstMatch let returnButton = app.buttons[UITestID.PasswordReset.returnToLoginButton] let deadline = Date().addingTimeInterval(longTimeout) var succeeded = false while Date() < deadline { if successText.exists || returnButton.exists { succeeded = true break } RunLoop.current.run(until: Date().addingTimeInterval(0.5)) } XCTAssertTrue(succeeded, "Expected success indication after password reset") // If return to login button appears, tap it if returnButton.exists && returnButton.isHittable { returnButton.tap() } // Verify we can login with the new password via API let loginResponse = TestAccountAPIClient.login( username: session.username, password: newPassword ) XCTAssertNotNil(loginResponse, "Should be able to login with new password after reset") } // 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) resetScreen.waitForLoad(timeout: longTimeout) } // 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() 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] let deadline = Date().addingTimeInterval(longTimeout) var resetSucceeded = false while Date() < deadline { if successText.exists || returnButton.exists { resetSucceeded = true break } RunLoop.current.run(until: Date().addingTimeInterval(0.5)) } XCTAssertTrue(resetSucceeded, "Expected success indication after password reset") // If the return-to-login button is present, tap it to go back to the login screen if returnButton.exists && returnButton.isHittable { returnButton.tap() } // Confirm the new password works by logging in via the API let loginResponse = TestAccountAPIClient.login( username: session.username, password: newPassword ) XCTAssertNotNil(loginResponse, "Should be able to login with the new password after a successful reset") } // 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) resetScreen.waitForLoad(timeout: longTimeout) 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") } }