summaryrefslogtreecommitdiffstats
path: root/test/server.dart
diff options
context:
space:
mode:
Diffstat (limited to 'test/server.dart')
-rw-r--r--test/server.dart126
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();
+ }
+ }
+}