// 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.payments;
import static org.mockito.AdditionalAnswers.answerVoid;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.MediumTest;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.chromium.IsReadyToPayService;
import org.chromium.IsReadyToPayServiceCallback;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.components.payments.intent.IsReadyToPayServiceHelper;
/** Tests for IsReadyToPayServiceHelper. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class IsReadyToPayServiceHelperTest {
@Rule
public final ChromeTabbedActivityTestRule mActivityTestRule =
new ChromeTabbedActivityTestRule();
@Rule public final ExpectedException mExpectedExceptionRule = ExpectedException.none();
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock private IBinder mBinderMock;
@Spy private IsReadyToPayService.Default mServiceSpy;
private boolean mErrorReceived;
private boolean mResponseReceived;
@Before
public void setUp() throws Throwable {
Looper.prepare();
}
@After
public void tearDown() throws Throwable {}
private interface ServiceCallbackHandler {
void handle(IsReadyToPayServiceCallback callback);
}
// Create a mock service that does not respond to the IsReadyToPay query.
private IBinder createUnresponsiveService() {
// Intentionally leave blank to be unresponsive.
return createService((serviceCallback) -> {});
}
// Create a mock service that always responds being ready.
private IBinder createAlwaysReadyService() {
return createService(
(serviceCallback) -> {
new Handler()
.post(
() -> {
try {
serviceCallback.handleIsReadyToPay(true);
} catch (Throwable e) {
Assert.fail(e.toString());
}
});
});
}
private IBinder createService(ServiceCallbackHandler serviceCallbackHandler) {
IBinder binder = Mockito.mock(IBinder.class);
Mockito.when(binder.queryLocalInterface(Mockito.any())).thenReturn(mServiceSpy);
try {
Mockito.doAnswer(
answerVoid(
(IsReadyToPayServiceCallback callback) ->
serviceCallbackHandler.handle(callback)))
.when(mServiceSpy)
.isReadyToPay(Mockito.any(IsReadyToPayServiceHelper.class));
} catch (Throwable e) {
Assert.fail(e.toString());
}
return binder;
}
/**
* Create a mock context that can run a specified service.
*
* @param serviceBinder The binder of a service to run. Null means no service.
*/
private Context createContext(IBinder serviceBinder) {
Context context = Mockito.mock(Context.class);
// Mock {@link Context#bindService}.
try {
Mockito.doAnswer(
(invocation) -> {
if (serviceBinder == null) return false;
ServiceConnection serviceConnection = invocation.getArgument(1);
new Handler()
.post(
() -> {
ComponentName mockComponentName = null;
try {
mockComponentName =
Mockito.any(ComponentName.class);
} catch (Throwable e) {
Assert.fail(e.toString());
}
serviceConnection.onServiceConnected(
mockComponentName, serviceBinder);
});
return true;
})
.when(context)
.bindService(
Mockito.any(Intent.class),
Mockito.any(ServiceConnection.class),
Mockito.anyInt());
} catch (Throwable e) {
Assert.fail(e.toString());
}
return context;
}
/** Create a mock context that never connects to a service. */
private Context createContextThatNeverConnectToService() {
Context context = Mockito.mock(Context.class);
// Mock {@link Context#bindService}.
try {
Mockito.doAnswer(
(invocation) -> {
ServiceConnection serviceConnection = invocation.getArgument(1);
new Handler()
.post(
() -> {
ComponentName mockComponentName = null;
try {
mockComponentName =
Mockito.any(ComponentName.class);
} catch (Throwable e) {
Assert.fail(e.toString());
}
});
return true;
})
.when(context)
.bindService(
Mockito.any(Intent.class),
Mockito.any(ServiceConnection.class),
Mockito.anyInt());
} catch (Throwable e) {
Assert.fail(e.toString());
}
return context;
}
@Test
@MediumTest
@Feature({"Payments"})
public void onErrorTest() throws Throwable {
mErrorReceived = false;
Intent intent = new Intent();
intent.setClassName("mock.package.name", "mock.service.name");
Context context = ApplicationProvider.getApplicationContext();
IsReadyToPayServiceHelper helper =
new IsReadyToPayServiceHelper(
context,
intent,
new IsReadyToPayServiceHelper.ResultHandler() {
@Override
public void onIsReadyToPayServiceResponse(boolean isReadyToPay) {
Assert.fail("IsReadyToPayService should not respond.");
}
@Override
public void onIsReadyToPayServiceError() {
mErrorReceived = true;
}
});
helper.query();
CriteriaHelper.pollInstrumentationThread(() -> mErrorReceived);
}
@Test
@MediumTest
@Feature({"Payments"})
public void onResponseTest() throws Throwable {
mResponseReceived = false;
ThreadUtils.runOnUiThreadBlocking(
() -> {
Intent intent = new Intent();
intent.setClassName("mock.package.name", "mock.service.name");
Context context = createContext(createAlwaysReadyService());
IsReadyToPayServiceHelper helper =
new IsReadyToPayServiceHelper(
context,
intent,
new IsReadyToPayServiceHelper.ResultHandler() {
@Override
public void onIsReadyToPayServiceResponse(
boolean isReadyToPay) {
mResponseReceived = true;
}
@Override
public void onIsReadyToPayServiceError() {
Assert.fail();
}
});
helper.query();
});
CriteriaHelper.pollInstrumentationThread(() -> mResponseReceived);
}
@Test
@MediumTest
@Feature({"Payments"})
public void unresponsiveServiceTest() throws Throwable {
mErrorReceived = false;
ThreadUtils.runOnUiThreadBlocking(
() -> {
Intent intent = new Intent();
intent.setClassName("mock.package.name", "mock.service.name");
Context context = createContext(createUnresponsiveService());
IsReadyToPayServiceHelper helper =
new IsReadyToPayServiceHelper(
context,
intent,
new IsReadyToPayServiceHelper.ResultHandler() {
@Override
public void onIsReadyToPayServiceResponse(
boolean isReadyToPay) {
Assert.fail();
}
@Override
public void onIsReadyToPayServiceError() {
mErrorReceived = true;
}
});
helper.query();
});
CriteriaHelper.pollInstrumentationThread(() -> mErrorReceived);
}
@Test
@MediumTest
@Feature({"Payments"})
public void noServiceTest() throws Throwable {
mErrorReceived = false;
ThreadUtils.runOnUiThreadBlocking(
() -> {
Intent intent = new Intent();
intent.setClassName("mock.package.name", "mock.service.name");
Context context = createContext(/* serviceBinder= */ null);
IsReadyToPayServiceHelper helper =
new IsReadyToPayServiceHelper(
context,
intent,
new IsReadyToPayServiceHelper.ResultHandler() {
@Override
public void onIsReadyToPayServiceResponse(
boolean isReadyToPay) {
Assert.fail();
}
@Override
public void onIsReadyToPayServiceError() {
mErrorReceived = true;
}
});
helper.query();
});
CriteriaHelper.pollInstrumentationThread(() -> mErrorReceived);
}
@Test
@MediumTest
@Feature({"Payments"})
public void serviceConnectionTimeoutTest() throws Throwable {
mErrorReceived = false;
ThreadUtils.runOnUiThreadBlocking(
() -> {
Intent intent = new Intent();
intent.setClassName("mock.package.name", "mock.service.name");
Context context = createContextThatNeverConnectToService();
IsReadyToPayServiceHelper helper =
new IsReadyToPayServiceHelper(
context,
intent,
new IsReadyToPayServiceHelper.ResultHandler() {
@Override
public void onIsReadyToPayServiceResponse(
boolean isReadyToPay) {
Assert.fail();
}
@Override
public void onIsReadyToPayServiceError() {
mErrorReceived = true;
}
});
helper.query();
});
// Assuming CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL >
// IsReadyToPayServiceHelper.SERVICE_CONNECTION_TIMEOUT_MS.
CriteriaHelper.pollInstrumentationThread(() -> mErrorReceived);
}
}