Line data Source code
1 : // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 : // for details. All rights reserved. Use of this source code is governed by a 3 : // BSD-style license that can be found in the LICENSE file. 4 : 5 : import 'dart:async'; 6 : import 'dart:convert'; 7 : import 'dart:io'; 8 : import 'dart:typed_data'; 9 : import 'package:test/test.dart'; 10 : 11 : /// The current server instance. 12 : HttpServer? _server; 13 : 14 1 : Encoding requiredEncodingForCharset(String charset) => 15 1 : Encoding.getByName(charset) ?? 16 0 : (throw FormatException('Unsupported encoding "$charset".')); 17 : 18 : /// The URL for the current server instance. 19 8 : Uri get serverUrl => Uri.parse('http://localhost:${_server?.port}'); 20 : 21 : /// Starts a new HTTP server. 22 2 : Future<void> startServer() async { 23 4 : _server = (await HttpServer.bind('localhost', 0)) 24 4 : ..listen((request) async { 25 4 : var path = request.uri.path; 26 2 : var response = request.response; 27 : 28 2 : if (path == '/error') { 29 : const content = 'error'; 30 : response 31 2 : ..statusCode = 400 32 4 : ..contentLength = content.length 33 2 : ..write(content); 34 2 : response.close(); 35 : return; 36 : } 37 : 38 2 : if (path == '/loop') { 39 0 : var n = int.parse(request.uri.query); 40 : response 41 0 : ..statusCode = 302 42 0 : ..headers 43 0 : .set('location', serverUrl.resolve('/loop?${n + 1}').toString()) 44 0 : ..contentLength = 0; 45 0 : response.close(); 46 : return; 47 : } 48 : 49 2 : if (path == '/redirect') { 50 : response 51 1 : ..statusCode = 302 52 5 : ..headers.set('location', serverUrl.resolve('/').toString()) 53 1 : ..contentLength = 0; 54 1 : response.close(); 55 : return; 56 : } 57 : 58 2 : if (path == '/no-content-length') { 59 : response 60 0 : ..statusCode = 200 61 0 : ..contentLength = -1 62 0 : ..write('body'); 63 0 : response.close(); 64 : return; 65 : } 66 : 67 2 : if (path == '/list') { 68 3 : response.headers.contentType = ContentType('application', 'json'); 69 : response 70 1 : ..statusCode = 200 71 2 : ..contentLength = -1 72 1 : ..write('[1,2,3]'); 73 1 : response.close(); 74 : return; 75 : } 76 : 77 2 : if (path == '/download') { 78 : const content = 'I am a text file'; 79 2 : response.headers.set('content-encoding', 'plain'); 80 : response 81 1 : ..statusCode = 200 82 2 : ..contentLength = content.length 83 1 : ..write(content); 84 : 85 3 : Future.delayed(Duration(milliseconds: 300), () { 86 1 : response.close(); 87 : }); 88 : return; 89 : } 90 : 91 3 : var requestBodyBytes = await ByteStream(request).toBytes(); 92 3 : var encodingName = request.uri.queryParameters['response-encoding']; 93 : var outputEncoding = encodingName == null 94 : ? ascii 95 0 : : requiredEncodingForCharset(encodingName); 96 : 97 2 : response.headers.contentType = 98 2 : ContentType('application', 'json', charset: outputEncoding.name); 99 2 : response.headers.set('single', 'value'); 100 : 101 : dynamic requestBody; 102 1 : if (requestBodyBytes.isEmpty) { 103 : requestBody = null; 104 3 : } else if (request.headers.contentType?.charset != null) { 105 : var encoding = 106 4 : requiredEncodingForCharset(request.headers.contentType!.charset!); 107 1 : requestBody = encoding.decode(requestBodyBytes); 108 : } else { 109 : requestBody = requestBodyBytes; 110 : } 111 : 112 1 : var content = <String, dynamic>{ 113 1 : 'method': request.method, 114 2 : 'path': request.uri.path, 115 2 : 'query': request.uri.query, 116 1 : 'headers': {} 117 : }; 118 1 : if (requestBody != null) content['body'] = requestBody; 119 3 : request.headers.forEach((name, values) { 120 : // These headers are automatically generated by dart:io, so we don't 121 : // want to test them here. 122 2 : if (name == 'cookie' || name == 'host') return; 123 : 124 2 : content['headers'][name] = values; 125 : }); 126 : 127 1 : var body = json.encode(content); 128 : response 129 2 : ..contentLength = body.length 130 1 : ..write(body); 131 1 : response.close(); 132 : }); 133 : } 134 : 135 : /// Stops the current HTTP server. 136 2 : void stopServer() { 137 : if (_server != null) { 138 2 : _server!.close(); 139 : _server = null; 140 : } 141 : } 142 : 143 : /// A matcher for functions that throw SocketException. 144 0 : final Matcher throwsSocketException = 145 : throwsA(const TypeMatcher<SocketException>()); 146 : 147 : /// A stream of chunks of bytes representing a single piece of data. 148 : class ByteStream extends StreamView<List<int>> { 149 2 : ByteStream(Stream<List<int>> stream) : super(stream); 150 : 151 : /// Returns a single-subscription byte stream that will emit the given bytes 152 : /// in a single chunk. 153 0 : factory ByteStream.fromBytes(List<int> bytes) => 154 0 : ByteStream(Stream.fromIterable([bytes])); 155 : 156 : /// Collects the data of this stream in a [Uint8List]. 157 1 : Future<Uint8List> toBytes() { 158 1 : var completer = Completer<Uint8List>(); 159 1 : var sink = ByteConversionSink.withCallback( 160 3 : (bytes) => completer.complete(Uint8List.fromList(bytes))); 161 2 : listen(sink.add, 162 1 : onError: completer.completeError, 163 1 : onDone: sink.close, 164 : cancelOnError: true); 165 1 : return completer.future; 166 : } 167 : 168 : /// Collect the data of this stream in a [String], decoded according to 169 : /// [encoding], which defaults to `UTF8`. 170 0 : Future<String> bytesToString([Encoding encoding = utf8]) => 171 0 : encoding.decodeStream(this); 172 : 173 0 : Stream<String> toStringStream([Encoding encoding = utf8]) => 174 0 : encoding.decoder.bind(this); 175 : }