Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'dart:convert';
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:querya_desktop/core/extensions/harness/extension_harness.dart';
import 'package:querya_desktop/core/extensions/harness/extension_harness_report.dart';

import 'e2e_stub_driver.dart';

const _timeout = Timeout(Duration(seconds: 60));

void main() {
final skip = stubDriverSkipReason();
late Directory root;

setUp(() => root = Directory.systemTemp.createTempSync('querya_e2e_rpc_'));
tearDown(() => root.deleteSync(recursive: true));

test('a real driver process passes the whole RPC lifecycle in order',
timeout: _timeout, skip: skip, () async {
writeStubDriver(root, mode: 'healthy');

final report = await QueryaExtensionHarness(
shutdownTimeout: const Duration(seconds: 3),
).run(extensionRoot: root.path, targetVersion: '0.5.0');

expect(report.isSuccess, isTrue, reason: report.toConsole());
expect(report.failedCount, 0);
final names = report.checks.map((c) => c.name).toList();
expect(
names.where((n) => n.startsWith('system.') || n.startsWith('db.')),
containsAllInOrder([
'system.handshake',
'db.connect',
'db.getSchemaTree',
'db.query',
'db.disconnect',
'system.shutdown',
]),
);
expect(
report.checks.firstWhere((c) => c.name == 'db.query').status,
HarnessCheckStatus.passed,
);
});

test('a driver that never answers the handshake fails fast and skips the rest',
timeout: _timeout, skip: skip, () async {
writeStubDriver(root, mode: 'mute');

final report = await QueryaExtensionHarness(
requestTimeout: const Duration(seconds: 1),
shutdownTimeout: const Duration(milliseconds: 500),
).run(extensionRoot: root.path, targetVersion: '0.5.0');

expect(report.isSuccess, isFalse);
expect(
report.checks.firstWhere((c) => c.name == 'system.handshake').status,
HarnessCheckStatus.failed,
);
expect(report.skippedCount, greaterThan(0));
expect(
report.checks.firstWhere((c) => c.name == 'db.query').status,
HarnessCheckStatus.skipped,
);
});

test('a missing entry point is reported as a launch failure',
timeout: _timeout, () async {
File(p.join(root.path, 'manifest.json'))
.writeAsStringSync(jsonEncode(stubManifest()));

final report = await QueryaExtensionHarness()
.run(extensionRoot: root.path, targetVersion: '0.5.0');

expect(report.isSuccess, isFalse);
expect(
report.checks.firstWhere((c) => c.name == 'launch').message,
contains('entry point not found'),
);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:querya_desktop/core/extensions/harness/extension_harness.dart';

import 'e2e_stub_driver.dart';

const _timeout = Timeout(Duration(seconds: 60));

/// Same probe the harness uses before it wraps a plugin in `bwrap`.
bool _bwrapUsable() {
if (!Platform.isLinux) return false;
try {
return Process.runSync(
'bwrap', const ['--ro-bind', '/', '/', '/bin/true']).exitCode ==
0;
} catch (_) {
return false;
}
}

void main() {
final skip = stubDriverSkipReason();
late Directory root;
late Directory hostDir;

setUp(() {
root = Directory.systemTemp.createTempSync('querya_e2e_sandbox_ext_');
hostDir = Directory.systemTemp.createTempSync('querya_e2e_sandbox_host_');
});
tearDown(() {
root.deleteSync(recursive: true);
hostDir.deleteSync(recursive: true);
});

Future<void> runProbe({required bool sandbox}) async {
final report = await QueryaExtensionHarness(
shutdownTimeout: const Duration(seconds: 3),
).run(
extensionRoot: root.path,
targetVersion: '0.5.0',
sandbox: sandbox,
);
expect(report.failedCount, 0, reason: report.toConsole());
}

test('without a sandbox the probe writes to the host (control)',
timeout: _timeout, skip: skip, () async {
final target = p.join(hostDir.path, 'escaped.txt');
writeStubDriver(root, mode: 'probe', probeFile: target);

await runProbe(sandbox: false);

expect(File(target).existsSync(), isTrue);
});

test('inside bubblewrap the same write cannot reach the host filesystem',
timeout: _timeout,
skip: skip ?? (_bwrapUsable() ? null : 'bwrap is not usable here'),
() async {
final target = p.join(hostDir.path, 'escaped.txt');
writeStubDriver(root, mode: 'probe', probeFile: target);

await runProbe(sandbox: true);

expect(File(target).existsSync(), isFalse);
expect(hostDir.listSync(), isEmpty);
});
}
90 changes: 90 additions & 0 deletions test/e2e/05_extensions_and_marketplace/e2e_stub_driver.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import 'dart:convert';
import 'dart:io';

import 'package:path/path.dart' as p;

/// Why scenarios that start a real driver process cannot run here, or null.
String? stubDriverSkipReason() {
if (Platform.isWindows) return 'the stub driver is a POSIX script';
try {
if (Process.runSync('python3', const ['--version']).exitCode == 0) {
return null;
}
} catch (_) {}
return 'python3 is not available';
}

Map<String, Object?> stubManifest() => {
'id': 'acme.stub',
'name': 'Stub',
'version': '1.0.0',
'type': 'database_driver',
'main': 'bin/driver',
'engines': {'querya_desktop': '^0.4.11'},
'contributions': {
'drivers': [
{'driverId': 'stub', 'displayName': 'Stub'},
],
},
};

/// Writes a manifest and an executable JSON-RPC driver speaking newline
/// delimited JSON over stdio.
///
/// [mode]: `healthy` answers everything, `mute` ignores `system.handshake`,
/// `probe` tries to create [probeFile] during `db.query`.
void writeStubDriver(Directory root, {required String mode, String? probeFile}) {
File(p.join(root.path, 'manifest.json'))
.writeAsStringSync(jsonEncode(stubManifest()));
final entry = File(p.join(root.path, 'bin', 'driver'))
..createSync(recursive: true)
..writeAsStringSync('''#!/usr/bin/env python3
import json
import sys

MODE = ${jsonEncode(mode)}
PROBE = ${jsonEncode(probeFile ?? '')}


def reply(i, result=None):
sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": i, "result": result}) + "\\n")
sys.stdout.flush()


for line in sys.stdin:
msg = json.loads(line)
method = msg["method"]
i = msg.get("id")
if MODE == "mute" and method == "system.handshake":
continue
if method == "system.shutdown":
reply(i)
sys.exit(0)
if method == "system.handshake":
result = {"protocolVersion": 1}
elif method == "system.ping":
result = "pong"
elif method == "db.connect":
result = {"serverVersion": "e2e"}
elif method == "db.getCapabilities":
result = {"supportsTransactions": False}
elif method == "db.getServerStats":
result = {"uptime": 1}
elif method == "db.getSchemaTree":
result = {"nodes": []}
elif method == "db.query":
if MODE == "probe" and PROBE:
try:
with open(PROBE, "w") as f:
f.write("escaped")
except OSError:
pass
result = {"columns": ["one"], "rows": [[1]]}
elif method == "db.getTableSchema":
result = {"columns": []}
else:
result = None
reply(i, result)
''');
Process.runSync('chmod', ['+x', entry.path]);
}
Loading