diff --git a/Sources/KeyChainWrapper/PasswordKeychainManager.swift b/Sources/KeyChainWrapper/PasswordKeychainManager.swift index 616008b..b5e4fd9 100644 --- a/Sources/KeyChainWrapper/PasswordKeychainManager.swift +++ b/Sources/KeyChainWrapper/PasswordKeychainManager.swift @@ -13,117 +13,151 @@ public final class PasswordKeychainManager { } // MARK: - Public Interface +// MARK: - Async/Await extension public extension PasswordKeychainManager { func savePassword(_ password: String, for userAccount: String) async throws { - - let encodedPassword: Data = try parsePasswordToData(password) - let passwordQuery: PasswordQuery = PasswordQuery(service: service, appGroup: appGroup) - - var query = passwordQuery.query - query[String(kSecAttrAccount)] = userAccount - - var status: OSStatus = await withCheckedContinuation { continuation in - DispatchQueue.global(qos: .userInteractive).async { - let status = SecItemCopyMatching(query as CFDictionary, nil) - continuation.resume(with: Result.success(status)) + try await withCheckedThrowingContinuation { [self] (continuation: CheckedContinuation) in + savePassword(password, for: userAccount) { error in + if let error = error { + continuation.resume(throwing: error) + return + } + continuation.resume() } } + } - switch status { - case errSecSuccess: - var attributesToUpdate: [String: Any] = [:] - attributesToUpdate[String(kSecValueData)] = encodedPassword - - status = await withCheckedContinuation { continuation in - DispatchQueue.global(qos: .userInteractive).async { - let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary) - continuation.resume(with: Result.success(status)) + func getPassword(for userAccount: String) async throws -> String? { + let password: String? = try await withCheckedThrowingContinuation { [self] continuation in + getPassword(for: userAccount) { password, error in + if let error = error { + continuation.resume(throwing: error) + return } + continuation.resume(returning: password) } + } + return password + } - if status != errSecSuccess { - throw error(from: status) - } - case errSecItemNotFound: - query[String(kSecValueData)] = encodedPassword - status = await withCheckedContinuation { continuation in - DispatchQueue.global(qos: .userInteractive).async { - let status = SecItemAdd(query as CFDictionary, nil) - continuation.resume(with: Result.success(status)) + func removePassword(for userAccount: String) async throws { + _ = try await withCheckedThrowingContinuation { [self] (continuation: CheckedContinuation) in + removePassword(for: userAccount) { error in + if let error = error { + continuation.resume(throwing: error) + return } + continuation.resume() } - - if status != errSecSuccess { - throw error(from: status) - } - default: - throw error(from: status) } } - func getPassword(for userAccount: String) async throws -> String? { - - let query = makeFindPasswordQuery(for: userAccount) - - var queryResult: AnyObject? - - let status: OSStatus = await withCheckedContinuation { continuation in - DispatchQueue.global(qos: .userInteractive).async { - let status: OSStatus = withUnsafeMutablePointer(to: &queryResult) { - SecItemCopyMatching(query, $0) + func removeAllPassword() async throws { + _ = try await withCheckedThrowingContinuation { [self] (continuation: CheckedContinuation) in + removeAllPassword { error in + if let error = error { + continuation.resume(throwing: error) + return } - continuation.resume(with: Result.success(status)) + continuation.resume() } } - - switch status { - case errSecSuccess: - guard let quriedItem = queryResult as? [String: Any], - let passwordData = quriedItem[String(kSecValueData)] as? Data, - let password = String(data: passwordData, encoding: .utf8) - else { - throw KeyChainError.dataToStringConversionError + } +} +// MARK: - Completion handler extension +public extension PasswordKeychainManager { + func savePassword(_ password: String, for userAccount: String, completion: ((Error?) -> Void)? = nil) { + DispatchQueue.global(qos: .userInteractive).async { + // password 를 데이터 convert 에 실패하면... 바로 completion handler 실행 + guard let encodedPassword: Data = try? self.parsePasswordToData(password) else { + completion?(KeyChainError.stringToDataConversionError) + return + } + let passwordQuery: PasswordQuery = PasswordQuery(service: self.service, appGroup: self.appGroup) + + var query = passwordQuery.query + query[String(kSecAttrAccount)] = userAccount + let status = SecItemCopyMatching(query as CFDictionary, nil) + switch status { + case errSecSuccess: + var attributesToUpdate: [String: Any] = [:] + attributesToUpdate[String(kSecValueData)] = encodedPassword + let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary) + + if status != errSecSuccess { + completion?(self.error(from: status)) + } else { + completion?(nil) + } + case errSecItemNotFound: + query[String(kSecValueData)] = encodedPassword + let status = SecItemAdd(query as CFDictionary, nil) + if status != errSecSuccess { + completion?(self.error(from: status)) + } else { + completion?(nil) + } + default: + completion?(self.error(from: status)) } - - return password - case errSecItemNotFound: - return nil - default: - throw error(from: status) } } - func removePassword(for userAccount: String) async throws { - var query = PasswordQuery(service: service, appGroup: appGroup).query - query[String(kSecAttrAccount)] = userAccount + func getPassword(for userAccount: String, completion: ((String? ,Error?) -> Void)? = nil) { + DispatchQueue.global(qos: .userInteractive).async { + let query = self.makeFindPasswordQuery(for: userAccount) - let status = await withCheckedContinuation { continuation in - DispatchQueue.global(qos: .userInteractive).async { - let status = SecItemDelete(query as CFDictionary) - continuation.resume(with: Result.success(status)) + var queryResult: AnyObject? + let status: OSStatus = withUnsafeMutablePointer(to: &queryResult) { + SecItemCopyMatching(query, $0) } - } - guard status == errSecSuccess || status == errSecItemNotFound else { - throw error(from: status) + switch status { + case errSecSuccess: + guard let quriedItem = queryResult as? [String: Any], + let passwordData = quriedItem[String(kSecValueData)] as? Data, + let password = String(data: passwordData, encoding: .utf8) + else { + completion?(nil, KeyChainError.dataToStringConversionError) + return + } + completion?(password, nil) + case errSecItemNotFound: + completion?(nil, nil) + default: + completion?(nil, self.error(from: status)) + } } } - func removeAllPassword() async throws { - let query = PasswordQuery(service: service, appGroup: appGroup).query + func removePassword(for userAccount: String, completion: ((Error?) -> Void)? = nil) { + DispatchQueue.global(qos: .userInteractive).async { [self] in + var query = PasswordQuery(service: service, appGroup: appGroup).query + query[String(kSecAttrAccount)] = userAccount + let status = SecItemDelete(query as CFDictionary) - let status = await withCheckedContinuation { continuation in - DispatchQueue.global(qos: .userInteractive).async { - let status = SecItemDelete(query as CFDictionary) - continuation.resume(with: Result.success(status)) + guard status == errSecSuccess || status == errSecItemNotFound else { + completion?(error(from: status)) + return } + completion?(nil) } - guard status == errSecSuccess || status == errSecItemNotFound else { - throw error(from: status) + } + + func removeAllPassword(completion: ((Error?) -> Void)? = nil) { + DispatchQueue.global(qos: .userInteractive).async { [self] in + let query = PasswordQuery(service: service, appGroup: appGroup).query + let status = SecItemDelete(query as CFDictionary) + guard status == errSecSuccess || status == errSecItemNotFound else { + completion?(error(from: status)) + return + } + completion?(nil) } } } + // MARK: - Implementation private extension PasswordKeychainManager { diff --git a/Tests/KeyChainWrapperTests/PasswordKeychainTests.swift b/Tests/KeyChainWrapperTests/PasswordKeychainTests.swift index 1dd5796..b0992ca 100644 --- a/Tests/KeyChainWrapperTests/PasswordKeychainTests.swift +++ b/Tests/KeyChainWrapperTests/PasswordKeychainTests.swift @@ -8,9 +8,9 @@ final class PasswordKeychainTests: XCTestCase { private let testAccount: String = "TestAccount" private let testPassword: String = "TestPassword" - override func setUpWithError() throws { + override func setUp() async throws { + try await super.setUp() passwordKeychainManager = .init(PasswordKeychainManager(service: testService)) - try super.setUpWithError() } override func tearDown() async throws { @@ -19,7 +19,7 @@ final class PasswordKeychainTests: XCTestCase { try await super.tearDown() } - func testSaveAndGetPassword() async throws { + func testSaveAndGetPassword() async { do { try await passwordKeychainManager.savePassword(testPassword, for: testAccount) let password = try await passwordKeychainManager.getPassword(for: testAccount) @@ -29,7 +29,7 @@ final class PasswordKeychainTests: XCTestCase { } } - func testUpdatePassword() async throws { + func testUpdatePassword() async { do { try await passwordKeychainManager.savePassword(testPassword, for: testAccount) try await passwordKeychainManager @@ -44,7 +44,7 @@ final class PasswordKeychainTests: XCTestCase { } } - func testRemovePassword() async throws { + func testRemovePassword() async { do { try await passwordKeychainManager.savePassword(testPassword, for: testAccount) try await passwordKeychainManager.removePassword(for: testAccount) @@ -55,4 +55,112 @@ final class PasswordKeychainTests: XCTestCase { XCTFail("Remove Password Failed with \(error.localizedDescription)") } } + + func testRemoveAllPassword() async { + do { + try await passwordKeychainManager.savePassword(testPassword, for: testAccount) + try await passwordKeychainManager.removeAllPassword() + + let password = try await passwordKeychainManager.getPassword(for: testAccount) + XCTAssertNil(password) + } catch { + XCTFail("Remove all passwords failed with \(error.localizedDescription)") + } + } + + func testSaveAndGetPasswordCompletionHandler() { + // given + let promise = expectation(description: "Test and save password success!") + var password: String? + // when + passwordKeychainManager.savePassword(testPassword, for: testAccount) { [self] error in + guard error == nil else { + promise.fulfill() + return + } + passwordKeychainManager.getPassword(for: self.testAccount) { pw, error in + password = pw + promise.fulfill() + } + } + // then + + wait(for: [promise], timeout: 1) + XCTAssertEqual(password, testPassword, "Password is \(testPassword), test success") + } + + func testUpdatePasswordCompletionHandler() { + // given + let promise = expectation(description: "Update password success!") + var password: String? + // when + passwordKeychainManager.savePassword(testPassword, for: testAccount) { [self] error in + guard error == nil else { + promise.fulfill() + return + } + self.passwordKeychainManager.savePassword(testPassword + "2", for: testAccount) { [self] error in + guard error == nil else { + promise.fulfill() + return + } + self.passwordKeychainManager.getPassword(for: self.testAccount) { pw, error in + password = pw + promise.fulfill() + } + } + } + + // then + wait(for: [promise], timeout: 1) + XCTAssertEqual(password, testPassword + "2") + } + + func testRemovePasswordCompletionHandler() async throws { + // given + let promise = expectation(description: "Remove password success!") + var password: String? + // when + try await passwordKeychainManager.savePassword(testPassword, for: testAccount) + let pw: String? = try await passwordKeychainManager.getPassword(for: testAccount) + XCTAssertEqual(testPassword, pw) + passwordKeychainManager.removePassword(for: testAccount) { [self] error in + guard error == nil else { + promise.fulfill() + return + } + passwordKeychainManager.getPassword(for: testAccount) { pw, error in + password = pw + promise.fulfill() + } + } + + // then + wait(for: [promise], timeout: 1) + XCTAssertNil(password) + } + + func testRemoveAllPasswordCompletionHandler() async throws { + // given + let promise = expectation(description: "Remove password success!") + var password: String? + // when + try await passwordKeychainManager.savePassword(testPassword, for: testAccount) + let pw: String? = try await passwordKeychainManager.getPassword(for: testAccount) + XCTAssertEqual(testPassword, pw) + passwordKeychainManager.removeAllPassword { [self] error in + guard error == nil else { + promise.fulfill() + return + } + passwordKeychainManager.getPassword(for: testAccount) { pw, error in + password = pw + promise.fulfill() + } + } + + // then + wait(for: [promise], timeout: 1) + XCTAssertNil(password) + } }