From f64078faf16b51d9f3b2ff65dae56ffcc58df9fb Mon Sep 17 00:00:00 2001 From: Prachi Gauriar Date: Sat, 30 Aug 2025 21:05:09 -0400 Subject: [PATCH] Disable GitHub Actions for iOS, tvOS, and watchOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Due to the poor reliability of GitHub Actions, we are disabling PR checks for iOS, tvOS, and watchOS. - We’ve added Scripts/test-all-platforms, which runs tests against iOS, macOS, tvOS, and watchOS. - We’ve updated install-git-hooks to install test-all-actions as a pre-push hook - We’ve added unit tests for StandardKeychainServices so that we can achieve near 100% coverage on non-iOS platforms. --- .github/workflows/VerifyChanges.yaml | 38 ++-- .swift-format | 1 + .../xcshareddata/swiftpm/Package.resolved | 6 +- ...ardKeychainServicesIntegrationTests.swift} | 2 +- CLAUDE.md | 52 +++-- Package.resolved | 6 +- Package.swift | 4 +- README.md | 31 ++- Scripts/install-git-hooks | 33 ++++ Scripts/test-all-platforms | 68 +++++++ .../DevKeychain/Core/KeychainServices.swift | 22 ++- .../Core/StandardKeychainServicesTests.swift | 186 ++++++++++++++++++ 12 files changed, 394 insertions(+), 55 deletions(-) rename App/Tests/DevKeychainAppTests/{StandardKeychainServicesTests.swift => StandardKeychainServicesIntegrationTests.swift} (98%) create mode 100755 Scripts/test-all-platforms create mode 100644 Tests/DevKeychainTests/Core/StandardKeychainServicesTests.swift diff --git a/.github/workflows/VerifyChanges.yaml b/.github/workflows/VerifyChanges.yaml index 6f43fca..b47054f 100644 --- a/.github/workflows/VerifyChanges.yaml +++ b/.github/workflows/VerifyChanges.yaml @@ -14,9 +14,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - name: Select Xcode 16.4 + - name: Select Xcode 26.0.0 run: | - sudo xcode-select -s /Applications/Xcode_16.4.0.app + sudo xcode-select -s /Applications/Xcode_26.0.0.app - name: Lint run: | Scripts/lint @@ -28,22 +28,22 @@ jobs: fail-fast: false matrix: include: - - platform: iOS - xcode_destination: "platform=iOS Simulator,name=iPhone 16 Pro" - xcode_project: "DevKeychain.xcodeproj" - xcode_scheme: "DevKeychainApp" +# - platform: iOS +# xcode_destination: "platform=iOS Simulator,name=iPhone 16 Pro" +# xcode_project: "DevKeychain.xcodeproj" +# xcode_scheme: "DevKeychainApp" - platform: macOS xcode_destination: "platform=macOS,arch=arm64" xcode_project: "" xcode_scheme: "DevKeychain" - - platform: tvOS - xcode_destination: "platform=tvOS Simulator,name=Apple TV 4K (3rd generation)" - xcode_project: "" - xcode_scheme: "DevKeychain" - - platform: watchOS - xcode_destination: "platform=watchOS Simulator,name=Apple Watch Series 10 (46mm)" - xcode_project: "" - xcode_scheme: "DevKeychain" +# - platform: tvOS +# xcode_destination: "platform=tvOS Simulator,name=Apple TV 4K (3rd generation)" +# xcode_project: "" +# xcode_scheme: "DevKeychain" +# - platform: watchOS +# xcode_destination: "platform=watchOS Simulator,name=Apple Watch Series 10 (46mm)" +# xcode_project: "" +# xcode_scheme: "DevKeychain" env: DEV_BUILDS: DevBuilds/Sources XCCOV_PRETTY_VERSION: 1.2.0 @@ -80,9 +80,9 @@ jobs: **/*.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved Package.resolved verbose: true - - name: Select Xcode 16.4 + - name: Select Xcode 26.0.0 run: | - sudo xcode-select -s /Applications/Xcode_16.4.0.app + sudo xcode-select -s /Applications/Xcode_26.0.0.app - name: Build for Testing run: | "$DEV_BUILDS"/build_and_test.sh --action build-for-testing @@ -128,9 +128,9 @@ jobs: - name: Download xccovPretty output uses: actions/download-artifact@v4 with: - name: xccovPrettyOutput-iOS + name: xccovPrettyOutput-macOS - name: Post Code Coverage Comment uses: thollander/actions-comment-pull-request@v3 with: - file-path: xccovPretty-iOS.output - comment-tag: codeCoverage-iOS + file-path: xccovPretty-macOS.output + comment-tag: codeCoverage-macOS diff --git a/.swift-format b/.swift-format index 6cd31d6..84042b2 100644 --- a/.swift-format +++ b/.swift-format @@ -14,6 +14,7 @@ "lineBreakBetweenDeclarationAttributes": false, "lineLength": 120, "maximumBlankLines": 2, + "multilineTrailingCommaBehavior": "alwaysUsed", "multiElementCollectionTrailingCommas": true, "noAssignmentInExpressions": { "allowedFunctions": [] diff --git a/App/DevKeychain.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/App/DevKeychain.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2e36189..2804d32 100644 --- a/App/DevKeychain.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/App/DevKeychain.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "ef7bf65dddf6feba04145a130a21accc9723d75a3b936f30e49767ebd2b3fd5d", + "originHash" : "3b27e6dfe272a431780fb06d3c59802cd4de440d92512500c49f2d700d0cc822", "pins" : [ { "identity" : "devtesting", "kind" : "remoteSourceControl", "location" : "https://github.com/Devkitorganization/devtesting", "state" : { - "revision" : "4a4dc1b3ba8d66fcbaa745f541ef95d0d0b36c23", - "version" : "1.0.0-beta.10" + "revision" : "33c75ae23d0996a71b4ab2acdd8926d90dcbcd2e", + "version" : "1.0.0-beta.11" } } ], diff --git a/App/Tests/DevKeychainAppTests/StandardKeychainServicesTests.swift b/App/Tests/DevKeychainAppTests/StandardKeychainServicesIntegrationTests.swift similarity index 98% rename from App/Tests/DevKeychainAppTests/StandardKeychainServicesTests.swift rename to App/Tests/DevKeychainAppTests/StandardKeychainServicesIntegrationTests.swift index ab140ab..c69574d 100644 --- a/App/Tests/DevKeychainAppTests/StandardKeychainServicesTests.swift +++ b/App/Tests/DevKeychainAppTests/StandardKeychainServicesIntegrationTests.swift @@ -1,5 +1,5 @@ // -// StandardKeychainServicesTests.swift +// StandardKeychainServicesIntegrationTests.swift // DevKeychainAppTests // // Created by Prachi Gauriar on 6/22/25. diff --git a/CLAUDE.md b/CLAUDE.md index 87755bf..6a0c05c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ repository. 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. +Swift 6.2+ toolchain. ## Common Development Commands @@ -121,18 +121,22 @@ Uses **Swift Testing** framework with comprehensive mocking: ## CI/CD Configuration -The project uses GitHub Actions with comprehensive testing: +The project uses GitHub Actions for continuous integration: - - Multi-platform testing (iOS, macOS, tvOS, watchOS) - - Code coverage reporting with xccovPretty - - Swift-format validation - - Test plans in `Build Support/Test Plans/` + - **Linting**: Automatically checks code formatting on all pull requests using `swift format` + - **Testing**: Runs tests on macOS (iOS, tvOS, and watchOS testing are disabled in CI due to + reliability issues) + - **Coverage**: Generates code coverage reports using xccovPretty + +For comprehensive cross-platform testing, developers should run `Scripts/test-all-platforms` +locally or rely on the pre-push git hook which automatically runs all platform tests before +pushing changes. ## Platform Requirements - - Swift 6.1 toolchain required - - Xcode 16.4 for CI/CD + - Swift 6.2+ toolchain required + - Xcode 26.0 for CI/CD - Apple platforms only (iOS 18+, macOS 15+, tvOS 18+, visionOS 2+, watchOS 11+) - Uses modern Swift concurrency features @@ -144,10 +148,11 @@ The `Scripts/` directory contains utility scripts for development workflow autom ### 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 + - Installs pre-commit and pre-push git hooks for automated quality checks + - Creates `.git/hooks/pre-commit` that calls `Scripts/lint` for formatting validation + - Creates `.git/hooks/pre-push` that calls `Scripts/test-all-platforms` for comprehensive testing + - Prevents commits with formatting issues and pushes with test failures + - Run once per repository to set up automated code quality and testing workflow **`Scripts/lint`**: - Runs swift-format lint validation with strict mode enabled @@ -155,12 +160,25 @@ The `Scripts/` directory contains utility scripts for development workflow autom - Returns non-zero exit code if formatting issues are found - Used by pre-commit hooks and can be run manually for code quality verification +**`Scripts/test-all-platforms`**: + - Runs comprehensive tests across all supported Apple platforms + - Tests on iOS Simulator (iPhone 16 Pro), macOS, tvOS Simulator (Apple TV 4K), and watchOS + Simulator (Apple Watch Series 10) + - Uses different project configurations: DevKeychainApp scheme for iOS, DevKeychain scheme for + other platforms + - Provides colored output with timestamps and clear success/failure indicators + - Returns non-zero exit code if any platform tests fail + - Essential for local cross-platform validation since CI only tests macOS + ### 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 + - Run `Scripts/install-git-hooks` after cloning the repository for complete automation + - Pre-commit hooks automatically run `Scripts/lint` before each commit + - Pre-push hooks automatically run `Scripts/test-all-platforms` before each push + - Manual operations: + - Code formatting check: `Scripts/lint` + - Cross-platform testing: `Scripts/test-all-platforms` + - All scripts work from any directory by calculating repository root path ## Key Files for Understanding @@ -172,4 +190,4 @@ The `Scripts/` directory contains utility scripts for development workflow autom - `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 + - `Scripts/`: Development workflow automation scripts diff --git a/Package.resolved b/Package.resolved index 5ccc385..2c6ff90 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "003a8708bcb212d81766df1361f285bb97940eed3bef5732a0b2edd644dec6ca", + "originHash" : "8ca096df508e25034462a13500b46be9548f43ac947babb10133baa4ffe5fac8", "pins" : [ { "identity" : "devtesting", "kind" : "remoteSourceControl", "location" : "https://github.com/DevKitOrganization/DevTesting", "state" : { - "revision" : "4a4dc1b3ba8d66fcbaa745f541ef95d0d0b36c23", - "version" : "1.0.0-beta.10" + "revision" : "33c75ae23d0996a71b4ab2acdd8926d90dcbcd2e", + "version" : "1.0.0-beta.11" } } ], diff --git a/Package.swift b/Package.swift index b27be7b..ceaad76 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.1 +// swift-tools-version: 6.2 import PackageDescription @@ -23,7 +23,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/DevKitOrganization/DevTesting", from: "1.0.0-beta.10") + .package(url: "https://github.com/DevKitOrganization/DevTesting", from: "1.0.0-beta.11") ], targets: [ .target( diff --git a/README.md b/README.md index 02c6ab7..4256df0 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,43 @@ # DevKeychain -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+. +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. ## Development Requirements -DevKeychain requires a Swift 6.1 toolchain to build. We only test on Apple platforms. We follow -the [Swift API Design Guidelines][SwiftAPIDesignGuidelines]. We take pride in the fact that our -public interfaces are fully documented and tested. We aim for overall test coverage over 99%. +DevKeychain requires a Swift 6.2+ toolchain to build. We only test on Apple platforms. We follow the +[Swift API Design Guidelines][SwiftAPIDesignGuidelines]. We take pride in the fact that our public +interfaces are fully documented and tested. We aim for overall test coverage over 99%. [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. + 1. Run `Scripts/install-git-hooks` to install git hooks that automatically check code + formatting on commits and run comprehensive tests before pushing. 2. Use `Scripts/lint` to manually check code formatting at any time. + 3. Use `Scripts/test-all-platforms` to run tests on all supported platforms locally. + + +## Continuous Integration + +DevKeychain uses GitHub Actions for continuous integration. The CI pipeline: + + - **Linting**: Automatically checks code formatting on all pull requests using `swift format` + - **Testing**: Runs tests on macOS (iOS, tvOS, and watchOS testing are disabled in CI due to + reliability issues) + - **Coverage**: Generates code coverage reports using xccovPretty + +For comprehensive cross-platform testing, developers should run `Scripts/test-all-platforms` +locally or rely on the pre-push git hook which automatically runs all platform tests before +pushing changes. ## Bugs and Feature Requests diff --git a/Scripts/install-git-hooks b/Scripts/install-git-hooks index 670ea93..e29caa6 100755 --- a/Scripts/install-git-hooks +++ b/Scripts/install-git-hooks @@ -43,8 +43,41 @@ EOF echo "Pre-commit hook installed successfully!" } +# Function to install the pre-push hook +install_pre_push_hook() { + local pre_push_hook="$REPO_ROOT/.git/hooks/pre-push" + + echo "Installing pre-push hook..." + + cat > "$pre_push_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 test-all-platforms script +echo "Running tests on all platforms..." +if ! "$REPO_ROOT/Scripts/test-all-platforms"; then + echo "Platform tests failed. Please fix issues before pushing." + exit 1 +fi + +echo "All platform tests passed." +EOF + + chmod +x "$pre_push_hook" + echo "Pre-push hook installed successfully!" +} + # Install the pre-commit hook install_pre_commit_hook +# Install the pre-push hook +install_pre_push_hook + echo "All git hooks installed successfully!" echo "The pre-commit hook will run 'Scripts/lint' before each commit." +echo "The pre-push hook will run 'Scripts/test-all-platforms' before each push." diff --git a/Scripts/test-all-platforms b/Scripts/test-all-platforms new file mode 100755 index 0000000..bd6d901 --- /dev/null +++ b/Scripts/test-all-platforms @@ -0,0 +1,68 @@ +#!/bin/bash + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${YELLOW}[$(date +'%H:%M:%S')] $1${NC}" +} + +print_success() { + echo -e "${GREEN}✓ $1${NC}" +} + +print_error() { + echo -e "${RED}✗ $1${NC}" +} + +# Platforms to test +PLATFORMS=( + "iOS Simulator,name=iPhone 16 Pro" + "macOS" + "tvOS Simulator,name=Apple TV 4K" + "watchOS Simulator,name=Apple Watch Series 10" +) + +DEFAULT_SCHEME="DevKeychain" +IOS_PROJECT="DevKeychain.xcodeproj" +IOS_SCHEME="DevKeychainApp" +FAILED_PLATFORMS=() + +print_status "Starting tests on all platforms..." +echo + +for platform in "${PLATFORMS[@]}"; do + platform_name=$(echo "$platform" | cut -d',' -f1) + print_status "Testing on $platform_name..." + + # Set project specifier based on platform + if [[ "$platform_name" == "iOS Simulator" ]]; then + PROJECT_SPECIFIER="-project $IOS_PROJECT -scheme $IOS_SCHEME" + else + PROJECT_SPECIFIER="-scheme $DEFAULT_SCHEME" + fi + + if xcodebuild test $PROJECT_SPECIFIER -destination "platform=$platform"; then + print_success "$platform_name tests passed" + else + print_error "$platform_name tests failed" + FAILED_PLATFORMS+=("$platform_name") + fi + echo +done + +# Summary +echo "==========================" +if [ ${#FAILED_PLATFORMS[@]} -eq 0 ]; then + print_success "All platform tests passed!" + exit 0 +else + print_error "Tests failed on: ${FAILED_PLATFORMS[*]}" + exit 1 +fi diff --git a/Sources/DevKeychain/Core/KeychainServices.swift b/Sources/DevKeychain/Core/KeychainServices.swift index 26bfc19..1c02b89 100644 --- a/Sources/DevKeychain/Core/KeychainServices.swift +++ b/Sources/DevKeychain/Core/KeychainServices.swift @@ -34,10 +34,26 @@ protocol KeychainServices: Sendable { /// This type uses standard Security framework functions like `SecItemAdd(_:_:)`, `SecItemCopyMatching(_:_:)`, and /// `SecItemDelete(_:)` to implement its behavior. struct StandardKeychainServices: KeychainServices { + /// A closure to use to add an item to the keychain. + /// + /// By default, this is `SecItemAdd(_:_:)`. It is provided for dependency injection purposes. + var addItem: @Sendable (CFDictionary, UnsafeMutablePointer?) -> OSStatus = SecItemAdd + + /// A closure to use to copy keychain items matching a query. + /// + /// By default, this is `SecItemCopyMatching(_:_:)`. It is provided for dependency injection purposes. + var copyMatchingItems: @Sendable (CFDictionary, UnsafeMutablePointer?) -> OSStatus = SecItemCopyMatching + + /// A closure to use to delete keychain items matching a query. + /// + /// By default, this is `SecItemDelete(_:)`. It is provided for dependency injection purposes. + var deleteItems: @Sendable (CFDictionary) -> OSStatus = SecItemDelete + + func addItem(withAttributes attributes: [CFString: Any]) throws -> AnyObject? { var result: AnyObject? - let osStatus = SecItemAdd(attributes as CFDictionary, &result) + let osStatus = addItem(attributes as CFDictionary, &result) guard osStatus == errSecSuccess else { throw KeychainServicesError(osStatus: osStatus) } @@ -49,7 +65,7 @@ struct StandardKeychainServices: KeychainServices { func items(matchingQuery query: [CFString: Any]) throws -> AnyObject? { var result: AnyObject? - let osStatus = SecItemCopyMatching(query as CFDictionary, &result) + let osStatus = copyMatchingItems(query as CFDictionary, &result) switch osStatus { case errSecSuccess: return result @@ -62,7 +78,7 @@ struct StandardKeychainServices: KeychainServices { func deleteItems(matchingQuery query: [CFString: Any]) throws { - let osStatus = SecItemDelete(query as CFDictionary) + let osStatus = deleteItems(query as CFDictionary) if osStatus != errSecSuccess && osStatus != errSecItemNotFound { throw KeychainServicesError(osStatus: osStatus) } diff --git a/Tests/DevKeychainTests/Core/StandardKeychainServicesTests.swift b/Tests/DevKeychainTests/Core/StandardKeychainServicesTests.swift new file mode 100644 index 0000000..b47ecc6 --- /dev/null +++ b/Tests/DevKeychainTests/Core/StandardKeychainServicesTests.swift @@ -0,0 +1,186 @@ +// +// StandardKeychainServicesTests.swift +// DevKeychain +// +// Created by Prachi Gauriar on 6/18/25. +// + +@preconcurrency import CoreFoundation +import DevTesting +import Foundation +import Testing + +@testable import struct DevKeychain.KeychainServicesError +@testable import struct DevKeychain.StandardKeychainServices + +struct StandardKeychainServicesTests: RandomValueGenerating { + var randomNumberGenerator = makeRandomNumberGenerator() + + + @Test(arguments: [true, false]) + mutating func addItemSucceedsWhenAddItemClosureReturnsSuccess(hasResult: Bool) throws { + let expectedResult: AnyObject? = hasResult ? randomKeychainDictionary() as CFDictionary : nil + let attributes = randomKeychainDictionary() as [CFString: Any] + + nonisolated(unsafe) let addItemStub = Stub( + defaultReturnValue: (errSecSuccess, expectedResult) + ) + + var services = StandardKeychainServices() + services.addItem = { (attributes, result) in + let (status, resultValue) = addItemStub(attributes) + result?.pointee = resultValue + return status + } + + let result = try services.addItem(withAttributes: attributes) + + #expect(result === expectedResult) + #expect(addItemStub.callArguments == [attributes as CFDictionary]) + } + + + @Test + mutating func addItemThrowsKeychainServicesErrorWhenAddItemClosureReturnsFailure() { + let attributes = randomKeychainDictionary() as [CFString: Any] + let expectedOSStatus = randomOSStatus(excluding: [errSecSuccess]) + + nonisolated(unsafe) let addItemStub = Stub( + defaultReturnValue: (expectedOSStatus, nil) + ) + + var services = StandardKeychainServices() + services.addItem = { (attributes, result) in + let (status, resultValue) = addItemStub(attributes) + result?.pointee = resultValue + return status + } + + #expect(throws: KeychainServicesError(osStatus: expectedOSStatus)) { + try services.addItem(withAttributes: attributes) + } + + #expect(addItemStub.callArguments == [attributes as CFDictionary]) + } + + + @Test(arguments: [true, false]) + mutating func itemsSucceedsWhenCopyMatchingItemsClosureReturnsSuccess(hasResult: Bool) throws { + let expectedResult: AnyObject? = hasResult ? randomKeychainDictionary() as CFDictionary : nil + let query = randomKeychainDictionary() as [CFString: Any] + + nonisolated(unsafe) let copyMatchingItemsStub = Stub( + defaultReturnValue: (errSecSuccess, expectedResult) + ) + + var services = StandardKeychainServices() + services.copyMatchingItems = { (query, result) in + let (status, resultValue) = copyMatchingItemsStub(query) + result?.pointee = resultValue + return status + } + + let result = try services.items(matchingQuery: query) + + #expect(result === expectedResult) + #expect(copyMatchingItemsStub.callArguments == [query as CFDictionary]) + } + + + @Test + mutating func itemsReturnsEmptyArrayWhenCopyMatchingItemsClosureReturnsItemNotFound() throws { + let query = randomKeychainDictionary() as [CFString: Any] + + nonisolated(unsafe) let copyMatchingItemsStub = Stub( + defaultReturnValue: (errSecItemNotFound, nil) + ) + + var services = StandardKeychainServices() + services.copyMatchingItems = { (query, result) in + let (status, resultValue) = copyMatchingItemsStub(query) + result?.pointee = resultValue + return status + } + + let result = try #require(try services.items(matchingQuery: query) as? NSArray) + + #expect(CFArrayGetCount(result) == 0) + #expect(copyMatchingItemsStub.callArguments == [query as CFDictionary]) + } + + + @Test + mutating func itemsThrowsKeychainServicesErrorWhenCopyMatchingItemsClosureReturnsOtherFailure() { + let query = randomKeychainDictionary() as [CFString: Any] + let expectedOSStatus = randomOSStatus(excluding: [errSecSuccess, errSecItemNotFound]) + + nonisolated(unsafe) let copyMatchingItemsStub = Stub( + defaultReturnValue: (expectedOSStatus, nil) + ) + + var services = StandardKeychainServices() + services.copyMatchingItems = { (query, result) in + let (status, resultValue) = copyMatchingItemsStub(query) + result?.pointee = resultValue + return status + } + + #expect(throws: KeychainServicesError(osStatus: expectedOSStatus)) { + try services.items(matchingQuery: query) + } + + #expect(copyMatchingItemsStub.callArguments == [query as CFDictionary]) + } + + + @Test(arguments: [errSecSuccess, errSecItemNotFound]) + mutating func deleteItemsSucceedsWhenDeleteItemsClosureReturnsSuccessOrItemNotFound(osStatus: OSStatus) throws { + let query = randomKeychainDictionary() as [CFString: Any] + + nonisolated(unsafe) let deleteItemsStub = Stub( + defaultReturnValue: osStatus + ) + + var services = StandardKeychainServices() + services.deleteItems = { (query) in + return deleteItemsStub(query) + } + + #expect(throws: Never.self) { + try services.deleteItems(matchingQuery: query) + } + + #expect(deleteItemsStub.callArguments == [query as CFDictionary]) + } + + + @Test + mutating func deleteItemsThrowsKeychainServicesErrorWhenDeleteItemsClosureReturnsOtherFailure() { + let query = randomKeychainDictionary() as [CFString: Any] + let expectedOSStatus = randomOSStatus(excluding: [errSecSuccess, errSecItemNotFound]) + + nonisolated(unsafe) let deleteItemsStub = Stub( + defaultReturnValue: expectedOSStatus + ) + + var services = StandardKeychainServices() + services.deleteItems = { (query) in + return deleteItemsStub(query) + } + + #expect(throws: KeychainServicesError(osStatus: expectedOSStatus)) { + try services.deleteItems(matchingQuery: query) + } + + #expect(deleteItemsStub.callArguments == [query as CFDictionary]) + } + + + private mutating func randomOSStatus(excluding statuses: Set) -> OSStatus { + var status: OSStatus + repeat { + status = random(Int32.self, in: .min ... .max) + } while statuses.contains(status) + return status + } +}