chromium/chrome/android/junit/src/org/chromium/chrome/browser/sync/SyncErrorNotifierTest.java

// Copyright 2023 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.sync;

import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.os.Bundle;

import androidx.test.filters.SmallTest;

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.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.ContextUtils;
import org.chromium.base.Promise;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.R;
import org.chromium.components.browser_ui.notifications.NotificationManagerProxy;
import org.chromium.components.browser_ui.notifications.NotificationWrapper;
import org.chromium.components.signin.base.CoreAccountInfo;
import org.chromium.components.sync.SyncService;

/** Unit tests for {@link SyncErrorNotifier}. */
@RunWith(BaseRobolectricTestRunner.class)
public class SyncErrorNotifierTest {
    @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule();

    private final Context mContext = ContextUtils.getApplicationContext();

    @Mock private NotificationManagerProxy mNotificationManagerProxy;
    @Mock private SyncService mSyncService;
    @Mock private TrustedVaultClient mTrustedVaultClient;

    @Captor private ArgumentCaptor<NotificationWrapper> mNotificationWrapperCaptor;

    @Test
    @SmallTest
    public void testNoNotification() {
        when(mSyncService.getAccountInfo()).thenReturn(null);
        when(mSyncService.isSyncFeatureEnabled()).thenReturn(false);
        when(mSyncService.isEngineInitialized()).thenReturn(false);
        when(mSyncService.isEncryptEverythingEnabled()).thenReturn(false);
        when(mSyncService.isPassphraseRequiredForPreferredDataTypes()).thenReturn(false);
        when(mSyncService.isPassphrasePromptMutedForCurrentProductVersion()).thenReturn(false);
        when(mSyncService.isTrustedVaultKeyRequiredForPreferredDataTypes()).thenReturn(false);

        SyncErrorNotifier notifier =
                new SyncErrorNotifier(mNotificationManagerProxy, mSyncService, mTrustedVaultClient);
        notifier.syncStateChanged();

        verify(mNotificationManagerProxy, Mockito.times(0)).notify(any());
        verify(mNotificationManagerProxy, Mockito.times(0)).cancel(anyInt());
    }

    @Test
    @SmallTest
    public void testPassphraseNotification() {
        when(mSyncService.getAccountInfo())
                .thenReturn(CoreAccountInfo.createFromEmailAndGaiaId("[email protected]", "gaiaId"));
        when(mSyncService.isSyncFeatureEnabled()).thenReturn(true);
        when(mSyncService.isEngineInitialized()).thenReturn(true);
        when(mSyncService.isEncryptEverythingEnabled()).thenReturn(true);
        when(mSyncService.isPassphraseRequiredForPreferredDataTypes()).thenReturn(true);
        when(mSyncService.isPassphrasePromptMutedForCurrentProductVersion()).thenReturn(false);
        when(mSyncService.isTrustedVaultKeyRequiredForPreferredDataTypes()).thenReturn(false);

        SyncErrorNotifier notifier =
                new SyncErrorNotifier(mNotificationManagerProxy, mSyncService, mTrustedVaultClient);
        notifier.syncStateChanged();

        verify(mSyncService).markPassphrasePromptMutedForCurrentProductVersion();
        verify(mNotificationManagerProxy).notify(mNotificationWrapperCaptor.capture());
        Bundle notificationExtras = mNotificationWrapperCaptor.getValue().getNotification().extras;
        assertEquals(
                notificationExtras.getCharSequence(Notification.EXTRA_TITLE),
                mContext.getString(R.string.sync_error_card_title));
        assertEquals(
                notificationExtras.getCharSequence(Notification.EXTRA_TEXT),
                mContext.getString(R.string.hint_passphrase_required));

        // Spurious syncStateChanged()...
        notifier.syncStateChanged();

        // ...must cause no additional notify() calls.
        verify(mNotificationManagerProxy).notify(any());
        verify(mNotificationManagerProxy, Mockito.times(0)).cancel(anyInt());

        // Resolve the error.
        when(mSyncService.isPassphraseRequiredForPreferredDataTypes()).thenReturn(false);
        notifier.syncStateChanged();

        // Notification must be cleared.
        verify(mNotificationManagerProxy).cancel(anyInt());
    }

    @Test
    @SmallTest
    public void testPassphraseNotificationMuted() {
        when(mSyncService.getAccountInfo())
                .thenReturn(CoreAccountInfo.createFromEmailAndGaiaId("[email protected]", "gaiaId"));
        when(mSyncService.isSyncFeatureEnabled()).thenReturn(true);
        when(mSyncService.isEngineInitialized()).thenReturn(true);
        when(mSyncService.isEncryptEverythingEnabled()).thenReturn(true);
        when(mSyncService.isPassphraseRequiredForPreferredDataTypes()).thenReturn(true);
        when(mSyncService.isPassphrasePromptMutedForCurrentProductVersion()).thenReturn(true);
        when(mSyncService.isTrustedVaultKeyRequiredForPreferredDataTypes()).thenReturn(false);

        SyncErrorNotifier notifier =
                new SyncErrorNotifier(mNotificationManagerProxy, mSyncService, mTrustedVaultClient);
        notifier.syncStateChanged();

        verify(mNotificationManagerProxy, Mockito.times(0)).cancel(anyInt());
        verify(mNotificationManagerProxy, Mockito.times(0)).notify(any());
    }

    @Test
    @SmallTest
    public void testTrustedVaultNotificationForPasswords() {
        when(mSyncService.getAccountInfo())
                .thenReturn(CoreAccountInfo.createFromEmailAndGaiaId("[email protected]", "gaiaId"));
        when(mSyncService.isSyncFeatureEnabled()).thenReturn(true);
        when(mSyncService.isEngineInitialized()).thenReturn(true);
        when(mSyncService.isEncryptEverythingEnabled()).thenReturn(false);
        when(mSyncService.isPassphraseRequiredForPreferredDataTypes()).thenReturn(false);
        when(mSyncService.isPassphrasePromptMutedForCurrentProductVersion()).thenReturn(false);
        when(mSyncService.isTrustedVaultKeyRequiredForPreferredDataTypes()).thenReturn(true);
        Promise<PendingIntent> intentPromise = new Promise<>();
        when(mTrustedVaultClient.createKeyRetrievalIntent(any())).thenReturn(intentPromise);

        SyncErrorNotifier notifier =
                new SyncErrorNotifier(mNotificationManagerProxy, mSyncService, mTrustedVaultClient);
        notifier.syncStateChanged();

        // Client started creating the intent but hasn't finished yet, so no notification.
        verify(mTrustedVaultClient).createKeyRetrievalIntent(any());
        verify(mNotificationManagerProxy, Mockito.times(0)).notify(any());

        notifier.syncStateChanged();

        // New calls to createKeyRetrievalIntent() must be suppressed because the first one is still
        // in flight. No notification yet.
        verify(mTrustedVaultClient).createKeyRetrievalIntent(any());
        verify(mNotificationManagerProxy, Mockito.times(0)).notify(any());

        // Return the intent (can be null as it's unused by the test).
        intentPromise.fulfill(null);
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        // Notification must be shown now.
        verify(mNotificationManagerProxy).notify(mNotificationWrapperCaptor.capture());
        Bundle notificationExtras = mNotificationWrapperCaptor.getValue().getNotification().extras;
        assertEquals(
                notificationExtras.getCharSequence(Notification.EXTRA_TITLE),
                mContext.getString(R.string.password_sync_error_summary));
        assertEquals(
                notificationExtras.getCharSequence(Notification.EXTRA_TEXT),
                mContext.getString(R.string.hint_sync_retrieve_keys_for_passwords));

        // Spurious syncStateChanged()...
        notifier.syncStateChanged();

        // ...must be a no-op, i.e. no additional notify() / createKeyRetrievalIntent() calls.
        verify(mNotificationManagerProxy).notify(any());
        verify(mTrustedVaultClient).createKeyRetrievalIntent(any());
        verify(mNotificationManagerProxy, Mockito.times(0)).cancel(anyInt());

        // Resolve the error.
        when(mSyncService.isTrustedVaultKeyRequiredForPreferredDataTypes()).thenReturn(false);
        notifier.syncStateChanged();

        // Notification must be cleared.
        verify(mNotificationManagerProxy).cancel(anyInt());
    }

    @Test
    @SmallTest
    public void testTrustedVaultNotificationForEverything() {
        when(mSyncService.getAccountInfo())
                .thenReturn(CoreAccountInfo.createFromEmailAndGaiaId("[email protected]", "gaiaId"));
        when(mSyncService.isSyncFeatureEnabled()).thenReturn(true);
        when(mSyncService.isEngineInitialized()).thenReturn(true);
        when(mSyncService.isEncryptEverythingEnabled()).thenReturn(true);
        when(mSyncService.isPassphraseRequiredForPreferredDataTypes()).thenReturn(false);
        when(mSyncService.isPassphrasePromptMutedForCurrentProductVersion()).thenReturn(false);
        when(mSyncService.isTrustedVaultKeyRequiredForPreferredDataTypes()).thenReturn(true);
        when(mTrustedVaultClient.createKeyRetrievalIntent(any()))
                .thenReturn(Promise.fulfilled(null));

        SyncErrorNotifier notifier =
                new SyncErrorNotifier(mNotificationManagerProxy, mSyncService, mTrustedVaultClient);
        notifier.syncStateChanged();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        // Strings must be different from testTrustedVaultNotificationForPasswords()
        verify(mNotificationManagerProxy, Mockito.times(0)).cancel(anyInt());
        verify(mNotificationManagerProxy).notify(mNotificationWrapperCaptor.capture());
        Bundle notificationExtras = mNotificationWrapperCaptor.getValue().getNotification().extras;
        assertEquals(
                notificationExtras.getCharSequence(Notification.EXTRA_TITLE),
                mContext.getString(R.string.sync_error_card_title));
        assertEquals(
                notificationExtras.getCharSequence(Notification.EXTRA_TEXT),
                mContext.getString(R.string.hint_sync_retrieve_keys_for_everything));
    }

    @Test
    @SmallTest
    public void testTrustedVaultIntentCreationFails() {
        when(mSyncService.getAccountInfo())
                .thenReturn(CoreAccountInfo.createFromEmailAndGaiaId("[email protected]", "gaiaId"));
        when(mSyncService.isSyncFeatureEnabled()).thenReturn(true);
        when(mSyncService.isEngineInitialized()).thenReturn(true);
        when(mSyncService.isEncryptEverythingEnabled()).thenReturn(true);
        when(mSyncService.isPassphraseRequiredForPreferredDataTypes()).thenReturn(false);
        when(mSyncService.isPassphrasePromptMutedForCurrentProductVersion()).thenReturn(false);
        when(mSyncService.isTrustedVaultKeyRequiredForPreferredDataTypes()).thenReturn(true);
        when(mTrustedVaultClient.createKeyRetrievalIntent(any())).thenReturn(Promise.rejected());

        SyncErrorNotifier notifier =
                new SyncErrorNotifier(mNotificationManagerProxy, mSyncService, mTrustedVaultClient);
        notifier.syncStateChanged();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        // There must've been no notify() calls - because the intent creation failed - and no
        // cancel() calls either - because there were no ongoing notifications to cancel.
        verify(mNotificationManagerProxy, Mockito.times(0)).notify(any());
        verify(mNotificationManagerProxy, Mockito.times(0)).cancel(anyInt());
    }

    @Test
    @SmallTest
    public void testPassphraseNotificationForSignedInUsers() {
        when(mSyncService.getAccountInfo())
                .thenReturn(CoreAccountInfo.createFromEmailAndGaiaId("[email protected]", "gaiaId"));
        when(mSyncService.isSyncFeatureEnabled()).thenReturn(false);
        when(mSyncService.isEngineInitialized()).thenReturn(true);
        when(mSyncService.isEncryptEverythingEnabled()).thenReturn(true);
        when(mSyncService.isPassphraseRequiredForPreferredDataTypes()).thenReturn(true);
        when(mSyncService.isPassphrasePromptMutedForCurrentProductVersion()).thenReturn(false);
        when(mSyncService.isTrustedVaultKeyRequiredForPreferredDataTypes()).thenReturn(false);

        SyncErrorNotifier notifier =
                new SyncErrorNotifier(mNotificationManagerProxy, mSyncService, mTrustedVaultClient);
        notifier.syncStateChanged();

        verify(mSyncService).markPassphrasePromptMutedForCurrentProductVersion();
        verify(mNotificationManagerProxy).notify(mNotificationWrapperCaptor.capture());
        Bundle notificationExtras = mNotificationWrapperCaptor.getValue().getNotification().extras;
        assertEquals(
                notificationExtras.getCharSequence(Notification.EXTRA_TITLE),
                mContext.getString(R.string.identity_error_message_title_passphrase_required));
        assertEquals(
                notificationExtras.getCharSequence(Notification.EXTRA_TEXT),
                mContext.getString(R.string.identity_error_message_body));

        // Spurious syncStateChanged()...
        notifier.syncStateChanged();

        // ...must cause no additional notify() calls.
        verify(mNotificationManagerProxy).notify(any());
        verify(mNotificationManagerProxy, Mockito.times(0)).cancel(anyInt());

        // Resolve the error.
        when(mSyncService.isPassphraseRequiredForPreferredDataTypes()).thenReturn(false);
        notifier.syncStateChanged();

        // Notification must be cleared.
        verify(mNotificationManagerProxy).cancel(anyInt());
    }

    @Test
    @SmallTest
    public void testTrustedVaultNotificationForPasswordsForSignedInUsers() {
        when(mSyncService.getAccountInfo())
                .thenReturn(CoreAccountInfo.createFromEmailAndGaiaId("[email protected]", "gaiaId"));
        when(mSyncService.isSyncFeatureEnabled()).thenReturn(false);
        when(mSyncService.isEngineInitialized()).thenReturn(true);
        when(mSyncService.isEncryptEverythingEnabled()).thenReturn(false);
        when(mSyncService.isPassphraseRequiredForPreferredDataTypes()).thenReturn(false);
        when(mSyncService.isPassphrasePromptMutedForCurrentProductVersion()).thenReturn(false);
        when(mSyncService.isTrustedVaultKeyRequiredForPreferredDataTypes()).thenReturn(true);
        Promise<PendingIntent> intentPromise = new Promise<>();
        when(mTrustedVaultClient.createKeyRetrievalIntent(any())).thenReturn(intentPromise);

        SyncErrorNotifier notifier =
                new SyncErrorNotifier(mNotificationManagerProxy, mSyncService, mTrustedVaultClient);
        notifier.syncStateChanged();

        // Client started creating the intent but hasn't finished yet, so no notification.
        verify(mTrustedVaultClient).createKeyRetrievalIntent(any());
        verify(mNotificationManagerProxy, Mockito.times(0)).notify(any());

        notifier.syncStateChanged();

        // New calls to createKeyRetrievalIntent() must be suppressed because the first one is still
        // in flight. No notification yet.
        verify(mTrustedVaultClient).createKeyRetrievalIntent(any());
        verify(mNotificationManagerProxy, Mockito.times(0)).notify(any());

        // Return the intent (can be null as it's unused by the test).
        intentPromise.fulfill(null);
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        // Notification must be shown now.
        verify(mNotificationManagerProxy).notify(mNotificationWrapperCaptor.capture());
        Bundle notificationExtras = mNotificationWrapperCaptor.getValue().getNotification().extras;
        assertEquals(
                notificationExtras.getCharSequence(Notification.EXTRA_TITLE),
                mContext.getString(R.string.identity_error_card_button_verify));
        assertEquals(
                notificationExtras.getCharSequence(Notification.EXTRA_TEXT),
                mContext.getString(
                        R.string.identity_error_message_body_sync_retrieve_keys_for_passwords));

        // Spurious syncStateChanged()...
        notifier.syncStateChanged();

        // ...must be a no-op, i.e. no additional notify() / createKeyRetrievalIntent() calls.
        verify(mNotificationManagerProxy).notify(any());
        verify(mTrustedVaultClient).createKeyRetrievalIntent(any());
        verify(mNotificationManagerProxy, Mockito.times(0)).cancel(anyInt());

        // Resolve the error.
        when(mSyncService.isTrustedVaultKeyRequiredForPreferredDataTypes()).thenReturn(false);
        notifier.syncStateChanged();

        // Notification must be cleared.
        verify(mNotificationManagerProxy).cancel(anyInt());
    }

    @Test
    @SmallTest
    public void testTrustedVaultNotificationForEverythingForSignedInUsers() {
        when(mSyncService.getAccountInfo())
                .thenReturn(CoreAccountInfo.createFromEmailAndGaiaId("[email protected]", "gaiaId"));
        when(mSyncService.isSyncFeatureEnabled()).thenReturn(false);
        when(mSyncService.isEngineInitialized()).thenReturn(true);
        when(mSyncService.isEncryptEverythingEnabled()).thenReturn(true);
        when(mSyncService.isPassphraseRequiredForPreferredDataTypes()).thenReturn(false);
        when(mSyncService.isPassphrasePromptMutedForCurrentProductVersion()).thenReturn(false);
        when(mSyncService.isTrustedVaultKeyRequiredForPreferredDataTypes()).thenReturn(true);
        when(mTrustedVaultClient.createKeyRetrievalIntent(any()))
                .thenReturn(Promise.fulfilled(null));

        SyncErrorNotifier notifier =
                new SyncErrorNotifier(mNotificationManagerProxy, mSyncService, mTrustedVaultClient);
        notifier.syncStateChanged();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        // Strings must be different from testTrustedVaultNotificationForPasswords().
        verify(mNotificationManagerProxy, Mockito.times(0)).cancel(anyInt());
        verify(mNotificationManagerProxy).notify(mNotificationWrapperCaptor.capture());
        Bundle notificationExtras = mNotificationWrapperCaptor.getValue().getNotification().extras;
        assertEquals(
                notificationExtras.getCharSequence(Notification.EXTRA_TITLE),
                mContext.getString(R.string.identity_error_card_button_verify));
        assertEquals(
                notificationExtras.getCharSequence(Notification.EXTRA_TEXT),
                mContext.getString(R.string.identity_error_message_body));
    }
}