chromium/chrome/test/data/webui/extensions/site_permissions_edit_permissions_dialog_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.

/**
 * @fileoverview
 * Suite of tests for site-permissions-edit-permissions-dialog.
 * */
import 'chrome://extensions/extensions.js';

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

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

suite('SitePermissionsEditPermissionsDialog', function() {
  let element: SitePermissionsEditPermissionsDialogElement;
  let delegate: TestService;
  const SiteSet = chrome.developerPrivate.SiteSet;
  const HostAccess = chrome.developerPrivate.HostAccess;

  const extensions = [
    createExtensionInfo({
      id: 'test_1',
      name: 'test_1',
      iconUrl: 'icon_url',
    }),
    createExtensionInfo({
      id: 'test_2',
      name: 'test_2',
      iconUrl: 'icon_url',
    }),
    createExtensionInfo({
      id: 'test_3',
      name: 'test_3',
      iconUrl: 'icon_url',
    }),
  ];

  const matchingExtensionsInfo = [
    {id: 'test_1', siteAccess: HostAccess.ON_CLICK, canRequestAllSites: true},
    {
      id: 'test_2',
      siteAccess: HostAccess.ON_SPECIFIC_SITES,
      canRequestAllSites: true,
    },
  ];

  const changeHostAccess =
      (select: HTMLSelectElement,
       access: chrome.developerPrivate.HostAccess) => {
        select.value = access;
        select.dispatchEvent(new CustomEvent('change'));
      };

  setup(function() {
    loadTimeData.overrideValues({'enableUserPermittedSites': true});

    delegate = new TestService();
    delegate.matchingExtensionsInfo = matchingExtensionsInfo;

    setupElement();
  });

  function setupElement() {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    element =
        document.createElement('site-permissions-edit-permissions-dialog');
    element.delegate = delegate;
    element.extensions = extensions;
    element.site = 'http://example.com';
    element.originalSiteSet = SiteSet.USER_PERMITTED;
    document.body.appendChild(element);
  }

  test('extra text shown if site matches subdomains', function() {
    assertEquals('http://example.com', element.$.site.innerText);
    assertFalse(isVisible(element.$.includesSubdomains));

    element.site = '*.example.com';
    flush();

    assertEquals('example.com', element.$.site.innerText);
    assertTrue(isVisible(element.$.includesSubdomains));
  });

  test('editing current site set', async function() {
    flush();
    const siteSetRadioGroup =
        element.shadowRoot!.querySelector('cr-radio-group');
    assertTrue(!!siteSetRadioGroup);
    assertEquals(SiteSet.USER_PERMITTED, siteSetRadioGroup.selected);

    const restrictSiteRadioButton =
        element.shadowRoot!.querySelector<HTMLElement>(
            `cr-radio-button[name=${SiteSet.USER_RESTRICTED}]`);
    assertTrue(!!restrictSiteRadioButton);
    restrictSiteRadioButton.click();
    await eventToPromise('selected-changed', siteSetRadioGroup);

    assertEquals(SiteSet.USER_RESTRICTED, siteSetRadioGroup.selected);

    const whenClosed = eventToPromise('close', element);
    element.$.submit.click();

    const [siteSet, sites] = await delegate.whenCalled('addUserSpecifiedSites');
    assertEquals(SiteSet.USER_RESTRICTED, siteSet);
    assertDeepEquals([element.site], sites);

    await whenClosed;
    assertFalse(element.$.dialog.open);
  });

  test(
      'list of matching extensions shown when changing options',
      async function() {
        element.site = 'example.com';
        flush();
        const siteSetRadioGroup =
            element.shadowRoot!.querySelector('cr-radio-group');
        assertTrue(!!siteSetRadioGroup);

        let extensionSiteAccessRows =
            element!.shadowRoot!.querySelectorAll<HTMLElement>(
                '.extension-row');
        assertEquals(0, extensionSiteAccessRows.length);

        const extensionSpecifiedRadioButton =
            element.shadowRoot!.querySelector<HTMLElement>(
                `cr-radio-button[name=${SiteSet.EXTENSION_SPECIFIED}]`);
        assertTrue(!!extensionSpecifiedRadioButton);
        extensionSpecifiedRadioButton.click();
        await eventToPromise('selected-changed', siteSetRadioGroup);

        const site = await delegate.whenCalled('getMatchingExtensionsForSite');
        assertEquals('*://example.com/', site);

        assertEquals(SiteSet.EXTENSION_SPECIFIED, siteSetRadioGroup.selected);
        extensionSiteAccessRows =
            element!.shadowRoot!.querySelectorAll<HTMLElement>(
                '.extension-row');
        assertEquals(2, extensionSiteAccessRows.length);

        const whenClosed = eventToPromise('close', element);
        element.$.submit.click();
        const [siteSet, sites] =
            await delegate.whenCalled('removeUserSpecifiedSites');
        assertEquals(SiteSet.USER_PERMITTED, siteSet);

        // Since the site being edited is just a host, remove both the http and
        // https url for the host.
        assertDeepEquals(
            [`http://${element.site}`, `https://${element.site}`], sites);

        await whenClosed;
        assertFalse(element.$.dialog.open);
      });

  test(
      'radio buttons not shown for site matching subdomains', async function() {
        flush();
        const extensionSpecifiedRadioButton =
            element.shadowRoot!.querySelector<HTMLElement>(
                `cr-radio-button[name=${SiteSet.EXTENSION_SPECIFIED}]`);
        const siteSetRadioGroup =
            element.shadowRoot!.querySelector('cr-radio-group');

        assertTrue(!!extensionSpecifiedRadioButton);
        assertTrue(!!siteSetRadioGroup);
        extensionSpecifiedRadioButton.click();
        await eventToPromise('selected-changed', siteSetRadioGroup);
        const site = await delegate.whenCalled('getMatchingExtensionsForSite');
        assertEquals('http://example.com/', site);

        assertTrue(
            isVisible(element.shadowRoot!.querySelector('cr-radio-group')));

        element.site = '*.etld.com';
        flush();

        assertFalse(
            isVisible(element.shadowRoot!.querySelector('cr-radio-group')));
      });

  test(
      'list of extensions changes in response to extensions updating',
      async function() {
        flush();
        const extensionSpecifiedRadioButton =
            element.shadowRoot!.querySelector<HTMLElement>(
                `cr-radio-button[name=${SiteSet.EXTENSION_SPECIFIED}]`);
        const siteSetRadioGroup =
            element.shadowRoot!.querySelector('cr-radio-group');
        assertTrue(!!extensionSpecifiedRadioButton);
        assertTrue(!!siteSetRadioGroup);
        extensionSpecifiedRadioButton.click();
        await eventToPromise('selected-changed', siteSetRadioGroup);
        let site = await delegate.whenCalled('getMatchingExtensionsForSite');
        assertEquals('http://example.com/', site);

        let extensionSiteAccessSelects =
            element.shadowRoot!.querySelectorAll('select');
        assertEquals(2, extensionSiteAccessSelects.length);
        assertEquals(HostAccess.ON_CLICK, extensionSiteAccessSelects[0]!.value);

        delegate.matchingExtensionsInfo = [
          {
            id: 'test_1',
            siteAccess: HostAccess.ON_ALL_SITES,
            canRequestAllSites: true,
          },
          {
            id: 'test_2',
            siteAccess: HostAccess.ON_SPECIFIC_SITES,
            canRequestAllSites: true,
          },
        ];

        element.extensions = [
          createExtensionInfo({
            id: 'test_1',
            name: 'test_1',
            iconUrl: 'icon_url',
          }),
          createExtensionInfo({
            id: 'test_2',
            name: 'test_2',
            iconUrl: 'icon_url',
          }),
        ];

        // Test that changing `element.extensions` causes a call to
        // getMatchingExtensionsForSite.
        site = await delegate.whenCalled('getMatchingExtensionsForSite');
        assertEquals('http://example.com/', site);
        flush();

        extensionSiteAccessSelects =
            element.shadowRoot!.querySelectorAll('select');
        assertEquals(2, extensionSiteAccessSelects.length);

        // Test that the value displayed for the first extension matches the
        // updated matchingExtensionsInfo.
        assertEquals(
            HostAccess.ON_ALL_SITES, extensionSiteAccessSelects[0]!.value);
      });

  test('editing extension site access', async function() {
    element.site = 'example.com';
    delegate.matchingExtensionsInfo = [
      ...matchingExtensionsInfo,
      {
        id: 'test_3',
        siteAccess: HostAccess.ON_ALL_SITES,
        canRequestAllSites: true,
      },
    ];

    flush();
    const extensionSpecifiedRadioButton =
        element.shadowRoot!.querySelector<HTMLElement>(
            `cr-radio-button[name=${SiteSet.EXTENSION_SPECIFIED}]`);
    assertTrue(!!extensionSpecifiedRadioButton);
    const siteSetRadioGroup =
        element.shadowRoot!.querySelector('cr-radio-group');
    assertTrue(!!siteSetRadioGroup);
    extensionSpecifiedRadioButton.click();
    await eventToPromise('selected-changed', siteSetRadioGroup);

    const site = await delegate.whenCalled('getMatchingExtensionsForSite');
    assertEquals('*://example.com/', site);

    const extensionSiteAccessRows =
        element.shadowRoot!.querySelectorAll<HTMLElement>('.extension-row');
    assertEquals(3, extensionSiteAccessRows.length);

    const siteAccessSelectMenus =
        element.shadowRoot!.querySelectorAll<HTMLSelectElement>(
            '.extension-host-access');
    assertEquals(3, siteAccessSelectMenus.length);

    // Edit the site access values for the first two extensions.
    changeHostAccess(siteAccessSelectMenus[0]!, HostAccess.ON_SPECIFIC_SITES);
    changeHostAccess(siteAccessSelectMenus[1]!, HostAccess.ON_ALL_SITES);

    // Edit the site access for the third extension once, then change it
    // back to the original value.
    changeHostAccess(siteAccessSelectMenus[2]!, HostAccess.ON_CLICK);
    changeHostAccess(siteAccessSelectMenus[2]!, HostAccess.ON_ALL_SITES);

    const whenClosed = eventToPromise('close', element);
    element.$.submit.click();
    await delegate.whenCalled('removeUserSpecifiedSites');

    const [siteToUpdate, siteAccessUpdates] =
        await delegate.whenCalled('updateSiteAccess');
    // For updating the extensions' site access, check that a wildcard
    // host is used if the site was a host only.
    assertEquals('*://example.com/', siteToUpdate);

    // Since the site access for extension "test_3" was ultimately not
    // changed through the select menu, it should not be included in
    // `siteAccessUpdates`.
    assertDeepEquals(
        [
          {id: 'test_1', siteAccess: HostAccess.ON_SPECIFIC_SITES},
          {id: 'test_2', siteAccess: HostAccess.ON_ALL_SITES},
        ],
        siteAccessUpdates);

    await whenClosed;
    assertFalse(element.$.dialog.open);
  });

  test(
      'updateSiteAccess arguments are updated in response to extension updates',
      async function() {
        element.site = 'http://example.com';
        delegate.matchingExtensionsInfo = [
          ...matchingExtensionsInfo,
          {
            id: 'test_3',
            siteAccess: HostAccess.ON_ALL_SITES,
            canRequestAllSites: true,
          },
        ];

        flush();
        const extensionSpecifiedRadioButton =
            element.shadowRoot!.querySelector<HTMLElement>(
                `cr-radio-button[name=${SiteSet.EXTENSION_SPECIFIED}]`);
        assertTrue(!!extensionSpecifiedRadioButton);
        const siteSetRadioGroup =
            element.shadowRoot!.querySelector('cr-radio-group');
        assertTrue(!!siteSetRadioGroup);
        extensionSpecifiedRadioButton.click();
        await eventToPromise('selected-changed', siteSetRadioGroup);

        let site = await delegate.whenCalled('getMatchingExtensionsForSite');
        assertEquals('http://example.com/', site);

        const siteAccessSelectMenus =
            element.shadowRoot!.querySelectorAll<HTMLSelectElement>(
                '.extension-host-access');
        assertEquals(3, siteAccessSelectMenus.length);

        // Edit the site access values for all three extensions.
        changeHostAccess(
            siteAccessSelectMenus[0]!, HostAccess.ON_SPECIFIC_SITES);
        changeHostAccess(siteAccessSelectMenus[1]!, HostAccess.ON_ALL_SITES);
        changeHostAccess(siteAccessSelectMenus[2]!, HostAccess.ON_CLICK);

        // Simulate an update event happening. Note that the new site access for
        // `test_1` is now the same as what was edited and `test_3` no longer
        // exists.
        delegate.matchingExtensionsInfo = [
          {
            id: 'test_1',
            siteAccess: HostAccess.ON_SPECIFIC_SITES,
            canRequestAllSites: true,
          },
          {
            id: 'test_2',
            siteAccess: HostAccess.ON_SPECIFIC_SITES,
            canRequestAllSites: true,
          },
        ];

        element.extensions = [
          createExtensionInfo({
            id: 'test_1',
            name: 'test_1',
            iconUrl: 'icon_url',
          }),
          createExtensionInfo({
            id: 'test_2',
            name: 'test_2',
            iconUrl: 'icon_url',
          }),
        ];

        // Changing `element.extensions` causes a call to
        // getMatchingExtensionsForSite.
        site = await delegate.whenCalled('getMatchingExtensionsForSite');
        assertEquals('http://example.com/', site);

        const whenClosed = eventToPromise('close', element);
        element.$.submit.click();
        await delegate.whenCalled('removeUserSpecifiedSites');

        const [siteToUpdate, siteAccessUpdates] =
            await delegate.whenCalled('updateSiteAccess');
        assertEquals('http://example.com/', siteToUpdate);

        // Only the site access update for `test_2` should be included, as after
        // the update, there's no change for site access for `test_1` and
        // `test_3` no longer exists.
        assertDeepEquals(
            [{id: 'test_2', siteAccess: HostAccess.ON_ALL_SITES}],
            siteAccessUpdates);

        await whenClosed;
        assertFalse(element.$.dialog.open);
      });

  test(
      'permitted sites not visible when enableUserPermittedSites flag is false',
      function() {
        loadTimeData.overrideValues({'enableUserPermittedSites': false});

        // set up the element again to capture the updated value of
        // enableUserPermittedSites.
        setupElement();

        flush();

        // Only the user restricted and extension specified radio buttons should
        // be visible.
        const permittedSiteRadioButton =
            element.shadowRoot!.querySelector<HTMLElement>(
                `cr-radio-button[name=${SiteSet.USER_PERMITTED}]`);
        assertFalse(isVisible(permittedSiteRadioButton));

        const restrictedSiteRadioButton =
            element.shadowRoot!.querySelector<HTMLElement>(
                `cr-radio-button[name=${SiteSet.USER_RESTRICTED}]`);
        assertTrue(isVisible(restrictedSiteRadioButton));

        const extensionSiteRadioButton =
            element.shadowRoot!.querySelector<HTMLElement>(
                `cr-radio-button[name=${SiteSet.EXTENSION_SPECIFIED}]`);
        assertTrue(isVisible(extensionSiteRadioButton));
      });

  test(
      'changing site access disabled for extensions installed by policy',
      async function() {
        // Set the second extension to be installed by policy.
        element.extensions = [
          createExtensionInfo({
            id: 'test_1',
            name: 'test_1',
            iconUrl: 'icon_url',
          }),
          createExtensionInfo({
            id: 'test_2',
            name: 'test_2',
            iconUrl: 'icon_url',
            controlledInfo: {text: 'policy'},
          }),
        ];

        flush();

        const extensionSpecifiedRadioButton =
            element.shadowRoot!.querySelector<HTMLElement>(
                `cr-radio-button[name=${SiteSet.EXTENSION_SPECIFIED}]`);
        assertTrue(!!extensionSpecifiedRadioButton);
        const siteSetRadioGroup =
            element.shadowRoot!.querySelector('cr-radio-group');
        assertTrue(!!siteSetRadioGroup);
        extensionSpecifiedRadioButton.click();
        await eventToPromise('selected-changed', siteSetRadioGroup);

        const site = await delegate.whenCalled('getMatchingExtensionsForSite');
        assertEquals('http://example.com/', site);

        const siteAccessSelectMenus =
            element.shadowRoot!.querySelectorAll<HTMLSelectElement>(
                '.extension-host-access');
        assertEquals(2, siteAccessSelectMenus.length);

        // The second extension's site access selector should be disabled
        // since it's installed by policy.
        assertFalse(siteAccessSelectMenus[0]!.disabled);
        assertTrue(siteAccessSelectMenus[1]!.disabled);
      });

  test(
      'all sites option hidden for extensions that do not request to all sites',
      async function() {
        delegate.matchingExtensionsInfo = [
          {
            id: 'test_1',
            siteAccess: HostAccess.ON_SPECIFIC_SITES,
            canRequestAllSites: true,
          },
          {
            id: 'test_2',
            siteAccess: HostAccess.ON_SPECIFIC_SITES,
            canRequestAllSites: false,
          },
        ];

        flush();

        const extensionSpecifiedRadioButton =
            element.shadowRoot!.querySelector<HTMLElement>(
                `cr-radio-button[name=${SiteSet.EXTENSION_SPECIFIED}]`);
        assertTrue(!!extensionSpecifiedRadioButton);
        const siteSetRadioGroup =
            element.shadowRoot!.querySelector('cr-radio-group');
        assertTrue(!!siteSetRadioGroup);
        extensionSpecifiedRadioButton.click();
        await eventToPromise('selected-changed', siteSetRadioGroup);

        // Changing `element.extensions` causes a call to
        // getMatchingExtensionsForSite.
        const site = await delegate.whenCalled('getMatchingExtensionsForSite');
        assertEquals('http://example.com/', site);
        flush();

        const siteAccessSelectMenus =
            element.shadowRoot!.querySelectorAll<HTMLSelectElement>(
                '.extension-host-access');
        assertEquals(2, siteAccessSelectMenus.length);

        // First select menu should have all options enabled, second menu
        // should have `ON_ALL_SITES` disabled.
        assertFalse(
            siteAccessSelectMenus[0]!
                .querySelector<HTMLSelectElement>(
                    `option[value=${HostAccess.ON_ALL_SITES}]`)!.disabled);
        assertTrue(
            siteAccessSelectMenus[1]!
                .querySelector<HTMLSelectElement>(
                    `option[value=${HostAccess.ON_ALL_SITES}]`)!.disabled);
      });
});