Finalised the OpenGroupAPI and more tests

Fixed an issue where messages where signed incorrectly when blinding wasn't enabled on a SOGS
Fixed an issue where a single invalid message would result in all messages in that request being dropped
Updated the final legacy endpoint (ban and delete all messages)
Moved the OpenGroupManager poller values into the 'Cache' (so they are thread safe)
Started adding unit tests for the OpenGroupManager
Removed some redundant parameters from the 'Request' type
This commit is contained in:
Morgan Pretty 2022-03-15 15:19:23 +11:00
parent c415fc9e06
commit f9c2655df4
29 changed files with 1419 additions and 426 deletions

View File

@ -773,6 +773,10 @@
F5765D284BC6ECAC0C1D33F0 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4A93ECA93B3DE800CC7D7F6 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */; };
FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3BD9871A30A790005B96BB /* Social.framework */; };
FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB11D8B1A129A76002F93FB /* CoreMedia.framework */; };
FD078E4627E02406000769AF /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383D27B4708600C60D73 /* Atomic.swift */; };
FD078E4827E02561000769AF /* CommonMockedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E4727E02561000769AF /* CommonMockedExtensions.swift */; };
FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E4727E02561000769AF /* CommonMockedExtensions.swift */; };
FD078E4B27E02C5D000769AF /* Failable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E4A27E02C5D000769AF /* Failable.swift */; };
FD0BA51B27CD88EC00CC6805 /* BlindedIdMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */; };
FD0BA51D27CDC34600CC6805 /* SOGSV4Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0BA51C27CDC34600CC6805 /* SOGSV4Migration.swift */; };
FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; };
@ -797,7 +801,7 @@
FD83B9C927D0487A005E1583 /* SendDirectMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */; };
FD83B9CC27D179BC005E1583 /* FSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */; };
FD83B9CE27D17A04005E1583 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9CD27D17A04005E1583 /* Request.swift */; };
FD83B9D227D59495005E1583 /* TestUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* TestUserDefaults.swift */; };
FD83B9D227D59495005E1583 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; };
FD83B9D427D5A7D5005E1583 /* ConversationViewItem+Refactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D327D5A7D5005E1583 /* ConversationViewItem+Refactor.swift */; };
FD859EF227BF6BA200510D0C /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF127BF6BA200510D0C /* Data+Utilities.swift */; };
FD859EF427C2F49200510D0C /* MockSodium.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF327C2F49200510D0C /* MockSodium.swift */; };
@ -826,8 +830,10 @@
FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; };
FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; };
FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A527D860CE005DAE71 /* Mock.swift */; };
FDC290AC27DB0B1C005DAE71 /* BoxKeyPair+Mocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290AB27DB0B1C005DAE71 /* BoxKeyPair+Mocked.swift */; };
FDC290AD27DB0B1C005DAE71 /* BoxKeyPair+Mocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290AB27DB0B1C005DAE71 /* BoxKeyPair+Mocked.swift */; };
FDC290AC27DB0B1C005DAE71 /* MockedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290AB27DB0B1C005DAE71 /* MockedExtensions.swift */; };
FDC290AF27DFEE97005DAE71 /* TestTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290AE27DFEE97005DAE71 /* TestTransaction.swift */; };
FDC290B327DFF9F5005DAE71 /* TestOnionRequestAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290B227DFF9F5005DAE71 /* TestOnionRequestAPI.swift */; };
FDC290B727E00FDB005DAE71 /* TestGroupThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290B627E00FDB005DAE71 /* TestGroupThread.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 */; };
@ -835,7 +841,6 @@
FDC4382F27B383AF00C60D73 /* UnregisterResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382E27B383AF00C60D73 /* UnregisterResponse.swift */; };
FDC4383127B3841C00C60D73 /* RegisterResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383027B3841C00C60D73 /* RegisterResponse.swift */; };
FDC4383827B3863200C60D73 /* VersionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383727B3863200C60D73 /* VersionResponse.swift */; };
FDC4383E27B4708600C60D73 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383D27B4708600C60D73 /* Atomic.swift */; };
FDC4384C27B47F7700C60D73 /* OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384B27B47F7700C60D73 /* OpenGroup.swift */; };
FDC4384F27B4804F00C60D73 /* Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384E27B4804F00C60D73 /* Header.swift */; };
FDC4385127B4807400C60D73 /* QueryParam.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385027B4807400C60D73 /* QueryParam.swift */; };
@ -1913,6 +1918,8 @@
F9BBF530D71905BA9007675F /* Pods-SessionShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionShareExtension/Pods-SessionShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
FC3BD9871A30A790005B96BB /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; };
FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; };
FD078E4727E02561000769AF /* CommonMockedExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonMockedExtensions.swift; sourceTree = "<group>"; };
FD078E4A27E02C5D000769AF /* Failable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Failable.swift; sourceTree = "<group>"; };
FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdMapping.swift; sourceTree = "<group>"; };
FD0BA51C27CDC34600CC6805 /* SOGSV4Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSV4Migration.swift; sourceTree = "<group>"; };
FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; };
@ -1936,7 +1943,7 @@
FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageResponse.swift; sourceTree = "<group>"; };
FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FSEndpoint.swift; sourceTree = "<group>"; };
FD83B9CD27D17A04005E1583 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; };
FD83B9D127D59495005E1583 /* TestUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUserDefaults.swift; sourceTree = "<group>"; };
FD83B9D127D59495005E1583 /* MockUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserDefaults.swift; sourceTree = "<group>"; };
FD83B9D327D5A7D5005E1583 /* ConversationViewItem+Refactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationViewItem+Refactor.swift"; sourceTree = "<group>"; };
FD859EEF27BF207700510D0C /* SessionProtos.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = SessionProtos.proto; sourceTree = "<group>"; };
FD859EF027BF207C00510D0C /* WebSocketResources.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = WebSocketResources.proto; sourceTree = "<group>"; };
@ -1966,7 +1973,10 @@
FDC290A127D85890005DAE71 /* TestInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestInteraction.swift; sourceTree = "<group>"; };
FDC290A527D860CE005DAE71 /* Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mock.swift; sourceTree = "<group>"; };
FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NimbleExtensions.swift; sourceTree = "<group>"; };
FDC290AB27DB0B1C005DAE71 /* BoxKeyPair+Mocked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BoxKeyPair+Mocked.swift"; sourceTree = "<group>"; };
FDC290AB27DB0B1C005DAE71 /* MockedExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockedExtensions.swift; sourceTree = "<group>"; };
FDC290AE27DFEE97005DAE71 /* TestTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTransaction.swift; sourceTree = "<group>"; };
FDC290B227DFF9F5005DAE71 /* TestOnionRequestAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestOnionRequestAPI.swift; sourceTree = "<group>"; };
FDC290B627E00FDB005DAE71 /* TestGroupThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGroupThread.swift; sourceTree = "<group>"; };
FDC4380827B31D4E00C60D73 /* SOGSError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSError.swift; sourceTree = "<group>"; };
FDC4381427B329CE00C60D73 /* NonceGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonceGenerator.swift; sourceTree = "<group>"; };
FDC4381627B32EC700C60D73 /* Personalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Personalization.swift; sourceTree = "<group>"; };
@ -2542,6 +2552,7 @@
C33FDB8A255A581200E217F9 /* AppContext.h */,
C33FDB85255A581100E217F9 /* AppContext.m */,
C3C2A5D12553860800C340D1 /* Array+Utilities.swift */,
FDC4383D27B4708600C60D73 /* Atomic.swift */,
FDC438CC27BC641200C60D73 /* Set+Utilities.swift */,
C33FDAA8255A57FF00E217F9 /* BuildConfiguration.swift */,
B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */,
@ -3422,13 +3433,13 @@
children = (
C33FDB01255A580700E217F9 /* AppReadiness.h */,
C33FDB75255A581000E217F9 /* AppReadiness.m */,
FDC4383D27B4708600C60D73 /* Atomic.swift */,
FD83B9A927CF149D005E1583 /* ContactUtilities.swift */,
FD859EF127BF6BA200510D0C /* Data+Utilities.swift */,
FDC438C027BB4E6800C60D73 /* Dependencies.swift */,
C38EF309255B6DBE007E1867 /* DeviceSleepManager.swift */,
C37F53E8255BA9BB002AEA92 /* Environment.h */,
C37F5402255BA9ED002AEA92 /* Environment.m */,
FD078E4A27E02C5D000769AF /* Failable.swift */,
C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */,
C33FDB7F255A581100E217F9 /* FullTextSearchFinder.swift */,
C33FDBC1255A581700E217F9 /* General.swift */,
@ -3886,6 +3897,7 @@
FDC290A527D860CE005DAE71 /* Mock.swift */,
FD83B9BD27CF2243005E1583 /* TestConstants.swift */,
FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */,
FD078E4727E02561000769AF /* CommonMockedExtensions.swift */,
);
path = SharedTest;
sourceTree = "<group>";
@ -4035,10 +4047,13 @@
FD859EF727C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift */,
FD859EF927C2F5C500510D0C /* MockGenericHash.swift */,
FD859EFB27C2F60700510D0C /* MockEd25519.swift */,
FD83B9D127D59495005E1583 /* TestUserDefaults.swift */,
FD83B9D127D59495005E1583 /* MockUserDefaults.swift */,
FDC290B227DFF9F5005DAE71 /* TestOnionRequestAPI.swift */,
FDC290AE27DFEE97005DAE71 /* TestTransaction.swift */,
FDC2909F27D85826005DAE71 /* TestThread.swift */,
FDC290B627E00FDB005DAE71 /* TestGroupThread.swift */,
FDC290A127D85890005DAE71 /* TestInteraction.swift */,
FDC290AB27DB0B1C005DAE71 /* BoxKeyPair+Mocked.swift */,
FDC290AB27DB0B1C005DAE71 /* MockedExtensions.swift */,
);
path = _TestUtilities;
sourceTree = "<group>";
@ -5241,6 +5256,7 @@
B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */,
C33FDEF8255A656D00E217F9 /* Promise+Delaying.swift in Sources */,
B8BC00C0257D90E30032E807 /* General.swift in Sources */,
FD078E4627E02406000769AF /* Atomic.swift in Sources */,
FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */,
C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */,
C3D9E41F25676C870040E4F3 /* OWSPrimaryStorageProtocol.swift in Sources */,
@ -5297,7 +5313,6 @@
C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */,
7B1581E2271E743B00848B49 /* OWSSounds.swift in Sources */,
FDC438A627BB113A00C60D73 /* UserUnbanRequest.swift in Sources */,
FDC4383E27B4708600C60D73 /* Atomic.swift in Sources */,
C32C5A76256DBBCF003C73A2 /* SignalAttachment.swift in Sources */,
C32C5CA4256DD1DC003C73A2 /* TSAccountManager.m in Sources */,
C352A3892557876500338F3E /* JobQueue.swift in Sources */,
@ -5400,6 +5415,7 @@
FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */,
C352A2FF25574B6300338F3E /* MessageSendJob.swift in Sources */,
FDC438C327BB512200C60D73 /* SodiumProtocols.swift in Sources */,
FD078E4B27E02C5D000769AF /* Failable.swift in Sources */,
B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */,
C3DB66C3260ACCE6001EFC55 /* OpenGroupPoller.swift in Sources */,
C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */,
@ -5619,7 +5635,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FDC290AD27DB0B1C005DAE71 /* BoxKeyPair+Mocked.swift in Sources */,
FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */,
FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */,
FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */,
FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */,
@ -5631,9 +5647,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FDC290AC27DB0B1C005DAE71 /* BoxKeyPair+Mocked.swift in Sources */,
FDC290AC27DB0B1C005DAE71 /* MockedExtensions.swift in Sources */,
FD859EFA27C2F5C500510D0C /* MockGenericHash.swift in Sources */,
FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */,
FDC290AF27DFEE97005DAE71 /* TestTransaction.swift in Sources */,
FDC290B327DFF9F5005DAE71 /* TestOnionRequestAPI.swift in Sources */,
FDC2909127D709CA005DAE71 /* SOGSMessageSpec.swift in Sources */,
FDC2909627D71252005DAE71 /* SOGSErrorSpec.swift in Sources */,
FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */,
@ -5641,6 +5659,7 @@
FD83B9C327CF33F7005E1583 /* ServerSpec.swift in Sources */,
FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */,
FDC2909A27D71376005DAE71 /* NonceGeneratorSpec.swift in Sources */,
FD078E4827E02561000769AF /* CommonMockedExtensions.swift in Sources */,
FDC290A027D85826005DAE71 /* TestThread.swift in Sources */,
FD859EF827C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift in Sources */,
FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */,
@ -5652,6 +5671,7 @@
FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */,
FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */,
FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */,
FDC290B727E00FDB005DAE71 /* TestGroupThread.swift in Sources */,
FDC2908F27D70938005DAE71 /* SendDirectMessageRequestSpec.swift in Sources */,
FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */,
FD859EF627C2F52C00510D0C /* MockSign.swift in Sources */,
@ -5660,7 +5680,7 @@
FDC2908D27D70905005DAE71 /* UpdateMessageRequestSpec.swift in Sources */,
FDC290A227D85890005DAE71 /* TestInteraction.swift in Sources */,
FDC4389D27BA01F000C60D73 /* MockStorage.swift in Sources */,
FD83B9D227D59495005E1583 /* TestUserDefaults.swift in Sources */,
FD83B9D227D59495005E1583 /* MockUserDefaults.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -815,11 +815,11 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
let publicKey = message.authorId
guard let openGroup = Storage.shared.getOpenGroup(for: threadID) else { return }
let promise = OpenGroupAPI.userBanAndDeleteAllMessage(publicKey, from: [openGroup.room], on: openGroup.server)
let promise = OpenGroupAPI.userBanAndDeleteAllMessages(publicKey, in: openGroup.room, on: openGroup.server)
promise.catch(on: DispatchQueue.main) { _ in
OWSAlerts.showErrorAlert(message: NSLocalizedString("context_menu_ban_user_error_alert_message", comment: ""))
}
promise.retainUntilComplete() // TODO: Test This
promise.retainUntilComplete()
self?.becomeFirstResponder()
}))
alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: { [weak self] _ in

View File

@ -25,10 +25,6 @@ struct Request<T: Encodable, Endpoint: EndpointType> {
/// **Warning:** The `bodyData` value should be used to when making the actual request instead of this as there
/// is custom handling for certain data types
let body: T?
let isAuthRequired: Bool
/// Always `true` under normal circumstances. You might want to disable
/// this when running over Lokinet.
let useOnionRouting: Bool
// MARK: - Initialization
@ -38,9 +34,7 @@ struct Request<T: Encodable, Endpoint: EndpointType> {
endpoint: Endpoint,
queryParameters: [QueryParam: String] = [:],
headers: [Header: String] = [:],
body: T? = nil,
isAuthRequired: Bool = true,
useOnionRouting: Bool = true
body: T? = nil
) {
self.method = method
self.server = server
@ -48,8 +42,6 @@ struct Request<T: Encodable, Endpoint: EndpointType> {
self.queryParameters = queryParameters
self.headers = headers
self.body = body
self.isAuthRequired = isAuthRequired
self.useOnionRouting = useOnionRouting
}
// MARK: - Internal Methods

View File

@ -75,10 +75,6 @@ public final class FileServerAPI: NSObject {
// MARK: - Convenience
private static func send<T: Encodable>(_ request: Request<T, Endpoint>, serverPublicKey: String) -> Promise<Data> {
guard request.useOnionRouting else {
preconditionFailure("It's currently not allowed to send non onion routed requests.")
}
let urlRequest: URLRequest
do {

View File

@ -76,7 +76,7 @@ public enum OpenGroupAPI {
.roomMessagesSince(openGroup.room, seqNo: targetSeqNo)
)
),
responseType: [Message].self
responseType: [Failable<Message>].self
)
]
}
@ -242,7 +242,7 @@ public enum OpenGroupAPI {
for roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) -> Promise<(capabilities: (OnionRequestResponseInfoType, Capabilities?), room: (OnionRequestResponseInfoType, Room?))> {
) -> Promise<(capabilities: (info: OnionRequestResponseInfoType, data: Capabilities), room: (info: OnionRequestResponseInfoType, data: Room))> {
let requestResponseType: [BatchRequestInfoType] = [
// Get the latest capabilities for the server (in case it's a new server or the cached ones are stale)
BatchRequestInfo(
@ -264,8 +264,8 @@ public enum OpenGroupAPI {
]
return sequence(server, requests: requestResponseType, using: dependencies)
.map { (response: [Endpoint: (OnionRequestResponseInfoType, Codable?)]) -> (capabilities: (OnionRequestResponseInfoType, Capabilities?), room: (OnionRequestResponseInfoType, Room?)) in
let maybeCapabilities: (OnionRequestResponseInfoType, Capabilities?)? = response[.capabilities]
.map { (response: [Endpoint: (OnionRequestResponseInfoType, Codable?)]) -> (capabilities: (OnionRequestResponseInfoType, Capabilities), room: (OnionRequestResponseInfoType, Room)) in
let maybeCapabilities: (info: OnionRequestResponseInfoType, data: Capabilities?)? = response[.capabilities]
.map { info, data in (info, (data as? BatchSubResponse<Capabilities>)?.body) }
let maybeRoomResponse: (OnionRequestResponseInfoType, Codable?)? = response
.first(where: { key, _ in
@ -275,14 +275,22 @@ public enum OpenGroupAPI {
}
})
.map { _, value in value }
let maybeRoom: (OnionRequestResponseInfoType, Room?)? = maybeRoomResponse
let maybeRoom: (info: OnionRequestResponseInfoType, data: Room?)? = maybeRoomResponse
.map { info, data in (info, (data as? BatchSubResponse<Room>)?.body) }
guard let capabilities: (OnionRequestResponseInfoType, Capabilities?) = maybeCapabilities, let room: (OnionRequestResponseInfoType, Room?) = maybeRoom else {
guard
let capabilitiesInfo: OnionRequestResponseInfoType = maybeCapabilities?.info,
let capabilities: Capabilities = maybeCapabilities?.data,
let roomInfo: OnionRequestResponseInfoType = maybeRoom?.info,
let room: Room = maybeRoom?.data
else {
throw HTTP.Error.parsingFailed
}
return (capabilities, room)
return (
(capabilitiesInfo, capabilities),
(roomInfo, room)
)
}
}
@ -298,7 +306,7 @@ public enum OpenGroupAPI {
fileIds: [String]?,
using dependencies: Dependencies = Dependencies()
) -> Promise<(OnionRequestResponseInfoType, Message)> {
guard let signResult: (publicKey: String, signature: Bytes) = sign(plaintext.bytes, for: server, using: dependencies) else {
guard let signResult: (publicKey: String, signature: Bytes) = sign(plaintext.bytes, for: server, fallbackSigningType: .standard, using: dependencies) else {
return Promise(error: Error.signingFailed)
}
@ -352,7 +360,7 @@ public enum OpenGroupAPI {
on server: String,
using dependencies: Dependencies = Dependencies()
) -> Promise<(OnionRequestResponseInfoType, Data?)> {
guard let signResult: (publicKey: String, signature: Bytes) = sign(plaintext.bytes, for: server, using: dependencies) else {
guard let signResult: (publicKey: String, signature: Bytes) = sign(plaintext.bytes, for: server, fallbackSigningType: .standard, using: dependencies) else {
return Promise(error: Error.signingFailed)
}
@ -429,6 +437,34 @@ public enum OpenGroupAPI {
.decoded(as: [Message].self, on: OpenGroupAPI.workQueue, using: dependencies)
}
/// Deletes all messages from a given sessionId within the provided rooms (or globally) on a server
///
/// - Parameters:
/// - sessionId: The sessionId (either standard or blinded) of the user whose messages should be deleted
///
/// - roomToken: The room token from which the messages should be deleted
///
/// The invoking user **must** be a moderator of the given room or an admin if trying to delete the messages
/// of another admin.
///
/// - server: The server to delete messages from
///
/// - dependencies: Injected dependencies (used for unit testing)
public static func messagesDeleteAll(
_ sessionId: String,
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) -> Promise<(OnionRequestResponseInfoType, Data?)> {
let request: Request = Request<NoBody, Endpoint>(
method: .delete,
server: server,
endpoint: Endpoint.roomDeleteMessages(roomToken, sessionId: sessionId)
)
return send(request, using: dependencies)
}
// MARK: - Pinning
/// Adds a pinned message to this room
@ -791,65 +827,19 @@ public enum OpenGroupAPI {
return send(request, using: dependencies)
}
// TODO: Need to test this once the API has been implemented
// TODO: Update docs to align with the API documentation once implemented
/// Deletes all messages from a given sessionId within the provided rooms (or globally) on a server
///
/// - Parameters:
/// - sessionId: The sessionId (either standard or blinded) of the user whose messages should be deleted
///
/// - roomTokens: List of one or more room tokens from which the messages should be deleted
///
/// The invoking user **must** be an admin of all of the given rooms.
///
/// This may be set to the single-element list `["*"]` to add or remove the moderator from all rooms in which the current user has admin
/// permissions (the call will succeed if the calling user is an admin in at least one channel)
///
/// **Note:** You can delete messages from all rooms on a server by providing a `nil` value for this parameter
///
/// - server: The server to delete messages from
///
/// - dependencies: Injected dependencies (used for unit testing)
public static func userDeleteMessages(
_ sessionId: String,
from roomTokens: [String]?,
on server: String,
using dependencies: Dependencies = Dependencies()
) -> Promise<(OnionRequestResponseInfoType, UserDeleteMessagesResponse)> {
let requestBody: UserDeleteMessagesRequest = UserDeleteMessagesRequest(
rooms: roomTokens,
global: (roomTokens == nil ? true : nil)
)
let request: Request = Request(
method: .post,
server: server,
endpoint: Endpoint.userDeleteMessages(sessionId),
body: requestBody
)
return send(request, using: dependencies)
.decoded(as: UserDeleteMessagesResponse.self, on: OpenGroupAPI.workQueue, using: dependencies)
}
// TODO: Need to test this once the API has been implemented
/// This is a convenience method which constructs a `/sequence` of the `userBan` and `userDeleteMessages` requests, refer to those
/// methods for the documented behaviour of each method
public static func userBanAndDeleteAllMessage(
public static func userBanAndDeleteAllMessages(
_ sessionId: String,
from roomTokens: [String]?,
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) -> Promise<[OnionRequestResponseInfoType]> {
let banRequestBody: UserBanRequest = UserBanRequest(
rooms: roomTokens,
global: (roomTokens == nil ? true : nil),
rooms: [roomToken],
global: nil,
timeout: nil
)
let deleteMessageRequestBody: UserDeleteMessagesRequest = UserDeleteMessagesRequest(
rooms: roomTokens,
global: (roomTokens == nil ? true : nil)
)
// Generate the requests
let requestResponseType: [BatchRequestInfoType] = [
@ -862,27 +852,22 @@ public enum OpenGroupAPI {
)
),
BatchRequestInfo(
request: Request(
method: .post,
request: Request<NoBody, Endpoint>(
method: .delete,
server: server,
endpoint: .userDeleteMessages(sessionId),
body: deleteMessageRequestBody
),
responseType: UserDeleteMessagesResponse.self
endpoint: Endpoint.roomDeleteMessages(roomToken, sessionId: sessionId)
)
)
]
return sequence(server, requests: requestResponseType, using: dependencies)
.map { results in
// TODO: Handle deletions...???? Hand off to OpenGroupAPIManager?
return results.values.map { responseInfo, _ in responseInfo }
}
.map { $0.values.map { responseInfo, _ in responseInfo } }
}
// MARK: - Authentication
/// Sign a message to be sent to SOGS (handles both un-blinded and blinded signing based on the server capabilities)
private static func sign(_ messageBytes: Bytes, for serverName: String, using dependencies: Dependencies = Dependencies()) -> (publicKey: String, signature: Bytes)? {
private static func sign(_ messageBytes: Bytes, for serverName: String, fallbackSigningType signingType: SessionId.Prefix, using dependencies: Dependencies = Dependencies()) -> (publicKey: String, signature: Bytes)? {
guard let userEdKeyPair: Box.KeyPair = dependencies.storage.getUserED25519KeyPair() else { return nil }
guard let serverPublicKey: String = dependencies.storage.getOpenGroupPublicKey(for: serverName) else {
return nil
@ -906,15 +891,30 @@ public enum OpenGroupAPI {
)
}
// Otherwise fall back to sign using the unblinded key
guard let signatureResult: Bytes = dependencies.sign.signature(message: messageBytes, secretKey: userEdKeyPair.secretKey) else {
return nil
// Otherwise sign using the fallback type
switch signingType {
case .unblinded:
guard let signatureResult: Bytes = dependencies.sign.signature(message: messageBytes, secretKey: userEdKeyPair.secretKey) else {
return nil
}
return (
publicKey: SessionId(.unblinded, publicKey: userEdKeyPair.publicKey).hexString,
signature: signatureResult
)
// Default to using the 'standard' key
default:
guard let userKeyPair: ECKeyPair = dependencies.storage.getUserKeyPair() else { return nil }
guard let signatureResult: Bytes = try? dependencies.ed25519.sign(data: messageBytes, keyPair: userKeyPair) else {
return nil
}
return (
publicKey: SessionId(.standard, publicKey: userKeyPair.publicKey.bytes).hexString,
signature: signatureResult
)
}
return (
publicKey: SessionId(.unblinded, publicKey: userEdKeyPair.publicKey).hexString,
signature: signatureResult
)
}
/// Sign a request to be sent to SOGS (handles both un-blinded and blinded signing based on the server capabilities)
@ -954,7 +954,7 @@ public enum OpenGroupAPI {
.appending(bodyHash ?? [])
/// Sign the above message
guard let signResult: (publicKey: String, signature: Bytes) = sign(messageBytes, for: serverName, using: dependencies) else {
guard let signResult: (publicKey: String, signature: Bytes) = sign(messageBytes, for: serverName, fallbackSigningType: .unblinded, using: dependencies) else {
return nil
}
@ -981,23 +981,15 @@ public enum OpenGroupAPI {
return Promise(error: error)
}
if request.useOnionRouting {
guard let publicKey = dependencies.storage.getOpenGroupPublicKey(for: request.server) else {
return Promise(error: Error.noPublicKey)
}
if request.isAuthRequired {
// Attempt to sign the request with the new auth
guard let signedRequest: URLRequest = sign(urlRequest, for: request.server, with: publicKey, using: dependencies) else {
return Promise(error: Error.signingFailed)
}
return dependencies.api.sendOnionRequest(signedRequest, to: request.server, with: publicKey)
}
return dependencies.api.sendOnionRequest(urlRequest, to: request.server, with: publicKey)
guard let publicKey = dependencies.storage.getOpenGroupPublicKey(for: request.server) else {
return Promise(error: Error.noPublicKey)
}
preconditionFailure("It's currently not allowed to send non onion routed requests.")
// Attempt to sign the request with the new auth
guard let signedRequest: URLRequest = sign(urlRequest, for: request.server, with: publicKey, using: dependencies) else {
return Promise(error: Error.signingFailed)
}
return dependencies.onionApi.sendOnionRequest(signedRequest, to: request.server, with: publicKey)
}
}

View File

@ -9,6 +9,9 @@ public final class OpenGroupManager: NSObject {
public var defaultRoomsPromise: Promise<[OpenGroupAPI.Room]>?
fileprivate var groupImagePromises: [String: Promise<Data>] = [:]
public var pollers: [String: OpenGroupAPI.Poller] = [:] // One for each server
public var isPolling: Bool = false
/// Server URL to room ID to set of user IDs
fileprivate var moderators: [String: [String: Set<String>]] = [:]
fileprivate var admins: [String: [String: Set<String>]] = [:]
@ -40,29 +43,31 @@ public final class OpenGroupManager: NSObject {
public let mutableCache: Atomic<Cache> = Atomic(Cache())
public var cache: Cache { return mutableCache.wrappedValue }
private var pollers: [String: OpenGroupAPI.Poller] = [:] // One for each server
private var isPolling = false
// MARK: - Polling
@objc public func startPolling() {
guard !isPolling else { return }
public func startPolling(using dependencies: Dependencies = Dependencies()) {
guard !cache.isPolling else { return }
isPolling = true
pollers = Set(Storage.shared.getAllOpenGroups().values.map { $0.server })
.reduce(into: [:]) { prev, server in
pollers[server]?.stop() // Should never occur
let poller = OpenGroupAPI.Poller(for: server)
poller.startIfNeeded()
prev[server] = poller
}
mutableCache.mutate { cache in
cache.isPolling = true
cache.pollers = Set(dependencies.storage.getAllOpenGroups().values.map { openGroup in openGroup.server })
.reduce(into: [:]) { prev, server in
cache.pollers[server]?.stop() // Should never occur
let poller = OpenGroupAPI.Poller(for: server)
poller.startIfNeeded(using: dependencies)
prev[server] = poller
}
}
}
@objc public func stopPolling() {
pollers.forEach { (_, openGroupPoller) in openGroupPoller.stop() }
pollers.removeAll()
mutableCache.mutate {
$0.pollers.forEach { (_, openGroupPoller) in openGroupPoller.stop() }
$0.pollers.removeAll()
$0.isPolling = false
}
}
// MARK: - Adding & Removing
@ -71,7 +76,7 @@ public final class OpenGroupManager: NSObject {
// 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)")
if OpenGroupManager.shared.pollers[server] != nil && TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupId), transaction: transaction) != nil {
if OpenGroupManager.shared.cache.pollers[server] != nil && TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupId), transaction: transaction) != nil {
SNLog("Ignoring join open group attempt (already joined), user initiated: \(!isConfigMessage)")
return Promise.value(())
}
@ -86,19 +91,13 @@ public final class OpenGroupManager: NSObject {
transaction.addCompletionQueue(DispatchQueue.global(qos: .userInitiated)) {
OpenGroupAPI.capabilitiesAndRoom(for: roomToken, on: server, using: dependencies)
.done(on: DispatchQueue.global(qos: .userInitiated)) { (capabilitiesResponse: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), roomResponse: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?)) in
guard let capabilities: OpenGroupAPI.Capabilities = capabilitiesResponse.data, let room: OpenGroupAPI.Room = roomResponse.data else {
SNLog("Failed to join open group due to invalid data.")
seal.reject(HTTP.Error.generic)
return
}
dependencies.storage.write { anyTransactionas in
guard let transaction: YapDatabaseReadWriteTransaction = anyTransactionas as? YapDatabaseReadWriteTransaction else { return }
.done(on: DispatchQueue.global(qos: .userInitiated)) { response in
dependencies.storage.write { anyTransaction in
guard let transaction: YapDatabaseReadWriteTransaction = anyTransaction as? YapDatabaseReadWriteTransaction else { return }
// Store the capabilities first
OpenGroupManager.handleCapabilities(
capabilities,
response.capabilities.data,
on: server,
using: transaction,
dependencies: dependencies
@ -106,7 +105,7 @@ public final class OpenGroupManager: NSObject {
// Then the room
OpenGroupManager.handleRoom(
room,
response.room.data,
publicKey: publicKey,
for: roomToken,
on: server,
@ -118,6 +117,7 @@ public final class OpenGroupManager: NSObject {
}
}
.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
SNLog("Failed to join open group.")
seal.reject(error)
}
}
@ -125,15 +125,13 @@ public final class OpenGroupManager: NSObject {
return promise
}
public func delete(_ openGroup: OpenGroup, associatedWith thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) {
let storage = SNMessagingKitConfiguration.shared.storage
public func delete(_ openGroup: OpenGroup, associatedWith thread: TSThread, using transaction: YapDatabaseReadWriteTransaction, dependencies: Dependencies = Dependencies()) {
// Stop the poller if needed
let openGroups = storage.getAllOpenGroups().values.filter { $0.server == openGroup.server }
let openGroups = dependencies.storage.getAllOpenGroups().values.filter { $0.server == openGroup.server }
if openGroups.count == 1 && openGroups.last == openGroup {
let poller = pollers[openGroup.server]
let poller = cache.pollers[openGroup.server]
poller?.stop()
pollers[openGroup.server] = nil
mutableCache.mutate { $0.pollers[openGroup.server] = nil }
}
// Remove all data
@ -143,17 +141,18 @@ public final class OpenGroupManager: NSObject {
messageIDs.insert(interaction.uniqueId!)
messageTimestamps.insert(interaction.timestamp)
}
storage.updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, using: transaction)
Storage.shared.removeReceivedMessageTimestamps(messageTimestamps, using: transaction)
Storage.shared.removeOpenGroupSequenceNumber(for: openGroup.room, on: openGroup.server, using: transaction)
dependencies.storage.updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, using: transaction)
dependencies.storage.removeReceivedMessageTimestamps(messageTimestamps, using: transaction)
dependencies.storage.removeOpenGroupSequenceNumber(for: openGroup.room, on: openGroup.server, using: transaction)
thread.removeAllThreadInteractions(with: transaction)
thread.remove(with: transaction)
Storage.shared.removeOpenGroup(for: thread.uniqueId!, using: transaction)
dependencies.storage.removeOpenGroup(for: thread.uniqueId!, using: transaction)
// Only remove the open group public key if the user isn't in any other rooms
// Only remove the open group public key and server info if the user isn't in any other rooms
if openGroups.count <= 1 {
Storage.shared.removeOpenGroupPublicKey(for: openGroup.server, using: transaction)
dependencies.storage.removeOpenGroupServer(name: openGroup.server, using: transaction)
dependencies.storage.removeOpenGroupPublicKey(for: openGroup.server, using: transaction)
}
}
@ -262,9 +261,11 @@ public final class OpenGroupManager: NSObject {
transaction.addCompletionQueue(DispatchQueue.global(qos: .userInitiated)) {
// Start the poller if needed
if OpenGroupManager.shared.pollers[server] == nil {
OpenGroupManager.shared.pollers[server] = OpenGroupAPI.Poller(for: server)
OpenGroupManager.shared.pollers[server]?.startIfNeeded()
if OpenGroupManager.shared.cache.pollers[server] == nil {
OpenGroupManager.shared.mutableCache.mutate {
$0.pollers[server] = OpenGroupAPI.Poller(for: server)
$0.pollers[server]?.startIfNeeded(using: dependencies)
}
}
// - Moderators
@ -649,6 +650,11 @@ public final class OpenGroupManager: NSObject {
}
extension OpenGroupManager {
@objc(startPolling)
public func objc_startPolling() {
startPolling()
}
@objc(getDefaultRoomsIfNeeded)
public static func objc_getDefaultRoomsIfNeeded() {
getDefaultRoomsIfNeeded()

View File

@ -24,6 +24,7 @@ extension OpenGroupAPI {
case roomMessagesRecent(String)
case roomMessagesBefore(String, id: UInt64)
case roomMessagesSince(String, seqNo: Int64)
case roomDeleteMessages(String, sessionId: String)
// Pinning
@ -50,7 +51,6 @@ extension OpenGroupAPI {
case userBan(String)
case userUnban(String)
case userModerator(String)
case userDeleteMessages(String)
var path: String {
switch self {
@ -84,6 +84,9 @@ extension OpenGroupAPI {
case .roomMessagesSince(let roomToken, let seqNo):
return "room/\(roomToken)/messages/since/\(seqNo)"
case .roomDeleteMessages(let roomToken, let sessionId):
return "room/\(roomToken)/all/\(sessionId)"
// Pinning
case .roomPinMessage(let roomToken, let messageId):
@ -114,7 +117,6 @@ extension OpenGroupAPI {
case .userBan(let sessionId): return "user/\(sessionId)/ban"
case .userUnban(let sessionId): return "user/\(sessionId)/unban"
case .userModerator(let sessionId): return "user/\(sessionId)/moderator"
case .userDeleteMessages(let sessionId): return "user/\(sessionId)/deleteMessages"
}
}
}

View File

@ -28,6 +28,7 @@ public protocol AeadXChaCha20Poly1305IetfType {
}
public protocol Ed25519Type {
func sign(data: Bytes, keyPair: ECKeyPair) throws -> Bytes?
func verifySignature(_ signature: Data, publicKey: Data, data: Data) throws -> Bool
}
@ -82,6 +83,10 @@ extension Sign: SignType {}
extension GenericHash: GenericHashType {}
struct Ed25519Wrapper: Ed25519Type {
func sign(data: Bytes, keyPair: ECKeyPair) throws -> Bytes? {
return try Ed25519.sign(Data(data), with: keyPair).bytes
}
func verifySignature(_ signature: Data, publicKey: Data, data: Data) throws -> Bool {
return try Ed25519.verifySignature(signature, publicKey: publicKey, data: data)
}

View File

@ -391,7 +391,8 @@ public final class MessageSender : NSObject {
on: server,
whisperTo: whisperTo,
whisperMods: whisperMods,
fileIds: fileIds
fileIds: fileIds,
using: dependencies
)
.done(on: DispatchQueue.global(qos: .userInitiated)) { responseInfo, data in
message.openGroupServerMessageID = UInt64(data.id)

View File

@ -18,15 +18,15 @@ extension OpenGroupAPI {
public init(for server: String) {
self.server = server
}
@objc public func startIfNeeded() {
public func startIfNeeded(using dependencies: Dependencies = Dependencies()) {
guard !hasStarted else { return }
hasStarted = true
timer = Timer.scheduledTimerOnMainThread(withTimeInterval: Poller.pollInterval, repeats: true) { _ in
self.poll().retainUntilComplete()
self.poll(using: dependencies).retainUntilComplete()
}
poll().retainUntilComplete()
poll(using: dependencies).retainUntilComplete()
}
@objc public func stop() {
@ -37,12 +37,12 @@ extension OpenGroupAPI {
// MARK: - Polling
@discardableResult
public func poll() -> Promise<Void> {
return poll(isBackgroundPoll: false)
public func poll(using dependencies: Dependencies = Dependencies()) -> Promise<Void> {
return poll(isBackgroundPoll: false, using: dependencies)
}
@discardableResult
public func poll(isBackgroundPoll: Bool) -> Promise<Void> {
public func poll(isBackgroundPoll: Bool, using dependencies: Dependencies = Dependencies()) -> Promise<Void> {
guard !self.isPolling else { return Promise.value(()) }
self.isPolling = true
@ -57,12 +57,13 @@ extension OpenGroupAPI {
hasPerformedInitialPoll: OpenGroupManager.shared.cache.hasPerformedInitialPoll[server] == true,
timeSinceLastPoll: (
OpenGroupManager.shared.cache.timeSinceLastPoll[server] ??
OpenGroupManager.shared.cache.getTimeSinceLastOpen()
)
OpenGroupManager.shared.cache.getTimeSinceLastOpen(using: dependencies)
),
using: dependencies
)
.done(on: OpenGroupAPI.workQueue) { [weak self] response in
self?.isPolling = false
self?.handlePollResponse(response, isBackgroundPoll: isBackgroundPoll)
self?.handlePollResponse(response, isBackgroundPoll: isBackgroundPoll, using: dependencies)
OpenGroupManager.shared.mutableCache.mutate { cache in
cache.hasPerformedInitialPoll[server] = true
@ -81,10 +82,10 @@ extension OpenGroupAPI {
return promise
}
private func handlePollResponse(_ response: [OpenGroupAPI.Endpoint: (info: OnionRequestResponseInfoType, data: Codable?)], isBackgroundPoll: Bool) {
private func handlePollResponse(_ response: [OpenGroupAPI.Endpoint: (info: OnionRequestResponseInfoType, data: Codable?)], isBackgroundPoll: Bool, using dependencies: Dependencies = Dependencies()) {
let server: String = self.server
Storage.shared.write { anyTransaction in
dependencies.storage.write { anyTransaction in
guard let transaction: YapDatabaseReadWriteTransaction = anyTransaction as? YapDatabaseReadWriteTransaction else {
SNLog("Open group polling failed due to invalid database transaction.")
return
@ -101,21 +102,30 @@ extension OpenGroupAPI {
OpenGroupManager.handleCapabilities(
responseBody,
on: server,
using: transaction
using: transaction,
dependencies: dependencies
)
case .roomMessagesRecent(let roomToken), .roomMessagesBefore(let roomToken, _), .roomMessagesSince(let roomToken, _):
guard let responseData: BatchSubResponse<[Message]> = endpointResponse.data as? BatchSubResponse<[Message]>, let responseBody: [Message] = responseData.body else {
guard let responseData: BatchSubResponse<[Failable<Message>]> = endpointResponse.data as? BatchSubResponse<[Failable<Message>]>, let responseBody: [Failable<Message>] = responseData.body else {
SNLog("Open group polling failed due to invalid data.")
return
}
let successfulMessages: [Message] = responseBody.compactMap { $0.value }
if successfulMessages.count != responseBody.count {
let droppedCount: Int = (responseBody.count - successfulMessages.count)
SNLog("Dropped \(droppedCount) invalid open group message\(droppedCount == 1 ? "" : "s").")
}
OpenGroupManager.handleMessages(
responseBody,
successfulMessages,
for: roomToken,
on: server,
isBackgroundPoll: isBackgroundPoll,
using: transaction
using: transaction,
dependencies: dependencies
)
case .roomPollInfo(let roomToken, _):
@ -129,7 +139,8 @@ extension OpenGroupAPI {
publicKey: nil,
for: roomToken,
on: server,
using: transaction
using: transaction,
dependencies: dependencies
)
case .inbox, .inboxSince, .outbox, .outboxSince:
@ -150,7 +161,8 @@ extension OpenGroupAPI {
fromOutbox: fromOutbox,
on: server,
isBackgroundPoll: isBackgroundPoll,
using: transaction
using: transaction,
dependencies: dependencies
)
default: break // No custom handling needed
@ -160,3 +172,10 @@ extension OpenGroupAPI {
}
}
}
extension OpenGroupAPI.Poller {
@objc(startIfNeeded)
public func objc_startIfNeeded() {
startIfNeeded()
}
}

View File

@ -8,10 +8,10 @@ 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 _onionApi: OnionRequestAPIType.Type?
public var onionApi: OnionRequestAPIType.Type {
get { getValueSettingIfNull(&_onionApi) { OnionRequestAPI.self } }
set { _onionApi = newValue }
}
private var _storage: SessionMessagingKitStorageProtocol?
@ -77,7 +77,7 @@ public class Dependencies {
// MARK: - Initialization
public init(
api: OnionRequestAPIType.Type? = nil,
onionApi: OnionRequestAPIType.Type? = nil,
storage: SessionMessagingKitStorageProtocol? = nil,
sodium: SodiumType? = nil,
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
@ -89,7 +89,7 @@ public class Dependencies {
standardUserDefaults: UserDefaultsType? = nil,
date: Date? = nil
) {
_api = api
_onionApi = onionApi
_storage = storage
_sodium = sodium
_aeadXChaCha20Poly1305Ietf = aeadXChaCha20Poly1305Ietf
@ -105,7 +105,7 @@ public class Dependencies {
// MARK: - Convenience
public func with(
api: OnionRequestAPIType.Type? = nil,
onionApi: OnionRequestAPIType.Type? = nil,
storage: SessionMessagingKitStorageProtocol? = nil,
sodium: SodiumType? = nil,
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
@ -118,7 +118,7 @@ public class Dependencies {
date: Date? = nil
) -> Dependencies {
return Dependencies(
api: (api ?? self._api),
onionApi: (onionApi ?? self._onionApi),
storage: (storage ?? self._storage),
sodium: (sodium ?? self._sodium),
aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf),

View File

@ -0,0 +1,24 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
struct Failable<T: Codable>: Codable {
let value: T?
init(from decoder: Decoder) throws {
guard let container = try? decoder.singleValueContainer() else {
self.value = nil
return
}
self.value = try? container.decode(T.self)
}
func encode(to encoder: Encoder) throws {
guard let value: T = value else { return }
var container: SingleValueEncodingContainer = encoder.singleValueContainer()
try container.encode(value)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,559 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Sodium
import SessionSnodeKit
import Quick
import Nimble
@testable import SessionMessagingKit
class OpenGroupManagerSpec: QuickSpec {
class TestCapabilitiesAndRoomApi: TestOnionRequestAPI {
static let capabilitiesData: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: nil)
static let roomData: OpenGroupAPI.Room = OpenGroupAPI.Room(
token: "test",
name: "test",
description: nil,
infoUpdates: 0,
messageSequence: 0,
created: 0,
activeUsers: 0,
activeUsersCutoff: 0,
imageId: nil,
pinnedMessages: nil,
admin: false,
globalAdmin: false,
admins: [],
hiddenAdmins: nil,
moderator: false,
globalModerator: false,
moderators: [],
hiddenModerators: nil,
read: false,
defaultRead: nil,
defaultAccessible: nil,
write: false,
defaultWrite: nil,
upload: false,
defaultUpload: nil
)
override class var mockResponse: Data? {
let responses: [Data] = [
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
code: 200,
headers: [:],
body: capabilitiesData,
failedToParseBody: false
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
code: 200,
headers: [:],
body: roomData,
failedToParseBody: false
)
)
]
return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8)
}
}
// MARK: - Spec
override func spec() {
var mockStorage: MockStorage!
var mockSodium: MockSodium!
var mockAeadXChaCha20Poly1305Ietf: MockAeadXChaCha20Poly1305Ietf!
var mockGenericHash: MockGenericHash!
var mockSign: MockSign!
var mockUserDefaults: MockUserDefaults!
var dependencies: Dependencies!
var testInteraction: TestInteraction!
var testGroupThread: TestGroupThread!
var testTransaction: TestTransaction!
describe("an OpenGroupAPI") {
// MARK: - Configuration
beforeEach {
mockStorage = MockStorage()
mockSodium = MockSodium()
mockAeadXChaCha20Poly1305Ietf = MockAeadXChaCha20Poly1305Ietf()
mockGenericHash = MockGenericHash()
mockSign = MockSign()
mockUserDefaults = MockUserDefaults()
dependencies = Dependencies(
onionApi: TestCapabilitiesAndRoomApi.self,
storage: mockStorage,
sodium: mockSodium,
aeadXChaCha20Poly1305Ietf: mockAeadXChaCha20Poly1305Ietf,
sign: mockSign,
genericHash: mockGenericHash,
ed25519: MockEd25519(),
nonceGenerator16: OpenGroupAPISpec.TestNonce16Generator(),
nonceGenerator24: OpenGroupAPISpec.TestNonce24Generator(),
standardUserDefaults: mockUserDefaults,
date: Date(timeIntervalSince1970: 1234567890)
)
testInteraction = TestInteraction()
testInteraction.mockData[.uniqueId] = "TestInteractionId"
testInteraction.mockData[.timestamp] = UInt64(123)
testGroupThread = TestGroupThread()
testGroupThread.mockData[.groupModel] = TSGroupModel(
title: "TestTitle",
memberIds: [],
image: nil,
groupId: LKGroupUtilities.getEncodedOpenGroupIDAsData("testServer.testRoom"),
groupType: .openGroup,
adminIds: [],
moderatorIds: []
)
testGroupThread.mockData[.interactions] = [testInteraction]
testTransaction = TestTransaction()
testTransaction.mockData[.objectForKey] = testGroupThread
mockStorage
.when { $0.write(with: { _ in }) }
.then { args in (args.first as? ((Any) -> Void))?(testTransaction as Any) }
.thenReturn(Promise.value(()))
mockStorage
.when { $0.write(with: { _ in }, completion: { }) }
.then { args in
(args.first as? ((Any) -> Void))?(testTransaction as Any)
(args.last as? (() -> Void))?()
}
.thenReturn(Promise.value(()))
mockStorage
.when { $0.getUserKeyPair() }
.thenReturn(
try! ECKeyPair(
publicKeyData: Data.data(fromHex: TestConstants.publicKey)!,
privateKeyData: Data.data(fromHex: TestConstants.privateKey)!
)
)
mockStorage
.when { $0.getUserED25519KeyPair() }
.thenReturn(
Box.KeyPair(
publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
)
)
mockStorage
.when { $0.getAllOpenGroups() }
.thenReturn([
"0": OpenGroup(
server: "testServer",
room: "testRoom",
publicKey: TestConstants.publicKey,
name: "Test",
groupDescription: nil,
imageID: nil,
infoUpdates: 0
)
])
mockStorage
.when { $0.getOpenGroup(for: any()) }
.thenReturn(
OpenGroup(
server: "testServer",
room: "testRoom",
publicKey: TestConstants.publicKey,
name: "Test",
groupDescription: nil,
imageID: nil,
infoUpdates: 0
)
)
mockStorage
.when { $0.getOpenGroupServer(name: any()) }
.thenReturn(
OpenGroupAPI.Server(
name: "testServer",
capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: [])
)
)
mockStorage
.when { $0.getOpenGroupPublicKey(for: any()) }
.thenReturn(TestConstants.publicKey)
mockGenericHash.when { $0.hash(message: any(), outputLength: any()) }.thenReturn([])
mockSodium
.when { $0.blindedKeyPair(serverPublicKey: any(), edKeyPair: any(), genericHash: mockGenericHash) }
.thenReturn(
Box.KeyPair(
publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
)
)
mockSodium
.when {
$0.sogsSignature(
message: any(),
secretKey: any(),
blindedSecretKey: any(),
blindedPublicKey: any()
)
}
.thenReturn("TestSogsSignature".bytes)
mockSign.when { $0.signature(message: any(), secretKey: any()) }.thenReturn("TestSignature".bytes)
}
afterEach {
OpenGroupManager.shared.stopPolling() // Need to stop any pollers which get created during tests
mockStorage = nil
mockSodium = nil
mockAeadXChaCha20Poly1305Ietf = nil
mockGenericHash = nil
mockSign = nil
mockUserDefaults = nil
dependencies = nil
testInteraction = nil
testGroupThread = nil
testTransaction = nil
}
// MARK: - Polling
context("when starting polling") {
beforeEach {
mockStorage
.when { $0.getAllOpenGroups() }
.thenReturn([
"0": OpenGroup(
server: "testServer",
room: "testRoom",
publicKey: TestConstants.publicKey,
name: "Test",
groupDescription: nil,
imageID: nil,
infoUpdates: 0
),
"1": OpenGroup(
server: "testServer1",
room: "testRoom1",
publicKey: TestConstants.publicKey,
name: "Test1",
groupDescription: nil,
imageID: nil,
infoUpdates: 0
)
])
mockStorage.when { $0.removeOpenGroupSequenceNumber(for: any(), on: any(), using: any()) }.thenReturn(())
mockStorage.when { $0.setOpenGroupPublicKey(for: any(), to: any(), using: any()) }.thenReturn(())
mockStorage.when { $0.setOpenGroupServer(any(), using: any()) }.thenReturn(())
mockStorage.when { $0.setOpenGroup(any(), for: any(), using: any()) }.thenReturn(())
mockStorage.when { $0.setUserCount(to: any(), forOpenGroupWithID: any(), using: any()) }.thenReturn(())
mockStorage.when { $0.getOpenGroupInboxLatestMessageId(for: any()) }.thenReturn(nil)
mockStorage.when { $0.getOpenGroupOutboxLatestMessageId(for: any()) }.thenReturn(nil)
mockStorage.when { $0.getOpenGroupSequenceNumber(for: any(), on: any()) }.thenReturn(nil)
mockUserDefaults
.when { $0.object(forKey: SNUserDefaults.Date.lastOpen.rawValue) }
.thenReturn(Date(timeIntervalSince1970: 1234567890))
}
it("creates pollers for all of the open groups") {
OpenGroupManager.shared.startPolling(using: dependencies)
expect(OpenGroupManager.shared.cache.pollers.keys.map { String($0) }.sorted)
.to(equal(["testserver", "testserver1"]))
}
it("updates the isPolling flag") {
OpenGroupManager.shared.startPolling(using: dependencies)
expect(OpenGroupManager.shared.cache.isPolling).to(beTrue())
}
it("does nothing if already polling") {
OpenGroupManager.shared.mutableCache.mutate { $0.isPolling = true }
OpenGroupManager.shared.startPolling(using: dependencies)
expect(OpenGroupManager.shared.cache.pollers.keys.map { String($0) })
.to(equal([]))
}
}
context("when stopping polling") {
beforeEach {
mockStorage
.when { $0.getAllOpenGroups() }
.thenReturn([
"0": OpenGroup(
server: "testServer",
room: "testRoom",
publicKey: TestConstants.publicKey,
name: "Test",
groupDescription: nil,
imageID: nil,
infoUpdates: 0
),
"1": OpenGroup(
server: "testServer1",
room: "testRoom1",
publicKey: TestConstants.publicKey,
name: "Test1",
groupDescription: nil,
imageID: nil,
infoUpdates: 0
)
])
mockStorage.when { $0.removeOpenGroupSequenceNumber(for: any(), on: any(), using: any()) }.thenReturn(())
mockStorage.when { $0.setOpenGroupPublicKey(for: any(), to: any(), using: any()) }.thenReturn(())
mockStorage.when { $0.setOpenGroupServer(any(), using: any()) }.thenReturn(())
mockStorage.when { $0.setOpenGroup(any(), for: any(), using: any()) }.thenReturn(())
mockStorage.when { $0.setUserCount(to: any(), forOpenGroupWithID: any(), using: any()) }.thenReturn(())
mockStorage.when { $0.getOpenGroupInboxLatestMessageId(for: any()) }.thenReturn(nil)
mockStorage.when { $0.getOpenGroupOutboxLatestMessageId(for: any()) }.thenReturn(nil)
mockStorage.when { $0.getOpenGroupSequenceNumber(for: any(), on: any()) }.thenReturn(nil)
mockUserDefaults
.when { $0.object(forKey: SNUserDefaults.Date.lastOpen.rawValue) }
.thenReturn(Date(timeIntervalSince1970: 1234567890))
OpenGroupManager.shared.startPolling(using: dependencies)
}
it("removes all pollers") {
OpenGroupManager.shared.stopPolling()
expect(OpenGroupManager.shared.cache.pollers.keys.map { String($0) })
.to(equal([]))
}
it("updates the isPolling flag") {
OpenGroupManager.shared.stopPolling()
expect(OpenGroupManager.shared.cache.isPolling).to(beFalse())
}
}
// MARK: - Adding & Removing
context("when adding") {
beforeEach {
mockStorage.when { $0.removeOpenGroupSequenceNumber(for: any(), on: any(), using: any()) }.thenReturn(())
mockStorage.when { $0.setOpenGroupPublicKey(for: any(), to: any(), using: any()) }.thenReturn(())
mockStorage.when { $0.setOpenGroupServer(any(), using: any()) }.thenReturn(())
mockStorage.when { $0.setOpenGroup(any(), for: any(), using: any()) }.thenReturn(())
mockStorage.when { $0.setUserCount(to: any(), forOpenGroupWithID: any(), using: any()) }.thenReturn(())
mockStorage.when { $0.getOpenGroupInboxLatestMessageId(for: any()) }.thenReturn(nil)
mockStorage.when { $0.getOpenGroupOutboxLatestMessageId(for: any()) }.thenReturn(nil)
mockStorage.when { $0.getOpenGroupSequenceNumber(for: any(), on: any()) }.thenReturn(nil)
mockUserDefaults
.when { $0.object(forKey: SNUserDefaults.Date.lastOpen.rawValue) }
.thenReturn(Date(timeIntervalSince1970: 1234567890))
}
it("resets the sequence number of the open group") {
OpenGroupManager.shared
.add(
roomToken: "testRoom",
server: "testServer",
publicKey: "testKey",
isConfigMessage: false,
using: testTransaction,
dependencies: dependencies
)
.retainUntilComplete()
expect(mockStorage)
.to(
call(.exactly(times: 1)) {
$0.removeOpenGroupSequenceNumber(
for: "testRoom",
on: "testServer",
using: testTransaction as Any
)
}
)
}
it("sets the public key of the open group server") {
OpenGroupManager.shared
.add(
roomToken: "testRoom",
server: "testServer",
publicKey: "testKey",
isConfigMessage: false,
using: testTransaction,
dependencies: dependencies
)
.retainUntilComplete()
expect(mockStorage)
.to(
call(.exactly(times: 1)) {
$0.setOpenGroupPublicKey(
for: "testRoom",
to: "testKey",
using: testTransaction as Any
)
}
)
}
it("adds a poller") {
OpenGroupManager.shared
.add(
roomToken: "testRoom",
server: "testServer",
publicKey: "testKey",
isConfigMessage: false,
using: testTransaction,
dependencies: dependencies
)
.retainUntilComplete()
expect(OpenGroupManager.shared.cache.pollers["testServer"])
.toEventuallyNot(
beNil(),
timeout: .milliseconds(100)
)
}
context("an existing room") {
beforeEach {
OpenGroupManager.shared
.add(
roomToken: "testRoom",
server: "testServer",
publicKey: "testKey",
isConfigMessage: false,
using: testTransaction,
dependencies: dependencies
)
.retainUntilComplete()
// There is a bunch of async code in this function so we need to wait until if finishes
// processing before doing the actual tests
expect(mockStorage)
.toEventually(
call { $0.setOpenGroup(any(), for: "testServer", using: testTransaction as Any) },
timeout: .milliseconds(100)
)
mockStorage.resetCallCounts()
}
it("does not reset the sequence number or update the public key") {
OpenGroupManager.shared
.add(
roomToken: "testRoom",
server: "testServer",
publicKey: "testKey",
isConfigMessage: false,
using: testTransaction,
dependencies: dependencies
)
.retainUntilComplete()
expect(mockStorage)
.toEventuallyNot(
call {
$0.removeOpenGroupSequenceNumber(
for: "testRoom",
on: "testServer",
using: testTransaction as Any
)
},
timeout: .milliseconds(100)
)
expect(mockStorage)
.toEventuallyNot(
call {
$0.setOpenGroupPublicKey(
for: "testRoom",
to: "testKey",
using: testTransaction as Any
)
},
timeout: .milliseconds(100)
)
}
}
context("with an invalid response") {
beforeEach {
class TestApi: TestOnionRequestAPI {
override class var mockResponse: Data? { return Data() }
}
dependencies = dependencies.with(onionApi: TestApi.self)
mockUserDefaults
.when { $0.object(forKey: SNUserDefaults.Date.lastOpen.rawValue) }
.thenReturn(Date(timeIntervalSince1970: 1234567890))
}
it("fails with the error") {
var error: Error?
let promise = OpenGroupManager.shared
.add(
roomToken: "testRoom",
server: "testServer",
publicKey: "testKey",
isConfigMessage: false,
using: testTransaction,
dependencies: dependencies
)
promise.catch { error = $0 }
promise.retainUntilComplete()
expect(error?.localizedDescription)
.toEventually(
equal(HTTP.Error.parsingFailed.localizedDescription),
timeout: .milliseconds(100)
)
}
}
}
context("when removing") {
// public func delete(_ openGroup: OpenGroup, associatedWith thread: TSThread, using transaction: YapDatabaseReadWriteTransaction, dependencies: Dependencies = Dependencies()) {
// // Stop the poller if needed
// let openGroups = dependencies.storage.getAllOpenGroups().values.filter { $0.server == openGroup.server }
// if openGroups.count == 1 && openGroups.last == openGroup {
// let poller = pollers[openGroup.server]
// poller?.stop()
// pollers[openGroup.server] = nil
// }
//
// // Remove all data
// var messageIDs: Set<String> = []
// var messageTimestamps: Set<UInt64> = []
// thread.enumerateInteractions(with: transaction) { interaction, _ in
// messageIDs.insert(interaction.uniqueId!)
// messageTimestamps.insert(interaction.timestamp)
// }
// dependencies.storage.updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, using: transaction)
// dependencies.storage.removeReceivedMessageTimestamps(messageTimestamps, using: transaction)
// dependencies.storage.removeOpenGroupSequenceNumber(for: openGroup.room, on: openGroup.server, using: transaction)
//
// thread.removeAllThreadInteractions(with: transaction)
// thread.remove(with: transaction)
// dependencies.storage.removeOpenGroup(for: thread.uniqueId!, using: transaction)
//
// // Only remove the open group public key and server info if the user isn't in any other rooms
// if openGroups.count <= 1 {
// dependencies.storage.removeOpenGroupServer(name: openGroup.server, using: transaction)
// dependencies.storage.removeOpenGroupPublicKey(for: openGroup.server, using: transaction)
// }
}
}
}
}

View File

@ -34,6 +34,8 @@ class SOGSEndpointSpec: QuickSpec {
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"))
expect(OpenGroupAPI.Endpoint.roomDeleteMessages("test", sessionId: "testId").path)
.to(equal("room/test/all/testId"))
// Pinning
@ -60,7 +62,6 @@ class SOGSEndpointSpec: QuickSpec {
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"))
}
}
}

View File

@ -7,6 +7,10 @@ import Sodium
@testable import SessionMessagingKit
class MockEd25519: Mock<Ed25519Type>, Ed25519Type {
func sign(data: Bytes, keyPair: ECKeyPair) throws -> Bytes? {
return accept(args: [data, keyPair]) as? Bytes
}
func verifySignature(_ signature: Data, publicKey: Data, data: Data) throws -> Bool {
return accept(args: [signature, publicKey, data]) as! Bool
}

View File

@ -0,0 +1,27 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
class MockUserDefaults: Mock<UserDefaultsType>, UserDefaultsType {
var storage: [String: Any] = [:]
func object(forKey defaultName: String) -> Any? { return accept(args: [defaultName]) }
func string(forKey defaultName: String) -> String? { return accept(args: [defaultName]) as? String }
func array(forKey defaultName: String) -> [Any]? { return accept(args: [defaultName]) as? [Any] }
func dictionary(forKey defaultName: String) -> [String: Any]? { return accept(args: [defaultName]) as? [String: Any] }
func data(forKey defaultName: String) -> Data? { return accept(args: [defaultName]) as? Data }
func stringArray(forKey defaultName: String) -> [String]? { return accept(args: [defaultName]) as? [String] }
func integer(forKey defaultName: String) -> Int { return ((accept(args: [defaultName]) as? Int) ?? 0) }
func float(forKey defaultName: String) -> Float { return ((accept(args: [defaultName]) as? Float) ?? 0) }
func double(forKey defaultName: String) -> Double { return ((accept(args: [defaultName]) as? Double) ?? 0) }
func bool(forKey defaultName: String) -> Bool { return ((accept(args: [defaultName]) as? Bool) ?? false) }
func url(forKey defaultName: String) -> URL? { return accept(args: [defaultName]) as? URL }
func set(_ value: Any?, forKey defaultName: String) { accept(args: [value, defaultName]) }
func set(_ value: Int, forKey defaultName: String) { accept(args: [value, defaultName]) }
func set(_ value: Float, forKey defaultName: String) { accept(args: [value, defaultName]) }
func set(_ value: Double, forKey defaultName: String) { accept(args: [value, defaultName]) }
func set(_ value: Bool, forKey defaultName: String) { accept(args: [value, defaultName]) }
func set(_ url: URL?, forKey defaultName: String) { accept(args: [url, defaultName]) }
}

View File

@ -7,9 +7,3 @@ protocol Mockable {
var mockData: [Key: Any] { get }
}
protocol StaticMockable {
associatedtype Key: Hashable
static var mockData: [Key: Any] { get }
}

View File

@ -0,0 +1,23 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionMessagingKit
extension OpenGroup: Mocked {
static var mockValue: OpenGroup = OpenGroup(
server: any(),
room: any(),
publicKey: TestConstants.publicKey,
name: any(),
groupDescription: any(),
imageID: any(),
infoUpdates: any()
)
}
extension OpenGroupAPI.Server: Mocked {
static var mockValue: OpenGroupAPI.Server = OpenGroupAPI.Server(
name: any(),
capabilities: OpenGroupAPI.Capabilities(capabilities: any(), missing: any())
)
}

View File

@ -0,0 +1,32 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionMessagingKit
// FIXME: Turn this into a protocol to make mocking possible
class TestGroupThread: TSGroupThread, Mockable {
// MARK: - Mockable
enum DataKey: Hashable {
case groupModel
case interactions
}
typealias Key = DataKey
var mockData: [DataKey: Any] = [:]
var didCallSave: Bool = false
// MARK: - TSGroupThread
override var groupModel: TSGroupModel {
get { (mockData[.groupModel] as! TSGroupModel) }
set {}
}
override func enumerateInteractions(_ block: @escaping (TSInteraction) -> Void) {
((mockData[.interactions] as? [TSInteraction]) ?? []).forEach(block)
}
override func save(with transaction: YapDatabaseReadWriteTransaction) { didCallSave = true }
}

View File

@ -1,3 +1,32 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionMessagingKit
// FIXME: Turn this into a protocol to make mocking possible
class TestInteraction: TSInteraction, Mockable {
// MARK: - Mockable
enum DataKey: Hashable {
case uniqueId
case timestamp
}
typealias Key = DataKey
var mockData: [DataKey: Any] = [:]
var didCallSave: Bool = true
// MARK: - TSInteraction
override var uniqueId: String? {
get { (mockData[.uniqueId] as? String) }
set { mockData[.uniqueId] = newValue }
}
override var timestamp: UInt64 {
(mockData[.timestamp] as! UInt64)
}
override func save(with transaction: YapDatabaseReadWriteTransaction) { didCallSave = true }
}

View File

@ -0,0 +1,60 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import SessionSnodeKit
@testable import SessionMessagingKit
// FIXME: Change 'OnionRequestAPIType' to have instance methods instead of static methods once everything is updated to use 'Dependencies'
class TestOnionRequestAPI: OnionRequestAPIType {
struct RequestData: Codable {
let urlString: String?
let httpMethod: String
let headers: [String: String]
let snodeMethod: String?
let body: Data?
let server: String
let version: OnionRequestAPI.Version
let publicKey: String?
}
class ResponseInfo: OnionRequestResponseInfoType {
let requestData: RequestData
let code: Int
let headers: [String: String]
init(requestData: RequestData, code: Int, headers: [String: String]) {
self.requestData = requestData
self.code = code
self.headers = headers
}
}
class var mockResponse: Data? { return nil }
static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPI.Version, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> {
let responseInfo: ResponseInfo = ResponseInfo(
requestData: RequestData(
urlString: request.url?.absoluteString,
httpMethod: (request.httpMethod ?? "GET"),
headers: (request.allHTTPHeaderFields ?? [:]),
snodeMethod: nil,
body: request.httpBody,
server: server,
version: version,
publicKey: x25519PublicKey
),
code: 200,
headers: [:]
)
return Promise.value((responseInfo, mockResponse))
}
static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, using version: OnionRequestAPI.Version, associatedWith publicKey: String?) -> Promise<Data> {
// TODO: Test the 'responseInfo' somehow?
return Promise.value(mockResponse!)
}
}

View File

@ -1,3 +1,26 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionMessagingKit
// FIXME: Turn this into a protocol to make mocking possible
class TestThread: TSThread, Mockable {
// MARK: - Mockable
enum DataKey: Hashable {
case interactions
}
typealias Key = DataKey
var mockData: [DataKey: Any] = [:]
var didCallSave: Bool = false
// MARK: - TSThread
override func enumerateInteractions(_ block: @escaping (TSInteraction) -> Void) {
((mockData[.interactions] as? [TSInteraction]) ?? []).forEach(block)
}
override func save(with transaction: YapDatabaseReadWriteTransaction) { didCallSave = true }
}

View File

@ -0,0 +1,31 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import YapDatabase
// FIXME: Turn this into a protocol to make mocking possible
final class TestTransaction: YapDatabaseReadWriteTransaction, Mockable {
// MARK: - Mockable
enum DataKey: Hashable {
case objectForKey
}
typealias Key = DataKey
var mockData: [DataKey: Any] = [:]
// MARK: - YapDatabaseReadWriteTransaction
override func object(forKey key: String, inCollection collection: String?) -> Any? {
return mockData[.objectForKey]
}
override func addCompletionQueue(_ completionQueue: DispatchQueue?, completionBlock: @escaping () -> Void) {
completionBlock()
}
}
extension TestTransaction: Mocked {
static var mockValue: TestTransaction = TestTransaction()
}

View File

@ -1,27 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
class TestUserDefaults: UserDefaultsType {
var storage: [String: Any] = [:]
func object(forKey defaultName: String) -> Any? { return storage[defaultName] }
func string(forKey defaultName: String) -> String? { return storage[defaultName] as? String }
func array(forKey defaultName: String) -> [Any]? { return storage[defaultName] as? [Any] }
func dictionary(forKey defaultName: String) -> [String: Any]? { return storage[defaultName] as? [String: Any] }
func data(forKey defaultName: String) -> Data? { return storage[defaultName] as? Data }
func stringArray(forKey defaultName: String) -> [String]? { return storage[defaultName] as? [String] }
func integer(forKey defaultName: String) -> Int { return ((storage[defaultName] as? Int) ?? 0) }
func float(forKey defaultName: String) -> Float { return ((storage[defaultName] as? Float) ?? 0) }
func double(forKey defaultName: String) -> Double { return ((storage[defaultName] as? Double) ?? 0) }
func bool(forKey defaultName: String) -> Bool { return ((storage[defaultName] as? Bool) ?? false) }
func url(forKey defaultName: String) -> URL? { return storage[defaultName] as? URL }
func set(_ value: Any?, forKey defaultName: String) { storage[defaultName] = value }
func set(_ value: Int, forKey defaultName: String) { storage[defaultName] = value }
func set(_ value: Float, forKey defaultName: String) { storage[defaultName] = value }
func set(_ value: Double, forKey defaultName: String) { storage[defaultName] = value }
func set(_ value: Bool, forKey defaultName: String) { storage[defaultName] = value }
func set(_ url: URL?, forKey defaultName: String) { storage[defaultName] = url }
}

View File

@ -27,13 +27,13 @@ public class Atomic<Value> {
// MARK: - Initialization
init(_ initialValue: Value) {
public init(_ initialValue: Value) {
self.value = initialValue
}
// MARK: - Functions
func mutate(_ mutation: (inout Value) -> Void) {
public func mutate(_ mutation: (inout Value) -> Void) {
return queue.sync {
mutation(&value)
}

View File

@ -2,6 +2,7 @@
import Foundation
import Sodium
import Curve25519Kit
extension Box.KeyPair: Mocked {
static var mockValue: Box.KeyPair = Box.KeyPair(
@ -9,3 +10,12 @@ extension Box.KeyPair: Mocked {
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
)
}
extension ECKeyPair: Mocked {
static var mockValue: Self {
try! Self.init(
publicKeyData: Data.data(fromHex: TestConstants.publicKey)!,
privateKeyData: Data.data(fromHex: TestConstants.privateKey)!
)
}
}

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
// MARK: - Mocked
@ -22,11 +23,15 @@ public class Mock<T> {
private let functionHandler: MockFunctionHandler
internal let functionConsumer: FunctionConsumer
// MARK: - Initialization
internal required init(functionHandler: MockFunctionHandler? = nil) {
self.functionConsumer = FunctionConsumer()
self.functionHandler = (functionHandler ?? self.functionConsumer)
}
// MARK: - MockFunctionHandler
@discardableResult internal func accept(funcName: String = #function, args: [Any?] = []) -> Any? {
return accept(funcName: funcName, checkArgs: args, actionArgs: args)
}
@ -35,6 +40,19 @@ public class Mock<T> {
return functionHandler.accept(funcName, parameterSummary: summary(for: checkArgs), actionArgs: actionArgs)
}
// MARK: - Functions
internal func reset() {
functionConsumer.trackCalls = true
functionConsumer.functionBuilders = []
functionConsumer.functionHandlers = [:]
functionConsumer.calls.mutate { $0 = [:] }
}
internal func resetCallCounts() {
functionConsumer.calls.mutate { $0 = [:] }
}
internal func when<R>(_ callBlock: @escaping (T) throws -> R) -> MockFunctionBuilder<T, R> {
let builder: MockFunctionBuilder<T, R> = MockFunctionBuilder(callBlock, mockInit: type(of: self).init)
functionConsumer.functionBuilders.append(builder.build)
@ -42,6 +60,8 @@ public class Mock<T> {
return builder
}
// MARK: - Convenience
private func summary(for argument: Any) -> String {
switch argument {
case let string as String: return string
@ -134,7 +154,7 @@ internal class FunctionConsumer: MockFunctionHandler {
var trackCalls: Bool = true
var functionBuilders: [() throws -> MockFunction?] = []
var functionHandlers: [String: [String: MockFunction]] = [:]
var calls: [String: [String]] = [:]
var calls: Atomic<[String: [String]]> = Atomic([:])
func accept(_ functionName: String, parameterSummary: String, actionArgs: [Any?]) -> Any? {
if !functionBuilders.isEmpty {
@ -154,7 +174,7 @@ internal class FunctionConsumer: MockFunctionHandler {
// Record the call so it can be validated later (assuming we are tracking calls)
if trackCalls {
calls[functionName] = (calls[functionName] ?? []).appending(parameterSummary)
calls.mutate { $0[functionName] = ($0[functionName] ?? []).appending(parameterSummary) }
}
for action in expectation.actions {

View File

@ -181,12 +181,12 @@ fileprivate func generateCallInfo<M, T, R>(_ actualExpression: Expression<M>, _
return
}
allFunctionsCalled = Array(validInstance.functionConsumer.calls.keys)
allFunctionsCalled = Array(validInstance.functionConsumer.calls.wrappedValue.keys)
let builder: MockFunctionBuilder<T, R> = builderCreator(validInstance)
validInstance.functionConsumer.trackCalls = false
maybeFunction = try? builder.build()
desiredFunctionCalls = (validInstance.functionConsumer.calls[maybeFunction?.name ?? ""] ?? [])
desiredFunctionCalls = (validInstance.functionConsumer.calls.wrappedValue[maybeFunction?.name ?? ""] ?? [])
validInstance.functionConsumer.trackCalls = true
}
catch {
@ -205,12 +205,12 @@ fileprivate func generateCallInfo<M, T, R>(_ actualExpression: Expression<M>, _
// Just hope for the best and if there is a force-cast there's not much we can do
guard let validInstance: M = try? actualExpression.evaluate() else { return CallInfo.error }
allFunctionsCalled = Array(validInstance.functionConsumer.calls.keys)
allFunctionsCalled = Array(validInstance.functionConsumer.calls.wrappedValue.keys)
let builder: MockExpectationBuilder<T, R> = builderCreator(validInstance)
validInstance.functionConsumer.trackCalls = false
maybeFunction = try? builder.build()
desiredFunctionCalls = (validInstance.functionConsumer.calls[maybeFunction?.name ?? ""] ?? [])
desiredFunctionCalls = (validInstance.functionConsumer.calls.wrappedValue[maybeFunction?.name ?? ""] ?? [])
validInstance.functionConsumer.trackCalls = true
#endif