366 lines
14 KiB
Swift
366 lines
14 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import GRDB
|
|
import LibSignalClient
|
|
import XCTest
|
|
|
|
@testable import SignalServiceKit
|
|
|
|
final class UsernameValidationManagerTest: XCTestCase {
|
|
typealias Username = String
|
|
|
|
private var mockAccountServiceClient: MockAccountServiceClient!
|
|
private var mockContext: UsernameValidationManagerImpl.Context!
|
|
private var mockDB: DB!
|
|
private var mockLocalUsernameManager: MockLocalUsernameManager!
|
|
private var mockMessageProcessor: MockMessageProcessor!
|
|
private var mockScheduler: TestScheduler!
|
|
private var mockStorageServiceManager: MockStorageServiceManager!
|
|
private var mockUsernameLinkManager: MockUsernameLinkManager!
|
|
|
|
private var validationManager: UsernameValidationManagerImpl!
|
|
|
|
override func setUp() {
|
|
mockAccountServiceClient = MockAccountServiceClient()
|
|
mockDB = MockDB()
|
|
mockLocalUsernameManager = MockLocalUsernameManager()
|
|
mockMessageProcessor = MockMessageProcessor()
|
|
mockScheduler = TestScheduler()
|
|
mockStorageServiceManager = MockStorageServiceManager()
|
|
mockUsernameLinkManager = MockUsernameLinkManager()
|
|
|
|
validationManager = UsernameValidationManagerImpl(context: .init(
|
|
accountServiceClient: mockAccountServiceClient,
|
|
database: mockDB,
|
|
keyValueStoreFactory: InMemoryKeyValueStoreFactory(),
|
|
localUsernameManager: mockLocalUsernameManager,
|
|
messageProcessor: mockMessageProcessor,
|
|
schedulers: TestSchedulers(scheduler: mockScheduler),
|
|
storageServiceManager: mockStorageServiceManager,
|
|
usernameLinkManager: mockUsernameLinkManager
|
|
))
|
|
}
|
|
|
|
override func tearDown() {
|
|
mockAccountServiceClient.whoamiResponse.ensureUnset()
|
|
mockMessageProcessor.processingResult.ensureUnset()
|
|
mockStorageServiceManager.pendingRestoreResult.ensureUnset()
|
|
mockUsernameLinkManager.decryptLinkResult.ensureUnset()
|
|
}
|
|
|
|
private func runRunRun() {
|
|
mockDB.read { tx in
|
|
validationManager.validateUsernameIfNecessary(tx)
|
|
}
|
|
|
|
mockScheduler.runUntilIdle()
|
|
}
|
|
|
|
func testUnsetValidationSuccessful() {
|
|
mockLocalUsernameManager.startingUsernameState = .unset
|
|
mockMessageProcessor.processingResult = .value(())
|
|
mockStorageServiceManager.pendingRestoreResult = .value(())
|
|
mockAccountServiceClient.whoamiResponse = .value(.noRemoteUsername)
|
|
|
|
runRunRun()
|
|
|
|
mockDB.read { tx in
|
|
XCTAssertNotNil(validationManager.lastValidationDate(tx))
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
|
|
}
|
|
}
|
|
|
|
func testUnsetValidationFailsIfWhoamiFails() {
|
|
mockLocalUsernameManager.startingUsernameState = .unset
|
|
mockMessageProcessor.processingResult = .value(())
|
|
mockStorageServiceManager.pendingRestoreResult = .value(())
|
|
mockAccountServiceClient.whoamiResponse = .error()
|
|
|
|
runRunRun()
|
|
|
|
mockDB.read { tx in
|
|
XCTAssertNil(validationManager.lastValidationDate(tx))
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
|
|
}
|
|
}
|
|
|
|
func testUnsetValidationFailsIfRemoteUsernamePresent() {
|
|
mockLocalUsernameManager.startingUsernameState = .unset
|
|
mockMessageProcessor.processingResult = .value(())
|
|
mockStorageServiceManager.pendingRestoreResult = .value(())
|
|
mockAccountServiceClient.whoamiResponse = .value(.withRemoteUsername("boba_fett.42"))
|
|
|
|
runRunRun()
|
|
|
|
mockDB.read { tx in
|
|
XCTAssertNil(validationManager.lastValidationDate(tx))
|
|
XCTAssertTrue(mockLocalUsernameManager.didSetCorruptedUsername)
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
|
|
}
|
|
}
|
|
|
|
func testAvailableValidationSuccessful() {
|
|
mockLocalUsernameManager.startingUsernameState = .available(
|
|
username: "boba_fett.42",
|
|
usernameLink: .mocked
|
|
)
|
|
mockMessageProcessor.processingResult = .value(())
|
|
mockStorageServiceManager.pendingRestoreResult = .value(())
|
|
mockAccountServiceClient.whoamiResponse = .value(.withRemoteUsername("boba_fett.42"))
|
|
mockUsernameLinkManager.decryptLinkResult = .value("boba_fett.42")
|
|
|
|
runRunRun()
|
|
|
|
mockDB.read { tx in
|
|
XCTAssertNotNil(validationManager.lastValidationDate(tx))
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
|
|
}
|
|
}
|
|
|
|
func testAvailableValidationFailsIfWhoamiFails() {
|
|
mockLocalUsernameManager.startingUsernameState = .available(
|
|
username: "boba_fett.42",
|
|
usernameLink: .mocked
|
|
)
|
|
mockMessageProcessor.processingResult = .value(())
|
|
mockStorageServiceManager.pendingRestoreResult = .value(())
|
|
mockAccountServiceClient.whoamiResponse = .error()
|
|
|
|
runRunRun()
|
|
|
|
mockDB.read { tx in
|
|
XCTAssertNil(validationManager.lastValidationDate(tx))
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
|
|
}
|
|
}
|
|
|
|
func testAvailableValidationFailsIfRemoteUsernameMismatch() {
|
|
mockLocalUsernameManager.startingUsernameState = .available(
|
|
username: "boba_fett.42",
|
|
usernameLink: .mocked
|
|
)
|
|
mockMessageProcessor.processingResult = .value(())
|
|
mockStorageServiceManager.pendingRestoreResult = .value(())
|
|
mockAccountServiceClient.whoamiResponse = .value(.withRemoteUsername("boba_fett.43"))
|
|
|
|
runRunRun()
|
|
|
|
mockDB.read { tx in
|
|
XCTAssertNil(validationManager.lastValidationDate(tx))
|
|
XCTAssertTrue(mockLocalUsernameManager.didSetCorruptedUsername)
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
|
|
}
|
|
}
|
|
|
|
func testAvailableValidationFailsIfLinkDecryptFails() {
|
|
mockLocalUsernameManager.startingUsernameState = .available(
|
|
username: "boba_fett.42",
|
|
usernameLink: .mocked
|
|
)
|
|
mockMessageProcessor.processingResult = .value(())
|
|
mockStorageServiceManager.pendingRestoreResult = .value(())
|
|
mockAccountServiceClient.whoamiResponse = .value(.withRemoteUsername("boba_fett.42"))
|
|
mockUsernameLinkManager.decryptLinkResult = .error()
|
|
|
|
runRunRun()
|
|
|
|
mockDB.read { tx in
|
|
XCTAssertNil(validationManager.lastValidationDate(tx))
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
|
|
}
|
|
}
|
|
|
|
func testAvailableValidationFailsIfUsernameLinkMismatch() {
|
|
mockLocalUsernameManager.startingUsernameState = .available(
|
|
username: "boba_fett.42",
|
|
usernameLink: .mocked
|
|
)
|
|
mockMessageProcessor.processingResult = .value(())
|
|
mockStorageServiceManager.pendingRestoreResult = .value(())
|
|
mockAccountServiceClient.whoamiResponse = .value(.withRemoteUsername("boba_fett.42"))
|
|
mockUsernameLinkManager.decryptLinkResult = .value("boba_fett.43")
|
|
|
|
runRunRun()
|
|
|
|
mockDB.read { tx in
|
|
XCTAssertNil(validationManager.lastValidationDate(tx))
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
|
|
XCTAssertTrue(mockLocalUsernameManager.didSetCorruptedLink)
|
|
}
|
|
}
|
|
|
|
func testLinkCorruptedValidationSuccessful() {
|
|
mockLocalUsernameManager.startingUsernameState = .linkCorrupted(username: "boba_fett.42")
|
|
mockMessageProcessor.processingResult = .value(())
|
|
mockStorageServiceManager.pendingRestoreResult = .value(())
|
|
mockAccountServiceClient.whoamiResponse = .value(.withRemoteUsername("boba_fett.42"))
|
|
|
|
runRunRun()
|
|
|
|
mockDB.read { tx in
|
|
XCTAssertNotNil(validationManager.lastValidationDate(tx))
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
|
|
}
|
|
}
|
|
|
|
func testLinkCorruptedValidationFailsIfWhoamiFails() {
|
|
mockLocalUsernameManager.startingUsernameState = .linkCorrupted(username: "boba_fett.42")
|
|
mockMessageProcessor.processingResult = .value(())
|
|
mockStorageServiceManager.pendingRestoreResult = .value(())
|
|
mockAccountServiceClient.whoamiResponse = .error()
|
|
|
|
runRunRun()
|
|
|
|
mockDB.read { tx in
|
|
XCTAssertNil(validationManager.lastValidationDate(tx))
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
|
|
}
|
|
}
|
|
|
|
func testLinkCorruptedValidationFailsIfRemoteUsernameMismatch() {
|
|
mockLocalUsernameManager.startingUsernameState = .linkCorrupted(username: "boba_fett.42")
|
|
mockMessageProcessor.processingResult = .value(())
|
|
mockStorageServiceManager.pendingRestoreResult = .value(())
|
|
mockAccountServiceClient.whoamiResponse = .value(.withRemoteUsername("boba_fett.43"))
|
|
|
|
runRunRun()
|
|
|
|
mockDB.read { tx in
|
|
XCTAssertNil(validationManager.lastValidationDate(tx))
|
|
XCTAssertTrue(mockLocalUsernameManager.didSetCorruptedUsername)
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
|
|
}
|
|
}
|
|
|
|
func testUsernameCorruptedValidationSuccessful() {
|
|
mockLocalUsernameManager.startingUsernameState = .usernameAndLinkCorrupted
|
|
mockMessageProcessor.processingResult = .value(())
|
|
mockStorageServiceManager.pendingRestoreResult = .value(())
|
|
|
|
runRunRun()
|
|
|
|
mockDB.read { tx in
|
|
XCTAssertNotNil(validationManager.lastValidationDate(tx))
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
|
|
}
|
|
}
|
|
|
|
func testValidationFailsIfStorageServiceRestoreFails() {
|
|
mockLocalUsernameManager.startingUsernameState = .usernameAndLinkCorrupted
|
|
mockMessageProcessor.processingResult = .value(())
|
|
mockStorageServiceManager.pendingRestoreResult = .error()
|
|
|
|
runRunRun()
|
|
|
|
mockDB.read { tx in
|
|
XCTAssertNil(validationManager.lastValidationDate(tx))
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
|
|
}
|
|
}
|
|
|
|
func testValidationSkippedIfValidatedRecently() {
|
|
mockDB.write { tx in
|
|
validationManager.setLastValidation(
|
|
date: Date().addingTimeInterval(-100),
|
|
tx
|
|
)
|
|
}
|
|
|
|
runRunRun()
|
|
|
|
mockDB.read { tx in
|
|
XCTAssertNotNil(validationManager.lastValidationDate(tx))
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
|
|
}
|
|
}
|
|
|
|
func testValidationFiresIfValidatedAWhileAgo() {
|
|
mockDB.write { tx in
|
|
validationManager.setLastValidation(
|
|
date: Date().addingTimeInterval(-kDayInterval).addingTimeInterval(-1),
|
|
tx
|
|
)
|
|
}
|
|
|
|
mockLocalUsernameManager.startingUsernameState = .usernameAndLinkCorrupted
|
|
mockMessageProcessor.processingResult = .value(())
|
|
mockStorageServiceManager.pendingRestoreResult = .value(())
|
|
|
|
runRunRun()
|
|
|
|
mockDB.read { tx in
|
|
XCTAssertNotNil(validationManager.lastValidationDate(tx))
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
|
|
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension WhoAmIRequestFactory.Responses.WhoAmI {
|
|
static let noRemoteUsername: Self = .init(
|
|
aci: Aci.randomForTesting(),
|
|
pni: Pni.randomForTesting(),
|
|
e164: E164("+16125550101")!,
|
|
usernameHash: nil
|
|
)
|
|
|
|
static func withRemoteUsername(_ remoteUsername: String) -> Self {
|
|
return .init(
|
|
aci: Aci.randomForTesting(),
|
|
pni: Pni.randomForTesting(),
|
|
e164: E164("+16125550101")!,
|
|
usernameHash: try! Usernames.HashedUsername(forUsername: remoteUsername).hashString
|
|
)
|
|
}
|
|
}
|
|
|
|
private extension Usernames.UsernameLink {
|
|
static var mocked: Usernames.UsernameLink {
|
|
return Usernames.UsernameLink(
|
|
handle: UUID(),
|
|
entropy: Data(repeating: 5, count: 32)
|
|
)!
|
|
}
|
|
}
|
|
|
|
// MARK: - Mocks
|
|
|
|
extension UsernameValidationManagerTest {
|
|
private class MockStorageServiceManager: Usernames.Validation.Shims.StorageServiceManager {
|
|
var pendingRestoreResult: ConsumableMockPromise<Void> = .unset
|
|
|
|
public func waitForPendingRestores() -> Promise<Void> {
|
|
return pendingRestoreResult.consumeIntoPromise()
|
|
}
|
|
}
|
|
|
|
private class MockMessageProcessor: Usernames.Validation.Shims.MessageProcessor {
|
|
var processingResult: ConsumableMockGuarantee<Void> = .unset
|
|
|
|
public func waitForFetchingAndProcessing() -> Guarantee<Void> {
|
|
return processingResult.consumeIntoGuarantee()
|
|
}
|
|
}
|
|
|
|
private class MockAccountServiceClient: Usernames.Validation.Shims.AccountServiceClient {
|
|
var whoamiResponse: ConsumableMockPromise<WhoAmIRequestFactory.Responses.WhoAmI> = .unset
|
|
|
|
func getAccountWhoAmI() -> Promise<WhoAmIRequestFactory.Responses.WhoAmI> {
|
|
return whoamiResponse.consumeIntoPromise()
|
|
}
|
|
}
|
|
}
|