diff --git a/AuthenticatorBridgeKit/Tests/AuthenticatorBridgeItemServiceTests.swift b/AuthenticatorBridgeKit/Tests/AuthenticatorBridgeItemServiceTests.swift index 26bc690b4..a28a6da07 100644 --- a/AuthenticatorBridgeKit/Tests/AuthenticatorBridgeItemServiceTests.swift +++ b/AuthenticatorBridgeKit/Tests/AuthenticatorBridgeItemServiceTests.swift @@ -355,21 +355,15 @@ final class AuthenticatorBridgeItemServiceTests: AuthenticatorBridgeKitTestCase let initialItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id } try await subject.insertItems(initialItems, forUserId: "userId") - var results: [[AuthenticatorBridgeItemDataView]] = [] - let publisher = try await subject.sharedItemsPublisher() - .sink( - receiveCompletion: { _ in }, - receiveValue: { value in - results.append(value) - }, - ) - defer { publisher.cancel() } + var iterator = try await subject.sharedItemsPublisher().valuesWithTimeout().makeAsyncIterator() + + let firstValue = try await iterator.next() + XCTAssertEqual(firstValue, initialItems) try await subject.replaceAllItems(with: [], forUserId: "userId") - waitFor(results.count == 2) - XCTAssertEqual(results[0], initialItems) - XCTAssertEqual(results[1], []) + let secondValue = try await iterator.next() + XCTAssertEqual(secondValue, []) } /// Verify that the shared items publisher publishes items that are inserted/replaced later. @@ -378,22 +372,16 @@ final class AuthenticatorBridgeItemServiceTests: AuthenticatorBridgeKitTestCase let initialItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id } try await subject.insertItems(initialItems, forUserId: "userId") - var results: [[AuthenticatorBridgeItemDataView]] = [] - let publisher = try await subject.sharedItemsPublisher() - .sink( - receiveCompletion: { _ in }, - receiveValue: { value in - results.append(value) - }, - ) - defer { publisher.cancel() } + 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") - waitFor(results.count == 2) - XCTAssertEqual(results[0], initialItems) - XCTAssertEqual(results[1], replacedItems) + let secondValue = try await iterator.next() + XCTAssertEqual(secondValue, replacedItems) } /// The shared items publisher deletes items if the user is timed out. diff --git a/AuthenticatorShared/Core/Vault/Services/Stores/AuthenticatorItemDataStoreTests.swift b/AuthenticatorShared/Core/Vault/Services/Stores/AuthenticatorItemDataStoreTests.swift index 7db9847b8..59156c35e 100644 --- a/AuthenticatorShared/Core/Vault/Services/Stores/AuthenticatorItemDataStoreTests.swift +++ b/AuthenticatorShared/Core/Vault/Services/Stores/AuthenticatorItemDataStoreTests.swift @@ -33,21 +33,15 @@ class AuthenticatorItemDataStoreTests: BitwardenTestCase { /// `authenticatorItemPublisher(userId:)` returns a publisher for a user's authenticatorItem objects. func test_authenticatorItemPublisher() async throws { - var publishedValues = [[AuthenticatorItem]]() - let publisher = subject.authenticatorItemPublisher(userId: "1") - .sink( - receiveCompletion: { _ in }, - receiveValue: { values in - publishedValues.append(values) - }, - ) - defer { publisher.cancel() } + var iterator = subject.authenticatorItemPublisher(userId: "1").valuesWithTimeout().makeAsyncIterator() + + let firstValue = try await iterator.next() + XCTAssertEqual(firstValue, []) try await subject.replaceAuthenticatorItems(authenticatorItems, userId: "1") - waitFor { publishedValues.count == 2 } - XCTAssertTrue(publishedValues[0].isEmpty) - XCTAssertEqual(publishedValues[1], authenticatorItems) + let secondValue = try await iterator.next() + XCTAssertEqual(secondValue, authenticatorItems) } /// `deleteAllAuthenticatorItems(user:)` removes all objects for the user. diff --git a/BitwardenShared/Core/Tools/Services/GeneratorDataStoreTests.swift b/BitwardenShared/Core/Tools/Services/GeneratorDataStoreTests.swift index 2d3fd9ba5..804ea2e19 100644 --- a/BitwardenShared/Core/Tools/Services/GeneratorDataStoreTests.swift +++ b/BitwardenShared/Core/Tools/Services/GeneratorDataStoreTests.swift @@ -120,28 +120,24 @@ class GeneratorDataStoreTests: BitwardenTestCase { /// `passwordHistoryPublisher(userId:)` returns a publisher for a user's password history objects. @MainActor func test_passwordHistoryPublisher() async throws { - var publishedValues = [[PasswordHistory]]() - let publisher = subject.passwordHistoryPublisher(userId: "1") - .sink( - receiveCompletion: { _ in }, - receiveValue: { values in - publishedValues.append(values) - }, - ) - defer { publisher.cancel() } + var iterator = subject.passwordHistoryPublisher(userId: "1").valuesWithTimeout().makeAsyncIterator() + + let firstValue = try await iterator.next() + XCTAssertEqual(firstValue, []) let passwordHistory1 = PasswordHistory(password: "PASSWORD1", lastUsedDate: Date()) try await subject.insertPasswordHistory(userId: "1", passwordHistory: passwordHistory1) + let secondValue = try await iterator.next() + XCTAssertEqual(secondValue, [passwordHistory1]) + let passwordHistoryOther = PasswordHistory(password: "PASSWORD_OTHER", lastUsedDate: Date()) try await subject.insertPasswordHistory(userId: "2", passwordHistory: passwordHistoryOther) let passwordHistory2 = PasswordHistory(password: "PASSWORD2", lastUsedDate: Date()) try await subject.insertPasswordHistory(userId: "1", passwordHistory: passwordHistory2) - waitFor { publishedValues.count == 3 } - XCTAssertTrue(publishedValues[0].isEmpty) - XCTAssertEqual(publishedValues[1], [passwordHistory1]) - XCTAssertEqual(publishedValues[2], [passwordHistory2, passwordHistory1]) + let thirdValue = try await iterator.next() + XCTAssertEqual(thirdValue, [passwordHistory2, passwordHistory1]) } } diff --git a/BitwardenShared/Core/Tools/Services/Stores/SendDataStoreTests.swift b/BitwardenShared/Core/Tools/Services/Stores/SendDataStoreTests.swift index f24893aa5..ddad1271f 100644 --- a/BitwardenShared/Core/Tools/Services/Stores/SendDataStoreTests.swift +++ b/BitwardenShared/Core/Tools/Services/Stores/SendDataStoreTests.swift @@ -57,40 +57,28 @@ class SendDataStoreTests: BitwardenTestCase { /// `sendPublisher(userId:)` returns a publisher for a single send. func test_sendPublisher() async throws { - var publishedValues = [Send?]() - let publisher = subject.sendPublisher(id: "1", userId: "1") - .sink( - receiveCompletion: { _ in }, - receiveValue: { value in - publishedValues.append(value) - }, - ) - defer { publisher.cancel() } + var iterator = subject.sendPublisher(id: "1", userId: "1").valuesWithTimeout().makeAsyncIterator() + + let firstValue = try await iterator.next() + XCTAssertEqual(firstValue, .some(nil)) try await subject.replaceSends(sends, userId: "1") - waitFor { publishedValues.count == 2 } - XCTAssertNil(publishedValues[0]) - XCTAssertEqual(publishedValues[1], Send.fixture(id: "1", name: "SEND1")) + let secondValue = try await iterator.next() + XCTAssertEqual(secondValue, Send.fixture(id: "1", name: "SEND1")) } /// `sendsPublisher(userId:)` returns a publisher for a user's send objects. func test_sendsPublisher() async throws { - var publishedValues = [[Send]]() - let publisher = subject.sendsPublisher(userId: "1") - .sink( - receiveCompletion: { _ in }, - receiveValue: { values in - publishedValues.append(values) - }, - ) - defer { publisher.cancel() } + var iterator = subject.sendsPublisher(userId: "1").valuesWithTimeout().makeAsyncIterator() + + let firstValue = try await iterator.next() + XCTAssertEqual(firstValue, []) try await subject.replaceSends(sends, userId: "1") - waitFor { publishedValues.count == 2 } - XCTAssertTrue(publishedValues[0].isEmpty) - XCTAssertEqual(publishedValues[1], sends) + let secondValue = try await iterator.next() + XCTAssertEqual(secondValue, sends) } /// `replaceSends(_:userId)` replaces the list of sends for the user. diff --git a/BitwardenShared/Core/Vault/Services/Stores/CipherDataStoreTests.swift b/BitwardenShared/Core/Vault/Services/Stores/CipherDataStoreTests.swift index 14b337804..ee2014937 100644 --- a/BitwardenShared/Core/Vault/Services/Stores/CipherDataStoreTests.swift +++ b/BitwardenShared/Core/Vault/Services/Stores/CipherDataStoreTests.swift @@ -46,21 +46,15 @@ class CipherDataStoreTests: BitwardenTestCase { /// `cipherPublisher(userId:)` returns a publisher for a user's cipher objects. func test_cipherPublisher() async throws { - var publishedValues = [[Cipher]]() - let publisher = subject.cipherPublisher(userId: "1") - .sink( - receiveCompletion: { _ in }, - receiveValue: { values in - publishedValues.append(values) - }, - ) - defer { publisher.cancel() } + var iterator = subject.cipherPublisher(userId: "1").valuesWithTimeout().makeAsyncIterator() + + let firstValue = try await iterator.next() + XCTAssertEqual(firstValue, []) try await subject.replaceCiphers(ciphers, userId: "1") - waitFor { publishedValues.count == 2 } - XCTAssertTrue(publishedValues[0].isEmpty) - XCTAssertEqual(publishedValues[1], ciphers) + let secondValue = try await iterator.next() + XCTAssertEqual(secondValue, ciphers) } /// `cipherChangesPublisher(userId:)` emits inserted ciphers for the user. diff --git a/BitwardenShared/Core/Vault/Services/Stores/CollectionDataStoreTests.swift b/BitwardenShared/Core/Vault/Services/Stores/CollectionDataStoreTests.swift index 32147d2a4..2ec5f8e2d 100644 --- a/BitwardenShared/Core/Vault/Services/Stores/CollectionDataStoreTests.swift +++ b/BitwardenShared/Core/Vault/Services/Stores/CollectionDataStoreTests.swift @@ -34,21 +34,15 @@ class CollectionDataStoreTests: BitwardenTestCase { /// `collectionPublisher(userId:)` returns a publisher for a user's collection objects. func test_collectionPublisher() async throws { - var publishedValues = [[Collection]]() - let publisher = subject.collectionPublisher(userId: "1") - .sink( - receiveCompletion: { _ in }, - receiveValue: { values in - publishedValues.append(values) - }, - ) - defer { publisher.cancel() } + var iterator = subject.collectionPublisher(userId: "1").valuesWithTimeout().makeAsyncIterator() + + let firstValue = try await iterator.next() + XCTAssertEqual(firstValue, []) try await subject.replaceCollections(collections, userId: "1") - waitFor { publishedValues.count == 2 } - XCTAssertTrue(publishedValues[0].isEmpty) - XCTAssertEqual(publishedValues[1], collections) + let secondValue = try await iterator.next() + XCTAssertEqual(secondValue, collections) } /// `deleteAllCollections(user:)` removes all objects for the user. diff --git a/BitwardenShared/Core/Vault/Services/Stores/FolderDataStoreTests.swift b/BitwardenShared/Core/Vault/Services/Stores/FolderDataStoreTests.swift index 7ec95cc15..88a232064 100644 --- a/BitwardenShared/Core/Vault/Services/Stores/FolderDataStoreTests.swift +++ b/BitwardenShared/Core/Vault/Services/Stores/FolderDataStoreTests.swift @@ -68,21 +68,15 @@ class FolderDataStoreTests: BitwardenTestCase { /// `folderPublisher(userId:)` returns a publisher for a user's folder objects. func test_folderPublisher() async throws { - var publishedValues = [[Folder]]() - let publisher = subject.folderPublisher(userId: "1") - .sink( - receiveCompletion: { _ in }, - receiveValue: { values in - publishedValues.append(values) - }, - ) - defer { publisher.cancel() } + var iterator = subject.folderPublisher(userId: "1").valuesWithTimeout().makeAsyncIterator() + + let firstValue = try await iterator.next() + XCTAssertEqual(firstValue, []) try await subject.replaceFolders(folders, userId: "1") - waitFor { publishedValues.count == 2 } - XCTAssertTrue(publishedValues[0].isEmpty) - XCTAssertEqual(publishedValues[1], folders) + let secondValue = try await iterator.next() + XCTAssertEqual(secondValue, folders) } /// `replaceFolders(_:userId)` replaces the list of folders for the user. diff --git a/BitwardenShared/Core/Vault/Services/Stores/OrganizationDataStoreTests.swift b/BitwardenShared/Core/Vault/Services/Stores/OrganizationDataStoreTests.swift index b2680db6b..1a27a3065 100644 --- a/BitwardenShared/Core/Vault/Services/Stores/OrganizationDataStoreTests.swift +++ b/BitwardenShared/Core/Vault/Services/Stores/OrganizationDataStoreTests.swift @@ -45,21 +45,15 @@ class OrganizationDataStoreTests: BitwardenTestCase { /// `organizationPublisher(userId:)` returns a publisher for a user's organization objects. func test_organizationPublisher() async throws { - var publishedValues = [[Organization]]() - let publisher = subject.organizationPublisher(userId: "1") - .sink( - receiveCompletion: { _ in }, - receiveValue: { values in - publishedValues.append(values) - }, - ) - defer { publisher.cancel() } + var iterator = subject.organizationPublisher(userId: "1").valuesWithTimeout().makeAsyncIterator() + + let firstValue = try await iterator.next() + XCTAssertEqual(firstValue, []) try await subject.replaceOrganizations(organizations, userId: "1") - waitFor { publishedValues.count == 2 } - XCTAssertTrue(publishedValues[0].isEmpty) - XCTAssertEqual(publishedValues[1], organizations.compactMap(Organization.init)) + let secondValue = try await iterator.next() + XCTAssertEqual(secondValue, organizations.compactMap(Organization.init)) } /// `fetchAllOrganizations(userId:)` fetches all organizations for a user. diff --git a/TestHelpers/Extensions/Publisher+ValuesWithTimeout.swift b/TestHelpers/Extensions/Publisher+ValuesWithTimeout.swift new file mode 100644 index 000000000..aa1c6ed94 --- /dev/null +++ b/TestHelpers/Extensions/Publisher+ValuesWithTimeout.swift @@ -0,0 +1,27 @@ +import Combine +import Foundation + +/// An error thrown when a publisher times out while awaiting values. +public struct PublisherTimeoutError: Error {} + +public extension Publisher where Failure == Error { + /// Returns an async sequence of the publisher's values with a timeout. + /// + /// This is useful in tests where you want to await publisher values without risking test hangs + /// if the publisher never emits. + /// + /// - Parameter timeout: The maximum time interval to wait for values, in seconds. Defaults to 10 seconds. + /// - Returns: An async throwing publisher that emits values or throws `PublisherTimeoutError` on timeout. + /// + func valuesWithTimeout( + _ timeout: TimeInterval = 10, + ) -> AsyncThrowingPublisher> { + self + .timeout( + .seconds(timeout), + scheduler: DispatchQueue.main, + customError: { PublisherTimeoutError() }, + ) + .values + } +}