// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:convert'; import 'package:fake_async/fake_async.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart' as io; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/net.dart'; import 'package:flutter_tools/src/base/platform.dart'; import '../../src/common.dart'; import '../../src/fake_http_client.dart'; void main() { late BufferLogger testLogger; setUp(() { testLogger = BufferLogger.test(); }); Net createNet(io.HttpClient client) { return Net( httpClientFactory: () => client, logger: testLogger, platform: FakePlatform(), ); } group('successful fetch', () { const String responseString = 'response string'; late List<int> responseData; setUp(() { responseData = utf8.encode(responseString); }); testWithoutContext('fetchUrl() gets the data', () async { final Net net = createNet( FakeHttpClient.list(<FakeRequest>[ FakeRequest(Uri.parse('http://example.invalid/'), response: FakeResponse( body: utf8.encode(responseString), )), ]) ); final List<int>? data = await net.fetchUrl(Uri.parse('http://example.invalid/')); expect(data, equals(responseData)); }); testWithoutContext('fetchUrl(destFile) writes the data to a file', () async { final Net net = createNet( FakeHttpClient.list(<FakeRequest>[ FakeRequest(Uri.parse('http://example.invalid/'), response: FakeResponse( body: utf8.encode(responseString), )), ]) ); final MemoryFileSystem fileSystem = MemoryFileSystem.test(); final File destFile = fileSystem.file('dest_file')..createSync(); final List<int>? data = await net.fetchUrl( Uri.parse('http://example.invalid/'), destFile: destFile, ); expect(data, equals(<int>[])); expect(destFile.readAsStringSync(), responseString); }); }); testWithoutContext('retry from 500', () async { final Net net = createNet( FakeHttpClient.list(<FakeRequest>[ FakeRequest(Uri.parse('http://example.invalid/'), response: const FakeResponse(statusCode: io.HttpStatus.internalServerError)), FakeRequest(Uri.parse('http://example.invalid/'), response: const FakeResponse(statusCode: io.HttpStatus.internalServerError)), FakeRequest(Uri.parse('http://example.invalid/'), response: const FakeResponse(statusCode: io.HttpStatus.internalServerError)), FakeRequest(Uri.parse('http://example.invalid/'), response: const FakeResponse(statusCode: io.HttpStatus.internalServerError)), ]) ); await net.fetchUrl(Uri.parse('http://example.invalid/'), maxAttempts: 4, durationOverride: Duration.zero); 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 -- retry 4\n', ); expect(testLogger.errorText, isEmpty); }); testWithoutContext('retry from network error', () async { final Uri invalid = Uri.parse('http://example.invalid/'); final Net net = createNet( FakeHttpClient.list(<FakeRequest>[ FakeRequest(invalid, responseError: const io.SocketException('test')), FakeRequest(invalid, responseError: const io.SocketException('test')), FakeRequest(invalid, responseError: const io.SocketException('test')), FakeRequest(invalid, responseError: const io.SocketException('test')), ]) ); await net.fetchUrl(Uri.parse('http://example.invalid/'), maxAttempts: 4, durationOverride: Duration.zero); 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 -- retry 4\n', ); expect(testLogger.errorText, isEmpty); }); testWithoutContext('retry from SocketException', () async { final Uri invalid = Uri.parse('http://example.invalid/'); final Net net = createNet( FakeHttpClient.list(<FakeRequest>[ FakeRequest(invalid, responseError: const io.SocketException('')), FakeRequest(invalid, responseError: const io.SocketException('')), FakeRequest(invalid, responseError: const io.SocketException('')), FakeRequest(invalid, responseError: const io.SocketException('')), ]) ); String? error; FakeAsync().run((FakeAsync time) { net.fetchUrl(invalid).then((List<int>? value) async { 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: SocketException')); }); testWithoutContext('no retry from HandshakeException', () async { final Uri invalid = Uri.parse('http://example.invalid/'); final Net net = createNet( FakeHttpClient.list(<FakeRequest>[ FakeRequest(invalid, responseError: const io.HandshakeException()), FakeRequest(invalid, responseError: const io.HandshakeException()), FakeRequest(invalid, responseError: const io.HandshakeException()), FakeRequest(invalid, responseError: const io.HandshakeException()), ]) ); String? error; FakeAsync().run((FakeAsync time) { net.fetchUrl(invalid).then((List<int>? value) async { 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')); }); testWithoutContext('check for bad override on ArgumentError', () async { final Uri invalid = Uri.parse('example.invalid/'); final Net net = Net( httpClientFactory: () { return FakeHttpClient.list(<FakeRequest>[ FakeRequest(invalid, responseError: ArgumentError()), ]); }, logger: testLogger, platform: FakePlatform( environment: <String, String>{ 'FLUTTER_STORAGE_BASE_URL': 'example.invalid', }, ), ); String? error; FakeAsync().run((FakeAsync time) { net.fetchUrl(Uri.parse('example.invalid/')).then((List<int>? value) async { 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')); }); testWithoutContext('retry from HttpException', () async { final Uri invalid = Uri.parse('http://example.invalid/'); final Net net = createNet( FakeHttpClient.list(<FakeRequest>[ FakeRequest(invalid, responseError: const io.HttpException('')), FakeRequest(invalid, responseError: const io.HttpException('')), FakeRequest(invalid, responseError: const io.HttpException('')), FakeRequest(invalid, responseError: const io.HttpException('')), ]) ); String? error; FakeAsync().run((FakeAsync time) { net.fetchUrl(invalid).then((List<int>? value) async { 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')); }); testWithoutContext('retry from HttpException when request throws', () async { final Uri invalid = Uri.parse('http://example.invalid/'); final Net net = createNet( FakeHttpClient.list(<FakeRequest>[ FakeRequest(invalid, responseError: const io.HttpException('')), FakeRequest(invalid, responseError: const io.HttpException('')), FakeRequest(invalid, responseError: const io.HttpException('')), FakeRequest(invalid, responseError: const io.HttpException('')), ]) ); String? error; FakeAsync().run((FakeAsync time) { net.fetchUrl(invalid).then((List<int>? value) async { 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')); }); testWithoutContext('max attempts', () async { final Uri invalid = Uri.parse('http://example.invalid/'); final Net net = createNet( FakeHttpClient.list(<FakeRequest>[ FakeRequest(invalid, response: const FakeResponse( statusCode: HttpStatus.internalServerError, )), FakeRequest(invalid, response: const FakeResponse( statusCode: HttpStatus.internalServerError, )), FakeRequest(invalid, response: const FakeResponse( statusCode: HttpStatus.internalServerError, )), ]) ); String? error; List<int>? actualResult; FakeAsync().run((FakeAsync time) { net.fetchUrl(invalid, maxAttempts: 3).then((List<int>? value) async { 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); }); testWithoutContext('remote file non-existent', () async { final Uri invalid = Uri.parse('http://example.invalid/'); final Net net = createNet( FakeHttpClient.list(<FakeRequest>[ FakeRequest(invalid, method: HttpMethod.head, response: const FakeResponse( statusCode: HttpStatus.notFound, )), ]) ); final bool result = await net.doesRemoteFileExist(invalid); expect(result, false); }); testWithoutContext('remote file server error', () async { final Uri valid = Uri.parse('http://example.valid/'); final Net net = createNet( FakeHttpClient.list(<FakeRequest>[ FakeRequest(valid, method: HttpMethod.head, response: const FakeResponse( statusCode: HttpStatus.internalServerError, )), ]) ); final bool result = await net.doesRemoteFileExist(valid); expect(result, false); }); testWithoutContext('remote file exists', () async { final Uri valid = Uri.parse('http://example.valid/'); final Net net = createNet( FakeHttpClient.list(<FakeRequest>[ FakeRequest(valid, method: HttpMethod.head), ]) ); final bool result = await net.doesRemoteFileExist(valid); expect(result, true); }); }