blob: 5d8e30e00acdfc190586ed2ddd9a5a47834134b9 [file] [log] [blame]
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. 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 'package:async/async.dart';
import 'package:dtd/dtd.dart';
import 'package:test/test.dart';
import 'utils.dart';
void main() {
late ToolingDaemonTestProcess toolingDaemonProcess;
late DartToolingDaemon client;
late String? dtdSecret;
Future<void> startDtd({bool unrestricted = false}) async {
toolingDaemonProcess = ToolingDaemonTestProcess(unrestricted: unrestricted);
await toolingDaemonProcess.start();
client = await DartToolingDaemon.connect(toolingDaemonProcess.uri);
dtdSecret = toolingDaemonProcess.trustedSecret;
}
group(ConnectedAppServiceConstants.serviceName, () {
tearDown(() async {
toolingDaemonProcess.kill();
});
group('restricted mode (default)', () {
setUp(() async {
await startDtd();
});
group(ConnectedAppServiceConstants.registerVmService, () {
test('succeeds', () async {
final testApp = DartCliAppProcess();
await testApp.start();
final response = await client.registerVmService(
uri: testApp.vmServiceUri,
secret: dtdSecret!,
);
expect(response.type, 'Success');
expect(response.value, null);
});
test('succeeds for URI that is already registered', () async {
final testApp = DartCliAppProcess();
await testApp.start();
var response = await client.registerVmService(
uri: testApp.vmServiceUri,
secret: dtdSecret!,
);
expect(response.type, 'Success');
expect(response.value, null);
expect(
(await client.getVmServices())
.vmServicesInfos
.map((s) => s.toJson()),
[
{'uri': testApp.vmServiceUri},
],
);
response = await client.registerVmService(
uri: testApp.vmServiceUri,
secret: dtdSecret!,
);
expect(response.type, 'Success');
expect(response.value, null);
expect(
(await client.getVmServices())
.vmServicesInfos
.map((s) => s.toJson()),
[
{'uri': testApp.vmServiceUri},
],
);
});
test('throws exception for invalid secret', () {
expect(
() => client.registerVmService(
uri: 'test_uri',
secret: 'fake_secret',
),
throwsAnRpcError(RpcErrorCodes.kPermissionDenied),
);
});
test('throws exception for invalid URI', () async {
expect(
() => client.registerVmService(uri: 'invalid', secret: dtdSecret!),
throwsAnRpcError(RpcErrorCodes.kConnectionFailed),
);
});
});
group(ConnectedAppServiceConstants.unregisterVmService, () {
test('succeeds', () async {
final testApp = DartCliAppProcess();
await testApp.start();
var response = await client.registerVmService(
uri: testApp.vmServiceUri,
secret: dtdSecret!,
);
expect(response.type, 'Success');
expect(response.value, null);
expect(
(await client.getVmServices())
.vmServicesInfos
.map((s) => s.toJson()),
[
{'uri': testApp.vmServiceUri},
],
);
response = await client.unregisterVmService(
uri: testApp.vmServiceUri,
secret: dtdSecret!,
);
expect(response.type, 'Success');
expect(response.value, null);
expect((await client.getVmServices()).vmServicesInfos, isEmpty);
});
test('succeeds for URI that is not in the registry', () async {
final response = await client.unregisterVmService(
uri: 'some_uri',
secret: dtdSecret!,
);
expect(response.type, 'Success');
expect(response.value, null);
});
test('throws exception for invalid secret', () {
expect(
() => client.unregisterVmService(
uri: 'test_uri',
secret: 'fake_secret',
),
throwsAnRpcError(RpcErrorCodes.kPermissionDenied),
);
});
});
});
group('unrestriced mode', () {
setUp(() async {
await startDtd(unrestricted: true);
});
test(
'${ConnectedAppServiceConstants.registerVmService} succeeds with '
'invalid secret', () async {
final testApp = DartCliAppProcess();
await testApp.start();
final response = await client.registerVmService(
uri: testApp.vmServiceUri,
secret: 'invalid secret',
);
expect(response.type, 'Success');
expect(response.value, null);
});
test(
'${ConnectedAppServiceConstants.unregisterVmService} succeeds with '
'invalid secret', () async {
final response = await client.unregisterVmService(
// The URI does not need to be a real URI to test this case.
uri: 'some_uri',
secret: 'invalid secret',
);
expect(response.type, 'Success');
expect(response.value, null);
});
});
test(ConnectedAppServiceConstants.getVmServices, () async {
await startDtd();
final testApp1 = DartCliAppProcess();
await testApp1.start();
var response = await client.registerVmService(
uri: testApp1.vmServiceUri,
name: 'app 1',
secret: dtdSecret!,
);
expect(response.type, 'Success');
expect(response.value, null);
final testApp2 = DartCliAppProcess();
await testApp2.start();
response = await client.registerVmService(
uri: testApp2.vmServiceUri,
exposedUri: testApp2.vmServiceUri,
secret: dtdSecret!,
);
expect(response.type, 'Success');
expect(response.value, null);
final servicesResponse = await client.getVmServices();
expect(
servicesResponse.vmServicesInfos.map((s) => s.toJson()),
[
{'uri': testApp1.vmServiceUri, 'name': 'app 1'},
{'uri': testApp2.vmServiceUri, 'exposedUri': testApp2.vmServiceUri},
],
);
});
group('sends stream updates', () {
late StreamController<DTDEvent> eventStream;
late StreamQueue<DTDEvent> events;
StreamSubscription<DTDEvent>? subscription;
setUp(() async {
await startDtd();
eventStream = StreamController<DTDEvent>();
events = StreamQueue<DTDEvent>(eventStream.stream);
});
tearDown(() async {
await events.cancel();
await eventStream.close();
await subscription?.cancel();
});
test('for register and unregister events', () async {
var eventsCount = 0;
subscription = client.onVmServiceUpdate().listen((e) {
eventStream.add(e);
eventsCount++;
});
await client.streamListen(ConnectedAppServiceConstants.serviceName);
expect(eventsCount, 0);
final testApp1 = DartCliAppProcess();
await testApp1.start();
var response = await client.registerVmService(
uri: testApp1.vmServiceUri,
name: 'name1',
secret: dtdSecret!,
);
expect(response.type, 'Success');
expect(response.value, null);
var next = await events.next;
expect(eventsCount, 1);
expect(
jsonDecode(next.toString()),
{
'stream': ConnectedAppServiceConstants.serviceName,
'kind': ConnectedAppServiceConstants.vmServiceRegistered,
'data': {
'uri': testApp1.vmServiceUri,
'exposedUri': null,
'name': 'name1',
},
'timestamp': isNotNull,
},
);
final testApp2 = DartCliAppProcess();
await testApp2.start();
response = await client.registerVmService(
uri: testApp2.vmServiceUri,
exposedUri: testApp2.vmServiceUri,
secret: dtdSecret!,
name: 'name2',
);
expect(response.type, 'Success');
expect(response.value, null);
next = await events.next;
expect(eventsCount, 2);
expect(
jsonDecode(next.toString()),
{
'stream': ConnectedAppServiceConstants.serviceName,
'kind': ConnectedAppServiceConstants.vmServiceRegistered,
'data': {
'uri': testApp2.vmServiceUri,
'exposedUri': testApp2.vmServiceUri,
'name': 'name2',
},
'timestamp': isNotNull,
},
);
response = await client.unregisterVmService(
uri: testApp1.vmServiceUri,
secret: dtdSecret!,
);
expect(response.type, 'Success');
expect(response.value, null);
next = await events.next;
expect(eventsCount, 3);
expect(
jsonDecode(next.toString()),
{
'stream': ConnectedAppServiceConstants.serviceName,
'kind': ConnectedAppServiceConstants.vmServiceUnregistered,
'data': {
'uri': testApp1.vmServiceUri,
'exposedUri': null,
'name': 'name1',
},
'timestamp': isNotNull,
},
);
// This should not send a
// [ConnectedAppServiceConstants.vmServiceUnregistered] event
// from the `VmService.onDone` handler added in [ConnectedAppService]
// because this VM Service was already unregistered above. This
// behavior is validated by checking the next event after calling
// `testApp2.kill()` below.
testApp1.kill();
// Sends a [ConnectedAppServiceConstants.vmServiceUnregistered] event
// from the `VmService.onDone` handler added in [ConnectedAppService].
testApp2.kill();
next = await events.next;
expect(eventsCount, 4);
expect(
jsonDecode(next.toString()),
{
'stream': ConnectedAppServiceConstants.serviceName,
'kind': ConnectedAppServiceConstants.vmServiceUnregistered,
'data': {
'uri': testApp2.vmServiceUri,
'exposedUri': testApp2.vmServiceUri,
'name': 'name2',
},
'timestamp': isNotNull,
},
);
});
test('only after subscription has been established', () async {
var eventsCount = 0;
expect(eventsCount, 0);
// This event will be dropped.
final testApp1 = DartCliAppProcess();
await testApp1.start();
var response = await client.registerVmService(
uri: testApp1.vmServiceUri,
secret: dtdSecret!,
);
expect(response.type, 'Success');
expect(response.value, null);
// This event will be dropped.
final testApp2 = DartCliAppProcess();
await testApp2.start();
response = await client.registerVmService(
uri: testApp2.vmServiceUri,
secret: dtdSecret!,
);
expect(response.type, 'Success');
expect(response.value, null);
// Await a zero delay to release the Dart event loop, ensuring the
// stream notifications from above have already occurred before we add
// a stream subscription.
await Future<void>.delayed(Duration.zero);
// Subscribe late and verify that we do not get any previous events.
expect(eventsCount, 0);
subscription = client.onVmServiceUpdate().listen((e) {
eventStream.add(e);
eventsCount++;
});
await client.streamListen(ConnectedAppServiceConstants.serviceName);
expect(eventsCount, 0);
// We expect to recieve the this event.
testApp1.kill();
var next = await events.next;
expect(eventsCount, 1);
expect(
jsonDecode(next.toString()),
{
'stream': ConnectedAppServiceConstants.serviceName,
'kind': ConnectedAppServiceConstants.vmServiceUnregistered,
'data': {
'uri': testApp1.vmServiceUri,
'exposedUri': null,
'name': null,
},
'timestamp': isNotNull,
},
);
// We expect to recieve the this event.
testApp2.kill();
next = await events.next;
expect(eventsCount, 2);
expect(
jsonDecode(next.toString()),
{
'stream': ConnectedAppServiceConstants.serviceName,
'kind': ConnectedAppServiceConstants.vmServiceUnregistered,
'data': {
'uri': testApp2.vmServiceUri,
'exposedUri': null,
'name': null,
},
'timestamp': isNotNull,
},
);
});
});
});
}