chromium/net/android/javatests/src/org/chromium/net/ProxyChangeListenerTest.java

// Copyright 2019 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.net;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Proxy;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;

import org.chromium.base.ContextUtils;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.AdvancedMockContext;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/** Tests for {@link ProxyChangeListener}. */
@RunWith(BaseJUnit4ClassRunner.class)
public class ProxyChangeListenerTest {
    FakeContext mAppContext;
    ProxyChangeListener mListener;
    ProxyChangeListener.Delegate mDelegate;

    private static class FakeContext extends AdvancedMockContext {
        private class RegisteredReceiver {
            final BroadcastReceiver mReceiver;
            final IntentFilter mFilter;

            RegisteredReceiver(BroadcastReceiver receiver, IntentFilter filter) {
                mReceiver = receiver;
                mFilter = filter;
            }

            void filterAndPostBroadcast(final Intent broadcast) {
                if (mFilter != null) {
                    if (mFilter.match(null, broadcast, false, "sendBroadcast") < 0) {
                        return;
                    }
                }
                mReceiver.onReceive(FakeContext.this, broadcast);
            }
        }

        private List<RegisteredReceiver> mReceivers = new ArrayList<>();

        FakeContext() {
            super(
                    InstrumentationRegistry.getInstrumentation()
                            .getTargetContext()
                            .getApplicationContext());
        }

        @Override
        public Intent registerReceiver(
                BroadcastReceiver receiver,
                IntentFilter filter,
                String broadcastPermission,
                Handler scheduler) {
            mReceivers.add(new RegisteredReceiver(receiver, filter));
            return null;
        }

        @Override
        public Intent registerReceiver(
                BroadcastReceiver receiver,
                IntentFilter filter,
                String broadcastPermission,
                Handler scheduler,
                int flags) {
            mReceivers.add(new RegisteredReceiver(receiver, filter));
            return null;
        }

        @Override
        public void unregisterReceiver(BroadcastReceiver receiver) {
            for (RegisteredReceiver r : mReceivers) {
                if (r.mReceiver == receiver) {
                    mReceivers.remove(r);
                    return;
                }
            }
            throw new IllegalStateException("Receiver not found.");
        }

        @Override
        public void sendBroadcast(Intent intent) {
            for (final RegisteredReceiver r : mReceivers) {
                r.filterAndPostBroadcast(intent);
            }
        }

        public List<BroadcastReceiver> getReceivers() {
            ArrayList<BroadcastReceiver> result = new ArrayList<>(mReceivers.size());
            for (final RegisteredReceiver r : mReceivers) {
                result.add(r.mReceiver);
            }
            return result;
        }
    }

    @Before
    public void setUp() {
        mAppContext = Mockito.spy(new FakeContext());
        ContextUtils.initApplicationContextForTests(mAppContext);

        Looper.prepare();

        // Create the listener that's going to be used for tests
        mListener = ProxyChangeListener.create();
        mListener.setDelegateForTesting(
                mDelegate = Mockito.mock(ProxyChangeListener.Delegate.class));
        mListener.start(0);

        // These are looking for a call to register*NonExported*BroadcastReceiver to register a
        // dummy ProxyReceiver, which matches no intents. (If registered, it is registered first.)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                Mockito.verify(mAppContext)
                        .registerReceiver(
                                Mockito.any(),
                                Mockito.argThat(
                                        (IntentFilter filter) ->
                                                !filter.matchAction(Proxy.PROXY_CHANGE_ACTION)),
                                ArgumentMatchers.isNull(),
                                ArgumentMatchers.isNull(),
                                ArgumentMatchers.eq(ContextUtils.RECEIVER_NOT_EXPORTED));
            } else {
                Mockito.verify(mAppContext)
                        .registerReceiver(
                                Mockito.any(),
                                Mockito.argThat(
                                        (IntentFilter filter) ->
                                                !filter.matchAction(Proxy.PROXY_CHANGE_ACTION)),
                                ArgumentMatchers.isNull(),
                                ArgumentMatchers.isNull());
            }
        }
        // These are looking for the main call to register*Protected*BroadcastReceiver.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            Mockito.verify(mAppContext)
                    .registerReceiver(
                            Mockito.any(),
                            Mockito.argThat(
                                    (IntentFilter filter) ->
                                            filter.matchAction(Proxy.PROXY_CHANGE_ACTION)),
                            ArgumentMatchers.isNull(),
                            ArgumentMatchers.isNull(),
                            ArgumentMatchers.eq(0));
        } else {
            Mockito.verify(mAppContext)
                    .registerReceiver(
                            Mockito.any(),
                            Mockito.argThat(
                                    (IntentFilter filter) ->
                                            filter.matchAction(Proxy.PROXY_CHANGE_ACTION)),
                            ArgumentMatchers.isNull(),
                            ArgumentMatchers.isNull());
        }
    }

    @After
    public void tearDown() {
        mListener.stop();
        Assert.assertEquals(
                "All receivers should have been unregistered",
                mAppContext.getReceivers().size(),
                0);
    }

    @Test
    @SmallTest
    public void testProxyChangeListenerDelegateCalled() {
        Intent intent = new Intent();
        intent.setAction(Proxy.PROXY_CHANGE_ACTION);
        mAppContext.sendBroadcast(intent);

        Mockito.verify(mDelegate, Mockito.times(1)).proxySettingsChanged();
    }

    @Test
    @SmallTest
    public void testProxyChangeListenerDelegateCalledByReflection() throws Exception {
        // Assuming that the reflection to get the LoadedApk list of broadcast receivers worked,
        // this tests that an app can correctly find the ProxyChangeListener (which may be fake),
        // and call its onReceived method, resulting in one delegate invocation.
        for (Object rec : mAppContext.getReceivers()) {
            Class<?> clazz = rec.getClass();
            if (clazz.getName().contains("ProxyChangeListener")) {
                Method onReceiveMethod =
                        clazz.getDeclaredMethod("onReceive", Context.class, Intent.class);
                Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
                onReceiveMethod.invoke(rec, mAppContext, intent);
            }
        }

        Mockito.verify(mDelegate, Mockito.times(1)).proxySettingsChanged();
    }
}