mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-11 04:34:55 -06:00
770 lines
34 KiB
Swift
770 lines
34 KiB
Swift
import AuthenticationServices
|
|
import BitwardenSdk
|
|
import XCTest
|
|
|
|
@testable import BitwardenShared
|
|
|
|
class AutofillCredentialServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body_length
|
|
// MARK: Properties
|
|
|
|
var autofillCredentialServiceDelegate: MockAutofillCredentialServiceDelegate!
|
|
var cipherService: MockCipherService!
|
|
var clientService: MockClientService!
|
|
var errorReporter: MockErrorReporter!
|
|
var eventService: MockEventService!
|
|
var fido2UserInterfaceHelperDelegate: MockFido2UserInterfaceHelperDelegate!
|
|
var fido2CredentialStore: MockFido2CredentialStore!
|
|
var fido2UserInterfaceHelper: MockFido2UserInterfaceHelper!
|
|
var identityStore: MockCredentialIdentityStore!
|
|
var pasteboardService: MockPasteboardService!
|
|
var stateService: MockStateService!
|
|
var totpService: MockTOTPService!
|
|
var subject: DefaultAutofillCredentialService!
|
|
var vaultTimeoutService: MockVaultTimeoutService!
|
|
|
|
// MARK: Setup & Teardown
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
|
|
autofillCredentialServiceDelegate = MockAutofillCredentialServiceDelegate()
|
|
cipherService = MockCipherService()
|
|
clientService = MockClientService()
|
|
errorReporter = MockErrorReporter()
|
|
eventService = MockEventService()
|
|
fido2UserInterfaceHelperDelegate = MockFido2UserInterfaceHelperDelegate()
|
|
fido2CredentialStore = MockFido2CredentialStore()
|
|
fido2UserInterfaceHelper = MockFido2UserInterfaceHelper()
|
|
identityStore = MockCredentialIdentityStore()
|
|
pasteboardService = MockPasteboardService()
|
|
stateService = MockStateService()
|
|
totpService = MockTOTPService()
|
|
vaultTimeoutService = MockVaultTimeoutService()
|
|
|
|
subject = DefaultAutofillCredentialService(
|
|
cipherService: cipherService,
|
|
clientService: clientService,
|
|
errorReporter: errorReporter,
|
|
eventService: eventService,
|
|
fido2CredentialStore: fido2CredentialStore,
|
|
fido2UserInterfaceHelper: fido2UserInterfaceHelper,
|
|
identityStore: identityStore,
|
|
pasteboardService: pasteboardService,
|
|
stateService: stateService,
|
|
totpService: totpService,
|
|
vaultTimeoutService: vaultTimeoutService
|
|
)
|
|
}
|
|
|
|
override func tearDown() {
|
|
super.tearDown()
|
|
|
|
autofillCredentialServiceDelegate = nil
|
|
cipherService = nil
|
|
clientService = nil
|
|
errorReporter = nil
|
|
eventService = nil
|
|
fido2UserInterfaceHelperDelegate = nil
|
|
fido2CredentialStore = nil
|
|
fido2UserInterfaceHelper = nil
|
|
identityStore = nil
|
|
pasteboardService = nil
|
|
stateService = nil
|
|
totpService = nil
|
|
subject = nil
|
|
vaultTimeoutService = nil
|
|
}
|
|
|
|
// MARK: Tests
|
|
|
|
/// `provideCredential(for:)` returns the credential containing the username and password for
|
|
/// the specified ID.
|
|
func test_provideCredential() async throws {
|
|
cipherService.fetchCipherResult = .success(
|
|
.fixture(login: .fixture(password: "password123", username: "user@bitwarden.com"))
|
|
)
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked["1"] = false
|
|
|
|
let credential = try await subject.provideCredential(
|
|
for: "1",
|
|
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
|
|
repromptPasswordValidated: false
|
|
)
|
|
|
|
XCTAssertEqual(credential.password, "password123")
|
|
XCTAssertEqual(credential.user, "user@bitwarden.com")
|
|
XCTAssertNil(pasteboardService.copiedString)
|
|
}
|
|
|
|
/// `provideCredential(for:)` throws an error if the cipher with the specified ID doesn't have a
|
|
/// username or password.
|
|
func test_provideCredential_cipherMissingUsernameOrPassword() async {
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked["1"] = false
|
|
|
|
cipherService.fetchCipherResult = .success(.fixture(type: .identity))
|
|
await assertAsyncThrows(error: ASExtensionError(.credentialIdentityNotFound)) {
|
|
_ = try await subject.provideCredential(
|
|
for: "1",
|
|
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
|
|
repromptPasswordValidated: false
|
|
)
|
|
}
|
|
|
|
cipherService.fetchCipherResult = .success(.fixture(login: .fixture(password: nil, username: "user@bitwarden")))
|
|
await assertAsyncThrows(error: ASExtensionError(.credentialIdentityNotFound)) {
|
|
_ = try await subject.provideCredential(
|
|
for: "1",
|
|
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
|
|
repromptPasswordValidated: false
|
|
)
|
|
}
|
|
|
|
cipherService.fetchCipherResult = .success(.fixture(login: .fixture(password: "test", username: nil)))
|
|
await assertAsyncThrows(error: ASExtensionError(.credentialIdentityNotFound)) {
|
|
_ = try await subject.provideCredential(
|
|
for: "1",
|
|
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
|
|
repromptPasswordValidated: false
|
|
)
|
|
}
|
|
}
|
|
|
|
/// `provideCredential(for:)` throws an error if a cipher with the specified ID doesn't exist.
|
|
func test_provideCredential_cipherNotFound() async {
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked["1"] = false
|
|
|
|
await assertAsyncThrows(error: ASExtensionError(.credentialIdentityNotFound)) {
|
|
_ = try await subject.provideCredential(
|
|
for: "1",
|
|
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
|
|
repromptPasswordValidated: false
|
|
)
|
|
}
|
|
}
|
|
|
|
/// `provideCredential(for:)` unlocks the user's vault if they use never lock.
|
|
func test_provideCredential_neverLock() async throws {
|
|
autofillCredentialServiceDelegate.unlockVaultWithNaverlockHandler = { [weak self] in
|
|
self?.vaultTimeoutService.isClientLocked["1"] = false
|
|
}
|
|
cipherService.fetchCipherResult = .success(
|
|
.fixture(login: .fixture(password: "password123", username: "user@bitwarden.com"))
|
|
)
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked["1"] = true
|
|
vaultTimeoutService.vaultTimeout["1"] = .never
|
|
|
|
let credential = try await subject.provideCredential(
|
|
for: "1",
|
|
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
|
|
repromptPasswordValidated: false
|
|
)
|
|
|
|
XCTAssertTrue(autofillCredentialServiceDelegate.unlockVaultWithNeverlockKeyCalled)
|
|
XCTAssertEqual(credential.password, "password123")
|
|
XCTAssertEqual(credential.user, "user@bitwarden.com")
|
|
XCTAssertNil(pasteboardService.copiedString)
|
|
}
|
|
|
|
/// `provideCredential(for:)` throws an error if reprompt is required.
|
|
func test_provideCredential_repromptRequired() async throws {
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked["1"] = false
|
|
|
|
cipherService.fetchCipherResult = .success(
|
|
.fixture(
|
|
login: .fixture(
|
|
password: "password123",
|
|
username: "user@bitwarden.com"
|
|
),
|
|
reprompt: .password
|
|
)
|
|
)
|
|
await assertAsyncThrows(error: ASExtensionError(.userInteractionRequired)) {
|
|
_ = try await subject.provideCredential(
|
|
for: "1",
|
|
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
|
|
repromptPasswordValidated: false
|
|
)
|
|
}
|
|
}
|
|
|
|
/// `provideCredential(for:)` copies the cipher's TOTP code when returning the credential.
|
|
func test_provideCredential_totpCopy() async throws {
|
|
cipherService.fetchCipherResult = .success(
|
|
.fixture(login: .fixture(
|
|
password: "password123",
|
|
username: "user@bitwarden.com",
|
|
totp: "totp"
|
|
))
|
|
)
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked["1"] = false
|
|
|
|
let credential = try await subject.provideCredential(
|
|
for: "1",
|
|
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
|
|
repromptPasswordValidated: false
|
|
)
|
|
|
|
XCTAssertEqual(credential.password, "password123")
|
|
XCTAssertEqual(credential.user, "user@bitwarden.com")
|
|
XCTAssertTrue(totpService.copyTotpIfPossibleCalled)
|
|
}
|
|
|
|
/// `provideCredential(for:)` attempting to copy the cipher's TOTP code when returning the credential
|
|
/// throws when gettning if active account has premium thus it gets logged by the reporter
|
|
/// but the credential is still returned.
|
|
func test_provideCredential_totpCopyThrows() async throws {
|
|
cipherService.fetchCipherResult = .success(
|
|
.fixture(login: .fixture(
|
|
password: "password123",
|
|
username: "user@bitwarden.com",
|
|
totp: "totp"
|
|
))
|
|
)
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked["1"] = false
|
|
totpService.copyTotpIfPossibleError = BitwardenTestError.example
|
|
|
|
let credential = try await subject.provideCredential(
|
|
for: "1",
|
|
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
|
|
repromptPasswordValidated: false
|
|
)
|
|
|
|
XCTAssertEqual(credential.password, "password123")
|
|
XCTAssertEqual(credential.user, "user@bitwarden.com")
|
|
XCTAssertTrue(totpService.copyTotpIfPossibleCalled)
|
|
XCTAssertEqual(errorReporter.errors as? [BitwardenTestError], [.example])
|
|
}
|
|
|
|
/// `provideCredential(for:)` throws an error if the user's vault is locked.
|
|
func test_provideCredential_vaultLocked() async {
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked["1"] = true
|
|
|
|
await assertAsyncThrows(error: ASExtensionError(.userInteractionRequired)) {
|
|
_ = try await subject.provideCredential(
|
|
for: "1",
|
|
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
|
|
repromptPasswordValidated: false
|
|
)
|
|
}
|
|
}
|
|
|
|
/// `provideFido2Credential(for:autofillCredentialServiceDelegate:fido2UserVerificationMediatorDelegate:)`
|
|
/// succeeds.
|
|
@available(iOS 17.0, *)
|
|
func test_provideFido2Credential_succeeds() async throws {
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked["1"] = false
|
|
let passkeyIdentity = ASPasskeyCredentialIdentity.fixture()
|
|
let passkeyRequest = ASPasskeyCredentialRequest.fixture(credentialIdentity: passkeyIdentity)
|
|
let expectedAssertionResult = GetAssertionResult.fixture()
|
|
|
|
clientService.mockPlatform.fido2Mock
|
|
.clientFido2AuthenticatorMock
|
|
.getAssertionMocker
|
|
.withVerification { request in
|
|
request.rpId == passkeyIdentity.relyingPartyIdentifier
|
|
&& request.clientDataHash == passkeyRequest.clientDataHash
|
|
&& request.allowList?[0].id == passkeyIdentity.credentialID
|
|
&& request.allowList?[0].ty == "public-key"
|
|
&& request.allowList?[0].transports == nil
|
|
&& !request.options.rk
|
|
&& request.options.uv == .discouraged
|
|
&& request.extensions == nil
|
|
}
|
|
.withResult(expectedAssertionResult)
|
|
|
|
let result = try await subject.provideFido2Credential(
|
|
for: passkeyRequest,
|
|
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
|
|
fido2UserInterfaceHelperDelegate: fido2UserInterfaceHelperDelegate
|
|
)
|
|
|
|
XCTAssertFalse(autofillCredentialServiceDelegate.unlockVaultWithNeverlockKeyCalled)
|
|
XCTAssertEqual(fido2UserInterfaceHelper.userVerificationPreferenceSetup, .discouraged)
|
|
|
|
XCTAssertTrue(totpService.copyTotpIfPossibleCalled)
|
|
XCTAssertTrue(errorReporter.errors.isEmpty)
|
|
|
|
XCTAssertEqual(result.userHandle, expectedAssertionResult.userHandle)
|
|
XCTAssertEqual(result.relyingParty, passkeyIdentity.relyingPartyIdentifier)
|
|
XCTAssertEqual(result.signature, expectedAssertionResult.signature)
|
|
XCTAssertEqual(result.clientDataHash, passkeyRequest.clientDataHash)
|
|
XCTAssertEqual(result.authenticatorData, expectedAssertionResult.authenticatorData)
|
|
XCTAssertEqual(result.credentialID, expectedAssertionResult.credentialId)
|
|
}
|
|
|
|
/// `provideFido2Credential(for:autofillCredentialServiceDelegate:fido2UserVerificationMediatorDelegate:)`
|
|
/// attempting to copy the cipher's TOTP code when returning the credential
|
|
/// throws when gettning if active account has premium thus it gets logged by the reporter
|
|
/// but the credential is still returned.
|
|
@available(iOS 17.0, *)
|
|
func test_provideFido2Credential_totpCopyThrows() async throws {
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked["1"] = false
|
|
let passkeyIdentity = ASPasskeyCredentialIdentity.fixture()
|
|
let passkeyRequest = ASPasskeyCredentialRequest.fixture(credentialIdentity: passkeyIdentity)
|
|
let expectedAssertionResult = GetAssertionResult.fixture(
|
|
selectedCredential: .fixture(
|
|
cipherView: .fixture(
|
|
login: .fixture(
|
|
totp: "totp"
|
|
)
|
|
)
|
|
)
|
|
)
|
|
totpService.copyTotpIfPossibleError = BitwardenTestError.example
|
|
|
|
clientService.mockPlatform.fido2Mock
|
|
.clientFido2AuthenticatorMock
|
|
.getAssertionMocker
|
|
.withVerification { request in
|
|
request.rpId == passkeyIdentity.relyingPartyIdentifier
|
|
&& request.clientDataHash == passkeyRequest.clientDataHash
|
|
&& request.allowList?[0].id == passkeyIdentity.credentialID
|
|
&& request.allowList?[0].ty == "public-key"
|
|
&& request.allowList?[0].transports == nil
|
|
&& !request.options.rk
|
|
&& request.options.uv == .discouraged
|
|
&& request.extensions == nil
|
|
}
|
|
.withResult(expectedAssertionResult)
|
|
|
|
let result = try await subject.provideFido2Credential(
|
|
for: passkeyRequest,
|
|
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
|
|
fido2UserInterfaceHelperDelegate: fido2UserInterfaceHelperDelegate
|
|
)
|
|
|
|
XCTAssertFalse(autofillCredentialServiceDelegate.unlockVaultWithNeverlockKeyCalled)
|
|
XCTAssertEqual(fido2UserInterfaceHelper.userVerificationPreferenceSetup, .discouraged)
|
|
|
|
XCTAssertEqual(result.userHandle, expectedAssertionResult.userHandle)
|
|
XCTAssertEqual(result.relyingParty, passkeyIdentity.relyingPartyIdentifier)
|
|
XCTAssertEqual(result.signature, expectedAssertionResult.signature)
|
|
XCTAssertEqual(result.clientDataHash, passkeyRequest.clientDataHash)
|
|
XCTAssertEqual(result.authenticatorData, expectedAssertionResult.authenticatorData)
|
|
XCTAssertEqual(result.credentialID, expectedAssertionResult.credentialId)
|
|
XCTAssertTrue(totpService.copyTotpIfPossibleCalled)
|
|
XCTAssertEqual(errorReporter.errors as? [BitwardenTestError], [.example])
|
|
}
|
|
|
|
/// `provideFido2Credential(for:autofillCredentialServiceDelegate:fido2UserVerificationMediatorDelegate:)`
|
|
/// succeeds when unlocking with never key.
|
|
@available(iOS 17.0, *)
|
|
func test_provideFido2Credential_succeedsWithUnlockingNeverKey() async throws {
|
|
autofillCredentialServiceDelegate.unlockVaultWithNaverlockHandler = { [weak self] in
|
|
self?.vaultTimeoutService.isClientLocked["1"] = false
|
|
}
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked["1"] = true
|
|
vaultTimeoutService.vaultTimeout["1"] = .never
|
|
|
|
let passkeyIdentity = ASPasskeyCredentialIdentity.fixture()
|
|
let passkeyRequest = ASPasskeyCredentialRequest.fixture(credentialIdentity: passkeyIdentity)
|
|
let expectedAssertionResult = GetAssertionResult.fixture()
|
|
|
|
clientService.mockPlatform.fido2Mock
|
|
.clientFido2AuthenticatorMock
|
|
.getAssertionMocker
|
|
.withVerification { request in
|
|
request.rpId == passkeyIdentity.relyingPartyIdentifier
|
|
&& request.clientDataHash == passkeyRequest.clientDataHash
|
|
&& request.allowList?[0].id == passkeyIdentity.credentialID
|
|
&& request.allowList?[0].ty == "public-key"
|
|
&& request.allowList?[0].transports == nil
|
|
&& !request.options.rk
|
|
&& request.options.uv == .discouraged
|
|
&& request.extensions == nil
|
|
}
|
|
.withResult(expectedAssertionResult)
|
|
|
|
let result = try await subject.provideFido2Credential(
|
|
for: passkeyRequest,
|
|
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
|
|
fido2UserInterfaceHelperDelegate: fido2UserInterfaceHelperDelegate
|
|
)
|
|
|
|
XCTAssertTrue(autofillCredentialServiceDelegate.unlockVaultWithNeverlockKeyCalled)
|
|
|
|
XCTAssertNotNil(fido2UserInterfaceHelper.fido2UserInterfaceHelperDelegate)
|
|
XCTAssertEqual(fido2UserInterfaceHelper.userVerificationPreferenceSetup, .discouraged)
|
|
|
|
XCTAssertEqual(result.userHandle, expectedAssertionResult.userHandle)
|
|
XCTAssertEqual(result.relyingParty, passkeyIdentity.relyingPartyIdentifier)
|
|
XCTAssertEqual(result.signature, expectedAssertionResult.signature)
|
|
XCTAssertEqual(result.clientDataHash, passkeyRequest.clientDataHash)
|
|
XCTAssertEqual(result.authenticatorData, expectedAssertionResult.authenticatorData)
|
|
XCTAssertEqual(result.credentialID, expectedAssertionResult.credentialId)
|
|
}
|
|
|
|
/// `provideFido2Credential(for:autofillCredentialServiceDelegate:fido2UserVerificationMediatorDelegate:)`
|
|
/// succeeds when unlocking with never key.
|
|
@available(iOS 17.0, *)
|
|
func test_provideFido2Credential_succeedsWithVaultUnlocked() async throws {
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked["1"] = false
|
|
|
|
let passkeyIdentity = ASPasskeyCredentialIdentity.fixture()
|
|
let passkeyRequest = ASPasskeyCredentialRequest.fixture(credentialIdentity: passkeyIdentity)
|
|
let expectedAssertionResult = GetAssertionResult.fixture()
|
|
|
|
clientService.mockPlatform.fido2Mock
|
|
.clientFido2AuthenticatorMock
|
|
.getAssertionMocker
|
|
.withVerification { request in
|
|
request.rpId == passkeyIdentity.relyingPartyIdentifier
|
|
&& request.clientDataHash == passkeyRequest.clientDataHash
|
|
&& request.allowList?[0].id == passkeyIdentity.credentialID
|
|
&& request.allowList?[0].ty == "public-key"
|
|
&& request.allowList?[0].transports == nil
|
|
&& !request.options.rk
|
|
&& request.options.uv == .discouraged
|
|
&& request.extensions == nil
|
|
}
|
|
.withResult(expectedAssertionResult)
|
|
|
|
let result = try await subject.provideFido2Credential(
|
|
for: passkeyRequest,
|
|
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
|
|
fido2UserInterfaceHelperDelegate: fido2UserInterfaceHelperDelegate
|
|
)
|
|
|
|
XCTAssertFalse(autofillCredentialServiceDelegate.unlockVaultWithNeverlockKeyCalled)
|
|
|
|
XCTAssertNotNil(fido2UserInterfaceHelper.fido2UserInterfaceHelperDelegate)
|
|
XCTAssertEqual(fido2UserInterfaceHelper.userVerificationPreferenceSetup, .discouraged)
|
|
|
|
XCTAssertEqual(result.userHandle, expectedAssertionResult.userHandle)
|
|
XCTAssertEqual(result.relyingParty, passkeyIdentity.relyingPartyIdentifier)
|
|
XCTAssertEqual(result.signature, expectedAssertionResult.signature)
|
|
XCTAssertEqual(result.clientDataHash, passkeyRequest.clientDataHash)
|
|
XCTAssertEqual(result.authenticatorData, expectedAssertionResult.authenticatorData)
|
|
XCTAssertEqual(result.credentialID, expectedAssertionResult.credentialId)
|
|
}
|
|
|
|
/// `provideFido2Credential(for:autofillCredentialServiceDelegate:fido2UserVerificationMediatorDelegate:)`
|
|
/// throws when no active user.
|
|
@available(iOS 17.0, *)
|
|
func test_provideFido2Credential_throwsNoActiveUser() async throws {
|
|
stateService.activeAccount = nil
|
|
|
|
let passkeyIdentity = ASPasskeyCredentialIdentity.fixture()
|
|
let passkeyRequest = ASPasskeyCredentialRequest.fixture(credentialIdentity: passkeyIdentity)
|
|
|
|
clientService.mockPlatform.fido2Mock
|
|
.clientFido2AuthenticatorMock
|
|
.getAssertionMocker
|
|
.throwing(BitwardenTestError.example)
|
|
|
|
await assertAsyncThrows(error: StateServiceError.noActiveAccount) {
|
|
_ = try await subject.provideFido2Credential(
|
|
for: passkeyRequest,
|
|
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
|
|
fido2UserInterfaceHelperDelegate: fido2UserInterfaceHelperDelegate
|
|
)
|
|
}
|
|
}
|
|
|
|
/// `provideFido2Credential(for:autofillCredentialServiceDelegate:fido2UserVerificationMediatorDelegate:)`
|
|
/// throws when needing user interaction.
|
|
@available(iOS 17.0, *)
|
|
func test_provideFido2Credential_throwsNeedingUserInteraction() async throws {
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked["1"] = true
|
|
|
|
let passkeyIdentity = ASPasskeyCredentialIdentity.fixture()
|
|
let passkeyRequest = ASPasskeyCredentialRequest.fixture(credentialIdentity: passkeyIdentity)
|
|
|
|
clientService.mockPlatform.fido2Mock
|
|
.clientFido2AuthenticatorMock
|
|
.getAssertionMocker
|
|
.throwing(BitwardenTestError.example)
|
|
|
|
await assertAsyncThrows(error: Fido2Error.userInteractionRequired) {
|
|
_ = try await subject.provideFido2Credential(
|
|
for: passkeyRequest,
|
|
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
|
|
fido2UserInterfaceHelperDelegate: fido2UserInterfaceHelperDelegate
|
|
)
|
|
}
|
|
}
|
|
|
|
/// `provideFido2Credential(for:autofillCredentialServiceDelegate:fido2UserVerificationMediatorDelegate:)`
|
|
/// throws when getting assertion with vault unlocked.
|
|
@available(iOS 17.0, *)
|
|
func test_provideFido2Credential_throwsGettingAssertion() async throws {
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked["1"] = false
|
|
|
|
let passkeyIdentity = ASPasskeyCredentialIdentity.fixture()
|
|
let passkeyRequest = ASPasskeyCredentialRequest.fixture(credentialIdentity: passkeyIdentity)
|
|
|
|
clientService.mockPlatform.fido2Mock
|
|
.clientFido2AuthenticatorMock
|
|
.getAssertionMocker
|
|
.throwing(BitwardenTestError.example)
|
|
|
|
await assertAsyncThrows(error: BitwardenTestError.example) {
|
|
_ = try await subject.provideFido2Credential(
|
|
for: passkeyRequest,
|
|
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
|
|
fido2UserInterfaceHelperDelegate: fido2UserInterfaceHelperDelegate
|
|
)
|
|
}
|
|
}
|
|
|
|
/// `provideFido2Credential(for:fido2UserVerificationMediatorDelegate:)`
|
|
/// succeeds.
|
|
@available(iOS 17.0, *)
|
|
func test_provideFido2Credential_passkeyRequestParameters_succeeds() async throws {
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked["1"] = false
|
|
let allowedCredentials = [
|
|
Data(repeating: 2, count: 32),
|
|
Data(repeating: 5, count: 32),
|
|
]
|
|
let passkeyParameters = MockPasskeyCredentialRequestParameters(allowedCredentials: allowedCredentials)
|
|
let expectedAssertionResult = GetAssertionResult.fixture()
|
|
|
|
clientService.mockPlatform.fido2Mock
|
|
.clientFido2AuthenticatorMock
|
|
.getAssertionMocker
|
|
.withVerification { request in
|
|
request.rpId == passkeyParameters.relyingPartyIdentifier
|
|
&& request.clientDataHash == passkeyParameters.clientDataHash
|
|
&& request.allowList == allowedCredentials.map { credentialId in
|
|
PublicKeyCredentialDescriptor(
|
|
ty: "public-key",
|
|
id: credentialId,
|
|
transports: nil
|
|
)
|
|
}
|
|
&& !request.options.rk
|
|
&& request.options.uv == .preferred
|
|
&& request.extensions == nil
|
|
}
|
|
.withResult(expectedAssertionResult)
|
|
|
|
let result = try await subject.provideFido2Credential(
|
|
for: passkeyParameters,
|
|
fido2UserInterfaceHelperDelegate: fido2UserInterfaceHelperDelegate
|
|
)
|
|
XCTAssertEqual(fido2UserInterfaceHelper.userVerificationPreferenceSetup, .preferred)
|
|
|
|
XCTAssertEqual(result.userHandle, expectedAssertionResult.userHandle)
|
|
XCTAssertEqual(result.relyingParty, passkeyParameters.relyingPartyIdentifier)
|
|
XCTAssertEqual(result.signature, expectedAssertionResult.signature)
|
|
XCTAssertEqual(result.clientDataHash, passkeyParameters.clientDataHash)
|
|
XCTAssertEqual(result.authenticatorData, expectedAssertionResult.authenticatorData)
|
|
XCTAssertEqual(result.credentialID, expectedAssertionResult.credentialId)
|
|
}
|
|
|
|
/// `provideFido2Credential(for:fido2UserVerificationMediatorDelegate:)`
|
|
/// throws when getting assertion.
|
|
@available(iOS 17.0, *)
|
|
func test_provideFido2Credential_passkeyRequestParameters_throwsGettingAssertion() async throws {
|
|
stateService.activeAccount = .fixture()
|
|
vaultTimeoutService.isClientLocked["1"] = false
|
|
|
|
let passkeyParameters = MockPasskeyCredentialRequestParameters()
|
|
|
|
clientService.mockPlatform.fido2Mock
|
|
.clientFido2AuthenticatorMock
|
|
.getAssertionMocker
|
|
.throwing(BitwardenTestError.example)
|
|
|
|
await assertAsyncThrows(error: BitwardenTestError.example) {
|
|
_ = try await subject.provideFido2Credential(
|
|
for: passkeyParameters,
|
|
fido2UserInterfaceHelperDelegate: fido2UserInterfaceHelperDelegate
|
|
)
|
|
}
|
|
}
|
|
|
|
/// `syncIdentities(vaultLockStatus:)` updates the credential identity store with the identities
|
|
/// from the user's vault.
|
|
func test_syncIdentities() {
|
|
cipherService.fetchAllCiphersResult = .success([
|
|
.fixture(
|
|
id: "1",
|
|
login: .fixture(
|
|
password: "password123",
|
|
uris: [.fixture(uri: "bitwarden.com")],
|
|
username: "user@bitwarden.com"
|
|
)
|
|
),
|
|
.fixture(id: "2", type: .identity),
|
|
.fixture(
|
|
id: "3",
|
|
login: .fixture(
|
|
password: "123321",
|
|
uris: [.fixture(uri: "example.com")],
|
|
username: "user@example.com"
|
|
)
|
|
),
|
|
.fixture(deletedDate: .now, id: "4", type: .login),
|
|
])
|
|
|
|
vaultTimeoutService.vaultLockStatusSubject.send(VaultLockStatus(isVaultLocked: false, userId: "1"))
|
|
waitFor(identityStore.replaceCredentialIdentitiesIdentities != nil)
|
|
|
|
XCTAssertEqual(
|
|
identityStore.replaceCredentialIdentitiesIdentities,
|
|
[
|
|
.password(PasswordCredentialIdentity(id: "1", uri: "bitwarden.com", username: "user@bitwarden.com")),
|
|
.password(PasswordCredentialIdentity(id: "3", uri: "example.com", username: "user@example.com")),
|
|
]
|
|
)
|
|
}
|
|
|
|
/// `syncIdentities(vaultLockStatus:)` doesn't remove identities if the store's state is disabled.
|
|
func test_syncIdentities_removeDisabled() async throws {
|
|
try await waitAndResetRemoveAllCredentialIdentitiesCalled()
|
|
identityStore.state.mockIsEnabled = false
|
|
|
|
vaultTimeoutService.vaultLockStatusSubject.send(nil)
|
|
try await waitForAsync {
|
|
self.identityStore.stateCalled
|
|
}
|
|
|
|
XCTAssertFalse(identityStore.removeAllCredentialIdentitiesCalled)
|
|
}
|
|
|
|
/// `syncIdentities(vaultLockStatus:)` logs an error if removing identities fails.
|
|
func test_syncIdentities_removeError() {
|
|
identityStore.removeAllCredentialIdentitiesResult = .failure(BitwardenTestError.example)
|
|
|
|
vaultTimeoutService.vaultLockStatusSubject.send(nil)
|
|
waitFor(identityStore.removeAllCredentialIdentitiesCalled)
|
|
|
|
XCTAssertEqual(errorReporter.errors as? [BitwardenTestError], [.example])
|
|
}
|
|
|
|
/// `syncIdentities(vaultLockStatus:)` removes identities from the store when the user switches from a previous
|
|
/// synced vault to another user.
|
|
func test_syncIdentities_removeOnSwitched() async throws {
|
|
try await waitAndResetRemoveAllCredentialIdentitiesCalled()
|
|
|
|
cipherService.fetchAllCiphersResult = .success([
|
|
.fixture(
|
|
id: "1",
|
|
login: .fixture(
|
|
password: "password123",
|
|
uris: [.fixture(uri: "bitwarden.com")],
|
|
username: "user@bitwarden.com"
|
|
)
|
|
),
|
|
])
|
|
|
|
vaultTimeoutService.vaultLockStatusSubject.send(VaultLockStatus(isVaultLocked: false, userId: "1"))
|
|
try await waitForAsync {
|
|
self.identityStore.replaceCredentialIdentitiesIdentities != nil
|
|
}
|
|
XCTAssertEqual(identityStore.replaceCredentialIdentitiesIdentities?.count, 1)
|
|
|
|
vaultTimeoutService.vaultLockStatusSubject.send(VaultLockStatus(isVaultLocked: true, userId: "2"))
|
|
try await waitForAsync {
|
|
self.identityStore.removeAllCredentialIdentitiesCalled
|
|
}
|
|
|
|
XCTAssertTrue(identityStore.removeAllCredentialIdentitiesCalled)
|
|
}
|
|
|
|
/// `syncIdentities(vaultLockStatus:)` doesn't remove identities from the store when the user locks their vault.
|
|
func test_syncIdentities_dontRemoveOnSwitchedEqualUser() async throws {
|
|
try await waitAndResetRemoveAllCredentialIdentitiesCalled()
|
|
|
|
cipherService.fetchAllCiphersResult = .success([
|
|
.fixture(
|
|
id: "1",
|
|
login: .fixture(
|
|
password: "password123",
|
|
uris: [.fixture(uri: "bitwarden.com")],
|
|
username: "user@bitwarden.com"
|
|
)
|
|
),
|
|
])
|
|
|
|
vaultTimeoutService.vaultLockStatusSubject.send(VaultLockStatus(isVaultLocked: false, userId: "1"))
|
|
try await waitForAsync {
|
|
self.identityStore.replaceCredentialIdentitiesIdentities != nil
|
|
}
|
|
XCTAssertEqual(identityStore.replaceCredentialIdentitiesIdentities?.count, 1)
|
|
|
|
vaultTimeoutService.vaultLockStatusSubject.send(VaultLockStatus(isVaultLocked: true, userId: "1"))
|
|
XCTAssertFalse(identityStore.removeAllCredentialIdentitiesCalled)
|
|
}
|
|
|
|
/// `syncIdentities(vaultLockStatus:)` doesn't remove identities from the store when it tries to sync
|
|
/// for the first time and it's locked (last user ID synced is `nil`).
|
|
func test_syncIdentities_dontRemoveOnFirstSyncLocked() async throws {
|
|
try await waitAndResetRemoveAllCredentialIdentitiesCalled()
|
|
|
|
vaultTimeoutService.vaultLockStatusSubject.send(VaultLockStatus(isVaultLocked: true, userId: "1"))
|
|
XCTAssertFalse(identityStore.removeAllCredentialIdentitiesCalled)
|
|
}
|
|
|
|
/// `syncIdentities(vaultLockStatus:)` removes identities from the store when the user logs out.
|
|
func test_syncIdentities_removeOnLogout() {
|
|
cipherService.fetchAllCiphersResult = .success([
|
|
.fixture(
|
|
id: "1",
|
|
login: .fixture(
|
|
password: "password123",
|
|
uris: [.fixture(uri: "bitwarden.com")],
|
|
username: "user@bitwarden.com"
|
|
)
|
|
),
|
|
])
|
|
|
|
vaultTimeoutService.vaultLockStatusSubject.send(VaultLockStatus(isVaultLocked: false, userId: "1"))
|
|
waitFor(identityStore.replaceCredentialIdentitiesIdentities != nil)
|
|
XCTAssertEqual(identityStore.replaceCredentialIdentitiesIdentities?.count, 1)
|
|
|
|
vaultTimeoutService.vaultLockStatusSubject.send(nil)
|
|
waitFor(identityStore.removeAllCredentialIdentitiesCalled)
|
|
XCTAssertTrue(identityStore.removeAllCredentialIdentitiesCalled)
|
|
}
|
|
|
|
/// `syncIdentities(vaultLockStatus:)` doesn't replace identities if the store's state is disabled.
|
|
func test_syncIdentities_replaceDisabled() {
|
|
identityStore.state.mockIsEnabled = false
|
|
|
|
vaultTimeoutService.vaultLockStatusSubject.send(VaultLockStatus(isVaultLocked: false, userId: "1"))
|
|
waitFor(identityStore.stateCalled)
|
|
|
|
XCTAssertFalse(identityStore.replaceCredentialIdentitiesCalled)
|
|
}
|
|
|
|
/// `syncIdentities(vaultLockStatus:)` logs an error if replacing identities fails.
|
|
func test_syncIdentities_replaceError() {
|
|
identityStore.replaceCredentialIdentitiesResult = .failure(BitwardenTestError.example)
|
|
|
|
vaultTimeoutService.vaultLockStatusSubject.send(VaultLockStatus(isVaultLocked: false, userId: "1"))
|
|
waitFor(identityStore.replaceCredentialIdentitiesCalled)
|
|
|
|
XCTAssertEqual(errorReporter.errors as? [BitwardenTestError], [.example])
|
|
}
|
|
|
|
// MARK: Private
|
|
|
|
/// Waits until `identityStore.removeAllCredentialIdentitiesCalled` is `true` and then resets it
|
|
/// to `false`. This happens because of the first value of `vaultTimeoutService.vaultLockStatusSubject`
|
|
/// which is `nil` and removes all credentials when the test is setup.
|
|
private func waitAndResetRemoveAllCredentialIdentitiesCalled() async throws {
|
|
try await waitForAsync {
|
|
self.identityStore.removeAllCredentialIdentitiesCalled
|
|
}
|
|
|
|
identityStore.removeAllCredentialIdentitiesCalled = false
|
|
}
|
|
} // swiftlint:disable:this file_length
|