[PM-24609] Removed deprecated function in CipherMatchingHelper (#2098)

This commit is contained in:
Federico Maccaroni 2025-11-04 18:19:50 -03:00 committed by GitHub
parent 9f71c1619d
commit 624c11c397
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 446 additions and 858 deletions

View File

@ -21,9 +21,9 @@ final class ItemListProcessor: StateProcessor<ItemListState, ItemListAction, Ite
& HasErrorReporter
& HasNotificationCenterService
& HasPasteboardService
& HasTimeProvider
& HasTOTPExpirationManagerFactory
& HasTOTPService
& HasTimeProvider
// MARK: Private Properties
@ -424,7 +424,7 @@ extension ItemListProcessor: AuthenticatorKeyCaptureDelegate {
captureCoordinator.navigate(
to: .dismiss(self?.parseKeyAndDismiss(key, sendToBitwarden: true)),
)
}
},
))
}
} else {

View File

@ -11,7 +11,7 @@ public enum UI {
// MARK: Utilities
/// App-wide flag that allows disabling UI animations for testing.
nonisolated(unsafe) public static var animated = true
public nonisolated(unsafe) static var animated = true
/// The language code at initialization.
public static var initialLanguageCode: String? {
@ -25,7 +25,7 @@ public enum UI {
#if DEBUG
/// App-wide flag that allows overriding the OS level sizeCategory for testing.
nonisolated(unsafe) public static var sizeCategory: UIContentSizeCategory?
public nonisolated(unsafe) static var sizeCategory: UIContentSizeCategory?
#endif
// MARK: Factories

View File

@ -1,6 +1,9 @@
import BitwardenKit
public extension Store {
/// Creates a Store with a mocked processor given a state.
/// - Parameter state: The state to initialize the mock processor with.
/// - Returns: A new `Store` with a `MockProcessor` having the passed `state`.
static func mock(state: State) -> Store<State, Action, Effect> {
Store(processor: MockProcessor(state: state))
}

View File

@ -51,7 +51,7 @@ public struct BitwardenBadge: View {
///
/// - Parameters:
/// - badgeValue: The value to display in the badge.
///
///
public init(badgeValue: String) {
self.badgeValue = badgeValue
}

View File

@ -2,6 +2,19 @@ import UIKit
#if DEBUG
public extension Alert {
/// Creates a test fixture for an `Alert` with customizable properties.
///
/// This fixture is intended for use in tests, previews, and debugging scenarios.
///
/// - Parameters:
/// - title: The title of the alert. Defaults to "🍎".
/// - message: The optional message text displayed in the alert. Defaults to "🥝".
/// - preferredStyle: The style of the alert controller. Defaults to `.alert`.
/// - alertActions: The actions to display in the alert. Defaults to a single OK action.
/// - alertTextFields: The text fields to display in the alert. Defaults to a single fixture text field.
///
/// - Returns: An `Alert` configured with the specified properties.
///
static func fixture(
title: String = "🍎",
message: String? = "🥝",
@ -20,6 +33,21 @@ public extension Alert {
}
public extension AlertAction {
/// Creates an OK-style alert action.
///
/// This factory method creates an alert action with a default "OK" title and default style,
/// suitable for positive confirmation actions.
///
/// - Parameters:
/// - title: The title of the action. Defaults to "OK".
/// - style: The style of the action button. Defaults to `.default`.
/// - handler: An optional async closure called when the action is triggered.
/// Receives the action and current text fields as parameters.
/// - shouldEnableAction: An optional closure that determines whether the action should be enabled
/// based on the current state of text fields.
///
/// - Returns: An `AlertAction` configured as an OK action.
///
static func ok(
title: String = "OK",
style: UIAlertAction.Style = .default,
@ -34,6 +62,21 @@ public extension AlertAction {
)
}
/// Creates a Cancel-style alert action.
///
/// This factory method creates an alert action with a default "Cancel" title and cancel style,
/// suitable for dismissive or cancellation actions.
///
/// - Parameters:
/// - title: The title of the action. Defaults to "Cancel".
/// - style: The style of the action button. Defaults to `.cancel`.
/// - handler: An optional async closure called when the action is triggered.
/// Receives the action and current text fields as parameters.
/// - shouldEnableAction: An optional closure that determines whether the action should be enabled
/// based on the current state of text fields.
///
/// - Returns: An `AlertAction` configured as a Cancel action.
///
static func cancel(
title: String = "Cancel",
style: UIAlertAction.Style = .cancel,
@ -50,6 +93,22 @@ public extension AlertAction {
}
public extension AlertTextField {
/// Creates a test fixture for an `AlertTextField` with customizable properties.
///
/// This fixture is intended for use in tests, previews, and debugging scenarios.
/// By default, it creates a secure text field with numeric keyboard input.
///
/// - Parameters:
/// - id: The unique identifier for the text field. Defaults to "field".
/// - autocapitalizationType: The auto-capitalization style for the text field. Defaults to `.allCharacters`.
/// - autocorrectionType: The autocorrection behavior for the text field. Defaults to `.yes`.
/// - isSecureTextEntry: Whether the text field obscures entered text for password entry. Defaults to `true`.
/// - keyboardType: The keyboard type to display. Defaults to `.numberPad`.
/// - placeholder: The placeholder text displayed when the field is empty. Defaults to "placeholder".
/// - text: The initial text value of the field. Defaults to "value".
///
/// - Returns: An `AlertTextField` configured with the specified properties.
///
static func fixture(
id: String = "field",
autocapitalizationType: UITextAutocapitalizationType = .allCharacters,

View File

@ -1,426 +0,0 @@
// swiftlint:disable:this file_name
import BitwardenSdk
import XCTest
@testable import BitwardenShared
class CipherMatchingHelperSingularMatchTests: BitwardenTestCase { // swiftlint:disable:this type_body_length
// MARK: Properties
var settingsService: MockSettingsService!
var stateService: MockStateService!
var subject: DefaultCipherMatchingHelper!
// MARK: Setup & Teardown
override func setUp() {
super.setUp()
settingsService = MockSettingsService()
stateService = MockStateService()
stateService.activeAccount = .fixture()
stateService.defaultUriMatchTypeByUserId["1"] = .domain
subject = DefaultCipherMatchingHelper(
settingsService: settingsService,
stateService: stateService,
)
}
override func tearDown() {
super.tearDown()
settingsService = nil
stateService = nil
subject = nil
}
// MARK: Tests
/// `doesCipherMatch(cipher:)` returns `.none` when there's no URI to match.
func test_doesCipherMatch_noURIToMatch() async {
subject.uriToMatch = nil
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture()),
),
)
XCTAssertEqual(result, .none)
}
/// `doesCipherMatch(cipher:)` returns `.none` when cipher is not a login.
func test_doesCipherMatch_noLogin() async {
subject.uriToMatch = "example.com"
let noLoginTypes: [CipherListViewType] = [.card(.fixture()), .identity, .secureNote, .sshKey]
for type in noLoginTypes {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: type,
),
)
XCTAssertEqual(result, .none)
}
}
/// `doesCipherMatch(cipher:)` returns `.none` when cipher is a login but doesn't have URIs.
func test_doesCipherMatch_loginNoUris() async {
subject.uriToMatch = "example.com"
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture()),
),
)
XCTAssertEqual(result, .none)
}
/// `doesCipherMatch(cipher:)` returns `.none` when cipher is a login, has URIs but is deleted.
func test_doesCipherMatch_loginDeleted() async {
subject.uriToMatch = "example.com"
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [.fixture()])),
deletedDate: .now,
),
)
XCTAssertEqual(result, .none)
}
/// `doesCipherMatch(cipher:)` returns `.exact` when match type is `.domain` and the match URI base domain
/// is the same as of the logins URI's base domains.
func test_doesCipherMatch_domainExact() async {
subject.uriToMatch = "https://google.com"
subject.matchingDomains = ["google.com"]
let loginUrisToSucceed = [
"http://google.com",
"https://accounts.google.com",
"google.com",
]
for uri in loginUrisToSucceed {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [.fixture(uri: uri, match: .domain)])),
),
)
XCTAssertEqual(result, .exact)
}
}
/// `doesCipherMatch(cipher:)` returns `.none` when match type is `.domain` and the match URI base domain
/// is not the same as none of the logins URI's base domains.
func test_doesCipherMatch_domainNone() async {
subject.uriToMatch = "https://google.com"
subject.matchingDomains = ["google.com"]
let loginUrisToFail = [
"https://google.net",
"http://yahoo.com",
"iosapp://yahoo.com",
]
for uri in loginUrisToFail {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [.fixture(uri: uri, match: .domain)])),
),
)
XCTAssertEqual(result, .none)
}
}
/// `doesCipherMatch(cipher:)` returns `.exact` when match type is `.domain` and the match URI base domain
/// is the same as of the logins URI's base domains in iosapp:// scheme.
func test_doesCipherMatch_domainExactAppScheme() async {
subject.uriToMatch = "iosapp://example.com"
subject.matchingDomains = ["example.com"]
let loginUrisToSucceed = [
"https://example.com",
"iosapp://example.com",
]
for uri in loginUrisToSucceed {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [.fixture(uri: uri, match: .domain)])),
),
)
XCTAssertEqual(result, .exact, "On \(uri)")
}
}
/// `doesCipherMatch(cipher:)` returns `.fuzzy` when match type is `.domain` and the match URI base domain
/// is the same as of the logins URI's base fuzzy domains in iosapp:// scheme.
func test_doesCipherMatch_domainFuzzyAppScheme() async {
subject.uriToMatch = "iosapp://example.com"
subject.matchingDomains = []
subject.matchingFuzzyDomains = ["example.com"]
let loginUrisToSucceed = [
"https://example.com",
"iosapp://example.com",
]
for uri in loginUrisToSucceed {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [.fixture(uri: uri, match: .domain)])),
),
)
XCTAssertEqual(result, .fuzzy)
}
}
/// `doesCipherMatch(cipher:)` returns `.exact` when match type is `.host`
/// and the match URI host is the same as the logins URI's host.
func test_doesCipherMatch_hostExact() async {
subject.uriToMatch = "https://sub.domain.com:4000"
subject.matchingDomains = ["example.com"]
let loginUrisToSucceed = [
"http://sub.domain.com:4000",
"https://sub.domain.com:4000/page.html",
]
for uri in loginUrisToSucceed {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [.fixture(uri: uri, match: .host)])),
),
)
XCTAssertEqual(result, .exact)
}
}
/// `doesCipherMatch(cipher:)` returns `.none` when match type is `.host`
/// and the match URI host is not the same as the logins URI's host.
func test_doesCipherMatch_hostNone() async {
subject.uriToMatch = "https://sub.domain.com:4000"
subject.matchingDomains = ["example.com"]
let loginUrisToFail = [
"https://domain.com",
"https://sub.domain.com",
"sub2.sub.domain.com:4000",
"https://sub.domain.com:5000",
"iosapp://domain.com",
]
for uri in loginUrisToFail {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [.fixture(uri: uri, match: .host)])),
),
)
XCTAssertEqual(result, .none)
}
}
/// `doesCipherMatch(cipher:)` returns `.exact` when match type is `.startsWith`
/// and the match URI starts with one of the login's URIs.
func test_doesCipherMatch_startsWithExact() async {
subject.uriToMatch = "https://vault.bitwarden.com"
subject.matchingDomains = ["example.com"]
let loginUrisToSucceed = [
"https://vault.bitwarden.com",
"https://vault.bit",
]
for uri in loginUrisToSucceed {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [
.fixture(uri: "noMatchExample.net", match: .startsWith),
.fixture(uri: uri, match: .startsWith),
])),
),
)
XCTAssertEqual(result, .exact)
}
}
/// `doesCipherMatch(cipher:)` returns `.none` when match type is `.startsWith` and the match URI
/// doesn't start with none of the login's URIs.
func test_doesCipherMatch_startsWithNone() async {
subject.uriToMatch = "https://vault.bitwarden.com"
subject.matchingDomains = ["example.com"]
let loginUrisToFail = [
"https://vault.bitwarden.net",
"https://vault.somethingelse.com",
"iosapp://example.com",
]
for uri in loginUrisToFail {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [
.fixture(uri: "noMatchExample.net", match: .startsWith),
.fixture(uri: uri, match: .startsWith),
])),
),
)
XCTAssertEqual(result, .none)
}
}
/// `doesCipherMatch(cipher:)` returns `.exact` when match type is `.exact`
/// and the match URI equals one of the login's URIs.
func test_doesCipherMatch_exact() async {
subject.uriToMatch = "https://vault.bitwarden.com"
subject.matchingDomains = ["example.com"]
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [
.fixture(uri: "noMatchExample.net", match: .exact),
.fixture(uri: "https://vault.bitwarden.com", match: .exact),
])),
),
)
XCTAssertEqual(result, .exact)
}
/// `doesCipherMatch(cipher:)` returns `.none` when match type is `.startsWith` and the match URI
/// is not equal to none of the login's URIs.
func test_doesCipherMatch_exactNone() async {
subject.uriToMatch = "https://vault.bitwarden.com"
subject.matchingDomains = ["example.com"]
let loginUrisToFail = [
"https://vault.bitwarden.net",
"https://vault.somethingelse.com",
"iosapp://example.com",
]
for uri in loginUrisToFail {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [
.fixture(uri: "noMatchExample.net", match: .exact),
.fixture(uri: uri, match: .exact),
])),
),
)
XCTAssertEqual(result, .none)
}
}
/// `doesCipherMatch(cipher:)` returns `.exact` when match type is `.regularExpression`
/// and the match URI matches the regular expression of one of the login's URIs.
func test_doesCipherMatch_regularExpressionExact() async {
let matchingUrisToSucceed = [
"https://en.wikipedia.org/w/index.php?title=Special:UserLogin&returnto=Bitwarden",
"https://pl.wikipedia.org/w/index.php?title=Specjalna:Zaloguj&returnto=Bitwarden",
"https://en.wikipedia.org/w/index.php",
]
for uri in matchingUrisToSucceed {
await subject.prepare(uri: uri)
subject.matchingDomains = ["example.com"]
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [
.fixture(uri: #"^https://[a-z]+\.wikipedia\.org/w/index\.php"#, match: .regularExpression),
])),
),
)
XCTAssertEqual(result, .exact)
}
}
/// `doesCipherMatch(cipher:)` returns `.none` when match type is `.regularExpression`
/// and the match URI doesn't match the regular expression of none of the login's URIs.
func test_doesCipherMatch_regularExpressionNone() async {
let matchingUrisToFail = [
"https://malicious-site.com",
"https://en.wikipedia.org/wiki/Bitwarden",
]
for uri in matchingUrisToFail {
await subject.prepare(uri: uri)
subject.matchingDomains = ["example.com"]
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [
.fixture(uri: #"^https://[a-z]+\.wikipedia\.org/w/index\.php"#, match: .regularExpression),
])),
),
)
XCTAssertEqual(result, .none)
}
}
/// `doesCipherMatch(cipher:)` returns `.none` when match type is `.never`.
func test_doesCipherMatch_never() async {
await subject.prepare(uri: "https://vault.bitwarden.com")
subject.matchingDomains = ["example.com"]
let matchUrisToTest = [
"https://vault.bitwarden.com",
"https://vault.bitwarden",
"https://vault.com",
]
for uri in matchUrisToTest {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [
.fixture(uri: uri, match: .never),
])),
),
)
XCTAssertEqual(result, .none)
}
}
/// `prepare(uri:)` sets the default match type in its state.
func test_prepare_setsDefaultMatchType() async {
stateService.defaultUriMatchTypeByUserId["1"] = .startsWith
await subject.prepare(uri: "https://example.com")
XCTAssertEqual(subject.defaultMatchType, .startsWith)
}
/// `prepare(uri:)` set matching/fuzzy domains as empty when the URI is empty.
func test_prepare_emptyUri() async {
await subject.prepare(uri: "")
XCTAssertTrue(subject.matchingDomains.isEmpty)
XCTAssertTrue(subject.matchingFuzzyDomains.isEmpty)
}
/// `prepare(uri:)` sets the passed URI domain as matching domains when there are no
/// equivalent domains.
func test_prepare_matchDomain() async {
await subject.prepare(uri: "https://example.com")
XCTAssertEqual(subject.matchingDomains.count, 1)
XCTAssertEqual(subject.matchingDomains.first, "example.com")
XCTAssertTrue(subject.matchingFuzzyDomains.isEmpty)
}
/// `prepare(uri:)` sets the domains matching the equivalent domains.
func test_prepare_matchDomainEquivalentDomains() async {
settingsService.fetchEquivalentDomainsResult = .success([
[
"google.com",
"youtube.com",
],
])
await subject.prepare(uri: "https://google.com")
XCTAssertEqual(subject.matchingDomains.sorted(), [
"google.com",
"youtube.com",
])
}
/// `prepare(uri:)` sets the domains matching the equivalent domains
/// when URI to match has iosapp:// scheme.
func test_prepare_matchDomainEquivalentDomainsiOSAppScheme() async {
settingsService.fetchEquivalentDomainsResult = .success([
[
"google.com",
"youtube.com",
],
])
await subject.prepare(uri: "iosapp://example.com")
XCTAssertEqual(Array(subject.matchingDomains), [
"iosapp://example.com",
])
XCTAssertEqual(Array(subject.matchingFuzzyDomains), [
"example.com",
])
}
} // swiftlint:disable:this file_length

View File

@ -21,16 +21,6 @@ enum CipherMatchResult {
/// A helper to handle filtering ciphers that match a URI.
///
protocol CipherMatchingHelper { // sourcery: AutoMockable
/// Returns the list of ciphers that match the URI.
///
/// - Parameters:
/// - uri: The URI used to filter the list of ciphers.
/// - ciphers: The list of ciphers to filter.
/// - Returns: The list of ciphers that match the URI.
///
@available(*, deprecated) // TODO: PM-24290 remove
func ciphersMatching(uri: String?, ciphers: [CipherListView]) async -> [CipherListView]
/// Returns the result of checking if a cipher matches the prepared URI.
///
/// - Parameters:
@ -84,34 +74,6 @@ class DefaultCipherMatchingHelper: CipherMatchingHelper {
// MARK: Methods
func ciphersMatching(uri: String?, ciphers: [CipherListView]) async -> [CipherListView] {
uriToMatch = uri
guard let uriToMatch, !uriToMatch.isEmpty else {
return []
}
(matchingDomains, matchingFuzzyDomains) = await getMatchingDomains(matchUri: uriToMatch)
defaultMatchType = await stateService.getDefaultUriMatchType()
let matchingCiphers = ciphers.reduce(
into: (exact: [CipherListView], fuzzy: [CipherListView])([], []),
) { result, cipher in
let match = doesCipherMatch(cipher: cipher)
switch match {
case .exact:
result.exact.append(cipher)
case .fuzzy:
result.fuzzy.append(cipher)
case .none:
// No-op: don't add non-matching ciphers.
break
}
}
return matchingCiphers.exact + matchingCiphers.fuzzy
}
func doesCipherMatch(cipher: CipherListView) -> CipherMatchResult {
guard let uriToMatch,
let login = cipher.type.loginListView,

View File

@ -7,33 +7,6 @@ import XCTest
class CipherMatchingHelperTests: BitwardenTestCase { // swiftlint:disable:this type_body_length
// MARK: Properties
let ciphers: [CipherListView] = [
.fixture(
login: .fixture(uris: [LoginUriView.fixture(uri: "https://vault.bitwarden.com", match: .exact)]),
name: "Bitwarden (Exact)",
),
.fixture(
login: .fixture(uris: [LoginUriView.fixture(uri: "https://vault.bitwarden.com", match: .startsWith)]),
name: "Bitwarden (Starts With)",
),
.fixture(
login: .fixture(uris: [LoginUriView.fixture(uri: "https://vault.bitwarden.com", match: .never)]),
name: "Bitwarden (Never)",
),
.fixture(
login: .fixture(uris: [LoginUriView.fixture(uri: "https://example.com", match: .startsWith)]),
name: "Example (Starts With)",
),
.fixture(
login: .fixture(),
name: "No URIs",
),
.fixture(
login: .fixture(uris: []),
name: "Empty URIs",
),
]
var settingsService: MockSettingsService!
var stateService: MockStateService!
var subject: DefaultCipherMatchingHelper!
@ -45,6 +18,8 @@ class CipherMatchingHelperTests: BitwardenTestCase { // swiftlint:disable:this t
settingsService = MockSettingsService()
stateService = MockStateService()
stateService.activeAccount = .fixture()
stateService.defaultUriMatchTypeByUserId["1"] = .domain
subject = DefaultCipherMatchingHelper(
settingsService: settingsService,
@ -62,373 +37,389 @@ class CipherMatchingHelperTests: BitwardenTestCase { // swiftlint:disable:this t
// MARK: Tests
/// `ciphersMatching(uri:ciphers)` returns the list of ciphers that match the URI for the base
/// domain match type.
func test_ciphersMatching_baseDomain() async {
let uris: [(String, String)] = [
("Google", "http://google.com"),
("Google Accounts", "https://accounts.google.com"),
("Google Domain", "google.com"),
("Google Net", "https://google.net"),
("Yahoo", "http://yahoo.com"),
]
let ciphers = uris.map { name, uri in
CipherListView.fixture(
login: .fixture(uris: [LoginUriView.fixture(uri: uri, match: .domain)]),
name: name,
)
}
let matchingCiphers = await subject.ciphersMatching(uri: "https://google.com", ciphers: ciphers)
assertInlineSnapshot(
of: dumpMatchingCiphers(matchingCiphers),
as: .lines,
) {
"""
Google
Google Accounts
Google Domain
"""
}
}
/// `ciphersMatching(uri:ciphers)` returns the list of ciphers that match the URI for the base
/// domain match type using the Bitwarden iOS app scheme.
func test_ciphersMatching_baseDomain_appScheme() async {
settingsService.fetchEquivalentDomainsResult = .success([["google.com", "youtube.com"]])
let ciphers = ciphersForUris(
[
("Example", "https://example.com"),
("Example App Scheme", "iosapp://example.com"),
("Other", "https://other.com"),
],
matchType: .domain,
/// `doesCipherMatch(cipher:)` returns `.none` when there's no URI to match.
func test_doesCipherMatch_noURIToMatch() async {
subject.uriToMatch = nil
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture()),
),
)
let matchingCiphers = await subject.ciphersMatching(uri: "iosapp://example.com", ciphers: ciphers)
assertInlineSnapshot(
of: dumpMatchingCiphers(matchingCiphers),
as: .lines,
) {
"""
Example App Scheme
Example
"""
}
XCTAssertEqual(result, .none)
}
/// `ciphersMatching(uri:ciphers)` returns the list of ciphers that match the URI for the base
/// domain match type using an equivalent domain.
func test_ciphersMatching_baseDomain_equivalentDomains() async {
settingsService.fetchEquivalentDomainsResult = .success([["google.com", "youtube.com"]])
let ciphers = ciphersForUris(
[
("Google", "https://google.com"),
("Google Account", "https://accounts.google.com"),
("Youtube", "https://youtube.com/login"),
("Yahoo", "https://yahoo.com"),
],
matchType: .domain,
)
let matchingCiphers = await subject.ciphersMatching(uri: "https://google.com", ciphers: ciphers)
assertInlineSnapshot(
of: dumpMatchingCiphers(matchingCiphers),
as: .lines,
) {
"""
Google
Google Account
Youtube
"""
}
}
/// `ciphersMatching(uri:ciphers)` returns the list of ciphers that match the URI for the base
/// domain match type using an equivalent domain when the uri to match doesn't have the protocol specified.
func test_ciphersMatching_baseDomain_equivalentDomainsNoHttpsInUri() async {
settingsService.fetchEquivalentDomainsResult = .success([["google.com", "youtube.com"]])
let ciphers = ciphersForUris(
[
("Google", "https://google.com"),
("Google Account", "https://accounts.google.com"),
("Youtube", "https://youtube.com/login"),
("Yahoo", "https://yahoo.com"),
],
matchType: .domain,
)
let matchingCiphers = await subject.ciphersMatching(uri: "google.com", ciphers: ciphers)
assertInlineSnapshot(
of: dumpMatchingCiphers(matchingCiphers),
as: .lines,
) {
"""
Google
Google Account
Youtube
"""
}
}
/// `ciphersMatching(uri:ciphers)` returns the list of ciphers that match the URI for the base
/// domain match type using an equivalent domain when the uri to match doesn't have the protocol specified
/// and the uris of the ciphers don't have protocols
func test_ciphersMatching_baseDomain_equivalentDomainsNoProtocols() async {
settingsService.fetchEquivalentDomainsResult = .success([["google.com", "youtube.com"]])
let ciphers = ciphersForUris(
[
("Google", "google.com"),
("Google Account", "accounts.google.com"),
("Youtube", "youtube.com/login"),
("Yahoo", "yahoo.com"),
],
matchType: .domain,
)
let matchingCiphers = await subject.ciphersMatching(uri: "google.com", ciphers: ciphers)
assertInlineSnapshot(
of: dumpMatchingCiphers(matchingCiphers),
as: .lines,
) {
"""
Google
Google Account
Youtube
"""
}
}
/// `ciphersMatching(uri:ciphers)` returns the list of ciphers that match the URI using the
/// default match type if the cipher doesn't specify a match type.
func test_ciphersMatching_defaultMatchType() async {
stateService.activeAccount = .fixture()
let ciphers = ciphersForUris(
[
("Google", "https://google.com"),
("Google Account", "https://accounts.google.com"),
("Youtube", "https://youtube.com/login"),
("Yahoo", "https://yahoo.com"),
],
matchType: nil,
)
stateService.defaultUriMatchTypeByUserId["1"] = .exact
var matchingCiphers = await subject.ciphersMatching(uri: "https://yahoo.com", ciphers: ciphers)
assertInlineSnapshot(
of: dumpMatchingCiphers(matchingCiphers),
as: .lines,
) {
"""
Yahoo
"""
}
stateService.defaultUriMatchTypeByUserId["1"] = .host
matchingCiphers = await subject.ciphersMatching(uri: "https://google.com", ciphers: ciphers)
assertInlineSnapshot(
of: dumpMatchingCiphers(matchingCiphers),
as: .lines,
) {
"""
Google
"""
}
}
/// `ciphersMatching(uri:ciphers)` returns the list of ciphers that match the URI for the exact
/// match type.
func test_ciphersMatching_exact() async {
let ciphers: [CipherListView] = [
.fixture(
login: .fixture(uris: [LoginUriView.fixture(uri: "https://vault.bitwarden.com", match: .exact)]),
name: "Bitwarden Vault",
),
.fixture(
login: .fixture(uris: [LoginUriView.fixture(uri: "https://bitwarden.com", match: .exact)]),
name: "Bitwarden",
),
.fixture(
login: .fixture(uris: [
LoginUriView.fixture(uri: "https://vault.bitwarden.com/login", match: .exact),
]),
name: "Bitwarden Login",
),
.fixture(
login: .fixture(uris: [
LoginUriView.fixture(uri: "https://bitwarden.com", match: .exact),
LoginUriView.fixture(uri: "https://vault.bitwarden.com", match: .exact),
]),
name: "Bitwarden Multiple",
),
]
var matchingCiphers = await subject.ciphersMatching(uri: "https://vault.bitwarden.com", ciphers: ciphers)
assertInlineSnapshot(
of: dumpMatchingCiphers(matchingCiphers),
as: .lines,
) {
"""
Bitwarden Vault
Bitwarden Multiple
"""
}
matchingCiphers = await subject.ciphersMatching(uri: "https://bitwarden.com", ciphers: ciphers)
assertInlineSnapshot(
of: dumpMatchingCiphers(matchingCiphers),
as: .lines,
) {
"""
Bitwarden
Bitwarden Multiple
"""
}
matchingCiphers = await subject.ciphersMatching(uri: "http://bitwarden.com", ciphers: ciphers)
XCTAssertTrue(matchingCiphers.isEmpty)
}
/// `ciphersMatching(uri:ciphers)` returns the list of ciphers that match the URI for the host
/// match type.
func test_ciphersMatching_host() async {
let uris: [(String, String)] = [
("Sub Domain 4000", "http://sub.domain.com:4000"),
("Sub Domain 4000 with Page", "https://sub.domain.com:4000/page.html"),
("Domain", "https://domain.com"),
("Sub Domain No Port", "https://sub.domain.com"),
("Sub Sub Domain", "https://sub2.sub.domain.com:4000"),
("Sub Domain 500", "https://sub.domain.com:5000"),
]
let ciphers = uris.map { name, uri in
CipherListView.fixture(
login: .fixture(uris: [LoginUriView.fixture(uri: uri, match: .host)]),
name: name,
)
}
let matchingCiphers = await subject.ciphersMatching(uri: "https://sub.domain.com:4000", ciphers: ciphers)
assertInlineSnapshot(
of: dumpMatchingCiphers(matchingCiphers),
as: .lines,
) {
"""
Sub Domain 4000
Sub Domain 4000 with Page
"""
}
}
/// `ciphersMatching(uri:ciphers)` returns the list of ciphers that match the URI for the never
/// match type.
func test_ciphersMatching_never() async {
let ciphers: [CipherListView] = [
.fixture(
login: .fixture(uris: [LoginUriView.fixture(uri: "https://vault.bitwarden.com", match: .never)]),
name: "Bitwarden Never",
),
.fixture(
login: .fixture(uris: [LoginUriView.fixture(uri: "https://vault.bitwarden.com", match: .exact)]),
name: "Bitwarden Exact",
),
]
var matchingCiphers = await subject.ciphersMatching(uri: "https://vault.bitwarden.com", ciphers: ciphers)
assertInlineSnapshot(
of: dumpMatchingCiphers(matchingCiphers),
as: .lines,
) {
"""
Bitwarden Exact
"""
}
matchingCiphers = await subject.ciphersMatching(uri: "http://bitwarden.com", ciphers: ciphers)
XCTAssertTrue(matchingCiphers.isEmpty)
}
/// `ciphersMatching(uri:ciphers)` returns the list of ciphers that match the URI for the
/// regular expression match type.
func test_ciphersMatching_regularExpression() async {
let cipher = CipherListView.fixture(
login: .fixture(uris: [
LoginUriView.fixture(
uri: #"^https://[a-z]+\.wikipedia\.org/w/index\.php"#,
match: .regularExpression,
/// `doesCipherMatch(cipher:)` returns `.none` when cipher is not a login.
func test_doesCipherMatch_noLogin() async {
subject.uriToMatch = "example.com"
let noLoginTypes: [CipherListViewType] = [.card(.fixture()), .identity, .secureNote, .sshKey]
for type in noLoginTypes {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: type,
),
]),
)
var matchingCiphers = await subject.ciphersMatching(
uri: "https://en.wikipedia.org/w/index.php?title=Special:UserLogin&returnto=Bitwarden",
ciphers: [cipher],
)
XCTAssertFalse(matchingCiphers.isEmpty)
matchingCiphers = await subject.ciphersMatching(
uri: "https://pl.wikipedia.org/w/index.php?title=Specjalna:Zaloguj&returnto=Bitwarden",
ciphers: [cipher],
)
XCTAssertFalse(matchingCiphers.isEmpty)
matchingCiphers = await subject.ciphersMatching(
uri: "https://en.wikipedia.org/w/index.php",
ciphers: [cipher],
)
XCTAssertFalse(matchingCiphers.isEmpty)
matchingCiphers = await subject.ciphersMatching(
uri: "https://malicious-site.com",
ciphers: [cipher],
)
XCTAssertTrue(matchingCiphers.isEmpty)
matchingCiphers = await subject.ciphersMatching(
uri: "https://en.wikipedia.org/wiki/Bitwarden",
ciphers: [cipher],
)
XCTAssertTrue(matchingCiphers.isEmpty)
}
/// `ciphersMatching(uri:ciphers)` returns the list of ciphers that match the URI for the starts
/// with match type.
func test_ciphersMatching_startsWith() async {
let matchingCiphers = await subject.ciphersMatching(uri: "https://vault.bitwarden.com", ciphers: ciphers)
assertInlineSnapshot(
of: dumpMatchingCiphers(matchingCiphers),
as: .lines,
) {
"""
Bitwarden (Exact)
Bitwarden (Starts With)
"""
}
}
/// `ciphersMatching(uri:ciphers)` returns empty when the uri is `nil` or empty.
func test_ciphersMatching_empty() async {
let matchingCiphersURINil = await subject.ciphersMatching(uri: nil, ciphers: ciphers)
XCTAssertTrue(matchingCiphersURINil.isEmpty)
let matchingCiphersURIEmpty = await subject.ciphersMatching(uri: "", ciphers: ciphers)
XCTAssertTrue(matchingCiphersURIEmpty.isEmpty)
}
// MARK: Private
/// Returns a list of `CipherListView`s created with the specified name, URI and match type.
func ciphersForUris(_ nameUris: [(String, String)], matchType: BitwardenSdk.UriMatchType?) -> [CipherListView] {
nameUris.map { name, uri in
CipherListView.fixture(
login: .fixture(uris: [LoginUriView.fixture(uri: uri, match: matchType)]),
name: name,
)
XCTAssertEqual(result, .none)
}
}
/// Returns a string containing a description of the matching ciphers.
func dumpMatchingCiphers(_ ciphers: [CipherListView]) -> String {
ciphers.map(\.name).joined(separator: "\n")
/// `doesCipherMatch(cipher:)` returns `.none` when cipher is a login but doesn't have URIs.
func test_doesCipherMatch_loginNoUris() async {
subject.uriToMatch = "example.com"
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture()),
),
)
XCTAssertEqual(result, .none)
}
/// `doesCipherMatch(cipher:)` returns `.none` when cipher is a login, has URIs but is deleted.
func test_doesCipherMatch_loginDeleted() async {
subject.uriToMatch = "example.com"
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [.fixture()])),
deletedDate: .now,
),
)
XCTAssertEqual(result, .none)
}
/// `doesCipherMatch(cipher:)` returns `.exact` when match type is `.domain` and the match URI base domain
/// is the same as of the logins URI's base domains.
func test_doesCipherMatch_domainExact() async {
subject.uriToMatch = "https://google.com"
subject.matchingDomains = ["google.com"]
let loginUrisToSucceed = [
"http://google.com",
"https://accounts.google.com",
"google.com",
]
for uri in loginUrisToSucceed {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [.fixture(uri: uri, match: .domain)])),
),
)
XCTAssertEqual(result, .exact)
}
}
/// `doesCipherMatch(cipher:)` returns `.none` when match type is `.domain` and the match URI base domain
/// is not the same as none of the logins URI's base domains.
func test_doesCipherMatch_domainNone() async {
subject.uriToMatch = "https://google.com"
subject.matchingDomains = ["google.com"]
let loginUrisToFail = [
"https://google.net",
"http://yahoo.com",
"iosapp://yahoo.com",
]
for uri in loginUrisToFail {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [.fixture(uri: uri, match: .domain)])),
),
)
XCTAssertEqual(result, .none)
}
}
/// `doesCipherMatch(cipher:)` returns `.exact` when match type is `.domain` and the match URI base domain
/// is the same as of the logins URI's base domains in iosapp:// scheme.
func test_doesCipherMatch_domainExactAppScheme() async {
subject.uriToMatch = "iosapp://example.com"
subject.matchingDomains = ["example.com"]
let loginUrisToSucceed = [
"https://example.com",
"iosapp://example.com",
]
for uri in loginUrisToSucceed {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [.fixture(uri: uri, match: .domain)])),
),
)
XCTAssertEqual(result, .exact, "On \(uri)")
}
}
/// `doesCipherMatch(cipher:)` returns `.fuzzy` when match type is `.domain` and the match URI base domain
/// is the same as of the logins URI's base fuzzy domains in iosapp:// scheme.
func test_doesCipherMatch_domainFuzzyAppScheme() async {
subject.uriToMatch = "iosapp://example.com"
subject.matchingDomains = []
subject.matchingFuzzyDomains = ["example.com"]
let loginUrisToSucceed = [
"https://example.com",
"iosapp://example.com",
]
for uri in loginUrisToSucceed {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [.fixture(uri: uri, match: .domain)])),
),
)
XCTAssertEqual(result, .fuzzy)
}
}
/// `doesCipherMatch(cipher:)` returns `.exact` when match type is `.host`
/// and the match URI host is the same as the logins URI's host.
func test_doesCipherMatch_hostExact() async {
subject.uriToMatch = "https://sub.domain.com:4000"
subject.matchingDomains = ["example.com"]
let loginUrisToSucceed = [
"http://sub.domain.com:4000",
"https://sub.domain.com:4000/page.html",
]
for uri in loginUrisToSucceed {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [.fixture(uri: uri, match: .host)])),
),
)
XCTAssertEqual(result, .exact)
}
}
/// `doesCipherMatch(cipher:)` returns `.none` when match type is `.host`
/// and the match URI host is not the same as the logins URI's host.
func test_doesCipherMatch_hostNone() async {
subject.uriToMatch = "https://sub.domain.com:4000"
subject.matchingDomains = ["example.com"]
let loginUrisToFail = [
"https://domain.com",
"https://sub.domain.com",
"sub2.sub.domain.com:4000",
"https://sub.domain.com:5000",
"iosapp://domain.com",
]
for uri in loginUrisToFail {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [.fixture(uri: uri, match: .host)])),
),
)
XCTAssertEqual(result, .none)
}
}
/// `doesCipherMatch(cipher:)` returns `.exact` when match type is `.startsWith`
/// and the match URI starts with one of the login's URIs.
func test_doesCipherMatch_startsWithExact() async {
subject.uriToMatch = "https://vault.bitwarden.com"
subject.matchingDomains = ["example.com"]
let loginUrisToSucceed = [
"https://vault.bitwarden.com",
"https://vault.bit",
]
for uri in loginUrisToSucceed {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [
.fixture(uri: "noMatchExample.net", match: .startsWith),
.fixture(uri: uri, match: .startsWith),
])),
),
)
XCTAssertEqual(result, .exact)
}
}
/// `doesCipherMatch(cipher:)` returns `.none` when match type is `.startsWith` and the match URI
/// doesn't start with none of the login's URIs.
func test_doesCipherMatch_startsWithNone() async {
subject.uriToMatch = "https://vault.bitwarden.com"
subject.matchingDomains = ["example.com"]
let loginUrisToFail = [
"https://vault.bitwarden.net",
"https://vault.somethingelse.com",
"iosapp://example.com",
]
for uri in loginUrisToFail {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [
.fixture(uri: "noMatchExample.net", match: .startsWith),
.fixture(uri: uri, match: .startsWith),
])),
),
)
XCTAssertEqual(result, .none)
}
}
/// `doesCipherMatch(cipher:)` returns `.exact` when match type is `.exact`
/// and the match URI equals one of the login's URIs.
func test_doesCipherMatch_exact() async {
subject.uriToMatch = "https://vault.bitwarden.com"
subject.matchingDomains = ["example.com"]
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [
.fixture(uri: "noMatchExample.net", match: .exact),
.fixture(uri: "https://vault.bitwarden.com", match: .exact),
])),
),
)
XCTAssertEqual(result, .exact)
}
/// `doesCipherMatch(cipher:)` returns `.none` when match type is `.startsWith` and the match URI
/// is not equal to none of the login's URIs.
func test_doesCipherMatch_exactNone() async {
subject.uriToMatch = "https://vault.bitwarden.com"
subject.matchingDomains = ["example.com"]
let loginUrisToFail = [
"https://vault.bitwarden.net",
"https://vault.somethingelse.com",
"iosapp://example.com",
]
for uri in loginUrisToFail {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [
.fixture(uri: "noMatchExample.net", match: .exact),
.fixture(uri: uri, match: .exact),
])),
),
)
XCTAssertEqual(result, .none)
}
}
/// `doesCipherMatch(cipher:)` returns `.exact` when match type is `.regularExpression`
/// and the match URI matches the regular expression of one of the login's URIs.
func test_doesCipherMatch_regularExpressionExact() async {
let matchingUrisToSucceed = [
"https://en.wikipedia.org/w/index.php?title=Special:UserLogin&returnto=Bitwarden",
"https://pl.wikipedia.org/w/index.php?title=Specjalna:Zaloguj&returnto=Bitwarden",
"https://en.wikipedia.org/w/index.php",
]
for uri in matchingUrisToSucceed {
await subject.prepare(uri: uri)
subject.matchingDomains = ["example.com"]
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [
.fixture(uri: #"^https://[a-z]+\.wikipedia\.org/w/index\.php"#, match: .regularExpression),
])),
),
)
XCTAssertEqual(result, .exact)
}
}
/// `doesCipherMatch(cipher:)` returns `.none` when match type is `.regularExpression`
/// and the match URI doesn't match the regular expression of none of the login's URIs.
func test_doesCipherMatch_regularExpressionNone() async {
let matchingUrisToFail = [
"https://malicious-site.com",
"https://en.wikipedia.org/wiki/Bitwarden",
]
for uri in matchingUrisToFail {
await subject.prepare(uri: uri)
subject.matchingDomains = ["example.com"]
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [
.fixture(uri: #"^https://[a-z]+\.wikipedia\.org/w/index\.php"#, match: .regularExpression),
])),
),
)
XCTAssertEqual(result, .none)
}
}
/// `doesCipherMatch(cipher:)` returns `.none` when match type is `.never`.
func test_doesCipherMatch_never() async {
await subject.prepare(uri: "https://vault.bitwarden.com")
subject.matchingDomains = ["example.com"]
let matchUrisToTest = [
"https://vault.bitwarden.com",
"https://vault.bitwarden",
"https://vault.com",
]
for uri in matchUrisToTest {
let result = subject.doesCipherMatch(
cipher: .fixture(
type: .login(.fixture(uris: [
.fixture(uri: uri, match: .never),
])),
),
)
XCTAssertEqual(result, .none)
}
}
/// `prepare(uri:)` sets the default match type in its state.
func test_prepare_setsDefaultMatchType() async {
stateService.defaultUriMatchTypeByUserId["1"] = .startsWith
await subject.prepare(uri: "https://example.com")
XCTAssertEqual(subject.defaultMatchType, .startsWith)
}
/// `prepare(uri:)` set matching/fuzzy domains as empty when the URI is empty.
func test_prepare_emptyUri() async {
await subject.prepare(uri: "")
XCTAssertTrue(subject.matchingDomains.isEmpty)
XCTAssertTrue(subject.matchingFuzzyDomains.isEmpty)
}
/// `prepare(uri:)` sets the passed URI domain as matching domains when there are no
/// equivalent domains.
func test_prepare_matchDomain() async {
await subject.prepare(uri: "https://example.com")
XCTAssertEqual(subject.matchingDomains.count, 1)
XCTAssertEqual(subject.matchingDomains.first, "example.com")
XCTAssertTrue(subject.matchingFuzzyDomains.isEmpty)
}
/// `prepare(uri:)` sets the domains matching the equivalent domains.
func test_prepare_matchDomainEquivalentDomains() async {
settingsService.fetchEquivalentDomainsResult = .success([
[
"google.com",
"youtube.com",
],
])
await subject.prepare(uri: "https://google.com")
XCTAssertEqual(subject.matchingDomains.sorted(), [
"google.com",
"youtube.com",
])
}
/// `prepare(uri:)` sets the domains matching the equivalent domains
/// when URI to match has iosapp:// scheme.
func test_prepare_matchDomainEquivalentDomainsiOSAppScheme() async {
settingsService.fetchEquivalentDomainsResult = .success([
[
"google.com",
"youtube.com",
],
])
await subject.prepare(uri: "iosapp://example.com")
XCTAssertEqual(Array(subject.matchingDomains), [
"iosapp://example.com",
])
XCTAssertEqual(Array(subject.matchingFuzzyDomains), [
"example.com",
])
}
} // swiftlint:disable:this file_length

View File

@ -1,4 +1,3 @@
// swiftlint:disable:this file_name
import BitwardenKit
import BitwardenKitMocks
import BitwardenResources

View File

@ -121,7 +121,7 @@ final class ExportVaultProcessor: StateProcessor<ExportVaultState, ExportVaultAc
/// - password: The password used to validate the export.
///
private func exportVault(format: ExportFormatType, password: String) async throws {
var exportFormat: ExportFileType = switch format {
let exportFormat: ExportFileType = switch format {
case .csv:
.csv
case .json: