diff --git a/Package.swift b/Package.swift
index 4f06554efce0916bf663521502ed565b0a93c8e4..0024dab56e3d9aa08441134301edf535ef878da4 100644
--- a/Package.swift
+++ b/Package.swift
@@ -31,6 +31,10 @@ let package = Package(
       url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git",
       .upToNextMajor(from: "0.4.0")
     ),
+    .package(
+      url: "https://github.com/kishikawakatsumi/KeychainAccess.git",
+      .upToNextMajor(from: "4.2.2")
+    ),
   ],
   targets: [
     .target(
@@ -53,6 +57,7 @@ let package = Package(
       name: "XXMessengerClient",
       dependencies: [
         .target(name: "XXClient"),
+        .product(name: "KeychainAccess", package: "KeychainAccess"),
         .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
       ],
       swiftSettings: swiftSettings
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerConnect.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerConnect.swift
new file mode 100644
index 0000000000000000000000000000000000000000..fa24b8ebf6ef0d813c6d72bad7566ae558251755
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerConnect.swift
@@ -0,0 +1,35 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerConnect {
+  public enum Error: Swift.Error, Equatable {
+    case notLoaded
+  }
+
+  public var run: () throws -> Void
+
+  public func callAsFunction() throws {
+    try run()
+  }
+}
+
+extension MessengerConnect {
+  public static func live(_ env: MessengerEnvironment) -> MessengerConnect {
+    MessengerConnect {
+      guard let cMix = env.ctx.cMix else {
+        throw Error.notLoaded
+      }
+      env.ctx.e2e = try env.login(
+        cMixId: cMix.getId(),
+        identity: try cMix.makeLegacyReceptionIdentity(),
+        e2eParamsJSON: env.getE2EParams()
+      )
+    }
+  }
+}
+
+extension MessengerConnect {
+  public static let unimplemented = MessengerConnect(
+    run: XCTUnimplemented()
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerCreate.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerCreate.swift
new file mode 100644
index 0000000000000000000000000000000000000000..a3514141508e1fa6d5a47de09d75cc3aa9d87600
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerCreate.swift
@@ -0,0 +1,35 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerCreate {
+  public var run: () throws -> Void
+
+  public func callAsFunction() throws {
+    try run()
+  }
+}
+
+extension MessengerCreate {
+  public static func live(_ env: MessengerEnvironment) -> MessengerCreate {
+    MessengerCreate {
+      let ndfData = try env.downloadNDF(env.ndfEnvironment)
+      let password = env.generateSecret()
+      try env.passwordStorage.save(password)
+      let storageDir = env.storageDir()
+      try env.fileManager.removeDirectory(storageDir)
+      try env.fileManager.createDirectory(storageDir)
+      try env.newCMix(
+        ndfJSON: String(data: ndfData, encoding: .utf8)!,
+        storageDir: storageDir,
+        password: password,
+        registrationCode: nil
+      )
+    }
+  }
+}
+
+extension MessengerCreate {
+  public static let unimplemented = MessengerCreate(
+    run: XCTUnimplemented()
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerIsConnected.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsConnected.swift
new file mode 100644
index 0000000000000000000000000000000000000000..6a790dd51d232fbc556933a5e8173fbe6fa452fe
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsConnected.swift
@@ -0,0 +1,25 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerIsConnected {
+  public var run: () -> Bool
+
+  public func callAsFunction() -> Bool {
+    run()
+  }
+}
+
+extension MessengerIsConnected {
+  public static func live(_ env: MessengerEnvironment) -> MessengerIsConnected {
+    MessengerIsConnected {
+      env.ctx.e2e != nil
+    }
+  }
+}
+
+extension MessengerIsConnected {
+  public static let unimplemented = MessengerIsConnected(
+    run: XCTUnimplemented()
+  )
+}
+
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerIsCreated.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsCreated.swift
new file mode 100644
index 0000000000000000000000000000000000000000..cf7f5a4f6da8d00d7d306c55e82a28b7cbf73d69
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsCreated.swift
@@ -0,0 +1,24 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerIsCreated {
+  public var run: () -> Bool
+
+  public func callAsFunction() -> Bool {
+    run()
+  }
+}
+
+extension MessengerIsCreated {
+  public static func live(_ env: MessengerEnvironment) -> MessengerIsCreated {
+    MessengerIsCreated {
+      env.fileManager.isDirectoryEmpty(env.storageDir()) == false
+    }
+  }
+}
+
+extension MessengerIsCreated {
+  public static let unimplemented = MessengerIsCreated(
+    run: XCTUnimplemented()
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerIsLoaded.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsLoaded.swift
new file mode 100644
index 0000000000000000000000000000000000000000..88750885c657d6e15fc7ecbc23e1e230a60b4845
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsLoaded.swift
@@ -0,0 +1,24 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerIsLoaded {
+  public var run: () -> Bool
+
+  public func callAsFunction() -> Bool {
+    run()
+  }
+}
+
+extension MessengerIsLoaded {
+  public static func live(_ env: MessengerEnvironment) -> MessengerIsLoaded {
+    MessengerIsLoaded {
+      env.ctx.cMix != nil
+    }
+  }
+}
+
+extension MessengerIsLoaded {
+  public static let unimplemented = MessengerIsLoaded(
+    run: XCTUnimplemented()
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerIsLoggedIn.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsLoggedIn.swift
new file mode 100644
index 0000000000000000000000000000000000000000..7f886bef096f578931834f01aaead0e0cb6f2c36
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsLoggedIn.swift
@@ -0,0 +1,24 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerIsLoggedIn {
+  public var run: () -> Bool
+
+  public func callAsFunction() -> Bool {
+    run()
+  }
+}
+
+extension MessengerIsLoggedIn {
+  public static func live(_ env: MessengerEnvironment) -> MessengerIsLoggedIn {
+    MessengerIsLoggedIn {
+      env.ctx.ud != nil
+    }
+  }
+}
+
+extension MessengerIsLoggedIn {
+  public static let unimplemented = MessengerIsLoggedIn(
+    run: XCTUnimplemented()
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerIsRegistered.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsRegistered.swift
new file mode 100644
index 0000000000000000000000000000000000000000..c91db5acb557b3a46c163dcacd44cb59a60d8c48
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsRegistered.swift
@@ -0,0 +1,31 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerIsRegistered {
+  public enum Error: Swift.Error, Equatable {
+    case notConnected
+  }
+
+  public var run: () throws -> Bool
+
+  public func callAsFunction() throws -> Bool {
+    try run()
+  }
+}
+
+extension MessengerIsRegistered {
+  public static func live(_ env: MessengerEnvironment) -> MessengerIsRegistered {
+    MessengerIsRegistered {
+      guard let e2e = env.ctx.e2e else {
+        throw Error.notConnected
+      }
+      return try env.isRegisteredWithUD(e2eId: e2e.getId())
+    }
+  }
+}
+
+extension MessengerIsRegistered {
+  public static let unimplemented = MessengerIsRegistered(
+    run: XCTUnimplemented()
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerLoad.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerLoad.swift
new file mode 100644
index 0000000000000000000000000000000000000000..9b0454a4ee689c5590ea9823420cc03500b4b0c6
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerLoad.swift
@@ -0,0 +1,28 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerLoad {
+  public var run: () throws -> Void
+
+  public func callAsFunction() throws {
+    try run()
+  }
+}
+
+extension MessengerLoad {
+  public static func live(_ env: MessengerEnvironment) -> MessengerLoad {
+    MessengerLoad {
+      env.ctx.cMix = try env.loadCMix(
+        storageDir: env.storageDir(),
+        password: try env.passwordStorage.load(),
+        cMixParamsJSON: env.getCMixParams()
+      )
+    }
+  }
+}
+
+extension MessengerLoad {
+  public static let unimplemented = MessengerLoad(
+    run: XCTUnimplemented()
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerLogIn.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerLogIn.swift
new file mode 100644
index 0000000000000000000000000000000000000000..4b43a56b24f14887217ede4513b815b1cd970920
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerLogIn.swift
@@ -0,0 +1,50 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerLogIn {
+  public enum Error: Swift.Error, Equatable {
+    case notLoaded
+    case notConnected
+  }
+
+  public var run: () throws -> Void
+
+  public func callAsFunction() throws {
+    try run()
+  }
+}
+
+extension MessengerLogIn {
+  public static func live(_ env: MessengerEnvironment) -> MessengerLogIn {
+    MessengerLogIn {
+      guard let cMix = env.ctx.cMix else {
+        throw Error.notLoaded
+      }
+      guard let e2e = env.ctx.e2e else {
+        throw Error.notConnected
+      }
+      if cMix.networkFollowerStatus() != .running {
+        try cMix.startNetworkFollower(timeoutMS: 30_000)
+      }
+      env.ctx.ud = try env.newOrLoadUd(
+        params: .init(
+          e2eId: e2e.getId(),
+          username: nil,
+          registrationValidationSignature: nil,
+          cert: env.udCert() ?? e2e.getUdCertFromNdf(),
+          contactFile: env.udContact() ?? (try e2e.getUdContactFromNdf()),
+          address: env.udAddress() ?? e2e.getUdAddressFromNdf()
+        ),
+        follower: .init {
+          cMix.networkFollowerStatus().rawValue
+        }
+      )
+    }
+  }
+}
+
+extension MessengerLogIn {
+  public static let unimplemented = MessengerLogIn(
+    run: XCTUnimplemented()
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerRegister.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerRegister.swift
new file mode 100644
index 0000000000000000000000000000000000000000..1440dd43f01f1c25765265a763ea0e5a45b31730
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerRegister.swift
@@ -0,0 +1,52 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerRegister {
+  public enum Error: Swift.Error, Equatable {
+    case notLoaded
+    case notConnected
+  }
+
+  public var run: (String) throws -> Void
+
+  public func callAsFunction(
+    username: String
+  ) throws {
+    try run(username)
+  }
+}
+
+extension MessengerRegister {
+  public static func live(_ env: MessengerEnvironment) -> MessengerRegister {
+    MessengerRegister { username in
+      guard let cMix = env.ctx.cMix else {
+        throw Error.notLoaded
+      }
+      guard let e2e = env.ctx.e2e else {
+        throw Error.notConnected
+      }
+      if cMix.networkFollowerStatus() != .running {
+        try cMix.startNetworkFollower(timeoutMS: 30_000)
+      }
+      env.ctx.ud = try env.newOrLoadUd(
+        params: .init(
+          e2eId: e2e.getId(),
+          username: username,
+          registrationValidationSignature: cMix.getReceptionRegistrationValidationSignature(),
+          cert: env.udCert() ?? e2e.getUdCertFromNdf(),
+          contactFile: env.udContact() ?? (try e2e.getUdContactFromNdf()),
+          address: env.udAddress() ?? e2e.getUdAddressFromNdf()
+        ),
+        follower: .init {
+          cMix.networkFollowerStatus().rawValue
+        }
+      )
+    }
+  }
+}
+
+extension MessengerRegister {
+  public static let unimplemented = MessengerRegister(
+    run: XCTUnimplemented()
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Messenger.swift b/Sources/XXMessengerClient/Messenger/Messenger.swift
new file mode 100644
index 0000000000000000000000000000000000000000..a477f7cc90ec06841b3d8820c28f8ed6eeaea9f8
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Messenger.swift
@@ -0,0 +1,46 @@
+import XXClient
+
+public struct Messenger {
+  public var isCreated: MessengerIsCreated
+  public var create: MessengerCreate
+  public var isLoaded: MessengerIsLoaded
+  public var load: MessengerLoad
+  public var isConnected: MessengerIsConnected
+  public var connect: MessengerConnect
+  public var isRegistered: MessengerIsRegistered
+  public var register: MessengerRegister
+  public var isLoggedIn: MessengerIsLoggedIn
+  public var logIn: MessengerLogIn
+}
+
+extension Messenger {
+  public static func live(_ env: MessengerEnvironment) -> Messenger {
+    Messenger(
+      isCreated: .live(env),
+      create: .live(env),
+      isLoaded: .live(env),
+      load: .live(env),
+      isConnected: .live(env),
+      connect: .live(env),
+      isRegistered: .live(env),
+      register: .live(env),
+      isLoggedIn: .live(env),
+      logIn: .live(env)
+    )
+  }
+}
+
+extension Messenger {
+  public static let unimplemented = Messenger(
+    isCreated: .unimplemented,
+    create: .unimplemented,
+    isLoaded: .unimplemented,
+    load: .unimplemented,
+    isConnected: .unimplemented,
+    connect: .unimplemented,
+    isRegistered: .unimplemented,
+    register: .unimplemented,
+    isLoggedIn: .unimplemented,
+    logIn: .unimplemented
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/MessengerContext.swift b/Sources/XXMessengerClient/Messenger/MessengerContext.swift
new file mode 100644
index 0000000000000000000000000000000000000000..ba67ed2b36b6674055120118f4c3060f50c15f44
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/MessengerContext.swift
@@ -0,0 +1,17 @@
+import XXClient
+
+public class MessengerContext {
+  public init(
+    cMix: CMix? = nil,
+    e2e: E2E? = nil,
+    ud: UserDiscovery? = nil
+  ) {
+    self.cMix = cMix
+    self.e2e = e2e
+    self.ud = ud
+  }
+
+  public var cMix: CMix?
+  public var e2e: E2E?
+  public var ud: UserDiscovery?
+}
diff --git a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
new file mode 100644
index 0000000000000000000000000000000000000000..dfa7dfcd3da15c9722d07349b306ba9b177ff39c
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
@@ -0,0 +1,73 @@
+import Foundation
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerEnvironment {
+  public var ctx: MessengerContext
+  public var downloadNDF: DownloadAndVerifySignedNdf
+  public var fileManager: MessengerFileManager
+  public var generateSecret: GenerateSecret
+  public var getCMixParams: GetCMixParams
+  public var getE2EParams: GetE2EParams
+  public var isRegisteredWithUD: IsRegisteredWithUD
+  public var loadCMix: LoadCMix
+  public var login: Login
+  public var ndfEnvironment: NDFEnvironment
+  public var newCMix: NewCMix
+  public var newOrLoadUd: NewOrLoadUd
+  public var passwordStorage: PasswordStorage
+  public var storageDir: () -> String
+  public var udAddress: () -> String?
+  public var udCert: () -> Data?
+  public var udContact: () -> Data?
+}
+
+extension MessengerEnvironment {
+  public static let defaultStorageDir = FileManager.default
+    .urls(for: .applicationSupportDirectory, in: .userDomainMask)
+    .first!
+    .appendingPathComponent("xx.network.client")
+    .path
+
+  public static let live = MessengerEnvironment(
+    ctx: .init(),
+    downloadNDF: .live,
+    fileManager: .live(),
+    generateSecret: .live,
+    getCMixParams: .liveDefault,
+    getE2EParams: .liveDefault,
+    isRegisteredWithUD: .live,
+    loadCMix: .live,
+    login: .live,
+    ndfEnvironment: .mainnet,
+    newCMix: .live,
+    newOrLoadUd: .live,
+    passwordStorage: .keychain,
+    storageDir: { MessengerEnvironment.defaultStorageDir },
+    udAddress: { nil },
+    udCert: { nil },
+    udContact: { nil }
+  )
+}
+
+extension MessengerEnvironment {
+  public static let unimplemented = MessengerEnvironment(
+    ctx: .init(),
+    downloadNDF: .unimplemented,
+    fileManager: .unimplemented,
+    generateSecret: .unimplemented,
+    getCMixParams: .unimplemented,
+    getE2EParams: .unimplemented,
+    isRegisteredWithUD: .unimplemented,
+    loadCMix: .unimplemented,
+    login: .unimplemented,
+    ndfEnvironment: .unimplemented,
+    newCMix: .unimplemented,
+    newOrLoadUd: .unimplemented,
+    passwordStorage: .unimplemented,
+    storageDir: XCTUnimplemented("\(Self.self).storageDir", placeholder: ""),
+    udAddress: XCTUnimplemented("\(Self.self).udAddress", placeholder: nil),
+    udCert: XCTUnimplemented("\(Self.self).udCert", placeholder: nil),
+    udContact: XCTUnimplemented("\(Self.self).udContact", placeholder: nil)
+  )
+}
diff --git a/Sources/XXMessengerClient/Unimplemented.swift b/Sources/XXMessengerClient/Unimplemented.swift
deleted file mode 100644
index 9454b9d77e43e6208b27e1a82464ffaee4378ba6..0000000000000000000000000000000000000000
--- a/Sources/XXMessengerClient/Unimplemented.swift
+++ /dev/null
@@ -1 +0,0 @@
-private enum Unimplemented {}
diff --git a/Sources/XXMessengerClient/Utils/MessengerFileManager.swift b/Sources/XXMessengerClient/Utils/MessengerFileManager.swift
new file mode 100644
index 0000000000000000000000000000000000000000..4ff49c5fe155e612852860d1d7276124729017d3
--- /dev/null
+++ b/Sources/XXMessengerClient/Utils/MessengerFileManager.swift
@@ -0,0 +1,40 @@
+import Foundation
+import XCTestDynamicOverlay
+
+public struct MessengerFileManager {
+  public var isDirectoryEmpty: (String) -> Bool
+  public var removeDirectory: (String) throws -> Void
+  public var createDirectory: (String) throws -> Void
+}
+
+extension MessengerFileManager {
+  public static func live(
+    fileManager: FileManager = .default
+  ) -> MessengerFileManager {
+    MessengerFileManager(
+      isDirectoryEmpty: { path in
+        let contents = try? fileManager.contentsOfDirectory(atPath: path)
+        return contents?.isEmpty ?? true
+      },
+      removeDirectory: { path in
+        if fileManager.fileExists(atPath: path) {
+          try fileManager.removeItem(atPath: path)
+        }
+      },
+      createDirectory: { path in
+        try fileManager.createDirectory(
+          atPath: path,
+          withIntermediateDirectories: true
+        )
+      }
+    )
+  }
+}
+
+extension MessengerFileManager {
+  public static let unimplemented = MessengerFileManager(
+    isDirectoryEmpty: XCTUnimplemented("\(Self.self).isDirectoryEmpty"),
+    removeDirectory: XCTUnimplemented("\(Self.self).removeDirectory"),
+    createDirectory: XCTUnimplemented("\(Self.self).createDirectory")
+  )
+}
diff --git a/Sources/XXMessengerClient/Utils/PasswordStorage+Keychain.swift b/Sources/XXMessengerClient/Utils/PasswordStorage+Keychain.swift
new file mode 100644
index 0000000000000000000000000000000000000000..0a062cc628cf037491a37ba8639e1f2529f209e8
--- /dev/null
+++ b/Sources/XXMessengerClient/Utils/PasswordStorage+Keychain.swift
@@ -0,0 +1,21 @@
+import KeychainAccess
+import XXClient
+
+extension PasswordStorage {
+  public static let keychain: PasswordStorage = {
+    let keychain = KeychainAccess.Keychain(
+      service: "xx.network.client.messenger"
+    )
+    return PasswordStorage(
+      save: { password in
+        keychain[data: "password"] = password
+      },
+      load: {
+        guard let password = keychain[data: "password"] else {
+          throw MissingPasswordError()
+        }
+        return password
+      }
+    )
+  }()
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerConnectTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerConnectTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..c44b4907449a0ba57507a7e92575b69091dd8b04
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerConnectTests.swift
@@ -0,0 +1,107 @@
+import CustomDump
+import XXClient
+import XCTest
+@testable import XXMessengerClient
+
+final class MessengerConnectTests: XCTestCase {
+  func testConnect() throws {
+    struct DidLogIn: Equatable {
+      var ephemeral: Bool
+      var cMixId: Int
+      var authCallbacksProvided: Bool
+      var identity: ReceptionIdentity
+      var e2eParamsJSON: Data
+    }
+    var didLogIn: [DidLogIn] = []
+
+    let cMixId = 1234
+    let receptionId = ReceptionIdentity.stub
+    let e2eParams = "e2e-params".data(using: .utf8)!
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = .unimplemented
+    env.ctx.cMix!.getId.run = { cMixId }
+    env.ctx.cMix!.makeLegacyReceptionIdentity.run = { receptionId }
+    env.getE2EParams.run = { e2eParams }
+    env.login.run = { ephemeral, cMixId, authCallbacks, identity, e2eParamsJSON in
+      didLogIn.append(.init(
+        ephemeral: ephemeral,
+        cMixId: cMixId,
+        authCallbacksProvided: authCallbacks != nil,
+        identity: identity,
+        e2eParamsJSON: e2eParamsJSON
+      ))
+      return .unimplemented
+    }
+    let connect: MessengerConnect = .live(env)
+
+    try connect()
+
+    XCTAssertNoDifference(didLogIn, [
+      DidLogIn(
+        ephemeral: false,
+        cMixId: 1234,
+        authCallbacksProvided: false,
+        identity: .stub,
+        e2eParamsJSON: e2eParams
+      )
+    ])
+
+    XCTAssertNotNil(env.ctx.e2e)
+  }
+
+  func testConnectWithoutCMix() {
+    let env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = nil
+    let connect: MessengerConnect = .live(env)
+
+    XCTAssertThrowsError(try connect()) { error in
+      XCTAssertEqual(
+        error as? MessengerConnect.Error,
+        MessengerConnect.Error.notLoaded
+      )
+    }
+  }
+
+  func testMakeLegacyReceptionIdentityFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    let env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = .unimplemented
+    env.ctx.cMix!.getId.run = { 1234 }
+    env.ctx.cMix!.makeLegacyReceptionIdentity.run = { throw error }
+    let connect: MessengerConnect = .live(env)
+
+    XCTAssertThrowsError(try connect()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+
+  func testLoginFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = .unimplemented
+    env.ctx.cMix!.getId.run = { 1234 }
+    env.ctx.cMix!.makeLegacyReceptionIdentity.run = { .stub }
+    env.getE2EParams.run = { "e2e-params".data(using: .utf8)! }
+    env.login.run = { _, _, _, _, _ in throw error }
+    let connect: MessengerConnect = .live(env)
+
+    XCTAssertThrowsError(try connect()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+}
+
+private extension ReceptionIdentity {
+  static let stub = ReceptionIdentity(
+    id: "id".data(using: .utf8)!,
+    rsaPrivatePem: "rsaPrivatePem".data(using: .utf8)!,
+    salt: "salt".data(using: .utf8)!,
+    dhKeyPrivate: "dhKeyPrivate".data(using: .utf8)!,
+    e2eGrp: "e2eGrp".data(using: .utf8)!
+  )
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerCreateTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerCreateTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..5db87012a6b984e3db1ed3cb018e2cda96038d60
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerCreateTests.swift
@@ -0,0 +1,159 @@
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+import CustomDump
+
+final class MessengerCreateTests: XCTestCase {
+  func testCreate() throws {
+    struct DidNewCMix: Equatable {
+      var ndfJSON: String
+      var storageDir: String
+      var password: Data
+      var registrationCode: String?
+    }
+
+    var didDownloadNDF: [NDFEnvironment] = []
+    var didGenerateSecret: [Int] = []
+    var didSavePassword: [Data] = []
+    var didRemoveDirectory: [String] = []
+    var didCreateDirectory: [String] = []
+    var didNewCMix: [DidNewCMix] = []
+
+    let ndf = "ndf".data(using: .utf8)!
+    let password = "password".data(using: .utf8)!
+    let storageDir = "storage-dir"
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ndfEnvironment = .unimplemented
+    env.downloadNDF.run = { ndfEnvironment in
+      didDownloadNDF.append(ndfEnvironment)
+      return ndf
+    }
+    env.generateSecret.run = { numBytes in
+      didGenerateSecret.append(numBytes)
+      return password
+    }
+    env.passwordStorage.save = { password in
+      didSavePassword.append(password)
+    }
+    env.storageDir = {
+      storageDir
+    }
+    env.fileManager.removeDirectory = { path in
+      didRemoveDirectory.append(path)
+    }
+    env.fileManager.createDirectory = { path in
+      didCreateDirectory.append(path)
+    }
+    env.newCMix.run = { ndfJSON, storageDir, password, registrationCode in
+      didNewCMix.append(.init(
+        ndfJSON: ndfJSON,
+        storageDir: storageDir,
+        password: password,
+        registrationCode: registrationCode
+      ))
+    }
+    let create: MessengerCreate = .live(env)
+
+    try create()
+
+    XCTAssertNoDifference(didDownloadNDF, [.unimplemented])
+    XCTAssertNoDifference(didGenerateSecret, [32])
+    XCTAssertNoDifference(didSavePassword, [password])
+    XCTAssertNoDifference(didRemoveDirectory, [storageDir])
+    XCTAssertNoDifference(didCreateDirectory, [storageDir])
+    XCTAssertNoDifference(didNewCMix, [.init(
+      ndfJSON: String(data: ndf, encoding: .utf8)!,
+      storageDir: storageDir,
+      password: password,
+      registrationCode: nil
+    )])
+  }
+
+  func testDownloadNDFFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ndfEnvironment = .unimplemented
+    env.downloadNDF.run = { _ in throw error }
+    let create: MessengerCreate = .live(env)
+
+    XCTAssertThrowsError(try create()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+
+  func testSavePasswordFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ndfEnvironment = .unimplemented
+    env.downloadNDF.run = { _ in "ndf".data(using: .utf8)! }
+    env.generateSecret.run = { _ in "password".data(using: .utf8)! }
+    env.passwordStorage.save = { _ in throw error }
+    let create: MessengerCreate = .live(env)
+
+    XCTAssertThrowsError(try create()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+
+  func testRemoveDirectoryFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ndfEnvironment = .unimplemented
+    env.downloadNDF.run = { _ in "ndf".data(using: .utf8)! }
+    env.generateSecret.run = { _ in "password".data(using: .utf8)! }
+    env.passwordStorage.save = { _ in }
+    env.storageDir = { "storage-dir" }
+    env.fileManager.removeDirectory = { _ in throw error }
+    let create: MessengerCreate = .live(env)
+
+    XCTAssertThrowsError(try create()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+
+  func testCreateDirectoryFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ndfEnvironment = .unimplemented
+    env.downloadNDF.run = { _ in "ndf".data(using: .utf8)! }
+    env.generateSecret.run = { _ in "password".data(using: .utf8)! }
+    env.passwordStorage.save = { _ in }
+    env.storageDir = { "storage-dir" }
+    env.fileManager.removeDirectory = { _ in }
+    env.fileManager.createDirectory = { _ in throw error }
+    let create: MessengerCreate = .live(env)
+
+    XCTAssertThrowsError(try create()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+
+  func testNewCMixFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ndfEnvironment = .unimplemented
+    env.downloadNDF.run = { _ in "ndf".data(using: .utf8)! }
+    env.generateSecret.run = { _ in "password".data(using: .utf8)! }
+    env.passwordStorage.save = { _ in }
+    env.storageDir = { "storage-dir" }
+    env.fileManager.removeDirectory = { _ in }
+    env.fileManager.createDirectory = { _ in }
+    env.newCMix.run = { _, _, _, _ in throw error }
+    let create: MessengerCreate = .live(env)
+
+    XCTAssertThrowsError(try create()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsConnectedTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsConnectedTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..e207e8a5d2eaca4c25f4c286d95bf991f20a270c
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsConnectedTests.swift
@@ -0,0 +1,20 @@
+import XCTest
+@testable import XXMessengerClient
+
+final class MessengerIsConnectedTests: XCTestCase {
+  func testWithE2E() {
+    let env: MessengerEnvironment = .unimplemented
+    env.ctx.e2e = .unimplemented
+    let isConnected: MessengerIsConnected = .live(env)
+
+    XCTAssertTrue(isConnected())
+  }
+
+  func testWithoutE2E() {
+    let env: MessengerEnvironment = .unimplemented
+    env.ctx.e2e = nil
+    let isConnected: MessengerIsConnected = .live(env)
+
+    XCTAssertFalse(isConnected())
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsCreatedTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsCreatedTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..a96c8a6073b8e34b3d5553628657d2d9cec0632e
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsCreatedTests.swift
@@ -0,0 +1,37 @@
+import CustomDump
+import XCTest
+@testable import XXMessengerClient
+
+final class MessengerIsCreatedTests: XCTestCase {
+  func testStorageDirNotEmpty() {
+    var didIsDirectoryEmpty: [String] = []
+    let storageDir = "storage-dir"
+
+    var env: MessengerEnvironment = .unimplemented
+    env.storageDir = { storageDir }
+    env.fileManager.isDirectoryEmpty = { path in
+      didIsDirectoryEmpty.append(path)
+      return false
+    }
+    let isCreated: MessengerIsCreated = .live(env)
+
+    XCTAssertTrue(isCreated())
+    XCTAssertNoDifference(didIsDirectoryEmpty, [storageDir])
+  }
+
+  func testStorageDirEmpty() {
+    var didIsDirectoryEmpty: [String] = []
+    let storageDir = "storage-dir"
+
+    var env: MessengerEnvironment = .unimplemented
+    env.storageDir = { storageDir }
+    env.fileManager.isDirectoryEmpty = { path in
+      didIsDirectoryEmpty.append(path)
+      return true
+    }
+    let isCreated: MessengerIsCreated = .live(env)
+
+    XCTAssertFalse(isCreated())
+    XCTAssertNoDifference(didIsDirectoryEmpty, [storageDir])
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsLoadedTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsLoadedTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..c83dc523f99e1049feea566ca6772a31394ea097
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsLoadedTests.swift
@@ -0,0 +1,20 @@
+import XCTest
+@testable import XXMessengerClient
+
+final class MessengerIsLoadedTests: XCTestCase {
+  func testWithCMix() {
+    let env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = .unimplemented
+    let isLoaded: MessengerIsLoaded = .live(env)
+
+    XCTAssertTrue(isLoaded())
+  }
+
+  func testWithoutCMix() {
+    let env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = nil
+    let isLoaded: MessengerIsLoaded = .live(env)
+
+    XCTAssertFalse(isLoaded())
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsLoggedInTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsLoggedInTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..6b5b79034acce19387e150bd8bc6c84c6c61ea0e
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsLoggedInTests.swift
@@ -0,0 +1,20 @@
+import XCTest
+@testable import XXMessengerClient
+
+final class MessengerIsLoggedInTests: XCTestCase {
+  func testWithUD() {
+    let env: MessengerEnvironment = .unimplemented
+    env.ctx.ud = .unimplemented
+    let isLoggedIn: MessengerIsLoggedIn = .live(env)
+
+    XCTAssertTrue(isLoggedIn())
+  }
+
+  func testWithoutUD() {
+    let env: MessengerEnvironment = .unimplemented
+    env.ctx.ud = nil
+    let isLoggedIn: MessengerIsLoggedIn = .live(env)
+
+    XCTAssertFalse(isLoggedIn())
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsRegisteredTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsRegisteredTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..250fdad884357e6cb4a7012a845c8bb37a000381
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsRegisteredTests.swift
@@ -0,0 +1,58 @@
+import XCTest
+@testable import XXMessengerClient
+
+final class MessengerIsRegisteredTests: XCTestCase {
+  func testRegistered() throws {
+    var didIsRegisteredWithUD: [Int] = []
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.e2e = .unimplemented
+    env.ctx.e2e!.getId.run = { 1234 }
+    env.isRegisteredWithUD.run = { e2eId in
+      didIsRegisteredWithUD.append(e2eId)
+      return true
+    }
+    let isRegistered: MessengerIsRegistered = .live(env)
+
+    XCTAssertTrue(try isRegistered())
+    XCTAssertEqual(didIsRegisteredWithUD, [1234])
+  }
+
+  func testNotRegistered() throws {
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.e2e = .unimplemented
+    env.ctx.e2e!.getId.run = { 1234 }
+    env.isRegisteredWithUD.run = { _ in false }
+    let isRegistered: MessengerIsRegistered = .live(env)
+
+    XCTAssertFalse(try isRegistered())
+  }
+
+  func testWithoutE2E() {
+    let env: MessengerEnvironment = .unimplemented
+    env.ctx.e2e = nil
+    let isRegistered: MessengerIsRegistered = .live(env)
+
+    XCTAssertThrowsError(try isRegistered()) { err in
+      XCTAssertEqual(
+        err as? MessengerIsRegistered.Error,
+        MessengerIsRegistered.Error.notConnected
+      )
+    }
+  }
+
+  func testIsRegisteredWithUDFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.e2e = .unimplemented
+    env.ctx.e2e!.getId.run = { 1234 }
+    env.isRegisteredWithUD.run = { _ in throw error }
+    let isRegistered: MessengerIsRegistered = .live(env)
+
+    XCTAssertThrowsError(try isRegistered()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerLoadTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerLoadTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..ef5694c2a8cb0b47e227154bc1d54d6c06dceca8
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerLoadTests.swift
@@ -0,0 +1,77 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class MessengerLoadTests: XCTestCase {
+  func testLoad() throws {
+    struct DidLoadCMix: Equatable {
+      var storageDir: String
+      var password: Data
+      var cMixParamsJSON: Data
+    }
+    var didLoadCMix: [DidLoadCMix] = []
+
+    let storageDir = "test-storage-dir"
+    let password = "password".data(using: .utf8)!
+    let cMixParams = "cmix-params".data(using: .utf8)!
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = nil
+    env.storageDir = { storageDir }
+    env.passwordStorage.load = { password }
+    env.getCMixParams.run = { cMixParams }
+    env.loadCMix.run = { storageDir, password, cMixParamsJSON in
+      didLoadCMix.append(.init(
+        storageDir: storageDir,
+        password: password,
+        cMixParamsJSON: cMixParamsJSON
+      ))
+      return .unimplemented
+    }
+    let load: MessengerLoad = .live(env)
+
+    try load()
+
+    XCTAssertNoDifference(didLoadCMix, [
+      DidLoadCMix(
+        storageDir: storageDir,
+        password: password,
+        cMixParamsJSON: cMixParams
+      )
+    ])
+    XCTAssertNotNil(env.ctx.cMix)
+  }
+
+  func testMissingPassword() {
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = nil
+    env.storageDir = { "storage-dir" }
+    env.passwordStorage.load = { throw PasswordStorage.MissingPasswordError() }
+    let load: MessengerLoad = .live(env)
+
+    XCTAssertThrowsError(try load()) { err in
+      XCTAssertEqual(
+        err as? PasswordStorage.MissingPasswordError,
+        PasswordStorage.MissingPasswordError()
+      )
+    }
+  }
+
+  func testLoadCMixFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = nil
+    env.storageDir = { "storage-dir" }
+    env.passwordStorage.load = { "password".data(using: .utf8)! }
+    env.getCMixParams.run = { "cmix-params".data(using: .utf8)! }
+    env.loadCMix.run = { _, _, _ in throw error }
+    let load: MessengerLoad = .live(env)
+
+    XCTAssertThrowsError(try load()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerLogInTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerLogInTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..92e511f2de3212091e85bd2b6e184d83d0740645
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerLogInTests.swift
@@ -0,0 +1,171 @@
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+import CustomDump
+
+final class MessengerLogInTests: XCTestCase {
+  func testLogin() throws {
+    var didStartNetworkFollower: [Int] = []
+    var didNewOrLoadUDWithParams: [NewOrLoadUd.Params] = []
+    var didNewOrLoadUDWithFollower: [UdNetworkStatus] = []
+
+    let e2eId = 1234
+    let networkFollowerStatus: NetworkFollowerStatus = .stopped
+    let udCertFromNDF = "ndf-ud-cert".data(using: .utf8)!
+    let udContactFromNDF = "ndf-ud-contact".data(using: .utf8)!
+    let udAddressFromNDF = "ndf-ud-address"
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = .unimplemented
+    env.ctx.cMix!.networkFollowerStatus.run = { networkFollowerStatus }
+    env.ctx.cMix!.startNetworkFollower.run = { timeout in
+      didStartNetworkFollower.append(timeout)
+    }
+    env.ctx.e2e = .unimplemented
+    env.ctx.e2e!.getId.run = { e2eId }
+    env.udCert = { nil }
+    env.udContact = { nil }
+    env.udAddress = { nil }
+    env.ctx.e2e!.getUdCertFromNdf.run = { udCertFromNDF }
+    env.ctx.e2e!.getUdContactFromNdf.run = { udContactFromNDF }
+    env.ctx.e2e!.getUdAddressFromNdf.run = { udAddressFromNDF }
+    env.newOrLoadUd.run = { params, follower in
+      didNewOrLoadUDWithParams.append(params)
+      didNewOrLoadUDWithFollower.append(follower)
+      return .unimplemented
+    }
+    let logIn: MessengerLogIn = .live(env)
+    try logIn()
+
+    XCTAssertEqual(didStartNetworkFollower, [30_000])
+    XCTAssertNoDifference(didNewOrLoadUDWithParams, [.init(
+      e2eId: e2eId,
+      username: nil,
+      registrationValidationSignature: nil,
+      cert: udCertFromNDF,
+      contactFile: udContactFromNDF,
+      address: udAddressFromNDF
+    )])
+    XCTAssertEqual(didNewOrLoadUDWithFollower.count, 1)
+    XCTAssertEqual(
+      didNewOrLoadUDWithFollower.first?.handle(),
+      networkFollowerStatus.rawValue
+    )
+    XCTAssertNotNil(env.ctx.ud)
+  }
+
+  func testLoginWithAlternativeUD() throws {
+    var didNewOrLoadUDWithParams: [NewOrLoadUd.Params] = []
+    let e2eId = 1234
+    let altUdCert = "alt-ud-cert".data(using: .utf8)!
+    let altUdContact = "alt-ud-contact".data(using: .utf8)!
+    let altUdAddress = "alt-ud-address"
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = .unimplemented
+    env.ctx.cMix!.networkFollowerStatus.run = { .running }
+    env.ctx.e2e = .unimplemented
+    env.ctx.e2e!.getId.run = { e2eId }
+    env.udCert = { altUdCert }
+    env.udContact = { altUdContact }
+    env.udAddress = { altUdAddress }
+    env.newOrLoadUd.run = { params, _ in
+      didNewOrLoadUDWithParams.append(params)
+      return .unimplemented
+    }
+    let logIn: MessengerLogIn = .live(env)
+    try logIn()
+
+    XCTAssertNoDifference(didNewOrLoadUDWithParams, [.init(
+      e2eId: e2eId,
+      username: nil,
+      registrationValidationSignature: nil,
+      cert: altUdCert,
+      contactFile: altUdContact,
+      address: altUdAddress
+    )])
+    XCTAssertNotNil(env.ctx.ud)
+  }
+
+  func testLoginWithoutCMix() {
+    let env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = nil
+    let logIn: MessengerLogIn = .live(env)
+
+    XCTAssertThrowsError(try logIn()) { error in
+      XCTAssertEqual(
+        error as? MessengerLogIn.Error,
+        MessengerLogIn.Error.notLoaded
+      )
+    }
+  }
+
+  func testLoginWithoutE2E() {
+    let env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = .unimplemented
+    env.ctx.e2e = nil
+    let logIn: MessengerLogIn = .live(env)
+
+    XCTAssertThrowsError(try logIn()) { error in
+      XCTAssertEqual(
+        error as? MessengerLogIn.Error,
+        MessengerLogIn.Error.notConnected
+      )
+    }
+  }
+
+  func testStartNetworkFollowerFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    let env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = .unimplemented
+    env.ctx.cMix!.networkFollowerStatus.run = { .stopped }
+    env.ctx.cMix!.startNetworkFollower.run = { _ in throw error }
+    let logIn: MessengerLogIn = .live(env)
+
+    XCTAssertThrowsError(try logIn()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+
+  func testGetUdContactFromNdfFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = .unimplemented
+    env.ctx.cMix!.networkFollowerStatus.run = { .running }
+    env.ctx.e2e = .unimplemented
+    env.ctx.e2e!.getId.run = { 1234 }
+    env.udCert = { nil }
+    env.udContact = { nil }
+    env.ctx.e2e!.getUdCertFromNdf.run = { "ndf-ud-cert".data(using: .utf8)! }
+    env.ctx.e2e!.getUdContactFromNdf.run = { throw error }
+    let logIn: MessengerLogIn = .live(env)
+
+    XCTAssertThrowsError(try logIn()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+
+  func testNewOrLoadUdFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = .unimplemented
+    env.ctx.cMix!.networkFollowerStatus.run = { .running }
+    env.ctx.e2e = .unimplemented
+    env.ctx.e2e!.getId.run = { 1234 }
+    env.udCert = { "ud-cert".data(using: .utf8)! }
+    env.udContact = { "ud-contact".data(using: .utf8)! }
+    env.udAddress = { "ud-address" }
+    env.newOrLoadUd.run = { _, _ in throw error }
+    let logIn: MessengerLogIn = .live(env)
+
+    XCTAssertThrowsError(try logIn()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerRegisterTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerRegisterTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..deb6a8dcfd209e99404b092033e870d42e13305b
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerRegisterTests.swift
@@ -0,0 +1,188 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class MessengerRegisterTests: XCTestCase {
+  func testRegister() throws {
+    var didStartNetworkFollower: [Int] = []
+    var didNewOrLoadUDWithParams: [NewOrLoadUd.Params] = []
+    var didNewOrLoadUDWithFollower: [UdNetworkStatus] = []
+
+    let e2eId = 1234
+    let networkFollowerStatus: NetworkFollowerStatus = .stopped
+    let registrationSignature = "registration-signature".data(using: .utf8)!
+    let udCertFromNDF = "ndf-ud-cert".data(using: .utf8)!
+    let udContactFromNDF = "ndf-ud-contact".data(using: .utf8)!
+    let udAddressFromNDF = "ndf-ud-address"
+    let username = "new-user-name"
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = .unimplemented
+    env.ctx.cMix!.networkFollowerStatus.run = { networkFollowerStatus }
+    env.ctx.cMix!.startNetworkFollower.run = { timeout in
+      didStartNetworkFollower.append(timeout)
+    }
+    env.ctx.cMix!.getReceptionRegistrationValidationSignature.run = {
+      registrationSignature
+    }
+    env.ctx.e2e = .unimplemented
+    env.ctx.e2e!.getId.run = { e2eId }
+    env.udCert = { nil }
+    env.udContact = { nil }
+    env.udAddress = { nil }
+    env.ctx.e2e!.getUdCertFromNdf.run = { udCertFromNDF }
+    env.ctx.e2e!.getUdContactFromNdf.run = { udContactFromNDF }
+    env.ctx.e2e!.getUdAddressFromNdf.run = { udAddressFromNDF }
+    env.newOrLoadUd.run = { params, follower in
+      didNewOrLoadUDWithParams.append(params)
+      didNewOrLoadUDWithFollower.append(follower)
+      return .unimplemented
+    }
+    let register: MessengerRegister = .live(env)
+    try register(username: username)
+
+    XCTAssertEqual(didStartNetworkFollower, [30_000])
+    XCTAssertNoDifference(didNewOrLoadUDWithParams, [.init(
+      e2eId: e2eId,
+      username: username,
+      registrationValidationSignature: registrationSignature,
+      cert: udCertFromNDF,
+      contactFile: udContactFromNDF,
+      address: udAddressFromNDF
+    )])
+    XCTAssertEqual(didNewOrLoadUDWithFollower.count, 1)
+    XCTAssertEqual(
+      didNewOrLoadUDWithFollower.first?.handle(),
+      networkFollowerStatus.rawValue
+    )
+    XCTAssertNotNil(env.ctx.ud)
+  }
+
+  func testRegisterWithAlternativeUD() throws {
+    var didNewOrLoadUDWithParams: [NewOrLoadUd.Params] = []
+    let e2eId = 1234
+    let registrationSignature = "registration-signature".data(using: .utf8)!
+    let altUdCert = "alt-ud-cert".data(using: .utf8)!
+    let altUdContact = "alt-ud-contact".data(using: .utf8)!
+    let altUdAddress = "alt-ud-address"
+    let username = "new-user-name"
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = .unimplemented
+    env.ctx.cMix!.networkFollowerStatus.run = { .running }
+    env.ctx.cMix!.getReceptionRegistrationValidationSignature.run = {
+      registrationSignature
+    }
+    env.ctx.e2e = .unimplemented
+    env.ctx.e2e!.getId.run = { e2eId }
+    env.udCert = { altUdCert }
+    env.udContact = { altUdContact }
+    env.udAddress = { altUdAddress }
+    env.newOrLoadUd.run = { params, _ in
+      didNewOrLoadUDWithParams.append(params)
+      return .unimplemented
+    }
+    let register: MessengerRegister = .live(env)
+    try register(username: username)
+
+    XCTAssertNoDifference(didNewOrLoadUDWithParams, [.init(
+      e2eId: e2eId,
+      username: username,
+      registrationValidationSignature: registrationSignature,
+      cert: altUdCert,
+      contactFile: altUdContact,
+      address: altUdAddress
+    )])
+    XCTAssertNotNil(env.ctx.ud)
+  }
+
+  func testRegisterWithoutCMix() {
+    let env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = nil
+    let register: MessengerRegister = .live(env)
+
+    XCTAssertThrowsError(try register(username: "new-user-name")) { error in
+      XCTAssertEqual(
+        error as? MessengerRegister.Error,
+        MessengerRegister.Error.notLoaded
+      )
+    }
+  }
+
+  func testRegisterWithoutE2E() {
+    let env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = .unimplemented
+    env.ctx.e2e = nil
+    let register: MessengerRegister = .live(env)
+
+    XCTAssertThrowsError(try register(username: "new-user-name")) { error in
+      XCTAssertEqual(
+        error as? MessengerRegister.Error,
+        MessengerRegister.Error.notConnected
+      )
+    }
+  }
+
+  func testStartNetworkFollowerFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    let env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = .unimplemented
+    env.ctx.cMix!.networkFollowerStatus.run = { .stopped }
+    env.ctx.cMix!.startNetworkFollower.run = { _ in throw error }
+    env.ctx.e2e = .unimplemented
+    let register: MessengerRegister = .live(env)
+
+    XCTAssertThrowsError(try register(username: "new-user-name")) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+
+  func testGetUdContactFromNdfFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = .unimplemented
+    env.ctx.cMix!.networkFollowerStatus.run = { .running }
+    env.ctx.cMix!.getReceptionRegistrationValidationSignature.run = {
+      "registration-signature".data(using: .utf8)!
+    }
+    env.ctx.e2e = .unimplemented
+    env.ctx.e2e!.getId.run = { 1234 }
+    env.udCert = { nil }
+    env.udContact = { nil }
+    env.ctx.e2e!.getUdCertFromNdf.run = { "ndf-ud-cert".data(using: .utf8)! }
+    env.ctx.e2e!.getUdContactFromNdf.run = { throw error }
+    let register: MessengerRegister = .live(env)
+
+    XCTAssertThrowsError(try register(username: "new-user-name")) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+
+  func testNewOrLoadUdFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.cMix = .unimplemented
+    env.ctx.cMix!.networkFollowerStatus.run = { .running }
+    env.ctx.cMix!.getReceptionRegistrationValidationSignature.run = {
+      "registration-signature".data(using: .utf8)!
+    }
+    env.ctx.e2e = .unimplemented
+    env.ctx.e2e!.getId.run = { 1234 }
+    env.udCert = { "ud-cert".data(using: .utf8)! }
+    env.udContact = { "ud-contact".data(using: .utf8)! }
+    env.udAddress = { "ud-address" }
+    env.newOrLoadUd.run = { _, _ in throw error }
+    let register: MessengerRegister = .live(env)
+
+    XCTAssertThrowsError(try register(username: "new-user-name")) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+}
diff --git a/Tests/XXMessengerClientTests/XXMessengerClientTests.swift b/Tests/XXMessengerClientTests/XXMessengerClientTests.swift
deleted file mode 100644
index 11caac7a87166ce97667d9cefba7df1942839dd7..0000000000000000000000000000000000000000
--- a/Tests/XXMessengerClientTests/XXMessengerClientTests.swift
+++ /dev/null
@@ -1,8 +0,0 @@
-import XCTest
-@testable import XXMessengerClient
-
-final class XXMessengerClientTests: XCTestCase {
-  func testExample() {
-    XCTAssert(true)
-  }
-}