feat(media): gestion des médias locaux
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>
This commit is contained in:
188
test/application/media_use_cases_test.dart
Normal file
188
test/application/media_use_cases_test.dart
Normal file
@ -0,0 +1,188 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user