chromium/chrome/browser/commerce/price_tracking/android/javatests/src/org/chromium/chrome/browser/price_tracking/CurrentTabPriceTrackingStateSupplierUnitTest.java

// Copyright 2024 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.price_tracking;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

import com.google.common.primitives.UnsignedLongs;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import org.chromium.base.Callback;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.commerce.PriceTrackingUtils;
import org.chromium.chrome.browser.commerce.PriceTrackingUtilsJni;
import org.chromium.chrome.browser.commerce.ShoppingServiceFactory;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.components.commerce.core.CommerceSubscription;
import org.chromium.components.commerce.core.ShoppingService;
import org.chromium.components.commerce.core.ShoppingService.ProductInfo;
import org.chromium.components.commerce.core.ShoppingService.ProductInfoCallback;
import org.chromium.components.commerce.core.SubscriptionsObserver;
import org.chromium.url.GURL;
import org.chromium.url.JUnitTestGURLs;

import java.util.Optional;

@RunWith(BaseRobolectricTestRunner.class)
/** Unit tests for {@link CurrentTabPriceTrackingStateSupplier} */
public class CurrentTabPriceTrackingStateSupplierUnitTest {

    @Rule public JniMocker mJniMocker = new JniMocker();

    private ObservableSupplierImpl<Tab> mTabSupplier;
    private ObservableSupplierImpl<Profile> mProfileSupplier;
    @Mock private Profile mMockProfile;
    @Mock private Tab mMockTab;
    @Mock private ShoppingService mMockShoppingService;
    @Mock PriceTrackingUtils.Natives mMockPriceTrackingUtilsJni;
    @Captor ArgumentCaptor<ProductInfoCallback> mProductInfoCallbackCaptor;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        mJniMocker.mock(PriceTrackingUtilsJni.TEST_HOOKS, mMockPriceTrackingUtilsJni);

        mTabSupplier = new ObservableSupplierImpl<>();
        mProfileSupplier = new ObservableSupplierImpl<>();

        ShoppingServiceFactory.setShoppingServiceForTesting(mMockShoppingService);
    }

    private ProductInfo createProductInfoWithId(long productId) {
        return new ProductInfo(
                "Product",
                GURL.emptyGURL(),
                Optional.of(productId),
                Optional.empty(),
                "USD",
                1000,
                "US",
                Optional.empty());
    }

    @Test
    public void testDestroyBeforeProfile() {
        CurrentTabPriceTrackingStateSupplier supplier =
                new CurrentTabPriceTrackingStateSupplier(mTabSupplier, mProfileSupplier);
        assertTrue(mProfileSupplier.hasObservers());

        supplier.destroy();
        // TODO(https://crbug.com/352082581): Enable this once observer is fixed.
        // assertFalse(mProfileSupplier.hasObservers());
        verifyNoInteractions(mMockShoppingService);
    }

    @Test
    public void testDestroyAfterProfile() {
        CurrentTabPriceTrackingStateSupplier supplier =
                new CurrentTabPriceTrackingStateSupplier(mTabSupplier, mProfileSupplier);
        assertTrue(mProfileSupplier.hasObservers());
        mProfileSupplier.set(mMockProfile);
        verify(mMockShoppingService).addSubscriptionsObserver(any());

        supplier.destroy();
        // TODO(https://crbug.com/352082581): Enable this once observer is fixed.
        // assertFalse(mProfileSupplier.hasObservers());
        verify(mMockShoppingService).removeSubscriptionsObserver(any());
    }

    @Test
    public void testWithEmptySuppliers() {
        Callback<Boolean> mockCallback = mock(Callback.class);

        var supplier = new CurrentTabPriceTrackingStateSupplier(mTabSupplier, mProfileSupplier);
        supplier.addObserver(mockCallback);

        verify(mockCallback, never()).onResult(anyBoolean());
        assertFalse(supplier.get());
    }

    @Test
    public void testWithTabWithoutProductInfo() {
        Callback<Boolean> mockCallback = mock(Callback.class);
        when(mMockTab.getUrl()).thenReturn(JUnitTestGURLs.GOOGLE_URL_CAT);

        var supplier = new CurrentTabPriceTrackingStateSupplier(mTabSupplier, mProfileSupplier);
        supplier.addObserver(mockCallback);

        mProfileSupplier.set(mMockProfile);
        mTabSupplier.set(mMockTab);

        verify(mMockShoppingService)
                .getProductInfoForUrl(
                        eq(JUnitTestGURLs.GOOGLE_URL_CAT), mProductInfoCallbackCaptor.capture());
        // Return no product info for the current tab.
        mProductInfoCallbackCaptor.getValue().onResult(JUnitTestGURLs.GOOGLE_URL_CAT, null);

        // Supplier shouldn't invoke the callback.
        verify(mockCallback, never()).onResult(anyBoolean());
        assertFalse(supplier.get());
    }

    @Test
    public void testWithTabWithProductInfo_untracked() {
        Callback<Boolean> mockCallback = mock(Callback.class);
        long productClusterId = 1234L;
        ShoppingService.ProductInfo productInfo = createProductInfoWithId(productClusterId);

        when(mMockTab.getUrl()).thenReturn(JUnitTestGURLs.GOOGLE_URL_CAT);
        // Set ShoppingService to return product info for the current tab.

        ArgumentCaptor<CommerceSubscription> commerceSubscriptionArgumentCaptor =
                ArgumentCaptor.forClass(CommerceSubscription.class);
        ArgumentCaptor<Callback<Boolean>> shoppingServiceCallbackCaptor =
                ArgumentCaptor.forClass(Callback.class);

        var supplier = new CurrentTabPriceTrackingStateSupplier(mTabSupplier, mProfileSupplier);
        supplier.addObserver(mockCallback);

        mProfileSupplier.set(mMockProfile);
        mTabSupplier.set(mMockTab);

        verify(mMockShoppingService)
                .getProductInfoForUrl(
                        eq(JUnitTestGURLs.GOOGLE_URL_CAT), mProductInfoCallbackCaptor.capture());
        // Return product info for the current tab.
        mProductInfoCallbackCaptor.getValue().onResult(JUnitTestGURLs.GOOGLE_URL_CAT, productInfo);

        // Ensure ShoppingService is called to check if the current product info is tracked.
        verify(mMockShoppingService)
                .isSubscribed(
                        commerceSubscriptionArgumentCaptor.capture(),
                        shoppingServiceCallbackCaptor.capture());
        // Ensure ShoppingService was called with the correct product ID.
        assertEquals(
                UnsignedLongs.toString(productClusterId),
                commerceSubscriptionArgumentCaptor.getValue().id);
        // Set ShoppingService to return false on the callback to isSubscribed.
        shoppingServiceCallbackCaptor.getValue().onResult(false);

        // Supplier shouldn't invoke the callback.
        verify(mockCallback, never()).onResult(anyBoolean());
        assertFalse(supplier.get());
    }

    @Test
    public void testWithTabWithProductInfo_tracked() {
        Callback<Boolean> mockCallback = mock(Callback.class);
        long productClusterId = 1234L;
        ShoppingService.ProductInfo productInfo = createProductInfoWithId(productClusterId);

        when(mMockTab.getUrl()).thenReturn(JUnitTestGURLs.GOOGLE_URL_CAT);

        ArgumentCaptor<Callback<Boolean>> shoppingServiceCallbackCaptor =
                ArgumentCaptor.forClass(Callback.class);

        var supplier = new CurrentTabPriceTrackingStateSupplier(mTabSupplier, mProfileSupplier);
        supplier.addObserver(mockCallback);

        mProfileSupplier.set(mMockProfile);
        mTabSupplier.set(mMockTab);

        verify(mMockShoppingService)
                .getProductInfoForUrl(
                        eq(JUnitTestGURLs.GOOGLE_URL_CAT), mProductInfoCallbackCaptor.capture());
        // Return product info for the current tab.
        mProductInfoCallbackCaptor.getValue().onResult(JUnitTestGURLs.GOOGLE_URL_CAT, productInfo);

        verify(mMockShoppingService).isSubscribed(any(), shoppingServiceCallbackCaptor.capture());
        // Set ShoppingService to return true on the callback to isSubscribed.
        shoppingServiceCallbackCaptor.getValue().onResult(true);

        // Supplier should invoke callback.
        verify(mockCallback).onResult(true);
        // Supplier value should now be true.
        assertTrue(supplier.get());
    }

    @Test
    public void testWithTabWithProductInfo_untrackedAndThenTracked() {
        Callback<Boolean> mockCallback = mock(Callback.class);
        long productClusterId = 1234L;
        ShoppingService.ProductInfo productInfo = createProductInfoWithId(productClusterId);

        when(mMockTab.getUrl()).thenReturn(JUnitTestGURLs.GOOGLE_URL_CAT);

        ArgumentCaptor<SubscriptionsObserver> subscriptionsObserverArgumentCaptor =
                ArgumentCaptor.forClass(SubscriptionsObserver.class);
        ArgumentCaptor<CommerceSubscription> commerceSubscriptionArgumentCaptor =
                ArgumentCaptor.forClass(CommerceSubscription.class);
        ArgumentCaptor<Callback<Boolean>> shoppingServiceCallbackCaptor =
                ArgumentCaptor.forClass(Callback.class);

        var supplier = new CurrentTabPriceTrackingStateSupplier(mTabSupplier, mProfileSupplier);
        supplier.addObserver(mockCallback);

        mProfileSupplier.set(mMockProfile);
        mTabSupplier.set(mMockTab);

        verify(mMockShoppingService)
                .getProductInfoForUrl(
                        eq(JUnitTestGURLs.GOOGLE_URL_CAT), mProductInfoCallbackCaptor.capture());
        // Return product info for the current tab.
        mProductInfoCallbackCaptor.getValue().onResult(JUnitTestGURLs.GOOGLE_URL_CAT, productInfo);

        verify(mMockShoppingService)
                .isSubscribed(
                        commerceSubscriptionArgumentCaptor.capture(),
                        shoppingServiceCallbackCaptor.capture());
        // Ensure the supplier is observing for subscription changes.
        verify(mMockShoppingService)
                .addSubscriptionsObserver(subscriptionsObserverArgumentCaptor.capture());
        // Set ShoppingService to return false on the callback to isSubscribed, indicating that the
        // product is not tracked when the tab loaded.
        shoppingServiceCallbackCaptor.getValue().onResult(false);
        // Invoke subscription change listener notifying that the product is now subscribed to.
        subscriptionsObserverArgumentCaptor
                .getValue()
                .onSubscribe(commerceSubscriptionArgumentCaptor.getValue(), true);

        // Supplier should invoke callback.
        verify(mockCallback).onResult(true);
        // Supplier value should now be true.
        assertTrue(supplier.get());
    }

    @Test
    public void testWithTabWithProductInfo_trackedAndThenUnTracked() {
        Callback<Boolean> mockCallback = mock(Callback.class);
        long productClusterId = 1234L;
        ShoppingService.ProductInfo productInfo = createProductInfoWithId(productClusterId);

        when(mMockTab.getUrl()).thenReturn(JUnitTestGURLs.GOOGLE_URL_CAT);

        ArgumentCaptor<SubscriptionsObserver> subscriptionsObserverArgumentCaptor =
                ArgumentCaptor.forClass(SubscriptionsObserver.class);
        ArgumentCaptor<CommerceSubscription> commerceSubscriptionArgumentCaptor =
                ArgumentCaptor.forClass(CommerceSubscription.class);
        ArgumentCaptor<Callback<Boolean>> shoppingServiceCallbackCaptor =
                ArgumentCaptor.forClass(Callback.class);

        var supplier = new CurrentTabPriceTrackingStateSupplier(mTabSupplier, mProfileSupplier);
        supplier.addObserver(mockCallback);

        mProfileSupplier.set(mMockProfile);
        mTabSupplier.set(mMockTab);

        verify(mMockShoppingService)
                .getProductInfoForUrl(
                        eq(JUnitTestGURLs.GOOGLE_URL_CAT), mProductInfoCallbackCaptor.capture());
        // Return product info for the current tab.
        mProductInfoCallbackCaptor.getValue().onResult(JUnitTestGURLs.GOOGLE_URL_CAT, productInfo);

        verify(mMockShoppingService)
                .isSubscribed(
                        commerceSubscriptionArgumentCaptor.capture(),
                        shoppingServiceCallbackCaptor.capture());
        verify(mMockShoppingService)
                .addSubscriptionsObserver(subscriptionsObserverArgumentCaptor.capture());
        // Set ShoppingService to return true on the callback to isSubscribed, indicating that the
        // product is tracked when the tab loaded.
        shoppingServiceCallbackCaptor.getValue().onResult(true);

        // Invoke subscription change listener notifying that the product is now unsubscribed to.
        subscriptionsObserverArgumentCaptor
                .getValue()
                .onUnsubscribe(commerceSubscriptionArgumentCaptor.getValue(), true);

        // Supplier callback should have been called twice, once on start indicating the product was
        // tracked then again indicating the product is no longer tracked.
        verify(mockCallback).onResult(true);
        verify(mockCallback).onResult(false);
        // Supplier value should now be false.
        assertFalse(supplier.get());
    }

    @Test
    public void testWithTabWithProductInfo_tabChangesWhileLoading() {
        Tab anotherTab = mock(Tab.class);
        Callback<Boolean> mockCallback = mock(Callback.class);
        long productClusterId = 1234L;
        ShoppingService.ProductInfo productInfo = createProductInfoWithId(productClusterId);

        when(mMockTab.getUrl()).thenReturn(JUnitTestGURLs.GOOGLE_URL_CAT);
        when(anotherTab.getUrl()).thenReturn(JUnitTestGURLs.GOOGLE_URL_DOG);

        ArgumentCaptor<Callback<Boolean>> shoppingServiceCallbackCaptor =
                ArgumentCaptor.forClass(Callback.class);

        var supplier = new CurrentTabPriceTrackingStateSupplier(mTabSupplier, mProfileSupplier);
        supplier.addObserver(mockCallback);

        mProfileSupplier.set(mMockProfile);
        mTabSupplier.set(mMockTab);

        verify(mMockShoppingService)
                .getProductInfoForUrl(
                        eq(JUnitTestGURLs.GOOGLE_URL_CAT), mProductInfoCallbackCaptor.capture());
        // Return product info for the current tab.
        mProductInfoCallbackCaptor.getValue().onResult(JUnitTestGURLs.GOOGLE_URL_CAT, productInfo);

        verify(mMockShoppingService).isSubscribed(any(), shoppingServiceCallbackCaptor.capture());

        // Change current tab.
        mTabSupplier.set(anotherTab);

        // Set ShoppingService to return true on the callback to isSubscribed.
        shoppingServiceCallbackCaptor.getValue().onResult(true);

        // Supplier shouldn't invoke callback, because the result of isSubscribed doesn't correspond
        // with the current tab.
        verify(mockCallback, never()).onResult(anyBoolean());
        assertFalse(supplier.get());
    }
}