You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
meshcore-client/test/services/local_cli_service_test.dart

147 lines
5.0 KiB

import 'dart:convert';
import 'dart:typed_data';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/connector/cli_command_codec.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/services/app_debug_log_service.dart';
import 'package:meshcore_open/services/command_correlation.dart';
import 'package:meshcore_open/services/local_cli_service.dart';
import 'package:meshcore_open/utils/app_logger.dart';
import 'package:meshcore_open/storage/drift/blob_store.dart';
import 'package:meshcore_open/storage/drift/offband_database.dart';
import 'package:meshcore_open/storage/prefs_manager.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
group('PrefixAllocator / splitCorrelated', () {
test('allocates unique XX| tokens and skips in-flight ones', () {
final a = PrefixAllocator();
expect(a.next({}), '00|');
expect(a.next({}), '01|');
expect(a.next({'02|'}), '03|');
});
test('wraps after 256 tokens', () {
final a = PrefixAllocator();
for (var i = 0; i < 256; i++) {
a.next({});
}
expect(a.next({}), '00|');
});
test('split honors the positional rule', () {
final c = splitCorrelated('0A| > ok');
expect(c.prefix, '0A|');
expect(c.payload, '> ok');
final plain = splitCorrelated('Unknown command');
expect(plain.prefix, isNull);
expect(plain.payload, 'Unknown command');
});
});
group('LocalCliService', () {
late OffbandDatabase db;
late MeshCoreConnector connector;
late LocalCliService service;
late List<Uint8List> sent;
setUp(() async {
SharedPreferences.setMockInitialValues({});
PrefsManager.reset();
await PrefsManager.initialize();
db = OffbandDatabase(NativeDatabase.memory());
BlobStore.overrideForTest(BlobStore(db));
connector = MeshCoreConnector();
sent = [];
connector.sendFrameOverrideForTest = sent.add;
connector.setConnectedForTest();
service = LocalCliService(connector);
});
tearDown(() async {
service.dispose();
BlobStore.clearTestOverride();
await db.close();
});
Uint8List replyFor(Uint8List request, String text) {
final prefix = ascii.decode(request.sublist(1, 4));
return Uint8List.fromList([
respCodeCliReply,
...ascii.encode('$prefix$text'),
]);
}
test('round trip: command out, correlated reply completes it', () async {
final future = service.run('get radio');
expect(sent, hasLength(1));
expect(sent.single[0], cmdRunCliCommand);
connector.handleFrameForTest(replyFor(sent.single, '> 910.525,62.5,7,5'));
final reply = await future;
expect(reply.text, '> 910.525,62.5,7,5');
expect(reply.prefix, isNotNull);
});
test('a reply with a foreign prefix never completes a waiting command'
' - the command times out instead', () async {
final future = service.run(
'ver',
timeout: const Duration(milliseconds: 80),
);
connector.handleFrameForTest(
Uint8List.fromList([respCodeCliReply, ...ascii.encode('ZZ|nope')]),
);
await expectLater(future, throwsA(isA<LocalCliTimeout>()));
});
test('timeout surfaces as LocalCliTimeout', () async {
final future = service.run(
'ver',
timeout: const Duration(milliseconds: 50),
);
await expectLater(future, throwsA(isA<LocalCliTimeout>()));
});
test('two in-flight commands complete independently', () async {
final f1 = service.run('get af');
final f2 = service.run('get cad');
expect(sent, hasLength(2));
// Answer the SECOND first; each reply must find its own command.
connector.handleFrameForTest(replyFor(sent[1], '> off'));
connector.handleFrameForTest(replyFor(sent[0], '> 1.0'));
expect((await f2).text, '> off');
expect((await f1).text, '> 1.0');
});
test('a late reply to a secret command never logs its cleartext'
' (Gemini review #883)', () async {
final logService = AppDebugLogService();
appLogger.initialize(logService, enabled: true);
addTearDown(() => appLogger.setEnabled(false));
// The command times out, so its pending entry is dropped.
final future = service.run(
'get wifi.pwd',
timeout: const Duration(milliseconds: 40),
);
await expectLater(future, throwsA(isA<LocalCliTimeout>()));
// The device answers late, carrying the secret in cleartext.
const secret = 'hunter2-cleartext';
connector.handleFrameForTest(replyFor(sent.single, secret));
// The unmatched reply is logged for diagnostics, but its text is not.
expect(logService.entries, isNotEmpty);
for (final e in logService.entries) {
expect(e.message, isNot(contains(secret)));
}
expect(
logService.entries.any((e) => e.message.contains('text withheld')),
isTrue,
);
});
});
}

Powered by TurnKey Linux.