From 08337fe0faa0152f8d7bb304b244754f6d7ea5b1 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 21 Nov 2023 15:58:03 +1100 Subject: [PATCH] More unit tests and trying to extend the CI test timeout --- .drone.jsonnet | 2 +- .../MessageReceiverGroupsSpec.swift | 484 +++++++++++++++++- 2 files changed, 479 insertions(+), 7 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 15ad85fc8..0c6259513 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -92,7 +92,7 @@ local update_cocoapods_cache = { name: 'Run Unit Tests', commands: [ 'mkdir build', - 'NSUnbufferedIO=YES set -o pipefail && xcodebuild test -workspace Session.xcworkspace -scheme Session -derivedDataPath ./build/derivedData -parallelizeTargets -destination "platform=iOS Simulator,name=iPhone 14" -parallel-testing-enabled YES -parallel-testing-worker-count 2 -test-timeouts-enabled YES -maximum-test-execution-time-allowance 5 -collect-test-diagnostics never 2>&1 | ./Pods/xcbeautify/xcbeautify --is-ci' + 'NSUnbufferedIO=YES set -o pipefail && xcodebuild test -workspace Session.xcworkspace -scheme Session -derivedDataPath ./build/derivedData -parallelizeTargets -destination "platform=iOS Simulator,name=iPhone 14" -parallel-testing-enabled YES -parallel-testing-worker-count 2 -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never 2>&1 | ./Pods/xcbeautify/xcbeautify --is-ci' ], }, { diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift index 78dfb0da7..eecf7a1d5 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift @@ -6,6 +6,7 @@ import GRDB import Quick import Nimble import SessionUtil +import SessionSnodeKit import SessionUtilitiesKit import SessionUIKit @@ -31,6 +32,7 @@ class MessageReceiverGroupsSpec: QuickSpec { customWriter: try! DatabaseQueue(), migrationTargets: [ SNUtilitiesKit.self, + SNSnodeKit.self, SNMessagingKit.self, SNUIKit.self ], @@ -132,14 +134,19 @@ class MessageReceiverGroupsSpec: QuickSpec { return conf }() - @TestState var groupInfoConfig: SessionUtil.Config! = .object(groupInfoConf) - @TestState var groupMembersConfig: SessionUtil.Config! = .object(groupMembersConf) - @TestState var groupKeysConfig: SessionUtil.Config! = { - var groupKeysConf: UnsafeMutablePointer! - _ = groups_keys_init(&groupKeysConf, &secretKey, &groupEdPK, &groupEdSK, groupInfoConf, groupMembersConf, nil, 0, nil) + @TestState var groupKeysConf: UnsafeMutablePointer! = { + var conf: UnsafeMutablePointer! + _ = groups_keys_init(&conf, &secretKey, &groupEdPK, &groupEdSK, groupInfoConf, groupMembersConf, nil, 0, nil) - return .groupKeys(groupKeysConf, info: groupInfoConf, members: groupMembersConf) + return conf }() + @TestState var groupInfoConfig: SessionUtil.Config! = .object(groupInfoConf) + @TestState var groupMembersConfig: SessionUtil.Config! = .object(groupMembersConf) + @TestState var groupKeysConfig: SessionUtil.Config! = .groupKeys( + groupKeysConf, + info: groupInfoConf, + members: groupMembersConf + ) @TestState(cache: .sessionUtil, in: dependencies) var mockSessionUtilCache: MockSessionUtilCache! = MockSessionUtilCache( initialSetup: { cache in let userSessionId: SessionId = SessionId(.standard, hex: TestConstants.publicKey) @@ -169,6 +176,9 @@ class MessageReceiverGroupsSpec: QuickSpec { poller .when { $0.startIfNeeded(for: .any, using: .any) } .thenReturn(()) + poller + .when { $0.stopPolling(for: .any) } + .thenReturn(()) } ) @TestState(singleton: .notificationsManager, in: dependencies) var mockNotificationsManager: MockNotificationsManager! = MockNotificationsManager( @@ -206,6 +216,10 @@ class MessageReceiverGroupsSpec: QuickSpec { return result }() + @TestState var deleteMessage: Data! = try! LibSessionMessage.groupKicked( + memberId: "05\(TestConstants.publicKey)", + groupKeysGen: 1 + ).1 // MARK: - a MessageReceiver dealing with Groups describe("a MessageReceiver dealing with Groups") { @@ -977,6 +991,464 @@ class MessageReceiverGroupsSpec: QuickSpec { expect(groupMember.promoted).to(equal(0)) } } + + // MARK: -- when receiving a delete message + context("when receiving a delete message") { + beforeEach { + var cGroupId: [CChar] = groupId.hexString.cArray.nullTerminated() + var userGroup: ugroups_group_info = ugroups_group_info() + user_groups_get_or_construct_group(userGroupsConfig.conf, &userGroup, &cGroupId) + userGroup.name = "TestName".toLibSession() + user_groups_set_group(userGroupsConfig.conf, &userGroup) + + // Rekey a couple of times to increase the key generation to 1 + var fakeHash1: [CChar] = "fakehash1".cArray.nullTerminated() + var fakeHash2: [CChar] = "fakehash2".cArray.nullTerminated() + var pushResult: UnsafePointer? = nil + var pushResultLen: Int = 0 + _ = groups_keys_rekey(groupKeysConf, groupInfoConf, groupMembersConf, &pushResult, &pushResultLen) + _ = groups_keys_load_message(groupKeysConf, &fakeHash1, pushResult, pushResultLen, 1234567890, groupInfoConf, groupMembersConf) + _ = groups_keys_rekey(groupKeysConf, groupInfoConf, groupMembersConf, &pushResult, &pushResultLen) + _ = groups_keys_load_message(groupKeysConf, &fakeHash2, pushResult, pushResultLen, 1234567890, groupInfoConf, groupMembersConf) + + mockStorage.write { db in + try SessionThread.fetchOrCreate( + db, + id: groupId.hexString, + variant: .group, + shouldBeVisible: true, + calledFromConfigHandling: false, + using: dependencies + ) + + try ClosedGroup( + threadId: groupId.hexString, + name: "TestGroup", + formationTimestamp: 1234567890, + shouldPoll: true, + groupIdentityPrivateKey: nil, + authData: Data([1, 2, 3]), + invited: false + ).upsert(db) + + try GroupMember( + groupId: groupId.hexString, + profileId: "05\(TestConstants.publicKey)", + role: .standard, + roleStatus: .accepted, + isHidden: false + ).insert(db) + + _ = try Interaction( + id: 1, + serverHash: nil, + messageUuid: nil, + threadId: groupId.hexString, + authorId: "051111111111111111111111111111111111111111111111111111111111111111", + variant: .standardIncoming, + body: "Test", + timestampMs: 1234567890, + receivedAtTimestampMs: 1234567890, + wasRead: false, + hasMention: false, + expiresInSeconds: 0, + expiresStartedAtMs: nil, + linkPreviewUrl: nil, + openGroupServerMessageId: nil, + openGroupWhisperMods: false, + openGroupWhisperTo: nil + ).inserted(db) + + try ConfigDump( + variant: .groupKeys, + sessionId: groupId.hexString, + data: Data([1, 2, 3]), + timestampMs: 1234567890 + ).insert(db) + + try ConfigDump( + variant: .groupInfo, + sessionId: groupId.hexString, + data: Data([1, 2, 3]), + timestampMs: 1234567890 + ).insert(db) + + try ConfigDump( + variant: .groupMembers, + sessionId: groupId.hexString, + data: Data([1, 2, 3]), + timestampMs: 1234567890 + ).insert(db) + } + } + + // MARK: ---- deletes any interactions from the conversation + it("deletes any interactions from the conversation") { + mockStorage.write { db in + try MessageReceiver.handleGroupDelete( + db, + groupSessionId: groupId, + plaintext: deleteMessage, + using: dependencies + ) + } + + let interactions: [Interaction]? = mockStorage.read { db in try Interaction.fetchAll(db) } + expect(interactions).to(beEmpty()) + } + + // MARK: ---- deletes the group auth data + it("deletes the group auth data") { + mockStorage.write { db in + try MessageReceiver.handleGroupDelete( + db, + groupSessionId: groupId, + plaintext: deleteMessage, + using: dependencies + ) + } + + let authData: [Data?]? = mockStorage.read { db in + try ClosedGroup + .select(ClosedGroup.Columns.authData) + .asRequest(of: Data?.self) + .fetchAll(db) + } + let privateKeyData: [Data?]? = mockStorage.read { db in + try ClosedGroup + .select(ClosedGroup.Columns.groupIdentityPrivateKey) + .asRequest(of: Data?.self) + .fetchAll(db) + } + expect(authData).to(equal([nil])) + expect(privateKeyData).to(equal([nil])) + } + + // MARK: ---- deletes the group members + it("deletes the group members") { + mockStorage.write { db in + try MessageReceiver.handleGroupDelete( + db, + groupSessionId: groupId, + plaintext: deleteMessage, + using: dependencies + ) + } + + let members: [GroupMember]? = mockStorage.read { db in try GroupMember.fetchAll(db) } + expect(members).to(beEmpty()) + } + + // MARK: ---- removes the group libSession state + it("removes the group libSession state") { + mockStorage.write { db in + try MessageReceiver.handleGroupDelete( + db, + groupSessionId: groupId, + plaintext: deleteMessage, + using: dependencies + ) + } + + expect(mockSessionUtilCache) + .to(call(.exactly(times: 1), matchingParameters: .all) { + $0.setConfig(for: .groupKeys, sessionId: groupId, to: nil) + }) + expect(mockSessionUtilCache) + .to(call(.exactly(times: 1), matchingParameters: .all) { + $0.setConfig(for: .groupInfo, sessionId: groupId, to: nil) + }) + expect(mockSessionUtilCache) + .to(call(.exactly(times: 1), matchingParameters: .all) { + $0.setConfig(for: .groupMembers, sessionId: groupId, to: nil) + }) + } + + // MARK: ---- removes the cached libSession state dumps + it("removes the cached libSession state dumps") { + mockStorage.write { db in + try MessageReceiver.handleGroupDelete( + db, + groupSessionId: groupId, + plaintext: deleteMessage, + using: dependencies + ) + } + + expect(mockSessionUtilCache) + .to(call(.exactly(times: 1), matchingParameters: .all) { + $0.setConfig(for: .groupKeys, sessionId: groupId, to: nil) + }) + expect(mockSessionUtilCache) + .to(call(.exactly(times: 1), matchingParameters: .all) { + $0.setConfig(for: .groupInfo, sessionId: groupId, to: nil) + }) + expect(mockSessionUtilCache) + .to(call(.exactly(times: 1), matchingParameters: .all) { + $0.setConfig(for: .groupMembers, sessionId: groupId, to: nil) + }) + + let dumps: [ConfigDump]? = mockStorage.read { db in + try ConfigDump + .filter(ConfigDump.Columns.publicKey == groupId.hexString) + .fetchAll(db) + } + expect(dumps).to(beEmpty()) + } + + // MARK: ------ unsubscribes from push notifications + it("unsubscribes from push notifications") { + mockUserDefaults + .when { $0.string(forKey: UserDefaults.StringKey.deviceToken.rawValue) } + .thenReturn(Data([5, 4, 3, 2, 1]).toHexString()) + mockUserDefaults + .when { $0.bool(forKey: UserDefaults.BoolKey.isUsingFullAPNs.rawValue) } + .thenReturn(true) + + let expectedRequest: URLRequest = mockStorage.read(using: dependencies) { db in + try PushNotificationAPI + .preparedUnsubscribe( + db, + token: Data([5, 4, 3, 2, 1]), + sessionIds: [groupId], + using: dependencies + ) + .request + }! + + mockStorage.write { db in + try MessageReceiver.handleGroupDelete( + db, + groupSessionId: groupId, + plaintext: deleteMessage, + using: dependencies + ) + } + + expect(mockNetwork) + .to(call(.exactly(times: 1), matchingParameters: .all) { network in + network.send( + .selectedNetworkRequest( + expectedRequest, + to: PushNotificationAPI.server.value(using: dependencies), + with: PushNotificationAPI.serverPublicKey, + timeout: HTTP.defaultTimeout, + using: .any + ) + ) + }) + } + + // MARK: ---- and the group is an invitation + context("and the group is an invitation") { + beforeEach { + mockStorage.write { db in + try ClosedGroup.updateAll(db, ClosedGroup.Columns.invited.set(to: true)) + } + } + + // MARK: ------ deletes the thread + it("deletes the thread") { + mockStorage.write { db in + try MessageReceiver.handleGroupDelete( + db, + groupSessionId: groupId, + plaintext: deleteMessage, + using: dependencies + ) + } + + let threads: [SessionThread]? = mockStorage.read { db in try SessionThread.fetchAll(db) } + expect(threads).to(beEmpty()) + } + + // MARK: ------ deletes the group + it("deletes the group") { + mockStorage.write { db in + try MessageReceiver.handleGroupDelete( + db, + groupSessionId: groupId, + plaintext: deleteMessage, + using: dependencies + ) + } + + let groups: [ClosedGroup]? = mockStorage.read { db in try ClosedGroup.fetchAll(db) } + expect(groups).to(beEmpty()) + } + + // MARK: ---- stops the poller + it("stops the poller") { + mockStorage.write { db in + try MessageReceiver.handleGroupDelete( + db, + groupSessionId: groupId, + plaintext: deleteMessage, + using: dependencies + ) + } + + expect(mockGroupsPoller) + .to(call(.exactly(times: 1), matchingParameters: .all) { + $0.stopPolling(for: groupId.hexString) + }) + } + + // MARK: ------ removes the group from the USER_GROUPS config + it("removes the group from the USER_GROUPS config") { + mockStorage.write { db in + try MessageReceiver.handleGroupDelete( + db, + groupSessionId: groupId, + plaintext: deleteMessage, + using: dependencies + ) + } + + var cGroupId: [CChar] = groupId.hexString.cArray.nullTerminated() + var userGroup: ugroups_group_info = ugroups_group_info() + expect(user_groups_get_group(userGroupsConfig.conf, &userGroup, &cGroupId)).to(beFalse()) + } + } + + // MARK: ---- and the group is not an invitation + context("and the group is not an invitation") { + beforeEach { + mockStorage.write { db in + try ClosedGroup.updateAll(db, ClosedGroup.Columns.invited.set(to: false)) + } + } + + // MARK: ------ does not delete the thread + it("does not delete the thread") { + mockStorage.write { db in + try MessageReceiver.handleGroupDelete( + db, + groupSessionId: groupId, + plaintext: deleteMessage, + using: dependencies + ) + } + + let threads: [SessionThread]? = mockStorage.read { db in try SessionThread.fetchAll(db) } + expect(threads).toNot(beEmpty()) + } + + // MARK: ------ does not remove the group from the USER_GROUPS config + it("does not remove the group from the USER_GROUPS config") { + mockStorage.write { db in + try MessageReceiver.handleGroupDelete( + db, + groupSessionId: groupId, + plaintext: deleteMessage, + using: dependencies + ) + } + + var cGroupId: [CChar] = groupId.hexString.cArray.nullTerminated() + var userGroup: ugroups_group_info = ugroups_group_info() + expect(user_groups_get_group(userGroupsConfig.conf, &userGroup, &cGroupId)).to(beTrue()) + } + + // MARK: ---- stops the poller and flags the group to not poll + it("stops the poller and flags the group to not poll") { + mockStorage.write { db in + try MessageReceiver.handleGroupDelete( + db, + groupSessionId: groupId, + plaintext: deleteMessage, + using: dependencies + ) + } + + let shouldPoll: [Bool]? = mockStorage.read { db in + try ClosedGroup + .select(ClosedGroup.Columns.shouldPoll) + .asRequest(of: Bool.self) + .fetchAll(db) + } + expect(mockGroupsPoller) + .to(call(.exactly(times: 1), matchingParameters: .all) { + $0.stopPolling(for: groupId.hexString) + }) + expect(shouldPoll).to(equal([false])) + } + + // MARK: ------ marks the group in USER_GROUPS as kicked + it("marks the group in USER_GROUPS as kicked") { + mockStorage.write { db in + try MessageReceiver.handleGroupDelete( + db, + groupSessionId: groupId, + plaintext: deleteMessage, + using: dependencies + ) + } + + var cGroupId: [CChar] = groupId.hexString.cArray.nullTerminated() + var userGroup: ugroups_group_info = ugroups_group_info() + expect(user_groups_get_group(userGroupsConfig.conf, &userGroup, &cGroupId)).to(beTrue()) + expect(ugroups_group_is_kicked(&userGroup)).to(beTrue()) + } + } + + // MARK: ---- throws if the data is invalid + it("throws if the data is invalid") { + deleteMessage = Data([1, 2, 3]) + + mockStorage.write { db in + expect { + try MessageReceiver.handleGroupDelete( + db, + groupSessionId: groupId, + plaintext: deleteMessage, + using: dependencies + ) + } + .to(throwError(MessageReceiverError.invalidMessage)) + } + } + + // MARK: ---- throws if the included member id does not match the current user + it("throws if the included member id does not match the current user") { + deleteMessage = try! LibSessionMessage.groupKicked( + memberId: "051111111111111111111111111111111111111111111111111111111111111111", + groupKeysGen: 1 + ).1 + + mockStorage.write { db in + expect { + try MessageReceiver.handleGroupDelete( + db, + groupSessionId: groupId, + plaintext: deleteMessage, + using: dependencies + ) + } + .to(throwError(MessageReceiverError.invalidMessage)) + } + } + + // MARK: ---- throws if the key generation is earlier than the current keys generation + it("throws if the key generation is earlier than the current keys generation") { + deleteMessage = try! LibSessionMessage.groupKicked( + memberId: "05\(TestConstants.publicKey)", + groupKeysGen: 0 + ).1 + + mockStorage.write { db in + expect { + try MessageReceiver.handleGroupDelete( + db, + groupSessionId: groupId, + plaintext: deleteMessage, + using: dependencies + ) + } + .to(throwError(MessageReceiverError.invalidMessage)) + } + } + } } } }