From 4aaacc04d4e4707eaa8c8800b729d647d056374d Mon Sep 17 00:00:00 2001 From: Anton Kharchevskyi Date: Sun, 25 Jul 2021 22:20:59 +0300 Subject: [PATCH 1/7] Extract message validation and sending logic to ComposeMessageService --- FlowCrypt.xcodeproj/project.pbxproj | 8 +- .../Compose/ComposeViewController.swift | 209 +++++------------- .../Compose/ComposeViewControllerInput.swift | 82 ++++--- .../Compose/ComposeViewDecorator.swift | 22 +- .../Msg/MessageViewController.swift | 4 +- FlowCrypt/Core/CoreTypes.swift | 1 + FlowCrypt/Extensions/CombineExtensions.swift | 20 -- .../Backup Services/BackupService.swift | 13 +- .../Services/ComposeMessageService.swift | 176 +++++++++++++++ .../Services/EnterpriseServerApi.swift | 4 +- 10 files changed, 290 insertions(+), 249 deletions(-) delete mode 100644 FlowCrypt/Extensions/CombineExtensions.swift create mode 100644 FlowCrypt/Functionality/Services/ComposeMessageService.swift diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 89a7abfc6..405f713e4 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -105,7 +105,6 @@ 9F5C2A99257E94E900DE9B4B /* Gmail+MessageOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5C2A98257E94E900DE9B4B /* Gmail+MessageOperations.swift */; }; 9F6EE1552597399D0059BA51 /* BackupProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6EE1542597399D0059BA51 /* BackupProvider.swift */; }; 9F6EE17B2598F9FA0059BA51 /* Gmail+Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6EE17A2598F9FA0059BA51 /* Gmail+Backup.swift */; }; - 9F6F603026A5F9F900C625C7 /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6F602F26A5F9F900C625C7 /* CombineExtensions.swift */; }; 9F716308234FC73E0031645E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9F71630A234FC73E0031645E /* Localizable.strings */; }; 9F7920F52667CEF100DA3D80 /* PassPraseSaveable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7920F42667CEF100DA3D80 /* PassPraseSaveable.swift */; }; 9F79228826696B0200DA3D80 /* PassPhraseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F79228726696B0200DA3D80 /* PassPhraseService.swift */; }; @@ -117,6 +116,7 @@ 9F8220D526336626004B2009 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8220D426336626004B2009 /* Logger.swift */; }; 9F82779823737E0900E19C07 /* MessageViewDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F82779723737E0900E19C07 /* MessageViewDecorator.swift */; }; 9F82D352256D74FA0069A702 /* InboxViewContainerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F82D351256D74FA0069A702 /* InboxViewContainerController.swift */; }; + 9F88CFF026AB2F4200B2312E /* ComposeMessageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F88CFEF26AB2F4200B2312E /* ComposeMessageService.swift */; }; 9F92EE72236F165E009BE0D7 /* EncryptedStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F92EE71236F165E009BE0D7 /* EncryptedStorage.swift */; }; 9F9361A52573CE260009912F /* MessageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9361A42573CE260009912F /* MessageProvider.swift */; }; 9F9362062573D0C80009912F /* Gmail+MessagesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9362052573D0C80009912F /* Gmail+MessagesList.swift */; }; @@ -511,7 +511,6 @@ 9F696294236091F4003712E1 /* SignInDescriptionNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInDescriptionNode.swift; sourceTree = ""; }; 9F6EE1542597399D0059BA51 /* BackupProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupProvider.swift; sourceTree = ""; }; 9F6EE17A2598F9FA0059BA51 /* Gmail+Backup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Gmail+Backup.swift"; sourceTree = ""; }; - 9F6F602F26A5F9F900C625C7 /* CombineExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = ""; }; 9F716301234FC6950031645E /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/LaunchScreen.strings; sourceTree = ""; }; 9F716304234FC7200031645E /* LocalizationExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationExtensions.swift; sourceTree = ""; }; 9F716309234FC73E0031645E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; @@ -531,6 +530,7 @@ 9F82779B23737E2A00E19C07 /* MessageSubjectNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSubjectNode.swift; sourceTree = ""; }; 9F82779D23737E3800E19C07 /* MessageTextSubjectNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTextSubjectNode.swift; sourceTree = ""; }; 9F82D351256D74FA0069A702 /* InboxViewContainerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxViewContainerController.swift; sourceTree = ""; }; + 9F88CFEF26AB2F4200B2312E /* ComposeMessageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMessageService.swift; sourceTree = ""; }; 9F8D5E61236B04E300186E43 /* CellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellNode.swift; sourceTree = ""; }; 9F92EE6F236F144C009BE0D7 /* TextFieldCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldCellNode.swift; sourceTree = ""; }; 9F92EE71236F165E009BE0D7 /* EncryptedStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedStorage.swift; sourceTree = ""; }; @@ -924,7 +924,6 @@ 9F31AB9D232BF2A600CF87EA /* UIColorExtension.swift */, D2D27B78248A8694007346FA /* BigIntExtension.swift */, 21C7DF0426697DA500C44800 /* PromiseKitExtension.swift */, - 9F6F602F26A5F9F900C625C7 /* CombineExtensions.swift */, ); path = Extensions; sourceTree = ""; @@ -1015,6 +1014,7 @@ D227C0E4250538190070F805 /* Folders Services */, D27B911724EFE787002DF0A1 /* Contacts Services */, 21489B81267CC3BC00BDE4AC /* Organisational Rules Service */, + 9F88CFEF26AB2F4200B2312E /* ComposeMessageService.swift */, ); path = Services; sourceTree = ""; @@ -2466,7 +2466,6 @@ C132B9D91EC30E1D00763715 /* InboxViewController.swift in Sources */, 9F56BD2C23438A8500A7371A /* Imap+messages.swift in Sources */, C132B9CB1EC2DE6400763715 /* GeneralConstants.swift in Sources */, - 9F6F603026A5F9F900C625C7 /* CombineExtensions.swift in Sources */, 5ADEDCBE23A4363700EC495E /* KeyDetailInfoViewController.swift in Sources */, D20D3C752520AB9A00D4AA9A /* BackupService.swift in Sources */, C192421F1EC48B6900C3D251 /* SetupBackupsViewController.swift in Sources */, @@ -2517,6 +2516,7 @@ 9FB22CDD25715CF50026EE64 /* GmailServiceError.swift in Sources */, 5ADEDCB923A42B9400EC495E /* KeyDetailViewDecorator.swift in Sources */, 9F416428266575DC00106194 /* BackupServiceType.swift in Sources */, + 9F88CFF026AB2F4200B2312E /* ComposeMessageService.swift in Sources */, 9F7E5137267AA51B00CE37C3 /* AlertsFactory.swift in Sources */, 5A39F437239ECC23001F4607 /* KeySettingsViewController.swift in Sources */, 9FF0673325520DE400FCC9E6 /* GmailService+send.swift in Sources */, diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index a03d2b742..9c3eb96b3 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -12,26 +12,9 @@ import FlowCryptUI * - User can be redirected here from *InboxViewController* by tapping on *+* * - Or from *MessageViewController* controller by tapping on *reply* */ -final class ComposeViewController: TableNodeViewController { - struct Recipient { - let email: String - var state: RecipientState - - init( - email: String, - state: RecipientState - ) { - self.email = email - self.state = state - } - } - - private struct Context { - var message: String? - var recipients: [Recipient] = [] - var subject: String? - } +// TODO: - ANTON check services which can be removed +final class ComposeViewController: TableNodeViewController { private enum Constants { static let endTypingCharacters = [",", " ", "\n", ";"] static let shouldShowScopeAlertIndex = "indexShould_ShowScope" @@ -50,10 +33,9 @@ final class ComposeViewController: TableNodeViewController { } private var cancellable = Set() - private let messageSender: MessageGateway private let notificationCenter: NotificationCenter private let dataService: KeyStorageType - private let decorator: ComposeViewDecoratorType + private let decorator: ComposeViewDecorator private let core: Core private let contactsService: ContactsServiceType @@ -61,26 +43,26 @@ final class ComposeViewController: TableNodeViewController { private let cloudContactProvider: CloudContactsProvider private let userDefaults: UserDefaults - private var input: Input - private var contextToSend = Context() + private var input: ComposeMessageInput + private var contextToSend = ComposeMessageContext() private var state: State = .main private let email: String + private let composeMessageService: ComposeMessageService init( email: String, - messageSender: MessageGateway = MailProvider.shared.messageSender, notificationCenter: NotificationCenter = .default, dataService: KeyStorageType = KeyDataStorage(), - decorator: ComposeViewDecoratorType = ComposeViewDecorator(), - input: ComposeViewController.Input = .empty, + decorator: ComposeViewDecorator = ComposeViewDecorator(), + input: ComposeMessageInput = .empty, core: Core = Core.shared, cloudContactProvider: CloudContactsProvider = UserContactsProvider(), userDefaults: UserDefaults = .standard, - contactsService: ContactsServiceType = ContactsService() + contactsService: ContactsServiceType = ContactsService(), + composeMessageService: ComposeMessageService = ComposeMessageService() ) { self.email = email - self.messageSender = messageSender self.notificationCenter = notificationCenter self.dataService = dataService self.input = input @@ -89,10 +71,11 @@ final class ComposeViewController: TableNodeViewController { self.cloudContactProvider = cloudContactProvider self.userDefaults = userDefaults self.contactsService = contactsService + self.composeMessageService = composeMessageService contextToSend.subject = input.subject if input.isReply { if let email = input.recipientReplyTitle { - contextToSend.recipients.append(Recipient(email: email, state: decorator.recipientIdleState)) + contextToSend.recipients.append(ComposeMessageRecipient(email: email, state: decorator.recipientIdleState)) } } super.init(node: TableNode()) @@ -219,79 +202,44 @@ extension ComposeViewController { private func sendMessage() { view.endEditing(true) - guard isInputValid() else { return } - - showSpinner("sending_title".localized) - - encryptAndSendMessage() - } - - // TODO: - Refactor send message checks. https://github.com/FlowCrypt/flowcrypt-ios/issues/399 - private func encryptAndSendMessage() { - let recipients = contextToSend.recipients - - guard recipients.isNotEmpty else { - showAlert(message: "Recipients should not be empty. Fail in checking") - return - } - - guard let text = self.contextToSend.message else { - showAlert(message: "Text and Email should not be nil at this point. Fail in checking") - return - } - - let subject = self.input.subjectReplyTitle - ?? self.contextToSend.subject - ?? "(no subject)" - - guard let myPubKey = self.dataService.publicKey() else { - self.showAlert(message: "compose_no_pub_sender".localized) - return - } - - guard let allRecipientPubs = getPubKeys(for: recipients) else { - showAlert(message: "Public key is missin") - return - } - - showSpinner() - - let encrypted = self.encryptMsg( - pubkeys: allRecipientPubs + [myPubKey], - subject: subject, - message: text, - to: recipients.map(\.email) + let result = composeMessageService.validateMessageInput( + with: recipients, + input: input, + contextToSend: contextToSend, + email: email, + atts: [] ) - messageSender - .sendMail(mime: encrypted.mimeEncoded) - .sink( - receiveCompletion: { [weak self] result in - guard let error = result.getError() else { - return - } - - self?.showAlert(error: error, message: "compose_error".localized) - }, - receiveValue: { [weak self] in - self?.handleSuccessfullySentMessage() - }) - .store(in: &cancellable) - } - - private func getPubKeys(for recepients: [Recipient]) -> [String]? { - let pubKeys = recipients.map { - ($0.email, contactsService.retrievePubKey(for: $0.email)) - } - - let emailsWithoutPubKeys = pubKeys.filter { $0.1 == nil }.map(\.0) - - guard emailsWithoutPubKeys.isEmpty else { - showNoPubKeyAlert(for: emailsWithoutPubKeys) - return nil - } - - return pubKeys.compactMap(\.1) + switch result { + case .success(let sendableMessage): + showSpinner("sending_title".localized) + + composeMessageService + .encryptAndSend(message: sendableMessage) + .sink( + receiveCompletion: { [weak self] result in + guard case .failure(let error) = result else { + return + } + self?.handle(error: error) + }, + receiveValue: { [weak self] in + self?.handleSuccessfullySentMessage() + }) + .store(in: &cancellable) + case .failure(let error): + handle(error: error) + } + } + + private func handle(error: ComposeMessageError) { + hideSpinner() + + let message = "compose_error".localized + + "\n\n" + + error.description + + showAlert(error: error, message: message) } private func showNoPubKeyAlert(for emails: [String]) { @@ -306,55 +254,6 @@ extension ComposeViewController { showToast(input.successfullySentToast) navigationController?.popViewController(animated: true) } - - private func encryptMsg( - pubkeys: [String], - subject: String, - message: String, - to: [String], - cc: [String] = [], - bcc: [String] = [], - atts: [SendableMsg.Attachment] = [] - ) -> CoreRes.ComposeEmail { - let replyToMimeMsg = input.replyToMime - .flatMap { String(data: $0, encoding: .utf8) } - let msg = SendableMsg( - text: message, - to: to, - cc: cc, - bcc: bcc, - from: email, - subject: subject, - replyToMimeMsg: replyToMimeMsg, - atts: atts - ) - - do { - return try core.composeEmail(msg: msg, fmt: MsgFmt.encryptInline, pubKeys: pubkeys) - } catch { - fatalError() - } - } - - private func isInputValid() -> Bool { - let emails = recipients.map(\.email) - let hasContent = emails.filter { $0.hasContent } - - guard emails.count == hasContent.count else { - showAlert(message: "compose_enter_recipient".localized) - return false - } - guard input.isReply || contextToSend.subject?.hasContent ?? false else { - showAlert(message: "compose_enter_subject".localized) - return false - } - - guard contextToSend.message?.hasContent ?? false else { - showAlert(message: "compose_enter_secure".localized) - return false - } - return true - } } // MARK: - ASTableDelegate, ASTableDataSource @@ -505,7 +404,7 @@ extension ComposeViewController { IndexPath(row: RecipientParts.recipient.rawValue, section: 0) } - private var recipients: [ComposeViewController.Recipient] { + private var recipients: [ComposeMessageRecipient] { contextToSend.recipients } @@ -561,7 +460,7 @@ extension ComposeViewController { return recipient } - let newRecipient = Recipient(email: text, state: decorator.recipientIdleState) + let newRecipient = ComposeMessageRecipient(email: text, state: decorator.recipientIdleState) let indexOfRecipient: Int if let index = contextToSend.recipients.firstIndex(where: { $0.email == newRecipient.email }) { @@ -648,7 +547,7 @@ extension ComposeViewController { } } - private func evaluate(recipient: Recipient) { + private func evaluate(recipient: ComposeMessageRecipient) { guard isValid(email: recipient.email) else { updateRecipientWithNew(state: self.decorator.recipientErrorState, for: .left(recipient)) return @@ -663,14 +562,14 @@ extension ComposeViewController { } } - private func handleEvaluation(for recipient: Recipient) { + private func handleEvaluation(for recipient: ComposeMessageRecipient) { updateRecipientWithNew( state: decorator.recipientKeyFoundState, for: .left(recipient) ) } - private func handleEvaluation(error: Error, with recipient: Recipient) { + private func handleEvaluation(error: Error, with recipient: ComposeMessageRecipient) { let recipientState: RecipientState = { switch error { case ContactsError.keyMissing: @@ -686,7 +585,7 @@ extension ComposeViewController { ) } - private func updateRecipientWithNew(state: RecipientState, for context: Either) { + private func updateRecipientWithNew(state: RecipientState, for context: Either) { let index: Int? = { switch context { case let .left(recipient): diff --git a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift index c02a537e7..fc35cf992 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewControllerInput.swift @@ -8,57 +8,55 @@ import Foundation -extension ComposeViewController { - struct Input { - static let empty = Input(type: .idle) - - struct ReplyInfo: Equatable { - let recipient: String? - let subject: String? - let mime: Data? - let sentDate: Date - let message: String - } +struct ComposeMessageInput { + static let empty = ComposeMessageInput(type: .idle) + + struct ReplyInfo: Equatable { + let recipient: String? + let subject: String? + let mime: Data? + let sentDate: Date + let message: String + } - enum InputType: Equatable { - case idle - case reply(ReplyInfo) - } + enum InputType: Equatable { + case idle + case reply(ReplyInfo) + } - let type: InputType + let type: InputType - var isReply: Bool { - switch type { - case .idle: return false - case .reply: return true - } + var isReply: Bool { + switch type { + case .idle: return false + case .reply: return true } + } - var recipientReplyTitle: String? { - guard case let .reply(info) = type else { return nil } - return info.recipient - } + var recipientReplyTitle: String? { + guard case let .reply(info) = type else { return nil } + return info.recipient + } - var subjectReplyTitle: String? { - guard case let .reply(info) = type else { return nil } - return "Re: \(info.subject ?? "(no subject)")" - } + var subjectReplyTitle: String? { + guard case let .reply(info) = type else { return nil } + return "Re: \(info.subject ?? "(no subject)")" + } - var successfullySentToast: String { - switch type { - case .idle: return "compose_sent".localized - case .reply: return "compose_reply_successfull".localized - } + var successfullySentToast: String { + switch type { + case .idle: return "compose_sent".localized + case .reply: return "compose_reply_successfull".localized } + } - var subject: String? { - guard case let .reply(info) = type else { return nil } - return info.subject - } + var subject: String? { + guard case let .reply(info) = type else { return nil } + return info.subject + } - var replyToMime: Data? { - guard case let .reply(info) = type else { return nil } - return info.mime - } + var replyToMime: Data? { + guard case let .reply(info) = type else { return nil } + return info.mime } } diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index be33d2138..b3ce4d40b 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -9,23 +9,7 @@ import FlowCryptUI import UIKit -protocol ComposeViewDecoratorType { - var recipientIdleState: RecipientState { get } - var recipientSelectedState: RecipientState { get } - var recipientKeyFoundState: RecipientState { get } - var recipientKeyNotFoundState: RecipientState { get } - var recipientErrorState: RecipientState { get } - var recipientErrorStateRetry: RecipientState { get } - - func styledTextViewInput(with height: CGFloat) -> TextViewCellNode.Input - func styledTextFieldInput(with text: String) -> TextFieldCellNode.Input - func styledRecipientInfo(with email: String) -> InfoCellNode.Input - func styledTitle(with text: String?) -> NSAttributedString? - func styledReplyQuote(with input: ComposeViewController.Input) -> NSAttributedString -} - -// MARK: - ComposeViewDecorator -struct ComposeViewDecorator: ComposeViewDecoratorType { +struct ComposeViewDecorator { let recipientIdleState: RecipientState = .idle(idleStateContext) let recipientSelectedState: RecipientState = .selected(selectedStateContext) let recipientKeyFoundState: RecipientState = .keyFound(keyFoundStateContext) @@ -77,7 +61,7 @@ struct ComposeViewDecorator: ComposeViewDecoratorType { ) } - func styledReplyQuote(with input: ComposeViewController.Input) -> NSAttributedString { + func styledReplyQuote(with input: ComposeMessageInput) -> NSAttributedString { guard case let .reply(info) = input.type else { return NSAttributedString(string: "") } let dateFormatter = DateFormatter() @@ -192,7 +176,7 @@ extension ComposeViewDecorator { // MARK: - RecipientEmailsCellNode.Input extension RecipientEmailsCellNode.Input { - init(_ recipient: ComposeViewController.Recipient) { + init(_ recipient: ComposeMessageRecipient) { self.init( email: recipient.email.lowercased().attributed( .regular(17), diff --git a/FlowCrypt/Controllers/Msg/MessageViewController.swift b/FlowCrypt/Controllers/Msg/MessageViewController.swift index bb731b789..25b9c5c12 100644 --- a/FlowCrypt/Controllers/Msg/MessageViewController.swift +++ b/FlowCrypt/Controllers/Msg/MessageViewController.swift @@ -347,7 +347,7 @@ extension MessageViewController { private func handleReplyTap() { guard let input = input, let email = DataService.shared.email else { return } - let replyInfo = ComposeViewController.Input.ReplyInfo( + let replyInfo = ComposeMessageInput.ReplyInfo( recipient: input.objMessage.sender, subject: input.objMessage.subject, mime: processedMessage.rawMimeData, @@ -355,7 +355,7 @@ extension MessageViewController { message: processedMessage.text ) - let composeInput = ComposeViewController.Input(type: .reply(replyInfo)) + let composeInput = ComposeMessageInput(type: .reply(replyInfo)) navigationController?.pushViewController( ComposeViewController(email: email, input: composeInput), animated: true diff --git a/FlowCrypt/Core/CoreTypes.swift b/FlowCrypt/Core/CoreTypes.swift index 20d12e14e..d0d34201c 100644 --- a/FlowCrypt/Core/CoreTypes.swift +++ b/FlowCrypt/Core/CoreTypes.swift @@ -120,6 +120,7 @@ struct SendableMsg { let subject: String let replyToMimeMsg: String? let atts: [Attachment] + let pubKeys: [String]? } struct MsgBlock: Decodable { diff --git a/FlowCrypt/Extensions/CombineExtensions.swift b/FlowCrypt/Extensions/CombineExtensions.swift deleted file mode 100644 index 45430eec8..000000000 --- a/FlowCrypt/Extensions/CombineExtensions.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// CombineExtensions.swift -// FlowCrypt -// -// Created by Anton Kharchevskyi on 19.07.2021. -// Copyright © 2021 FlowCrypt Limited. All rights reserved. -// - -import Combine - -extension Subscribers.Completion { - func getError() -> Error? { - switch self { - case .failure(let error): - return error - case .finished: - return nil - } - } -} diff --git a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift index 49af60786..cac38b64f 100644 --- a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift +++ b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift @@ -71,18 +71,21 @@ extension BackupService: BackupServiceType { from: userId.toMime, subject: "Your FlowCrypt Backup", replyToMimeMsg: nil, - atts: attachments + atts: attachments, + pubKeys: nil ) - let backupEmail = try self.core.composeEmail(msg: message, fmt: .plain, pubKeys: nil) + let backupEmail = try self.core.composeEmail(msg: message, fmt: .plain, pubKeys: message.pubKeys) self.messageSender .sendMail(mime: backupEmail.mimeEncoded) .sink( receiveCompletion: { result in - guard let error = result.getError() else { - return + switch result { + case .failure(let error): + reject(error) + case .finished: + break } - reject(error) }, receiveValue: { resolve(()) diff --git a/FlowCrypt/Functionality/Services/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/ComposeMessageService.swift new file mode 100644 index 000000000..e1f05fa73 --- /dev/null +++ b/FlowCrypt/Functionality/Services/ComposeMessageService.swift @@ -0,0 +1,176 @@ +// +// ComposeMessageService.swift +// FlowCrypt +// +// Created by Anton Kharchevskyi on 23.07.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Combine +import FlowCryptUI +import Foundation + +struct ComposeMessageContext { + var message: String? + var recipients: [ComposeMessageRecipient] = [] + var subject: String? +} + +struct ComposeMessageRecipient { + let email: String + var state: RecipientState +} + +enum ComposeMessageError: Error, CustomStringConvertible { + case validationError(MessageValidationError) + case gatewayError(Error) + + // TODO: - ANTON - add proper description + var description: String { + switch self { + case .validationError(let messageValidationError): + return "" + case .gatewayError(let error): + return "" + } + } +} + +enum MessageValidationError: Error { + // showAlert(message: "compose_enter_recipient".localized) + case emptyRecipient + // showAlert(message: "compose_enter_subject".localized) + case emptySubject + // showAlert(message: "compose_enter_secure".localized) + case emptyMessage + + // self.showAlert(message: "compose_no_pub_sender".localized) + case missedPublicKey + + // showAlert(message: "Public key is missing") + case missedRecipientPublicKey + + // showAlert(message: "Recipients should not be empty. Fail in checking") + case internalError(String) +} + +final class ComposeMessageService { + private let messageGateway: MessageGateway + private let dataService: KeyStorageType + private let contactsService: ContactsServiceType + private let core: Core + + init( + messageGateway: MessageGateway = MailProvider.shared.messageSender, + dataService: KeyStorageType = KeyDataStorage(), + contactsService: ContactsServiceType = ContactsService(), + core: Core = Core.shared + ) { + self.messageGateway = messageGateway + self.dataService = dataService + self.contactsService = contactsService + self.core = core + } + + func validateMessageInput( + with recipients: [ComposeMessageRecipient], + input: ComposeMessageInput, + contextToSend: ComposeMessageContext, + email: String, + atts: [SendableMsg.Attachment] + ) -> Result { + let emails = recipients.map(\.email) + let hasContent = emails.filter { $0.hasContent } + + guard emails.count == hasContent.count else { + return .failure(.validationError(.emptyRecipient)) + } + + guard input.isReply || contextToSend.subject?.hasContent ?? false else { + return .failure(.validationError(.emptySubject)) + } + + guard let text = contextToSend.message, text.hasContent else { + return .failure(.validationError(.emptyMessage)) + } + + let recipients = contextToSend.recipients + + guard recipients.isNotEmpty else { + return .failure(.validationError(.internalError("Recipients should not be empty. Fail in checking"))) + } + + let subject = input.subjectReplyTitle + ?? contextToSend.subject + ?? "(no subject)" + + guard let myPubKey = self.dataService.publicKey() else { + return .failure(.validationError(.missedPublicKey)) + } + + guard let allRecipientPubs = getPubKeys(for: recipients) else { + return .failure(.validationError(.missedRecipientPublicKey)) + } + + let replyToMimeMsg = input.replyToMime + .flatMap { String(data: $0, encoding: .utf8) } + + let msg = SendableMsg( + text: text, + to: recipients.map(\.email), + cc: [], + bcc: [], + from: email, + subject: subject, + replyToMimeMsg: replyToMimeMsg, + atts: atts, + pubKeys: allRecipientPubs + [myPubKey] + ) + + return .success(msg) + } + + private func getPubKeys(for recepients: [ComposeMessageRecipient]) -> [String]? { + let pubKeys = recepients.map { + ($0.email, contactsService.retrievePubKey(for: $0.email)) + } + + let emailsWithoutPubKeys = pubKeys.filter { $0.1 == nil }.map(\.0) + + guard emailsWithoutPubKeys.isEmpty else { + // TODO: - ANTON - return error +// showNoPubKeyAlert(for: emailsWithoutPubKeys) + return nil + } + + return pubKeys.compactMap(\.1) + } + + func encryptAndSend(message: SendableMsg) -> AnyPublisher { + messageGateway.sendMail(mime: encryptMessage(with: message)) + .mapError { ComposeMessageError.gatewayError($0) } + .eraseToAnyPublisher() + } + + private func encryptMessage(with msg: SendableMsg) -> Data { + do { + return try core.composeEmail(msg: msg, fmt: MsgFmt.encryptInline, pubKeys: msg.pubKeys).mimeEncoded + } catch { + fatalError() + } + } +} + +struct ComposedMessage { + let email: String + let pubkeys: [String] + let subject: String + let message: String + let to: [String] + let cc: [String] + let bcc: [String] + let atts: [SendableMsg.Attachment] +} + +// TODO: - ANTON +// add tests for ComposeMessageService diff --git a/FlowCrypt/Functionality/Services/EnterpriseServerApi.swift b/FlowCrypt/Functionality/Services/EnterpriseServerApi.swift index d2fdb4e5c..f1a8ce626 100644 --- a/FlowCrypt/Functionality/Services/EnterpriseServerApi.swift +++ b/FlowCrypt/Functionality/Services/EnterpriseServerApi.swift @@ -42,7 +42,7 @@ class EnterpriseServerApi: EnterpriseServerApiType { let clientConfiguration: ClientConfiguration private enum CodingKeys: String, CodingKey { - case clientConfiguration = "clientConfiguration" + case clientConfiguration } } @@ -118,7 +118,7 @@ class EnterpriseServerApi: EnterpriseServerApiType { func getClientConfigurationForCurrentUser() -> Promise { guard let email = DataService.shared.currentUser?.email else { - return Promise { _, reject in + return Promise { _, _ in fatalError("User has to be set while getting client configuration") } } From 75291f1519621d89caeee4dfaae5b4c8dc11b874 Mon Sep 17 00:00:00 2001 From: Anton Kharchevskyi Date: Sun, 25 Jul 2021 22:50:06 +0300 Subject: [PATCH 2/7] Work on ComposeMessageService --- FlowCrypt.xcodeproj/project.pbxproj | 20 +++- .../Compose/ComposeViewController.swift | 58 ++++++------ .../ComposeMessageError.swift | 56 +++++++++++ .../ComposeMessageService.swift | 93 +++++-------------- 4 files changed, 124 insertions(+), 103 deletions(-) create mode 100644 FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift rename FlowCrypt/Functionality/Services/{ => Compose Message Service}/ComposeMessageService.swift (62%) diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 405f713e4..69e730f4f 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -105,6 +105,8 @@ 9F5C2A99257E94E900DE9B4B /* Gmail+MessageOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5C2A98257E94E900DE9B4B /* Gmail+MessageOperations.swift */; }; 9F6EE1552597399D0059BA51 /* BackupProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6EE1542597399D0059BA51 /* BackupProvider.swift */; }; 9F6EE17B2598F9FA0059BA51 /* Gmail+Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6EE17A2598F9FA0059BA51 /* Gmail+Backup.swift */; }; + 9F6F3BEE26ADF5DE005BD9C6 /* ComposeMessageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6F3BEC26ADF5DE005BD9C6 /* ComposeMessageService.swift */; }; + 9F6F3BEF26ADF5DE005BD9C6 /* ComposeMessageError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6F3BED26ADF5DE005BD9C6 /* ComposeMessageError.swift */; }; 9F716308234FC73E0031645E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9F71630A234FC73E0031645E /* Localizable.strings */; }; 9F7920F52667CEF100DA3D80 /* PassPraseSaveable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7920F42667CEF100DA3D80 /* PassPraseSaveable.swift */; }; 9F79228826696B0200DA3D80 /* PassPhraseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F79228726696B0200DA3D80 /* PassPhraseService.swift */; }; @@ -116,7 +118,6 @@ 9F8220D526336626004B2009 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8220D426336626004B2009 /* Logger.swift */; }; 9F82779823737E0900E19C07 /* MessageViewDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F82779723737E0900E19C07 /* MessageViewDecorator.swift */; }; 9F82D352256D74FA0069A702 /* InboxViewContainerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F82D351256D74FA0069A702 /* InboxViewContainerController.swift */; }; - 9F88CFF026AB2F4200B2312E /* ComposeMessageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F88CFEF26AB2F4200B2312E /* ComposeMessageService.swift */; }; 9F92EE72236F165E009BE0D7 /* EncryptedStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F92EE71236F165E009BE0D7 /* EncryptedStorage.swift */; }; 9F9361A52573CE260009912F /* MessageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9361A42573CE260009912F /* MessageProvider.swift */; }; 9F9362062573D0C80009912F /* Gmail+MessagesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9362052573D0C80009912F /* Gmail+MessagesList.swift */; }; @@ -511,6 +512,8 @@ 9F696294236091F4003712E1 /* SignInDescriptionNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInDescriptionNode.swift; sourceTree = ""; }; 9F6EE1542597399D0059BA51 /* BackupProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupProvider.swift; sourceTree = ""; }; 9F6EE17A2598F9FA0059BA51 /* Gmail+Backup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Gmail+Backup.swift"; sourceTree = ""; }; + 9F6F3BEC26ADF5DE005BD9C6 /* ComposeMessageService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposeMessageService.swift; sourceTree = ""; }; + 9F6F3BED26ADF5DE005BD9C6 /* ComposeMessageError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposeMessageError.swift; sourceTree = ""; }; 9F716301234FC6950031645E /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/LaunchScreen.strings; sourceTree = ""; }; 9F716304234FC7200031645E /* LocalizationExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationExtensions.swift; sourceTree = ""; }; 9F716309234FC73E0031645E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; @@ -530,7 +533,6 @@ 9F82779B23737E2A00E19C07 /* MessageSubjectNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSubjectNode.swift; sourceTree = ""; }; 9F82779D23737E3800E19C07 /* MessageTextSubjectNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTextSubjectNode.swift; sourceTree = ""; }; 9F82D351256D74FA0069A702 /* InboxViewContainerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxViewContainerController.swift; sourceTree = ""; }; - 9F88CFEF26AB2F4200B2312E /* ComposeMessageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMessageService.swift; sourceTree = ""; }; 9F8D5E61236B04E300186E43 /* CellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellNode.swift; sourceTree = ""; }; 9F92EE6F236F144C009BE0D7 /* TextFieldCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldCellNode.swift; sourceTree = ""; }; 9F92EE71236F165E009BE0D7 /* EncryptedStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedStorage.swift; sourceTree = ""; }; @@ -1009,12 +1011,12 @@ 214A023926A3029700C24066 /* EmailKeyManagerApi.swift */, D274724024F97C5C006BA6EF /* CacheService.swift */, C132B9CA1EC2DE6400763715 /* GeneralConstants.swift */, + 9F6F3BEB26ADF5DE005BD9C6 /* Compose Message Service */, 9FB22CFD25715DDF0026EE64 /* Key Services */, 9F41FA1C25372C2D003B970D /* Backup Services */, D227C0E4250538190070F805 /* Folders Services */, D27B911724EFE787002DF0A1 /* Contacts Services */, 21489B81267CC3BC00BDE4AC /* Organisational Rules Service */, - 9F88CFEF26AB2F4200B2312E /* ComposeMessageService.swift */, ); path = Services; sourceTree = ""; @@ -1224,6 +1226,15 @@ path = "Backup Provider"; sourceTree = ""; }; + 9F6F3BEB26ADF5DE005BD9C6 /* Compose Message Service */ = { + isa = PBXGroup; + children = ( + 9F6F3BEC26ADF5DE005BD9C6 /* ComposeMessageService.swift */, + 9F6F3BED26ADF5DE005BD9C6 /* ComposeMessageError.swift */, + ); + path = "Compose Message Service"; + sourceTree = ""; + }; 9F7E902726A1AD280021C07F /* Core */ = { isa = PBXGroup; children = ( @@ -2465,6 +2476,7 @@ 9F31AB91232993F500CF87EA /* Imap+session.swift in Sources */, C132B9D91EC30E1D00763715 /* InboxViewController.swift in Sources */, 9F56BD2C23438A8500A7371A /* Imap+messages.swift in Sources */, + 9F6F3BEE26ADF5DE005BD9C6 /* ComposeMessageService.swift in Sources */, C132B9CB1EC2DE6400763715 /* GeneralConstants.swift in Sources */, 5ADEDCBE23A4363700EC495E /* KeyDetailInfoViewController.swift in Sources */, D20D3C752520AB9A00D4AA9A /* BackupService.swift in Sources */, @@ -2516,7 +2528,6 @@ 9FB22CDD25715CF50026EE64 /* GmailServiceError.swift in Sources */, 5ADEDCB923A42B9400EC495E /* KeyDetailViewDecorator.swift in Sources */, 9F416428266575DC00106194 /* BackupServiceType.swift in Sources */, - 9F88CFF026AB2F4200B2312E /* ComposeMessageService.swift in Sources */, 9F7E5137267AA51B00CE37C3 /* AlertsFactory.swift in Sources */, 5A39F437239ECC23001F4607 /* KeySettingsViewController.swift in Sources */, 9FF0673325520DE400FCC9E6 /* GmailService+send.swift in Sources */, @@ -2535,6 +2546,7 @@ 9F23EA50237217140017DFED /* ComposeViewDecorator.swift in Sources */, D21574B724376852006B094F /* ConnectionType.swift in Sources */, 9F7920F52667CEF100DA3D80 /* PassPraseSaveable.swift in Sources */, + 9F6F3BEF26ADF5DE005BD9C6 /* ComposeMessageError.swift in Sources */, 5ADEDCAF23A3EA9E00EC495E /* KeySettingsViewDecorator.swift in Sources */, 9F3EF33123B1785600FA0CEF /* MsgListViewConroller.swift in Sources */, 9F31ABA0232C071700CF87EA /* GlobalRouter.swift in Sources */, diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 9c3eb96b3..1b5e38f27 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -12,8 +12,6 @@ import FlowCryptUI * - User can be redirected here from *InboxViewController* by tapping on *+* * - Or from *MessageViewController* controller by tapping on *reply* */ - -// TODO: - ANTON check services which can be removed final class ComposeViewController: TableNodeViewController { private enum Constants { static let endTypingCharacters = [",", " ", "\n", ";"] @@ -32,31 +30,28 @@ final class ComposeViewController: TableNodeViewController { case subject, subjectDivider, text } - private var cancellable = Set() + private let composeMessageService: ComposeMessageService private let notificationCenter: NotificationCenter - private let dataService: KeyStorageType private let decorator: ComposeViewDecorator - private let core: Core private let contactsService: ContactsServiceType private let searchThrottler = Throttler(seconds: 1) private let cloudContactProvider: CloudContactsProvider private let userDefaults: UserDefaults + private let email: String + + private var cancellable = Set() private var input: ComposeMessageInput private var contextToSend = ComposeMessageContext() private var state: State = .main - private let email: String - private let composeMessageService: ComposeMessageService init( email: String, notificationCenter: NotificationCenter = .default, - dataService: KeyStorageType = KeyDataStorage(), decorator: ComposeViewDecorator = ComposeViewDecorator(), input: ComposeMessageInput = .empty, - core: Core = Core.shared, cloudContactProvider: CloudContactsProvider = UserContactsProvider(), userDefaults: UserDefaults = .standard, contactsService: ContactsServiceType = ContactsService(), @@ -64,15 +59,13 @@ final class ComposeViewController: TableNodeViewController { ) { self.email = email self.notificationCenter = notificationCenter - self.dataService = dataService self.input = input self.decorator = decorator - self.core = core self.cloudContactProvider = cloudContactProvider self.userDefaults = userDefaults self.contactsService = contactsService self.composeMessageService = composeMessageService - contextToSend.subject = input.subject + self.contextToSend.subject = input.subject if input.isReply { if let email = input.recipientReplyTitle { contextToSend.recipients.append(ComposeMessageRecipient(email: email, state: decorator.recipientIdleState)) @@ -212,36 +205,40 @@ extension ComposeViewController { switch result { case .success(let sendableMessage): - showSpinner("sending_title".localized) - - composeMessageService - .encryptAndSend(message: sendableMessage) - .sink( - receiveCompletion: { [weak self] result in - guard case .failure(let error) = result else { - return - } - self?.handle(error: error) - }, - receiveValue: { [weak self] in - self?.handleSuccessfullySentMessage() - }) - .store(in: &cancellable) + handleValid(message: sendableMessage) case .failure(let error): handle(error: error) } } - + private func handle(error: ComposeMessageError) { hideSpinner() - + let message = "compose_error".localized + "\n\n" + error.description - + showAlert(error: error, message: message) } + private func handleValid(message sendableMessage: SendableMsg) { + showSpinner("sending_title".localized) + + composeMessageService + .encryptAndSend(message: sendableMessage) + .sink( + receiveCompletion: { [weak self] result in + guard case .failure(let error) = result else { + return + } + self?.handle(error: error) + }, + receiveValue: { [weak self] in + self?.handleSuccessfullySentMessage() + }) + .store(in: &cancellable) + } + private func showNoPubKeyAlert(for emails: [String]) { let message = emails.count == 1 ? "compose_no_pub_recipient".localized @@ -394,7 +391,6 @@ extension ComposeViewController { } // MARK: - Recipients Input - extension ComposeViewController { private var textField: TextFieldNode? { (node.nodeForRow(at: IndexPath(row: RecipientParts.recipientsInput.rawValue, section: 0)) as? TextFieldCellNode)?.textField diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift new file mode 100644 index 000000000..76f6b49eb --- /dev/null +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift @@ -0,0 +1,56 @@ +// +// ComposeMessageError.swift +// FlowCrypt +// +// Created by Anton Kharchevskyi on 25.07.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation + +enum MessageValidationError: Error, CustomStringConvertible { + case emptyRecipient + case emptySubject + case emptyMessage + case missedPublicKey + case missedRecipientPublicKey + case noPubRecipients([String]) + case internalError(String) + + var description: String { + switch self { + case .emptyRecipient: + return "compose_enter_recipient".localized + case .emptySubject: + return "compose_enter_subject".localized + case .emptyMessage: + return "compose_enter_secure".localized + case .missedPublicKey: + return "compose_no_pub_sender".localized + case .missedRecipientPublicKey: + return "Public key is missing" + case .noPubRecipients(let emails): + return emails.count == 1 + ? "compose_no_pub_recipient".localized + : "compose_no_pub_multiple".localized + + "\n" + + emails.joined(separator: ",") + case .internalError(let message): + return message + } + } +} + +enum ComposeMessageError: Error, CustomStringConvertible { + case validationError(MessageValidationError) + case gatewayError(Error) + + var description: String { + switch self { + case .validationError(let messageValidationError): + return messageValidationError.description + case .gatewayError(let error): + return error.localizedDescription + } + } +} diff --git a/FlowCrypt/Functionality/Services/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift similarity index 62% rename from FlowCrypt/Functionality/Services/ComposeMessageService.swift rename to FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index e1f05fa73..67d1f8a54 100644 --- a/FlowCrypt/Functionality/Services/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -21,39 +21,6 @@ struct ComposeMessageRecipient { var state: RecipientState } -enum ComposeMessageError: Error, CustomStringConvertible { - case validationError(MessageValidationError) - case gatewayError(Error) - - // TODO: - ANTON - add proper description - var description: String { - switch self { - case .validationError(let messageValidationError): - return "" - case .gatewayError(let error): - return "" - } - } -} - -enum MessageValidationError: Error { - // showAlert(message: "compose_enter_recipient".localized) - case emptyRecipient - // showAlert(message: "compose_enter_subject".localized) - case emptySubject - // showAlert(message: "compose_enter_secure".localized) - case emptyMessage - - // self.showAlert(message: "compose_no_pub_sender".localized) - case missedPublicKey - - // showAlert(message: "Public key is missing") - case missedRecipientPublicKey - - // showAlert(message: "Recipients should not be empty. Fail in checking") - case internalError(String) -} - final class ComposeMessageService { private let messageGateway: MessageGateway private let dataService: KeyStorageType @@ -72,6 +39,7 @@ final class ComposeMessageService { self.core = core } + // MARK: - Validation func validateMessageInput( with recipients: [ComposeMessageRecipient], input: ComposeMessageInput, @@ -108,29 +76,30 @@ final class ComposeMessageService { return .failure(.validationError(.missedPublicKey)) } - guard let allRecipientPubs = getPubKeys(for: recipients) else { - return .failure(.validationError(.missedRecipientPublicKey)) + switch getPubKeys(for: recipients) { + case .success(let allRecipientPubs): + let replyToMimeMsg = input.replyToMime + .flatMap { String(data: $0, encoding: .utf8) } + + let msg = SendableMsg( + text: text, + to: recipients.map(\.email), + cc: [], + bcc: [], + from: email, + subject: subject, + replyToMimeMsg: replyToMimeMsg, + atts: atts, + pubKeys: allRecipientPubs + [myPubKey] + ) + + return .success(msg) + case .failure(let error): + return .failure(.validationError(error)) } - - let replyToMimeMsg = input.replyToMime - .flatMap { String(data: $0, encoding: .utf8) } - - let msg = SendableMsg( - text: text, - to: recipients.map(\.email), - cc: [], - bcc: [], - from: email, - subject: subject, - replyToMimeMsg: replyToMimeMsg, - atts: atts, - pubKeys: allRecipientPubs + [myPubKey] - ) - - return .success(msg) } - private func getPubKeys(for recepients: [ComposeMessageRecipient]) -> [String]? { + private func getPubKeys(for recepients: [ComposeMessageRecipient]) -> Result<[String], MessageValidationError> { let pubKeys = recepients.map { ($0.email, contactsService.retrievePubKey(for: $0.email)) } @@ -138,14 +107,13 @@ final class ComposeMessageService { let emailsWithoutPubKeys = pubKeys.filter { $0.1 == nil }.map(\.0) guard emailsWithoutPubKeys.isEmpty else { - // TODO: - ANTON - return error -// showNoPubKeyAlert(for: emailsWithoutPubKeys) - return nil + return .failure(.noPubRecipients(emailsWithoutPubKeys)) } - return pubKeys.compactMap(\.1) + return .success(pubKeys.compactMap(\.1)) } + // MARK: - Encrypt and Send func encryptAndSend(message: SendableMsg) -> AnyPublisher { messageGateway.sendMail(mime: encryptMessage(with: message)) .mapError { ComposeMessageError.gatewayError($0) } @@ -161,16 +129,5 @@ final class ComposeMessageService { } } -struct ComposedMessage { - let email: String - let pubkeys: [String] - let subject: String - let message: String - let to: [String] - let cc: [String] - let bcc: [String] - let atts: [SendableMsg.Attachment] -} - // TODO: - ANTON // add tests for ComposeMessageService From dec9b7bc7d242f47a4871c59b46a3d626dc5c26b Mon Sep 17 00:00:00 2001 From: Anton Kharchevskyi Date: Sun, 25 Jul 2021 23:20:57 +0300 Subject: [PATCH 3/7] Add mocks to test ComposeMessageService --- FlowCrypt.xcodeproj/project.pbxproj | 38 ++++++++++++++----- FlowCrypt/App/PromiseExtensions.swift | 11 ++++++ FlowCrypt/Core/Core.swift | 2 +- .../ComposeMessageService.swift | 13 ++++--- .../Core/FlowCryptCoreTests.swift | 6 +-- .../Services/ComposeMessageServiceTests.swift | 28 ++++++++++++++ .../BackupServiceMock.swift | 0 .../Mocks/ContactsServiceMock.swift | 23 +++++++++++ .../Mocks/CoreComposeMessageMock.swift | 17 +++++++++ FlowCryptAppTests/Mocks/KeyStorageMock.swift | 30 +++++++++++++++ .../Mocks/MessageGatewayMock.swift | 20 ++++++++++ 11 files changed, 169 insertions(+), 19 deletions(-) create mode 100644 FlowCryptAppTests/Functionallity/Services/ComposeMessageServiceTests.swift rename FlowCryptAppTests/{Functionallity/Services/Backup Services => Mocks}/BackupServiceMock.swift (100%) create mode 100644 FlowCryptAppTests/Mocks/ContactsServiceMock.swift create mode 100644 FlowCryptAppTests/Mocks/CoreComposeMessageMock.swift create mode 100644 FlowCryptAppTests/Mocks/KeyStorageMock.swift create mode 100644 FlowCryptAppTests/Mocks/MessageGatewayMock.swift diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 69e730f4f..7cc397e43 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -107,6 +107,11 @@ 9F6EE17B2598F9FA0059BA51 /* Gmail+Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6EE17A2598F9FA0059BA51 /* Gmail+Backup.swift */; }; 9F6F3BEE26ADF5DE005BD9C6 /* ComposeMessageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6F3BEC26ADF5DE005BD9C6 /* ComposeMessageService.swift */; }; 9F6F3BEF26ADF5DE005BD9C6 /* ComposeMessageError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6F3BED26ADF5DE005BD9C6 /* ComposeMessageError.swift */; }; + 9F6F3C3526ADFA27005BD9C6 /* ComposeMessageServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6F3C3426ADFA27005BD9C6 /* ComposeMessageServiceTests.swift */; }; + 9F6F3C3C26ADFBC7005BD9C6 /* CoreComposeMessageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6F3C3B26ADFBC7005BD9C6 /* CoreComposeMessageMock.swift */; }; + 9F6F3C6A26ADFBEB005BD9C6 /* MessageGatewayMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6F3C6926ADFBEB005BD9C6 /* MessageGatewayMock.swift */; }; + 9F6F3C7626ADFC37005BD9C6 /* KeyStorageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6F3C7526ADFC37005BD9C6 /* KeyStorageMock.swift */; }; + 9F6F3C7D26ADFC60005BD9C6 /* ContactsServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6F3C7C26ADFC60005BD9C6 /* ContactsServiceMock.swift */; }; 9F716308234FC73E0031645E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9F71630A234FC73E0031645E /* Localizable.strings */; }; 9F7920F52667CEF100DA3D80 /* PassPraseSaveable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7920F42667CEF100DA3D80 /* PassPraseSaveable.swift */; }; 9F79228826696B0200DA3D80 /* PassPhraseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F79228726696B0200DA3D80 /* PassPhraseService.swift */; }; @@ -514,6 +519,11 @@ 9F6EE17A2598F9FA0059BA51 /* Gmail+Backup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Gmail+Backup.swift"; sourceTree = ""; }; 9F6F3BEC26ADF5DE005BD9C6 /* ComposeMessageService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposeMessageService.swift; sourceTree = ""; }; 9F6F3BED26ADF5DE005BD9C6 /* ComposeMessageError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposeMessageError.swift; sourceTree = ""; }; + 9F6F3C3426ADFA27005BD9C6 /* ComposeMessageServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMessageServiceTests.swift; sourceTree = ""; }; + 9F6F3C3B26ADFBC7005BD9C6 /* CoreComposeMessageMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreComposeMessageMock.swift; sourceTree = ""; }; + 9F6F3C6926ADFBEB005BD9C6 /* MessageGatewayMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageGatewayMock.swift; sourceTree = ""; }; + 9F6F3C7526ADFC37005BD9C6 /* KeyStorageMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyStorageMock.swift; sourceTree = ""; }; + 9F6F3C7C26ADFC60005BD9C6 /* ContactsServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsServiceMock.swift; sourceTree = ""; }; 9F716301234FC6950031645E /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/LaunchScreen.strings; sourceTree = ""; }; 9F716304234FC7200031645E /* LocalizationExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationExtensions.swift; sourceTree = ""; }; 9F716309234FC73E0031645E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; @@ -1078,6 +1088,7 @@ D2A9CA44242622F800E1D898 /* GeneralConstantsTest.swift */, 9F2AC5CA267BE99E00F6149B /* Info.plist */, 9F7E902726A1AD280021C07F /* Core */, + 9F6F3C6326ADFBDB005BD9C6 /* Mocks */, ); path = FlowCryptAppTests; sourceTree = ""; @@ -1110,20 +1121,12 @@ 9F4163F3266574CF00106194 /* Services */ = { isa = PBXGroup; children = ( - 9F4163FE2665750500106194 /* Backup Services */, 9FC7EBB6266EBDF000F3BF5D /* PassPhraseStorageTests */, + 9F6F3C3426ADFA27005BD9C6 /* ComposeMessageServiceTests.swift */, ); path = Services; sourceTree = ""; }; - 9F4163FE2665750500106194 /* Backup Services */ = { - isa = PBXGroup; - children = ( - 9F4163EC266574CB00106194 /* BackupServiceMock.swift */, - ); - path = "Backup Services"; - sourceTree = ""; - }; 9F4164162665757700106194 /* PGP */ = { isa = PBXGroup; children = ( @@ -1235,6 +1238,18 @@ path = "Compose Message Service"; sourceTree = ""; }; + 9F6F3C6326ADFBDB005BD9C6 /* Mocks */ = { + isa = PBXGroup; + children = ( + 9F4163EC266574CB00106194 /* BackupServiceMock.swift */, + 9F6F3C3B26ADFBC7005BD9C6 /* CoreComposeMessageMock.swift */, + 9F6F3C6926ADFBEB005BD9C6 /* MessageGatewayMock.swift */, + 9F6F3C7526ADFC37005BD9C6 /* KeyStorageMock.swift */, + 9F6F3C7C26ADFC60005BD9C6 /* ContactsServiceMock.swift */, + ); + path = Mocks; + sourceTree = ""; + }; 9F7E902726A1AD280021C07F /* Core */ = { isa = PBXGroup; children = ( @@ -2417,8 +2432,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9F6F3C3526ADFA27005BD9C6 /* ComposeMessageServiceTests.swift in Sources */, 9FC41090268100B6004C0A69 /* CoreTypesTest.swift in Sources */, 9F976584267E194F0058419D /* TestData.swift in Sources */, + 9F6F3C6A26ADFBEB005BD9C6 /* MessageGatewayMock.swift in Sources */, 9F7E903926A1AD7A0021C07F /* KeyDetailsTests.swift in Sources */, 9FC41183268118B1004C0A69 /* EmailProviderMock.swift in Sources */, 9F976490267E11880058419D /* ImapHelperTest.swift in Sources */, @@ -2429,6 +2446,8 @@ 9FC4117D268118AE004C0A69 /* PassPhraseStorageMock.swift in Sources */, 9F97650E267E16620058419D /* WKDURLsConstructorTests.swift in Sources */, 9F976585267E194F0058419D /* FlowCryptCoreTests.swift in Sources */, + 9F6F3C7D26ADFC60005BD9C6 /* ContactsServiceMock.swift in Sources */, + 9F6F3C3C26ADFBC7005BD9C6 /* CoreComposeMessageMock.swift in Sources */, 9FC4116B2681186D004C0A69 /* KeyMethodsTest.swift in Sources */, 9F97653D267E17C90058419D /* LocalStorageTests.swift in Sources */, 9F9764F4267E15CC0058419D /* ExtensionTests.swift in Sources */, @@ -2437,6 +2456,7 @@ 9F7E8EC6269877E70021C07F /* KeyInfoTests.swift in Sources */, 9F976556267E186D0058419D /* ClientConfigurationTests.swift in Sources */, 9FC41171268118A7004C0A69 /* PassPhraseStorageTests.swift in Sources */, + 9F6F3C7626ADFC37005BD9C6 /* KeyStorageMock.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/FlowCrypt/App/PromiseExtensions.swift b/FlowCrypt/App/PromiseExtensions.swift index f4a7dce81..f110bc692 100644 --- a/FlowCrypt/App/PromiseExtensions.swift +++ b/FlowCrypt/App/PromiseExtensions.swift @@ -6,6 +6,7 @@ // Copyright © 2021 FlowCrypt Limited. All rights reserved. // +import Combine import Foundation import Promises @@ -24,6 +25,16 @@ extension Promise { } } +extension Future { + static func resolveAfter(timeout: TimeInterval = 5, with result: Result) -> Future { + Future { promise in + DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { + promise(result) + } + } + } +} + enum MockError: Error { case some } diff --git a/FlowCrypt/Core/Core.swift b/FlowCrypt/Core/Core.swift index 4d7cd2a47..6d1240c2f 100644 --- a/FlowCrypt/Core/Core.swift +++ b/FlowCrypt/Core/Core.swift @@ -16,7 +16,7 @@ protocol KeyDecrypter { func decryptKey(armoredPrv: String, passphrase: String) throws -> CoreRes.DecryptKey } -final class Core: KeyDecrypter { +final class Core: KeyDecrypter, CoreComposeMessageType { static let shared = Core() private var jsEndpointListener: JSValue? diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 67d1f8a54..e69b6a934 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -21,17 +21,21 @@ struct ComposeMessageRecipient { var state: RecipientState } +protocol CoreComposeMessageType { + func composeEmail(msg: SendableMsg, fmt: MsgFmt, pubKeys: [String]?) throws -> CoreRes.ComposeEmail +} + final class ComposeMessageService { private let messageGateway: MessageGateway private let dataService: KeyStorageType private let contactsService: ContactsServiceType - private let core: Core + private let core: CoreComposeMessageType init( messageGateway: MessageGateway = MailProvider.shared.messageSender, dataService: KeyStorageType = KeyDataStorage(), contactsService: ContactsServiceType = ContactsService(), - core: Core = Core.shared + core: CoreComposeMessageType = Core.shared ) { self.messageGateway = messageGateway self.dataService = dataService @@ -99,7 +103,7 @@ final class ComposeMessageService { } } - private func getPubKeys(for recepients: [ComposeMessageRecipient]) -> Result<[String], MessageValidationError> { + private func getPubKeys(for recepients: [ComposeMessageRecipient]) -> Result<[String], MessageValidationError> { let pubKeys = recepients.map { ($0.email, contactsService.retrievePubKey(for: $0.email)) } @@ -128,6 +132,3 @@ final class ComposeMessageService { } } } - -// TODO: - ANTON -// add tests for ComposeMessageService diff --git a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift index c6231c30e..95e60a6cc 100644 --- a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift +++ b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift @@ -89,7 +89,7 @@ class FlowCryptCoreTests: XCTestCase { } func testComposeEmailPlain() throws { - let msg = SendableMsg(text: "this is the message", to: ["email@hello.com"], cc: [], bcc: [], from: "sender@hello.com", subject: "subj", replyToMimeMsg: nil, atts: []) + let msg = SendableMsg(text: "this is the message", to: ["email@hello.com"], cc: [], bcc: [], from: "sender@hello.com", subject: "subj", replyToMimeMsg: nil, atts: [], pubKeys: nil) let composeEmailRes = try core.composeEmail(msg: msg, fmt: MsgFmt.plain, pubKeys: nil) let mime = String(data: composeEmailRes.mimeEncoded, encoding: .utf8)! XCTAssertNil(mime.range(of: "-----BEGIN PGP MESSAGE-----")) // not encrypted @@ -99,7 +99,7 @@ class FlowCryptCoreTests: XCTestCase { } func testComposeEmailEncryptInline() throws { - let msg = SendableMsg(text: "this is the message", to: ["email@hello.com"], cc: [], bcc: [], from: "sender@hello.com", subject: "subj", replyToMimeMsg: nil, atts: []) + let msg = SendableMsg(text: "this is the message", to: ["email@hello.com"], cc: [], bcc: [], from: "sender@hello.com", subject: "subj", replyToMimeMsg: nil, atts: [], pubKeys: nil) let composeEmailRes = try core.composeEmail(msg: msg, fmt: MsgFmt.encryptInline, pubKeys: [TestData.k0.pub, TestData.k1.pub]) let mime = String(data: composeEmailRes.mimeEncoded, encoding: .utf8)! XCTAssertNotNil(mime.range(of: "-----BEGIN PGP MESSAGE-----")) // encrypted @@ -114,7 +114,7 @@ class FlowCryptCoreTests: XCTestCase { let text = "this is the encrypted e2e content" let generateKeyRes = try core.generateKey(passphrase: passphrase, variant: KeyVariant.curve25519, userIds: [UserId(email: email, name: "End to end")]) let k = generateKeyRes.key - let msg = SendableMsg(text: text, to: [email], cc: [], bcc: [], from: email, subject: "e2e subj", replyToMimeMsg: nil, atts: []) + let msg = SendableMsg(text: text, to: [email], cc: [], bcc: [], from: email, subject: "e2e subj", replyToMimeMsg: nil, atts: [], pubKeys: nil) let mime = try core.composeEmail(msg: msg, fmt: MsgFmt.encryptInline, pubKeys: [k.public]) let keys = [PrvKeyInfo(private: k.private!, longid: k.ids[0].longid, passphrase: passphrase, fingerprints: k.fingerprints)] let decrypted = try core.parseDecryptMsg(encrypted: mime.mimeEncoded, keys: keys, msgPwd: nil, isEmail: true) diff --git a/FlowCryptAppTests/Functionallity/Services/ComposeMessageServiceTests.swift b/FlowCryptAppTests/Functionallity/Services/ComposeMessageServiceTests.swift new file mode 100644 index 000000000..8db8d8962 --- /dev/null +++ b/FlowCryptAppTests/Functionallity/Services/ComposeMessageServiceTests.swift @@ -0,0 +1,28 @@ +// +// ComposeMessageServiceTests.swift +// FlowCryptAppTests +// +// Created by Anton Kharchevskyi on 25.07.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import XCTest +@testable import FlowCrypt + +class ComposeMessageServiceTests: XCTestCase { + + var sut: ComposeMessageService! + + + override func setUp() { + super.setUp() + + sut = ComposeMessageService( + messageGateway: MessageGatewayMock(), + dataService: KeyStorageMock(), + contactsService: ContactsServiceMock(), + core: CoreComposeMessageMock() + ) + } + +} diff --git a/FlowCryptAppTests/Functionallity/Services/Backup Services/BackupServiceMock.swift b/FlowCryptAppTests/Mocks/BackupServiceMock.swift similarity index 100% rename from FlowCryptAppTests/Functionallity/Services/Backup Services/BackupServiceMock.swift rename to FlowCryptAppTests/Mocks/BackupServiceMock.swift diff --git a/FlowCryptAppTests/Mocks/ContactsServiceMock.swift b/FlowCryptAppTests/Mocks/ContactsServiceMock.swift new file mode 100644 index 000000000..f642fb914 --- /dev/null +++ b/FlowCryptAppTests/Mocks/ContactsServiceMock.swift @@ -0,0 +1,23 @@ +// +// ContactsServiceMock.swift +// FlowCryptAppTests +// +// Created by Anton Kharchevskyi on 25.07.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation +import Promises +@testable import FlowCrypt + +class ContactsServiceMock: ContactsServiceType { + var retrievePubKeyResult: ((String) -> (String))! + func retrievePubKey(for email: String) -> String? { + retrievePubKeyResult(email) + } + + var searchContactResult: Result! + func searchContact(with email: String) -> Promise { + Promise.resolveAfter(with: searchContactResult) + } +} diff --git a/FlowCryptAppTests/Mocks/CoreComposeMessageMock.swift b/FlowCryptAppTests/Mocks/CoreComposeMessageMock.swift new file mode 100644 index 000000000..7be4129cd --- /dev/null +++ b/FlowCryptAppTests/Mocks/CoreComposeMessageMock.swift @@ -0,0 +1,17 @@ +// +// CoreComposeMessageMock.swift +// FlowCryptAppTests +// +// Created by Anton Kharchevskyi on 25.07.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation +@testable import FlowCrypt + +class CoreComposeMessageMock: CoreComposeMessageType { + var composeEmailResult: ((SendableMsg, MsgFmt, [String]?) -> (CoreRes.ComposeEmail))! + func composeEmail(msg: SendableMsg, fmt: MsgFmt, pubKeys: [String]?) throws -> CoreRes.ComposeEmail { + composeEmailResult(msg, fmt, pubKeys) + } +} diff --git a/FlowCryptAppTests/Mocks/KeyStorageMock.swift b/FlowCryptAppTests/Mocks/KeyStorageMock.swift new file mode 100644 index 000000000..a36f1b7a9 --- /dev/null +++ b/FlowCryptAppTests/Mocks/KeyStorageMock.swift @@ -0,0 +1,30 @@ +// +// KeyStorageMock.swift +// FlowCryptAppTests +// +// Created by Anton Kharchevskyi on 25.07.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation +@testable import FlowCrypt + +class KeyStorageMock: KeyStorageType { + func addKeys(keyDetails: [KeyDetails], source: KeySource, for email: String) { + + } + + func updateKeys(keyDetails: [KeyDetails], source: KeySource, for email: String) { + + } + + var publicKeyResult: (() -> (String?))! + func publicKey() -> String? { + publicKeyResult() + } + + var keysInfoResult: (() -> ([KeyInfo]))! + func keysInfo() -> [KeyInfo] { + keysInfoResult() + } +} diff --git a/FlowCryptAppTests/Mocks/MessageGatewayMock.swift b/FlowCryptAppTests/Mocks/MessageGatewayMock.swift new file mode 100644 index 000000000..a6414b01f --- /dev/null +++ b/FlowCryptAppTests/Mocks/MessageGatewayMock.swift @@ -0,0 +1,20 @@ +// +// MessageGatewayMock.swift +// FlowCryptAppTests +// +// Created by Anton Kharchevskyi on 25.07.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation +import Combine +@testable import FlowCrypt + +class MessageGatewayMock: MessageGateway { + var sendMailResult: ((Data) -> (Result))! + func sendMail(mime: Data) -> Future { + Future { promise in + promise(self.sendMailResult(mime)) + } + } +} From d4f729e8f710a70f74368b1996629536e71c4ce2 Mon Sep 17 00:00:00 2001 From: Anton Kharchevskyi Date: Mon, 26 Jul 2021 16:03:56 +0300 Subject: [PATCH 4/7] Add tests --- .../Compose/ComposeViewController.swift | 13 +- .../Compose/ComposeViewDecorator.swift | 5 +- FlowCrypt/Core/CoreTypes.swift | 4 +- .../ComposeMessageError.swift | 8 +- .../ComposeMessageService.swift | 19 +- .../Services/ComposeMessageServiceTests.swift | 289 +++++++++++++++++- .../Mocks/ContactsServiceMock.swift | 2 +- .../RecipientEmailsCellNodeInput.swift | 3 - 8 files changed, 315 insertions(+), 28 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 1b5e38f27..65a3cc948 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -195,8 +195,7 @@ extension ComposeViewController { private func sendMessage() { view.endEditing(true) - let result = composeMessageService.validateMessageInput( - with: recipients, + let result = composeMessageService.validateMessage( input: input, contextToSend: contextToSend, email: email, @@ -375,8 +374,9 @@ extension ComposeViewController { ) { [weak self] action in self?.handleTextFieldAction(with: action) } - .onShouldReturn { [weak self] textField -> Bool in - self?.shouldReturn(with: textField) ?? true + .onShouldReturn { textField -> Bool in + textField.resignFirstResponder() + return true } .onShouldChangeCharacters { [weak self] textField, character -> (Bool) in self?.shouldChange(with: textField, and: character) ?? true @@ -404,11 +404,6 @@ extension ComposeViewController { contextToSend.recipients } - private func shouldReturn(with textField: UITextField) -> Bool { - textField.resignFirstResponder() - return true - } - private func shouldChange(with textField: UITextField, and character: String) -> Bool { func nextResponder() { guard let node = node.visibleNodes[safe: ComposeParts.subject.rawValue] as? TextFieldCellNode else { return } diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index b3ce4d40b..00d6c23c6 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -9,6 +9,9 @@ import FlowCryptUI import UIKit +typealias RecipientState = RecipientEmailsCellNode.Input.State +typealias RecipientStateContext = RecipientEmailsCellNode.Input.StateContext + struct ComposeViewDecorator { let recipientIdleState: RecipientState = .idle(idleStateContext) let recipientSelectedState: RecipientState = .selected(selectedStateContext) @@ -119,7 +122,7 @@ extension UIColor { // MARK: - RecipientState extension ComposeViewDecorator { - private static var idleStateContext: RecipientStateContext { + static var idleStateContext: RecipientStateContext { RecipientStateContext( backgroundColor: .titleNodeBackgroundColor, borderColor: .borderColor, diff --git a/FlowCrypt/Core/CoreTypes.swift b/FlowCrypt/Core/CoreTypes.swift index d0d34201c..0c8793bd4 100644 --- a/FlowCrypt/Core/CoreTypes.swift +++ b/FlowCrypt/Core/CoreTypes.swift @@ -105,8 +105,8 @@ struct UserId: Encodable { let name: String } -struct SendableMsg { - struct Attachment { +struct SendableMsg: Equatable { + struct Attachment: Equatable { let name: String let type: String let base64: String diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift index 76f6b49eb..7db099779 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift @@ -8,7 +8,7 @@ import Foundation -enum MessageValidationError: Error, CustomStringConvertible { +enum MessageValidationError: Error, CustomStringConvertible, Equatable { case emptyRecipient case emptySubject case emptyMessage @@ -41,7 +41,7 @@ enum MessageValidationError: Error, CustomStringConvertible { } } -enum ComposeMessageError: Error, CustomStringConvertible { +enum ComposeMessageError: Error, CustomStringConvertible, Equatable { case validationError(MessageValidationError) case gatewayError(Error) @@ -53,4 +53,8 @@ enum ComposeMessageError: Error, CustomStringConvertible { return error.localizedDescription } } + + static func == (lhs: ComposeMessageError, rhs: ComposeMessageError) -> Bool { + lhs.description == rhs.description + } } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index e69b6a934..2b055d7e9 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -44,16 +44,25 @@ final class ComposeMessageService { } // MARK: - Validation - func validateMessageInput( - with recipients: [ComposeMessageRecipient], + func validateMessage( input: ComposeMessageInput, contextToSend: ComposeMessageContext, email: String, atts: [SendableMsg.Attachment] ) -> Result { + let recipients = contextToSend.recipients + + guard recipients.isNotEmpty else { + return .failure(.validationError(.emptyRecipient)) + } + let emails = recipients.map(\.email) let hasContent = emails.filter { $0.hasContent } + guard emails.isNotEmpty else { + return .failure(.validationError(.emptyRecipient)) + } + guard emails.count == hasContent.count else { return .failure(.validationError(.emptyRecipient)) } @@ -66,12 +75,6 @@ final class ComposeMessageService { return .failure(.validationError(.emptyMessage)) } - let recipients = contextToSend.recipients - - guard recipients.isNotEmpty else { - return .failure(.validationError(.internalError("Recipients should not be empty. Fail in checking"))) - } - let subject = input.subjectReplyTitle ?? contextToSend.subject ?? "(no subject)" diff --git a/FlowCryptAppTests/Functionallity/Services/ComposeMessageServiceTests.swift b/FlowCryptAppTests/Functionallity/Services/ComposeMessageServiceTests.swift index 8db8d8962..57f6c15ae 100644 --- a/FlowCryptAppTests/Functionallity/Services/ComposeMessageServiceTests.swift +++ b/FlowCryptAppTests/Functionallity/Services/ComposeMessageServiceTests.swift @@ -9,20 +9,305 @@ import XCTest @testable import FlowCrypt +private let recipientIdleState: RecipientState = .idle(ComposeViewDecorator.idleStateContext) + class ComposeMessageServiceTests: XCTestCase { var sut: ComposeMessageService! + let recipients: [ComposeMessageRecipient] = [ + ComposeMessageRecipient(email: "test@gmail.com", state: recipientIdleState), + ComposeMessageRecipient(email: "test2@gmail.com", state: recipientIdleState), + ComposeMessageRecipient(email: "test3@gmail.com", state: recipientIdleState) + ] + + var keyStorage = KeyStorageMock() + var contactsService = ContactsServiceMock() override func setUp() { super.setUp() sut = ComposeMessageService( messageGateway: MessageGatewayMock(), - dataService: KeyStorageMock(), - contactsService: ContactsServiceMock(), + dataService: keyStorage, + contactsService: contactsService, core: CoreComposeMessageMock() ) } + + func testValidateMessageInputWithEmptyRecipients() { + let result = sut.validateMessage( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "", + recipients: [], + subject: nil + ), + email: "some@gmail.com", + atts: [] + ) + + var thrownError: Error? + XCTAssertThrowsError(try result.get()) { thrownError = $0 } + + let error = expectComposeMessageError(for: thrownError) + XCTAssertEqual(error, .validationError(.emptyRecipient)) + } + + func testValidateMessageInputWithWhitespaceRecipients() { + let recipients: [ComposeMessageRecipient] = [ + ComposeMessageRecipient(email: " ", state: recipientIdleState), + ComposeMessageRecipient(email: " ", state: recipientIdleState), + ComposeMessageRecipient(email: "sdfff", state: recipientIdleState) + ] + let result = sut.validateMessage( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "", + recipients: recipients, + subject: nil + ), + email: "some@gmail.com", + atts: [] + ) + + var thrownError: Error? + XCTAssertThrowsError(try result.get()) { thrownError = $0 } + + let error = expectComposeMessageError(for: thrownError) + XCTAssertEqual(error, .validationError(.emptyRecipient)) + } + + func testValidateMessageInputWithEmptySubject() { + func test() { + var thrownError: Error? = nil + XCTAssertThrowsError(try result.get()) { thrownError = $0 } + + let error = expectComposeMessageError(for: thrownError) + XCTAssertEqual(error, .validationError(.emptySubject)) + } + + var result = sut.validateMessage( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "", + recipients: recipients, + subject: nil + ), + email: "some@gmail.com", + atts: [] + ) + + test() + + result = sut.validateMessage( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "", + recipients: recipients, + subject: "" + ), + email: "some@gmail.com", + atts: [] + ) + + test() + + result = sut.validateMessage( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "", + recipients: recipients, + subject: " " + ), + email: "some@gmail.com", + atts: [] + ) + } + + func testValidateMessageInputWithEmptyMessage() { + func test() { + var thrownError: Error? + XCTAssertThrowsError(try result.get()) { thrownError = $0 } + let error = expectComposeMessageError(for: thrownError) + XCTAssertEqual(error, .validationError(.emptyMessage)) + } + + var result = sut.validateMessage( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: nil, + recipients: recipients, + subject: "Some subject" + ), + email: "some@gmail.com", + atts: [] + ) + test() + + result = sut.validateMessage( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "", + recipients: recipients, + subject: "Some subject" + ), + email: "some@gmail.com", + atts: [] + ) + + test() + + result = sut.validateMessage( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: " ", + recipients: recipients, + subject: "Some subject" + ), + email: "some@gmail.com", + atts: [] + ) + + test() + } + + func testValidateMessageInputWithEmptyPublicKey() { + keyStorage.publicKeyResult = { + nil + } + + let result = sut.validateMessage( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "some message", + recipients: recipients, + subject: "Some subject" + ), + email: "some@gmail.com", + atts: [] + ) + + var thrownError: Error? + XCTAssertThrowsError(try result.get()) { thrownError = $0 } + let error = expectComposeMessageError(for: thrownError) + XCTAssertEqual(error, .validationError(.missedPublicKey)) + } + + func testValidateMessageInputWithAllEmptyRecipientPubKeys() { + keyStorage.publicKeyResult = { + "public key" + } + + recipients.forEach { recipient in + contactsService.retrievePubKeyResult = { _ in + nil + } + } + + let result = sut.validateMessage( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "some message", + recipients: recipients, + subject: "Some subject" + ), + email: "some@gmail.com", + atts: [] + ) + + var thrownError: Error? + XCTAssertThrowsError(try result.get()) { thrownError = $0 } + let error = expectComposeMessageError(for: thrownError) + XCTAssertEqual(error, .validationError(.noPubRecipients(recipients.map(\.email)))) + } + + func testValidateMessageInputWithoutOneRecipientPubKey() { + keyStorage.publicKeyResult = { + "public key" + } + + let recWithoutPubKey = recipients[0].email + recipients.forEach { _ in + contactsService.retrievePubKeyResult = { recipient in + if recipient == recWithoutPubKey { + return nil + } + return "recipient pub key" + } + } + + let result = sut.validateMessage( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "some message", + recipients: recipients, + subject: "Some subject" + ), + email: "some@gmail.com", + atts: [] + ) + + var thrownError: Error? + XCTAssertThrowsError(try result.get()) { thrownError = $0 } + let error = expectComposeMessageError(for: thrownError) + XCTAssertEqual(error, .validationError(.noPubRecipients([recWithoutPubKey]))) + } + + func testSuccessfulMessageValidation() { + keyStorage.publicKeyResult = { + "public key" + } + + recipients.enumerated().forEach { (element, index) in + contactsService.retrievePubKeyResult = { recipient in + "pubKey" + } + } + + let message = "some message" + let subject = "Some subject" + let email = "some@gmail.com" + let input = ComposeMessageInput(type: .idle) + + let result = try? sut.validateMessage( + input: input, + contextToSend: ComposeMessageContext( + message: message, + recipients: recipients, + subject: subject + ), + email: email, + atts: [] + ).get() + + let expected = SendableMsg( + text: message, + to: recipients.map(\.email), + cc: [], + bcc: [], + from: email, + subject: subject, + replyToMimeMsg: nil, + atts: [], + pubKeys: [ + "pubKey", + "pubKey", + "pubKey", + "public key" + ]) + + XCTAssertNotNil(result) + XCTAssertEqual(result!, expected) + + } + + private func expectComposeMessageError(for thrownError: Error?) -> ComposeMessageError { + if let thrownError = thrownError as? ComposeMessageError { return thrownError + } else { + XCTFail() + return ComposeMessageError.validationError(.internalError("")) + } + } } diff --git a/FlowCryptAppTests/Mocks/ContactsServiceMock.swift b/FlowCryptAppTests/Mocks/ContactsServiceMock.swift index f642fb914..1cad0d2f1 100644 --- a/FlowCryptAppTests/Mocks/ContactsServiceMock.swift +++ b/FlowCryptAppTests/Mocks/ContactsServiceMock.swift @@ -11,7 +11,7 @@ import Promises @testable import FlowCrypt class ContactsServiceMock: ContactsServiceType { - var retrievePubKeyResult: ((String) -> (String))! + var retrievePubKeyResult: ((String) -> (String?))! func retrievePubKey(for email: String) -> String? { retrievePubKeyResult(email) } diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift index d674516e4..8058ca931 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailsCellNodeInput.swift @@ -8,9 +8,6 @@ import UIKit -public typealias RecipientState = RecipientEmailsCellNode.Input.State -public typealias RecipientStateContext = RecipientEmailsCellNode.Input.StateContext - // MARK: Input extension RecipientEmailsCellNode { public struct Input { From 32c31afd74b5fdb25c82fbeaea7eb6e8f4222951 Mon Sep 17 00:00:00 2001 From: Anton Kharchevskyi Date: Mon, 26 Jul 2021 16:24:18 +0300 Subject: [PATCH 5/7] Move Combine and Promise test extensions to test target --- FlowCrypt.xcodeproj/project.pbxproj | 12 +++++++---- FlowCryptAppTests/CombineTestExtension.swift | 20 +++++++++++++++++++ .../PromiseTestExtension.swift | 11 ---------- 3 files changed, 28 insertions(+), 15 deletions(-) create mode 100644 FlowCryptAppTests/CombineTestExtension.swift rename FlowCrypt/App/PromiseExtensions.swift => FlowCryptAppTests/PromiseTestExtension.swift (69%) diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 7cc397e43..407d776ef 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -78,6 +78,8 @@ 9F23EA50237217140017DFED /* ComposeViewDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23EA4F237217140017DFED /* ComposeViewDecorator.swift */; }; 9F268891237DC55600428A94 /* SetupManuallyImportKeyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F268890237DC55600428A94 /* SetupManuallyImportKeyViewController.swift */; }; 9F2AC5B1267BDED100F6149B /* GmailSearchExpressionGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2AC5B0267BDED100F6149B /* GmailSearchExpressionGenerator.swift */; }; + 9F2F206826AEEAA60044E144 /* CombineTestExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2F206726AEEAA60044E144 /* CombineTestExtension.swift */; }; + 9F2F207326AEECFB0044E144 /* PromiseTestExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC411892681191D004C0A69 /* PromiseTestExtension.swift */; }; 9F31AB8C23298B3F00CF87EA /* Imap+retry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F31AB8B23298B3F00CF87EA /* Imap+retry.swift */; }; 9F31AB8E23298BCF00CF87EA /* Imap+folders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F31AB8D23298BCF00CF87EA /* Imap+folders.swift */; }; 9F31AB91232993F500CF87EA /* Imap+session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F31AB90232993F500CF87EA /* Imap+session.swift */; }; @@ -164,7 +166,6 @@ 9FC41171268118A7004C0A69 /* PassPhraseStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EBA2266EB95300F3BF5D /* PassPhraseStorageTests.swift */; }; 9FC4117D268118AE004C0A69 /* PassPhraseStorageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EBCF266EBE1D00F3BF5D /* PassPhraseStorageMock.swift */; }; 9FC41183268118B1004C0A69 /* EmailProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EBC1266EBE0100F3BF5D /* EmailProviderMock.swift */; }; - 9FC4120926811D00004C0A69 /* PromiseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC411892681191D004C0A69 /* PromiseExtensions.swift */; }; 9FC413182683C492004C0A69 /* InMemoryPassPhraseStorageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC413172683C491004C0A69 /* InMemoryPassPhraseStorageTest.swift */; }; 9FC413442683C912004C0A69 /* GmailServiceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC413432683C912004C0A69 /* GmailServiceTest.swift */; }; 9FC7EAB3266A404D00F3BF5D /* PassPhraseObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EAB2266A404D00F3BF5D /* PassPhraseObject.swift */; }; @@ -477,6 +478,7 @@ 9F2AC5B0267BDED100F6149B /* GmailSearchExpressionGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GmailSearchExpressionGenerator.swift; sourceTree = ""; }; 9F2AC5C6267BE99E00F6149B /* FlowCryptAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FlowCryptAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 9F2AC5CA267BE99E00F6149B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9F2F206726AEEAA60044E144 /* CombineTestExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineTestExtension.swift; sourceTree = ""; }; 9F31AB8B23298B3F00CF87EA /* Imap+retry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Imap+retry.swift"; sourceTree = ""; }; 9F31AB8D23298BCF00CF87EA /* Imap+folders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Imap+folders.swift"; sourceTree = ""; }; 9F31AB90232993F500CF87EA /* Imap+session.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Imap+session.swift"; sourceTree = ""; }; @@ -570,7 +572,7 @@ 9FC4112D2595EA8B001180A8 /* Gmail+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Gmail+Search.swift"; sourceTree = ""; }; 9FC411342595EA94001180A8 /* Imap+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Imap+Search.swift"; sourceTree = ""; }; 9FC4114B25961CEA001180A8 /* MailServiceProviderType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailServiceProviderType.swift; sourceTree = ""; }; - 9FC411892681191D004C0A69 /* PromiseExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromiseExtensions.swift; sourceTree = ""; }; + 9FC411892681191D004C0A69 /* PromiseTestExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromiseTestExtension.swift; sourceTree = ""; }; 9FC413172683C491004C0A69 /* InMemoryPassPhraseStorageTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemoryPassPhraseStorageTest.swift; sourceTree = ""; }; 9FC413432683C912004C0A69 /* GmailServiceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GmailServiceTest.swift; sourceTree = ""; }; 9FC7EAB2266A404D00F3BF5D /* PassPhraseObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassPhraseObject.swift; sourceTree = ""; }; @@ -1089,6 +1091,8 @@ 9F2AC5CA267BE99E00F6149B /* Info.plist */, 9F7E902726A1AD280021C07F /* Core */, 9F6F3C6326ADFBDB005BD9C6 /* Mocks */, + 9F2F206726AEEAA60044E144 /* CombineTestExtension.swift */, + 9FC411892681191D004C0A69 /* PromiseTestExtension.swift */, ); path = FlowCryptAppTests; sourceTree = ""; @@ -1274,7 +1278,6 @@ children = ( 9FDF3653235A218E00614596 /* main.swift */, 9FDF3655235A22DA00614596 /* AppReset.swift */, - 9FC411892681191D004C0A69 /* PromiseExtensions.swift */, 9F8220D426336626004B2009 /* Logger.swift */, 21C7DF0A266C0E3600C44800 /* Configuration.swift */, ); @@ -2443,6 +2446,7 @@ 9FC413182683C492004C0A69 /* InMemoryPassPhraseStorageTest.swift in Sources */, 9F9764C5267E14AB0058419D /* GeneralConstantsTest.swift in Sources */, 9F976507267E165D0058419D /* ZBase32EncodingTests.swift in Sources */, + 9F2F207326AEECFB0044E144 /* PromiseTestExtension.swift in Sources */, 9FC4117D268118AE004C0A69 /* PassPhraseStorageMock.swift in Sources */, 9F97650E267E16620058419D /* WKDURLsConstructorTests.swift in Sources */, 9F976585267E194F0058419D /* FlowCryptCoreTests.swift in Sources */, @@ -2451,6 +2455,7 @@ 9FC4116B2681186D004C0A69 /* KeyMethodsTest.swift in Sources */, 9F97653D267E17C90058419D /* LocalStorageTests.swift in Sources */, 9F9764F4267E15CC0058419D /* ExtensionTests.swift in Sources */, + 9F2F206826AEEAA60044E144 /* CombineTestExtension.swift in Sources */, 9FC413442683C912004C0A69 /* GmailServiceTest.swift in Sources */, 9F976556267E186D0058419D /* ClientConfigurationTests.swift in Sources */, 9F7E8EC6269877E70021C07F /* KeyInfoTests.swift in Sources */, @@ -2655,7 +2660,6 @@ D2F6D1352433753B00DB4065 /* SMTPSession.swift in Sources */, 9FBEAF3125DFB8E1009E98D4 /* DBMigrationService.swift in Sources */, 9F17976D2368EEBD002BF770 /* SetupViewDecorator.swift in Sources */, - 9FC4120926811D00004C0A69 /* PromiseExtensions.swift in Sources */, 5ADEDCC023A43B0800EC495E /* KeyDetailInfoViewDecorator.swift in Sources */, D227C0E6250538780070F805 /* RemoteFoldersProvider.swift in Sources */, 9F2AC5B1267BDED100F6149B /* GmailSearchExpressionGenerator.swift in Sources */, diff --git a/FlowCryptAppTests/CombineTestExtension.swift b/FlowCryptAppTests/CombineTestExtension.swift new file mode 100644 index 000000000..7bc257ac2 --- /dev/null +++ b/FlowCryptAppTests/CombineTestExtension.swift @@ -0,0 +1,20 @@ +// +// CombineTestExtension.swift +// FlowCryptAppTests +// +// Created by Anton Kharchevskyi on 26.07.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation +import Combine + +extension Future { + static func resolveAfter(timeout: TimeInterval = 5, with result: Result) -> Future { + Future { promise in + DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { + promise(result) + } + } + } +} diff --git a/FlowCrypt/App/PromiseExtensions.swift b/FlowCryptAppTests/PromiseTestExtension.swift similarity index 69% rename from FlowCrypt/App/PromiseExtensions.swift rename to FlowCryptAppTests/PromiseTestExtension.swift index f110bc692..f4a7dce81 100644 --- a/FlowCrypt/App/PromiseExtensions.swift +++ b/FlowCryptAppTests/PromiseTestExtension.swift @@ -6,7 +6,6 @@ // Copyright © 2021 FlowCrypt Limited. All rights reserved. // -import Combine import Foundation import Promises @@ -25,16 +24,6 @@ extension Promise { } } -extension Future { - static func resolveAfter(timeout: TimeInterval = 5, with result: Result) -> Future { - Future { promise in - DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { - promise(result) - } - } - } -} - enum MockError: Error { case some } From c4e5b5c6d679f1f36b85b67de2da05b03c5ecef8 Mon Sep 17 00:00:00 2001 From: Anton Kharchevskyi Date: Tue, 27 Jul 2021 11:59:01 +0300 Subject: [PATCH 6/7] Minor fixes --- .../Controllers/Compose/ComposeViewController.swift | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 65a3cc948..0d9c36419 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -217,7 +217,7 @@ extension ComposeViewController { + "\n\n" + error.description - showAlert(error: error, message: message) + showAlert(message: message) } private func handleValid(message sendableMessage: SendableMsg) { @@ -238,13 +238,6 @@ extension ComposeViewController { .store(in: &cancellable) } - private func showNoPubKeyAlert(for emails: [String]) { - let message = emails.count == 1 - ? "compose_no_pub_recipient".localized - : "compose_no_pub_multiple".localized + "\n" + emails.joined(separator: ",") - showAlert(message: message) - } - private func handleSuccessfullySentMessage() { hideSpinner() showToast(input.successfullySentToast) From 4c0107f48590eaa86992609706f4d6bfcd4bb868 Mon Sep 17 00:00:00 2001 From: Anton Kharchevskyi Date: Tue, 27 Jul 2021 17:26:09 +0300 Subject: [PATCH 7/7] PR fixes --- .../Compose/ComposeViewController.swift | 42 +++++++------------ .../ComposeMessageService.swift | 39 ++++++++--------- 2 files changed, 34 insertions(+), 47 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 0d9c36419..9368cef98 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -195,19 +195,27 @@ extension ComposeViewController { private func sendMessage() { view.endEditing(true) - let result = composeMessageService.validateMessage( + showSpinner("sending_title".localized) + + composeMessageService.validateMessage( input: input, contextToSend: contextToSend, email: email, atts: [] ) - - switch result { - case .success(let sendableMessage): - handleValid(message: sendableMessage) - case .failure(let error): - handle(error: error) - } + .publisher + .flatMap(composeMessageService.encryptAndSend) + .sink( + receiveCompletion: { [weak self] result in + guard case .failure(let error) = result else { + return + } + self?.handle(error: error) + }, + receiveValue: { [weak self] in + self?.handleSuccessfullySentMessage() + }) + .store(in: &cancellable) } private func handle(error: ComposeMessageError) { @@ -220,24 +228,6 @@ extension ComposeViewController { showAlert(message: message) } - private func handleValid(message sendableMessage: SendableMsg) { - showSpinner("sending_title".localized) - - composeMessageService - .encryptAndSend(message: sendableMessage) - .sink( - receiveCompletion: { [weak self] result in - guard case .failure(let error) = result else { - return - } - self?.handle(error: error) - }, - receiveValue: { [weak self] in - self?.handleSuccessfullySentMessage() - }) - .store(in: &cancellable) - } - private func handleSuccessfullySentMessage() { hideSpinner() showToast(input.successfullySentToast) diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 2b055d7e9..85a540bb8 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -83,27 +83,24 @@ final class ComposeMessageService { return .failure(.validationError(.missedPublicKey)) } - switch getPubKeys(for: recipients) { - case .success(let allRecipientPubs): - let replyToMimeMsg = input.replyToMime - .flatMap { String(data: $0, encoding: .utf8) } - - let msg = SendableMsg( - text: text, - to: recipients.map(\.email), - cc: [], - bcc: [], - from: email, - subject: subject, - replyToMimeMsg: replyToMimeMsg, - atts: atts, - pubKeys: allRecipientPubs + [myPubKey] - ) - - return .success(msg) - case .failure(let error): - return .failure(.validationError(error)) - } + return getPubKeys(for: recipients) + .mapError { ComposeMessageError.validationError($0) } + .map { allRecipientPubs in + let replyToMimeMsg = input.replyToMime + .flatMap { String(data: $0, encoding: .utf8) } + + return SendableMsg( + text: text, + to: recipients.map(\.email), + cc: [], + bcc: [], + from: email, + subject: subject, + replyToMimeMsg: replyToMimeMsg, + atts: atts, + pubKeys: allRecipientPubs + [myPubKey] + ) + } } private func getPubKeys(for recepients: [ComposeMessageRecipient]) -> Result<[String], MessageValidationError> {