mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Ensure new profile starts empty
This commit is contained in:
@@ -92,14 +92,6 @@ class ProfilesScreen extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
child: const Text('Open'),
|
child: const Text('Open'),
|
||||||
),
|
),
|
||||||
FilledButton(
|
|
||||||
onPressed: () async {
|
|
||||||
await context
|
|
||||||
.read<ProfileWorkspaceCoordinator>()
|
|
||||||
.applyProfile(profile.id);
|
|
||||||
},
|
|
||||||
child: const Text('Apply'),
|
|
||||||
),
|
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final resolved =
|
final resolved =
|
||||||
|
|||||||
@@ -134,8 +134,7 @@ class ProfileWorkspaceCoordinator {
|
|||||||
name: name,
|
name: name,
|
||||||
notes: notes,
|
notes: notes,
|
||||||
);
|
);
|
||||||
await messagesProvider.cloneCurrentStorageTo(profileId);
|
await _initializeEmptyStorageNamespace(profileId);
|
||||||
await contactsProvider.cloneCurrentStorageTo(profileId);
|
|
||||||
await profileManager.upsertProfile(profile);
|
await profileManager.upsertProfile(profile);
|
||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
@@ -180,6 +179,7 @@ class ProfileWorkspaceCoordinator {
|
|||||||
|
|
||||||
Future<void> openProfile(String profileId) async {
|
Future<void> openProfile(String profileId) async {
|
||||||
await _saveActiveCustomProfileSnapshot();
|
await _saveActiveCustomProfileSnapshot();
|
||||||
|
await connectionProvider.disconnect();
|
||||||
await profileManager.setActiveProfileId(profileId);
|
await profileManager.setActiveProfileId(profileId);
|
||||||
await _switchRuntimeScope(profileId);
|
await _switchRuntimeScope(profileId);
|
||||||
|
|
||||||
@@ -360,4 +360,11 @@ class ProfileWorkspaceCoordinator {
|
|||||||
namespace: targetNamespace,
|
namespace: targetNamespace,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _initializeEmptyStorageNamespace(String namespace) async {
|
||||||
|
await _messageStorageService.clearMessages(namespace: namespace);
|
||||||
|
await _contactStorageService.clearContacts(namespace: namespace);
|
||||||
|
await _contactStorageService.clearContactGroups(namespace: namespace);
|
||||||
|
await _contactStorageService.clearPendingAdverts(namespace: namespace);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
333
test/services/profile_workspace_coordinator_test.dart
Normal file
333
test/services/profile_workspace_coordinator_test.dart
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:meshcore_client/meshcore_client.dart';
|
||||||
|
import 'package:meshcore_sar_app/models/config_profile.dart';
|
||||||
|
import 'package:meshcore_sar_app/models/contact.dart';
|
||||||
|
import 'package:meshcore_sar_app/models/contact_group.dart';
|
||||||
|
import 'package:meshcore_sar_app/models/device_info.dart';
|
||||||
|
import 'package:meshcore_sar_app/providers/app_provider.dart';
|
||||||
|
import 'package:meshcore_sar_app/providers/channels_provider.dart';
|
||||||
|
import 'package:meshcore_sar_app/providers/connection_provider.dart';
|
||||||
|
import 'package:meshcore_sar_app/providers/contacts_provider.dart';
|
||||||
|
import 'package:meshcore_sar_app/providers/drawing_provider.dart';
|
||||||
|
import 'package:meshcore_sar_app/providers/map_provider.dart';
|
||||||
|
import 'package:meshcore_sar_app/providers/messages_provider.dart';
|
||||||
|
import 'package:meshcore_sar_app/services/app_config_snapshot_service.dart';
|
||||||
|
import 'package:meshcore_sar_app/services/contact_storage_service.dart';
|
||||||
|
import 'package:meshcore_sar_app/services/device_config_applicator.dart';
|
||||||
|
import 'package:meshcore_sar_app/services/message_storage_service.dart';
|
||||||
|
import 'package:meshcore_sar_app/services/profile_manager.dart';
|
||||||
|
import 'package:meshcore_sar_app/services/profile_workspace_coordinator.dart';
|
||||||
|
import 'package:meshcore_sar_app/services/profiles_feature_service.dart';
|
||||||
|
import 'package:meshcore_sar_app/services/map_workspace_snapshot_service.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
ProfileStorageScope.setScope(
|
||||||
|
profilesEnabled: true,
|
||||||
|
activeProfileId: ConfigProfile.defaultProfileId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('ProfileWorkspaceCoordinator', () {
|
||||||
|
test(
|
||||||
|
'createProfileFromCurrent starts with empty contacts and messages',
|
||||||
|
() async {
|
||||||
|
final manager = ProfileManager();
|
||||||
|
await manager.initialize();
|
||||||
|
await manager.setProfilesEnabled(true);
|
||||||
|
|
||||||
|
final messageStorage = MessageStorageService();
|
||||||
|
final contactStorage = ContactStorageService();
|
||||||
|
|
||||||
|
await messageStorage.saveMessages([_buildMessage('default-msg')]);
|
||||||
|
await contactStorage.saveContacts([
|
||||||
|
_buildContact('Default Contact', 1),
|
||||||
|
]);
|
||||||
|
await contactStorage.saveContactGroups([
|
||||||
|
SavedContactGroup(
|
||||||
|
id: 'group-1',
|
||||||
|
sectionKey: 'manual',
|
||||||
|
label: 'Team',
|
||||||
|
query: '',
|
||||||
|
createdAt: DateTime.parse('2026-03-16T12:00:00Z'),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
await contactStorage.savePendingAdverts([
|
||||||
|
{'id': 'pending-1'},
|
||||||
|
]);
|
||||||
|
|
||||||
|
final coordinator = _buildCoordinator(profileManager: manager);
|
||||||
|
|
||||||
|
final profile = await coordinator.createProfileFromCurrent(
|
||||||
|
name: 'Empty Profile',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await messageStorage.loadMessages(namespace: profile.id),
|
||||||
|
isEmpty,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
await contactStorage.loadContacts(namespace: profile.id),
|
||||||
|
isEmpty,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
await contactStorage.loadContactGroups(namespace: profile.id),
|
||||||
|
isEmpty,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
await contactStorage.loadPendingAdverts(namespace: profile.id),
|
||||||
|
isEmpty,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await messageStorage.loadMessages(), hasLength(1));
|
||||||
|
expect(await contactStorage.loadContacts(), hasLength(1));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'duplicateProfile copies contacts and messages from source profile',
|
||||||
|
() async {
|
||||||
|
final manager = ProfileManager();
|
||||||
|
await manager.initialize();
|
||||||
|
await manager.setProfilesEnabled(true);
|
||||||
|
|
||||||
|
final source = ConfigProfile(
|
||||||
|
id: 'profile-source',
|
||||||
|
name: 'Source',
|
||||||
|
createdAt: DateTime.parse('2026-03-16T12:00:00Z'),
|
||||||
|
updatedAt: DateTime.parse('2026-03-16T12:00:00Z'),
|
||||||
|
sections: const ConfigProfileSections(),
|
||||||
|
);
|
||||||
|
await manager.upsertProfile(source);
|
||||||
|
|
||||||
|
final messageStorage = MessageStorageService();
|
||||||
|
final contactStorage = ContactStorageService();
|
||||||
|
|
||||||
|
await messageStorage.saveMessages([
|
||||||
|
_buildMessage('source-msg'),
|
||||||
|
], namespace: source.id);
|
||||||
|
await contactStorage.saveContacts([
|
||||||
|
_buildContact('Source Contact', 2),
|
||||||
|
], namespace: source.id);
|
||||||
|
await contactStorage.saveContactGroups([
|
||||||
|
SavedContactGroup(
|
||||||
|
id: 'source-group',
|
||||||
|
sectionKey: 'manual',
|
||||||
|
label: 'Source Team',
|
||||||
|
query: '',
|
||||||
|
createdAt: DateTime.parse('2026-03-16T12:00:00Z'),
|
||||||
|
),
|
||||||
|
], namespace: source.id);
|
||||||
|
await contactStorage.savePendingAdverts([
|
||||||
|
{'id': 'source-pending'},
|
||||||
|
], namespace: source.id);
|
||||||
|
|
||||||
|
final coordinator = _buildCoordinator(profileManager: manager);
|
||||||
|
|
||||||
|
final duplicate = await coordinator.duplicateProfile(source);
|
||||||
|
|
||||||
|
final duplicatedMessages = await messageStorage.loadMessages(
|
||||||
|
namespace: duplicate.id,
|
||||||
|
);
|
||||||
|
final duplicatedContacts = await contactStorage.loadContacts(
|
||||||
|
namespace: duplicate.id,
|
||||||
|
);
|
||||||
|
final duplicatedGroups = await contactStorage.loadContactGroups(
|
||||||
|
namespace: duplicate.id,
|
||||||
|
);
|
||||||
|
final duplicatedPending = await contactStorage.loadPendingAdverts(
|
||||||
|
namespace: duplicate.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(duplicatedMessages.map((item) => item.id), ['source-msg']);
|
||||||
|
expect(duplicatedContacts.map((item) => item.advName), [
|
||||||
|
'Source Contact',
|
||||||
|
]);
|
||||||
|
expect(duplicatedGroups.map((item) => item.id), ['source-group']);
|
||||||
|
expect(duplicatedPending, [
|
||||||
|
{'id': 'source-pending'},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'openProfile disconnects current connection before switching',
|
||||||
|
() async {
|
||||||
|
final manager = ProfileManager();
|
||||||
|
await manager.initialize();
|
||||||
|
await manager.setProfilesEnabled(true);
|
||||||
|
|
||||||
|
final target = ConfigProfile(
|
||||||
|
id: 'profile-target',
|
||||||
|
name: 'Target',
|
||||||
|
createdAt: DateTime.parse('2026-03-16T12:00:00Z'),
|
||||||
|
updatedAt: DateTime.parse('2026-03-16T12:00:00Z'),
|
||||||
|
sections: const ConfigProfileSections(),
|
||||||
|
);
|
||||||
|
await manager.upsertProfile(target);
|
||||||
|
|
||||||
|
final connectionProvider = _FakeConnectionProvider();
|
||||||
|
final coordinator = _buildCoordinator(
|
||||||
|
profileManager: manager,
|
||||||
|
connectionProvider: connectionProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
await coordinator.openProfile(target.id);
|
||||||
|
|
||||||
|
expect(connectionProvider.disconnectCallCount, 1);
|
||||||
|
expect(manager.activeProfileId, target.id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileWorkspaceCoordinator _buildCoordinator({
|
||||||
|
required ProfileManager profileManager,
|
||||||
|
_FakeConnectionProvider? connectionProvider,
|
||||||
|
}) {
|
||||||
|
return ProfileWorkspaceCoordinator(
|
||||||
|
profileManager: profileManager,
|
||||||
|
connectionProvider: connectionProvider ?? _FakeConnectionProvider(),
|
||||||
|
contactsProvider: _FakeContactsProvider(),
|
||||||
|
messagesProvider: _FakeMessagesProvider(),
|
||||||
|
mapProvider: _FakeMapProvider(),
|
||||||
|
drawingProvider: _FakeDrawingProvider(),
|
||||||
|
channelsProvider: _FakeChannelsProvider(),
|
||||||
|
appProvider: _FakeAppProvider(),
|
||||||
|
appConfigSnapshotService: _FakeAppConfigSnapshotService(),
|
||||||
|
mapWorkspaceSnapshotService: _FakeMapWorkspaceSnapshotService(),
|
||||||
|
deviceConfigApplicator: _FakeDeviceConfigApplicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Message _buildMessage(String id) {
|
||||||
|
return Message(
|
||||||
|
id: id,
|
||||||
|
messageType: MessageType.channel,
|
||||||
|
senderPublicKeyPrefix: Uint8List.fromList([1, 2, 3, 4, 5, 6]),
|
||||||
|
channelIdx: 0,
|
||||||
|
pathLen: 0,
|
||||||
|
textType: MessageTextType.plain,
|
||||||
|
senderTimestamp: 1700000000,
|
||||||
|
text: id,
|
||||||
|
receivedAt: DateTime.fromMillisecondsSinceEpoch(1700000000500),
|
||||||
|
isRead: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Contact _buildContact(String name, int fillByte) {
|
||||||
|
return Contact(
|
||||||
|
publicKey: Uint8List.fromList(List<int>.filled(32, fillByte)),
|
||||||
|
type: ContactType.chat,
|
||||||
|
flags: 0,
|
||||||
|
outPathLen: 0,
|
||||||
|
outPath: Uint8List(64),
|
||||||
|
advName: name,
|
||||||
|
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
advLat: (46.0569 * 1e6).round(),
|
||||||
|
advLon: (14.5058 * 1e6).round(),
|
||||||
|
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeAppConfigSnapshotService extends AppConfigSnapshotService {
|
||||||
|
@override
|
||||||
|
Future<AppSettingsProfileSection> capture(AppProvider appProvider) async {
|
||||||
|
return const AppSettingsProfileSection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeMapWorkspaceSnapshotService extends MapWorkspaceSnapshotService {
|
||||||
|
@override
|
||||||
|
Future<MapWorkspaceProfileSection> capture({
|
||||||
|
required MapProvider mapProvider,
|
||||||
|
required DrawingProvider drawingProvider,
|
||||||
|
}) async {
|
||||||
|
return const MapWorkspaceProfileSection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeDeviceConfigApplicator extends DeviceConfigApplicator {
|
||||||
|
@override
|
||||||
|
ConfigProfileSections capture({
|
||||||
|
required ConnectionProvider connectionProvider,
|
||||||
|
required ChannelsProvider channelsProvider,
|
||||||
|
}) {
|
||||||
|
return const ConfigProfileSections();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeConnectionProvider implements ConnectionProvider {
|
||||||
|
int disconnectCallCount = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DeviceInfo get deviceInfo => DeviceInfo();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> disconnect() async {
|
||||||
|
disconnectCallCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeContactsProvider implements ContactsProvider {
|
||||||
|
@override
|
||||||
|
Future<void> persistNow() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> reloadFromStorage({
|
||||||
|
String? namespace,
|
||||||
|
Uint8List? devicePublicKey,
|
||||||
|
}) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeMessagesProvider implements MessagesProvider {
|
||||||
|
@override
|
||||||
|
Future<void> persistNow() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> reloadFromStorage({String? namespace}) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeMapProvider implements MapProvider {
|
||||||
|
@override
|
||||||
|
Future<void> reloadProfileScopedState() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeDrawingProvider implements DrawingProvider {
|
||||||
|
@override
|
||||||
|
Future<void> reloadProfileScopedState() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeChannelsProvider implements ChannelsProvider {
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeAppProvider implements AppProvider {
|
||||||
|
@override
|
||||||
|
Future<void> reloadProfileScopedSettings() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user