Added more unit tests, fixed a couple of minor bugs

Added a dev setting to disable the group auto-approve for admins which are contacts (for testing purposes)
Added logic to unsubscribe and resubscribe for PNs when swapping environments
pull/941/head
Morgan Pretty 1 year ago
parent e4354b7370
commit 875bf88be4

@ -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

@ -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.
<b>Note:</b> 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
)

@ -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
}
}
}

@ -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)
}

@ -1,4 +1,6 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import GRDB

@ -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,

@ -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

@ -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

@ -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 }

@ -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

@ -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

@ -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)
}

@ -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

@ -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

@ -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 {

@ -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<SessionUtil.Config> {
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
}
}

@ -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)

@ -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<config_object>!
@ -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<config_object>!
@ -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<config_object>? {
switch self {
case .object(let conf): return conf
default: return nil
}
}
}

@ -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

@ -29,6 +29,10 @@ public extension FeatureStorage {
)
)
static let updatedGroupsDisableAutoApprove: FeatureConfig<Bool> = Dependencies.create(
identifier: "updatedGroupsDisableAutoApprove"
)
static let updatedGroupsRemoveMessagesOnKick: FeatureConfig<Bool> = Dependencies.create(
identifier: "updatedGroupsRemoveMessagesOnKick"
)

@ -28,45 +28,48 @@ public class Mock<T> {
// MARK: - MockFunctionHandler
@discardableResult internal func mock<Output>(funcName: String = #function, args: [Any?] = []) -> Output {
return mock(funcName: funcName, checkArgs: args, actionArgs: args)
@discardableResult internal func mock<Output>(funcName: String = #function, args: [Any?] = [], untrackedArgs: [Any?] = []) -> Output {
return mock(funcName: funcName, checkArgs: args, actionArgs: args, untrackedArgs: untrackedArgs)
}
@discardableResult internal func mock<Output>(funcName: String = #function, checkArgs: [Any?], actionArgs: [Any?]) -> Output {
@discardableResult internal func mock<Output>(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<Output>(funcName: String = #function, args: [Any?] = []) throws -> Output {
return try mockThrowing(funcName: funcName, checkArgs: args, actionArgs: args)
@discardableResult internal func mockThrowing<Output>(funcName: String = #function, args: [Any?] = [], untrackedArgs: [Any?] = []) throws -> Output {
return try mockThrowing(funcName: funcName, checkArgs: args, actionArgs: args, untrackedArgs: untrackedArgs)
}
@discardableResult internal func mockThrowing<Output>(funcName: String = #function, checkArgs: [Any?], actionArgs: [Any?]) throws -> Output {
@discardableResult internal func mockThrowing<Output>(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<Output>(
@ -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<T, R>: 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<T, R>: MockFunctionHandler {
// MARK: - Behaviours
/// Closure parameter is an array of arguments called by the function
@discardableResult func then(_ block: @escaping ([Any?]) -> Void) -> MockFunctionBuilder<T, R> {
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<T, R> {
actions.append(block)
return self
}
@ -233,7 +246,8 @@ internal class MockFunctionBuilder<T, R>: 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<T, R>: 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<T, R>: 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

@ -38,19 +38,19 @@ class MockJobRunner: Mock<JobRunnerType>, 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) -> ()) {

Loading…
Cancel
Save