platform/packages/framework/test/http2/adapter_test.dart
2024-09-22 21:39:29 -07:00

305 lines
9.2 KiB
Dart

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<List<int>> 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>[
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<DataStreamMessage>()
.fold<BytesBuilder>(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<String> 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<DataStreamMessage>()
.fold<BytesBuilder>(
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'}
]));
});
});
}