Skip to content

Commit

Permalink
Replace subscripts with getters+setters
Browse files Browse the repository at this point in the history
  • Loading branch information
JacobHearst committed Oct 18, 2024
1 parent 52a5d96 commit 7a9a6b5
Show file tree
Hide file tree
Showing 7 changed files with 473 additions and 220 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
### Added
- Added convenience methods for accessing mock data values on the `HaversackEphemeralStrategy`

## [1.3.0] - 2024-02-11
### Added
- Added support for visionOS.
Expand Down
335 changes: 179 additions & 156 deletions Sources/Haversack/Haversack.swift

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions Sources/Haversack/HaversackStrategy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ open class HaversackStrategy {
///
/// This should not be called directly but is used by ``Haversack/Haversack/exportItems(_:config:)`` to perform the actual exporting
/// - Parameters:
/// - item: The keys, certificates, or identities to export
/// - items: The keys, certificates, or identities to export
/// - configuration: A configuration representing all the options that can be provided to `SecItemExport`
/// - Returns: A `Data` representation of the keychain item
open func exportItems(_ items: [any KeychainExportable], configuration: KeychainExportConfig) throws -> Data {
Expand Down Expand Up @@ -232,8 +232,9 @@ open class HaversackStrategy {

/// Imports one or more keys, certificates, or identities and adds them to the keychain
/// - Parameters:
/// - item: The keys, certificates, or identities to import
/// - items: The keys, certificates, or identities to import
/// - configuration: A configuration representing all the options that can be provided to `SecItemImport`
/// - importKeychain: The keychain to import the items to
/// - Returns: An array of all the keychain items imported
open func importItems<EntityType: KeychainImportable>(_ items: Data, configuration: KeychainImportConfig<EntityType>, importKeychain: SecKeychain? = nil) throws -> [EntityType] {
var inputFormat = configuration.inputFormat
Expand Down
1 change: 0 additions & 1 deletion Sources/Haversack/ImportExport/KeychainImportConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ public struct KeychainImportConfig<T: KeychainImportable> {
}

/// Make any imported keys extractable. By default keys are not extractable after import
/// - Parameter extractable: Whether or not the keychain item can be exported from its keychain (Defaults to false)
/// - Returns: A `KeychainImportConfig` struct
public func extractable() throws -> Self where T: PrivateKeyImporting {
// swiftlint:disable:next line_length
Expand Down
156 changes: 156 additions & 0 deletions Sources/HaversackMock/HaversackEphemeralStrategy+mocking.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// SPDX-License-Identifier: MIT
// Copyright 2023, Jamf

import Haversack
import Security
import XCTest

extension Haversack {
/// A convenience accessor that handles typecasting the `HaversackStrategy` to a `HaversackEphemeralStrategy`
/// via XCTest's `XCTUnwrap` function.
var ephemeralStrategy: HaversackEphemeralStrategy {
get throws {
try XCTUnwrap(configuration.strategy as? HaversackEphemeralStrategy)
}
}
}

// MARK: Mock data setters
extension Haversack {
/// Mocks data for calls to `Haversack.first(where:)`
/// - Parameters:
/// - query: The query to set a mock value for
/// - mockValue: The mock value to set
public func setSearchFirstMock<T: KeychainQuerying>(where query: T, mockValue: T.Entity) throws {
let query = try makeSearchQuery(query, singleItem: true)
try ephemeralStrategy.setMock(mockValue, forQuery: query.query)
}

/// Retrieves the value for a call to `Haversack.first(where:)` from ``HaversackEphemeralStrategy/mockData``
/// - Parameter query: The query to retrieve a value for
/// - Returns: The value associated with `query` in ``HaversackEphemeralStrategy/mockData``
public func getSearchFirstMock<T: KeychainQuerying>(where query: T) throws -> T.Entity? {
let query = try makeSearchQuery(query, singleItem: true)
return try ephemeralStrategy.getMockDataValue(for: query.query)
}

/// Mocks data for calls to `Haversack.search(where:)`
/// - Parameters:
/// - query: The query to set a mock value for
/// - mockValue: The mock value to set
public func setSearchMock<T: KeychainQuerying>(where query: T, mockValue: [T.Entity]) throws {
let query = try makeSearchQuery(query, singleItem: false)
try ephemeralStrategy.setMock(mockValue, forQuery: query.query)
}

/// Retrieves the value for a call to `Haversack.search(where:)` from ``HaversackEphemeralStrategy/mockData``
/// - Parameter query: The query to retrieve a value for
/// - Returns: The value associated with `query` in ``HaversackEphemeralStrategy/mockData``
public func getSearchMock<T: KeychainQuerying>(where query: T) throws -> [T.Entity]? {
let query = try makeSearchQuery(query, singleItem: false)
return try ephemeralStrategy.getMockDataValue(for: query.query)
}

/// Mocks data for calls to `Haversack.save(_:itemSecurity:updateExisting:)`. This is useful when
/// you want to test the behavior of your code when the item being saved already exists in the keychain.
/// - Parameters:
/// - item: The item being saved
/// - itemSecurity: The security the item should have
/// - mockValue: The mock value to set
public func setSaveMock<T: KeychainStorable>(item: T, itemSecurity: ItemSecurity = .standard, mockValue: Any) throws {
let query = try makeSaveQuery(item, itemSecurity: itemSecurity)
try ephemeralStrategy.setMock(mockValue, forQuery: query)
}

/// Retrieves the value for a call to `Haversack.save(_:itemSecurity:updateExisting:)` from ``HaversackEphemeralStrategy/mockData``
/// - Parameters:
/// - item: The item being saved
/// - itemSecurity: The security the item should have
/// - Returns: The value associated with `query` in ``HaversackEphemeralStrategy/mockData``
public func getSaveMock<T: KeychainStorable>(item: T, itemSecurity: ItemSecurity = .standard) throws -> Any? {
let query = try makeSaveQuery(item, itemSecurity: itemSecurity)
return try ephemeralStrategy.getMockDataValue(for: query)
}

/// Mocks data for calls to `Haversack.delete(_:treatNotFoundAsSuccess:)`
/// - Parameters:
/// - item: The item to generate a delete query and set a mock value for
/// - mockValue: The mock value to set
public func setDeleteMock<T: KeychainStorable>(item: T, mockValue: Any) throws {
let query = try makeDeleteQuery(item)
try ephemeralStrategy.setMock(mockValue, forQuery: query)
}

/// Retrieves the value for a call to `Haversack.delete(_:treatNotFoundAsSuccess:)` from ``HaversackEphemeralStrategy/mockData``
/// - Parameter item: The item to generate a delete query and set a mock value for
/// - Returns: The value associated with `query` in ``HaversackEphemeralStrategy/mockData``
public func getDeleteMock<T: KeychainStorable>(item: T) throws -> Any? {
let query = try makeDeleteQuery(item)
return try ephemeralStrategy.getMockDataValue(for: query)
}

/// Mocks data for calls to `Haversack.delete(where:treatNotFoundAsSuccess:)`
/// - Parameters:
/// - query: The query to set a mock value for
/// - mockValue: The mock value to set
public func setDeleteMock<T: KeychainQuerying>(where query: T, mockValue: Any) throws {
let query = try makeDeleteQuery(query)
try ephemeralStrategy.setMock(mockValue, forQuery: query.query)
}

/// Retrieves the value for a call to `Haversack.delete(where:treatNotFoundAsSuccess:)` from ``HaversackEphemeralStrategy/mockData``
/// - Parameter query: The query to retrieve a value for
/// - Returns: The value associated with `query` in ``HaversackEphemeralStrategy/mockData``
public func getDeleteMock<T: KeychainQuerying>(where query: T) throws -> Any? {
let query = try makeDeleteQuery(query)
return try ephemeralStrategy.getMockDataValue(for: query.query)
}

/// Mocks data for calls to `Haversack.generateKey(fromConfig:itemSecurity:)`
/// - Parameters:
/// - config: The key generation configuration values that the query should include
/// - itemSecurity: The item security the query should specify
/// - mockValue: The mock value to set
public func setGenerateKeyMock(config: KeyGenerationConfig, itemSecurity: ItemSecurity = .standard, mockValue: SecKey) throws {
let query = try makeKeyGenerationQuery(fromConfig: config, itemSecurity: itemSecurity)
try ephemeralStrategy.setMock(mockValue, forQuery: query)
}

/// Retrieves the value for a call to `Haversack.generateKey(fromConfig:itemSecurity:)` from ``HaversackEphemeralStrategy/mockData``
/// - Parameters:
/// - config: The key generation configuration values that the query should include
/// - itemSecurity: The item security the query should specify
/// - Returns: The value associated with `query` in ``HaversackEphemeralStrategy/mockData``
public func getGenerateKeyMock(config: KeyGenerationConfig, itemSecurity: ItemSecurity = .standard) throws -> SecKey? {
let query = try makeKeyGenerationQuery(fromConfig: config, itemSecurity: itemSecurity)
return try ephemeralStrategy.getMockDataValue(for: query)
}
}

extension HaversackEphemeralStrategy {
/// Generates a ``mockData`` key for the query and sets the value of that key to `mockValue`
/// - Parameters:
/// - mockValue: The value to mock
/// - query: The query that the mock value is associated with
func setMock(_ mockValue: Any, forQuery query: SecurityFrameworkQuery) {
mockData[key(for: query)] = mockValue
}

/// Retrieves the ``mockData`` value for the provided query
///
/// This overload is required because `Optional<Any>` can be typecast to `Any`.
/// This means that if the generic version of this function were called where `T == Any`,
/// it would always return a non-nil value of `Any` with the actual type of `Optional<Any>.none`.
/// - Parameter query: The query to retreive a value for
/// - Returns: The value
func getMockDataValue(for query: SecurityFrameworkQuery) -> Any? {
mockData[key(for: query)]
}

/// Retrieves and typecasts the ``mockData`` value for the provided query
/// - Parameter query: The query to retreive a value for
/// - Returns: The typecasted value
func getMockDataValue<T>(for query: SecurityFrameworkQuery) -> T? {
getMockDataValue(for: query) as? T
}
}
31 changes: 7 additions & 24 deletions Sources/HaversackMock/HaversackEphemeralStrategy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@
import Foundation
import Haversack

/// A strategy which uses a simple dictionary to search, store, and delete data instead of hitting an actual keychain.
/// A strategy which uses a simple dictionary to import, export, search, store, and delete data instead of hitting an actual keychain.
///
/// The keys of the ``mockData`` dictionary are calculated from the queries that are sent through Haversack.
///
/// You can also use either the untyped ``subscript(_:)-7weqp`` or the typed ``subscript(_:)-6jjzx``
/// accessors to avoid having to hold onto the calculated keys.
open class HaversackEphemeralStrategy: HaversackStrategy {
/// The dictionary that is used for storage of keychain items
///
Expand Down Expand Up @@ -41,25 +38,6 @@ open class HaversackEphemeralStrategy: HaversackStrategy {
/// If the strategy has any problems it will throw `NSError` with this domain.
public static let errorDomain = "haversack.unit_testing.mock"

/// Untyped access to ``mockData`` values via keychain queries instead of `String`s
/// - Parameter query: The keychain query to read/write to
/// - Returns: The ``mockData`` value for the `query`
public subscript(_ query: any KeychainQuerying) -> Any? {
get {
mockData[key(for: query.query)]
}
set {
mockData[key(for: query.query)] = newValue
}
}

/// Typed access to ``mockData`` values via keychain queries instead of `String`s
/// - Parameter query: The keychain query to read the value for
/// - Returns: The ``mockData`` value for the `query`
public subscript<T>(_ query: any KeychainQuerying) -> T? {
self[query] as? T
}

/// Looks through the ``mockData`` dictionary for an entry matching the query.
/// - Parameter querying: An instance of a type that conforms to the `KeychainQuerying` protocol.
/// - Throws: An `NSError` with the ``errorDomain`` domain if no entry is found in the dictionary.
Expand Down Expand Up @@ -117,6 +95,7 @@ open class HaversackEphemeralStrategy: HaversackStrategy {
/// - Parameter query: An instance of a `Haversack/SecurityFrameworkQuery`.
/// - Returns: Returns the private key of a new cryptographic key pair.
/// - Throws: An `NSError` object if the key cannot be found. Prior to throwing, also stores the query in the ``mockData`` for future inspection.
/// - Important: The mock value must be an instance of `SecKey`
override open func generateKey(_ query: SecurityFrameworkQuery) throws -> SecKey {
let theKey = key(for: query)

Expand Down Expand Up @@ -159,7 +138,11 @@ open class HaversackEphemeralStrategy: HaversackStrategy {
/// - Returns: The items in ``mockImportedEntities``
/// - Throws: Either ``mockImportError`` or an `NSError` with the ``errorDomain`` domain if the
/// items in ``mockImportedEntities`` don't match the type of the `EntityType` of the `configuration` parameter.
override open func importItems<EntityType: KeychainImportable>(_ items: Data, configuration: KeychainImportConfig<EntityType>, importKeychain: SecKeychain? = nil) throws -> [EntityType] {
override open func importItems<EntityType: KeychainImportable>(
_ items: Data,
configuration: KeychainImportConfig<EntityType>,
importKeychain: SecKeychain? = nil
) throws -> [EntityType] {
if let keyImportConfig = configuration as? KeychainImportConfig<KeyEntity> {
keyImportConfiguration = keyImportConfig
} else if let certificateImportConfig = configuration as? KeychainImportConfig<CertificateEntity> {
Expand Down
Loading

0 comments on commit 7a9a6b5

Please sign in to comment.