281 lines
8.8 KiB
Dart
281 lines
8.8 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:uto_manager/domain/services/command_event_store.dart';
|
|
|
|
void main() {
|
|
// ---------------------------------------------------------------------------
|
|
// CommandEvent
|
|
// ---------------------------------------------------------------------------
|
|
|
|
group('CommandEvent', () {
|
|
test('toJson / fromJson round-trip preserves all fields', () {
|
|
final event = CommandEvent(
|
|
eventId: 'evt-001',
|
|
type: CommandEventType.attachNode,
|
|
payload: CommandPayloads.attachNode(
|
|
unitId: 'unit-1',
|
|
newParentId: 'unit-2',
|
|
previousParentId: 'unit-0',
|
|
),
|
|
occurredAt: DateTime.utc(2026, 8, 9, 10),
|
|
);
|
|
|
|
final json = event.toJson();
|
|
final restored = CommandEvent.fromJson(json);
|
|
|
|
expect(restored.eventId, event.eventId);
|
|
expect(restored.type, event.type);
|
|
expect(restored.payload['unitId'], 'unit-1');
|
|
expect(restored.payload['newParentId'], 'unit-2');
|
|
expect(restored.payload['previousParentId'], 'unit-0');
|
|
expect(restored.occurredAt, event.occurredAt);
|
|
expect(restored.isSynced, false);
|
|
});
|
|
|
|
test('fromJson handles missing isSynced (defaults to false)', () {
|
|
final map = {
|
|
'eventId': 'evt-002',
|
|
'type': 'detachNode',
|
|
'payload': {'unitId': 'u1', 'previousParentId': 'u0'},
|
|
'occurredAt': '2026-08-09T10:00:00.000Z',
|
|
// isSynced intentionally omitted
|
|
};
|
|
final event = CommandEvent.fromJson(map);
|
|
expect(event.isSynced, false);
|
|
});
|
|
|
|
test('toString includes eventId, type, and synced flag', () {
|
|
final event = CommandEvent(
|
|
eventId: 'evt-003',
|
|
type: CommandEventType.assignPersonnel,
|
|
payload: {},
|
|
occurredAt: DateTime.now(),
|
|
);
|
|
final str = event.toString();
|
|
expect(str, contains('evt-003'));
|
|
expect(str, contains('assignPersonnel'));
|
|
expect(str, contains('false'));
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CommandPayloads
|
|
// ---------------------------------------------------------------------------
|
|
|
|
group('CommandPayloads', () {
|
|
test('attachNode includes optional previousParentId when provided', () {
|
|
final p = CommandPayloads.attachNode(
|
|
unitId: 'u1',
|
|
newParentId: 'u2',
|
|
previousParentId: 'u0',
|
|
);
|
|
expect(p['unitId'], 'u1');
|
|
expect(p['newParentId'], 'u2');
|
|
expect(p['previousParentId'], 'u0');
|
|
});
|
|
|
|
test('attachNode omits previousParentId when null', () {
|
|
final p = CommandPayloads.attachNode(unitId: 'u1', newParentId: 'u2');
|
|
expect(p.containsKey('previousParentId'), false);
|
|
});
|
|
|
|
test('detachNode payload is correct', () {
|
|
final p = CommandPayloads.detachNode(unitId: 'u1', previousParentId: 'u0');
|
|
expect(p['unitId'], 'u1');
|
|
expect(p['previousParentId'], 'u0');
|
|
});
|
|
|
|
test('assignPersonnel payload is correct', () {
|
|
final p = CommandPayloads.assignPersonnel(
|
|
soldierId: 's1',
|
|
slotId: 'sl1',
|
|
unitId: 'u1',
|
|
);
|
|
expect(p['soldierId'], 's1');
|
|
expect(p['slotId'], 'sl1');
|
|
expect(p['unitId'], 'u1');
|
|
});
|
|
|
|
test('unassignPersonnel payload is correct', () {
|
|
final p = CommandPayloads.unassignPersonnel(
|
|
soldierId: 's1',
|
|
slotId: 'sl1',
|
|
unitId: 'u1',
|
|
);
|
|
expect(p['soldierId'], 's1');
|
|
expect(p['slotId'], 'sl1');
|
|
});
|
|
|
|
test('modifySlot payload includes changes map', () {
|
|
final p = CommandPayloads.modifySlot(
|
|
slotId: 'sl1',
|
|
unitId: 'u1',
|
|
changes: {'positionTitle': 'Squad Leader'},
|
|
);
|
|
expect(p['slotId'], 'sl1');
|
|
expect((p['changes'] as Map)['positionTitle'], 'Squad Leader');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CommandEventStore
|
|
// ---------------------------------------------------------------------------
|
|
|
|
group('CommandEventStore', () {
|
|
late CommandEventStore store;
|
|
|
|
setUp(() => store = CommandEventStore());
|
|
|
|
test('starts empty', () {
|
|
expect(store.eventCount, 0);
|
|
expect(store.pendingCount, 0);
|
|
expect(store.allEvents, isEmpty);
|
|
expect(store.pendingEvents, isEmpty);
|
|
});
|
|
|
|
test('append adds event to store', () {
|
|
final event = _makeEvent('e1', CommandEventType.attachNode);
|
|
store.append(event);
|
|
expect(store.eventCount, 1);
|
|
expect(store.pendingCount, 1);
|
|
});
|
|
|
|
test('allEvents is unmodifiable', () {
|
|
store.append(_makeEvent('e1', CommandEventType.attachNode));
|
|
expect(
|
|
() => store.allEvents.add(_makeEvent('e2', CommandEventType.detachNode)),
|
|
throwsUnsupportedError,
|
|
);
|
|
});
|
|
|
|
test('pendingEvents excludes synced events', () {
|
|
final e1 = _makeEvent('e1', CommandEventType.attachNode);
|
|
final e2 = _makeEvent('e2', CommandEventType.detachNode);
|
|
store
|
|
..append(e1)
|
|
..append(e2)
|
|
..markSynced('e1');
|
|
|
|
expect(store.pendingCount, 1);
|
|
expect(store.pendingEvents.first.eventId, 'e2');
|
|
});
|
|
|
|
test('markSynced returns true for existing event', () {
|
|
store.append(_makeEvent('e1', CommandEventType.attachNode));
|
|
expect(store.markSynced('e1'), true);
|
|
expect(store.pendingCount, 0);
|
|
});
|
|
|
|
test('markSynced returns false for unknown eventId', () {
|
|
expect(store.markSynced('nonexistent'), false);
|
|
});
|
|
|
|
test('markAllSynced marks every event', () {
|
|
store
|
|
..append(_makeEvent('e1', CommandEventType.attachNode))
|
|
..append(_makeEvent('e2', CommandEventType.assignPersonnel))
|
|
..append(_makeEvent('e3', CommandEventType.modifySlot))
|
|
..markAllSynced();
|
|
expect(store.pendingCount, 0);
|
|
});
|
|
|
|
test('exportPendingToJson produces valid JSON array', () {
|
|
store
|
|
..append(_makeEvent('e1', CommandEventType.attachNode))
|
|
..append(_makeEvent('e2', CommandEventType.detachNode))
|
|
..markSynced('e1'); // e1 is synced, only e2 should appear
|
|
|
|
final json = store.exportPendingToJson();
|
|
final decoded = jsonDecode(json) as List<dynamic>;
|
|
expect(decoded.length, 1);
|
|
expect((decoded.first as Map)['eventId'], 'e2');
|
|
});
|
|
|
|
test('clear removes all events', () {
|
|
store
|
|
..append(_makeEvent('e1', CommandEventType.attachNode))
|
|
..append(_makeEvent('e2', CommandEventType.detachNode))
|
|
..clear();
|
|
expect(store.eventCount, 0);
|
|
});
|
|
|
|
test('multiple event types can be stored together', () {
|
|
for (final type in CommandEventType.values) {
|
|
store.append(_makeEvent(type.name, type));
|
|
}
|
|
expect(store.eventCount, CommandEventType.values.length);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DeltaSynchronizer
|
|
// ---------------------------------------------------------------------------
|
|
|
|
group('DeltaSynchronizer', () {
|
|
late CommandEventStore store;
|
|
late DeltaSynchronizer sync;
|
|
|
|
setUp(() {
|
|
store = CommandEventStore();
|
|
sync = DeltaSynchronizer(store: store);
|
|
});
|
|
|
|
test('syncIfOnline returns 0 when no pending events', () async {
|
|
final count = await sync.syncIfOnline();
|
|
expect(count, 0);
|
|
});
|
|
|
|
test('syncIfOnline returns 0 when offline', () async {
|
|
store.append(_makeEvent('e1', CommandEventType.attachNode));
|
|
sync.isOnline = false;
|
|
final count = await sync.syncIfOnline();
|
|
expect(count, 0);
|
|
expect(store.pendingCount, 1); // still pending
|
|
});
|
|
|
|
test('syncIfOnline transmits pending events when online', () async {
|
|
store
|
|
..append(_makeEvent('e1', CommandEventType.attachNode))
|
|
..append(_makeEvent('e2', CommandEventType.assignPersonnel));
|
|
sync.isOnline = true;
|
|
|
|
final count = await sync.syncIfOnline();
|
|
expect(count, 2);
|
|
expect(store.pendingCount, 0); // all marked synced
|
|
});
|
|
|
|
test('isOnline getter reflects set value', () {
|
|
sync.isOnline = false;
|
|
expect(sync.isOnline, false);
|
|
sync.isOnline = true;
|
|
expect(sync.isOnline, true);
|
|
});
|
|
|
|
test('already-synced events are not re-transmitted', () async {
|
|
final e1 = _makeEvent('e1', CommandEventType.attachNode);
|
|
final e2 = _makeEvent('e2', CommandEventType.detachNode);
|
|
store
|
|
..append(e1)
|
|
..append(e2)
|
|
..markSynced('e1');
|
|
|
|
final count = await sync.syncIfOnline();
|
|
expect(count, 1); // only e2 was pending
|
|
});
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
CommandEvent _makeEvent(String id, CommandEventType type) {
|
|
return CommandEvent(
|
|
eventId: id,
|
|
type: type,
|
|
payload: {'test': id},
|
|
occurredAt: DateTime.utc(2026, 8, 9),
|
|
);
|
|
}
|