import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:machweis/core/storage/local_storage.dart';
import 'package:machweis/core/sync/report_asset_sync.dart';
import 'package:shared_preferences/shared_preferences.dart';
class _ResumableAssetApi implements ReportAssetSyncApi {
final BytesBuilder received = BytesBuilder(copy: false);
bool loseFirstChunkResponse = true;
int serverOffset = 0;
int starts = 0;
int deletes = 0;
@override
Future<AssetUploadProgress> start(
String accessToken,
ReportAssetSyncOperation operation,
) async {
starts++;
return AssetUploadProgress(
complete: false,
uploadId: 'upload-1',
offset: serverOffset,
);
}
@override
Future<AssetUploadProgress> append(
String accessToken,
String uploadId,
int offset,
Uint8List bytes,
) async {
if (offset == serverOffset) {
received.add(bytes);
serverOffset += bytes.lengthInBytes;
if (loseFirstChunkResponse) {
loseFirstChunkResponse = false;
throw const ReportAssetSyncException('offline');
}
} else {
expect(offset, lessThan(serverOffset));
}
final complete = serverOffset == 300000;
return AssetUploadProgress(
complete: complete,
uploadId: uploadId,
offset: serverOffset,
);
}
@override
Future<void> cancel(String accessToken, String uploadId) async {}
@override
Future<void> delete(String accessToken, String assetId) async {
deletes++;
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test(
'Asset-Outbox setzt verlorenen Chunk idempotent fort und löscht eindeutig',
() async {
SharedPreferences.setMockInitialValues({});
await LocalStorage.instance.initialize(forcePreferences: true);
const tenant = 'tenant-assets';
const reportId = '10000000-0000-4000-8000-000000000001';
const assetId = '20000000-0000-4000-8000-000000000002';
final bytes = Uint8List.fromList(
List<int>.generate(300000, (index) => index % 251),
);
final api = _ResumableAssetApi();
final controller = ReportAssetSyncController(
api: api,
supported: true,
tenantProvider: () => tenant,
revisionProvider: (_, _) => 4,
photoBytesProvider: (_) async => bytes,
tokenProvider: ({bool forceRefresh = false}) async => 'access',
);
await controller.initialize();
await controller.enqueuePhoto(
assetId: assetId,
reportId: reportId,
fileName: 'beleg.jpg',
mimeType: 'image/jpeg',
bytes: bytes,
);
final firstAttempt = DateTime(2026, 9, 1, 8);
await controller.synchronize(now: firstAttempt);
expect(controller.status, ReportAssetSyncStatus.offline);
expect(controller.pendingCount, 1);
expect(controller.operations.single.uploadId, 'upload-1');
expect(controller.operations.single.offset, 0);
expect(controller.operations.single.reportVersion, 4);
expect(
controller.operations.single.sha256,
sha256.convert(bytes).toString(),
);
await controller.synchronize(
now: firstAttempt.add(const Duration(minutes: 1)),
force: true,
);
expect(controller.status, ReportAssetSyncStatus.success);
expect(controller.pendingCount, 0);
expect(api.starts, 1);
expect(api.received.takeBytes(), bytes);
await controller.enqueueDelete(assetId: assetId, reportId: reportId);
await controller.synchronize(
now: firstAttempt.add(const Duration(minutes: 4)),
);
expect(api.deletes, 1);
expect(controller.pendingCount, 0);
},
);
test(
'Signatur wird als integritätsgeschütztes Inline-Asset vorgemerkt',
() async {
const tenant = 'tenant-signature';
final controller = ReportAssetSyncController(
api: _ResumableAssetApi(),
supported: true,
tenantProvider: () => tenant,
revisionProvider: (_, _) => 7,
tokenProvider: ({bool forceRefresh = false}) async => 'access',
);
await controller.initialize();
await controller.enqueueSignature(
assetId: '30000000-0000-4000-8000-000000000003',
reportId: '40000000-0000-4000-8000-000000000004',
points: const [
[0.1, 0.2],
[0.2, 0.3],
[-1, -1],
],
);
final operation = controller.operations.singleWhere(
(item) => item.tenantId == tenant,
);
expect(operation.kind, ReportAssetKind.signature);
expect(operation.mimeType, 'application/vnd.machweis.signature+json');
expect(operation.inlineDataBase64, isNotEmpty);
expect(operation.reportVersion, 7);
},
);
}
