/*
* Copyright 2016 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.linecorp.armeria.server.grpc.interop;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import java.io.IOException;
import java.lang.reflect.Method;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.slf4j.bridge.SLF4JBridgeHandler;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.ByteString;
import com.linecorp.armeria.common.http.HttpSessionProtocols;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.server.ServiceRequestContext;
import com.linecorp.armeria.server.grpc.GrpcServiceBuilder;
import io.grpc.ManagedChannel;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.NettyChannelBuilder;
import io.grpc.stub.StreamObserver;
import io.grpc.testing.TestUtils;
import io.grpc.testing.integration.AbstractInteropTest;
import io.grpc.testing.integration.Messages.EchoStatus;
import io.grpc.testing.integration.Messages.Payload;
import io.grpc.testing.integration.Messages.PayloadType;
import io.grpc.testing.integration.Messages.ResponseParameters;
import io.grpc.testing.integration.Messages.SimpleRequest;
import io.grpc.testing.integration.Messages.StreamingOutputCallRequest;
import io.grpc.testing.integration.Messages.StreamingOutputCallResponse;
import io.grpc.testing.integration.TestServiceGrpc;
import io.grpc.testing.integration.TestServiceGrpc.TestServiceStub;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol;
import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior;
import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior;
import io.netty.handler.ssl.ClientAuth;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SupportedCipherSuiteFilter;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.handler.ssl.util.SelfSignedCertificate;
/**
* Interop test based on grpc-interop-testing. Should provide reasonable confidence in armeria's
* handling of the grpc protocol.
*/
@Ignore // TODO(trustin): Unignore once GRPC upgrades to Netty 4.1.10
public class ArmeriaGrpcServerInteropTest extends AbstractInteropTest {
private static final ApplicationProtocolConfig ALPN = new ApplicationProtocolConfig(
Protocol.ALPN,
SelectorFailureBehavior.NO_ADVERTISE,
SelectedListenerFailureBehavior.ACCEPT,
ImmutableList.of("h2"));
private static final AtomicReference<ServiceRequestContext> ctxCapture = new AtomicReference<>();
/** Starts the server with HTTPS. */
@BeforeClass
public static void startServer() {
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
try {
SelfSignedCertificate ssc = new SelfSignedCertificate();
ServerBuilder sb = new ServerBuilder()
.port(0, HttpSessionProtocols.HTTPS)
.defaultMaxRequestLength(16 * 1024 * 1024)
.sslContext(
SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
.applicationProtocolConfig(ALPN)
.clientAuth(ClientAuth.REQUIRE)
.trustManager(TestUtils.loadCert("ca.pem"))
.ciphers(TestUtils.preferredTestCiphers(),
SupportedCipherSuiteFilter.INSTANCE)
.build());
startStaticServer(new ArmeriaGrpcServerBuilder(sb, new GrpcServiceBuilder(), ctxCapture));
} catch (IOException | CertificateException ex) {
throw new RuntimeException(ex);
}
}
@AfterClass
public static void stopServer() {
stopStaticServer();
}
@After
public void clearCtxCapture() {
ctxCapture.set(null);
}
@Override
protected ManagedChannel createChannel() {
try {
// Use reflection to access package-private method.
Method getPort = AbstractInteropTest.class.getDeclaredMethod("getPort");
getPort.setAccessible(true);
return NettyChannelBuilder
.forAddress("localhost", (int) getPort.invoke(this))
.flowControlWindow(65 * 1024)
.maxInboundMessageSize(16 * 1024 * 1024)
.sslContext(GrpcSslContexts
.forClient()
.keyManager(TestUtils.loadCert("client.pem"),
TestUtils.loadCert("client.key"))
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.ciphers(TestUtils.preferredTestCiphers(),
SupportedCipherSuiteFilter.INSTANCE)
.build())
.build();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
@Override
protected boolean metricsExpected() {
return false;
}
@Test
@Override
@Ignore
// TODO(anuraag): Enable after adding support in ServiceRequestContext to define custom timeout handling.
public void deadlineExceededServerStreaming() {
}
@Test(timeout = 10000)
public void sendsTimeoutHeader() {
long configuredTimeoutMinutes = 100;
TestServiceGrpc.TestServiceBlockingStub stub =
TestServiceGrpc.newBlockingStub(channel)
.withDeadlineAfter(configuredTimeoutMinutes, TimeUnit.MINUTES);
stub.emptyCall(EMPTY);
long transferredTimeoutMinutes = TimeUnit.MILLISECONDS.toMinutes(
ctxCapture.get().requestTimeoutMillis());
Assert.assertTrue(
"configuredTimeoutMinutes=" + configuredTimeoutMinutes +
", transferredTimeoutMinutes=" + transferredTimeoutMinutes,
configuredTimeoutMinutes - transferredTimeoutMinutes >= 0 &&
configuredTimeoutMinutes - transferredTimeoutMinutes <= 1);
}
// Several tests copied due to Mockito version mismatch (timeout() was moved).
@Override
@Test(timeout = 10000)
public void pingPong() throws Exception {
final List<StreamingOutputCallRequest> requests = Arrays.asList(
StreamingOutputCallRequest.newBuilder()
.addResponseParameters(ResponseParameters.newBuilder()
.setSize(31415))
.setPayload(Payload.newBuilder()
.setBody(ByteString.copyFrom(new byte[27182])))
.build(),
StreamingOutputCallRequest.newBuilder()
.addResponseParameters(ResponseParameters.newBuilder()
.setSize(9))
.setPayload(Payload.newBuilder()
.setBody(ByteString.copyFrom(new byte[8])))
.build(),
StreamingOutputCallRequest.newBuilder()
.addResponseParameters(ResponseParameters.newBuilder()
.setSize(2653))
.setPayload(Payload.newBuilder()
.setBody(ByteString.copyFrom(new byte[1828])))
.build(),
StreamingOutputCallRequest.newBuilder()
.addResponseParameters(ResponseParameters.newBuilder()
.setSize(58979))
.setPayload(Payload.newBuilder()
.setBody(ByteString.copyFrom(new byte[45904])))
.build());
final List<StreamingOutputCallResponse> goldenResponses = Arrays.asList(
StreamingOutputCallResponse.newBuilder()
.setPayload(Payload.newBuilder()
.setType(PayloadType.COMPRESSABLE)
.setBody(ByteString.copyFrom(new byte[31415])))
.build(),
StreamingOutputCallResponse.newBuilder()
.setPayload(Payload.newBuilder()
.setType(PayloadType.COMPRESSABLE)
.setBody(ByteString.copyFrom(new byte[9])))
.build(),
StreamingOutputCallResponse.newBuilder()
.setPayload(Payload.newBuilder()
.setType(PayloadType.COMPRESSABLE)
.setBody(ByteString.copyFrom(new byte[2653])))
.build(),
StreamingOutputCallResponse.newBuilder()
.setPayload(Payload.newBuilder()
.setType(PayloadType.COMPRESSABLE)
.setBody(ByteString.copyFrom(new byte[58979])))
.build());
@SuppressWarnings("unchecked")
StreamObserver<StreamingOutputCallResponse> responseObserver = mock(StreamObserver.class);
StreamObserver<StreamingOutputCallRequest> requestObserver
= asyncStub.fullDuplexCall(responseObserver);
for (int i = 0; i < requests.size(); i++) {
requestObserver.onNext(requests.get(i));
verify(responseObserver, timeout(operationTimeoutMillis())).onNext(goldenResponses.get(i));
verifyNoMoreInteractions(responseObserver);
}
requestObserver.onCompleted();
verify(responseObserver, timeout(operationTimeoutMillis())).onCompleted();
verifyNoMoreInteractions(responseObserver);
}
@Test(timeout = 10000)
public void statusCodeAndMessage() throws Exception {
int errorCode = 2;
String errorMessage = "test status message";
EchoStatus responseStatus = EchoStatus.newBuilder()
.setCode(errorCode)
.setMessage(errorMessage)
.build();
SimpleRequest simpleRequest = SimpleRequest.newBuilder()
.setResponseStatus(responseStatus)
.build();
StreamingOutputCallRequest streamingRequest =
StreamingOutputCallRequest.newBuilder()
.setResponseStatus(responseStatus)
.build();
// Test UnaryCall
try {
blockingStub.unaryCall(simpleRequest);
fail();
} catch (StatusRuntimeException e) {
assertEquals(Status.UNKNOWN.getCode(), e.getStatus().getCode());
assertEquals(errorMessage, e.getStatus().getDescription());
}
// Test FullDuplexCall
@SuppressWarnings("unchecked")
StreamObserver<StreamingOutputCallResponse> responseObserver =
mock(StreamObserver.class);
StreamObserver<StreamingOutputCallRequest> requestObserver
= asyncStub.fullDuplexCall(responseObserver);
requestObserver.onNext(streamingRequest);
requestObserver.onCompleted();
ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
verify(responseObserver, timeout(operationTimeoutMillis())).onError(captor.capture());
assertEquals(Status.UNKNOWN.getCode(), Status.fromThrowable(captor.getValue()).getCode());
assertEquals(errorMessage, Status.fromThrowable(captor.getValue()).getDescription());
verifyNoMoreInteractions(responseObserver);
}
// Disable Metadata tests, which armeria does not support.
@Ignore
@Override
@Test
public void exchangeMetadataUnaryCall() throws Exception {}
@Ignore
@Override
@Test
public void exchangeMetadataStreamingCall() throws Exception {}
@Ignore
@Override
@Test
public void customMetadata() throws Exception {}
// FIXME: This doesn't work yet and may require some complicated changes. Armeria should continue to accept
// requests after a channel is gracefully closed but doesn't appear to (maybe because it supports both
// HTTP1, which has no concept of graceful shutdown, and HTTP2).
@Ignore
@Override
@Test(timeout = 10000)
public void gracefulShutdown() throws Exception {
final List<StreamingOutputCallRequest> requests = Arrays.asList(
StreamingOutputCallRequest.newBuilder()
.addResponseParameters(ResponseParameters.newBuilder()
.setSize(3))
.setPayload(Payload.newBuilder()
.setBody(ByteString.copyFrom(new byte[2])))
.build(),
StreamingOutputCallRequest.newBuilder()
.addResponseParameters(ResponseParameters.newBuilder()
.setSize(1))
.setPayload(Payload.newBuilder()
.setBody(ByteString.copyFrom(new byte[7])))
.build(),
StreamingOutputCallRequest.newBuilder()
.addResponseParameters(ResponseParameters.newBuilder()
.setSize(4))
.setPayload(Payload.newBuilder()
.setBody(ByteString.copyFrom(new byte[1])))
.build());
final List<StreamingOutputCallResponse> goldenResponses = Arrays.asList(
StreamingOutputCallResponse.newBuilder()
.setPayload(Payload.newBuilder()
.setType(PayloadType.COMPRESSABLE)
.setBody(ByteString.copyFrom(new byte[3])))
.build(),
StreamingOutputCallResponse.newBuilder()
.setPayload(Payload.newBuilder()
.setType(PayloadType.COMPRESSABLE)
.setBody(ByteString.copyFrom(new byte[1])))
.build(),
StreamingOutputCallResponse.newBuilder()
.setPayload(Payload.newBuilder()
.setType(PayloadType.COMPRESSABLE)
.setBody(ByteString.copyFrom(new byte[4])))
.build());
@SuppressWarnings("unchecked")
StreamObserver<StreamingOutputCallResponse> responseObserver = mock(StreamObserver.class);
StreamObserver<StreamingOutputCallRequest> requestObserver
= asyncStub.fullDuplexCall(responseObserver);
requestObserver.onNext(requests.get(0));
verify(responseObserver, timeout(operationTimeoutMillis())).onNext(goldenResponses.get(0));
// Initiate graceful shutdown.
channel.shutdown();
requestObserver.onNext(requests.get(1));
verify(responseObserver, timeout(operationTimeoutMillis())).onNext(goldenResponses.get(1));
// The previous ping-pong could have raced with the shutdown, but this one certainly shouldn't.
requestObserver.onNext(requests.get(2));
verify(responseObserver, timeout(operationTimeoutMillis())).onNext(goldenResponses.get(2));
requestObserver.onCompleted();
verify(responseObserver, timeout(operationTimeoutMillis())).onCompleted();
verifyNoMoreInteractions(responseObserver);
}
@Override
@Test(timeout = 10000)
public void timeoutOnSleepingServer() {
TestServiceStub stub = TestServiceGrpc.newStub(channel)
.withDeadlineAfter(1, TimeUnit.MILLISECONDS);
@SuppressWarnings("unchecked")
StreamObserver<StreamingOutputCallResponse> responseObserver = mock(StreamObserver.class);
StreamObserver<StreamingOutputCallRequest> requestObserver
= stub.fullDuplexCall(responseObserver);
try {
requestObserver.onNext(StreamingOutputCallRequest.newBuilder()
.setPayload(Payload.newBuilder()
.setBody(ByteString.copyFrom(
new byte[27182])))
.build());
} catch (IllegalStateException expected) {
// This can happen if the stream has already been terminated due to deadline exceeded.
}
ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
verify(responseObserver, timeout(operationTimeoutMillis())).onError(captor.capture());
assertEquals(Status.DEADLINE_EXCEEDED.getCode(),
Status.fromThrowable(captor.getValue()).getCode());
verifyNoMoreInteractions(responseObserver);
}
@Override
@Test(timeout = 10000)
public void cancelAfterFirstResponse() throws Exception {
final StreamingOutputCallRequest request = StreamingOutputCallRequest
.newBuilder()
.addResponseParameters(
ResponseParameters
.newBuilder()
.setSize(31415))
.setPayload(Payload.newBuilder()
.setBody(
ByteString
.copyFrom(
new byte[27182])))
.build();
final StreamingOutputCallResponse goldenResponse = StreamingOutputCallResponse
.newBuilder()
.setPayload(
Payload.newBuilder()
.setType(
PayloadType.COMPRESSABLE)
.setBody(
ByteString
.copyFrom(
new byte[31415])))
.build();
@SuppressWarnings("unchecked")
StreamObserver<StreamingOutputCallResponse> responseObserver = mock(StreamObserver.class);
StreamObserver<StreamingOutputCallRequest> requestObserver
= asyncStub.fullDuplexCall(responseObserver);
requestObserver.onNext(request);
verify(responseObserver, timeout(operationTimeoutMillis())).onNext(goldenResponse);
verifyNoMoreInteractions(responseObserver);
requestObserver.onError(new RuntimeException());
ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
verify(responseObserver, timeout(operationTimeoutMillis())).onError(captor.capture());
assertEquals(Status.Code.CANCELLED, Status.fromThrowable(captor.getValue()).getCode());
verifyNoMoreInteractions(responseObserver);
}
@Override
@Test(timeout = 10000)
public void emptyStream() throws Exception {
@SuppressWarnings("unchecked")
StreamObserver<StreamingOutputCallResponse> responseObserver = mock(StreamObserver.class);
StreamObserver<StreamingOutputCallRequest> requestObserver
= asyncStub.fullDuplexCall(responseObserver);
requestObserver.onCompleted();
verify(responseObserver, timeout(operationTimeoutMillis())).onCompleted();
verifyNoMoreInteractions(responseObserver);
}
}