diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index b3cb6f453..f9e645af6 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1172,7 +1172,7 @@ extension ConversationVC: openGroupPublicKey: String? ) { guard viewModel.threadData.canWrite else { return } - // FIXME: Add in support for starting a thread with a 'blinded25' id + // FIXME: Add in support for starting a thread with a 'blinded25' id (disabled until we support this decoding) guard (try? SessionId.Prefix(from: sessionId)) != .blinded25 else { return } guard (try? SessionId.Prefix(from: sessionId)) == .blinded15 else { viewModel.dependencies[singleton: .storage].write { [dependencies = viewModel.dependencies] db in diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index 368f7bf82..c0db73b01 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -63,6 +63,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case debugDisappearingMessageDurations case updatedGroups + case updatedGroupsDisableAutoApprove case updatedGroupsRemoveMessagesOnKick case updatedGroupsAllowHistoricAccessOnInvite case updatedGroupsAllowDisplayPicture @@ -84,6 +85,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, let updatedDisappearingMessages: Bool let updatedGroups: Bool + let updatedGroupsDisableAutoApprove: Bool let updatedGroupsRemoveMessagesOnKick: Bool let updatedGroupsAllowHistoricAccessOnInvite: Bool let updatedGroupsAllowDisplayPicture: Bool @@ -102,6 +104,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, debugDisappearingMessageDurations: dependencies[feature: .debugDisappearingMessageDurations], updatedDisappearingMessages: dependencies[feature: .updatedDisappearingMessages], updatedGroups: dependencies[feature: .updatedGroups], + updatedGroupsDisableAutoApprove: dependencies[feature: .updatedGroupsDisableAutoApprove], updatedGroupsRemoveMessagesOnKick: dependencies[feature: .updatedGroupsRemoveMessagesOnKick], updatedGroupsAllowHistoricAccessOnInvite: dependencies[feature: .updatedGroupsAllowHistoricAccessOnInvite], updatedGroupsAllowDisplayPicture: dependencies[feature: .updatedGroupsAllowDisplayPicture], @@ -257,6 +260,27 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ), onTap: { self?.updateFlag(for: .updatedGroups, to: !current.updatedGroups) } ), + SessionCell.Info( + id: .updatedGroupsDisableAutoApprove, + title: "Disable Auto Approve", + subtitle: """ + Prevents a group from automatically getting approved if the admin is already approved. + + Note: The default behaviour is to automatically approve new groups if the admin that sent the invitation is an approved contact. + """, + trailingAccessory: .toggle( + .boolValue( + current.updatedGroupsDisableAutoApprove, + oldValue: (previous ?? current).updatedGroupsDisableAutoApprove + ) + ), + onTap: { + self?.updateFlag( + for: .updatedGroupsDisableAutoApprove, + to: !current.updatedGroupsDisableAutoApprove + ) + } + ), SessionCell.Info( id: .updatedGroupsRemoveMessagesOnKick, title: "Remove Messages on Kick", @@ -403,6 +427,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .updatedDisappearingMessages: updateFlag(for: .updatedDisappearingMessages, to: nil) case .updatedGroups: updateFlag(for: .updatedGroups, to: nil) + case .updatedGroupsDisableAutoApprove: updateFlag(for: .updatedGroupsDisableAutoApprove, to: nil) case .updatedGroupsRemoveMessagesOnKick: updateFlag(for: .updatedGroupsRemoveMessagesOnKick, to: nil) case .updatedGroupsAllowHistoricAccessOnInvite: updateFlag(for: .updatedGroupsAllowHistoricAccessOnInvite, to: nil) @@ -472,6 +497,13 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, networkCache.currentRequests = [:] } + /// Unsubscribe from push notifications (do this after cancelling pending network requests as we don't want these to be cancelled) + if let existingToken: String = dependencies[singleton: .storage, key: .lastRecordedPushToken] { + PushNotificationAPI + .unsubscribeAll(token: Data(hex: existingToken), using: dependencies) + .sinkUntilComplete() + } + /// Clear the snodeAPI and getSnodePool caches dependencies.mutate(cache: .snodeAPI) { $0.snodePool = [] @@ -535,6 +567,9 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, onComplete: { [dependencies] _ in /// Restart the current user poller (there won't be any other pollers though) dependencies[singleton: .currentUserPoller].start(using: dependencies) + + /// Re-sync the push tokens (if there are any) + SyncPushTokensJob.run(uploadOnlyIfStale: false) }, using: dependencies ) diff --git a/Session/Shared/UserListViewModel.swift b/Session/Shared/UserListViewModel.swift index a31eac67f..a66ea4bae 100644 --- a/Session/Shared/UserListViewModel.swift +++ b/Session/Shared/UserListViewModel.swift @@ -255,7 +255,7 @@ public enum UserListError: LocalizedError { public var errorDescription: String? { switch self { - case .error(let content): content + case .error(let content): return content } } } diff --git a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift b/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift index 5ce2996c3..d74c7a8de 100644 --- a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift +++ b/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift @@ -55,13 +55,14 @@ enum _014_GenerateInitialUserConfigDumps: Migration { in: config ) - if config.needsDump { + if config.needsDump(using: dependencies) { try SessionUtil .createDump( config: config, for: .userProfile, sessionId: userSessionId, - timestampMs: timestampMs + timestampMs: timestampMs, + using: dependencies )? .upsert(db) } @@ -121,13 +122,14 @@ enum _014_GenerateInitialUserConfigDumps: Migration { in: config ) - if config.needsDump { + if config.needsDump(using: dependencies) { try SessionUtil .createDump( config: config, for: .contacts, sessionId: userSessionId, - timestampMs: timestampMs + timestampMs: timestampMs, + using: dependencies )? .upsert(db) } @@ -146,13 +148,14 @@ enum _014_GenerateInitialUserConfigDumps: Migration { in: config ) - if config.needsDump { + if config.needsDump(using: dependencies) { try SessionUtil .createDump( config: config, for: .convoInfoVolatile, sessionId: userSessionId, - timestampMs: timestampMs + timestampMs: timestampMs, + using: dependencies )? .upsert(db) } @@ -182,13 +185,14 @@ enum _014_GenerateInitialUserConfigDumps: Migration { in: config ) - if config.needsDump { + if config.needsDump(using: dependencies) { try SessionUtil .createDump( config: config, for: .userGroups, sessionId: userSessionId, - timestampMs: timestampMs + timestampMs: timestampMs, + using: dependencies )? .upsert(db) } diff --git a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInfoChangeMessage.swift b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInfoChangeMessage.swift index b07fde00b..30b6fa3a6 100644 --- a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInfoChangeMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInfoChangeMessage.swift @@ -1,4 +1,6 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable import Foundation import GRDB diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift index 7fc96e51d..a5bdfac34 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift @@ -132,7 +132,11 @@ extension MessageReceiver { /// With updated groups they should be considered message requests (`invited: true`) unless person sending the invitation is /// an approved contact of the user, this is designed to reduce spam via groups getting around message requests if users are on old /// or modified clients - let inviteSenderIsApproved: Bool = ((try? Contact.fetchOne(db, id: sender))?.isApproved == true) + let inviteSenderIsApproved: Bool = { + guard !dependencies[feature: .updatedGroupsDisableAutoApprove] else { return false } + + return ((try? Contact.fetchOne(db, id: sender))?.isApproved == true) + }() let threadAlreadyExisted: Bool = ((try? SessionThread.exists(db, id: message.groupSessionId.hexString)) ?? false) let wasKickedFromGroup: Bool = SessionUtil.wasKickedFromGroup( groupSessionId: message.groupSessionId, diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift index 14b9e6e37..3a9d87b3e 100644 --- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift +++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift @@ -37,7 +37,7 @@ internal extension SessionUtil { serverTimestampMs: Int64, using dependencies: Dependencies ) throws { - guard config.needsDump else { return } + guard config.needsDump(using: dependencies) else { return } guard case .object(let conf) = config else { throw SessionUtilError.invalidConfigObject } // The current users contact data is handled separately so exclude it if it's present (as that's diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+ConvoInfoVolatile.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+ConvoInfoVolatile.swift index c4ea213ea..e41e338db 100644 --- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+ConvoInfoVolatile.swift +++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+ConvoInfoVolatile.swift @@ -19,7 +19,7 @@ internal extension SessionUtil { in config: Config?, using dependencies: Dependencies ) throws { - guard config.needsDump else { return } + guard config.needsDump(using: dependencies) else { return } guard case .object(let conf) = config else { throw SessionUtilError.invalidConfigObject } // Get the volatile thread info from the conf and local conversations diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+GroupInfo.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+GroupInfo.swift index a17146674..cd1b51477 100644 --- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+GroupInfo.swift +++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+GroupInfo.swift @@ -40,7 +40,7 @@ internal extension SessionUtil { ) throws { typealias GroupData = (profileName: String, profilePictureUrl: String?, profilePictureKey: Data?) - guard config.needsDump else { return } + guard config.needsDump(using: dependencies) else { return } guard case .object(let conf) = config else { throw SessionUtilError.invalidConfigObject } // If the group is destroyed then remove the group date (want to keep the group itself around because @@ -67,11 +67,16 @@ internal extension SessionUtil { let groupName: String = String(cString: groupNamePtr) let groupDesc: String? = groupDescPtr.map { String(cString: $0) } let formationTimestamp: TimeInterval = TimeInterval(groups_info_get_created(conf)) + + // The `displayPic.key` can contain junk data so if the `displayPictureUrl` is null then just + // set the `displayPictureKey` to null as well let displayPic: user_profile_pic = groups_info_get_pic(conf) let displayPictureUrl: String? = String(libSessionVal: displayPic.url, nullIfEmpty: true) - let displayPictureKey: Data? = Data( - libSessionVal: displayPic.key, - count: DisplayPictureManager.aes256KeyByteLength + let displayPictureKey: Data? = (displayPictureUrl == nil ? nil : + Data( + libSessionVal: displayPic.key, + count: DisplayPictureManager.aes256KeyByteLength + ) ) // Update the group name @@ -90,7 +95,7 @@ internal extension SessionUtil { ((existingGroup?.groupDescription == groupDesc) ? nil : ClosedGroup.Columns.groupDescription.set(to: groupDesc) ), - ((existingGroup?.formationTimestamp != formationTimestamp && formationTimestamp != 0) ? nil : + ((existingGroup?.formationTimestamp == formationTimestamp || formationTimestamp == 0) ? nil : ClosedGroup.Columns.formationTimestamp.set(to: formationTimestamp) ), // If we are removing the display picture do so here @@ -104,7 +109,7 @@ internal extension SessionUtil { ClosedGroup.Columns.displayPictureEncryptionKey.set(to: nil) ), (!needsDisplayPictureUpdate || displayPictureUrl != nil ? nil : - ClosedGroup.Columns.lastDisplayPictureUpdate.set(to: dependencies.dateNow) + ClosedGroup.Columns.lastDisplayPictureUpdate.set(to: (serverTimestampMs / 1000)) ) ].compactMap { $0 } diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+GroupMembers.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+GroupMembers.swift index ae6367795..b355bfa52 100644 --- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+GroupMembers.swift +++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+GroupMembers.swift @@ -29,7 +29,7 @@ internal extension SessionUtil { serverTimestampMs: Int64, using dependencies: Dependencies ) throws { - guard config.needsDump else { return } + guard config.needsDump(using: dependencies) else { return } guard case .object(let conf) = config else { throw SessionUtilError.invalidConfigObject } // Get the two member sets diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Shared.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Shared.swift index 9a902b852..21742fc1a 100644 --- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Shared.swift +++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Shared.swift @@ -79,13 +79,14 @@ internal extension SessionUtil { if let lastError: SessionUtilError = config?.lastError { throw lastError } // If we don't need to dump the data the we can finish early - guard config.needsDump else { return config.needsPush } + guard config.needsDump(using: dependencies) else { return config.needsPush } try SessionUtil.createDump( config: config, for: variant, sessionId: sessionId, - timestampMs: SnodeAPI.currentOffsetTimestampMs() + timestampMs: SnodeAPI.currentOffsetTimestampMs(using: dependencies), + using: dependencies )?.upsert(db) return config.needsPush diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+SharedGroup.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+SharedGroup.swift index eed5b6a57..a7ac5ed88 100644 --- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+SharedGroup.swift +++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+SharedGroup.swift @@ -152,7 +152,8 @@ internal extension SessionUtil { config: config, for: variant, sessionId: SessionId(.group, hex: group.id), - timestampMs: Int64(floor(group.formationTimestamp * 1000)) + timestampMs: Int64(floor(group.formationTimestamp * 1000)), + using: dependencies )?.upsert(db) } diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserGroups.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserGroups.swift index 5f6d48cf2..525e27c88 100644 --- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserGroups.swift +++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserGroups.swift @@ -39,7 +39,7 @@ internal extension SessionUtil { serverTimestampMs: Int64, using dependencies: Dependencies ) throws { - guard config.needsDump else { return } + guard config.needsDump(using: dependencies) else { return } guard case .object(let conf) = config else { throw SessionUtilError.invalidConfigObject } var infiniteLoopGuard: Int = 0 diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserProfile.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserProfile.swift index 2e43b2d07..165ee7c06 100644 --- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserProfile.swift +++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserProfile.swift @@ -26,7 +26,7 @@ internal extension SessionUtil { ) throws { typealias ProfileData = (profileName: String, profilePictureUrl: String?, profilePictureKey: Data?) - guard config.needsDump else { return } + guard config.needsDump(using: dependencies) else { return } guard case .object(let conf) = config else { throw SessionUtilError.invalidConfigObject } // A profile must have a name so if this is null then it's invalid and can be ignored diff --git a/SessionMessagingKit/SessionUtil/SessionUtil.swift b/SessionMessagingKit/SessionUtil/SessionUtil.swift index 39093566f..bae77dd10 100644 --- a/SessionMessagingKit/SessionUtil/SessionUtil.swift +++ b/SessionMessagingKit/SessionUtil/SessionUtil.swift @@ -11,20 +11,6 @@ import SessionUtilitiesKit public enum SessionUtil { internal static let logLevel: config_log_level = LOG_LEVEL_INFO - public struct ConfResult { - let needsPush: Bool - let needsDump: Bool - } - - public struct IncomingConfResult { - let needsPush: Bool - let needsDump: Bool - let messageHashes: [String] - let latestSentTimestamp: TimeInterval - - var result: ConfResult { ConfResult(needsPush: needsPush, needsDump: needsDump) } - } - // MARK: - Variables internal static func syncDedupeId(_ sessionIdHexString: String) -> String { @@ -296,11 +282,12 @@ public enum SessionUtil { config: Config?, for variant: ConfigDump.Variant, sessionId: SessionId, - timestampMs: Int64 + timestampMs: Int64, + using dependencies: Dependencies ) throws -> ConfigDump? { // If it doesn't need a dump then do nothing guard - config.needsDump, + config.needsDump(using: dependencies), let dumpData: Data = try config?.dump() else { return nil } @@ -384,7 +371,8 @@ public enum SessionUtil { config: config, for: variant, sessionId: sessionId, - timestampMs: sentTimestamp + timestampMs: sentTimestamp, + using: dependencies ) } } @@ -499,7 +487,7 @@ public enum SessionUtil { // Need to check if the config needs to be dumped (this might have changed // after handling the merge changes) - guard config.needsDump else { + guard config.needsDump(using: dependencies) else { try ConfigDump .filter( ConfigDump.Columns.variant == next.key && @@ -517,7 +505,8 @@ public enum SessionUtil { config: config, for: next.key, sessionId: sessionId, - timestampMs: latestServerTimestampMs + timestampMs: latestServerTimestampMs, + using: dependencies )?.upsert(db) } catch { diff --git a/SessionMessagingKit/SessionUtil/Types/Config.swift b/SessionMessagingKit/SessionUtil/Types/Config.swift index 6f0ec630d..8a710c245 100644 --- a/SessionMessagingKit/SessionUtil/Types/Config.swift +++ b/SessionMessagingKit/SessionUtil/Types/Config.swift @@ -51,14 +51,6 @@ public extension SessionUtil { } } - var needsDump: Bool { - switch self { - case .invalid: return false - case .object(let conf): return config_needs_dump(conf) - case .groupKeys(let conf, _, _): return groups_keys_needs_dump(conf) - } - } - var lastError: SessionUtilError? { let maybeErrorString: String? = { switch self { @@ -82,6 +74,19 @@ public extension SessionUtil { // MARK: - Functions + func needsDump(using dependencies: Dependencies) -> Bool { + return dependencies.mockableValue( + key: "needsDump", + { + switch self { + case .invalid: return false + case .object(let conf): return config_needs_dump(conf) + case .groupKeys(let conf, _, _): return groups_keys_needs_dump(conf) + } + }() + ) + } + func addingLogger() -> Config { switch self { case .object(let conf): @@ -344,13 +349,6 @@ public extension Optional where Wrapped == SessionUtil.Config { } } - var needsDump: Bool { - switch self { - case .some(let config): return config.needsDump - case .none: return false - } - } - var lastError: SessionUtilError? { switch self { case .some(let config): return config.lastError @@ -360,6 +358,13 @@ public extension Optional where Wrapped == SessionUtil.Config { // MARK: - Functions + func needsDump(using dependencies: Dependencies) -> Bool { + switch self { + case .some(let config): return config.needsDump(using: dependencies) + case .none: return false + } + } + func confirmPushed(seqNo: Int64, hash: String) { switch self { case .some(let config): return config.confirmPushed(seqNo: seqNo, hash: hash) @@ -386,5 +391,14 @@ public extension Optional where Wrapped == SessionUtil.Config { public extension Atomic where Value == Optional { var needsPush: Bool { return wrappedValue.needsPush } - var needsDump: Bool { return wrappedValue.needsDump } + + func needsDump(using dependencies: Dependencies) -> Bool { return wrappedValue.needsDump(using: dependencies) } +} + +// MARK: - Formatting + +extension String.StringInterpolation { + mutating func appendInterpolation(_ error: SessionUtilError?) { + appendLiteral(error.map { "\($0)" } ?? "Unknown Error") // stringlint:disable + } } diff --git a/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift b/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift index c6c9750fe..739867334 100644 --- a/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift @@ -57,9 +57,9 @@ class MessageSendJobSpec: QuickSpec { .thenReturn([:]) jobRunner .when { $0.insert(any(), job: any(), before: any()) } - .then { args in - let db: Database = args[0] as! Database - var job: Job = args[1] as! Job + .then { args, untrackedArgs in + let db: Database = untrackedArgs[0] as! Database + var job: Job = args[0] as! Job job.id = 1000 try! job.insert(db) diff --git a/SessionMessagingKitTests/LibSessionUtil/SessionUtilSpec.swift b/SessionMessagingKitTests/LibSessionUtil/SessionUtilSpec.swift index 594fb0ff5..effa3494f 100644 --- a/SessionMessagingKitTests/LibSessionUtil/SessionUtilSpec.swift +++ b/SessionMessagingKitTests/LibSessionUtil/SessionUtilSpec.swift @@ -55,6 +55,28 @@ class SessionUtilSpec: QuickSpec { ) } ) + @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( + initialSetup: { jobRunner in + jobRunner + .when { $0.add(any(), job: any(), dependantJob: any(), canStartJob: any(), using: any()) } + .thenReturn(nil) + } + ) + + @TestState var createGroupOutput: SessionUtil.CreatedGroupInfo! = { + mockStorage.write(using: dependencies) { db in + try SessionUtil.createGroup( + db, + name: "TestGroup", + description: nil, + displayPictureUrl: nil, + displayPictureFilename: nil, + displayPictureEncryptionKey: nil, + members: [], + using: dependencies + ) + } + }() @TestState(cache: .sessionUtil, in: dependencies) var mockSessionUtilCache: MockSessionUtilCache! = MockSessionUtilCache( initialSetup: { cache in var conf: UnsafeMutablePointer! @@ -64,9 +86,14 @@ class SessionUtilSpec: QuickSpec { cache.when { $0.setConfig(for: any(), sessionId: any(), to: any()) }.thenReturn(()) cache.when { $0.config(for: .userGroups, sessionId: any()) } .thenReturn(Atomic(.object(conf))) + cache.when { $0.config(for: .groupInfo, sessionId: any()) } + .thenReturn(Atomic(createGroupOutput.groupState[.groupInfo])) + cache.when { $0.config(for: .groupMembers, sessionId: any()) } + .thenReturn(Atomic(createGroupOutput.groupState[.groupMembers])) + cache.when { $0.config(for: .groupKeys, sessionId: any()) } + .thenReturn(Atomic(createGroupOutput.groupState[.groupKeys])) } ) - @TestState var createGroupOutput: SessionUtil.CreatedGroupInfo! @TestState var userGroupsConfig: SessionUtil.Config! // MARK: - SessionUtil @@ -272,7 +299,7 @@ class SessionUtilSpec: QuickSpec { } } - // MARK: - when creating a group + // MARK: -- when creating a group context("when creating a group") { beforeEach { var userGroupsConf: UnsafeMutablePointer! @@ -285,7 +312,7 @@ class SessionUtilSpec: QuickSpec { .thenReturn(Atomic(userGroupsConfig)) } - // MARK: -- throws when there is no user ed25519 keyPair + // MARK: ---- throws when there is no user ed25519 keyPair it("throws when there is no user ed25519 keyPair") { var resultError: Error? = nil @@ -311,7 +338,7 @@ class SessionUtilSpec: QuickSpec { expect(resultError).to(matchError(MessageSenderError.noKeyPair)) } - // MARK: -- throws when it fails to generate a new identity ed25519 keyPair + // MARK: ---- throws when it fails to generate a new identity ed25519 keyPair it("throws when it fails to generate a new identity ed25519 keyPair") { var resultError: Error? = nil @@ -338,7 +365,7 @@ class SessionUtilSpec: QuickSpec { expect(resultError).to(matchError(MessageSenderError.noKeyPair)) } - // MARK: -- throws when given an invalid member id + // MARK: ---- throws when given an invalid member id it("throws when given an invalid member id") { var resultError: Error? = nil @@ -375,7 +402,7 @@ class SessionUtilSpec: QuickSpec { )) } - // MARK: -- returns the correct identity keyPair + // MARK: ---- returns the correct identity keyPair it("returns the correct identity keyPair") { createGroupOutput = mockStorage.write(using: dependencies) { db in try SessionUtil.createGroup( @@ -399,7 +426,7 @@ class SessionUtilSpec: QuickSpec { )) } - // MARK: -- returns a closed group with the correct data set + // MARK: ---- returns a closed group with the correct data set it("returns a closed group with the correct data set") { createGroupOutput = mockStorage.write(using: dependencies) { db in try SessionUtil.createGroup( @@ -429,7 +456,7 @@ class SessionUtilSpec: QuickSpec { expect(createGroupOutput.group.invited).to(beFalse()) } - // MARK: -- returns the members setup correctly + // MARK: ---- returns the members setup correctly it("returns the members setup correctly") { createGroupOutput = mockStorage.write(using: dependencies) { db in try SessionUtil.createGroup( @@ -475,7 +502,7 @@ class SessionUtilSpec: QuickSpec { ])) } - // MARK: -- adds the current user as an admin when not provided + // MARK: ---- adds the current user as an admin when not provided it("adds the current user as an admin when not provided") { createGroupOutput = mockStorage.write(using: dependencies) { db in try SessionUtil.createGroup( @@ -503,7 +530,7 @@ class SessionUtilSpec: QuickSpec { expect(createGroupOutput.members.map { $0.role }).to(contain(.admin)) } - // MARK: -- handles members without profile data correctly + // MARK: ---- handles members without profile data correctly it("handles members without profile data correctly") { createGroupOutput = mockStorage.write(using: dependencies) { db in try SessionUtil.createGroup( @@ -529,7 +556,7 @@ class SessionUtilSpec: QuickSpec { expect(createGroupOutput.members.map { $0.role }).to(contain(.standard)) } - // MARK: -- stores the config states in the cache correctly + // MARK: ---- stores the config states in the cache correctly it("stores the config states in the cache correctly") { createGroupOutput = mockStorage.write(using: dependencies) { db in try SessionUtil.createGroup( @@ -586,9 +613,9 @@ class SessionUtilSpec: QuickSpec { } } - // MARK: - when saving a created a group + // MARK: -- when saving a created a group context("when saving a created a group") { - // MARK: -- saves config dumps for the stored configs + // MARK: ---- saves config dumps for the stored configs it("saves config dumps for the stored configs") { mockStorage.write(using: dependencies) { db in createGroupOutput = try SessionUtil.createGroup( @@ -630,7 +657,7 @@ class SessionUtilSpec: QuickSpec { .to(contain([1234567890000])) } - // MARK: -- adds the group to the user groups config + // MARK: ---- adds the group to the user groups config it("adds the group to the user groups config") { mockStorage.write(using: dependencies) { db in createGroupOutput = try SessionUtil.createGroup( @@ -663,6 +690,287 @@ class SessionUtilSpec: QuickSpec { expect(result?.map { $0.timestampMs }.asSet()).to(contain([1234567890000])) } } + + // MARK: -- when receiving a GROUP_INFO update + context("when receiving a GROUP_INFO update") { + @TestState var latestGroup: ClosedGroup? + @TestState var initialDisappearingConfig: DisappearingMessagesConfiguration? + @TestState var latestDisappearingConfig: DisappearingMessagesConfiguration? + + beforeEach { + mockStorage.write(using: dependencies) { db in + try SessionThread.fetchOrCreate( + db, + id: createGroupOutput.group.threadId, + variant: .group, + shouldBeVisible: true, + calledFromConfigHandling: false, + using: dependencies + ) + try createGroupOutput.group.insert(db) + try createGroupOutput.members.forEach { try $0.insert(db) } + initialDisappearingConfig = try DisappearingMessagesConfiguration + .fetchOne(db, id: createGroupOutput.group.threadId) + .defaulting( + to: DisappearingMessagesConfiguration.defaultWith(createGroupOutput.group.threadId) + ) + } + } + + // MARK: ---- does nothing if there are no changes + it("does nothing if there are no changes") { + dependencies.setMockableValue(key: "needsDump", false) + + mockStorage.write(using: dependencies) { db in + try SessionUtil.handleGroupInfoUpdate( + db, + in: createGroupOutput.groupState[.groupInfo], + groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId), + serverTimestampMs: 1234567891000, + using: dependencies + ) + } + + latestGroup = mockStorage.read(using: dependencies) { db in + try ClosedGroup.fetchOne(db, id: createGroupOutput.group.threadId) + } + expect(createGroupOutput.groupState[.groupInfo]).toNot(beNil()) + expect(createGroupOutput.group).to(equal(latestGroup)) + } + + // MARK: ---- throws if the config is invalid + it("throws if the config is invalid") { + dependencies.setMockableValue(key: "needsDump", true) + + mockStorage.write(using: dependencies) { db in + expect { + try SessionUtil.handleGroupInfoUpdate( + db, + in: .invalid, + groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId), + serverTimestampMs: 1234567891000, + using: dependencies + ) + } + .to(throwError()) + } + } + + // MARK: ---- removes group data if the group is destroyed + it("removes group data if the group is destroyed") { + createGroupOutput.groupState[.groupInfo]?.conf.map { groups_info_destroy_group($0) } + dependencies.setMockableValue(key: "needsDump", true) + + mockStorage.write(using: dependencies) { db in + try SessionUtil.handleGroupInfoUpdate( + db, + in: createGroupOutput.groupState[.groupInfo], + groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId), + serverTimestampMs: 1234567891000, + using: dependencies + ) + } + + latestGroup = mockStorage.read(using: dependencies) { db in + try ClosedGroup.fetchOne(db, id: createGroupOutput.group.threadId) + } + expect(latestGroup?.authData).to(beNil()) + expect(latestGroup?.groupIdentityPrivateKey).to(beNil()) + } + + // MARK: ---- updates the name if it changed + it("updates the name if it changed") { + createGroupOutput.groupState[.groupInfo]?.conf.map { + var updatedName: [CChar] = "UpdatedName".cArray.nullTerminated() + groups_info_set_name($0, &updatedName) + } + dependencies.setMockableValue(key: "needsDump", true) + + mockStorage.write(using: dependencies) { db in + try SessionUtil.handleGroupInfoUpdate( + db, + in: createGroupOutput.groupState[.groupInfo], + groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId), + serverTimestampMs: 1234567891000, + using: dependencies + ) + } + + latestGroup = mockStorage.read(using: dependencies) { db in + try ClosedGroup.fetchOne(db, id: createGroupOutput.group.threadId) + } + expect(createGroupOutput.group.name).to(equal("TestGroup")) + expect(latestGroup?.name).to(equal("UpdatedName")) + } + + // MARK: ---- updates the description if it changed + it("updates the description if it changed") { + createGroupOutput.groupState[.groupInfo]?.conf.map { + var updatedDesc: [CChar] = "UpdatedDesc".cArray.nullTerminated() + groups_info_set_description($0, &updatedDesc) + } + dependencies.setMockableValue(key: "needsDump", true) + + mockStorage.write(using: dependencies) { db in + try SessionUtil.handleGroupInfoUpdate( + db, + in: createGroupOutput.groupState[.groupInfo], + groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId), + serverTimestampMs: 1234567891000, + using: dependencies + ) + } + + latestGroup = mockStorage.read(using: dependencies) { db in + try ClosedGroup.fetchOne(db, id: createGroupOutput.group.threadId) + } + expect(createGroupOutput.group.groupDescription).to(beNil()) + expect(latestGroup?.groupDescription).to(equal("UpdatedDesc")) + } + + // MARK: ---- updates the formation timestamp if it changed + it("updates the formation timestamp if it changed") { + createGroupOutput.groupState[.groupInfo]?.conf.map { groups_info_set_created($0, 54321) } + dependencies.setMockableValue(key: "needsDump", true) + + mockStorage.write(using: dependencies) { db in + try SessionUtil.handleGroupInfoUpdate( + db, + in: createGroupOutput.groupState[.groupInfo], + groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId), + serverTimestampMs: 1234567891000, + using: dependencies + ) + } + + latestGroup = mockStorage.read(using: dependencies) { db in + try ClosedGroup.fetchOne(db, id: createGroupOutput.group.threadId) + } + expect(createGroupOutput.group.formationTimestamp).to(equal(1234567890)) + expect(latestGroup?.formationTimestamp).to(equal(54321)) + } + + // MARK: ---- and the display picture was changed + context("and the display picture was changed") { + // MARK: ------ removes the display picture + it("removes the display picture") { + mockStorage.write(using: dependencies) { db in + try ClosedGroup + .updateAll( + db, + ClosedGroup.Columns.displayPictureUrl.set(to: "TestUrl"), + ClosedGroup.Columns.displayPictureEncryptionKey.set(to: Data([1, 2, 3])), + ClosedGroup.Columns.displayPictureFilename.set(to: "TestFilename") + ) + } + dependencies.setMockableValue(key: "needsDump", true) + + mockStorage.write(using: dependencies) { db in + try SessionUtil.handleGroupInfoUpdate( + db, + in: createGroupOutput.groupState[.groupInfo], + groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId), + serverTimestampMs: 1234567891000, + using: dependencies + ) + } + + latestGroup = mockStorage.read(using: dependencies) { db in + try ClosedGroup.fetchOne(db, id: createGroupOutput.group.threadId) + } + expect(latestGroup?.displayPictureUrl).to(beNil()) + expect(latestGroup?.displayPictureEncryptionKey).to(beNil()) + expect(latestGroup?.displayPictureFilename).to(beNil()) + expect(latestGroup?.lastDisplayPictureUpdate).to(equal(1234567891)) + } + + // MARK: ------ schedules a display picture download job if there is a new one + it("schedules a display picture download job if there is a new one") { + createGroupOutput.groupState[.groupInfo]?.conf.map { + var displayPic: user_profile_pic = user_profile_pic() + displayPic.url = "https://www.oxen.io/file/1234".toLibSession() + displayPic.key = Data( + repeating: 1, + count: DisplayPictureManager.aes256KeyByteLength + ).toLibSession() + groups_info_set_pic($0, displayPic) + } + dependencies.setMockableValue(key: "needsDump", true) + + mockStorage.write(using: dependencies) { db in + try SessionUtil.handleGroupInfoUpdate( + db, + in: createGroupOutput.groupState[.groupInfo], + groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId), + serverTimestampMs: 1234567891000, + using: dependencies + ) + } + + expect(mockJobRunner) + .to(call(.exactly(times: 1), matchingParameters: .all) { jobRunner in + jobRunner.add( + any(), + job: Job( + variant: .displayPictureDownload, + behaviour: .runOnce, + shouldBlock: false, + shouldBeUnique: true, + shouldSkipLaunchBecomeActive: false, + details: DisplayPictureDownloadJob.Details( + target: .group( + id: createGroupOutput.group.threadId, + url: "https://www.oxen.io/file/1234", + encryptionKey: Data( + repeating: 1, + count: DisplayPictureManager.aes256KeyByteLength + ) + ), + timestamp: 1234567891 + ) + ), + canStartJob: true, + using: any() + ) + }) + } + } + + // MARK: ---- updates the disappearing messages config + it("updates the disappearing messages config") { + createGroupOutput.groupState[.groupInfo]?.conf.map { groups_info_set_expiry_timer($0, 10) } + dependencies.setMockableValue(key: "needsDump", true) + + mockStorage.write(using: dependencies) { db in + try SessionUtil.handleGroupInfoUpdate( + db, + in: createGroupOutput.groupState[.groupInfo], + groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId), + serverTimestampMs: 1234567891000, + using: dependencies + ) + } + + latestDisappearingConfig = mockStorage.read(using: dependencies) { db in + try DisappearingMessagesConfiguration.fetchOne(db, id: createGroupOutput.group.threadId) + } + expect(initialDisappearingConfig?.isEnabled).to(beFalse()) + expect(initialDisappearingConfig?.durationSeconds).to(equal(0)) + expect(latestDisappearingConfig?.isEnabled).to(beTrue()) + expect(latestDisappearingConfig?.durationSeconds).to(equal(10)) + } + } + } + } +} + +// MARK: - Convenience + +private extension SessionUtil.Config { + var conf: UnsafeMutablePointer? { + switch self { + case .object(let conf): return conf + default: return nil } } } diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index 696bc660c..fb3f3cab9 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -798,7 +798,6 @@ public extension ValueObservation { // MARK: - Debug Convenience -#if DEBUG public extension Storage { func exportInfo(password: String) throws -> (dbPath: String, keyPath: String) { var keySpec: Data = try Storage.getOrGenerateDatabaseKeySpec() @@ -829,4 +828,3 @@ public extension Storage { ) } } -#endif diff --git a/SessionUtilitiesKit/General/Feature.swift b/SessionUtilitiesKit/General/Feature.swift index e47e15c65..1ceb12609 100644 --- a/SessionUtilitiesKit/General/Feature.swift +++ b/SessionUtilitiesKit/General/Feature.swift @@ -29,6 +29,10 @@ public extension FeatureStorage { ) ) + static let updatedGroupsDisableAutoApprove: FeatureConfig = Dependencies.create( + identifier: "updatedGroupsDisableAutoApprove" + ) + static let updatedGroupsRemoveMessagesOnKick: FeatureConfig = Dependencies.create( identifier: "updatedGroupsRemoveMessagesOnKick" ) diff --git a/_SharedTestUtilities/Mock.swift b/_SharedTestUtilities/Mock.swift index 2b8a64039..edb47be8a 100644 --- a/_SharedTestUtilities/Mock.swift +++ b/_SharedTestUtilities/Mock.swift @@ -28,45 +28,48 @@ public class Mock { // MARK: - MockFunctionHandler - @discardableResult internal func mock(funcName: String = #function, args: [Any?] = []) -> Output { - return mock(funcName: funcName, checkArgs: args, actionArgs: args) + @discardableResult internal func mock(funcName: String = #function, args: [Any?] = [], untrackedArgs: [Any?] = []) -> Output { + return mock(funcName: funcName, checkArgs: args, actionArgs: args, untrackedArgs: untrackedArgs) } - @discardableResult internal func mock(funcName: String = #function, checkArgs: [Any?], actionArgs: [Any?]) -> Output { + @discardableResult internal func mock(funcName: String = #function, checkArgs: [Any?], actionArgs: [Any?], untrackedArgs: [Any?]) -> Output { return functionHandler.mock( funcName, parameterCount: checkArgs.count, parameterSummary: summary(for: checkArgs), allParameterSummaryCombinations: summaries(for: checkArgs), - actionArgs: actionArgs + actionArgs: actionArgs, + untrackedArgs: untrackedArgs ) } - internal func mockNoReturn(funcName: String = #function, args: [Any?] = []) { - mockNoReturn(funcName: funcName, checkArgs: args, actionArgs: args) + internal func mockNoReturn(funcName: String = #function, args: [Any?] = [], untrackedArgs: [Any?] = []) { + mockNoReturn(funcName: funcName, checkArgs: args, actionArgs: args, untrackedArgs: untrackedArgs) } - internal func mockNoReturn(funcName: String = #function, checkArgs: [Any?], actionArgs: [Any?]) { + internal func mockNoReturn(funcName: String = #function, checkArgs: [Any?], actionArgs: [Any?], untrackedArgs: [Any?]) { functionHandler.mockNoReturn( funcName, parameterCount: checkArgs.count, parameterSummary: summary(for: checkArgs), allParameterSummaryCombinations: summaries(for: checkArgs), - actionArgs: actionArgs + actionArgs: actionArgs, + untrackedArgs: untrackedArgs ) } - @discardableResult internal func mockThrowing(funcName: String = #function, args: [Any?] = []) throws -> Output { - return try mockThrowing(funcName: funcName, checkArgs: args, actionArgs: args) + @discardableResult internal func mockThrowing(funcName: String = #function, args: [Any?] = [], untrackedArgs: [Any?] = []) throws -> Output { + return try mockThrowing(funcName: funcName, checkArgs: args, actionArgs: args, untrackedArgs: untrackedArgs) } - @discardableResult internal func mockThrowing(funcName: String = #function, checkArgs: [Any?], actionArgs: [Any?]) throws -> Output { + @discardableResult internal func mockThrowing(funcName: String = #function, checkArgs: [Any?], actionArgs: [Any?], untrackedArgs: [Any?]) throws -> Output { return try functionHandler.mockThrowing( funcName, parameterCount: checkArgs.count, parameterSummary: summary(for: checkArgs), allParameterSummaryCombinations: summaries(for: checkArgs), - actionArgs: actionArgs + actionArgs: actionArgs, + untrackedArgs: untrackedArgs ) } @@ -126,7 +129,8 @@ protocol MockFunctionHandler { parameterCount: Int, parameterSummary: String, allParameterSummaryCombinations: [ParameterCombination], - actionArgs: [Any?] + actionArgs: [Any?], + untrackedArgs: [Any?] ) -> Output func mockNoReturn( @@ -134,7 +138,8 @@ protocol MockFunctionHandler { parameterCount: Int, parameterSummary: String, allParameterSummaryCombinations: [ParameterCombination], - actionArgs: [Any?] + actionArgs: [Any?], + untrackedArgs: [Any?] ) func mockThrowing( @@ -142,7 +147,8 @@ protocol MockFunctionHandler { parameterCount: Int, parameterSummary: String, allParameterSummaryCombinations: [ParameterCombination], - actionArgs: [Any?] + actionArgs: [Any?], + untrackedArgs: [Any?] ) throws -> Output } @@ -167,7 +173,7 @@ internal class MockFunction { var parameterCount: Int var parameterSummary: String var allParameterSummaryCombinations: [ParameterCombination] - var actions: [([Any?]) -> Void] + var actions: [([Any?], [Any?]) -> Void] var returnError: (any Error)? var returnValue: Any? @@ -176,7 +182,7 @@ internal class MockFunction { parameterCount: Int, parameterSummary: String, allParameterSummaryCombinations: [ParameterCombination], - actions: [([Any?]) -> Void], + actions: [([Any?], [Any?]) -> Void], returnError: (any Error)?, returnValue: Any? ) { @@ -199,7 +205,7 @@ internal class MockFunctionBuilder: MockFunctionHandler { private var parameterCount: Int? private var parameterSummary: String? private var allParameterSummaryCombinations: [ParameterCombination]? - private var actions: [([Any?]) -> Void] = [] + private var actions: [([Any?], [Any?]) -> Void] = [] private var returnValue: R? internal var returnValueGenerator: ((String, Int, String, [ParameterCombination]) -> R?)? private var returnError: Error? @@ -213,7 +219,14 @@ internal class MockFunctionBuilder: MockFunctionHandler { // MARK: - Behaviours + /// Closure parameter is an array of arguments called by the function @discardableResult func then(_ block: @escaping ([Any?]) -> Void) -> MockFunctionBuilder { + actions.append({ args, _ in block(args) }) + return self + } + + /// Closure parameters are an array of arguments, followed by an array of "untracked" arguments called by the function + @discardableResult func then(_ block: @escaping ([Any?], [Any?]) -> Void) -> MockFunctionBuilder { actions.append(block) return self } @@ -233,7 +246,8 @@ internal class MockFunctionBuilder: MockFunctionHandler { parameterCount: Int, parameterSummary: String, allParameterSummaryCombinations: [ParameterCombination], - actionArgs: [Any?] + actionArgs: [Any?], + untrackedArgs: [Any?] ) -> Output { self.functionName = functionName self.parameterCount = parameterCount @@ -253,7 +267,8 @@ internal class MockFunctionBuilder: MockFunctionHandler { parameterCount: Int, parameterSummary: String, allParameterSummaryCombinations: [ParameterCombination], - actionArgs: [Any?] + actionArgs: [Any?], + untrackedArgs: [Any?] ) { self.functionName = functionName self.parameterCount = parameterCount @@ -266,7 +281,8 @@ internal class MockFunctionBuilder: MockFunctionHandler { parameterCount: Int, parameterSummary: String, allParameterSummaryCombinations: [ParameterCombination], - actionArgs: [Any?] + actionArgs: [Any?], + untrackedArgs: [Any?] ) throws -> Output { self.functionName = functionName self.parameterCount = parameterCount @@ -329,7 +345,8 @@ internal class FunctionConsumer: MockFunctionHandler { parameterCount: Int, parameterSummary: String, allParameterSummaryCombinations: [ParameterCombination], - actionArgs: [Any?] + actionArgs: [Any?], + untrackedArgs: [Any?] ) -> MockFunction { let key: Key = Key(name: functionName, paramCount: parameterCount) @@ -373,7 +390,7 @@ internal class FunctionConsumer: MockFunctionHandler { } for action in expectation.actions { - action(actionArgs) + action(actionArgs, untrackedArgs) } return expectation @@ -384,14 +401,16 @@ internal class FunctionConsumer: MockFunctionHandler { parameterCount: Int, parameterSummary: String, allParameterSummaryCombinations: [ParameterCombination], - actionArgs: [Any?] + actionArgs: [Any?], + untrackedArgs: [Any?] ) -> Output { let expectation: MockFunction = getExpectation( functionName, parameterCount: parameterCount, parameterSummary: parameterSummary, allParameterSummaryCombinations: allParameterSummaryCombinations, - actionArgs: actionArgs + actionArgs: actionArgs, + untrackedArgs: untrackedArgs ) return (expectation.returnValue as! Output) @@ -402,14 +421,16 @@ internal class FunctionConsumer: MockFunctionHandler { parameterCount: Int, parameterSummary: String, allParameterSummaryCombinations: [ParameterCombination], - actionArgs: [Any?] + actionArgs: [Any?], + untrackedArgs: [Any?] ) { _ = getExpectation( functionName, parameterCount: parameterCount, parameterSummary: parameterSummary, allParameterSummaryCombinations: allParameterSummaryCombinations, - actionArgs: actionArgs + actionArgs: actionArgs, + untrackedArgs: untrackedArgs ) } @@ -418,14 +439,16 @@ internal class FunctionConsumer: MockFunctionHandler { parameterCount: Int, parameterSummary: String, allParameterSummaryCombinations: [ParameterCombination], - actionArgs: [Any?] + actionArgs: [Any?], + untrackedArgs: [Any?] ) throws -> Output { let expectation: MockFunction = getExpectation( functionName, parameterCount: parameterCount, parameterSummary: parameterSummary, allParameterSummaryCombinations: allParameterSummaryCombinations, - actionArgs: actionArgs + actionArgs: actionArgs, + untrackedArgs: untrackedArgs ) switch (expectation.returnError, expectation.returnValue) { @@ -447,7 +470,6 @@ internal class FunctionConsumer: MockFunctionHandler { // do this by sorting based on the largest param count and checking if there is a match let maybeExpectation: MockFunction? = allParameterSummaryCombinations .sorted(by: { lhs, rhs in lhs.count > rhs.count }) - .reversed() .compactMap { combination in possibleExpectations[combination.summary] } .first diff --git a/_SharedTestUtilities/MockJobRunner.swift b/_SharedTestUtilities/MockJobRunner.swift index f9e0e3b07..31e0c5784 100644 --- a/_SharedTestUtilities/MockJobRunner.swift +++ b/_SharedTestUtilities/MockJobRunner.swift @@ -38,19 +38,19 @@ class MockJobRunner: Mock, JobRunnerType { // MARK: - Job Scheduling @discardableResult func add(_ db: Database, job: Job?, dependantJob: Job?, canStartJob: Bool, using dependencies: Dependencies) -> Job? { - return mock(args: [db, job, canStartJob]) + return mock(args: [job, dependantJob, canStartJob], untrackedArgs: [db, dependencies]) } func upsert(_ db: Database, job: Job?, canStartJob: Bool, using dependencies: Dependencies) { - mockNoReturn(args: [db, job, canStartJob]) + mockNoReturn(args: [job, canStartJob], untrackedArgs: [db, dependencies]) } func insert(_ db: Database, job: Job?, before otherJob: Job) -> (Int64, Job)? { - return mock(args: [db, job, otherJob]) + return mock(args: [job, otherJob], untrackedArgs: [db]) } func enqueueDependenciesIfNeeded(_ jobs: [Job], using dependencies: Dependencies) { - mockNoReturn(args: [jobs, dependencies]) + mockNoReturn(args: [jobs], untrackedArgs: [dependencies]) } func afterJob(_ job: Job?, state: JobRunner.JobState, callback: @escaping (JobRunner.JobResult) -> ()) {