| // 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:convert'; |
| import 'dart:io'; |
| |
| import 'package:cocoon_service/cocoon_service.dart'; |
| import 'package:mockito/mockito.dart'; |
| import 'package:test/test.dart'; |
| |
| void main() { |
| HttpServer destServer; |
| |
| setUpAll(() async { |
| destServer = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); |
| destServer.defaultResponseHeaders.clear(); |
| destServer.serverHeader = null; |
| destServer.listen((HttpRequest request) async { |
| final HttpResponse response = request.response; |
| response.headers.add('X-Single-Header', 'SingleValue'); |
| response.headers.add('X-Multi-Header', 'Value1'); |
| response.headers.add('X-Multi-Header', 'Value2'); |
| |
| switch (request.uri.path) { |
| case '/ok': |
| response.statusCode = HttpStatus.ok; |
| break; |
| case '/notFound': |
| response.statusCode = HttpStatus.notFound; |
| break; |
| case '/redirect': |
| response.headers.add(HttpHeaders.locationHeader, '/foo'); |
| response.statusCode = HttpStatus.movedTemporarily; |
| break; |
| case '/rawBody': |
| response.write('hello world'); |
| await response.flush(); |
| await response.close(); |
| return; |
| case '/gzipped': |
| final String body = |
| List<String>.generate(1024, (int index) => '$index').join(); |
| response.headers.add(HttpHeaders.contentEncodingHeader, 'gzip'); |
| response.add(gzip.encode(utf8.encode(body))); |
| await response.flush(); |
| await response.close(); |
| return; |
| default: |
| response.statusCode = HttpStatus.methodNotAllowed; |
| } |
| |
| bool isIgnoredHeader(String key, String value) { |
| return key.toLowerCase() == |
| HttpHeaders.contentLengthHeader.toLowerCase(); |
| } |
| |
| final Map<String, String> headers = <String, String>{}; |
| request.headers.forEach( |
| (String name, List<String> values) => headers[name] = values.single); |
| response.write(json.encode(<String, dynamic>{ |
| 'headers': headers..removeWhere(isIgnoredHeader), |
| 'path': request.uri.path, |
| 'query': request.uri.query, |
| 'fragment': request.uri.fragment, |
| 'body': await utf8.decoder.bind(request).join(), |
| })); |
| await response.flush(); |
| await response.close(); |
| }); |
| }); |
| |
| tearDownAll(() async { |
| await destServer.close(); |
| }); |
| |
| group('ProxyRequestHandler', () { |
| ProxyRequestHandler handler; |
| HttpServer proxyServer; |
| HttpClient client; |
| Uri url; |
| |
| HttpClientRequest request; |
| HttpClientResponse response; |
| |
| setUp(() async { |
| handler = ProxyRequestHandler( |
| config: MockConfig(), |
| scheme: 'http', |
| host: 'localhost', |
| port: destServer.port, |
| ); |
| proxyServer = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); |
| proxyServer.listen(handler.service); |
| client = HttpClient()..userAgent = null; |
| url = Uri(scheme: 'http', host: 'localhost', port: proxyServer.port); |
| }); |
| |
| tearDown(() async { |
| await proxyServer.close(); |
| }); |
| |
| test('forwards basic request', () async { |
| request = await client.getUrl(url.replace(path: '/ok')); |
| request.headers.clear(); |
| response = await request.close(); |
| final Map<String, dynamic> data = await utf8.decoder |
| .bind(response) |
| .transform(json.decoder) |
| .single as Map<String, dynamic>; |
| expect(data['headers'], <String, dynamic>{ |
| HttpHeaders.hostHeader: 'localhost:${destServer.port}', |
| }); |
| expect(data['path'], '/ok'); |
| expect(data['query'], isEmpty); |
| expect(data['fragment'], isEmpty); |
| expect(data['body'], isEmpty); |
| }); |
| |
| test('forwards HTTP request headers', () async { |
| request = await client.getUrl(url.replace(path: '/ok')); |
| request.headers.clear(); |
| request.headers.add('foo', 'bar'); |
| request.headers.add('baz', 'qux'); |
| request.headers.add('multi', 'value1'); |
| request.headers.add('multi', 'value2'); |
| response = await request.close(); |
| final Map<String, dynamic> data = await utf8.decoder |
| .bind(response) |
| .transform(json.decoder) |
| .single as Map<String, dynamic>; |
| expect(data['headers'], <String, dynamic>{ |
| 'foo': 'bar', |
| 'baz': 'qux', |
| 'multi': 'value1, value2', |
| HttpHeaders.hostHeader: 'localhost:${destServer.port}', |
| }); |
| }); |
| |
| test('forwards HTTP request body', () async { |
| request = await client.getUrl(url.replace(path: '/ok')); |
| request.contentLength = 'request body'.length; |
| request.write('request body'); |
| await request.flush(); |
| response = await request.close(); |
| final Map<String, dynamic> data = await utf8.decoder |
| .bind(response) |
| .transform(json.decoder) |
| .single as Map<String, dynamic>; |
| expect(data['body'], 'request body'); |
| }); |
| |
| test('forwards HTTP request query parameters', () async { |
| request = await client.getUrl( |
| url.replace(path: '/ok', query: 'foo=bar&baz=qux%26quz%3Dquw')); |
| response = await request.close(); |
| final Map<String, dynamic> data = await utf8.decoder |
| .bind(response) |
| .transform(json.decoder) |
| .single as Map<String, dynamic>; |
| expect(data['query'], 'foo=bar&baz=qux%26quz%3Dquw'); |
| }); |
| |
| test('URL fragment is dropped', () async { |
| request = await client |
| .getUrl(url.replace(path: '/ok', fragment: 'foo&bar=baz')); |
| response = await request.close(); |
| final Map<String, dynamic> data = await utf8.decoder |
| .bind(response) |
| .transform(json.decoder) |
| .single as Map<String, dynamic>; |
| expect(data['fragment'], isEmpty); |
| }); |
| |
| test('forwards HTTP response status code', () async { |
| request = await client.getUrl(url.replace(path: '/ok')); |
| response = await request.close(); |
| expect(response.statusCode, HttpStatus.ok); |
| request = await client.getUrl(url.replace(path: '/notFound')); |
| response = await request.close(); |
| expect(response.statusCode, HttpStatus.notFound); |
| request = await client.getUrl(url.replace(path: '/redirect')); |
| request.followRedirects = false; |
| response = await request.close(); |
| expect(response.statusCode, HttpStatus.movedTemporarily); |
| expect(response.headers.value(HttpHeaders.locationHeader), '/foo'); |
| }); |
| |
| test('forwards HTTP response headers', () async { |
| request = await client.getUrl(url.replace(path: '/ok')); |
| response = await request.close(); |
| expect(response.headers.value('X-Single-Header'), 'SingleValue'); |
| expect(response.headers.value('X-Multi-Header'), 'Value1, Value2'); |
| }); |
| |
| test('forwards HTTP response body', () async { |
| request = await client.getUrl(url.replace(path: '/rawBody')); |
| response = await request.close(); |
| expect(await utf8.decoder.bind(response).join(), 'hello world'); |
| }); |
| |
| test('Handles gzipped responses from the dest server', () async { |
| final String expectedBody = |
| List<String>.generate(1024, (int index) => '$index').join(); |
| request = await client.getUrl(url.replace(path: '/gzipped')); |
| response = await request.close(); |
| expect(await utf8.decoder.bind(response).join(), expectedBody); |
| }); |
| }); |
| } |
| |
| // ignore: must_be_immutable |
| class MockConfig extends Mock implements Config {} |