summaryrefslogtreecommitdiffstats
path: root/test/server.dart
blob: b8f28de900744119c5ac3d5595cdc32949ec62b7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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();
    }
  }
}