blob: 646b01c44186d348998876eb49b38242eb40c94a [file] [log] [blame]
// Copyright 2019 The Flutter Authors. 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 'dart:convert';
import 'dart:io';
import 'package:cocoon_common/core_extensions.dart';
import 'package:cocoon_common_test/cocoon_common_test.dart';
import 'package:cocoon_server/logging.dart';
import 'package:cocoon_server_test/test_logging.dart';
import 'package:cocoon_service/src/request_handling/exceptions.dart';
import 'package:cocoon_service/src/request_handling/request_handler.dart';
import 'package:cocoon_service/src/request_handling/response.dart';
import 'package:gcloud/service_scope.dart' as ss;
import 'package:test/test.dart';
import '../src/fake_config.dart';
void main() {
useTestLoggerPerTest();
group('RequestHandler', () {
late HttpServer server;
late RequestHandler handler;
setUpAll(() async {
server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
server.listen((HttpRequest request) {
runZoned<dynamic>(() {
return ss.fork(() {
return handler.service(request);
});
});
});
});
tearDownAll(() async {
await server.close();
});
Future<HttpClientResponse> issueRequest(String method) async {
final client = HttpClient();
final url = Uri(
scheme: 'http',
host: 'localhost',
port: server.port,
path: '/path',
);
final request = await client.openUrl(method, url);
return request.close();
}
Future<HttpClientResponse> issueGet() async => issueRequest('get');
Future<HttpClientResponse> issuePost() async => issueRequest('post');
Future<Map<String, Object?>> decodeBody(HttpClientResponse response) async {
final body = await response.collectBytes();
return body.parseAsJsonObject();
}
test('Unimplemented methods yield HTTP method not allowed', () async {
handler = MethodNotAllowed();
var response = await issueGet();
expect(response.statusCode, HttpStatus.methodNotAllowed);
response = await issuePost();
expect(response.statusCode, HttpStatus.methodNotAllowed);
expect(log, bufferedLoggerOf(isEmpty));
});
test('empty body yields empty HTTP response body', () async {
handler = EmptyBodyHandler();
final response = await issueGet();
expect(response.statusCode, HttpStatus.ok);
expect(await response.toList(), isEmpty);
expect(log, bufferedLoggerOf(isEmpty));
});
test('string body yields string HTTP response body', () async {
handler = StringBodyHandler();
final response = await issueGet();
expect(response.statusCode, HttpStatus.ok);
expect(await utf8.decoder.bind(response).join(), 'Hello world');
expect(log, bufferedLoggerOf(isEmpty));
});
test('throwing HttpException yields corresponding HTTP status', () async {
handler = ThrowsHttpException();
final response = await issueGet();
expect(response.statusCode, HttpStatus.badRequest);
expect(
await utf8.decoder.bind(response).join(),
'{"error":"Bad request"}',
);
expect(log, bufferedLoggerOf(isEmpty));
});
test(
'throwing general exception yields HTTP 500 and logs to server logs',
() async {
handler = ThrowsStateError();
final response = await issueGet();
expect(response.statusCode, HttpStatus.internalServerError);
expect(
await utf8.decoder.bind(response).join(),
contains('error message'),
);
expect(
log,
bufferedLoggerOf(
equals([
logThat(
message: contains('Internal server error'),
error: isA<StateError>().having(
(e) => e.message,
'message',
contains('error message'),
),
),
]),
),
);
},
);
test('may access the request and response directly', () async {
handler = AccessesRequestAndResponseDirectly();
final response = await issueGet();
final jsonBody = await decodeBody(response);
expect(jsonBody, {'request.uri.path': '/path'});
expect(log, bufferedLoggerOf(isEmpty));
});
test('may implement both GET and POST', () async {
handler = ImplementsBothGetAndPost();
var response = await issueGet();
var jsonBody = await decodeBody(response);
expect(jsonBody, {'method': 'GET'});
response = await issuePost();
jsonBody = await decodeBody(response);
expect(jsonBody, {'method': 'POST'});
expect(log, bufferedLoggerOf(isEmpty));
});
test('may implement only POST', () async {
handler = ImplementsOnlyPost();
var response = await issueGet();
expect(response.statusCode, HttpStatus.methodNotAllowed);
response = await issuePost();
expect(response.statusCode, HttpStatus.ok);
expect(log, bufferedLoggerOf(isEmpty));
});
});
}
final class MethodNotAllowed extends RequestHandler {
MethodNotAllowed() : super(config: FakeConfig());
}
final class EmptyBodyHandler extends RequestHandler {
EmptyBodyHandler() : super(config: FakeConfig());
@override
Future<Response> get(_) async => Response.emptyOk;
}
final class StringBodyHandler extends RequestHandler {
StringBodyHandler() : super(config: FakeConfig());
@override
Future<Response> get(_) async => Response.string('Hello world');
}
final class ThrowsHttpException extends RequestHandler {
ThrowsHttpException() : super(config: FakeConfig());
@override
Future<Response> get(_) async => throw const BadRequestException();
}
final class ThrowsStateError extends RequestHandler {
ThrowsStateError() : super(config: FakeConfig());
@override
Future<Response> get(_) async => throw StateError('error message');
}
final class AccessesRequestAndResponseDirectly extends RequestHandler {
AccessesRequestAndResponseDirectly() : super(config: FakeConfig());
@override
Future<Response> get(Request request) async {
return Response.json({'request.uri.path': request.uri.path});
}
}
final class ImplementsBothGetAndPost extends RequestHandler {
ImplementsBothGetAndPost() : super(config: FakeConfig());
@override
Future<Response> get(_) async {
return Response.json({'method': 'GET'});
}
@override
Future<Response> post(_) async {
return Response.json({'method': 'POST'});
}
}
final class ImplementsOnlyPost extends RequestHandler {
ImplementsOnlyPost() : super(config: FakeConfig());
@override
Future<Response> post(_) async => Response.emptyOk;
}