Signal-iOS/SignalServiceKit/tests/Messages/MessageSendLogTests.swift
2021-10-28 12:40:56 -07:00

548 lines
25 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
@testable import SignalServiceKit
import XCTest
import GRDB
class MessageSendLogTests: SSKBaseTestSwift {
override func setUp() {
super.setUp()
}
override func tearDown() {
super.tearDown()
}
func testStoreAndRetrieveValidPayload() {
databaseStorage.write { writeTx in
// Create and save the message payload
let newMessage = createOutgoingMessage(transaction: writeTx)
let payloadData = CommonGenerator.sentence.data(using: .utf8)!
let payloadIndex = MessageSendLog.recordPayload(payloadData, forMessageBeingSent: newMessage, transaction: writeTx) as! Int64
// "Send" the message to a recipient
let recipientAddress = CommonGenerator.address()
let deviceId = Int64.random(in: 0..<100)
MessageSendLog.recordPendingDelivery(
payloadId: payloadIndex,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: deviceId,
message: newMessage,
transaction: writeTx)
// Re-fetch the payload
let fetchedPayload = MessageSendLog.fetchPayload(
address: recipientAddress,
deviceId: deviceId,
timestamp: newMessage.timestamp,
transaction: writeTx)!
XCTAssertEqual(fetchedPayload.contentHint, .implicit)
XCTAssertEqual(fetchedPayload.plaintextContent, payloadData)
XCTAssertEqual(fetchedPayload.uniqueThreadId, newMessage.uniqueThreadId)
}
}
func testStoreAndRetrievePayloadForInvalidRecipient() {
databaseStorage.write { writeTx in
// Create and save the message payload
let newMessage = createOutgoingMessage(transaction: writeTx)
let payloadData = CommonGenerator.sentence.data(using: .utf8)!
let payloadIndex = MessageSendLog.recordPayload(payloadData, forMessageBeingSent: newMessage, transaction: writeTx) as! Int64
// "Send" the message to one recipient
let recipientAddress = CommonGenerator.address()
let deviceId = Int64.random(in: 0..<100)
MessageSendLog.recordPendingDelivery(
payloadId: payloadIndex,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: deviceId,
message: newMessage,
transaction: writeTx)
// Expect no results when re-fetching the payload with a different deviceId
XCTAssertNil(MessageSendLog.fetchPayload(
address: recipientAddress,
deviceId: deviceId+1,
timestamp: newMessage.timestamp,
transaction: writeTx))
// Expect no results when re-fetching the payload with a different address
XCTAssertNil(MessageSendLog.fetchPayload(
address: CommonGenerator.address(),
deviceId: deviceId,
timestamp: newMessage.timestamp,
transaction: writeTx))
}
}
func testStoreAndRetrievePayloadForDeliveredRecipient() {
databaseStorage.write { writeTx in
// Create and save the message payload
let newMessage = createOutgoingMessage(transaction: writeTx)
let payloadData = CommonGenerator.sentence.data(using: .utf8)!
let payloadIndex = MessageSendLog.recordPayload(payloadData, forMessageBeingSent: newMessage, transaction: writeTx) as! Int64
// "Send" the message to two devices
let recipientAddress = CommonGenerator.address()
for deviceId: Int64 in [0, 1] {
MessageSendLog.recordPendingDelivery(
payloadId: payloadIndex,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: deviceId,
message: newMessage,
transaction: writeTx)
}
// Mark the payload as "delivered" to the first device
MessageSendLog.recordSuccessfulDelivery(
timestamp: newMessage.timestamp,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: 0,
transaction: writeTx)
// Expect no results when re-fetching the payload for the first device
XCTAssertNil(MessageSendLog.fetchPayload(
address: recipientAddress,
deviceId: 0,
timestamp: newMessage.timestamp,
transaction: writeTx))
// Expect some results when re-fetching the payload for the second device
XCTAssertNotNil(MessageSendLog.fetchPayload(
address: recipientAddress,
deviceId: 1,
timestamp: newMessage.timestamp,
transaction: writeTx))
}
}
func testStoreAndRetrieveExpiredPayload() {
databaseStorage.write { writeTx in
// Create and save the message payload. Outgoing message date is long ago
let newMessage = createOutgoingMessage(date: Date(timeIntervalSince1970: 10000), transaction: writeTx)
let payloadData = CommonGenerator.sentence.data(using: .utf8)!
let payloadIndex = MessageSendLog.recordPayload(payloadData, forMessageBeingSent: newMessage, transaction: writeTx) as! Int64
// "Send" the message to a recipient
let recipientAddress = CommonGenerator.address()
let deviceId = Int64.random(in: 0..<100)
MessageSendLog.recordPendingDelivery(
payloadId: payloadIndex,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: deviceId,
message: newMessage,
transaction: writeTx)
// Expect no results when re-fetching the payload since it's expired
XCTAssertNil(MessageSendLog.fetchPayload(
address: recipientAddress,
deviceId: deviceId,
timestamp: newMessage.timestamp,
transaction: writeTx))
}
}
func testFinalDeliveryRemovesPayload() {
databaseStorage.write { writeTx in
// Create and save the message payload
let newMessage = createOutgoingMessage(transaction: writeTx)
let payloadData = CommonGenerator.sentence.data(using: .utf8)!
let payloadIndex = MessageSendLog.recordPayload(payloadData, forMessageBeingSent: newMessage, transaction: writeTx) as! Int64
// "Send" the message to one recipient, two devices
let recipientAddress = CommonGenerator.address()
for deviceId: Int64 in [0, 1] {
MessageSendLog.recordPendingDelivery(
payloadId: payloadIndex,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: deviceId,
message: newMessage,
transaction: writeTx)
}
MessageSendLog.sendComplete(message: newMessage, transaction: writeTx)
// Deliver to first device
MessageSendLog.recordSuccessfulDelivery(
timestamp: newMessage.timestamp,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: 0,
transaction: writeTx)
// Verify the payload still exists
XCTAssertTrue(isPayloadAlive(index: payloadIndex, transaction: writeTx))
// Deliver to second device
MessageSendLog.recordSuccessfulDelivery(
timestamp: newMessage.timestamp,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: 1,
transaction: writeTx)
// Verify the payload was deleted
XCTAssertFalse(isPayloadAlive(index: payloadIndex, transaction: writeTx))
}
}
func testReceiveDeliveryBeforeSendFinished() {
databaseStorage.write { writeTx in
// Create and save the message payload
let newMessage = createOutgoingMessage(transaction: writeTx)
let payloadData = CommonGenerator.sentence.data(using: .utf8)!
let payloadIndex = MessageSendLog.recordPayload(payloadData, forMessageBeingSent: newMessage, transaction: writeTx) as! Int64
let recipientAddress = CommonGenerator.address()
// "Send" the message to one device. It acks delivery
MessageSendLog.recordPendingDelivery(
payloadId: payloadIndex,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: 0,
message: newMessage,
transaction: writeTx)
MessageSendLog.recordSuccessfulDelivery(
timestamp: newMessage.timestamp,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: 0,
transaction: writeTx)
// Verify the payload still exists since we haven't finished sending
XCTAssertTrue(isPayloadAlive(index: payloadIndex, transaction: writeTx))
// "Send" the message to two more devices. Mark send as complete
for deviceId: Int64 in [1, 2] {
MessageSendLog.recordPendingDelivery(
payloadId: payloadIndex,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: deviceId,
message: newMessage,
transaction: writeTx)
}
MessageSendLog.sendComplete(message: newMessage, transaction: writeTx)
// Deliver to second device
MessageSendLog.recordSuccessfulDelivery(
timestamp: newMessage.timestamp,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: 1,
transaction: writeTx)
// Verify the payload still exists
XCTAssertTrue(isPayloadAlive(index: payloadIndex, transaction: writeTx))
// Deliver to third device
MessageSendLog.recordSuccessfulDelivery(
timestamp: newMessage.timestamp,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: 2,
transaction: writeTx)
// Verify the payload was deleted
XCTAssertFalse(isPayloadAlive(index: payloadIndex, transaction: writeTx))
}
}
func testPartialFailureReusesPayloadEntry() {
databaseStorage.write { writeTx in
// Create and save the message payload
let newMessage = createOutgoingMessage(transaction: writeTx)
let payloadData = CommonGenerator.sentence.data(using: .utf8)!
let initialPayloadId = MessageSendLog.recordPayload(payloadData, forMessageBeingSent: newMessage, transaction: writeTx) as! Int64
let recipientAddress = CommonGenerator.address()
// "Send" the message to one device. Complete send but don't mark as delivered.
MessageSendLog.recordPendingDelivery(
payloadId: initialPayloadId,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: 0,
message: newMessage,
transaction: writeTx)
MessageSendLog.sendComplete(message: newMessage, transaction: writeTx)
// Simulate a "retry" of a failed send. Try a fresh insert of the payload data
// Then send to another device and complete the send.
let retryPayloadId = MessageSendLog.recordPayload(
payloadData,
forMessageBeingSent: newMessage,
transaction: writeTx) as! Int64
MessageSendLog.recordPendingDelivery(
payloadId: initialPayloadId,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: 1,
message: newMessage,
transaction: writeTx)
MessageSendLog.sendComplete(message: newMessage, transaction: writeTx)
// Both payloadIds should be the same. The payload is still alive.
XCTAssertEqual(initialPayloadId, retryPayloadId)
XCTAssertTrue(isPayloadAlive(index: initialPayloadId, transaction: writeTx))
// Deliver to first device
MessageSendLog.recordSuccessfulDelivery(
timestamp: newMessage.timestamp,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: 0,
transaction: writeTx)
// Verify the payload still exists
XCTAssertTrue(isPayloadAlive(index: initialPayloadId, transaction: writeTx))
// Deliver to second device
MessageSendLog.recordSuccessfulDelivery(
timestamp: newMessage.timestamp,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: 1,
transaction: writeTx)
// Verify the payload was deleted
XCTAssertFalse(isPayloadAlive(index: initialPayloadId, transaction: writeTx))
}
}
func testRetryPartialFailureAfterAllInitialRecipientsAcked() {
databaseStorage.write { writeTx in
// Create and save the message payload
let newMessage = createOutgoingMessage(transaction: writeTx)
let payloadData = CommonGenerator.sentence.data(using: .utf8)!
let recipientAddress = CommonGenerator.address()
// Record + Send + Deliver + Complete (Deliver and Complete can happen in either order)
let initialPayloadId = MessageSendLog.recordPayload(
payloadData,
forMessageBeingSent: newMessage,
transaction: writeTx) as! Int64
MessageSendLog.recordPendingDelivery(
payloadId: initialPayloadId,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: 0,
message: newMessage,
transaction: writeTx)
MessageSendLog.recordSuccessfulDelivery(
timestamp: newMessage.timestamp,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: 0,
transaction: writeTx)
MessageSendLog.sendComplete(message: newMessage, transaction: writeTx)
// Verify payload is deleted:
XCTAssertFalse(isPayloadAlive(index: initialPayloadId, transaction: writeTx))
// Record + Send + Complete + Deliver (Deliver and Complete can happen in either order)
let secondPayloadId = MessageSendLog.recordPayload(
payloadData,
forMessageBeingSent: newMessage,
transaction: writeTx) as! Int64
MessageSendLog.recordPendingDelivery(
payloadId: secondPayloadId,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: 1,
message: newMessage,
transaction: writeTx)
MessageSendLog.sendComplete(message: newMessage, transaction: writeTx)
MessageSendLog.recordSuccessfulDelivery(
timestamp: newMessage.timestamp,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: 1,
transaction: writeTx)
// Verify payload is deleted:
XCTAssertFalse(isPayloadAlive(index: secondPayloadId, transaction: writeTx))
// Verify the ID was not reusued
XCTAssertNotEqual(initialPayloadId, secondPayloadId)
}
}
// Test disabled since it exercises an owsFailDebug()
// Works correctly if assertions are disabled and the test is enabled.
func testPlaintextMismatchFails() throws {
databaseStorage.write { writeTx in
// Create and save the message payload
let newMessage = createOutgoingMessage(transaction: writeTx)
let payloadData = CommonGenerator.sentence.data(using: .utf8)!
let initialPayloadId = MessageSendLog.recordPayload(
payloadData,
forMessageBeingSent: newMessage,
transaction: writeTx)
let secondPayloadId = MessageSendLog.recordPayload(
payloadData + Data([1]), // append a byte so the payload doesn't match
forMessageBeingSent: newMessage,
transaction: writeTx)
XCTAssertNotNil(initialPayloadId)
XCTAssertNil(secondPayloadId)
}
}
func testDeleteMessageWithOnePayload() {
databaseStorage.write { writeTx in
// Create and save the message payload
let newMessage = createOutgoingMessage(transaction: writeTx)
let payloadData = CommonGenerator.sentence.data(using: .utf8)!
let payloadIndex = MessageSendLog.recordPayload(payloadData, forMessageBeingSent: newMessage, transaction: writeTx) as! Int64
// "Send" the message to a recipient
let recipientAddress = CommonGenerator.address()
let deviceId = Int64.random(in: 0..<100)
MessageSendLog.recordPendingDelivery(
payloadId: payloadIndex,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: deviceId,
message: newMessage,
transaction: writeTx)
// Delete the message
MessageSendLog.deleteAllPayloadsForInteraction(newMessage, transaction: writeTx)
// Verify the corresponding payload was deleted
XCTAssertFalse(isPayloadAlive(index: payloadIndex, transaction: writeTx))
}
}
func testDeleteMessageWithManyPayloads() {
databaseStorage.write { writeTx in
// Create and save several message payloads
let message1 = createOutgoingMessage(transaction: writeTx)
let data1 = CommonGenerator.sentence.data(using: .utf8)!
let index1 = MessageSendLog.recordPayload(data1, forMessageBeingSent: message1, transaction: writeTx) as! Int64
let message2 = createOutgoingMessage(transaction: writeTx)
let data2 = CommonGenerator.sentence.data(using: .utf8)!
let index2 = MessageSendLog.recordPayload(data2, forMessageBeingSent: message2, transaction: writeTx) as! Int64
let readReceiptMessage = createOutgoingMessage(relatedMessageIds: [message1.uniqueId, message2.uniqueId], transaction: writeTx)
let data3 = CommonGenerator.sentence.data(using: .utf8)!
let index3 = MessageSendLog.recordPayload(data3, forMessageBeingSent: readReceiptMessage, transaction: writeTx) as! Int64
// "Send" the messages to a recipient
let recipientAddress = CommonGenerator.address()
let deviceId = Int64.random(in: 0..<100)
for index in [index1, index2, index3] {
MessageSendLog.recordPendingDelivery(
payloadId: index,
recipientUuid: recipientAddress.uuid!,
recipientDeviceId: deviceId,
message: message1,
transaction: writeTx)
}
// Delete message1.
MessageSendLog.deleteAllPayloadsForInteraction(message1, transaction: writeTx)
// We expect that the read receipt message is deleted because it relates to the deleted mesage
// We expect message2's payload to stick around, because none of its content is dependent on message1
XCTAssertFalse(isPayloadAlive(index: index1, transaction: writeTx))
XCTAssertTrue(isPayloadAlive(index: index2, transaction: writeTx))
XCTAssertFalse(isPayloadAlive(index: index3, transaction: writeTx))
}
}
func testCleanupExpiredPayloads() {
databaseStorage.write { writeTx in
let oldMessage = createOutgoingMessage(date: Date(timeIntervalSince1970: 1000), transaction: writeTx)
let oldData = CommonGenerator.sentence.data(using: .utf8)!
let oldIndex = MessageSendLog.recordPayload(oldData, forMessageBeingSent: oldMessage, transaction: writeTx) as! Int64
let newMessage = createOutgoingMessage(transaction: writeTx)
let newData = CommonGenerator.sentence.data(using: .utf8)!
let newIndex = MessageSendLog.recordPayload(newData, forMessageBeingSent: newMessage, transaction: writeTx) as! Int64
// Verify both messages exist
XCTAssertTrue(isPayloadAlive(index: oldIndex, transaction: writeTx))
XCTAssertTrue(isPayloadAlive(index: newIndex, transaction: writeTx))
// Kick off cleanup
MessageSendLog.test_forceCleanupStaleEntries(transaction: writeTx)
// Verify only the old message was deleted
XCTAssertFalse(isPayloadAlive(index: oldIndex, transaction: writeTx))
XCTAssertTrue(isPayloadAlive(index: newIndex, transaction: writeTx))
}
}
func testTimestampMismatch() {
// IOS-1762: Greyson reported an issue where a resent message would have a timestamp mismatch on the outside vs
// inside of the envelope. In his case, the outside had a timestamp of 1629210680139 versus the inside
// which had 1629210680140.
//
// Casting back and forth from Date/TimeInterval and millisecond timestamps would lead to float math
// incorrectly coercing to the wrong timestamp. In this case, constructing a Date from that millisecond
// timestamp would result in a time interval of 1629210680.1399999. Reconverting back to a timestamp and
// we get 1629210680139.
databaseStorage.write { writeTx in
let originalTimestamp: UInt64 = 1629210680140
let originalDate = Date(millisecondsSince1970: originalTimestamp)
XCTAssertEqual(originalDate.ows_millisecondsSince1970, originalTimestamp)
let address = CommonGenerator.address()
let message = createOutgoingMessage(date: originalDate, transaction: writeTx)
let data = CommonGenerator.sentence.data(using: .utf8)!
XCTAssertEqual(message.timestamp, originalTimestamp)
let index = MessageSendLog.recordPayload(data, forMessageBeingSent: message, transaction: writeTx)!.int64Value
MessageSendLog.recordPendingDelivery(payloadId: index, recipientUuid: address.uuid!, recipientDeviceId: 1, message: message, transaction: writeTx)
let fetchedPayload = MessageSendLog.test_fetchPayload(
address: address,
deviceId: 1,
timestamp: originalTimestamp,
allowExpired: true,
transaction: writeTx)
XCTAssertEqual(fetchedPayload?.sentTimestamp, originalTimestamp)
}
}
// MARK: - Helpers
class MSLTestMessage: TSOutgoingMessage {
override init(outgoingMessageWithBuilder outgoingMessageBuilder: TSOutgoingMessageBuilder) {
super.init(outgoingMessageWithBuilder: outgoingMessageBuilder)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
required init(dictionary dictionaryValue: [String: Any]!) throws {
fatalError("init(dictionary:) has not been implemented")
}
var _contentHint: SealedSenderContentHint = .resendable
override var contentHint: SealedSenderContentHint { _contentHint }
var _relatedMessageIds: [String] = []
override var relatedUniqueIds: Set<String> { Set(_relatedMessageIds) }
}
func createOutgoingMessage(
date: Date? = nil,
contentHint: SealedSenderContentHint = .implicit,
relatedMessageIds: [String] = [],
transaction writeTx: SDSAnyWriteTransaction
) -> TSOutgoingMessage {
let resolvedDate = date ?? {
let newDate = Date()
usleep(2000) // If we're taking the timestamp of Now, wait a bit to avoid collisions
return newDate
}()
let builder = TSOutgoingMessageBuilder(thread: ContactThreadFactory().create(transaction: writeTx),
timestamp: resolvedDate.ows_millisecondsSince1970)
let testMessage = MSLTestMessage(outgoingMessageWithBuilder: builder)
testMessage._contentHint = contentHint
testMessage._relatedMessageIds = [testMessage.uniqueId] + relatedMessageIds
return testMessage
}
func isPayloadAlive(index: Int64, transaction writeTx: SDSAnyWriteTransaction) -> Bool {
let count = try! MessageSendLog.Payload
.filter(Column("payloadId") == index)
.fetchCount(writeTx.unwrapGrdbWrite.database)
return count > 0
}
}