net_test.dart 13.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:convert';
7

8 9 10
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
11
import 'package:flutter_tools/src/base/io.dart' as io;
12
import 'package:flutter_tools/src/base/logger.dart';
13
import 'package:flutter_tools/src/base/net.dart';
14 15
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:platform/platform.dart';
16 17
import 'package:quiver/testing/async.dart';

18
import '../../src/common.dart';
19
import '../../src/mocks.dart' show MockStdio;
20 21

void main() {
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
  BufferLogger testLogger;

  setUp(() {
    testLogger = BufferLogger(
      terminal: AnsiTerminal(
        stdio: MockStdio(),
        platform: FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false,
      ),
      outputPreferences: OutputPreferences.test(),
    );
  });

  Net createNet(io.HttpClient client) {
    return Net(
      httpClientFactory: () => client,
      logger: testLogger,
      platform: FakePlatform.fromPlatform(const LocalPlatform()),
    );
  }

42 43 44 45 46 47 48 49
  group('successful fetch', () {
    const String responseString = 'response string';
    List<int> responseData;

    setUp(() {
      responseData = utf8.encode(responseString);
    });

50 51 52
    testWithoutContext('fetchUrl() gets the data', () async {
      final Net net = createNet(FakeHttpClient(200, data: responseString));
      final List<int> data = await net.fetchUrl(Uri.parse('http://example.invalid/'));
53 54 55
      expect(data, equals(responseData));
    });

56 57 58 59 60
    testWithoutContext('fetchUrl(destFile) writes the data to a file', () async {
      final Net net = createNet(FakeHttpClient(200, data: responseString));
      final MemoryFileSystem fs = MemoryFileSystem();
      final File destFile = fs.file('dest_file')..createSync();
      final List<int> data = await net.fetchUrl(
61 62 63 64 65 66 67 68
        Uri.parse('http://example.invalid/'),
        destFile: destFile,
      );
      expect(data, equals(<int>[]));
      expect(destFile.readAsStringSync(), equals(responseString));
    });
  });

69 70
  testWithoutContext('retry from 500', () async {
    final Net net = createNet(FakeHttpClient(500));
71
    String error;
72
    FakeAsync().run((FakeAsync time) {
73
      net.fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
74
        error = 'test completed unexpectedly';
75 76
      }, onError: (dynamic exception) {
        error = 'test failed unexpectedly: $exception';
77 78 79 80 81 82 83
      });
      expect(testLogger.statusText, '');
      time.elapse(const Duration(milliseconds: 10000));
      expect(testLogger.statusText,
        'Download failed -- attempting retry 1 in 1 second...\n'
        'Download failed -- attempting retry 2 in 2 seconds...\n'
        'Download failed -- attempting retry 3 in 4 seconds...\n'
84
        'Download failed -- attempting retry 4 in 8 seconds...\n',
85 86 87 88 89 90
      );
    });
    expect(testLogger.errorText, isEmpty);
    expect(error, isNull);
  });

91 92
  testWithoutContext('retry from network error', () async {
    final Net net = createNet(FakeHttpClient(200));
93
    String error;
94
    FakeAsync().run((FakeAsync time) {
95
      net.fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
96
        error = 'test completed unexpectedly';
97 98
      }, onError: (dynamic exception) {
        error = 'test failed unexpectedly: $exception';
99 100 101 102 103 104 105
      });
      expect(testLogger.statusText, '');
      time.elapse(const Duration(milliseconds: 10000));
      expect(testLogger.statusText,
        'Download failed -- attempting retry 1 in 1 second...\n'
        'Download failed -- attempting retry 2 in 2 seconds...\n'
        'Download failed -- attempting retry 3 in 4 seconds...\n'
106
        'Download failed -- attempting retry 4 in 8 seconds...\n',
107 108 109 110 111
      );
    });
    expect(testLogger.errorText, isEmpty);
    expect(error, isNull);
  });
112

113 114 115 116
  testWithoutContext('retry from SocketException', () async {
    final Net net = createNet(FakeHttpClientThrowing(
      const io.SocketException('test exception handling'),
    ));
117
    String error;
118
    FakeAsync().run((FakeAsync time) {
119
      net.fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
120 121 122 123 124 125 126 127 128 129
        error = 'test completed unexpectedly';
      }, onError: (dynamic exception) {
        error = 'test failed unexpectedly: $exception';
      });
      expect(testLogger.statusText, '');
      time.elapse(const Duration(milliseconds: 10000));
      expect(testLogger.statusText,
        'Download failed -- attempting retry 1 in 1 second...\n'
        'Download failed -- attempting retry 2 in 2 seconds...\n'
        'Download failed -- attempting retry 3 in 4 seconds...\n'
130
        'Download failed -- attempting retry 4 in 8 seconds...\n',
131 132 133 134 135 136 137
      );
    });
    expect(testLogger.errorText, isEmpty);
    expect(error, isNull);
    expect(testLogger.traceText, contains('Download error: SocketException'));
  });

138 139 140 141
  testWithoutContext('no retry from HandshakeException', () async {
    final Net net = createNet(FakeHttpClientThrowing(
      const io.HandshakeException('test exception handling'),
    ));
142
    String error;
143
    FakeAsync().run((FakeAsync time) {
144
      net.fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
145 146 147 148 149 150 151 152 153 154 155
        error = 'test completed unexpectedly';
      }, onError: (dynamic exception) {
        error = 'test failed: $exception';
      });
      expect(testLogger.statusText, '');
      time.elapse(const Duration(milliseconds: 10000));
      expect(testLogger.statusText, '');
    });
    expect(error, startsWith('test failed'));
    expect(testLogger.traceText, contains('HandshakeException'));
  });
156

157 158 159 160 161 162 163 164 165 166 167
  testWithoutContext('check for bad override on ArgumentError', () async {
    final Net net = Net(
      httpClientFactory: () => FakeHttpClientThrowing(
        ArgumentError('test exception handling'),
      ),
      logger: testLogger,
      platform: FakePlatform.fromPlatform(const LocalPlatform())
        ..environment = <String, String>{
          'FLUTTER_STORAGE_BASE_URL': 'example.invalid',
        },
    );
168 169
    String error;
    FakeAsync().run((FakeAsync time) {
170
      net.fetchUrl(Uri.parse('example.invalid/')).then((List<int> value) {
171 172 173 174 175 176 177 178 179 180 181 182 183
        error = 'test completed unexpectedly';
      }, onError: (dynamic exception) {
        error = 'test failed: $exception';
      });
      expect(testLogger.statusText, '');
      time.elapse(const Duration(milliseconds: 10000));
      expect(testLogger.statusText, '');
    });
    expect(error, startsWith('test failed'));
    expect(testLogger.errorText, contains('Invalid argument'));
    expect(error, contains('FLUTTER_STORAGE_BASE_URL'));
  });

184 185 186 187
  testWithoutContext('retry from HttpException', () async {
    final Net net = createNet(FakeHttpClientThrowing(
      const io.HttpException('test exception handling'),
    ));
188 189
    String error;
    FakeAsync().run((FakeAsync time) {
190
      net.fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
        error = 'test completed unexpectedly';
      }, onError: (dynamic exception) {
        error = 'test failed unexpectedly: $exception';
      });
      expect(testLogger.statusText, '');
      time.elapse(const Duration(milliseconds: 10000));
      expect(testLogger.statusText,
        'Download failed -- attempting retry 1 in 1 second...\n'
        'Download failed -- attempting retry 2 in 2 seconds...\n'
        'Download failed -- attempting retry 3 in 4 seconds...\n'
        'Download failed -- attempting retry 4 in 8 seconds...\n',
      );
    });
    expect(testLogger.errorText, isEmpty);
    expect(error, isNull);
    expect(testLogger.traceText, contains('Download error: HttpException'));
207 208
  });

209 210 211 212
  testWithoutContext('retry from HttpException when request throws', () async {
    final Net net = createNet(FakeHttpClientThrowingRequest(
      const io.HttpException('test exception handling'),
    ));
213 214
    String error;
    FakeAsync().run((FakeAsync time) {
215
      net.fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
        error = 'test completed unexpectedly';
      }, onError: (dynamic exception) {
        error = 'test failed unexpectedly: $exception';
      });
      expect(testLogger.statusText, '');
      time.elapse(const Duration(milliseconds: 10000));
      expect(testLogger.statusText,
        'Download failed -- attempting retry 1 in 1 second...\n'
        'Download failed -- attempting retry 2 in 2 seconds...\n'
        'Download failed -- attempting retry 3 in 4 seconds...\n'
        'Download failed -- attempting retry 4 in 8 seconds...\n',
      );
    });
    expect(testLogger.errorText, isEmpty);
    expect(error, isNull);
    expect(testLogger.traceText, contains('Download error: HttpException'));
232 233
  });

234 235
  testWithoutContext('max attempts', () async {
    final Net net = createNet(FakeHttpClient(500));
236 237 238
    String error;
    List<int> actualResult;
    FakeAsync().run((FakeAsync time) {
239
      net.fetchUrl(Uri.parse('http://example.invalid/'), maxAttempts: 3).then((List<int> value) {
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
        actualResult = value;
      }, onError: (dynamic exception) {
        error = 'test failed unexpectedly: $exception';
      });
      expect(testLogger.statusText, '');
      time.elapse(const Duration(milliseconds: 10000));
      expect(testLogger.statusText,
        'Download failed -- attempting retry 1 in 1 second...\n'
        'Download failed -- attempting retry 2 in 2 seconds...\n'
        'Download failed -- retry 3\n',
      );
    });
    expect(testLogger.errorText, isEmpty);
    expect(error, isNull);
    expect(actualResult, isNull);
  });

257 258
  testWithoutContext('remote file non-existant', () async {
    final Net net = createNet(FakeHttpClient(404));
259
    final Uri invalid = Uri.parse('http://example.invalid/');
260
    final bool result = await net.doesRemoteFileExist(invalid);
261 262 263
    expect(result, false);
  });

264 265
  testWithoutContext('remote file server error', () async {
    final Net net = createNet(FakeHttpClient(500));
266
    final Uri valid = Uri.parse('http://example.valid/');
267
    final bool result = await net.doesRemoteFileExist(valid);
268 269 270
    expect(result, false);
  });

271 272
  testWithoutContext('remote file exists', () async {
    final Net net = createNet(FakeHttpClient(200));
273
    final Uri valid = Uri.parse('http://example.valid/');
274
    final bool result = await net.doesRemoteFileExist(valid);
275 276
    expect(result, true);
  });
277 278
}

279 280
class FakeHttpClientThrowing implements io.HttpClient {
  FakeHttpClientThrowing(this.exception);
281

282
  final Object exception;
283 284 285 286 287 288 289 290 291 292

  @override
  Future<io.HttpClientRequest> getUrl(Uri url) async {
    throw exception;
  }

  @override
  dynamic noSuchMethod(Invocation invocation) {
    throw 'io.HttpClient - $invocation';
  }
293 294
}

295
class FakeHttpClient implements io.HttpClient {
296
  FakeHttpClient(this.statusCode, { this.data });
297 298

  final int statusCode;
299
  final String data;
300 301 302

  @override
  Future<io.HttpClientRequest> getUrl(Uri url) async {
303
    return FakeHttpClientRequest(statusCode, data: data);
304 305
  }

306 307
  @override
  Future<io.HttpClientRequest> headUrl(Uri url) async {
308
    return FakeHttpClientRequest(statusCode);
309 310
  }

311 312 313 314 315 316
  @override
  dynamic noSuchMethod(Invocation invocation) {
    throw 'io.HttpClient - $invocation';
  }
}

317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
class FakeHttpClientThrowingRequest implements io.HttpClient {
  FakeHttpClientThrowingRequest(this.exception);

  final Object exception;

  @override
  Future<io.HttpClientRequest> getUrl(Uri url) async {
    return FakeHttpClientRequestThrowing(exception);
  }

  @override
  dynamic noSuchMethod(Invocation invocation) {
    throw 'io.HttpClient - $invocation';
  }
}

class FakeHttpClientRequest implements io.HttpClientRequest {
334
  FakeHttpClientRequest(this.statusCode, { this.data });
335 336

  final int statusCode;
337
  final String data;
338 339 340

  @override
  Future<io.HttpClientResponse> close() async {
341
    return FakeHttpClientResponse(statusCode, data: data);
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
  }

  @override
  dynamic noSuchMethod(Invocation invocation) {
    throw 'io.HttpClientRequest - $invocation';
  }
}

class FakeHttpClientRequestThrowing implements io.HttpClientRequest {
  FakeHttpClientRequestThrowing(this.exception);

  final Object exception;

  @override
  Future<io.HttpClientResponse> close() async {
    throw exception;
358 359 360 361 362 363 364 365
  }

  @override
  dynamic noSuchMethod(Invocation invocation) {
    throw 'io.HttpClientRequest - $invocation';
  }
}

366
class FakeHttpClientResponse implements io.HttpClientResponse {
367
  FakeHttpClientResponse(this.statusCode, { this.data });
368 369 370 371

  @override
  final int statusCode;

372 373
  final String data;

374 375 376 377
  @override
  String get reasonPhrase => '<reason phrase>';

  @override
378 379
  StreamSubscription<List<int>> listen(
    void onData(List<int> event), {
380 381 382
    Function onError,
    void onDone(),
    bool cancelOnError,
383
  }) {
384
    if (data == null) {
385
      return Stream<List<int>>.fromFuture(Future<List<int>>.error(
386 387 388
        const io.SocketException('test'),
      )).listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
    } else {
389 390
      return Stream<List<int>>.fromFuture(Future<List<int>>.value(
        utf8.encode(data),
391 392
      )).listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
    }
393 394
  }

395
  @override
396
  Future<dynamic> forEach(void Function(List<int> element) action) async {
397 398 399 400 401
    if (data == null) {
      return Future<void>.error(const io.SocketException('test'));
    } else {
      return Future<void>.microtask(() => action(utf8.encode(data)));
    }
402 403
  }

404 405 406 407 408
  @override
  dynamic noSuchMethod(Invocation invocation) {
    throw 'io.HttpClientResponse - $invocation';
  }
}