diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index 70066bd2241017e30bc4702ec1127cc25fed1688..57081f3ef7c5ae6abcf0cb130aa994d8a6460abc 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -57,6 +57,7 @@ let package = Package( .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), .product(name: "XXDatabase", package: "client-ios-db"), + .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), .product(name: "XXModels", package: "client-ios-db"), ], swiftSettings: swiftSettings diff --git a/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandler.swift b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandler.swift new file mode 100644 index 0000000000000000000000000000000000000000..c6e03cbedb7f1d0020d11a94c6fc0172a5fc5247 --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandler.swift @@ -0,0 +1,49 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct AuthCallbackHandler { + public typealias OnError = (Error) -> Void + + public var run: (@escaping OnError) -> Cancellable + + public func callAsFunction(onError: @escaping OnError) -> Cancellable { + run(onError) + } +} + +extension AuthCallbackHandler { + public static func live( + messenger: Messenger, + handleRequest: AuthCallbackHandlerRequest, + handleConfirm: AuthCallbackHandlerConfirm, + handleReset: AuthCallbackHandlerReset + ) -> AuthCallbackHandler { + AuthCallbackHandler { onError in + messenger.registerAuthCallbacks(.init { callback in + do { + switch callback { + case .request(let contact, _, _, _): + try handleRequest(contact) + + case .confirm(let contact, _, _, _): + try handleConfirm(contact) + + case .reset(let contact, _, _, _): + try handleReset(contact) + } + } catch { + onError(error) + } + }) + } + } +} + +extension AuthCallbackHandler { + public static let unimplemented = AuthCallbackHandler( + run: XCTUnimplemented("\(Self.self)", placeholder: Cancellable {}) + ) +} diff --git a/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerConfirm.swift b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerConfirm.swift new file mode 100644 index 0000000000000000000000000000000000000000..2aa6787fff741651d649fb8f88397669b448d92f --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerConfirm.swift @@ -0,0 +1,31 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXModels + +public struct AuthCallbackHandlerConfirm { + public var run: (XXClient.Contact) throws -> Void + + public func callAsFunction(_ contact: XXClient.Contact) throws { + try run(contact) + } +} + +extension AuthCallbackHandlerConfirm { + public static func live(db: DBManagerGetDB) -> AuthCallbackHandlerConfirm { + AuthCallbackHandlerConfirm { xxContact in + let id = try xxContact.getId() + guard var dbContact = try db().fetchContacts(.init(id: [id])).first else { + return + } + dbContact.authStatus = .friend + dbContact = try db().saveContact(dbContact) + } + } +} + +extension AuthCallbackHandlerConfirm { + public static let unimplemented = AuthCallbackHandlerConfirm( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerRequest.swift b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerRequest.swift new file mode 100644 index 0000000000000000000000000000000000000000..1ec239230874c79e3d670bf377c57ab45400895b --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerRequest.swift @@ -0,0 +1,40 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXModels + +public struct AuthCallbackHandlerRequest { + public var run: (XXClient.Contact) throws -> Void + + public func callAsFunction(_ contact: XXClient.Contact) throws { + try run(contact) + } +} + +extension AuthCallbackHandlerRequest { + public static func live( + db: DBManagerGetDB, + now: @escaping () -> Date + ) -> AuthCallbackHandlerRequest { + AuthCallbackHandlerRequest { xxContact in + let id = try xxContact.getId() + guard try db().fetchContacts(.init(id: [id])).isEmpty else { + return + } + var dbContact = XXModels.Contact(id: id) + dbContact.marshaled = xxContact.data + dbContact.username = try xxContact.getFact(.username)?.value + dbContact.email = try xxContact.getFact(.email)?.value + dbContact.phone = try xxContact.getFact(.phone)?.value + dbContact.authStatus = .verificationInProgress + dbContact.createdAt = now() + dbContact = try db().saveContact(dbContact) + } + } +} + +extension AuthCallbackHandlerRequest { + public static let unimplemented = AuthCallbackHandlerRequest( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift new file mode 100644 index 0000000000000000000000000000000000000000..729fe13eff8ee42ed1a12ba2ce03d082e5b2cfa8 --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift @@ -0,0 +1,31 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXModels + +public struct AuthCallbackHandlerReset { + public var run: (XXClient.Contact) throws -> Void + + public func callAsFunction(_ contact: XXClient.Contact) throws { + try run(contact) + } +} + +extension AuthCallbackHandlerReset { + public static func live(db: DBManagerGetDB) -> AuthCallbackHandlerReset { + AuthCallbackHandlerReset { xxContact in + let id = try xxContact.getId() + guard var dbContact = try db().fetchContacts(.init(id: [id])).first else { + return + } + dbContact.authStatus = .stranger + dbContact = try db().saveContact(dbContact) + } + } +} + +extension AuthCallbackHandlerReset { + public static let unimplemented = AuthCallbackHandlerReset( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerConfirmTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerConfirmTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..1a1b5f19573669403a069bd4e3cecd2e11347e73 --- /dev/null +++ b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerConfirmTests.swift @@ -0,0 +1,54 @@ +import CustomDump +import XCTest +import XXModels +import XXClient +@testable import AppCore + +final class AuthCallbackHandlerConfirmTests: XCTestCase { + func testConfirm() throws { + var didFetchContacts: [XXModels.Contact.Query] = [] + var didSaveContact: [XXModels.Contact] = [] + + let dbContact = XXModels.Contact( + id: "id".data(using: .utf8)!, + authStatus: .requested + ) + let confirm = AuthCallbackHandlerConfirm.live( + db: .init { + var db: Database = .failing + db.fetchContacts.run = { query in + didFetchContacts.append(query) + return [dbContact] + } + db.saveContact.run = { contact in + didSaveContact.append(contact) + return contact + } + return db + } + ) + var xxContact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!) + xxContact.getIdFromContact.run = { _ in "id".data(using: .utf8)! } + + try confirm(xxContact) + + XCTAssertNoDifference(didFetchContacts, [.init(id: ["id".data(using: .utf8)!])]) + var expectedSavedContact = dbContact + expectedSavedContact.authStatus = .friend + XCTAssertNoDifference(didSaveContact, [expectedSavedContact]) + } + + func testConfirmWhenContactNotInDatabase() throws { + let confirm = AuthCallbackHandlerConfirm.live( + db: .init { + var db: Database = .failing + db.fetchContacts.run = { _ in [] } + return db + } + ) + var contact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!) + contact.getIdFromContact.run = { _ in "id".data(using: .utf8)! } + + try confirm(contact) + } +} diff --git a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerRequestTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerRequestTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..fb92a5b8871258b61c88f5bfd43bdb53d64208ba --- /dev/null +++ b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerRequestTests.swift @@ -0,0 +1,67 @@ +import CustomDump +import XCTest +import XXModels +import XXClient +import XCTestDynamicOverlay +@testable import AppCore + +final class AuthCallbackHandlerRequestTests: XCTestCase { + func testRequestFromNewContact() throws { + let now = Date() + var didFetchContacts: [XXModels.Contact.Query] = [] + var didSaveContact: [XXModels.Contact] = [] + + let request = AuthCallbackHandlerRequest.live( + db: .init { + var db: Database = .failing + db.fetchContacts.run = { query in + didFetchContacts.append(query) + return [] + } + db.saveContact.run = { contact in + didSaveContact.append(contact) + return contact + } + return db + }, + now: { now } + ) + var xxContact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!) + xxContact.getIdFromContact.run = { _ in "id".data(using: .utf8)! } + xxContact.getFactsFromContact.run = { _ in + [ + Fact(type: .username, value: "username"), + Fact(type: .email, value: "email"), + Fact(type: .phone, value: "phone"), + ] + } + + try request(xxContact) + + XCTAssertNoDifference(didFetchContacts, [.init(id: ["id".data(using: .utf8)!])]) + XCTAssertNoDifference(didSaveContact, [.init( + id: "id".data(using: .utf8)!, + marshaled: "contact".data(using: .utf8)!, + username: "username", + email: "email", + phone: "phone", + authStatus: .verificationInProgress, + createdAt: now + )]) + } + + func testRequestWhenContactInDatabase() throws { + let request = AuthCallbackHandlerRequest.live( + db: .init { + var db: Database = .failing + db.fetchContacts.run = { _ in [.init(id: "id".data(using: .utf8)!)] } + return db + }, + now: XCTUnimplemented("now", placeholder: Date()) + ) + var contact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!) + contact.getIdFromContact.run = { _ in "id".data(using: .utf8)! } + + try request(contact) + } +} diff --git a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerResetTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerResetTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..9a4407bf758a0c0ba922c154d199b758dea701be --- /dev/null +++ b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerResetTests.swift @@ -0,0 +1,54 @@ +import CustomDump +import XCTest +import XXModels +import XXClient +@testable import AppCore + +final class AuthCallbackHandlerResetTests: XCTestCase { + func testReset() throws { + var didFetchContacts: [XXModels.Contact.Query] = [] + var didSaveContact: [XXModels.Contact] = [] + + let dbContact = XXModels.Contact( + id: "id".data(using: .utf8)!, + authStatus: .friend + ) + let reset = AuthCallbackHandlerReset.live( + db: .init { + var db: Database = .failing + db.fetchContacts.run = { query in + didFetchContacts.append(query) + return [dbContact] + } + db.saveContact.run = { contact in + didSaveContact.append(contact) + return contact + } + return db + } + ) + var xxContact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!) + xxContact.getIdFromContact.run = { _ in "id".data(using: .utf8)! } + + try reset(xxContact) + + XCTAssertNoDifference(didFetchContacts, [.init(id: ["id".data(using: .utf8)!])]) + var expectedSavedContact = dbContact + expectedSavedContact.authStatus = .stranger + XCTAssertNoDifference(didSaveContact, [expectedSavedContact]) + } + + func testResetWhenContactNotInDatabase() throws { + let reset = AuthCallbackHandlerReset.live( + db: .init { + var db: Database = .failing + db.fetchContacts.run = { _ in [] } + return db + } + ) + var contact = XXClient.Contact.unimplemented("contact".data(using: .utf8)!) + contact.getIdFromContact.run = { _ in "id".data(using: .utf8)! } + + try reset(contact) + } +} diff --git a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..2eddbbb9e3b9d7524e6ea0e1e65e481cbe921437 --- /dev/null +++ b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerTests.swift @@ -0,0 +1,114 @@ +import CustomDump +import XCTest +import XXClient +import XXMessengerClient +@testable import AppCore + +final class AuthCallbackHandlerTests: XCTestCase { + func testCallbackHandling() throws { + struct TestState: Equatable { + var didRegisterAuthCallbacks = 0 + var didCancelAuthCallbacks = 0 + var didHandleRequest: [Contact] = [] + var didHandleConfirm: [Contact] = [] + var didHandleReset: [Contact] = [] + } + var registeredAuthCallbacks: [AuthCallbacks] = [] + var state = TestState() + var expectedState = state + + var messenger: Messenger = .unimplemented + messenger.registerAuthCallbacks.run = { callbacks in + state.didRegisterAuthCallbacks += 1 + registeredAuthCallbacks.append(callbacks) + return Cancellable { state.didCancelAuthCallbacks += 1 } + } + + let handle = AuthCallbackHandler.live( + messenger: messenger, + handleRequest: .init { state.didHandleRequest.append($0) }, + handleConfirm: .init { state.didHandleConfirm.append($0) }, + handleReset: .init { state.didHandleReset.append($0) } + ) + + var cancellable: Any? = handle(onError: { error in + XCTFail("Unexpected error: \(error)") + }) + + expectedState.didRegisterAuthCallbacks += 1 + XCTAssertNoDifference(state, expectedState) + + let contact1 = XXClient.Contact.unimplemented("1".data(using: .utf8)!) + registeredAuthCallbacks.first?.handle( + .request(contact: contact1, receptionId: Data(), ephemeralId: 0, roundId: 0) + ) + + expectedState.didHandleRequest.append(contact1) + XCTAssertNoDifference(state, expectedState) + + let contact2 = XXClient.Contact.unimplemented("2".data(using: .utf8)!) + registeredAuthCallbacks.first?.handle( + .confirm(contact: contact2, receptionId: Data(), ephemeralId: 0, roundId: 0) + ) + + expectedState.didHandleConfirm.append(contact2) + XCTAssertNoDifference(state, expectedState) + + let contact3 = XXClient.Contact.unimplemented("3".data(using: .utf8)!) + registeredAuthCallbacks.first?.handle( + .reset(contact: contact3, receptionId: Data(), ephemeralId: 0, roundId: 0) + ) + + expectedState.didHandleReset.append(contact3) + XCTAssertNoDifference(state, expectedState) + + cancellable = nil + + expectedState.didCancelAuthCallbacks += 1 + XCTAssertNoDifference(state, expectedState) + + _ = cancellable + } + + func testCallbackHandlingFailure() { + enum Failure: Error, Equatable { + case request + case confirm + case reset + } + var registeredAuthCallbacks: [AuthCallbacks] = [] + var errors: [Error] = [] + + var messenger: Messenger = .unimplemented + messenger.registerAuthCallbacks.run = { callbacks in + registeredAuthCallbacks.append(callbacks) + return Cancellable {} + } + + let handle = AuthCallbackHandler.live( + messenger: messenger, + handleRequest: .init { _ in throw Failure.request }, + handleConfirm: .init { _ in throw Failure.confirm }, + handleReset: .init { _ in throw Failure.reset } + ) + + let cancellable = handle(onError: { errors.append($0) }) + + registeredAuthCallbacks.first?.handle( + .request(contact: .unimplemented(Data()), receptionId: Data(), ephemeralId: 0, roundId: 0) + ) + registeredAuthCallbacks.first?.handle( + .confirm(contact: .unimplemented(Data()), receptionId: Data(), ephemeralId: 0, roundId: 0) + ) + registeredAuthCallbacks.first?.handle( + .reset(contact: .unimplemented(Data()), receptionId: Data(), ephemeralId: 0, roundId: 0) + ) + + XCTAssertNoDifference( + errors.map { $0 as? Failure }, + [.request, .confirm, .reset] + ) + + _ = cancellable + } +}