/// 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 {}}); /// 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 headers; } /// Per-test programmable routes, plus a record of what was asked. class Router { final Map> _routes = >{}; /// Every path requested so far, in order, query string included. final List calls = []; /// The `User-Agent` of every request so far, in order. final List userAgents = []; /// 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]; } /// 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 replies) { _routes[path] = List.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({'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 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 stop() => _server.close(force: true); Future _serve() async { await for (final HttpRequest request in _server) { await request.drain(); 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 header in reply.headers.entries) { response.headers.set(header.key, header.value); } response.add(utf8.encode(reply.body)); await response.close(); } } }