chromium/chrome/test/data/webui/extensions/runtime_host_permissions_test.ts

// Copyright 2018 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://extensions/extensions.js';

import type {ExtensionsRuntimeHostPermissionsElement} from 'chrome://extensions/extensions.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {eventToPromise, isChildVisible} from 'chrome://webui-test/test_util.js';

import {TestService} from './test_service.js';
import {MetricsPrivateMock} from './test_util.js';

suite('RuntimeHostPermissions', function() {
  let element: ExtensionsRuntimeHostPermissionsElement;
  let delegate: TestService;
  let metricsPrivateMock: MetricsPrivateMock;

  const HostAccess = chrome.developerPrivate.HostAccess;
  const ITEM_ID = 'a'.repeat(32);

  setup(function() {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    element = document.createElement('extensions-runtime-host-permissions');
    delegate = new TestService();
    delegate.userSiteSettings = {permittedSites: [], restrictedSites: []};
    element.delegate = delegate;
    element.itemId = ITEM_ID;
    element.enableEnhancedSiteControls = false;

    document.body.appendChild(element);

    metricsPrivateMock = new MetricsPrivateMock();
    chrome.metricsPrivate =
        metricsPrivateMock as unknown as typeof chrome.metricsPrivate;
  });

  teardown(function() {
    element.remove();
  });

  test('permissions display', function() {
    const permissions: chrome.developerPrivate.RuntimeHostPermissions = {
      hostAccess: HostAccess.ON_CLICK,
      hasAllHosts: true,
      hosts: [{granted: false, host: 'https://*/*'}],
    };

    element.permissions = permissions;
    flush();

    const testIsVisible = isChildVisible.bind(null, element);
    assertTrue(testIsVisible('#hostAccess'));

    const selectHostAccess = element.getSelectMenu();
    assertEquals(HostAccess.ON_CLICK, selectHostAccess.value);
    // For on-click mode, there should be no runtime hosts listed.
    assertFalse(testIsVisible('#hosts'));

    // Changing the data's access should change the UI appropriately.
    element.set('permissions.hostAccess', HostAccess.ON_ALL_SITES);
    flush();
    assertEquals(HostAccess.ON_ALL_SITES, selectHostAccess.value);
    assertFalse(testIsVisible('#hosts'));

    // Setting the mode to on specific sites should display the runtime hosts
    // list.
    element.set('permissions.hostAccess', HostAccess.ON_SPECIFIC_SITES);
    element.set('permissions.hosts', [
      {host: 'https://example.com', granted: true},
      {host: 'https://chromium.org', granted: true},
    ]);
    flush();
    assertEquals(HostAccess.ON_SPECIFIC_SITES, selectHostAccess.value);
    assertTrue(testIsVisible('#hosts'));
    // Expect three entries in the list: the two hosts + the add-host button.
    assertEquals(
        3,
        element.shadowRoot!.querySelector('#hosts')!.getElementsByTagName('li')
            .length);
    assertTrue(testIsVisible('#add-host'));
  });

  test('permissions display with enableEnhancedSiteControls flag', function() {
    element.enableEnhancedSiteControls = true;
    const permissions: chrome.developerPrivate.RuntimeHostPermissions = {
      hostAccess: HostAccess.ON_CLICK,
      hasAllHosts: true,
      hosts: [
        {host: 'https://example.com', granted: true},
        {host: 'https://chromium.org', granted: true},
      ],
    };

    element.permissions = permissions;
    flush();

    const testIsVisible = isChildVisible.bind(null, element);
    assertTrue(testIsVisible('#newHostAccess'));
    assertTrue(testIsVisible('#new-section-heading'));

    const selectHostAccess = element.getSelectMenu();
    assertEquals(HostAccess.ON_CLICK, selectHostAccess.value);
    // For on-click mode, there should be no runtime hosts listed.
    assertFalse(testIsVisible('#hosts'));
    assertFalse(testIsVisible('#add-site-button'));

    // Changing the data's access should change the UI appropriately.
    element.set('permissions.hostAccess', HostAccess.ON_ALL_SITES);
    flush();
    assertEquals(HostAccess.ON_ALL_SITES, selectHostAccess.value);
    assertFalse(testIsVisible('#hosts'));
    assertFalse(testIsVisible('#add-site-button'));

    element.set('permissions.hostAccess', HostAccess.ON_SPECIFIC_SITES);
    flush();
    assertEquals(HostAccess.ON_SPECIFIC_SITES, selectHostAccess.value);
    assertTrue(testIsVisible('#hosts'));
    assertTrue(testIsVisible('#add-site-button'));
    assertFalse(testIsVisible('#add-host'));
  });

  test('permissions selection', async () => {
    const permissions: chrome.developerPrivate.RuntimeHostPermissions = {
      hostAccess: HostAccess.ON_CLICK,
      hasAllHosts: true,
      hosts: [{granted: false, host: 'https://*.com/*'}],
    };

    element.permissions = permissions;
    flush();

    const selectHostAccess = element.getSelectMenu();
    assertTrue(!!selectHostAccess);

    // Changes the value of the selectHostAccess menu and fires the change
    // event, then verifies that the delegate was called with the correct
    // value.
    async function assertDelegateCallOnAccessChange(
        newValue: chrome.developerPrivate.HostAccess): Promise<void> {
      selectHostAccess.value = newValue;
      selectHostAccess.dispatchEvent(new CustomEvent('change'));
      const args = await delegate.whenCalled('setItemHostAccess');
      assertEquals(ITEM_ID, args[0] /* id */);
      assertEquals(newValue, args[1] /* access */);
      delegate.resetResolver('setItemHostAccess');
    }

    // Check that selecting different values correctly notifies the delegate.
    await assertDelegateCallOnAccessChange(HostAccess.ON_ALL_SITES);
    assertEquals(
        metricsPrivateMock.getUserActionCount(
            'Extensions.Settings.Hosts.OnAllSitesSelected'),
        1);
    await assertDelegateCallOnAccessChange(HostAccess.ON_CLICK);
    assertEquals(
        metricsPrivateMock.getUserActionCount(
            'Extensions.Settings.Hosts.OnClickSelected'),
        1);
    // Finally select back to all sites and ensure the user action for it has
    // incremented.
    await assertDelegateCallOnAccessChange(HostAccess.ON_ALL_SITES);
    assertEquals(
        metricsPrivateMock.getUserActionCount(
            'Extensions.Settings.Hosts.OnAllSitesSelected'),
        2);
  });

  test('on select sites cancel', async () => {
    const permissions: chrome.developerPrivate.RuntimeHostPermissions = {
      hostAccess: HostAccess.ON_CLICK,
      hasAllHosts: true,
      hosts: [{granted: false, host: 'https://*/*'}],
    };

    element.permissions = permissions;
    flush();

    const selectHostAccess = element.getSelectMenu();
    assertTrue(!!selectHostAccess);

    selectHostAccess.value = HostAccess.ON_SPECIFIC_SITES;
    selectHostAccess.dispatchEvent(new CustomEvent('change'));

    flush();
    const dialog =
        element.shadowRoot!.querySelector('extensions-runtime-hosts-dialog');
    assertTrue(!!dialog);
    assertEquals(
        metricsPrivateMock.getUserActionCount(
            'Extensions.Settings.Hosts.OnClickSelected'),
        0);
    assertEquals(
        metricsPrivateMock.getUserActionCount(
            'Extensions.Settings.Hosts.OnSpecificSitesSelected'),
        1);

    assertTrue(dialog.updateHostAccess);

    // Canceling the dialog should reset the selectHostAccess value to ON_CLICK,
    // since no host was added.
    assertTrue(dialog.isOpen());
    const whenClosed = eventToPromise('close', dialog);
    dialog.shadowRoot!.querySelector<HTMLElement>('.cancel-button')!.click();
    await whenClosed;

    flush();
    assertEquals(HostAccess.ON_CLICK, selectHostAccess.value);
    assertEquals(
        metricsPrivateMock.getUserActionCount(
            'Extensions.Settings.Hosts.AddHostDialogCanceled'),
        1);
    // Reverting to a previous option when canceling the dialog doesn't count
    // as a user action, so the on-click action count should still be 0.
    assertEquals(
        metricsPrivateMock.getUserActionCount(
            'Extensions.Settings.Hosts.OnClickSelected'),
        0);
    // Changing to a different option after this should still log a user action
    // as asserted.
    selectHostAccess.value = HostAccess.ON_ALL_SITES;
    selectHostAccess.dispatchEvent(new CustomEvent('change'));
    flush();
    assertEquals(
        metricsPrivateMock.getUserActionCount(
            'Extensions.Settings.Hosts.OnAllSitesSelected'),
        1);
  });

  test('on select sites accept', async () => {
    const permissions: chrome.developerPrivate.RuntimeHostPermissions = {
      hostAccess: HostAccess.ON_CLICK,
      hasAllHosts: true,
      hosts: [{granted: false, host: 'https://*/*'}],
    };

    element.permissions = permissions;
    flush();

    const selectHostAccess = element.getSelectMenu();
    assertTrue(!!selectHostAccess);

    selectHostAccess.value = HostAccess.ON_SPECIFIC_SITES;
    selectHostAccess.dispatchEvent(new CustomEvent('change'));
    assertEquals(
        metricsPrivateMock.getUserActionCount(
            'Extensions.Settings.Hosts.OnSpecificSitesSelected'),
        1);

    flush();
    const dialog =
        element.shadowRoot!.querySelector('extensions-runtime-hosts-dialog');
    assertTrue(!!dialog);

    assertTrue(dialog.updateHostAccess);

    // Make the add button clickable by entering valid input.
    const input = dialog.shadowRoot!.querySelector('cr-input');
    assertTrue(!!input);
    input.value = 'https://example.com';
    input.dispatchEvent(
        new CustomEvent('input', {bubbles: true, composed: true}));
    await input.updateComplete;

    // Closing the dialog (as opposed to canceling) should keep the
    // selectHostAccess value at ON_SPECIFIC_SITES.
    assertTrue(dialog.isOpen());
    const whenClosed = eventToPromise('close', dialog);
    dialog.$.submit.click();
    await whenClosed;
    flush();
    assertEquals(HostAccess.ON_SPECIFIC_SITES, selectHostAccess.value);
    assertEquals(
        metricsPrivateMock.getUserActionCount(
            'Extensions.Settings.Hosts.AddHostDialogSubmitted'),
        1);

    // Simulate the new host being added.
    const updatedPermissions = {
      hostAccess: HostAccess.ON_SPECIFIC_SITES,
      hasAllHosts: true,
      hosts: [
        {host: 'https://example.com/*', granted: true},
        {host: 'https://*/*', granted: false},
      ],
    };
    element.permissions = updatedPermissions;
    flush();

    // Open the dialog by clicking to edit the host permission.
    const editHost =
        element.shadowRoot!.querySelector<HTMLElement>('.open-edit-host');
    assertTrue(!!editHost);
    editHost.click();
    assertEquals(
        metricsPrivateMock.getUserActionCount(
            'Extensions.Settings.Hosts.ActionMenuOpened'),
        1);
    const actionMenu = element.shadowRoot!.querySelector('cr-action-menu');
    assertTrue(!!actionMenu);
    const actionMenuEdit =
        actionMenu.querySelector<HTMLElement>('#action-menu-edit');
    assertTrue(!!actionMenuEdit);
    actionMenuEdit.click();
    flush();
    assertEquals(
        metricsPrivateMock.getUserActionCount(
            'Extensions.Settings.Hosts.ActionMenuEditActivated'),
        1);

    // Verify that the dialog does not want to update the old host access.
    // Regression test for https://crbug.com/903082.
    const newDialog =
        element.shadowRoot!.querySelector('extensions-runtime-hosts-dialog');
    assertTrue(!!newDialog);
    assertTrue(newDialog.$.dialog.open);
    assertFalse(newDialog.updateHostAccess);
    assertEquals('https://example.com/*', newDialog.currentSite);
  });

  test('clicking add host triggers dialog', function() {
    const permissions: chrome.developerPrivate.RuntimeHostPermissions = {
      hostAccess: HostAccess.ON_SPECIFIC_SITES,
      hasAllHosts: true,
      hosts: [
        {host: 'https://www.example.com/*', granted: true},
        {host: 'https://*.google.com', granted: false},
        {host: '*://*.com/*', granted: false},
      ],
    };

    element.permissions = permissions;
    flush();

    const addHostButton =
        element.shadowRoot!.querySelector<HTMLElement>('#add-host');
    assertTrue(!!addHostButton);
    assertTrue(isChildVisible(element, '#add-host'));

    addHostButton.click();
    flush();
    assertEquals(
        metricsPrivateMock.getUserActionCount(
            'Extensions.Settings.Hosts.AddHostActivated'),
        1);
    const dialog =
        element.shadowRoot!.querySelector('extensions-runtime-hosts-dialog');
    assertTrue(!!dialog);
    assertTrue(dialog.$.dialog.open);
    assertEquals(null, dialog.currentSite);
    assertFalse(dialog.updateHostAccess);
  });

  test('removing runtime host permissions', async function() {
    const permissions: chrome.developerPrivate.RuntimeHostPermissions = {
      hostAccess: HostAccess.ON_SPECIFIC_SITES,
      hasAllHosts: true,
      hosts: [
        {host: 'https://example.com', granted: true},
        {host: 'https://chromium.org', granted: true},
        {host: '*://*.com/*', granted: false},
      ],
    };
    element.permissions = permissions;
    flush();

    const editHost =
        element.shadowRoot!.querySelector<HTMLElement>('.open-edit-host');
    assertTrue(!!editHost);
    editHost.click();
    assertEquals(
        metricsPrivateMock.getUserActionCount(
            'Extensions.Settings.Hosts.ActionMenuOpened'),
        1);
    const actionMenu = element.shadowRoot!.querySelector('cr-action-menu');
    assertTrue(!!actionMenu);
    assertTrue(actionMenu.open);

    const remove = actionMenu.querySelector<HTMLElement>('#action-menu-remove');
    assertTrue(!!remove);

    remove.click();
    assertEquals(
        metricsPrivateMock.getUserActionCount(
            'Extensions.Settings.Hosts.ActionMenuRemoveActivated'),
        1);
    const [id, site] = await delegate.whenCalled('removeRuntimeHostPermission');
    assertEquals(ITEM_ID, id);
    assertEquals('https://chromium.org', site);
    assertFalse(actionMenu.open);
  });

  test('clicking edit host triggers dialog', function() {
    const permissions: chrome.developerPrivate.RuntimeHostPermissions = {
      hostAccess: HostAccess.ON_SPECIFIC_SITES,
      hasAllHosts: true,
      hosts: [
        {host: 'https://example.com', granted: true},
        {host: 'https://chromium.org', granted: true},
        {host: '*://*.com/*', granted: false},
      ],
    };
    element.permissions = permissions;
    flush();

    const editHost =
        element.shadowRoot!.querySelector<HTMLElement>('.open-edit-host');
    assertTrue(!!editHost);
    editHost.click();
    const actionMenu = element.shadowRoot!.querySelector('cr-action-menu');
    assertTrue(!!actionMenu);

    const actionMenuEdit =
        actionMenu.querySelector<HTMLElement>('#action-menu-edit');
    assertTrue(!!actionMenuEdit);

    actionMenuEdit.click();
    flush();
    const dialog =
        element.shadowRoot!.querySelector('extensions-runtime-hosts-dialog');
    assertTrue(!!dialog);
    assertTrue(dialog.$.dialog.open);
    assertFalse(dialog.updateHostAccess);
    assertEquals('https://chromium.org', dialog.currentSite);
  });

  test('clicking edit host with enableEnhancedSiteControls flag', function() {
    element.enableEnhancedSiteControls = true;
    const permissions: chrome.developerPrivate.RuntimeHostPermissions = {
      hostAccess: HostAccess.ON_SPECIFIC_SITES,
      hasAllHosts: true,
      hosts: [
        {host: 'https://chromium.org', granted: true},
      ],
    };

    element.permissions = permissions;
    flush();

    const editHost =
        element.shadowRoot!.querySelector<HTMLElement>('.edit-host');
    assertTrue(!!editHost);
    editHost.click();
    flush();

    // clicking the `editHost` for the site should open the dialog.
    const dialog =
        element.shadowRoot!.querySelector('extensions-runtime-hosts-dialog');
    assertTrue(!!dialog);
    assertTrue(dialog.$.dialog.open);
    assertFalse(dialog.updateHostAccess);
    assertEquals('https://chromium.org', dialog.currentSite);
  });

  test(
      'clicking remove host with enableEnhancedSiteControls flag',
      async function() {
        element.enableEnhancedSiteControls = true;
        const permissions: chrome.developerPrivate.RuntimeHostPermissions = {
          hostAccess: HostAccess.ON_SPECIFIC_SITES,
          hasAllHosts: true,
          hosts: [
            {host: 'https://chromium.org', granted: true},
          ],
        };

        element.permissions = permissions;
        flush();

        const removeHost =
            element.shadowRoot!.querySelector<HTMLElement>('.remove-host');
        assertTrue(!!removeHost);
        removeHost.click();
        flush();

        const [id, site] =
            await delegate.whenCalled('removeRuntimeHostPermission');
        assertEquals(ITEM_ID, id);
        assertEquals('https://chromium.org', site);
      });

  test(
      'switching away from ON_SPECIFIC_SITES with flag enabled triggers dialog',
      async function() {
        element.enableEnhancedSiteControls = true;

        const permissions: chrome.developerPrivate.RuntimeHostPermissions = {
          hostAccess: HostAccess.ON_SPECIFIC_SITES,
          hasAllHosts: true,
          hosts: [
            {host: 'https://example.com', granted: true},
            {host: 'https://chromium.org', granted: true},
            {host: '*://*.com/*', granted: false},
          ],
        };

        element.permissions = permissions;
        flush();

        const selectHostAccess = element.getSelectMenu();
        assertTrue(!!selectHostAccess);

        // Change the `selectHostAccess` value and the dialog should be open.
        selectHostAccess.value = HostAccess.ON_CLICK;
        selectHostAccess.dispatchEvent(new CustomEvent('change'));
        flush();

        let dialog = element.getRemoveSiteDialog();
        assertTrue(!!dialog);
        assertTrue(dialog.open);

        // Clicking cancel on the dialog should revert the `selectHostAccess`
        // value back to ON_SPECIFIC_SITES.
        const cancel = dialog.querySelector<HTMLElement>('.cancel-button');
        assertTrue(!!cancel);
        cancel.click();

        flush();
        assertFalse(!!element.getRemoveSiteDialog());
        assertEquals(HostAccess.ON_SPECIFIC_SITES, selectHostAccess.value);

        // Change the `selectHostAccess` value and the dialog should be open.
        selectHostAccess.value = HostAccess.ON_CLICK;
        selectHostAccess.dispatchEvent(new CustomEvent('change'));
        flush();

        dialog = element.getRemoveSiteDialog();
        assertTrue(!!dialog);
        assertTrue(dialog.open);

        // Clicking remove on the dialog should a call to the delegate to set
        // the host access to the `selectHostAccess` value.
        const remove = dialog.querySelector<HTMLElement>('.action-button');
        assertTrue(!!remove);
        remove.click();

        const [id, access] = await delegate.whenCalled('setItemHostAccess');
        assertEquals(ITEM_ID, id);
        assertEquals(HostAccess.ON_CLICK, access);

        flush();
        assertFalse(!!element.getRemoveSiteDialog());
        assertEquals(HostAccess.ON_CLICK, selectHostAccess.value);
      });
});