ChannelRegionScopeStore persists a channel-index -> region-name map per device (first 10 hex of the device key, matching the other channel stores). set/clear/scopeFor, null = unscoped. Tests cover round-trip, per-device and per-channel isolation, persistence, and no-device-key guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/844-per-radio-notify
parent
2ff7c71aa4
commit
d83cbfae0c
@ -0,0 +1,39 @@
|
||||
import '../utils/app_logger.dart';
|
||||
import 'prefs_manager.dart';
|
||||
|
||||
/// Per-channel region scope, persisted per device (Feature #812, Epic #815).
|
||||
///
|
||||
/// Stores the region name a channel's outgoing messages should ride, keyed by
|
||||
/// the first 10 hex chars of the connected device's public key (the same
|
||||
/// scoping the other channel stores use). The name is the `#`-stripped region
|
||||
/// name from discovery; null/absent means the channel is unscoped.
|
||||
class ChannelRegionScopeStore {
|
||||
static const String _keyPrefix = 'channel_region_scope_';
|
||||
|
||||
String publicKeyHex = '';
|
||||
set setPublicKeyHex(String value) =>
|
||||
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
|
||||
|
||||
String _key(int channelIndex) => '$_keyPrefix${publicKeyHex}_$channelIndex';
|
||||
|
||||
/// The region name scoping [channelIndex], or null when unscoped.
|
||||
Future<String?> scopeFor(int channelIndex) async {
|
||||
if (publicKeyHex.isEmpty) return null;
|
||||
return PrefsManager.instance.getString(_key(channelIndex));
|
||||
}
|
||||
|
||||
/// Scope [channelIndex] to [regionName].
|
||||
Future<void> setScope(int channelIndex, String regionName) async {
|
||||
if (publicKeyHex.isEmpty) {
|
||||
appLogger.warn('No device key set; cannot save channel region scope.');
|
||||
return;
|
||||
}
|
||||
await PrefsManager.instance.setString(_key(channelIndex), regionName);
|
||||
}
|
||||
|
||||
/// Remove the region scope on [channelIndex] (back to unscoped).
|
||||
Future<void> clearScope(int channelIndex) async {
|
||||
if (publicKeyHex.isEmpty) return;
|
||||
await PrefsManager.instance.remove(_key(channelIndex));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
// Epic #815 T C1: per-channel region-scope store.
|
||||
// Device-key scoped (first 10 hex chars of the device public key), channel -> region name.
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_open/storage/channel_region_scope_store.dart';
|
||||
import 'package:meshcore_open/storage/prefs_manager.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
const _devA = 'aabbccddee0011223344';
|
||||
const _devB = 'ffeeddccbb0011223344';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
PrefsManager.reset();
|
||||
await PrefsManager.initialize();
|
||||
});
|
||||
|
||||
ChannelRegionScopeStore storeFor(String deviceKeyHex) {
|
||||
final s = ChannelRegionScopeStore();
|
||||
s.setPublicKeyHex = deviceKeyHex;
|
||||
return s;
|
||||
}
|
||||
|
||||
test('set / get / clear round-trip', () async {
|
||||
final store = storeFor(_devA);
|
||||
expect(await store.scopeFor(0), isNull);
|
||||
await store.setScope(0, 'oki');
|
||||
expect(await store.scopeFor(0), 'oki');
|
||||
await store.clearScope(0);
|
||||
expect(await store.scopeFor(0), isNull);
|
||||
});
|
||||
|
||||
test('scopes are isolated per device key', () async {
|
||||
await storeFor(_devA).setScope(2, 'oki');
|
||||
expect(await storeFor(_devB).scopeFor(2), isNull);
|
||||
expect(await storeFor(_devA).scopeFor(2), 'oki');
|
||||
});
|
||||
|
||||
test('scopes are isolated per channel index', () async {
|
||||
final store = storeFor(_devA);
|
||||
await store.setScope(1, 'oki');
|
||||
expect(await store.scopeFor(3), isNull);
|
||||
expect(await store.scopeFor(1), 'oki');
|
||||
});
|
||||
|
||||
test('survives a fresh store instance (persisted to prefs)', () async {
|
||||
await storeFor(_devA).setScope(5, 'test');
|
||||
final reopened = storeFor(_devA);
|
||||
expect(await reopened.scopeFor(5), 'test');
|
||||
});
|
||||
|
||||
test('with no device key set, reads null and writes are refused', () async {
|
||||
final store = ChannelRegionScopeStore();
|
||||
await store.setScope(0, 'oki');
|
||||
expect(await store.scopeFor(0), isNull);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in new issue