241 lines
7.9 KiB
Dart
241 lines
7.9 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:uto_manager/domain/entities/authorized_rank.dart';
|
|
import 'package:uto_manager/domain/entities/doctrinal_branch.dart';
|
|
import 'package:uto_manager/domain/entities/echelon.dart';
|
|
import 'package:uto_manager/domain/entities/personnel_record.dart';
|
|
import 'package:uto_manager/domain/entities/position_slot.dart';
|
|
import 'package:uto_manager/domain/entities/unit_node.dart';
|
|
import 'package:uto_manager/domain/value_objects/duty_mos.dart';
|
|
import 'package:uto_manager/infrastructure/codecs/unit_tree_codec.dart';
|
|
|
|
void main() {
|
|
const codec = UnitTreeCodec();
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Fixtures
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const brigade = UnitNode(
|
|
unitId: 'bde-1',
|
|
echelon: Echelon.brigade,
|
|
branch: DoctrinalBranch.infantry,
|
|
name: 'Test Brigade',
|
|
isKeyLeader: false,
|
|
);
|
|
|
|
const battalion = UnitNode(
|
|
unitId: 'bn-1',
|
|
echelon: Echelon.battalion,
|
|
branch: DoctrinalBranch.infantry,
|
|
parentUnitId: 'bde-1',
|
|
name: 'Test Battalion',
|
|
isKeyLeader: false,
|
|
);
|
|
|
|
final slot = PositionSlot(
|
|
slotId: 'slot-1',
|
|
unitId: 'bde-1',
|
|
positionTitle: 'Brigade Commander',
|
|
dutyMos: DutyMos.unvalidated('11A'),
|
|
authorizedRank: AuthorizedRank.o6,
|
|
isKeyLeader: true,
|
|
);
|
|
|
|
final soldier = PersonnelRecord(
|
|
soldierId: 'p-1',
|
|
name: 'COL Test',
|
|
primaryMos: DutyMos.unvalidated('11A'),
|
|
payGrade: AuthorizedRank.o6,
|
|
readinessState: ReadinessState.fullyMissionCapable,
|
|
assignedSlotId: 'slot-1',
|
|
);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// encode / decode round-trip
|
|
// ---------------------------------------------------------------------------
|
|
|
|
group('encode / decode round-trip', () {
|
|
test('empty snapshot round-trips correctly', () {
|
|
final json = codec.encode(units: [], slots: [], personnel: []);
|
|
final snapshot = codec.decode(json);
|
|
|
|
expect(snapshot.units, isEmpty);
|
|
expect(snapshot.slots, isEmpty);
|
|
expect(snapshot.personnel, isEmpty);
|
|
});
|
|
|
|
test('units round-trip with all fields preserved', () {
|
|
final json = codec.encode(
|
|
units: [brigade, battalion],
|
|
slots: [],
|
|
personnel: [],
|
|
);
|
|
final snapshot = codec.decode(json);
|
|
|
|
expect(snapshot.units.length, 2);
|
|
|
|
final decodedBde = snapshot.units.firstWhere((u) => u.unitId == 'bde-1');
|
|
expect(decodedBde.name, brigade.name);
|
|
expect(decodedBde.echelon, brigade.echelon);
|
|
expect(decodedBde.branch, brigade.branch);
|
|
expect(decodedBde.parentUnitId, isNull);
|
|
expect(decodedBde.isKeyLeader, brigade.isKeyLeader);
|
|
|
|
final decodedBn = snapshot.units.firstWhere((u) => u.unitId == 'bn-1');
|
|
expect(decodedBn.parentUnitId, 'bde-1');
|
|
});
|
|
|
|
test('slots round-trip with all fields preserved', () {
|
|
final json = codec.encode(
|
|
units: [brigade],
|
|
slots: [slot],
|
|
personnel: [],
|
|
);
|
|
final snapshot = codec.decode(json);
|
|
|
|
expect(snapshot.slots.length, 1);
|
|
final decodedSlot = snapshot.slots.first;
|
|
expect(decodedSlot.slotId, slot.slotId);
|
|
expect(decodedSlot.unitId, slot.unitId);
|
|
expect(decodedSlot.positionTitle, slot.positionTitle);
|
|
expect(decodedSlot.dutyMos.code, slot.dutyMos.code);
|
|
expect(decodedSlot.authorizedRank, slot.authorizedRank);
|
|
expect(decodedSlot.isKeyLeader, slot.isKeyLeader);
|
|
expect(decodedSlot.assignedPersonnelId, isNull);
|
|
});
|
|
|
|
test('personnel round-trip with all fields preserved', () {
|
|
final json = codec.encode(
|
|
units: [],
|
|
slots: [],
|
|
personnel: [soldier],
|
|
);
|
|
final snapshot = codec.decode(json);
|
|
|
|
expect(snapshot.personnel.length, 1);
|
|
final decoded = snapshot.personnel.first;
|
|
expect(decoded.soldierId, soldier.soldierId);
|
|
expect(decoded.name, soldier.name);
|
|
expect(decoded.primaryMos.code, soldier.primaryMos.code);
|
|
expect(decoded.payGrade, soldier.payGrade);
|
|
expect(decoded.readinessState, soldier.readinessState);
|
|
expect(decoded.assignedSlotId, soldier.assignedSlotId);
|
|
});
|
|
|
|
test('full snapshot round-trips correctly', () {
|
|
final json = codec.encode(
|
|
units: [brigade, battalion],
|
|
slots: [slot],
|
|
personnel: [soldier],
|
|
);
|
|
final snapshot = codec.decode(json);
|
|
|
|
expect(snapshot.units.length, 2);
|
|
expect(snapshot.slots.length, 1);
|
|
expect(snapshot.personnel.length, 1);
|
|
});
|
|
|
|
test('encodeToMap / decodeFromMap round-trip', () {
|
|
final map = codec.encodeToMap(
|
|
units: [brigade],
|
|
slots: [slot],
|
|
personnel: [],
|
|
);
|
|
final snapshot = codec.decodeFromMap(map);
|
|
|
|
expect(snapshot.units.length, 1);
|
|
expect(snapshot.slots.length, 1);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Schema version
|
|
// ---------------------------------------------------------------------------
|
|
|
|
group('schema version', () {
|
|
test('encoded JSON contains version field', () {
|
|
final json = codec.encode(units: [], slots: [], personnel: []);
|
|
expect(json, contains('"version"'));
|
|
expect(json, contains('"exportedAt"'));
|
|
});
|
|
|
|
test('throws FormatException for wrong schema version', () {
|
|
final map = codec.encodeToMap(units: [], slots: [], personnel: []);
|
|
map['version'] = 99;
|
|
|
|
expect(
|
|
() => codec.decodeFromMap(map),
|
|
throwsA(isA<FormatException>()),
|
|
);
|
|
});
|
|
|
|
test('throws FormatException for missing version', () {
|
|
final map = codec.encodeToMap(units: [], slots: [], personnel: [])..remove('version');
|
|
|
|
expect(
|
|
() => codec.decodeFromMap(map),
|
|
throwsA(isA<FormatException>()),
|
|
);
|
|
});
|
|
|
|
test('throws FormatException for non-object JSON root', () {
|
|
expect(
|
|
() => codec.decode('[1, 2, 3]'),
|
|
throwsA(isA<FormatException>()),
|
|
);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Null / optional fields
|
|
// ---------------------------------------------------------------------------
|
|
|
|
group('optional fields', () {
|
|
test('null parentUnitId is preserved through round-trip', () {
|
|
final json = codec.encode(units: [brigade], slots: [], personnel: []);
|
|
final snapshot = codec.decode(json);
|
|
expect(snapshot.units.first.parentUnitId, isNull);
|
|
});
|
|
|
|
test('null assignedPersonnelId is preserved through round-trip', () {
|
|
final json = codec.encode(units: [], slots: [slot], personnel: []);
|
|
final snapshot = codec.decode(json);
|
|
expect(snapshot.slots.first.assignedPersonnelId, isNull);
|
|
});
|
|
|
|
test('null assignedSlotId is preserved through round-trip', () {
|
|
final unassigned = PersonnelRecord(
|
|
soldierId: 'p-2',
|
|
name: 'SGT Free',
|
|
primaryMos: DutyMos.unvalidated('11B'),
|
|
payGrade: AuthorizedRank.e5,
|
|
readinessState: ReadinessState.unknown,
|
|
);
|
|
final json = codec.encode(units: [], slots: [], personnel: [unassigned]);
|
|
final snapshot = codec.decode(json);
|
|
expect(snapshot.personnel.first.assignedSlotId, isNull);
|
|
});
|
|
|
|
test('qualifications list round-trips correctly', () {
|
|
final soldierWithQuals = PersonnelRecord(
|
|
soldierId: 'p-3',
|
|
name: 'SGT Qualified',
|
|
primaryMos: DutyMos.unvalidated('11B'),
|
|
payGrade: AuthorizedRank.e5,
|
|
readinessState: ReadinessState.fullyMissionCapable,
|
|
qualifications: const ['Airborne', 'Ranger', 'SERE'],
|
|
);
|
|
final json = codec.encode(
|
|
units: [],
|
|
slots: [],
|
|
personnel: [soldierWithQuals],
|
|
);
|
|
final snapshot = codec.decode(json);
|
|
expect(
|
|
snapshot.personnel.first.qualifications,
|
|
containsAll(['Airborne', 'Ranger', 'SERE']),
|
|
);
|
|
});
|
|
});
|
|
}
|