Ajoute le stockage local des médias (infrastructure/local/local_media_storage.dart) et les use cases/ports associés. flutter analyze propre, 7/7 tests unitaires verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
189 lines
5.0 KiB
Dart
189 lines
5.0 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:gametime/application/application.dart';
|
|
import 'package:gametime/domain/domain.dart';
|
|
|
|
void main() {
|
|
test('imports media and stores sync-ready asset metadata', () async {
|
|
final mediaRepository = _FakeMediaAssetRepository();
|
|
final useCase = MediaUseCases(
|
|
mediaRepository: mediaRepository,
|
|
exerciseRepository: _FakeExerciseRepository(),
|
|
storage: _FakeMediaStorage(),
|
|
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
|
ids: _FakeIds(),
|
|
originDeviceId: 'device-1',
|
|
);
|
|
|
|
final asset = await useCase.importMedia(
|
|
sourcePath: '/tmp/source.png',
|
|
kind: MediaKind.image,
|
|
);
|
|
|
|
expect(asset.kind, MediaKind.image);
|
|
expect(asset.localUri, 'file:///app/media/id-1.png');
|
|
expect(asset.mimeType, 'image/png');
|
|
expect(asset.sizeBytes, 128);
|
|
expect(asset.width, 10);
|
|
expect(asset.height, 20);
|
|
expect(asset.remoteUri, isNull);
|
|
expect(asset.checksum, isNull);
|
|
expect(mediaRepository.saved.single.metadata.id, asset.metadata.id);
|
|
});
|
|
|
|
test('attaches imported image to exercise image field', () async {
|
|
final exerciseRepository = _FakeExerciseRepository()
|
|
..exercise = Exercise(
|
|
metadata: _metadata('exercise-1'),
|
|
name: 'Squat',
|
|
hasTimeMeasure: false,
|
|
hasRepsMeasure: true,
|
|
hasScoreMeasure: false,
|
|
);
|
|
final mediaRepository = _FakeMediaAssetRepository()
|
|
..asset = MediaAsset(
|
|
metadata: _metadata('media-1'),
|
|
kind: MediaKind.image,
|
|
localUri: 'file:///app/media/media-1.png',
|
|
);
|
|
final useCase = MediaUseCases(
|
|
mediaRepository: mediaRepository,
|
|
exerciseRepository: exerciseRepository,
|
|
storage: _FakeMediaStorage(),
|
|
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
|
ids: _FakeIds(),
|
|
originDeviceId: 'device-1',
|
|
);
|
|
|
|
final updated = await useCase.attachToExercise(
|
|
exerciseId: 'exercise-1',
|
|
mediaAssetId: 'media-1',
|
|
);
|
|
|
|
expect(updated.imageMediaId, 'media-1');
|
|
expect(updated.videoMediaId, isNull);
|
|
expect(exerciseRepository.saved.single.imageMediaId, 'media-1');
|
|
});
|
|
|
|
test('deletes managed files not referenced by active media assets', () async {
|
|
final storage = _FakeMediaStorage()
|
|
..managedUris = {
|
|
'file:///app/media/kept.png',
|
|
'file:///app/media/orphan.png',
|
|
};
|
|
final mediaRepository = _FakeMediaAssetRepository()
|
|
..activeAssets = [
|
|
MediaAsset(
|
|
metadata: _metadata('media-1'),
|
|
kind: MediaKind.image,
|
|
localUri: 'file:///app/media/kept.png',
|
|
),
|
|
];
|
|
final useCase = MediaUseCases(
|
|
mediaRepository: mediaRepository,
|
|
exerciseRepository: _FakeExerciseRepository(),
|
|
storage: storage,
|
|
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
|
ids: _FakeIds(),
|
|
originDeviceId: 'device-1',
|
|
);
|
|
|
|
final deletedCount = await useCase.deleteOrphanedManagedFiles();
|
|
|
|
expect(deletedCount, 1);
|
|
expect(storage.deletedUris, ['file:///app/media/orphan.png']);
|
|
});
|
|
}
|
|
|
|
EntityMetadata _metadata(String id) {
|
|
return EntityMetadata(
|
|
id: id,
|
|
createdAt: DateTime.utc(2026, 7, 17),
|
|
updatedAt: DateTime.utc(2026, 7, 17),
|
|
originDeviceId: 'device-1',
|
|
);
|
|
}
|
|
|
|
final class _FakeClock implements Clock {
|
|
const _FakeClock(this.value);
|
|
|
|
final DateTime value;
|
|
|
|
@override
|
|
DateTime now() => value;
|
|
}
|
|
|
|
final class _FakeIds implements IdGenerator {
|
|
var _next = 0;
|
|
|
|
@override
|
|
String newId() {
|
|
_next += 1;
|
|
return 'id-$_next';
|
|
}
|
|
}
|
|
|
|
final class _FakeMediaStorage implements LocalMediaStorage {
|
|
Set<String> managedUris = const {};
|
|
final deletedUris = <String>[];
|
|
|
|
@override
|
|
Future<StoredMediaFile> importFile({
|
|
required String sourcePath,
|
|
required MediaKind kind,
|
|
required String stableFileName,
|
|
}) async {
|
|
return StoredMediaFile(
|
|
localUri: 'file:///app/media/$stableFileName.png',
|
|
mimeType: 'image/png',
|
|
sizeBytes: 128,
|
|
width: 10,
|
|
height: 20,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<Set<String>> listManagedLocalUris() async => managedUris;
|
|
|
|
@override
|
|
Future<void> deleteByLocalUri(String localUri) async {
|
|
deletedUris.add(localUri);
|
|
}
|
|
}
|
|
|
|
final class _FakeMediaAssetRepository implements MediaAssetRepository {
|
|
MediaAsset? asset;
|
|
List<MediaAsset> activeAssets = const [];
|
|
final saved = <MediaAsset>[];
|
|
|
|
@override
|
|
Future<MediaAsset?> findById(String id) async => asset;
|
|
|
|
@override
|
|
Future<List<MediaAsset>> listActive() async => activeAssets;
|
|
|
|
@override
|
|
Future<void> save(MediaAsset mediaAsset) async {
|
|
saved.add(mediaAsset);
|
|
asset = mediaAsset;
|
|
}
|
|
}
|
|
|
|
final class _FakeExerciseRepository implements ExerciseRepository {
|
|
Exercise? exercise;
|
|
final saved = <Exercise>[];
|
|
|
|
@override
|
|
Future<Exercise?> findById(String id) async => exercise;
|
|
|
|
@override
|
|
Future<List<Exercise>> listActive() async {
|
|
return exercise == null ? const [] : [exercise!];
|
|
}
|
|
|
|
@override
|
|
Future<void> save(Exercise exercise) async {
|
|
saved.add(exercise);
|
|
this.exercise = exercise;
|
|
}
|
|
}
|