chromium/chrome/test/data/webui/password_manager/checkup_section_test.ts

// 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.

import 'chrome://password-manager/password_manager.js';

import type {PluralStringProxy} from 'chrome://password-manager/password_manager.js';
import {CheckupSubpage, Page, PasswordCheckInteraction, PasswordManagerImpl, PluralStringProxyImpl, Router, UrlParam} from 'chrome://password-manager/password_manager.js';
import {assertArrayEquals, assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
import {TestBrowserProxy} from 'chrome://webui-test/test_browser_proxy.js';
import {isVisible} from 'chrome://webui-test/test_util.js';

import {TestPasswordManagerProxy} from './test_password_manager_proxy.js';
import {createCredentialGroup, makeInsecureCredential, makePasswordCheckStatus} from './test_util.js';

const PasswordCheckState = chrome.passwordsPrivate.PasswordCheckState;

// This is a special implementation of TestPluralStringProxy. It allows to await
// a call to |getPluralString| with a specific |messageName| parameter. The list
// of possible |messageNames| is passed to the constructor of TestBrowserProxy.
// This simplifies tests and allows to await for a |getPluralString| call with
// (checkedPasswords e.g.) and get the number being passed.
class CheckupTestPluralStringProxy extends TestBrowserProxy implements
    PluralStringProxy {
  constructor() {
    super([
      'checkedPasswords',
      'checkingPasswords',
      'compromisedPasswords',
      'compromisedPasswordsTitle',
      'reusedPasswords',
      'weakPasswords',
    ]);
  }

  getPluralString(messageName: string, itemCount: number) {
    this.methodCalled(messageName, itemCount);
    return Promise.resolve(messageName);
  }

  getPluralStringTupleWithComma(
      _messageName1: string, _itemCount1: number, _messageName2: string,
      _itemCount2: number) {
    return Promise.resolve('some text');
  }

  getPluralStringTupleWithPeriods(
      _messageName1: string, _itemCount1: number, _messageName2: string,
      _itemCount2: number) {
    return Promise.resolve('some text');
  }
}

suite('CheckupSectionTest', function() {
  const CompromiseType = chrome.passwordsPrivate.CompromiseType;

  let passwordManager: TestPasswordManagerProxy;
  let pluralString: CheckupTestPluralStringProxy;

  setup(function() {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    passwordManager = new TestPasswordManagerProxy();
    PasswordManagerImpl.setInstance(passwordManager);
    pluralString = new CheckupTestPluralStringProxy();
    PluralStringProxyImpl.setInstance(pluralString);
    Router.getInstance().navigateTo(Page.CHECKUP);
    return flushTasks();
  });

  test('IDLE state', async function() {
    const elapsedTime = 'Two days ago.';
    passwordManager.data.checkStatus = makePasswordCheckStatus(
        {state: PasswordCheckState.IDLE, lastCheck: elapsedTime});

    const section = document.createElement('checkup-section');
    document.body.appendChild(section);
    await flushTasks();

    assertTrue(isVisible(section.$.checkupResult));
    assertTrue(isVisible(section.$.refreshButton));
    assertFalse(section.$.refreshButton.disabled);
    assertTrue(isVisible(section.$.checkupStatusSubLabel));
    assertEquals(
        elapsedTime, section.$.checkupStatusSubLabel.textContent!.trim());
    assertFalse(isVisible(section.$.retryButton));
    assertFalse(isVisible(section.$.spinner));
  });

  test('Running state', async function() {
    passwordManager.data.checkStatus = makePasswordCheckStatus(
        {state: PasswordCheckState.RUNNING, totalNumber: 10, checked: 4});

    const section = document.createElement('checkup-section');
    document.body.appendChild(section);
    assertEquals(10, await pluralString.whenCalled('checkingPasswords'));
    await flushTasks();

    assertFalse(isVisible(section.$.checkupResult));
    assertTrue(isVisible(section.$.refreshButton));
    assertTrue(section.$.refreshButton.disabled);
    assertTrue(isVisible(section.$.checkupStatusSubLabel));
    assertEquals(
        section.i18n('checkupProgress', 4, 10),
        section.$.checkupStatusSubLabel.textContent!.trim());
    assertFalse(isVisible(section.$.retryButton));
    assertTrue(isVisible(section.$.spinner));
  });

  test('check NO_PASSWORDS state', async function() {
    passwordManager.data.checkStatus =
        makePasswordCheckStatus({state: PasswordCheckState.NO_PASSWORDS});

    const section = document.createElement('checkup-section');
    document.body.appendChild(section);
    await flushTasks();

    assertFalse(isVisible(section.$.checkupResult));
    assertFalse(isVisible(section.$.refreshButton));
    assertTrue(isVisible(section.$.checkupStatusLabel));
    assertTrue(isVisible(section.$.checkupStatusSubLabel));
    assertEquals(
        section.i18n(
            'checkupErrorNoPasswords', section.i18n('localPasswordManager')),
        section.$.checkupStatusSubLabel.textContent!.trim());
    assertFalse(isVisible(section.$.retryButton));
    assertFalse(isVisible(section.$.spinner));
  });

  [{state: PasswordCheckState.QUOTA_LIMIT, message: 'checkupErrorQuota'},
   {state: PasswordCheckState.OFFLINE, message: 'checkupErrorOffline'},
   {state: PasswordCheckState.SIGNED_OUT, message: 'checkupErrorSignedOut'},
   {state: PasswordCheckState.OTHER_ERROR, message: 'checkupErrorGeneric'}]
      .forEach(status => test(`Error state ${status.state}`, async function() {
                 passwordManager.data.checkStatus = makePasswordCheckStatus(
                     {state: status.state, lastCheck: 'One week ago'});

                 const section = document.createElement('checkup-section');
                 document.body.appendChild(section);
                 await flushTasks();

                 assertTrue(isVisible(section.$.checkupResult));
                 assertFalse(isVisible(section.$.refreshButton));
                 assertTrue(isVisible(section.$.checkupStatusSubLabel));
                 assertEquals(
                     passwordManager.data.checkStatus.elapsedTimeSinceLastCheck,
                     section.$.checkupStatusSubLabel.textContent!.trim());
                 assertTrue(isVisible(section.$.retryButton));
                 assertFalse(isVisible(section.$.spinner));
                 assertEquals(
                     section.i18n('compromisedRowWithError'),
                     section.$.compromisedRow.label);
                 assertEquals(
                     section.i18n(
                         status.message, section.i18n('localPasswordManager')),
                     section.$.compromisedRow.subLabel);
               }));

  test('Start check', async function() {
    passwordManager.data.checkStatus =
        makePasswordCheckStatus({state: PasswordCheckState.IDLE});

    const section = document.createElement('checkup-section');
    document.body.appendChild(section);
    await flushTasks();

    section.$.refreshButton.click();
    await passwordManager.whenCalled('startBulkPasswordCheck');
    const interaction =
        await passwordManager.whenCalled('recordPasswordCheckInteraction');
    assertEquals(PasswordCheckInteraction.START_CHECK_MANUALLY, interaction);
  });

  test('Number of issues reflected in sections', async function() {
    passwordManager.data.checkStatus =
        makePasswordCheckStatus({state: PasswordCheckState.IDLE});

    // 3 compromised, 0 reused, 4 weak credentials
    passwordManager.data.insecureCredentials = [
      makeInsecureCredential({
        types: [
          CompromiseType.PHISHED,
          CompromiseType.LEAKED,
          CompromiseType.WEAK,
        ],
      }),
      makeInsecureCredential({
        types: [
          CompromiseType.PHISHED,
          CompromiseType.WEAK,
        ],
      }),
      makeInsecureCredential({
        types: [
          CompromiseType.LEAKED,
          CompromiseType.WEAK,
        ],
      }),
      makeInsecureCredential({types: [CompromiseType.WEAK]}),
    ];

    const section = document.createElement('checkup-section');
    document.body.appendChild(section);
    await passwordManager.whenCalled('getInsecureCredentials');
    await passwordManager.whenCalled('getPasswordCheckStatus');

    // Expect a proper number of insecure credentials as a parameter to
    // PluralStringProxy.
    assertEquals(3, await pluralString.whenCalled('compromisedPasswords'));
    assertEquals(0, await pluralString.whenCalled('reusedPasswords'));
    assertEquals(4, await pluralString.whenCalled('weakPasswords'));
    await flushTasks();

    // Expect string returned by PluralStringProxy.
    assertEquals('compromisedPasswords', section.$.compromisedRow.label);
    assertEquals('reusedPasswords', section.$.reusedRow.label);
    assertEquals('weakPasswords', section.$.weakRow.label);

    // Expect a proper attribute for front icon color
    assertTrue(section.$.compromisedRow.hasAttribute('show-red-icon'));
    assertFalse(section.$.reusedRow.hasAttribute('show-yellow-icon'));
    assertTrue(section.$.weakRow.hasAttribute('show-yellow-icon'));

    // Expect a proper rear icon state
    assertFalse(section.$.compromisedRow.hasAttribute('non-clickable'));
    assertTrue(section.$.reusedRow.hasAttribute('non-clickable'));
    assertFalse(section.$.weakRow.hasAttribute('non-clickable'));
  });

  test('Number of checked sites shown', async function() {
    passwordManager.data.groups = Array(10).fill(createCredentialGroup());
    passwordManager.data.checkStatus = makePasswordCheckStatus(
        {state: PasswordCheckState.IDLE, totalNumber: 20});

    const section = document.createElement('checkup-section');
    document.body.appendChild(section);
    passwordManager.whenCalled('getPasswordCheckStatus');

    await flushTasks();

    await pluralString.whenCalled('checkedPasswords');
    // getPluralString() for 'checkedPasswords' is called 2 times with 0 and 10.
    assertArrayEquals([0, 10], pluralString.getArgs('checkedPasswords'));
    assertEquals(
        'checkedPasswords', section.$.checkupStatusLabel.textContent!.trim());
  });

  [CheckupSubpage.COMPROMISED, CheckupSubpage.REUSED, CheckupSubpage.WEAK]
      .forEach(
          type => test(
              `clicking ${type} row navigates to details page`,
              async function() {
                passwordManager.data.checkStatus =
                    makePasswordCheckStatus({state: PasswordCheckState.IDLE});

                passwordManager.data.insecureCredentials =
                    [makeInsecureCredential({
                      types: [
                        CompromiseType.LEAKED,
                        CompromiseType.REUSED,
                        CompromiseType.WEAK,
                      ],
                    })];

                const section = document.createElement('checkup-section');
                document.body.appendChild(section);
                await passwordManager.whenCalled('getInsecureCredentials');
                await passwordManager.whenCalled('getPasswordCheckStatus');

                const listRow = section.shadowRoot!.querySelector<HTMLElement>(
                    `#${type}Row`);
                assertTrue(!!listRow);
                listRow.click();

                assertEquals(
                    Page.CHECKUP_DETAILS,
                    Router.getInstance().currentRoute.page);
                assertEquals(type, Router.getInstance().currentRoute.details);
              }));


  [CheckupSubpage.COMPROMISED, CheckupSubpage.REUSED, CheckupSubpage.WEAK]
      .forEach(
          type => test(
              `clicking ${type} row has no effect if no issues`,
              async function() {
                passwordManager.data.checkStatus =
                    makePasswordCheckStatus({state: PasswordCheckState.IDLE});
                passwordManager.data.insecureCredentials = [];

                const section = document.createElement('checkup-section');
                document.body.appendChild(section);
                await passwordManager.whenCalled('getInsecureCredentials');
                await passwordManager.whenCalled('getPasswordCheckStatus');

                const listRow = section.shadowRoot!.querySelector<HTMLElement>(
                    `#${type}Row`);
                assertTrue(!!listRow);
                listRow.click();

                assertEquals(
                    Page.CHECKUP, Router.getInstance().currentRoute.page);
              }));

  test('Start check automatically', async function() {
    const newParams = new URLSearchParams();
    newParams.set(UrlParam.START_CHECK, 'true');
    Router.getInstance().updateRouterParams(newParams);

    passwordManager.data.checkStatus =
        makePasswordCheckStatus({state: PasswordCheckState.IDLE});

    const section = document.createElement('checkup-section');
    document.body.appendChild(section);
    await flushTasks();

    await passwordManager.whenCalled('startBulkPasswordCheck');
    const interaction =
        await passwordManager.whenCalled('recordPasswordCheckInteraction');
    assertEquals(
        PasswordCheckInteraction.START_CHECK_AUTOMATICALLY, interaction);
  });

  test('changing number of groups changes title', async function() {
    passwordManager.data.groups = Array(10).fill(createCredentialGroup());
    passwordManager.data.checkStatus = makePasswordCheckStatus(
        {state: PasswordCheckState.IDLE, totalNumber: 20});

    const section = document.createElement('checkup-section');
    document.body.appendChild(section);
    passwordManager.whenCalled('getPasswordCheckStatus');

    await flushTasks();

    await pluralString.whenCalled('checkedPasswords');
    // getPluralString() for 'checkedPasswords' is called 2 times with 0 and 10.
    assertArrayEquals([0, 10], pluralString.getArgs('checkedPasswords'));

    passwordManager.data.groups = Array(9).fill(createCredentialGroup());
    assertTrue(!!passwordManager.listeners.savedPasswordListChangedListener);
    passwordManager.listeners.savedPasswordListChangedListener([]);

    await pluralString.whenCalled('checkedPasswords');
    // getPluralString() for 'checkedPasswords' is called 3 times with 0, 10
    // and 9.
    assertArrayEquals([0, 10, 9], pluralString.getArgs('checkedPasswords'));
  });

  test('Compromised section - subheader', async function() {
    passwordManager.data.checkStatus =
        makePasswordCheckStatus({state: PasswordCheckState.IDLE});

    // 3 compromised, 0 reused, 4 weak credentials
    passwordManager.data.insecureCredentials = [
      makeInsecureCredential({
        types: [
          CompromiseType.PHISHED,
          CompromiseType.LEAKED,
          CompromiseType.WEAK,
        ],
      }),
      makeInsecureCredential({
        types: [
          CompromiseType.PHISHED,
          CompromiseType.WEAK,
        ],
      }),
      makeInsecureCredential({
        types: [
          CompromiseType.LEAKED,
          CompromiseType.WEAK,
        ],
      }),
      makeInsecureCredential({types: [CompromiseType.WEAK]}),
    ];

    const section = document.createElement('checkup-section');
    document.body.appendChild(section);

    await passwordManager.whenCalled('getInsecureCredentials');
    await passwordManager.whenCalled('getPasswordCheckStatus');

    // Expect a proper number of insecure credentials as a parameter to
    // PluralStringProxy.
    assertEquals(3, await pluralString.whenCalled('compromisedPasswords'));
    assertEquals(3, await pluralString.whenCalled('compromisedPasswordsTitle'));
    await flushTasks();

    // Expect string returned by PluralStringProxy.
    assertEquals('compromisedPasswords', section.$.compromisedRow.label);
    assertEquals(
        'compromisedPasswordsTitle', section.$.compromisedRow.subLabel);
  });
});