| // Copyright 2019 The Fuchsia 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:convert'; |
| import 'dart:io'; |
| |
| import 'package:quiver/testing/async.dart'; |
| import 'package:test/test.dart'; |
| import 'package:mockito/mockito.dart'; |
| |
| import 'package:sl4f/sl4f.dart'; |
| |
| class MockDump extends Mock implements Dump {} |
| |
| class MockHttpClient extends Mock implements HttpClient {} |
| |
| class MockHttpRequest extends Mock implements HttpClientRequest {} |
| |
| class MockHttpResponse extends Mock implements HttpClientResponse { |
| final Stream<List<int>> data; |
| MockHttpResponse(this.data); |
| |
| @override |
| Stream<S> transform<S>(StreamTransformer<List<int>, S> streamTransformer) { |
| return streamTransformer.bind(data); |
| } |
| } |
| |
| class MockHttpHeaders extends Mock implements HttpHeaders {} |
| |
| class MockIOSink extends Mock implements IOSink {} |
| |
| class MockSsh extends Mock implements Ssh {} |
| |
| class MockProcess extends Mock implements Process {} |
| |
| class MockStream<T> extends Mock implements Stream<T> {} |
| |
| class MockSubscription<T> extends Mock implements StreamSubscription<T> {} |
| |
| void main() { |
| group('Sl4f diagnostics', () { |
| test('are performed if dump is enabled', () async { |
| final dump = MockDump(); |
| final ssh = MockSsh(); |
| final sl4f = Sl4f('', ssh); |
| |
| final dumps = <String, Future<String>>{}; |
| |
| when(dump.hasDumpDirectory).thenReturn(true); |
| when(dump.openForWrite(any, any)).thenAnswer((invocation) { |
| // Just ensure that the names are unique and don't have spaces. |
| final name = invocation.positionalArguments[0]; |
| expect(name, isNot(contains(' '))); |
| |
| // ignore: close_sinks |
| final controller = StreamController<List<int>>(); |
| dumps[name] = systemEncoding.decodeStream(controller.stream); |
| return IOSink(controller, encoding: systemEncoding); |
| }); |
| // Use `echo -n` as our invocation so we can verify both that the expected |
| // command was issued and that the dump works, as the command itself is |
| // dumped. |
| when(ssh.start(any)).thenAnswer((invocation) => Process.start( |
| '/bin/echo', ['-n', invocation.positionalArguments[0]])); |
| |
| await sl4f.dumpDiagnostics('dump', dump: dump); |
| expect(dumps, hasLength(Sl4f.diagnostics.length)); |
| expect(await Future.wait(dumps.values), |
| unorderedEquals(Sl4f.diagnostics.values)); |
| }); |
| |
| test('are skipped if dump is not enabled', () async { |
| final dump = MockDump(); |
| final ssh = MockSsh(); |
| final sl4f = Sl4f('', ssh); |
| |
| when(dump.hasDumpDirectory).thenReturn(false); |
| await sl4f.dumpDiagnostics('you cannot dump', dump: dump); |
| verifyNever(ssh.start(any)); |
| }); |
| |
| // fxbug.dev/4745 |
| test('do not hang even if the tools do', () { |
| const eventually = Duration(hours: 1); |
| |
| FakeAsync().run((async) { |
| final dump = MockDump(); |
| final ssh = MockSsh(); |
| // ignore: close_sinks |
| final mockIOSink = MockIOSink(); |
| final sl4f = Sl4f('', ssh); |
| final verifiers = []; |
| |
| when(dump.hasDumpDirectory).thenReturn(true); |
| when(dump.openForWrite(any, any)).thenAnswer((_) => mockIOSink); |
| when(mockIOSink.add(any)).thenReturn(null); |
| when(ssh.start(any)).thenAnswer((_) async { |
| // Using an actual process or streams here causes problems when the |
| // code under test attempts to cancel the stream subscriptions, as |
| // the resulting future does not complete immediately. So we create |
| // a mock process where stdout and stderr are mock streams that give |
| // mock subscriptions that verify that they are cancelled. |
| final mockProcess = MockProcess(); |
| when(mockProcess.kill(any)).thenAnswer((_) => null); |
| when(mockProcess.stdin).thenAnswer((_) => MockIOSink()); |
| // These mock streams just provide a StreamSubscription that can be |
| // cancelled, but never produce any values. |
| when(mockProcess.stdout).thenAnswer((_) { |
| final mockStream = MockStream<List<int>>(); |
| when(mockStream.listen(any, onDone: anyNamed('onDone'))) |
| .thenAnswer((_) { |
| final mockSubscription = MockSubscription<List<int>>(); |
| when(mockSubscription.cancel()) |
| .thenAnswer((_) => Future.value(null)); |
| verifiers.add(() => verify(mockSubscription.cancel()).called(1)); |
| return mockSubscription; |
| }); |
| return mockStream; |
| }); |
| when(mockProcess.stderr).thenAnswer((_) { |
| final mockStream = MockStream<List<int>>(); |
| when(mockStream.listen(any, onDone: anyNamed('onDone'))) |
| .thenAnswer((_) { |
| final mockSubscription = MockSubscription<List<int>>(); |
| when(mockSubscription.cancel()) |
| .thenAnswer((_) => Future.value(null)); |
| verifiers.add(() => verify(mockSubscription.cancel()).called(1)); |
| return mockSubscription; |
| }); |
| return mockStream; |
| }); |
| |
| return mockProcess; |
| }); |
| |
| expect(sl4f.dumpDiagnostics('dumpName', dump: dump).timeout(eventually), |
| completes); |
| async.elapse(eventually); |
| |
| for (final verifier in verifiers) { |
| verifier(); |
| } |
| }); |
| }); |
| |
| test('hostIpAddress', () async { |
| final ssh = MockSsh(); |
| final sl4f = Sl4f('', ssh); |
| // Normally, both addresses (before and after the ~) will be the same. |
| // They differ in this test for the sake of testing (knowing which is |
| // used). |
| when(ssh.runWithOutput(any)).thenAnswer((invocation) => Process.run( |
| '/bin/echo', |
| ['fe80::fefe:fefe:fefe:fefe%4 234~fe80::fefe:fefe:fefe:abab%4 987'])); |
| expect(await sl4f.hostIpAddress(), equals('fe80::fefe:fefe:fefe:fefe%4')); |
| when(ssh.runWithOutput(any)).thenAnswer((invocation) => |
| Process.run('/bin/echo', ['-n', '~fe80::fefe:fefe:fefe:abab%4 987'])); |
| expect(await sl4f.hostIpAddress(), equals('fe80::fefe:fefe:fefe:abab%4')); |
| when(ssh.runWithOutput(any)) |
| .thenAnswer((invocation) => Process.run('/bin/echo', ['-n', '~'])); |
| expect(() async => await sl4f.hostIpAddress(), |
| throwsA(TypeMatcher<Sl4fException>())); |
| }); |
| }); |
| |
| group('Sl4f fromEnvironment', () { |
| test('throws exception if no IP address', () { |
| expect(() => Sl4f.fromEnvironment(environment: {}), |
| throwsA(TypeMatcher<Sl4fException>())); |
| }); |
| test('throws exception if ssh cannot be used', () { |
| expect( |
| () => Sl4f.fromEnvironment( |
| environment: {'FUCHSIA_IPV4_ADDR': '1.2.3.4'}), |
| throwsA(TypeMatcher<Sl4fException>())); |
| }); |
| |
| test('accepts SSH_AUTH_SOCK', () { |
| expect( |
| Sl4f.fromEnvironment(environment: { |
| 'FUCHSIA_IPV4_ADDR': '1.2.3.4', |
| 'SSH_AUTH_SOCK': '/foo' |
| }), |
| TypeMatcher<Sl4f>()); |
| }); |
| |
| test('accepts FUCHSIA_SSH_KEY', () { |
| expect( |
| Sl4f.fromEnvironment(environment: { |
| 'FUCHSIA_IPV4_ADDR': '1.2.3.4', |
| 'FUCHSIA_SSH_KEY': '/foo' |
| }), |
| TypeMatcher<Sl4f>()); |
| }); |
| |
| test('accepts FUCHSIA_SSH_PORT', () { |
| expect( |
| Sl4f.fromEnvironment(environment: { |
| 'FUCHSIA_IPV4_ADDR': '1.2.3.4', |
| 'FUCHSIA_SSH_KEY': '/foo', |
| 'FUCHSIA_SSH_PORT': '8022' |
| }), |
| TypeMatcher<Sl4f>()); |
| }); |
| |
| test('default HTTP Port', () { |
| Sl4f client = Sl4f.fromEnvironment(environment: { |
| 'FUCHSIA_IPV4_ADDR': '1.2.3.4', |
| 'FUCHSIA_SSH_KEY': '/foo', |
| 'FUCHSIA_SSH_PORT': '8022' |
| }); |
| expect(client.port, equals(80)); |
| }); |
| |
| test('override HTTP Port', () { |
| Sl4f client = Sl4f.fromEnvironment(environment: { |
| 'FUCHSIA_IPV4_ADDR': '1.2.3.4', |
| 'FUCHSIA_SSH_KEY': '/foo', |
| 'FUCHSIA_SSH_PORT': '8022', |
| 'SL4F_HTTP_PORT': '8282' |
| }); |
| expect(client.port, equals(8282)); |
| }); |
| |
| test('IPv6 address using FUCHSIA_IPV4_ADDR', () { |
| Sl4f client = Sl4f.fromEnvironment(environment: { |
| 'FUCHSIA_IPV4_ADDR': '::1', |
| 'FUCHSIA_SSH_KEY': '/foo', |
| 'FUCHSIA_SSH_PORT': '8022', |
| 'SL4F_HTTP_PORT': '8282' |
| }); |
| expect(client.target, equals('[::1]')); |
| expect(client.port, equals(8282)); |
| }); |
| |
| test('IPv6 address using FUCHSIA_DEVICE_ADDR', () { |
| Sl4f client = Sl4f.fromEnvironment(environment: { |
| 'FUCHSIA_DEVICE_ADDR': '::1', |
| 'FUCHSIA_SSH_KEY': '/foo', |
| 'FUCHSIA_SSH_PORT': '8022', |
| 'SL4F_HTTP_PORT': '8282' |
| }); |
| expect(client.target, equals('[::1]')); |
| expect(client.port, equals(8282)); |
| }); |
| |
| test('IPv6 link-local address using FUCHSIA_DEVICE_ADDR', () { |
| Sl4f client = Sl4f.fromEnvironment(environment: { |
| 'FUCHSIA_DEVICE_ADDR': 'fe80::1234:44f%eth0', |
| 'FUCHSIA_SSH_KEY': '/foo', |
| 'FUCHSIA_SSH_PORT': '8022', |
| 'SL4F_HTTP_PORT': '8282' |
| }); |
| expect(client.target, equals('[fe80::1234:44f%eth0]')); |
| expect(client.port, equals(8282)); |
| }); |
| |
| test('IPv6 address using FUCHSIA_DEVICE_ADDR', () { |
| Sl4f client = Sl4f.fromEnvironment(environment: { |
| 'FUCHSIA_IPV6_ADDR': '::1', |
| 'FUCHSIA_SSH_KEY': '/foo', |
| 'FUCHSIA_SSH_PORT': '8022', |
| 'SL4F_HTTP_PORT': '8282' |
| }); |
| expect(client.target, equals('[::1]')); |
| expect(client.port, equals(8282)); |
| }); |
| |
| test('IPv6 link-local address using FUCHSIA_DEVICE_ADDR', () { |
| Sl4f client = Sl4f.fromEnvironment(environment: { |
| 'FUCHSIA_IPV6_ADDR': 'fe80::1234:44f%eth0', |
| 'FUCHSIA_SSH_KEY': '/foo', |
| 'FUCHSIA_SSH_PORT': '8022', |
| 'SL4F_HTTP_PORT': '8282' |
| }); |
| expect(client.target, equals('[fe80::1234:44f%eth0]')); |
| expect(client.port, equals(8282)); |
| }); |
| |
| test('IPv6 address using both FUCHSIA_IPV4_ADDR and FUCHSIA_DEVICE_ADDR', |
| () { |
| Sl4f client = Sl4f.fromEnvironment(environment: { |
| 'FUCHSIA_DEVICE_ADDR': '::1', |
| 'FUCHSIA_IPV4_ADDR': '127.0.0.1', |
| 'FUCHSIA_SSH_KEY': '/foo', |
| 'FUCHSIA_SSH_PORT': '8022', |
| 'SL4F_HTTP_PORT': '8282' |
| }); |
| // FUCHSIA_DEVICE_ADDR has preference over FUCHSIA_IPV4_ADDR: |
| expect(client.target, equals('[::1]')); |
| expect(client.port, equals(8282)); |
| }); |
| |
| test('IPv6 address using both FUCHSIA_IPV4_ADDR and FUCHSIA_IPV6_ADDR', () { |
| Sl4f client = Sl4f.fromEnvironment(environment: { |
| 'FUCHSIA_IPV6_ADDR': '::1', |
| 'FUCHSIA_IPV4_ADDR': '127.0.0.1', |
| 'FUCHSIA_SSH_KEY': '/foo', |
| 'FUCHSIA_SSH_PORT': '8022', |
| 'SL4F_HTTP_PORT': '8282' |
| }); |
| // FUCHSIA_IPV4_ADDR has preference over FUCHSIA_IPV6_ADDR: |
| expect(client.target, equals('127.0.0.1')); |
| expect(client.port, equals(8282)); |
| }); |
| |
| test( |
| 'IPv6 address using FUCHSIA_IPV4_ADDR and FUCHSIA_IPV6_ADDR and FUCHSIA_DEVICE_ADDR', |
| () { |
| Sl4f client = Sl4f.fromEnvironment(environment: { |
| 'FUCHSIA_DEVICE_ADDR': '::1', |
| 'FUCHSIA_IPV6_ADDR': 'fe80::1234:44f%eth0', |
| 'FUCHSIA_IPV4_ADDR': '127.0.0.1', |
| 'FUCHSIA_SSH_KEY': '/foo', |
| 'FUCHSIA_SSH_PORT': '8022', |
| 'SL4F_HTTP_PORT': '8282' |
| }); |
| // FUCHSIA_DEVICE_ADDR has preference over the other two: |
| expect(client.target, equals('[::1]')); |
| expect(client.port, equals(8282)); |
| }); |
| |
| test('tunnel proxy ports', () { |
| Sl4f client = Sl4f.fromEnvironment(environment: { |
| 'FUCHSIA_IPV4_ADDR': '1.2.3.4', |
| 'FUCHSIA_SSH_KEY': '/foo', |
| 'FUCHSIA_SSH_PORT': '8022', |
| 'FUCHSIA_PROXY_PORTS': '9001,9002,9003' |
| }); |
| expect(client.proxy.proxyPorts, [9001, 9002, 9003]); |
| }); |
| |
| test('invalid proxy ports', () { |
| expect( |
| () => Sl4f.fromEnvironment(environment: { |
| 'FUCHSIA_IPV4_ADDR': '1.2.3.4', |
| 'FUCHSIA_SSH_KEY': '/foo', |
| 'FUCHSIA_SSH_PORT': '8022', |
| 'FUCHSIA_PROXY_PORTS': '9001 9002 9003' |
| }), |
| throwsA(isFormatException), |
| ); |
| }); |
| |
| test('skips negative proxy ports', () { |
| Sl4f client = Sl4f.fromEnvironment(environment: { |
| 'FUCHSIA_IPV4_ADDR': '1.2.3.4', |
| 'FUCHSIA_SSH_KEY': '/foo', |
| 'FUCHSIA_SSH_PORT': '8022', |
| 'FUCHSIA_PROXY_PORTS': '9001,-2,9003' |
| }); |
| expect(client.proxy.proxyPorts, [9001, 9003]); |
| }); |
| }); |
| |
| group('Sl4f constructor', () { |
| test('empty path in targetUrl', () { |
| Sl4f client = Sl4f('1.2.3.4', null); |
| expect(client.targetUrl.path, isEmpty); |
| }); |
| test('throws exception if target ipv4 address has port', () { |
| expect(() => Sl4f('1.2.3.4:8282', null), |
| throwsA(TypeMatcher<FormatException>())); |
| }); |
| test('throws exception if target ipv6 address has port', () { |
| expect(() => Sl4f('[::1]:8282', null), |
| throwsA(TypeMatcher<FormatException>())); |
| }); |
| }); |
| |
| group('Sl4f fromUrl', () { |
| test('correct target and port', () { |
| Sl4f client = Sl4f.fromUrl(Uri.http('sl4f.com:1234', '')); |
| expect(client.target, equals('sl4f.com')); |
| expect(client.port, equals(1234)); |
| }); |
| test('null url throws exception', () { |
| expect(() => Sl4f.fromUrl(null), throwsA(TypeMatcher<ArgumentError>())); |
| }); |
| test('uses user provided HttpClient', () async { |
| final mockHttpClient = MockHttpClient(); |
| final mockRequest = MockHttpRequest(); |
| final mockResponse = |
| MockHttpResponse(Stream.value(utf8.encoder.convert('{}'))); |
| when(mockHttpClient.postUrl(any)) |
| .thenAnswer((_) => Future.value(mockRequest)); |
| when(mockRequest.headers).thenReturn(MockHttpHeaders()); |
| when(mockRequest.close()).thenAnswer((_) => Future.value(mockResponse)); |
| |
| Sl4f client = Sl4f.fromUrl(Uri.http('sl4f.com:1234', ''), mockHttpClient); |
| await client.request('method'); |
| |
| verify(mockHttpClient.postUrl(any)); |
| }); |
| }); |
| |
| group('Sl4f request', () { |
| HttpServer fakeServer; |
| Sl4f sl4fUnderTest; |
| setUp(() async { |
| fakeServer = await HttpServer.bind('127.0.0.1', 0); |
| sl4fUnderTest = |
| Sl4f.fromUrl(Uri.http('127.0.0.1:${fakeServer.port}', '/')); |
| }); |
| tearDown(() async { |
| await fakeServer.close(); |
| }); |
| test('can send request with utf-8 characters', () async { |
| Future<void> handler(HttpRequest request) async { |
| expect(request?.headers?.contentType?.mimeType, 'application/json'); |
| expect(request?.headers?.contentType?.charset, 'utf-8'); |
| final Map<String, dynamic> content = await request |
| .cast<List<int>>() |
| .transform(utf8.decoder) |
| .transform(json.decoder) |
| .first; |
| expect(content['params']['text'], equals('6′ 1″')); |
| request.response.write('{"result": "Success"}'); |
| await request.response.close(); |
| } |
| |
| fakeServer.listen(handler); |
| // do .request with JSON containing utf-8. |
| final response = |
| await sl4fUnderTest.request('foo.test', {'text': '6′ 1″'}); |
| expect(response, 'Success'); |
| }); |
| }); |
| } |