From ae99fe362d8487727934ff43fdc7c22b963b418d Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Thu, 17 Jul 2025 10:57:28 -0300 Subject: [PATCH] [PM-13607] Implement SDK client-managed state repository registration (#1738) Co-authored-by: Matt Czech --- .../MockPlatformClientService.swift | 5 + .../TestHelpers/MockStateClient.swift | 11 ++ .../Services/PlatformClientService.swift | 7 ++ .../TestHelpers/MockVaultClientService.swift | 8 ++ .../xcshareddata/swiftpm/Package.resolved | 2 +- .../MockPlatformClientService.swift | 5 + .../TestHelpers/MockStateClient.swift | 11 ++ .../Services/ClientBuilderTests.swift | 10 +- .../Platform/Services/ClientService.swift | 31 ++++-- .../Services/ClientServiceTests.swift | 16 +++ .../Services/PlatformClientService.swift | 7 ++ .../Platform/Services/ServiceContainer.swift | 22 ++-- .../Repositories/SdkCipherRepository.swift | 51 +++++++++ .../SdkCipherRepositoryTests.swift | 101 ++++++++++++++++++ .../Repositories/SdkRepositoryFactory.swift | 42 ++++++++ .../SdkRepositoryFactoryTests.swift | 40 +++++++ .../TestHelpers/MockSdkCipherRepository.swift | 38 +++++++ .../TestHelpers/MockCipherDataStore.swift | 4 +- .../TestHelpers/MockVaultClientService.swift | 8 ++ project-bwa.yml | 2 +- project-bwk.yml | 2 +- project-pm.yml | 2 +- 22 files changed, 402 insertions(+), 23 deletions(-) create mode 100644 AuthenticatorShared/Core/Auth/Services/TestHelpers/MockStateClient.swift create mode 100644 BitwardenShared/Core/Auth/Services/TestHelpers/MockStateClient.swift create mode 100644 BitwardenShared/Core/Vault/Repositories/SdkCipherRepository.swift create mode 100644 BitwardenShared/Core/Vault/Repositories/SdkCipherRepositoryTests.swift create mode 100644 BitwardenShared/Core/Vault/Repositories/SdkRepositoryFactory.swift create mode 100644 BitwardenShared/Core/Vault/Repositories/SdkRepositoryFactoryTests.swift create mode 100644 BitwardenShared/Core/Vault/Repositories/TestHelpers/MockSdkCipherRepository.swift diff --git a/AuthenticatorShared/Core/Auth/Services/TestHelpers/MockPlatformClientService.swift b/AuthenticatorShared/Core/Auth/Services/TestHelpers/MockPlatformClientService.swift index 1a615f4be..2217192c6 100644 --- a/AuthenticatorShared/Core/Auth/Services/TestHelpers/MockPlatformClientService.swift +++ b/AuthenticatorShared/Core/Auth/Services/TestHelpers/MockPlatformClientService.swift @@ -8,6 +8,7 @@ class MockPlatformClientService: PlatformClientService { var fingerprintResult: Result = .success("a-fingerprint-phrase-string-placeholder") var featureFlags: [String: Bool] = [:] var loadFlagsError: Error? + var stateMock = MockStateClient() var userFingerprintCalled = false func fido2() -> ClientFido2Service { @@ -25,6 +26,10 @@ class MockPlatformClientService: PlatformClientService { featureFlags = flags } + func state() -> StateClientProtocol { + stateMock + } + func userFingerprint(material fingerprintMaterial: String) throws -> String { fingerprintMaterialString = fingerprintMaterial userFingerprintCalled = true diff --git a/AuthenticatorShared/Core/Auth/Services/TestHelpers/MockStateClient.swift b/AuthenticatorShared/Core/Auth/Services/TestHelpers/MockStateClient.swift new file mode 100644 index 000000000..9cc42128d --- /dev/null +++ b/AuthenticatorShared/Core/Auth/Services/TestHelpers/MockStateClient.swift @@ -0,0 +1,11 @@ +import BitwardenSdk + +@testable import AuthenticatorShared + +final class MockStateClient: StateClientProtocol { + var registerCipherRepositoryReceivedStore: CipherRepository? + + func registerCipherRepository(store: CipherRepository) { + registerCipherRepositoryReceivedStore = store + } +} diff --git a/AuthenticatorShared/Core/Platform/Services/PlatformClientService.swift b/AuthenticatorShared/Core/Platform/Services/PlatformClientService.swift index 2d29a2f05..a48bd6d1d 100644 --- a/AuthenticatorShared/Core/Platform/Services/PlatformClientService.swift +++ b/AuthenticatorShared/Core/Platform/Services/PlatformClientService.swift @@ -17,6 +17,9 @@ protocol PlatformClientService: AnyObject { /// - Parameter flags: Flags to load. func loadFlags(_ flags: [String: Bool]) throws + /// Returns an object to handle state. + func state() -> StateClientProtocol + /// Fingerprint using logged in user's public key /// - Parameter material: Fingerprint material to use /// - Returns: User fingerprint @@ -38,6 +41,10 @@ extension PlatformClient: PlatformClientService { try loadFlags(flags: flags) } + func state() -> StateClientProtocol { + state() as StateClient + } + func userFingerprint(material fingerprintMaterial: String) throws -> String { try userFingerprint(fingerprintMaterial: fingerprintMaterial) } diff --git a/AuthenticatorShared/Core/Vault/Services/TestHelpers/MockVaultClientService.swift b/AuthenticatorShared/Core/Vault/Services/TestHelpers/MockVaultClientService.swift index e5f264419..dbd725a86 100644 --- a/AuthenticatorShared/Core/Vault/Services/TestHelpers/MockVaultClientService.swift +++ b/AuthenticatorShared/Core/Vault/Services/TestHelpers/MockVaultClientService.swift @@ -92,6 +92,7 @@ class MockClientCiphers: CiphersClientProtocol { } var decryptFido2CredentialsResult = [BitwardenSdk.Fido2CredentialView]() + var decryptListWithFailuresResult: DecryptCipherListResult? var encryptCipherResult: Result? var encryptError: Error? var encryptedCiphers = [CipherView]() @@ -115,6 +116,13 @@ class MockClientCiphers: CiphersClientProtocol { ciphers.map(CipherListView.init) } + func decryptListWithFailures(ciphers: [Cipher]) -> DecryptCipherListResult { + decryptListWithFailuresResult ?? DecryptCipherListResult( + successes: ciphers.map(CipherListView.init), + failures: [] + ) + } + func encrypt(cipherView: CipherView) throws -> BitwardenSdk.EncryptionContext { encryptedCiphers.append(cipherView) if let encryptError { diff --git a/Bitwarden.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Bitwarden.xcworkspace/xcshareddata/swiftpm/Package.resolved index 25260fbed..9828c7e28 100644 --- a/Bitwarden.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Bitwarden.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -123,7 +123,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/bitwarden/sdk-swift", "state" : { - "revision" : "0c3baf9d372cd941146616a3d842b78c96a1170f" + "revision" : "33a7bb7334aeea402256633cbbffe7fb03501e40" } }, { diff --git a/BitwardenShared/Core/Auth/Services/TestHelpers/MockPlatformClientService.swift b/BitwardenShared/Core/Auth/Services/TestHelpers/MockPlatformClientService.swift index b72103f81..e145376e6 100644 --- a/BitwardenShared/Core/Auth/Services/TestHelpers/MockPlatformClientService.swift +++ b/BitwardenShared/Core/Auth/Services/TestHelpers/MockPlatformClientService.swift @@ -8,6 +8,7 @@ class MockPlatformClientService: PlatformClientService { var fingerprintResult: Result = .success("a-fingerprint-phrase-string-placeholder") var featureFlags: [String: Bool] = [:] var loadFlagsError: Error? + var stateMock = MockStateClient() var userFingerprintCalled = false func fido2() -> ClientFido2Service { @@ -25,6 +26,10 @@ class MockPlatformClientService: PlatformClientService { featureFlags = flags } + func state() -> StateClientProtocol { + stateMock + } + func userFingerprint(material fingerprintMaterial: String) throws -> String { fingerprintMaterialString = fingerprintMaterial userFingerprintCalled = true diff --git a/BitwardenShared/Core/Auth/Services/TestHelpers/MockStateClient.swift b/BitwardenShared/Core/Auth/Services/TestHelpers/MockStateClient.swift new file mode 100644 index 000000000..5c035701f --- /dev/null +++ b/BitwardenShared/Core/Auth/Services/TestHelpers/MockStateClient.swift @@ -0,0 +1,11 @@ +import BitwardenSdk + +@testable import BitwardenShared + +final class MockStateClient: StateClientProtocol { + var registerCipherRepositoryReceivedStore: CipherRepository? + + func registerCipherRepository(store: CipherRepository) { + registerCipherRepositoryReceivedStore = store + } +} diff --git a/BitwardenShared/Core/Platform/Services/ClientBuilderTests.swift b/BitwardenShared/Core/Platform/Services/ClientBuilderTests.swift index e05240545..bbe0c04a9 100644 --- a/BitwardenShared/Core/Platform/Services/ClientBuilderTests.swift +++ b/BitwardenShared/Core/Platform/Services/ClientBuilderTests.swift @@ -17,7 +17,9 @@ class ClientBuilderTests: BitwardenTestCase { errorReporter = MockErrorReporter() mockPlatform = MockPlatformClientService() - subject = DefaultClientBuilder(errorReporter: errorReporter) + subject = DefaultClientBuilder( + errorReporter: errorReporter + ) } override func tearDown() { @@ -30,11 +32,11 @@ class ClientBuilderTests: BitwardenTestCase { // MARK: Tests - /// `buildClient()` creates a client and loads feature flags. + /// `buildClient(for:)` creates a client and loads feature flags. func test_buildClient() { - let client = subject.buildClient() + let builtClient = subject.buildClient() - XCTAssertNotNil(client) + XCTAssertNotNil(builtClient) XCTAssertNotNil(mockPlatform.featureFlags) } } diff --git a/BitwardenShared/Core/Platform/Services/ClientService.swift b/BitwardenShared/Core/Platform/Services/ClientService.swift index 4606c568b..dc25bc521 100644 --- a/BitwardenShared/Core/Platform/Services/ClientService.swift +++ b/BitwardenShared/Core/Platform/Services/ClientService.swift @@ -144,6 +144,9 @@ actor DefaultClientService: ClientService { /// The service used by the application to report non-fatal errors. private let errorReporter: ErrorReporter + /// The factory to create SDK repositories. + private let sdkRepositoryFactory: SdkRepositoryFactory + /// Basic client behavior settings. private let settings: ClientSettings? @@ -161,6 +164,7 @@ actor DefaultClientService: ClientService { /// - clientBuilder: A helper object that builds a Bitwarden SDK `Client`. /// - configService: The service to get server-specified configuration. /// - errorReporter: The service used by the application to report non-fatal errors. + /// - sdkRepositoryFactory: The factory to create SDK repositories. /// - settings: The settings to apply to the client. Defaults to `nil`. /// - stateService: The service used by the application to manage account state. /// @@ -168,12 +172,14 @@ actor DefaultClientService: ClientService { clientBuilder: ClientBuilder, configService: ConfigService, errorReporter: ErrorReporter, + sdkRepositoryFactory: SdkRepositoryFactory, settings: ClientSettings? = nil, stateService: StateService ) { self.clientBuilder = clientBuilder self.configService = configService self.errorReporter = errorReporter + self.sdkRepositoryFactory = sdkRepositoryFactory self.settings = settings self.stateService = stateService @@ -254,9 +260,7 @@ actor DefaultClientService: ClientService { // If not, create one, map it to the user, then return it. let newClient = await createAndMapClient(for: userId) - // Get the current config and load the flags. - let config = await configService.getConfig() - await loadFlags(config, for: newClient) + await configureNewClient(newClient, for: userId) return newClient } @@ -275,6 +279,20 @@ actor DefaultClientService: ClientService { return clientBuilder.buildClient() } } + + /// Configures a new SDK client. + /// - Parameters: + /// - client: The SDK client to configure. + /// - userId: The user ID the SDK client instance belongs to. + func configureNewClient(_ client: BitwardenSdkClient, for userId: String) async { + client.platform().state().registerCipherRepository( + store: sdkRepositoryFactory.makeCipherRepository(userId: userId) + ) + + // Get the current config and load the flags. + let config = await configService.getConfig() + await loadFlags(config, for: client) + } /// Creates a new client and maps it to an ID. /// @@ -315,7 +333,6 @@ actor DefaultClientService: ClientService { /// protocol ClientBuilder { /// Creates a `BitwardenSdkClient`. - /// /// - Returns: A new `BitwardenSdkClient`. /// func buildClient() -> BitwardenSdkClient @@ -341,8 +358,10 @@ class DefaultClientBuilder: ClientBuilder { /// - Parameters: /// - errorReporter: The service used by the application to report non-fatal errors. /// - settings: The settings applied to the client. - /// - init(errorReporter: ErrorReporter, settings: ClientSettings? = nil) { + init( + errorReporter: ErrorReporter, + settings: ClientSettings? = nil + ) { self.errorReporter = errorReporter self.settings = settings } diff --git a/BitwardenShared/Core/Platform/Services/ClientServiceTests.swift b/BitwardenShared/Core/Platform/Services/ClientServiceTests.swift index a81182b2c..2fe4d4a28 100644 --- a/BitwardenShared/Core/Platform/Services/ClientServiceTests.swift +++ b/BitwardenShared/Core/Platform/Services/ClientServiceTests.swift @@ -12,6 +12,7 @@ final class ClientServiceTests: BitwardenTestCase { // swiftlint:disable:this ty var clientBuilder: MockClientBuilder! var configService: MockConfigService! var errorReporter: MockErrorReporter! + var sdkRepositoryFactory: MockSdkRepositoryFactory! var stateService: MockStateService! var subject: DefaultClientService! var vaultTimeoutService: MockVaultTimeoutService! @@ -24,11 +25,14 @@ final class ClientServiceTests: BitwardenTestCase { // swiftlint:disable:this ty clientBuilder = MockClientBuilder() configService = MockConfigService() errorReporter = MockErrorReporter() + sdkRepositoryFactory = MockSdkRepositoryFactory() + sdkRepositoryFactory.makeCipherRepositoryUserIdReturnValue = MockSdkCipherRepository() stateService = MockStateService() subject = DefaultClientService( clientBuilder: clientBuilder, configService: configService, errorReporter: errorReporter, + sdkRepositoryFactory: sdkRepositoryFactory, stateService: stateService ) vaultTimeoutService = MockVaultTimeoutService() @@ -40,6 +44,7 @@ final class ClientServiceTests: BitwardenTestCase { // swiftlint:disable:this ty clientBuilder = nil configService = nil errorReporter = nil + sdkRepositoryFactory = nil stateService = nil subject = nil vaultTimeoutService = nil @@ -215,6 +220,17 @@ final class ClientServiceTests: BitwardenTestCase { // swiftlint:disable:this ty ) } + /// `client(for:)` registers the SDK cipher repository. + func test_client_registersCipherRepository() async throws { + stateService.activeAccount = .fixture(profile: .fixture(userId: "1")) + + let auth = try await subject.auth() + let client = try XCTUnwrap(clientBuilder.clients.first) + XCTAssertIdentical(auth, client.authClient) + XCTAssertTrue(sdkRepositoryFactory.makeCipherRepositoryUserIdCalled) + XCTAssertNotNil(client.platformClient.stateMock.registerCipherRepositoryReceivedStore) + } + /// `configPublisher` loads flags into the SDK. @MainActor func test_configPublisher_loadFlags() async throws { diff --git a/BitwardenShared/Core/Platform/Services/PlatformClientService.swift b/BitwardenShared/Core/Platform/Services/PlatformClientService.swift index 2d29a2f05..a48bd6d1d 100644 --- a/BitwardenShared/Core/Platform/Services/PlatformClientService.swift +++ b/BitwardenShared/Core/Platform/Services/PlatformClientService.swift @@ -17,6 +17,9 @@ protocol PlatformClientService: AnyObject { /// - Parameter flags: Flags to load. func loadFlags(_ flags: [String: Bool]) throws + /// Returns an object to handle state. + func state() -> StateClientProtocol + /// Fingerprint using logged in user's public key /// - Parameter material: Fingerprint material to use /// - Returns: User fingerprint @@ -38,6 +41,10 @@ extension PlatformClient: PlatformClientService { try loadFlags(flags: flags) } + func state() -> StateClientProtocol { + state() as StateClient + } + func userFingerprint(material fingerprintMaterial: String) throws -> String { try userFingerprint(fingerprintMaterial: fingerprintMaterial) } diff --git a/BitwardenShared/Core/Platform/Services/ServiceContainer.swift b/BitwardenShared/Core/Platform/Services/ServiceContainer.swift index 4ad2d8eb7..52d48252e 100644 --- a/BitwardenShared/Core/Platform/Services/ServiceContainer.swift +++ b/BitwardenShared/Core/Platform/Services/ServiceContainer.swift @@ -455,11 +455,24 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le timeProvider: timeProvider ) - let clientBuilder = DefaultClientBuilder(errorReporter: errorReporter) + let cipherService = DefaultCipherService( + cipherAPIService: apiService, + cipherDataStore: dataStore, + fileAPIService: apiService, + stateService: stateService + ) + + let clientBuilder = DefaultClientBuilder( + errorReporter: errorReporter + ) let clientService = DefaultClientService( clientBuilder: clientBuilder, configService: configService, errorReporter: errorReporter, + sdkRepositoryFactory: DefaultSdkRepositoryFactory( + cipherDataStore: dataStore, + errorReporter: errorReporter + ), stateService: stateService ) @@ -495,13 +508,6 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le stateService: stateService ) - let cipherService = DefaultCipherService( - cipherAPIService: apiService, - cipherDataStore: dataStore, - fileAPIService: apiService, - stateService: stateService - ) - let eventService = DefaultEventService( cipherService: cipherService, errorReporter: errorReporter, diff --git a/BitwardenShared/Core/Vault/Repositories/SdkCipherRepository.swift b/BitwardenShared/Core/Vault/Repositories/SdkCipherRepository.swift new file mode 100644 index 000000000..8f415e543 --- /dev/null +++ b/BitwardenShared/Core/Vault/Repositories/SdkCipherRepository.swift @@ -0,0 +1,51 @@ +import BitwardenKit +import BitwardenSdk + +/// `CipherRepository` implementation to be used on SDK client-managed state. +final class SdkCipherRepository: BitwardenSdk.CipherRepository { + /// The data store for managing the persisted ciphers for the user. + let cipherDataStore: CipherDataStore + /// The service used by the application to report non-fatal errors. + let errorReporter: ErrorReporter + /// The user ID of the SDK instance this repository belongs to. + let userId: String + + /// Initializes a `SdkCipherRepository`. + /// - Parameters: + /// - cipherDataStore: The data store for managing the persisted ciphers for the user. + /// - errorReporter: The service used by the application to report non-fatal errors. + /// - userId: The user ID of the SDK instance this repository belongs to + init( + cipherDataStore: CipherDataStore, + errorReporter: ErrorReporter, + userId: String + ) { + self.cipherDataStore = cipherDataStore + self.errorReporter = errorReporter + self.userId = userId + } + + func get(id: String) async throws -> BitwardenSdk.Cipher? { + try await cipherDataStore.fetchCipher(withId: id, userId: userId) + } + + func has(id: String) async throws -> Bool { + let cipher = try await cipherDataStore.fetchCipher(withId: id, userId: userId) + return cipher != nil + } + + func list() async throws -> [BitwardenSdk.Cipher] { + try await cipherDataStore.fetchAllCiphers(userId: userId) + } + + func remove(id: String) async throws { + try await cipherDataStore.deleteCipher(id: id, userId: userId) + } + + func set(id: String, value: BitwardenSdk.Cipher) async throws { + guard id == value.id else { + throw BitwardenError.dataError("CipherRepository: Trying to update a cipher with mismatch IDs") + } + try await cipherDataStore.upsertCipher(value, userId: userId) + } +} diff --git a/BitwardenShared/Core/Vault/Repositories/SdkCipherRepositoryTests.swift b/BitwardenShared/Core/Vault/Repositories/SdkCipherRepositoryTests.swift new file mode 100644 index 000000000..b9d60a7c6 --- /dev/null +++ b/BitwardenShared/Core/Vault/Repositories/SdkCipherRepositoryTests.swift @@ -0,0 +1,101 @@ +import BitwardenKitMocks +import TestHelpers +import XCTest + +@testable import BitwardenShared + +// MARK: - SdkCipherRepositoryTests + +class SdkCipherRepositoryTests: BitwardenTestCase { + // MARK: Properties + + var cipherDataStore: MockCipherDataStore! + var errorReporter: MockErrorReporter! + var subject: SdkCipherRepository! + let expectedUserId = "1" + + // MARK: Setup & Teardown + + override func setUp() { + super.setUp() + + cipherDataStore = MockCipherDataStore() + errorReporter = MockErrorReporter() + subject = SdkCipherRepository( + cipherDataStore: cipherDataStore, + errorReporter: errorReporter, + userId: expectedUserId + ) + } + + override func tearDown() { + super.tearDown() + + cipherDataStore = nil + errorReporter = nil + subject = nil + } + + // MARK: Tests + + /// `get(id:)` fetches the cipher by its ID. + func test_get() async throws { + cipherDataStore.fetchCipherResult = .fixture(id: "1") + let cipher = try await subject.get(id: "1") + XCTAssertNotNil(cipher) + XCTAssertEqual(cipher?.id, "1") + XCTAssertEqual(cipherDataStore.fetchCipherUserId, expectedUserId) + } + + /// `has(id:)` returns whether there is a cipher with the ID. + func test_has() async throws { + cipherDataStore.fetchCipherResult = .fixture(id: "1") + + let hasCipher1 = try await subject.has(id: "1") + XCTAssertTrue(hasCipher1) + XCTAssertEqual(cipherDataStore.fetchCipherUserId, expectedUserId) + + cipherDataStore.fetchCipherResult = nil + + let hasCipher2 = try await subject.has(id: "2") + XCTAssertFalse(hasCipher2) + } + + /// `list()` returns a list of ciphers. + func test_list() async throws { + cipherDataStore.fetchAllCiphersResult = .success([.fixture(id: "1"), .fixture(id: "2")]) + + let ciphers = try await subject.list() + XCTAssertEqual(ciphers.map(\.id), ["1", "2"]) + XCTAssertEqual(cipherDataStore.fetchAllCiphersUserId, expectedUserId) + } + + /// `remove(id:)` deletes the cipher from local storage by ID. + func test_remove() async throws { + try await subject.remove(id: "1") + XCTAssertEqual(cipherDataStore.deleteCipherId, "1") + XCTAssertEqual(cipherDataStore.deleteCipherUserId, expectedUserId) + } + + /// `set(id:value:)` updates the cipher with local storage. + func test_set() async throws { + try await subject.set(id: "1", value: .fixture(id: "1")) + XCTAssertEqual(cipherDataStore.upsertCipherValue?.id, "1") + XCTAssertEqual(cipherDataStore.upsertCipherUserId, expectedUserId) + } + + /// `set(id:value:)` doesn't update the cipher with local storage when the ID being passed + /// doesn't match the ID of the `value` and throws an error. + func test_set_nonMatchingIds() async throws { + do { + try await subject.set(id: "1", value: .fixture(id: "5")) + } catch { + XCTAssertEqual( + (error as NSError).userInfo["ErrorMessage"] as? String, + "CipherRepository: Trying to update a cipher with mismatch IDs" + ) + } + + XCTAssertNil(cipherDataStore.upsertCipherValue) + } +} diff --git a/BitwardenShared/Core/Vault/Repositories/SdkRepositoryFactory.swift b/BitwardenShared/Core/Vault/Repositories/SdkRepositoryFactory.swift new file mode 100644 index 000000000..696d1a9cb --- /dev/null +++ b/BitwardenShared/Core/Vault/Repositories/SdkRepositoryFactory.swift @@ -0,0 +1,42 @@ +import BitwardenKit +import BitwardenSdk + +/// A factory to create SDK repositories. +protocol SdkRepositoryFactory { // sourcery: AutoMockable + /// Makes a `BitwardenSdk.CipherRepository` for the given `userId`. + /// - Parameter userId: The user ID to use in the repository which belongs to the SDK instance + /// the repository will be registered in. + /// - Returns: The repository for the given `userId`. + func makeCipherRepository(userId: String) -> BitwardenSdk.CipherRepository +} + +/// Default implementation of `SdkRepositoryFactory`. +struct DefaultSdkRepositoryFactory: SdkRepositoryFactory { + // MARK: Properties + + /// The data store for managing the persisted ciphers for the user. + private let cipherDataStore: CipherDataStore + /// The service used by the application to report non-fatal errors. + private let errorReporter: ErrorReporter + + // MARK: Init + + /// Initializes a `DefaultSdkRepositoryFactory`. + /// - Parameters: + /// - cipherDataStore: The data store for managing the persisted ciphers for the user. + /// - errorReporter: The service used by the application to report non-fatal errors. + init(cipherDataStore: CipherDataStore, errorReporter: ErrorReporter) { + self.cipherDataStore = cipherDataStore + self.errorReporter = errorReporter + } + + // MARK: Methods + + func makeCipherRepository(userId: String) -> BitwardenSdk.CipherRepository { + SdkCipherRepository( + cipherDataStore: cipherDataStore, + errorReporter: errorReporter, + userId: userId + ) + } +} diff --git a/BitwardenShared/Core/Vault/Repositories/SdkRepositoryFactoryTests.swift b/BitwardenShared/Core/Vault/Repositories/SdkRepositoryFactoryTests.swift new file mode 100644 index 000000000..ad38d73f8 --- /dev/null +++ b/BitwardenShared/Core/Vault/Repositories/SdkRepositoryFactoryTests.swift @@ -0,0 +1,40 @@ +import BitwardenKitMocks +import XCTest + +@testable import BitwardenShared + +// MARK: - SdkRepositoryFactoryTests + +class SdkRepositoryFactoryTests: BitwardenTestCase { + // MARK: Properties + + var cipherDataStore: MockCipherDataStore! + var errorReporter: MockErrorReporter! + var subject: SdkRepositoryFactory! + + // MARK: Setup & Teardown + + override func setUp() { + super.setUp() + + cipherDataStore = MockCipherDataStore() + errorReporter = MockErrorReporter() + subject = DefaultSdkRepositoryFactory(cipherDataStore: cipherDataStore, errorReporter: errorReporter) + } + + override func tearDown() { + super.tearDown() + + cipherDataStore = nil + errorReporter = nil + subject = nil + } + + // MARK: Tests + + /// `makeCipherRepository(userId:)` makes a cipher repository for the given user ID. + func test_makeCipherRepository() { + let repository = subject.makeCipherRepository(userId: "1") + XCTAssertTrue(repository is SdkCipherRepository) + } +} diff --git a/BitwardenShared/Core/Vault/Repositories/TestHelpers/MockSdkCipherRepository.swift b/BitwardenShared/Core/Vault/Repositories/TestHelpers/MockSdkCipherRepository.swift new file mode 100644 index 000000000..a4cc4fc82 --- /dev/null +++ b/BitwardenShared/Core/Vault/Repositories/TestHelpers/MockSdkCipherRepository.swift @@ -0,0 +1,38 @@ +import BitwardenKitMocks +import BitwardenSdk + +@testable import BitwardenShared + +final class MockSdkCipherRepository: BitwardenSdk.CipherRepository { + var getResult: Result = .success(.fixture()) + var hasResult: Result = .success(true) + var listResult: Result<[BitwardenSdk.Cipher], Error> = .success([]) + var removeResult: Result = .success(()) + var removeReceivedId: String? + var setReceivedId: String? + var setReceivedCipher: Cipher? + var setResult: Result = .success(()) + + func get(id: String) async throws -> BitwardenSdk.Cipher? { + try getResult.get() + } + + func has(id: String) async throws -> Bool { + try hasResult.get() + } + + func list() async throws -> [BitwardenSdk.Cipher] { + try listResult.get() + } + + func remove(id: String) async throws { + removeReceivedId = id + try removeResult.get() + } + + func set(id: String, value: BitwardenSdk.Cipher) async throws { + setReceivedId = id + setReceivedCipher = value + try setResult.get() + } +} diff --git a/BitwardenShared/Core/Vault/Services/Stores/TestHelpers/MockCipherDataStore.swift b/BitwardenShared/Core/Vault/Services/Stores/TestHelpers/MockCipherDataStore.swift index 2c73e22e7..2970d36f8 100644 --- a/BitwardenShared/Core/Vault/Services/Stores/TestHelpers/MockCipherDataStore.swift +++ b/BitwardenShared/Core/Vault/Services/Stores/TestHelpers/MockCipherDataStore.swift @@ -17,6 +17,7 @@ class MockCipherDataStore: CipherDataStore { var fetchCipherId: String? var fetchCipherResult: Cipher? + var fetchCipherUserId: String? var cipherSubjectByUserId: [String: CurrentValueSubject<[Cipher], Error>] = [:] @@ -45,8 +46,9 @@ class MockCipherDataStore: CipherDataStore { return try fetchAllCiphersResult.get() } - func fetchCipher(withId id: String, userId _: String) async -> Cipher? { + func fetchCipher(withId id: String, userId: String) async -> Cipher? { fetchCipherId = id + fetchCipherUserId = userId return fetchCipherResult } diff --git a/BitwardenShared/Core/Vault/Services/TestHelpers/MockVaultClientService.swift b/BitwardenShared/Core/Vault/Services/TestHelpers/MockVaultClientService.swift index 34ba868c8..0c3156a97 100644 --- a/BitwardenShared/Core/Vault/Services/TestHelpers/MockVaultClientService.swift +++ b/BitwardenShared/Core/Vault/Services/TestHelpers/MockVaultClientService.swift @@ -107,6 +107,7 @@ class MockClientCiphers: CiphersClientProtocol { var decryptFido2CredentialsResult = [BitwardenSdk.Fido2CredentialView]() var decryptListError: Error? + var decryptListWithFailuresResult: DecryptCipherListResult? var decryptListErrorWhenCiphers: (([Cipher]) -> Error?)? var decryptListReceivedCiphersInvocations: [[Cipher]] = [] var encryptCipherResult: Result? @@ -139,6 +140,13 @@ class MockClientCiphers: CiphersClientProtocol { return ciphers.map(CipherListView.init) } + func decryptListWithFailures(ciphers: [Cipher]) -> DecryptCipherListResult { + decryptListWithFailuresResult ?? DecryptCipherListResult( + successes: ciphers.map(CipherListView.init), + failures: [] + ) + } + func encrypt(cipherView: CipherView) throws -> EncryptionContext { encryptedCiphers.append(cipherView) if let encryptError { diff --git a/project-bwa.yml b/project-bwa.yml index 5499936f1..c8938fb2c 100644 --- a/project-bwa.yml +++ b/project-bwa.yml @@ -23,7 +23,7 @@ include: packages: BitwardenSdk: url: https://github.com/bitwarden/sdk-swift - revision: 0c3baf9d372cd941146616a3d842b78c96a1170f + revision: 33a7bb7334aeea402256633cbbffe7fb03501e40 branch: unstable Firebase: url: https://github.com/firebase/firebase-ios-sdk diff --git a/project-bwk.yml b/project-bwk.yml index 82bf0bc1b..87b52b262 100644 --- a/project-bwk.yml +++ b/project-bwk.yml @@ -23,7 +23,7 @@ include: packages: BitwardenSdk: url: https://github.com/bitwarden/sdk-swift - revision: 0c3baf9d372cd941146616a3d842b78c96a1170f + revision: 33a7bb7334aeea402256633cbbffe7fb03501e40 branch: unstable Firebase: url: https://github.com/firebase/firebase-ios-sdk diff --git a/project-pm.yml b/project-pm.yml index 7bcee9cb5..d407a3c61 100644 --- a/project-pm.yml +++ b/project-pm.yml @@ -24,7 +24,7 @@ include: packages: BitwardenSdk: url: https://github.com/bitwarden/sdk-swift - revision: 0c3baf9d372cd941146616a3d842b78c96a1170f + revision: 33a7bb7334aeea402256633cbbffe7fb03501e40 branch: unstable Firebase: url: https://github.com/firebase/firebase-ios-sdk