chromium/chrome/android/javatests/src/org/chromium/chrome/browser/net/connectivitydetector/ConnectivityDetectorTest.java

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

package org.chromium.chrome.browser.net.connectivitydetector;

import androidx.test.filters.MediumTest;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.content_public.browser.test.NativeLibraryTestUtils;
import org.chromium.net.NetworkChangeNotifier;
import org.chromium.net.test.EmbeddedTestServerRule;

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/** Tests for {@link ConnectivityDetector}. */
@RunWith(BaseJUnit4ClassRunner.class)
@Batch(Batch.UNIT_TESTS)
public class ConnectivityDetectorTest implements ConnectivityDetector.Observer {
    private static final int TIMEOUT_MS = 5000;

    @ClassRule public static EmbeddedTestServerRule sTestServerRule = new EmbeddedTestServerRule();

    private ConnectivityDetector mConnectivityDetector;
    private @ConnectivityDetector.ConnectionState int mConnectionState =
            ConnectivityDetector.ConnectionState.NONE;
    private Semaphore mSemaphore = new Semaphore(0);
    // This test focuses on testing ConnectivityDetector functionalities, including http probes.
    // So don't skip http probes here.
    private ConnectivityDetectorDelegateStub mConnectivityDetectorDelegate =
            new ConnectivityDetectorDelegateStub(
                    ConnectivityDetector.ConnectionState.NO_INTERNET, /* skipHttpProbes= */ false);

    @Before
    public void setUp() throws Exception {
        NativeLibraryTestUtils.loadNativeLibraryNoBrowserProcess();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    if (!NetworkChangeNotifier.isInitialized()) {
                        NetworkChangeNotifier.init();
                    }
                    NetworkChangeNotifier.forceConnectivityState(true);
                    ConnectivityDetector.overrideConnectivityCheckInitialDelayMs(1000);
                    ConnectivityDetector.setDelegateForTesting(mConnectivityDetectorDelegate);
                    mConnectivityDetector =
                            new ConnectivityDetector(this, "ConnectivityDetectorTest");
                    mConnectivityDetectorDelegate.setConnectionStateFromSystem(
                            ConnectivityDetector.ConnectionState.NONE);
                });

        // Wait until the initial detection logic finishes to give all tests the same starting
        // point.
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.NO_INTERNET, mConnectionState);
    }

    @After
    public void tearDown() {
        ConnectivityDetector.resetDefaultProbeUrlForTesting();
        ConnectivityDetector.resetFallbackProbeUrlForTesting();
        ConnectivityDetector.resetProbeMethodForTesting();
    }

    @Override
    public void onConnectionStateChanged(
            @ConnectivityDetector.ConnectionState int connectionState) {
        mConnectionState = connectionState;
        mSemaphore.release();
    }

    @Test
    @MediumTest
    public void testEvaulateConnectionStateBySystem() throws Exception {
        setNetworkConnectivity(false);
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.DISCONNECTED, mConnectionState);

        mConnectivityDetectorDelegate.setConnectionStateFromSystem(
                ConnectivityDetector.ConnectionState.CAPTIVE_PORTAL);
        setNetworkConnectivity(true);
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.CAPTIVE_PORTAL, mConnectionState);
    }

    @Test
    @MediumTest
    public void testReevaulateConnectionStateBySystem() throws Exception {
        setNetworkConnectivity(false);
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.DISCONNECTED, mConnectionState);

        mConnectivityDetectorDelegate.setConnectionStateFromSystem(
                ConnectivityDetector.ConnectionState.CAPTIVE_PORTAL);
        setNetworkConnectivity(true);
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.CAPTIVE_PORTAL, mConnectionState);

        // detect() should get the connection info from the system again.
        mConnectivityDetectorDelegate.setConnectionStateFromSystem(
                ConnectivityDetector.ConnectionState.VALIDATED);
        mConnectivityDetector.detect();
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.VALIDATED, mConnectionState);
    }

    @Test
    @MediumTest
    public void testReevaulateConnectionStateBySystemAfterBackoff() throws Exception {
        setNetworkConnectivity(false);
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.DISCONNECTED, mConnectionState);

        mConnectivityDetectorDelegate.setConnectionStateFromSystem(
                ConnectivityDetector.ConnectionState.CAPTIVE_PORTAL);
        setNetworkConnectivity(true);
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.CAPTIVE_PORTAL, mConnectionState);

        // We should get the connection info from the system again after some delay.
        mConnectivityDetectorDelegate.setConnectionStateFromSystem(
                ConnectivityDetector.ConnectionState.VALIDATED);
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.VALIDATED, mConnectionState);
    }

    @Test
    @MediumTest
    public void testProbeDefaultUrlReturning204() throws Exception {
        String testUrl = sTestServerRule.getServer().getURL("/nocontent");

        ConnectivityDetector.overrideDefaultProbeUrlForTesting(testUrl);
        checkConnectivityViaDefaultUrl();
        Assert.assertEquals(ConnectivityDetector.ConnectionState.VALIDATED, mConnectionState);
    }

    @Test
    @MediumTest
    public void testProbeDefaultUrlReturning200WithoutContent() throws Exception {
        String testUrl = sTestServerRule.getServer().getURL("/echo?status=200");

        ConnectivityDetector.overrideDefaultProbeUrlForTesting(testUrl);
        // This will make the test sever return empty content.
        ConnectivityDetector.overrideProbeMethodForTesting("POST");
        checkConnectivityViaDefaultUrl();
        Assert.assertEquals(ConnectivityDetector.ConnectionState.VALIDATED, mConnectionState);
    }

    @Test
    @MediumTest
    @DisabledTest(message = "b/343926825")
    public void testProbeDefaultUrlReturning200WithContent() throws Exception {
        String testUrl = sTestServerRule.getServer().getURL("/echo?status=200");

        ConnectivityDetector.overrideDefaultProbeUrlForTesting(testUrl);
        checkConnectivityViaDefaultUrl();
        Assert.assertEquals(ConnectivityDetector.ConnectionState.CAPTIVE_PORTAL, mConnectionState);
    }

    @Test
    @MediumTest
    public void testProbeDefaultUrlReturning304() throws Exception {
        String testUrl = sTestServerRule.getServer().getURL("/echo?status=304");

        ConnectivityDetector.overrideDefaultProbeUrlForTesting(testUrl);
        checkConnectivityViaDefaultUrl();
        Assert.assertEquals(ConnectivityDetector.ConnectionState.CAPTIVE_PORTAL, mConnectionState);
    }

    @Test
    @MediumTest
    public void testProbeDefaultUrlReturning500() throws Exception {
        String testUrl = sTestServerRule.getServer().getURL("/echo?status=500");
        String noContentUrl = sTestServerRule.getServer().getURL("/nocontent");

        ConnectivityDetector.overrideDefaultProbeUrlForTesting(testUrl);
        ConnectivityDetector.overrideFallbackProbeUrlForTesting(noContentUrl);
        checkConnectivityViaDefaultUrl();
        // Probing default URL gets back 500 which should cause no change to connection state but
        // probing fallback URL immediately. We should then trigger VALIDATED state.
        Assert.assertEquals(ConnectivityDetector.ConnectionState.VALIDATED, mConnectionState);
        Assert.assertFalse(hasScheduledRetry());
    }

    @Test
    @MediumTest
    public void testProbeFallbackUrlReturning204() throws Exception {
        String testUrl = sTestServerRule.getServer().getURL("/nocontent");

        ConnectivityDetector.overrideFallbackProbeUrlForTesting(testUrl);
        checkConnectivityViaFallbackUrl();
        Assert.assertEquals(ConnectivityDetector.ConnectionState.VALIDATED, mConnectionState);
        Assert.assertFalse(hasScheduledRetry());
    }

    @Test
    @MediumTest
    public void testProbeFallbackUrlReturning304() throws Exception {
        String testUrl = sTestServerRule.getServer().getURL("/echo?status=304");

        ConnectivityDetector.overrideFallbackProbeUrlForTesting(testUrl);
        checkConnectivityViaFallbackUrl();
        Assert.assertEquals(ConnectivityDetector.ConnectionState.CAPTIVE_PORTAL, mConnectionState);
        Assert.assertTrue(hasScheduledRetry());
    }

    @Test
    @MediumTest
    public void testDetectDisconnectedState() throws Exception {
        setNetworkConnectivity(false);
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.DISCONNECTED, mConnectionState);
    }

    @Test
    @MediumTest
    public void testDetectValidatedState() throws Exception {
        String testUrl = sTestServerRule.getServer().getURL("/nocontent");
        ConnectivityDetector.overrideDefaultProbeUrlForTesting(testUrl);

        setNetworkConnectivity(false);
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.DISCONNECTED, mConnectionState);
        setNetworkConnectivity(true);
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.VALIDATED, mConnectionState);
    }

    @Test
    @MediumTest
    public void testDetectCaptivePortalAndThenValidatedState() throws Exception {
        String hasContentUrl = sTestServerRule.getServer().getURL("/echo?status=200");
        ConnectivityDetector.overrideDefaultProbeUrlForTesting(hasContentUrl);
        String noContentUrl = sTestServerRule.getServer().getURL("/nocontent");
        ConnectivityDetector.overrideFallbackProbeUrlForTesting(noContentUrl);

        setNetworkConnectivity(false);
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.DISCONNECTED, mConnectionState);
        setNetworkConnectivity(true);
        // Probing default URL gets back 200 which should trigger CAPTIVE_PORTAL state.
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.CAPTIVE_PORTAL, mConnectionState);
        // Probing fallback URL immediately after default URL gets back "204 no content" which
        // should trigger VALIDATED state.
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.VALIDATED, mConnectionState);
    }

    @Test
    @MediumTest
    public void testDetectCaptivePortalStateForBothDefaultAndFallbackUrls() throws Exception {
        String hasContentUrl = sTestServerRule.getServer().getURL("/echo?status=200");
        ConnectivityDetector.overrideDefaultProbeUrlForTesting(hasContentUrl);
        String hasContentUrl2 = sTestServerRule.getServer().getURL("/echo?status=304");
        ConnectivityDetector.overrideFallbackProbeUrlForTesting(hasContentUrl2);

        setNetworkConnectivity(false);
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.DISCONNECTED, mConnectionState);
        setNetworkConnectivity(true);
        // Probing default URL gets back 200 which should trigger CAPTIVE_PORTAL state.
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.CAPTIVE_PORTAL, mConnectionState);
        // Twist the connection state to one different from CAPTIVE_PORTAL in order to get the
        // callback called with 2nd probe.
        setConnectionState(ConnectivityDetector.ConnectionState.NONE);
        // Probing fallback URL immediately after default URL gets back 304 should keep
        // CAPTIVE_PORTAL state.
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.CAPTIVE_PORTAL, mConnectionState);
    }

    @Test
    @MediumTest
    public void testDetectValidatedStateAfterBackoff() throws Exception {
        String hasContentUrl = sTestServerRule.getServer().getURL("/echo?status=200");
        String noContentUrl = sTestServerRule.getServer().getURL("/nocontent");
        ConnectivityDetector.overrideDefaultProbeUrlForTesting(hasContentUrl);
        ConnectivityDetector.overrideFallbackProbeUrlForTesting(hasContentUrl);

        setNetworkConnectivity(false);
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.DISCONNECTED, mConnectionState);
        setNetworkConnectivity(true);
        // Probing default URL gets back 200 which should trigger CAPTIVE_PORTAL state.
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.CAPTIVE_PORTAL, mConnectionState);
        // Twist the connection state to one different from CAPTIVE_PORTAL in order to get the
        // callback called with 2nd probe. This is because the callback will not be called if
        // connection state does not change based on the result from 2nd probe.
        setConnectionState(ConnectivityDetector.ConnectionState.NONE);
        // Probing fallback URL immediately after default URL gets back 200 which should trigger
        // CAPTIVE_PORTAL state.
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.CAPTIVE_PORTAL, mConnectionState);
        // After backoff, 2nd probe of default URL gets back "204 no content" which should trigger
        // VALIDATED state.
        ConnectivityDetector.overrideDefaultProbeUrlForTesting(noContentUrl);
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        Assert.assertEquals(ConnectivityDetector.ConnectionState.VALIDATED, mConnectionState);
    }

    private static void setNetworkConnectivity(boolean connected) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    NetworkChangeNotifier.forceConnectivityState(connected);
                });
    }

    private boolean hasScheduledRetry() {
        final boolean[] result = new boolean[1];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    result[0] = mConnectivityDetector.getHandlerForTesting().hasMessages(0);
                });
        return result[0];
    }

    private void checkConnectivityViaDefaultUrl() throws Exception {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mConnectivityDetector.setUseDefaultUrlForTesting(true);
                    mConnectivityDetector.checkConnectivityViaHttpProbe();
                });
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
    }

    private void checkConnectivityViaFallbackUrl() throws Exception {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mConnectivityDetector.setUseDefaultUrlForTesting(false);
                    mConnectivityDetector.checkConnectivityViaHttpProbe();
                });
        Assert.assertTrue(mSemaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
    }

    private void setConnectionState(@ConnectivityDetector.ConnectionState int connectionState) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mConnectivityDetector.forceConnectionStateForTesting(connectionState);
                });
    }
}