chromium/chrome/android/javatests/src/org/chromium/chrome/browser/browserservices/digitalgoods/DigitalGoodsTest.java

// Copyright 2020 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.browserservices.digitalgoods;

import static org.junit.Assert.assertEquals;

import static org.chromium.chrome.browser.browserservices.TestTrustedWebActivityService.COMMAND_SET_RESPONSE;
import static org.chromium.chrome.browser.browserservices.TestTrustedWebActivityService.SET_RESPONSE_BUNDLE;
import static org.chromium.chrome.browser.browserservices.TestTrustedWebActivityService.SET_RESPONSE_NAME;

import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.trusted.TrustedWebActivityCallback;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisableIf;
import org.chromium.chrome.browser.ChromeApplicationImpl;
import org.chromium.chrome.browser.browserservices.TrustedWebActivityClient;
import org.chromium.chrome.browser.browserservices.TrustedWebActivityClientWrappers;
import org.chromium.chrome.browser.customtabs.CustomTabActivityTestRule;
import org.chromium.chrome.browser.customtabs.CustomTabsIntentTestUtils;
import org.chromium.chrome.browser.dependency_injection.ChromeAppComponent;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.components.embedder_support.util.Origin;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.net.test.ServerCertificate;
import org.chromium.payments.mojom.ItemDetails;
import org.chromium.ui.test.util.UiDisableIf;
import org.chromium.url.GURL;

import java.util.concurrent.TimeoutException;

/** Tests for the Digital Goods flow. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({
    ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
    "enable-experimental-web-platform-features",
    "enable-blink-features=DigitalGoods"
})
public class DigitalGoodsTest {
    @Rule
    public CustomTabActivityTestRule mCustomTabActivityTestRule = new CustomTabActivityTestRule();

    private static final String TEST_PAGE = "/chrome/test/data/android/digital_goods.html";
    private static final String TWA_SERVICE_SCOPE = "https://www.example.com/notifications";

    private EmbeddedTestServer mTestServer;
    private String mTestPage;
    private TrustedWebActivityClient mClient;

    @Before
    public void setUp() throws TimeoutException {
        // Native needs to be initialized to start the test server.
        LibraryLoader.getInstance().ensureInitialized();

        ChromeAppComponent component = ChromeApplicationImpl.getComponent();
        component
                .resolvePermissionManager()
                .addDelegateApp(
                        Origin.createOrThrow(TWA_SERVICE_SCOPE),
                        "org.chromium.chrome.tests.support");
        mClient = component.resolveTrustedWebActivityClient();

        // TWAs only work with HTTPS.
        mTestServer =
                EmbeddedTestServer.createAndStartHTTPSServer(
                        InstrumentationRegistry.getInstrumentation().getContext(),
                        ServerCertificate.CERT_OK);
        mTestPage = mTestServer.getURL(TEST_PAGE);

        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(
                        ApplicationProvider.getApplicationContext(), mTestPage));
    }

    /**
     * Test that calling methods in JavaScript in the page gets correctly plumbed through to
     * DigitalGoodsFactoryImpl.
     */
    @Test
    @MediumTest
    @DisableIf.Device(type = {UiDisableIf.TABLET})
    public void javaImplConnected() throws TimeoutException {
        FakeDigitalGoods fake = new FakeDigitalGoods();
        DigitalGoodsFactoryImpl.setDigitalGoodsForTesting(fake);

        fake.addItem("id1", "Item 1", "desc", "GBP", "10");

        exec("populateDigitalGoodsService()");
        waitForNonNull("digitalGoodsService");
        exec("populateItemDetails(['id1'])");
        waitForNonNull("itemDetails");

        assertEquals("\"Item 1\"", exec("itemDetails[0].title"));
    }

    /**
     * Tests that plumbing between the {@link DigitalGoodsImpl} and the TrustedWebActivityService in
     * the TWA is working.
     */
    @Test
    @MediumTest
    public void twaServiceConnected() throws TimeoutException {
        DigitalGoodsImpl impl = createFixedDigitalGoods();

        setTwaServiceResponse(
                GetDetailsConverter.RESPONSE_COMMAND,
                GetDetailsConverter.createResponseBundle(
                        0,
                        GetDetailsConverter.createItemDetailsBundle(
                                "id1", "Item 1", "Desc 1", "GBP", "10")));

        CallbackHelper helper = new CallbackHelper();
        impl.getDetails(
                new String[] {"id1"},
                (int responseCode, ItemDetails[] details) -> {
                    assertEquals(0, responseCode);
                    assertEquals("id1", details[0].itemId);
                    assertEquals("Item 1", details[0].title);
                    assertEquals("Desc 1", details[0].description);
                    assertEquals("GBP", details[0].price.currency);
                    assertEquals("10", details[0].price.value);
                    helper.notifyCalled();
                });
        helper.waitForOnly();
    }

    private DigitalGoodsImpl createFixedDigitalGoods() {
        // There is one bit of production code that we need to change to make these tests work.
        // The page executing the JavaScript will be running in the EmbeddedTestServer and will
        // therefore have an origin like localhost:12345 which won't match the intent filter that
        // has been set up for the TestTrustedWebActivityService.

        // To work around this, we create our own DigitalGoodsImpl with a custom Delegate that
        // provides the URL we want to see.
        DigitalGoodsImpl.Delegate delegate = () -> new GURL(TWA_SERVICE_SCOPE);
        DigitalGoodsAdapter adapter = new DigitalGoodsAdapter(mClient);
        return new DigitalGoodsImpl(adapter, delegate);
    }

    private void setTwaServiceResponse(String name, Bundle args) throws TimeoutException {
        // TestTrustedWebActivityService doesn't currently depend on any Chromium code, so instead
        // of dragging in a lot of dependencies, we use this method to set what its response will
        // be. This also keeps the TestTrustedWebActivityService simpler.
        Bundle response = new Bundle();
        response.putString(SET_RESPONSE_NAME, name);
        response.putBundle(SET_RESPONSE_BUNDLE, args);

        final CallbackHelper helper = new CallbackHelper();

        mClient.connectAndExecute(
                Uri.parse(TWA_SERVICE_SCOPE),
                new TrustedWebActivityClient.ExecutionCallback() {
                    @Override
                    public void onConnected(
                            Origin origin, TrustedWebActivityClientWrappers.Connection service)
                            throws RemoteException {
                        service.sendExtraCommand(
                                COMMAND_SET_RESPONSE,
                                response,
                                new TrustedWebActivityCallback() {
                                    @Override
                                    public void onExtraCallback(
                                            @NonNull String callbackName, @Nullable Bundle args) {
                                        helper.notifyCalled();
                                    }
                                });
                    }

                    @Override
                    public void onNoTwaFound() {}
                });
        helper.waitForOnly();
    }

    private void waitForNonNull(String variable) {
        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    try {
                        Assert.assertNotEquals("null", exec(variable));
                    } catch (TimeoutException e) {
                        Assert.fail();
                    }
                });
    }

    private String exec(String command) throws TimeoutException {
        return mCustomTabActivityTestRule.runJavaScriptCodeInCurrentTab(command);
    }
}