import 'dart:typed_data'; import 'package:abawo_bt_app/model/firmware_file_selection.dart'; import 'package:abawo_bt_app/model/shifter_types.dart'; import 'package:abawo_bt_app/service/dfu_protocol.dart'; import 'package:abawo_bt_app/service/firmware_file_selection_service.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('FirmwareFileSelectionService', () { test('prepares bootloader metadata for selected .bin firmware', () async { final image = _validBootloaderImage(); final service = FirmwareFileSelectionService( filePicker: _FakeFirmwareFilePicker( selection: FirmwarePickerSelection( fileName: 'firmware.BIN', filePath: '/tmp/firmware.BIN', fileBytes: image, ), ), sessionIdGenerator: () => 0x1AB, ); final result = await service.selectAndPrepareBootloaderDfu(); expect(result.isSuccess, isTrue); final firmware = result.firmware!; expect(firmware.fileName, 'firmware.BIN'); expect(firmware.filePath, '/tmp/firmware.BIN'); expect(firmware.fileBytes, image); expect(firmware.metadata.totalLength, image.length); expect(firmware.metadata.crc32, BootloaderDfuProtocol.crc32(image)); expect(firmware.metadata.appStart, universalShifterDfuAppStart); expect(firmware.metadata.imageVersion, 0); expect(firmware.metadata.sessionId, 0xAB); expect(firmware.metadata.flags, universalShifterDfuFlagNone); expect(firmware.metadata.vectorStackPointer, 0x20001000); expect(firmware.metadata.vectorReset, 0x00030009); }); test('returns canceled result when user dismisses picker', () async { final service = FirmwareFileSelectionService( filePicker: _FakeFirmwareFilePicker(selection: null), ); final result = await service.selectAndPrepareBootloaderDfu(); expect(result.isSuccess, isFalse); expect(result.isCanceled, isTrue); expect(result.failure?.reason, FirmwareSelectionFailureReason.canceled); }); test('rejects unsupported extension', () async { final service = FirmwareFileSelectionService( filePicker: _FakeFirmwareFilePicker( selection: FirmwarePickerSelection( fileName: 'firmware.hex', fileBytes: _validBootloaderImage(), ), ), ); final result = await service.selectAndPrepareBootloaderDfu(); expect(result.isSuccess, isFalse); expect(result.failure?.reason, FirmwareSelectionFailureReason.unsupportedExtension); }); test('rejects empty payload', () async { final service = FirmwareFileSelectionService( filePicker: _FakeFirmwareFilePicker( selection: FirmwarePickerSelection( fileName: 'firmware.bin', fileBytes: Uint8List(0), ), ), ); final result = await service.selectAndPrepareBootloaderDfu(); expect(result.isSuccess, isFalse); expect(result.failure?.reason, FirmwareSelectionFailureReason.emptyFile); }); test('rejects images that are too small for a vector table', () async { final service = FirmwareFileSelectionService( filePicker: _FakeFirmwareFilePicker( selection: FirmwarePickerSelection( fileName: 'firmware.bin', fileBytes: Uint8List.fromList([1, 2, 3, 4]), ), ), ); final result = await service.selectAndPrepareBootloaderDfu(); expect(result.isSuccess, isFalse); expect( result.failure?.reason, FirmwareSelectionFailureReason.imageTooSmall); }); test('rejects images larger than the application slot', () async { final image = Uint8List(universalShifterDfuAppSlotSizeBytes + 1); _writeLeU32(image, 0, 0x20001000); _writeLeU32(image, 4, 0x00030009); final service = FirmwareFileSelectionService( filePicker: _FakeFirmwareFilePicker( selection: FirmwarePickerSelection( fileName: 'firmware.bin', fileBytes: image, ), ), ); final result = await service.selectAndPrepareBootloaderDfu(); expect(result.isSuccess, isFalse); expect( result.failure?.reason, FirmwareSelectionFailureReason.imageTooLarge); }); test('rejects images with invalid vector table', () async { final image = _validBootloaderImage(); _writeLeU32(image, 0, 0x10001000); final service = FirmwareFileSelectionService( filePicker: _FakeFirmwareFilePicker( selection: FirmwarePickerSelection( fileName: 'firmware.bin', fileBytes: image, ), ), ); final result = await service.selectAndPrepareBootloaderDfu(); expect(result.isSuccess, isFalse); expect(result.failure?.reason, FirmwareSelectionFailureReason.invalidVectorTable); }); test('generates session id per run', () async { var nextSession = 9; final service = FirmwareFileSelectionService( filePicker: _FakeFirmwareFilePicker( selection: FirmwarePickerSelection( fileName: 'firmware.bin', fileBytes: _validBootloaderImage(), ), ), sessionIdGenerator: () => nextSession++, ); final first = await service.selectAndPrepareBootloaderDfu(); final second = await service.selectAndPrepareBootloaderDfu(); expect(first.firmware?.metadata.sessionId, 9); expect(second.firmware?.metadata.sessionId, 10); }); test('maps picker read failure to explicit validation error', () async { final service = FirmwareFileSelectionService( filePicker: _FakeFirmwareFilePicker( selection: null, error: const FormatException('broken pick payload'), ), ); final result = await service.selectAndPrepareBootloaderDfu(); expect(result.isSuccess, isFalse); expect(result.failure?.reason, FirmwareSelectionFailureReason.readFailed); expect(result.failure?.message, contains('broken pick payload')); }); }); } Uint8List _validBootloaderImage() { final image = Uint8List(16); _writeLeU32(image, 0, 0x20001000); _writeLeU32(image, 4, 0x00030009); return image; } void _writeLeU32(Uint8List bytes, int offset, int value) { final data = ByteData.sublistView(bytes); data.setUint32(offset, value, Endian.little); } class _FakeFirmwareFilePicker implements FirmwareFilePicker { _FakeFirmwareFilePicker({ required this.selection, this.error, }); final FirmwarePickerSelection? selection; final Object? error; @override Future pickFirmwareFile() async { if (error != null) { throw error!; } return selection; } }