Close all 25 codex audit findings across KMP, iOS, and Android

Remediate all P0-S priority findings from cross-platform architecture audit:
- Harden token storage with EncryptedSharedPreferences (Android) and Keychain (iOS)
- Add SSL pinning and certificate validation to API clients
- Fix subscription cache race conditions and add thread-safe access
- Add input validation for document uploads and file type restrictions
- Refactor DocumentApi to use proper multipart upload flow
- Add rate limiting awareness and retry logic to API layer
- Harden subscription tier enforcement in SubscriptionHelper
- Add biometric prompt for sensitive actions (Login, Onboarding)
- Fix notification permission handling and device registration
- Add UI test infrastructure (page objects, fixtures, smoke tests)
- Add CI workflow for mobile builds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-18 13:15:34 -06:00
parent ffe5716167
commit 7444f73b46
56 changed files with 1539 additions and 569 deletions

View File

@@ -0,0 +1,73 @@
import XCTest
/// Base class for all page objects providing common waiting and assertion utilities.
///
/// Replaces ad-hoc `sleep()` calls with condition-based waits for reliable,
/// non-flaky UI tests. All screen page objects should inherit from this class.
class BaseScreen {
let app: XCUIApplication
let timeout: TimeInterval
init(app: XCUIApplication, timeout: TimeInterval = 10) {
self.app = app
self.timeout = timeout
}
// MARK: - Wait Helpers (replaces fixed sleeps)
/// Waits for an element to exist within the timeout period.
/// Fails the test with a descriptive message if the element does not appear.
@discardableResult
func waitForElement(_ element: XCUIElement, timeout: TimeInterval? = nil) -> XCUIElement {
let t = timeout ?? self.timeout
XCTAssertTrue(element.waitForExistence(timeout: t), "Element \(element) did not appear within \(t)s")
return element
}
/// Waits for an element to disappear within the timeout period.
/// Fails the test if the element is still present after the timeout.
func waitForElementToDisappear(_ element: XCUIElement, timeout: TimeInterval? = nil) {
let t = timeout ?? self.timeout
let predicate = NSPredicate(format: "exists == false")
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
let result = XCTWaiter().wait(for: [expectation], timeout: t)
XCTAssertEqual(result, .completed, "Element \(element) did not disappear within \(t)s")
}
/// Waits for an element to become hittable (visible and interactable).
/// Returns the element for chaining.
@discardableResult
func waitForHittable(_ element: XCUIElement, timeout: TimeInterval? = nil) -> XCUIElement {
let t = timeout ?? self.timeout
let predicate = NSPredicate(format: "isHittable == true")
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
_ = XCTWaiter().wait(for: [expectation], timeout: t)
return element
}
// MARK: - State Assertions
/// Asserts that an element with the given accessibility identifier exists.
func assertExists(_ identifier: String, file: StaticString = #file, line: UInt = #line) {
let element = app.descendants(matching: .any)[identifier]
XCTAssertTrue(element.waitForExistence(timeout: timeout), "Element '\(identifier)' not found", file: file, line: line)
}
/// Asserts that an element with the given accessibility identifier does not exist.
func assertNotExists(_ identifier: String, file: StaticString = #file, line: UInt = #line) {
let element = app.descendants(matching: .any)[identifier]
XCTAssertFalse(element.exists, "Element '\(identifier)' should not exist", file: file, line: line)
}
// MARK: - Navigation
/// Taps the first button in the navigation bar (typically the back button).
func tapBackButton() {
app.navigationBars.buttons.element(boundBy: 0).tap()
}
/// Subclasses must override this property to indicate whether the screen is currently displayed.
var isDisplayed: Bool {
fatalError("Subclasses must override isDisplayed")
}
}

View File

@@ -0,0 +1,86 @@
import XCTest
/// Page object for the login screen.
///
/// Uses accessibility identifiers from `AccessibilityIdentifiers.Authentication`
/// to locate elements. Provides typed actions for login flow interactions.
class LoginScreen: BaseScreen {
// MARK: - Elements
var emailField: XCUIElement {
app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
}
var passwordField: XCUIElement {
// Password field may be a SecureTextField or regular TextField depending on visibility toggle
let secure = app.secureTextFields[AccessibilityIdentifiers.Authentication.passwordField]
if secure.exists { return secure }
return app.textFields[AccessibilityIdentifiers.Authentication.passwordField]
}
var loginButton: XCUIElement {
app.buttons[AccessibilityIdentifiers.Authentication.loginButton]
}
var appleSignInButton: XCUIElement {
app.buttons[AccessibilityIdentifiers.Authentication.appleSignInButton]
}
var signUpButton: XCUIElement {
app.buttons[AccessibilityIdentifiers.Authentication.signUpButton]
}
var forgotPasswordButton: XCUIElement {
app.buttons[AccessibilityIdentifiers.Authentication.forgotPasswordButton]
}
var passwordVisibilityToggle: XCUIElement {
app.buttons[AccessibilityIdentifiers.Authentication.passwordVisibilityToggle]
}
var welcomeText: XCUIElement {
app.staticTexts["Welcome Back"]
}
override var isDisplayed: Bool {
emailField.waitForExistence(timeout: timeout)
}
// MARK: - Actions
/// Logs in with the provided credentials and returns a MainTabScreen.
/// Waits for the email field to appear before typing.
@discardableResult
func login(email: String, password: String) -> MainTabScreen {
waitForElement(emailField).tap()
emailField.typeText(email)
let pwField = passwordField
pwField.tap()
pwField.typeText(password)
loginButton.tap()
return MainTabScreen(app: app)
}
/// Taps the sign up / register link and returns a RegisterScreen.
@discardableResult
func tapSignUp() -> RegisterScreen {
waitForElement(signUpButton).tap()
return RegisterScreen(app: app)
}
/// Taps the forgot password link.
func tapForgotPassword() {
waitForElement(forgotPasswordButton).tap()
}
/// Toggles password visibility and returns whether the password is now visible.
@discardableResult
func togglePasswordVisibility() -> Bool {
waitForElement(passwordVisibilityToggle).tap()
// If a regular text field with the password identifier exists, password is visible
return app.textFields[AccessibilityIdentifiers.Authentication.passwordField].exists
}
}

View File

@@ -0,0 +1,88 @@
import XCTest
/// Page object for the main tab view that appears after login.
///
/// Provides navigation to each tab (Residences, Tasks, Contractors, Documents, Profile)
/// and a logout flow. Uses predicate-based element lookup to match the existing test patterns.
class MainTabScreen: BaseScreen {
// MARK: - Tab Elements
var residencesTab: XCUIElement {
app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
}
var tasksTab: XCUIElement {
app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch
}
var contractorsTab: XCUIElement {
app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch
}
var documentsTab: XCUIElement {
app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Documents'")).firstMatch
}
var profileTab: XCUIElement {
app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Profile'")).firstMatch
}
override var isDisplayed: Bool {
residencesTab.waitForExistence(timeout: timeout)
}
// MARK: - Navigation
@discardableResult
func goToResidences() -> Self {
waitForElement(residencesTab).tap()
return self
}
@discardableResult
func goToTasks() -> Self {
waitForElement(tasksTab).tap()
return self
}
@discardableResult
func goToContractors() -> Self {
waitForElement(contractorsTab).tap()
return self
}
@discardableResult
func goToDocuments() -> Self {
waitForElement(documentsTab).tap()
return self
}
@discardableResult
func goToProfile() -> Self {
waitForElement(profileTab).tap()
return self
}
// MARK: - Logout
/// Logs out by navigating to the Profile tab and tapping the logout button.
/// Handles the confirmation alert automatically.
func logout() {
goToProfile()
let logoutButton = app.buttons[AccessibilityIdentifiers.Profile.logoutButton]
if logoutButton.waitForExistence(timeout: 5) {
waitForHittable(logoutButton).tap()
// Handle confirmation alert
let alert = app.alerts.firstMatch
if alert.waitForExistence(timeout: 3) {
let confirmLogout = alert.buttons["Log Out"]
if confirmLogout.exists {
confirmLogout.tap()
}
}
}
}
}

View File

@@ -0,0 +1,86 @@
import XCTest
/// Page object for the registration screen.
///
/// Uses accessibility identifiers from `AccessibilityIdentifiers.Authentication`
/// to locate registration form elements and perform sign-up actions.
class RegisterScreen: BaseScreen {
// MARK: - Elements
var usernameField: XCUIElement {
app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
}
var emailField: XCUIElement {
app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField]
}
var passwordField: XCUIElement {
app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
}
var confirmPasswordField: XCUIElement {
app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
}
var registerButton: XCUIElement {
app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
}
var cancelButton: XCUIElement {
app.buttons[AccessibilityIdentifiers.Authentication.registerCancelButton]
}
/// Fallback element lookup for the register/create account button using predicate
var registerButtonByLabel: XCUIElement {
app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Register' OR label CONTAINS[c] 'Create Account'")).firstMatch
}
override var isDisplayed: Bool {
// Registration screen is visible if any of the register-specific fields exist
let usernameExists = usernameField.waitForExistence(timeout: timeout)
let emailExists = emailField.exists
return usernameExists || emailExists
}
// MARK: - Actions
/// Fills in the registration form and submits it.
/// Returns a MainTabScreen assuming successful registration leads to the main app.
@discardableResult
func register(username: String, email: String, password: String) -> MainTabScreen {
waitForElement(usernameField).tap()
usernameField.typeText(username)
emailField.tap()
emailField.typeText(email)
passwordField.tap()
passwordField.typeText(password)
confirmPasswordField.tap()
confirmPasswordField.typeText(password)
// Try accessibility identifier first, fall back to label search
if registerButton.exists {
registerButton.tap()
} else {
registerButtonByLabel.tap()
}
return MainTabScreen(app: app)
}
/// Taps cancel to return to the login screen.
@discardableResult
func tapCancel() -> LoginScreen {
if cancelButton.exists {
cancelButton.tap()
} else {
// Fall back to navigation back button
tapBackButton()
}
return LoginScreen(app: app)
}
}