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
This commit is contained in:
188
iOS/KordantUITests/AuthFlowUITests.swift
Normal file
188
iOS/KordantUITests/AuthFlowUITests.swift
Normal file
@@ -0,0 +1,188 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user