import 'dart:io'; import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:minstrel/player/album_cover_cache.dart'; class _RecordingAdapter implements HttpClientAdapter { _RecordingAdapter(this.body); final List body; int callCount = 0; @override Future fetch( RequestOptions options, Stream? requestStream, Future? cancelFuture, ) async { callCount++; return ResponseBody.fromBytes(body, 200, headers: { Headers.contentTypeHeader: ['image/jpeg'], }); } @override void close({bool force = false}) {} } class _FailingAdapter implements HttpClientAdapter { @override Future fetch( RequestOptions options, Stream? requestStream, Future? cancelFuture, ) async { throw DioException( requestOptions: options, type: DioExceptionType.connectionError, message: 'simulated network failure', ); } @override void close({bool force = false}) {} } Future _tmpDirFactory() async => Directory.systemTemp.createTempSync('cover_cache_test_'); void main() { test('cache miss writes file and returns path', () async { final adapter = _RecordingAdapter([1, 2, 3, 4]); final dio = Dio(BaseOptions(baseUrl: 'http://test'))..httpClientAdapter = adapter; final cache = AlbumCoverCache( dioFactory: () async => dio, cacheDirFactory: _tmpDirFactory, ); final path = await cache.getOrFetch('alb-1'); expect(path, isNotNull); expect(File(path!).readAsBytesSync(), [1, 2, 3, 4]); expect(adapter.callCount, 1); }); test('cache hit returns same path without re-fetching', () async { final adapter = _RecordingAdapter([9, 9, 9]); final dio = Dio(BaseOptions(baseUrl: 'http://test'))..httpClientAdapter = adapter; final tmp = await _tmpDirFactory(); final cache = AlbumCoverCache( dioFactory: () async => dio, cacheDirFactory: () async => tmp, ); final p1 = await cache.getOrFetch('alb-2'); final p2 = await cache.getOrFetch('alb-2'); expect(p1, p2); expect(adapter.callCount, 1); }); test('concurrent calls for same albumId dedupe', () async { final adapter = _RecordingAdapter([1, 2, 3]); final dio = Dio(BaseOptions(baseUrl: 'http://test'))..httpClientAdapter = adapter; final cache = AlbumCoverCache( dioFactory: () async => dio, cacheDirFactory: _tmpDirFactory, ); final results = await Future.wait([ cache.getOrFetch('alb-3'), cache.getOrFetch('alb-3'), cache.getOrFetch('alb-3'), ]); expect(results[0], isNotNull); expect(results[1], results[0]); expect(results[2], results[0]); expect(adapter.callCount, 1); }); test('failure returns null without writing file', () async { final adapter = _FailingAdapter(); final dio = Dio(BaseOptions(baseUrl: 'http://test'))..httpClientAdapter = adapter; final tmp = await _tmpDirFactory(); final cache = AlbumCoverCache( dioFactory: () async => dio, cacheDirFactory: () async => tmp, ); final path = await cache.getOrFetch('alb-4'); expect(path, isNull); expect(File('${tmp.path}/album_covers/alb-4.jpg').existsSync(), isFalse); }); test('empty albumId returns null without dio call', () async { final adapter = _RecordingAdapter([1, 2]); final dio = Dio(BaseOptions(baseUrl: 'http://test'))..httpClientAdapter = adapter; final cache = AlbumCoverCache( dioFactory: () async => dio, cacheDirFactory: _tmpDirFactory, ); final path = await cache.getOrFetch(''); expect(path, isNull); expect(adapter.callCount, 0); }); }