uto_manager/test/domain/services/command_event_store_test.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),
);
}