// Copyright 2015 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 androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.preference.TwoStatePreference;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.contrib.RecyclerViewActions;
import org.junit.Assert;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.chromium.base.IntentUtils;
import org.chromium.base.Promise;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.chrome.browser.autofill.AutofillTestHelper;
import org.chromium.chrome.browser.autofill.PersonalDataManager.CreditCard;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.signin.services.UnifiedConsentServiceBridge;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.util.browser.signin.AccountManagerTestRule;
import org.chromium.chrome.test.util.browser.signin.SigninTestRule;
import org.chromium.chrome.test.util.browser.signin.SigninTestUtil;
import org.chromium.chrome.test.util.browser.sync.SyncTestUtil;
import org.chromium.components.signin.base.CoreAccountInfo;
import org.chromium.components.signin.identitymanager.ConsentLevel;
import org.chromium.components.sync.SyncService;
import org.chromium.components.sync.protocol.AutofillWalletSpecifics;
import org.chromium.components.sync.protocol.EntitySpecifics;
import org.chromium.components.sync.protocol.SyncEntity;
import org.chromium.components.sync.protocol.WalletMaskedCreditCard;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
/**
* TestRule for common functionality between sync tests. TODO(crbug.com/40743432): Support batching
* tests with SyncTestRule.
*/
public class SyncTestRule extends ChromeTabbedActivityTestRule {
/** Simple activity that mimics a trusted vault key retrieval flow that succeeds immediately. */
public static class FakeKeyRetrievalActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setResult(RESULT_OK);
FakeTrustedVaultClientBackend.get().startPopulateKeys();
finish();
}
}
/**
* Simple activity that mimics a trusted vault degraded recoverability fix flow that succeeds
* immediately.
*/
public static class FakeRecoverabilityDegradedFixActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setResult(RESULT_OK);
FakeTrustedVaultClientBackend.get().setRecoverabilityDegraded(false);
finish();
}
}
/**
* A fake implementation of TrustedVaultClient.Backend. Allows to specify keys to be fetched.
* Keys aren't populated through fetchKeys() unless startPopulateKeys() is called.
* startPopulateKeys() is called by FakeKeyRetrievalActivity before its completion to mimic real
* TrustedVaultClient.Backend implementation.
*
* <p>Similarly, recoverability-degraded logic is implemented with a fake activity. Tests can
* choose to enter this state via invoking setRecoverabilityDegraded(true), and the state can be
* resolved with FakeRecoverabilityDegradedFixActivity.
*/
public static class FakeTrustedVaultClientBackend implements TrustedVaultClient.Backend {
private static FakeTrustedVaultClientBackend sInstance;
private boolean mPopulateKeys;
private boolean mRecoverabilityDegraded;
private @Nullable List<byte[]> mKeys;
public FakeTrustedVaultClientBackend() {
mPopulateKeys = false;
mRecoverabilityDegraded = false;
}
public static FakeTrustedVaultClientBackend get() {
if (sInstance == null) {
sInstance = new FakeTrustedVaultClientBackend();
}
return sInstance;
}
@Override
public Promise<List<byte[]>> fetchKeys(CoreAccountInfo accountInfo) {
if (mKeys == null || !mPopulateKeys) {
return Promise.rejected();
}
return Promise.fulfilled(mKeys);
}
@Override
public Promise<PendingIntent> createKeyRetrievalIntent(CoreAccountInfo accountInfo) {
Context context = ApplicationProvider.getApplicationContext();
Intent intent = new Intent(context, FakeKeyRetrievalActivity.class);
return Promise.fulfilled(
PendingIntent.getActivity(
context,
/* requestCode= */ 0,
intent,
IntentUtils.getPendingIntentMutabilityFlag(false)));
}
@Override
public Promise<Boolean> markLocalKeysAsStale(CoreAccountInfo accountInfo) {
return Promise.rejected();
}
@Override
public Promise<Boolean> getIsRecoverabilityDegraded(CoreAccountInfo accountInfo) {
return Promise.fulfilled(mRecoverabilityDegraded);
}
@Override
public Promise<Void> addTrustedRecoveryMethod(
CoreAccountInfo accountInfo, byte[] publicKey, int methodTypeHint) {
return Promise.fulfilled(null);
}
@Override
public Promise<PendingIntent> createRecoverabilityDegradedIntent(
CoreAccountInfo accountInfo) {
Context context = ApplicationProvider.getApplicationContext();
Intent intent = new Intent(context, FakeRecoverabilityDegradedFixActivity.class);
return Promise.fulfilled(
PendingIntent.getActivity(
context,
/* requestCode= */ 0,
intent,
IntentUtils.getPendingIntentMutabilityFlag(false)));
}
@Override
public Promise<PendingIntent> createOptInIntent(CoreAccountInfo accountInfo) {
return Promise.rejected();
}
public void setKeys(List<byte[]> keys) {
mKeys = Collections.unmodifiableList(keys);
}
public void startPopulateKeys() {
mPopulateKeys = true;
}
public void setRecoverabilityDegraded(boolean degraded) {
mRecoverabilityDegraded = degraded;
}
}
private FakeServerHelper mFakeServerHelper;
private SyncService mSyncService;
private final SigninTestRule mSigninTestRule = new SigninTestRule();
public SigninTestRule getSigninTestRule() {
return mSigninTestRule;
}
public SyncTestRule() {}
/** Getters for Test variables */
public Context getTargetContext() {
return ApplicationProvider.getApplicationContext();
}
public FakeServerHelper getFakeServerHelper() {
return mFakeServerHelper;
}
public SyncService getSyncService() {
return mSyncService;
}
public void startMainActivityForSyncTest() {
// Start the activity by opening about:blank. This URL is ideal because it is not synced as
// a typed URL. If another URL is used, it could interfere with test data.
startMainActivityOnBlankPage();
}
/**
* Adds an account of default account name to AccountManagerFacade and waits for the seeding.
*/
public CoreAccountInfo addTestAccount() {
return addAccount(AccountManagerTestRule.TEST_ACCOUNT_EMAIL);
}
/** Adds an account of given account name to AccountManagerFacade and waits for the seeding. */
public CoreAccountInfo addAccount(String accountName) {
CoreAccountInfo coreAccountInfo = mSigninTestRule.addAccountAndWaitForSeeding(accountName);
Assert.assertFalse(SyncTestUtil.isSyncFeatureEnabled());
return coreAccountInfo;
}
/**
* @return The primary account of the requested {@link ConsentLevel}.
*/
public CoreAccountInfo getPrimaryAccount(@ConsentLevel int consentLevel) {
return mSigninTestRule.getPrimaryAccount(consentLevel);
}
/**
* Set up a test account, sign in and enable sync. FirstSetupComplete bit will be set after
* this. For most purposes this function should be used as this emulates the basic sign in flow.
*
* @return the test account that is signed in.
*/
public CoreAccountInfo setUpAccountAndEnableSyncForTesting() {
return setUpAccountAndEnableSyncForTesting(false);
}
/**
* Set up a child test account, sign in and enable sync. FirstSetupComplete bit will be set
* after this. For most purposes this function should be used as this emulates the basic sign in
* flow.
*
* @return the test account that is signed in.
*/
public CoreAccountInfo setUpChildAccountAndEnableSyncForTesting() {
return setUpAccountAndEnableSyncForTesting(true);
}
/**
* Set up a test account and sign in. Does not setup sync.
*
* @return the test accountInfo that is signed in.
*/
public CoreAccountInfo setUpAccountAndSignInForTesting() {
return mSigninTestRule.addTestAccountThenSignin();
}
/**
* Set up a test account, sign in but don't mark sync setup complete.
*
* @return the test account that is signed in.
*/
public CoreAccountInfo setUpTestAccountAndSignInWithSyncSetupAsIncomplete() {
CoreAccountInfo accountInfo =
mSigninTestRule.addTestAccountThenSigninAndEnableSync(/* syncService= */ null);
SyncTestUtil.waitForSyncTransportActive();
return accountInfo;
}
public void signinAndEnableSync(final CoreAccountInfo accountInfo) {
SigninTestUtil.signinAndEnableSync(accountInfo, mSyncService);
// Enable UKM when enabling sync as it is done by the sync confirmation UI.
enableUKM();
SyncTestUtil.waitForSyncFeatureActive();
SyncTestUtil.triggerSyncAndWaitForCompletion();
}
public void signOut() {
mSigninTestRule.signOut();
Assert.assertNull(mSigninTestRule.getPrimaryAccount(ConsentLevel.SYNC));
Assert.assertFalse(SyncTestUtil.isSyncFeatureEnabled());
}
public void clearServerData() {
mFakeServerHelper.clearServerData();
// SyncTestRule doesn't currently exercise invalidations, as opposed to
// C++ sync integration tests (based on SyncTest) which use
// FakeServerInvalidationSender to mimic invalidations. Hence, it is
// necessary to invoke triggerSync() explicitly, just like many Java
// tests do.
SyncTestUtil.triggerSync();
CriteriaHelper.pollUiThread(
() -> {
return !SyncTestUtil.getSyncServiceForLastUsedProfile().isSyncFeatureEnabled();
},
SyncTestUtil.TIMEOUT_MS,
SyncTestUtil.INTERVAL_MS);
}
/*
* Enables the Sync data type in USER_SELECTABLE_TYPES.
*/
public void enableDataType(final int userSelectableType) {
ThreadUtils.runOnUiThreadBlocking(
() -> {
Set<Integer> chosenTypes = mSyncService.getSelectedTypes();
chosenTypes.add(userSelectableType);
mSyncService.setSelectedTypes(false, chosenTypes);
});
}
/*
* Enables the |selectedTypes| in USER_SELECTABLE_TYPES.
*/
public void setSelectedTypes(boolean syncEverything, Set<Integer> selectedTypes) {
ThreadUtils.runOnUiThreadBlocking(
() -> {
mSyncService.setSelectedTypes(syncEverything, selectedTypes);
});
}
/*
* Disables the Sync data type in USER_SELECTABLE_TYPES.
*/
public void disableDataType(final int userSelectableType) {
ThreadUtils.runOnUiThreadBlocking(
() -> {
Set<Integer> chosenTypes = mSyncService.getSelectedTypes();
chosenTypes.remove(userSelectableType);
mSyncService.setSelectedTypes(false, chosenTypes);
});
}
public void pollInstrumentationThread(Runnable criteria) {
CriteriaHelper.pollInstrumentationThread(
criteria, SyncTestUtil.TIMEOUT_MS, SyncTestUtil.INTERVAL_MS);
}
public void pollInstrumentationThread(Callable<Boolean> criteria, String reason) {
CriteriaHelper.pollInstrumentationThread(
criteria, reason, SyncTestUtil.TIMEOUT_MS, SyncTestUtil.INTERVAL_MS);
}
@Override
public Statement apply(final Statement base, final Description desc) {
final Statement superStatement = super.apply(base, desc);
return mSigninTestRule.apply(superStatement, desc);
}
@Override
protected void before() throws Throwable {
super.before();
TrustedVaultClient.setInstanceForTesting(
new TrustedVaultClient(FakeTrustedVaultClientBackend.get()));
startMainActivityForSyncTest();
ThreadUtils.runOnUiThreadBlocking(
() -> {
SyncService syncService = createSyncServiceImpl();
if (syncService != null) {
SyncServiceFactory.setInstanceForTesting(syncService);
}
mSyncService = SyncTestUtil.getSyncServiceForLastUsedProfile();
mFakeServerHelper = FakeServerHelper.createInstanceAndGet();
});
}
@Override
protected void after() {
super.after();
ThreadUtils.runOnUiThreadBlocking(
() -> {
mSyncService = null;
mFakeServerHelper = null;
FakeServerHelper.destroyInstance();
});
}
/*
* Adds a credit card to server for autofill.
*/
public void addServerAutofillCreditCard() {
final String serverId = "025eb937c022489eb8dc78cbaa969218";
WalletMaskedCreditCard card =
WalletMaskedCreditCard.newBuilder()
.setId(serverId)
.setStatus(WalletMaskedCreditCard.WalletCardStatus.VALID)
.setNameOnCard("Jon Doe")
.setType(WalletMaskedCreditCard.WalletCardType.UNKNOWN)
.setLastFour("1111")
.setExpMonth(11)
.setExpYear(2020)
.build();
AutofillWalletSpecifics wallet_specifics =
AutofillWalletSpecifics.newBuilder()
.setType(AutofillWalletSpecifics.WalletInfoType.MASKED_CREDIT_CARD)
.setMaskedCard(card)
.build();
EntitySpecifics specifics =
EntitySpecifics.newBuilder().setAutofillWallet(wallet_specifics).build();
SyncEntity entity =
SyncEntity.newBuilder()
.setName(serverId)
.setIdString(serverId)
.setSpecifics(specifics)
.build();
getFakeServerHelper().setWalletData(entity);
SyncTestUtil.triggerSyncAndWaitForCompletion();
}
/*
* Checks if server has any credit card information to autofill.
*/
public boolean hasServerAutofillCreditCards() {
return ThreadUtils.runOnUiThreadBlocking(
() -> {
List<CreditCard> cards =
AutofillTestHelper.getPersonalDataManagerForLastUsedProfile()
.getCreditCardsForSettings();
for (int i = 0; i < cards.size(); i++) {
if (!cards.get(i).getIsLocal()) return true;
}
return false;
});
}
// UI interaction convenience methods.
public void togglePreference(final TwoStatePreference pref) {
onView(withId(R.id.recycler_view))
.perform(
RecyclerViewActions.actionOnItem(
hasDescendant(withText(pref.getTitle().toString())), click()));
}
/** Returns an instance of SyncService that can be overridden by subclasses. */
protected SyncService createSyncServiceImpl() {
return null;
}
private static void enableUKM() {
ThreadUtils.runOnUiThreadBlocking(
() -> {
// Outside of tests, URL-keyed anonymized data collection is enabled by sign-in
// UI.
UnifiedConsentServiceBridge.setUrlKeyedAnonymizedDataCollectionEnabled(
ProfileManager.getLastUsedRegularProfile(), true);
});
}
private CoreAccountInfo setUpAccountAndEnableSyncForTesting(boolean isChildAccount) {
CoreAccountInfo accountInfo =
mSigninTestRule.addTestAccountThenSigninAndEnableSync(mSyncService, isChildAccount);
// Enable UKM when enabling sync as it is done by the sync confirmation UI.
enableUKM();
SyncTestUtil.waitForSyncFeatureActive();
SyncTestUtil.triggerSyncAndWaitForCompletion();
return accountInfo;
}
}