diff --git a/Podfile b/Podfile index 7a19155e..30331571 100644 --- a/Podfile +++ b/Podfile @@ -27,18 +27,29 @@ target 'TinodiosDB' do db_pods end +def app_pods + pod 'Firebase' + pod 'FirebaseCore' + pod 'FirebaseMessaging' + pod 'FirebaseAnalytics' + pod 'FirebaseCrashlytics' + pod 'Kingfisher', '~> 5.0' + pod 'MobileVLCKit', '~> 3.4.1b9' + pod 'PhoneNumberKit', '~> 3.3' + pod 'WebRTC-lib', '~> 96.0.0' +end + +# UI tests. +target 'TinodiosUITests' do + project 'Tinodios' + db_pods + app_pods +end + target 'Tinodios' do project 'Tinodios' db_pods - pod 'Firebase' - pod 'FirebaseCore' - pod 'FirebaseMessaging' - pod 'FirebaseAnalytics' - pod 'FirebaseCrashlytics' - pod 'Kingfisher', '~> 5.0' - pod 'MobileVLCKit', '~> 3.4.1b9' - pod 'PhoneNumberKit', '~> 3.3' - pod 'WebRTC-lib', '~> 96.0.0' + app_pods end post_install do | installer | diff --git a/Podfile.lock b/Podfile.lock index 0128662a..5828e1c6 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -173,6 +173,6 @@ SPEC CHECKSUMS: SwiftKeychainWrapper: 6fc49fbf7d4a6b0772917acb0e53a1639f6078d6 WebRTC-lib: 508fe02efa0c1a3a8867082a77d24c9be5d29aeb -PODFILE CHECKSUM: e9f601f4ca354f5670dce2ce6c24f5977405364d +PODFILE CHECKSUM: 4035dae6b590b3116328f9f6d8a57f4a08a25e48 COCOAPODS: 1.11.3 diff --git a/TinodeSDK/model/Acs.swift b/TinodeSDK/model/Acs.swift index a770c9ce..6ada8387 100644 --- a/TinodeSDK/model/Acs.swift +++ b/TinodeSDK/model/Acs.swift @@ -115,6 +115,19 @@ public class Acs: Codable, CustomStringConvertible, Equatable { self.mode = AcsHelper(str: modeStr) } } + public func encode(to encoder: Encoder) throws { + // Used in testing only. + var container = encoder.container(keyedBy: CodingKeys.self) + let givenStr = givenString + if !givenStr.isEmpty { try container.encode(givenStr, forKey: .given) } + + let wantStr = wantString + if !wantStr.isEmpty { try container.encode(wantStr, forKey: .want) } + + let modeStr = modeString + if !modeStr.isEmpty { try container.encode(modeStr, forKey: .mode) } + } + public func isJoiner(for side: Acs.Side) -> Bool { switch side { case .mode: return mode?.isJoiner ?? false diff --git a/TinodeSDK/model/ClientMessages.swift b/TinodeSDK/model/ClientMessages.swift index 457bd7b4..03698290 100644 --- a/TinodeSDK/model/ClientMessages.swift +++ b/TinodeSDK/model/ClientMessages.swift @@ -7,15 +7,15 @@ import Foundation -public class MsgClientHi: Encodable { - let id: String? - let ver: String? +public class MsgClientHi: Codable { + public let id: String? + public let ver: String? // User Agent. - let ua: String? + public let ua: String? // Push notification token. - let dev: String? - let lang: String? - let bkg: Bool? + public let dev: String? + public let lang: String? + public let bkg: Bool? init(id: String?, ver: String?, ua: String?, dev: String?, lang: String?, background: Bool) { self.id = id @@ -73,7 +73,7 @@ public class Credential: Codable, Comparable, CustomStringConvertible { } } -public class MsgClientAcc: Encodable { +public class MsgClientAcc: Codable { var id: String? var user: String? var scheme: String? @@ -112,11 +112,11 @@ public class MsgClientAcc: Encodable { } } -public class MsgClientLogin: Encodable { - let id: String? - let scheme: String? - let secret: String? - var cred: [Credential]? +public class MsgClientLogin: Codable { + public let id: String? + public let scheme: String? + public let secret: String? + public var cred: [Credential]? init(id: String?, scheme: String?, secret: String?, credentials: [Credential]?) { self.id = id @@ -133,7 +133,7 @@ public class MsgClientLogin: Encodable { } } -public class MetaGetData: Encodable { +public class MetaGetData: Codable { /// Load messages/ranges with IDs equal or greater than this (inclusive or closed). let since: Int? /// Load messages/ranges with IDs lower than this (exclusive or open). @@ -146,14 +146,14 @@ public class MetaGetData: Encodable { self.limit = limit } } -public class MetaGetDesc: Encodable { +public class MetaGetDesc: Codable { // ims = If modified since... let ims: Date? public init(ims: Date? = nil) { self.ims = ims } } -public class MetaGetSub: Encodable { +public class MetaGetSub: Codable { let user: String? let ims: Date? let limit: Int? @@ -163,7 +163,7 @@ public class MetaGetSub: Encodable { self.limit = limit } } -public class MsgGetMeta: CustomStringConvertible, Encodable { +public class MsgGetMeta: CustomStringConvertible, Codable { private static let kDescSet = 0x01 private static let kSubSet = 0x02 private static let kDataSet = 0x04 @@ -296,7 +296,7 @@ public class MsgGetMeta: CustomStringConvertible, Encodable { } } -public class MetaSetDesc: Encodable { +public class MetaSetDesc: Codable { var defacs: Defacs? var pub: P? var priv: R? @@ -321,7 +321,7 @@ public class MetaSetDesc: Encodable { } } -public class MetaSetSub: Encodable { +public class MetaSetSub: Codable { let user: String? let mode: String? public init() { @@ -337,7 +337,7 @@ public class MetaSetSub: Encodable { self.mode = mode } } -public class MsgSetMeta: Encodable { +public class MsgSetMeta: Codable { let desc: MetaSetDesc? let sub: MetaSetSub? let tags: [String]? @@ -351,7 +351,7 @@ public class MsgSetMeta: Encodable { } } -public class MsgClientSub: Encodable { +public class MsgClientSub: Codable { var id: String? var topic: String? var set: MsgSetMeta? @@ -364,7 +364,7 @@ public class MsgClientSub: Encodable { } } -public class MsgClientGet: Encodable { +public class MsgClientGet: Codable { let id: String? let topic: String? let what: String? @@ -382,7 +382,7 @@ public class MsgClientGet: Encodable { } } -public class MsgClientSet: Encodable { +public class MsgClientSet: Codable { let id: String? let topic: String? let desc: MetaSetDesc? @@ -402,7 +402,7 @@ public class MsgClientSet: Encodable { } } -public class MsgClientLeave: Encodable { +public class MsgClientLeave: Codable { let id: String? let topic: String? let unsub: Bool? @@ -414,7 +414,7 @@ public class MsgClientLeave: Encodable { } /// Typing, read/received and video call notifications packet. -public class MsgClientNote: Encodable { +public class MsgClientNote: Codable { let topic: String? let what: String? let seq: Int? @@ -432,7 +432,7 @@ public class MsgClientNote: Encodable { } } -public class MsgClientPub: Encodable { +public class MsgClientPub: Codable { let id: String? let topic: String? let noecho: Bool? @@ -448,7 +448,7 @@ public class MsgClientPub: Encodable { } } -public class MsgClientDel: Encodable { +public class MsgClientDel: Codable { static let kStrTopic = "topic" static let kStrMsg = "msg" static let kStrSub = "sub" @@ -520,7 +520,7 @@ public class MsgClientDel: Encodable { } -public class MsgClientExtra: Encodable { +public class MsgClientExtra: Codable { let attachments: [String]? init(attachments: [String]?) { @@ -528,17 +528,17 @@ public class MsgClientExtra: Encodable { } } -public class ClientMessage: Encodable { - var hi: MsgClientHi? - var acc: MsgClientAcc? - var login: MsgClientLogin? - var sub: MsgClientSub? - var get: MsgClientGet? - var set: MsgClientSet? - var leave: MsgClientLeave? - var note: MsgClientNote? - var pub: MsgClientPub? - var del: MsgClientDel? +public class ClientMessage: Codable { + public var hi: MsgClientHi? + public var acc: MsgClientAcc? + public var login: MsgClientLogin? + public var sub: MsgClientSub? + public var get: MsgClientGet? + public var set: MsgClientSet? + public var leave: MsgClientLeave? + public var note: MsgClientNote? + public var pub: MsgClientPub? + public var del: MsgClientDel? // Optional field for sending attachment references. var extra: MsgClientExtra? diff --git a/TinodeSDK/model/ServerMessages.swift b/TinodeSDK/model/ServerMessages.swift index 94164ff3..1d3d7012 100644 --- a/TinodeSDK/model/ServerMessages.swift +++ b/TinodeSDK/model/ServerMessages.swift @@ -7,7 +7,7 @@ import Foundation -public class MsgServerCtrl: Decodable { +public class MsgServerCtrl: Codable { public let id: String? public let topic: String? public let code: Int @@ -64,14 +64,24 @@ public class MsgServerCtrl: Decodable { } return nil } + + // Testing only. + internal init(id: String?, topic: String?, code: Int, text: String, ts: Date, params: [String: JSONValue]?) { + self.id = id + self.topic = topic + self.code = code + self.text = text + self.ts = ts + self.params = params + } } -public class DelValues: Decodable { +public class DelValues: Codable { let clear: Int let delseq: [MsgRange] } -public class MsgServerMeta: Decodable { +public class MsgServerMeta: Codable { public let id: String? public let topic: String? public let ts: Date? @@ -84,6 +94,7 @@ public class MsgServerMeta: Decodable { private enum CodingKeys: String, CodingKey { case id, topic, ts, desc, sub, del, tags, cred } + required public init (from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try? container.decode(String.self, forKey: .id) @@ -103,9 +114,43 @@ public class MsgServerMeta: Decodable { sub = try? container.decode(Array.self, forKey: .sub) } } + + public func encode(to encoder: Encoder) throws { + // Used in testing only. + var container = encoder.container(keyedBy: CodingKeys.self) + if let id = id { try container.encode(id, forKey: .id) } + if let topic = topic { try container.encode(topic, forKey: .topic) } + if let ts = ts { try container.encode(ts, forKey: .ts) } + if let del = del { try container.encode(del, forKey: .del) } + if let tags = tags { try container.encode(tags, forKey: .tags) } + if let cred = cred { try container.encode(cred, forKey: .cred) } + if topic == Tinode.kTopicMe { + if let desc = desc as? DefaultDescription { try container.encode(desc, forKey: .desc) } + if let sub = sub as? Array { try container.encode(sub, forKey: .sub) } + } else if topic == Tinode.kTopicFnd { + if let desc = desc as? FndDescription { try container.encode(desc, forKey: .desc) } + if let sub = sub as? Array { try container.encode(sub, forKey: .sub) } + } else { + if let desc = desc as? DefaultDescription { try container.encode(desc, forKey: .desc) } + if let sub = sub as? Array { try container.encode(sub, forKey: .sub) } + } + } + + // Testing only. + internal init(id: String?, topic: String?, ts: Date, desc: DescriptionProto?, + sub: [SubscriptionProto]?, del: DelValues?, tags: [String]?, cred: [Credential]?) { + self.id = id + self.topic = topic + self.ts = ts + self.desc = desc + self.sub = sub + self.del = del + self.tags = tags + self.cred = cred + } } -open class MsgServerData: Decodable { +open class MsgServerData: Codable { public enum WebRTC: String { case kAccepted = "accepted" case kDeclined = "declined" @@ -131,9 +176,21 @@ open class MsgServerData: Decodable { // todo: make it drafty public var content: Drafty? public init() {} + + // Testing only. + internal init(id: String?, topic: String?, from: String?, ts: Date?, + head: [String: JSONValue]?, seq: Int?, content: Drafty?) { + self.id = id + self.topic = topic + self.from = from + self.ts = ts + self.head = head + self.seq = seq + self.content = content + } } -public class AccessChange: Decodable { +public class AccessChange: Codable { let want: String? let given: String? @@ -142,7 +199,7 @@ public class AccessChange: Decodable { } } -public class MsgServerPres: Decodable { +public class MsgServerPres: Codable { enum What { case kOn, kOff, kUpd, kGone, kTerm, kAcs, kMsg, kUa, kRecv, kRead, kDel, kTags, kUnknown } @@ -192,7 +249,7 @@ public class MsgServerPres: Decodable { } } -public class MsgServerInfo: Decodable { +public class MsgServerInfo: Codable { public var topic: String? public var src: String? public var from: String? @@ -204,7 +261,7 @@ public class MsgServerInfo: Decodable { public var payload: JSONValue? } -public class ServerMessage: Decodable { +public class ServerMessage: Codable { // RFC 7231 HTTP status messages // https://tools.ietf.org/html/rfc7231#section-6 public static let kStatusOk = 200 // 6.3.1 diff --git a/TinodeSDK/model/Subscription.swift b/TinodeSDK/model/Subscription.swift index 46564cc9..9e29d631 100644 --- a/TinodeSDK/model/Subscription.swift +++ b/TinodeSDK/model/Subscription.swift @@ -74,7 +74,7 @@ extension SubscriptionProto { } } -public class Subscription: SubscriptionProto { +public class Subscription: Codable, SubscriptionProto { public var user: String? public var updated: Date? public var deleted: Date? diff --git a/Tinodios.xcodeproj/project.pbxproj b/Tinodios.xcodeproj/project.pbxproj index 76c0e84a..0e264871 100644 --- a/Tinodios.xcodeproj/project.pbxproj +++ b/Tinodios.xcodeproj/project.pbxproj @@ -50,12 +50,15 @@ 0AD5552A21C625BB0027FD16 /* ChatListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AD5552921C625BB0027FD16 /* ChatListInteractor.swift */; }; 0AD6873D27233C0800A39486 /* QuotedAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AD6873C27233C0800A39486 /* QuotedAttachment.swift */; }; 0ADDBC8B2249F617000BD00D /* ChatListViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADDBC8A2249F617000BD00D /* ChatListViewCell.swift */; }; + 0AE21E4E28D21849008F486C /* TinodiosUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE21E4D28D21849008F486C /* TinodiosUITests.swift */; }; + 0AE21E5028D21849008F486C /* TinodiosUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE21E4F28D21849008F486C /* TinodiosUITestsLaunchTests.swift */; }; 0AE50D4422156A250021F7B7 /* FindViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE50D4322156A250021F7B7 /* FindViewController.swift */; }; 0AE50D4622156A4B0021F7B7 /* FindInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE50D4522156A4B0021F7B7 /* FindInteractor.swift */; }; 0AE50D4822156A5B0021F7B7 /* FindPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE50D4722156A5B0021F7B7 /* FindPresenter.swift */; }; 0AE7339C22C33FEF0089C8D8 /* ChatListViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0AE7339B22C33FEF0089C8D8 /* ChatListViewCell.xib */; }; 0AEE3A33283E57230037BEC4 /* CallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEE3A32283E57230037BEC4 /* CallViewController.swift */; }; 0AEE8BEE226456080022EC3F /* UiUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEE8BED226456080022EC3F /* UiUtils.swift */; }; + 0FE526C4E14CE1F2D7FABB30 /* libPods-TinodiosUITests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8941EC0EF6DEEA661A222467 /* libPods-TinodiosUITests.a */; }; 6AB6957DBFD1FB12CA01DCE4 /* libPods-Tinodios.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E30BDE2C5D7E82671DD7DE4 /* libPods-Tinodios.a */; }; 7DDADC5221D5C219009733B5 /* ValidatedCredential.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDADC5121D5C219009733B5 /* ValidatedCredential.swift */; }; 7DDADC5421D5C2E6009733B5 /* CredentialsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDADC5321D5C2E6009733B5 /* CredentialsViewController.swift */; }; @@ -135,6 +138,13 @@ remoteGlobalIDString = 0ACE9E7123813A1D006BE575; remoteInfo = TinodiosNSExtension; }; + 0AE21E5128D21849008F486C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0A0380CC21B3CC8100A3FA0E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0A0380D321B3CC8100A3FA0E; + remoteInfo = Tinodios; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -182,6 +192,7 @@ 0A36776822B09C1900724873 /* EditMembersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMembersViewController.swift; sourceTree = ""; }; 0A3AE3B4274CAD2500CB673E /* ForwardMessageBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardMessageBar.swift; sourceTree = ""; }; 0A3AE3B6274CAD5800CB673E /* ForwardMessageBar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ForwardMessageBar.xib; sourceTree = ""; }; + 0A3EECCC28DA0EEF00313C35 /* TinodiosUITests.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = TinodiosUITests.entitlements; sourceTree = ""; }; 0A431C022244A26200A837F7 /* TinodeSDK.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TinodeSDK.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0A4B11FD2373DC1400199310 /* TagsEditDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsEditDialogView.swift; sourceTree = ""; }; 0A4E1D2F22829C46006C0912 /* ContactsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsManager.swift; sourceTree = ""; }; @@ -209,17 +220,22 @@ 0AD5552921C625BB0027FD16 /* ChatListInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListInteractor.swift; sourceTree = ""; }; 0AD6873C27233C0800A39486 /* QuotedAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotedAttachment.swift; sourceTree = ""; }; 0ADDBC8A2249F617000BD00D /* ChatListViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListViewCell.swift; sourceTree = ""; }; + 0AE21E4B28D21849008F486C /* TinodiosUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TinodiosUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 0AE21E4D28D21849008F486C /* TinodiosUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TinodiosUITests.swift; sourceTree = ""; }; + 0AE21E4F28D21849008F486C /* TinodiosUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TinodiosUITestsLaunchTests.swift; sourceTree = ""; }; 0AE50D4322156A250021F7B7 /* FindViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindViewController.swift; sourceTree = ""; }; 0AE50D4522156A4B0021F7B7 /* FindInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindInteractor.swift; sourceTree = ""; }; 0AE50D4722156A5B0021F7B7 /* FindPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindPresenter.swift; sourceTree = ""; }; 0AE7339B22C33FEF0089C8D8 /* ChatListViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ChatListViewCell.xib; sourceTree = ""; }; 0AEE3A32283E57230037BEC4 /* CallViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallViewController.swift; sourceTree = ""; }; 0AEE8BED226456080022EC3F /* UiUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UiUtils.swift; sourceTree = ""; }; + 4F6E20F7F9A02F33DBC019E7 /* Pods-TinodiosUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TinodiosUITests.release.xcconfig"; path = "Pods/Target Support Files/Pods-TinodiosUITests/Pods-TinodiosUITests.release.xcconfig"; sourceTree = ""; }; 5900B7863CDFB018A86DD3AC /* Pods-Tinodios.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tinodios.release.xcconfig"; path = "Pods/Target Support Files/Pods-Tinodios/Pods-Tinodios.release.xcconfig"; sourceTree = ""; }; 5E30BDE2C5D7E82671DD7DE4 /* libPods-Tinodios.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Tinodios.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 75BC852198885F22E865D563 /* Pods-Tinodios.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tinodios.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Tinodios/Pods-Tinodios.debug.xcconfig"; sourceTree = ""; }; 7DDADC5121D5C219009733B5 /* ValidatedCredential.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidatedCredential.swift; sourceTree = ""; }; 7DDADC5321D5C2E6009733B5 /* CredentialsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CredentialsViewController.swift; sourceTree = ""; }; + 8941EC0EF6DEEA661A222467 /* libPods-TinodiosUITests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-TinodiosUITests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; B008593B22741B89002C5168 /* FullFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullFormatter.swift; sourceTree = ""; }; B01120B72357469300EFCB1F /* devel.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = devel.xcconfig; sourceTree = ""; }; B01120B8235746BF00EFCB1F /* prod.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = prod.xcconfig; sourceTree = ""; }; @@ -314,6 +330,7 @@ B0DF032E2282FF46000F9492 /* SendMessageBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageBar.swift; sourceTree = ""; }; B0DF03302283FFD8000F9492 /* PlaceholderTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderTextView.swift; sourceTree = ""; }; B0F98C8222E381C300444121 /* MessageMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageMenuItem.swift; sourceTree = ""; }; + B33FA5FDB8E984980BADDAB2 /* Pods-TinodiosUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TinodiosUITests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TinodiosUITests/Pods-TinodiosUITests.debug.xcconfig"; sourceTree = ""; }; B36DC000235D850000724348 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/LaunchScreen.strings"; sourceTree = ""; }; FA37AD23238B0D6F00301261 /* KeyboardInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardInfo.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -337,6 +354,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 0AE21E4828D21849008F486C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0FE526C4E14CE1F2D7FABB30 /* libPods-TinodiosUITests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -351,6 +376,7 @@ 0A431C022244A26200A837F7 /* TinodeSDK.framework */, 0A0380D621B3CC8100A3FA0E /* Tinodios */, 0ACE9E7323813A1D006BE575 /* TinodiosNSExtension */, + 0AE21E4C28D21849008F486C /* TinodiosUITests */, 0A0380D521B3CC8100A3FA0E /* Products */, 579A1EAE0D0B7D996BF48345 /* Pods */, 4805E89223B1ECF4F9F439D5 /* Frameworks */, @@ -362,6 +388,7 @@ children = ( 0A0380D421B3CC8100A3FA0E /* Tinodios.app */, 0ACE9E7223813A1D006BE575 /* TinodiosNSExtension.appex */, + 0AE21E4B28D21849008F486C /* TinodiosUITests.xctest */, ); name = Products; sourceTree = ""; @@ -461,12 +488,23 @@ path = TinodiosNSExtension; sourceTree = ""; }; + 0AE21E4C28D21849008F486C /* TinodiosUITests */ = { + isa = PBXGroup; + children = ( + 0A3EECCC28DA0EEF00313C35 /* TinodiosUITests.entitlements */, + 0AE21E4D28D21849008F486C /* TinodiosUITests.swift */, + 0AE21E4F28D21849008F486C /* TinodiosUITestsLaunchTests.swift */, + ); + path = TinodiosUITests; + sourceTree = ""; + }; 4805E89223B1ECF4F9F439D5 /* Frameworks */ = { isa = PBXGroup; children = ( B091BA472809048000317CDC /* AVFoundation.framework */, 0ACE9E7E23814157006BE575 /* TinodiosDB.framework */, 5E30BDE2C5D7E82671DD7DE4 /* libPods-Tinodios.a */, + 8941EC0EF6DEEA661A222467 /* libPods-TinodiosUITests.a */, ); name = Frameworks; sourceTree = ""; @@ -476,6 +514,8 @@ children = ( 75BC852198885F22E865D563 /* Pods-Tinodios.debug.xcconfig */, 5900B7863CDFB018A86DD3AC /* Pods-Tinodios.release.xcconfig */, + B33FA5FDB8E984980BADDAB2 /* Pods-TinodiosUITests.debug.xcconfig */, + 4F6E20F7F9A02F33DBC019E7 /* Pods-TinodiosUITests.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -597,13 +637,34 @@ productReference = 0ACE9E7223813A1D006BE575 /* TinodiosNSExtension.appex */; productType = "com.apple.product-type.app-extension"; }; + 0AE21E4A28D21849008F486C /* TinodiosUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0AE21E5528D21849008F486C /* Build configuration list for PBXNativeTarget "TinodiosUITests" */; + buildPhases = ( + A775F41E632ADA951685301C /* [CP] Check Pods Manifest.lock */, + 0AE21E4728D21849008F486C /* Sources */, + 0AE21E4828D21849008F486C /* Frameworks */, + 0AE21E4928D21849008F486C /* Resources */, + A71215C9F652A1468BB98E79 /* [CP] Embed Pods Frameworks */, + 37009181C8B2B0198CE4CBBA /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + 0AE21E5228D21849008F486C /* PBXTargetDependency */, + ); + name = TinodiosUITests; + productName = TinodiosUITests; + productReference = 0AE21E4B28D21849008F486C /* TinodiosUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 0A0380CC21B3CC8100A3FA0E /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1110; + LastSwiftUpdateCheck = 1400; LastUpgradeCheck = 1210; ORGANIZATIONNAME = "Tinode LLC"; TargetAttributes = { @@ -613,6 +674,10 @@ 0ACE9E7123813A1D006BE575 = { CreatedOnToolsVersion = 11.1; }; + 0AE21E4A28D21849008F486C = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 0A0380D321B3CC8100A3FA0E; + }; }; }; buildConfigurationList = 0A0380CF21B3CC8100A3FA0E /* Build configuration list for PBXProject "Tinodios" */; @@ -634,6 +699,7 @@ targets = ( 0A0380D321B3CC8100A3FA0E /* Tinodios */, 0ACE9E7123813A1D006BE575 /* TinodiosNSExtension */, + 0AE21E4A28D21849008F486C /* TinodiosUITests */, ); }; /* End PBXProject section */ @@ -675,9 +741,33 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 0AE21E4928D21849008F486C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 37009181C8B2B0198CE4CBBA /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-TinodiosUITests/Pods-TinodiosUITests-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-TinodiosUITests/Pods-TinodiosUITests-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-TinodiosUITests/Pods-TinodiosUITests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 46966AD24309BE3586965D61 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -713,6 +803,45 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Tinodios/Pods-Tinodios-resources.sh\"\n"; showEnvVarsInLog = 0; }; + A71215C9F652A1468BB98E79 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-TinodiosUITests/Pods-TinodiosUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-TinodiosUITests/Pods-TinodiosUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-TinodiosUITests/Pods-TinodiosUITests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + A775F41E632ADA951685301C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-TinodiosUITests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; B094C9AA2315841E00C98B64 /* Assign build version from git */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -890,6 +1019,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 0AE21E4728D21849008F486C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0AE21E5028D21849008F486C /* TinodiosUITestsLaunchTests.swift in Sources */, + 0AE21E4E28D21849008F486C /* TinodiosUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -898,6 +1036,11 @@ target = 0ACE9E7123813A1D006BE575 /* TinodiosNSExtension */; targetProxy = 0ACE9E7723813A1D006BE575 /* PBXContainerItemProxy */; }; + 0AE21E5228D21849008F486C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0A0380D321B3CC8100A3FA0E /* Tinodios */; + targetProxy = 0AE21E5128D21849008F486C /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -1215,6 +1358,47 @@ }; name = Release; }; + 0AE21E5328D21849008F486C /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B33FA5FDB8E984980BADDAB2 /* Pods-TinodiosUITests.debug.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = TinodiosUITests/TinodiosUITests.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.tinode.ios.TinodiosUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Tinodios; + }; + name = Debug; + }; + 0AE21E5428D21849008F486C /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4F6E20F7F9A02F33DBC019E7 /* Pods-TinodiosUITests.release.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = TinodiosUITests/TinodiosUITests.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.tinode.ios.TinodiosUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Tinodios; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1245,6 +1429,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; + 0AE21E5528D21849008F486C /* Build configuration list for PBXNativeTarget "TinodiosUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0AE21E5328D21849008F486C /* Debug */, + 0AE21E5428D21849008F486C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; /* End XCConfigurationList section */ }; rootObject = 0A0380CC21B3CC8100A3FA0E /* Project object */; diff --git a/Tinodios.xcodeproj/xcshareddata/xcschemes/Tinodios.xcscheme b/Tinodios.xcodeproj/xcshareddata/xcschemes/Tinodios.xcscheme index b730665a..98d84a29 100644 --- a/Tinodios.xcodeproj/xcshareddata/xcschemes/Tinodios.xcscheme +++ b/Tinodios.xcodeproj/xcshareddata/xcschemes/Tinodios.xcscheme @@ -28,6 +28,17 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + Bool { @@ -853,14 +876,13 @@ extension CallViewController: TinodeVideoCallDelegate { } func handleAcceptedMsg() { + assert(Thread.isMainThread) self.playSoundEffect(nil) - DispatchQueue.main.async { - // Stop animation, hide peer name & avatar. - self.dialingAnimation(on: false) - self.peerNameLabel.alpha = 0 - self.peerAvatarImageView.alpha = 0 - } + // Stop animation, hide peer name & avatar. + self.dialingAnimation(on: false) + self.peerNameLabel.alpha = 0 + self.peerAvatarImageView.alpha = 0 // The callee has informed us (the caller) of the call acceptance. guard self.webRTCClient.createPeerConnection() else { Cache.log.error("CallVC.handleAcceptedMsg - createPeerConnection failed") @@ -868,8 +890,9 @@ extension CallViewController: TinodeVideoCallDelegate { return } } - + func handleOfferMsg(with payload: JSONValue?) { + assert(Thread.isMainThread) guard case let .dict(offer) = payload, let desc = RTCSessionDescription.deserialize(from: offer) else { Cache.log.error("CallVC.handleOfferMsg - invalid offer payload") self.handleCallClose() @@ -880,52 +903,38 @@ extension CallViewController: TinodeVideoCallDelegate { self.handleCallClose() return } - self.webRTCClient.handleRemoteDescription(desc) + self.webRTCClient.handleRemoteOfferDescription(desc) } - + func handleAnswerMsg(with payload: JSONValue?) { + assert(Thread.isMainThread) guard case let .dict(answer) = payload, let desc = RTCSessionDescription.deserialize(from: answer) else { Cache.log.error("CallVC.handleAnswerMsg - invalid answer payload") self.handleCallClose() return } - self.webRTCClient.localPeer?.setRemoteDescription(desc, completionHandler: { (error) in - if let e = error { - Cache.log.error("CallVC - error setting remote description %@", e.localizedDescription) - self.handleCallClose() - } - self.markConnectionSetupComplete() - }) + self.webRTCClient.handleRemoteAnswerDescription(desc) } - + func handleIceCandidateMsg(with payload: JSONValue?) { + assert(Thread.isMainThread) guard case let .dict(iceDict) = payload, let candidate = RTCIceCandidate.deserialize(from: iceDict) else { Cache.log.error("CallVC.handleIceCandidateMsg - invalid ICE candidate payload") self.handleCallClose() return } - if self.callInitialSetupComplete { - self.webRTCClient.localPeer?.add(candidate) { err in - if let err = err { - Cache.log.error("CallVC.handleIceCandidateMsg - could not add ICE candidate: %@", err.localizedDescription) - self.handleCallClose() - return - } - } - } else { - self.webRTCClient.addIceCandidateToCache(candidate) - } + self.webRTCClient.handleRemoteIceCandidate(candidate, saveInCache: !self.callInitialSetupComplete) } - + func handleRemoteHangup() { + assert(Thread.isMainThread) self.playSoundEffect("call-end") self.handleCallClose() } func handleRinging() { - DispatchQueue.main.async { - self.dialingAnimation(on: true) - } + assert(Thread.isMainThread) + self.dialingAnimation(on: true) self.playSoundEffect("call-out", loop: true) } } diff --git a/Tinodios/ChatListViewController.swift b/Tinodios/ChatListViewController.swift index 0b850727..218b9144 100644 --- a/Tinodios/ChatListViewController.swift +++ b/Tinodios/ChatListViewController.swift @@ -95,6 +95,8 @@ class ChatListViewController: UITableViewController, ChatListDisplayLogic { func appBecameActive() { self.interactor?.setup() self.interactor?.attachToMeTopic() + // Reload topics after the app became active. + self.interactor?.loadAndPresentTopics() } @objc func appGoingInactive() { diff --git a/TinodiosUITests/TinodiosUITests.entitlements b/TinodiosUITests/TinodiosUITests.entitlements new file mode 100644 index 00000000..c326c834 --- /dev/null +++ b/TinodiosUITests/TinodiosUITests.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/TinodiosUITests/TinodiosUITests.swift b/TinodiosUITests/TinodiosUITests.swift new file mode 100644 index 00000000..e50abff2 --- /dev/null +++ b/TinodiosUITests/TinodiosUITests.swift @@ -0,0 +1,407 @@ +// +// TinodiosUITests.swift +// TinodiosUITests +// +// Copyright © 2022 Tinode LLC. All rights reserved. +// + +import XCTest + +import Network +@testable import TinodeSDK + +class FakeTinodeServer { + var listener: NWListener + var connectedClients: [NWConnection] = [] + + // Request types. + enum RequestType { + case none, hi, acc, login, sub, get, set, pub, leave, note, del + } + // Request handlers. + var requestHandlers: [RequestType : ((ClientMessage) -> [ServerMessage])] = [:] + + init(port: UInt16) { + let parameters = NWParameters(tls: nil) + parameters.allowLocalEndpointReuse = true + parameters.includePeerToPeer = true + + let wsOptions = NWProtocolWebSocket.Options() + wsOptions.autoReplyPing = true + + parameters.defaultProtocolStack.applicationProtocols.insert(wsOptions, at: 0) + + do { + if let port = NWEndpoint.Port(rawValue: port) { + listener = try NWListener(using: parameters, on: port) + } else { + fatalError("Unable to start WebSocket server on port \(port)") + } + } catch { + fatalError(error.localizedDescription) + } + } + + func startServer() { + let serverQueue = DispatchQueue(label: "ServerQueue") + + listener.newConnectionHandler = { newConnection in + print("New connection connecting") + + func receive() { + newConnection.receiveMessage { (data, context, isComplete, error) in + if let data = data, let context = context { + print("Received a new message from client") + try! self.handleClientMessage(data: data, context: context, stringVal: "", connection: newConnection) + receive() + } + } + } + receive() + + newConnection.stateUpdateHandler = { state in + switch state { + case .ready: + print("Client ready") + case .failed(let error): + print("Client connection failed \(error.localizedDescription)") + case .waiting(let error): + print("Waiting for long time \(error.localizedDescription)") + default: + break + } + } + + newConnection.start(queue: serverQueue) + } + + listener.stateUpdateHandler = { state in + print(state) + switch state { + case .ready: + print("Server Ready") + case .failed(let error): + print("Server failed with \(error.localizedDescription)") + default: + break + } + } + + listener.start(queue: serverQueue) + } + + func stopServer() { + listener.cancel() + } + + func handleClientMessage(data: Data, context: NWConnection.ContentContext, stringVal: String, connection: NWConnection) throws { + let message = try Tinode.jsonDecoder.decode(ClientMessage.self, from: data) + + print("--> received: \(String(decoding: data, as: UTF8.self))") + var reqType: RequestType = .none + if message.hi != nil { + reqType = .hi + } else if message.login != nil { + reqType = .login + } else if message.sub != nil { + reqType = .sub + } else if message.get != nil { + reqType = .get + } else if message.set != nil { + reqType = .set + } else if message.pub != nil { + reqType = .pub + } else if message.leave != nil { + reqType = .leave + } else if message.note != nil { + reqType = .note + } else if message.del != nil { + reqType = .del + } + self.requestHandlers[reqType]?(message).forEach { self.sendResponse(response: $0, into: connection) } + } + + func addHandler(forRequestType type: RequestType, handler: @escaping ((ClientMessage) -> [ServerMessage])) { + self.requestHandlers[type] = handler + } + + func sendResponse(response: ServerMessage, into connection: NWConnection) { + let metadata = NWProtocolWebSocket.Metadata(opcode: .text) + let context = NWConnection.ContentContext(identifier: "textContext", + metadata: [metadata]) + do { + let data = try Tinode.jsonEncoder.encode(response) + print("sending --> \(String(decoding: data, as: UTF8.self))") + connection.send(content: data, contentContext: context, isComplete: true, + completion: .contentProcessed({ error in + if let error = error { + print(error.localizedDescription) + } + })) + } catch { + print("Error sending response: \(error)") + } + } +} + +final class TinodiosUITests: XCTestCase { + let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + var tinodeServer: FakeTinodeServer! + var app: XCUIApplication! + + // Delete installed Tinode app. + private func deleteTinode() { + app.terminate() + let icon = springboard.icons["Tinode"] + if icon.exists { + let iconFrame = icon.frame + let springboardFrame = springboard.frame + icon.press(forDuration: 5) + + // Tap the little "-" button at approximately where it is. The "-" is not exposed directly + springboard.coordinate(withNormalizedOffset: CGVector(dx: (iconFrame.minX + 3) / springboardFrame.maxX, dy: (iconFrame.minY + 3) / springboardFrame.maxY)).tap() + + springboard.alerts.buttons["Delete App"].tap() + // Confirm the choice once again. + springboard.alerts.buttons["Delete"].tap() + } + } + + override func setUpWithError() throws { + app = XCUIApplication() + app.launch() + tinodeServer = FakeTinodeServer(port: 6060) + tinodeServer.startServer() + + continueAfterFailure = false + } + + override func tearDownWithError() throws { + deleteTinode() + tinodeServer.requestHandlers.removeAll() + tinodeServer.stopServer() + } + + // Tinode message handlers. + private func hiHandler() { + tinodeServer.addHandler(forRequestType: .hi, handler: { req in + let hi = req.hi! + let response = ServerMessage() + response.ctrl = MsgServerCtrl(id: hi.id, topic: nil, code: 200, text: "", ts: Date(), params: nil) + return [response] + }) + } + + private func loginHandler(success: Bool) { + tinodeServer.addHandler(forRequestType: .login, handler: { req in + let login = req.login! + let response = ServerMessage() + response.ctrl = success ? + MsgServerCtrl(id: login.id, topic: nil, code: 200, text: "ok", ts: Date(), + params: ["authlvl": .string("auth"), "token": .string("fake"), + "user": .string("usrAlice")]) : + MsgServerCtrl(id: login.id, topic: nil, code: 401, text: "authentication failed", ts: Date(), params: nil) + return [response] + }) + } + + private func subHandler() { + tinodeServer.addHandler(forRequestType: .sub, handler: { req in + let sreq = req.sub! + if sreq.topic == "me" { + if let get = sreq.get, get.what.split(separator: " ").sorted().elementsEqual(["cred", "desc", "sub", "tags"]) { + let now = Date() + let responseCtrl = ServerMessage() + responseCtrl.ctrl = MsgServerCtrl(id: sreq.id, topic: sreq.topic, code: 200, text: "ok", ts: now, params: nil) + + let metaDesc = ServerMessage() + let desc = Description() + desc.created = now.addingTimeInterval(-86400) + desc.updated = desc.created + desc.touched = desc.created + desc.defacs = Defacs(auth: "JRWPA", anon: "N") + desc.pub = TheCard(fn: "Alice") + desc.priv = ["comment": .string("no comment")] + metaDesc.meta = MsgServerMeta(id: sreq.id, topic: "me", ts: now, desc: desc, sub: nil, del: nil, tags: nil, cred: nil) + + let metaSub = ServerMessage() + let sub = DefaultSubscription() + sub.topic = "usrBob" + sub.updated = now.addingTimeInterval(-86400) + sub.read = 2 + sub.recv = 2 + sub.pub = TheCard(fn: "Bob") + sub.priv = ["comment": .string("bla")] + sub.acs = Acs(given: "JRWPS", want: "JRWPS", mode: "JRWPS") + metaSub.meta = MsgServerMeta(id: sreq.id, topic: "me", ts: now, desc: nil, sub: [sub], del: nil, tags: nil, cred: nil) + return [responseCtrl, metaDesc, metaSub] + } + } else if sreq.topic == "usrBob" { + if let get = sreq.get, get.what.split(separator: " ").sorted().elementsEqual(["data", "del", "desc", "sub"]) { + let now = Date() + let responseCtrl = ServerMessage() + responseCtrl.ctrl = MsgServerCtrl(id: sreq.id, topic: sreq.topic, code: 200, text: "ok", ts: now, params: nil) + + let metaDesc = ServerMessage() + let desc = Description() + desc.acs = Acs(given: "JRWPA", want: "JRWPA", mode: "JRWPA") + desc.seen = LastSeen(when: now.addingTimeInterval(-100), ua: "my UA") + metaDesc.meta = MsgServerMeta(id: sreq.id, topic: sreq.topic, ts: now, desc: desc, sub: nil, del: nil, tags: nil, cred: nil) + + let metaSub = ServerMessage() + let sub1 = DefaultSubscription() + sub1.topic = "usrBob" + sub1.updated = now.addingTimeInterval(-100) + sub1.read = 2 + sub1.recv = 2 + sub1.acs = Acs(given: "JRWPS", want: "JRWPS", mode: "JRWPS") + + let sub2 = DefaultSubscription() + sub2.topic = "usrAlice" + sub2.updated = now.addingTimeInterval(-100) + sub2.read = 2 + sub2.recv = 2 + sub2.acs = Acs(given: "JRWPS", want: "JRWPS", mode: "JRWPS") + + metaSub.meta = MsgServerMeta(id: sreq.id, topic: sreq.topic, ts: now, desc: nil, sub: [sub1, sub2], del: nil, tags: nil, cred: nil) + + let data1 = ServerMessage() + data1.data = MsgServerData(id: sreq.id, topic: sreq.topic, from: sreq.topic, ts: now.addingTimeInterval(-2000), head: nil, seq: 1, content: Drafty(plainText: "hello message")) + + let data2 = ServerMessage() + data2.data = MsgServerData(id: sreq.id, topic: sreq.topic, from: "usrAlice", ts: now.addingTimeInterval(-1000), head: nil, seq: 2, content: Drafty(plainText: "wassup?")) + + return [responseCtrl, metaDesc, metaSub, data1, data2] + } + } + return [] + }) + } + + private func allowLocalNotifications() -> NSObjectProtocol { + return addUIInterruptionMonitor(withDescription: "Local Notifications") { (alert) -> Bool in + let notifPermission = "Would Like to Send You Notifications" + if alert.label.contains(notifPermission) { + alert.buttons["Allow"].tap() + return true + } + return false + } + } + + private func logIntoTinode(shouldSucceed: Bool) { + // Log in as "alice". + let elementsQuery = app.scrollViews.otherElements + let loginText = elementsQuery.textFields["usernameText"] + XCTAssertTrue(loginText.exists) + loginText.tap() + loginText.typeText("alice") + + let passwordText = elementsQuery.secureTextFields["passwordText"] + XCTAssertTrue(passwordText.exists) + passwordText.tap() + passwordText.typeText("alice123") + + let signInButton = elementsQuery.staticTexts["Sign In"] + XCTAssertTrue(signInButton.exists) + signInButton.tap() + + // Check if user name field is still available. If so login has failed. + XCTAssertNotEqual(loginText.exists, shouldSucceed) + } + + func testLoginFailure() throws { + hiHandler() + loginHandler(success: false) + + logIntoTinode(shouldSucceed: false) + } + + func testLoginBasic() throws { + hiHandler() + loginHandler(success: true) + subHandler() + + // Allow notifications. + let monitor = allowLocalNotifications() + defer { removeUIInterruptionMonitor(monitor) } + + logIntoTinode(shouldSucceed: true) + + // "Allow Notifications?" dialog. Make sure modal dialog handler gets triggered. + app.tap() + + let table = app.tables.element + XCTAssertTrue(table.exists) + + let cell = table.cells.element(boundBy: 0) + // Wait for UI to update asynchronously. + let exists = NSPredicate(format: "exists == 1") + expectation(for: exists, evaluatedWith: cell) + waitForExpectations(timeout: 5, handler: nil) + + XCTAssertTrue(cell.staticTexts["Bob"].waitForExistence(timeout: 5)) + } + + func testPublishBasic() { + hiHandler() + loginHandler(success: true) + subHandler() + tinodeServer.addHandler(forRequestType: .pub, handler: { req in + let preq = req.pub! + let now = Date() + let responseCtrl = ServerMessage() + responseCtrl.ctrl = MsgServerCtrl(id: preq.id, topic: preq.topic, code: 200, text: "accepted", ts: now, + params: ["seq": .int(3)]) + return [responseCtrl] + }) + + // Allow notifications. + let monitor = allowLocalNotifications() + defer { removeUIInterruptionMonitor(monitor) } + + logIntoTinode(shouldSucceed: true) + + // "Allow Notifications?" dialog. Make sure modal dialog handler gets triggered. + app.tap() + + let table = app.tables.element + XCTAssertTrue(table.exists) + + let cell = table.cells.element(boundBy: 0) + // Wait for UI to update asynchronously. + let exists = NSPredicate(format: "exists == 1") + expectation(for: exists, evaluatedWith: cell) + waitForExpectations(timeout: 5, handler: nil) + + XCTAssertTrue(cell.staticTexts["Bob"].waitForExistence(timeout: 5)) + + cell.tap() + let collectionViewsQuery = app.collectionViews + sleep(1) + + // 2 messages. + XCTAssertEqual(collectionViewsQuery.cells.count, 2) + + // Send another one. + let inputField = app.children(matching: .window).element(boundBy: 1).children(matching: .other).element.children(matching: .other).element(boundBy: 1) + inputField.tap() + inputField.typeText("new msg") + + let arrowUpCircleButton = app.buttons["Arrow Up Circle"] + arrowUpCircleButton.tap() + + sleep(1) + // We should now have 3 messages. + XCTAssertEqual(collectionViewsQuery.cells.count, 3) + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/TinodiosUITests/TinodiosUITestsLaunchTests.swift b/TinodiosUITests/TinodiosUITestsLaunchTests.swift new file mode 100644 index 00000000..166aeaea --- /dev/null +++ b/TinodiosUITests/TinodiosUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// TinodiosUITestsLaunchTests.swift +// TinodiosUITests +// +// Copyright © 2022 Tinode LLC. All rights reserved. +// + +import XCTest + +final class TinodiosUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +}