TM-SGNL-iOS/SignalServiceKit/tests/Usernames/UsernameValidationManagerTests.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

355 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 mockContext: UsernameValidationManagerImpl.Context!
private var mockDB: (any DB)!
private var mockLocalUsernameManager: MockLocalUsernameManager!
private var mockMessageProcessor: MockMessageProcessor!
private var mockStorageServiceManager: MockStorageServiceManager!
private var mockUsernameLinkManager: MockUsernameLinkManager!
private var mockWhoAmIManager: MockWhoAmIManager!
private var validationManager: UsernameValidationManagerImpl!
override func setUp() {
mockDB = InMemoryDB()
mockLocalUsernameManager = MockLocalUsernameManager()
mockMessageProcessor = MockMessageProcessor()
mockStorageServiceManager = MockStorageServiceManager()
mockUsernameLinkManager = MockUsernameLinkManager()
mockWhoAmIManager = MockWhoAmIManager()
validationManager = UsernameValidationManagerImpl(context: .init(
database: mockDB,
localUsernameManager: mockLocalUsernameManager,
messageProcessor: mockMessageProcessor,
storageServiceManager: mockStorageServiceManager,
usernameLinkManager: mockUsernameLinkManager,
whoAmIManager: mockWhoAmIManager
))
}
override func tearDown() {
mockWhoAmIManager.whoamiResponse.ensureUnset()
mockMessageProcessor.processingResult.ensureUnset()
mockStorageServiceManager.pendingRestoreResult.ensureUnset()
mockUsernameLinkManager.decryptLinkResult.ensureUnset()
}
func testUnsetValidationSuccessful() async {
mockLocalUsernameManager.startingUsernameState = .unset
mockMessageProcessor.processingResult = .value(())
mockStorageServiceManager.pendingRestoreResult = .value(())
mockWhoAmIManager.whoamiResponse = .value(.noRemoteUsername)
await validationManager.validateUsernameIfNecessary()
mockDB.read { tx in
XCTAssertNotNil(validationManager.lastValidationDate(tx))
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
}
}
func testUnsetValidationFailsIfWhoamiFails() async {
mockLocalUsernameManager.startingUsernameState = .unset
mockMessageProcessor.processingResult = .value(())
mockStorageServiceManager.pendingRestoreResult = .value(())
mockWhoAmIManager.whoamiResponse = .error()
await validationManager.validateUsernameIfNecessary()
mockDB.read { tx in
XCTAssertNil(validationManager.lastValidationDate(tx))
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
}
}
func testUnsetValidationFailsIfRemoteUsernamePresent() async {
mockLocalUsernameManager.startingUsernameState = .unset
mockMessageProcessor.processingResult = .value(())
mockStorageServiceManager.pendingRestoreResult = .value(())
mockWhoAmIManager.whoamiResponse = .value(.withRemoteUsername("boba_fett.42"))
await validationManager.validateUsernameIfNecessary()
mockDB.read { tx in
XCTAssertNil(validationManager.lastValidationDate(tx))
XCTAssertTrue(mockLocalUsernameManager.didSetCorruptedUsername)
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
}
}
func testAvailableValidationSuccessful() async {
mockLocalUsernameManager.startingUsernameState = .available(
username: "boba_fett.42",
usernameLink: .mocked
)
mockMessageProcessor.processingResult = .value(())
mockStorageServiceManager.pendingRestoreResult = .value(())
mockWhoAmIManager.whoamiResponse = .value(.withRemoteUsername("boba_fett.42"))
mockUsernameLinkManager.decryptLinkResult = .value("boba_fett.42")
await validationManager.validateUsernameIfNecessary()
mockDB.read { tx in
XCTAssertNotNil(validationManager.lastValidationDate(tx))
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
}
}
func testAvailableValidationFailsIfWhoamiFails() async {
mockLocalUsernameManager.startingUsernameState = .available(
username: "boba_fett.42",
usernameLink: .mocked
)
mockMessageProcessor.processingResult = .value(())
mockStorageServiceManager.pendingRestoreResult = .value(())
mockWhoAmIManager.whoamiResponse = .error()
await validationManager.validateUsernameIfNecessary()
mockDB.read { tx in
XCTAssertNil(validationManager.lastValidationDate(tx))
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
}
}
func testAvailableValidationFailsIfRemoteUsernameMismatch() async {
mockLocalUsernameManager.startingUsernameState = .available(
username: "boba_fett.42",
usernameLink: .mocked
)
mockMessageProcessor.processingResult = .value(())
mockStorageServiceManager.pendingRestoreResult = .value(())
mockWhoAmIManager.whoamiResponse = .value(.withRemoteUsername("boba_fett.43"))
await validationManager.validateUsernameIfNecessary()
mockDB.read { tx in
XCTAssertNil(validationManager.lastValidationDate(tx))
XCTAssertTrue(mockLocalUsernameManager.didSetCorruptedUsername)
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
}
}
func testAvailableValidationFailsIfLinkDecryptFails() async {
mockLocalUsernameManager.startingUsernameState = .available(
username: "boba_fett.42",
usernameLink: .mocked
)
mockMessageProcessor.processingResult = .value(())
mockStorageServiceManager.pendingRestoreResult = .value(())
mockWhoAmIManager.whoamiResponse = .value(.withRemoteUsername("boba_fett.42"))
mockUsernameLinkManager.decryptLinkResult = .error()
await validationManager.validateUsernameIfNecessary()
mockDB.read { tx in
XCTAssertNil(validationManager.lastValidationDate(tx))
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
}
}
func testAvailableValidationFailsIfUsernameLinkMismatch() async {
mockLocalUsernameManager.startingUsernameState = .available(
username: "boba_fett.42",
usernameLink: .mocked
)
mockMessageProcessor.processingResult = .value(())
mockStorageServiceManager.pendingRestoreResult = .value(())
mockWhoAmIManager.whoamiResponse = .value(.withRemoteUsername("boba_fett.42"))
mockUsernameLinkManager.decryptLinkResult = .value("boba_fett.43")
await validationManager.validateUsernameIfNecessary()
mockDB.read { tx in
XCTAssertNil(validationManager.lastValidationDate(tx))
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
XCTAssertTrue(mockLocalUsernameManager.didSetCorruptedLink)
}
}
func testLinkCorruptedValidationSuccessful() async {
mockLocalUsernameManager.startingUsernameState = .linkCorrupted(username: "boba_fett.42")
mockMessageProcessor.processingResult = .value(())
mockStorageServiceManager.pendingRestoreResult = .value(())
mockWhoAmIManager.whoamiResponse = .value(.withRemoteUsername("boba_fett.42"))
await validationManager.validateUsernameIfNecessary()
mockDB.read { tx in
XCTAssertNotNil(validationManager.lastValidationDate(tx))
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
}
}
func testLinkCorruptedValidationFailsIfWhoamiFails() async {
mockLocalUsernameManager.startingUsernameState = .linkCorrupted(username: "boba_fett.42")
mockMessageProcessor.processingResult = .value(())
mockStorageServiceManager.pendingRestoreResult = .value(())
mockWhoAmIManager.whoamiResponse = .error()
await validationManager.validateUsernameIfNecessary()
mockDB.read { tx in
XCTAssertNil(validationManager.lastValidationDate(tx))
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
}
}
func testLinkCorruptedValidationFailsIfRemoteUsernameMismatch() async {
mockLocalUsernameManager.startingUsernameState = .linkCorrupted(username: "boba_fett.42")
mockMessageProcessor.processingResult = .value(())
mockStorageServiceManager.pendingRestoreResult = .value(())
mockWhoAmIManager.whoamiResponse = .value(.withRemoteUsername("boba_fett.43"))
await validationManager.validateUsernameIfNecessary()
mockDB.read { tx in
XCTAssertNil(validationManager.lastValidationDate(tx))
XCTAssertTrue(mockLocalUsernameManager.didSetCorruptedUsername)
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
}
}
func testUsernameCorruptedValidationSuccessful() async {
mockLocalUsernameManager.startingUsernameState = .usernameAndLinkCorrupted
mockMessageProcessor.processingResult = .value(())
mockStorageServiceManager.pendingRestoreResult = .value(())
await validationManager.validateUsernameIfNecessary()
mockDB.read { tx in
XCTAssertNotNil(validationManager.lastValidationDate(tx))
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
}
}
func testValidationFailsIfStorageServiceRestoreFails() async {
mockLocalUsernameManager.startingUsernameState = .usernameAndLinkCorrupted
mockMessageProcessor.processingResult = .value(())
mockStorageServiceManager.pendingRestoreResult = .error()
await validationManager.validateUsernameIfNecessary()
mockDB.read { tx in
XCTAssertNil(validationManager.lastValidationDate(tx))
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
}
}
func testValidationSkippedIfValidatedRecently() async {
mockDB.write { tx in
validationManager.setLastValidation(
date: Date().addingTimeInterval(-100),
tx
)
}
await validationManager.validateUsernameIfNecessary()
mockDB.read { tx in
XCTAssertNotNil(validationManager.lastValidationDate(tx))
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
}
}
func testValidationFiresIfValidatedAWhileAgo() async {
mockDB.write { tx in
validationManager.setLastValidation(
date: Date().addingTimeInterval(-kDayInterval).addingTimeInterval(-1),
tx
)
}
mockLocalUsernameManager.startingUsernameState = .usernameAndLinkCorrupted
mockMessageProcessor.processingResult = .value(())
mockStorageServiceManager.pendingRestoreResult = .value(())
await validationManager.validateUsernameIfNecessary()
mockDB.read { tx in
XCTAssertNotNil(validationManager.lastValidationDate(tx))
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedUsername)
XCTAssertFalse(mockLocalUsernameManager.didSetCorruptedLink)
}
}
}
private extension WhoAmIManager.WhoAmIResponse {
static let noRemoteUsername: Self = .init(
aci: Aci.randomForTesting(),
pni: Pni.randomForTesting(),
e164: E164("+16125550101")!,
usernameHash: nil,
entitlements: Entitlements(backup: nil, badges: [])
)
static func withRemoteUsername(_ remoteUsername: String) -> Self {
return .init(
aci: Aci.randomForTesting(),
pni: Pni.randomForTesting(),
e164: E164("+16125550101")!,
usernameHash: try! Usernames.HashedUsername(forUsername: remoteUsername).hashString,
entitlements: Entitlements(backup: nil, badges: [])
)
}
}
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 MockWhoAmIManager: WhoAmIManager {
var whoamiResponse: ConsumableMockPromise<WhoAmIResponse> = .unset
func makeWhoAmIRequest() async throws -> WhoAmIResponse {
return try await whoamiResponse.consumeIntoPromise().awaitable()
}
}
}