ios/AuthenticatorBridgeKit/Tests/AuthenticatorBridgeItemServiceTests.swift

448 lines
18 KiB
Swift

import AuthenticatorBridgeKit
import AuthenticatorBridgeKitMocks
import BitwardenKit
import BitwardenKitMocks
import Foundation
import TestHelpers
import XCTest
// swiftlint:disable file_length type_body_length
final class AuthenticatorBridgeItemServiceTests: AuthenticatorBridgeKitTestCase {
// MARK: Properties
let accessGroup = "group.com.example.bitwarden-authenticator"
var cryptoService: MockSharedCryptographyService!
var dataStore: AuthenticatorBridgeDataStore!
var errorReporter: ErrorReporter!
var keychainRepository: MockSharedKeychainRepository!
var sharedTimeoutService: MockSharedTimeoutService!
var subject: AuthenticatorBridgeItemService!
// MARK: Setup & Teardown
override func setUp() {
super.setUp()
cryptoService = MockSharedCryptographyService()
errorReporter = MockErrorReporter()
dataStore = AuthenticatorBridgeDataStore(
errorReporter: errorReporter,
groupIdentifier: accessGroup,
storeType: .memory,
)
keychainRepository = MockSharedKeychainRepository()
sharedTimeoutService = MockSharedTimeoutService()
subject = DefaultAuthenticatorBridgeItemService(
cryptoService: cryptoService,
dataStore: dataStore,
sharedKeychainRepository: keychainRepository,
sharedTimeoutService: sharedTimeoutService,
)
}
override func tearDown() {
cryptoService = nil
dataStore = nil
errorReporter = nil
keychainRepository = nil
sharedTimeoutService = nil
subject = nil
super.tearDown()
}
// MARK: Tests
/// `deleteAll()` deletes all items for all users in the shared store,
/// and deletes the authenticator key.
///
func test_deleteAll_success() async throws {
try await subject.insertItems(
AuthenticatorBridgeItemDataView.fixtures(),
forUserId: "userID 1",
)
try await subject.insertItems(
AuthenticatorBridgeItemDataView.fixtures(),
forUserId: "userID 2",
)
keychainRepository.authenticatorKey = keychainRepository.generateMockKeyData()
try await subject.deleteAll()
// Verify items were deleted
let fetchResultOne = try await subject.fetchAllForUserId("userID 1")
XCTAssertNotNil(fetchResultOne)
XCTAssertEqual(fetchResultOne.count, 0)
let fetchResultTwo = try await subject.fetchAllForUserId("userID 2")
XCTAssertNotNil(fetchResultTwo)
XCTAssertEqual(fetchResultTwo.count, 0)
XCTAssertNil(keychainRepository.authenticatorKey)
}
/// `deleteAll()` rethrows errors.
///
func test_deleteAll_error() async throws {
try await subject.insertItems(
AuthenticatorBridgeItemDataView.fixtures(),
forUserId: "userID 1",
)
try await subject.insertItems(
AuthenticatorBridgeItemDataView.fixtures(),
forUserId: "userID 2",
)
keychainRepository.errorToThrow = BitwardenTestError.example
await assertAsyncThrows(error: BitwardenTestError.example) {
try await subject.deleteAll()
}
}
/// Verify that the `deleteAllForUserId` method successfully deletes all of the data for a given
/// userId from the store. Verify that it does NOT delete the data for a different userId
///
func test_deleteAllForUserId_success() async throws {
let items = AuthenticatorBridgeItemDataView.fixtures()
// First Insert for "userId"
try await subject.insertItems(items, forUserId: "userId")
// Separate Insert for "differentUserId"
try await subject.insertItems(AuthenticatorBridgeItemDataView.fixtures(),
forUserId: "differentUserId")
// Remove the items for "differentUserId"
try await subject.deleteAllForUserId("differentUserId")
// Verify items are removed for "differentUserId"
let deletedFetchResult = try await subject.fetchAllForUserId("differentUserId")
XCTAssertNotNil(deletedFetchResult)
XCTAssertEqual(deletedFetchResult.count, 0)
// Verify items are still present for "userId"
let result = try await subject.fetchAllForUserId("userId")
XCTAssertNotNil(result)
XCTAssertEqual(result.count, items.count)
}
/// Verify that the `fetchAllForUserId` method successfully fetches the data for the given user id, and does not
/// include data for a different user id.
///
func test_fetchAllForUserId_success() async throws {
// Insert items for "userId"
let expectedItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id }
try await subject.insertItems(expectedItems, forUserId: "userId")
// Separate Insert for "differentUserId"
let differentUserItem = AuthenticatorBridgeItemDataView.fixture()
try await subject.insertItems([differentUserItem], forUserId: "differentUserId")
// Fetch should return only the expectedItem
let result = try await subject.fetchAllForUserId("userId")
XCTAssertTrue(cryptoService.decryptCalled,
"Items should have been decrypted when calling fetchAllForUser!")
XCTAssertNotNil(result)
XCTAssertEqual(result.count, expectedItems.count)
XCTAssertEqual(result, expectedItems)
// None of the items for userId should contain the item inserted for differentUserId
let emptyResult = result.filter { $0.id == differentUserItem.id }
XCTAssertEqual(emptyResult.count, 0)
}
/// When no temporary item has been stored, `fetchTemporaryItem()` returns `nil`
///
func test_fetchTemporaryItem_emptyResult() async throws {
let result = try await subject.fetchTemporaryItem()
XCTAssertNil(result)
}
/// Verify that the `fetchTemporaryItem()` is able to fetch the temporary item that was inserted
/// and removes it after fetching.
///
func test_fetchTemporaryItem_success() async throws {
let expectedItem = AuthenticatorBridgeItemDataView.fixture()
try await subject.insertTemporaryItem(expectedItem)
let result = try await subject.fetchTemporaryItem()
XCTAssertEqual(result, expectedItem)
let retryResult = try await subject.fetchTemporaryItem()
XCTAssertNil(retryResult)
}
/// Verify that the `insertItems(_:forUserId:)` method successfully inserts the list of items
/// for the given user id.
///
func test_insertItemsForUserId_success() async throws {
let expectedItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id }
try await subject.insertItems(expectedItems, forUserId: "userId")
let result = try await subject.fetchAllForUserId("userId")
XCTAssertTrue(cryptoService.encryptCalled,
"Items should have been encrypted before inserting!!")
XCTAssertEqual(result, expectedItems)
}
/// When `insertTemporaryItem(_)` is called multiple times, it should replace the temporary
/// item - i.e. only the last item will end up stored.
///
func test_insertTemporaryItem_replacesItem() async throws {
let initialItem = AuthenticatorBridgeItemDataView.fixture(name: "Initial Insert")
let expectedItem = AuthenticatorBridgeItemDataView.fixture()
try await subject.insertTemporaryItem(initialItem)
try await subject.insertTemporaryItem(expectedItem)
let result = try await subject.fetchTemporaryItem()
XCTAssertEqual(result, expectedItem)
}
/// Verify that the `insertTemporaryItem(_)` method successfully inserts a temporary item that is
/// then able to be retrieved by `fetchTemporaryItem()`.
///
func test_insertTemporaryItem_success() async throws {
let expectedItem = AuthenticatorBridgeItemDataView.fixture()
try await subject.insertTemporaryItem(expectedItem)
let result = try await subject.fetchTemporaryItem()
XCTAssertTrue(cryptoService.encryptCalled,
"Items should have been encrypted before inserting!!")
XCTAssertEqual(result, expectedItem)
}
/// Verify that `isSyncOn` returns false when the key is not present in the keychain.
///
func test_isSyncOn_false() async throws {
try keychainRepository.deleteAuthenticatorKey()
let sync = await subject.isSyncOn()
XCTAssertFalse(sync)
}
/// Verify that `isSyncOn` returns true when the key is present in the keychain.
///
func test_isSyncOn_true() async throws {
let key = keychainRepository.generateMockKeyData()
try await keychainRepository.setAuthenticatorKey(key)
let sync = await subject.isSyncOn()
XCTAssertTrue(sync)
}
/// Verify the `replaceAllItems` correctly deletes all of the items in the store previously when given
/// an empty list of items to insert for the given userId.
///
func test_replaceAllItems_emptyInsertDeletesExisting() async throws {
// Insert initial items for "userId"
let expectedItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id }
try await subject.insertItems(expectedItems, forUserId: "userId")
// Replace with empty list, deleting all
try await subject.replaceAllItems(with: [], forUserId: "userId")
let result = try await subject.fetchAllForUserId("userId")
XCTAssertEqual(result, [])
}
/// Verify the `replaceAllItems` correctly replaces all of the items in the store previously with the new
/// list of items for the given userId
///
func test_replaceAllItems_replacesExisting() async throws {
// Insert initial items for "userId"
let initialItems = [AuthenticatorBridgeItemDataView.fixture()]
try await subject.insertItems(initialItems, forUserId: "userId")
// Replace items for "userId"
let expectedItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id }
try await subject.replaceAllItems(with: expectedItems, forUserId: "userId")
let result = try await subject.fetchAllForUserId("userId")
XCTAssertTrue(cryptoService.encryptCalled,
"Items should have been encrypted before inserting!!")
XCTAssertEqual(result, expectedItems)
XCTAssertFalse(result.contains { $0 == initialItems.first })
}
/// Verify the `replaceAllItems` correctly inserts items when a userId doesn't contain any
/// items in the store previously.
///
func test_replaceAllItems_startingFromEmpty() async throws {
// Insert items for "userId"
let expectedItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id }
try await subject.replaceAllItems(with: expectedItems, forUserId: "userId")
let result = try await subject.fetchAllForUserId("userId")
XCTAssertTrue(cryptoService.encryptCalled,
"Items should have been encrypted before inserting!!")
XCTAssertEqual(result, expectedItems)
}
/// Verify that the shared items publisher publishes items for all users at once.
///
func test_sharedItemsPublisher_containsAllUsers() async throws {
let initialItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id }
let otherUserItems = [AuthenticatorBridgeItemDataView.fixture(name: "New Item")]
try await subject.insertItems(initialItems, forUserId: "userId")
try await subject.replaceAllItems(with: otherUserItems, forUserId: "differentUser")
var results: [[AuthenticatorBridgeItemDataView]] = []
let publisher = try await subject.sharedItemsPublisher()
.sink(
receiveCompletion: { _ in },
receiveValue: { value in
results.append(value)
},
)
defer { publisher.cancel() }
waitFor(results.count == 1)
let combined = (otherUserItems + initialItems)
XCTAssertEqual(results[0], combined)
}
/// Verify that the shared items publisher does not publish any temporary items
///
func test_sharedItemsPublisher_noTemporaryItems() async throws {
let expectedItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id }
try await subject.insertItems(expectedItems, forUserId: "userId")
try await subject.insertTemporaryItem(.fixture())
var results: [[AuthenticatorBridgeItemDataView]] = []
let publisher = try await subject.sharedItemsPublisher()
.sink(
receiveCompletion: { _ in },
receiveValue: { value in
results.append(value)
},
)
defer { publisher.cancel() }
try await subject.insertTemporaryItem(.fixture())
waitFor(results.count == 1)
XCTAssertEqual(results[0], expectedItems)
}
/// Verify that the shared items publisher publishes all the items inserted initially.
///
func test_sharedItemsPublisher_success() async throws {
let expectedItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id }
try await subject.insertItems(expectedItems, forUserId: "userId")
var results: [[AuthenticatorBridgeItemDataView]] = []
let publisher = try await subject.sharedItemsPublisher()
.sink(
receiveCompletion: { _ in },
receiveValue: { value in
results.append(value)
},
)
defer { publisher.cancel() }
waitFor(results.count == 1)
XCTAssertEqual(results[0], expectedItems)
}
/// Verify that the shared items publisher publishes new lists when items are deleted.
///
func test_sharedItemsPublisher_withDeletes() async throws {
let initialItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id }
try await subject.insertItems(initialItems, forUserId: "userId")
var iterator = try await subject.sharedItemsPublisher().valuesWithTimeout().makeAsyncIterator()
let firstValue = try await iterator.next()
XCTAssertEqual(firstValue, initialItems)
try await subject.replaceAllItems(with: [], forUserId: "userId")
let secondValue = try await iterator.next()
XCTAssertEqual(secondValue, [])
}
/// Verify that the shared items publisher publishes items that are inserted/replaced later.
///
func test_sharedItemsPublisher_withUpdates() async throws {
let initialItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id }
try await subject.insertItems(initialItems, forUserId: "userId")
var iterator = try await subject.sharedItemsPublisher().valuesWithTimeout().makeAsyncIterator()
let firstValue = try await iterator.next()
XCTAssertEqual(firstValue, initialItems)
let replacedItems = [AuthenticatorBridgeItemDataView.fixture(name: "New Item")]
try await subject.replaceAllItems(with: replacedItems, forUserId: "userId")
let secondValue = try await iterator.next()
XCTAssertEqual(secondValue, replacedItems)
}
/// The shared items publisher deletes items if the user is timed out.
///
func test_sharedItemsPublisher_deletesItemsOnTimeout() async throws {
let pastTimeoutItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id }
let withinTimeoutItems = [AuthenticatorBridgeItemDataView.fixture(name: "New Item")]
try await subject.insertItems(pastTimeoutItems, forUserId: "pastTimeoutUserId")
try await subject.replaceAllItems(with: withinTimeoutItems, forUserId: "withinTimeoutUserId")
sharedTimeoutService.hasPassedTimeoutResult = .success([
"pastTimeoutUserId": true,
"withinTimeoutUserId": false,
])
var results: [[AuthenticatorBridgeItemDataView]] = []
let publisher = try await subject.sharedItemsPublisher()
.sink(
receiveCompletion: { _ in },
receiveValue: { value in
results.append(value)
},
)
defer { publisher.cancel() }
// Verify items are removed for "userId"
let itemsForPastTimeoutUser = try await subject.fetchAllForUserId("pastTimeoutUserId")
XCTAssertNotNil(itemsForPastTimeoutUser)
XCTAssertEqual(itemsForPastTimeoutUser.count, 0)
// Verify items are still present for "differentUserId"
let itemsForWithinTimeoutUser = try await subject.fetchAllForUserId("withinTimeoutUserId")
XCTAssertNotNil(itemsForWithinTimeoutUser)
XCTAssertEqual(itemsForWithinTimeoutUser.count, withinTimeoutItems.count)
}
/// `sharedItemsPublisher()` throws if checking for logout throws
///
func test_sharedItemsPublisher_logoutError() async throws {
let initialItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id }
try await subject.insertItems(initialItems, forUserId: "userId")
sharedTimeoutService.hasPassedTimeoutResult = .failure(BitwardenTestError.example)
await assertAsyncThrows(error: BitwardenTestError.example) {
var results: [[AuthenticatorBridgeItemDataView]] = []
let publisher = try await subject.sharedItemsPublisher()
.sink(
receiveCompletion: { _ in },
receiveValue: { value in
results.append(value)
},
)
publisher.cancel()
}
XCTAssertFalse(cryptoService.decryptCalled)
}
}
// swiftlint:enable file_length type_body_length