diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 78ef59b8b0..60f6fb876e 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -1,12 +1,86 @@ -# Bitwarden iOS Password Manager & Authenticator Apps +# Bitwarden iOS Password Manager & Authenticator Apps Claude Guidelines + +Core directives for maintaining code quality and consistency in the Bitwarden iOS project. + +## Core Directives + +**You MUST follow these directives at all times.** + +1. **Adhere to Architecture**: All code modifications MUST follow patterns in `./../Docs/Architecture.md` +2. **Follow Code Style**: ALWAYS follow https://contributing.bitwarden.com/contributing/code-style/swift +3. **Follow Testing Guidelines**: Analyzing or implementing tests MUST follow guidelines in `./../Docs/Testing.md`. +4. **Best Practices**: Follow Swift / SwiftUI general best practices (value over reference types, guard clauses, extensions, protocol oriented programming) +5. **Document Everything**: Everything in the code requires DocC documentation except for protocol properties/functions implementations as the docs for them will be in the protocol. +6. **Dependency Management**: Use `ServiceContainer` as established in the project +7. **Use Established Patterns**: Leverage existing components before creating new ones +8. **File References**: Use file:line_number format when referencing code + +## Security Requirements + +**Every change must consider:** +- Zero-knowledge architecture preservation +- Proper encryption key handling (iOS Keychain) +- Input validation and sanitization +- Secure data storage patterns +- Threat model implications + +## Workflow Practices + +### Before Implementation + +1. Read relevant architecture documentation +2. Search for existing patterns to follow +3. Identify affected targets and dependencies +4. Consider security implications + +### During Implementation + +1. Follow existing code style in surrounding files +2. Write tests alongside implementation +3. Add DocC to everything except protocol implementations +4. Validate against architecture guidelines + +### After Implementation + +1. Ensure all tests pass +2. Verify compilation succeeds +3. Review security considerations +4. Update relevant documentation + +## Anti-Patterns + +**Avoid these:** +- Creating new patterns when established ones exist +- Exception-based error handling in business logic +- Direct dependency access (use DI) +- Undocumented public APIs +- Tight coupling between targets + +## Communication & Decision-Making + +Always clarify ambiguous requirements before implementing. Use specific questions: +- "Should this use [Approach A] or [Approach B]?" +- "This affects [X]. Proceed or review first?" +- "Expected behavior for [specific requirement]?" + +Defer high-impact decisions to the user: +- Architecture/module changes, public API modifications +- Security mechanisms, database migrations +- Third-party library additions ## References -- [iOS Architecture](./../Docs/Architecture.md) +### Critical resources: +- `./../Docs/Architecture.md` - Architecture patterns and principles +- `./../Docs/Testing.md` - Testing guidelines +- https://contributing.bitwarden.com/contributing/code-style/swift - Code style guidelines + +**Do not duplicate information from these files - reference them instead.** + +### Additional resources: - [Architectural Decision Records (ADRs)](https://contributing.bitwarden.com/architecture/adr/) - [Contributing Guidelines](https://contributing.bitwarden.com/contributing/) - [Accessibility](https://contributing.bitwarden.com/contributing/accessibility/) - [Setup Guide](https://contributing.bitwarden.com/getting-started/mobile/ios/) -- [Code Style](https://contributing.bitwarden.com/contributing/code-style/swift) - [Security Whitepaper](https://bitwarden.com/help/bitwarden-security-white-paper/) - [Security Definitions](https://contributing.bitwarden.com/architecture/security/definitions) diff --git a/.claude/prompts/review-code.md b/.claude/prompts/review-code.md index 93b97cfad3..b6af313f2c 100644 --- a/.claude/prompts/review-code.md +++ b/.claude/prompts/review-code.md @@ -1,9 +1,4 @@ -Please review this pull request with a focus on: - -- Code quality and best practices -- Potential bugs or issues -- Security implications -- Performance considerations +Use the `reviewing-changes` skill to review this pull request. Note: The PR branch is already checked out in the current working directory. diff --git a/.claude/skills/reviewing-changes/SKILL.md b/.claude/skills/reviewing-changes/SKILL.md new file mode 100644 index 0000000000..30c2a56fb0 --- /dev/null +++ b/.claude/skills/reviewing-changes/SKILL.md @@ -0,0 +1,109 @@ +--- +name: reviewing-changes +description: Performs comprehensive code reviews for Bitwarden iOS projects, verifying architecture compliance, style guidelines, compilation safety, test coverage, and security requirements. Use when reviewing pull requests, checking commits, analyzing code changes, verifying Bitwarden coding standards, evaluating unidirectional data flow pattern, checking services container dependency injection usage, reviewing security implementations, or assessing test coverage. Automatically invoked by CI pipeline or manually for interactive code reviews. +--- + +# Reviewing Changes + +## Instructions + +Follow this process to review code changes for Bitwarden iOS: + +### Step 1: Understand Context + +Start with high-level assessment of the change's purpose and approach. Read PR/commit descriptions and understand what problem is being solved. + +### Step 2: Verify Compliance + +Systematically check each area against Bitwarden standards documented in `CLAUDE.md`: + +1. **Architecture**: Follow patterns in `Docs/Architecture.md` + - Unidirectional data flow Coordinators + Processors (using SwiftUI) + - Dependency Injection using `ServiceContainer` + - Repository pattern and proper data flow + +2. **Style**: Adhere to [Code Style](https://contributing.bitwarden.com/contributing/code-style/swift) + - Naming conventions, code organization, formatting + - Swift/SwiftUI guidelines + +3. **Compilation**: Analyze for potential build issues + - Import statements and dependencies + - Type safety and null safety + - API compatibility and deprecation warnings + - Resource/SDK references and requirements + +4. **Testing**: Verify appropriate test coverage + - Unit tests for business logic and utility functions + - Snapshot/View inspector tests for user-facing features when applicable + - Test coverage for edge cases and error scenarios + +5. **Security**: Given Bitwarden's security-focused nature + - Proper handling of sensitive data + - Secure storage practices (Keychain) + - Authentication and authorization patterns + - Data encryption and decryption flows + - Zero-knowledge architecture preservation + +### Step 3: Document Findings + +Identify specific violations with `file:line_number` references. Be precise about locations. + +### Step 4: Provide Recommendations + +Give actionable recommendations for improvements. Explain why changes are needed and suggest specific solutions. + +### Step 5: Flag Critical Issues + +Highlight issues that must be addressed before merge. Distinguish between blockers and suggestions. + +### Step 6: Acknowledge Quality + +Note well-implemented patterns (briefly, without elaboration). Keep positive feedback concise. + +## Review Anti-Patterns (DO NOT) + +- Be nitpicky about linter-catchable style issues +- Review without understanding context - ask for clarification first +- Focus only on new code - check surrounding context for issues +- Request changes outside the scope of this changeset + +## Examples + +### Good Review Format + +```markdown +## Summary +This PR adds biometric authentication to the login flow, implementing unidirectional data flow pattern with proper state management. + +## Critical Issues +- `BitwardenShared/UI/Auth/Login/LoginView.swift:25` - No `scrollView` added, user can't scroll through the view. +- `BitwardenShared/Core/Auth/Services/AuthService.swift:120` - You must not use `try!`, change it to `try` in a `do...catch` block or throwing function. + +## Suggested Improvements +- Consider extracting biometric prompt logic to separate struct +- Add missing tests for biometric failure scenarios +- `BitwardenShared/UI/Auth/Login/LoginView.swift:43` - Consider using existing `BitwardenTextField` component + +## Good Practices +- Proper comments documentation +- Comprehensive unit test coverage +- Clear separation of concerns + +## Action Items +1. Add scroll view in `LoginView` +2. Change `try!` to `try` in `AuthService` +3. Consider adding tests for error flows +``` + +### What to Focus On + +**DO focus on:** +- Architecture violations (incorrect patterns) +- Security issues (data handling, encryption) +- Missing tests for critical paths +- Compilation risks (type safety, null safety) + +**DON'T focus on:** +- Minor formatting (handled by linters) +- Personal preferences without architectural basis +- Issues outside the changeset scope diff --git a/.gitignore b/.gitignore index 98e5f686ea..d0db926621 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,6 @@ Authenticator/Application/Support/Settings.bundle/Acknowledgements.latest_result # Backup files *.bak + +# AI +.claude/settings.local.json diff --git a/Docs/Architecture.md b/Docs/Architecture.md index ac3e142a58..9fc947d427 100644 --- a/Docs/Architecture.md +++ b/Docs/Architecture.md @@ -1,6 +1,11 @@ # Architecture - [Overview](#overview) + - [Password Manager App Targets](#password-manager-app-targets) + - [Authenticator App Targets](#authenticator-app-targets) + - [Shared Frameworks](#shared-frameworks) + - [Test Helpers](#test-helpers) + - [Architecture Structure](#architecture-structure) - [Core Layer](#core-layer) - [Models](#models) - [Data Stores](#data-stores) @@ -16,28 +21,50 @@ - [View](#view) - [Actions and Effects](#actions-and-effects) - [Example](#example) -- [Tests](#tests) - - [Overview](#overview-1) - - [Strategies](#strategies) +- [Test Architecture](#test-architecture) + - [Testing Philosophy](#testing-philosophy) + - [Test Layer Alignment](#test-layer-alignment) + - [Testing Strategies by Component Type](#testing-strategies-by-component-type) + - [Test Organization](#test-organization) + - [Dependency Mocking](#dependency-mocking) + - [Test Isolation](#test-isolation) ## Overview -The Bitwarden app is composed of the following targets: +The iOS repository contains two main apps: Bitwarden Password Manager and Bitwarden Authenticator. -- `Bitwarden`: The main iOS app. -- `BitwardenActionExtension`: An [Action extension](https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/Action.html) that can be accessed via the system share sheet "Autofill with Bitwarden" option. +### Password Manager App Targets + +- `Bitwarden`: The main iOS Password Manager app. +- `BitwardenActionExtension`: An [Action extension](https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/Action.html) that can be accessed via the system share sheet "Autofill with Bitwarden" option. - `BitwardenAutoFillExtension`: An AutoFill Credential Provider extension which allows Bitwarden to offer up credentials for [Password AutoFill](https://developer.apple.com/documentation/security/password_autofill/). -- `BitwardenShareExtension`: A [Share extension](https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/Share.html) that allows creating text or file sends via the system share sheet. +- `BitwardenShareExtension`: A [Share extension](https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/Share.html) that allows creating text or file sends via the system share sheet. - `BitwardenWatchApp`: The companion watchOS app. +- `BitwardenShared`: The main Password Manager framework containing the Core and UI layers shared between the app and extensions. + +### Authenticator App Targets -Additionally, the following top-level folders provide shared functionality between the targets: +- `Authenticator`: The main iOS Authenticator app. +- `AuthenticatorShared`: The main Authenticator framework containing the Core and UI layers. -- `BitwardenShared`: A framework that is shared between the app and extensions. +### Shared Frameworks + +- `BitwardenKit`: A shared framework providing common functionality across apps. +- `BitwardenResources`: A framework containing shared resources (assets, localizations, etc.). +- `AuthenticatorBridgeKit`: A framework for communication between the Password Manager and Authenticator apps. - `BitwardenWatchShared`: Models and encoding/decoding logic for communicating between the iOS and watchOS apps. -- `GlobalTestHelpers`: Shared functionality between the app's test targets. -- `Networking`: A local Swift package that implements the app's networking layer on top of `URLSession`. +- `Networking`: A local Swift package that implements the app's networking layer on top of `URLSession`. + +### Test Helpers -Most of the app's functionality is implemented in the `BitwardenShared` target. The files within this target are split up between two top-level folders, `Core` and `UI`. Each of these folders is then subdivided into the following folders: +- `GlobalTestHelpers`: Shared functionality between the app's test targets. +- `BitwardenKitMocks`: Mock implementations for BitwardenKit components. +- `AuthenticatorBridgeKitMocks`: Mock implementations for AuthenticatorBridgeKit components. +- `TestHelpers`: Additional test utilities and helpers. + +### Architecture Structure + +Most of the app's functionality is implemented in the `BitwardenShared` and `AuthenticatorShared` frameworks. The files within these frameworks are mainly split up between two top-level folders, `Core` and `UI`. Each of these folders is then subdivided into the following folders: - `Auth` - `Autofill` @@ -45,7 +72,7 @@ Most of the app's functionality is implemented in the `BitwardenShared` target. - `Tools` - `Vault` -These folders align with the [CODEOWNERS](../.github/CODEOWNERS) file for the project; no additional direct subfolders of `Core` or `UI` should be added. While this top-level structure is deliberately inflexible, the folder structure within the subfolders are not specifically prescribed. +These folders align with the [CODEOWNERS](../.github/CODEOWNERS) file for the project. One **MUST** not add additional direct subfolders to `Core` or `UI`. While this top-level structure is deliberately inflexible, the folder structure within the subfolders are not specifically prescribed. The responsibilities of the core layer are to manage the storage and retrieval of data from low-level sources (such as from the network, persistence, or Bitwarden SDK) and to expose them in a more ready-to-consume manner by the UI layer via "repository" and "service" classes. The UI layer is then responsible for any final processing of this data for display in the UI as well as receiving events from the UI and updating the tracked state accordingly. @@ -55,7 +82,7 @@ The core layer is where all the UI-independent data is stored and retrieved. It ### Models -The lowest level of the core layer are the data model objects. These are the raw sources of data that include data retrieved or sent via network requests, data persisted with [CoreData](https://developer.apple.com/documentation/coredata/), and data that is used to interact with the [Bitwarden SDK](https://github.com/bitwarden/sdk). +The lowest level of the core layer are the data model objects. These are the raw sources of data that include data retrieved or sent via network requests, data persisted with [CoreData](https://developer.apple.com/documentation/coredata/), and data that is used to interact with the [Bitwarden SDK](https://github.com/bitwarden/sdk-internal). The models are roughly organized based on their use and type: @@ -310,14 +337,67 @@ struct ExampleView: View { -## Tests +## Test Architecture + +### Testing Philosophy + +The test architecture mirrors the layered structure of the application, ensuring that each layer can be tested in isolation through dependency injection and protocol-based abstractions. + +### Test Layer Alignment + +#### Core Layer Testing + +Tests for the core layer focus on data integrity, business logic, and repository/service behavior: + +- **Repositories**: Test data synthesis from multiple sources, error handling, and asynchronous operations +- **Services**: Test discrete responsibilities and interactions with lower-level stores +- **Data Stores**: Test persistence operations (CoreData, Keychain, UserDefaults) +- **Models**: Test data transformations, encoding/decoding, and domain logic + +Core layer tests should use mocked dependencies to isolate the system under test from external services. + +#### UI Layer Testing + +Tests for the UI layer validate the unidirectional data flow and state management: + +- **Processors**: Test state mutations in response to actions, effect handling, and coordinator navigation requests +- **Coordinators**: Test route handling and child coordinator creation using mocked modules +- **Views**: Test UI rendering based on state and user interaction handling +- **State**: Test state equality and state transformations + +UI layer tests leverage the `Store` abstraction to verify the connection between processors and views. + +### Testing Strategies by Component Type + +The architecture employs three complementary testing strategies: + +1. **Logic Testing**: Unit tests validate business logic, state management, and data transformations using protocol mocks +2. **Interaction Testing**: View inspector tests verify user interactions send correct actions/effects through the store +3. **Visual Testing**: Snapshot tests capture visual regressions across different display modes and accessibility settings + +### Test Organization + +Test files are co-located with the code they test, maintaining the same folder structure as the main codebase. This organization: + +- Makes it easy to find tests for a given type +- Ensures tests evolve alongside the code +- Reinforces the architectural boundaries (Auth, Autofill, Platform, Tools, Vault) + +### Dependency Mocking + +The architecture's use of protocol composition in the `Services` typealias enables comprehensive mocking: + +- All services and repositories are defined as protocols +- Mock implementations are generated using Sourcery's `AutoMockable` annotation +- Coordinators can be tested with mocked modules to verify navigation logic +- Processors can be tested with mocked services to verify state management -### Overview +### Test Isolation -Every type containing logic should be tested. Test files should be named `Tests.swift`. A test file should exist in the same folder as the type being tested. For example, [AppProcessorTests](../BitwardenShared/UI/Platform/Application/AppProcessorTests.swift) is in the same folder as [AppProcessor](../BitwardenShared/UI/Platform/Application/AppProcessor.swift). This makes it convenient to switch between these files or open them side-by-side. +Each architectural layer can be tested independently: -### Strategies +- **Core layer** tests mock network responses, SDK interactions, and persistence layers +- **UI layer** tests mock repositories and services from the core layer +- **Integration points** between layers are tested by verifying protocol conformance -- **Unit**: Unit tests compose the majority of tests in the suite. These are written using [XCTest](https://developer.apple.com/documentation/xctest) assertions and should be used to test all logic portions within a type. -- **View**: In a SwiftUI view test, [ViewInspector](https://github.com/nalexn/ViewInspector) is used to test any user interactions within the view. This is commonly used to assert that tapping a button sends an action or effect to the processor, but it can also be used to test other view interactions. -- **Snapshot**: In addition to using [ViewInspector](https://github.com/nalexn/ViewInspector) to interact with a view under test, [SnapshotTesting](https://github.com/pointfreeco/swift-snapshot-testing) is used to take snapshots of the view to test for visual changes from one test run to another. The resulting snapshot images are stored in the repository and are compared against on future test runs. Any visual differences on future test runs will result in a failing test. Snapshot tests are usually recorded in light mode, dark mode, and with a large dynamic type size. ⚠️ These tests are done using the simulator device and iOS version specified in `.test-simulator-device-name` and `.test-simulator-ios-version` files, otherwise tests may fail because of subtle differences between iOS versions. +This isolation enables fast, reliable tests that pinpoint failures to specific architectural components. diff --git a/Docs/Testing.md b/Docs/Testing.md new file mode 100644 index 0000000000..79460547ae --- /dev/null +++ b/Docs/Testing.md @@ -0,0 +1,1089 @@ +# Testing Guide + +## Table of Contents + +- [Testing Philosophy](#testing-philosophy) +- [Test File Naming and Location](#test-file-naming-and-location) +- [Decision Tree: What Tests to Write](#decision-tree-what-tests-to-write) +- [Testing Strategies](#testing-strategies) +- [Testing by Component Type](#testing-by-component-type) + - [Testing Processors](#testing-processors) + - [Testing Services](#testing-services) + - [Testing Repositories](#testing-repositories) + - [Testing Views](#testing-views) + - [Testing Coordinators](#testing-coordinators) +- [Common Testing Patterns](#common-testing-patterns) +- [Mock Generation and Usage](#mock-generation-and-usage) +- [Running Tests](#running-tests) + - [Test Plans](#test-plans) + - [Running Tests from Command Line](#running-tests-from-command-line) + - [Running Tests from Xcode](#running-tests-from-xcode) + - [Simulator Configuration](#simulator-configuration) + - [Recording New Snapshots](#recording-new-snapshots) +- [Test Maintenance](#test-maintenance) + - [When to Update Tests](#when-to-update-tests) + - [Test Smells to Avoid](#test-smells-to-avoid) + - [Test Coverage Guidelines](#test-coverage-guidelines) + - [Debugging Failing Tests](#debugging-failing-tests) + - [Continuous Integration](#continuous-integration) +- [Quick Reference for AI Agents](#quick-reference-for-ai-agents) + - [Decision Matrix: Test Type Selection](#decision-matrix-test-type-selection) + - [Common Test Patterns Quick Reference](#common-test-patterns-quick-reference) + - [Test Checklist for AI Agents](#test-checklist-for-ai-agents) + - [Test Ordering Guidelines](#test-ordering-guidelines) + +## Testing Philosophy + +Every type containing logic **must** be tested. The test suite should: + +1. Validate business logic and state management +2. Verify user interactions trigger correct behaviors +3. Catch visual regressions across display modes +4. Enable confident refactoring through comprehensive coverage + +## Test File Naming and Location + +### Naming Conventions + +- **Unit tests**: `Tests.swift` +- **Snapshot tests**: `+SnapshotTests.swift` +- **View Inspector tests**: `+ViewInspectorTests.swift` + +### File Location + +Test files **must** be co-located with the implementation file in the same folder: + +``` +BitwardenShared/UI/Platform/Application/ +├── AppProcessor.swift +├── AppProcessorTests.swift # Unit tests +├── AppView.swift +├── AppView+SnapshotTests.swift # Snapshot tests +└── AppView+ViewInspectorTests.swift # View inspector tests +``` + +This makes it easy to: +- Find tests for a specific type +- Open implementation and tests side-by-side +- Ensure tests evolve with the code + +## Decision Tree: What Tests to Write + +Use this decision tree to determine which tests to write for a new or modified component: + +``` +┌─────────────────────────────────────┐ +│ What type of component is this? │ +└─────────────────────────────────────┘ + │ + ┌────────────┼────────────┐ + │ │ │ +┌───▼────┐ ┌────▼───┐ ┌─────▼──┐ +│ Model │ │ Logic │ │ View │ +└───┬────┘ └───┬────┘ └────┬───┘ + +┌──────────────────────────────────────┐ +│ Model (Domain/Request/Response) │ +│ ✓ Unit tests for: │ +│ - Codable conformance │ +│ - Custom init logic │ +│ - Computed properties │ +│ - Validation methods │ +└──────────────────────────────────────┘ + +┌──────────────────────────────────────┐ +│ Logic Component │ +│ (Processor/Service/Repository/Store) │ +│ ✓ Unit tests for: │ +│ - All public methods │ +│ - State mutations │ +│ - Error handling │ +│ - Async operations │ +│ - Edge cases │ +└──────────────────────────────────────┘ + +┌──────────────────────────────────────┐ +│ View (SwiftUI) │ +│ ✓ ViewInspector tests for: │ +│ - Button taps send actions │ +│ - Toggle changes send actions │ +│ - TextField bindings work │ +│ - Navigation triggers │ +│ ✓ Snapshot tests for: │ +│ - Light mode │ +│ - Dark mode │ +│ - Large dynamic type │ +│ - Loading/error/empty states │ +└──────────────────────────────────────┘ + +┌──────────────────────────────────────┐ +│ Coordinator │ +│ ✓ Unit tests for: │ +│ - Route navigation │ +│ - Child coordinator creation │ +│ - Event handling │ +│ - Context passing │ +└──────────────────────────────────────┘ +``` + +## Testing Strategies + +### 1. Unit Tests (XCTest / Swift Testing) + +**Purpose**: Validate business logic, state management, and data transformations + +**Tools**: +- [XCTest](https://developer.apple.com/documentation/xctest) (legacy, still widely used) +- [Swift Testing Framework](https://developer.apple.com/xcode/swift-testing/) (preferred for new tests) + +**Use for**: +- Processors (state mutations, action handling, effects) +- Services (business logic, data transformations) +- Repositories (data synthesis, error handling) +- Data Stores (persistence operations) +- Models (codable, computed properties, validation) +- Coordinators (navigation logic) + +### 2. ViewInspector Tests + +**Purpose**: Verify user interactions send correct actions/effects through the Store + +**Tool**: [ViewInspector](https://github.com/nalexn/ViewInspector) + +**Use for**: +- Button taps trigger actions +- Toggle changes send state updates +- TextField bindings work correctly +- Sheet/alert presentations +- Navigation link triggers + +### 3. Snapshot Tests + +**Purpose**: Catch visual regressions across different display modes + +**Tool**: [SnapshotTesting](https://github.com/pointfreeco/swift-snapshot-testing) + +**Use for**: +- All user-facing views +- Different states (loading, error, empty, populated) +- Accessibility configurations + +**Required Snapshots**: +- ✅ Light mode +- ✅ Dark mode +- ✅ Large dynamic type (accessibility) + +**Important**: Snapshot tests **must** run on the specific simulator defined in: +- Device: [.test-simulator-device-name](../.test-simulator-device-name) +- iOS Version: [.test-simulator-ios-version](../.test-simulator-ios-version) + +Otherwise, tests will fail due to rendering differences between iOS versions. + +## Testing by Component Type + +### Testing Processors + +Processors manage state and handle actions/effects. Focus on: + +#### What to Test +- ✅ Initial state is correct +- ✅ Actions update state correctly +- ✅ Effects perform async work and update state +- ✅ Navigation requests are sent to coordinator +- ✅ Error handling updates state appropriately + +#### Example Test Structure + +```swift +@testable import BitwardenShared +import XCTest + +class ExampleProcessorTests: BitwardenTestCase { + var subject: ExampleProcessor! + var coordinator: MockCoordinator! + var exampleRepository: MockExampleRepository! + + override func setUp() { + super.setUp() + coordinator = MockCoordinator() + exampleRepository = MockExampleRepository() + + subject = ExampleProcessor( + coordinator: coordinator.asAnyCoordinator(), + services: ServiceContainer.withMocks( + exampleRepository: exampleRepository + ), + state: ExampleState() + ) + } + + override func tearDown() { + super.tearDown() + subject = nil + coordinator = nil + exampleRepository = nil + } + + // Test action handling + func test_receive_toggleAction_updatesState() { + subject.state.isToggleOn = false + + subject.receive(.toggleChanged(true)) + + XCTAssertTrue(subject.state.isToggleOn) + } + + // Test effect handling + func test_perform_loadData_success_updatesState() async { + exampleRepository.loadDataResult = .success("Test Data") + + await subject.perform(.loadData) + + XCTAssertEqual(subject.state.data, "Test Data") + XCTAssertFalse(subject.state.isLoading) + } + + // Test navigation + func test_receive_nextAction_navigatesToNextScreen() { + subject.receive(.next) + + XCTAssertEqual(coordinator.routes.last, .nextExample) + } + + // Test error handling + func test_perform_loadData_failure_showsError() async { + exampleRepository.loadDataResult = .failure(BitwardenTestError.example) + + await subject.perform(.loadData) + + XCTAssertNotNil(subject.state.errorAlert) + } +} +``` + +### Testing Services + +Services have discrete responsibilities. Focus on: + +#### What to Test +- ✅ All public method signatures +- ✅ Data transformations +- ✅ Error propagation +- ✅ Interaction with dependencies (use mocks) +- ✅ Edge cases and boundary conditions + +#### Example Pattern + +```swift +class ExampleServiceTests: BitwardenTestCase { + var subject: DefaultExampleService! + var dataStore: MockDataStore! + var apiService: MockAPIService! + + override func setUp() { + super.setUp() + dataStore = MockDataStore() + apiService = MockAPIService() + subject = DefaultExampleService( + dataStore: dataStore, + apiService: apiService + ) + } + + func test_fetchData_returnsMergedData() async throws { + // Arrange + dataStore.fetchResult = [/* local data */] + apiService.fetchResult = .success(/* remote data */) + + // Act + let result = try await subject.fetchData() + + // Assert + XCTAssertEqual(result.count, expectedCount) + XCTAssertTrue(dataStore.fetchCalled) + XCTAssertTrue(apiService.fetchCalled) + } +} +``` + +### Testing Repositories + +Repositories synthesize data from multiple sources. Focus on: + +#### What to Test +- ✅ Data synthesis from multiple services +- ✅ Async operation coordination +- ✅ Error handling from various sources +- ✅ Publisher streams emit correct values +- ✅ State synchronization + +#### Example Pattern + +```swift +class ExampleRepositoryTests: BitwardenTestCase { + var subject: DefaultExampleRepository! + var exampleService: MockExampleService! + var otherService: MockOtherService! + + func test_loadData_combinesMultipleSources() async throws { + exampleService.dataResult = .success(data1) + otherService.dataResult = .success(data2) + + let result = try await subject.loadData() + + // Verify data was combined correctly + XCTAssertEqual(result.combinedField, expectedValue) + } +} +``` + +### Testing Views + +Views render state and send actions. Focus on: + +#### ViewInspector Tests: User Interactions + +```swift +@testable import BitwardenShared +import ViewInspector +import XCTest + +class ExampleViewTests: BitwardenTestCase { + var processor: MockProcessor! + var subject: ExampleView! + + override func setUp() { + super.setUp() + processor = MockProcessor(state: ExampleState()) + subject = ExampleView(store: Store(processor: processor)) + } + + // Test button tap + func test_nextButton_tapped_sendsAction() throws { + let button = try subject.inspect().find(button: "Next") + try button.tap() + + XCTAssertEqual(processor.actions.last, .next) + } + + // Test toggle + func test_toggle_changed_sendsAction() throws { + let toggle = try subject.inspect().find(ViewType.Toggle.self) + try toggle.tap() + + XCTAssertEqual(processor.actions.last, .toggleChanged(true)) + } +} +``` + +#### Snapshot Tests: Visual Verification + +```swift +@testable import BitwardenShared +import SnapshotTesting +import XCTest + +class ExampleView_SnapshotTests: BitwardenTestCase { + var subject: ExampleView! + + override func setUp() { + super.setUp() + subject = ExampleView( + store: Store( + processor: StateProcessor( + state: ExampleState(/* configure state */) + ) + ) + ) + } + + // Test all required modes + func test_snapshot_lightMode() { + assertSnapshot(of: subject, as: .defaultPortrait) + } + + func test_snapshot_darkMode() { + assertSnapshot(of: subject, as: .defaultPortraitDark) + } + + func test_snapshot_largeDynamicType() { + assertSnapshot(of: subject, as: .defaultPortraitAX5) + } + + // Test different states + func test_snapshot_loadingState() { + subject.store.state.isLoading = true + assertSnapshot(of: subject, as: .defaultPortrait) + } + + func test_snapshot_errorState() { + subject.store.state.errorMessage = "Something went wrong" + assertSnapshot(of: subject, as: .defaultPortrait) + } +} +``` + +### Testing Coordinators + +Coordinators handle navigation. Focus on: + +#### What to Test +- ✅ Route navigation creates correct views/coordinators +- ✅ Child coordinators are created with correct dependencies +- ✅ Event handling triggers correct routes +- ✅ Context is passed correctly + +#### Example Pattern + +```swift +class ExampleCoordinatorTests: BitwardenTestCase { + var subject: ExampleCoordinator! + var module: MockAppModule! + var stackNavigator: MockStackNavigator! + + override func setUp() { + super.setUp() + module = MockAppModule() + stackNavigator = MockStackNavigator() + subject = ExampleCoordinator( + module: module, + services: ServiceContainer.withMocks(), + stackNavigator: stackNavigator + ) + } + + func test_navigate_example_showsView() { + subject.navigate(to: .example) + + XCTAssertTrue(stackNavigator.pushed) + XCTAssertTrue(stackNavigator.pushedView is ExampleView) + } + + func test_navigate_nextExample_createsChildCoordinator() { + subject.navigate(to: .nextExample) + + XCTAssertTrue(module.makeNextExampleCoordinatorCalled) + } +} +``` + +## Common Testing Patterns + +### Pattern 1: Testing Async Operations + +```swift +func test_asyncOperation_updatesState() async { + // Setup mock result + mockService.result = .success(expectedData) + + // Perform async effect + await subject.perform(.loadData) + + // Assert state was updated + XCTAssertEqual(subject.state.data, expectedData) + XCTAssertFalse(subject.state.isLoading) +} +``` + +### Pattern 2: Testing Error Handling + +```swift +func test_operation_failure_showsAlert() async { + mockService.result = .failure(TestError.example) + + await subject.perform(.loadData) + + XCTAssertNotNil(subject.state.alert) + XCTAssertEqual(subject.state.alert?.title, "Error") +} +``` + +### Pattern 3: Testing Publisher Streams + +```swift +func test_publisher_emitsCorrectValues() async throws { + var receivedValues: [String] = [] + + let cancellable = subject.dataPublisher + .sink { value in + receivedValues.append(value) + } + + // Trigger updates + await subject.updateData("Value1") + await subject.updateData("Value2") + + XCTAssertEqual(receivedValues, ["Value1", "Value2"]) + cancellable.cancel() +} +``` + +### Pattern 4: Testing State Equality + +```swift +func test_state_equality() { + let state1 = ExampleState(data: "test", isLoading: false) + let state2 = ExampleState(data: "test", isLoading: false) + let state3 = ExampleState(data: "other", isLoading: false) + + XCTAssertEqual(state1, state2) + XCTAssertNotEqual(state1, state3) +} +``` + +## Mock Generation and Usage + +### Generating Mocks with Sourcery + +The codebase uses [Sourcery](https://github.com/krzysztofzablocki/Sourcery) to auto-generate mocks. + +#### Mark a Protocol for Mocking + +```swift +// sourcery: AutoMockable +protocol ExampleService { + func fetchData() async throws -> [String] + var dataPublisher: AnyPublisher<[String], Never> { get } +} +``` + +#### Generated Mock Location + +Mocks are generated in: +- `BitwardenShared/Core/Platform/Models/Sourcery/AutoMockable.generated.swift` + +#### Using Generated Mocks + +```swift +class ExampleTests: BitwardenTestCase { + var mockService: MockExampleService! + + override func setUp() { + super.setUp() + mockService = MockExampleService() + } + + func test_example() async throws { + // Setup mock behavior + mockService.fetchDataResult = .success(["data1", "data2"]) + + // Use in system under test + let result = try await mockService.fetchData() + + // Verify + XCTAssertEqual(result.count, 2) + XCTAssertTrue(mockService.fetchDataCalled) + } +} +``` + +### ServiceContainer with Mocks + +Use `ServiceContainer.withMocks()` to inject dependencies: + +```swift +let services = ServiceContainer.withMocks( + exampleRepository: mockExampleRepository, + exampleService: mockExampleService +) + +let processor = ExampleProcessor( + coordinator: coordinator.asAnyCoordinator(), + services: services, + state: ExampleState() +) +``` + +## Running Tests + +### Test Plans + +Test plans are organized in the `TestPlans` folder. Each project has multiple test plans to allow running specific subsets of tests: + +#### Test Plan Structure + +- `{ProjectName}-Default.xctestplan`: All tests (unit, snapshot, view inspector) +- `{ProjectName}-Unit.xctestplan`: Unit tests only (any simulator) +- `{ProjectName}-Snapshot.xctestplan`: Snapshot tests only (specific simulator required) +- `{ProjectName}-ViewInspector.xctestplan`: View inspector tests only + +#### Available Projects + +- `Bitwarden`: Password Manager app +- `Authenticator`: Authenticator app +- `BitwardenKit`: Shared framework + +### Running Tests from Command Line + +#### Run All Tests + +```bash +xcodebuild test \ + -project Bitwarden.xcodeproj \ + -scheme Bitwarden \ + -testPlan Bitwarden-Default \ + -destination 'platform=iOS Simulator,name=iPhone 15 Pro' +``` + +#### Run Unit Tests Only + +```bash +xcodebuild test \ + -project Bitwarden.xcodeproj \ + -scheme Bitwarden \ + -testPlan Bitwarden-Unit \ + -destination 'platform=iOS Simulator,name=iPhone 15 Pro' +``` + +#### Run Snapshot Tests Only + +**Important**: Must use the specific simulator from configuration files: + +```bash +# Read the required simulator configuration +DEVICE=$(cat .test-simulator-device-name) +IOS_VERSION=$(cat .test-simulator-ios-version) + +xcodebuild test \ + -project Bitwarden.xcodeproj \ + -scheme Bitwarden \ + -testPlan Bitwarden-Snapshot \ + -destination "platform=iOS Simulator,name=$DEVICE,OS=$IOS_VERSION" +``` + +#### Run a Specific Test + +```bash +xcodebuild test \ + -project Bitwarden.xcodeproj \ + -scheme Bitwarden \ + -only-testing:BitwardenShared-Tests/ExampleProcessorTests/test_receive_action_updatesState +``` + +### Running Tests from Xcode + +1. **Select Test Plan**: Product → Scheme → Edit Scheme → Test → Select Test Plan +2. **Run All Tests**: Cmd+U +3. **Run Specific Test**: Click the diamond icon next to the test method +4. **Run Test Class**: Click the diamond icon next to the class name + +### Simulator Configuration + +#### Unit Tests + +Unit tests can run on **any simulator** thanks to the `SKIP_SIMULATOR_CHECK_FOR_TESTS` environment variable enabled in all Unit test plans. + +#### Snapshot Tests + +Snapshot tests **must** run on the specific simulator to avoid false failures: + +- **Device**: Defined in [.test-simulator-device-name](../.test-simulator-device-name) +- **iOS Version**: Defined in [.test-simulator-ios-version](../.test-simulator-ios-version) + +To verify you're using the correct simulator: + +```bash +cat .test-simulator-device-name # e.g., "iPhone 15 Pro" +cat .test-simulator-ios-version # e.g., "17.0" +``` + +### Recording New Snapshots + +When creating new snapshot tests or updating UI: + +1. Run snapshot tests with recording enabled: + ```bash + # Set environment variable to record new snapshots + RECORD_MODE=1 xcodebuild test -testPlan Bitwarden-Snapshot ... + ``` + +2. Or in Xcode, edit the test scheme and add environment variable: + - Key: `RECORD_MODE` + - Value: `1` + +3. Run the tests to record snapshots + +4. Remove the environment variable and run tests again to verify + +5. Commit the new snapshot images with your changes + +## Test Maintenance + +### When to Update Tests + +#### After Changing Logic +- ✅ Update tests immediately after changing business logic +- ✅ Ensure all affected test cases pass +- ✅ Add new test cases for new branches/edge cases + +#### After Changing UI +- ✅ Update ViewInspector tests if interactions changed +- ✅ Re-record snapshots if visual changes are intentional +- ✅ Verify snapshots in all modes (light, dark, accessibility) + +#### After Refactoring +- ✅ Update test setup if dependencies changed +- ✅ Ensure tests still cover the same scenarios +- ✅ Remove obsolete tests for deleted code + +### Test Smells to Avoid + +#### ❌ Flaky Tests + +**Symptoms**: +- Tests pass sometimes, fail other times +- Tests depend on timing or order +- Tests depend on external state + +**Solutions**: +- Use async/await properly with proper waits +- Mock time-dependent operations +- Reset state in `setUp()` and `tearDown()` +- Avoid shared mutable state between tests + +#### ❌ Testing Multiple Concerns + +**Bad**: One test validates multiple unrelated things + +```swift +// Bad: Testing too much in one test +func test_everything() async { + await subject.perform(.loadData) + XCTAssertFalse(subject.state.isLoading) + XCTAssertNotNil(subject.state.data) + + subject.receive(.toggleChanged(true)) + XCTAssertTrue(subject.state.isToggleOn) + + subject.receive(.next) + XCTAssertEqual(coordinator.routes.last, .next) +} +``` + +**Good**: Separate tests for separate concerns + +```swift +// Good: One test, one concern +func test_perform_loadData_updatesState() async { + await subject.perform(.loadData) + XCTAssertEqual(subject.state.data, expectedData) +} + +func test_receive_toggleChanged_updatesToggle() { + subject.receive(.toggleChanged(true)) + XCTAssertTrue(subject.state.isToggleOn) +} + +func test_receive_next_navigates() { + subject.receive(.next) + XCTAssertEqual(coordinator.routes.last, .next) +} +``` + +#### ❌ Not Using Mocks + +**Bad**: Tests that depend on real services/network + +```swift +// Bad: Using real repository +func test_loadData() async { + let repository = DefaultExampleRepository(/* real dependencies */) + // This makes real API calls! +} +``` + +**Good**: Tests that use mocks for isolation + +```swift +// Good: Using mocked repository +func test_loadData() async { + mockRepository.loadDataResult = .success(testData) + await subject.perform(.loadData) + XCTAssertEqual(subject.state.data, testData) +} +``` + +### Test Coverage Guidelines + +#### Aim for High Coverage + +- **Processors**: 100% coverage of actions, effects, state mutations +- **Services**: 90%+ coverage of public methods +- **Repositories**: 90%+ coverage of public methods +- **Coordinators**: 80%+ coverage of routes +- **Views**: All user interactions tested with ViewInspector +- **Models**: Test custom logic, computed properties, validation + +#### What NOT to Test + +- ❌ Third-party framework internals +- ❌ Apple SDK behaviors +- ❌ Simple getters/setters without logic +- ❌ Trivial computed properties that delegate to another property + +### Debugging Failing Tests + +#### Step 1: Isolate the Failure + +```bash +# Run only the failing test +xcodebuild test -only-testing:Target/TestClass/testMethod +``` + +#### Step 2: Check Test Output + +- Read the failure message carefully +- Check expected vs actual values +- Look for assertion failures + +#### Step 3: Add Debugging + +```swift +func test_example() async { + print("State before: \(subject.state)") + await subject.perform(.loadData) + print("State after: \(subject.state)") + print("Mock was called: \(mockService.fetchDataCalled)") + + XCTAssertEqual(subject.state.data, expectedData) +} +``` + +#### Step 4: Check Mock Setup + +```swift +// Verify mock is configured correctly +func test_example() async { + // Explicitly verify mock result is set + XCTAssertNotNil(mockService.fetchDataResult) + + await subject.perform(.loadData) + + // Verify mock was called + XCTAssertTrue(mockService.fetchDataCalled) +} +``` + +#### Step 5: Snapshot Test Failures + +For snapshot test failures: + +1. Check if you're using the correct simulator +2. Review the failure diff images in the test results +3. If visual changes are intentional, re-record the snapshot +4. If unintentional, fix the UI bug + +### Continuous Integration + +Tests run automatically on CI for: +- ✅ Pull requests to `main` +- ✅ Commits to `main` +- ✅ Release branches + +CI runs all test plans: +- Unit tests (fast feedback) +- Snapshot tests (visual regression detection) +- View inspector tests (interaction validation) + +Ensure all tests pass locally before pushing to prevent CI failures. + +## Quick Reference for AI Agents + +### Decision Matrix: Test Type Selection + +| Component Type | Unit Tests | ViewInspector | Snapshots | +|---------------|------------|---------------|-----------| +| Processor | ✅ Required | ❌ N/A | ❌ N/A | +| Service | ✅ Required | ❌ N/A | ❌ N/A | +| Repository | ✅ Required | ❌ N/A | ❌ N/A | +| Store | ✅ Required | ❌ N/A | ❌ N/A | +| Coordinator | ✅ Required | ❌ N/A | ❌ N/A | +| Model | ✅ If logic | ❌ N/A | ❌ N/A | +| View | ❌ N/A | ✅ Required | ✅ Required | + +### Common Test Patterns Quick Reference + +```swift +// 1. Processor Test Template +func test_action_behavior() { + subject.receive(.action) + XCTAssertEqual(subject.state.property, expected) +} + +// 2. Async Effect Test Template +func test_effect_behavior() async { + mockService.result = .success(data) + await subject.perform(.effect) + XCTAssertEqual(subject.state.data, data) +} + +// 3. Navigation Test Template +func test_action_navigates() { + subject.receive(.action) + XCTAssertEqual(coordinator.routes.last, .expectedRoute) +} + +// 4. ViewInspector Test Template +func test_button_sendsAction() throws { + let button = try subject.inspect().find(button: "Title") + try button.tap() + XCTAssertEqual(processor.actions.last, .expectedAction) +} + +// 5. Snapshot Test Template +func test_snapshot_mode() { + assertSnapshot(of: subject, as: .defaultPortrait) +} +``` + +### Test Checklist for AI Agents + +When writing tests for a component, ensure: + +- [ ] Test file named correctly (`ComponentTests.swift`) +- [ ] Test file in same folder as implementation +- [ ] Inherits from `BitwardenTestCase` +- [ ] `setUp()` creates fresh instances +- [ ] `tearDown()` cleans up (sets to `nil`) +- [ ] All public methods tested +- [ ] Error cases tested +- [ ] Edge cases tested +- [ ] Mocks used for dependencies +- [ ] Assertions are specific (not just "not nil") +- [ ] Test names describe behavior (`test_action_outcome`) +- [ ] Tests ordered by function name (see ordering guidelines below) +- [ ] Async tests use `async`/`await` +- [ ] Views tested with ViewInspector AND Snapshots +- [ ] Snapshots include light/dark/accessibility modes + +### Test Ordering Guidelines + +Tests should be organized to maximize readability and maintainability: + +#### Ordering Principle + +**Primary Sort**: Alphabetically by the function/method being tested (second part of test name) +**Secondary Sort**: Logically by behavior cluster (not strictly alphabetical) + +#### Test Naming Pattern + +``` +test__ +``` + +- **Part 1**: Always `test_` +- **Part 2**: The function/method/action being tested (e.g., `receive`, `perform`, `loadData`) +- **Part 3**: The behavior being verified (e.g., `updatesState`, `showsError`, `navigates`) + +#### Ordering Examples + +**✅ Correct Ordering**: + +```swift +class ExampleProcessorTests: BitwardenTestCase { + // Tests for loadData() - grouped together, ordered logically + func test_loadData_success_updatesState() async { } + func test_loadData_failure_showsError() async { } + func test_loadData_emptyResponse_setsEmptyState() async { } + + // Tests for receive(_:) with different actions - grouped by action + func test_receive_cancelAction_dismissesView() { } + func test_receive_nextAction_navigates() { } + func test_receive_nextAction_whenInvalid_showsError() { } + func test_receive_toggleAction_updatesState() { } + func test_receive_toggleAction_whenDisabled_doesNothing() { } + + // Tests for saveData() - grouped together + func test_saveData_success_showsConfirmation() async { } + func test_saveData_failure_showsError() async { } + + // Tests for validateInput() - grouped together + func test_validateInput_validEmail_returnsTrue() { } + func test_validateInput_invalidEmail_returnsFalse() { } +} +``` + +**❌ Incorrect Ordering**: + +```swift +class ExampleProcessorTests: BitwardenTestCase { + // Bad: Tests scattered, not grouped by function + func test_loadData_success_updatesState() async { } + func test_receive_toggleAction_updatesState() { } + func test_saveData_failure_showsError() async { } + func test_loadData_failure_showsError() async { } + + // Bad: Strictly alphabetical by behavior ignores logical clustering + func test_receive_cancelAction_dismissesView() { } + func test_receive_nextAction_navigates() { } + func test_receive_toggleAction_updatesState() { } + func test_receive_toggleAction_whenDisabled_doesNothing() { } + func test_receive_nextAction_whenInvalid_showsError() { } // Should be with other nextAction tests +} +``` + +#### Rationale + +**Why group by function name?** +- Related tests stay together +- Easy to find all tests for a specific function +- Makes gaps in test coverage obvious + +**Why not strict alphabetical on behavior?** +- Logical flow is more important (success → failure → edge cases) +- Related behaviors should cluster together +- "Happy path" tests typically come before error cases + +#### Common Grouping Patterns + +1. **Success → Failure → Edge Cases** + ```swift + func test_fetchData_success_returnsData() async { } + func test_fetchData_failure_throwsError() async { } + func test_fetchData_emptyResponse_returnsEmptyArray() async { } + func test_fetchData_timeout_throwsTimeoutError() async { } + ``` + +2. **By Action Type (for receive/perform tests)** + ```swift + // Group all tests for the same action together + func test_receive_submitAction_validInput_savesData() { } + func test_receive_submitAction_invalidInput_showsError() { } + func test_receive_submitAction_emptyInput_showsValidationError() { } + ``` + +3. **By State Condition** + ```swift + func test_appear_authenticated_loadsData() async { } + func test_appear_unauthenticated_showsLogin() async { } + func test_appear_offline_showsOfflineMessage() async { } + ``` + +#### Special Cases + +**Lifecycle Methods**: Group together at the top +```swift +class ExampleTests: BitwardenTestCase { + // Initialization tests first + func test_init_setsInitialState() { } + func test_init_withParameters_configuresCorrectly() { } + + // Then alphabetically by function + func test_loadData_success() async { } + // ... +} +``` + +**Computed Properties**: Group by property name +```swift +func test_isValid_whenAllFieldsPopulated_returnsTrue() { } +func test_isValid_whenMissingFields_returnsFalse() { } +``` + +#### Quick Reference for AI Agents + +When adding a new test: +1. Find the group of tests for the same function/method +2. Add the new test within that group +3. Order within the group by logical flow (not strict alphabetical) +4. If testing a new function, insert the group alphabetically by function name