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:
John Cormie 2025-05-09 14:52:12 -07:00 committed by GitHub
parent 80cc988b3c
commit 64fe061ccd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 57 additions and 141 deletions

View File

@ -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}. */
}
} }