Skip to content

Commit

Permalink
Merged in latest from main; fixed conflict
Browse files Browse the repository at this point in the history
  • Loading branch information
brant-livefront committed Nov 7, 2024
2 parents fba68b5 + e50d624 commit b117cf3
Show file tree
Hide file tree
Showing 20 changed files with 356 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ protocol AuthenticatorItemRepository: AnyObject {
///
func refreshTotpCodes(on items: [ItemListItem]) async throws -> [ItemListItem]

/// Create a temporary shared item based on a `AuthenticatorItemView` for sharing with the PM app.
/// This method will store it as a temporary item in the shared store.
///
/// - Parameter item: The item to be shared with the PM app
///
func saveTemporarySharedItem(_ item: AuthenticatorItemView) async throws

/// Updates an item in the user's storage
///
/// - Parameters:
Expand Down Expand Up @@ -81,13 +88,6 @@ protocol AuthenticatorItemRepository: AnyObject {
///
func itemListPublisher() async throws -> AsyncThrowingPublisher<AnyPublisher<[ItemListSection], Error>>

/// Create a temporary shared item based on a `ItemListItem` for sharing with the PM app. This method will store it
/// as a temporary item in the shared store.
///
/// - Parameter item: The item to be shared with the PM app
///
func saveTemporarySharedItem(_ item: ItemListItem) async throws

/// A publisher for searching a user's cipher objects based on the specified search text and filter type.
///
/// - Parameters:
Expand Down Expand Up @@ -306,17 +306,15 @@ extension DefaultAuthenticatorItemRepository: AuthenticatorItemRepository {
}
}

func saveTemporarySharedItem(_ item: ItemListItem) async throws {
guard case let .totp(model) = item.itemType else { return }

func saveTemporarySharedItem(_ item: AuthenticatorItemView) async throws {
try await sharedItemService.insertTemporaryItem(AuthenticatorBridgeItemDataView(
accountDomain: nil,
accountEmail: nil,
favorite: false,
id: item.id,
name: item.name,
totpKey: model.itemView.totpKey,
username: item.accountName
totpKey: item.totpKey,
username: item.username
))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,20 +233,10 @@ class AuthenticatorItemRepositoryTests: AuthenticatorTestCase { // swiftlint:dis
}
}

/// `saveTemporarySharedItem(_)` doesn't allow shared items to be stored as temporary items
func test_saveTemporarySharedItem_sharedItemsIgnored() async throws {
let item = ItemListItem.fixtureShared()

try await subject.saveTemporarySharedItem(item)
let result = sharedItemService.tempItem

XCTAssertNil(result)
}

/// `saveTemporarySharedItem(_)` saves a temporary item into the Authenticator Bridge shared store.
func test_saveTemporarySharedItem_success() async throws {
let totpKey = "TOTP Key"
let item = ItemListItem.fixture(totp: .fixture(itemView: .fixture(totpKey: totpKey)))
let item = AuthenticatorItemView.fixture(totpKey: totpKey)

try await subject.saveTemporarySharedItem(item)
let result = try XCTUnwrap(sharedItemService.tempItem)
Expand All @@ -255,12 +245,12 @@ class AuthenticatorItemRepositoryTests: AuthenticatorTestCase { // swiftlint:dis
XCTAssertEqual(result.id, item.id)
XCTAssertEqual(result.name, item.name)
XCTAssertEqual(result.totpKey, totpKey)
XCTAssertEqual(result.username, item.accountName)
XCTAssertEqual(result.username, item.username)
}

/// `saveTemporarySharedItem(_)` throws errors received from the `AuthenticatorBridgeItemService`.
func test_saveTemporarySharedItem_throwsError() async throws {
let item = ItemListItem.fixture(totp: .fixture())
let item = AuthenticatorItemView.fixture()
let error = AuthenticatorTestError.example

sharedItemService.errorToThrow = error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class MockAuthenticatorItemRepository: AuthenticatorItemRepository {

var searchItemListSubject = CurrentValueSubject<[ItemListItem], Error>([])

var tempItem: ItemListItem?
var tempItem: AuthenticatorItemView?
var tempItemErrorToThrow: Error?

var timeProvider: TimeProvider = MockTimeProvider(.currentTime)
Expand Down Expand Up @@ -67,7 +67,7 @@ class MockAuthenticatorItemRepository: AuthenticatorItemRepository {
return try refreshTotpCodesResult.get()
}

func saveTemporarySharedItem(_ item: ItemListItem) async throws {
func saveTemporarySharedItem(_ item: AuthenticatorItemView) async throws {
if let tempItemErrorToThrow {
throw tempItemErrorToThrow
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,5 @@
"LocalCodes" = "Local codes";
"AccountsSyncedFromBitwardenApp" = "Accounts synced from Bitwarden app";
"MoveToBitwarden" = "Move to Bitwarden";
"AddCodeToBitwarden" = "Add code to Bitwarden";
"AddCodeLocally" = "Add code locally";
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ extension ImportItemsProcessor: AuthenticatorKeyCaptureDelegate {
func didCompleteManualCapture(
_ captureCoordinator: AnyCoordinator<AuthenticatorKeyCaptureRoute, AuthenticatorKeyCaptureEvent>,
key: String,
name: String
name: String,
sendToBitwarden: Bool
) {}

func showCameraScan(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator {
& TutorialModule

typealias Services = HasApplication
& HasAuthenticatorItemRepository
& HasBiometricsRepository
& HasCameraService
& HasConfigService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ final class ItemListProcessor: StateProcessor<ItemListState, ItemListAction, Ite
await generateAndCopyTotpCode(totpKey: totpKey)
}
case let .moveToBitwardenPressed(item):
guard case .totp = item.itemType else { return }
await moveItemToBitwarden(item: item)
guard case let .totp(model) = item.itemType else { return }
await moveItemToBitwarden(item: model.itemView)
case .refresh:
await streamItemList()
case let .search(text):
Expand Down Expand Up @@ -171,7 +171,7 @@ final class ItemListProcessor: StateProcessor<ItemListState, ItemListAction, Ite
///
/// - Parameter item: the item to be moved.
///
private func moveItemToBitwarden(item: ItemListItem) async {
private func moveItemToBitwarden(item: AuthenticatorItemView) async {
guard await services.configService.getFeatureFlag(.enablePasswordManagerSync),
let application = services.application,
application.canOpenURL(ExternalLinksConstants.passwordManagerScheme)
Expand Down Expand Up @@ -465,17 +465,18 @@ extension ItemListProcessor: AuthenticatorKeyCaptureDelegate {
func didCompleteManualCapture(
_ captureCoordinator: AnyCoordinator<AuthenticatorKeyCaptureRoute, AuthenticatorKeyCaptureEvent>,
key: String,
name: String
name: String,
sendToBitwarden: Bool
) {
let dismissAction = DismissAction(action: { [weak self] in
Task {
await self?.parseAndValidateManualKey(key: key, name: name)
await self?.parseAndValidateManualKey(key: key, name: name, sendToBitwarden: sendToBitwarden)
}
})
captureCoordinator.navigate(to: .dismiss(dismissAction))
}

func parseAndValidateManualKey(key: String, name: String) async {
func parseAndValidateManualKey(key: String, name: String, sendToBitwarden: Bool) async {
do {
let authKeyModel = try services.totpService.getTOTPConfiguration(key: key)
let loginTotpState: LoginTOTPState
Expand All @@ -499,9 +500,13 @@ extension ItemListProcessor: AuthenticatorKeyCaptureDelegate {
totpKey: key,
username: nil
)
try await services.authenticatorItemRepository.addAuthenticatorItem(newItem)
state.toast = Toast(text: Localizations.verificationCodeAdded)
await perform(.refresh)
if sendToBitwarden {
await moveItemToBitwarden(item: newItem)
} else {
try await services.authenticatorItemRepository.addAuthenticatorItem(newItem)
state.toast = Toast(text: Localizations.verificationCodeAdded)
await perform(.refresh)
}
} catch {
coordinator.showAlert(.totpScanFailureAlert())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,12 +279,13 @@ class ItemListProcessorTests: AuthenticatorTestCase { // swiftlint:disable:this
func test_perform_moveToBitwardenPressed_localItem() async throws {
configService.featureFlagsBool[.enablePasswordManagerSync] = true
application.canOpenUrlResponse = true
let localItem = ItemListItem.fixture()
let expected = AuthenticatorItemView.fixture()
let localItem = ItemListItem.fixture(totp: .fixture(itemView: expected))

await subject.perform(.moveToBitwardenPressed(localItem))

waitFor(authItemRepository.tempItem != nil)
XCTAssertEqual(authItemRepository.tempItem, localItem)
XCTAssertEqual(authItemRepository.tempItem, expected)
XCTAssertEqual(subject.state.url, ExternalLinksConstants.passwordManagerNewItem)
}

Expand Down Expand Up @@ -642,6 +643,90 @@ class ItemListProcessorTests: AuthenticatorTestCase { // swiftlint:disable:this
XCTAssertEqual(item.totpKey, String.base32Key)
}

/// `didCompleteManualCapture` failure
func test_didCompleteManualCapture_failure() {
totpService.getTOTPConfigResult = .failure(TOTPServiceError.invalidKeyFormat)
let captureCoordinator = MockCoordinator<AuthenticatorKeyCaptureRoute, AuthenticatorKeyCaptureEvent>()
subject.didCompleteManualCapture(captureCoordinator.asAnyCoordinator(),
key: "1234",
name: "name",
sendToBitwarden: false)
var dismissAction: DismissAction?
if case let .dismiss(onDismiss) = captureCoordinator.routes.last {
dismissAction = onDismiss
}
XCTAssertNotNil(dismissAction)
dismissAction?.action()
waitFor(!coordinator.alertShown.isEmpty)
XCTAssertEqual(
coordinator.alertShown.last,
Alert(
title: Localizations.keyReadError,
message: nil,
alertActions: [
AlertAction(title: Localizations.ok, style: .default),
]
)
)
XCTAssertEqual(authItemRepository.addAuthItemAuthItems, [])
XCTAssertNil(subject.state.toast)
}

/// `didCompleteManualCapture` success with a locally saved item
func test_didCompleteManualCapture_localSuccess() throws {
let key = String.base32Key
let keyConfig = try XCTUnwrap(TOTPKeyModel(authenticatorKey: key))
totpService.getTOTPConfigResult = .success(keyConfig)
authItemRepository.itemListSubject.value = [ItemListSection.fixture()]
let captureCoordinator = MockCoordinator<AuthenticatorKeyCaptureRoute, AuthenticatorKeyCaptureEvent>()
subject.didCompleteManualCapture(captureCoordinator.asAnyCoordinator(),
key: key,
name: "name",
sendToBitwarden: false)
var dismissAction: DismissAction?
if case let .dismiss(onDismiss) = captureCoordinator.routes.last {
dismissAction = onDismiss
}
XCTAssertNotNil(dismissAction)
dismissAction?.action()
waitFor(!authItemRepository.addAuthItemAuthItems.isEmpty)
waitFor(subject.state.loadingState != .loading(nil))
guard let item = authItemRepository.addAuthItemAuthItems.first
else {
XCTFail("Unable to get authenticator item")
return
}
XCTAssertEqual(item.name, "name")
XCTAssertEqual(item.totpKey, String.base32Key)
}

/// `didCompleteManualCapture` success with `sendToBitwarden` item
func test_didCompleteManualCapture_sendToBitwardenSuccess() throws {
configService.featureFlagsBool[.enablePasswordManagerSync] = true
application.canOpenUrlResponse = true
let key = String.otpAuthUriKeyComplete
let keyConfig = try XCTUnwrap(TOTPKeyModel(authenticatorKey: key))
let expected = AuthenticatorItemView.fixture(name: "name", totpKey: key)
totpService.getTOTPConfigResult = .success(keyConfig)
authItemRepository.itemListSubject.value = [ItemListSection.fixture()]
let captureCoordinator = MockCoordinator<AuthenticatorKeyCaptureRoute, AuthenticatorKeyCaptureEvent>()
subject.didCompleteManualCapture(captureCoordinator.asAnyCoordinator(),
key: key,
name: "name",
sendToBitwarden: true)
var dismissAction: DismissAction?
if case let .dismiss(onDismiss) = captureCoordinator.routes.last {
dismissAction = onDismiss
}
XCTAssertNotNil(dismissAction)
dismissAction?.action()

waitFor(authItemRepository.tempItem != nil)
XCTAssertEqual(authItemRepository.tempItem?.totpKey, expected.totpKey)
XCTAssertEqual(authItemRepository.tempItem?.name, expected.name)
XCTAssertEqual(subject.state.url, ExternalLinksConstants.passwordManagerNewItem)
}

/// Tests that the `itemListCardState` is set to `none` if the download card has been closed.
func test_determineItemListCardState_closed_download() async {
configService.featureFlagsBool = [.enablePasswordManagerSync: true]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@ protocol AuthenticatorKeyCaptureDelegate: AnyObject {
/// Called when the manual key entry flow has been completed.
///
/// - Parameters:
/// - coordinator: The coordinator sending the action.
/// - captureCoordinator: The coordinator sending the action.
/// - key: The key the user input.
/// - name: The name the user input.
/// - sendToBitwarden: `true` if the code should be sent to the Password Manager app,
/// `false` is it should be stored locally.
///
func didCompleteManualCapture(
_ captureCoordinator: AnyCoordinator<AuthenticatorKeyCaptureRoute, AuthenticatorKeyCaptureEvent>,
key: String,
name: String
name: String,
sendToBitwarden: Bool
)

/// Called when the scan flow requests the scan code screen.
Expand All @@ -56,7 +59,9 @@ protocol AuthenticatorKeyCaptureDelegate: AnyObject {
final class AuthenticatorKeyCaptureCoordinator: Coordinator, HasStackNavigator {
// MARK: Types

typealias Services = HasCameraService
typealias Services = HasAuthenticatorItemRepository
& HasCameraService
& HasConfigService
& HasErrorReporter

// MARK: Private Properties
Expand Down Expand Up @@ -121,11 +126,12 @@ final class AuthenticatorKeyCaptureCoordinator: Coordinator, HasStackNavigator {
stackNavigator?.dismiss(completion: {
onDismiss?.action()
})
case let .addManual(key: authKey, name: name):
case let .addManual(key: authKey, name: name, sendToBitwarden: sendToBitwarden):
delegate?.didCompleteManualCapture(
asAnyCoordinator(),
key: authKey,
name: name
name: name,
sendToBitwarden: sendToBitwarden
)
case .manualKeyEntry:
guard let stackNavigator else { return }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,29 @@ class AuthenticatorKeyCaptureCoordinatorTests: AuthenticatorTestCase {
// MARK: Tests

/// `navigate(to:)` with `.addManual` instructs the delegate that the capture flow has
/// completed.
/// completed. Passing `false` to `sendToBitwarden` passes `false` to the delegate.
func test_navigateTo_addManual() {
delegate.didCompleteManualCaptureSendToBitwarden = true
let name = "manual name"
let entry = "manuallyManagedMagic"
subject.navigate(to: .addManual(key: entry, name: name, sendToBitwarden: false))
XCTAssertTrue(delegate.didCompleteManualCaptureCalled)
XCTAssertEqual(delegate.didCompleteManualCaptureKey, entry)
XCTAssertEqual(delegate.didCompleteManualCaptureName, name)
XCTAssertFalse(delegate.didCompleteManualCaptureSendToBitwarden)
XCTAssertNotNil(delegate.capturedCaptureCoordinator)
}

/// `navigate(to:)` with `.addManual` instructs the delegate that the capture flow has
/// completed. Passing `true` to `sendToBitwarden` passes `true` to the delegate.
func test_navigateTo_addManual_sendToBitwarden() {
let name = "manual name"
let entry = "manuallyManagedMagic"
subject.navigate(to: .addManual(key: entry, name: name))
subject.navigate(to: .addManual(key: entry, name: name, sendToBitwarden: true))
XCTAssertTrue(delegate.didCompleteManualCaptureCalled)
XCTAssertEqual(delegate.didCompleteManualCaptureKey, entry)
XCTAssertEqual(delegate.didCompleteManualCaptureName, name)
XCTAssertTrue(delegate.didCompleteManualCaptureSendToBitwarden)
XCTAssertNotNil(delegate.capturedCaptureCoordinator)
}

Expand Down Expand Up @@ -237,6 +252,7 @@ class MockAuthenticatorKeyCaptureDelegate: AuthenticatorKeyCaptureDelegate {
var didCompleteManualCaptureCalled = false
var didCompleteManualCaptureKey: String?
var didCompleteManualCaptureName: String?
var didCompleteManualCaptureSendToBitwarden = false

/// A flag to capture a `showCameraScan` call.
var didRequestCamera: Bool = false
Expand Down Expand Up @@ -271,12 +287,14 @@ class MockAuthenticatorKeyCaptureDelegate: AuthenticatorKeyCaptureDelegate {
func didCompleteManualCapture(
_ captureCoordinator: AnyCoordinator<AuthenticatorKeyCaptureRoute, AuthenticatorKeyCaptureEvent>,
key: String,
name: String
name: String,
sendToBitwarden: Bool
) {
didCompleteManualCaptureCalled = true
capturedCaptureCoordinator = captureCoordinator
didCompleteManualCaptureKey = key
didCompleteManualCaptureName = name
didCompleteManualCaptureSendToBitwarden = sendToBitwarden
}

func showCameraScan(
Expand Down
Loading

0 comments on commit b117cf3

Please sign in to comment.