import PromiseKit
import ThreemaEssentials
import ThreemaProtocols
import XCTest
@testable import ThreemaFramework
final class TaskQueueTests: XCTestCase {
private var testDatabase: TestDatabase!
private var dbPreparer: TestDatabasePreparer!
private var ddLoggerMock: DDLoggerMock!
override func setUpWithError() throws {
AppGroup.setGroupID("group.ch.threema")
testDatabase = TestDatabase()
dbPreparer = testDatabase.backgroundPreparer
// Workaround to ensure remote secret is initialized
AppLaunchManager.shared.setRemoteSecretManager(testDatabase.remoteSecretManagerMock)
ddLoggerMock = DDLoggerMock()
DDTTYLogger.sharedInstance?.logFormatter = LogFormatterCustom()
DDLog.add(ddLoggerMock)
}
override func tearDownWithError() throws {
DDLog.remove(ddLoggerMock)
}
func testInterrupt() {
let frameworkInjectorMock = BusinessInjectorMock(entityManager: testDatabase.backgroundEntityManager)
let msg = ContactDeletePhotoMessage()
msg.messageID = BytesUtility.generateMessageID()
let tq = TaskQueue(
frameworkInjectorResolver: FrameworkInjectorResolverMock(frameworkInjector: frameworkInjectorMock)
)
let taskVolatile = TaskDefinitionSendAbstractMessage(message: msg, type: .volatile)
try? tq.enqueue(task: taskVolatile, completionHandler: nil)
let taskDropOnDisconnect = TaskDefinitionSendAbstractMessage(message: msg, type: .dropOnDisconnect)
try? tq.enqueue(task: taskDropOnDisconnect, completionHandler: nil)
XCTAssertEqual(.pending, taskVolatile.state)
XCTAssertEqual(2, tq.list.count)
tq.interrupt()
XCTAssertEqual(.pending, taskVolatile.state, "Test interrupt on pending item")
XCTAssertEqual(1, tq.list.count)
tq.list.first?.taskDefinition.state = .executing
XCTAssertEqual(.executing, taskVolatile.state, "Set state to executing for testing reason")
tq.interrupt()
XCTAssertEqual(.interrupted, taskVolatile.state, "Test interrupt on executing item")
XCTAssertEqual(1, tq.list.count)
}
func testInterruptWithExecutingDropOnDisconnectTask() async throws {
let frameworkInjectorMock = BusinessInjectorMock(entityManager: testDatabase.backgroundEntityManager)
let msg = ContactDeletePhotoMessage()
msg.messageID = BytesUtility.generateMessageID()
let tq = TaskQueue(
frameworkInjectorResolver: FrameworkInjectorResolverMock(frameworkInjector: frameworkInjectorMock)
)
let taskDropOnDisconnectExecuting = TaskDefinitionSendAbstractMessage(message: msg, type: .dropOnDisconnect)
try? tq.enqueue(task: taskDropOnDisconnectExecuting) { _, _ in
XCTFail("This should never be called")
}
let dropOnDisconnectExpectation1 = expectation(description: "drop on disconnect callback 1")
let taskDropOnDisconnect1 = TaskDefinitionSendAbstractMessage(message: msg, type: .dropOnDisconnect)
try tq.enqueue(task: taskDropOnDisconnect1) { _, error in
XCTAssertEqual("\(TaskExecutionError.taskDropped)", "\(error!)")
dropOnDisconnectExpectation1.fulfill()
}
let taskPersistent = TaskDefinitionSendAbstractMessage(message: msg, type: .persistent)
try tq.enqueue(task: taskPersistent) { _, _ in
XCTFail("This should never be called")
}
let dropOnDisconnectExpectation2 = expectation(description: "drop on disconnect callback 2")
let taskDropOnDisconnect2 = TaskDefinitionSendAbstractMessage(message: msg, type: .dropOnDisconnect)
try tq.enqueue(task: taskDropOnDisconnect2) { _, error in
XCTAssertEqual("\(TaskExecutionError.taskDropped)", "\(error!)")
dropOnDisconnectExpectation2.fulfill()
}
XCTAssertEqual(.pending, taskDropOnDisconnectExecuting.state)
XCTAssertFalse(taskDropOnDisconnectExecuting.isDropped)
XCTAssertEqual(.pending, taskDropOnDisconnect1.state)
XCTAssertFalse(taskDropOnDisconnect1.isDropped)
XCTAssertEqual(.pending, taskPersistent.state)
XCTAssertFalse(taskPersistent.isDropped)
XCTAssertEqual(.pending, taskDropOnDisconnect2.state)
XCTAssertFalse(taskDropOnDisconnect2.isDropped)
XCTAssertEqual(4, tq.list.count)
tq.list.first?.taskDefinition.state = .executing
XCTAssertEqual(.executing, taskDropOnDisconnectExecuting.state)
tq.interrupt()
// taskDropOnDisconnectX should be removed and completion handler should be called
await fulfillment(of: [dropOnDisconnectExpectation1, dropOnDisconnectExpectation2])
XCTAssertEqual(2, tq.list.count)
XCTAssertTrue(taskDropOnDisconnect1.isDropped)
XCTAssertTrue(taskDropOnDisconnect2.isDropped)
// taskPersistent should be still pending
XCTAssertEqual(.pending, taskPersistent.state)
// taskDropOnDisconnectExecuting should be marked as interrupted and dropped, but still in the queue
let firstItem = try XCTUnwrap(tq.list.first)
XCTAssertEqual(taskDropOnDisconnectExecuting, firstItem.taskDefinition)
XCTAssertEqual(.interrupted, taskDropOnDisconnectExecuting.state)
XCTAssertTrue(taskDropOnDisconnectExecuting.isDropped)
}
func testSpoolServerConnectorDisconnected() {
let frameworkInjectorMock = BusinessInjectorMock(entityManager: testDatabase.backgroundEntityManager)
let tq = TaskQueue(
frameworkInjectorResolver: FrameworkInjectorResolverMock(frameworkInjector: frameworkInjectorMock)
)
tq.spool()
DDLog.flushLog()
XCTAssertTrue(ddLoggerMock.exists(message: "Task queue spool interrupt, because not logged in to server"))
}
func testSpoolTaskDefinitionSendAbstractMessage() async throws {
let expectedReceiverIdentity = "ECHOECHO"
dbPreparer.save {
let contactEntity = dbPreparer.createContact(
publicKey: BytesUtility.generatePublicKey(),
identity: expectedReceiverIdentity
)
dbPreparer.createConversation(contactEntity: contactEntity)
}
let serverConnectorMock = ServerConnectorMock(connectionState: .loggedIn)
let myIdentityStoreMock = MyIdentityStoreMock()
let frameworkInjectorMock = BusinessInjectorMock(
entityManager: testDatabase.backgroundEntityManager,
myIdentityStore: myIdentityStoreMock,
serverConnector: serverConnectorMock
)
let tq = TaskQueue(
frameworkInjectorResolver: FrameworkInjectorResolverMock(frameworkInjector: frameworkInjectorMock)
)
let message = TypingIndicatorMessage()
message.fromIdentity = myIdentityStoreMock.identity
message.toIdentity = expectedReceiverIdentity
message.typing = true
let expec = expectation(description: "spool")
let task = TaskDefinitionSendAbstractMessage(message: message, type: .volatile)
try tq.enqueue(task: task) { _, error in
XCTAssertNil(error)
DDLog.flushLog()
expec.fulfill()
}
tq.spool()
await fulfillment(of: [expec], timeout: 6)
XCTAssertEqual(serverConnectorMock.sendMessageCalls.count, 1)
XCTAssertTrue(
ddLoggerMock.exists(
message: "<TaskDefinitionSendAbstractMessage (type: typingIndicator; id: \(message.messageID.hexString))> done"
)
)
}
func testSpoolInterruptedDroppedTaskDefinitionSendAbstractMessage() async throws {
let expectedReceiverIdentity = "ECHOECHO"
dbPreparer.save {
let contactEntity = dbPreparer.createContact(
publicKey: BytesUtility.generatePublicKey(),
identity: expectedReceiverIdentity
)
dbPreparer.createConversation(contactEntity: contactEntity)
}
let serverConnectorMock = ServerConnectorMock(connectionState: .loggedIn)
let myIdentityStoreMock = MyIdentityStoreMock()
let frameworkInjectorMock = BusinessInjectorMock(
entityManager: testDatabase.backgroundEntityManager,
myIdentityStore: myIdentityStoreMock,
serverConnector: serverConnectorMock
)
let tq = TaskQueue(
frameworkInjectorResolver: FrameworkInjectorResolverMock(frameworkInjector: frameworkInjectorMock)
)
let message = TypingIndicatorMessage()
message.fromIdentity = myIdentityStoreMock.identity
message.toIdentity = expectedReceiverIdentity
message.typing = true
let task = TaskDefinitionSendAbstractMessage(message: message, type: .dropOnDisconnect)
try tq.enqueue(task: task) { _, _ in
XCTFail("This should not be called")
}
task.state = .interrupted
task.isDropped = true
tq.spool()
DDLog.flushLog()
XCTAssertEqual(serverConnectorMock.sendMessageCalls.count, 0)
XCTAssertEqual(.interrupted, task.state, "Task is not executed again")
}
func testSpoolTaskDefinitionReceiveMessageProcessingFailedWithRetry() {
let expectedReceiver = "ECHOECHO"
let expectedError = NSError(domain: "Test domain", code: 1, userInfo: nil)
dbPreparer.createContact(
publicKey: BytesUtility.generatePublicKey(),
identity: expectedReceiver
)
let serverConnectorMock = ServerConnectorMock(connectionState: .loggedIn)
let messageProcessorMock = MessageProcessorMock()
messageProcessorMock.error = expectedError
let frameworkInjectorMock = BusinessInjectorMock(
entityManager: testDatabase.backgroundEntityManager,
serverConnector: serverConnectorMock,
messageProcessor: messageProcessorMock
)
// 2 tests with: retry / processIncomingMessageCalls / completedProcessingMessageCalls / retryCount
for test in [[false, 1, 0, 0] as [Any], [true, 2, 0, 1]] {
// Reset mocks first
ddLoggerMock.logMessages.removeAll()
messageProcessorMock.processIncomingBoxedMessageCalls.removeAll()
serverConnectorMock.completedProcessingMessageCalls.removeAll()
let tq = TaskQueue(
frameworkInjectorResolver: FrameworkInjectorResolverMock(frameworkInjector: frameworkInjectorMock)
)
let message = BoxedMessage()
message.messageID = BytesUtility.generateMessageID()
message.fromIdentity = "TESTER01"
message.toIdentity = expectedReceiver
let expec = expectation(description: "spool")
let task = TaskDefinitionReceiveMessage(
message: message,
receivedAfterInitialQueueSend: true,
maxBytesToDecrypt: 0,
timeoutDownloadThumbnail: 0
)
task.retry = test[0] as! Bool
try? tq.enqueue(task: task) { _, error in
XCTAssertNotNil(error)
DDLog.flushLog()
expec.fulfill()
}
tq.spool()
waitForExpectations(timeout: 6) { error in
XCTAssertNil(error)
XCTAssertEqual(tq.list.count, 0, "Task must be removed from incoming queue, if it failed")
XCTAssertEqual(messageProcessorMock.processIncomingBoxedMessageCalls.count, test[1] as! Int)
XCTAssertEqual(serverConnectorMock.completedProcessingMessageCalls.count, test[2] as! Int)
XCTAssertEqual(task.retryCount, test[3] as! Int)
XCTAssertTrue(
self.ddLoggerMock
.exists(
message: "<TaskDefinitionReceiveMessage (type: BoxedMessage; id: \(message.messageID.hexString))> failed Error Domain=\(expectedError.domain) Code=\(expectedError.code) \"(null)\""
)
)
if task.retry {
XCTAssertTrue(
self.ddLoggerMock
.exists(
message: "Retry of <TaskDefinitionReceiveMessage (type: BoxedMessage; id: \(message.messageID.hexString))> after execution failing"
)
)
}
else {
XCTAssertFalse(
self.ddLoggerMock
.exists(
message: "Retry of <TaskDefinitionReceiveMessage (type: BoxedMessage; id: \(message.messageID.hexString))> after execution failing"
)
)
}
}
}
}
func testSpoolExecuteNextTaskExponentialBackoff() throws {
let maxSpoolingDelayAttempts = 3
var spoolingDelayAttempts = 0
let expectedReflectedMessage =
Data(
base64Encoded: "ggAAABAAAABrAgAAOdq1mnABAAByNgeC13aJplJlX+cv+jaZcTO0oUpqDeZNu9uFLPxl1L4e5tm1R7ZSBo8LFBhEnfP6ckPX1Eqyonirb3JPDrMRXiu6ugDjVPrzUPY3K50hG4sKIjXvUVvX/zMWkefgCcJtRPjFcB+5Tjv5VBKQ/25unQl3TCCSSmtDBftU+vDdAEnfJ3ReaQ8poPd9bg=="
)!
let expectedReflectID = Data(base64Encoded: "awIAAA==")!
let expectedEnvelop = D2d_Envelope()
let serverConnectorMock = ServerConnectorMock(
connectionState: .loggedIn,
deviceID: MockMultiDevice.deviceID,
deviceGroupKeys: MockMultiDevice.deviceGroupKeys
)
let mediatorMessageProtocolMock = try MediatorMessageProtocolMock(
deviceGroupKeys: serverConnectorMock.deviceGroupKeys!,
returnValues: [
MediatorMessageProtocolMock.ReflectData(
id: expectedReflectID,
message: expectedEnvelop.serializedData()
),
]
)
let mediatorReflectedProcessorMock = MediatorReflectedProcessorMock()
mediatorReflectedProcessorMock.process = { _, _, _, _, _ -> Promise<Void> in
guard spoolingDelayAttempts >= maxSpoolingDelayAttempts else {
spoolingDelayAttempts += 1
return Promise(error: ThreemaError.threemaError(
"Not logged in",
withCode: ThreemaProtocolError.notLoggedIn.rawValue
))
}
return Promise()
}
let frameworkInjectorMock = BusinessInjectorMock(
entityManager: testDatabase.backgroundEntityManager,
serverConnector: serverConnectorMock,
mediatorMessageProtocol: mediatorMessageProtocolMock,
mediatorReflectedProcessor: mediatorReflectedProcessorMock
)
let tq = TaskQueue(
frameworkInjectorResolver: FrameworkInjectorResolverMock(frameworkInjector: frameworkInjectorMock)
)
let expect = expectation(description: "spool")
let task = TaskDefinitionReceiveReflectedMessage(
reflectedMessage: expectedReflectedMessage,
receivedAfterInitialQueueSend: true,
maxBytesToDecrypt: 0,
timeoutDownloadThumbnail: 0
)
try? tq.enqueue(task: task) { _, error in
XCTAssertNil(error)
DDLog.flushLog()
expect.fulfill()
}
tq.spool()
wait(for: [expect], timeout: 6)
XCTAssertTrue(ddLoggerMock.exists(message: "Waiting 0.5 seconds before execute next task"))
XCTAssertTrue(ddLoggerMock.exists(message: "Waiting 1.0 seconds before execute next task"))
XCTAssertTrue(ddLoggerMock.exists(message: "Waiting 2.0 seconds before execute next task"))
}
func testSpoolAndDroppingRacesTask() async throws {
let serverConnectorMock = ServerConnectorMock(
connectionState: .loggedIn,
deviceID: MockMultiDevice.deviceID,
deviceGroupKeys: MockMultiDevice.deviceGroupKeys
)
let frameworkInjectorMock = BusinessInjectorMock(
entityManager: testDatabase.backgroundEntityManager,
userSettings: UserSettingsMock(enableMultiDevice: true),
serverConnector: serverConnectorMock
)
// Reset mocks first
ddLoggerMock.logMessages.removeAll()
serverConnectorMock.reflectMessageCalls.removeAll()
let taskQueue = TaskQueue(
frameworkInjectorResolver: FrameworkInjectorResolverMock(frameworkInjector: frameworkInjectorMock)
)
let expect = expectation(description: "spool")
// This test will alway run at least for 1s as we wait such that the interrupt can run and then we wait if the
// completion closure is called more than once.
let task = TaskDefinitionMock(sleepInterval: 0.5)
var completionCalled = 0
try taskQueue.enqueue(task: task) { _, error in
XCTAssertEqual("\(TaskExecutionError.taskDropped)", "\(error!)")
completionCalled += 1
DDLog.flushLog()
// This helps us to check if the closure is called more than once
Task {
try await Task.sleep(seconds: 0.5)
expect.fulfill()
}
}
taskQueue.spool()
taskQueue.spool()
taskQueue.interrupt()
taskQueue.spool()
await fulfillment(of: [expect], timeout: 10)
XCTAssertEqual(1, completionCalled)
XCTAssertEqual(taskQueue.list.count, 0)
XCTAssertTrue(task.isDropped)
XCTAssertTrue(ddLoggerMock.exists(message: "<TaskDefinitionMock> state 'pending' execute"))
XCTAssertTrue(ddLoggerMock.exists(message: "<TaskDefinitionMock> is still running"))
XCTAssertTrue(ddLoggerMock.exists(message: "<TaskDefinitionMock> interrupted and marked as dropped"))
XCTAssertTrue(ddLoggerMock.exists(
message: "<TaskDefinitionMock> state 'interrupted' was dropped. Don't execute"
))
XCTAssertTrue(ddLoggerMock.exists(message: "<TaskDefinitionMock> reported dropped error"))
XCTAssertTrue(ddLoggerMock.exists(message: "<TaskDefinitionMock> dropped"))
}
func testSpoolInterruptAndFailWithNonDroppingError() async throws {
let serverConnectorMock = ServerConnectorMock(
connectionState: .loggedIn,
deviceID: MockMultiDevice.deviceID,
deviceGroupKeys: MockMultiDevice.deviceGroupKeys
)
let frameworkInjectorMock = BusinessInjectorMock(
entityManager: testDatabase.backgroundEntityManager,
userSettings: UserSettingsMock(enableMultiDevice: true),
serverConnector: serverConnectorMock
)
// Reset mocks first
ddLoggerMock.logMessages.removeAll()
serverConnectorMock.reflectMessageCalls.removeAll()
let taskQueue = TaskQueue(
frameworkInjectorResolver: FrameworkInjectorResolverMock(frameworkInjector: frameworkInjectorMock)
)
let expect = expectation(description: "spool")
// This test will alway run at least for 1s as we wait such that the interrupt can run and then we wait if the
// completion closure is called more than once.
let task = TaskDefinitionMock(sleepInterval: 0.5, customError: TaskExecutionError.wrongTaskDefinitionType)
var completionCalled = 0
try taskQueue.enqueue(task: task) { _, error in
XCTAssertEqual("\(TaskExecutionError.taskDropped)", "\(error!)")
completionCalled += 1
DDLog.flushLog()
// This helps us to check if the closure is called more than once
Task {
try await Task.sleep(seconds: 0.5)
expect.fulfill()
}
}
taskQueue.spool()
taskQueue.interrupt()
await fulfillment(of: [expect], timeout: 10)
XCTAssertEqual(1, completionCalled)
XCTAssertEqual(taskQueue.list.count, 0)
XCTAssertTrue(task.isDropped)
XCTAssertTrue(ddLoggerMock.exists(message: "<TaskDefinitionMock> state 'pending' execute"))
XCTAssertTrue(ddLoggerMock.exists(message: "<TaskDefinitionMock> interrupted and marked as dropped"))
XCTAssertTrue(ddLoggerMock.exists(message: "<TaskDefinitionMock> failed and marked as dropped"))
XCTAssertTrue(ddLoggerMock.exists(message: "<TaskDefinitionMock> dropped"))
}
func testSpoolTaskDefinitionSendAbstractMessageReflectFailedWithRetry() {
let expectedReceiver = "ECHOECHO"
let serverConnectorMock = ServerConnectorMock(connectionState: .loggedIn)
let frameworkInjectorMock = BusinessInjectorMock(
entityManager: testDatabase.backgroundEntityManager,
serverConnector: serverConnectorMock
)
// 2 tests with: retry / sendMessageCalls / retryCount
for test in [[false, 0, 0] as [Any], [true, 0, 1]] {
// Reset mocks first
ddLoggerMock.logMessages.removeAll()
serverConnectorMock.sendMessageCalls.removeAll()
let tq = TaskQueue(
frameworkInjectorResolver: FrameworkInjectorResolverMock(frameworkInjector: frameworkInjectorMock)
)
let message = ContactDeletePhotoMessage()
message.fromIdentity = "TESTER01"
message.toIdentity = expectedReceiver
let expec = expectation(description: "spool")
let task = TaskDefinitionSendAbstractMessage(message: message, type: .volatile)
task.retry = test[0] as! Bool
try? tq.enqueue(task: task) { _, error in
XCTAssertNotNil(error)
DDLog.flushLog()
expec.fulfill()
}
tq.spool()
waitForExpectations(timeout: 6) { error in
XCTAssertNil(error)
XCTAssertEqual(tq.list.count, 1, "Task must not removed from outgoing queue, if it failed")
XCTAssertEqual(serverConnectorMock.sendMessageCalls.count, test[1] as! Int)
XCTAssertEqual(task.retryCount, test[2] as! Int)
XCTAssertTrue(
self.ddLoggerMock
.exists(
message: "<TaskDefinitionSendAbstractMessage (type: contactDeleteProfilePicture; id: \(message.messageID.hexString))> failed sendMessageFailed(message: \"Contact not found for identity \(expectedReceiver) ((type: contactDeleteProfilePicture; id: \(message.messageID.hexString)))\")"
)
)
if task.retry {
XCTAssertTrue(
self.ddLoggerMock
.exists(
message: "Retry of <TaskDefinitionSendAbstractMessage (type: contactDeleteProfilePicture; id: \(message.messageID.hexString))> after execution failing"
)
)
}
else {
XCTAssertFalse(
self.ddLoggerMock
.exists(
message: "Retry of <TaskDefinitionSendAbstractMessage> after execution failing"
)
)
}
}
}
}
func testDiscardMultiDeviceNotActivated() throws {
let serverConnectorMock = ServerConnectorMock(connectionState: .loggedIn)
let frameworkInjectorMock = BusinessInjectorMock(
entityManager: testDatabase.backgroundEntityManager,
serverConnector: serverConnectorMock
)
let tq = TaskQueue(
frameworkInjectorResolver: FrameworkInjectorResolverMock(frameworkInjector: frameworkInjectorMock)
)
let expec = expectation(description: "discard")
let task = TaskDefinitionUpdateContactSync(deltaSyncContacts: [])
try tq.enqueue(task: task) { _, error in
XCTAssertNil(error)
expec.fulfill()
}
tq.spool()
waitForExpectations(timeout: 6) { error in
XCTAssertNil(error)
XCTAssertEqual(tq.list.count, 0)
XCTAssertEqual(task.retryCount, 0)
XCTAssertTrue(self.ddLoggerMock.exists(
message: "<TaskDefinitionUpdateContactSync> \(TaskExecutionError.multiDeviceNotRegistered)"
))
XCTAssertTrue(self.ddLoggerMock.exists(message: "<TaskDefinitionUpdateContactSync> done"))
XCTAssertTrue(self.ddLoggerMock.exists(message: "<TaskDefinitionUpdateContactSync> removed from queue"))
}
}
func testSpoolTaskDefinitionMdmParameterSyncReflectFailedWithRetry() {
let serverConnectorMock = ServerConnectorMock(
connectionState: .loggedIn,
deviceID: MockMultiDevice.deviceID,
deviceGroupKeys: MockMultiDevice.deviceGroupKeys
)
serverConnectorMock.reflectMessageClosure = { _ in
ThreemaError.threemaError("Not logged in", withCode: ThreemaProtocolError.notLoggedIn.rawValue) as? NSError
}
let frameworkInjectorMock = BusinessInjectorMock(
entityManager: testDatabase.backgroundEntityManager,
userSettings: UserSettingsMock(enableMultiDevice: true),
serverConnector: serverConnectorMock
)
let testCases: [(retry: Bool, reflectMessageCalls: Int, retryCount: Int)] = [
(false, 1, 0),
(true, 2, 1),
]
for testCase in testCases {
// Reset mocks first
ddLoggerMock.logMessages.removeAll()
serverConnectorMock.reflectMessageCalls.removeAll()
let tq = TaskQueue(
frameworkInjectorResolver: FrameworkInjectorResolverMock(frameworkInjector: frameworkInjectorMock)
)
let expect = expectation(description: "spool")
let task = TaskDefinitionMdmParameterSync(mdmParameters: D2dSync_MdmParameters())
task.retry = testCase.retry
try? tq.enqueue(task: task) { _, error in
XCTAssertNotNil(error)
DDLog.flushLog()
expect.fulfill()
}
tq.spool()
waitForExpectations(timeout: 10) { error in
XCTAssertNil(error)
XCTAssertEqual(tq.list.count, 1)
XCTAssertEqual(serverConnectorMock.reflectMessageCalls.count, testCase.reflectMessageCalls)
XCTAssertEqual(task.retryCount, testCase.retryCount)
for m in self.ddLoggerMock.logMessages {
print(m.message)
}
XCTAssertTrue(
self.ddLoggerMock
.exists(
message: "<TaskDefinitionMdmParameterSync> failed reflectMessageFailed(message: Optional(\"message type: lock / Error Domain=ThreemaErrorDomain Code=675 \\\"Not logged in\\\" UserInfo={NSLocalizedDescription=Not logged in}\"))"
)
)
if task.retry {
XCTAssertTrue(
self.ddLoggerMock
.exists(message: "Retry of <TaskDefinitionMdmParameterSync> after execution failing")
)
}
else {
XCTAssertFalse(
self.ddLoggerMock
.exists(message: "Retry of task <TaskDefinitionMdmParameterSync> after execution failing")
)
}
}
}
}
func testSpoolChatServerMessageWithErrors() throws {
let tests: [(
expectedMessageProcessorErrors: [Error],
expectedSendMessage: Bool,
expectedLogMessages: [String],
expectedAck: Bool,
expectedTaskDequeued: Bool
)] = [
(
[Error](),
true,
[
"<TaskDefinitionReceiveMessage (type: BoxedMessage; id: %@)> done",
"<TaskDefinitionReceiveMessage (type: BoxedMessage; id: %@)> removed from queue",
],
true,
true
),
(
[
TaskExecutionError.wrongTaskDefinitionType,
],
false,
["<TaskDefinitionReceiveMessage (type: BoxedMessage; id: %@)> failed %@"],
false,
true
),
(
[
ThreemaProtocolError.badMessage,
ThreemaProtocolError.blockUnknownContact,
ThreemaProtocolError.messageAlreadyProcessed,
ThreemaProtocolError.messageBlobDecryptionFailed,
ThreemaProtocolError.messageNonceReuse,
ThreemaProtocolError.messageSenderMismatch,
ThreemaProtocolError.messageToDeleteNotFound,
ThreemaProtocolError.messageToEditNotFound,
ThreemaProtocolError.unknownMessageType,
ThreemaError.threemaError(
"Message already processed",
withCode: ThreemaProtocolError.messageAlreadyProcessed.rawValue
),
],
true,
[
"<TaskDefinitionReceiveMessage (type: BoxedMessage; id: %@)> discard incoming message: %@",
"<TaskDefinitionReceiveMessage (type: BoxedMessage; id: %@)> done",
"<TaskDefinitionReceiveMessage (type: BoxedMessage; id: %@)> removed from queue",
],
true,
true
),
(
[
MediatorReflectedProcessorError.doNotAckIncomingVoIPMessage,
],
true,
[
"<TaskDefinitionReceiveMessage (type: BoxedMessage; id: %@)> %@",
"<TaskDefinitionReceiveMessage (type: BoxedMessage; id: %@)> done",
"<TaskDefinitionReceiveMessage (type: BoxedMessage; id: %@)> removed from queue",
],
false,
true
),
(
[
TaskExecutionError.conversationNotFound(for: TaskDefinition(type: .volatile)),
],
true,
[
"<TaskDefinitionReceiveMessage (type: BoxedMessage; id: %@)> failed: %@",
"<TaskDefinitionReceiveMessage (type: BoxedMessage; id: %@)> done",
"<TaskDefinitionReceiveMessage (type: BoxedMessage; id: %@)> removed from queue",
],
false,
true
),
(
[
TaskExecutionError.createAbstractMessageFailed,
TaskExecutionError.messageReceiverBlockedOrUnknown,
],
true,
[
"<TaskDefinitionReceiveMessage (type: BoxedMessage; id: %@)> outgoing message failed: %@",
"<TaskDefinitionReceiveMessage (type: BoxedMessage; id: %@)> done",
"<TaskDefinitionReceiveMessage (type: BoxedMessage; id: %@)> removed from queue",
],
false,
true
),
(
[
TaskExecutionError.taskDropped,
],
false,
[
"<TaskDefinitionReceiveMessage (type: BoxedMessage; id: %@)> dropped",
"<TaskDefinitionReceiveMessage (type: BoxedMessage; id: %@)> removed from queue",
],
false,
true
),
]
for test in tests {
if test.expectedMessageProcessorErrors.isEmpty {
spoolChatServerMessageWithError(
nil,
test.expectedSendMessage,
test.expectedLogMessages,
test.expectedAck,
test.expectedTaskDequeued
)
}
else {
for expectedMessageProcessorError in test.expectedMessageProcessorErrors {
spoolChatServerMessageWithError(
expectedMessageProcessorError,
test.expectedSendMessage,
test.expectedLogMessages,
test.expectedAck,
test.expectedTaskDequeued
)
}
}
}
}
func spoolChatServerMessageWithError(
_ expectedMessageProcessorError: Error?,
_ expectedSendMessage: Bool,
_ expectedLogMessages: [String],
_ expectedAck: Bool,
_ expectedTaskDequeue: Bool
) {
let incomingBoxedMessage = BoxedMessage()
incomingBoxedMessage.messageID = BytesUtility.generateMessageID()
incomingBoxedMessage.fromIdentity = MyIdentityStoreMock().identity
incomingBoxedMessage.toIdentity = "ECHECHO"
incomingBoxedMessage.nonce = BytesUtility.generateMessageNonce()
let serverConnectorMock = ServerConnectorMock(
connectionState: .loggedIn,
deviceID: MockMultiDevice.deviceID,
deviceGroupKeys: MockMultiDevice.deviceGroupKeys
)
serverConnectorMock.sendMessageClosure = { _ in
expectedSendMessage
}
let nonceGuardMock = NonceGuardMock()
let messageProcessorMock = MessageProcessorMock()
messageProcessorMock.error = expectedMessageProcessorError
let frameworkInjectorMock = BusinessInjectorMock(
entityManager: testDatabase.backgroundEntityManager,
userSettings: UserSettingsMock(enableMultiDevice: true),
serverConnector: serverConnectorMock,
messageProcessor: messageProcessorMock,
nonceGuard: nonceGuardMock
)
// Reset mocks first
ddLoggerMock.logMessages.removeAll()
serverConnectorMock.reflectMessageCalls.removeAll()
let taskQueue = TaskQueue(
frameworkInjectorResolver: FrameworkInjectorResolverMock(frameworkInjector: frameworkInjectorMock)
)
let expect = expectation(description: "spool")
let task = TaskDefinitionReceiveMessage(
message: incomingBoxedMessage,
receivedAfterInitialQueueSend: true,
maxBytesToDecrypt: 0,
timeoutDownloadThumbnail: 0
)
try? taskQueue.enqueue(task: task) { _, error in
if expectedSendMessage {
XCTAssertNil(error)
}
DDLog.flushLog()
expect.fulfill()
}
taskQueue.spool()
waitForExpectations(timeout: 10) { error in
XCTAssertNil(error)
XCTAssertEqual(taskQueue.list.count, expectedTaskDequeue ? 0 : 1)
XCTAssertEqual(
nonceGuardMock.processedCalls.count,
expectedMessageProcessorError != nil && expectedAck ? 1 : 0
)
XCTAssertEqual(serverConnectorMock.completedProcessingMessageCalls.count, expectedAck ? 1 : 0)
XCTAssertEqual(serverConnectorMock.reflectMessageCalls.count, 0)
for msg in self.ddLoggerMock.logMessages {
print(msg.message)
}
for expectedLogMessage in expectedLogMessages {
if let expectedMessageProcessorError {
print(String(
format: expectedLogMessage,
incomingBoxedMessage.messageID.hexString,
"\(expectedMessageProcessorError)"
))
XCTAssertTrue(
self.ddLoggerMock
.starts(with: String(
format: expectedLogMessage,
incomingBoxedMessage.messageID.hexString,
"\(expectedMessageProcessorError)"
))
)
}
else {
print(String(
format: expectedLogMessage,
incomingBoxedMessage.messageID.hexString
))
XCTAssertTrue(
self.ddLoggerMock
.starts(with: String(
format: expectedLogMessage,
incomingBoxedMessage.messageID.hexString
))
)
}
}
}
}
func testSpoolMediatorMessageWithErrors() throws {
let testCases: [(
expectedTestCase: Int,
expectedMediatorReflectedProcessorErrors: [Error],
expectedServerConnectorError: Error?,
expectedLogMessages: [String],
expectedAck: Bool,
expectedNonceProcessed: Bool,
expectedTaskDequeued: Bool
)] = [
(
0,
[Error](),
nil,
[
"<TaskDefinitionReceiveReflectedMessage> done",
"<TaskDefinitionReceiveReflectedMessage> removed from queue",
],
true,
false, // In this case message nonce already stored
true
),
(
1,
[Error](),
ThreemaError.threemaError(
"Not logged in",
withCode: ThreemaProtocolError.notLoggedIn.rawValue
) as? NSError,
[
"<TaskDefinitionReceiveReflectedMessage> failed Error Domain=ThreemaErrorDomain Code=675 \"Not logged in\" UserInfo={NSLocalizedDescription=Not logged in}",
"Waiting 0.5 seconds before execute next task",
"<TaskDefinitionReceiveReflectedMessage> failed",
],
true,
false, // In this case message nonce already stored
true
),
(
2,
[
TaskExecutionError.wrongTaskDefinitionType,
],
nil,
[
"<TaskDefinitionReceiveReflectedMessage> discard reflected message (type: text; id: %@) with error: %@",
"<TaskDefinitionReceiveReflectedMessage> done",
"<TaskDefinitionReceiveReflectedMessage> removed from queue",
],
true,
true,
true
),
(
3,
[
ThreemaProtocolError.badMessage,
ThreemaProtocolError.blockUnknownContact,
ThreemaProtocolError.messageAlreadyProcessed,
ThreemaProtocolError.messageBlobDecryptionFailed,
ThreemaProtocolError.messageNonceReuse,
ThreemaProtocolError.unknownMessageType,
ThreemaError.threemaError(
"Message already processed",
withCode: ThreemaProtocolError.messageAlreadyProcessed.rawValue
),
],
nil,
[
"<TaskDefinitionReceiveReflectedMessage> discard reflected message (type: text; id: %@) with error: %@",
"<TaskDefinitionReceiveReflectedMessage> done",
"<TaskDefinitionReceiveReflectedMessage> removed from queue",
],
true,
true,
true
),
(
4,
[
ThreemaProtocolError.badMessage,
ThreemaProtocolError.blockUnknownContact,
ThreemaProtocolError.messageAlreadyProcessed,
ThreemaProtocolError.messageBlobDecryptionFailed,
ThreemaProtocolError.messageNonceReuse,
ThreemaProtocolError.unknownMessageType,
ThreemaError.threemaError(
"Message already processed",
withCode: ThreemaProtocolError.messageAlreadyProcessed.rawValue
),
],
ThreemaError.threemaError(
"Not logged in",
withCode: ThreemaProtocolError.notLoggedIn.rawValue
) as? NSError,
[
"<TaskDefinitionReceiveReflectedMessage> discard reflected message (type: text; id: %@) with error: %@",
"<TaskDefinitionReceiveReflectedMessage> failed Error Domain=ThreemaErrorDomain Code=675 \"Not logged in\" UserInfo={NSLocalizedDescription=Not logged in}",
"Waiting 0.5 seconds before execute next task",
"<TaskDefinitionReceiveReflectedMessage> done",
],
true,
true,
true
),
(
5,
[
MediatorReflectedProcessorError.conversationNotFound(message: ""),
],
nil,
[
"<TaskDefinitionReceiveReflectedMessage> discard reflected message (type: text; id: %@) with error: %@",
"<TaskDefinitionReceiveReflectedMessage> done",
"<TaskDefinitionReceiveReflectedMessage> removed from queue",
],
true,
true,
true
),
(
6,
[
MediatorReflectedProcessorError.conversationNotFound(message: ""),
],
ThreemaError.threemaError(
"Not logged in",
withCode: ThreemaProtocolError.notLoggedIn.rawValue
) as? NSError,
[
"<TaskDefinitionReceiveReflectedMessage> discard reflected message (type: text; id: %@) with error: %@",
"<TaskDefinitionReceiveReflectedMessage> failed Error Domain=ThreemaErrorDomain Code=675 \"Not logged in\" UserInfo={NSLocalizedDescription=Not logged in}",
"Waiting 0.5 seconds before execute next task",
"<TaskDefinitionReceiveReflectedMessage> failed",
],
true,
true,
true
),
(
7,
[
MediatorReflectedProcessorError.conversationNotFound(message: ""),
],
ThreemaError.threemaError(
"Not connected to mediator",
withCode: ThreemaProtocolError.notConnectedToMediator.rawValue
) as? NSError,
[
"<TaskDefinitionReceiveReflectedMessage> discard reflected message (type: text; id: %@) with error: %@",
"<TaskDefinitionReceiveReflectedMessage> sending server ack of reflected message failed: Error Domain=ThreemaErrorDomain Code=676 \"Not connected to mediator\" UserInfo={NSLocalizedDescription=Not connected to mediator}",
"<TaskDefinitionReceiveReflectedMessage> failed",
"<TaskDefinitionReceiveReflectedMessage> removed from queue",
],
true,
true,
true
),
(
8,
[
MediatorReflectedProcessorError.doNotAckIncomingVoIPMessage,
],
nil,
[
"<TaskDefinitionReceiveReflectedMessage> %@",
"<TaskDefinitionReceiveReflectedMessage> done",
],
false,
false,
true
),
]
for testCase in testCases {
if testCase.expectedMediatorReflectedProcessorErrors.isEmpty {
try spoolMediatorMessageWithError(
testCase.expectedTestCase,
nil,
testCase.expectedServerConnectorError,
testCase.expectedLogMessages,
testCase.expectedAck,
testCase.expectedNonceProcessed,
testCase.expectedTaskDequeued
)
}
else {
for expectedMediatorReflectedProcessorError in testCase.expectedMediatorReflectedProcessorErrors {
try spoolMediatorMessageWithError(
testCase.expectedTestCase,
expectedMediatorReflectedProcessorError,
testCase.expectedServerConnectorError,
testCase.expectedLogMessages,
testCase.expectedAck,
testCase.expectedNonceProcessed,
testCase.expectedTaskDequeued
)
}
}
}
}
func spoolMediatorMessageWithError(
_ expectedTestCase: Int,
_ expectedMediatorReflectedProcessorError: Error?,
_ expectedServerConnectorError: Error?,
_ expectedLogMessages: [String],
_ expectedAck: Bool,
_ expectedNonceProcessed: Bool,
_ expectedTaskDequeued: Bool
) throws {
let assertTestCaseMessage = "Test case \(expectedTestCase)"
print("\(assertTestCaseMessage) start")
var incomingMessage = D2d_IncomingMessage()
incomingMessage.messageID = try BytesUtility.generateMessageID().littleEndian()
incomingMessage.nonce = BytesUtility.generateMessageNonce()
incomingMessage.type = .text
var incomingEnvelop = D2d_Envelope()
incomingEnvelop.incomingMessage = incomingMessage
let expectedReflectID = BytesUtility.generateReflectID()
var expectedReflectedMessage = Data(BytesUtility.padding([0x80], pad: 0x00, length: 4))
expectedReflectedMessage.append(Data(BytesUtility.padding([0x08], pad: 0x00, length: 4)))
expectedReflectedMessage.append(expectedReflectID)
try expectedReflectedMessage.append(incomingEnvelop.serializedData())
let deviceGroupKeys = MockMultiDevice.deviceGroupKeys
let serverConnectorMock = ServerConnectorMock(
connectionState: .loggedIn,
deviceID: MockMultiDevice.deviceID,
deviceGroupKeys: deviceGroupKeys
)
let maxSpoolingDelayAttempts = 1
var spoolingDelayAttempts = 0
serverConnectorMock.reflectMessageClosure = { _ in
if let error = expectedServerConnectorError as? NSError,
error.code == ThreemaProtocolError.notLoggedIn.rawValue {
guard spoolingDelayAttempts >= maxSpoolingDelayAttempts else {
spoolingDelayAttempts += 1
return expectedServerConnectorError as? NSError
}
return nil
}
return expectedServerConnectorError as? NSError
}
let mediatorMessageProtocolMock = MediatorMessageProtocolMock(
deviceGroupKeys: deviceGroupKeys,
returnValues: [
MediatorMessageProtocolMock
.ReflectData(id: expectedReflectID, message: BytesUtility.generateRandomBytes(length: 24)!),
]
)
let mediatorReflectedProcessorMock = MediatorReflectedProcessorMock()
mediatorReflectedProcessorMock.process = { _, _, _, _, _ -> Promise<Void> in
if let expectedMediatorReflectedProcessorError {
return Promise(error: expectedMediatorReflectedProcessorError)
}
return Promise()
}
let nonceGuardMock = NonceGuardMock()
let frameworkInjectorMock = BusinessInjectorMock(
entityManager: testDatabase.backgroundEntityManager,
userSettings: UserSettingsMock(enableMultiDevice: true),
serverConnector: serverConnectorMock,
mediatorMessageProtocol: mediatorMessageProtocolMock,
mediatorReflectedProcessor: mediatorReflectedProcessorMock,
nonceGuard: nonceGuardMock
)
// Reset mocks first
ddLoggerMock.logMessages.removeAll()
serverConnectorMock.reflectMessageCalls.removeAll()
let taskQueue = TaskQueue(
frameworkInjectorResolver: FrameworkInjectorResolverMock(frameworkInjector: frameworkInjectorMock)
)
let expect = expectation(description: "spool")
expect.assertForOverFulfill = expectedServerConnectorError == nil // Because of retry after error
let task = TaskDefinitionReceiveReflectedMessage(
reflectedMessage: expectedReflectedMessage,
receivedAfterInitialQueueSend: true,
maxBytesToDecrypt: 0,
timeoutDownloadThumbnail: 0
)
try? taskQueue.enqueue(task: task) { _, error in
if expectedServerConnectorError == nil {
XCTAssertNil(error, assertTestCaseMessage)
}
DDLog.flushLog()
expect.fulfill()
}
taskQueue.spool()
waitForExpectations(timeout: 10) { error in
XCTAssertNil(error, assertTestCaseMessage)
XCTAssertEqual(taskQueue.list.count, expectedTaskDequeued ? 0 : 1, assertTestCaseMessage)
XCTAssertEqual(nonceGuardMock.processedCalls.count, expectedNonceProcessed ? 1 : 0, assertTestCaseMessage)
XCTAssertEqual(serverConnectorMock.completedProcessingMessageCalls.count, 0, assertTestCaseMessage)
if expectedAck {
XCTAssertTrue(!serverConnectorMock.reflectMessageCalls.isEmpty, assertTestCaseMessage)
}
else {
XCTAssertTrue(serverConnectorMock.reflectMessageCalls.isEmpty, assertTestCaseMessage)
}
for msg in self.ddLoggerMock.logMessages {
print(msg.message)
}
for expectedLogMessage in expectedLogMessages {
if let expectedMediatorReflectedProcessorError {
let testLogMessage =
if expectedLogMessage.contains("; id:") {
String(
format: expectedLogMessage,
incomingMessage.messageID.littleEndianData.hexString,
"\(expectedMediatorReflectedProcessorError)"
)
}
else {
String(
format: expectedLogMessage,
"\(expectedMediatorReflectedProcessorError)"
)
}
print("Test -> \(testLogMessage)")
XCTAssertTrue(
self.ddLoggerMock
.starts(with: testLogMessage),
assertTestCaseMessage
)
}
else {
XCTAssertTrue(
self.ddLoggerMock.starts(with: expectedLogMessage),
assertTestCaseMessage
)
}
}
}
print("\(assertTestCaseMessage) end")
}
func testEncodeDecodeWithAllTaskTypes() throws {
let expectedContactEntity = dbPreparer.save {
let expectedContactEntity = dbPreparer.createContact(
publicKey: BytesUtility.generatePublicKey(),
identity: "ECHOECHO"
)
dbPreparer.createConversation(
typing: false,
unreadMessageCount: 0,
visibility: .default
) { conversation in
conversation.contact = expectedContactEntity
}
return expectedContactEntity
}
let (_, groupEntity, conversation) = dbPreparer.createGroup(
groupID: BytesUtility.generateGroupID(),
groupCreatorIdentity: "ADMIN007",
members: ["MEMBER01", "MEMBER02", "MEMBER03"]
)
let expectedGroup = Group(
myIdentityStore: MyIdentityStoreMock(),
userSettings: UserSettingsMock(),
pushSettingManager: PushSettingManagerMock(),
groupEntity: groupEntity,
conversation: conversation,
lastSyncRequest: nil
)
let expectedFromMember = "MEMBER01"
let expectedToMembers = ["MEMBER02", "MEMBER03"]
let frameworkInjectorMock = BusinessInjectorMock(entityManager: testDatabase.backgroundEntityManager)
let tq = TaskQueue(
frameworkInjectorResolver: FrameworkInjectorResolverMock(frameworkInjector: frameworkInjectorMock)
)
// Add TaskDefinitionGroupDissolve
let expectedGroupDissolveToMember = "MEMBER07"
let taskGroupDissolve = TaskDefinitionGroupDissolve(group: expectedGroup)
taskGroupDissolve.toMembers = [expectedGroupDissolveToMember]
try! tq.enqueue(task: taskGroupDissolve, completionHandler: nil)
// Add TaskDefinitionSendAbstractMessage
let expectedAbstractMessageID = BytesUtility.generateMessageID()
let expectedAbstractBallotCreator = "CONTACT1"
let expectedAbstractBallotID = BytesUtility.generateBallotID()
let expectedAbstractBallotJSONChoiceData = Data(repeating: 0x12, count: 10)
let expectedAbstractMessage = BoxBallotVoteMessage()
expectedAbstractMessage.messageID = expectedAbstractMessageID
expectedAbstractMessage.ballotCreator = expectedAbstractBallotCreator
expectedAbstractMessage.ballotID = expectedAbstractBallotID
expectedAbstractMessage.jsonChoiceData = expectedAbstractBallotJSONChoiceData
expectedAbstractMessage.toIdentity = "ECHOECHO"
let taskAbstract = TaskDefinitionSendAbstractMessage(message: expectedAbstractMessage)
try! tq.enqueue(task: taskAbstract, completionHandler: nil)
// Add TaskDefinitionSendBallotVoteMessage
let expectedBallotVoteBallotID = BytesUtility.generateBallotID()
let taskBallotVote = TaskDefinitionSendBallotVoteMessage(
ballotID: expectedBallotVoteBallotID,
receiverIdentity: expectedContactEntity.identity,
group: nil,
sendContactProfilePicture: false
)
try! tq.enqueue(task: taskBallotVote, completionHandler: nil)
// Add TaskDefinitionSendBaseMessage
let expectedBaseMessageGroupName = "Test group name"
let expectedBaseMessageIsNoteGroup = false
let expectedBaseMessageID = BytesUtility.generateMessageID()
let taskBase = TaskDefinitionSendBaseMessage(
messageID: expectedBaseMessageID,
group: expectedGroup,
receivers: expectedGroup.members.map(\.identity),
sendContactProfilePicture: false
)
taskBase.groupName = expectedBaseMessageGroupName
taskBase.isNoteGroup = expectedBaseMessageIsNoteGroup
try! tq.enqueue(task: taskBase, completionHandler: nil)
// Add TaskDefinitionSendDeleteEditMessage
let expectedReceiverIdentity = ThreemaIdentity("ECHOECHO")
let expectedDeleteMessage = try CspE2e_DeleteMessage.with { message in
message.messageID = try BytesUtility.generateMessageID().littleEndian()
}
let taskDeleteEditMessage = TaskDefinitionSendDeleteEditMessage(
receiverIdentity: expectedReceiverIdentity,
group: nil,
deleteMessage: expectedDeleteMessage
)
try! tq.enqueue(task: taskDeleteEditMessage, completionHandler: nil)
// Add TaskDefinitionSendDeliveryReceiptsMessage
let expectedReceiptFromIdentity = "CONTACT1"
let expectedReceiptToIdentity = "CONTACT2"
let expectedReceiptType: ReceiptType = .read
let expectedReceiptMessageIDs = [
BytesUtility.generateMessageID(),
BytesUtility.generateMessageID(),
]
let expectedReceiptReadDates = [
Date(),
Date(),
]
let taskDeliveryReceipt = TaskDefinitionSendDeliveryReceiptsMessage(
fromIdentity: expectedReceiptFromIdentity,
toIdentity: expectedReceiptToIdentity,
receiptType: expectedReceiptType,
receiptMessageIDs: expectedReceiptMessageIDs,
receiptReadDates: expectedReceiptReadDates,
excludeFromSending: [Data]()
)
try! tq.enqueue(task: taskDeliveryReceipt, completionHandler: nil)
// Add TaskDefinitionSendLocationMessage
let expectedLocationMessageID = BytesUtility.generateMessageID()
let expectedLocationPoiAddress = "poi address"
let taskLocation = TaskDefinitionSendLocationMessage(
poiAddress: expectedLocationPoiAddress,
messageID: expectedLocationMessageID,
receiverIdentity: expectedContactEntity.identity
)
try! tq.enqueue(task: taskLocation, completionHandler: nil)
// Add TaskDefinitionSendGroupCreateMessage
let expectedGroupCreateMessageMembers = ["TESTER01", "TESTER03"]
let taskGroupCreateMessage = TaskDefinitionSendGroupCreateMessage(
group: expectedGroup,
to: expectedGroupCreateMessageMembers,
members: Set(expectedGroupCreateMessageMembers),
sendContactProfilePicture: false
)
try! tq.enqueue(task: taskGroupCreateMessage, completionHandler: nil)
// Add TaskDefinitionSendGroupLeaveMessage
let taskGroupLeaveMessage = TaskDefinitionSendGroupLeaveMessage(sendContactProfilePicture: false)
taskGroupLeaveMessage.fromMember = expectedFromMember
taskGroupLeaveMessage.toMembers = expectedToMembers
try! tq.enqueue(task: taskGroupLeaveMessage, completionHandler: nil)
// Add TaskDefinitionSendGroupRenameMessage
let expectedGroupRenameName = "group name 123!!!"
let taskGroupRenameMessage = TaskDefinitionSendGroupRenameMessage(
group: expectedGroup,
from: expectedFromMember,
to: expectedToMembers,
newName: expectedGroupRenameName,
sendContactProfilePicture: false
)
try! tq.enqueue(task: taskGroupRenameMessage, completionHandler: nil)
// Add TaskDefinitionSendGroupSetPhotoMessage
let expectedGroupSetPhotoSize: UInt32 = 10
let expectedGroupSetPhotoBlobID = BytesUtility.generateBlobID()
let expectedGroupSetPhotoEncryptionKey = BytesUtility.generateBlobEncryptionKey()
let taskGroupSetPhoto = TaskDefinitionSendGroupSetPhotoMessage(
group: expectedGroup,
from: expectedFromMember,
to: expectedToMembers,
size: expectedGroupSetPhotoSize,
blobID: expectedGroupSetPhotoBlobID,
encryptionKey: expectedGroupSetPhotoEncryptionKey,
sendContactProfilePicture: false
)
try! tq.enqueue(task: taskGroupSetPhoto, completionHandler: nil)
// Add TaskDefinitionSendGroupDeletePhotoMessage
let taskGroupDeletePhoto = TaskDefinitionSendGroupDeletePhotoMessage(
group: expectedGroup,
from: expectedFromMember,
to: expectedToMembers,
sendContactProfilePicture: false
)
try! tq.enqueue(task: taskGroupDeletePhoto, completionHandler: nil)
// Add TaskDefinitionSendGroupDeliveryReceiptMessage
let expectedGroupReceiptType: ReceiptType = .ack
let expectedGroupReceiptMessageIDs = [
BytesUtility.generateMessageID(),
BytesUtility.generateMessageID(),
]
let taskGroupDeliveryReceipt = TaskDefinitionSendGroupDeliveryReceiptsMessage(
group: expectedGroup,
from: expectedFromMember,
to: expectedToMembers,
receiptType: expectedGroupReceiptType,
receiptMessageIDs: expectedGroupReceiptMessageIDs,
receiptReadDates: [nil, .now]
)
try! tq.enqueue(task: taskGroupDeliveryReceipt, completionHandler: nil)
// Add TaskDefinitionProfileSync
var syncUserProfile = D2dSync_UserProfile()
syncUserProfile.profilePicture.updated = Common_Image()
syncUserProfile.nickname = "Test Case"
var contacts = Common_Identities()
contacts.identities = ["ECHOECHO", "*SUPPORT"]
syncUserProfile.profilePictureShareWith.policy = .allowList(contacts)
var linkPhoneNumber = D2dSync_UserProfile.IdentityLinks.IdentityLink()
linkPhoneNumber.phoneNumber = "+41 000 00 00"
syncUserProfile.identityLinks.links.append(linkPhoneNumber)
var linkEmail = D2dSync_UserProfile.IdentityLinks.IdentityLink()
linkEmail.email = "test@test.test"
syncUserProfile.identityLinks.links.append(linkEmail)
let profileImage = BytesUtility.generateRandomBytes(length: 512)!
let taskProfileSync = TaskDefinitionProfileSync(
syncUserProfile: syncUserProfile,
profileImage: profileImage,
linkMobileNoPending: false,
linkEmailPending: false
)
try! tq.enqueue(task: taskProfileSync, completionHandler: nil)
// Add TaskDefinitionUpdateContactSync
var syncContact1 = D2dSync_Contact()
syncContact1.identity = "ECHOECHO"
let contact1 = DeltaSyncContact(syncContact: syncContact1, syncAction: .update)
var syncContact2 = D2dSync_Contact()
syncContact2.identity = "ECHOECHO"
var contact2 = DeltaSyncContact(syncContact: syncContact1, syncAction: .update)
contact2.image = Data([1])
let updateableContacts = [contact1, contact2, contact1]
let taskUpdateContactSync = TaskDefinitionUpdateContactSync(deltaSyncContacts: updateableContacts)
try! tq.enqueue(task: taskUpdateContactSync, completionHandler: nil)
// Add TaskDefinitionSettingsSync
var syncSettings = D2dSync_Settings()
syncSettings.contactSyncPolicy = .sync
syncSettings.readReceiptPolicy = .sendReadReceipt
syncSettings.unknownContactPolicy = .blockUnknown
syncSettings.typingIndicatorPolicy = .sendTypingIndicator
syncSettings.o2OCallPolicy = .allowO2OCall
syncSettings.o2OCallConnectionPolicy = .requireRelayedConnection
syncSettings.blockedIdentities.identities = ["ECHOECHO"]
syncSettings.excludeFromSyncIdentities.identities = ["ECHOECHO"]
let taskSettingsSync = TaskDefinitionSettingsSync(syncSettings: syncSettings)
try! tq.enqueue(task: taskSettingsSync, completionHandler: nil)
// Add TaskDefinitionReceiveMessage
let taskReceiveMessage = TaskDefinitionReceiveMessage(
message: BoxedMessage(),
receivedAfterInitialQueueSend: true,
maxBytesToDecrypt: 0,
timeoutDownloadThumbnail: 0
)
try! tq.enqueue(task: taskReceiveMessage, completionHandler: nil)
// Add TaskDefinitionReceiveReflectedMessage
let taskReceiveReflectedMessage = TaskDefinitionReceiveReflectedMessage(
reflectedMessage: BytesUtility.generateRandomBytes(length: 16)!,
receivedAfterInitialQueueSend: false,
maxBytesToDecrypt: 20,
timeoutDownloadThumbnail: 30
)
try! tq.enqueue(task: taskReceiveReflectedMessage, completionHandler: nil)
// Add TaskDefinitionRunForwardSecurityRefreshSteps
let taskRunForwardSecurityRefreshSteps = TaskDefinitionRunForwardSecurityRefreshSteps(
with: expectedToMembers
.map { ThreemaIdentity($0) }
)
try! tq.enqueue(task: taskRunForwardSecurityRefreshSteps, completionHandler: nil)
// Add TaskDefinitionNewDeviceSync
let taskNewDeviceSync = TaskDefinitionNewDeviceSync { _ in
XCTFail("This should never be called...")
}
try! tq.enqueue(task: taskNewDeviceSync, completionHandler: nil)
// Add TaskDefinitionSendReactionMessage
var expectedReaction = CspE2e_Reaction()
expectedReaction.messageID = try BytesUtility.generateMessageID().littleEndian()
expectedReaction.action = .apply(Data("✌".utf8))
let expectedReactionReceiver = "ECHOECHO"
let taskReactionMessage = TaskDefinitionSendReactionMessage(
reaction: expectedReaction,
receiverIdentity: expectedReactionReceiver
)
try! tq.enqueue(task: taskReactionMessage, completionHandler: nil)
// Add TaskDefinitionSendCallOfferAnswerMessage
let abstractMessage = BoxVoIPCallAnswerMessage()
let taskCallOfferAnswer = TaskDefinitionSendCallOfferAnswerMessage(
message: abstractMessage,
callID: VoIPCallID(callID: UInt32(1337))
)
try! tq.enqueue(task: taskCallOfferAnswer, completionHandler: nil)
let expectedItemCount = 22
guard tq.list.count == expectedItemCount else {
XCTFail("TaskList has wrong number of items. Expected \(expectedItemCount) but was \(tq.list.count)")
return
}
// Check none-persistent tasks
if let task = tq.list[13].taskDefinition as? TaskDefinitionProfileSync {
XCTAssertEqual(TaskExecutionState.pending, task.state)
XCTAssertEqual(task.scope, .userProfileSync)
XCTAssertEqual(task.profileImage, profileImage)
XCTAssertEqual(task.syncUserProfile.nickname, syncUserProfile.nickname)
XCTAssertEqual(task.syncUserProfile.profilePictureShareWith.policy, .allowList(contacts))
XCTAssertEqual(task.syncUserProfile.identityLinks.links, syncUserProfile.identityLinks.links)
XCTAssertEqual(task.linkMobileNoPending, false)
XCTAssertEqual(task.linkEmailPending, false)
XCTAssertTrue(task.retry)
}
else {
XCTFail()
}
if let task = tq.list[15].taskDefinition as? TaskDefinitionSettingsSync {
XCTAssertEqual(TaskExecutionState.pending, task.state)
XCTAssertEqual(task.scope, .settingsSync)
XCTAssertEqual(task.syncSettings.contactSyncPolicy, .sync)
XCTAssertEqual(task.syncSettings.readReceiptPolicy, .sendReadReceipt)
XCTAssertEqual(task.syncSettings.unknownContactPolicy, .blockUnknown)
XCTAssertEqual(task.syncSettings.typingIndicatorPolicy, .sendTypingIndicator)
XCTAssertEqual(task.syncSettings.o2OCallPolicy, .allowO2OCall)
XCTAssertEqual(task.syncSettings.o2OCallConnectionPolicy, .requireRelayedConnection)
XCTAssertEqual(task.syncSettings.blockedIdentities.identities, ["ECHOECHO"])
XCTAssertEqual(task.syncSettings.excludeFromSyncIdentities.identities, ["ECHOECHO"])
XCTAssertTrue(task.retry)
}
else {
XCTFail()
}
if let task = tq.list[16].taskDefinition as? TaskDefinitionReceiveMessage {
XCTAssertEqual(TaskExecutionState.pending, task.state)
XCTAssertNotNil(task.message)
XCTAssertEqual(task.receivedAfterInitialQueueSend, true)
XCTAssertEqual(task.maxBytesToDecrypt, 0)
XCTAssertEqual(task.timeoutDownloadThumbnail, 0)
XCTAssertFalse(task.retry)
}
else {
XCTFail()
}
if let task = tq.list[17].taskDefinition as? TaskDefinitionReceiveReflectedMessage {
XCTAssertEqual(TaskExecutionState.pending, task.state)
XCTAssertNotNil(task.reflectedMessage)
XCTAssertEqual(task.receivedAfterInitialQueueSend, false)
XCTAssertEqual(task.maxBytesToDecrypt, 20)
XCTAssertEqual(task.timeoutDownloadThumbnail, 30)
XCTAssertFalse(task.retry)
}
else {
XCTFail()
}
if !(tq.list[19].taskDefinition is TaskDefinitionNewDeviceSync) {
XCTFail()
}
if let task = tq.list[21].taskDefinition as? TaskDefinitionSendCallOfferAnswerMessage {
XCTAssertEqual(TaskExecutionState.pending, task.state)
XCTAssertEqual(task.scope, .o2OCall)
XCTAssertEqual(task.callID, VoIPCallID(callID: UInt32(1337)))
XCTAssertTrue(task.retry)
}
else {
XCTFail()
}
// Encode queue
guard let data = tq.encode() else {
XCTFail("Could not encode queue")
return
}
// Remove all items
tq.removeAll()
// Decode queue
tq.decode(data)
// Check persistent tasks
let expectedItemCountAfterDecode = 16
guard tq.list.count == expectedItemCountAfterDecode else {
XCTFail(
"TaskList has wrong number of items. Expected \(expectedItemCountAfterDecode) but was \(tq.list.count)"
)
return
}
if let task = tq.list[0].taskDefinition as? TaskDefinitionGroupDissolve {
XCTAssertNil(task.receiverIdentity)
XCTAssertEqual(expectedGroup.groupID, task.groupID)
XCTAssertEqual(expectedGroup.groupCreatorIdentity, task.groupCreatorIdentity)
XCTAssertEqual(1, task.toMembers.count)
XCTAssertTrue(task.toMembers.contains(expectedGroupDissolveToMember))
XCTAssertTrue(task.retry)
}
else {
XCTFail()
}
if let task = tq.list[1].taskDefinition as? TaskDefinitionSendAbstractMessage {
XCTAssertEqual(.interrupted, task.state)
XCTAssertTrue(expectedAbstractMessageID.elementsEqual(task.message.messageID))
XCTAssertEqual(expectedAbstractBallotCreator, (task.message as! BoxBallotVoteMessage).ballotCreator)
XCTAssertEqual(expectedAbstractBallotID, (task.message as! BoxBallotVoteMessage).ballotID)
XCTAssertEqual(expectedAbstractBallotJSONChoiceData, (task.message as! BoxBallotVoteMessage).jsonChoiceData)
XCTAssertTrue(task.retry)
}
else {
XCTFail()
}
if let task = tq.list[2].taskDefinition as? TaskDefinitionSendBallotVoteMessage {
XCTAssertEqual(expectedContactEntity.identity, task.receiverIdentity)
XCTAssertNil(task.groupID)
XCTAssertNil(task.groupCreatorIdentity)
XCTAssertEqual(.interrupted, task.state)
XCTAssertTrue(expectedBallotVoteBallotID.elementsEqual(task.ballotID))
XCTAssertTrue(task.retry)
}
else {
XCTFail()
}
if let task = tq.list[3].taskDefinition as? TaskDefinitionSendBaseMessage {
XCTAssertNil(task.receiverIdentity)
XCTAssertEqual(expectedGroup.groupID, task.groupID)
XCTAssertEqual(expectedGroup.groupCreatorIdentity, task.groupCreatorIdentity)
XCTAssertEqual(.interrupted, task.state)
XCTAssertEqual(expectedBaseMessageGroupName, task.groupName)
XCTAssertEqual(
Set(expectedGroup.members.map(\.identity.rawValue)),
try XCTUnwrap(task.receivingGroupMembers)
)
XCTAssertEqual(expectedBaseMessageIsNoteGroup, task.isNoteGroup)
XCTAssertTrue(expectedBaseMessageID.elementsEqual(task.messageID))
XCTAssertTrue(task.retry)
}
else {
XCTFail()
}
if let task = tq.list[4].taskDefinition as? TaskDefinitionSendDeleteEditMessage {
XCTAssertNotNil(task.receiverIdentity)
XCTAssertNil(task.groupID)
XCTAssertNil(task.groupCreatorIdentity)
XCTAssertEqual(task.deleteMessage, expectedDeleteMessage)
XCTAssertNil(task.editMessage)
}
else {
XCTFail()
}
if let task = tq.list[5].taskDefinition as? TaskDefinitionSendDeliveryReceiptsMessage {
XCTAssertNil(task.receiverIdentity)
XCTAssertNil(task.groupID)
XCTAssertNil(task.groupCreatorIdentity)
XCTAssertEqual(.interrupted, task.state)
XCTAssertEqual(expectedReceiptFromIdentity, task.fromIdentity)
XCTAssertEqual(expectedReceiptToIdentity, task.toIdentity)
XCTAssertEqual(expectedReceiptType, task.receiptType)
XCTAssertEqual(expectedReceiptMessageIDs, task.receiptMessageIDs)
XCTAssertEqual(expectedReceiptReadDates, task.receiptReadDates)
XCTAssertTrue(task.retry)
}
else {
XCTFail()
}
if let task = tq.list[6].taskDefinition as? TaskDefinitionSendLocationMessage {
XCTAssertEqual(expectedContactEntity.identity, task.receiverIdentity)
XCTAssertNil(task.groupID)
XCTAssertNil(task.groupCreatorIdentity)
XCTAssertEqual(.interrupted, task.state)
XCTAssertTrue(expectedLocationMessageID.elementsEqual(task.messageID))
XCTAssertEqual(expectedLocationPoiAddress, task.poiAddress)
XCTAssertNil(task.groupID)
XCTAssertTrue(task.retry)
}
else {
XCTFail()
}
if let task = tq.list[7].taskDefinition as? TaskDefinitionSendGroupCreateMessage {
XCTAssertNil(task.receiverIdentity)
XCTAssertEqual(expectedGroup.groupID, task.groupID)
XCTAssertEqual(expectedGroup.groupCreatorIdentity, task.groupCreatorIdentity)
XCTAssertEqual(.interrupted, task.state)
XCTAssertEqual(Set(expectedGroupCreateMessageMembers), task.members)
XCTAssertTrue(task.retry)
}
else {
XCTFail()
}
if let task = tq.list[8].taskDefinition as? TaskDefinitionSendGroupLeaveMessage {
XCTAssertNil(task.receiverIdentity)
XCTAssertNil(task.groupID)
XCTAssertNil(task.groupCreatorIdentity)
XCTAssertEqual(.interrupted, task.state)
XCTAssertEqual(expectedFromMember, task.fromMember)
XCTAssertEqual(expectedToMembers, task.toMembers)
XCTAssertEqual(0, task.hiddenContacts.count)
XCTAssertTrue(task.retry)
}
else {
XCTFail()
}
if let task = tq.list[9].taskDefinition as? TaskDefinitionSendGroupRenameMessage {
XCTAssertNil(task.receiverIdentity)
XCTAssertEqual(expectedGroup.groupID, task.groupID)
XCTAssertEqual(expectedGroup.groupCreatorIdentity, task.groupCreatorIdentity)
XCTAssertEqual(.interrupted, task.state)
XCTAssertEqual(expectedFromMember, task.fromMember)
XCTAssertEqual(expectedToMembers, task.toMembers)
XCTAssertEqual(expectedGroupRenameName, task.name)
XCTAssertTrue(task.retry)
}
else {
XCTFail()
}
if let task = tq.list[10].taskDefinition as? TaskDefinitionSendGroupSetPhotoMessage {
XCTAssertNil(task.receiverIdentity)
XCTAssertEqual(expectedGroup.groupID, task.groupID)
XCTAssertEqual(expectedGroup.groupCreatorIdentity, task.groupCreatorIdentity)
XCTAssertEqual(.interrupted, task.state)
XCTAssertEqual(expectedFromMember, task.fromMember)
XCTAssertEqual(expectedToMembers, task.toMembers)
XCTAssertEqual(expectedGroupSetPhotoSize, task.size)
XCTAssertTrue(expectedGroupSetPhotoBlobID.elementsEqual(taskGroupSetPhoto.blobID))
XCTAssertTrue(expectedGroupSetPhotoEncryptionKey.elementsEqual(taskGroupSetPhoto.encryptionKey))
XCTAssertTrue(task.retry)
}
else {
XCTFail()
}
if let task = tq.list[11].taskDefinition as? TaskDefinitionSendGroupDeletePhotoMessage {
XCTAssertNil(task.receiverIdentity)
XCTAssertEqual(expectedGroup.groupID, task.groupID)
XCTAssertEqual(expectedGroup.groupCreatorIdentity, task.groupCreatorIdentity)
XCTAssertEqual(.interrupted, task.state)
XCTAssertEqual(expectedFromMember, task.fromMember)
XCTAssertEqual(expectedToMembers, task.toMembers)
XCTAssertTrue(task.retry)
}
else {
XCTFail()
}
if let task = tq.list[12].taskDefinition as? TaskDefinitionSendGroupDeliveryReceiptsMessage {
XCTAssertNil(task.receiverIdentity)
XCTAssertEqual(expectedGroup.groupID, task.groupID)
XCTAssertEqual(expectedGroup.groupCreatorIdentity, task.groupCreatorIdentity)
XCTAssertEqual(.interrupted, task.state)
XCTAssertEqual(expectedFromMember, task.fromMember)
XCTAssertEqual(expectedToMembers, task.toMembers)
XCTAssertEqual(expectedGroupReceiptType, task.receiptType)
XCTAssertEqual(expectedGroupReceiptMessageIDs, task.receiptMessageIDs)
XCTAssertTrue(task.retry)
}
else {
XCTFail()
}
if let task = tq.list[13].taskDefinition as? TaskDefinitionUpdateContactSync {
XCTAssertEqual(.interrupted, task.state)
XCTAssertEqual(task.scope, .contactSync)
XCTAssertEqual(task.deltaSyncContacts.count, updateableContacts.count)
XCTAssertEqual(
task.deltaSyncContacts[0].syncContact.identity,
updateableContacts[0].syncContact.identity
)
XCTAssertEqual(
task.deltaSyncContacts[0].image,
updateableContacts[0].image
)
XCTAssertEqual(
task.deltaSyncContacts[1].syncContact.identity,
updateableContacts[1].syncContact.identity
)
XCTAssertEqual(
task.deltaSyncContacts[1].image,
updateableContacts[1].image
)
XCTAssertEqual(
task.deltaSyncContacts[2].syncContact.identity,
updateableContacts[2].syncContact.identity
)
XCTAssertEqual(
task.deltaSyncContacts[2].image,
updateableContacts[2].image
)
XCTAssertTrue(task.retry)
}
else {
XCTFail()
}
if let task = tq.list[14].taskDefinition as? TaskDefinitionRunForwardSecurityRefreshSteps {
XCTAssertEqual(.interrupted, task.state)
XCTAssertEqual(task.contactIdentities, expectedToMembers.map { ThreemaIdentity($0) })
XCTAssertTrue(task.retry)
}
else {
XCTFail()
}
if let task = tq.list[15].taskDefinition as? TaskDefinitionSendReactionMessage {
XCTAssertEqual(.interrupted, task.state)
XCTAssertEqual(task.reaction.messageID, expectedReaction.messageID)
XCTAssertEqual(task.reaction.action, expectedReaction.action)
XCTAssertTrue(task.retry)
}
else {
XCTFail()
}
}
}
// MARK: - Helper classes
private final class FrameworkInjectorResolverMock: FrameworkInjectorResolverProtocol {
init(frameworkInjector: BusinessInjectorMock) {
self.backgroundFrameworkInjector = frameworkInjector
}
private(set) var backgroundFrameworkInjector: FrameworkInjectorProtocol
}
private final class TaskDefinitionMock: TaskDefinition {
override func create(
frameworkInjector: FrameworkInjectorProtocol,
taskContext: TaskContextProtocol
) -> TaskExecutionProtocol {
TaskExecutionMock(
taskContext: taskContext,
taskDefinition: self,
backgroundFrameworkInjector: frameworkInjector
)
}
override func create(frameworkInjector: FrameworkInjectorProtocol) -> TaskExecutionProtocol {
create(frameworkInjector: frameworkInjector, taskContext: TaskContext())
}
override var description: String {
"<\(Swift.type(of: self))>"
}
let sleepInterval: TimeInterval
let customError: Error?
init(sleepInterval: TimeInterval, customError: Error? = nil) {
self.sleepInterval = sleepInterval
self.customError = customError
super.init(type: .dropOnDisconnect)
}
required init(from decoder: any Decoder) throws {
fatalError("init(from:) has not been implemented")
}
}
private final class TaskExecutionMock: TaskExecution, TaskExecutionProtocol {
func execute() -> PromiseKit.Promise<Void> {
guard let task = taskDefinition as? TaskDefinitionMock else {
return Promise(error: TaskExecutionError.wrongTaskDefinitionType)
}
return Promise { seal in
Task {
try await Task.sleep(seconds: task.sleepInterval)
if let customError = task.customError {
seal.reject(customError)
return
}
do {
try task.checkDropping()
seal.fulfill_()
}
catch {
seal.reject(error)
}
}
}
}
}
¤ Dauer der Verarbeitung: 0.30 Sekunden
¤
*© Formatika GbR, Deutschland
|
|