804 lines
26 KiB
Swift
804 lines
26 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import LibSignalClient
|
|
import SignalRingRTC
|
|
import XCTest
|
|
|
|
@testable import SignalServiceKit
|
|
|
|
class LocalUsernameManagerTests: XCTestCase {
|
|
private var mockDB: InMemoryDB!
|
|
private var testScheduler: TestScheduler!
|
|
|
|
private var mockReachabilityManager: MockReachabilityManager!
|
|
private var mockStorageServiceManager: MockStorageServiceManager!
|
|
private var mockUsernameApiClient: MockUsernameApiClient!
|
|
private var mockUsernameLinkManager: MockUsernameLinkManager!
|
|
|
|
private var kvStore: KeyValueStore!
|
|
|
|
private var localUsernameManager: LocalUsernameManager!
|
|
|
|
override func setUp() {
|
|
mockDB = InMemoryDB()
|
|
testScheduler = TestScheduler()
|
|
|
|
mockReachabilityManager = MockReachabilityManager()
|
|
mockStorageServiceManager = MockStorageServiceManager()
|
|
mockUsernameApiClient = MockUsernameApiClient()
|
|
mockUsernameLinkManager = MockUsernameLinkManager()
|
|
|
|
kvStore = KeyValueStore(collection: "localUsernameManager")
|
|
|
|
setLocalUsernameManager(maxNetworkRequestRetries: 0)
|
|
}
|
|
|
|
private func setLocalUsernameManager(maxNetworkRequestRetries: Int) {
|
|
localUsernameManager = LocalUsernameManagerImpl(
|
|
db: mockDB,
|
|
reachabilityManager: mockReachabilityManager,
|
|
schedulers: TestSchedulers(scheduler: testScheduler),
|
|
storageServiceManager: mockStorageServiceManager,
|
|
usernameApiClient: mockUsernameApiClient,
|
|
usernameLinkManager: mockUsernameLinkManager,
|
|
maxNetworkRequestRetries: maxNetworkRequestRetries
|
|
)
|
|
}
|
|
|
|
override func tearDown() {
|
|
mockUsernameApiClient.confirmationResult.ensureUnset()
|
|
mockUsernameApiClient.deletionResult.ensureUnset()
|
|
mockUsernameApiClient.setLinkResult.ensureUnset()
|
|
XCTAssertNil(mockUsernameLinkManager.entropyToGenerate)
|
|
}
|
|
|
|
// MARK: Local state changes
|
|
|
|
func testLocalUsernameStateChanges() {
|
|
let linkHandle = UUID()
|
|
|
|
XCTAssertEqual(usernameState(), .unset)
|
|
|
|
mockDB.write { tx in
|
|
localUsernameManager.setLocalUsername(
|
|
username: "boba-fett",
|
|
usernameLink: .mock(handle: linkHandle),
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
XCTAssertEqual(
|
|
usernameState(),
|
|
.available(username: "boba-fett", usernameLink: .mock(handle: linkHandle))
|
|
)
|
|
|
|
mockDB.write { tx in
|
|
localUsernameManager.setLocalUsernameWithCorruptedLink(
|
|
username: "boba-fett",
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
XCTAssertEqual(usernameState(), .linkCorrupted(username: "boba-fett"))
|
|
|
|
mockDB.write { tx in
|
|
localUsernameManager.clearLocalUsername(tx: tx)
|
|
}
|
|
|
|
XCTAssertEqual(usernameState(), .unset)
|
|
}
|
|
|
|
func testUsernameQRCodeColorChanges() {
|
|
func color() -> QRCodeColor {
|
|
return mockDB.read { tx in
|
|
return localUsernameManager.usernameLinkQRCodeColor(tx: tx)
|
|
}
|
|
}
|
|
|
|
XCTAssertEqual(color(), .unknown)
|
|
|
|
mockDB.write { tx in
|
|
localUsernameManager.setUsernameLinkQRCodeColor(
|
|
color: .olive,
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
XCTAssertEqual(color(), .olive)
|
|
}
|
|
|
|
// MARK: Confirmation
|
|
|
|
func testConfirmUsernameHappyPath() {
|
|
let linkHandle = UUID()
|
|
let username = "boba_fett.42"
|
|
|
|
mockUsernameLinkManager.entropyToGenerate = .success(.mockEntropy)
|
|
mockUsernameApiClient.confirmationResult = .value(.success(
|
|
usernameLinkHandle: linkHandle
|
|
))
|
|
|
|
XCTAssertEqual(usernameState(), .unset)
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.confirmUsername(
|
|
reservedUsername: .mock(username),
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(
|
|
guarantee.value,
|
|
.success(.success(username: username, usernameLink: .mock(handle: linkHandle)))
|
|
)
|
|
XCTAssertEqual(
|
|
usernameState(),
|
|
.available(username: username, usernameLink: .mock(handle: linkHandle))
|
|
)
|
|
XCTAssertTrue(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testConfirmBailsEarlyIfNotReachable() {
|
|
mockReachabilityManager.isReachable = false
|
|
|
|
let stateBeforeConfirm = setUsername(username: "boba_fett.42")
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.confirmUsername(
|
|
reservedUsername: .mock("boba_fett.43"),
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value, .failure(.networkError))
|
|
XCTAssertEqual(usernameState(), stateBeforeConfirm)
|
|
XCTAssertFalse(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testNoCorruptionIfFailToGenerateLink() {
|
|
mockUsernameLinkManager.entropyToGenerate = .failure(OWSGenericError("A Sarlacc"))
|
|
|
|
let stateBeforeConfirm = setUsername(username: "boba_fett.42")
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.confirmUsername(
|
|
reservedUsername: .mock("boba_fett.43"),
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value, .failure(.otherError))
|
|
XCTAssertEqual(usernameState(), stateBeforeConfirm)
|
|
XCTAssertFalse(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testCorruptionIfNetworkErrorWhileConfirming() {
|
|
mockUsernameLinkManager.entropyToGenerate = .success(.mockEntropy)
|
|
mockUsernameApiClient.confirmationResult = .error(OWSHTTPError.mockNetworkFailure)
|
|
|
|
XCTAssertEqual(usernameState(), .unset)
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.confirmUsername(
|
|
reservedUsername: .mock("boba_fett.42"),
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value, .failure(.networkError))
|
|
XCTAssertEqual(usernameState(), .usernameAndLinkCorrupted)
|
|
XCTAssertFalse(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testCorruptionIfErrorWhileConfirming() {
|
|
mockUsernameLinkManager.entropyToGenerate = .success(.mockEntropy)
|
|
mockUsernameApiClient.confirmationResult = .error()
|
|
|
|
XCTAssertEqual(usernameState(), .unset)
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.confirmUsername(
|
|
reservedUsername: .mock("boba_fett.42"),
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value, .failure(.otherError))
|
|
XCTAssertEqual(usernameState(), .usernameAndLinkCorrupted)
|
|
XCTAssertFalse(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testNoCorruptionIfRejectedWhileConfirming() {
|
|
mockUsernameLinkManager.entropyToGenerate = .success(.mockEntropy)
|
|
mockUsernameApiClient.confirmationResult = .value(.rejected)
|
|
|
|
let stateBeforeConfirm = setUsername(username: "boba_fett.42")
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.confirmUsername(
|
|
reservedUsername: .mock("boba_fett.43"),
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value, .success(.rejected))
|
|
XCTAssertEqual(usernameState(), stateBeforeConfirm)
|
|
XCTAssertFalse(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testNoCorruptionIfRateLimitedWhileConfirming() {
|
|
mockUsernameLinkManager.entropyToGenerate = .success(.mockEntropy)
|
|
mockUsernameApiClient.confirmationResult = .value(.rateLimited)
|
|
|
|
let stateBeforeConfirm = setUsername(username: "boba_fett.42")
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.confirmUsername(
|
|
reservedUsername: .mock("boba_fett.43"),
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value, .success(.rateLimited))
|
|
XCTAssertEqual(usernameState(), stateBeforeConfirm)
|
|
XCTAssertFalse(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testSuccessfulConfirmationClearsLinkCorruption() {
|
|
let newHandle = UUID()
|
|
|
|
mockUsernameLinkManager.entropyToGenerate = .success(.mockEntropy)
|
|
mockUsernameApiClient.confirmationResult = .value(.success(usernameLinkHandle: newHandle))
|
|
|
|
mockDB.write { tx in
|
|
localUsernameManager.setLocalUsernameWithCorruptedLink(
|
|
username: "boba_fett.42",
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
XCTAssertEqual(usernameState(), .linkCorrupted(username: "boba_fett.42"))
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.confirmUsername(
|
|
reservedUsername: try! Usernames.HashedUsername(forUsername: "boba_fett.43"),
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
let expectedNewLink = Usernames.UsernameLink(handle: newHandle, entropy: .mockEntropy)!
|
|
|
|
XCTAssertEqual(
|
|
guarantee.value,
|
|
.success(.success(username: "boba_fett.43", usernameLink: expectedNewLink))
|
|
)
|
|
XCTAssertEqual(
|
|
usernameState(),
|
|
.available(username: "boba_fett.43", usernameLink: expectedNewLink)
|
|
)
|
|
XCTAssertTrue(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testSuccessfulConfirmationClearsUsernameCorruption() {
|
|
let newHandle = UUID()
|
|
|
|
mockUsernameLinkManager.entropyToGenerate = .success(.mockEntropy)
|
|
mockUsernameApiClient.confirmationResult = .value(.success(usernameLinkHandle: newHandle))
|
|
|
|
mockDB.write { tx in
|
|
localUsernameManager.setLocalUsernameCorrupted(tx: tx)
|
|
}
|
|
|
|
XCTAssertEqual(usernameState(), .usernameAndLinkCorrupted)
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.confirmUsername(
|
|
reservedUsername: try! Usernames.HashedUsername(forUsername: "boba_fett.43"),
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
let expectedNewLink = Usernames.UsernameLink(handle: newHandle, entropy: .mockEntropy)!
|
|
|
|
XCTAssertEqual(
|
|
guarantee.value,
|
|
.success(.success(username: "boba_fett.43", usernameLink: expectedNewLink))
|
|
)
|
|
XCTAssertEqual(
|
|
usernameState(),
|
|
.available(username: "boba_fett.43", usernameLink: expectedNewLink)
|
|
)
|
|
XCTAssertTrue(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
// MARK: Deletion
|
|
|
|
func testDeletionHappyPath() {
|
|
mockUsernameApiClient.deletionResult = .value(())
|
|
|
|
_ = setUsername(username: "boba_fett.42")
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.deleteUsername(tx: tx)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value?.isSuccess, true)
|
|
XCTAssertEqual(usernameState(), .unset)
|
|
XCTAssertTrue(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testDeleteBailsEarlyIfNotReachable() {
|
|
mockReachabilityManager.isReachable = false
|
|
|
|
let stateBeforeConfirm = setUsername(username: "boba_fett.42")
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.deleteUsername(tx: tx)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value?.isNetworkError, true)
|
|
XCTAssertEqual(usernameState(), stateBeforeConfirm)
|
|
XCTAssertFalse(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testCorruptionIfNetworkErrorWhileDeleting() {
|
|
mockUsernameApiClient.deletionResult = .error(OWSHTTPError.mockNetworkFailure)
|
|
|
|
_ = setUsername(username: "boba_fett.42")
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.deleteUsername(tx: tx)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value?.isNetworkError, true)
|
|
XCTAssertEqual(usernameState(), .usernameAndLinkCorrupted)
|
|
XCTAssertFalse(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testCorruptionIfErrorWhileDeleting() {
|
|
mockUsernameApiClient.deletionResult = .error()
|
|
|
|
_ = setUsername(username: "boba_fett.42")
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.deleteUsername(tx: tx)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value?.isOtherError, true)
|
|
XCTAssertEqual(usernameState(), .usernameAndLinkCorrupted)
|
|
XCTAssertFalse(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testDeletionClearsCorruption() {
|
|
mockUsernameApiClient.deletionResult = .value(())
|
|
|
|
mockDB.write { tx in
|
|
localUsernameManager.setLocalUsernameCorrupted(tx: tx)
|
|
}
|
|
|
|
XCTAssertEqual(usernameState(), .usernameAndLinkCorrupted)
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.deleteUsername(tx: tx)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value?.isSuccess, true)
|
|
XCTAssertEqual(usernameState(), .unset)
|
|
XCTAssertTrue(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testDeletionClearsLinkCorruption() {
|
|
mockUsernameApiClient.deletionResult = .value(())
|
|
|
|
mockDB.write { tx in
|
|
localUsernameManager.setLocalUsernameWithCorruptedLink(
|
|
username: "boba_fett.42",
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
XCTAssertEqual(usernameState(), .linkCorrupted(username: "boba_fett.42"))
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.deleteUsername(tx: tx)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value?.isSuccess, true)
|
|
XCTAssertEqual(usernameState(), .unset)
|
|
XCTAssertTrue(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
// MARK: Rotate link
|
|
|
|
func testRotationHappyPath() {
|
|
let newHandle = UUID()
|
|
|
|
mockUsernameLinkManager.entropyToGenerate = .success(.mockEntropy)
|
|
mockUsernameApiClient.setLinkResult = .value(newHandle)
|
|
|
|
_ = setUsername(username: "boba_fett.42")
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.rotateUsernameLink(tx: tx)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
let expectedNewLink = Usernames.UsernameLink(handle: newHandle, entropy: .mockEntropy)!
|
|
|
|
XCTAssertEqual(guarantee.value, .success(expectedNewLink))
|
|
XCTAssertEqual(
|
|
usernameState(),
|
|
.available(username: "boba_fett.42", usernameLink: expectedNewLink)
|
|
)
|
|
XCTAssertTrue(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testRotationBailsEarlyIfNotReachable() {
|
|
mockReachabilityManager.isReachable = false
|
|
|
|
let stateBeforeConfirm = setUsername(username: "boba_fett.42")
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.rotateUsernameLink(tx: tx)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value!, .failure(.networkError))
|
|
XCTAssertEqual(usernameState(), stateBeforeConfirm)
|
|
XCTAssertFalse(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testNoCorruptionIfFailToGenerateNewLink() {
|
|
mockUsernameLinkManager.entropyToGenerate = .failure(OWSGenericError("Jabba's Sudden But Inevitable Betrayal"))
|
|
|
|
let stateBeforeRotate = setUsername(username: "boba_fett.42")
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.rotateUsernameLink(tx: tx)
|
|
}
|
|
|
|
XCTAssertEqual(guarantee.value, .failure(.otherError))
|
|
XCTAssertEqual(usernameState(), stateBeforeRotate)
|
|
XCTAssertFalse(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testCorruptionIfNetworkErrorWhileRotatingLink() {
|
|
mockUsernameLinkManager.entropyToGenerate = .success(.mockEntropy)
|
|
mockUsernameApiClient.setLinkResult = .error(OWSHTTPError.mockNetworkFailure)
|
|
|
|
_ = setUsername(username: "boba_fett.42")
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.rotateUsernameLink(tx: tx)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value, .failure(.networkError))
|
|
XCTAssertEqual(usernameState(), .linkCorrupted(username: "boba_fett.42"))
|
|
XCTAssertFalse(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testCorruptionIfErrorWhileRotatingLink() {
|
|
mockUsernameLinkManager.entropyToGenerate = .success(.mockEntropy)
|
|
mockUsernameApiClient.setLinkResult = .error()
|
|
|
|
_ = setUsername(username: "boba_fett.42")
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.rotateUsernameLink(tx: tx)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value, .failure(.otherError))
|
|
XCTAssertEqual(usernameState(), .linkCorrupted(username: "boba_fett.42"))
|
|
XCTAssertFalse(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testSuccessfulRotationClearsCorruption() {
|
|
let newHandle = UUID()
|
|
|
|
mockUsernameLinkManager.entropyToGenerate = .success(.mockEntropy)
|
|
mockUsernameApiClient.setLinkResult = .value(newHandle)
|
|
|
|
mockDB.write { tx in
|
|
localUsernameManager.setLocalUsernameWithCorruptedLink(
|
|
username: "boba_fett.42",
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
XCTAssertEqual(usernameState(), .linkCorrupted(username: "boba_fett.42"))
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.rotateUsernameLink(tx: tx)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
let expectedNewLink = Usernames.UsernameLink(handle: newHandle, entropy: .mockEntropy)!
|
|
|
|
XCTAssertEqual(guarantee.value, .success(expectedNewLink))
|
|
XCTAssertEqual(
|
|
usernameState(),
|
|
.available(username: "boba_fett.42", usernameLink: expectedNewLink)
|
|
)
|
|
XCTAssertTrue(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testUpdateVisibleCaseHappyPath() {
|
|
let linkHandle = UUID()
|
|
|
|
mockUsernameApiClient.setLinkMock = { _, keepLinkHandle in
|
|
XCTAssertTrue(keepLinkHandle)
|
|
return .value(linkHandle)
|
|
}
|
|
|
|
let currentLink = setUsername(username: "boba_fett.42", linkHandle: linkHandle).usernameLink!
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.updateVisibleCaseOfExistingUsername(
|
|
newUsername: "BoBa_fEtT.42",
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value?.isSuccess, true)
|
|
XCTAssertEqual(
|
|
usernameState(),
|
|
.available(username: "BoBa_fEtT.42", usernameLink: currentLink)
|
|
)
|
|
XCTAssertTrue(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testUpdateVisibleCaseBailsEarlyIfNotReachable() {
|
|
mockReachabilityManager.isReachable = false
|
|
|
|
let stateBeforeConfirm = setUsername(username: "boba_fett.42")
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.updateVisibleCaseOfExistingUsername(
|
|
newUsername: "BoBa_fEtT.42",
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value?.isNetworkError, true)
|
|
XCTAssertEqual(usernameState(), stateBeforeConfirm)
|
|
XCTAssertFalse(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testUpdateVisibleCaseSetsLocalEvenIfNetworkError() {
|
|
let linkHandle = UUID()
|
|
|
|
mockUsernameApiClient.setLinkMock = { _, keepLinkHandle in
|
|
XCTAssertTrue(keepLinkHandle)
|
|
return Promise(error: OWSHTTPError.mockNetworkFailure)
|
|
}
|
|
|
|
_ = setUsername(username: "boba_fett.42", linkHandle: linkHandle).usernameLink!
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.updateVisibleCaseOfExistingUsername(
|
|
newUsername: "BoBa_fEtT.42",
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value?.isNetworkError, true)
|
|
XCTAssertEqual(
|
|
usernameState(),
|
|
.linkCorrupted(username: "BoBa_fEtT.42")
|
|
)
|
|
XCTAssertTrue(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
func testUpdateVisibleCaseSetsLocalEvenIfError() {
|
|
let linkHandle = UUID()
|
|
|
|
mockUsernameApiClient.setLinkMock = { _, keepLinkHandle in
|
|
XCTAssertTrue(keepLinkHandle)
|
|
return Promise(error: OWSGenericError("oopsie"))
|
|
}
|
|
|
|
_ = setUsername(username: "boba_fett.42", linkHandle: linkHandle).usernameLink!
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.updateVisibleCaseOfExistingUsername(
|
|
newUsername: "BoBa_fEtT.42",
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(guarantee.value?.isOtherError, true)
|
|
XCTAssertEqual(
|
|
usernameState(),
|
|
.linkCorrupted(username: "BoBa_fEtT.42")
|
|
)
|
|
XCTAssertTrue(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
// MARK: Network retries
|
|
|
|
func testUpdateVisibleCaseWorkSecondTimeAfterNetworkError() {
|
|
setLocalUsernameManager(maxNetworkRequestRetries: 1)
|
|
|
|
var networkAttempts = 0
|
|
let linkHandle = UUID()
|
|
|
|
mockUsernameApiClient.setLinkMock = { _, keepLinkHandle in
|
|
networkAttempts += 1
|
|
XCTAssertTrue(keepLinkHandle)
|
|
|
|
if networkAttempts == 1 {
|
|
return Promise(error: OWSHTTPError.mockNetworkFailure)
|
|
}
|
|
|
|
return .value(linkHandle)
|
|
}
|
|
|
|
let currentLink = setUsername(username: "boba_fett.42", linkHandle: linkHandle).usernameLink!
|
|
|
|
let guarantee = mockDB.write { tx in
|
|
localUsernameManager.updateVisibleCaseOfExistingUsername(
|
|
newUsername: "BoBa_fEtT.42",
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
testScheduler.runUntilIdle()
|
|
|
|
XCTAssertEqual(networkAttempts, 2)
|
|
XCTAssertEqual(guarantee.value?.isSuccess, true)
|
|
XCTAssertEqual(
|
|
usernameState(),
|
|
.available(username: "BoBa_fEtT.42", usernameLink: currentLink)
|
|
)
|
|
XCTAssertTrue(mockStorageServiceManager.didRecordPendingLocalAccountUpdates)
|
|
}
|
|
|
|
// MARK: Utilities
|
|
|
|
private func setUsername(
|
|
username: String,
|
|
linkHandle: UUID? = nil
|
|
) -> Usernames.LocalUsernameState {
|
|
return mockDB.write { tx in
|
|
localUsernameManager.setLocalUsername(
|
|
username: username,
|
|
usernameLink: .mock(handle: linkHandle ?? UUID()),
|
|
tx: tx
|
|
)
|
|
|
|
return localUsernameManager.usernameState(tx: tx)
|
|
}
|
|
}
|
|
|
|
private func usernameState() -> Usernames.LocalUsernameState {
|
|
return mockDB.read { tx in
|
|
return localUsernameManager.usernameState(tx: tx)
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension Usernames.RemoteMutationResult<Void> {
|
|
var isSuccess: Bool {
|
|
switch self {
|
|
case .success: return true
|
|
case .failure: return false
|
|
}
|
|
}
|
|
|
|
var isNetworkError: Bool {
|
|
switch self {
|
|
case .failure(.networkError): return true
|
|
case .success, .failure(.otherError): return false
|
|
}
|
|
}
|
|
|
|
var isOtherError: Bool {
|
|
switch self {
|
|
case .failure(.otherError): return true
|
|
case .success, .failure(.networkError): return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Mocks
|
|
|
|
private extension OWSHTTPError {
|
|
static var mockNetworkFailure: OWSHTTPError {
|
|
return .networkFailure
|
|
}
|
|
}
|
|
|
|
private extension Usernames.HashedUsername {
|
|
static func mock(_ username: String) -> Usernames.HashedUsername {
|
|
try! Usernames.HashedUsername(forUsername: username)
|
|
}
|
|
}
|
|
|
|
private extension Usernames.UsernameLink {
|
|
static func mock(handle: UUID) -> Usernames.UsernameLink {
|
|
Usernames.UsernameLink(
|
|
handle: handle,
|
|
entropy: .mockEntropy
|
|
)!
|
|
}
|
|
}
|
|
|
|
private extension Data {
|
|
static let mockEntropy = Data(repeating: 12, count: 32)
|
|
}
|
|
|
|
private class MockReachabilityManager: SSKReachabilityManager {
|
|
var isReachable: Bool = true
|
|
func isReachable(via reachabilityType: ReachabilityType) -> Bool { owsFail("Not implemented!") }
|
|
}
|
|
|
|
private class MockStorageServiceManager: StorageServiceManager {
|
|
var didRecordPendingLocalAccountUpdates: Bool = false
|
|
|
|
func recordPendingLocalAccountUpdates() {
|
|
didRecordPendingLocalAccountUpdates = true
|
|
}
|
|
|
|
func setLocalIdentifiers(_ localIdentifiers: LocalIdentifiers) { owsFail("Not implemented!") }
|
|
func currentManifestVersion(tx: DBReadTransaction) -> UInt64 { owsFail("Not implemented") }
|
|
func currentManifestHasRecordIkm(tx: DBReadTransaction) -> Bool { owsFail("Not implemented") }
|
|
func waitForPendingRestores() -> Promise<Void> { owsFail("Not implemented") }
|
|
func resetLocalData(transaction: DBWriteTransaction) { owsFail("Not implemented!") }
|
|
func recordPendingUpdates(updatedRecipientUniqueIds: [RecipientUniqueId]) { owsFail("Not implemented!") }
|
|
func recordPendingUpdates(updatedAddresses: [SignalServiceAddress]) { owsFail("Not implemented!") }
|
|
func recordPendingUpdates(updatedGroupV2MasterKeys: [Data]) { owsFail("Not implemented!") }
|
|
func recordPendingUpdates(updatedStoryDistributionListIds: [Data]) { owsFail("Not implemented!") }
|
|
func recordPendingUpdates(callLinkRootKeys: [CallLinkRootKey]) { owsFail("Not implemented!") }
|
|
func recordPendingUpdates(groupModel: TSGroupModel) { owsFail("Not implemented!") }
|
|
func backupPendingChanges(authedDevice: AuthedDevice) { owsFail("Not implemented!") }
|
|
func restoreOrCreateManifestIfNecessary(authedDevice: AuthedDevice) -> Promise<Void> { owsFail("Not implemented!") }
|
|
func rotateManifest(mode: ManifestRotationMode, authedDevice: AuthedDevice) async throws { owsFail("Not implemented!") }
|
|
}
|