blob: 3b735e70f5cf78c6ebee3a59505148c62cd5171f [file] [log] [blame]
// 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 {}