8000 BIT-434: Add folders view by shannon-livefront · Pull Request #219 · bitwarden/ios · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

BIT-434: Add folders view #219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import BitwardenSdk
import Foundation

@testable import BitwardenShared

extension FolderView {
static func fixture(
id: Uuid = "",
name: String = "",
revisionDate: Date = Date.now
) -> FolderView {
self.init(id: id, name: name, revisionDate: revisionDate)
}
}

extension Folder {
static func fixture(
id: Uuid = "",
name: String = "",
revisionDate: Date = Date.now
) -> Folder {
self.init(id: id, name: name, revisionDate: revisionDate)
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import BitwardenSdk
import Combine
import Foundation

Expand Down Expand Up @@ -31,6 +32,11 @@ protocol SettingsRepository: AnyObject {
/// Defaults to active account if nil.
///
func unlockVault(userId: String?) async

// MARK: Publishers

/// The publisher to keep track of the list of the user's current folders.
func foldersListPublisher() async throws -> AsyncThrowingPublisher<AnyPublisher<[FolderView], Error>>
}

// MARK: - DefaultSettingsRepository
Expand All @@ -40,29 +46,41 @@ protocol SettingsRepository: AnyObject {
class DefaultSettingsRepository {
// MARK: Properties

/// The client used by the application to handle vault encryption and decryption tasks.
private let clientVault: ClientVaultService

/// The service used by the application to manage account state.
let stateService: StateService
private let stateService: StateService

/// The service used to manage syncing and updates to the user's folders.
private let folderService: FolderService

/// The service used to handle syncing vault data with the API.
let syncService: SyncService

/// The service used to manage vault access.
let vaultTimeoutService: VaultTimeoutService
private let vaultTimeoutService: VaultTimeoutService

// MARK: Initialization

/// Initialize a `DefaultSettingsRepository`.
///
/// - Parameters:
/// - clientVault: The client used by the application to handle vault encryption and decryption tasks.
/// - folderService: The service used to manage syncing and updates to the user's folders.
/// - stateService: The service used by the application to manage account state.
/// - syncService: The service used to handle syncing vault data with the API.
/// - vaultTimeoutService: The service used to manage vault access.
///
init(
clientVault: ClientVaultService,
folderService: FolderService,
stateService: StateService,
syncService: SyncService,
vaultTimeoutService: VaultTimeoutService
) {
self.clientVault = clientVault
self.folderService = folderService
self.stateService = stateService
self.syncService = syncService
self.vaultTimeoutService = vaultTimeoutService
Expand Down Expand Up @@ -91,4 +109,15 @@ extension DefaultSettingsRepository: SettingsRepository {
func unlockVault(userId: String?) async {
await vaultTimeoutService.unlockVault(userId: userId)
}

// MARK: Publishers

func foldersListPublisher() async throws -> AsyncThrowingPublisher<AnyPublisher<[FolderView], Error>> {
try await folderService.foldersPublisher()
.asyncTryMap { folders in
try await self.clientVault.folders().decryptList(folders: folders)
}
.eraseToAnyPublisher()
.values
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import BitwardenSdk
import XCTest

@testable import BitwardenShared

class SettingsRepositoryTests: BitwardenTestCase {
// MARK: Properties

var clientVault: MockClientVaultService!
var folderService: MockFolderService!
var stateService: MockStateService!
var subject: DefaultSettingsRepository!
var syncService: MockSyncService!
Expand All @@ -15,11 +18,15 @@ class SettingsRepositoryTests: BitwardenTestCase {
override func setUp() {
super.setUp()

clientVault = MockClientVaultService()
folderService = MockFolderService()
stateService = MockStateService()
syncService = MockSyncService()
vaultTimeoutService = MockVaultTimeoutService()

subject = DefaultSettingsRepository(
clientVault: clientVault,
folderService: folderService,
stateService: stateService,
syncService: syncService,
vaultTimeoutService: vaultTimeoutService
Expand All @@ -29,6 +36,8 @@ class SettingsRepositoryTests: BitwardenTestCase {
override func tearDown() {
super.tearDown()

clientVault = nil
folderService = nil
stateService = nil
subject = nil
syncService = nil
Expand All @@ -53,6 +62,27 @@ class SettingsRepositoryTests: BitwardenTestCase {
}
}

/// `foldersListPublisher()` returns a decrypted flow of the user's folders.
func test_foldersListPublisher_emitsDecryptedList() async throws {
// Prepare the publisher.
var iterator = try await subject.foldersListPublisher().makeAsyncIterator()
_ = try await iterator.next()

// Prepare the sample data.
let date = Date(year: 2023, month: 12, day: 25)
let folder = Folder.fixture(revisionDate: date)
let folderView = FolderView.fixture(revisionDate: date)

// Ensure the list of folders is updated as expected.
folderService.foldersSubject.value = [folder]
let publisherValue = try await iterator.next()
try XCTAssertNotNil(XCTUnwrap(publisherValue))
try XCTAssertEqual(XCTUnwrap(publisherValue), [folderView])

// Ensure the folders were decrypted by the client vault.
XCTAssertEqual(clientVault.clientFolders.decryptedFolders, [folder])
}

/// `lastSyncTimePublisher` returns a publisher of the user's last sync time.
func test_lastSyncTimePublisher() async throws {
var iterator = try await subject.lastSyncTimePublisher().makeAsyncIterator()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import BitwardenSdk
import Combine
import Foundation

Expand All @@ -6,19 +7,21 @@ import Foundation
class MockSettingsRepository: SettingsRepository {
var fetchSyncCalled = false
var fetchSyncResult: Result<Void, Error> = .success(())
var foldersListError: Error?
var isLockedResult: Result<Bool, VaultTimeoutServiceError> = .failure(.noAccountFound)
var lastSyncTimeError: Error?
var lastSyncTimeSubject = CurrentValueSubject<Date?, Never>(nil)
var lockVaultCalls = [String?]()
var unlockVaultCalls = [String?]()
var logoutResult: Result<Void, StateServiceError> = .failure(.noActiveAccount)
var foldersListSubject = CurrentValueSubject<[FolderView], Error>([])

func fetchSync() async throws {
fetchSyncCalled = true
try fetchSyncResult.get()
}

func isLocked(userId: String) throws -> Bool {
func isLocked(userId _: String) throws -> Bool {
try isLockedResult.get()
}

Expand All @@ -40,4 +43,11 @@ class MockSettingsRepository: SettingsRepository {
func logout() async throws {
try logoutResult.get()
}

func foldersListPublisher() async throws -> AsyncThrowingPublisher<AnyPublisher<[FolderView], Error>> {
if let foldersListError {
throw foldersListError
}
return AsyncThrowingPublisher(foldersListSubject.eraseToAnyPublisher())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ public class ServiceContainer: Services {
)

let settingsRepository = DefaultSettingsRepository(
clientVault: clientService.clientVault(),
folderService: folderService,
stateService: stateService,
syncService: syncService,
vaultTimeoutService: vaultTimeoutService
Expand Down
14 changes: 14 additions & 0 deletions BitwardenShared/Core/Vault/Services/FolderService.swift
10000
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import BitwardenSdk
import Combine

// MARK: - FolderService

Expand All @@ -11,6 +12,14 @@ protocol FolderService {
/// - userId: The user ID associated with the folders.
///
func replaceFolders(_ folders: [FolderResponseModel], userId: String) async throws

// MARK: Publishers

/// A publisher for the list of folders.
///
/// - Returns: The list of encrypted folders.
///
func foldersPublisher() async throws -> AnyPublisher<[Folder], Error>
}

// MARK: - DefaultFolderService
Expand Down Expand Up @@ -42,4 +51,9 @@ extension DefaultFolderService {
func replaceFolders(_ folders: [FolderResponseModel], userId: String) async throws {
try await folderDataStore.replaceFolders(folders.map(Folder.init), userId: userId)
}

func foldersPublisher() async throws -> AnyPublisher<[Folder], Error> {
let userID = try await stateService.getActiveAccountId()
return folderDataStore.folderPublisher(userId: userID)
}
}
19 changes: 18 additions & 1 deletion BitwardenShared/Core/Vault/Services/FolderServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class FolderServiceTests: XCTestCase {
// MARK: Properties

var folderDataStore: MockFolderDataStore!
var stateService: MockStateService!
var subject: FolderService!

// MARK: Setup & Teardown
Expand All @@ -15,17 +16,19 @@ class FolderServiceTests: XCTestCase {
super.setUp()

folderDataStore = MockFolderDataStore()
stateService = MockStateService()

subject = DefaultFolderService(
folderDataStore: folderDataStore,
stateService: MockStateService()
stateService: stateService
)
}

override func tearDown() {
super.tearDown()

folderDataStore = nil
stateService = nil
subject = nil
}

Expand All @@ -43,4 +46,18 @@ class FolderServiceTests: XCTestCase {
XCTAssertEqual(folderDataStore.replaceFoldersValue, folders.map(Folder.init))
XCTAssertEqual(folderDataStore.replaceFoldersUserId, "1")
}

/// `foldersPublisher()` returns a publisher that emits data as the data store changes.
func test_foldersPublisher() async throws {
stateService.activeAccount = .fixtureAccountLogin()

var iterator = try await subject.foldersPublisher().values.makeAsyncIterator()
_ = try await iterator.next()

let folder = Folder.fixture()
folderDataStore.folderSubject.value = [folder]
let publisherValue = try await iterator.next()
try XCTAssertNotNil(XCTUnwrap(publisherValue))
try XCTAssertEqual(XCTUnwrap(publisherValue), [folder])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class MockClientCiphers: ClientCiphersProtocol {
// MARK: - MockClientCollections

class MockClientCollections: ClientCollectionsProtocol {
func decrypt(collection: Collection) async throws -> CollectionView {
func decrypt(collection _: Collection) async throws -> CollectionView {
fatalError("Not implemented yet")
}

Expand All @@ -69,15 +69,18 @@ class MockClientCollections: ClientCollectionsProtocol {
// MARK: - MockClientFolders

class MockClientFolders: ClientFoldersProtocol {
var decryptedFolders = [Folder]()

func decrypt(folder: Folder) async throws -> FolderView {
FolderView(folder: folder)
}

func decryptList(folders: [Folder]) async throws -> [FolderView] {
folders.map(FolderView.init)
decryptedFolders = folders
return folders.map(FolderView.init)
}

func encrypt(folder: FolderView) async throws -> Folder {
func encrypt(folder _: FolderView) async throws -> Folder {
fatalError("Not implemented yet")
}
}
Expand All @@ -100,31 +103,31 @@ class MockClientPasswordHistory: ClientPasswordHistoryProtocol {
// MARK: - MockClientSends

class MockClientSends: ClientSendsProtocol {
func decrypt(send: Send) async throws -> SendView {
func decrypt(send _: Send) async throws -> SendView {
fatalError("Not implemented yet")
}

func decryptBuffer(send: Send, buffer: Data) async throws -> Data {
func decryptBuffer(send _: Send, buffer _: Data) async throws -> Data {
fatalError("Not implemented yet")
}

func decryptFile(send: Send, encryptedFilePath: String, decryptedFilePath: String) async throws {
func decryptFile(send _: Send, encryptedFilePath _: String, decryptedFilePath _: String) async throws {
fatalError("Not implemented yet")
}

func decryptList(sends: [Send]) async throws -> [BitwardenSdk.SendListView] {
func decryptList(sends _: [Send]) async throws -> [BitwardenSdk.SendListView] {
fatalError("Not implemented yet")
}

func encrypt(send: SendView) async throws -> Send {
func encrypt(send _: SendView) async throws -> Send {
fatalError("Not implemented yet")
}

func encryptBuffer(send: Send, buffer: Data) async throws -> Data {
func encryptBuffer(send _: Send, buffer _: Data) async throws -> Data {
fatalError("Not implemented yet")
}

func encryptFile(send: Send, decryptedFilePath: String, encryptedFilePath: String) async throws {
func encryptFile(send _: Send, decryptedFilePath _: String, encryptedFilePath _: String) async throws {
fatalError("Not implemented yet")
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import BitwardenSdk
import Combine

@testable import BitwardenShared

class MockFolderService: FolderService {
var replaceFoldersFolders: [FolderResponseModel]?
var replaceFoldersUserId: String?

var foldersSubject = CurrentValueSubject<[Folder], Error>([])

func replaceFolders(_ folders: [FolderResponseModel], userId: String) async throws {
replaceFoldersFolders = folders
replaceFoldersUserId = userId
}

func foldersPublisher() async throws -> AnyPublisher<[Folder], Error> {
foldersSubject.eraseToAnyPublisher()
}
}
Loading
0