Signal-iOS/SignalServiceKit/tests/Account/LearnMyOwnPniManagerTest.swift
2023-08-02 16:53:51 -05:00

414 lines
14 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import LibSignalClient
import XCTest
@testable import SignalServiceKit
class LearnMyOwnPniManagerTest: XCTestCase {
private struct TestKeyValueStore {
static let hasSucceededKey = "hasCompletedPniLearning"
private let kvStore: KeyValueStore
init(kvStoreFactory: KeyValueStoreFactory) {
kvStore = kvStoreFactory.keyValueStore(collection: "LearnMyOwnPniManagerImpl")
}
func hasSucceeded(tx: DBReadTransaction) -> Bool {
return kvStore.getBool(Self.hasSucceededKey, defaultValue: false, transaction: tx)
}
func setHasSucceeded(tx: DBWriteTransaction) {
kvStore.setBool(true, key: Self.hasSucceededKey, transaction: tx)
}
}
private var accountServiceClientMock: AccountServiceClientMock!
private var identityManagerMock: IdentityManagerMock!
private var preKeyManagerMock: PreKeyManagerMock!
private var profileFetcherMock: ProfileFetcherMock!
private var tsAccountManagerMock: TSAccountManagerMock!
private var kvStore: TestKeyValueStore!
private let db = MockDB()
private var learnMyOwnPniManager: LearnMyOwnPniManager!
override func setUp() {
accountServiceClientMock = .init()
identityManagerMock = .init()
preKeyManagerMock = .init()
profileFetcherMock = .init()
tsAccountManagerMock = .init()
let kvStoreFactory = InMemoryKeyValueStoreFactory()
kvStore = TestKeyValueStore(kvStoreFactory: kvStoreFactory)
let schedulers = TestSchedulers(scheduler: TestScheduler())
schedulers.scheduler.start()
learnMyOwnPniManager = LearnMyOwnPniManagerImpl(
accountServiceClient: accountServiceClientMock,
identityManager: identityManagerMock,
preKeyManager: preKeyManagerMock,
profileFetcher: profileFetcherMock,
tsAccountManager: tsAccountManagerMock,
databaseStorage: db,
keyValueStoreFactory: kvStoreFactory,
schedulers: schedulers
)
}
func testSkipsIfAlreadySucceeded() {
db.write { kvStore.setHasSucceeded(tx: $0) }
db.read { tx in
_ = learnMyOwnPniManager.learnMyOwnPniIfNecessary(tx: tx)
}
XCTAssertFalse(accountServiceClientMock.completeWhoAmIRequest())
XCTAssertNil(tsAccountManagerMock.updatedPni)
XCTAssertFalse(profileFetcherMock.completeProfileFetch())
XCTAssertFalse(preKeyManagerMock.completeCreatePniKeys())
XCTAssertTrue(db.read { kvStore.hasSucceeded(tx: $0) })
}
func testSkipsIfLinkedDevice() {
tsAccountManagerMock.isPrimaryDevice = false
db.read { tx in
_ = learnMyOwnPniManager.learnMyOwnPniIfNecessary(tx: tx)
}
XCTAssertFalse(accountServiceClientMock.completeWhoAmIRequest())
XCTAssertNil(tsAccountManagerMock.updatedPni)
XCTAssertFalse(profileFetcherMock.completeProfileFetch())
XCTAssertFalse(preKeyManagerMock.completeCreatePniKeys())
XCTAssertFalse(db.read { kvStore.hasSucceeded(tx: $0) })
}
func testSkipsIfNoLocalIdentifiers() {
db.read { tx in
_ = learnMyOwnPniManager.learnMyOwnPniIfNecessary(tx: tx)
}
XCTAssertFalse(accountServiceClientMock.completeWhoAmIRequest())
XCTAssertNil(tsAccountManagerMock.updatedPni)
XCTAssertFalse(profileFetcherMock.completeProfileFetch())
XCTAssertFalse(preKeyManagerMock.completeCreatePniKeys())
XCTAssertFalse(db.read { kvStore.hasSucceeded(tx: $0) })
}
func testFetchesPniIfMissing() {
let localAci = Aci.randomForTesting()
let localE164 = E164("+17735550199")!
let remotePni = Pni.randomForTesting()
tsAccountManagerMock.mockIdentifiers = .init(aci: localAci, pni: nil, e164: localE164)
accountServiceClientMock.mockWhoAmI = .init(aci: localAci, pni: remotePni, e164: localE164)
db.read { tx in
_ = learnMyOwnPniManager.learnMyOwnPniIfNecessary(tx: tx)
}
XCTAssertTrue(accountServiceClientMock.completeWhoAmIRequest())
XCTAssertEqual(remotePni, tsAccountManagerMock.updatedPni)
XCTAssertFalse(profileFetcherMock.completeProfileFetch())
XCTAssertTrue(preKeyManagerMock.completeCreatePniKeys())
XCTAssertTrue(db.read { kvStore.hasSucceeded(tx: $0) })
}
func testSkipsPniFetchIfPresent() {
let localAci = Aci.randomForTesting()
let localPni = Pni.randomForTesting()
let localE164 = E164("+17735550199")!
tsAccountManagerMock.mockIdentifiers = .init(aci: localAci, pni: localPni, e164: localE164)
db.read { tx in
_ = learnMyOwnPniManager.learnMyOwnPniIfNecessary(tx: tx)
}
XCTAssertFalse(accountServiceClientMock.completeWhoAmIRequest())
XCTAssertNil(tsAccountManagerMock.updatedPni)
XCTAssertFalse(profileFetcherMock.completeProfileFetch())
XCTAssertTrue(preKeyManagerMock.completeCreatePniKeys())
XCTAssertTrue(db.read { kvStore.hasSucceeded(tx: $0) })
}
func testSkipsPniSaveIfMismatchedAci() {
let localAci = Aci.randomForTesting()
let localE164 = E164("+17735550199")!
let remoteAci = Aci.randomForTesting()
let remotePni = Pni.randomForTesting()
tsAccountManagerMock.mockIdentifiers = .init(aci: localAci, pni: nil, e164: localE164)
accountServiceClientMock.mockWhoAmI = .init(aci: remoteAci, pni: remotePni, e164: localE164)
db.read { tx in
_ = learnMyOwnPniManager.learnMyOwnPniIfNecessary(tx: tx)
}
XCTAssertTrue(accountServiceClientMock.completeWhoAmIRequest())
XCTAssertNil(tsAccountManagerMock.updatedPni)
XCTAssertFalse(profileFetcherMock.completeProfileFetch())
XCTAssertFalse(preKeyManagerMock.completeCreatePniKeys())
XCTAssertFalse(db.read { kvStore.hasSucceeded(tx: $0) })
}
func testCreatesPniKeysWithoutFetchingRemoteIfNoneLocal() {
let localAci = Aci.randomForTesting()
let localPni = Pni.randomForTesting()
let localE164 = E164("+17735550199")!
tsAccountManagerMock.mockIdentifiers = .init(aci: localAci, pni: localPni, e164: localE164)
db.read { tx in
_ = learnMyOwnPniManager.learnMyOwnPniIfNecessary(tx: tx)
}
XCTAssertFalse(accountServiceClientMock.completeWhoAmIRequest())
XCTAssertNil(tsAccountManagerMock.updatedPni)
XCTAssertFalse(profileFetcherMock.completeProfileFetch())
XCTAssertTrue(preKeyManagerMock.completeCreatePniKeys())
XCTAssertTrue(db.read { kvStore.hasSucceeded(tx: $0) })
}
func testCreatesPniKeysIfRemoteMissing() {
let localAci = Aci.randomForTesting()
let localPni = Pni.randomForTesting()
let localE164 = E164("+17735550199")!
tsAccountManagerMock.mockIdentifiers = .init(aci: localAci, pni: localPni, e164: localE164)
identityManagerMock.pniPublicKeyData = Data()
profileFetcherMock.profileFetchResult = .success(nil)
db.read { tx in
_ = learnMyOwnPniManager.learnMyOwnPniIfNecessary(tx: tx)
}
XCTAssertFalse(accountServiceClientMock.completeWhoAmIRequest())
XCTAssertNil(tsAccountManagerMock.updatedPni)
XCTAssertTrue(profileFetcherMock.completeProfileFetch())
XCTAssertTrue(preKeyManagerMock.completeCreatePniKeys())
XCTAssertTrue(db.read { kvStore.hasSucceeded(tx: $0) })
}
func testCreatesPniKeysIfRemoteDoesNotMatchLocal() {
let localAci = Aci.randomForTesting()
let localPni = Pni.randomForTesting()
let localE164 = E164("+17735550199")!
tsAccountManagerMock.mockIdentifiers = .init(aci: localAci, pni: localPni, e164: localE164)
identityManagerMock.pniPublicKeyData = Data(repeating: 3, count: 12)
profileFetcherMock.profileFetchResult = .success(Data(repeating: 4, count: 12))
db.read { tx in
_ = learnMyOwnPniManager.learnMyOwnPniIfNecessary(tx: tx)
}
XCTAssertFalse(accountServiceClientMock.completeWhoAmIRequest())
XCTAssertNil(tsAccountManagerMock.updatedPni)
XCTAssertTrue(profileFetcherMock.completeProfileFetch())
XCTAssertTrue(preKeyManagerMock.completeCreatePniKeys())
XCTAssertTrue(db.read { kvStore.hasSucceeded(tx: $0) })
}
func testDoesNotCreatePniKeysIfErrorFetchingRemoteToCompare() {
let localAci = Aci.randomForTesting()
let localPni = Pni.randomForTesting()
let localE164 = E164("+17735550199")!
tsAccountManagerMock.mockIdentifiers = .init(aci: localAci, pni: localPni, e164: localE164)
identityManagerMock.pniPublicKeyData = Data(repeating: 3, count: 12)
profileFetcherMock.profileFetchResult = .failure(OWSGenericError("whoops"))
db.read { tx in
_ = learnMyOwnPniManager.learnMyOwnPniIfNecessary(tx: tx)
}
XCTAssertFalse(accountServiceClientMock.completeWhoAmIRequest())
XCTAssertNil(tsAccountManagerMock.updatedPni)
XCTAssertTrue(profileFetcherMock.completeProfileFetch())
XCTAssertFalse(preKeyManagerMock.completeCreatePniKeys())
XCTAssertFalse(db.read { kvStore.hasSucceeded(tx: $0) })
}
func testDoesNotCreatePniKeysIfRemoteMatchesLocal() {
let localAci = Aci.randomForTesting()
let localPni = Pni.randomForTesting()
let localE164 = E164("+17735550199")!
tsAccountManagerMock.mockIdentifiers = .init(aci: localAci, pni: localPni, e164: localE164)
identityManagerMock.pniPublicKeyData = Data(repeating: 3, count: 12)
profileFetcherMock.profileFetchResult = .success(Data(repeating: 3, count: 12))
db.read { tx in
_ = learnMyOwnPniManager.learnMyOwnPniIfNecessary(tx: tx)
}
XCTAssertFalse(accountServiceClientMock.completeWhoAmIRequest())
XCTAssertNil(tsAccountManagerMock.updatedPni)
XCTAssertTrue(profileFetcherMock.completeProfileFetch())
XCTAssertFalse(preKeyManagerMock.completeCreatePniKeys())
XCTAssertTrue(db.read { kvStore.hasSucceeded(tx: $0) })
}
}
private extension WhoAmIRequestFactory.Responses.WhoAmI {
init(aci: Aci, pni: Pni, e164: E164) {
self.init(aci: aci.rawUUID, pni: pni.rawUUID, e164: e164, usernameHash: nil)
}
}
// MARK: - Mocks
// MARK: AccountServiceClient
private class AccountServiceClientMock: LearnMyOwnPniManagerImpl.Shims.AccountServiceClient {
typealias WhoAmI = WhoAmIRequestFactory.Responses.WhoAmI
var mockWhoAmI: WhoAmI?
private var whoAmIRequestFuture: Future<WhoAmI>?
/// Completes a mocked operation that is async in the real app. Otherwise,
/// since tests complete promises synchronously we may get database
/// re-entrance.
/// - Returns whether or not a mocked operation was completed.
func completeWhoAmIRequest() -> Bool {
guard let whoAmIRequestFuture else {
return false
}
guard let mockWhoAmI else {
whoAmIRequestFuture.reject(OWSGenericError("Missing mock!"))
return false
}
whoAmIRequestFuture.resolve(mockWhoAmI)
return true
}
func getAccountWhoAmI() -> Promise<WhoAmI> {
guard whoAmIRequestFuture == nil else {
XCTFail("Request already in-flight!")
return Promise(error: OWSGenericError("Request already in flight!"))
}
let (promise, future) = Promise<WhoAmI>.pending()
whoAmIRequestFuture = future
return promise
}
}
// MARK: IdentityManager
private class IdentityManagerMock: LearnMyOwnPniManagerImpl.Shims.IdentityManager {
var pniPublicKeyData: Data?
func pniIdentityPublicKeyData(tx _: DBReadTransaction) -> Data? {
return pniPublicKeyData
}
}
// MARK: PreKeyManager
private class PreKeyManagerMock: MockPreKeyManager {
private var createKeysFuture: Future<Void>?
/// Completes a mocked operation that is async in the real app. Otherwise,
/// since tests complete promises synchronously we may get database
/// re-entrance.
/// - Returns whether or not a mocked operation was completed.
func completeCreatePniKeys() -> Bool {
guard let createKeysFuture else {
return false
}
createKeysFuture.resolve()
return true
}
override func createPreKeys(identity: OWSIdentity) -> Promise<Void> {
guard createKeysFuture == nil else {
XCTFail("Creation already in-flight!")
return Promise(error: OWSGenericError("Creation already in-flight!"))
}
let (promise, future) = Promise<Void>.pending()
createKeysFuture = future
return promise
}
}
// MARK: ProfileFetcher
private class ProfileFetcherMock: LearnMyOwnPniManagerImpl.Shims.ProfileFetcher {
var profileFetchResult: Result<Data?, Error>?
private var profileFetchFuture: Future<Data?>?
/// Completes a mocked operation that is async in the real app. Otherwise,
/// since tests complete promises synchronously we may get database
/// re-entrance.
/// - Returns whether or not a mocked operation was completed.
func completeProfileFetch() -> Bool {
guard let profileFetchFuture else {
return false
}
guard let profileFetchResult else {
XCTFail("Missing mock!")
return false
}
switch profileFetchResult {
case .success(let data):
profileFetchFuture.resolve(data)
case .failure(let error):
profileFetchFuture.reject(error)
}
return true
}
func fetchPniIdentityPublicKey(localPni: UntypedServiceId) -> Promise<Data?> {
guard profileFetchFuture == nil else {
XCTFail("Fetch already in-flight!")
return Promise(error: OWSGenericError("Fetch already in flight!"))
}
let (promise, future) = Promise<Data?>.pending()
profileFetchFuture = future
return promise
}
}
// MARK: TSAccountManager
private class TSAccountManagerMock: LearnMyOwnPniManagerImpl.Shims.TSAccountManager {
var isPrimaryDevice: Bool = true
var mockIdentifiers: LocalIdentifiers?
var updatedPni: Pni?
func isPrimaryDevice(tx _: DBReadTransaction) -> Bool {
return isPrimaryDevice
}
func localIdentifiers(tx _: DBReadTransaction) -> LocalIdentifiers? {
return mockIdentifiers
}
func updateLocalIdentifiers(e164 _: E164, aci _: Aci, pni: Pni, tx _: DBWriteTransaction) {
updatedPni = pni
}
}