mirror of https://github.com/grpc/grpc-java.git
Simplify RobolectricBinderSecurityTest (#12058)
- Use INSTRUMENTATION_TEST @LooperMode to avoid custom Executors and idleLoopers() toil. - No android.app.Service is actually needed with Robolectric.
This commit is contained in:
parent
80cc988b3c
commit
64fe061ccd
|
@ -22,11 +22,6 @@ import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
|
||||||
import static org.robolectric.Shadows.shadowOf;
|
import static org.robolectric.Shadows.shadowOf;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.Looper;
|
|
||||||
import androidx.lifecycle.LifecycleService;
|
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import com.google.common.util.concurrent.Futures;
|
import com.google.common.util.concurrent.Futures;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
@ -42,90 +37,117 @@ import io.grpc.ServerMethodDefinition;
|
||||||
import io.grpc.ServerServiceDefinition;
|
import io.grpc.ServerServiceDefinition;
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
import io.grpc.StatusRuntimeException;
|
import io.grpc.StatusRuntimeException;
|
||||||
import io.grpc.binder.internal.MainThreadScheduledExecutorService;
|
|
||||||
import io.grpc.protobuf.lite.ProtoLiteUtils;
|
import io.grpc.protobuf.lite.ProtoLiteUtils;
|
||||||
import io.grpc.stub.ClientCalls;
|
import io.grpc.stub.ClientCalls;
|
||||||
import io.grpc.stub.ServerCalls;
|
import io.grpc.stub.ServerCalls;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.concurrent.ArrayBlockingQueue;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.robolectric.Robolectric;
|
|
||||||
import org.robolectric.RobolectricTestRunner;
|
import org.robolectric.RobolectricTestRunner;
|
||||||
import org.robolectric.android.controller.ServiceController;
|
import org.robolectric.annotation.LooperMode;
|
||||||
|
import org.robolectric.annotation.LooperMode.Mode;
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
@LooperMode(Mode.INSTRUMENTATION_TEST)
|
||||||
public final class RobolectricBinderSecurityTest {
|
public final class RobolectricBinderSecurityTest {
|
||||||
|
|
||||||
private static final String SERVICE_NAME = "fake_service";
|
private static final String SERVICE_NAME = "fake_service";
|
||||||
private static final String FULL_METHOD_NAME = "fake_service/fake_method";
|
private static final String FULL_METHOD_NAME = "fake_service/fake_method";
|
||||||
private final Application context = ApplicationProvider.getApplicationContext();
|
private final Application context = ApplicationProvider.getApplicationContext();
|
||||||
private ServiceController<SomeService> controller;
|
private final ArrayBlockingQueue<SettableFuture<Status>> statusesToSet =
|
||||||
private SomeService service;
|
new ArrayBlockingQueue<>(128);
|
||||||
private ManagedChannel channel;
|
private ManagedChannel channel;
|
||||||
|
private Server server;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
controller = Robolectric.buildService(SomeService.class);
|
AndroidComponentAddress listenAddress =
|
||||||
service = controller.create().get();
|
AndroidComponentAddress.forRemoteComponent(context.getPackageName(), "HostService");
|
||||||
|
|
||||||
AndroidComponentAddress listenAddress = AndroidComponentAddress.forContext(service);
|
MethodDescriptor<Empty, Empty> methodDesc = getMethodDescriptor();
|
||||||
ScheduledExecutorService executor = service.getExecutor();
|
ServerCallHandler<Empty, Empty> callHandler =
|
||||||
|
ServerCalls.asyncUnaryCall(
|
||||||
|
(req, respObserver) -> {
|
||||||
|
respObserver.onNext(req);
|
||||||
|
respObserver.onCompleted();
|
||||||
|
});
|
||||||
|
ServerMethodDefinition<Empty, Empty> methodDef =
|
||||||
|
ServerMethodDefinition.create(methodDesc, callHandler);
|
||||||
|
ServerServiceDefinition def =
|
||||||
|
ServerServiceDefinition.builder(SERVICE_NAME).addMethod(methodDef).build();
|
||||||
|
|
||||||
|
IBinderReceiver binderReceiver = new IBinderReceiver();
|
||||||
|
server =
|
||||||
|
BinderServerBuilder.forAddress(listenAddress, binderReceiver)
|
||||||
|
.addService(def)
|
||||||
|
.securityPolicy(
|
||||||
|
ServerSecurityPolicy.newBuilder()
|
||||||
|
.servicePolicy(
|
||||||
|
SERVICE_NAME,
|
||||||
|
new AsyncSecurityPolicy() {
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<Status> checkAuthorizationAsync(int uid) {
|
||||||
|
SettableFuture<Status> status = SettableFuture.create();
|
||||||
|
statusesToSet.add(status);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
try {
|
||||||
|
server.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
shadowOf(context)
|
||||||
|
.setComponentNameAndServiceForBindServiceForIntent(
|
||||||
|
listenAddress.asBindIntent(),
|
||||||
|
listenAddress.getComponent(),
|
||||||
|
checkNotNull(binderReceiver.get()));
|
||||||
channel =
|
channel =
|
||||||
BinderChannelBuilder.forAddress(listenAddress, context)
|
BinderChannelBuilder.forAddress(listenAddress, context)
|
||||||
.executor(executor)
|
|
||||||
.scheduledExecutorService(executor)
|
|
||||||
.offloadExecutor(executor)
|
|
||||||
.build();
|
.build();
|
||||||
idleLoopers();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
public void tearDown() {
|
public void tearDown() {
|
||||||
channel.shutdownNow();
|
channel.shutdownNow();
|
||||||
controller.destroy();
|
server.shutdownNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAsyncServerSecurityPolicy_failed_returnsFailureStatus() throws Exception {
|
public void testAsyncServerSecurityPolicy_failed_returnsFailureStatus() throws Exception {
|
||||||
ListenableFuture<Status> status = makeCall();
|
ListenableFuture<Status> status = makeCall();
|
||||||
service.setSecurityPolicyStatusWhenReady(Status.ALREADY_EXISTS);
|
statusesToSet.take().set(Status.ALREADY_EXISTS);
|
||||||
idleLoopers();
|
|
||||||
|
|
||||||
assertThat(Futures.getDone(status).getCode()).isEqualTo(Status.Code.ALREADY_EXISTS);
|
assertThat(status.get().getCode()).isEqualTo(Status.Code.ALREADY_EXISTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAsyncServerSecurityPolicy_failedFuture_failsWithCodeInternal() throws Exception {
|
public void testAsyncServerSecurityPolicy_failedFuture_failsWithCodeInternal() throws Exception {
|
||||||
ListenableFuture<Status> status = makeCall();
|
ListenableFuture<Status> status = makeCall();
|
||||||
service.setSecurityPolicyFailed(new IllegalStateException("oops"));
|
statusesToSet.take().setException(new IllegalStateException("oops"));
|
||||||
idleLoopers();
|
|
||||||
|
|
||||||
assertThat(Futures.getDone(status).getCode()).isEqualTo(Status.Code.INTERNAL);
|
assertThat(status.get().getCode()).isEqualTo(Status.Code.INTERNAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAsyncServerSecurityPolicy_allowed_returnsOkStatus() throws Exception {
|
public void testAsyncServerSecurityPolicy_allowed_returnsOkStatus() throws Exception {
|
||||||
ListenableFuture<Status> status = makeCall();
|
ListenableFuture<Status> status = makeCall();
|
||||||
service.setSecurityPolicyStatusWhenReady(Status.OK);
|
statusesToSet.take().set(Status.OK);
|
||||||
idleLoopers();
|
|
||||||
|
|
||||||
assertThat(Futures.getDone(status).getCode()).isEqualTo(Status.Code.OK);
|
assertThat(status.get().getCode()).isEqualTo(Status.Code.OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ListenableFuture<Status> makeCall() {
|
private ListenableFuture<Status> makeCall() {
|
||||||
ClientCall<Empty, Empty> call =
|
ClientCall<Empty, Empty> call = channel.newCall(getMethodDescriptor(), CallOptions.DEFAULT);
|
||||||
channel.newCall(
|
|
||||||
getMethodDescriptor(), CallOptions.DEFAULT.withExecutor(service.getExecutor()));
|
|
||||||
ListenableFuture<Empty> responseFuture =
|
ListenableFuture<Empty> responseFuture =
|
||||||
ClientCalls.futureUnaryCall(call, Empty.getDefaultInstance());
|
ClientCalls.futureUnaryCall(call, Empty.getDefaultInstance());
|
||||||
|
|
||||||
idleLoopers();
|
|
||||||
|
|
||||||
return Futures.catching(
|
return Futures.catching(
|
||||||
Futures.transform(responseFuture, unused -> Status.OK, directExecutor()),
|
Futures.transform(responseFuture, unused -> Status.OK, directExecutor()),
|
||||||
StatusRuntimeException.class,
|
StatusRuntimeException.class,
|
||||||
|
@ -133,10 +155,6 @@ public final class RobolectricBinderSecurityTest {
|
||||||
directExecutor());
|
directExecutor());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void idleLoopers() {
|
|
||||||
shadowOf(Looper.getMainLooper()).idle();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MethodDescriptor<Empty, Empty> getMethodDescriptor() {
|
private static MethodDescriptor<Empty, Empty> getMethodDescriptor() {
|
||||||
MethodDescriptor.Marshaller<Empty> marshaller =
|
MethodDescriptor.Marshaller<Empty> marshaller =
|
||||||
ProtoLiteUtils.marshaller(Empty.getDefaultInstance());
|
ProtoLiteUtils.marshaller(Empty.getDefaultInstance());
|
||||||
|
@ -147,106 +165,4 @@ public final class RobolectricBinderSecurityTest {
|
||||||
.setSampledToLocalTracing(true)
|
.setSampledToLocalTracing(true)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class SomeService extends LifecycleService {
|
|
||||||
|
|
||||||
private final IBinderReceiver binderReceiver = new IBinderReceiver();
|
|
||||||
private final ArrayBlockingQueue<SettableFuture<Status>> statusesToSet =
|
|
||||||
new ArrayBlockingQueue<>(128);
|
|
||||||
private Server server;
|
|
||||||
private final ScheduledExecutorService scheduledExecutorService =
|
|
||||||
new MainThreadScheduledExecutorService();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
|
|
||||||
MethodDescriptor<Empty, Empty> methodDesc = getMethodDescriptor();
|
|
||||||
ServerCallHandler<Empty, Empty> callHandler =
|
|
||||||
ServerCalls.asyncUnaryCall(
|
|
||||||
(req, respObserver) -> {
|
|
||||||
respObserver.onNext(req);
|
|
||||||
respObserver.onCompleted();
|
|
||||||
});
|
|
||||||
ServerMethodDefinition<Empty, Empty> methodDef =
|
|
||||||
ServerMethodDefinition.create(methodDesc, callHandler);
|
|
||||||
ServerServiceDefinition def =
|
|
||||||
ServerServiceDefinition.builder(SERVICE_NAME).addMethod(methodDef).build();
|
|
||||||
|
|
||||||
server =
|
|
||||||
BinderServerBuilder.forAddress(AndroidComponentAddress.forContext(this), binderReceiver)
|
|
||||||
.addService(def)
|
|
||||||
.securityPolicy(
|
|
||||||
ServerSecurityPolicy.newBuilder()
|
|
||||||
.servicePolicy(
|
|
||||||
SERVICE_NAME,
|
|
||||||
new AsyncSecurityPolicy() {
|
|
||||||
@Override
|
|
||||||
public ListenableFuture<Status> checkAuthorizationAsync(int uid) {
|
|
||||||
return Futures.submitAsync(
|
|
||||||
() -> {
|
|
||||||
SettableFuture<Status> status = SettableFuture.create();
|
|
||||||
statusesToSet.add(status);
|
|
||||||
return status;
|
|
||||||
},
|
|
||||||
getExecutor());
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.build())
|
|
||||||
.executor(getExecutor())
|
|
||||||
.scheduledExecutorService(getExecutor())
|
|
||||||
.build();
|
|
||||||
try {
|
|
||||||
server.start();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IllegalStateException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
Application context = ApplicationProvider.getApplicationContext();
|
|
||||||
ComponentName componentName = new ComponentName(context, SomeService.class);
|
|
||||||
shadowOf(context)
|
|
||||||
.setComponentNameAndServiceForBindService(
|
|
||||||
componentName, checkNotNull(binderReceiver.get()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an {@link ScheduledExecutorService} under which all of the gRPC computations run. The
|
|
||||||
* execution of any pending tasks on this executor can be triggered via {@link #idleLoopers()}.
|
|
||||||
*/
|
|
||||||
ScheduledExecutorService getExecutor() {
|
|
||||||
return scheduledExecutorService;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setSecurityPolicyStatusWhenReady(Status status) {
|
|
||||||
getNextEnqueuedStatus().set(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setSecurityPolicyFailed(Exception e) {
|
|
||||||
getNextEnqueuedStatus().setException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
private SettableFuture<Status> getNextEnqueuedStatus() {
|
|
||||||
@Nullable SettableFuture<Status> future = statusesToSet.poll();
|
|
||||||
while (future == null) {
|
|
||||||
// Keep idling until the future is available.
|
|
||||||
idleLoopers();
|
|
||||||
future = statusesToSet.poll();
|
|
||||||
}
|
|
||||||
return checkNotNull(future);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
super.onBind(intent);
|
|
||||||
return checkNotNull(binderReceiver.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
server.shutdownNow();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A future representing a task submitted to a {@link Handler}. */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue