From a6c3cdfcc51609e99a8e0c9059424a1e86de86d3 Mon Sep 17 00:00:00 2001 From: Prachi Gauriar Date: Fri, 4 Jul 2025 17:58:27 -0400 Subject: [PATCH] Add .swift-format and fix lint warnings --- .github/workflows/VerifyChanges.yaml | 14 + .swift-format | 75 +++++ .../DevKeychainApp/DevKeychainApp.swift | 1 - .../GenericPasswordIntegrationTests.swift | 13 +- .../InternetPasswordIntegrationTests.swift | 13 +- .../StandardKeychainServicesTests.swift | 8 +- CLAUDE.md | 175 +++++++++++ Documentation/MarkdownStyleGuide.md | 190 ++++++++++++ Documentation/TestMocks.md | 275 ++++++++++++++++++ Package.swift | 3 +- README.md | 14 +- Scripts/install-git-hooks | 50 ++++ Scripts/lint | 11 + Sources/DevKeychain/Core/Keychain.swift | 9 +- .../Core/KeychainItemAdditionAttributes.swift | 5 +- .../DevKeychain/Core/KeychainItemQuery.swift | 1 - .../DevKeychain/Core/KeychainServices.swift | 1 - .../Errors/KeychainItemMappingError.swift | 3 +- .../Errors/KeychainServicesError.swift | 1 - .../Dictionary+KeychainItemMapping.swift | 1 - .../Keychain Items/GenericPassword.swift | 19 +- .../Keychain Items/InternetPassword.swift | 15 +- .../DevKeychainTests/Core/KeychainTests.swift | 4 +- .../Errors/KeychainErrorTests.swift | 2 +- .../KeychainItemMappingErrorTests.swift | 2 +- .../Dictionary+KeychainItemMappingTests.swift | 2 +- .../Keychain Items/GenericPasswordTests.swift | 9 +- .../InternetPasswordTests.swift | 9 +- .../Testing Helpers/MockError.swift | 1 - .../MockKeychainItemAdditionAttributes.swift | 10 +- .../MockKeychainItemQuery.swift | 16 +- .../Testing Helpers/MockKeychainService.swift | 19 +- .../RandomValueGenerating+DevKeychain.swift | 1 - 33 files changed, 868 insertions(+), 104 deletions(-) create mode 100644 .swift-format create mode 100644 CLAUDE.md create mode 100644 Documentation/MarkdownStyleGuide.md create mode 100644 Documentation/TestMocks.md create mode 100755 Scripts/install-git-hooks create mode 100755 Scripts/lint diff --git a/.github/workflows/VerifyChanges.yaml b/.github/workflows/VerifyChanges.yaml index fc3f83d..996e962 100644 --- a/.github/workflows/VerifyChanges.yaml +++ b/.github/workflows/VerifyChanges.yaml @@ -8,8 +8,22 @@ on: branches: [ $default-branch ] jobs: + lint: + name: Lint + runs-on: macos-15 + if: ${{ github.event_name == 'pull_request' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Select Xcode 16.4 + run: | + sudo xcode-select -s /Applications/Xcode_16.4.0.app + - name: Lint + run: | + Scripts/lint build-and-test: name: Build and Test (${{ matrix.platform }}) + needs: lint runs-on: macos-15 strategy: fail-fast: false diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..6cd31d6 --- /dev/null +++ b/.swift-format @@ -0,0 +1,75 @@ +{ + "fileScopedDeclarationPrivacy": { + "accessLevel": "private" + }, + "indentConditionalCompilationBlocks": false, + "indentSwitchCaseLabels": false, + "indentation": { + "spaces": 4 + }, + "lineBreakAroundMultilineExpressionChainComponents": false, + "lineBreakBeforeControlFlowKeywords": false, + "lineBreakBeforeEachArgument": false, + "lineBreakBeforeEachGenericRequirement": false, + "lineBreakBetweenDeclarationAttributes": false, + "lineLength": 120, + "maximumBlankLines": 2, + "multiElementCollectionTrailingCommas": true, + "noAssignmentInExpressions": { + "allowedFunctions": [] + }, + "prioritizeKeepingFunctionOutputTogether": true, + "reflowMultilineStringLiterals": { + "never": {} + }, + "respectsExistingLineBreaks": true, + "rules": { + "AllPublicDeclarationsHaveDocumentation": false, + "AlwaysUseLiteralForEmptyCollectionInit": true, + "AlwaysUseLowerCamelCase": true, + "AmbiguousTrailingClosureOverload": true, + "AvoidRetroactiveConformances": false, + "BeginDocumentationCommentWithOneLineSummary": true, + "DoNotUseSemicolons": true, + "DontRepeatTypeInStaticProperties": true, + "FileScopedDeclarationPrivacy": true, + "FullyIndirectEnum": true, + "GroupNumericLiterals": true, + "IdentifiersMustBeASCII": true, + "NeverForceUnwrap": false, + "NeverUseForceTry": false, + "NeverUseImplicitlyUnwrappedOptionals": false, + "NoAccessLevelOnExtensionDeclaration": true, + "NoAssignmentInExpressions": true, + "NoBlockComments": true, + "NoCasesWithOnlyFallthrough": true, + "NoEmptyLinesOpeningClosingBraces": false, + "NoEmptyTrailingClosureParentheses": true, + "NoLabelsInCasePatterns": true, + "NoLeadingUnderscores": false, + "NoParensAroundConditions": true, + "NoPlaygroundLiterals": true, + "NoVoidReturnOnFunctionSignature": true, + "OmitExplicitReturns": false, + "OneCasePerLine": true, + "OneVariableDeclarationPerLine": true, + "OnlyOneTrailingClosureArgument": true, + "OrderedImports": true, + "ReplaceForEachWithForLoop": true, + "ReturnVoidInsteadOfEmptyTuple": true, + "TypeNamesShouldBeCapitalized": true, + "UseEarlyExits": true, + "UseExplicitNilCheckInConditions": true, + "UseLetInEveryBoundCaseVariable": true, + "UseShorthandTypeNames": true, + "UseSingleLinePropertyGetter": true, + "UseSynthesizedInitializer": true, + "UseTripleSlashForDocumentationComments": true, + "UseWhereClausesInForLoops": true, + "ValidateDocumentationComments": false + }, + "spacesAroundRangeFormationOperators": true, + "spacesBeforeEndOfLineComments": 4, + "tabWidth": 4, + "version": 1 +} diff --git a/App/Sources/DevKeychainApp/DevKeychainApp.swift b/App/Sources/DevKeychainApp/DevKeychainApp.swift index d34cc9c..c734f73 100644 --- a/App/Sources/DevKeychainApp/DevKeychainApp.swift +++ b/App/Sources/DevKeychainApp/DevKeychainApp.swift @@ -7,7 +7,6 @@ import SwiftUI - @main struct DevKeychainAppApp: App { var body: some Scene { diff --git a/App/Tests/DevKeychainAppTests/GenericPasswordIntegrationTests.swift b/App/Tests/DevKeychainAppTests/GenericPasswordIntegrationTests.swift index 9db435c..dcbb267 100644 --- a/App/Tests/DevKeychainAppTests/GenericPasswordIntegrationTests.swift +++ b/App/Tests/DevKeychainAppTests/GenericPasswordIntegrationTests.swift @@ -10,7 +10,6 @@ import DevTesting import Foundation import Testing - #if !os(macOS) struct GenericPasswordIntegrationTests: RandomValueGenerating { var randomNumberGenerator = makeRandomNumberGenerator() @@ -25,12 +24,12 @@ struct GenericPasswordIntegrationTests: RandomValueGenerating { } ) let keychain = Keychain() - + // Delete any existing items with the service let serviceQuery = GenericPassword.Query(service: service) try keychain.deleteItems(matching: serviceQuery) #expect(try keychain.items(matching: serviceQuery).isEmpty) - + // Add something for each of our accounts for account in accounts { let attributes = GenericPassword.AdditionAttributes( @@ -38,20 +37,20 @@ struct GenericPasswordIntegrationTests: RandomValueGenerating { account: account, data: randomData() ) - + let addedItem = try keychain.addItem(with: attributes) #expect(addedItem.service == attributes.service) #expect(addedItem.account == attributes.account) #expect(addedItem.data == attributes.data) - + let queryResults = try keychain.items(matching: addedItem.query, options: .init(limit: 1)) #expect(queryResults == [addedItem]) } - + // Query all items with the service let allItems = try keychain.items(matching: serviceQuery) #expect(allItems.count == accounts.count) - + // Delete everything try keychain.deleteItems(matching: serviceQuery) #expect(try keychain.items(matching: serviceQuery).isEmpty) diff --git a/App/Tests/DevKeychainAppTests/InternetPasswordIntegrationTests.swift b/App/Tests/DevKeychainAppTests/InternetPasswordIntegrationTests.swift index 6b7e88b..9b27ead 100644 --- a/App/Tests/DevKeychainAppTests/InternetPasswordIntegrationTests.swift +++ b/App/Tests/DevKeychainAppTests/InternetPasswordIntegrationTests.swift @@ -10,7 +10,6 @@ import DevTesting import Foundation import Testing - #if !os(macOS) struct InternetPasswordIntegrationTests: RandomValueGenerating { var randomNumberGenerator = makeRandomNumberGenerator() @@ -25,12 +24,12 @@ struct InternetPasswordIntegrationTests: RandomValueGenerating { } ) let keychain = Keychain() - + // Delete any existing items with the server let serverQuery = InternetPassword.Query(server: server) try keychain.deleteItems(matching: serverQuery) #expect(try keychain.items(matching: serverQuery).isEmpty) - + // Add something for each of our accounts for account in accounts { let attributes = InternetPassword.AdditionAttributes( @@ -38,20 +37,20 @@ struct InternetPasswordIntegrationTests: RandomValueGenerating { account: account, data: randomData() ) - + let addedItem = try keychain.addItem(with: attributes) #expect(addedItem.server == attributes.server) #expect(addedItem.account == attributes.account) #expect(addedItem.data == attributes.data) - + let queryResults = try keychain.items(matching: addedItem.query, options: .init(limit: 1)) #expect(queryResults == [addedItem]) } - + // Query all items with the server let allItems = try keychain.items(matching: serverQuery) #expect(allItems.count == accounts.count) - + // Delete everything try keychain.deleteItems(matching: serverQuery) #expect(try keychain.items(matching: serverQuery).isEmpty) diff --git a/App/Tests/DevKeychainAppTests/StandardKeychainServicesTests.swift b/App/Tests/DevKeychainAppTests/StandardKeychainServicesTests.swift index e10154e..e6a4f23 100644 --- a/App/Tests/DevKeychainAppTests/StandardKeychainServicesTests.swift +++ b/App/Tests/DevKeychainAppTests/StandardKeychainServicesTests.swift @@ -5,11 +5,11 @@ // Created by Prachi Gauriar on 6/22/25. // -@testable import DevKeychain import DevTesting import Foundation import Testing +@testable import DevKeychain struct StandardKeychainServicesTests: RandomValueGenerating { var randomNumberGenerator = makeRandomNumberGenerator() @@ -29,7 +29,7 @@ struct StandardKeychainServicesTests: RandomValueGenerating { let serviceQuery: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: service, - kSecReturnAttributes: true + kSecReturnAttributes: true, ] try keychainServices.deleteItems(matchingQuery: serviceQuery) @@ -80,7 +80,7 @@ struct StandardKeychainServicesTests: RandomValueGenerating { let object = try keychainServices.items( matchingQuery: [ kSecClass: kSecClassGenericPassword, - kSecAttrService: randomAlphanumericString(count: 32) + kSecAttrService: randomAlphanumericString(count: 32), ] ) let items = try #require(object as? [Any]) @@ -106,7 +106,7 @@ struct StandardKeychainServicesTests: RandomValueGenerating { try keychainServices.deleteItems( matchingQuery: [ kSecClass: kSecClassGenericPassword, - kSecAttrService: randomAlphanumericString(count: 32) + kSecAttrService: randomAlphanumericString(count: 32), ] ) } diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..87755bf --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,175 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this +repository. + + +## Project Overview + +DevKeychain is a Swift package providing a modern, type-safe interface to Apple's keychain +services. It supports iOS 18+, macOS 15+, tvOS 18+, visionOS 2+, and watchOS 11+, requiring +Swift 6.1 toolchain. + + +## Common Development Commands + +### Building and Testing + + # Build the package + swift build + + # Run all tests + swift test + + # Run specific test + swift test --filter + + # Build and test with code coverage + swift test --enable-code-coverage + +### Code Quality + + # Format code (requires swift-format) + swift-format --in-place --recursive Sources/ Tests/ + + # Check formatting + swift-format --recursive Sources/ Tests/ + +### Git Hooks + + # Install pre-commit hooks (if Scripts directory exists) + Scripts/install-git-hooks + + # Run linting manually + Scripts/lint + + +## Architecture Overview + +### Core Design Pattern + +The codebase follows a **protocol-based architecture** with dependency injection: + + - `KeychainServices` protocol abstracts Security framework operations + - `StandardKeychainServices` provides real implementation + - Mock implementations enable comprehensive testing + - All keychain item types implement `KeychainItemAdditionAttributes` and `KeychainItemQuery` + protocols + +### Key Components + +**Core Layer** (`Sources/DevKeychain/Core/`): + + - `Keychain.swift`: Main keychain interface + - `KeychainServices.swift`: Protocol abstraction over Security framework + - `KeychainItemAdditionAttributes.swift` & `KeychainItemQuery.swift`: Generic protocols for + type-safe operations + +**Keychain Item Types** (`Sources/DevKeychain/Keychain Items/`): + + - `GenericPassword.swift`: Service/account-based passwords + - `InternetPassword.swift`: Server/account-based credentials + - Each type has nested `AdditionAttributes` and `Query` types + +**Error Handling** (`Sources/DevKeychain/Errors/`): + + - `KeychainServicesError.swift`: Wraps OSStatus errors from Security framework + - `KeychainItemMappingError.swift`: Data conversion and mapping errors + +### Testing Architecture + +Uses **Swift Testing** framework with comprehensive mocking: + + - All tests inject mock `KeychainServices` implementations + - `DevTesting` dependency provides Stub system for mocking + - Tests are organized by component in `Tests/DevKeychainTests/` + - Integration tests in `App/Tests/` use real keychain operations + +### Code Organization Patterns + + 1. **Nested Types**: Related functionality grouped within parent types + 2. **Generic Protocols**: Type-safe operations without runtime checks + 3. **Extension-based Helpers**: `Dictionary+KeychainItemMapping.swift` for attribute conversion + 4. **Dependency Injection**: Constructor injection for `KeychainServices` + + +## Development Guidelines + +### Adding New Keychain Item Types + + 1. Create new file in `Sources/DevKeychain/Keychain Items/` + 2. Implement `KeychainItemAdditionAttributes` protocol for adding items + 3. Implement `KeychainItemQuery` protocol for querying items + 4. Add comprehensive tests with both success and failure scenarios + 5. Follow the pattern established by `GenericPassword` and `InternetPassword` + +### Testing Patterns + + - Always use mock `KeychainServices` in unit tests + - Use `DevTesting.Stub` for consistent mocking behavior + - Test both success and error scenarios + - Use randomized test data via `RandomValueGenerating+DevKeychain.swift` + - Follow naming convention: `Tests.swift` + +### Code Style + + - Swift API Design Guidelines strictly enforced + - 120 character line limit (configured in `.swift-format`) + - All public APIs must be documented + - Use descriptive variable names and clear function signatures + + +## CI/CD Configuration + +The project uses GitHub Actions with comprehensive testing: + + - Multi-platform testing (iOS, macOS, tvOS, watchOS) + - Code coverage reporting with xccovPretty + - Swift-format validation + - Test plans in `Build Support/Test Plans/` + + +## Platform Requirements + + - Swift 6.1 toolchain required + - Xcode 16.4 for CI/CD + - Apple platforms only (iOS 18+, macOS 15+, tvOS 18+, visionOS 2+, watchOS 11+) + - Uses modern Swift concurrency features + + +## Scripts Directory + +The `Scripts/` directory contains utility scripts for development workflow automation: + +### Available Scripts + +**`Scripts/install-git-hooks`**: + - Installs pre-commit git hooks that automatically run lint checks + - Creates `.git/hooks/pre-commit` that calls `Scripts/lint` + - Prevents commits with formatting issues + - Run once per repository to set up automated code quality checks + +**`Scripts/lint`**: + - Runs swift-format lint validation with strict mode enabled + - Checks `App/`, `Sources/`, and `Tests/` directories recursively + - Returns non-zero exit code if formatting issues are found + - Used by pre-commit hooks and can be run manually for code quality verification + +### Usage Patterns + + - Run `Scripts/install-git-hooks` after cloning the repository + - Pre-commit hooks will automatically run `Scripts/lint` before each commit + - Manual lint checking: `Scripts/lint` + - Both scripts work from any directory by calculating repository root path + + +## Key Files for Understanding + + - `Sources/DevKeychain/Core/Keychain.swift`: Main API entry point + - `Sources/DevKeychain/Core/KeychainServices.swift`: Core protocol abstraction + - `Sources/DevKeychain/Keychain Items/GenericPassword.swift`: Example keychain item + implementation + - `Tests/DevKeychainTests/Testing Helpers/`: Mock implementations and test utilities + - `Package.swift`: Dependencies and platform requirements + - `.swift-format`: Code formatting configuration + - `Scripts/`: Development workflow automation scripts \ No newline at end of file diff --git a/Documentation/MarkdownStyleGuide.md b/Documentation/MarkdownStyleGuide.md new file mode 100644 index 0000000..d94f75c --- /dev/null +++ b/Documentation/MarkdownStyleGuide.md @@ -0,0 +1,190 @@ +# Markdown Style Guide + +This document defines the Markdown formatting standards for documentation in the Shopper iOS +codebase. + + +## General Formatting + +### Line Length + +Keep all lines under **100 characters**. Break long sentences and paragraphs at natural points +to stay within this limit. + + ✅ Good: + Faucibus consectetur lacinia nostra eros conubia nibh inceptos hendrerit, ante blandit + vulputate imperdiet amet porttitor torquent mattis. + + ❌ Bad: + Faucibus consectetur lacinia nostra eros conubia nibh inceptos hendrerit, ante blandit vulputate imperdiet amet porttitor torquent mattis. + + +### Spacing + +Use consistent spacing for visual hierarchy: + +- **Between major sections**: 2 blank lines +- **After code blocks**: 1 blank line +- **Before subsections**: 1 blank line +- **After section headers**: 1 blank line + +Example: + + ## Major Section + + Content here. + + + ## Another Major Section + + ### Subsection + + Content after subsection header. + + code block here + + Content after code block. + + +## Headers + +### Structure + +Use consistent header hierarchy: + + - `#` for document title + - `##` for major sections + - `###` for subsections + - `####` for sub-subsections (use sparingly) + +### Numbering + +Number subsections when they represent sequential steps or ordered items: + + ## Usage Patterns + + ### 1. Basic Setup + ### 2. Advanced Configuration + ### 3. Verification + + +## Code Blocks + +### Indented Code Blocks + +Use **4-space indentation** for all code blocks instead of fenced blocks: + + ✅ Good: + import DevTesting + + final class MockService: ServiceProtocol { + nonisolated(unsafe) var performActionStub: Stub! + } + + ❌ Bad: + ```swift + import DevTesting + + final class MockService: ServiceProtocol { + nonisolated(unsafe) var performActionStub: Stub! + } + ``` + + +## Lists + +### Unordered Lists + +Use `-` as the bullet character for unordered lists. Place the hyphen 2 spaces from current +indentation level, followed by a space, then your text. When a bullet point spans multiple lines, +align continuation lines with the start of the text (not the hyphen). This ensures all text within a +bullet aligns vertically and makes proper indentation on continuation lines a matter of pressing tab +one or more times. + + - Turpis cubilia sit urna dis ultricies consequat massa hendrerit enim natoque. + - Consectetur sapien posuere sit arcu finibus mattis dictumst dis, lectus ipsum in dictum + lobortis bibendum enim, suscipit aliquet nulla porta erat id class purus. + - Scelerisque massa rutrum dapibus placerat aenean tellus arcu cursus. + - Iaculis, cubilia tristique efficitur bibendum urna imperdiet facilisis turpis hac, + platea est habitant auctor quisque nec pulvinar fermentum sociosqu. + - Parturient justo, venenatis nunc lobortis senectus tortor orci elementum consequat. + - In nibh nisl venenatis bibendum neque mattis habitant tempor proin, tincidunt lobortis + vulputate commodo. + +Blank lines can be placed between bullets if it aids in readability. + +### Ordered Lists + +Use standard numbered lists for sequential items. Follow similar indentation rules as for unordered +lists. Note that the `.` characters in the bullets leads to some strange indentation, but this is +unavoidable. + + 1. Turpis cubilia sit urna dis ultricies consequat massa hendrerit enim natoque. + + 2. Consectetur sapien posuere sit arcu finibus mattis dictumst dis, lectus ipsum in dictum + lobortis bibendum enim, suscipit aliquet nulla porta erat id class purus. + + 1. Scelerisque massa rutrum dapibus placerat aenean tellus arcu cursus. + 2. Iaculis, cubilia tristique efficitur bibendum urna imperdiet facilisis turpis hac, + platea est habitant auctor quisque nec pulvinar fermentum sociosqu. + 3. Parturient justo, venenatis nunc lobortis senectus tortor orci elementum consequat. + + 4. In nibh nisl venenatis bibendum neque mattis habitant tempor proin, tincidunt lobortis + vulputate commodo. + + +## Text Formatting + +### Bold Text + +Use bold for emphasis on key terms: + + - **File names**: `Mock[ProtocolName].swift` + - **Type names**: `Mock[ProtocolName]` + +### Code Spans + +Use backticks for: + + - Type names: `Stub` + - Function names: `performAction(_:)` + - File names: `MockAppServices.swift` + - Keywords: `nonisolated(unsafe)` + +### Terminology Consistency + +Use consistent terminology throughout documents: + +- Prefer "function" over "method" when referring to Swift functions +- Use "type" instead of "class" when referring generically to classes/structs/enums +- Use "property" for stored and computed properties + + +## File Structure Examples + +Use indented text blocks for file structure diagrams: + + Tests/ + ├── Folder 1/ + │ └── Folder 2/ + │ ├── File1.swift + │ └── File2.swift + └── Folder 3/ + └── Folder 4/ + ├── File3.swift + └── File4.swift + + +## Links and References + +### Internal References + +Use relative paths for internal documentation: + + See [Test Mock Documentation](TestMocks.md) for details. + +### Code References + +Reference specific locations using this pattern: + + The main implementation is in `Stub.swift:42-68`. diff --git a/Documentation/TestMocks.md b/Documentation/TestMocks.md new file mode 100644 index 0000000..3e9ab80 --- /dev/null +++ b/Documentation/TestMocks.md @@ -0,0 +1,275 @@ +# Test Mock Documentation + +This document outlines the patterns and conventions for writing test mocks in the Shopper iOS +codebase. + + +## Overview + +The codebase uses a consistent approach to mocking based on the DevTesting framework's `Stub` type. +All mocks follow standardized patterns that make them predictable, testable, and maintainable. + + +## Core Mock Patterns + +### 1. Stub-Based Architecture + +All mocks use `DevTesting`’s `Stub` or `ThrowingStub` types +for function and property implementations: + + import DevTesting + + + final class MockService: ServiceProtocol { + nonisolated(unsafe) + var performActionStub: Stub! + + + func performAction(_ input: String) -> Bool { + performActionStub(input) + } + } + +**Key characteristics:** + + - Properties are marked `nonisolated(unsafe)` for Swift 6 concurrency + - Stub properties are force-unwrapped (`!`). Tests must configure them. + - Function implementations simply call the corresponding stub + + +### 2. Argument Structures for Complex Parameters + +When functions have multiple parameters, create dedicated argument structures: + + final class MockTelemetryDestination: TelemetryDestination { + struct LogErrorArguments { + let error: any Error + let attributes: [String : any Encodable] + } + + + nonisolated(unsafe) + var logErrorStub: Stub! + + + func logError(_ error: some Error, attributes: [String : any Encodable]) { + logErrorStub(.init(error: error, attributes: attributes)) + } + } + +**Benefits:** + + - Simplifies stub configuration in tests + - Provides clear parameter documentation + - Enables easy verification of function calls + + +### 3. Property-Only Services + +For services that only expose properties (like `MockAppServices`), each property delegates to a +stub: + + final class MockAppServices: PlatformAppServices { + nonisolated(unsafe) + var stylesheetStub: Stub! + + nonisolated(unsafe) + var telemetryEventLoggerStub: Stub! + + + var stylesheet: Stylesheet { + stylesheetStub() + } + + + var telemetryEventLogger: any TelemetryEventLogging { + telemetryEventLoggerStub() + } + } + + +### 4. Generic Mock Types + +For protocols with associated types, create generic mocks: + + struct MockTelemetryEvent: TelemetryEvent + where EventData: Encodable & Sendable { + let name: TelemetryEventName + var eventData: EventData + } + + extension MockTelemetryEvent: Equatable where EventData: Equatable { } + extension MockTelemetryEvent: Hashable where EventData: Hashable { } + + +**Pattern:** + + - Use generic parameters for flexible data types + - Add conditional conformances for `Equatable` and `Hashable` when possible + - Maintain protocol requirements while allowing test customization + + +### 5. Simple Error Mocks + +For testing error scenarios, use simple enum-based errors: + + enum MockError: Error, CaseIterable, Hashable, Sendable { + case error1 + case error2 + case error3 + } + +**Characteristics:** + + - Implement `CaseIterable` for easy test iteration + - Include `Hashable` and `Sendable` for Swift 6 compatibility + - Use descriptive but simple case names + + +## Mock Organization + +### File Structure + + Tests/ + ├── AppPlatformTests/ + │ └── Testing Support/ + │ ├── MockAppServices.swift + │ ├── MockBootstrapper.swift + │ └── MockSubapp.swift + └── TelemetryTests/ + └── Testing Support/ + ├── MockTelemetryDestination.swift + ├── MockTelemetryEvent.swift + └── MockError.swift + + +### Naming Conventions + + - **File names**: `Mock[ProtocolName].swift` + - **Type names**: `Mock[ProtocolName]` + - **Argument structures**: `[FunctionName]Arguments` + - **Stub properties**: `[functionName]Stub` + + +## Special Patterns + +### 1. Static Stubs for Initializers + +When mocking types with custom initializers, use static stubs: + + final class MockSubapp: Subapp { + struct InitArguments { + let appConfiguration: AppConfiguration + let subappServices: any SubappServices + } + + + nonisolated(unsafe) + static var initStub: Stub! + + + init(appConfiguration: AppConfiguration, subappServices: any SubappServices) async { + Self.initStub(.init(appConfiguration: appConfiguration, subappServices: subappServices)) + } + } + + +### 2. Default Stub Values + +For functions that might not be called in every test, provide default stub values: + + final class MockSubapp: Subapp { + // Initialize to non-nil to avoid crashes in tests that don't configure this stub + nonisolated(unsafe) + var installTelemetryBusEventHandlersStub: Stub = .init() + } + + +### 3. Protocol Imports with @testable + +Import protocols under test with `@testable` when accessing internal details: + + import AppPlatform + @testable import protocol AppPlatform.PlatformAppServices + @testable import protocol Telemetry.TelemetryDestination + + +## Usage in Tests + +### 1. Typical Test Setup + + @Test + mutating func testEventLogging() throws { + let mockDestination = MockTelemetryDestination() + mockDestination.logEventStub = .init() // Initialize stub + + let logger = TelemetryEventLogger(telemetryDestination: mockDestination) + logger.logEvent(someEvent) + + // Verify the call was made + #expect(mockDestination.logEventStub.calls.count == 1) + + // Extract and verify arguments + let arguments = try #require(mockDestination.logEventStub.callArguments.first) + #expect(arguments.name == expectedEventName) + #expect(arguments.attributes["key"] as? String == "expectedValue") + } + + +### 2. Return Value Configuration + + @Test + func testServiceCall() { + let mockServices = MockAppServices() + mockServices.stylesheetStub = Stub(defaultReturnValue: .standard) + + let result = systemUnderTest.processWithServices(mockServices) + + // Verify the service was called + #expect(mockServices.stylesheetStub.calls.count == 1) + } + + +### 3. Argument Verification Patterns + + @Test + mutating func testComplexMethodCall() throws { + let mockDestination = MockTelemetryDestination() + mockDestination.logErrorStub = .init() + + let error = MockError.error1 + let attributes = ["key": "value"] + + logger.logError(error, attributes: attributes) + + // Verify call count + #expect(mockDestination.logErrorStub.calls.count == 1) + + // Extract and verify arguments + let arguments = try #require(mockDestination.logErrorStub.callArguments.first) + #expect(arguments.error as? MockError == error) + #expect(arguments.attributes["key"] as? String == "value") + } + + +## Best Practices + + 1. **Always configure stubs**: Force-unwrapped stubs will crash if not configured + 2. **Use argument structures**: Simplifies complex parameter verification + 3. **Maintain protocol fidelity**: Mocks should behave like real implementations + 4. **Leverage DevTesting**: Use the framework's call tracking and verification capabilities + 5. **Keep mocks simple**: Avoid complex logic in mock implementations + 6. **Group related mocks**: Place mocks in appropriate Testing Support directories + 7. **Follow naming conventions**: Consistent naming improves maintainability + 8. **Use Swift Testing**: Leverage `@Test`, `#expect()`, and `#require()` for assertions + + +## Thread Safety + +All mocks use `nonisolated(unsafe)` markings for Swift 6 compatibility. This assumes: + + - Tests run on a single thread or properly synchronize access + - Stub configuration happens during test setup before concurrent access + - Mock usage patterns don't require additional synchronization + +When mocking concurrent code, consider additional synchronization mechanisms if needed. diff --git a/Package.swift b/Package.swift index 63b2066..5b23d01 100644 --- a/Package.swift +++ b/Package.swift @@ -2,7 +2,6 @@ import PackageDescription - let swiftSettings: [SwiftSetting] = [ .enableUpcomingFeature("ExistentialAny") ] @@ -23,7 +22,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/DevKitOrganization/DevTesting", from: "1.0.0-beta.7"), + .package(url: "https://github.com/DevKitOrganization/DevTesting", from: "1.0.0-beta.7") ], targets: [ .target( diff --git a/README.md b/README.md index b02e4cb..02c6ab7 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ # DevKeychain -Description forthcoming. - -DevKeychain is fully documented and tested and supports iOS 18+, macOS 15+, tvOS 18+, visionOS 2+, -and watchOS 11+. +DevKeychain is a small Swift package that provides a Swift interface to Apple’s keychain services. It is fully +documented and tested and supports iOS 18+, macOS 15+, tvOS 18+, visionOS 2+, and watchOS 11+. View our [changelog](CHANGELOG.md) to see what’s new. @@ -16,6 +14,14 @@ public interfaces are fully documented and tested. We aim for overall test cover [SwiftAPIDesignGuidelines]: https://swift.org/documentation/api-design-guidelines/ +### Development Setup + +To set up the development environment: + + 1. Run `Scripts/install-git-hooks` to install pre-commit hooks that automatically check code + formatting. + 2. Use `Scripts/lint` to manually check code formatting at any time. + ## Bugs and Feature Requests diff --git a/Scripts/install-git-hooks b/Scripts/install-git-hooks new file mode 100755 index 0000000..670ea93 --- /dev/null +++ b/Scripts/install-git-hooks @@ -0,0 +1,50 @@ +#!/bin/bash + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Go to the repository root (one level up from Scripts) +REPO_ROOT="$(dirname "$SCRIPT_DIR")" + +# Check if we're in a git repository +if [ ! -d "$REPO_ROOT/.git" ]; then + echo "Error: Not in a git repository" + exit 1 +fi + +mkdir -p "$REPO_ROOT/.git/hooks" + +# Function to install the pre-commit hook +install_pre_commit_hook() { + local pre_commit_hook="$REPO_ROOT/.git/hooks/pre-commit" + + echo "Installing pre-commit hook..." + + cat > "$pre_commit_hook" << 'EOF' +#!/bin/bash + +# Get the directory where this hook is located +HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Go to the repository root (two levels up from .git/hooks) +REPO_ROOT="$(dirname "$(dirname "$HOOK_DIR")")" + +# Run the lint script +echo "Running lint check..." +if ! "$REPO_ROOT/Scripts/lint"; then + echo "Lint check failed. Please fix formatting issues before committing." + exit 1 +fi + +echo "Lint check passed." +EOF + + chmod +x "$pre_commit_hook" + echo "Pre-commit hook installed successfully!" +} + +# Install the pre-commit hook +install_pre_commit_hook + +echo "All git hooks installed successfully!" +echo "The pre-commit hook will run 'Scripts/lint' before each commit." diff --git a/Scripts/lint b/Scripts/lint new file mode 100755 index 0000000..fc8a054 --- /dev/null +++ b/Scripts/lint @@ -0,0 +1,11 @@ +#!/bin/bash + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Go to the repository root (one level up from Scripts) +REPO_ROOT="$(dirname "$SCRIPT_DIR")" + +# Run swift format lint with full paths to preserve user's PWD +swift format lint --recursive --strict "$REPO_ROOT/App/" "$REPO_ROOT/Sources/" "$REPO_ROOT/Tests/" + diff --git a/Sources/DevKeychain/Core/Keychain.swift b/Sources/DevKeychain/Core/Keychain.swift index 27734de..feb21bd 100644 --- a/Sources/DevKeychain/Core/Keychain.swift +++ b/Sources/DevKeychain/Core/Keychain.swift @@ -7,7 +7,6 @@ import Foundation - /// An keychain in which secrets can be created, queried, and deleted. public struct Keychain: Sendable { /// The underlying keychain service that the keychain uses. @@ -84,13 +83,13 @@ extension Keychain { public struct QueryOptions: Sendable { /// Whether queries should ignore string case when comparing strings. public var isCaseInsensitive: Bool - + /// The maximum number of items that queries should return. /// /// `nil` indicates no maximum. `nil` by default. public var limit: Int? - + /// Creates new query options. /// /// - Parameters: @@ -105,10 +104,10 @@ extension Keychain { /// A dictionary-representation of the options to pass to the keychain services API. - var optionDictionary: [CFString : Any] { + var optionDictionary: [CFString: Any] { return [ kSecMatchCaseInsensitive: isCaseInsensitive, - kSecMatchLimit: limit ?? kSecMatchLimitAll + kSecMatchLimit: limit ?? kSecMatchLimitAll, ] } } diff --git a/Sources/DevKeychain/Core/KeychainItemAdditionAttributes.swift b/Sources/DevKeychain/Core/KeychainItemAdditionAttributes.swift index 50bfd43..86d9eeb 100644 --- a/Sources/DevKeychain/Core/KeychainItemAdditionAttributes.swift +++ b/Sources/DevKeychain/Core/KeychainItemAdditionAttributes.swift @@ -7,15 +7,14 @@ import Foundation - /// A type that describes the attributes of an item to add to the keychain. public protocol KeychainItemAdditionAttributes: Sendable { /// The type of item that the attributes describe. associatedtype Item: Sendable - + /// A dictionary-representation of the new item to pass to the keychain services API. var attributesDictionary: [CFString: Any] { get } - + /// Maps the raw return value from the keychain services API into the instance’s `Item` type. /// /// - Parameter rawItem: An object returned by the keychain services API that represents the added item. diff --git a/Sources/DevKeychain/Core/KeychainItemQuery.swift b/Sources/DevKeychain/Core/KeychainItemQuery.swift index c289719..0fd91eb 100644 --- a/Sources/DevKeychain/Core/KeychainItemQuery.swift +++ b/Sources/DevKeychain/Core/KeychainItemQuery.swift @@ -7,7 +7,6 @@ import Foundation - /// A type that describes a query for specific kinds of items in the keychain. public protocol KeychainItemQuery: Sendable { /// The type of item that the query returns. diff --git a/Sources/DevKeychain/Core/KeychainServices.swift b/Sources/DevKeychain/Core/KeychainServices.swift index 4e32328..26bfc19 100644 --- a/Sources/DevKeychain/Core/KeychainServices.swift +++ b/Sources/DevKeychain/Core/KeychainServices.swift @@ -7,7 +7,6 @@ import Foundation - /// A type that can provides access to Apple’s keychain services API. /// /// This protocol exists for dependency injection purposes. diff --git a/Sources/DevKeychain/Errors/KeychainItemMappingError.swift b/Sources/DevKeychain/Errors/KeychainItemMappingError.swift index 987d4a5..6f4a4f9 100644 --- a/Sources/DevKeychain/Errors/KeychainItemMappingError.swift +++ b/Sources/DevKeychain/Errors/KeychainItemMappingError.swift @@ -7,7 +7,6 @@ import Foundation - /// An error that can occur while mapping a keychain services API return value to a keychain item. enum KeychainItemMappingError: Error, Equatable { /// Indicates that the data being mapped is corrupted or otherwise invalid. @@ -20,7 +19,7 @@ enum KeychainItemMappingError: Error, Equatable { case attributeTypeMismatch(attribute: String, type: Any.Type) - static func ==(lhs: KeychainItemMappingError, rhs: KeychainItemMappingError) -> Bool { + static func == (lhs: KeychainItemMappingError, rhs: KeychainItemMappingError) -> Bool { switch (lhs, rhs) { case (.dataCorrupted, .dataCorrupted): return true diff --git a/Sources/DevKeychain/Errors/KeychainServicesError.swift b/Sources/DevKeychain/Errors/KeychainServicesError.swift index 0927af2..5cf03bb 100644 --- a/Sources/DevKeychain/Errors/KeychainServicesError.swift +++ b/Sources/DevKeychain/Errors/KeychainServicesError.swift @@ -7,7 +7,6 @@ import Foundation - /// An error from the keychain services API. struct KeychainServicesError: CustomStringConvertible, Error, Hashable { /// The underlying `OSStatus` that describes the error. diff --git a/Sources/DevKeychain/Extensions/Dictionary+KeychainItemMapping.swift b/Sources/DevKeychain/Extensions/Dictionary+KeychainItemMapping.swift index 1fd3af1..aa4db22 100644 --- a/Sources/DevKeychain/Extensions/Dictionary+KeychainItemMapping.swift +++ b/Sources/DevKeychain/Extensions/Dictionary+KeychainItemMapping.swift @@ -7,7 +7,6 @@ import Foundation - extension Dictionary where Key == CFString, Value == Any { /// Accesses the value for an attribute of a specified type, throwing errors as appropriate. /// diff --git a/Sources/DevKeychain/Keychain Items/GenericPassword.swift b/Sources/DevKeychain/Keychain Items/GenericPassword.swift index d442d5b..e1c027f 100644 --- a/Sources/DevKeychain/Keychain Items/GenericPassword.swift +++ b/Sources/DevKeychain/Keychain Items/GenericPassword.swift @@ -7,7 +7,6 @@ import Foundation - /// A generic password keychain item. public struct GenericPassword: Hashable, Sendable { /// The item’s service. @@ -21,7 +20,7 @@ public struct GenericPassword: Hashable, Sendable { /// If this data is textual, you can use ``password(using:)`` to easily access it. public let data: Data - + /// Returns the item’s secret data as a string. /// /// - Parameter encoding: The data’s string encoding. Defaults to `.utf8`. @@ -42,7 +41,7 @@ extension GenericPassword { /// Creates a new generic password with the specified attributes. /// /// - Parameter attributes: A dictionary of attributes from the keychain services API. - init(attributes: [CFString : Any]) throws { + init(attributes: [CFString: Any]) throws { self.init( service: try attributes.value(forKeychainAttribute: kSecAttrService, type: String.self), account: try attributes.value(forKeychainAttribute: kSecAttrAccount, type: String.self), @@ -96,7 +95,7 @@ extension GenericPassword { } - public var attributesDictionary: [CFString : Any] { + public var attributesDictionary: [CFString: Any] { return [ kSecAttrAccount: account, kSecAttrService: service, @@ -133,7 +132,7 @@ extension GenericPassword { /// If `nil`, matching items can have any account. `nil` by default. public var account: String? - + /// Creates a new generic password query. /// /// - Parameters: @@ -147,7 +146,7 @@ extension GenericPassword { } - public var attributesDictionary: [CFString : Any] { + public var attributesDictionary: [CFString: Any] { var dictionary: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecUseDataProtectionKeychain: true, @@ -165,7 +164,7 @@ extension GenericPassword { } - public var returnDictionary: [CFString : Any] { + public var returnDictionary: [CFString: Any] { return [ kSecReturnAttributes: true, kSecReturnData: true, @@ -174,11 +173,11 @@ extension GenericPassword { public func mapMatchingItems(_ rawItems: AnyObject) throws -> [GenericPassword] { - let attributesArray: [[CFString : Any]] + let attributesArray: [[CFString: Any]] - if let singleItem = rawItems as? [CFString : Any] { + if let singleItem = rawItems as? [CFString: Any] { attributesArray = [singleItem] - } else if let array = rawItems as? [[CFString : Any]] { + } else if let array = rawItems as? [[CFString: Any]] { attributesArray = array } else { throw KeychainItemMappingError.dataCorrupted diff --git a/Sources/DevKeychain/Keychain Items/InternetPassword.swift b/Sources/DevKeychain/Keychain Items/InternetPassword.swift index 577412a..2f942cc 100644 --- a/Sources/DevKeychain/Keychain Items/InternetPassword.swift +++ b/Sources/DevKeychain/Keychain Items/InternetPassword.swift @@ -7,7 +7,6 @@ import Foundation - /// An internet password keychain item. public struct InternetPassword: Hashable, Sendable { /// The item’s server. @@ -42,7 +41,7 @@ extension InternetPassword { /// Creates a new internet password with the specified attributes. /// /// - Parameter attributes: A dictionary of attributes from the keychain services API. - init(attributes: [CFString : Any]) throws { + init(attributes: [CFString: Any]) throws { self.init( server: try attributes.value(forKeychainAttribute: kSecAttrServer, type: String.self), account: try attributes.value(forKeychainAttribute: kSecAttrAccount, type: String.self), @@ -96,7 +95,7 @@ extension InternetPassword { } - public var attributesDictionary: [CFString : Any] { + public var attributesDictionary: [CFString: Any] { return [ kSecAttrAccount: account, kSecAttrServer: server, @@ -147,7 +146,7 @@ extension InternetPassword { } - public var attributesDictionary: [CFString : Any] { + public var attributesDictionary: [CFString: Any] { var dictionary: [CFString: Any] = [ kSecClass: kSecClassInternetPassword, kSecUseDataProtectionKeychain: true, @@ -165,7 +164,7 @@ extension InternetPassword { } - public var returnDictionary: [CFString : Any] { + public var returnDictionary: [CFString: Any] { return [ kSecReturnAttributes: true, kSecReturnData: true, @@ -174,11 +173,11 @@ extension InternetPassword { public func mapMatchingItems(_ rawItems: AnyObject) throws -> [InternetPassword] { - let attributesArray: [[CFString : Any]] + let attributesArray: [[CFString: Any]] - if let singleItem = rawItems as? [CFString : Any] { + if let singleItem = rawItems as? [CFString: Any] { attributesArray = [singleItem] - } else if let array = rawItems as? [[CFString : Any]] { + } else if let array = rawItems as? [[CFString: Any]] { attributesArray = array } else { throw KeychainItemMappingError.dataCorrupted diff --git a/Tests/DevKeychainTests/Core/KeychainTests.swift b/Tests/DevKeychainTests/Core/KeychainTests.swift index eb704ea..c6a672e 100644 --- a/Tests/DevKeychainTests/Core/KeychainTests.swift +++ b/Tests/DevKeychainTests/Core/KeychainTests.swift @@ -6,11 +6,11 @@ // import DevKeychain -@testable import struct DevKeychain.Keychain import DevTesting import Foundation import Testing +@testable import struct DevKeychain.Keychain struct KeychainTests: RandomValueGenerating { var randomNumberGenerator = makeRandomNumberGenerator() @@ -259,7 +259,7 @@ struct Keychain_QueryOptionsTests: RandomValueGenerating { let expected: [CFString: Any] = [ kSecMatchCaseInsensitive: isCaseInsensitive, - kSecMatchLimit: limit ?? kSecMatchLimitAll + kSecMatchLimit: limit ?? kSecMatchLimitAll, ] #expect(options.optionDictionary as CFDictionary == expected as CFDictionary) diff --git a/Tests/DevKeychainTests/Errors/KeychainErrorTests.swift b/Tests/DevKeychainTests/Errors/KeychainErrorTests.swift index 4f91422..8f1da40 100644 --- a/Tests/DevKeychainTests/Errors/KeychainErrorTests.swift +++ b/Tests/DevKeychainTests/Errors/KeychainErrorTests.swift @@ -5,11 +5,11 @@ // Created by Prachi Gauriar on 6/19/25. // -@testable import struct DevKeychain.KeychainServicesError import DevTesting import Foundation import Testing +@testable import struct DevKeychain.KeychainServicesError struct KeychainServicesErrorTests: RandomValueGenerating { var randomNumberGenerator = makeRandomNumberGenerator() diff --git a/Tests/DevKeychainTests/Errors/KeychainItemMappingErrorTests.swift b/Tests/DevKeychainTests/Errors/KeychainItemMappingErrorTests.swift index 1e8bb6f..52e82fa 100644 --- a/Tests/DevKeychainTests/Errors/KeychainItemMappingErrorTests.swift +++ b/Tests/DevKeychainTests/Errors/KeychainItemMappingErrorTests.swift @@ -5,11 +5,11 @@ // Created by Prachi Gauriar on 6/19/25. // -@testable import enum DevKeychain.KeychainItemMappingError import DevTesting import Foundation import Testing +@testable import enum DevKeychain.KeychainItemMappingError struct KeychainItemMappingErrorTests: RandomValueGenerating { var randomNumberGenerator = makeRandomNumberGenerator() diff --git a/Tests/DevKeychainTests/Extensions/Dictionary+KeychainItemMappingTests.swift b/Tests/DevKeychainTests/Extensions/Dictionary+KeychainItemMappingTests.swift index 8977f5e..b8c84c6 100644 --- a/Tests/DevKeychainTests/Extensions/Dictionary+KeychainItemMappingTests.swift +++ b/Tests/DevKeychainTests/Extensions/Dictionary+KeychainItemMappingTests.swift @@ -6,11 +6,11 @@ // import DevKeychain -@testable import struct DevKeychain.Keychain import DevTesting import Foundation import Testing +@testable import struct DevKeychain.Keychain struct Dictionary_KeychainItemMappingTests: RandomValueGenerating { var randomNumberGenerator = makeRandomNumberGenerator() diff --git a/Tests/DevKeychainTests/Keychain Items/GenericPasswordTests.swift b/Tests/DevKeychainTests/Keychain Items/GenericPasswordTests.swift index 424190e..7eea3b3 100644 --- a/Tests/DevKeychainTests/Keychain Items/GenericPasswordTests.swift +++ b/Tests/DevKeychainTests/Keychain Items/GenericPasswordTests.swift @@ -6,11 +6,11 @@ // import DevKeychain -@testable import struct DevKeychain.GenericPassword import DevTesting import Foundation import Testing +@testable import struct DevKeychain.GenericPassword struct GenericPasswordTests: RandomValueGenerating { var randomNumberGenerator = makeRandomNumberGenerator() @@ -171,7 +171,6 @@ struct GenericPassword_AdditionAttributesTests: RandomValueGenerating { } - @Test mutating func attributesDictionaryIsCorrect() { let service = randomAlphanumericString() @@ -234,7 +233,7 @@ struct GenericPassword_AdditionAttributesTests: RandomValueGenerating { [ kSecAttrService: service, kSecAttrAccount: account, - kSecValueData: data + kSecValueData: data, ] as CFDictionary ) @@ -267,7 +266,7 @@ struct GenericPassword_QueryTests: RandomValueGenerating { kSecAttrAccount: account, kSecAttrService: service, kSecClass: kSecClassGenericPassword, - kSecUseDataProtectionKeychain: true + kSecUseDataProtectionKeychain: true, ] for isServiceNil in [false, true] { @@ -340,7 +339,7 @@ struct GenericPassword_QueryTests: RandomValueGenerating { [ kSecAttrAccount: item.account, kSecAttrService: item.service, - kSecValueData: item.data + kSecValueData: item.data, ] } diff --git a/Tests/DevKeychainTests/Keychain Items/InternetPasswordTests.swift b/Tests/DevKeychainTests/Keychain Items/InternetPasswordTests.swift index e783188..8c90b09 100644 --- a/Tests/DevKeychainTests/Keychain Items/InternetPasswordTests.swift +++ b/Tests/DevKeychainTests/Keychain Items/InternetPasswordTests.swift @@ -6,11 +6,11 @@ // import DevKeychain -@testable import struct DevKeychain.InternetPassword import DevTesting import Foundation import Testing +@testable import struct DevKeychain.InternetPassword struct InternetPasswordTests: RandomValueGenerating { var randomNumberGenerator = makeRandomNumberGenerator() @@ -171,7 +171,6 @@ struct InternetPassword_AdditionAttributesTests: RandomValueGenerating { } - @Test mutating func attributesDictionaryIsCorrect() { let server = randomAlphanumericString() @@ -234,7 +233,7 @@ struct InternetPassword_AdditionAttributesTests: RandomValueGenerating { [ kSecAttrServer: server, kSecAttrAccount: account, - kSecValueData: data + kSecValueData: data, ] as CFDictionary ) @@ -267,7 +266,7 @@ struct InternetPassword_QueryTests: RandomValueGenerating { kSecAttrAccount: account, kSecAttrServer: server, kSecClass: kSecClassInternetPassword, - kSecUseDataProtectionKeychain: true + kSecUseDataProtectionKeychain: true, ] for isServerNil in [false, true] { @@ -340,7 +339,7 @@ struct InternetPassword_QueryTests: RandomValueGenerating { [ kSecAttrAccount: item.account, kSecAttrServer: item.server, - kSecValueData: item.data + kSecValueData: item.data, ] } diff --git a/Tests/DevKeychainTests/Testing Helpers/MockError.swift b/Tests/DevKeychainTests/Testing Helpers/MockError.swift index bbeb88e..1760ee1 100644 --- a/Tests/DevKeychainTests/Testing Helpers/MockError.swift +++ b/Tests/DevKeychainTests/Testing Helpers/MockError.swift @@ -7,7 +7,6 @@ import Foundation - enum MockError: CaseIterable, Error { case error1 case error2 diff --git a/Tests/DevKeychainTests/Testing Helpers/MockKeychainItemAdditionAttributes.swift b/Tests/DevKeychainTests/Testing Helpers/MockKeychainItemAdditionAttributes.swift index 7a98d2d..fbed623 100644 --- a/Tests/DevKeychainTests/Testing Helpers/MockKeychainItemAdditionAttributes.swift +++ b/Tests/DevKeychainTests/Testing Helpers/MockKeychainItemAdditionAttributes.swift @@ -9,16 +9,12 @@ import DevKeychain import DevTesting import Foundation - final class MockKeychainItemAdditionAttributes: KeychainItemAdditionAttributes where Item: Sendable { - nonisolated(unsafe) - var attributesDictionaryStub: Stub! - - nonisolated(unsafe) - var mapStub: ThrowingStub! + nonisolated(unsafe) var attributesDictionaryStub: Stub! + nonisolated(unsafe) var mapStub: ThrowingStub! - var attributesDictionary: [CFString : Any] { + var attributesDictionary: [CFString: Any] { return attributesDictionaryStub() } diff --git a/Tests/DevKeychainTests/Testing Helpers/MockKeychainItemQuery.swift b/Tests/DevKeychainTests/Testing Helpers/MockKeychainItemQuery.swift index d31431b..1cff155 100644 --- a/Tests/DevKeychainTests/Testing Helpers/MockKeychainItemQuery.swift +++ b/Tests/DevKeychainTests/Testing Helpers/MockKeychainItemQuery.swift @@ -9,24 +9,18 @@ import DevKeychain import DevTesting import Foundation - final class MockKeychainItemQuery: KeychainItemQuery where Item: Sendable { - nonisolated(unsafe) - var attributesDictionaryStub: Stub! - - nonisolated(unsafe) - var returnDictionaryStub: Stub! - - nonisolated(unsafe) - var mapStub: ThrowingStub! + nonisolated(unsafe) var attributesDictionaryStub: Stub! + nonisolated(unsafe) var returnDictionaryStub: Stub! + nonisolated(unsafe) var mapStub: ThrowingStub! - var attributesDictionary: [CFString : Any] { + var attributesDictionary: [CFString: Any] { return attributesDictionaryStub() } - var returnDictionary: [CFString : Any] { + var returnDictionary: [CFString: Any] { return returnDictionaryStub() } diff --git a/Tests/DevKeychainTests/Testing Helpers/MockKeychainService.swift b/Tests/DevKeychainTests/Testing Helpers/MockKeychainService.swift index 6867381..edf9714 100644 --- a/Tests/DevKeychainTests/Testing Helpers/MockKeychainService.swift +++ b/Tests/DevKeychainTests/Testing Helpers/MockKeychainService.swift @@ -5,33 +5,28 @@ // Created by Prachi Gauriar on 6/18/25. // -@testable import protocol DevKeychain.KeychainServices import DevTesting import Foundation +@testable import protocol DevKeychain.KeychainServices final class MockKeychainServices: KeychainServices { - nonisolated(unsafe) - var addItemStub: ThrowingStub<[CFString: Any], AnyObject?, any Error>! - - nonisolated(unsafe) - var itemsStub: ThrowingStub<[CFString: Any], AnyObject?, any Error>! - - nonisolated(unsafe) - var deleteItemsStub: ThrowingStub<[CFString: Any], Void, any Error>! + nonisolated(unsafe) var addItemStub: ThrowingStub<[CFString: Any], AnyObject?, any Error>! + nonisolated(unsafe) var itemsStub: ThrowingStub<[CFString: Any], AnyObject?, any Error>! + nonisolated(unsafe) var deleteItemsStub: ThrowingStub<[CFString: Any], Void, any Error>! - func addItem(withAttributes attributes: [CFString : Any]) throws -> AnyObject? { + func addItem(withAttributes attributes: [CFString: Any]) throws -> AnyObject? { return try addItemStub(attributes) } - func items(matchingQuery query: [CFString : Any]) throws -> AnyObject? { + func items(matchingQuery query: [CFString: Any]) throws -> AnyObject? { return try itemsStub(query) } - func deleteItems(matchingQuery attributes: [CFString : Any]) throws { + func deleteItems(matchingQuery attributes: [CFString: Any]) throws { return try deleteItemsStub(attributes) } } diff --git a/Tests/DevKeychainTests/Testing Helpers/RandomValueGenerating+DevKeychain.swift b/Tests/DevKeychainTests/Testing Helpers/RandomValueGenerating+DevKeychain.swift index e0ad217..bb67cbf 100644 --- a/Tests/DevKeychainTests/Testing Helpers/RandomValueGenerating+DevKeychain.swift +++ b/Tests/DevKeychainTests/Testing Helpers/RandomValueGenerating+DevKeychain.swift @@ -9,7 +9,6 @@ import DevKeychain import DevTesting import Foundation - extension RandomValueGenerating { mutating func randomError() -> MockError { return randomCase(of: MockError.self)!