Skip to content
Snippets Groups Projects
Commit d826d7a0 authored by Dariusz Rybicki's avatar Dariusz Rybicki
Browse files

Implement messenger client

parent bf756200
No related branches found
No related tags found
2 merge requests!102Release 1.0.0,!33XXMessengerClient
Showing
with 816 additions and 1 deletion
......@@ -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
......
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()
)
}
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()
)
}
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()
)
}
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()
)
}
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()
)
}
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()
)
}
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()
)
}
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()
)
}
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()
)
}
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()
)
}
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
)
}
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?
}
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)
)
}
private enum Unimplemented {}
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")
)
}
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
}
)
}()
}
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)!
)
}
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)
}
}
}
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())
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment