import 'dart:async'; import 'dart:convert'; import 'dart:io' hide BytesBuilder; import 'dart:typed_data'; import 'package:platform_container/mirrors.dart'; import 'package:platform_framework/platform_framework.dart' hide Header; import 'package:platform_framework/http2.dart'; import 'package:collection/collection.dart' show IterableExtension; import 'package:http/src/multipart_file.dart' as http; import 'package:http/src/multipart_request.dart' as http; import 'package:http/io_client.dart'; import 'package:http2/transport.dart'; import 'package:http_parser/http_parser.dart'; import 'package:logging/logging.dart'; import 'package:test/test.dart'; import 'http2_client.dart'; const String jfk = 'Ask not what your country can do for you, but what you can do for your country.'; Stream> jfkStream() { return Stream.fromIterable([utf8.encode(jfk)]); } void main() { var client = Http2Client(); late IOClient h1c; Protevus app; late ProtevusHttp2 http2; late Uri serverRoot; setUp(() async { app = Protevus(reflector: MirrorsReflector()) ..encoders['gzip'] = gzip.encoder; hierarchicalLoggingEnabled = true; app.logger = Logger.detached('angel.http2') ..onRecord.listen((rec) { print(rec); if (rec.error == null) return; print(rec.error); if (rec.stackTrace != null) print(rec.stackTrace); }); app.get('/', (req, res) async { res.write('Hello world'); await res.close(); }); app.all('/method', (req, res) => req.method); app.get('/json', (_, __) => {'foo': 'bar'}); app.get('/stream', (req, res) => jfkStream().pipe(res)); app.get('/headers', (req, res) async { res.headers.addAll({'foo': 'bar', 'x-angel': 'http2'}); await res.close(); }); app.get('/status', (req, res) async { res.statusCode = 1337; await res.close(); }); app.post('/body', (req, res) => req.parseBody().then((_) => req.bodyAsMap)); app.post('/upload', (req, res) async { await req.parseBody(); var body = req.bodyAsMap; var files = req.uploadedFiles ?? []; var file = files.firstWhereOrNull((f) => f.name == 'file')!; return [ await file.data.map((l) => l.length).reduce((a, b) => a + b), file.contentType.mimeType, body ]; }); app.get('/push', (req, res) async { res.write('ok'); if (res is Http2ResponseContext && res.canPush) { var a = res.push('a')..write('a'); await a.close(); var b = res.push('b')..write('b'); await b.close(); } await res.close(); }); app.get('/param/:name', (req, res) => req.params); app.get('/query', (req, res) { print('incoming URI: ${req.uri}'); return req.queryParameters; }); var ctx = SecurityContext() ..useCertificateChain('dev.pem') ..usePrivateKey('dev.key', password: 'dartdart') ..setAlpnProtocols(['h2'], true); // Create an HTTP client that trusts our server. h1c = IOClient(HttpClient()..badCertificateCallback = (_, __, ___) => true); http2 = ProtevusHttp2(app, ctx, allowHttp1: true); var server = await http2.startServer(); serverRoot = Uri.parse('https://127.0.0.1:${server.port}'); }); tearDown(() async { await http2.close(); h1c.close(); }); test('buffered response', () async { var response = await client.get(serverRoot); expect(response.body, 'Hello world'); }); test('allowHttp1', () async { var response = await h1c.get(serverRoot); expect(response.body, 'Hello world'); }); test('streamed response', () async { var response = await client.get(serverRoot.replace(path: '/stream')); expect(response.body, jfk); }); group('gzip', () { test('buffered response', () async { var response = await client .get(serverRoot, headers: {'accept-encoding': 'gzip, deflate, br'}); expect(response.headers['content-encoding'], 'gzip'); var decoded = gzip.decode(response.bodyBytes); expect(utf8.decode(decoded), 'Hello world'); }); test('streamed response', () async { var response = await client.get(serverRoot.replace(path: '/stream'), headers: {'accept-encoding': 'gzip'}); expect(response.headers['content-encoding'], 'gzip'); //print(response.body); var decoded = gzip.decode(response.bodyBytes); expect(utf8.decode(decoded), jfk); }); }); test('query uri decoded', () async { var uri = serverRoot.replace(path: '/query', queryParameters: {'foo!': 'bar?'}); var response = await client.get(uri); print('Sent $uri'); expect(response.body, json.encode({'foo!': 'bar?'})); }); test('params uri decoded', () async { var response = await client.get(serverRoot.replace(path: '/param/foo!')); expect(response.body, json.encode({'name': 'foo!'})); }); test('method parsed', () async { var response = await client.delete(serverRoot.replace(path: '/method')); expect(response.body, json.encode('DELETE')); }); test('json response', () async { var response = await client.get(serverRoot.replace(path: '/json')); expect(response.body, json.encode({'foo': 'bar'})); expect(ContentType.parse(response.headers['content-type']!).mimeType, ContentType.json.mimeType); }); test('status sent', () async { var response = await client.get(serverRoot.replace(path: '/status')); expect(response.statusCode, 1337); }); test('headers sent', () async { var response = await client.get(serverRoot.replace(path: '/headers')); expect(response.headers['foo'], 'bar'); expect(response.headers['x-angel'], 'http2'); }); test('server push', () async { var socket = await SecureSocket.connect( serverRoot.host, serverRoot.port, onBadCertificate: (_) => true, supportedProtocols: ['h2'], ); var connection = ClientTransportConnection.viaSocket( socket, settings: ClientSettings(allowServerPushes: true), ); var headers =
[ Header.ascii(':authority', serverRoot.authority), Header.ascii(':method', 'GET'), Header.ascii(':path', serverRoot.replace(path: '/push').path), Header.ascii(':scheme', serverRoot.scheme), ]; var stream = connection.makeRequest(headers, endStream: true); var bb = await stream.incomingMessages .where((s) => s is DataStreamMessage) .cast() .fold(BytesBuilder(), (out, msg) => out..add(msg.bytes)); // Check that main body was sent expect(utf8.decode(bb.takeBytes()), 'ok'); var pushes = await stream.peerPushes.toList(); expect(pushes, hasLength(2)); var pushA = pushes[0], pushB = pushes[1]; String getPath(TransportStreamPush p) => ascii.decode(p.requestHeaders .firstWhere((h) => ascii.decode(h.name) == ':path') .value); /* Future getBody(ClientTransportStream stream) async { await stream.outgoingMessages.close(); var bb = await stream.incomingMessages .map((s) { if (s is HeadersStreamMessage) { for (var h in s.headers) { print('${ASCII.decode(h.name)}: ${ASCII.decode(h.value)}'); } } else if (s is DataStreamMessage) { print(UTF8.decode(s.bytes)); } return s; }) .where((s) => s is DataStreamMessage) .cast() .fold( BytesBuilder(), (out, msg) => out..add(msg.bytes)); return UTF8.decode(bb.takeBytes()); } */ expect(getPath(pushA), '/a'); expect(getPath(pushB), '/b'); // However, Chrome, Firefox, Edge all can //expect(await getBody(pushA.stream), 'a'); //expect(await getBody(pushB.stream), 'b'); }); group('body parsing', () { test('urlencoded body parsed', () async { var response = await client.post( serverRoot.replace(path: '/body'), headers: { 'accept': 'application/json', 'content-type': 'application/x-www-form-urlencoded' }, body: 'foo=bar', ); expect(response.body, json.encode({'foo': 'bar'})); }); test('json body parsed', () async { var response = await client.post(serverRoot.replace(path: '/body'), headers: { 'accept': 'application/json', 'content-type': 'application/json' }, body: json.encode({'foo': 'bar'})); expect(response.body, json.encode({'foo': 'bar'})); }); test('multipart body parsed', () async { var rq = http.MultipartRequest('POST', serverRoot.replace(path: '/upload')); rq.headers.addAll({'accept': 'application/json'}); rq.fields['foo'] = 'bar'; rq.files.add(http.MultipartFile( 'file', Stream.fromIterable([utf8.encode('hello world')]), 11, contentType: MediaType('angel', 'framework'))); var response = await client.send(rq); var responseBody = await response.stream.transform(utf8.decoder).join(); expect( responseBody, json.encode([ 11, 'angel/framework', {'foo': 'bar'} ])); }); }); }