diff --git a/BitwardenShared/Core/Vault/Extensions/BitwardenSdk+Vault.swift b/BitwardenShared/Core/Vault/Extensions/BitwardenSdk+Vault.swift index 5600cc61b..b956d0e84 100644 --- a/BitwardenShared/Core/Vault/Extensions/BitwardenSdk+Vault.swift +++ b/BitwardenShared/Core/Vault/Extensions/BitwardenSdk+Vault.swift @@ -80,6 +80,7 @@ extension CipherDetailsResponseModel { reprompt: BitwardenShared.CipherRepromptType(type: cipher.reprompt), revisionDate: cipher.revisionDate, secureNote: cipher.secureNote.map(CipherSecureNoteModel.init), + sshKey: cipher.sshKey.map(CipherSSHKeyModel.init), type: BitwardenShared.CipherType(type: cipher.type), viewPassword: cipher.viewPassword ) @@ -192,6 +193,16 @@ extension CipherSecureNoteModel { } } +extension CipherSSHKeyModel { + init(sshKey: BitwardenSdk.SshKey) { + self.init( + keyFingerprint: sshKey.fingerprint, + privateKey: sshKey.privateKey, + publicKey: sshKey.publicKey + ) + } +} + extension CipherType { init(type: BitwardenSdk.CipherType) { switch type { @@ -203,6 +214,8 @@ extension CipherType { self = .login case .secureNote: self = .secureNote + case .sshKey: + self = .sshKey } } } @@ -300,6 +313,7 @@ extension BitwardenSdk.Cipher { identity: model.identity.map(Identity.init), card: model.card.map(Card.init), secureNote: model.secureNote.map(SecureNote.init), + sshKey: model.sshKey.map(SshKey.init), favorite: model.favorite, reprompt: BitwardenSdk.CipherRepromptType(model.reprompt), organizationUseTotp: model.organizationUseTotp, @@ -347,6 +361,7 @@ extension BitwardenSdk.CipherView: Identifiable { identity: nil, card: nil, secureNote: nil, + sshKey: nil, favorite: false, reprompt: .none, organizationUseTotp: false, @@ -528,6 +543,16 @@ extension BitwardenSdk.SecureNoteType { } } +extension BitwardenSdk.SshKey { + init(sshKeyModel model: CipherSSHKeyModel) { + self.init( + privateKey: model.privateKey, + publicKey: model.publicKey, + fingerprint: model.keyFingerprint + ) + } +} + extension BitwardenSdk.UriMatchType { init(type: UriMatchType) { switch type { diff --git a/BitwardenShared/Core/Vault/Extensions/BitwardenSdkVaultTests.swift b/BitwardenShared/Core/Vault/Extensions/BitwardenSdkVaultTests.swift index be1a9f14f..b4fcfdc09 100644 --- a/BitwardenShared/Core/Vault/Extensions/BitwardenSdkVaultTests.swift +++ b/BitwardenShared/Core/Vault/Extensions/BitwardenSdkVaultTests.swift @@ -5,6 +5,91 @@ import XCTest @testable import BitwardenShared +// MARK: - Cipher + +class BitwardenSdkVaultCipherTests: BitwardenTestCase { + // MARK: Tests + + /// `init(responseModel:)` inits the correct Cipher from CipherDetailsResponseModel with `.sshKey` type. + func test_init_fromCipherDetailsResponseModelWithSSHKey() { + let responseModel = CipherDetailsResponseModel.fixture( + id: "1", + sshKey: .fixture(), + type: .sshKey + ) + let cipher = Cipher(responseModel: responseModel) + XCTAssertEqual(cipher.id, responseModel.id) + XCTAssertEqual(Int(cipher.type.rawValue), responseModel.type.rawValue) + XCTAssertEqual(cipher.sshKey?.publicKey, responseModel.sshKey?.publicKey) + XCTAssertEqual(cipher.sshKey?.privateKey, responseModel.sshKey?.privateKey) + XCTAssertEqual(cipher.sshKey?.fingerprint, responseModel.sshKey?.keyFingerprint) + } +} + +// MARK: - CipherDetailsResponseModel + +class BitwardenSdkVaultCipherDetailsResponseModelTests: BitwardenTestCase { // swiftlint:disable:this type_name + // MARK: Tests + + /// `init(cipher:)` Inits a cipher details response model from an SDK cipher without id throws. + func test_init_fromSdkNoIdThrows() throws { + let cipher = Cipher.fixture( + id: nil + ) + XCTAssertThrowsError(try CipherDetailsResponseModel(cipher: cipher)) + } + + /// `init(cipher:)` Inits a cipher details response model from an SDK cipher that is an SSH key. + func test_init_fromSdkCipherSSHKey() throws { + let cipher = Cipher.fixture( + id: "1", + sshKey: .fixture(), + type: .sshKey + ) + let responseModel = try CipherDetailsResponseModel(cipher: cipher) + XCTAssertEqual(responseModel.id, cipher.id) + XCTAssertEqual(responseModel.sshKey?.privateKey, cipher.sshKey?.privateKey) + XCTAssertEqual(responseModel.sshKey?.publicKey, cipher.sshKey?.publicKey) + XCTAssertEqual(responseModel.sshKey?.keyFingerprint, cipher.sshKey?.fingerprint) + } +} + +// MARK: - CipherSSHKeyModel + +class BitwardenSdkVaultCipherSSHKeyModelTests: BitwardenTestCase { + // MARK: Tests + + /// `init(sshKey:)` Inits cipher SSH key model from the SDK one. + func test_init_fromSdkSSHKey() { + let model = CipherSSHKeyModel( + sshKey: .init( + privateKey: "privateKey", + publicKey: "publicKey", + fingerprint: "fingerprint" + ) + ) + + XCTAssertEqual(model.privateKey, "privateKey") + XCTAssertEqual(model.publicKey, "publicKey") + XCTAssertEqual(model.keyFingerprint, "fingerprint") + } +} + +// MARK: - CipherType + +class BitwardenSdkVaultCipherTypeTests: BitwardenTestCase { + // MARK: Tests + + /// `init(type:)` initializes the cipher type based on the SDK cipher type. + func test_init_bySdkCipherType() { + XCTAssertEqual(CipherType(type: .login), .login) + XCTAssertEqual(CipherType(type: .card), .card) + XCTAssertEqual(CipherType(type: .identity), .identity) + XCTAssertEqual(CipherType(type: .secureNote), .secureNote) + XCTAssertEqual(CipherType(type: .sshKey), .sshKey) + } +} + // MARK: - CipherView class CipherViewTests: BitwardenTestCase { @@ -57,6 +142,7 @@ class CipherViewTests: BitwardenTestCase { identity: nil, card: nil, secureNote: nil, + sshKey: nil, favorite: false, reprompt: .none, organizationUseTotp: false, @@ -105,6 +191,7 @@ class CipherViewTests: BitwardenTestCase { identity: nil, card: nil, secureNote: nil, + sshKey: nil, favorite: false, reprompt: .none, organizationUseTotp: false, diff --git a/BitwardenShared/Core/Vault/Models/API/CipherSSHKeyModel.swift b/BitwardenShared/Core/Vault/Models/API/CipherSSHKeyModel.swift new file mode 100644 index 000000000..434b502a5 --- /dev/null +++ b/BitwardenShared/Core/Vault/Models/API/CipherSSHKeyModel.swift @@ -0,0 +1,14 @@ +/// API model for an SSH key. +/// +struct CipherSSHKeyModel: Codable, Equatable { + // MARK: Properties + + /// The key fingerprint of the SSH key. + let keyFingerprint: String? + + /// The private key of the SSH key. + let privateKey: String? + + /// The public key of the SSH key. + let publicKey: String? +} diff --git a/BitwardenShared/Core/Vault/Models/API/Fixtures/CipherSSHKeyModel+Fixtures.swift b/BitwardenShared/Core/Vault/Models/API/Fixtures/CipherSSHKeyModel+Fixtures.swift new file mode 100644 index 000000000..8c5749272 --- /dev/null +++ b/BitwardenShared/Core/Vault/Models/API/Fixtures/CipherSSHKeyModel+Fixtures.swift @@ -0,0 +1,17 @@ +import Foundation + +@testable import BitwardenShared + +extension CipherSSHKeyModel { + static func fixture( + keyFingerprint: String? = "keyFingerprint", + privateKey: String? = "privateKey", + publicKey: String? = "publicKey" + ) -> CipherSSHKeyModel { + self.init( + keyFingerprint: keyFingerprint, + privateKey: privateKey, + publicKey: publicKey + ) + } +} diff --git a/BitwardenShared/Core/Vault/Models/Response/CipherDetailsResponseModel.swift b/BitwardenShared/Core/Vault/Models/Response/CipherDetailsResponseModel.swift index 440db9597..7f1c09ec5 100644 --- a/BitwardenShared/Core/Vault/Models/Response/CipherDetailsResponseModel.swift +++ b/BitwardenShared/Core/Vault/Models/Response/CipherDetailsResponseModel.swift @@ -70,6 +70,9 @@ struct CipherDetailsResponseModel: JSONResponse, Equatable { /// Secure note data if the cipher is a secure note. let secureNote: CipherSecureNoteModel? + /// SSH key if the `type` is `.sshKey`. + let sshKey: CipherSSHKeyModel? + /// The type of the cipher. let type: CipherType diff --git a/BitwardenShared/Core/Vault/Models/Response/Fixtures/CipherDetailsResponseModel+Fixtures.swift b/BitwardenShared/Core/Vault/Models/Response/Fixtures/CipherDetailsResponseModel+Fixtures.swift index d4163afe6..f1dd1df78 100644 --- a/BitwardenShared/Core/Vault/Models/Response/Fixtures/CipherDetailsResponseModel+Fixtures.swift +++ b/BitwardenShared/Core/Vault/Models/Response/Fixtures/CipherDetailsResponseModel+Fixtures.swift @@ -25,6 +25,7 @@ extension CipherDetailsResponseModel { reprompt: CipherRepromptType = .none, revisionDate: Date = Date(), secureNote: CipherSecureNoteModel? = nil, + sshKey: CipherSSHKeyModel? = nil, type: CipherType = .login, viewPassword: Bool = false ) -> CipherDetailsResponseModel { @@ -50,6 +51,7 @@ extension CipherDetailsResponseModel { reprompt: reprompt, revisionDate: revisionDate, secureNote: secureNote, + sshKey: sshKey, type: type, viewPassword: viewPassword ) diff --git a/BitwardenShared/Core/Vault/Services/API/Cipher/CipherAPIServiceTests.swift b/BitwardenShared/Core/Vault/Services/API/Cipher/CipherAPIServiceTests.swift index 4bcb0bde7..727c15370 100644 --- a/BitwardenShared/Core/Vault/Services/API/Cipher/CipherAPIServiceTests.swift +++ b/BitwardenShared/Core/Vault/Services/API/Cipher/CipherAPIServiceTests.swift @@ -72,6 +72,7 @@ class CipherAPIServiceTests: XCTestCase { // swiftlint:disable:this type_body_le reprompt: .none, revisionDate: Date(timeIntervalSince1970: 1_691_656_425.345), secureNote: nil, + sshKey: nil, type: .login, viewPassword: true ) @@ -121,6 +122,7 @@ class CipherAPIServiceTests: XCTestCase { // swiftlint:disable:this type_body_le reprompt: .none, revisionDate: Date(timeIntervalSince1970: 1_691_656_425.345), secureNote: nil, + sshKey: nil, type: .login, viewPassword: true ) @@ -248,6 +250,7 @@ class CipherAPIServiceTests: XCTestCase { // swiftlint:disable:this type_body_le reprompt: .none, revisionDate: Date(timeIntervalSince1970: 1_691_656_425.345), secureNote: nil, + sshKey: nil, type: .login, viewPassword: true ), @@ -300,6 +303,7 @@ class CipherAPIServiceTests: XCTestCase { // swiftlint:disable:this type_body_le reprompt: .none, revisionDate: Date(timeIntervalSince1970: 1_691_656_425.345), secureNote: nil, + sshKey: nil, type: .login, viewPassword: true ) diff --git a/BitwardenShared/Core/Vault/Services/TestHelpers/BitwardenSdk+VaultMocking.swift b/BitwardenShared/Core/Vault/Services/TestHelpers/BitwardenSdk+VaultMocking.swift index b74478e9c..fe2a586e7 100644 --- a/BitwardenShared/Core/Vault/Services/TestHelpers/BitwardenSdk+VaultMocking.swift +++ b/BitwardenShared/Core/Vault/Services/TestHelpers/BitwardenSdk+VaultMocking.swift @@ -89,6 +89,8 @@ extension CipherListViewType { self = .login(hasFido2: !(cipher.login?.fido2Credentials?.isEmpty ?? true), totp: cipher.login?.totp) case .secureNote: self = .secureNote + case .sshKey: + self = .sshKey } } } @@ -108,6 +110,7 @@ extension Cipher { identity: cipherView.identity.map(Identity.init), card: cipherView.card.map(Card.init), secureNote: cipherView.secureNote.map(SecureNote.init), + sshKey: cipherView.sshKey.map(SshKey.init), favorite: cipherView.favorite, reprompt: cipherView.reprompt, organizationUseTotp: cipherView.organizationUseTotp, @@ -139,6 +142,7 @@ extension CipherView { identity: cipher.identity.map(IdentityView.init), card: cipher.card.map(CardView.init), secureNote: cipher.secureNote.map(SecureNoteView.init), + sshKey: cipher.sshKey.map(SshKeyView.init), favorite: cipher.favorite, reprompt: cipher.reprompt, organizationUseTotp: cipher.organizationUseTotp, @@ -481,4 +485,24 @@ extension Send { expirationDate: sendView.expirationDate ) } +} + +extension SshKey { + init(sshKeyView: SshKeyView) { + self.init( + privateKey: sshKeyView.privateKey, + publicKey: sshKeyView.publicKey, + fingerprint: sshKeyView.fingerprint + ) + } +} + +extension SshKeyView { + init(sshKey: SshKey) { + self.init( + privateKey: sshKey.privateKey, + publicKey: sshKey.publicKey, + fingerprint: sshKey.fingerprint + ) + } } // swiftlint:disable:this file_length diff --git a/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings b/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings index 7bd8397df..bbf132ffd 100644 --- a/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings +++ b/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings @@ -1055,3 +1055,6 @@ "SomethingWentWrong" = "Something went wrong"; "UnableToMoveTheSelectedItemPleaseTryAgain" = "Unable to move the selected item. Please try again."; "Done" = "Done"; +"CopyPublicKey" = "Copy public key"; +"CopyPrivateKey" = "Copy private key"; +"CopyFingerprint" = "Copy fingerprint"; diff --git a/BitwardenShared/UI/Platform/Application/Utilities/Alert/Alert/TestHelpers/Alert+TestHelpers.swift b/BitwardenShared/UI/Platform/Application/Utilities/Alert/Alert/TestHelpers/Alert+TestHelpers.swift index b29e4376e..64bc8942d 100644 --- a/BitwardenShared/UI/Platform/Application/Utilities/Alert/Alert/TestHelpers/Alert+TestHelpers.swift +++ b/BitwardenShared/UI/Platform/Application/Utilities/Alert/Alert/TestHelpers/Alert+TestHelpers.swift @@ -22,6 +22,24 @@ extension Alert { try await tapAction(title: Localizations.cancel) } + /// Simulates a user interaction with the alert action that is in the specified index and matches the title. + /// - Parameters: + /// - byIndex: The index to get the alert action. + /// - withTitle: The title of the alert action to trigger. + /// - alertTextFields: `AlertTextField` list to execute the action + /// - Throws: Throws an `AlertError` if the alert action cannot be found. + func tapAction( + byIndex: Int, + withTitle: String, + _ alertTextFields: [AlertTextField]? = nil + ) async throws { + let alertAction = alertActions[byIndex] + guard alertAction.title == withTitle else { + throw AlertError.alertActionNotFound(title: withTitle) + } + await alertAction.handler?(alertAction, alertTextFields ?? self.alertTextFields) + } + /// Simulates a user interaction with the alert action that matches the provided title. /// /// - Parameters: diff --git a/BitwardenShared/UI/Vault/Extensions/Alert+Vault.swift b/BitwardenShared/UI/Vault/Extensions/Alert+Vault.swift index 2fbc178ad..201375fd1 100644 --- a/BitwardenShared/UI/Vault/Extensions/Alert+Vault.swift +++ b/BitwardenShared/UI/Vault/Extensions/Alert+Vault.swift @@ -161,7 +161,7 @@ extension Alert { /// /// - Returns: An alert presenting the user with options to select an attachment type. @MainActor - static func moreOptions( // swiftlint:disable:this function_body_length function_parameter_count + static func moreOptions( // swiftlint:disable:this function_body_length function_parameter_count cyclomatic_complexity line_length canCopyTotp: Bool, cipherView: CipherView, hasMasterPassword: Bool, @@ -263,6 +263,41 @@ extension Alert { )) }) } + case .sshKey: + if let publicKey = cipherView.sshKey?.publicKey { + alertActions.append(AlertAction(title: Localizations.copyPublicKey, style: .default) { _, _ in + await action(.copy( + toast: Localizations.publicKey, + value: publicKey, + requiresMasterPasswordReprompt: false, + logEvent: nil, + cipherId: cipherView.id + )) + }) + } + if let privateKey = cipherView.sshKey?.privateKey, + cipherView.viewPassword { + alertActions.append(AlertAction(title: Localizations.copyPrivateKey, style: .default) { _, _ in + await action(.copy( + toast: Localizations.privateKey, + value: privateKey, + requiresMasterPasswordReprompt: cipherView.reprompt == .password && hasMasterPassword, + logEvent: nil, + cipherId: cipherView.id + )) + }) + } + if let fingerprint = cipherView.sshKey?.fingerprint { + alertActions.append(AlertAction(title: Localizations.copyFingerprint, style: .default) { _, _ in + await action(.copy( + toast: Localizations.fingerprint, + value: fingerprint, + requiresMasterPasswordReprompt: false, + logEvent: nil, + cipherId: cipherView.id + )) + }) + } } // Return the alert. diff --git a/BitwardenShared/UI/Vault/Extensions/AlertVaultTests.swift b/BitwardenShared/UI/Vault/Extensions/AlertVaultTests.swift index 908965c37..80bbb21ae 100644 --- a/BitwardenShared/UI/Vault/Extensions/AlertVaultTests.swift +++ b/BitwardenShared/UI/Vault/Extensions/AlertVaultTests.swift @@ -1,3 +1,4 @@ +import BitwardenSdk import XCTest @testable import BitwardenShared @@ -121,6 +122,88 @@ class AlertVaultTests: BitwardenTestCase { XCTAssertTrue(actionCalled) } + /// `static moreOptions(canCopyTotp:cipherView:hasMasterPassword:id:showEdit:action:)` returns + /// the appropirate options for `.sshKey` type + @MainActor + func test_moreOptions_sshKey() async throws { // swiftlint:disable:this function_body_length + var capturedAction: MoreOptionsAction? + let action: (MoreOptionsAction) -> Void = { action in + capturedAction = action + } + let cipher = CipherView.fixture( + edit: false, + id: "123", + name: "Test Cipher", + sshKey: .fixture(), + type: .sshKey, + viewPassword: true + ) + let alert = Alert.moreOptions( + canCopyTotp: false, + cipherView: cipher, + hasMasterPassword: false, + id: cipher.id!, + showEdit: true, + action: action + ) + XCTAssertEqual(alert.title, cipher.name) + XCTAssertEqual(alert.preferredStyle, .actionSheet) + XCTAssertEqual(alert.alertActions.count, 6) + + try await alert.tapAction(byIndex: 0, withTitle: Localizations.view) + XCTAssertEqual(capturedAction, .view(id: "123")) + capturedAction = nil + + try await alert.tapAction(byIndex: 1, withTitle: Localizations.edit) + XCTAssertEqual( + capturedAction, + .edit(cipherView: cipher, requiresMasterPasswordReprompt: false) + ) + capturedAction = nil + + try await alert.tapAction(byIndex: 2, withTitle: Localizations.copyPublicKey) + XCTAssertEqual( + capturedAction, + .copy( + toast: Localizations.publicKey, + value: "publicKey", + requiresMasterPasswordReprompt: false, + logEvent: nil, + cipherId: "123" + ) + ) + capturedAction = nil + + try await alert.tapAction(byIndex: 3, withTitle: Localizations.copyPrivateKey) + XCTAssertEqual( + capturedAction, + .copy( + toast: Localizations.privateKey, + value: "privateKey", + requiresMasterPasswordReprompt: false, + logEvent: nil, + cipherId: "123" + ) + ) + capturedAction = nil + + try await alert.tapAction(byIndex: 4, withTitle: Localizations.copyFingerprint) + XCTAssertEqual( + capturedAction, + .copy( + toast: Localizations.fingerprint, + value: "fingerprint", + requiresMasterPasswordReprompt: false, + logEvent: nil, + cipherId: "123" + ) + ) + capturedAction = nil + + try await alert.tapAction(byIndex: 5, withTitle: Localizations.cancel) + XCTAssertNil(capturedAction) + } + /// `passwordAutofillInformation()` constructs an `Alert` that informs the user about password /// autofill. func test_passwordAutofillInformation() { diff --git a/BitwardenShared/UI/Vault/PreviewContent/BitwardenSdk+VaultFixtures.swift b/BitwardenShared/UI/Vault/PreviewContent/BitwardenSdk+VaultFixtures.swift index a2b0ecccd..6308111bf 100644 --- a/BitwardenShared/UI/Vault/PreviewContent/BitwardenSdk+VaultFixtures.swift +++ b/BitwardenShared/UI/Vault/PreviewContent/BitwardenSdk+VaultFixtures.swift @@ -49,6 +49,7 @@ extension Cipher { reprompt: BitwardenSdk.CipherRepromptType = .none, revisionDate: Date = Date(year: 2023, month: 11, day: 5, hour: 9, minute: 41), secureNote: SecureNote? = nil, + sshKey: SshKey? = nil, type: BitwardenSdk.CipherType = .login, viewPassword: Bool = true ) -> Cipher { @@ -65,6 +66,7 @@ extension Cipher { identity: identity, card: card, secureNote: secureNote, + sshKey: sshKey, favorite: favorite, reprompt: reprompt, organizationUseTotp: organizationUseTotp, @@ -105,6 +107,7 @@ extension CipherView { reprompt: BitwardenSdk.CipherRepromptType = .none, revisionDate: Date = Date(year: 2023, month: 11, day: 5, hour: 9, minute: 41), secureNote: SecureNoteView? = nil, + sshKey: SshKeyView? = nil, type: BitwardenSdk.CipherType = .login, viewPassword: Bool = true ) -> CipherView { @@ -121,6 +124,7 @@ extension CipherView { identity: identity, card: card, secureNote: secureNote, + sshKey: sshKey, favorite: favorite, reprompt: reprompt, organizationUseTotp: organizationUseTotp, @@ -171,6 +175,7 @@ extension CipherView { identity: nil, card: card, secureNote: nil, + sshKey: nil, favorite: favorite, reprompt: reprompt, organizationUseTotp: organizationUseTotp, @@ -221,6 +226,7 @@ extension CipherView { identity: nil, card: nil, secureNote: nil, + sshKey: nil, favorite: favorite, reprompt: reprompt, organizationUseTotp: organizationUseTotp, @@ -471,6 +477,26 @@ extension BitwardenSdk.LoginUriView { } } +extension BitwardenSdk.SshKey { + static func fixture( + privateKey: String? = "privateKey", + publicKey: String? = "publicKey", + fingerprint: String? = "fingerprint" + ) -> SshKey { + SshKey(privateKey: privateKey, publicKey: publicKey, fingerprint: fingerprint) + } +} + +extension BitwardenSdk.SshKeyView { + static func fixture( + privateKey: String? = "privateKey", + publicKey: String? = "publicKey", + fingerprint: String? = "fingerprint" + ) -> SshKeyView { + SshKeyView(privateKey: privateKey, publicKey: publicKey, fingerprint: fingerprint) + } +} + extension PasswordHistoryView { static func fixture( password: String = "", diff --git a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItem.swift b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItem.swift index 1ba11b4d0..0bf80f7b5 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItem.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItem.swift @@ -93,6 +93,8 @@ extension VaultListItem { fido2CredentialAutofillView != nil ? Asset.Images.passkey24 : Asset.Images.globe24 case .secureNote: Asset.Images.file24 + case .sshKey: + Asset.Images.key16 } case let .group(group, _): switch group { @@ -132,6 +134,8 @@ extension VaultListItem { return "LoginCipherIcon" case .secureNote: return "SecureNoteCipherIcon" + case .sshKey: + return "SSHKeyCipherIcon" } default: return "" @@ -199,6 +203,8 @@ extension CipherView { return login?.username case .secureNote: return nil + case .sshKey: + return nil } } } diff --git a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItemTests.swift b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItemTests.swift index 33be5b1b5..22fa8c79b 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItemTests.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItemTests.swift @@ -142,6 +142,10 @@ class VaultListItemTests: BitwardenTestCase { // swiftlint:disable:this type_bod VaultListItem(cipherView: .fixture(type: .secureNote))?.icon.name, Asset.Images.file24.name ) + XCTAssertEqual( + VaultListItem(cipherView: .fixture(type: .sshKey))?.icon.name, + Asset.Images.key16.name + ) XCTAssertEqual( VaultListItem(id: "", itemType: .group(.card, 1)).icon.name, @@ -186,6 +190,47 @@ class VaultListItemTests: BitwardenTestCase { // swiftlint:disable:this type_bod ) } + /// `getter:iconAccessibilityId` gets the appropriate id for each icon. + func test_iconAccessibilityId() { + XCTAssertEqual( + VaultListItem(cipherView: .fixture(type: .card))?.iconAccessibilityId, + "CardCipherIcon" + ) + XCTAssertEqual( + VaultListItem(cipherView: .fixture(type: .identity))?.iconAccessibilityId, + "IdentityCipherIcon" + ) + XCTAssertEqual( + VaultListItem(cipherView: .fixture(type: .login))?.iconAccessibilityId, + "LoginCipherIcon" + ) + XCTAssertEqual( + VaultListItem( + cipherView: .fixture(type: .login), + fido2CredentialAutofillView: .fixture() + )?.iconAccessibilityId, + "LoginCipherIcon" + ) + XCTAssertEqual( + VaultListItem(cipherView: .fixture(type: .secureNote))?.iconAccessibilityId, + "SecureNoteCipherIcon" + ) + XCTAssertEqual( + VaultListItem(cipherView: .fixture(type: .sshKey))?.iconAccessibilityId, + "SSHKeyCipherIcon" + ) + + XCTAssertEqual( + VaultListItem(id: "", itemType: .group(.card, 1)).iconAccessibilityId, + "" + ) + + XCTAssertEqual( + VaultListItem.fixtureTOTP(totp: .fixture()).iconAccessibilityId, + "" + ) + } + /// `name` returns the expected value. func test_name() { XCTAssertEqual(subject.name, "") @@ -301,6 +346,7 @@ class VaultListItemTests: BitwardenTestCase { // swiftlint:disable:this type_bod ) XCTAssertNil(VaultListItem(cipherView: .fixture(type: .secureNote))?.subtitle) + XCTAssertNil(VaultListItem(cipherView: .fixture(type: .sshKey))?.subtitle) XCTAssertNil(VaultListItem(id: "1", itemType: .group(.card, 1)).subtitle) XCTAssertNil(VaultListItem.fixtureTOTP(totp: .fixture()).subtitle) diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddItemStateTests.swift b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddItemStateTests.swift index 8c7ba3926..ed2ddd87c 100644 --- a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddItemStateTests.swift +++ b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddItemStateTests.swift @@ -47,6 +47,7 @@ class AddItemStateTests: XCTestCase { - reprompt: CipherRepromptType.none - revisionDate: 2023-10-20T00:00:00Z - secureNote: Optional.none + - sshKey: Optional.none - type: CipherType.login - viewPassword: true @@ -107,6 +108,7 @@ class AddItemStateTests: XCTestCase { - reprompt: CipherRepromptType.password - revisionDate: 2023-09-01T00:00:00Z - secureNote: Optional.none + - sshKey: Optional.none - type: CipherType.login - viewPassword: true diff --git a/BitwardenShared/UI/Vault/VaultItem/CipherItemState.swift b/BitwardenShared/UI/Vault/VaultItem/CipherItemState.swift index 856a72143..8b9405ba7 100644 --- a/BitwardenShared/UI/Vault/VaultItem/CipherItemState.swift +++ b/BitwardenShared/UI/Vault/VaultItem/CipherItemState.swift @@ -357,6 +357,7 @@ extension CipherItemState { identity: type == .identity ? identityState.identityView : nil, card: type == .card ? cardItemState.cardView : nil, secureNote: type == .secureNote ? .init(type: .generic) : nil, + sshKey: nil, // SSH keys cannot be created in mobile yet. favorite: isFavoriteOn, reprompt: isMasterPasswordRePromptOn ? .password : .none, organizationUseTotp: false, diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewLoginItem/Extensions/Cipher+Update.swift b/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewLoginItem/Extensions/Cipher+Update.swift index 0d3d371cc..dd2625038 100644 --- a/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewLoginItem/Extensions/Cipher+Update.swift +++ b/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewLoginItem/Extensions/Cipher+Update.swift @@ -20,6 +20,7 @@ extension Cipher { identity: identity, card: card, secureNote: secureNote, + sshKey: sshKey, favorite: favorite, reprompt: reprompt, organizationUseTotp: organizationUseTotp, @@ -54,6 +55,7 @@ extension Cipher { identity: identity, card: card, secureNote: secureNote, + sshKey: sshKey, favorite: favorite, reprompt: reprompt, organizationUseTotp: organizationUseTotp, diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewLoginItem/Extensions/CipherView+Update.swift b/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewLoginItem/Extensions/CipherView+Update.swift index 1cb6f3f17..f398a2d29 100644 --- a/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewLoginItem/Extensions/CipherView+Update.swift +++ b/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewLoginItem/Extensions/CipherView+Update.swift @@ -187,6 +187,7 @@ extension CipherView { identity: (addEditState.type == .identity) ? addEditState.identityState.identityView : nil, card: (addEditState.type == .card) ? addEditState.cardItemState.cardView : nil, secureNote: (addEditState.type == .secureNote) ? secureNote : nil, + sshKey: (addEditState.type == .sshKey) ? sshKey : nil, favorite: addEditState.isFavoriteOn, reprompt: addEditState.isMasterPasswordRePromptOn ? .password : .none, organizationUseTotp: organizationUseTotp, @@ -335,6 +336,7 @@ extension CipherView { identity: identity, card: card, secureNote: secureNote, + sshKey: sshKey, favorite: favorite, reprompt: reprompt, organizationUseTotp: organizationUseTotp, diff --git a/project.yml b/project.yml index d871725df..0589bae30 100644 --- a/project.yml +++ b/project.yml @@ -24,7 +24,8 @@ include: packages: BitwardenSdk: url: https://github.com/bitwarden/sdk-swift - revision: b5853c98f31ff9230c5eff83888081ea34c76afe + revision: f66f5546dcfe70480e4783367b119cfbc5d0268f + branch: unstable Firebase: url: https://github.com/firebase/firebase-ios-sdk exactVersion: 10.24.0