Add basic server tests. (#27)

This commit is contained in:
Jakob Andersen 2017-07-18 15:14:13 +02:00 committed by GitHub
parent 0a0a9ffc89
commit 1d12c5b87d
11 changed files with 639 additions and 72 deletions

View File

@ -11,7 +11,6 @@ dependencies:
grpc:
path: ../../
protobuf: ^0.5.4
http2: ^0.1.2
dev_dependencies:
test: ^0.12.0

View File

@ -11,7 +11,6 @@ dependencies:
grpc:
path: ../../
protobuf: ^0.5.4
http2: ^0.1.2
dev_dependencies:
test: ^0.12.0

View File

@ -12,7 +12,6 @@ dependencies:
grpc:
path: ../
protobuf: ^0.5.4
http2: ^0.1.2
dev_dependencies:
test: ^0.12.0

View File

@ -139,18 +139,17 @@ class ClientCall<Q, R> implements Response {
}
}
Future<Null> _initiateCall() async {
final connection = await _channel.connect();
final timeout = options?.timeout ?? _channel.options?.timeout;
static List<Header> createCallHeaders(String path, String authority,
{String timeout, Map<String, String> metadata}) {
// TODO(jakobr): Populate HTTP-specific headers in connection?
final headers = <Header>[
_methodPost,
_schemeHttp,
new Header.ascii(':path', _method.path),
new Header.ascii(':authority', _channel.host),
new Header.ascii(':path', path),
new Header.ascii(':authority', authority),
];
if (timeout != null) {
headers.add(new Header.ascii('grpc-timeout', toTimeoutString(timeout)));
headers.add(new Header.ascii('grpc-timeout', timeout));
}
headers.addAll([
_contentTypeGrpc,
@ -158,14 +157,23 @@ class ClientCall<Q, R> implements Response {
_grpcAcceptEncoding,
_userAgent,
]);
metadata?.forEach((key, value) {
headers.add(new Header.ascii(key, value));
});
return headers;
}
Future<Null> _initiateCall() async {
final connection = await _channel.connect();
final timeout = options?.timeout ?? _channel.options?.timeout;
final timeoutString = timeout != null ? toTimeoutString(timeout) : null;
// TODO(jakobr): Flip this around, and have the Channel create the call
// object and apply options (including the above TODO).
final customMetadata = <String, String>{};
customMetadata.addAll(_channel.options?.metadata ?? {});
customMetadata.addAll(options?.metadata ?? {});
customMetadata.forEach((key, value) {
headers.add(new Header.ascii(key, value));
});
final headers = createCallHeaders(_method.path, _channel.host,
timeout: timeoutString, metadata: customMetadata);
_stream = connection.makeRequest(headers);
_requests.stream
.map(_method.requestSerializer)

View File

@ -76,9 +76,7 @@ class Server {
_server.listen((socket) {
final connection = new ServerTransportConnection.viaSocket(socket);
_connections.add(connection);
connection.incomingStreams.listen((stream) {
new ServerHandler(lookupService, stream).handle();
}, onError: (error) {
connection.incomingStreams.listen(serveStream, onError: (error) {
print('Connection error: $error');
}, onDone: () {
_connections.remove(connection);
@ -88,6 +86,10 @@ class Server {
});
}
void serveStream(ServerTransportStream stream) {
new ServerHandler(lookupService, stream).handle();
}
Future<Null> shutdown() {
final done = _connections.map((connection) => connection.finish()).toList();
if (_server != null) {
@ -198,6 +200,7 @@ class ServerHandler {
void _onDataIdle(GrpcMessage message) {
if (message is! GrpcMetadata) {
_sendError(new GrpcError.unimplemented('Expected header frame'));
_sinkIncoming();
return;
}
final headerMessage = message
@ -222,6 +225,28 @@ class ServerHandler {
_startStreamingRequest();
}
Future<T> _toSingleFuture<T>(Stream<T> stream) {
T _ensureOnlyOneRequest(T previous, T element) {
if (previous != null) {
throw new GrpcError.unimplemented('More than one request received');
}
return element;
}
T _ensureOneRequest(T value) {
if (value == null)
throw new GrpcError.unimplemented('No requests received');
return value;
}
final future =
stream.fold(null, _ensureOnlyOneRequest).then(_ensureOneRequest);
// Make sure errors on the future aren't unhandled, but return the original
// future so the request handler can also get the error.
future.catchError((_) {});
return future;
}
void _startStreamingRequest() {
_incomingSubscription.pause();
_requests = new StreamController(
@ -236,17 +261,20 @@ class ServerHandler {
if (_descriptor.streamingRequest) {
_responses = _descriptor.handler(context, _requests.stream);
} else {
_responses = _descriptor.handler(context, _requests.stream.single);
_responses =
_descriptor.handler(context, _toSingleFuture(_requests.stream));
}
} else {
Future response;
if (_descriptor.streamingRequest) {
response = _descriptor.handler(context, _requests.stream);
} else {
response = _descriptor.handler(context, _requests.stream.single);
response =
_descriptor.handler(context, _toSingleFuture(_requests.stream));
}
_responses = response.asStream();
}
_responseSubscription = _responses.listen(_onResponse,
onError: _onResponseError,
onDone: _onResponseDone,
@ -259,9 +287,10 @@ class ServerHandler {
void _onDataActive(GrpcMessage message) {
if (message is! GrpcData) {
_sendError(new GrpcError.unimplemented('Expected data frame'));
final error = new GrpcError.unimplemented('Expected request');
_sendError(error);
_requests
..addError(new GrpcError.unimplemented('No request received'))
..addError(error)
..close();
return;
}
@ -272,6 +301,7 @@ class ServerHandler {
_requests
..addError(error)
..close();
return;
}
// TODO(jakobr): Cast should not be necessary here.
@ -296,10 +326,10 @@ class ServerHandler {
void _onResponse(response) {
try {
final bytes = _descriptor.responseSerializer(response);
if (!_headersSent) {
_sendHeaders();
}
final bytes = _descriptor.responseSerializer(response);
_stream.sendData(GrpcHttpEncoder.frame(bytes));
} catch (error) {
final grpcError =
@ -312,7 +342,6 @@ class ServerHandler {
}
_sendError(grpcError);
_cancelResponseSubscription();
_sinkIncoming();
}
}
@ -330,6 +359,7 @@ class ServerHandler {
void _sendHeaders() {
if (_headersSent) throw new GrpcError.internal('Headers already sent');
final headersMap = <String, String>{};
headersMap.addAll(_customHeaders);
_customHeaders = null;
@ -369,6 +399,7 @@ class ServerHandler {
_stream.sendHeaders(trailers, endStream: true);
// We're done!
_cancelResponseSubscription();
_sinkIncoming();
}
// -- All states, incoming error / stream closed --
@ -381,6 +412,8 @@ class ServerHandler {
_requests.addError(new GrpcError.cancelled('Cancelled'));
}
_cancelResponseSubscription();
_incomingSubscription.cancel();
_stream.terminate();
}
void _onDoneError() {
@ -390,7 +423,7 @@ class ServerHandler {
void _onDoneExpected() {
if (!(_hasReceivedRequest || _descriptor.streamingRequest)) {
final error = new GrpcError.unimplemented('Expected request message');
final error = new GrpcError.unimplemented('No request received');
_sendError(error);
_requests.addError(error);
}

View File

@ -9,7 +9,7 @@ environment:
dependencies:
async: ^1.13.3
meta: ^1.0.5
http2: ^0.1.2
http2: ^0.1.3
dev_dependencies:
mockito: ^2.0.2

View File

@ -5,12 +5,12 @@
import 'dart:async';
import 'package:grpc/src/status.dart';
import 'package:grpc/src/streams.dart';
import 'package:http2/transport.dart';
import 'package:test/test.dart';
import 'package:mockito/mockito.dart';
import 'src/client_utils.dart';
import 'src/utils.dart';
void main() {
const dummyValue = 0;
@ -25,14 +25,12 @@ void main() {
harness.tearDown();
});
test('Unary calls work end-to-end', () async {
test('Unary calls work on the client', () async {
const requestValue = 17;
const responseValue = 19;
void _handleRequest(StreamMessage message) {
expect(message, new isInstanceOf<DataStreamMessage>());
expect(message.endStream, false);
final data = new GrpcHttpDecoder().convert(message) as GrpcData;
void handleRequest(StreamMessage message) {
final data = validateDataMessage(message);
expect(mockDecode(data.data), requestValue);
harness
@ -45,20 +43,18 @@ void main() {
clientCall: harness.client.unary(requestValue),
expectedResult: responseValue,
expectedPath: '/Test/Unary',
serverHandlers: [_handleRequest],
serverHandlers: [handleRequest],
);
});
test('Client-streaming calls work end-to-end', () async {
test('Client-streaming calls work on the client', () async {
const requests = const [17, 3];
const response = 12;
var index = 0;
void handleRequest(StreamMessage message) {
expect(message, new isInstanceOf<DataStreamMessage>());
expect(message.endStream, false);
final data = new GrpcHttpDecoder().convert(message) as GrpcData;
final data = validateDataMessage(message);
expect(mockDecode(data.data), requests[index++]);
}
@ -79,14 +75,12 @@ void main() {
);
});
test('Server-streaming calls work end-to-end', () async {
test('Server-streaming calls work on the client', () async {
const request = 4;
const responses = const [3, 17, 9];
void handleRequest(StreamMessage message) {
expect(message, new isInstanceOf<DataStreamMessage>());
expect(message.endStream, false);
final data = new GrpcHttpDecoder().convert(message) as GrpcData;
final data = validateDataMessage(message);
expect(mockDecode(data.data), request);
harness.sendResponseHeader();
@ -102,16 +96,14 @@ void main() {
);
});
test('Bidirectional calls work end-to-end', () async {
test('Bidirectional calls work on the client', () async {
const requests = const [1, 15, 7];
const responses = const [3, 17, 9];
var index = 0;
void handleRequest(StreamMessage message) {
expect(message, new isInstanceOf<DataStreamMessage>());
expect(message.endStream, false);
final data = new GrpcHttpDecoder().convert(message) as GrpcData;
final data = validateDataMessage(message);
expect(mockDecode(data.data), requests[index]);
if (index == 0) {

270
test/server_test.dart Normal file
View File

@ -0,0 +1,270 @@
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:grpc/grpc.dart';
import 'package:grpc/src/status.dart';
import 'package:http2/transport.dart';
import 'package:test/test.dart';
import 'src/server_utils.dart';
void main() {
const dummyValue = 17;
ServerHarness harness;
setUp(() {
harness = new ServerHarness()..setUp();
});
tearDown(() {
harness.tearDown();
});
test('Unary calls work on the server', () async {
const expectedRequest = 5;
const expectedResponse = 7;
Future<int> methodHandler(ServiceCall call, Future<int> request) async {
expect(await request, expectedRequest);
return expectedResponse;
}
harness
..service.unaryHandler = methodHandler
..runTest('/Test/Unary', [expectedRequest], [expectedResponse]);
await harness.fromServer.done;
});
test('Client-streaming calls work on the server', () async {
const expectedRequests = const [5, 3, 17];
const expectedResponse = 12;
Future<int> methodHandler(ServiceCall call, Stream<int> request) async {
expect(await request.toList(), expectedRequests);
return expectedResponse;
}
harness
..service.clientStreamingHandler = methodHandler
..runTest('/Test/ClientStreaming', expectedRequests, [expectedResponse]);
await harness.fromServer.done;
});
test('Server-streaming calls work on the server', () async {
const expectedRequest = 5;
const expectedResponses = const [7, 9, 1];
Stream<int> methodHandler(ServiceCall call, Future<int> request) async* {
expect(await request, expectedRequest);
for (final value in expectedResponses) {
yield value;
}
}
harness
..service.serverStreamingHandler = methodHandler
..runTest('/Test/ServerStreaming', [expectedRequest], expectedResponses);
await harness.fromServer.done;
});
test('Bidirectional calls work on the server', () async {
const expectedRequests = const [3, 1, 7];
final expectedResponses = expectedRequests.map((v) => v + 5).toList();
Stream<int> methodHandler(ServiceCall call, Stream<int> request) async* {
yield* request.map((value) => value + 5);
}
harness
..service.bidirectionalHandler = methodHandler
..runTest('/Test/Bidirectional', expectedRequests, expectedResponses);
await harness.fromServer.done;
});
test('Server returns error on missing call header', () async {
harness
..expectErrorResponse(StatusCode.unimplemented, 'Expected header frame')
..sendData(dummyValue);
await harness.fromServer.done;
});
test('Server returns error on invalid path', () async {
harness
..expectErrorResponse(StatusCode.unimplemented, 'Invalid path')
..sendRequestHeader('InvalidPath');
await harness.fromServer.done;
});
test('Server returns error on unimplemented path', () async {
harness
..expectErrorResponse(
StatusCode.unimplemented, 'Path /Test/NotFound not found')
..sendRequestHeader('/Test/NotFound');
await harness.fromServer.done;
});
/// Returns a service method handler that verifies that awaiting the request
/// throws a specific error.
Future<int> Function(ServiceCall call, Future<int> request) expectError(
expectedError) {
return expectAsync2((ServiceCall call, Future<int> request) async {
try {
final result = await request;
registerException('Did not throw');
return result;
} catch (caughtError) {
try {
expect(caughtError, expectedError);
} catch (failure, stack) {
registerException(failure, stack);
}
rethrow;
}
}, count: 1);
}
/// Returns a service method handler that verifies that awaiting the request
/// stream throws a specific error.
Stream<int> Function(ServiceCall call, Stream<int> request)
expectErrorStreaming(expectedError) {
return (ServiceCall call, Stream<int> request) async* {
try {
await for (var entry in request) {
yield entry;
}
registerException('Did not throw');
} catch (caughtError) {
try {
expect(caughtError, expectedError);
} catch (failure, stack) {
registerException(failure, stack);
}
rethrow;
}
};
}
test('Server returns error on missing request for unary call', () async {
harness
..service.unaryHandler =
expectError(new GrpcError.unimplemented('No request received'))
..expectErrorResponse(StatusCode.unimplemented, 'No request received')
..sendRequestHeader('/Test/Unary')
..toServer.close();
await harness.fromServer.done;
});
test('Server returns error if multiple headers are received for unary call',
() async {
harness
..service.unaryHandler =
expectError(new GrpcError.unimplemented('Expected request'))
..expectErrorResponse(StatusCode.unimplemented, 'Expected request')
..sendRequestHeader('/Test/Unary')
..toServer.add(new HeadersStreamMessage([]))
..toServer.close();
await harness.fromServer.done;
});
test('Server returns error on too many requests for unary call', () async {
harness
..service.unaryHandler =
expectError(new GrpcError.unimplemented('Too many requests'))
..expectErrorResponse(StatusCode.unimplemented, 'Too many requests')
..sendRequestHeader('/Test/Unary')
..sendData(dummyValue)
..sendData(dummyValue)
..toServer.close();
await harness.fromServer.done;
});
test('Server returns request deserialization errors', () async {
harness
..service.bidirectionalHandler = expectErrorStreaming(
new GrpcError.internal('Error deserializing request: Failed'))
..expectErrorResponse(
StatusCode.internal, 'Error deserializing request: Failed')
..sendRequestHeader('/Test/RequestError')
..sendData(dummyValue)
..toServer.close();
await harness.fromServer.done;
});
test('Server returns response serialization errors', () async {
harness
..service.bidirectionalHandler = expectErrorStreaming(
new GrpcError.internal('Error sending response: Failed'))
..expectErrorResponse(
StatusCode.internal, 'Error sending response: Failed')
..sendRequestHeader('/Test/ResponseError')
..sendData(dummyValue)
..sendData(dummyValue)
..toServer.close();
await harness.fromServer.done;
});
test('Header can only be sent once', () async {
Future<int> methodHandler(ServiceCall call, Future<int> request) async {
call.sendHeaders();
call.sendHeaders();
return await request;
}
harness
..service.unaryHandler = methodHandler
..expectTrailingErrorResponse(StatusCode.internal, 'Headers already sent')
..sendRequestHeader('/Test/Unary');
await harness.fromServer.done;
});
test('Server receives cancel', () async {
final success = new Completer<bool>();
Future<int> methodHandler(ServiceCall call, Future<int> request) async {
try {
final result = await request;
registerException('Did not throw');
return result;
} catch (caughtError) {
try {
expect(caughtError, new GrpcError.cancelled('Cancelled'));
expect(call.isCanceled, isTrue);
success.complete(true);
} catch (failure, stack) {
registerException(failure, stack);
}
} finally {
if (!success.isCompleted) {
success.complete(false);
}
}
return dummyValue;
}
harness
..service.unaryHandler = methodHandler
..fromServer.stream.listen(expectAsync1((_) {}, count: 0),
onError: expectAsync1((error) {
expect(error, 'TERMINATED');
}, count: 1),
onDone: expectAsync0(() {}, count: 1))
..sendRequestHeader('/Test/Unary')
..toServer.addError('CANCEL');
expect(await success.future, isTrue);
await harness.toServer.close();
await harness.fromServer.done;
});
test(
'Server returns error if request stream is closed before sending anything',
() async {
harness
..expectErrorResponse(
StatusCode.unavailable, 'Request stream closed unexpectedly')
..toServer.close();
await harness.fromServer.done;
});
}

View File

@ -3,7 +3,6 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'package:grpc/src/streams.dart';
import 'package:http2/transport.dart';
@ -12,6 +11,8 @@ import 'package:mockito/mockito.dart';
import 'package:grpc/grpc.dart';
import 'utils.dart';
class MockConnection extends Mock implements ClientTransportConnection {}
class MockStream extends Mock implements ClientTransportStream {}
@ -20,9 +21,6 @@ class MockChannel extends Mock implements ClientChannel {}
typedef ServerMessageHandler = void Function(StreamMessage message);
List<int> mockEncode(int value) => new List.filled(value, 0);
int mockDecode(List<int> value) => value.length;
class TestClient {
final ClientChannel _channel;
@ -73,8 +71,8 @@ class ClientHarness {
MockChannel channel;
MockStream stream;
StreamController fromClient;
StreamController toClient;
StreamController<StreamMessage> fromClient;
StreamController<StreamMessage> toClient;
TestClient client;
@ -112,33 +110,12 @@ class ClientHarness {
if (closeStream) toClient.close();
}
void validateHeaders(List<Header> headers,
{String path, Map<String, String> customHeaders}) {
final headerMap = new Map.fromIterable(headers,
key: (h) => ASCII.decode(h.name), value: (h) => ASCII.decode(h.value));
expect(headerMap[':method'], 'POST');
expect(headerMap[':scheme'], 'http');
if (path != null) {
expect(headerMap[':path'], path);
}
expect(headerMap[':authority'], 'test');
expect(headerMap['grpc-timeout'], isNull);
expect(headerMap['content-type'], 'application/grpc');
expect(headerMap['te'], 'trailers');
expect(headerMap['grpc-accept-encoding'], 'identity');
expect(headerMap['user-agent'], startsWith('dart-grpc/'));
customHeaders?.forEach((key, value) {
expect(headerMap[key], value);
});
}
Future<Null> runTest(
{Future clientCall,
dynamic expectedResult,
String expectedPath,
Map<String, String> expectedCustomHeaders,
List<ServerMessageHandler> serverHandlers = const [],
List<MessageHandler> serverHandlers = const [],
Function doneHandler,
bool expectDone = true}) async {
int serverHandlerIndex = 0;
@ -160,7 +137,7 @@ class ClientHarness {
final List<Header> capturedHeaders =
verify(connection.makeRequest(captureAny)).captured.single;
validateHeaders(capturedHeaders,
validateRequestHeaders(capturedHeaders,
path: expectedPath, customHeaders: expectedCustomHeaders);
await clientSubscription.cancel();
@ -180,7 +157,7 @@ class ClientHarness {
dynamic expectedException,
String expectedPath,
Map<String, String> expectedCustomHeaders,
List<ServerMessageHandler> serverHandlers = const [],
List<MessageHandler> serverHandlers = const [],
bool expectDone = true}) async {
return runTest(
clientCall: expectThrows(clientCall, expectedException),

183
test/src/server_utils.dart Normal file
View File

@ -0,0 +1,183 @@
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:grpc/src/streams.dart';
import 'package:http2/transport.dart';
import 'package:test/test.dart';
import 'package:grpc/grpc.dart';
import 'utils.dart';
class TestService extends Service {
@override
String get $name => 'Test';
Future<int> Function(ServiceCall call, Future<int> request) unaryHandler;
Future<int> Function(ServiceCall call, Stream<int> request)
clientStreamingHandler;
Stream<int> Function(ServiceCall call, Future<int> request)
serverStreamingHandler;
Stream<int> Function(ServiceCall call, Stream<int> request)
bidirectionalHandler;
TestService() {
$addMethod(ServerHarness.createMethod('Unary', _unary, false, false));
$addMethod(ServerHarness.createMethod(
'ClientStreaming', _clientStreaming, true, false));
$addMethod(ServerHarness.createMethod(
'ServerStreaming', _serverStreaming, false, true));
$addMethod(ServerHarness.createMethod(
'Bidirectional', _bidirectional, true, true));
$addMethod(new ServiceMethod('RequestError', _bidirectional, true, true,
(List<int> value) => throw 'Failed', mockEncode));
$addMethod(new ServiceMethod('ResponseError', _bidirectional, true, true,
mockDecode, (int value) => throw 'Failed'));
}
Future<int> _unary(ServiceCall call, Future<int> request) async {
if (unaryHandler == null) {
fail('Should not invoke Unary');
}
return unaryHandler(call, request);
}
Future<int> _clientStreaming(ServiceCall call, Stream<int> request) async {
if (clientStreamingHandler == null) {
fail('Should not invoke ClientStreaming');
}
return clientStreamingHandler(call, request);
}
Stream<int> _serverStreaming(ServiceCall call, Future<int> request) async* {
if (serverStreamingHandler == null) {
fail('Should not invoke ServerStreaming');
}
yield* serverStreamingHandler(call, request);
}
Stream<int> _bidirectional(ServiceCall call, Stream<int> request) async* {
if (bidirectionalHandler == null) {
fail('Should not invoke Bidirectional');
}
yield* bidirectionalHandler(call, request);
}
}
class TestServerStream extends ServerTransportStream {
final Stream<StreamMessage> incomingMessages;
final StreamSink<StreamMessage> outgoingMessages;
TestServerStream(this.incomingMessages, this.outgoingMessages);
@override
int get id => -1;
@override
void terminate() {
outgoingMessages.addError('TERMINATED');
outgoingMessages.close();
}
@override
set onTerminated(void value(int)) {}
@override
bool get canPush => true;
@override
ServerTransportStream push(List<Header> requestHeaders) => null;
}
class ServerHarness {
final toServer = new StreamController<StreamMessage>();
final fromServer = new StreamController<StreamMessage>();
final service = new TestService();
final server = new Server();
static ServiceMethod<int, int> createMethod(String name,
Function methodHandler, bool clientStreaming, bool serverStreaming) {
return new ServiceMethod<int, int>(name, methodHandler, clientStreaming,
serverStreaming, mockDecode, mockEncode);
}
void setUp() {
server.addService(service);
final stream = new TestServerStream(toServer.stream, fromServer.sink);
server.serveStream(stream);
}
void tearDown() {
fromServer.close();
toServer.close();
}
void setupTest(List<MessageHandler> handlers) {
int handlerIndex = 0;
void handleMessages(StreamMessage message) {
handlers[handlerIndex++](message);
}
fromServer.stream.listen(
expectAsync1(handleMessages, count: handlers.length),
onError: expectAsync1((_) {}, count: 0),
onDone: expectAsync0(() {}, count: 1));
}
void expectErrorResponse(int status, String message) {
setupTest([errorTrailerValidator(status, message, validateHeader: true)]);
}
void expectTrailingErrorResponse(int status, String message) {
setupTest([
headerValidator(),
errorTrailerValidator(status, message, validateHeader: false)
]);
}
void sendRequestHeader(String path,
{String authority = 'test',
String timeout,
Map<String, String> metadata}) {
final headers = ClientCall.createCallHeaders(path, authority,
timeout: timeout, metadata: metadata);
toServer.add(new HeadersStreamMessage(headers));
}
void sendData(int value) {
toServer
.add(new DataStreamMessage(GrpcHttpEncoder.frame(mockEncode(value))));
}
void runTest(String path, List<int> requests, List<int> expectedResponses) {
void handleHeader(StreamMessage message) {
final header = validateMetadataMessage(message);
validateResponseHeaders(header.metadata);
}
int responseIndex = 0;
void handleResponse(StreamMessage message) {
final response = validateDataMessage(message);
expect(mockDecode(response.data), expectedResponses[responseIndex++]);
}
void handleTrailer(StreamMessage message) {
final trailer = validateMetadataMessage(message, endStream: true);
validateResponseTrailers(trailer.metadata);
}
final handlers = [handleHeader];
for (var i = 0; i < expectedResponses.length; i++) {
handlers.add(handleResponse);
}
handlers.add(handleTrailer);
setupTest(handlers);
sendRequestHeader(path);
requests.forEach(sendData);
toServer.close();
}
}

107
test/src/utils.dart Normal file
View File

@ -0,0 +1,107 @@
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
import 'package:grpc/src/streams.dart';
import 'package:http2/transport.dart';
import 'package:test/test.dart';
typedef MessageHandler = void Function(StreamMessage message);
List<int> mockEncode(int value) => new List.filled(value, 0);
int mockDecode(List<int> value) => value.length;
Map<String, String> headersToMap(List<Header> headers) =>
new Map.fromIterable(headers,
key: (h) => ASCII.decode(h.name), value: (h) => ASCII.decode(h.value));
void validateRequestHeaders(List<Header> headers,
{String path,
String authority = 'test',
String timeout,
Map<String, String> customHeaders}) {
final headerMap = headersToMap(headers);
expect(headerMap[':method'], 'POST');
expect(headerMap[':scheme'], 'http');
if (path != null) {
expect(headerMap[':path'], path);
}
expect(headerMap[':authority'], authority);
expect(headerMap['grpc-timeout'], timeout);
expect(headerMap['content-type'], 'application/grpc');
expect(headerMap['te'], 'trailers');
expect(headerMap['grpc-accept-encoding'], 'identity');
expect(headerMap['user-agent'], startsWith('dart-grpc/'));
customHeaders?.forEach((key, value) {
expect(headerMap[key], value);
});
}
void validateResponseHeaders(Map<String, String> headers,
{int status = 200,
bool allowTrailers = false,
Map<String, String> customHeaders}) {
expect(headers[':status'], '200');
expect(headers['content-type'], startsWith('application/grpc'));
if (!allowTrailers) {
expect(headers.containsKey('grpc-status'), isFalse);
expect(headers.containsKey('grpc-message'), isFalse);
}
customHeaders?.forEach((key, value) {
expect(headers[key], value);
});
}
void validateResponseTrailers(Map<String, String> trailers,
{int status = 0, String message, Map<String, String> customTrailers}) {
expect(trailers['grpc-status'], '$status');
if (message != null) {
expect(trailers['grpc-message'], message);
}
customTrailers?.forEach((key, value) {
expect(trailers[key], value);
});
}
GrpcMetadata validateMetadataMessage(StreamMessage message,
{bool endStream = false}) {
expect(message, new isInstanceOf<HeadersStreamMessage>());
expect(message.endStream, endStream);
final decoded = new GrpcHttpDecoder().convert(message);
expect(decoded, new isInstanceOf<GrpcMetadata>());
return decoded;
}
GrpcData validateDataMessage(StreamMessage message, {bool endStream = false}) {
expect(message, new isInstanceOf<DataStreamMessage>());
expect(message.endStream, endStream);
final decoded = new GrpcHttpDecoder().convert(message);
expect(decoded, new isInstanceOf<GrpcData>());
return decoded;
}
void Function(StreamMessage message) headerValidator() {
return (StreamMessage message) {
final header = validateMetadataMessage(message, endStream: false);
validateResponseHeaders(header.metadata, allowTrailers: true);
};
}
void Function(StreamMessage message) errorTrailerValidator(
int status, String statusMessage,
{bool validateHeader = false}) {
return (StreamMessage message) {
final trailer = validateMetadataMessage(message, endStream: true);
if (validateHeader) {
validateResponseHeaders(trailer.metadata, allowTrailers: true);
}
validateResponseTrailers(trailer.metadata,
status: status, message: statusMessage);
};
}