[PM-28339] Convert data store publisher tests to use async iterators (#2181)

This commit is contained in:
Matt Czech 2025-12-05 09:16:11 -06:00 committed by GitHub
parent c368f60889
commit e9c25c9b06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 90 additions and 121 deletions

View File

@ -355,21 +355,15 @@ final class AuthenticatorBridgeItemServiceTests: AuthenticatorBridgeKitTestCase
let initialItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id } let initialItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id }
try await subject.insertItems(initialItems, forUserId: "userId") try await subject.insertItems(initialItems, forUserId: "userId")
var results: [[AuthenticatorBridgeItemDataView]] = [] var iterator = try await subject.sharedItemsPublisher().valuesWithTimeout().makeAsyncIterator()
let publisher = try await subject.sharedItemsPublisher()
.sink( let firstValue = try await iterator.next()
receiveCompletion: { _ in }, XCTAssertEqual(firstValue, initialItems)
receiveValue: { value in
results.append(value)
},
)
defer { publisher.cancel() }
try await subject.replaceAllItems(with: [], forUserId: "userId") try await subject.replaceAllItems(with: [], forUserId: "userId")
waitFor(results.count == 2) let secondValue = try await iterator.next()
XCTAssertEqual(results[0], initialItems) XCTAssertEqual(secondValue, [])
XCTAssertEqual(results[1], [])
} }
/// Verify that the shared items publisher publishes items that are inserted/replaced later. /// 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 } let initialItems = AuthenticatorBridgeItemDataView.fixtures().sorted { $0.id < $1.id }
try await subject.insertItems(initialItems, forUserId: "userId") try await subject.insertItems(initialItems, forUserId: "userId")
var results: [[AuthenticatorBridgeItemDataView]] = [] var iterator = try await subject.sharedItemsPublisher().valuesWithTimeout().makeAsyncIterator()
let publisher = try await subject.sharedItemsPublisher()
.sink( let firstValue = try await iterator.next()
receiveCompletion: { _ in }, XCTAssertEqual(firstValue, initialItems)
receiveValue: { value in
results.append(value)
},
)
defer { publisher.cancel() }
let replacedItems = [AuthenticatorBridgeItemDataView.fixture(name: "New Item")] let replacedItems = [AuthenticatorBridgeItemDataView.fixture(name: "New Item")]
try await subject.replaceAllItems(with: replacedItems, forUserId: "userId") try await subject.replaceAllItems(with: replacedItems, forUserId: "userId")
waitFor(results.count == 2) let secondValue = try await iterator.next()
XCTAssertEqual(results[0], initialItems) XCTAssertEqual(secondValue, replacedItems)
XCTAssertEqual(results[1], replacedItems)
} }
/// The shared items publisher deletes items if the user is timed out. /// The shared items publisher deletes items if the user is timed out.

View File

@ -33,21 +33,15 @@ class AuthenticatorItemDataStoreTests: BitwardenTestCase {
/// `authenticatorItemPublisher(userId:)` returns a publisher for a user's authenticatorItem objects. /// `authenticatorItemPublisher(userId:)` returns a publisher for a user's authenticatorItem objects.
func test_authenticatorItemPublisher() async throws { func test_authenticatorItemPublisher() async throws {
var publishedValues = [[AuthenticatorItem]]() var iterator = subject.authenticatorItemPublisher(userId: "1").valuesWithTimeout().makeAsyncIterator()
let publisher = subject.authenticatorItemPublisher(userId: "1")
.sink( let firstValue = try await iterator.next()
receiveCompletion: { _ in }, XCTAssertEqual(firstValue, [])
receiveValue: { values in
publishedValues.append(values)
},
)
defer { publisher.cancel() }
try await subject.replaceAuthenticatorItems(authenticatorItems, userId: "1") try await subject.replaceAuthenticatorItems(authenticatorItems, userId: "1")
waitFor { publishedValues.count == 2 } let secondValue = try await iterator.next()
XCTAssertTrue(publishedValues[0].isEmpty) XCTAssertEqual(secondValue, authenticatorItems)
XCTAssertEqual(publishedValues[1], authenticatorItems)
} }
/// `deleteAllAuthenticatorItems(user:)` removes all objects for the user. /// `deleteAllAuthenticatorItems(user:)` removes all objects for the user.

View File

@ -120,28 +120,24 @@ class GeneratorDataStoreTests: BitwardenTestCase {
/// `passwordHistoryPublisher(userId:)` returns a publisher for a user's password history objects. /// `passwordHistoryPublisher(userId:)` returns a publisher for a user's password history objects.
@MainActor @MainActor
func test_passwordHistoryPublisher() async throws { func test_passwordHistoryPublisher() async throws {
var publishedValues = [[PasswordHistory]]() var iterator = subject.passwordHistoryPublisher(userId: "1").valuesWithTimeout().makeAsyncIterator()
let publisher = subject.passwordHistoryPublisher(userId: "1")
.sink( let firstValue = try await iterator.next()
receiveCompletion: { _ in }, XCTAssertEqual(firstValue, [])
receiveValue: { values in
publishedValues.append(values)
},
)
defer { publisher.cancel() }
let passwordHistory1 = PasswordHistory(password: "PASSWORD1", lastUsedDate: Date()) let passwordHistory1 = PasswordHistory(password: "PASSWORD1", lastUsedDate: Date())
try await subject.insertPasswordHistory(userId: "1", passwordHistory: passwordHistory1) 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()) let passwordHistoryOther = PasswordHistory(password: "PASSWORD_OTHER", lastUsedDate: Date())
try await subject.insertPasswordHistory(userId: "2", passwordHistory: passwordHistoryOther) try await subject.insertPasswordHistory(userId: "2", passwordHistory: passwordHistoryOther)
let passwordHistory2 = PasswordHistory(password: "PASSWORD2", lastUsedDate: Date()) let passwordHistory2 = PasswordHistory(password: "PASSWORD2", lastUsedDate: Date())
try await subject.insertPasswordHistory(userId: "1", passwordHistory: passwordHistory2) try await subject.insertPasswordHistory(userId: "1", passwordHistory: passwordHistory2)
waitFor { publishedValues.count == 3 } let thirdValue = try await iterator.next()
XCTAssertTrue(publishedValues[0].isEmpty) XCTAssertEqual(thirdValue, [passwordHistory2, passwordHistory1])
XCTAssertEqual(publishedValues[1], [passwordHistory1])
XCTAssertEqual(publishedValues[2], [passwordHistory2, passwordHistory1])
} }
} }

View File

@ -57,40 +57,28 @@ class SendDataStoreTests: BitwardenTestCase {
/// `sendPublisher(userId:)` returns a publisher for a single send. /// `sendPublisher(userId:)` returns a publisher for a single send.
func test_sendPublisher() async throws { func test_sendPublisher() async throws {
var publishedValues = [Send?]() var iterator = subject.sendPublisher(id: "1", userId: "1").valuesWithTimeout().makeAsyncIterator()
let publisher = subject.sendPublisher(id: "1", userId: "1")
.sink( let firstValue = try await iterator.next()
receiveCompletion: { _ in }, XCTAssertEqual(firstValue, .some(nil))
receiveValue: { value in
publishedValues.append(value)
},
)
defer { publisher.cancel() }
try await subject.replaceSends(sends, userId: "1") try await subject.replaceSends(sends, userId: "1")
waitFor { publishedValues.count == 2 } let secondValue = try await iterator.next()
XCTAssertNil(publishedValues[0]) XCTAssertEqual(secondValue, Send.fixture(id: "1", name: "SEND1"))
XCTAssertEqual(publishedValues[1], Send.fixture(id: "1", name: "SEND1"))
} }
/// `sendsPublisher(userId:)` returns a publisher for a user's send objects. /// `sendsPublisher(userId:)` returns a publisher for a user's send objects.
func test_sendsPublisher() async throws { func test_sendsPublisher() async throws {
var publishedValues = [[Send]]() var iterator = subject.sendsPublisher(userId: "1").valuesWithTimeout().makeAsyncIterator()
let publisher = subject.sendsPublisher(userId: "1")
.sink( let firstValue = try await iterator.next()
receiveCompletion: { _ in }, XCTAssertEqual(firstValue, [])
receiveValue: { values in
publishedValues.append(values)
},
)
defer { publisher.cancel() }
try await subject.replaceSends(sends, userId: "1") try await subject.replaceSends(sends, userId: "1")
waitFor { publishedValues.count == 2 } let secondValue = try await iterator.next()
XCTAssertTrue(publishedValues[0].isEmpty) XCTAssertEqual(secondValue, sends)
XCTAssertEqual(publishedValues[1], sends)
} }
/// `replaceSends(_:userId)` replaces the list of sends for the user. /// `replaceSends(_:userId)` replaces the list of sends for the user.

View File

@ -46,21 +46,15 @@ class CipherDataStoreTests: BitwardenTestCase {
/// `cipherPublisher(userId:)` returns a publisher for a user's cipher objects. /// `cipherPublisher(userId:)` returns a publisher for a user's cipher objects.
func test_cipherPublisher() async throws { func test_cipherPublisher() async throws {
var publishedValues = [[Cipher]]() var iterator = subject.cipherPublisher(userId: "1").valuesWithTimeout().makeAsyncIterator()
let publisher = subject.cipherPublisher(userId: "1")
.sink( let firstValue = try await iterator.next()
receiveCompletion: { _ in }, XCTAssertEqual(firstValue, [])
receiveValue: { values in
publishedValues.append(values)
},
)
defer { publisher.cancel() }
try await subject.replaceCiphers(ciphers, userId: "1") try await subject.replaceCiphers(ciphers, userId: "1")
waitFor { publishedValues.count == 2 } let secondValue = try await iterator.next()
XCTAssertTrue(publishedValues[0].isEmpty) XCTAssertEqual(secondValue, ciphers)
XCTAssertEqual(publishedValues[1], ciphers)
} }
/// `cipherChangesPublisher(userId:)` emits inserted ciphers for the user. /// `cipherChangesPublisher(userId:)` emits inserted ciphers for the user.

View File

@ -34,21 +34,15 @@ class CollectionDataStoreTests: BitwardenTestCase {
/// `collectionPublisher(userId:)` returns a publisher for a user's collection objects. /// `collectionPublisher(userId:)` returns a publisher for a user's collection objects.
func test_collectionPublisher() async throws { func test_collectionPublisher() async throws {
var publishedValues = [[Collection]]() var iterator = subject.collectionPublisher(userId: "1").valuesWithTimeout().makeAsyncIterator()
let publisher = subject.collectionPublisher(userId: "1")
.sink( let firstValue = try await iterator.next()
receiveCompletion: { _ in }, XCTAssertEqual(firstValue, [])
receiveValue: { values in
publishedValues.append(values)
},
)
defer { publisher.cancel() }
try await subject.replaceCollections(collections, userId: "1") try await subject.replaceCollections(collections, userId: "1")
waitFor { publishedValues.count == 2 } let secondValue = try await iterator.next()
XCTAssertTrue(publishedValues[0].isEmpty) XCTAssertEqual(secondValue, collections)
XCTAssertEqual(publishedValues[1], collections)
} }
/// `deleteAllCollections(user:)` removes all objects for the user. /// `deleteAllCollections(user:)` removes all objects for the user.

View File

@ -68,21 +68,15 @@ class FolderDataStoreTests: BitwardenTestCase {
/// `folderPublisher(userId:)` returns a publisher for a user's folder objects. /// `folderPublisher(userId:)` returns a publisher for a user's folder objects.
func test_folderPublisher() async throws { func test_folderPublisher() async throws {
var publishedValues = [[Folder]]() var iterator = subject.folderPublisher(userId: "1").valuesWithTimeout().makeAsyncIterator()
let publisher = subject.folderPublisher(userId: "1")
.sink( let firstValue = try await iterator.next()
receiveCompletion: { _ in }, XCTAssertEqual(firstValue, [])
receiveValue: { values in
publishedValues.append(values)
},
)
defer { publisher.cancel() }
try await subject.replaceFolders(folders, userId: "1") try await subject.replaceFolders(folders, userId: "1")
waitFor { publishedValues.count == 2 } let secondValue = try await iterator.next()
XCTAssertTrue(publishedValues[0].isEmpty) XCTAssertEqual(secondValue, folders)
XCTAssertEqual(publishedValues[1], folders)
} }
/// `replaceFolders(_:userId)` replaces the list of folders for the user. /// `replaceFolders(_:userId)` replaces the list of folders for the user.

View File

@ -45,21 +45,15 @@ class OrganizationDataStoreTests: BitwardenTestCase {
/// `organizationPublisher(userId:)` returns a publisher for a user's organization objects. /// `organizationPublisher(userId:)` returns a publisher for a user's organization objects.
func test_organizationPublisher() async throws { func test_organizationPublisher() async throws {
var publishedValues = [[Organization]]() var iterator = subject.organizationPublisher(userId: "1").valuesWithTimeout().makeAsyncIterator()
let publisher = subject.organizationPublisher(userId: "1")
.sink( let firstValue = try await iterator.next()
receiveCompletion: { _ in }, XCTAssertEqual(firstValue, [])
receiveValue: { values in
publishedValues.append(values)
},
)
defer { publisher.cancel() }
try await subject.replaceOrganizations(organizations, userId: "1") try await subject.replaceOrganizations(organizations, userId: "1")
waitFor { publishedValues.count == 2 } let secondValue = try await iterator.next()
XCTAssertTrue(publishedValues[0].isEmpty) XCTAssertEqual(secondValue, organizations.compactMap(Organization.init))
XCTAssertEqual(publishedValues[1], organizations.compactMap(Organization.init))
} }
/// `fetchAllOrganizations(userId:)` fetches all organizations for a user. /// `fetchAllOrganizations(userId:)` fetches all organizations for a user.

View File

@ -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<Publishers.Timeout<Self, DispatchQueue>> {
self
.timeout(
.seconds(timeout),
scheduler: DispatchQueue.main,
customError: { PublisherTimeoutError() },
)
.values
}
}