mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
999 lines
52 KiB
Swift
999 lines
52 KiB
Swift
// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import Foundation
|
|
import GRDB
|
|
import SessionUtil
|
|
import SessionUtilitiesKit
|
|
import SessionSnodeKit
|
|
|
|
import Quick
|
|
import Nimble
|
|
|
|
@testable import SessionSnodeKit
|
|
@testable import SessionMessagingKit
|
|
|
|
class LibSessionGroupInfoSpec: QuickSpec {
|
|
override class func spec() {
|
|
// MARK: Configuration
|
|
|
|
@TestState var dependencies: TestDependencies! = TestDependencies { dependencies in
|
|
dependencies.dateNow = Date(timeIntervalSince1970: 1234567890)
|
|
dependencies.forceSynchronous = true
|
|
}
|
|
@TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache(
|
|
initialSetup: { cache in
|
|
cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey))
|
|
}
|
|
)
|
|
@TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage(
|
|
customWriter: try! DatabaseQueue(),
|
|
migrationTargets: [
|
|
SNUtilitiesKit.self,
|
|
SNMessagingKit.self,
|
|
SNSnodeKit.self
|
|
],
|
|
using: dependencies,
|
|
initialData: { db in
|
|
try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db)
|
|
try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db)
|
|
try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db)
|
|
try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db)
|
|
}
|
|
)
|
|
@TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork(
|
|
initialSetup: { network in
|
|
network
|
|
.when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) }
|
|
.thenReturn(MockNetwork.response(data: Data([1, 2, 3])))
|
|
}
|
|
)
|
|
@TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner(
|
|
initialSetup: { jobRunner in
|
|
jobRunner
|
|
.when { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) }
|
|
.thenReturn(nil)
|
|
jobRunner
|
|
.when { $0.upsert(.any, job: .any, canStartJob: .any) }
|
|
.thenReturn(nil)
|
|
jobRunner
|
|
.when { $0.jobInfoFor(jobs: .any, state: .any, variant: .any) }
|
|
.thenReturn([:])
|
|
}
|
|
)
|
|
@TestState var createGroupOutput: LibSession.CreatedGroupInfo! = {
|
|
mockStorage.write { db in
|
|
try LibSession.createGroup(
|
|
db,
|
|
name: "TestGroup",
|
|
description: nil,
|
|
displayPictureUrl: nil,
|
|
displayPictureFilename: nil,
|
|
displayPictureEncryptionKey: nil,
|
|
members: [],
|
|
using: dependencies
|
|
)
|
|
}
|
|
}()
|
|
@TestState(cache: .libSession, in: dependencies) var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache(
|
|
initialSetup: { cache in
|
|
var conf: UnsafeMutablePointer<config_object>!
|
|
var secretKey: [UInt8] = Array(Data(hex: TestConstants.edSecretKey))
|
|
_ = user_groups_init(&conf, &secretKey, nil, 0, nil)
|
|
|
|
cache.when { $0.setConfig(for: .any, sessionId: .any, to: .any) }.thenReturn(())
|
|
cache.when { $0.removeConfigs(for: .any) }.thenReturn(())
|
|
cache.when { $0.config(for: .userGroups, sessionId: .any) }
|
|
.thenReturn(.userGroups(conf))
|
|
cache.when { $0.config(for: .groupInfo, sessionId: .any) }
|
|
.thenReturn(createGroupOutput.groupState[.groupInfo])
|
|
cache.when { $0.config(for: .groupMembers, sessionId: .any) }
|
|
.thenReturn(createGroupOutput.groupState[.groupMembers])
|
|
cache.when { $0.config(for: .groupKeys, sessionId: .any) }
|
|
.thenReturn(createGroupOutput.groupState[.groupKeys])
|
|
cache.when { $0.configNeedsDump(.any) }.thenReturn(true)
|
|
cache.when { try $0.createDump(config: .any, for: .any, sessionId: .any, timestampMs: .any) }.thenReturn(nil)
|
|
cache.when { try $0.performAndPushChange(.any, for: .any, sessionId: .any, change: { _ in }) }.thenReturn(nil)
|
|
cache
|
|
.when { $0.pinnedPriority(.any, threadId: .any, threadVariant: .any) }
|
|
.thenReturn(LibSession.defaultNewThreadPriority)
|
|
cache.when { $0.disappearingMessagesConfig(threadId: .any, threadVariant: .any) }
|
|
.thenReturn(nil)
|
|
cache.when { $0.isAdmin(groupSessionId: .any) }.thenReturn(true)
|
|
}
|
|
)
|
|
|
|
// MARK: - LibSessionGroupInfo
|
|
describe("LibSessionGroupInfo") {
|
|
// MARK: -- when handling a GROUP_INFO update
|
|
context("when handling a GROUP_INFO update") {
|
|
@TestState var latestGroup: ClosedGroup?
|
|
@TestState var initialDisappearingConfig: DisappearingMessagesConfiguration?
|
|
@TestState var latestDisappearingConfig: DisappearingMessagesConfiguration?
|
|
|
|
beforeEach {
|
|
mockStorage.write { db in
|
|
try SessionThread.upsert(
|
|
db,
|
|
id: createGroupOutput.group.threadId,
|
|
variant: .group,
|
|
values: SessionThread.TargetValues(
|
|
creationDateTimestamp: .setTo(1234567890),
|
|
shouldBeVisible: .setTo(true)
|
|
),
|
|
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") {
|
|
mockLibSessionCache.when { $0.configNeedsDump(.any) }.thenReturn(false)
|
|
|
|
mockStorage.write { db in
|
|
try mockLibSessionCache.handleGroupInfoUpdate(
|
|
db,
|
|
in: createGroupOutput.groupState[.groupInfo],
|
|
groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
|
|
serverTimestampMs: 1234567891000
|
|
)
|
|
}
|
|
|
|
latestGroup = mockStorage.read { 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") {
|
|
mockStorage.write { db in
|
|
expect {
|
|
try mockLibSessionCache.handleGroupInfoUpdate(
|
|
db,
|
|
in: createGroupOutput.groupState[.groupMembers]!,
|
|
groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
|
|
serverTimestampMs: 1234567891000
|
|
)
|
|
}
|
|
.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) }
|
|
|
|
mockStorage.write { db in
|
|
try mockLibSessionCache.handleGroupInfoUpdate(
|
|
db,
|
|
in: createGroupOutput.groupState[.groupInfo],
|
|
groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
|
|
serverTimestampMs: 1234567891000
|
|
)
|
|
}
|
|
|
|
latestGroup = mockStorage.read { 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".cString(using: .utf8)!
|
|
groups_info_set_name($0, &updatedName)
|
|
}
|
|
|
|
mockStorage.write { db in
|
|
try mockLibSessionCache.handleGroupInfoUpdate(
|
|
db,
|
|
in: createGroupOutput.groupState[.groupInfo],
|
|
groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
|
|
serverTimestampMs: 1234567891000
|
|
)
|
|
}
|
|
|
|
latestGroup = mockStorage.read { 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".cString(using: .utf8)!
|
|
groups_info_set_description($0, &updatedDesc)
|
|
}
|
|
|
|
mockStorage.write { db in
|
|
try mockLibSessionCache.handleGroupInfoUpdate(
|
|
db,
|
|
in: createGroupOutput.groupState[.groupInfo],
|
|
groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
|
|
serverTimestampMs: 1234567891000
|
|
)
|
|
}
|
|
|
|
latestGroup = mockStorage.read { 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 is later than the current value
|
|
it("updates the formation timestamp if it is later than the current value") {
|
|
// Note: the 'formationTimestamp' stores the "joinedAt" date so we on'y update it if it's later
|
|
// than the current value (as we don't want to replace the record of when the current user joined
|
|
// the group with when the group was originally created)
|
|
mockStorage.write { db in try ClosedGroup.updateAll(db, ClosedGroup.Columns.formationTimestamp.set(to: 50000)) }
|
|
createGroupOutput.groupState[.groupInfo]?.conf.map { groups_info_set_created($0, 54321) }
|
|
let originalGroup: ClosedGroup? = mockStorage.read { db in
|
|
try ClosedGroup.fetchOne(db, id: createGroupOutput.group.threadId)
|
|
}
|
|
|
|
mockStorage.write { db in
|
|
try mockLibSessionCache.handleGroupInfoUpdate(
|
|
db,
|
|
in: createGroupOutput.groupState[.groupInfo],
|
|
groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
|
|
serverTimestampMs: 1234567891000
|
|
)
|
|
}
|
|
|
|
latestGroup = mockStorage.read { db in
|
|
try ClosedGroup.fetchOne(db, id: createGroupOutput.group.threadId)
|
|
}
|
|
expect(originalGroup?.formationTimestamp).to(equal(50000))
|
|
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 { 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")
|
|
)
|
|
}
|
|
|
|
mockStorage.write { db in
|
|
try mockLibSessionCache.handleGroupInfoUpdate(
|
|
db,
|
|
in: createGroupOutput.groupState[.groupInfo],
|
|
groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
|
|
serverTimestampMs: 1234567891000
|
|
)
|
|
}
|
|
|
|
latestGroup = mockStorage.read { 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.set(\.url, to: "https://www.oxen.io/file/1234")
|
|
displayPic.set(\.key, to: Data(repeating: 1, count: DisplayPictureManager.aes256KeyByteLength))
|
|
groups_info_set_pic($0, displayPic)
|
|
}
|
|
|
|
mockStorage.write { db in
|
|
try mockLibSessionCache.handleGroupInfoUpdate(
|
|
db,
|
|
in: createGroupOutput.groupState[.groupInfo],
|
|
groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
|
|
serverTimestampMs: 1234567891000
|
|
)
|
|
}
|
|
|
|
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
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
// MARK: ---- updates the disappearing messages config
|
|
it("updates the disappearing messages config") {
|
|
createGroupOutput.groupState[.groupInfo]?.conf.map { groups_info_set_expiry_timer($0, 10) }
|
|
|
|
mockStorage.write { db in
|
|
try mockLibSessionCache.handleGroupInfoUpdate(
|
|
db,
|
|
in: createGroupOutput.groupState[.groupInfo],
|
|
groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
|
|
serverTimestampMs: 1234567891000
|
|
)
|
|
}
|
|
|
|
latestDisappearingConfig = mockStorage.read { 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: ---- containing a deleteBefore timestamp
|
|
context("containing a deleteBefore timestamp") {
|
|
@TestState var numInteractions: Int!
|
|
|
|
// MARK: ------ deletes messages before the timestamp
|
|
it("deletes messages before the timestamp") {
|
|
mockStorage.write { db in
|
|
try SessionThread.upsert(
|
|
db,
|
|
id: createGroupOutput.group.threadId,
|
|
variant: .contact,
|
|
values: SessionThread.TargetValues(
|
|
creationDateTimestamp: .setTo(1234567890),
|
|
shouldBeVisible: .setTo(true)
|
|
),
|
|
using: dependencies
|
|
)
|
|
_ = try Interaction(
|
|
serverHash: "1234",
|
|
messageUuid: nil,
|
|
threadId: createGroupOutput.group.threadId,
|
|
authorId: "4321",
|
|
variant: .standardIncoming,
|
|
body: nil,
|
|
timestampMs: 100000000,
|
|
receivedAtTimestampMs: 1234567890,
|
|
wasRead: false,
|
|
hasMention: false,
|
|
expiresInSeconds: nil,
|
|
expiresStartedAtMs: nil,
|
|
linkPreviewUrl: nil,
|
|
openGroupServerMessageId: nil,
|
|
openGroupWhisper: false,
|
|
openGroupWhisperMods: false,
|
|
openGroupWhisperTo: nil,
|
|
state: .sent,
|
|
recipientReadTimestampMs: nil,
|
|
mostRecentFailureText: nil,
|
|
transientDependencies: nil
|
|
).inserted(db)
|
|
}
|
|
|
|
createGroupOutput.groupState[.groupInfo]?.conf.map { groups_info_set_delete_before($0, 123456) }
|
|
|
|
mockStorage.write { db in
|
|
try mockLibSessionCache.handleGroupInfoUpdate(
|
|
db,
|
|
in: createGroupOutput.groupState[.groupInfo],
|
|
groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
|
|
serverTimestampMs: 1234567891000
|
|
)
|
|
}
|
|
|
|
let result: [Interaction]? = mockStorage.read { db in
|
|
try Interaction.fetchAll(db)
|
|
}
|
|
expect(result?.count).to(equal(1))
|
|
expect(result?.map { $0.variant }).to(equal([.standardIncomingDeleted]))
|
|
}
|
|
|
|
// MARK: ------ does not delete messages after the timestamp
|
|
it("does not delete messages after the timestamp") {
|
|
mockStorage.write { db in
|
|
try SessionThread.upsert(
|
|
db,
|
|
id: createGroupOutput.group.threadId,
|
|
variant: .contact,
|
|
values: SessionThread.TargetValues(
|
|
creationDateTimestamp: .setTo(1234567890),
|
|
shouldBeVisible: .setTo(true)
|
|
),
|
|
using: dependencies
|
|
)
|
|
_ = try Interaction(
|
|
serverHash: "1234",
|
|
messageUuid: nil,
|
|
threadId: createGroupOutput.group.threadId,
|
|
authorId: "4321",
|
|
variant: .standardIncoming,
|
|
body: nil,
|
|
timestampMs: 100000000,
|
|
receivedAtTimestampMs: 1234567890,
|
|
wasRead: false,
|
|
hasMention: false,
|
|
expiresInSeconds: nil,
|
|
expiresStartedAtMs: nil,
|
|
linkPreviewUrl: nil,
|
|
openGroupServerMessageId: nil,
|
|
openGroupWhisper: false,
|
|
openGroupWhisperMods: false,
|
|
openGroupWhisperTo: nil,
|
|
state: .sent,
|
|
recipientReadTimestampMs: nil,
|
|
mostRecentFailureText: nil,
|
|
transientDependencies: nil
|
|
).inserted(db)
|
|
_ = try Interaction(
|
|
serverHash: "1235",
|
|
messageUuid: nil,
|
|
threadId: createGroupOutput.group.threadId,
|
|
authorId: "4322",
|
|
variant: .standardIncoming,
|
|
body: nil,
|
|
timestampMs: 200000000,
|
|
receivedAtTimestampMs: 2234567890,
|
|
wasRead: false,
|
|
hasMention: false,
|
|
expiresInSeconds: nil,
|
|
expiresStartedAtMs: nil,
|
|
linkPreviewUrl: nil,
|
|
openGroupServerMessageId: nil,
|
|
openGroupWhisper: false,
|
|
openGroupWhisperMods: false,
|
|
openGroupWhisperTo: nil,
|
|
state: .sent,
|
|
recipientReadTimestampMs: nil,
|
|
mostRecentFailureText: nil,
|
|
transientDependencies: nil
|
|
).inserted(db)
|
|
}
|
|
|
|
createGroupOutput.groupState[.groupInfo]?.conf.map { groups_info_set_delete_before($0, 123456) }
|
|
|
|
mockStorage.write { db in
|
|
try mockLibSessionCache.handleGroupInfoUpdate(
|
|
db,
|
|
in: createGroupOutput.groupState[.groupInfo],
|
|
groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
|
|
serverTimestampMs: 1234567891000
|
|
)
|
|
}
|
|
|
|
let result: [Interaction]? = mockStorage.read { db in
|
|
try Interaction.fetchAll(db)
|
|
}
|
|
expect(result?.count).to(equal(2))
|
|
expect(result?.map { $0.variant }).to(equal([.standardIncomingDeleted, .standardIncoming]))
|
|
}
|
|
}
|
|
|
|
// MARK: ---- containing a deleteAttachmentsBefore timestamp
|
|
context("containing a deleteAttachmentsBefore timestamp") {
|
|
@TestState var numInteractions: Int!
|
|
|
|
// MARK: ------ deletes messages with attachments before the timestamp
|
|
it("deletes messages with attachments before the timestamp") {
|
|
mockStorage.write { db in
|
|
try SessionThread.upsert(
|
|
db,
|
|
id: createGroupOutput.group.threadId,
|
|
variant: .contact,
|
|
values: SessionThread.TargetValues(
|
|
creationDateTimestamp: .setTo(1234567890),
|
|
shouldBeVisible: .setTo(true)
|
|
),
|
|
using: dependencies
|
|
)
|
|
let interaction: Interaction = try Interaction(
|
|
serverHash: "1234",
|
|
messageUuid: nil,
|
|
threadId: createGroupOutput.group.threadId,
|
|
authorId: "4321",
|
|
variant: .standardIncoming,
|
|
body: nil,
|
|
timestampMs: 100000000,
|
|
receivedAtTimestampMs: 1234567890,
|
|
wasRead: false,
|
|
hasMention: false,
|
|
expiresInSeconds: nil,
|
|
expiresStartedAtMs: nil,
|
|
linkPreviewUrl: nil,
|
|
openGroupServerMessageId: nil,
|
|
openGroupWhisper: false,
|
|
openGroupWhisperMods: false,
|
|
openGroupWhisperTo: nil,
|
|
state: .sent,
|
|
recipientReadTimestampMs: nil,
|
|
mostRecentFailureText: nil,
|
|
transientDependencies: nil
|
|
).inserted(db)
|
|
_ = try Attachment(
|
|
id: "AttachmentId",
|
|
variant: .standard,
|
|
contentType: "Test",
|
|
byteCount: 1234
|
|
).inserted(db)
|
|
_ = try InteractionAttachment(
|
|
albumIndex: 1,
|
|
interactionId: interaction.id!,
|
|
attachmentId: "AttachmentId"
|
|
).inserted(db)
|
|
}
|
|
|
|
createGroupOutput.groupState[.groupInfo]?.conf.map {
|
|
groups_info_set_attach_delete_before($0, 123456)
|
|
}
|
|
|
|
mockStorage.write { db in
|
|
try mockLibSessionCache.handleGroupInfoUpdate(
|
|
db,
|
|
in: createGroupOutput.groupState[.groupInfo],
|
|
groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
|
|
serverTimestampMs: 1234567891000
|
|
)
|
|
}
|
|
|
|
let result: [Interaction]? = mockStorage.read { db in
|
|
try Interaction.fetchAll(db)
|
|
}
|
|
expect(result?.count).to(equal(1))
|
|
expect(result?.map { $0.variant }).to(equal([.standardIncomingDeleted]))
|
|
}
|
|
|
|
// MARK: ------ schedules a garbage collection job to clean up the attachments
|
|
it("schedules a garbage collection job to clean up the attachments") {
|
|
mockStorage.write { db in
|
|
try SessionThread.upsert(
|
|
db,
|
|
id: createGroupOutput.group.threadId,
|
|
variant: .contact,
|
|
values: SessionThread.TargetValues(
|
|
creationDateTimestamp: .setTo(1234567890),
|
|
shouldBeVisible: .setTo(true)
|
|
),
|
|
using: dependencies
|
|
)
|
|
let interaction: Interaction = try Interaction(
|
|
serverHash: "1234",
|
|
messageUuid: nil,
|
|
threadId: createGroupOutput.group.threadId,
|
|
authorId: "4321",
|
|
variant: .standardIncoming,
|
|
body: nil,
|
|
timestampMs: 100000000,
|
|
receivedAtTimestampMs: 1234567890,
|
|
wasRead: false,
|
|
hasMention: false,
|
|
expiresInSeconds: nil,
|
|
expiresStartedAtMs: nil,
|
|
linkPreviewUrl: nil,
|
|
openGroupServerMessageId: nil,
|
|
openGroupWhisper: false,
|
|
openGroupWhisperMods: false,
|
|
openGroupWhisperTo: nil,
|
|
state: .sent,
|
|
recipientReadTimestampMs: nil,
|
|
mostRecentFailureText: nil,
|
|
transientDependencies: nil
|
|
).inserted(db)
|
|
_ = try Attachment(
|
|
id: "AttachmentId",
|
|
variant: .standard,
|
|
contentType: "Test",
|
|
byteCount: 1234
|
|
).inserted(db)
|
|
_ = try InteractionAttachment(
|
|
albumIndex: 1,
|
|
interactionId: interaction.id!,
|
|
attachmentId: "AttachmentId"
|
|
).inserted(db)
|
|
}
|
|
|
|
createGroupOutput.groupState[.groupInfo]?.conf.map {
|
|
groups_info_set_attach_delete_before($0, 123456)
|
|
}
|
|
|
|
mockStorage.write { db in
|
|
try mockLibSessionCache.handleGroupInfoUpdate(
|
|
db,
|
|
in: createGroupOutput.groupState[.groupInfo],
|
|
groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
|
|
serverTimestampMs: 1234567891000
|
|
)
|
|
}
|
|
|
|
expect(mockJobRunner)
|
|
.to(call(.exactly(times: 1), matchingParameters: .all) { jobRunner in
|
|
jobRunner.add(
|
|
.any,
|
|
job: Job(
|
|
variant: .garbageCollection,
|
|
behaviour: .runOnce,
|
|
shouldBlock: false,
|
|
shouldBeUnique: false,
|
|
shouldSkipLaunchBecomeActive: false,
|
|
details: GarbageCollectionJob.Details(
|
|
typesToCollect: [.orphanedAttachments, .orphanedAttachmentFiles]
|
|
)
|
|
),
|
|
canStartJob: true
|
|
)
|
|
})
|
|
}
|
|
|
|
// MARK: ------ does not delete messages with attachments after the timestamp
|
|
it("does not delete messages with attachments after the timestamp") {
|
|
mockStorage.write { db in
|
|
try SessionThread.upsert(
|
|
db,
|
|
id: createGroupOutput.group.threadId,
|
|
variant: .contact,
|
|
values: SessionThread.TargetValues(
|
|
creationDateTimestamp: .setTo(1234567890),
|
|
shouldBeVisible: .setTo(true)
|
|
),
|
|
using: dependencies
|
|
)
|
|
let interaction1: Interaction = try Interaction(
|
|
serverHash: "1234",
|
|
messageUuid: nil,
|
|
threadId: createGroupOutput.group.threadId,
|
|
authorId: "4321",
|
|
variant: .standardIncoming,
|
|
body: nil,
|
|
timestampMs: 100000000,
|
|
receivedAtTimestampMs: 1234567890,
|
|
wasRead: false,
|
|
hasMention: false,
|
|
expiresInSeconds: nil,
|
|
expiresStartedAtMs: nil,
|
|
linkPreviewUrl: nil,
|
|
openGroupServerMessageId: nil,
|
|
openGroupWhisper: false,
|
|
openGroupWhisperMods: false,
|
|
openGroupWhisperTo: nil,
|
|
state: .sent,
|
|
recipientReadTimestampMs: nil,
|
|
mostRecentFailureText: nil,
|
|
transientDependencies: nil
|
|
).inserted(db)
|
|
let interaction2: Interaction = try Interaction(
|
|
serverHash: "1235",
|
|
messageUuid: nil,
|
|
threadId: createGroupOutput.group.threadId,
|
|
authorId: "4321",
|
|
variant: .standardIncoming,
|
|
body: nil,
|
|
timestampMs: 200000000,
|
|
receivedAtTimestampMs: 2234567890,
|
|
wasRead: false,
|
|
hasMention: false,
|
|
expiresInSeconds: nil,
|
|
expiresStartedAtMs: nil,
|
|
linkPreviewUrl: nil,
|
|
openGroupServerMessageId: nil,
|
|
openGroupWhisper: false,
|
|
openGroupWhisperMods: false,
|
|
openGroupWhisperTo: nil,
|
|
state: .sent,
|
|
recipientReadTimestampMs: nil,
|
|
mostRecentFailureText: nil,
|
|
transientDependencies: nil
|
|
).inserted(db)
|
|
_ = try Attachment(
|
|
id: "AttachmentId",
|
|
variant: .standard,
|
|
contentType: "Test",
|
|
byteCount: 1234
|
|
).inserted(db)
|
|
_ = try Attachment(
|
|
id: "AttachmentId2",
|
|
variant: .standard,
|
|
contentType: "Test",
|
|
byteCount: 1234
|
|
).inserted(db)
|
|
_ = try InteractionAttachment(
|
|
albumIndex: 1,
|
|
interactionId: interaction1.id!,
|
|
attachmentId: "AttachmentId"
|
|
).inserted(db)
|
|
_ = try InteractionAttachment(
|
|
albumIndex: 1,
|
|
interactionId: interaction2.id!,
|
|
attachmentId: "AttachmentId2"
|
|
).inserted(db)
|
|
}
|
|
|
|
createGroupOutput.groupState[.groupInfo]?.conf.map {
|
|
groups_info_set_attach_delete_before($0, 123456)
|
|
}
|
|
|
|
mockStorage.write { db in
|
|
try mockLibSessionCache.handleGroupInfoUpdate(
|
|
db,
|
|
in: createGroupOutput.groupState[.groupInfo],
|
|
groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
|
|
serverTimestampMs: 1234567891000
|
|
)
|
|
}
|
|
|
|
let result: [Interaction]? = mockStorage.read { db in
|
|
try Interaction.fetchAll(db)
|
|
}
|
|
expect(result?.count).to(equal(2))
|
|
expect(result?.map { $0.variant }).to(equal([.standardIncomingDeleted, .standardIncoming]))
|
|
}
|
|
|
|
// MARK: ------ does not delete messages before the timestamp that have no attachments
|
|
it("does not delete messages before the timestamp that have no attachments") {
|
|
mockStorage.write { db in
|
|
try SessionThread.upsert(
|
|
db,
|
|
id: createGroupOutput.group.threadId,
|
|
variant: .contact,
|
|
values: SessionThread.TargetValues(
|
|
creationDateTimestamp: .setTo(1234567890),
|
|
shouldBeVisible: .setTo(true)
|
|
),
|
|
using: dependencies
|
|
)
|
|
let interaction1: Interaction = try Interaction(
|
|
serverHash: "1234",
|
|
messageUuid: nil,
|
|
threadId: createGroupOutput.group.threadId,
|
|
authorId: "4321",
|
|
variant: .standardIncoming,
|
|
body: nil,
|
|
timestampMs: 100000000,
|
|
receivedAtTimestampMs: 1234567890,
|
|
wasRead: false,
|
|
hasMention: false,
|
|
expiresInSeconds: nil,
|
|
expiresStartedAtMs: nil,
|
|
linkPreviewUrl: nil,
|
|
openGroupServerMessageId: nil,
|
|
openGroupWhisper: false,
|
|
openGroupWhisperMods: false,
|
|
openGroupWhisperTo: nil,
|
|
state: .sent,
|
|
recipientReadTimestampMs: nil,
|
|
mostRecentFailureText: nil,
|
|
transientDependencies: nil
|
|
).inserted(db)
|
|
_ = try Interaction(
|
|
serverHash: "1235",
|
|
messageUuid: nil,
|
|
threadId: createGroupOutput.group.threadId,
|
|
authorId: "4321",
|
|
variant: .standardIncoming,
|
|
body: nil,
|
|
timestampMs: 200000000,
|
|
receivedAtTimestampMs: 2234567890,
|
|
wasRead: false,
|
|
hasMention: false,
|
|
expiresInSeconds: nil,
|
|
expiresStartedAtMs: nil,
|
|
linkPreviewUrl: nil,
|
|
openGroupServerMessageId: nil,
|
|
openGroupWhisper: false,
|
|
openGroupWhisperMods: false,
|
|
openGroupWhisperTo: nil,
|
|
state: .sent,
|
|
recipientReadTimestampMs: nil,
|
|
mostRecentFailureText: nil,
|
|
transientDependencies: nil
|
|
).inserted(db)
|
|
_ = try Attachment(
|
|
id: "AttachmentId",
|
|
variant: .standard,
|
|
contentType: "Test",
|
|
byteCount: 1234
|
|
).inserted(db)
|
|
_ = try InteractionAttachment(
|
|
albumIndex: 1,
|
|
interactionId: interaction1.id!,
|
|
attachmentId: "AttachmentId"
|
|
).inserted(db)
|
|
}
|
|
|
|
createGroupOutput.groupState[.groupInfo]?.conf.map {
|
|
groups_info_set_attach_delete_before($0, 123456)
|
|
}
|
|
|
|
mockStorage.write { db in
|
|
try mockLibSessionCache.handleGroupInfoUpdate(
|
|
db,
|
|
in: createGroupOutput.groupState[.groupInfo],
|
|
groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
|
|
serverTimestampMs: 1234567891000
|
|
)
|
|
}
|
|
|
|
let result: [Interaction]? = mockStorage.read { db in
|
|
try Interaction.fetchAll(db)
|
|
}
|
|
expect(result?.count).to(equal(2))
|
|
expect(result?.map { $0.variant }).to(equal([.standardIncomingDeleted, .standardIncoming]))
|
|
}
|
|
}
|
|
|
|
// MARK: ---- deletes from the server after deleting messages before a given timestamp
|
|
it("deletes from the server after deleting messages before a given timestamp") {
|
|
mockStorage.write { db in
|
|
try SessionThread.upsert(
|
|
db,
|
|
id: createGroupOutput.group.threadId,
|
|
variant: .contact,
|
|
values: SessionThread.TargetValues(
|
|
creationDateTimestamp: .setTo(1234567890),
|
|
shouldBeVisible: .setTo(true)
|
|
),
|
|
using: dependencies
|
|
)
|
|
_ = try Interaction(
|
|
serverHash: "1234",
|
|
messageUuid: nil,
|
|
threadId: createGroupOutput.group.threadId,
|
|
authorId: "4321",
|
|
variant: .standardIncoming,
|
|
body: nil,
|
|
timestampMs: 100000000,
|
|
receivedAtTimestampMs: 1234567890,
|
|
wasRead: false,
|
|
hasMention: false,
|
|
expiresInSeconds: nil,
|
|
expiresStartedAtMs: nil,
|
|
linkPreviewUrl: nil,
|
|
openGroupServerMessageId: nil,
|
|
openGroupWhisper: false,
|
|
openGroupWhisperMods: false,
|
|
openGroupWhisperTo: nil,
|
|
state: .sent,
|
|
recipientReadTimestampMs: nil,
|
|
mostRecentFailureText: nil,
|
|
transientDependencies: nil
|
|
).inserted(db)
|
|
}
|
|
|
|
createGroupOutput.groupState[.groupInfo]?.conf.map { groups_info_set_delete_before($0, 123456) }
|
|
|
|
mockStorage.write { db in
|
|
try mockLibSessionCache.handleGroupInfoUpdate(
|
|
db,
|
|
in: createGroupOutput.groupState[.groupInfo],
|
|
groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
|
|
serverTimestampMs: 1234567891000
|
|
)
|
|
}
|
|
|
|
let expectedRequest: Network.PreparedRequest<[String: Bool]> = try SnodeAPI.preparedDeleteMessages(
|
|
serverHashes: ["1234"],
|
|
requireSuccessfulDeletion: false,
|
|
authMethod: Authentication.groupAdmin(
|
|
groupSessionId: createGroupOutput.groupSessionId,
|
|
ed25519SecretKey: createGroupOutput.identityKeyPair.secretKey
|
|
),
|
|
using: dependencies
|
|
)
|
|
expect(mockNetwork)
|
|
.to(call(.exactly(times: 1), matchingParameters: .all) { network in
|
|
network.send(
|
|
expectedRequest.body,
|
|
to: expectedRequest.destination,
|
|
requestTimeout: expectedRequest.requestTimeout,
|
|
requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout
|
|
)
|
|
})
|
|
}
|
|
|
|
// MARK: ---- does not delete from the server if there is no server hash
|
|
it("does not delete from the server if there is no server hash") {
|
|
mockStorage.write { db in
|
|
try SessionThread.upsert(
|
|
db,
|
|
id: createGroupOutput.group.threadId,
|
|
variant: .contact,
|
|
values: SessionThread.TargetValues(
|
|
creationDateTimestamp: .setTo(1234567890),
|
|
shouldBeVisible: .setTo(true)
|
|
),
|
|
using: dependencies
|
|
)
|
|
_ = try Interaction(
|
|
serverHash: nil,
|
|
messageUuid: nil,
|
|
threadId: createGroupOutput.group.threadId,
|
|
authorId: "4321",
|
|
variant: .standardIncoming,
|
|
body: nil,
|
|
timestampMs: 100000000,
|
|
receivedAtTimestampMs: 1234567890,
|
|
wasRead: false,
|
|
hasMention: false,
|
|
expiresInSeconds: nil,
|
|
expiresStartedAtMs: nil,
|
|
linkPreviewUrl: nil,
|
|
openGroupServerMessageId: nil,
|
|
openGroupWhisper: false,
|
|
openGroupWhisperMods: false,
|
|
openGroupWhisperTo: nil,
|
|
state: .sent,
|
|
recipientReadTimestampMs: nil,
|
|
mostRecentFailureText: nil,
|
|
transientDependencies: nil
|
|
).inserted(db)
|
|
}
|
|
|
|
createGroupOutput.groupState[.groupInfo]?.conf.map { groups_info_set_delete_before($0, 123456) }
|
|
|
|
mockStorage.write { db in
|
|
try mockLibSessionCache.handleGroupInfoUpdate(
|
|
db,
|
|
in: createGroupOutput.groupState[.groupInfo],
|
|
groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
|
|
serverTimestampMs: 1234567891000
|
|
)
|
|
}
|
|
|
|
let result: [Interaction]? = mockStorage.read { db in
|
|
try Interaction.fetchAll(db)
|
|
}
|
|
expect(result?.count).to(equal(1))
|
|
expect(result?.map { $0.variant }).to(equal([.standardIncomingDeleted]))
|
|
expect(mockNetwork)
|
|
.toNot(call { network in
|
|
network.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Convenience
|
|
|
|
private extension LibSession.Config {
|
|
var conf: UnsafeMutablePointer<config_object>? {
|
|
switch self {
|
|
case .userProfile(let conf), .contacts(let conf),
|
|
.convoInfoVolatile(let conf), .userGroups(let conf),
|
|
.groupInfo(let conf), .groupMembers(let conf):
|
|
return conf
|
|
default: return nil
|
|
}
|
|
}
|
|
}
|