chromium/chrome/browser/password_manager/android/junit/src/org/chromium/chrome/browser/password_manager/FakePasswordStoreAndroidBackendTest.java

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.password_manager;

import static androidx.test.espresso.matcher.ViewMatchers.assertThat;

import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.fail;

import android.accounts.Account;

import androidx.annotation.Nullable;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;
import org.robolectric.shadows.ShadowSystemClock;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.PayloadCallbackHelper;
import org.chromium.components.password_manager.core.browser.proto.ListPasswordsResult;
import org.chromium.components.password_manager.core.browser.proto.PasswordWithLocalData;
import org.chromium.components.signin.AccountUtils;
import org.chromium.components.sync.protocol.PasswordSpecificsData;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeoutException;

/** Tests for {@link FakePasswordStoreAndroidBackend}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
        manifest = Config.NONE,
        shadows = {ShadowSystemClock.class})
public class FakePasswordStoreAndroidBackendTest {
    private FakePasswordStoreAndroidBackend mBackend;

    private static final PasswordSpecificsData sPasswordData =
            PasswordSpecificsData.newBuilder()
                    .setUsernameValue("Todd Tester")
                    .setUsernameElement("username")
                    .setPasswordElement("pwd")
                    .setOrigin("https://accounts.google.com/signin")
                    .setSignonRealm("https://accounts.google.com")
                    .setPasswordValue("password")
                    .build();
    private static final PasswordSpecificsData sPasswordDataBlocklisted =
            PasswordSpecificsData.newBuilder()
                    .setUsernameElement("username")
                    .setPasswordElement("pwd")
                    .setOrigin("https://accounts.google.com/signin")
                    .setSignonRealm("https://accounts.google.com")
                    .setPasswordValue("password")
                    .setBlacklisted(true)
                    .build();
    private static final PasswordSpecificsData sPasswordDataNoOrigin =
            PasswordSpecificsData.newBuilder()
                    .setUsernameValue("Todd Tester")
                    .setUsernameElement("username")
                    .setPasswordElement("pwd")
                    .setSignonRealm("https://www.google.com")
                    .setPasswordValue("password")
                    .build();

    private static final PasswordWithLocalData sPwdWithLocalData =
            PasswordWithLocalData.newBuilder().setPasswordSpecificsData(sPasswordData).build();
    private static final PasswordWithLocalData sPwdWithLocalDataBlocklisted =
            PasswordWithLocalData.newBuilder()
                    .setPasswordSpecificsData(sPasswordDataBlocklisted)
                    .build();
    private static final PasswordWithLocalData sPwdWithLocalDataNoOrigin =
            PasswordWithLocalData.newBuilder()
                    .setPasswordSpecificsData(sPasswordDataNoOrigin)
                    .build();
    private static final String sTestAccountEmail = "[email protected]";
    private static final Optional<Account> sTestAccount =
            Optional.of(AccountUtils.createAccountFromName(sTestAccountEmail));

    @Before
    public void setUp() {
        mBackend = new FakePasswordStoreAndroidBackend();
        mBackend.setSyncingAccount(sTestAccount.get());
    }

    @Test
    public void testAddLogin() throws TimeoutException {
        CallbackHelper successCallback = new CallbackHelper();

        mBackend.addLogin(
                sPwdWithLocalData.toByteArray(),
                sTestAccount,
                successCallback::notifyCalled,
                unexpected -> fail());

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        Map<Account, List<PasswordWithLocalData>> allPasswords = mBackend.getAllSavedPasswords();
        assertThat(successCallback.getCallCount(), is(1));
        assertThat(allPasswords.get(sTestAccount.get()), hasSize(1));
        assertThat(allPasswords, hasEntry(is(sTestAccount.get()), hasItem(sPwdWithLocalData)));
    }

    @Test
    public void testGetAllLogins() throws TimeoutException {
        fillPasswordStore();

        PayloadCallbackHelper<byte[]> successCallback = new PayloadCallbackHelper<>();
        mBackend.getAllLogins(sTestAccount, successCallback::notifyCalled, unexpected -> fail());

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        ListPasswordsResult actualPasswords =
                parseListPasswordResultOrFail(successCallback.getOnlyPayloadBlocking());
        ListPasswordsResult expectedPasswords =
                ListPasswordsResult.newBuilder()
                        .addPasswordData(sPwdWithLocalData)
                        .addPasswordData(sPwdWithLocalDataBlocklisted)
                        .addPasswordData(sPwdWithLocalDataNoOrigin)
                        .build();
        assertThat(actualPasswords, is(expectedPasswords));
    }

    @Test
    public void testGetAutofillableLogins() throws TimeoutException {
        fillPasswordStore();

        PayloadCallbackHelper<byte[]> successCallback = new PayloadCallbackHelper<>();
        mBackend.getAutofillableLogins(
                sTestAccount, successCallback::notifyCalled, unexpected -> fail());

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        ListPasswordsResult actualPasswords =
                parseListPasswordResultOrFail(successCallback.getOnlyPayloadBlocking());
        ListPasswordsResult expectedPasswords =
                ListPasswordsResult.newBuilder()
                        .addPasswordData(sPwdWithLocalData)
                        .addPasswordData(sPwdWithLocalDataNoOrigin)
                        .build();
        assertThat(actualPasswords, is(expectedPasswords));
    }

    @Test
    public void testGetLoginsForSignonRealm() throws TimeoutException {
        fillPasswordStore();

        PayloadCallbackHelper<byte[]> successCallback = new PayloadCallbackHelper<>();
        mBackend.getLoginsForSignonRealm(
                sPwdWithLocalData.getPasswordSpecificsData().getSignonRealm(),
                sTestAccount,
                successCallback::notifyCalled,
                unexpected -> fail());

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        ListPasswordsResult actualPasswords =
                parseListPasswordResultOrFail(successCallback.getOnlyPayloadBlocking());
        ListPasswordsResult expectedPasswords =
                ListPasswordsResult.newBuilder()
                        .addPasswordData(sPwdWithLocalData)
                        .addPasswordData(sPwdWithLocalDataBlocklisted)
                        .build();
        assertThat(actualPasswords, is(expectedPasswords));
    }

    @Test
    public void testUpdateLoginReplacesExisting() throws TimeoutException {
        fillPasswordStore();

        CallbackHelper successCallback = new CallbackHelper();
        PasswordSpecificsData updatedPasswordData =
                PasswordSpecificsData.newBuilder()
                        .setUsernameValue("Todd Tester")
                        .setUsernameElement("username")
                        .setPasswordElement("pwd")
                        .setOrigin("https://accounts.google.com/signin")
                        .setSignonRealm("https://accounts.google.com")
                        .setPasswordValue("UpdatedPassword")
                        .build();
        PasswordWithLocalData updatedPwdWithLocalData =
                PasswordWithLocalData.newBuilder()
                        .setPasswordSpecificsData(updatedPasswordData)
                        .build();
        mBackend.updateLogin(
                updatedPwdWithLocalData.toByteArray(),
                sTestAccount,
                successCallback::notifyCalled,
                unexpected -> fail());

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        Map<Account, List<PasswordWithLocalData>> allPasswords = mBackend.getAllSavedPasswords();
        assertThat(successCallback.getCallCount(), is(1));
        assertThat(allPasswords.get(sTestAccount.get()), hasSize(3));
        assertThat(
                allPasswords, hasEntry(is(sTestAccount.get()), hasItem(updatedPwdWithLocalData)));
    }

    @Test
    public void testUpdateLoginAddsNew() throws TimeoutException {
        fillPasswordStore();

        CallbackHelper successCallback = new CallbackHelper();
        PasswordSpecificsData updatedPasswordData =
                PasswordSpecificsData.newBuilder()
                        .setUsernameValue("Elisa Tester")
                        .setUsernameElement("username")
                        .setPasswordElement("pwd1")
                        .setOrigin("https://accounts.google.com/signin")
                        .setSignonRealm("https://accounts.google.com")
                        .setPasswordValue("UpdatedPassword")
                        .build();
        PasswordWithLocalData updatedPwdWithLocalData =
                PasswordWithLocalData.newBuilder()
                        .setPasswordSpecificsData(updatedPasswordData)
                        .build();
        mBackend.updateLogin(
                updatedPwdWithLocalData.toByteArray(),
                sTestAccount,
                successCallback::notifyCalled,
                unexpected -> fail());

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        Map<Account, List<PasswordWithLocalData>> allPasswords = mBackend.getAllSavedPasswords();
        assertThat(successCallback.getCallCount(), is(1));
        assertThat(allPasswords.get(sTestAccount.get()), hasSize(4));
        assertThat(
                allPasswords, hasEntry(is(sTestAccount.get()), hasItem(updatedPwdWithLocalData)));
    }

    @Test
    public void testRemoveLogin() throws TimeoutException {
        fillPasswordStore();

        CallbackHelper successCallback = new CallbackHelper();
        PasswordSpecificsData removedLogin =
                PasswordSpecificsData.newBuilder()
                        .setUsernameValue(sPasswordData.getUsernameValue())
                        .setUsernameElement(sPasswordData.getUsernameElement())
                        .setPasswordElement(sPasswordData.getPasswordElement())
                        .setOrigin(sPasswordData.getOrigin())
                        .setSignonRealm(sPasswordData.getSignonRealm())
                        .build();
        mBackend.removeLogin(
                removedLogin.toByteArray(),
                sTestAccount,
                successCallback::notifyCalled,
                unexpected -> fail());

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        Map<Account, List<PasswordWithLocalData>> allPasswords = mBackend.getAllSavedPasswords();
        assertThat(successCallback.getCallCount(), is(1));
        assertThat(allPasswords.get(sTestAccount.get()), hasSize(2));
        assertThat(allPasswords, hasEntry(is(sTestAccount.get()), not(hasItem(sPwdWithLocalData))));
    }

    private void fillPasswordStore() {
        mBackend.addLogin(
                sPwdWithLocalData.toByteArray(), sTestAccount, () -> {}, unexpected -> fail());
        mBackend.addLogin(
                sPwdWithLocalDataBlocklisted.toByteArray(),
                sTestAccount,
                () -> {},
                unexpected -> fail());
        mBackend.addLogin(
                sPwdWithLocalDataNoOrigin.toByteArray(),
                sTestAccount,
                () -> {},
                unexpected -> fail());
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
    }

    private static @Nullable ListPasswordsResult parseListPasswordResultOrFail(
            byte[] listPwdResult) {
        try {
            return ListPasswordsResult.parseFrom(listPwdResult);
        } catch (Exception parsingError) {
            Assert.fail("Could not parse byte array into ListPasswordsResult.");
            return null;
        }
    }
}