chromium/chrome/android/javatests/src/org/chromium/chrome/browser/notifications/channels/SiteChannelsManagerTest.java

// Copyright 2017 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.notifications.channels;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import android.app.NotificationChannel;
import android.content.Context;
import android.os.Build;

import androidx.annotation.RequiresApi;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;

import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.MinAndroidSdkLevel;
import org.chromium.chrome.browser.notifications.NotificationChannelStatus;
import org.chromium.chrome.browser.notifications.NotificationSettingsBridge;
import org.chromium.chrome.browser.profiles.OTRProfileID;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.test.ChromeBrowserTestRule;
import org.chromium.components.browser_ui.notifications.NotificationManagerProxy;
import org.chromium.components.browser_ui.notifications.NotificationManagerProxyImpl;
import org.chromium.components.browser_ui.site_settings.PermissionInfo;
import org.chromium.components.content_settings.ContentSettingValues;
import org.chromium.components.content_settings.ContentSettingsType;
import org.chromium.components.content_settings.SessionModel;
import org.chromium.content_public.browser.test.NativeLibraryTestUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Instrumentation unit tests for SiteChannelsManager.
 *
 * <p>They run against the real NotificationManager so we can be sure Android does what we expect.
 *
 * <p>Note that channels are persistent by Android so even if a channel is deleted, if it is
 * recreated with the same id then the previous properties will be restored, including whether the
 * channel was blocked. Thus some of these tests use different channel ids to avoid this problem.
 */
@RunWith(BaseJUnit4ClassRunner.class)
@RequiresApi(Build.VERSION_CODES.O)
public class SiteChannelsManagerTest {
    private SiteChannelsManager mSiteChannelsManager;
    @Rule public ChromeBrowserTestRule mChromeBrowserTestRule = new ChromeBrowserTestRule();

    @Before
    public void setUp() {
        NativeLibraryTestUtils.loadNativeLibraryAndInitBrowserProcess();

        Context mContext = ApplicationProvider.getApplicationContext();
        NotificationManagerProxy notificationManagerProxy =
                new NotificationManagerProxyImpl(mContext);
        clearExistingSiteChannels(notificationManagerProxy);
        mSiteChannelsManager = new SiteChannelsManager(notificationManagerProxy);
    }

    private static void clearExistingSiteChannels(
            NotificationManagerProxy notificationManagerProxy) {
        for (NotificationChannel channel : notificationManagerProxy.getNotificationChannels()) {
            if (SiteChannelsManager.isValidSiteChannelId(channel.getId())
                    || (channel.getGroup() != null
                            && channel.getGroup()
                                    .equals(ChromeChannelDefinitions.ChannelGroupId.SITES))) {
                notificationManagerProxy.deleteNotificationChannel(channel.getId());
            }
        }
    }

    @Test
    @MinAndroidSdkLevel(Build.VERSION_CODES.O)
    @SmallTest
    public void testCreateSiteChannel_enabled() {
        mSiteChannelsManager.createSiteChannel("https://example-enabled.org", 62102180000L, true);
        assertThat(Arrays.asList(mSiteChannelsManager.getSiteChannels()), hasSize(1));
        NotificationSettingsBridge.SiteChannel channel = mSiteChannelsManager.getSiteChannels()[0];
        assertThat(channel.getOrigin(), is("https://example-enabled.org"));
        assertThat(channel.getStatus(), matchesChannelStatus(NotificationChannelStatus.ENABLED));
        assertThat(channel.getTimestamp(), is(62102180000L));
    }

    @Test
    @MinAndroidSdkLevel(Build.VERSION_CODES.O)
    @SmallTest
    public void testCreateSiteChannel_stripsSchemaForChannelName() {
        mSiteChannelsManager.createSiteChannel("http://127.0.0.1", 0L, true);
        mSiteChannelsManager.createSiteChannel("https://example.com", 0L, true);
        mSiteChannelsManager.createSiteChannel("ftp://127.0.0.1", 0L, true);
        List<String> channelNames = new ArrayList<>();
        for (NotificationSettingsBridge.SiteChannel siteChannel :
                mSiteChannelsManager.getSiteChannels()) {
            channelNames.add(siteChannel.toChannel().getName().toString());
        }
        assertThat(channelNames, containsInAnyOrder("ftp://127.0.0.1", "example.com", "127.0.0.1"));
    }

    @Test
    @MinAndroidSdkLevel(Build.VERSION_CODES.O)
    @RequiresApi(Build.VERSION_CODES.O)
    @SmallTest
    public void testCreateSiteChannel_disabled() {
        mSiteChannelsManager.createSiteChannel("https://example-blocked.org", 0L, false);
        assertThat(Arrays.asList(mSiteChannelsManager.getSiteChannels()), hasSize(1));
        NotificationSettingsBridge.SiteChannel channel = mSiteChannelsManager.getSiteChannels()[0];
        assertThat(channel.getOrigin(), is("https://example-blocked.org"));
        assertThat(channel.getStatus(), matchesChannelStatus(NotificationChannelStatus.BLOCKED));
    }

    @Test
    @MinAndroidSdkLevel(Build.VERSION_CODES.O)
    @SmallTest
    public void testDeleteSiteChannel_channelExists() {
        NotificationSettingsBridge.SiteChannel channel =
                mSiteChannelsManager.createSiteChannel("https://chromium.org", 0L, true);
        mSiteChannelsManager.deleteSiteChannel(channel.getId());
        assertThat(Arrays.asList(mSiteChannelsManager.getSiteChannels()), hasSize(0));
    }

    @Test
    @MinAndroidSdkLevel(Build.VERSION_CODES.O)
    @SmallTest
    public void testDeleteAllSiteChannels() {
        mSiteChannelsManager.createSiteChannel("https://chromium.org", 0L, true);
        mSiteChannelsManager.createSiteChannel("https://tests.peter.sh", 0L, true);
        mSiteChannelsManager.deleteAllSiteChannels();
        assertThat(Arrays.asList(mSiteChannelsManager.getSiteChannels()), hasSize(0));
    }

    @Test
    @MinAndroidSdkLevel(Build.VERSION_CODES.O)
    @SmallTest
    public void testDeleteSiteChannel_channelDoesNotExist() {
        mSiteChannelsManager.createSiteChannel("https://chromium.org", 0L, true);
        mSiteChannelsManager.deleteSiteChannel("https://some-other-origin.org");
        assertThat(Arrays.asList(mSiteChannelsManager.getSiteChannels()), hasSize(1));
    }

    @Test
    @MinAndroidSdkLevel(Build.VERSION_CODES.O)
    @SmallTest
    public void testGetChannelStatus_channelCreatedAsEnabled() {
        NotificationSettingsBridge.SiteChannel channel =
                mSiteChannelsManager.createSiteChannel("https://example-enabled.org", 0L, true);
        assertThat(
                mSiteChannelsManager.getChannelStatus(channel.getId()),
                matchesChannelStatus(NotificationChannelStatus.ENABLED));
    }

    @Test
    @MinAndroidSdkLevel(Build.VERSION_CODES.O)
    @SmallTest
    public void testGetChannelStatus_channelCreatedAsBlocked() {
        assertThat(
                mSiteChannelsManager.getChannelStatus("https://example-blocked.com"),
                matchesChannelStatus(NotificationChannelStatus.UNAVAILABLE));
        NotificationSettingsBridge.SiteChannel channel =
                mSiteChannelsManager.createSiteChannel("https://example-blocked.com", 0L, false);
        assertThat(
                mSiteChannelsManager.getChannelStatus(channel.getId()),
                matchesChannelStatus(NotificationChannelStatus.BLOCKED));
    }

    @Test
    @MinAndroidSdkLevel(Build.VERSION_CODES.O)
    @SmallTest
    public void testGetChannelStatus_channelNotCreated() {
        assertThat(
                mSiteChannelsManager.getChannelStatus("invalid-channel-id"),
                matchesChannelStatus(NotificationChannelStatus.UNAVAILABLE));
    }

    @Test
    @MinAndroidSdkLevel(Build.VERSION_CODES.O)
    @SmallTest
    public void testGetChannelStatus_channelCreatedThenDeleted() {
        NotificationSettingsBridge.SiteChannel channel =
                mSiteChannelsManager.createSiteChannel("https://chromium.org", 0L, true);
        mSiteChannelsManager.deleteSiteChannel(channel.getId());
        assertThat(
                mSiteChannelsManager.getChannelStatus(channel.getId()),
                matchesChannelStatus(NotificationChannelStatus.UNAVAILABLE));
    }

    @Test
    @MinAndroidSdkLevel(Build.VERSION_CODES.O)
    @SmallTest
    public void testBlockingPermissionInIncognitoTabbedActivityCreatesNoChannels() {
        PermissionInfo info =
                new PermissionInfo(
                        ContentSettingsType.NOTIFICATIONS,
                        "https://example-incognito.com",
                        null,
                        /* isEmbargo= */ true,
                        SessionModel.DURABLE);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    info.setContentSetting(
                            ProfileManager.getLastUsedRegularProfile()
                                    .getPrimaryOTRProfile(/* createIfNeeded= */ true),
                            ContentSettingValues.BLOCK);
                });
        assertThat(Arrays.asList(mSiteChannelsManager.getSiteChannels()), hasSize(0));
    }

    @Test
    @MinAndroidSdkLevel(Build.VERSION_CODES.O)
    @SmallTest
    public void testBlockingPermissionInIncognitoCCTCreatesNoChannels() {
        PermissionInfo info =
                new PermissionInfo(
                        ContentSettingsType.NOTIFICATIONS,
                        "https://example-incognito.com",
                        null,
                        /* isEmbargo= */ true,
                        SessionModel.DURABLE);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    OTRProfileID otrProfileID = OTRProfileID.createUnique("CCT:Incognito");
                    Profile nonPrimaryOTRProfile =
                            ProfileManager.getLastUsedRegularProfile()
                                    .getOffTheRecordProfile(
                                            otrProfileID, /* createIfNeeded= */ true);
                    assertNotNull(nonPrimaryOTRProfile);
                    assertTrue(nonPrimaryOTRProfile.isOffTheRecord());
                    info.setContentSetting(nonPrimaryOTRProfile, ContentSettingValues.BLOCK);
                });
        assertThat(Arrays.asList(mSiteChannelsManager.getSiteChannels()), hasSize(0));
    }

    private static Matcher<Integer> matchesChannelStatus(
            @NotificationChannelStatus final int status) {
        return new BaseMatcher<Integer>() {
            @Override
            public boolean matches(Object o) {
                return status == (int) o;
            }

            @Override
            public void describeTo(Description description) {
                description.appendText("NotificationChannelStatus." + statusToString(status));
            }

            @Override
            public void describeMismatch(final Object item, final Description description) {
                description.appendText(
                        "was NotificationChannelStatus." + statusToString((int) item));
            }
        };
    }

    private static String statusToString(@NotificationChannelStatus int status) {
        switch (status) {
            case NotificationChannelStatus.ENABLED:
                return "ENABLED";
            case NotificationChannelStatus.BLOCKED:
                return "BLOCKED";
            case NotificationChannelStatus.UNAVAILABLE:
                return "UNAVAILABLE";
        }
        return null;
    }

    @Test
    @MinAndroidSdkLevel(Build.VERSION_CODES.O)
    @MediumTest
    public void testGetChannelIdForOrigin_unknownOrigin() {
        String channelId = mSiteChannelsManager.getChannelIdForOrigin("https://unknown.com");

        assertThat(channelId, is(ChromeChannelDefinitions.ChannelId.SITES));

        assertThat(
                RecordHistogram.getHistogramTotalCountForTesting(
                        "Notifications.Android.SitesChannel"),
                is(1));
    }
}