Skip to content

Commit

Permalink
Feat: Add completion handler API & refactoring async await API
Browse files Browse the repository at this point in the history
async await 를 사용하지 않는 프로젝트에서도 사용할 수 있도록 completion handler API 를 추가함.

또한 기존 async await 코드를 completion handler 코드를 wrapping 하도록 리팩토링함 

completion handler 추가로 completion handler 를 사용하는 API 테스트도 추가하였음
  • Loading branch information
HoJongE committed Sep 6, 2022
1 parent f9be4be commit 9c2007e
Show file tree
Hide file tree
Showing 2 changed files with 225 additions and 83 deletions.
190 changes: 112 additions & 78 deletions Sources/KeyChainWrapper/PasswordKeychainManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Void, Error>) 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<Void, Error>) 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<Void, Error>) 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 {

Expand Down
118 changes: 113 additions & 5 deletions Tests/KeyChainWrapperTests/PasswordKeychainTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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)
}
}

0 comments on commit 9c2007e

Please sign in to comment.