chromium/chrome/test/data/webui/password_manager/password_details_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';

// clang-format off
import type {PasskeyDetailsCardElement, PasswordDetailsCardElement, PasswordDetailsSectionElement} from 'chrome://password-manager/password_manager.js';
import {Page, PasswordManagerImpl, PasswordViewPageInteractions, Router, SyncBrowserProxyImpl, UrlParam} from 'chrome://password-manager/password_manager.js';
import {assertArrayEquals, assertDeepEquals, assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
import {isVisible} from 'chrome://webui-test/test_util.js';

import {TestPasswordManagerProxy} from './test_password_manager_proxy.js';
import {TestSyncBrowserProxy} from './test_sync_browser_proxy.js';
import {createAffiliatedDomain, createCredentialGroup, createPasswordEntry} from './test_util.js';

// <if expr="_google_chrome">
import {PASSWORD_SHARE_BUTTON_BUTTON_ELEMENT_ID} from 'chrome://password-manager/password_manager.js';
import {waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';
// </if>
// clang-format on

suite('PasswordDetailsSectionTest', function() {
  let passwordManager: TestPasswordManagerProxy;
  let syncProxy: TestSyncBrowserProxy;


  setup(function() {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    passwordManager = new TestPasswordManagerProxy();
    PasswordManagerImpl.setInstance(passwordManager);
    syncProxy = new TestSyncBrowserProxy();
    SyncBrowserProxyImpl.setInstance(syncProxy);
    Router.getInstance().navigateTo(Page.PASSWORDS);
    return flushTasks();
  });

  test('Navigation from passwords section', async function() {
    const section: PasswordDetailsSectionElement =
        document.createElement('password-details-section');
    document.body.appendChild(section);
    await flushTasks();

    // Simulate navigation from passwords list.
    const group = createCredentialGroup({name: 'test.com'});
    Router.getInstance().navigateTo(Page.PASSWORD_DETAILS, group);

    const title = section.shadowRoot!.querySelector('#title');
    assertTrue(!!title);
    assertEquals(group.name, title.textContent!.trim());
  });

  test('Navigating directly', async function() {
    // Simulate direct navigation.
    Router.getInstance().navigateTo(Page.PASSWORD_DETAILS, 'test.com');
    passwordManager.data.groups = [
      createCredentialGroup({
        name: 'test.com',
        credentials: [
          createPasswordEntry({id: 0}),
          createPasswordEntry({id: 1}),
        ],
      }),
      createCredentialGroup({name: 'test1.com'}),
      createCredentialGroup({name: 'test2.com'}),
    ];
    // Simulate successful reauth.
    passwordManager.setRequestCredentialsDetailsResponse(
        passwordManager.data.groups[0]!.entries.slice());

    const section: PasswordDetailsSectionElement =
        document.createElement('password-details-section');
    document.body.appendChild(section);
    await passwordManager.whenCalled('getCredentialGroups');
    assertEquals(
        PasswordViewPageInteractions.CREDENTIAL_REQUESTED_BY_URL,
        await passwordManager.whenCalled('recordPasswordViewInteraction'));
    assertArrayEquals(
        [0, 1], await passwordManager.whenCalled('requestCredentialsDetails'));
    await flushTasks();

    const title = section.$.title;
    assertTrue(!!title);
    assertEquals('test.com', title.textContent!.trim());
  });

  test('Navigating directly fails when group is not found', async function() {
    // Simulate direct navigation.
    Router.getInstance().navigateTo(Page.PASSWORD_DETAILS, 'test.com');
    assertEquals(Page.PASSWORD_DETAILS, Router.getInstance().currentRoute.page);

    const section: PasswordDetailsSectionElement =
        document.createElement('password-details-section');
    document.body.appendChild(section);
    await passwordManager.whenCalled('getCredentialGroups');
    await flushTasks();

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

  test('Navigating directly fails when auth failed', async function() {
    // Simulate direct navigation.
    Router.getInstance().navigateTo(Page.PASSWORD_DETAILS, 'test.com');
    assertEquals(Page.PASSWORD_DETAILS, Router.getInstance().currentRoute.page);
    passwordManager.data.groups = [
      createCredentialGroup({
        name: 'test.com',
        credentials: [
          createPasswordEntry({id: 0}),
          createPasswordEntry({id: 1}),
        ],
      }),
    ];

    const section: PasswordDetailsSectionElement =
        document.createElement('password-details-section');
    document.body.appendChild(section);
    await passwordManager.whenCalled('getCredentialGroups');
    // Since setRequestCredentialsDetailsResponse was not called, auth has
    // failed.
    assertArrayEquals(
        [0, 1], await passwordManager.whenCalled('requestCredentialsDetails'));
    await flushTasks();

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

  test('Clicking back navigates to passwords section', async function() {
    const group = createCredentialGroup({name: 'test.com'});
    Router.getInstance().navigateTo(Page.PASSWORD_DETAILS, group);

    const section: PasswordDetailsSectionElement =
        document.createElement('password-details-section');
    document.body.appendChild(section);
    await flushTasks();

    const backButton = section.$.backButton;

    assertEquals(Page.PASSWORD_DETAILS, Router.getInstance().currentRoute.page);
    backButton.click();
    assertEquals(Page.PASSWORDS, Router.getInstance().currentRoute.page);
  });

  test('All credential entries are displayed', async function() {
    const group = createCredentialGroup({
      name: 'test.com',
      credentials: [
        createPasswordEntry({id: 0, username: 'test1'}),
        createPasswordEntry({id: 1, username: 'test2'}),
        createPasswordEntry({isPasskey: true, id: 2, username: 'test3'}),
      ],
    });
    Router.getInstance().navigateTo(Page.PASSWORD_DETAILS, group);

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

    const passwordEntries =
        section.shadowRoot!.querySelectorAll<PasswordDetailsCardElement>(
            'password-details-card');
    assertTrue(!!passwordEntries.length);
    assertEquals(passwordEntries.length, 2);
    for (let index = 0; index < passwordEntries.length; ++index) {
      assertDeepEquals(passwordEntries[index]!.password, group.entries[index]);
    }

    const passkeyEntries =
        section.shadowRoot!.querySelectorAll<PasskeyDetailsCardElement>(
            'passkey-details-card');
    assertEquals(passkeyEntries.length, 1);
    assertEquals(passkeyEntries[0]!.passkey, group.entries[2]);
  });

  test('Details section closes when password deleted', async function() {
    const group = createCredentialGroup({
      name: 'test.com',
      credentials: [
        createPasswordEntry({id: 0, username: 'test1'}),
      ],
    });
    passwordManager.data.groups = [group];
    Router.getInstance().navigateTo(Page.PASSWORD_DETAILS, group);

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

    assertEquals(
        1,
        section.shadowRoot!.querySelectorAll('password-details-card').length);

    // Assert that details section subscribed as a listener.
    assertTrue(!!passwordManager.listeners.savedPasswordListChangedListener);

    // Clear groups and invoke password changes.
    passwordManager.data.groups = [];
    passwordManager.listeners.savedPasswordListChangedListener([]);
    await passwordManager.whenCalled('getCredentialGroups');

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

  test('Details section ignores irrelevant updates', async function() {
    const group = createCredentialGroup({
      name: 'test.com',
      credentials: [
        createPasswordEntry({id: 0, username: 'test1'}),
      ],
    });
    passwordManager.data.groups = [group];
    Router.getInstance().navigateTo(Page.PASSWORD_DETAILS, group);

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

    assertEquals(
        1,
        section.shadowRoot!.querySelectorAll('password-details-card').length);

    // Assert that details section subscribed as a listener.
    assertTrue(!!passwordManager.listeners.savedPasswordListChangedListener);

    // Invoke password changes to trigger group refresh logic even though
    // nothing has changed.
    passwordManager.listeners.savedPasswordListChangedListener([]);
    await passwordManager.whenCalled('getCredentialGroups');

    // Verify no calls were made to requestCredentialsDetails().
    assertEquals(0, passwordManager.getCallCount('requestCredentialsDetails'));
    // Verify details page is still visible.
    assertEquals(Page.PASSWORD_DETAILS, Router.getInstance().currentRoute.page);
    assertEquals(
        1,
        section.shadowRoot!.querySelectorAll('password-details-card').length);
  });

  test('Details section updates info when id changed', async function() {
    const group = createCredentialGroup({
      name: 'test.com',
      credentials: [
        createPasswordEntry({id: 0, username: 'test1'}),
      ],
    });
    passwordManager.data.groups = [group];
    passwordManager.setRequestCredentialsDetailsResponse(group.entries);
    Router.getInstance().navigateTo(Page.PASSWORD_DETAILS, group);

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

    assertEquals(
        1,
        section.shadowRoot!.querySelectorAll('password-details-card').length);

    // Assert that details section subscribed as a listener.
    assertTrue(!!passwordManager.listeners.savedPasswordListChangedListener);

    // Update id and invoke password changes.
    passwordManager.data.groups = [createCredentialGroup({
      name: 'test.com',
      credentials: [
        createPasswordEntry({id: 1, username: 'test1'}),
      ],
    })];
    passwordManager.listeners.savedPasswordListChangedListener([]);
    await passwordManager.whenCalled('getCredentialGroups');
    await passwordManager.whenCalled('requestCredentialsDetails');

    // Verify details page is still visible.
    assertEquals(Page.PASSWORD_DETAILS, Router.getInstance().currentRoute.page);
    assertEquals(
        1,
        section.shadowRoot!.querySelectorAll('password-details-card').length);
  });

  test('Url is updated based on new group info', async function() {
    const group = createCredentialGroup({
      name: 'test.com',
      credentials: [
        createPasswordEntry({id: 1, username: 'test1'}),
        createPasswordEntry({id: 2, username: 'test2'}),
      ],
    });
    passwordManager.data.groups = [group];
    Router.getInstance().navigateTo(Page.PASSWORD_DETAILS, group);

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

    await new Promise(resolve => setTimeout(resolve));

    assertEquals(
        2,
        section.shadowRoot!.querySelectorAll('password-details-card').length);

    // Assert that details section subscribed as a listener.
    assertTrue(!!passwordManager.listeners.savedPasswordListChangedListener);

    // Delete one credential in the group and invoke password changes.
    passwordManager.data.groups = [createCredentialGroup({
      name: 'test.de',
      credentials: [
        createPasswordEntry({id: 1, username: 'test1'}),
      ],
    })];
    passwordManager.setRequestCredentialsDetailsResponse(
        passwordManager.data.groups[0]!.entries);
    passwordManager.listeners.savedPasswordListChangedListener([]);
    await passwordManager.whenCalled('getCredentialGroups');
    await passwordManager.whenCalled('requestCredentialsDetails');
    await flushTasks();

    // Verify details page is still visible and path is matching new group name.
    assertEquals(Page.PASSWORD_DETAILS, Router.getInstance().currentRoute.page);
    assertEquals(
        '/passwords/test.de', Router.getInstance().currentRoute.path());
    assertEquals(
        1,
        section.shadowRoot!.querySelectorAll('password-details-card').length);
  });

  test('Page closes when auth times out', async function() {
    const group = createCredentialGroup({
      name: 'test.com',
      credentials: [
        createPasswordEntry({id: 0, username: 'test1'}),
      ],
    });
    passwordManager.data.groups = [group];
    Router.getInstance().navigateTo(Page.PASSWORD_DETAILS, group);

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

    // Assert that details section subscribed as a listener.
    assertTrue(!!passwordManager.listeners.passwordManagerAuthTimeoutListener);

    passwordManager.listeners.passwordManagerAuthTimeoutListener();
    assertEquals(
        PasswordViewPageInteractions.TIMED_OUT_IN_VIEW_PAGE,
        await passwordManager.whenCalled('recordPasswordViewInteraction'));
    await flushTasks();

    // Assert that now Passwords page is shown.
    assertEquals(Page.PASSWORDS, Router.getInstance().currentRoute.page);
  });

  test('Navigating by domain name', async function() {
    // Simulate direct navigation.
    Router.getInstance().navigateTo(Page.PASSWORD_DETAILS, 'www.test.com');
    passwordManager.data.groups = [
      createCredentialGroup({
        name: 'test.com',
        credentials: [
          createPasswordEntry({id: 0}),
          createPasswordEntry({id: 1}),
        ],
      }),
    ];
    passwordManager.data.groups[0]!.entries[0]!.affiliatedDomains =
        [createAffiliatedDomain('www.test.com')];
    passwordManager.data.groups[0]!.entries[1]!.affiliatedDomains =
        [createAffiliatedDomain('test app')];

    // Simulate successful reauth.
    passwordManager.setRequestCredentialsDetailsResponse(
        passwordManager.data.groups[0]!.entries.slice());

    const section: PasswordDetailsSectionElement =
        document.createElement('password-details-section');
    document.body.appendChild(section);
    await passwordManager.whenCalled('getCredentialGroups');
    assertArrayEquals(
        [0, 1], await passwordManager.whenCalled('requestCredentialsDetails'));
    await flushTasks();

    assertEquals(Page.PASSWORD_DETAILS, Router.getInstance().currentRoute.page);
    const title = section.$.title;
    assertEquals('test.com', title.textContent!.trim());

    const entries =
        section.shadowRoot!.querySelectorAll<PasswordDetailsCardElement>(
            'password-details-card');
    assertEquals(2, entries.length);
  });

  test(
      'Clicking back navigates to passwords section and keeps old query',
      async function() {
        const group = createCredentialGroup({name: 'test.com'});
        const query = new URLSearchParams();
        query.set(UrlParam.SEARCH_TERM, 'bar');
        Router.getInstance().navigateTo(Page.PASSWORD_DETAILS, group, query);

        const section: PasswordDetailsSectionElement =
            document.createElement('password-details-section');
        document.body.appendChild(section);
        await flushTasks();

        assertEquals(
            Page.PASSWORD_DETAILS, Router.getInstance().currentRoute.page);
        section.$.backButton.click();
        assertEquals(Page.PASSWORDS, Router.getInstance().currentRoute.page);
        assertEquals(query, Router.getInstance().currentRoute.queryParameters);
      });

  // <if expr="_google_chrome">
  test('Register password sharing IPH for password card', async function() {
    syncProxy.syncInfo = {
      isEligibleForAccountStorage: false,
      isSyncingPasswords: true,
    };

    const group = createCredentialGroup({
      name: 'test.com',
      credentials: [
        createPasswordEntry({id: 0, username: 'test1'}),
      ],
    });
    Router.getInstance().navigateTo(Page.PASSWORD_DETAILS, group);

    const section = document.createElement('password-details-section');
    document.body.appendChild(section);
    await waitAfterNextRender(section);
    await flushTasks();

    const card = section.shadowRoot!.querySelector('password-details-card');
    assertTrue(!!card);

    assertDeepEquals(
        card.getSortedAnchorStatusesForTesting(),
        [
          [PASSWORD_SHARE_BUTTON_BUTTON_ELEMENT_ID, true],
        ],
    );
  });

  test(
      'Password sharing IPH is not registered with passkey card present',
      async function() {
        syncProxy.syncInfo = {
          isEligibleForAccountStorage: false,
          isSyncingPasswords: true,
        };

        const group = createCredentialGroup({
          name: 'test.com',
          credentials: [
            createPasswordEntry({isPasskey: true, id: 0, username: 'test1'}),
            createPasswordEntry({id: 1, username: 'test2'}),
            createPasswordEntry({id: 2, username: 'test3'}),
          ],
        });
        Router.getInstance().navigateTo(Page.PASSWORD_DETAILS, group);

        const section = document.createElement('password-details-section');
        document.body.appendChild(section);
        await waitAfterNextRender(section);
        await flushTasks();

        section.shadowRoot!.querySelectorAll('password-details-card')
            .forEach(entry => {
              assertDeepEquals(
                  entry.getSortedAnchorStatusesForTesting(),
                  [],
              );
            });
      });
  // </if>

  test('should show button to move password', async function() {
    passwordManager.data.isOptedInAccountStorage = true;
    syncProxy.syncInfo = {
      isEligibleForAccountStorage: true,
      isSyncingPasswords: false,
    };

    const group = createCredentialGroup({
      name: 'test.com',
      credentials: [
        createPasswordEntry({
          id: 0,
          username: 'test1',
          inProfileStore: true,
          inAccountStore: false,
        }),
      ],
    });
    Router.getInstance().navigateTo(Page.PASSWORD_DETAILS, group);

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

    const passwordEntry =
        section.shadowRoot!.querySelector<PasswordDetailsCardElement>(
            'password-details-card');
    assertTrue(!!passwordEntry);
    assertTrue(isVisible(passwordEntry!.shadowRoot!.querySelector<HTMLElement>(
        '.move-password-container')));
  });

  test('should not show button to move password', async function() {
    passwordManager.data.isOptedInAccountStorage = true;
    syncProxy.syncInfo = {
      isEligibleForAccountStorage: true,
      isSyncingPasswords: false,
    };

    const group = createCredentialGroup({
      name: 'test.com',
      credentials: [
        createPasswordEntry({id: 0, username: 'test1', inAccountStore: true}),
      ],
    });
    Router.getInstance().navigateTo(Page.PASSWORD_DETAILS, group);

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

    const passwordEntry =
        section.shadowRoot!.querySelector<PasswordDetailsCardElement>(
            'password-details-card');
    assertTrue(!!passwordEntry);
    assertFalse(isVisible(passwordEntry!.shadowRoot!.querySelector<HTMLElement>(
        '.move-password-container')));
  });
});