diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 266ed7a9f..0c736aa13 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -805,6 +805,17 @@ FD859F0027C4691300510D0C /* MockDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EFF27C4691300510D0C /* MockDataGenerator.swift */; }; FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */; }; FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */; }; + FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908627D7047F005DAE71 /* RoomSpec.swift */; }; + FDC2908927D70656005DAE71 /* RoomPollInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */; }; + FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908A27D707F3005DAE71 /* SendMessageRequestSpec.swift */; }; + FDC2908D27D70905005DAE71 /* UpdateMessageRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908C27D70905005DAE71 /* UpdateMessageRequestSpec.swift */; }; + FDC2908F27D70938005DAE71 /* SendDirectMessageRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908E27D70938005DAE71 /* SendDirectMessageRequestSpec.swift */; }; + FDC2909127D709CA005DAE71 /* SOGSMessageSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909027D709CA005DAE71 /* SOGSMessageSpec.swift */; }; + FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909327D710B4005DAE71 /* SOGSEndpointSpec.swift */; }; + FDC2909627D71252005DAE71 /* SOGSErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */; }; + FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */; }; + FDC2909A27D71376005DAE71 /* NonceGeneratorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909927D71376005DAE71 /* NonceGeneratorSpec.swift */; }; + FDC2909C27D713D2005DAE71 /* SodiumProtocolsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909B27D713D2005DAE71 /* SodiumProtocolsSpec.swift */; }; FDC4380927B31D4E00C60D73 /* SOGSError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380827B31D4E00C60D73 /* SOGSError.swift */; }; FDC4381527B329CE00C60D73 /* NonceGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381427B329CE00C60D73 /* NonceGenerator.swift */; }; FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381627B32EC700C60D73 /* Personalization.swift */; }; @@ -1940,6 +1951,17 @@ FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = ""; }; FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = ""; }; FD9039443F7CB729CF71350E /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; sourceTree = ""; }; + FDC2908627D7047F005DAE71 /* RoomSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSpec.swift; sourceTree = ""; }; + FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollInfoSpec.swift; sourceTree = ""; }; + FDC2908A27D707F3005DAE71 /* SendMessageRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequestSpec.swift; sourceTree = ""; }; + FDC2908C27D70905005DAE71 /* UpdateMessageRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateMessageRequestSpec.swift; sourceTree = ""; }; + FDC2908E27D70938005DAE71 /* SendDirectMessageRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageRequestSpec.swift; sourceTree = ""; }; + FDC2909027D709CA005DAE71 /* SOGSMessageSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSMessageSpec.swift; sourceTree = ""; }; + FDC2909327D710B4005DAE71 /* SOGSEndpointSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSEndpointSpec.swift; sourceTree = ""; }; + FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSErrorSpec.swift; sourceTree = ""; }; + FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalizationSpec.swift; sourceTree = ""; }; + FDC2909927D71376005DAE71 /* NonceGeneratorSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonceGeneratorSpec.swift; sourceTree = ""; }; + FDC2909B27D713D2005DAE71 /* SodiumProtocolsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SodiumProtocolsSpec.swift; sourceTree = ""; }; FDC4380827B31D4E00C60D73 /* SOGSError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSError.swift; sourceTree = ""; }; FDC4381427B329CE00C60D73 /* NonceGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonceGenerator.swift; sourceTree = ""; }; FDC4381627B32EC700C60D73 /* Personalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Personalization.swift; sourceTree = ""; }; @@ -3397,6 +3419,7 @@ FDC4383D27B4708600C60D73 /* Atomic.swift */, FD83B9A927CF149D005E1583 /* ContactUtilities.swift */, FD859EF127BF6BA200510D0C /* Data+Utilities.swift */, + FDC438C027BB4E6800C60D73 /* Dependencies.swift */, C38EF309255B6DBE007E1867 /* DeviceSleepManager.swift */, C37F53E8255BA9BB002AEA92 /* Environment.h */, C37F5402255BA9ED002AEA92 /* Environment.m */, @@ -3865,6 +3888,12 @@ FD83B9C227CF33F7005E1583 /* ServerSpec.swift */, FD83B9C427CF3E2A005E1583 /* OpenGroupSpec.swift */, FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */, + FDC2908627D7047F005DAE71 /* RoomSpec.swift */, + FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */, + FDC2908A27D707F3005DAE71 /* SendMessageRequestSpec.swift */, + FDC2908C27D70905005DAE71 /* UpdateMessageRequestSpec.swift */, + FDC2909027D709CA005DAE71 /* SOGSMessageSpec.swift */, + FDC2908E27D70938005DAE71 /* SendDirectMessageRequestSpec.swift */, ); path = Models; sourceTree = ""; @@ -3885,6 +3914,18 @@ path = Views; sourceTree = ""; }; + FDC2909227D710A9005DAE71 /* Types */ = { + isa = PBXGroup; + children = ( + FDC2909327D710B4005DAE71 /* SOGSEndpointSpec.swift */, + FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */, + FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */, + FDC2909927D71376005DAE71 /* NonceGeneratorSpec.swift */, + FDC2909B27D713D2005DAE71 /* SodiumProtocolsSpec.swift */, + ); + path = Types; + sourceTree = ""; + }; FDC4380727B31D3A00C60D73 /* Types */ = { isa = PBXGroup; children = ( @@ -3892,7 +3933,6 @@ FDC4380827B31D4E00C60D73 /* SOGSError.swift */, FDC4381627B32EC700C60D73 /* Personalization.swift */, FDC4381427B329CE00C60D73 /* NonceGenerator.swift */, - FDC438C027BB4E6800C60D73 /* Dependencies.swift */, FDC438C227BB512200C60D73 /* SodiumProtocols.swift */, ); path = Types; @@ -3972,6 +4012,7 @@ isa = PBXGroup; children = ( FD83B9C127CF33EE005E1583 /* Models */, + FDC2909227D710A9005DAE71 /* Types */, FDC4389927BA002500C60D73 /* OpenGroupAPISpec.swift */, ); path = "Open Groups"; @@ -5578,16 +5619,27 @@ buildActionMask = 2147483647; files = ( FD859EFA27C2F5C500510D0C /* TestGenericHash.swift in Sources */, + FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */, + FDC2909127D709CA005DAE71 /* SOGSMessageSpec.swift in Sources */, + FDC2909627D71252005DAE71 /* SOGSErrorSpec.swift in Sources */, + FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */, + FDC2909C27D713D2005DAE71 /* SodiumProtocolsSpec.swift in Sources */, FD83B9C327CF33F7005E1583 /* ServerSpec.swift in Sources */, FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */, + FDC2909A27D71376005DAE71 /* NonceGeneratorSpec.swift in Sources */, FD859EF827C2F58900510D0C /* TestAeadXChaCha20Poly1305Ietf.swift in Sources */, + FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */, FD859EF427C2F49200510D0C /* TestSodium.swift in Sources */, FD859EFC27C2F60700510D0C /* TestEd25519.swift in Sources */, FD83B9C027CF2294005E1583 /* TestConstants.swift in Sources */, FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */, FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */, + FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */, + FDC2908F27D70938005DAE71 /* SendDirectMessageRequestSpec.swift in Sources */, FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */, FD859EF627C2F52C00510D0C /* TestSign.swift in Sources */, + FDC2908927D70656005DAE71 /* RoomPollInfoSpec.swift in Sources */, + FDC2908D27D70905005DAE71 /* UpdateMessageRequestSpec.swift in Sources */, FDC4389D27BA01F000C60D73 /* TestStorage.swift in Sources */, FD83B9D227D59495005E1583 /* TestUserDefaults.swift in Sources */, ); diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 5f5769205..d91c984e8 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -813,7 +813,8 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak self] _ in let publicKey = message.authorId guard let openGroup = Storage.shared.getOpenGroup(for: threadID) else { return } - let promise = OpenGroupAPI.userBanAndDeleteAllMessage(publicKey, for: [openGroup.room], on: openGroup.server) + + let promise = OpenGroupAPI.userBanAndDeleteAllMessage(publicKey, from: [openGroup.room], on: openGroup.server) promise.catch(on: DispatchQueue.main) { _ in OWSAlerts.showErrorAlert(message: NSLocalizedString("context_menu_ban_user_error_alert_message", comment: "")) } diff --git a/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift b/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift index 2d961910c..fb3ac4e41 100644 --- a/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift +++ b/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift @@ -143,13 +143,13 @@ protocol BatchRequestInfoType { // MARK: - Convenience public extension Decodable { - static func decoded(from data: Data, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) throws -> Self { + static func decoded(from data: Data, using dependencies: Dependencies = Dependencies()) throws -> Self { return try data.decoded(as: Self.self, using: dependencies) } } extension Promise where T == (OnionRequestResponseInfoType, Data?) { - func decoded(as types: OpenGroupAPI.BatchResponseTypes, on queue: DispatchQueue? = nil, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise { + func decoded(as types: OpenGroupAPI.BatchResponseTypes, on queue: DispatchQueue? = nil, using dependencies: Dependencies = Dependencies()) -> Promise { self.map(on: queue) { responseInfo, maybeData -> OpenGroupAPI.BatchResponse in // Need to split the data into an array of data so each item can be Decoded correctly guard let data: Data = maybeData else { throw HTTP.Error.parsingFailed } diff --git a/SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift b/SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift index 9523f5d33..de43a68a6 100644 --- a/SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift +++ b/SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift @@ -108,7 +108,7 @@ extension OpenGroupAPI.RoomPollInfo { defaultWrite: room.defaultWrite, upload: room.upload, defaultUpload: room.defaultUpload, - details: nil + details: room ) } } diff --git a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift index c50620960..296bd8467 100644 --- a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift +++ b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift @@ -3,7 +3,7 @@ import Foundation extension OpenGroupAPI { - public struct Message: Codable { + public struct Message: Codable, Equatable { enum CodingKeys: String, CodingKey { case id case sender = "session_id" @@ -47,7 +47,7 @@ extension OpenGroupAPI.Message { guard let sender: String = maybeSender, let data = Data(base64Encoded: base64EncodedData), let signature = Data(base64Encoded: base64EncodedSignature) else { throw HTTP.Error.parsingFailed } - guard let dependencies: OpenGroupAPI.Dependencies = decoder.userInfo[OpenGroupAPI.Dependencies.userInfoKey] as? OpenGroupAPI.Dependencies else { + guard let dependencies: Dependencies = decoder.userInfo[Dependencies.userInfoKey] as? Dependencies else { throw HTTP.Error.parsingFailed } diff --git a/SessionMessagingKit/Open Groups/Models/SendDirectMessageResponse.swift b/SessionMessagingKit/Open Groups/Models/SendDirectMessageResponse.swift index 5768b863a..a8e998f8a 100644 --- a/SessionMessagingKit/Open Groups/Models/SendDirectMessageResponse.swift +++ b/SessionMessagingKit/Open Groups/Models/SendDirectMessageResponse.swift @@ -3,7 +3,7 @@ import Foundation extension OpenGroupAPI { - public struct SendDirectMessageResponse: Codable { + public struct SendDirectMessageResponse: Codable, Equatable { enum CodingKeys: String, CodingKey { case id case sender diff --git a/SessionMessagingKit/Open Groups/Models/UserDeleteMessagesResponse.swift b/SessionMessagingKit/Open Groups/Models/UserDeleteMessagesResponse.swift index 9cbde6f7f..5b5d5ac74 100644 --- a/SessionMessagingKit/Open Groups/Models/UserDeleteMessagesResponse.swift +++ b/SessionMessagingKit/Open Groups/Models/UserDeleteMessagesResponse.swift @@ -3,7 +3,7 @@ import Foundation extension OpenGroupAPI { - public struct UserDeleteMessagesResponse: Codable { + public struct UserDeleteMessagesResponse: Codable, Equatable { enum CodingKeys: String, CodingKey { case id case messagesDeleted = "messages_deleted" diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 4962e1f47..953b14911 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -812,7 +812,7 @@ public enum OpenGroupAPI { /// - dependencies: Injected dependencies (used for unit testing) public static func userDeleteMessages( _ sessionId: String, - for roomTokens: [String]?, + from roomTokens: [String]?, on server: String, using dependencies: Dependencies = Dependencies() ) -> Promise<(OnionRequestResponseInfoType, UserDeleteMessagesResponse)> { @@ -837,7 +837,7 @@ public enum OpenGroupAPI { /// methods for the documented behaviour of each method public static func userBanAndDeleteAllMessage( _ sessionId: String, - for roomTokens: [String]?, + from roomTokens: [String]?, on server: String, using dependencies: Dependencies = Dependencies() ) -> Promise<[OnionRequestResponseInfoType]> { diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index f968234ac..2c1ac5be2 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -18,7 +18,7 @@ public final class OpenGroupManager: NSObject { public var timeSinceLastPoll: [String: TimeInterval] = [:] fileprivate var _timeSinceLastOpen: TimeInterval? - public func getTimeSinceLastOpen(using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> TimeInterval { + public func getTimeSinceLastOpen(using dependencies: Dependencies = Dependencies()) -> TimeInterval { if let storedTimeSinceLastOpen: TimeInterval = _timeSinceLastOpen { return storedTimeSinceLastOpen } @@ -67,7 +67,7 @@ public final class OpenGroupManager: NSObject { // MARK: - Adding & Removing - public func add(roomToken: String, server: String, publicKey: String, isConfigMessage: Bool, using transaction: YapDatabaseReadWriteTransaction, dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise { + public func add(roomToken: String, server: String, publicKey: String, isConfigMessage: Bool, using transaction: YapDatabaseReadWriteTransaction, dependencies: Dependencies = Dependencies()) -> Promise { // If we are currently polling for this server and already have a TSGroupThread for this room the do nothing let groupId: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData("\(server).\(roomToken)") @@ -163,7 +163,7 @@ public final class OpenGroupManager: NSObject { _ capabilities: OpenGroupAPI.Capabilities, on server: String, using transaction: YapDatabaseReadWriteTransaction, - dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies() + dependencies: Dependencies = Dependencies() ) { let updatedServer: OpenGroupAPI.Server = OpenGroupAPI.Server( name: server, @@ -179,7 +179,7 @@ public final class OpenGroupManager: NSObject { for roomToken: String, on server: String, using transaction: YapDatabaseReadWriteTransaction, - dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies(), + dependencies: Dependencies = Dependencies(), completion: (() -> ())? = nil ) { OpenGroupManager.handlePollInfo( @@ -199,7 +199,7 @@ public final class OpenGroupManager: NSObject { for roomToken: String, on server: String, using transaction: YapDatabaseReadWriteTransaction, - dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies(), + dependencies: Dependencies = Dependencies(), completion: (() -> ())? = nil ) { // Create the open group model and get or create the thread @@ -307,7 +307,7 @@ public final class OpenGroupManager: NSObject { on server: String, isBackgroundPoll: Bool, using transaction: YapDatabaseReadWriteTransaction, - dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies() + dependencies: Dependencies = Dependencies() ) { // Sorting the messages by server ID before importing them fixes an issue where messages // that quote older messages can't find those older messages @@ -371,7 +371,7 @@ public final class OpenGroupManager: NSObject { on server: String, isBackgroundPoll: Bool, using transaction: YapDatabaseReadWriteTransaction, - dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies() + dependencies: Dependencies = Dependencies() ) { // Don't need to do anything if we have no messages (it's a valid case) guard !messages.isEmpty else { return } @@ -476,10 +476,10 @@ public final class OpenGroupManager: NSObject { /// This method specifies if the given publicKey is a moderator or an admin within a specified Open Group @objc(isUserModeratorOrAdmin:forRoom:onServer:) public static func isUserModeratorOrAdmin(_ publicKey: String, for room: String, on server: String) -> Bool { - return isUserModeratorOrAdmin(publicKey, for: room, on: server, using: OpenGroupAPI.Dependencies()) + return isUserModeratorOrAdmin(publicKey, for: room, on: server) } - public static func isUserModeratorOrAdmin(_ publicKey: String, for room: String, on server: String, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Bool { + public static func isUserModeratorOrAdmin(_ publicKey: String, for room: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Bool { let modAndAdminKeys: Set = (OpenGroupManager.shared.cache.moderators[server]?[room] ?? Set()) .union(OpenGroupManager.shared.cache.admins[server]?[room] ?? Set()) @@ -527,7 +527,7 @@ public final class OpenGroupManager: NSObject { } } - public static func getDefaultRoomsIfNeeded(using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) { + public static func getDefaultRoomsIfNeeded(using dependencies: Dependencies = Dependencies()) { // Note: If we already have a 'defaultRoomsPromise' then there is no need to get it again guard OpenGroupManager.shared.cache.defaultRoomsPromise == nil else { return } @@ -572,7 +572,7 @@ public final class OpenGroupManager: NSObject { _ fileId: UInt64, for roomToken: String, on server: String, - using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies() + using dependencies: Dependencies = Dependencies() ) -> Promise { // Normally the image for a given group is stored with the group thread, so it's only // fetched once. However, on the join open group screen we show images for groups the diff --git a/SessionMessagingKit/Open Groups/Types/Dependencies.swift b/SessionMessagingKit/Open Groups/Types/Dependencies.swift deleted file mode 100644 index 9ba55334c..000000000 --- a/SessionMessagingKit/Open Groups/Types/Dependencies.swift +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import Sodium -import SessionSnodeKit -import SessionUtilitiesKit - -// MARK: - Dependencies - -extension OpenGroupAPI { - public class Dependencies { - private var _api: OnionRequestAPIType.Type? - public var api: OnionRequestAPIType.Type { - get { getValueSettingIfNull(&_api) { OnionRequestAPI.self } } - set { _api = newValue } - } - - private var _storage: SessionMessagingKitStorageProtocol? - public var storage: SessionMessagingKitStorageProtocol { - get { getValueSettingIfNull(&_storage) { SNMessagingKitConfiguration.shared.storage } } - set { _storage = newValue } - } - - private var _sodium: SodiumType? - public var sodium: SodiumType { - get { getValueSettingIfNull(&_sodium) { Sodium() } } - set { _sodium = newValue } - } - - private var _aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? - public var aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType { - get { getValueSettingIfNull(&_aeadXChaCha20Poly1305Ietf) { sodium.getAeadXChaCha20Poly1305Ietf() } } - set { _aeadXChaCha20Poly1305Ietf = newValue } - } - - private var _sign: SignType? - public var sign: SignType { - get { getValueSettingIfNull(&_sign) { sodium.getSign() } } - set { _sign = newValue } - } - - private var _genericHash: GenericHashType? - public var genericHash: GenericHashType { - get { getValueSettingIfNull(&_genericHash) { sodium.getGenericHash() } } - set { _genericHash = newValue } - } - - private var _ed25519: Ed25519Type.Type? - public var ed25519: Ed25519Type.Type { - get { getValueSettingIfNull(&_ed25519) { Ed25519.self } } - set { _ed25519 = newValue } - } - - private var _nonceGenerator16: NonceGenerator16ByteType? - public var nonceGenerator16: NonceGenerator16ByteType { - get { getValueSettingIfNull(&_nonceGenerator16) { NonceGenerator16Byte() } } - set { _nonceGenerator16 = newValue } - } - - private var _nonceGenerator24: NonceGenerator24ByteType? - public var nonceGenerator24: NonceGenerator24ByteType { - get { getValueSettingIfNull(&_nonceGenerator24) { NonceGenerator24Byte() } } - set { _nonceGenerator24 = newValue } - } - - private var _standardUserDefaults: UserDefaultsType? - public var standardUserDefaults: UserDefaultsType { - get { getValueSettingIfNull(&_standardUserDefaults) { UserDefaults.standard } } - set { _standardUserDefaults = newValue } - } - - private var _date: Date? - public var date: Date { - get { getValueSettingIfNull(&_date) { Date() } } - set { _date = newValue } - } - - // MARK: - Initialization - - public init( - api: OnionRequestAPIType.Type? = nil, - storage: SessionMessagingKitStorageProtocol? = nil, - sodium: SodiumType? = nil, - aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil, - sign: SignType? = nil, - genericHash: GenericHashType? = nil, - ed25519: Ed25519Type.Type? = nil, - nonceGenerator16: NonceGenerator16ByteType? = nil, - nonceGenerator24: NonceGenerator24ByteType? = nil, - standardUserDefaults: UserDefaultsType? = nil, - date: Date? = nil - ) { - _api = api - _storage = storage - _sodium = sodium - _aeadXChaCha20Poly1305Ietf = aeadXChaCha20Poly1305Ietf - _sign = sign - _genericHash = genericHash - _ed25519 = ed25519 - _nonceGenerator16 = nonceGenerator16 - _nonceGenerator24 = nonceGenerator24 - _standardUserDefaults = standardUserDefaults - _date = date - } - - // MARK: - Convenience - - public func with( - api: OnionRequestAPIType.Type? = nil, - storage: SessionMessagingKitStorageProtocol? = nil, - sodium: SodiumType? = nil, - aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil, - sign: SignType? = nil, - genericHash: GenericHashType? = nil, - ed25519: Ed25519Type.Type? = nil, - nonceGenerator16: NonceGenerator16ByteType? = nil, - nonceGenerator24: NonceGenerator24ByteType? = nil, - standardUserDefaults: UserDefaultsType? = nil, - date: Date? = nil - ) -> Dependencies { - return Dependencies( - api: (api ?? self._api), - storage: (storage ?? self._storage), - sodium: (sodium ?? self._sodium), - aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf), - sign: (sign ?? self._sign), - genericHash: (genericHash ?? self._genericHash), - ed25519: (ed25519 ?? self._ed25519), - nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16), - nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24), - standardUserDefaults: (standardUserDefaults ?? self._standardUserDefaults), - date: (date ?? self._date) - ) - } - } -} - -// MARK: - Convenience - -fileprivate func getValueSettingIfNull(_ maybeValue: inout T?, _ valueGenerator: () -> T) -> T { - guard let value: T = maybeValue else { - let value: T = valueGenerator() - maybeValue = value - return value - } - - return value -} diff --git a/SessionMessagingKit/Open Groups/Types/NonceGenerator.swift b/SessionMessagingKit/Open Groups/Types/NonceGenerator.swift index a2ff6aad8..50bcf5db9 100644 --- a/SessionMessagingKit/Open Groups/Types/NonceGenerator.swift +++ b/SessionMessagingKit/Open Groups/Types/NonceGenerator.swift @@ -17,13 +17,9 @@ public protocol NonceGenerator24ByteType { extension OpenGroupAPI { public class NonceGenerator16Byte: NonceGenerator, NonceGenerator16ByteType { public var NonceBytes: Int { 16 } - - public init() {} } public class NonceGenerator24Byte: NonceGenerator, NonceGenerator24ByteType { public var NonceBytes: Int { 24 } - - public init() {} } } diff --git a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift index a1b8c9e94..920eb2693 100644 --- a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift +++ b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift @@ -49,7 +49,6 @@ extension OpenGroupAPI { case userBan(String) case userUnban(String) - case userPermission(String) case userModerator(String) case userDeleteMessages(String) @@ -114,7 +113,6 @@ extension OpenGroupAPI { case .userBan(let sessionId): return "user/\(sessionId)/ban" case .userUnban(let sessionId): return "user/\(sessionId)/unban" - case .userPermission(let sessionId): return "user/\(sessionId)/permission" case .userModerator(let sessionId): return "user/\(sessionId)/moderator" case .userDeleteMessages(let sessionId): return "user/\(sessionId)/deleteMessages" } diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Decryption.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Decryption.swift index cf5b03e44..1565ed80e 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Decryption.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Decryption.swift @@ -27,7 +27,7 @@ extension MessageReceiver { return (Data(plaintext), SessionId(.standard, publicKey: senderX25519PublicKey).hexString) } - internal static func decryptWithSessionBlindingProtocol(data: Data, isOutgoing: Bool, otherBlindedPublicKey: String, with openGroupPublicKey: String, userEd25519KeyPair: Box.KeyPair, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) throws -> (plaintext: Data, senderX25519PublicKey: String) { + internal static func decryptWithSessionBlindingProtocol(data: Data, isOutgoing: Bool, otherBlindedPublicKey: String, with openGroupPublicKey: String, userEd25519KeyPair: Box.KeyPair, using dependencies: Dependencies = Dependencies()) throws -> (plaintext: Data, senderX25519PublicKey: String) { guard let blindedKeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: openGroupPublicKey, edKeyPair: userEd25519KeyPair, genericHash: dependencies.genericHash) else { throw Error.decryptionFailed } diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+Encryption.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+Encryption.swift index 9d9d236ee..0e37d5e2a 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+Encryption.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+Encryption.swift @@ -24,7 +24,7 @@ extension MessageSender { return Data(ciphertext) } - internal static func encryptWithSessionBlindingProtocol(_ plaintext: Data, for recipientBlindedId: String, openGroupPublicKey: String, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) throws -> Data { + internal static func encryptWithSessionBlindingProtocol(_ plaintext: Data, for recipientBlindedId: String, openGroupPublicKey: String, using dependencies: Dependencies = Dependencies()) throws -> Data { guard SessionId.Prefix(from: recipientBlindedId) == .blinded else { throw Error.signingFailed } guard let userEd25519KeyPair = SNMessagingKitConfiguration.shared.storage.getUserED25519KeyPair() else { throw Error.noUserED25519KeyPair diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 073a274f6..ff54a524f 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -281,7 +281,7 @@ public final class MessageSender : NSObject { // MARK: - Open Groups - internal static func sendToOpenGroupDestination(_ destination: Message.Destination, message: Message, using transaction: Any, dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise { + internal static func sendToOpenGroupDestination(_ destination: Message.Destination, message: Message, using transaction: Any, dependencies: Dependencies = Dependencies()) -> Promise { let (promise, seal) = Promise.pending() let transaction = transaction as! YapDatabaseReadWriteTransaction @@ -410,7 +410,7 @@ public final class MessageSender : NSObject { return promise } - internal static func sendToOpenGroupInboxDestination(_ destination: Message.Destination, message: Message, using transaction: Any, dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise { + internal static func sendToOpenGroupInboxDestination(_ destination: Message.Destination, message: Message, using transaction: Any, dependencies: Dependencies = Dependencies()) -> Promise { let (promise, seal) = Promise.pending() let storage = SNMessagingKitConfiguration.shared.storage let transaction = transaction as! YapDatabaseReadWriteTransaction diff --git a/SessionMessagingKit/Utilities/ContactUtilities.swift b/SessionMessagingKit/Utilities/ContactUtilities.swift index 751a55e0a..80a730eb8 100644 --- a/SessionMessagingKit/Utilities/ContactUtilities.swift +++ b/SessionMessagingKit/Utilities/ContactUtilities.swift @@ -49,7 +49,7 @@ public enum ContactUtilities { } } - public static func mapping(for blindedId: String, serverPublicKey: String, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> BlindedIdMapping? { + public static func mapping(for blindedId: String, serverPublicKey: String, using dependencies: Dependencies = Dependencies()) -> BlindedIdMapping? { var result: BlindedIdMapping? Storage.write { transaction in @@ -59,7 +59,7 @@ public enum ContactUtilities { return result } - public static func mapping(for blindedId: String, serverPublicKey: String, using transaction: YapDatabaseReadWriteTransaction, dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> BlindedIdMapping? { + public static func mapping(for blindedId: String, serverPublicKey: String, using transaction: YapDatabaseReadWriteTransaction, dependencies: Dependencies = Dependencies()) -> BlindedIdMapping? { // Unfortunately the whole point of id-blinding is to make it hard to reverse-engineer a standard // sessionId, as a result in order to see if there is an unblinded contact for this blindedId we // can only really generate blinded ids for each contact and check if any match diff --git a/SessionMessagingKit/Utilities/Data+Utilities.swift b/SessionMessagingKit/Utilities/Data+Utilities.swift index 47a0622ad..b99c50da0 100644 --- a/SessionMessagingKit/Utilities/Data+Utilities.swift +++ b/SessionMessagingKit/Utilities/Data+Utilities.swift @@ -4,15 +4,15 @@ import Foundation // MARK: - Decoding -extension OpenGroupAPI.Dependencies { +extension Dependencies { static let userInfoKey: CodingUserInfoKey = CodingUserInfoKey(rawValue: "io.oxen.dependencies.codingOptions")! } public extension Data { - func decoded(as type: T.Type, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) throws -> T { + func decoded(as type: T.Type, using dependencies: Dependencies = Dependencies()) throws -> T { do { let decoder: JSONDecoder = JSONDecoder() - decoder.userInfo = [ OpenGroupAPI.Dependencies.userInfoKey: dependencies ] + decoder.userInfo = [ Dependencies.userInfoKey: dependencies ] return try decoder.decode(type, from: self) } diff --git a/SessionMessagingKit/Utilities/Dependencies.swift b/SessionMessagingKit/Utilities/Dependencies.swift new file mode 100644 index 000000000..583a2ec9e --- /dev/null +++ b/SessionMessagingKit/Utilities/Dependencies.swift @@ -0,0 +1,146 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Sodium +import SessionSnodeKit +import SessionUtilitiesKit + +// MARK: - Dependencies + +public class Dependencies { + private var _api: OnionRequestAPIType.Type? + public var api: OnionRequestAPIType.Type { + get { getValueSettingIfNull(&_api) { OnionRequestAPI.self } } + set { _api = newValue } + } + + private var _storage: SessionMessagingKitStorageProtocol? + public var storage: SessionMessagingKitStorageProtocol { + get { getValueSettingIfNull(&_storage) { SNMessagingKitConfiguration.shared.storage } } + set { _storage = newValue } + } + + private var _sodium: SodiumType? + public var sodium: SodiumType { + get { getValueSettingIfNull(&_sodium) { Sodium() } } + set { _sodium = newValue } + } + + private var _aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? + public var aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType { + get { getValueSettingIfNull(&_aeadXChaCha20Poly1305Ietf) { sodium.getAeadXChaCha20Poly1305Ietf() } } + set { _aeadXChaCha20Poly1305Ietf = newValue } + } + + private var _sign: SignType? + public var sign: SignType { + get { getValueSettingIfNull(&_sign) { sodium.getSign() } } + set { _sign = newValue } + } + + private var _genericHash: GenericHashType? + public var genericHash: GenericHashType { + get { getValueSettingIfNull(&_genericHash) { sodium.getGenericHash() } } + set { _genericHash = newValue } + } + + private var _ed25519: Ed25519Type.Type? + public var ed25519: Ed25519Type.Type { + get { getValueSettingIfNull(&_ed25519) { Ed25519.self } } + set { _ed25519 = newValue } + } + + private var _nonceGenerator16: NonceGenerator16ByteType? + public var nonceGenerator16: NonceGenerator16ByteType { + get { getValueSettingIfNull(&_nonceGenerator16) { OpenGroupAPI.NonceGenerator16Byte() } } + set { _nonceGenerator16 = newValue } + } + + private var _nonceGenerator24: NonceGenerator24ByteType? + public var nonceGenerator24: NonceGenerator24ByteType { + get { getValueSettingIfNull(&_nonceGenerator24) { OpenGroupAPI.NonceGenerator24Byte() } } + set { _nonceGenerator24 = newValue } + } + + private var _standardUserDefaults: UserDefaultsType? + public var standardUserDefaults: UserDefaultsType { + get { getValueSettingIfNull(&_standardUserDefaults) { UserDefaults.standard } } + set { _standardUserDefaults = newValue } + } + + private var _date: Date? + public var date: Date { + get { getValueSettingIfNull(&_date) { Date() } } + set { _date = newValue } + } + + // MARK: - Initialization + + public init( + api: OnionRequestAPIType.Type? = nil, + storage: SessionMessagingKitStorageProtocol? = nil, + sodium: SodiumType? = nil, + aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil, + sign: SignType? = nil, + genericHash: GenericHashType? = nil, + ed25519: Ed25519Type.Type? = nil, + nonceGenerator16: NonceGenerator16ByteType? = nil, + nonceGenerator24: NonceGenerator24ByteType? = nil, + standardUserDefaults: UserDefaultsType? = nil, + date: Date? = nil + ) { + _api = api + _storage = storage + _sodium = sodium + _aeadXChaCha20Poly1305Ietf = aeadXChaCha20Poly1305Ietf + _sign = sign + _genericHash = genericHash + _ed25519 = ed25519 + _nonceGenerator16 = nonceGenerator16 + _nonceGenerator24 = nonceGenerator24 + _standardUserDefaults = standardUserDefaults + _date = date + } + + // MARK: - Convenience + + public func with( + api: OnionRequestAPIType.Type? = nil, + storage: SessionMessagingKitStorageProtocol? = nil, + sodium: SodiumType? = nil, + aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil, + sign: SignType? = nil, + genericHash: GenericHashType? = nil, + ed25519: Ed25519Type.Type? = nil, + nonceGenerator16: NonceGenerator16ByteType? = nil, + nonceGenerator24: NonceGenerator24ByteType? = nil, + standardUserDefaults: UserDefaultsType? = nil, + date: Date? = nil + ) -> Dependencies { + return Dependencies( + api: (api ?? self._api), + storage: (storage ?? self._storage), + sodium: (sodium ?? self._sodium), + aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf), + sign: (sign ?? self._sign), + genericHash: (genericHash ?? self._genericHash), + ed25519: (ed25519 ?? self._ed25519), + nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16), + nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24), + standardUserDefaults: (standardUserDefaults ?? self._standardUserDefaults), + date: (date ?? self._date) + ) + } +} + +// MARK: - Convenience + +fileprivate func getValueSettingIfNull(_ maybeValue: inout T?, _ valueGenerator: () -> T) -> T { + guard let value: T = maybeValue else { + let value: T = valueGenerator() + maybeValue = value + return value + } + + return value +} diff --git a/SessionMessagingKit/Utilities/Promise+Utilities.swift b/SessionMessagingKit/Utilities/Promise+Utilities.swift index 2bbcb78f5..cd72cd3c3 100644 --- a/SessionMessagingKit/Utilities/Promise+Utilities.swift +++ b/SessionMessagingKit/Utilities/Promise+Utilities.swift @@ -5,7 +5,7 @@ import PromiseKit import SessionSnodeKit extension Promise where T == Data { - func decoded(as type: R.Type, on queue: DispatchQueue? = nil, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise { + func decoded(as type: R.Type, on queue: DispatchQueue? = nil, using dependencies: Dependencies = Dependencies()) -> Promise { self.map(on: queue) { data -> R in try data.decoded(as: type, using: dependencies) } @@ -13,7 +13,7 @@ extension Promise where T == Data { } extension Promise where T == (OnionRequestResponseInfoType, Data?) { - func decoded(as type: R.Type, on queue: DispatchQueue? = nil, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise<(OnionRequestResponseInfoType, R)> { + func decoded(as type: R.Type, on queue: DispatchQueue? = nil, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, R)> { self.map(on: queue) { responseInfo, maybeData -> (OnionRequestResponseInfoType, R) in guard let data: Data = maybeData else { throw HTTP.Error.parsingFailed } diff --git a/SessionMessagingKitTests/Open Groups/Models/RoomPollInfoSpec.swift b/SessionMessagingKitTests/Open Groups/Models/RoomPollInfoSpec.swift new file mode 100644 index 000000000..b58227f43 --- /dev/null +++ b/SessionMessagingKitTests/Open Groups/Models/RoomPollInfoSpec.swift @@ -0,0 +1,124 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class RoomPollInfoSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("a RoomPollInfo") { + context("when initializing with a room") { + it("copies all the relevant values across") { + let room: OpenGroupAPI.Room = OpenGroupAPI.Room( + token: "testToken", + name: "testName", + description: nil, + infoUpdates: 123, + messageSequence: 0, + created: 0, + activeUsers: 234, + activeUsersCutoff: 0, + imageId: nil, + pinnedMessages: nil, + admin: true, + globalAdmin: true, + admins: [], + hiddenAdmins: nil, + moderator: true, + globalModerator: true, + moderators: [], + hiddenModerators: nil, + read: true, + defaultRead: true, + defaultAccessible: true, + write: true, + defaultWrite: true, + upload: true, + defaultUpload: true + ) + let roomPollInfo: OpenGroupAPI.RoomPollInfo = OpenGroupAPI.RoomPollInfo(room: room) + + expect(roomPollInfo.token).to(equal(room.token)) + expect(roomPollInfo.activeUsers).to(equal(room.activeUsers)) + expect(roomPollInfo.admin).to(equal(room.admin)) + expect(roomPollInfo.globalAdmin).to(equal(room.globalAdmin)) + expect(roomPollInfo.moderator).to(equal(room.moderator)) + expect(roomPollInfo.globalModerator).to(equal(room.globalModerator)) + expect(roomPollInfo.read).to(equal(room.read)) + expect(roomPollInfo.defaultRead).to(equal(room.defaultRead)) + expect(roomPollInfo.defaultAccessible).to(equal(room.defaultAccessible)) + expect(roomPollInfo.write).to(equal(room.write)) + expect(roomPollInfo.defaultWrite).to(equal(room.defaultWrite)) + expect(roomPollInfo.upload).to(equal(room.upload)) + expect(roomPollInfo.defaultUpload).to(equal(room.defaultUpload)) + expect(roomPollInfo.details).to(equal(room)) + } + } + + context("when decoding") { + it("defaults admin and moderator values to false if omitted") { + let roomPollInfoJson: String = """ + { + "token": "testToken", + "active_users": 0, + + "read": true, + "default_read": true, + "default_accessible": true, + "write": true, + "default_write": true, + "upload": true, + "default_upload": true, + + "details": null + } + """ + let roomData: Data = roomPollInfoJson.data(using: .utf8)! + let result: OpenGroupAPI.RoomPollInfo = try! JSONDecoder().decode(OpenGroupAPI.RoomPollInfo.self, from: roomData) + + expect(result.admin).to(beFalse()) + expect(result.globalAdmin).to(beFalse()) + expect(result.moderator).to(beFalse()) + expect(result.globalModerator).to(beFalse()) + } + + it("sets the admin and moderator values when provided") { + let roomPollInfoJson: String = """ + { + "token": "testToken", + "active_users": 0, + + "admin": true, + "global_admin": true, + + "moderator": true, + "global_moderator": true, + + "read": true, + "default_read": true, + "default_accessible": true, + "write": true, + "default_write": true, + "upload": true, + "default_upload": true, + + "details": null + } + """ + let roomData: Data = roomPollInfoJson.data(using: .utf8)! + let result: OpenGroupAPI.RoomPollInfo = try! JSONDecoder().decode(OpenGroupAPI.RoomPollInfo.self, from: roomData) + + expect(result.admin).to(beTrue()) + expect(result.globalAdmin).to(beTrue()) + expect(result.moderator).to(beTrue()) + expect(result.globalModerator).to(beTrue()) + } + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/Models/RoomSpec.swift b/SessionMessagingKitTests/Open Groups/Models/RoomSpec.swift new file mode 100644 index 000000000..16a3ab84b --- /dev/null +++ b/SessionMessagingKitTests/Open Groups/Models/RoomSpec.swift @@ -0,0 +1,100 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class RoomSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("a Room") { + context("when decoding") { + it("defaults admin and moderator values to false if omitted") { + let roomJson: String = """ + { + "token": "testToken", + "name": "testName", + "description": "testDescription", + "info_updates": 0, + "message_sequence": 0, + "created": 1, + + "active_users": 0, + "active_users_cutoff": 0, + "image_id": 0, + "pinned_messages": [], + + "admins": [], + "hidden_admins": [], + + "moderators": [], + "hidden_moderators": [], + + "read": true, + "default_read": true, + "default_accessible": true, + "write": true, + "default_write": true, + "upload": true, + "default_upload": true + } + """ + let roomData: Data = roomJson.data(using: .utf8)! + let result: OpenGroupAPI.Room = try! JSONDecoder().decode(OpenGroupAPI.Room.self, from: roomData) + + expect(result.admin).to(beFalse()) + expect(result.globalAdmin).to(beFalse()) + expect(result.moderator).to(beFalse()) + expect(result.globalModerator).to(beFalse()) + } + + it("sets the admin and moderator values when provided") { + let roomJson: String = """ + { + "token": "testToken", + "name": "testName", + "description": "testDescription", + "info_updates": 0, + "message_sequence": 0, + "created": 1, + + "active_users": 0, + "active_users_cutoff": 0, + "image_id": 0, + "pinned_messages": [], + + "admin": true, + "global_admin": true, + "admins": [], + "hidden_admins": [], + + "moderator": true, + "global_moderator": true, + "moderators": [], + "hidden_moderators": [], + + "read": true, + "default_read": true, + "default_accessible": true, + "write": true, + "default_write": true, + "upload": true, + "default_upload": true + } + """ + let roomData: Data = roomJson.data(using: .utf8)! + let result: OpenGroupAPI.Room = try! JSONDecoder().decode(OpenGroupAPI.Room.self, from: roomData) + + expect(result.admin).to(beTrue()) + expect(result.globalAdmin).to(beTrue()) + expect(result.moderator).to(beTrue()) + expect(result.globalModerator).to(beTrue()) + } + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift b/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift new file mode 100644 index 000000000..75c8ed404 --- /dev/null +++ b/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift @@ -0,0 +1,253 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class SOGSMessageSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("a SOGSMessage") { + var messageJson: String! + var messageData: Data! + var decoder: JSONDecoder! + var testSign: TestSign! + var dependencies: Dependencies! + + beforeEach { + messageJson = """ + { + "id": 123, + "session_id": "05\(TestConstants.publicKey)", + "posted": 234, + "seqno": 345, + "whisper": false, + "whisper_mods": false, + + "data": "VGVzdERhdGE=", + "signature": "VGVzdERhdGE=" + } + """ + messageData = messageJson.data(using: .utf8)! + testSign = TestSign() + dependencies = Dependencies( + sign: testSign, + ed25519: TestEd25519.self + ) + decoder = JSONDecoder() + decoder.userInfo = [ Dependencies.userInfoKey: dependencies as Any ] + } + + context("when decoding") { + it("defaults the whisper values to false") { + messageJson = """ + { + "id": 123, + "posted": 234, + "seqno": 345 + } + """ + messageData = messageJson.data(using: .utf8)! + let result: OpenGroupAPI.Message? = try? decoder.decode(OpenGroupAPI.Message.self, from: messageData) + + expect(result).toNot(beNil()) + expect(result?.whisper).to(beFalse()) + expect(result?.whisperMods).to(beFalse()) + } + + context("and there is no content") { + it("does not need a sender") { + messageJson = """ + { + "id": 123, + "posted": 234, + "seqno": 345, + "whisper": false, + "whisper_mods": false + } + """ + messageData = messageJson.data(using: .utf8)! + let result: OpenGroupAPI.Message? = try? decoder.decode(OpenGroupAPI.Message.self, from: messageData) + + expect(result).toNot(beNil()) + expect(result?.sender).to(beNil()) + expect(result?.base64EncodedData).to(beNil()) + expect(result?.base64EncodedSignature).to(beNil()) + } + } + + context("and there is content") { + it("errors if there is no sender") { + messageJson = """ + { + "id": 123, + "posted": 234, + "seqno": 345, + "whisper": false, + "whisper_mods": false, + + "data": "VGVzdERhdGE=", + "signature": "VGVzdERhdGE=" + } + """ + messageData = messageJson.data(using: .utf8)! + + expect { + try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + } + .to(throwError(HTTP.Error.parsingFailed)) + } + + it("errors if the data is not a base64 encoded string") { + messageJson = """ + { + "id": 123, + "session_id": "05\(TestConstants.publicKey)", + "posted": 234, + "seqno": 345, + "whisper": false, + "whisper_mods": false, + + "data": "Test!!!", + "signature": "VGVzdERhdGE=" + } + """ + messageData = messageJson.data(using: .utf8)! + + expect { + try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + } + .to(throwError(HTTP.Error.parsingFailed)) + } + + it("errors if the signature is not a base64 encoded string") { + messageJson = """ + { + "id": 123, + "session_id": "05\(TestConstants.publicKey)", + "posted": 234, + "seqno": 345, + "whisper": false, + "whisper_mods": false, + + "data": "VGVzdERhdGE=", + "signature": "Test!!!" + } + """ + messageData = messageJson.data(using: .utf8)! + + expect { + try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + } + .to(throwError(HTTP.Error.parsingFailed)) + } + + it("errors if the dependencies are not provided to the JSONDecoder") { + decoder = JSONDecoder() + + expect { + try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + } + .to(throwError(HTTP.Error.parsingFailed)) + } + + it("errors if the session_id value is not valid") { + messageJson = """ + { + "id": 123, + "session_id": "TestId", + "posted": 234, + "seqno": 345, + "whisper": false, + "whisper_mods": false, + + "data": "VGVzdERhdGE=", + "signature": "VGVzdERhdGE=" + } + """ + messageData = messageJson.data(using: .utf8)! + + expect { + try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + } + .to(throwError(HTTP.Error.parsingFailed)) + } + + + context("that is blinded") { + beforeEach { + messageJson = """ + { + "id": 123, + "session_id": "15\(TestConstants.publicKey)", + "posted": 234, + "seqno": 345, + "whisper": false, + "whisper_mods": false, + + "data": "VGVzdERhdGE=", + "signature": "VGVzdERhdGE=" + } + """ + messageData = messageJson.data(using: .utf8)! + } + + it("succeeds if it succeeds verification") { + testSign.mockData[.verify] = true + + expect { + try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + } + .toNot(beNil()) + } + + it("throws if it fails verification") { + testSign.mockData[.verify] = false + + expect { + try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + } + .to(throwError(HTTP.Error.parsingFailed)) + } + } + + context("that is unblinded") { + it("succeeds if it succeeds verification") { + TestEd25519.mockData[ + .verifySignature( + signature: Data(base64Encoded: "VGVzdERhdGE=")!, + publicKey: Data(hex: TestConstants.publicKey), + data: Data(base64Encoded: "VGVzdERhdGE=")! + ) + ] = true + + expect { + try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + } + .toNot(beNil()) + } + + it("throws if it fails verification") { + TestEd25519.mockData[ + .verifySignature( + signature: Data(base64Encoded: "VGVzdERhdGE=")!, + publicKey: Data(hex: TestConstants.publicKey), + data: Data(base64Encoded: "VGVzdERhdGE=")! + ) + ] = false + + expect { + try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + } + .to(throwError(HTTP.Error.parsingFailed)) + } + } + } + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/Models/SendDirectMessageRequestSpec.swift b/SessionMessagingKitTests/Open Groups/Models/SendDirectMessageRequestSpec.swift new file mode 100644 index 000000000..228176a15 --- /dev/null +++ b/SessionMessagingKitTests/Open Groups/Models/SendDirectMessageRequestSpec.swift @@ -0,0 +1,29 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class SendDirectMessageRequestSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("a SendDirectMessageRequest") { + context("when encoding") { + it("encodes the data as a base64 string") { + let request: OpenGroupAPI.SendDirectMessageRequest = OpenGroupAPI.SendDirectMessageRequest( + message: "TestData".data(using: .utf8)! + ) + let requestData: Data = try! JSONEncoder().encode(request) + let requestDataString: String = String(data: requestData, encoding: .utf8)! + + expect(requestDataString).toNot(contain("TestData")) + expect(requestDataString).to(contain("VGVzdERhdGE=")) + } + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/Models/SendMessageRequestSpec.swift b/SessionMessagingKitTests/Open Groups/Models/SendMessageRequestSpec.swift new file mode 100644 index 000000000..7fd3554a3 --- /dev/null +++ b/SessionMessagingKitTests/Open Groups/Models/SendMessageRequestSpec.swift @@ -0,0 +1,61 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class SendMessageRequestSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("a SendMessageRequest") { + context("when initializing") { + it("defaults the optional values to nil") { + let request: OpenGroupAPI.SendMessageRequest = OpenGroupAPI.SendMessageRequest( + data: "TestData".data(using: .utf8)!, + signature: "TestSignature".data(using: .utf8)! + ) + + expect(request.whisperTo).to(beNil()) + expect(request.whisperMods).to(beNil()) + expect(request.fileIds).to(beNil()) + } + } + + context("when encoding") { + it("encodes the data as a base64 string") { + let request: OpenGroupAPI.SendMessageRequest = OpenGroupAPI.SendMessageRequest( + data: "TestData".data(using: .utf8)!, + signature: "TestSignature".data(using: .utf8)!, + whisperTo: nil, + whisperMods: nil, + fileIds: nil + ) + let requestData: Data = try! JSONEncoder().encode(request) + let requestDataString: String = String(data: requestData, encoding: .utf8)! + + expect(requestDataString).toNot(contain("TestData")) + expect(requestDataString).to(contain("VGVzdERhdGE=")) + } + + it("encodes the signature as a base64 string") { + let request: OpenGroupAPI.SendMessageRequest = OpenGroupAPI.SendMessageRequest( + data: "TestData".data(using: .utf8)!, + signature: "TestSignature".data(using: .utf8)!, + whisperTo: nil, + whisperMods: nil, + fileIds: nil + ) + let requestData: Data = try! JSONEncoder().encode(request) + let requestDataString: String = String(data: requestData, encoding: .utf8)! + + expect(requestDataString).toNot(contain("TestSignature")) + expect(requestDataString).to(contain("VGVzdFNpZ25hdHVyZQ==")) + } + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/Models/UpdateMessageRequestSpec.swift b/SessionMessagingKitTests/Open Groups/Models/UpdateMessageRequestSpec.swift new file mode 100644 index 000000000..f63b2e16c --- /dev/null +++ b/SessionMessagingKitTests/Open Groups/Models/UpdateMessageRequestSpec.swift @@ -0,0 +1,44 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class UpdateMessageRequestSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("a UpdateMessageRequest") { + context("when encoding") { + it("encodes the data as a base64 string") { + let request: OpenGroupAPI.UpdateMessageRequest = OpenGroupAPI.UpdateMessageRequest( + data: "TestData".data(using: .utf8)!, + signature: "TestSignature".data(using: .utf8)!, + fileIds: nil + ) + let requestData: Data = try! JSONEncoder().encode(request) + let requestDataString: String = String(data: requestData, encoding: .utf8)! + + expect(requestDataString).toNot(contain("TestData")) + expect(requestDataString).to(contain("VGVzdERhdGE=")) + } + + it("encodes the signature as a base64 string") { + let request: OpenGroupAPI.UpdateMessageRequest = OpenGroupAPI.UpdateMessageRequest( + data: "TestData".data(using: .utf8)!, + signature: "TestSignature".data(using: .utf8)!, + fileIds: nil + ) + let requestData: Data = try! JSONEncoder().encode(request) + let requestDataString: String = String(data: requestData, encoding: .utf8)! + + expect(requestDataString).toNot(contain("TestSignature")) + expect(requestDataString).to(contain("VGVzdFNpZ25hdHVyZQ==")) + } + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index 661c0a154..3f0c0c5d6 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -84,7 +84,7 @@ class OpenGroupAPISpec: QuickSpec { var testGenericHash: TestGenericHash! var testSign: TestSign! var testUserDefaults: TestUserDefaults! - var dependencies: OpenGroupAPI.Dependencies! + var dependencies: Dependencies! var response: (OnionRequestResponseInfoType, Codable)? = nil var pollResponse: [OpenGroupAPI.Endpoint: (OnionRequestResponseInfoType, Codable?)]? @@ -100,7 +100,7 @@ class OpenGroupAPISpec: QuickSpec { testGenericHash = TestGenericHash() testSign = TestSign() testUserDefaults = TestUserDefaults() - dependencies = OpenGroupAPI.Dependencies( + dependencies = Dependencies( api: TestApi.self, storage: testStorage, sodium: testSodium, @@ -1003,18 +1003,1015 @@ class OpenGroupAPISpec: QuickSpec { } } + // MARK: - Messages + + context("when sending messages") { + var messageData: OpenGroupAPI.Message! + + beforeEach { + class LocalTestApi: TestApi { + static let data: OpenGroupAPI.Message = OpenGroupAPI.Message( + id: 126, + sender: "testSender", + posted: 321, + edited: nil, + seqNo: 10, + whisper: false, + whisperMods: false, + whisperTo: nil, + base64EncodedData: nil, + base64EncodedSignature: nil + ) + + override class var mockResponse: Data? { return try! JSONEncoder().encode(data) } + } + messageData = LocalTestApi.data + dependencies = dependencies.with(api: LocalTestApi.self) + + testStorage.mockData[.userEdKeyPair] = Box.KeyPair(publicKey: [], secretKey: []) + } + + afterEach { + messageData = nil + } + + it("correctly sends the message") { + var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)? + + OpenGroupAPI + .send( + "test".data(using: .utf8)!, + to: "testRoom", + on: "testServer", + whisperTo: nil, + whisperMods: false, + fileIds: nil, + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate the response data + expect(response?.data).to(equal(messageData)) + + // Validate signature headers + let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + expect(requestData?.httpMethod).to(equal("POST")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.urlString).to(equal("testServer/room/testRoom/message")) + } + + it("saves the received message timestamp to the database in milliseconds") { + var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)? + + OpenGroupAPI + .send( + "test".data(using: .utf8)!, + to: "testRoom", + on: "testServer", + whisperTo: nil, + whisperMods: false, + fileIds: nil, + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + expect(testStorage.mockData[.receivedMessageTimestamp] as? UInt64).to(equal(321000)) + } + + context("when unblinded") { + beforeEach { + testStorage.mockData[.openGroupServer] = OpenGroupAPI.Server( + name: "testServer", + capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: []) + ) + } + + it("signs the message correctly") { + var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)? + + OpenGroupAPI + .send( + "test".data(using: .utf8)!, + to: "testRoom", + on: "testServer", + whisperTo: nil, + whisperMods: false, + fileIds: nil, + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request body + let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + let requestBody: OpenGroupAPI.SendMessageRequest = try! JSONDecoder().decode(OpenGroupAPI.SendMessageRequest.self, from: requestData!.body!) + + expect(requestBody.data).to(equal("test".data(using: .utf8))) + expect(requestBody.signature).to(equal("TestSignature".data(using: .utf8))) + } + + it("fails to sign if there is no public key") { + testStorage.mockData[.openGroupPublicKeys] = [:] + + var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)? + + OpenGroupAPI + .send( + "test".data(using: .utf8)!, + to: "testRoom", + on: "testServer", + whisperTo: nil, + whisperMods: false, + fileIds: nil, + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + } + + context("when blinded") { + beforeEach { + testStorage.mockData[.openGroupServer] = OpenGroupAPI.Server( + name: "testServer", + capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs, .blind], missing: []) + ) + } + + it("signs the message correctly") { + var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)? + + OpenGroupAPI + .send( + "test".data(using: .utf8)!, + to: "testRoom", + on: "testServer", + whisperTo: nil, + whisperMods: false, + fileIds: nil, + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request body + let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + let requestBody: OpenGroupAPI.SendMessageRequest = try! JSONDecoder().decode(OpenGroupAPI.SendMessageRequest.self, from: requestData!.body!) + + expect(requestBody.data).to(equal("test".data(using: .utf8))) + expect(requestBody.signature).to(equal("TestSogsSignature".data(using: .utf8))) + } + + it("fails to sign if there is no public key") { + testStorage.mockData[.openGroupPublicKeys] = [:] + + var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)? + + OpenGroupAPI + .send( + "test".data(using: .utf8)!, + to: "testRoom", + on: "testServer", + whisperTo: nil, + whisperMods: false, + fileIds: nil, + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + } + } + + context("when getting an individual message") { + it("generates the request and handles the response correctly") { + class LocalTestApi: TestApi { + static let data: OpenGroupAPI.Message = OpenGroupAPI.Message( + id: 126, + sender: "testSender", + posted: 321, + edited: nil, + seqNo: 10, + whisper: false, + whisperMods: false, + whisperTo: nil, + base64EncodedData: nil, + base64EncodedSignature: nil + ) + + override class var mockResponse: Data? { return try! JSONEncoder().encode(data) } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)? + + OpenGroupAPI.message(123, in: "testRoom", on: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate the response data + expect(response?.data).to(equal(LocalTestApi.data)) + + // Validate request data + let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + expect(requestData?.httpMethod).to(equal("GET")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.urlString).to(equal("testServer/room/testRoom/message/123")) + } + } + + context("when updating a message") { + beforeEach { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { return Data() } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + testStorage.mockData[.userEdKeyPair] = Box.KeyPair(publicKey: [], secretKey: []) + } + + it("correctly sends the update") { + var response: (info: OnionRequestResponseInfoType, data: Data?)? + + OpenGroupAPI + .messageUpdate( + 123, + plaintext: "test".data(using: .utf8)!, + fileIds: nil, + in: "testRoom", + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate signature headers + let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + expect(requestData?.httpMethod).to(equal("PUT")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.urlString).to(equal("testServer/room/testRoom/message/123")) + } + + context("when unblinded") { + beforeEach { + testStorage.mockData[.openGroupServer] = OpenGroupAPI.Server( + name: "testServer", + capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: []) + ) + } + + it("signs the message correctly") { + var response: (info: OnionRequestResponseInfoType, data: Data?)? + + OpenGroupAPI + .messageUpdate( + 123, + plaintext: "test".data(using: .utf8)!, + fileIds: nil, + in: "testRoom", + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request body + let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + let requestBody: OpenGroupAPI.UpdateMessageRequest = try! JSONDecoder().decode(OpenGroupAPI.UpdateMessageRequest.self, from: requestData!.body!) + + expect(requestBody.data).to(equal("test".data(using: .utf8))) + expect(requestBody.signature).to(equal("TestSignature".data(using: .utf8))) + } + + it("fails to sign if there is no public key") { + testStorage.mockData[.openGroupPublicKeys] = [:] + + var response: (info: OnionRequestResponseInfoType, data: Data?)? + + OpenGroupAPI + .messageUpdate( + 123, + plaintext: "test".data(using: .utf8)!, + fileIds: nil, + in: "testRoom", + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + } + + context("when blinded") { + beforeEach { + testStorage.mockData[.openGroupServer] = OpenGroupAPI.Server( + name: "testServer", + capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs, .blind], missing: []) + ) + } + + it("signs the message correctly") { + var response: (info: OnionRequestResponseInfoType, data: Data?)? + + OpenGroupAPI + .messageUpdate( + 123, + plaintext: "test".data(using: .utf8)!, + fileIds: nil, + in: "testRoom", + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request body + let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + let requestBody: OpenGroupAPI.UpdateMessageRequest = try! JSONDecoder().decode(OpenGroupAPI.UpdateMessageRequest.self, from: requestData!.body!) + + expect(requestBody.data).to(equal("test".data(using: .utf8))) + expect(requestBody.signature).to(equal("TestSogsSignature".data(using: .utf8))) + } + + it("fails to sign if there is no public key") { + testStorage.mockData[.openGroupPublicKeys] = [:] + + var response: (info: OnionRequestResponseInfoType, data: Data?)? + + OpenGroupAPI + .messageUpdate( + 123, + plaintext: "test".data(using: .utf8)!, + fileIds: nil, + in: "testRoom", + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + } + } + + context("when deleting a message") { + it("generates the request and handles the response correctly") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { return Data() } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + var response: (info: OnionRequestResponseInfoType, data: Data?)? + + OpenGroupAPI.messageDelete(123, in: "testRoom", on: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request data + let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + expect(requestData?.httpMethod).to(equal("DELETE")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.urlString).to(equal("testServer/room/testRoom/message/123")) + } + } + + // MARK: - Pinning + + context("when pinning a message") { + it("generates the request and handles the response correctly") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { return Data() } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + var response: OnionRequestResponseInfoType? + + OpenGroupAPI.pinMessage(id: 123, in: "testRoom", on: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request data + let requestData: TestApi.RequestData? = (response as? TestResponseInfo)?.requestData + expect(requestData?.httpMethod).to(equal("POST")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.urlString).to(equal("testServer/room/testRoom/pin/123")) + } + } + + context("when unpinning a message") { + it("generates the request and handles the response correctly") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { return Data() } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + var response: OnionRequestResponseInfoType? + + OpenGroupAPI.unpinMessage(id: 123, in: "testRoom", on: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request data + let requestData: TestApi.RequestData? = (response as? TestResponseInfo)?.requestData + expect(requestData?.httpMethod).to(equal("POST")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.urlString).to(equal("testServer/room/testRoom/unpin/123")) + } + } + + context("when unpinning all messages") { + it("generates the request and handles the response correctly") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { return Data() } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + var response: OnionRequestResponseInfoType? + + OpenGroupAPI.unpinAll(in: "testRoom", on: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request data + let requestData: TestApi.RequestData? = (response as? TestResponseInfo)?.requestData + expect(requestData?.httpMethod).to(equal("POST")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.urlString).to(equal("testServer/room/testRoom/unpin/all")) + } + } + // MARK: - Files - context("when uploading files") { - it("doesn't add a fileName to the content-disposition header when not provided") { + context("when uploading files") { + it("generates the request and handles the response correctly") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { + return try! JSONEncoder().encode(FileUploadResponse(id: "1")) + } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + OpenGroupAPI.uploadFile([], to: "testRoom", on: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate signature headers + let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + expect(requestData?.httpMethod).to(equal("POST")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.urlString).to(equal("testServer/room/testRoom/file")) + } + + it("doesn't add a fileName to the content-disposition header when not provided") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { + return try! JSONEncoder().encode(FileUploadResponse(id: "1")) + } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + OpenGroupAPI.uploadFile([], to: "testRoom", on: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate signature headers + let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + expect(requestData?.headers[Header.contentDisposition.rawValue]) + .toNot(contain("filename")) + } + + it("adds the fileName to the content-disposition header when provided") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { + return try! JSONEncoder().encode(FileUploadResponse(id: "1")) + } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + OpenGroupAPI.uploadFile([], fileName: "TestFileName", to: "testRoom", on: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate signature headers + let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + expect(requestData?.headers[Header.contentDisposition.rawValue]).to(contain("TestFileName")) + } + } + + context("when downloading files") { + it("generates the request and handles the response correctly") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { + return Data() + } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + OpenGroupAPI.downloadFile(1, from: "testRoom", on: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate signature headers + let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + expect(requestData?.httpMethod).to(equal("GET")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.urlString).to(equal("testServer/room/testRoom/file/1")) + } + } + + // MARK: - Inbox/Outbox (Message Requests) + + context("when sending message requests") { + var messageData: OpenGroupAPI.SendDirectMessageResponse! + + beforeEach { + class LocalTestApi: TestApi { + static let data: OpenGroupAPI.SendDirectMessageResponse = OpenGroupAPI.SendDirectMessageResponse( + id: 126, + sender: "testSender", + recipient: "testRecipient", + posted: 321, + expires: 456 + ) + + override class var mockResponse: Data? { return try! JSONEncoder().encode(data) } + } + messageData = LocalTestApi.data + dependencies = dependencies.with(api: LocalTestApi.self) + } + + afterEach { + messageData = nil + } + + it("correctly sends the message request") { + var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.SendDirectMessageResponse)? + + OpenGroupAPI + .send( + "test".data(using: .utf8)!, + toInboxFor: "testUserId", + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate the response data + expect(response?.data).to(equal(messageData)) + + // Validate signature headers + let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + expect(requestData?.httpMethod).to(equal("POST")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.urlString).to(equal("testServer/inbox/testUserId")) + } + + it("saves the received message timestamp to the database in milliseconds") { + var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.SendDirectMessageResponse)? + + OpenGroupAPI + .send( + "test".data(using: .utf8)!, + toInboxFor: "testUserId", + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + expect(testStorage.mockData[.receivedMessageTimestamp] as? UInt64).to(equal(321000)) + } + } + + // MARK: - Users + + context("when banning a user") { + var response: (info: OnionRequestResponseInfoType, data: Data?)? + + beforeEach { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { return Data() } + } + dependencies = dependencies.with(api: LocalTestApi.self) + } + + afterEach { + response = nil + } + + it("generates the request and handles the response correctly") { + OpenGroupAPI + .userBan( + "testUserId", + for: nil, + from: nil, + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request data + let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + expect(requestData?.httpMethod).to(equal("POST")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.urlString).to(equal("testServer/user/testUserId/ban")) + } + + it("does a global ban if no room tokens are provided") { + OpenGroupAPI + .userBan( + "testUserId", + for: nil, + from: nil, + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request data + let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + let requestBody: OpenGroupAPI.UserBanRequest = try! JSONDecoder().decode(OpenGroupAPI.UserBanRequest.self, from: requestData!.body!) + + expect(requestBody.global).to(beTrue()) + expect(requestBody.rooms).to(beNil()) + } + + it("does room specific bans if room tokens are provided") { + OpenGroupAPI + .userBan( + "testUserId", + for: nil, + from: ["testRoom"], + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request data + let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + let requestBody: OpenGroupAPI.UserBanRequest = try! JSONDecoder().decode(OpenGroupAPI.UserBanRequest.self, from: requestData!.body!) + + expect(requestBody.global).to(beNil()) + expect(requestBody.rooms).to(equal(["testRoom"])) + } + } + + context("when unbanning a user") { + var response: (info: OnionRequestResponseInfoType, data: Data?)? + + beforeEach { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { return Data() } + } + dependencies = dependencies.with(api: LocalTestApi.self) + } + + afterEach { + response = nil + } + + it("generates the request and handles the response correctly") { + OpenGroupAPI + .userUnban( + "testUserId", + from: nil, + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request data + let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + expect(requestData?.httpMethod).to(equal("POST")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.urlString).to(equal("testServer/user/testUserId/unban")) + } + + it("does a global ban if no room tokens are provided") { + OpenGroupAPI + .userUnban( + "testUserId", + from: nil, + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request data + let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + let requestBody: OpenGroupAPI.UserUnbanRequest = try! JSONDecoder().decode(OpenGroupAPI.UserUnbanRequest.self, from: requestData!.body!) + + expect(requestBody.global).to(beTrue()) + expect(requestBody.rooms).to(beNil()) + } + + it("does room specific bans if room tokens are provided") { + OpenGroupAPI + .userUnban( + "testUserId", + from: ["testRoom"], + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request data + let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + let requestBody: OpenGroupAPI.UserUnbanRequest = try! JSONDecoder().decode(OpenGroupAPI.UserUnbanRequest.self, from: requestData!.body!) + + expect(requestBody.global).to(beNil()) + expect(requestBody.rooms).to(equal(["testRoom"])) + } + } + + context("when updating a users permissions") { + var response: (info: OnionRequestResponseInfoType, data: Data?)? + + beforeEach { class LocalTestApi: TestApi { - override class var mockResponse: Data? { - return try! JSONEncoder().encode(FileUploadResponse(id: "1")) - } + override class var mockResponse: Data? { return Data() } } dependencies = dependencies.with(api: LocalTestApi.self) + } + + afterEach { + response = nil + } + + it("generates the request and handles the response correctly") { + OpenGroupAPI + .userModeratorUpdate( + "testUserId", + moderator: true, + admin: nil, + visible: true, + for: nil, + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() - OpenGroupAPI.uploadFile([], to: "testRoom", on: "testServer", using: dependencies) + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request data + let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + expect(requestData?.httpMethod).to(equal("POST")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.urlString).to(equal("testServer/user/testUserId/moderator")) + } + + it("does a global update if no room tokens are provided") { + OpenGroupAPI + .userModeratorUpdate( + "testUserId", + moderator: true, + admin: nil, + visible: true, + for: nil, + on: "testServer", + using: dependencies + ) .get { result in response = result } .catch { requestError in error = requestError } .retainUntilComplete() @@ -1026,24 +2023,222 @@ class OpenGroupAPISpec: QuickSpec { ) expect(error?.localizedDescription).to(beNil()) - // Validate signature headers - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData - expect(requestData?.urlString).to(equal("testServer/room/testRoom/file")) + // Validate request data + let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + let requestBody: OpenGroupAPI.UserModeratorRequest = try! JSONDecoder().decode(OpenGroupAPI.UserModeratorRequest.self, from: requestData!.body!) + + expect(requestBody.global).to(beTrue()) + expect(requestBody.rooms).to(beNil()) + } + + it("does room specific updates if room tokens are provided") { + OpenGroupAPI + .userModeratorUpdate( + "testUserId", + moderator: true, + admin: nil, + visible: true, + for: ["testRoom"], + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request data + let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + let requestBody: OpenGroupAPI.UserModeratorRequest = try! JSONDecoder().decode(OpenGroupAPI.UserModeratorRequest.self, from: requestData!.body!) + + expect(requestBody.global).to(beNil()) + expect(requestBody.rooms).to(equal(["testRoom"])) + } + + it("fails if neither moderator or admin are set") { + OpenGroupAPI + .userModeratorUpdate( + "testUserId", + moderator: nil, + admin: nil, + visible: true, + for: nil, + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(HTTP.Error.generic.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + } + + context("when deleting a users messages") { + var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.UserDeleteMessagesResponse)? + var messageData: OpenGroupAPI.UserDeleteMessagesResponse! + + beforeEach { + class LocalTestApi: TestApi { + static let data: OpenGroupAPI.UserDeleteMessagesResponse = OpenGroupAPI.UserDeleteMessagesResponse( + id: "testId", + messagesDeleted: 10 + ) + + override class var mockResponse: Data? { return try! JSONEncoder().encode(data) } + } + messageData = LocalTestApi.data + dependencies = dependencies.with(api: LocalTestApi.self) + } + + afterEach { + response = nil + } + + it("generates the request and handles the response correctly") { + OpenGroupAPI + .userDeleteMessages( + "testUserId", + from: nil, + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate the response data + expect(response?.data).to(equal(messageData)) + + // Validate request data + let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData expect(requestData?.httpMethod).to(equal("POST")) - expect(requestData?.headers).to(haveCount(6)) - expect(requestData?.headers[Header.contentDisposition.rawValue]) - .toNot(contain("filename")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.urlString).to(equal("testServer/user/testUserId/deleteMessages")) } - it("adds the fileName to the content-disposition header when provided") { + it("does a global delete if no room tokens are provided") { + OpenGroupAPI + .userDeleteMessages( + "testUserId", + from: nil, + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request data + let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + let requestBody: OpenGroupAPI.UserDeleteMessagesRequest = try! JSONDecoder().decode(OpenGroupAPI.UserDeleteMessagesRequest.self, from: requestData!.body!) + + expect(requestBody.global).to(beTrue()) + expect(requestBody.rooms).to(beNil()) + } + + it("does room specific bans if room tokens are provided") { + OpenGroupAPI + .userDeleteMessages( + "testUserId", + from: ["testRoom"], + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request data + let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + let requestBody: OpenGroupAPI.UserDeleteMessagesRequest = try! JSONDecoder().decode(OpenGroupAPI.UserDeleteMessagesRequest.self, from: requestData!.body!) + + expect(requestBody.global).to(beNil()) + expect(requestBody.rooms).to(equal(["testRoom"])) + } + } + + context("when banning and deleting all messages for a user") { + var response: [OnionRequestResponseInfoType]? + + beforeEach { class LocalTestApi: TestApi { + static let deleteMessagesData: OpenGroupAPI.UserDeleteMessagesResponse = OpenGroupAPI.UserDeleteMessagesResponse( + id: "123", + messagesDeleted: 10 + ) + override class var mockResponse: Data? { - return try! JSONEncoder().encode(FileUploadResponse(id: "1")) + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: nil, + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: deleteMessagesData, + failedToParseBody: false + ) + ) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) } } dependencies = dependencies.with(api: LocalTestApi.self) - - OpenGroupAPI.uploadFile([], fileName: "TestFileName", to: "testRoom", on: "testServer", using: dependencies) + } + + afterEach { + response = nil + } + + it("generates the request and handles the response correctly") { + OpenGroupAPI + .userBanAndDeleteAllMessage( + "testUserId", + from: nil, + on: "testServer", + using: dependencies + ) .get { result in response = result } .catch { requestError in error = requestError } .retainUntilComplete() @@ -1055,12 +2250,91 @@ class OpenGroupAPISpec: QuickSpec { ) expect(error?.localizedDescription).to(beNil()) - // Validate signature headers - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData - expect(requestData?.urlString).to(equal("testServer/room/testRoom/file")) + // Validate request data + let requestData: TestApi.RequestData? = (response?.first as? TestResponseInfo)?.requestData expect(requestData?.httpMethod).to(equal("POST")) - expect(requestData?.headers).to(haveCount(6)) - expect(requestData?.headers[Header.contentDisposition.rawValue]).to(contain("TestFileName")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.urlString).to(equal("testServer/sequence")) + } + + it("does a global ban and delete if no room tokens are provided") { + OpenGroupAPI + .userBanAndDeleteAllMessage( + "testUserId", + from: nil, + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request data + let requestData: TestApi.RequestData? = (response?.first as? TestResponseInfo)?.requestData + let jsonObject: Any = try! JSONSerialization.jsonObject( + with: requestData!.body!, + options: [.fragmentsAllowed] + ) + let anyArray: [Any] = jsonObject as! [Any] + let dataArray: [Data] = anyArray.compactMap { + try! JSONSerialization.data(withJSONObject: ($0 as! [String: Any])["json"]!) + } + let firstRequestBody: OpenGroupAPI.UserBanRequest = try! JSONDecoder() + .decode(OpenGroupAPI.UserBanRequest.self, from: dataArray.first!) + let lastRequestBody: OpenGroupAPI.UserDeleteMessagesRequest = try! JSONDecoder() + .decode(OpenGroupAPI.UserDeleteMessagesRequest.self, from: dataArray.last!) + + expect(firstRequestBody.global).to(beTrue()) + expect(firstRequestBody.rooms).to(beNil()) + expect(lastRequestBody.global).to(beTrue()) + expect(lastRequestBody.rooms).to(beNil()) + } + + it("does room specific bans and deletes if room tokens are provided") { + OpenGroupAPI + .userBanAndDeleteAllMessage( + "testUserId", + from: ["testRoom"], + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate request data + let requestData: TestApi.RequestData? = (response?.first as? TestResponseInfo)?.requestData + let jsonObject: Any = try! JSONSerialization.jsonObject( + with: requestData!.body!, + options: [.fragmentsAllowed] + ) + let anyArray: [Any] = jsonObject as! [Any] + let dataArray: [Data] = anyArray.compactMap { + try! JSONSerialization.data(withJSONObject: ($0 as! [String: Any])["json"]!) + } + let firstRequestBody: OpenGroupAPI.UserBanRequest = try! JSONDecoder() + .decode(OpenGroupAPI.UserBanRequest.self, from: dataArray.first!) + let lastRequestBody: OpenGroupAPI.UserDeleteMessagesRequest = try! JSONDecoder() + .decode(OpenGroupAPI.UserDeleteMessagesRequest.self, from: dataArray.last!) + + expect(firstRequestBody.global).to(beNil()) + expect(firstRequestBody.rooms).to(equal(["testRoom"])) + expect(lastRequestBody.global).to(beNil()) + expect(lastRequestBody.rooms).to(equal(["testRoom"])) } } @@ -1111,6 +2385,23 @@ class OpenGroupAPISpec: QuickSpec { expect(response).to(beNil()) } + it("fails when the serverPublicKey is not a hex string") { + testStorage.mockData[.openGroupPublicKeys] = ["testServer": "TestString!!!"] + + OpenGroupAPI.rooms(for: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + context("when unblinded") { beforeEach { testStorage.mockData[.openGroupServer] = OpenGroupAPI.Server( diff --git a/SessionMessagingKitTests/Open Groups/Types/NonceGeneratorSpec.swift b/SessionMessagingKitTests/Open Groups/Types/NonceGeneratorSpec.swift new file mode 100644 index 000000000..b7db2898f --- /dev/null +++ b/SessionMessagingKitTests/Open Groups/Types/NonceGeneratorSpec.swift @@ -0,0 +1,26 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class NonceGeneratorSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("a NonceGenerator16Byte") { + it("has the correct number of bytes") { + expect(OpenGroupAPI.NonceGenerator16Byte().NonceBytes).to(equal(16)) + } + } + + describe("a NonceGenerator24Byte") { + it("has the correct number of bytes") { + expect(OpenGroupAPI.NonceGenerator24Byte().NonceBytes).to(equal(24)) + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/Types/PersonalizationSpec.swift b/SessionMessagingKitTests/Open Groups/Types/PersonalizationSpec.swift new file mode 100644 index 000000000..f82ccaed0 --- /dev/null +++ b/SessionMessagingKitTests/Open Groups/Types/PersonalizationSpec.swift @@ -0,0 +1,23 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class PersonalizationSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("a Personalization") { + it("generates bytes correctly") { + expect(OpenGroupAPI.Personalization.sharedKeys.bytes) + .to(equal([115, 111, 103, 115, 46, 115, 104, 97, 114, 101, 100, 95, 107, 101, 121, 115])) + expect(OpenGroupAPI.Personalization.authHeader.bytes) + .to(equal([115, 111, 103, 115, 46, 97, 117, 116, 104, 95, 104, 101, 97, 100, 101, 114])) + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift b/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift new file mode 100644 index 000000000..733ca8ef8 --- /dev/null +++ b/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift @@ -0,0 +1,67 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class SOGSEndpointSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("a SOGSEndpoint") { + it("generates the path value correctly") { + // Utility + + expect(OpenGroupAPI.Endpoint.onion.path).to(equal("oxen/v4/lsrpc")) + expect(OpenGroupAPI.Endpoint.batch.path).to(equal("batch")) + expect(OpenGroupAPI.Endpoint.sequence.path).to(equal("sequence")) + expect(OpenGroupAPI.Endpoint.capabilities.path).to(equal("capabilities")) + + // Rooms + + expect(OpenGroupAPI.Endpoint.rooms.path).to(equal("rooms")) + expect(OpenGroupAPI.Endpoint.room("test").path).to(equal("room/test")) + expect(OpenGroupAPI.Endpoint.roomPollInfo("test", 123).path).to(equal("room/test/pollInfo/123")) + + // Messages + + expect(OpenGroupAPI.Endpoint.roomMessage("test").path).to(equal("room/test/message")) + expect(OpenGroupAPI.Endpoint.roomMessageIndividual("test", id: 123).path).to(equal("room/test/message/123")) + expect(OpenGroupAPI.Endpoint.roomMessagesRecent("test").path).to(equal("room/test/messages/recent")) + expect(OpenGroupAPI.Endpoint.roomMessagesBefore("test", id: 123).path).to(equal("room/test/messages/before/123")) + expect(OpenGroupAPI.Endpoint.roomMessagesSince("test", seqNo: 123).path) + .to(equal("room/test/messages/since/123")) + + // Pinning + + expect(OpenGroupAPI.Endpoint.roomPinMessage("test", id: 123).path).to(equal("room/test/pin/123")) + expect(OpenGroupAPI.Endpoint.roomUnpinMessage("test", id: 123).path).to(equal("room/test/unpin/123")) + expect(OpenGroupAPI.Endpoint.roomUnpinAll("test").path).to(equal("room/test/unpin/all")) + + // Files + + expect(OpenGroupAPI.Endpoint.roomFile("test").path).to(equal("room/test/file")) + expect(OpenGroupAPI.Endpoint.roomFileIndividual("test", 123).path).to(equal("room/test/file/123")) + + // Inbox/Outbox (Message Requests) + + expect(OpenGroupAPI.Endpoint.inbox.path).to(equal("inbox")) + expect(OpenGroupAPI.Endpoint.inboxSince(id: 123).path).to(equal("inbox/since/123")) + expect(OpenGroupAPI.Endpoint.inboxFor(sessionId: "test").path).to(equal("inbox/test")) + + expect(OpenGroupAPI.Endpoint.outbox.path).to(equal("outbox")) + expect(OpenGroupAPI.Endpoint.outboxSince(id: 123).path).to(equal("outbox/since/123")) + + // Users + + expect(OpenGroupAPI.Endpoint.userBan("test").path).to(equal("user/test/ban")) + expect(OpenGroupAPI.Endpoint.userUnban("test").path).to(equal("user/test/unban")) + expect(OpenGroupAPI.Endpoint.userModerator("test").path).to(equal("user/test/moderator")) + expect(OpenGroupAPI.Endpoint.userDeleteMessages("test").path).to(equal("user/test/deleteMessages")) + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/Types/SOGSErrorSpec.swift b/SessionMessagingKitTests/Open Groups/Types/SOGSErrorSpec.swift new file mode 100644 index 000000000..f637692b5 --- /dev/null +++ b/SessionMessagingKitTests/Open Groups/Types/SOGSErrorSpec.swift @@ -0,0 +1,25 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class SOGSErrorSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("a SOGSError") { + it("generates the error description correctly") { + expect(OpenGroupAPI.Error.decryptionFailed.errorDescription) + .to(equal("Couldn't decrypt response.")) + expect(OpenGroupAPI.Error.signingFailed.errorDescription) + .to(equal("Couldn't sign message.")) + expect(OpenGroupAPI.Error.noPublicKey.errorDescription) + .to(equal("Couldn't find server public key.")) + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/Types/SodiumProtocolsSpec.swift b/SessionMessagingKitTests/Open Groups/Types/SodiumProtocolsSpec.swift new file mode 100644 index 000000000..d746e40e5 --- /dev/null +++ b/SessionMessagingKitTests/Open Groups/Types/SodiumProtocolsSpec.swift @@ -0,0 +1,47 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class SodiumProtocolsSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("an AeadXChaCha20Poly1305IetfType") { + let testValue: [UInt8] = [1, 2, 3] + + it("provides the default values in it's extensions") { + let testAead: TestAeadXChaCha20Poly1305Ietf = TestAeadXChaCha20Poly1305Ietf() + testAead.mockData[.encrypt] = testValue + testAead.mockData[.decrypt] = testValue + + expect(testAead.encrypt(message: [], secretKey: [], nonce: [])).to(equal(testValue)) + expect(testAead.encrypt(message: [], secretKey: [], nonce: [], additionalData: nil)).to(equal(testValue)) + expect(testAead.decrypt(authenticatedCipherText: [], secretKey: [], nonce: [])).to(equal(testValue)) + expect(testAead.decrypt(authenticatedCipherText: [], secretKey: [], nonce: [], additionalData: nil)) + .to(equal(testValue)) + } + } + + describe("a GenericHashType") { + let testValue: [UInt8] = [1, 2, 3] + + it("provides the default values in it's extensions") { + let testGenericHash: TestGenericHash = TestGenericHash() + testGenericHash.mockData[.hash] = testValue + testGenericHash.mockData[.hashSaltPersonal] = testValue + + expect(testGenericHash.hash(message: [])).to(equal(testValue)) + expect(testGenericHash.hash(message: [], key: nil)).to(equal(testValue)) + expect(testGenericHash.hashSaltPersonal(message: [], outputLength: 0, salt: [], personal: [])) + .to(equal(testValue)) + expect(testGenericHash.hashSaltPersonal(message: [], outputLength: 0, key: nil, salt: [], personal: [])) + .to(equal(testValue)) + } + } + } +} diff --git a/SessionMessagingKitTests/_TestUtilities/TestStorage.swift b/SessionMessagingKitTests/_TestUtilities/TestStorage.swift index 3424a7749..b5d71b3b5 100644 --- a/SessionMessagingKitTests/_TestUtilities/TestStorage.swift +++ b/SessionMessagingKitTests/_TestUtilities/TestStorage.swift @@ -21,6 +21,7 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable { case openGroupSequenceNumber case openGroupInboxLatestMessageId case openGroupOutboxLatestMessageId + case receivedMessageTimestamp } typealias Key = DataKey @@ -176,8 +177,14 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable { func getAllMessageRequestThreads() -> [String: TSContactThread] { return [:] } func getAllMessageRequestThreads(using transaction: YapDatabaseReadTransaction) -> [String: TSContactThread] { return [:] } - func getReceivedMessageTimestamps(using transaction: Any) -> [UInt64] { return [] } - func addReceivedMessageTimestamp(_ timestamp: UInt64, using transaction: Any) {} + func getReceivedMessageTimestamps(using transaction: Any) -> [UInt64] { + return ((mockData[.receivedMessageTimestamp] as? UInt64).map { [$0] } ?? []) + } + + func addReceivedMessageTimestamp(_ timestamp: UInt64, using transaction: Any) { + mockData[.receivedMessageTimestamp] = timestamp + } + func getOrCreateThread(for publicKey: String, groupPublicKey: String?, openGroupID: String?, using transaction: Any) -> String? { return nil } func persist(_ message: VisibleMessage, quotedMessage: TSQuotedMessage?, linkPreview: OWSLinkPreview?, groupPublicKey: String?, openGroupID: String?, using transaction: Any) -> String? { return nil } func persist(_ attachments: [VisibleMessage.Attachment], using transaction: Any) -> [String] { return [] }