| // 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:async'; |
| import 'dart:io'; |
| |
| import 'package:flutter_tools/src/base/common.dart'; |
| import 'package:flutter_tools/src/base/file_system.dart'; |
| import 'package:flutter_tools/src/base/io.dart'; |
| import 'package:flutter_tools/src/base/platform.dart'; |
| import 'package:flutter_tools/src/convert.dart'; |
| import 'package:flutter_tools/src/web/devfs_web.dart'; |
| import 'package:mockito/mockito.dart'; |
| |
| import '../../src/common.dart'; |
| import '../../src/testbed.dart'; |
| |
| const List<int> kTransparentImage = <int>[ |
| 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, |
| 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, |
| 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, |
| 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, |
| 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, |
| ]; |
| |
| void main() { |
| MockHttpServer mockHttpServer; |
| StreamController<HttpRequest> requestController; |
| Testbed testbed; |
| MockHttpRequest request; |
| MockHttpResponse response; |
| MockHttpHeaders headers; |
| Completer<void> closeCompleter; |
| WebAssetServer webAssetServer; |
| MockPlatform windows; |
| MockPlatform linux; |
| |
| setUp(() { |
| windows = MockPlatform(); |
| linux = MockPlatform(); |
| when(windows.environment).thenReturn(const <String, String>{}); |
| when(windows.isWindows).thenReturn(true); |
| when(linux.isWindows).thenReturn(false); |
| when(linux.environment).thenReturn(const <String, String>{}); |
| testbed = Testbed(setup: () { |
| mockHttpServer = MockHttpServer(); |
| requestController = StreamController<HttpRequest>.broadcast(); |
| request = MockHttpRequest(); |
| response = MockHttpResponse(); |
| headers = MockHttpHeaders(); |
| closeCompleter = Completer<void>(); |
| when(mockHttpServer.listen(any, onError: anyNamed('onError'))).thenAnswer((Invocation invocation) { |
| final void Function(HttpRequest) callback = invocation.positionalArguments.first as void Function(HttpRequest); |
| return requestController.stream.listen(callback); |
| }); |
| when(request.response).thenReturn(response); |
| when(response.headers).thenReturn(headers); |
| when(response.close()).thenAnswer((Invocation invocation) async { |
| closeCompleter.complete(); |
| }); |
| webAssetServer = WebAssetServer(mockHttpServer, onError: (dynamic error, StackTrace stackTrace) { |
| closeCompleter.completeError(error, stackTrace); |
| }); |
| }); |
| }); |
| |
| tearDown(() async { |
| await webAssetServer.dispose(); |
| await requestController.close(); |
| }); |
| |
| test('Throws a tool exit if bind fails with a SocketException', () => testbed.run(() async { |
| expect(WebAssetServer.start('hello', 1234), throwsA(isInstanceOf<ToolExit>())); |
| })); |
| |
| test('Can catch exceptions through the onError callback', () => testbed.run(() async { |
| when(response.close()).thenAnswer((Invocation invocation) { |
| throw StateError('Something bad'); |
| }); |
| webAssetServer.writeFile('/foo.js', 'main() {}'); |
| |
| when(request.uri).thenReturn(Uri.parse('http://foobar/foo.js')); |
| requestController.add(request); |
| |
| expect(closeCompleter.future, throwsA(isInstanceOf<StateError>())); |
| })); |
| |
| test('Handles against malformed manifest', () => testbed.run(() async { |
| final File source = fs.file('source') |
| ..writeAsStringSync('main() {}'); |
| final File sourcemap = fs.file('sourcemap') |
| ..writeAsStringSync('{}'); |
| |
| // Missing ending offset. |
| final File manifestMissingOffset = fs.file('manifestA') |
| ..writeAsStringSync(json.encode(<String, Object>{'/foo.js': <String, Object>{ |
| 'code': <int>[0], |
| 'sourcemap': <int>[0], |
| }})); |
| final File manifestOutOfBounds = fs.file('manifest') |
| ..writeAsStringSync(json.encode(<String, Object>{'/foo.js': <String, Object>{ |
| 'code': <int>[0, 100], |
| 'sourcemap': <int>[0], |
| }})); |
| |
| expect(webAssetServer.write(source, manifestMissingOffset, sourcemap), isEmpty); |
| expect(webAssetServer.write(source, manifestOutOfBounds, sourcemap), isEmpty); |
| })); |
| |
| test('serves JavaScript files from in memory cache', () => testbed.run(() async { |
| final File source = fs.file('source') |
| ..writeAsStringSync('main() {}'); |
| final File sourcemap = fs.file('sourcemap') |
| ..writeAsStringSync('{}'); |
| final File manifest = fs.file('manifest') |
| ..writeAsStringSync(json.encode(<String, Object>{'/foo.js': <String, Object>{ |
| 'code': <int>[0, source.lengthSync()], |
| 'sourcemap': <int>[0, 2], |
| }})); |
| webAssetServer.write(source, manifest, sourcemap); |
| |
| when(request.uri).thenReturn(Uri.parse('http://foobar/foo.js')); |
| requestController.add(request); |
| await closeCompleter.future; |
| |
| verify(headers.add('Content-Length', source.lengthSync())).called(1); |
| verify(headers.add('Content-Type', 'application/javascript')).called(1); |
| verify(response.add(source.readAsBytesSync())).called(1); |
| }, overrides: <Type, Generator>{ |
| Platform: () => linux, |
| })); |
| |
| test('serves JavaScript files from in memory cache on Windows', () => testbed.run(() async { |
| final File source = fs.file('source') |
| ..writeAsStringSync('main() {}'); |
| final File sourcemap = fs.file('sourcemap') |
| ..writeAsStringSync('{}'); |
| final File manifest = fs.file('manifest') |
| ..writeAsStringSync(json.encode(<String, Object>{'/C:/foo.js': <String, Object>{ |
| 'code': <int>[0, source.lengthSync()], |
| 'sourcemap': <int>[0, 2], |
| }})); |
| webAssetServer.write(source, manifest, sourcemap); |
| |
| when(request.uri).thenReturn(Uri.parse('http://foobar/C:/foo.js')); |
| requestController.add(request); |
| await closeCompleter.future; |
| |
| verify(headers.add('Content-Length', source.lengthSync())).called(1); |
| verify(headers.add('Content-Type', 'application/javascript')).called(1); |
| verify(response.add(source.readAsBytesSync())).called(1); |
| }, overrides: <Type, Generator>{ |
| Platform: () => windows, |
| })); |
| |
| test('serves JavaScript files from in memory cache not from manifest', () => testbed.run(() async { |
| webAssetServer.writeFile('/foo.js', 'main() {}'); |
| |
| when(request.uri).thenReturn(Uri.parse('http://foobar/foo.js')); |
| requestController.add(request); |
| await closeCompleter.future; |
| |
| verify(headers.add('Content-Length', 9)).called(1); |
| verify(headers.add('Content-Type', 'application/javascript')).called(1); |
| verify(response.add(any)).called(1); |
| })); |
| |
| test('handles missing JavaScript files from in memory cache', () => testbed.run(() async { |
| final File source = fs.file('source') |
| ..writeAsStringSync('main() {}'); |
| final File sourcemap = fs.file('sourcemap') |
| ..writeAsStringSync('{}'); |
| final File manifest = fs.file('manifest') |
| ..writeAsStringSync(json.encode(<String, Object>{'/foo.js': <String, Object>{ |
| 'code': <int>[0, source.lengthSync()], |
| 'sourcemap': <int>[0, 2], |
| }})); |
| webAssetServer.write(source, manifest, sourcemap); |
| |
| when(request.uri).thenReturn(Uri.parse('http://foobar/bar.js')); |
| requestController.add(request); |
| await closeCompleter.future; |
| |
| verify(response.statusCode = 404).called(1); |
| })); |
| |
| test('serves Dart files from in filesystem on Windows', () => testbed.run(() async { |
| final File source = fs.file('foo.dart').absolute |
| ..createSync(recursive: true) |
| ..writeAsStringSync('void main() {}'); |
| |
| when(request.uri).thenReturn(Uri.parse('http://foobar/C:/foo.dart')); |
| requestController.add(request); |
| await closeCompleter.future; |
| |
| verify(headers.add('Content-Length', source.lengthSync())).called(1); |
| verify(response.addStream(any)).called(1); |
| }, overrides: <Type, Generator>{ |
| Platform: () => windows, |
| })); |
| |
| test('serves Dart files from in filesystem on Linux/macOS', () => testbed.run(() async { |
| final File source = fs.file('foo.dart').absolute |
| ..createSync(recursive: true) |
| ..writeAsStringSync('void main() {}'); |
| |
| when(request.uri).thenReturn(Uri.parse('http://foobar/foo.dart')); |
| requestController.add(request); |
| await closeCompleter.future; |
| |
| verify(headers.add('Content-Length', source.lengthSync())).called(1); |
| verify(response.addStream(any)).called(1); |
| }, overrides: <Type, Generator>{ |
| Platform: () => linux, |
| })); |
| |
| test('Handles missing Dart files from filesystem', () => testbed.run(() async { |
| when(request.uri).thenReturn(Uri.parse('http://foobar/foo.dart')); |
| requestController.add(request); |
| await closeCompleter.future; |
| |
| verify(response.statusCode = 404).called(1); |
| })); |
| |
| test('serves asset files from in filesystem with known mime type', () => testbed.run(() async { |
| final File source = fs.file(fs.path.join('build', 'flutter_assets', 'foo.png')) |
| ..createSync(recursive: true) |
| ..writeAsBytesSync(kTransparentImage); |
| |
| when(request.uri).thenReturn(Uri.parse('http://foobar/assets/foo.png')); |
| requestController.add(request); |
| await closeCompleter.future; |
| |
| verify(headers.add('Content-Length', source.lengthSync())).called(1); |
| verify(headers.add('Content-Type', 'image/png')).called(1); |
| verify(response.addStream(any)).called(1); |
| })); |
| |
| test('serves asset files from in filesystem with known mime type on Windows', () => testbed.run(() async { |
| final File source = fs.file(fs.path.join('build', 'flutter_assets', 'foo.png')) |
| ..createSync(recursive: true) |
| ..writeAsBytesSync(kTransparentImage); |
| |
| when(request.uri).thenReturn(Uri.parse('http://foobar/assets/foo.png')); |
| requestController.add(request); |
| await closeCompleter.future; |
| |
| verify(headers.add('Content-Length', source.lengthSync())).called(1); |
| verify(headers.add('Content-Type', 'image/png')).called(1); |
| verify(response.addStream(any)).called(1); |
| }, overrides: <Type, Generator>{ |
| Platform: () => windows, |
| })); |
| |
| |
| test('serves asset files files from in filesystem with unknown mime type and length > 12', () => testbed.run(() async { |
| final File source = fs.file(fs.path.join('build', 'flutter_assets', 'foo')) |
| ..createSync(recursive: true) |
| ..writeAsBytesSync(List<int>.filled(100, 0)); |
| |
| when(request.uri).thenReturn(Uri.parse('http://foobar/assets/foo')); |
| requestController.add(request); |
| await closeCompleter.future; |
| |
| verify(headers.add('Content-Length', source.lengthSync())).called(1); |
| verify(headers.add('Content-Type', 'application/octet-stream')).called(1); |
| verify(response.addStream(any)).called(1); |
| })); |
| |
| test('serves asset files files from in filesystem with unknown mime type and length < 12', () => testbed.run(() async { |
| final File source = fs.file(fs.path.join('build', 'flutter_assets', 'foo')) |
| ..createSync(recursive: true) |
| ..writeAsBytesSync(<int>[1, 2, 3]); |
| |
| when(request.uri).thenReturn(Uri.parse('http://foobar/assets/foo')); |
| requestController.add(request); |
| await closeCompleter.future; |
| |
| verify(headers.add('Content-Length', source.lengthSync())).called(1); |
| verify(headers.add('Content-Type', 'application/octet-stream')).called(1); |
| verify(response.addStream(any)).called(1); |
| })); |
| |
| test('handles serving missing asset file', () => testbed.run(() async { |
| when(request.uri).thenReturn(Uri.parse('http://foobar/assets/foo')); |
| requestController.add(request); |
| await closeCompleter.future; |
| |
| verify(response.statusCode = HttpStatus.notFound).called(1); |
| })); |
| |
| test('calling dispose closes the http server', () => testbed.run(() async { |
| await webAssetServer.dispose(); |
| |
| verify(mockHttpServer.close()).called(1); |
| })); |
| } |
| |
| class MockHttpServer extends Mock implements HttpServer {} |
| class MockHttpRequest extends Mock implements HttpRequest {} |
| class MockHttpResponse extends Mock implements HttpResponse {} |
| class MockHttpHeaders extends Mock implements HttpHeaders {} |
| class MockPlatform extends Mock implements Platform {} |