Files
Kordant/iOS/KordantUITests/AuthFlowUITests.swift
Michael Freno e33ddf3002 feat: complete Tasks 21-28 — backend integration, security hardening, UI tests & CI
- Add Apple Sign-In backend (JWKS verification, account linking, session management)
- Implement push notification deep linking with NotificationDeepLinkRouter
- Add jailbreak detection, runtime integrity monitoring, secure enclave service
- Implement OAuth social login, token refresh, and secure logout flows
- Add image caching (memory/disk), optimizer, upload queue, async semaphore
- Implement notification analytics, type preferences, and category setup
- Expand UI test suite with UITestBase, accessibility, auth flow, performance tests
- Add CI pipeline for iOS UI tests (3 device sizes) and performance benchmarks
- Restructure Xcode project to manual groups with KordantWidgets target
- Add SwiftLint, Swift Collections/Algorithms/GoogleSignIn dependencies
- Update project.yml for XcodeGen with new targets and configurations
2026-06-02 15:01:38 -04:00

189 lines
7.2 KiB
Swift

import XCTest
/// UI tests for authentication flows: login, signup, forgot password, toggle.
final class AuthFlowUITests: UITestBase {
/// Test class overrides scenario to unauthenticated
override class var scenario: UITestScenario { .unauthenticated }
// MARK: - Helpers
/// Relaunch the app with a specific scenario and reassign self.app
private func relaunch(scenario: UITestScenario) {
app.terminate()
app = XCUIApplication()
app.launchArguments = ["-UITesting"]
app.launchEnvironment["UITestScenario"] = scenario.rawValue
app.launch()
}
// MARK: - Launch & Branding
/// Verify the app launches to the auth screen when unauthenticated
func testLaunchAppShowsOnboardingScreen() {
// Verify branding elements are visible
XCTAssertTrue(text("Kordant").exists, "Brand name should be visible")
XCTAssertTrue(text("Protect what matters most").exists, "Tagline should be visible")
XCTAssertTrue(button("Continue with Google").exists, "Google sign-in button should exist")
}
// MARK: - Login / Signup Toggle
/// Verify user can toggle between login and signup forms
func testToggleBetweenLoginAndSignup() {
// Should start on login view
XCTAssertTrue(button("Sign In").exists, "Sign In button should be visible on login form")
// Tap the toggle link to show signup
button("Don't have an account? Sign up").tap()
XCTAssertTrue(button("Create Account").exists, "Create Account button should be visible on signup form")
// Tap back to login
button("Already have an account? Sign in").tap()
XCTAssertTrue(button("Sign In").exists, "Sign In button should be visible after toggling back")
}
// MARK: - Login with Valid Credentials
/// Test successful login navigates to the dashboard
func testLoginWithValidCredentialsNavigatesToDashboard() {
// Re-launch with authenticated scenario
relaunch(scenario: .authenticated)
// When already authenticated, the app should skip auth and show the main tab view
XCTAssertTrue(app.tabBars.buttons["Dashboard"].waitForExistence(timeout: 5),
"Dashboard tab should be visible when authenticated")
}
// MARK: - Login with Invalid Credentials
/// Test login with invalid credentials shows error state
func testLoginWithInvalidCredentialsShowsError() {
// Re-launch with authError scenario
relaunch(scenario: .authError)
// In .authError scenario, the mock API will fail
let emailField = app.textFields["Email"]
guard emailField.waitForExistence(timeout: 3) else {
XCTFail("Email field not found")
return
}
emailField.tap()
emailField.typeText("wrong@email.com")
let passwordField = app.secureTextFields["Password"]
passwordField.tap()
passwordField.typeText("wrongpassword")
button("Sign In").tap()
// Should show an error (either as inline text or alert)
let errorExists = app.staticTexts.containing(
NSPredicate(format: "label CONTAINS[c] 'error' OR label CONTAINS[c] 'Invalid' OR label CONTAINS[c] 'Unauthorized'")
).element.exists
|| app.alerts.element.exists
XCTAssertTrue(errorExists, "Error should be shown for invalid credentials")
captureScreen(name: "LoginInvalidCredentials")
}
// MARK: - Signup Form Validation
/// Test signup form shows validation errors for invalid input
func testSignupFormValidationShowsErrors() {
// Switch to signup
let toggleButton = button("Don't have an account? Sign up")
guard toggleButton.waitForExistence(timeout: 2) else {
XCTFail("Toggle to signup button not found")
return
}
toggleButton.tap()
// Wait for signup form to appear
XCTAssertTrue(button("Create Account").waitForExistence(timeout: 2),
"Create Account button should be visible on signup form")
// Try to submit empty form - tap Create Account with empty fields
button("Create Account").tap()
// Should show validation errors
let errorExists = app.staticTexts.containing(
NSPredicate(format: "label CONTAINS[c] 'required' OR label CONTAINS[c] 'must'")
).element.exists
XCTAssertTrue(errorExists, "Validation errors should be shown for empty form")
captureScreen(name: "SignupValidationErrors")
}
// MARK: - Forgot Password Flow
/// Test the forgot password flow shows confirmation
func testForgotPasswordFlowShowsConfirmation() {
// Re-launch with forgotPasswordSuccess scenario
relaunch(scenario: .forgotPasswordSuccess)
// Tap "Forgot password?" link
let forgotButton = button("Forgot password?")
guard forgotButton.waitForExistence(timeout: 3) else {
XCTFail("Forgot password link not found")
return
}
forgotButton.tap()
// Forgot password sheet should appear
let emailField = app.textFields["Email"]
guard emailField.waitForExistence(timeout: 3) else {
XCTFail("Email field in forgot password sheet not found")
return
}
emailField.tap()
emailField.typeText("test@kordant.com")
button("Send Reset Link").tap()
// Should see success state
let successExists = text("Check your email").waitForExistence(timeout: 3)
XCTAssertTrue(successExists, "Forgot password success state should be shown")
captureScreen(name: "ForgotPasswordSuccess")
}
// MARK: - Authenticated Scenario
/// Test that the authenticated scenario loads the dashboard
func testAuthenticatedScenarioLoadsDashboard() {
relaunch(scenario: .authenticated)
// Verify we're on the dashboard
XCTAssertTrue(app.navigationBars["Dashboard"].waitForExistence(timeout: 5),
"Dashboard navigation bar should appear after authentication")
}
// MARK: - Biometric Prompt
/// Test auth flow completes successfully (biometric prompt is shown when applicable)
func testAuthFlowCompletesSuccessfully() {
relaunch(scenario: .authenticated)
// Verify we reach the main app with tab bar
let dashboardTab = app.tabBars.buttons["Dashboard"]
XCTAssertTrue(dashboardTab.waitForExistence(timeout: 5),
"App should show tab bar after authentication")
}
// MARK: - Email Field Accessibility
/// Verify email and password fields are accessible in login form
func testLoginFormFieldsAreAccessible() {
// Verify fields exist on the login form
// With `.unauthenticated` scenario (current), should see login form
let emailField = app.textFields["Email"]
XCTAssertTrue(emailField.waitForExistence(timeout: 3),
"Email text field should exist on login form")
XCTAssertTrue(emailField.isEnabled, "Email field should be enabled")
let passwordField = app.secureTextFields["Password"]
XCTAssertTrue(passwordField.exists, "Password secure field should exist")
XCTAssertTrue(passwordField.isEnabled, "Password field should be enabled")
}
}