[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 }
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.

View File

@ -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.

View File

@ -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])
}
}

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

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
}
}