diff options
Diffstat (limited to 'test/server.dart')
| -rw-r--r-- | test/server.dart | 126 |
1 files changed, 126 insertions, 0 deletions
diff --git a/test/server.dart b/test/server.dart new file mode 100644 index 0000000..b8f28de --- /dev/null +++ b/test/server.dart @@ -0,0 +1,126 @@ +/// A throwaway HTTP server on a random loopback port. +/// +/// The suite points a real `Client` at a real socket: no test touches the +/// network, and no test stubs the HTTP layer. The code path exercised is the +/// one that runs in production, request construction and error body reading +/// included. +/// +/// It doubles as the test that `baseUrl` works — if it did not, none of this +/// would run. +library; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +/// What the fake server should answer for one request. +class Reply { + /// Creates an answer with [status], [body] and any extra [headers]. + const Reply(this.status, this.body, + {this.headers = const <String, String>{}}); + + /// HTTP status to answer with. + final int status; + + /// Body to write, encoded as UTF-8. + final String body; + + /// Extra response headers, such as `Retry-After`. + final Map<String, String> headers; +} + +/// Per-test programmable routes, plus a record of what was asked. +class Router { + final Map<String, List<Reply>> _routes = <String, List<Reply>>{}; + + /// Every path requested so far, in order, query string included. + final List<String> calls = <String>[]; + + /// The `User-Agent` of every request so far, in order. + final List<String> userAgents = <String>[]; + + /// Answers any path with no registered route. + Reply Function(String path)? fallback; + + /// Registers the single answer for [path]. + void respond(String path, Reply reply) { + _routes[path] = <Reply>[reply]; + } + + /// Registers a sequence of answers for [path], one per request. + /// + /// The last entry repeats once the sequence runs out, which is how retrying + /// is tested. + void respondInSequence(String path, List<Reply> replies) { + _routes[path] = List<Reply>.of(replies); + } + + /// Records the call and returns the answer registered for [path]. + Reply next(String path) { + calls.add(path); + final replies = _routes[path]; + if (replies == null) { + final handler = fallback; + if (handler != null) { + return handler(path); + } + return Reply( + 404, jsonEncode(<String, String>{'erro': 'cep_nao_encontrado'})); + } + return replies.length > 1 ? replies.removeAt(0) : replies.first; + } + + /// How many requests so far began with [prefix]. + int count(String prefix) => + calls.where((String call) => call.startsWith(prefix)).length; +} + +/// A running fake API, with its router. +class FakeServer { + FakeServer._(this._server, this.routes); + + final HttpServer _server; + + /// The routes this server answers from. + final Router routes; + + /// Starts a fake API on `127.0.0.1` and a port the kernel picks. + static Future<FakeServer> start() async { + final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); + final fake = FakeServer._(server, Router()); + unawaited(fake._serve()); + return fake; + } + + /// Where to point a client's `baseUrl`. + String get url => 'http://127.0.0.1:${_server.port}'; + + /// Stops the server and drops any open connection. + Future<void> stop() => _server.close(force: true); + + Future<void> _serve() async { + await for (final HttpRequest request in _server) { + await request.drain<void>(); + routes.userAgents.add( + request.headers.value(HttpHeaders.userAgentHeader) ?? '', + ); + + final uri = request.uri; + final path = uri.query.isEmpty ? uri.path : '${uri.path}?${uri.query}'; + final reply = routes.next(path); + + final response = request.response + ..statusCode = reply.status + ..headers.contentType = ContentType( + 'application', + 'json', + charset: 'utf-8', + ); + for (final MapEntry<String, String> header in reply.headers.entries) { + response.headers.set(header.key, header.value); + } + response.add(utf8.encode(reply.body)); + await response.close(); + } + } +} |