Skip to content

Commit

Permalink
[PM-10403] Added SSH key cipher item view (#921)
Browse files Browse the repository at this point in the history
Co-authored-by: Katherine Bertelsen <[email protected]>
Co-authored-by: Robyn MacCallum <[email protected]>
  • Loading branch information
3 people authored Oct 3, 2024
1 parent 5e5f36f commit cab739d
Show file tree
Hide file tree
Showing 41 changed files with 744 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,9 @@ extension BitwardenSdk.CipherType {
self = .card
case .identity:
self = .identity
case .sshKey:
// TODO: PM-10401 set self = .sshKey when SDK is ready.
self = .init(rawValue: 5)!
}
}
}
Expand Down
11 changes: 10 additions & 1 deletion BitwardenShared/Core/Vault/Models/Enum/CipherType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ enum CipherType: Int, Codable {

/// Personal information for filling out forms.
case identity = 4

/// An SSH key.
case sshKey = 5
}

extension CipherType {
Expand Down Expand Up @@ -40,7 +43,7 @@ extension CipherType {
}

extension CipherType: CaseIterable {
static let allCases: [CipherType] = [.login, .card, .identity, .secureNote]
static let allCases: [CipherType] = [.login, .card, .identity, .secureNote, .sshKey]
}

extension CipherType: Menuable {
Expand All @@ -50,6 +53,12 @@ extension CipherType: Menuable {
case .identity: return Localizations.typeIdentity
case .login: return Localizations.typeLogin
case .secureNote: return Localizations.typeSecureNote
case .sshKey: return Localizations.sshKey
}
}
}

extension CipherType {
/// These are the cases of `CipherType` that the user can use to create a cipher.
static let canCreateCases: [CipherType] = [.login, .card, .identity, .secureNote]
}
5 changes: 5 additions & 0 deletions BitwardenShared/Core/Vault/Models/Enum/CipherTypeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@ class CipherTypeTests: BitwardenTestCase {
XCTAssertEqual(CipherType(group: .secureNote), .secureNote)
XCTAssertNil(CipherType(group: .trash))
}

/// `canCreateCases` return the correct cipher types that the user can use to create ciphers.
func test_canCreateCases() {
XCTAssertEqual(CipherType.canCreateCases, [.login, .card, .identity, .secureNote])
}
}
2 changes: 2 additions & 0 deletions BitwardenShared/Core/Vault/Models/Enum/LinkedIdType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ extension LinkedIdType {
]
case .secureNote:
[]
case .sshKey:
[]
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,10 @@ class LinkedIdTypeTests: BitwardenTestCase {
let expected: [LinkedIdType] = []
XCTAssertEqual(result, expected)
}

func test_getLinkedIdType_sshKey() {
let result = LinkedIdType.getLinkedIdType(for: .sshKey)
let expected: [LinkedIdType] = []
XCTAssertEqual(result, expected)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,9 @@
"PleaseRestartRegistrationOrTryLoggingInYouMayAlreadyHaveAnAccount" = "Please restart registration or try logging in. You may already have an account.";
"RemovePasskey" = "Remove passkey";
"PasskeyRemoved" = "Passkey removed";
"SSHKey" = "SSH key";
"PrivateKey" = "Private key";
"PublicKey" = "Public key";
"TheRegionForTheGivenEmailCouldNotBeLoaded" = "The region for the given email could not be loaded.";
"SetUpLaterQuestion" = "Set up later?";
"YouCanFinishSetupUnlockAnytimeDescriptionLong" = "You can finish setup anytime in Account Security under Settings. You’ll use your master password to unlock until you set up another method.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ enum AlertError: LocalizedError {
}

extension Alert {
/// Simulates tapping the cancel button of the alert.
func tapCancel() async throws{

Check warning on line 21 in BitwardenShared/UI/Platform/Application/Utilities/Alert/Alert/TestHelpers/Alert+TestHelpers.swift

View workflow job for this annotation

GitHub Actions / Test

Opening Brace Spacing Violation: Opening braces should be preceded by a single space and on the same line as the declaration (opening_brace)

Check warning on line 21 in BitwardenShared/UI/Platform/Application/Utilities/Alert/Alert/TestHelpers/Alert+TestHelpers.swift

View workflow job for this annotation

GitHub Actions / Test

(spaceAroundBraces) Add or remove space around curly braces.
try await tapAction(title: Localizations.cancel)
}

/// Simulates a user interaction with the alert action that matches the provided title.
///
/// - Parameters:
Expand Down Expand Up @@ -51,4 +56,18 @@ extension Alert {
simulatedTextField.text = text
textField.textChanged(in: simulatedTextField)
}

/// Fills the "password" TextField with the `with` parameter and simulatess tapping the "Submit" button.
/// - Parameter with: Value to enter into the TextField.
func submitMasterPasswordReprompt(with password: String) async throws {
try await tapAction(
title: Localizations.submit,
alertTextFields: [
AlertTextField(
id: "password",
text: password
),
]
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ enum AddEditItemAction: Equatable, Sendable {
/// The password field was changed.
case passwordChanged(String)

/// The ssh key item action.
case sshKeyItemAction(ViewSSHKeyItemAction)

/// The toast was shown or hidden.
case toastShown(Toast?)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ final class AddEditItemProcessor: StateProcessor<// swiftlint:disable:this type_
)
}
}
case let .sshKeyItemAction(sshKeyAction):
handleSSHKeyAction(sshKeyAction)
case let .toastShown(newValue):
state.toast = newValue
case .totpFieldLeftFocus:
Expand Down Expand Up @@ -347,6 +349,18 @@ final class AddEditItemProcessor: StateProcessor<// swiftlint:disable:this type_
}
}

/// Handles `ViewSSHKeyItemAction` events.
/// - Parameter sshKeyAction: The action to handle
private func handleSSHKeyAction(_ sshKeyAction: ViewSSHKeyItemAction) {
switch sshKeyAction {
case .copyPressed:
return
case .privateKeyVisibilityPressed:
state.sshKeyState.isPrivateKeyVisible.toggle()
// TODO: PM-11977 Collect visibility toggled event
}
}

/// Receives an `AddEditCardItem` action from the `AddEditCardView` view's store, and updates
/// the `AddEditCardState`.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1567,6 +1567,16 @@ class AddEditItemProcessorTests: BitwardenTestCase {
XCTAssertEqual(subject.state.loginState.password, "")
}

/// `receive(_:)` with `.sshKeyItemAction` and `privateKeyVisibilityPressed` toggles
/// the visibility of the `privateKey` field.
@MainActor
func test_receive_sshKeyItemAction_withoutValue() {
subject.state.sshKeyState.isPrivateKeyVisible = false
subject.receive(.sshKeyItemAction(.privateKeyVisibilityPressed))

XCTAssertTrue(subject.state.sshKeyState.isPrivateKeyVisible)
}

/// `receive(_:)` with `.toastShown` without a value updates the state correctly.
@MainActor
func test_receive_toastShown_withoutValue() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ protocol AddEditItemState: Sendable {
/// If master password reprompt toggle should be shown.
var showMasterPasswordReprompt: Bool { get set }

/// The SSH key item state.
var sshKeyState: SSHKeyItemState { get set }

/// A toast message to show in the view.
var toast: Toast? { get set }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ struct AddEditItemView: View {
BitwardenMenuField(
title: Localizations.type,
accessibilityIdentifier: "ItemTypePicker",
options: CipherType.allCases,
options: CipherType.canCreateCases,
selection: store.binding(
get: \.type,
send: AddEditItemAction.typeChanged
Expand All @@ -163,6 +163,8 @@ struct AddEditItemView: View {
EmptyView()
case .identity:
identityItems
case .sshKey:
sshKeyItems
}
}
}
Expand Down Expand Up @@ -192,6 +194,17 @@ struct AddEditItemView: View {
)
)
}

@ViewBuilder private var sshKeyItems: some View {
ViewSSHKeyItemView(
showCopyButtons: false,
store: store.child(
state: { _ in store.state.sshKeyState },
mapAction: { .sshKeyItemAction($0) },
mapEffect: nil
)
)
}
}

private extension AddEditItemView {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -805,4 +805,47 @@ class AddEditItemViewTests: BitwardenTestCase { // swiftlint:disable:this type_b
)
}
}

/// Snapshots the previews for SSH key type.
@MainActor
func test_snapshot_sshKey() {
processor.state = sshKeyCipherItemState(isPrivateKeyVisible: false)
assertSnapshots(
of: subject,
as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5]
)
}

/// Snapshots the previews for SSH key type when private key is visible.
@MainActor
func test_snapshot_sshKeyPrivateKeyVisible() {
processor.state = sshKeyCipherItemState(isPrivateKeyVisible: true)
assertSnapshots(
of: subject,
as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5]
)
}

// MARK: Private

/// Creates a `CipherItemState` for an SSH key item.
/// - Parameter isPrivateKeyVisible: Whether the private key is visible.
/// - Returns: The `CipherItemState` for SSH key item.
private func sshKeyCipherItemState(isPrivateKeyVisible: Bool) -> CipherItemState {
var state = CipherItemState(
existing: .fixture(
id: "fake-id"
),
hasPremium: true
)!
state.name = "Example"
state.type = .sshKey
state.sshKeyState = SSHKeyItemState(
isPrivateKeyVisible: isPrivateKeyVisible,
privateKey: "ajsdfopij1ZXCVZXC12312QW",
publicKey: "ssh-ed25519 AAAAA/asdjfoiwejrpo23323j23ASdfas",
keyFingerprint: "SHA-256:2qwer233ADJOIq1adfweqe21321qw"
)
return state
}
} // swiftlint:disable:this file_length
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import BitwardenSdk
import Foundation

// MARK: - SSHKeyItemState

/// The state for an SSH key item.
struct SSHKeyItemState: Equatable, Sendable {
/// The visibility of the private key.
var isPrivateKeyVisible: Bool = false

/// The private key of the SSH key.
var privateKey: String = ""

/// The public key of the SSH key.
var publicKey: String = ""

/// The fingerprint of the SSH key.
var keyFingerprint: String = ""
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions BitwardenShared/UI/Vault/VaultItem/CipherItemState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ struct CipherItemState: Equatable {
/// If master password reprompt toggle should be shown
var showMasterPasswordReprompt: Bool

/// The SSH key item state.
var sshKeyState: SSHKeyItemState

/// A toast for the AddEditItemView
var toast: Toast?

Expand Down Expand Up @@ -162,6 +165,7 @@ struct CipherItemState: Equatable {
name: String,
notes: String,
organizationId: String?,
sshKeyState: SSHKeyItemState,
type: CipherType,
updatedDate: Date
) {
Expand All @@ -183,6 +187,7 @@ struct CipherItemState: Equatable {
self.organizationId = organizationId
ownershipOptions = []
showMasterPasswordReprompt = true
self.sshKeyState = sshKeyState
self.type = type
self.updatedDate = updatedDate
self.configuration = configuration
Expand Down Expand Up @@ -224,6 +229,7 @@ struct CipherItemState: Equatable {
name: name ?? uri.flatMap(URL.init)?.host ?? "",
notes: "",
organizationId: organizationId,
sshKeyState: .init(),
type: type,
updatedDate: .now
)
Expand All @@ -246,6 +252,7 @@ struct CipherItemState: Equatable {
name: "\(cipherView.name) - \(Localizations.clone)",
notes: cipherView.notes ?? "",
organizationId: cipherView.organizationId,
sshKeyState: cipherView.sshKeyItemState(),
type: .init(type: cipherView.type),
updatedDate: cipherView.revisionDate
)
Expand Down Expand Up @@ -276,6 +283,7 @@ struct CipherItemState: Equatable {
name: cipherView.name,
notes: cipherView.notes ?? "",
organizationId: cipherView.organizationId,
sshKeyState: cipherView.sshKeyItemState(),
type: .init(type: cipherView.type),
updatedDate: cipherView.revisionDate
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class CipherItemStateTests: BitwardenTestCase {
XCTAssertEqual(state.loginState, cipher.loginItemState(excludeFido2Credentials: true, showTOTP: true))
XCTAssertTrue(state.loginState.fido2Credentials.isEmpty)
XCTAssertEqual(state.notes, cipher.notes ?? "")
XCTAssertEqual(state.sshKeyState, cipher.sshKeyItemState())
XCTAssertEqual(state.type, .init(type: cipher.type))
XCTAssertEqual(state.updatedDate, cipher.revisionDate)
}
Expand Down
Loading

0 comments on commit cab739d

Please sign in to comment.