Signal-iOS/SignalServiceKit/tests/MessageBackup/MessageBackupIntegrationTestCase.swift
2024-05-17 13:59:49 -07:00

176 lines
8.6 KiB
Swift

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import LibSignalClient
import XCTest
@testable import SignalServiceKit
class MessageBackupIntegrationTestCase: XCTestCase {
override func setUp() {
DDLog.add(DDTTYLogger.sharedInstance!)
}
// MARK: -
private var messageBackupManager: MessageBackupManager {
DependenciesBridge.shared.messageBackupManager
}
private var localIdentifiers: LocalIdentifiers {
/// A backup doesn't contain our own local identifiers. Rather, those
/// are determined as part of registration for a backup import, and are
/// already-known for a backup export.
///
/// Consequently, we can use any local identifiers for our test
/// purposes without worrying about the contents of each test case's
/// backup file.
return .forUnitTests
}
func runTest(
backupName: String,
assertionsBlock: (SDSAnyReadTransaction, DBReadTransaction) throws -> Void
) async throws {
try await importAndAssert(
localIdentifiers: localIdentifiers,
backupUrl: backupFileUrl(named: backupName),
assertionsBlock: assertionsBlock
)
let exportedBackupUrl = try await messageBackupManager
.exportPlaintextBackup(localIdentifiers: localIdentifiers)
try await importAndAssert(
localIdentifiers: localIdentifiers,
backupUrl: exportedBackupUrl,
assertionsBlock: assertionsBlock
)
}
private func backupFileUrl(named backupName: String) -> URL {
let testBundle = Bundle(for: type(of: self))
return testBundle.url(forResource: backupName, withExtension: "binproto")!
}
private func importAndAssert(
localIdentifiers: LocalIdentifiers,
backupUrl: URL,
assertionsBlock: (SDSAnyReadTransaction, DBReadTransaction) throws -> Void
) async throws {
await initializeApp()
try await messageBackupManager.importPlaintextBackup(
fileUrl: backupUrl,
localIdentifiers: localIdentifiers
)
try NSObject.databaseStorage.read { tx in
try assertionsBlock(tx, tx.asV2Read)
}
}
// MARK: -
@MainActor
final func initializeApp() async {
let testAppContext = TestAppContext()
SetCurrentAppContext(testAppContext)
/// Note that ``SDSDatabaseStorage/grdbDatabaseFileUrl``, through a few
/// layers of abstraction, uses the "current app context" to decide
/// where to put the database,
///
/// For a ``TestAppContext`` as configured above, this will be a
/// subdirectory of our temp directory unique to the instantiation of
/// the app context.
let databaseStorage = try! SDSDatabaseStorage(
databaseFileUrl: SDSDatabaseStorage.grdbDatabaseFileUrl,
keychainStorage: MockKeychainStorage()
)
/// We use crashy versions of dependencies that should never be called
/// during backups, and no-op implementations of payments because those
/// are bound to the SignalUI target.
_ = await AppSetup().start(
appContext: testAppContext,
databaseStorage: databaseStorage,
paymentsEvents: PaymentsEventsNoop(),
mobileCoinHelper: MobileCoinHelperMock(),
callMessageHandler: CrashyMocks.MockCallMessageHandler(),
currentCallThreadProvider: CrashyMocks.MockCurrentCallThreadProvider(),
notificationPresenter: CrashyMocks.MockNotificationPresenter(),
testDependencies: AppSetup.TestDependencies(
networkManager: CrashyMocks.MockNetworkManager(),
webSocketFactory: CrashyMocks.MockWebSocketFactory()
)
).prepareDatabase().awaitable()
}
}
// MARK: -
private func failTest<T>(
_ type: T.Type,
_ function: StaticString = #function
) -> Never {
let message = "Unexpectedly called \(type)#\(function)!"
XCTFail(message)
owsFail(message)
}
/// As a rule, integration tests for message backup should not mock out their
/// dependencies as their goal is to validate how the real, production app will
/// behave with respect to Backups.
///
/// These mocks are the exceptions to that rule, and encompass managers that
/// should never be invoked during Backup import or export.
private enum CrashyMocks {
final class MockNetworkManager: NetworkManager {
override func makePromise(request: TSRequest, canUseWebSocket: Bool = false) -> Promise<any HTTPResponse> { failTest(Self.self) }
}
final class MockWebSocketFactory: WebSocketFactory {
var canBuildWebSocket: Bool { failTest(Self.self) }
func buildSocket(request: WebSocketRequest, callbackScheduler: any Scheduler) -> (any SSKWebSocket)? { failTest(Self.self) }
}
final class MockCallMessageHandler: CallMessageHandler {
func action(for envelope: SSKProtoEnvelope, callMessage: SSKProtoCallMessage, serverDeliveryTimestamp: UInt64) -> CallMessageAction { failTest(Self.self) }
func receivedOffer(_ offer: SSKProtoCallMessageOffer, from caller: (aci: Aci, deviceId: UInt32), sentAtTimestamp: UInt64, serverReceivedTimestamp: UInt64, serverDeliveryTimestamp: UInt64, tx: SDSAnyWriteTransaction) { failTest(Self.self) }
func receivedAnswer(_ answer: SSKProtoCallMessageAnswer, from caller: (aci: Aci, deviceId: UInt32)) { failTest(Self.self) }
func receivedIceUpdate(_ iceUpdate: [SSKProtoCallMessageIceUpdate], from caller: (aci: Aci, deviceId: UInt32)) { failTest(Self.self) }
func receivedHangup(_ hangup: SSKProtoCallMessageHangup, from caller: (aci: Aci, deviceId: UInt32)) { failTest(Self.self) }
func receivedBusy(_ busy: SSKProtoCallMessageBusy, from caller: (aci: Aci, deviceId: UInt32)) { failTest(Self.self) }
func receivedOpaque(_ opaque: SSKProtoCallMessageOpaque, from caller: (aci: Aci, deviceId: UInt32), serverReceivedTimestamp: UInt64, serverDeliveryTimestamp: UInt64, tx: SDSAnyReadTransaction) { failTest(Self.self) }
func receivedGroupCallUpdateMessage(_ updateMessage: SSKProtoDataMessageGroupCallUpdate, for thread: TSGroupThread, serverReceivedTimestamp: UInt64) async { failTest(Self.self) }
func externallyHandleCallMessage(envelope: SSKProtoEnvelope, plaintextData: Data, wasReceivedByUD: Bool, serverDeliveryTimestamp: UInt64, tx: SDSAnyWriteTransaction) { failTest(Self.self) }
}
final class MockCurrentCallThreadProvider: CurrentCallThreadProvider {
var currentCallThread: TSThread? { failTest(Self.self) }
}
final class MockNotificationPresenter: NotificationPresenter {
func notifyUser(forIncomingMessage: TSIncomingMessage, thread: TSThread, transaction: SDSAnyReadTransaction) { failTest(Self.self) }
func notifyUser(forIncomingMessage: TSIncomingMessage, editTarget: TSIncomingMessage, thread: TSThread, transaction: SDSAnyReadTransaction) { failTest(Self.self) }
func notifyUser(forReaction: OWSReaction, onOutgoingMessage: TSOutgoingMessage, thread: TSThread, transaction: SDSAnyReadTransaction) { failTest(Self.self) }
func notifyUser(forErrorMessage: TSErrorMessage, thread: TSThread, transaction: SDSAnyWriteTransaction) { failTest(Self.self) }
func notifyUser(forTSMessage: TSMessage, thread: TSThread, wantsSound: Bool, transaction: SDSAnyWriteTransaction) { failTest(Self.self) }
func notifyUser(forPreviewableInteraction: any TSInteraction & OWSPreviewText, thread: TSThread, wantsSound: Bool, transaction: SDSAnyWriteTransaction) { failTest(Self.self) }
func notifyTestPopulation(ofErrorMessage errorString: String) { failTest(Self.self) }
func notifyUser(forFailedStorySend: StoryMessage, to: TSThread, transaction: SDSAnyWriteTransaction) { failTest(Self.self) }
func notifyUserToRelaunchAfterTransfer(completion: (() -> Void)?) { failTest(Self.self) }
func notifyUserOfDeregistration(transaction: SDSAnyWriteTransaction) { failTest(Self.self) }
func clearAllNotifications() { failTest(Self.self) }
func cancelNotifications(threadId: String) { failTest(Self.self) }
func cancelNotifications(messageIds: [String]) { failTest(Self.self) }
func cancelNotifications(reactionId: String) { failTest(Self.self) }
func cancelNotificationsForMissedCalls(threadUniqueId: String) { failTest(Self.self) }
func cancelNotifications(for storyMessage: StoryMessage) { failTest(Self.self) }
func notifyUserOfDeregistration(tx: any DBWriteTransaction) { failTest(Self.self) }
}
}