| // 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_service/src/request_handling/body.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/service/logging.dart'; |
| import 'package:gcloud/service_scope.dart' as ss; |
| import 'package:logging/logging.dart'; |
| import 'package:test/test.dart'; |
| |
| import '../src/datastore/fake_config.dart'; |
| |
| void main() { |
| group('RequestHandler', () { |
| late HttpServer server; |
| late RequestHandler<dynamic> handler; |
| |
| final List<LogRecord> records = <LogRecord>[]; |
| |
| 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(); |
| }); |
| |
| setUp(() { |
| records.clear(); |
| log.onRecord.listen((LogRecord record) => records.add(record)); |
| }); |
| |
| Future<HttpClientResponse> issueRequest(String method) async { |
| final HttpClient client = HttpClient(); |
| final Uri url = Uri(scheme: 'http', host: 'localhost', port: server.port, path: '/path'); |
| final HttpClientRequest request = await client.openUrl(method, url); |
| return request.close(); |
| } |
| |
| Future<HttpClientResponse> issueGet() => issueRequest('get'); |
| |
| Future<HttpClientResponse> issuePost() => issueRequest('post'); |
| |
| test('Unimplemented methods yield HTTP method not allowed', () async { |
| handler = MethodNotAllowed(); |
| HttpClientResponse response = await issueGet(); |
| expect(response.statusCode, HttpStatus.methodNotAllowed); |
| response = await issuePost(); |
| expect(response.statusCode, HttpStatus.methodNotAllowed); |
| expect(records, isEmpty); |
| }); |
| |
| test('empty body yields empty HTTP response body', () async { |
| handler = EmptyBodyHandler(); |
| final HttpClientResponse response = await issueGet(); |
| expect(response.statusCode, HttpStatus.ok); |
| expect(await response.toList(), isEmpty); |
| expect(records, isEmpty); |
| }); |
| |
| test('string body yields string HTTP response body', () async { |
| handler = StringBodyHandler(); |
| final HttpClientResponse response = await issueGet(); |
| expect(response.statusCode, HttpStatus.ok); |
| expect(await utf8.decoder.bind(response).join(), 'Hello world'); |
| expect(records, isEmpty); |
| }); |
| |
| test('JsonBody yields JSON HTTP response body', () async { |
| handler = JsonBodyHandler(); |
| final HttpClientResponse response = await issueGet(); |
| expect(response.statusCode, HttpStatus.ok); |
| expect(await utf8.decoder.bind(response).join(), '{"key":"value"}'); |
| expect(records, isEmpty); |
| }); |
| |
| test('throwing HttpException yields corresponding HTTP status', () async { |
| handler = ThrowsHttpException(); |
| final HttpClientResponse response = await issueGet(); |
| expect(response.statusCode, HttpStatus.badRequest); |
| expect(await utf8.decoder.bind(response).join(), 'Bad request'); |
| expect(records, isEmpty); |
| }); |
| |
| test('throwing general exception yields HTTP 500 and logs to server logs', () async { |
| handler = ThrowsStateError(); |
| final HttpClientResponse response = await issueGet(); |
| expect(response.statusCode, HttpStatus.internalServerError); |
| expect(await utf8.decoder.bind(response).join(), contains('error message')); |
| expect(records.first.message, contains('error message')); |
| }); |
| |
| test('may access the request and response directly', () async { |
| handler = AccessesRequestAndResponseDirectly(); |
| final HttpClientResponse response = await issueGet(); |
| expect(response.headers.value('X-Test-Path'), '/path'); |
| expect(records, isEmpty); |
| }); |
| |
| test('may implement both GET and POST', () async { |
| handler = ImplementsBothGetAndPost(); |
| HttpClientResponse response = await issueGet(); |
| expect(response.headers.value('X-Test-Get'), 'true'); |
| expect(response.headers.value('X-Test-Post'), isNull); |
| response = await issuePost(); |
| expect(response.headers.value('X-Test-Get'), isNull); |
| expect(response.headers.value('X-Test-Post'), 'true'); |
| expect(records, isEmpty); |
| }); |
| |
| test('may implement only POST', () async { |
| handler = ImplementsOnlyPost(); |
| HttpClientResponse response = await issueGet(); |
| expect(response.statusCode, HttpStatus.methodNotAllowed); |
| response = await issuePost(); |
| expect(response.statusCode, HttpStatus.ok); |
| expect(records, isEmpty); |
| }); |
| }); |
| } |
| |
| class TestBody extends JsonBody { |
| const TestBody(); |
| |
| @override |
| Map<String, dynamic> toJson() => const <String, dynamic>{'key': 'value'}; |
| } |
| |
| class MethodNotAllowed extends RequestHandler<Body> { |
| MethodNotAllowed() : super(config: FakeConfig()); |
| } |
| |
| class EmptyBodyHandler extends RequestHandler<Body> { |
| EmptyBodyHandler() : super(config: FakeConfig()); |
| |
| @override |
| Future<Body> get() async => Body.empty; |
| } |
| |
| class StringBodyHandler extends RequestHandler<Body> { |
| StringBodyHandler() : super(config: FakeConfig()); |
| |
| @override |
| Future<Body> get() async => Body.forString('Hello world'); |
| } |
| |
| class JsonBodyHandler extends RequestHandler<TestBody> { |
| JsonBodyHandler() : super(config: FakeConfig()); |
| |
| @override |
| Future<TestBody> get() async => const TestBody(); |
| } |
| |
| class ThrowsHttpException extends RequestHandler<Body> { |
| ThrowsHttpException() : super(config: FakeConfig()); |
| |
| @override |
| Future<Body> get() async => throw const BadRequestException(); |
| } |
| |
| class ThrowsStateError extends RequestHandler<Body> { |
| ThrowsStateError() : super(config: FakeConfig()); |
| |
| @override |
| Future<Body> get() async => throw StateError('error message'); |
| } |
| |
| class AccessesRequestAndResponseDirectly extends RequestHandler<Body> { |
| AccessesRequestAndResponseDirectly() : super(config: FakeConfig()); |
| |
| @override |
| Future<Body> get() async { |
| response!.headers.add('X-Test-Path', request!.uri.path); |
| return Body.empty; |
| } |
| } |
| |
| class ImplementsBothGetAndPost extends RequestHandler<Body> { |
| ImplementsBothGetAndPost() : super(config: FakeConfig()); |
| |
| @override |
| Future<Body> get() async { |
| response!.headers.add('X-Test-Get', 'true'); |
| return Body.empty; |
| } |
| |
| @override |
| Future<Body> post() async { |
| response!.headers.add('X-Test-Post', 'true'); |
| return Body.empty; |
| } |
| } |
| |
| class ImplementsOnlyPost extends RequestHandler<Body> { |
| ImplementsOnlyPost() : super(config: FakeConfig()); |
| |
| @override |
| Future<Body> post() async => Body.empty; |
| } |