blob: ff770c1f02ab30658831ce8f62935236efac5dfb [file] [log] [blame]
// Copyright (c) 2024, 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:io';
import 'dart:math';
import 'package:dtd/dtd.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'utils.dart';
void main() {
final fooDirContents = <Uri>[];
final barDirContents = <Uri>[];
late ToolingDaemonTestProcess toolingDaemonProcess;
late Directory tmpDirectory;
late Directory fooDirectory;
late Directory barDirectory;
late File fooPubspecFile;
late File barPubspecFile;
late File aFile;
late File bFile;
late Directory cDir;
late File eFile;
late File fFile;
final aFileContents = 'These are the contents for aFile';
final eFileContents = 'These are the contents for eFile';
late DartToolingDaemon client;
late String dtdSecret;
late Uri dtdUri;
setUp(() async {
tmpDirectory = await Directory.systemTemp.createTemp();
// Test directory structure:
//
// foo/
// a.txt
// b.txt
// pubspec.yaml
// C/
// d.txt
// pubspec.yaml
// bar/
// e.txt
// f.txt
// pubspec.yaml
// Setup foo dir
fooDirectory = Directory(p.join(tmpDirectory.path, 'foo'))..createSync();
aFile = File(p.join(fooDirectory.path, 'a.txt'))
..writeAsStringSync(aFileContents)
..createSync();
bFile = File(p.join(fooDirectory.path, 'b.txt'))..createSync();
fooPubspecFile = File(p.join(fooDirectory.path, 'pubspec.yaml'))
..createSync();
cDir = Directory(p.join(fooDirectory.path, 'C'))..createSync();
File(p.join(fooDirectory.path, 'C', 'd.txt')).createSync();
File(p.join(fooDirectory.path, 'C', 'pubspec.yaml')).createSync();
fooDirContents.clear();
fooDirContents.addAll([aFile.uri, bFile.uri, fooPubspecFile.uri, cDir.uri]);
// Setup bar dir
barDirectory = Directory(p.join(tmpDirectory.path, 'bar'));
barDirectory.createSync();
eFile = File(p.join(barDirectory.path, 'e.txt'))
..writeAsStringSync(eFileContents)
..createSync();
fFile = File(p.join(barDirectory.path, 'f.txt'))..createSync();
barPubspecFile = File(p.join(barDirectory.path, 'pubspec.yaml'))
..createSync();
barDirContents.clear();
barDirContents.addAll([eFile.uri, fFile.uri, barPubspecFile.uri]);
});
tearDown(() {
tmpDirectory.deleteSync(recursive: true);
toolingDaemonProcess.kill();
});
group('restricted', () {
setUp(() async {
toolingDaemonProcess = ToolingDaemonTestProcess();
await toolingDaemonProcess.start();
dtdUri = toolingDaemonProcess.uri;
dtdSecret = toolingDaemonProcess.trustedSecret!;
client = await DartToolingDaemon.connect(dtdUri);
});
group(FileSystemServiceConstants.serviceName, () {
group(FileSystemServiceConstants.setIDEWorkspaceRoots, () {
test('wrong secret is unauthorized', () {
expect(
() => client.setIDEWorkspaceRoots('abc123', [Uri.directory('/')]),
throwsAnRpcError(RpcErrorCodes.kPermissionDenied),
);
expect(
() => client.readFileAsString(aFile.uri),
throwsAnRpcError(RpcErrorCodes.kPermissionDenied),
);
});
test('root must have file scheme', () {
expect(
() => client
.setIDEWorkspaceRoots(dtdSecret, [Uri.parse('/some/path/')]),
throwsAnRpcError(RpcErrorCodes.kExpectsUriParamWithFileScheme),
);
});
test('no IDE workspace roots', () {
final file = File(p.join(fooDirectory.path, 'newfile.txt'));
expect(
() => client.listDirectoryContents(fooDirectory.uri),
throwsAnRpcError(RpcErrorCodes.kPermissionDenied),
);
expect(
() => client.readFileAsString(
file.uri,
),
throwsAnRpcError(RpcErrorCodes.kPermissionDenied),
);
expect(
() => client.writeFileAsString(
file.uri,
'this should not be written',
),
throwsAnRpcError(RpcErrorCodes.kPermissionDenied),
);
expect(file.existsSync(), false);
});
test('one IDE workspace root', () async {
await client.setIDEWorkspaceRoots(dtdSecret, [fooDirectory.uri]);
final fileContents = 'New file contents';
final listResult = await client.listDirectoryContents(
fooDirectory.uri,
);
expect(listResult.uris, containsAll(fooDirContents));
await client.writeFileAsString(aFile.uri, fileContents);
final readResult = await client.readFileAsString(aFile.uri);
expect(readResult.content, fileContents);
});
test('multiple IDE workspace roots', () async {
await client.setIDEWorkspaceRoots(dtdSecret, [
fooDirectory.uri,
barDirectory.uri,
]);
// Operate in foo
final newAFileContents = 'New afile contents';
final fooListResult = await client.listDirectoryContents(
fooDirectory.uri,
);
expect(fooListResult.uris, containsAll(fooDirContents));
await client.writeFileAsString(aFile.uri, newAFileContents);
final readResult = await client.readFileAsString(aFile.uri);
expect(readResult.content, newAFileContents);
// Operate in bar
final newEFileContents = 'New efile contents';
final barListResult = await client.listDirectoryContents(
fooDirectory.uri,
);
expect(barListResult.uris, containsAll(fooDirContents));
await client.writeFileAsString(aFile.uri, newEFileContents);
final eReadResult = await client.readFileAsString(aFile.uri);
expect(eReadResult.content, newEFileContents);
});
test('remove an IDE workspace root', () async {
await client.setIDEWorkspaceRoots(dtdSecret, [
fooDirectory.uri,
barDirectory.uri,
]);
await client.setIDEWorkspaceRoots(dtdSecret, [fooDirectory.uri]);
final fileContents = 'New file contents';
final listResult = await client.listDirectoryContents(
fooDirectory.uri,
);
expect(
listResult.uris,
containsAll(fooDirContents),
);
await client.writeFileAsString(aFile.uri, fileContents);
final readResult = await client.readFileAsString(aFile.uri);
expect(readResult.content, fileContents);
expect(
() => client.listDirectoryContents(barDirectory.uri),
throwsAnRpcError(RpcErrorCodes.kPermissionDenied),
);
expect(
() => client.readFileAsString(eFile.uri),
throwsAnRpcError(RpcErrorCodes.kPermissionDenied),
);
expect(
() => client.writeFileAsString(eFile.uri, fileContents),
throwsAnRpcError(RpcErrorCodes.kPermissionDenied),
);
});
});
group(FileSystemServiceConstants.getIDEWorkspaceRoots, () {
test('empty IDE workspace roots', () async {
final roots = await client.getIDEWorkspaceRoots();
expect(roots.ideWorkspaceRoots, isEmpty);
});
test('multiple IDE workspace roots', () async {
await client.setIDEWorkspaceRoots(dtdSecret, [
fooDirectory.uri,
barDirectory.uri,
]);
final roots = await client.getIDEWorkspaceRoots();
expect(
roots.ideWorkspaceRoots.map((e) => p.normalize(e.path)),
containsAll(
[fooDirectory.uri, barDirectory.uri]
.map((e) => p.normalize(e.path)),
),
);
});
});
group(FileSystemServiceConstants.getProjectRoots, () {
test('with empty IDE workspace roots', () async {
final roots = await client.getIDEWorkspaceRoots();
expect(roots.ideWorkspaceRoots, isEmpty);
final projectRoots = await client.getProjectRoots();
expect(projectRoots.uris, isEmpty);
});
test('with a single IDE workspace root', () async {
await client.setIDEWorkspaceRoots(dtdSecret, [fooDirectory.uri]);
final projectRoots = await client.getProjectRoots();
final expected = [fooDirectory.uri, cDir.uri];
expect(projectRoots.uris, containsAll(expected));
expect(projectRoots.uris?.length, expected.length);
});
test('with a multiple IDE workspace roots', () async {
await client.setIDEWorkspaceRoots(
dtdSecret,
[fooDirectory.uri, barDirectory.uri],
);
final projectRoots = await client.getProjectRoots();
final expected = [fooDirectory.uri, cDir.uri, barDirectory.uri];
expect(projectRoots.uris, containsAll(expected));
expect(projectRoots.uris?.length, expected.length);
});
test('searches up to a specified depth', () async {
await client.setIDEWorkspaceRoots(
dtdSecret,
[fooDirectory.uri, barDirectory.uri],
);
final projectRoots = await client.getProjectRoots(depth: 1);
final expected = [fooDirectory.uri, barDirectory.uri];
expect(projectRoots.uris, containsAll(expected));
expect(projectRoots.uris?.length, expected.length);
});
test('does not follow symlinks', () async {
// Add a symlink under [fooDirectory] that points to the
// [tmpDirectory]. Since the [tmpDirectory] contains [fooDirectory]
// and [barDirectory], [client.getProjectRoots] would contain
// duplicates of each project root if the traversal followed symlinks.
final symlinkDir = Link(p.join(fooDirectory.path, 'SomeDir'))
..createSync(tmpDirectory.path, recursive: true);
final extraRoot = Directory(p.join(tmpDirectory.path, 'Extra'))
..createSync();
final symlinkFile = Link(p.join(extraRoot.path, 'pubspec.yaml'))
..createSync(
p.join(tmpDirectory.path, 'pubspec.yaml'),
recursive: true,
);
await client.setIDEWorkspaceRoots(
dtdSecret,
[fooDirectory.uri, barDirectory.uri, extraRoot.uri],
);
final projectRoots = await client.getProjectRoots();
final expectedUris = [fooDirectory.uri, cDir.uri, barDirectory.uri];
expect(projectRoots.uris, containsAll(expectedUris));
expect(projectRoots.uris?.length, expectedUris.length);
symlinkDir.deleteSync();
symlinkFile.deleteSync();
extraRoot.deleteSync();
});
});
group(FileSystemServiceConstants.listDirectoryContents, () {
test('listing a file should fail', () async {
await client.setIDEWorkspaceRoots(dtdSecret, [fooDirectory.uri]);
expect(
() => client.listDirectoryContents(aFile.uri),
throwsAnRpcError(RpcErrorCodes.kDirectoryDoesNotExist),
);
});
test('non-existent directory should fail', () async {
await client.setIDEWorkspaceRoots(dtdSecret, [fooDirectory.uri]);
expect(
() => client.listDirectoryContents(
Uri.directory(p.join(fooDirectory.path, 'A')),
),
throwsAnRpcError(RpcErrorCodes.kDirectoryDoesNotExist),
);
});
test('should work for an empty directory', () async {
await client.setIDEWorkspaceRoots(dtdSecret, [fooDirectory.uri]);
final emptyFooDir = Directory(p.join(fooDirectory.path, 'emptyDir'));
emptyFooDir.createSync();
final listResult =
await client.listDirectoryContents(emptyFooDir.uri);
expect(listResult.uris, isEmpty);
});
test('must have a file scheme', () {
expect(
() => client.listDirectoryContents(
Uri.parse('/some/path/'),
),
throwsAnRpcError(RpcErrorCodes.kExpectsUriParamWithFileScheme),
);
});
});
group(FileSystemServiceConstants.readFileAsString, () {
test('fails on a non-existent file', () async {
await client.setIDEWorkspaceRoots(dtdSecret, [fooDirectory.uri]);
expect(
() => client.readFileAsString(
File(p.join(fooDirectory.path, 'nonExistentFile.txt')).uri,
),
throwsAnRpcError(RpcErrorCodes.kFileDoesNotExist),
);
});
test('must have a file scheme', () {
expect(
() => client.readFileAsString(Uri.parse('/some/path/')),
throwsAnRpcError(RpcErrorCodes.kExpectsUriParamWithFileScheme),
);
});
group(
'windows paths',
() {
// Test paths in various formats. We will test all combinations of
// these both as the set roots and the readFile path to ensure the
// calling client doesn't need to use the same escaping as the
// editor.
final roots = [
Uri.parse('file:///C:/foo'),
Uri.parse('file:///C%3A/foo'),
Uri.parse('file:///c:/foo'),
];
final files = roots
.map((uri) => uri.replace(path: '${uri.path}/file.txt'))
.toList();
for (final root in roots) {
for (final file in files) {
test('can read $file in $root', () async {
await client.setIDEWorkspaceRoots(dtdSecret, [root]);
expect(
() => client.readFileAsString(file),
// Expect file does not exist (NOT a permission error).
throwsAnRpcError(RpcErrorCodes.kFileDoesNotExist),
);
});
}
}
},
skip: !Platform.isWindows,
);
});
group(FileSystemServiceConstants.writeFileAsString, () {
final newFileContents = 'Some new file contents';
test('can overwrite an existing file', () async {
await client.setIDEWorkspaceRoots(dtdSecret, [fooDirectory.uri]);
expect(aFile.readAsStringSync(), aFileContents);
await client.writeFileAsString(aFile.uri, newFileContents);
expect(aFile.readAsStringSync(), newFileContents);
});
test('creates the file if it does not exist', () async {
final nonExistentFile = File(
p.join(fooDirectory.path, 'nonExistentFile.txt'),
);
await client.setIDEWorkspaceRoots(dtdSecret, [fooDirectory.uri]);
expect(nonExistentFile.existsSync(), false);
await client.writeFileAsString(nonExistentFile.uri, newFileContents);
expect(nonExistentFile.existsSync(), true);
expect(nonExistentFile.readAsStringSync(), newFileContents);
});
test('creates sub directories if they don\'t exist', () async {
final fileInNonExistentDirectory = File(
p.join(
fooDirectory.path,
'a',
'b',
'c',
'nonExistentFile.txt',
),
);
await client.setIDEWorkspaceRoots(dtdSecret, [fooDirectory.uri]);
expect(fileInNonExistentDirectory.existsSync(), false);
await client.writeFileAsString(
fileInNonExistentDirectory.uri,
newFileContents,
);
expect(fileInNonExistentDirectory.existsSync(), true);
expect(
fileInNonExistentDirectory.readAsStringSync(),
newFileContents,
);
});
test('must have a file scheme', () {
expect(
() => client.writeFileAsString(
Uri.parse('/some/path/'),
'some contents',
),
throwsAnRpcError(RpcErrorCodes.kExpectsUriParamWithFileScheme),
);
});
});
});
group('relative paths', () {
test('normalizes paths when setting pub root', () async {
final relativePath = p.join(fooDirectory.path, '..', 'bar', 'a.txt');
final simplifiedPath = p.join(barDirectory.path, 'a.txt');
await client.call(
FileSystemServiceConstants.serviceName,
FileSystemServiceConstants.setIDEWorkspaceRoots,
params: {
DtdParameters.secret: dtdSecret,
DtdParameters.roots: [
Uri.file(relativePath).toString(),
],
},
);
final roots = await client.getIDEWorkspaceRoots();
expect(
roots.ideWorkspaceRoots.map((e) => e.toFilePath()),
[simplifiedPath],
);
});
test('prevents access outside of workspace roots for relative paths',
() async {
await client.setIDEWorkspaceRoots(dtdSecret, [fooDirectory.uri]);
expect(
() => client.call(
FileSystemServiceConstants.serviceName,
FileSystemServiceConstants.readFileAsString,
params: {
DtdParameters.uri: p.join('${fooDirectory.uri}', '..', 'a.txt'),
},
),
throwsAnRpcError(RpcErrorCodes.kPermissionDenied),
);
expect(
() => client.call(
FileSystemServiceConstants.serviceName,
FileSystemServiceConstants.writeFileAsString,
params: {
DtdParameters.uri: p.join('${fooDirectory.uri}', '..', 'a.txt'),
DtdParameters.contents: 'abc',
DtdParameters.encoding: 'utf-8',
},
),
throwsAnRpcError(RpcErrorCodes.kPermissionDenied),
);
expect(
() => client.call(
FileSystemServiceConstants.serviceName,
FileSystemServiceConstants.listDirectoryContents,
params: {
DtdParameters.uri: p.join('${fooDirectory.uri}', '..'),
},
),
throwsAnRpcError(RpcErrorCodes.kPermissionDenied),
);
});
test('allows access to relative paths with ide workspace roots',
() async {
await client.setIDEWorkspaceRoots(dtdSecret, [fooDirectory.uri]);
final writeResult = await client.call(
FileSystemServiceConstants.serviceName,
FileSystemServiceConstants.writeFileAsString,
params: {
DtdParameters.uri: p.join(
fooDirectory.uri.toString(),
'C',
'D',
'..',
'..',
'C',
'd.txt',
),
DtdParameters.contents: 'abc',
DtdParameters.encoding: 'utf-8',
},
);
expect(writeResult.result, {'type': 'Success'});
final readResult = await client.call(
FileSystemServiceConstants.serviceName,
FileSystemServiceConstants.readFileAsString,
params: {
DtdParameters.uri: p.join(
fooDirectory.uri.toString(),
'C',
'D',
'..',
'..',
'C',
'd.txt',
),
},
);
expect(readResult.result, {'type': 'FileContent', 'content': 'abc'});
final listResult = await client.call(
FileSystemServiceConstants.serviceName,
FileSystemServiceConstants.listDirectoryContents,
params: {
DtdParameters.uri: p.join(
fooDirectory.uri.toString(),
'C',
'D',
'..',
'..',
'C',
),
},
);
expect(listResult.result, {
'type': 'UriList',
'uris': containsAll([
'${fooDirectory.uri}C/pubspec.yaml',
'${fooDirectory.uri}C/d.txt',
]),
});
});
final invalidDirectories = [
{
'dir': './',
'error':
throwsAnRpcError(RpcErrorCodes.kExpectsUriParamWithFileScheme),
},
{
'dir': '/',
'error':
throwsAnRpcError(RpcErrorCodes.kExpectsUriParamWithFileScheme),
},
{
'dir': '../',
'error':
throwsAnRpcError(RpcErrorCodes.kExpectsUriParamWithFileScheme),
},
{
'dir': 'file:///~/',
'error': throwsAnRpcError(RpcErrorCodes.kPermissionDenied),
},
];
for (final invalidDirectory in invalidDirectories) {
test('prevents use of invalid uri: ${invalidDirectory['dir']}', () {
final dir = invalidDirectory['dir'] as String;
final error = invalidDirectory['error'] as Matcher;
expect(
() => client.call(
FileSystemServiceConstants.serviceName,
FileSystemServiceConstants.readFileAsString,
params: {DtdParameters.uri: '${dir}a.txt'},
),
error,
);
expect(
() => client.call(
FileSystemServiceConstants.serviceName,
FileSystemServiceConstants.writeFileAsString,
params: {
DtdParameters.uri: '${dir}a.txt',
DtdParameters.contents: 'abc',
DtdParameters.encoding: 'utf-8',
},
),
error,
);
expect(
() => client.call(
FileSystemServiceConstants.serviceName,
FileSystemServiceConstants.listDirectoryContents,
params: {
DtdParameters.uri: dir,
},
),
error,
);
});
}
});
});
group('unrestricted', () {
setUp(() async {
toolingDaemonProcess = ToolingDaemonTestProcess(unrestricted: true);
await toolingDaemonProcess.start();
dtdUri = toolingDaemonProcess.uri;
client = await DartToolingDaemon.connect(dtdUri);
});
test('works when no roots set', () async {
final fileContents = 'New file contents';
expect((await client.getIDEWorkspaceRoots()).ideWorkspaceRoots, isEmpty);
final listResult = await client.listDirectoryContents(fooDirectory.uri);
expect(listResult.uris, containsAll(fooDirContents));
await client.writeFileAsString(aFile.uri, fileContents);
final readResult = await client.readFileAsString(aFile.uri);
expect(readResult.content, fileContents);
});
test(
'works when ide workspace roots set to a different directory',
() async {
await client.setIDEWorkspaceRoots(
Random().nextInt(10000).toString(),
[barDirectory.uri],
);
final fileContents = 'New file contents';
final listResult = await client.listDirectoryContents(fooDirectory.uri);
expect(listResult.uris, containsAll(fooDirContents));
await client.writeFileAsString(aFile.uri, fileContents);
final readResult = await client.readFileAsString(aFile.uri);
expect(readResult.content, fileContents);
expect(
(await client.getIDEWorkspaceRoots())
.ideWorkspaceRoots
.map((e) => p.normalize(e.path)),
[
p.normalize(barDirectory.uri.path),
]);
},
);
});
}