- 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
189 lines
7.2 KiB
Swift
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")
|
|
}
|
|
}
|