ios/AuthenticatorBridgeKit/SharedKeychain/SharedKeychainStorageTests.swift
2025-06-10 12:03:49 -05:00

129 lines
5.5 KiB
Swift

import AuthenticatorBridgeKit
import AuthenticatorBridgeKitMocks
import CryptoKit
import Foundation
import XCTest
final class SharedKeychainStorageTests: BitwardenTestCase {
// MARK: Properties
let accessGroup = "group.com.example.bitwarden"
var keychainService: MockSharedKeychainService!
var subject: SharedKeychainStorage!
// MARK: Setup & Teardown
override func setUp() {
keychainService = MockSharedKeychainService()
subject = DefaultSharedKeychainStorage(
keychainService: keychainService,
sharedAppGroupIdentifier: accessGroup
)
}
override func tearDown() {
keychainService = nil
subject = nil
}
// MARK: Tests
/// Verify that `deleteValue(for:)` issues a delete with the correct search attributes specified.
///
func test_deleteValue_success() async throws {
try await subject.deleteValue(for: .authenticatorKey)
let queries = try XCTUnwrap(keychainService.deleteQueries as? [[CFString: Any]])
XCTAssertEqual(queries.count, 1)
let query = try XCTUnwrap(queries.first)
try XCTAssertEqual(XCTUnwrap(query[kSecAttrAccessGroup] as? String), accessGroup)
try XCTAssertEqual(XCTUnwrap(query[kSecAttrAccessible] as? String),
String(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly))
try XCTAssertEqual(XCTUnwrap(query[kSecAttrAccount] as? String),
SharedKeychainItem.authenticatorKey.unformattedKey)
try XCTAssertEqual(XCTUnwrap(query[kSecClass] as? String),
String(kSecClassGenericPassword))
}
/// Verify that `getValue(for:)` returns a value successfully when one is set. Additionally, verify the
/// search attributes are specified correctly.
///
func test_getValue_success() async throws {
let key = SymmetricKey(size: .bits256)
let data = key.withUnsafeBytes { Data(Array($0)) }
keychainService.setSearchResultData(data)
let returnData: Data = try await subject.getValue(for: .authenticatorKey)
XCTAssertEqual(returnData, data)
let query = try XCTUnwrap(keychainService.searchQuery as? [CFString: Any])
try XCTAssertEqual(XCTUnwrap(query[kSecAttrAccessGroup] as? String), accessGroup)
try XCTAssertEqual(XCTUnwrap(query[kSecAttrAccessible] as? String),
String(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly))
try XCTAssertEqual(XCTUnwrap(query[kSecAttrAccount] as? String),
SharedKeychainItem.authenticatorKey.unformattedKey)
try XCTAssertEqual(XCTUnwrap(query[kSecClass] as? String), String(kSecClassGenericPassword))
try XCTAssertEqual(XCTUnwrap(query[kSecMatchLimit] as? String), String(kSecMatchLimitOne))
try XCTAssertTrue(XCTUnwrap(query[kSecReturnAttributes] as? Bool))
try XCTAssertTrue(XCTUnwrap(query[kSecReturnData] as? Bool))
}
/// Verify that `getAuthenticatorKey()` fails with a `keyNotFound` error when an unexpected
/// result is returned instead of the key data from the keychain
///
func test_getValue_badResult() async throws {
let key = SharedKeychainItem.accountAutoLogout(userId: "1")
let error = SharedKeychainServiceError.keyNotFound(key)
keychainService.searchResult = .success([kSecValueData as String: NSObject()] as AnyObject)
await assertAsyncThrows(error: error) {
let _: Data = try await subject.getValue(for: key)
}
}
/// Verify that `getValue(for:)` fails with a `keyNotFound` error when a nil
/// result is returned instead of the key data from the keychain
///
func test_getValue_nilResult() async throws {
let key = SharedKeychainItem.accountAutoLogout(userId: "1")
let error = SharedKeychainServiceError.keyNotFound(key)
keychainService.searchResult = .success(nil)
await assertAsyncThrows(error: error) {
let _: Data = try await subject.getValue(for: key)
}
}
/// Verify that `getValue(for:)` fails with an error when the Authenticator key is not
/// present in the keychain
///
func test_getAuthenticatorKey_keyNotFound() async throws {
let error = SharedKeychainServiceError.keyNotFound(SharedKeychainItem.authenticatorKey)
keychainService.searchResult = .failure(error)
await assertAsyncThrows(error: error) {
let _: Data = try await subject.getValue(for: .authenticatorKey)
}
}
/// Verify that `setValue(_:for:)` sets a value with the correct search attributes specified.
///
func test_setAuthenticatorKey_success() async throws {
let key = SymmetricKey(size: .bits256)
let data = key.withUnsafeBytes { Data(Array($0)) }
try await subject.setValue(data, for: .authenticatorKey)
let attributes = try XCTUnwrap(keychainService.addAttributes as? [CFString: Any])
try XCTAssertEqual(XCTUnwrap(attributes[kSecAttrAccessGroup] as? String), accessGroup)
try XCTAssertEqual(XCTUnwrap(attributes[kSecAttrAccessible] as? String),
String(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly))
try XCTAssertEqual(XCTUnwrap(attributes[kSecAttrAccount] as? String),
SharedKeychainItem.authenticatorKey.unformattedKey)
try XCTAssertEqual(XCTUnwrap(attributes[kSecClass] as? String),
String(kSecClassGenericPassword))
try XCTAssertEqual(XCTUnwrap(attributes[kSecValueData] as? Data), data)
}
}