A clide:// link is an untrusted external vector (any webpage can fire one), so it no longer translates to a command in parseArgv. It routes the raw URL to a new builtin.deeplink handler that is doubly defensive: a default-deny allowlist (kDeepLinkSafeActions — only the read-only 'open' verb; run/git/write/passthrough rejected) AND a mandatory 'an external link wants to: … allow?' confirmation before anything runs. Records the security boundary as D-90. The earlier silent editor.open passthrough is replaced; open still works, now behind the prompt. Tests cover the allowlist (the boundary) + the gating. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
57 lines
2.0 KiB
Dart
57 lines
2.0 KiB
Dart
/// Tests for the deeplink handler's gating (T-56, D-90): an allowlisted link
|
|
/// prompts before acting; a non-allowlisted one is rejected with no prompt.
|
|
library;
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:clide/builtin/deeplink/deeplink.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import '../../helpers/kernel_fixture.dart';
|
|
import '../../helpers/widget_harness.dart';
|
|
|
|
void main() {
|
|
late KernelFixture f;
|
|
setUp(() async {
|
|
f = await KernelFixture.create();
|
|
f.services.extensions.register(DeepLinkExtension());
|
|
await f.services.extensions.activate('builtin.deeplink');
|
|
});
|
|
tearDown(() => f.dispose());
|
|
|
|
testWidgets('an allowlisted link prompts before doing anything', (tester) async {
|
|
await tester.pumpWidget(harness(f, const SizedBox()));
|
|
await tester.pump();
|
|
expect(f.services.dialog.isOpen, isFalse);
|
|
|
|
// The handler blocks on the confirmation, so don't await it.
|
|
unawaited(f.services.commands.execute('deeplink.invoke', args: ['clide://open?path=/x.dart']));
|
|
await tester.pump();
|
|
|
|
expect(f.services.dialog.isOpen, isTrue, reason: 'a confirmation must be shown before acting');
|
|
f.services.dialog.dismiss(false); // decline → cleanup
|
|
await tester.pump();
|
|
});
|
|
|
|
testWidgets('a non-allowlisted link is rejected with no prompt', (tester) async {
|
|
await tester.pumpWidget(harness(f, const SizedBox()));
|
|
await tester.pump();
|
|
|
|
final r = await f.services.commands.execute('deeplink.invoke', args: ['clide://run?cmd=rm%20-rf']);
|
|
await tester.pump();
|
|
|
|
expect(f.services.dialog.isOpen, isFalse, reason: 'no dialog for a rejected link');
|
|
expect(r.data['status'], 'rejected');
|
|
});
|
|
|
|
testWidgets('a malformed link is rejected with no prompt', (tester) async {
|
|
await tester.pumpWidget(harness(f, const SizedBox()));
|
|
await tester.pump();
|
|
|
|
final r = await f.services.commands.execute('deeplink.invoke', args: ['https://evil.example/open?path=/x']);
|
|
expect(f.services.dialog.isOpen, isFalse);
|
|
expect(r.data['status'], 'rejected');
|
|
});
|
|
}
|