From 93d6db5e7cb2ebf39e1b77c4ddf32d4aacebb24d Mon Sep 17 00:00:00 2001 From: mehmetbahadirgungor Date: Fri, 23 Oct 2026 20:41:16 +0300 Subject: [PATCH 1/4] fix: catch unhandled errors when updating cache-hit metadata (#535) --- flutter_cache_manager/CHANGELOG.md | 4 ++ .../lib/src/cache_store.dart | 60 +++++++++++++------ flutter_cache_manager/pubspec.yaml | 2 +- .../test/cache_store_test.dart | 48 +++++++++++++++ 4 files changed, 96 insertions(+), 18 deletions(-) diff --git a/flutter_cache_manager/CHANGELOG.md b/flutter_cache_manager/CHANGELOG.md index 9513fb80..bd1dd16b 100644 --- a/flutter_cache_manager/CHANGELOG.md +++ b/flutter_cache_manager/CHANGELOG.md @@ -1,3 +1,7 @@ +## [3.4.6] - 2026-10-23 + +* Catches unhandled asynchronous errors when updating cache-hit metadata and during scheduled cleanup in `CacheStore` ([#535](https://github.com/Baseflow/flutter_cache_manager/issues/535)). + ## [3.4.5] - 2026-09-18 * Prevents image resize decoding from completing more than once and propagates decoding errors ([#400](https://github.com/Baseflow/flutter_cache_manager/issues/400)). diff --git a/flutter_cache_manager/lib/src/cache_store.dart b/flutter_cache_manager/lib/src/cache_store.dart index 44ffbfeb..453292c7 100644 --- a/flutter_cache_manager/lib/src/cache_store.dart +++ b/flutter_cache_manager/lib/src/cache_store.dart @@ -75,21 +75,32 @@ class CacheStore { } if (!_futureCache.containsKey(key)) { final completer = Completer(); - _getCacheDataFromDatabase(key).then((cacheObject) async { - if (cacheObject?.id != null && !await _fileExists(cacheObject)) { - final provider = await _cacheInfoRepository; - await provider.delete(cacheObject!.id!); - cacheObject = null; - } - - if (cacheObject == null) { - _memCache.remove(key); - } else { - _memCache[key] = cacheObject; - } - completer.complete(cacheObject); - _futureCache.remove(key); - }); + _getCacheDataFromDatabase(key).then( + (cacheObject) async { + try { + if (cacheObject?.id != null && !await _fileExists(cacheObject)) { + final provider = await _cacheInfoRepository; + await provider.delete(cacheObject!.id!); + cacheObject = null; + } + + if (cacheObject == null) { + _memCache.remove(key); + } else { + _memCache[key] = cacheObject; + } + completer.complete(cacheObject); + } catch (e, s) { + completer.completeError(e, s); + } finally { + _futureCache.remove(key); + } + }, + onError: (Object e, StackTrace s) { + completer.completeError(e, s); + _futureCache.remove(key); + }, + ); _futureCache[key] = completer.future; } return _futureCache[key]; @@ -121,7 +132,15 @@ class CacheStore { final provider = await _cacheInfoRepository; final data = await provider.get(key); if (await _fileExists(data)) { - _updateCacheDataInDatabase(data!); + unawaited( + _updateCacheDataInDatabase(data!).catchError((Object e) { + cacheLogger.log( + 'CacheStore: Failed to update cache metadata for $key with error:\n$e', + CacheManagerLogLevel.warning, + ); + return null; + }), + ); } _scheduleCleanup(); return data; @@ -133,7 +152,14 @@ class CacheStore { } _scheduledCleanup = Timer(cleanupRunMinInterval, () { _scheduledCleanup = null; - _cleanupCache(); + unawaited( + _cleanupCache().catchError((Object e) { + cacheLogger.log( + 'CacheStore: Failed to cleanup cache with error:\n$e', + CacheManagerLogLevel.warning, + ); + }), + ); }); } diff --git a/flutter_cache_manager/pubspec.yaml b/flutter_cache_manager/pubspec.yaml index 3f0c9ce3..8456c4b5 100644 --- a/flutter_cache_manager/pubspec.yaml +++ b/flutter_cache_manager/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_cache_manager description: Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite. -version: 3.4.5 +version: 3.4.6 homepage: https://github.com/Baseflow/flutter_cache_manager topics: - cache diff --git a/flutter_cache_manager/test/cache_store_test.dart b/flutter_cache_manager/test/cache_store_test.dart index 412a35f1..b37d0007 100644 --- a/flutter_cache_manager/test/cache_store_test.dart +++ b/flutter_cache_manager/test/cache_store_test.dart @@ -54,6 +54,54 @@ void main() { expect(await store.getFile('baseflow.com/test.png'), isNotNull); }); + test( + 'Store should return FileInfo and handle error when database update fails on cache hit', + () async { + var config = createTestConfig(); + await config.returnsFile(fileName); + config.returnsCacheObject( + fileUrl, + fileName, + DateTime.now(), + id: fileId, + ); + when( + config.mockRepo.updateOrInsert(any), + ).thenAnswer((_) => Future.error(Exception('Database update failed'))); + + var store = CacheStore(config); + + final fileInfo = await store.getFile(fileUrl); + expect(fileInfo, isNotNull); + + // Allow any unawaited async operations to complete without unhandled errors + await Future.delayed(const Duration(milliseconds: 50)); + }, + ); + + test( + 'Store should propagate error and clean up pending cache when get from database fails', + () async { + var config = createTestConfig(); + when( + config.mockRepo.get(any), + ).thenAnswer((_) => Future.error(Exception('Database read failed'))); + + var store = CacheStore(config); + + await expectLater( + () => store.retrieveCacheData('failed-key'), + throwsA(isA()), + ); + + // Verify that subsequent calls are not permanently stuck on a dead completer + await expectLater( + () => store.retrieveCacheData('failed-key'), + throwsA(isA()), + ); + }, + ); + test('Store should return null if file is not cached', () async { var config = createTestConfig(); await config.returnsFile(fileName); From b289abfa6c82ddd22491a8b28c2be8dccd37f1e8 Mon Sep 17 00:00:00 2001 From: mehmetbahadirgungor Date: Tue, 27 Oct 2026 09:36:15 +0300 Subject: [PATCH 2/4] fix: address review feedback on cache error handling and cleanup --- .../lib/src/cache_store.dart | 114 ++++++++++-------- .../test/cache_store_test.dart | 80 ++++++++++-- 2 files changed, 136 insertions(+), 58 deletions(-) diff --git a/flutter_cache_manager/lib/src/cache_store.dart b/flutter_cache_manager/lib/src/cache_store.dart index 453292c7..e4b8889f 100644 --- a/flutter_cache_manager/lib/src/cache_store.dart +++ b/flutter_cache_manager/lib/src/cache_store.dart @@ -74,38 +74,45 @@ class CacheStore { } } if (!_futureCache.containsKey(key)) { - final completer = Completer(); - _getCacheDataFromDatabase(key).then( - (cacheObject) async { - try { - if (cacheObject?.id != null && !await _fileExists(cacheObject)) { - final provider = await _cacheInfoRepository; - await provider.delete(cacheObject!.id!); - cacheObject = null; - } - - if (cacheObject == null) { - _memCache.remove(key); - } else { - _memCache[key] = cacheObject; - } - completer.complete(cacheObject); - } catch (e, s) { - completer.completeError(e, s); - } finally { - _futureCache.remove(key); - } - }, - onError: (Object e, StackTrace s) { - completer.completeError(e, s); - _futureCache.remove(key); - }, - ); - _futureCache[key] = completer.future; + late final Future future; + future = _loadCacheData(key, () => future); + _futureCache[key] = future; } return _futureCache[key]; } + Future _loadCacheData( + String key, + Future Function() currentFuture, + ) async { + try { + var cacheObject = await _getCacheDataFromDatabase(key); + if (cacheObject?.id != null && !await _fileExists(cacheObject)) { + try { + final provider = await _cacheInfoRepository; + await provider.delete(cacheObject!.id!); + } catch (e, s) { + cacheLogger.log( + 'CacheStore: Failed to delete non-existing file metadata for $key with error:\n$e\n$s', + CacheManagerLogLevel.warning, + ); + } + cacheObject = null; + } + + if (cacheObject == null) { + _memCache.remove(key); + } else { + _memCache[key] = cacheObject; + } + return cacheObject; + } finally { + if (identical(_futureCache[key], currentFuture())) { + _futureCache.remove(key); + } + } + } + Future getFileFromMemory(String key) async { final cacheObject = _memCache[key]; if (cacheObject == null) { @@ -129,21 +136,32 @@ class CacheStore { } Future _getCacheDataFromDatabase(String key) async { - final provider = await _cacheInfoRepository; - final data = await provider.get(key); - if (await _fileExists(data)) { - unawaited( - _updateCacheDataInDatabase(data!).catchError((Object e) { - cacheLogger.log( - 'CacheStore: Failed to update cache metadata for $key with error:\n$e', - CacheManagerLogLevel.warning, - ); - return null; - }), + try { + final provider = await _cacheInfoRepository; + final data = await provider.get(key); + if (await _fileExists(data)) { + unawaited( + _updateCacheDataInDatabase(data!).catchError(( + Object e, + StackTrace s, + ) { + cacheLogger.log( + 'CacheStore: Failed to update cache metadata for $key with error:\n$e\n$s', + CacheManagerLogLevel.warning, + ); + return null; + }), + ); + } + _scheduleCleanup(); + return data; + } catch (e, s) { + cacheLogger.log( + 'CacheStore: Failed to read cache metadata for $key with error:\n$e\n$s', + CacheManagerLogLevel.warning, ); + return null; } - _scheduleCleanup(); - return data; } void _scheduleCleanup() { @@ -153,9 +171,9 @@ class CacheStore { _scheduledCleanup = Timer(cleanupRunMinInterval, () { _scheduledCleanup = null; unawaited( - _cleanupCache().catchError((Object e) { + _cleanupCache().catchError((Object e, StackTrace s) { cacheLogger.log( - 'CacheStore: Failed to cleanup cache with error:\n$e', + 'CacheStore: Failed to cleanup cache with error:\n$e\n$s', CacheManagerLogLevel.warning, ); }), @@ -173,15 +191,17 @@ class CacheStore { final provider = await _cacheInfoRepository; final overCapacity = await provider.getObjectsOverCapacity(_capacity); + final futures = []; for (final cacheObject in overCapacity) { - _removeCachedFile(cacheObject, toRemove); + futures.add(_removeCachedFile(cacheObject, toRemove)); } final oldObjects = await provider.getOldObjects(_maxAge); for (final cacheObject in oldObjects) { - _removeCachedFile(cacheObject, toRemove); + futures.add(_removeCachedFile(cacheObject, toRemove)); } + await Future.wait(futures); await provider.deleteAll(toRemove); } @@ -218,9 +238,7 @@ class CacheStore { if (_memCache.containsKey(cacheObject.key)) { _memCache.remove(cacheObject.key); } - if (_futureCache.containsKey(cacheObject.key)) { - await _futureCache.remove(cacheObject.key); - } + _futureCache.remove(cacheObject.key); final file = await fileSystem.createFile(cacheObject.relativePath); if (file.existsSync()) { diff --git a/flutter_cache_manager/test/cache_store_test.dart b/flutter_cache_manager/test/cache_store_test.dart index b37d0007..443716fc 100644 --- a/flutter_cache_manager/test/cache_store_test.dart +++ b/flutter_cache_manager/test/cache_store_test.dart @@ -2,6 +2,7 @@ import 'package:clock/clock.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_cache_manager/src/cache_store.dart'; +import 'package:flutter_cache_manager/src/logger.dart'; import 'package:flutter_cache_manager/src/storage/cache_object.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; @@ -57,6 +58,11 @@ void main() { test( 'Store should return FileInfo and handle error when database update fails on cache hit', () async { + final originalLogger = cacheLogger; + final testLogger = TestCacheLogger(); + cacheLogger = testLogger; + addTearDown(() => cacheLogger = originalLogger); + var config = createTestConfig(); await config.returnsFile(fileName); config.returnsCacheObject( @@ -74,14 +80,25 @@ void main() { final fileInfo = await store.getFile(fileUrl); expect(fileInfo, isNotNull); - // Allow any unawaited async operations to complete without unhandled errors - await Future.delayed(const Duration(milliseconds: 50)); + await untilCalled(config.mockRepo.updateOrInsert(any)); + verify(config.mockRepo.updateOrInsert(any)).called(1); + expect( + testLogger.logs.any( + (l) => l.contains('Failed to update cache metadata'), + ), + isTrue, + ); }, ); test( - 'Store should propagate error and clean up pending cache when get from database fails', + 'Store should treat failed database read as cache miss and clean up pending cache', () async { + final originalLogger = cacheLogger; + final testLogger = TestCacheLogger(); + cacheLogger = testLogger; + addTearDown(() => cacheLogger = originalLogger); + var config = createTestConfig(); when( config.mockRepo.get(any), @@ -89,15 +106,49 @@ void main() { var store = CacheStore(config); - await expectLater( - () => store.retrieveCacheData('failed-key'), - throwsA(isA()), + final result1 = await store.retrieveCacheData('failed-key'); + expect(result1, isNull); + + final result2 = await store.retrieveCacheData('failed-key'); + expect(result2, isNull); + + verify(config.mockRepo.get(any)).called(2); + expect( + testLogger.logs.any( + (l) => l.contains('Failed to read cache metadata'), + ), + isTrue, ); + }, + ); + + test( + 'Store cleanup error should be handled without unhandled async error', + () async { + final originalLogger = cacheLogger; + final testLogger = TestCacheLogger(); + cacheLogger = testLogger; + addTearDown(() => cacheLogger = originalLogger); - // Verify that subsequent calls are not permanently stuck on a dead completer - await expectLater( - () => store.retrieveCacheData('failed-key'), - throwsA(isA()), + var config = createTestConfig(); + var store = CacheStore(config); + store.cleanupRunMinInterval = const Duration(milliseconds: 1); + await config.returnsFile(fileName); + config.returnsCacheObject(fileUrl, fileName, validTill, id: fileId); + + when( + config.mockRepo.getObjectsOverCapacity(any), + ).thenAnswer((_) => Future.error(Exception('Cleanup failure'))); + + expect(await store.getFile(fileUrl), isNotNull); + + await untilCalled(config.mockRepo.getObjectsOverCapacity(any)); + verify(config.mockRepo.getObjectsOverCapacity(any)).called(1); + + await Future.delayed(const Duration(milliseconds: 10)); + expect( + testLogger.logs.any((l) => l.contains('Failed to cleanup cache')), + isTrue, ); }, ); @@ -512,3 +563,12 @@ Future createDir() async { final fileSystem = MemoryFileSystem(); return fileSystem.systemTempDirectory.createTemp('test'); } + +class TestCacheLogger extends CacheLogger { + final logs = []; + + @override + void log(String message, CacheManagerLogLevel level) { + logs.add(message); + } +} From 8b43e5cf96f5afb9a72e7f4b6f305ce93445437b Mon Sep 17 00:00:00 2001 From: mehmetbahadirgungor Date: Tue, 27 Oct 2026 09:57:01 +0300 Subject: [PATCH 3/4] fix: clean up orphaned file on putFile failure and polish tests --- flutter_cache_manager/CHANGELOG.md | 1 + .../lib/src/web/web_helper.dart | 9 ++- .../test/cache_store_test.dart | 62 ++++++++++--------- .../test/web_helper_test.dart | 44 +++++++++++++ 4 files changed, 86 insertions(+), 30 deletions(-) diff --git a/flutter_cache_manager/CHANGELOG.md b/flutter_cache_manager/CHANGELOG.md index bd1dd16b..d1e96100 100644 --- a/flutter_cache_manager/CHANGELOG.md +++ b/flutter_cache_manager/CHANGELOG.md @@ -1,6 +1,7 @@ ## [3.4.6] - 2026-10-23 * Catches unhandled asynchronous errors when updating cache-hit metadata and during scheduled cleanup in `CacheStore` ([#535](https://github.com/Baseflow/flutter_cache_manager/issues/535)). +* Treats failed metadata reads as a cache miss with a warning log so downloads can fall back to the network, and cleans up downloaded files if persisting fails. ## [3.4.5] - 2026-09-18 diff --git a/flutter_cache_manager/lib/src/web/web_helper.dart b/flutter_cache_manager/lib/src/web/web_helper.dart index d5b77146..5fe120e3 100644 --- a/flutter_cache_manager/lib/src/web/web_helper.dart +++ b/flutter_cache_manager/lib/src/web/web_helper.dart @@ -156,7 +156,14 @@ class WebHelper { newCacheObject = newCacheObject.copyWith(length: savedBytes); } - await _store.putFile(newCacheObject); + try { + await _store.putFile(newCacheObject); + } catch (e) { + if (statusCodesNewFile.contains(response.statusCode)) { + await _removeOldFile(newCacheObject.relativePath); + } + rethrow; + } if (newCacheObject.relativePath != oldCacheObject.relativePath) { await _removeOldFile(oldCacheObject.relativePath); } diff --git a/flutter_cache_manager/test/cache_store_test.dart b/flutter_cache_manager/test/cache_store_test.dart index 443716fc..02a18320 100644 --- a/flutter_cache_manager/test/cache_store_test.dart +++ b/flutter_cache_manager/test/cache_store_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:clock/clock.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; @@ -58,10 +60,7 @@ void main() { test( 'Store should return FileInfo and handle error when database update fails on cache hit', () async { - final originalLogger = cacheLogger; - final testLogger = TestCacheLogger(); - cacheLogger = testLogger; - addTearDown(() => cacheLogger = originalLogger); + final testLogger = setUpTestLogger(); var config = createTestConfig(); await config.returnsFile(fileName); @@ -82,22 +81,14 @@ void main() { await untilCalled(config.mockRepo.updateOrInsert(any)); verify(config.mockRepo.updateOrInsert(any)).called(1); - expect( - testLogger.logs.any( - (l) => l.contains('Failed to update cache metadata'), - ), - isTrue, - ); + await testLogger.waitForLog('Failed to update cache metadata'); }, ); test( 'Store should treat failed database read as cache miss and clean up pending cache', () async { - final originalLogger = cacheLogger; - final testLogger = TestCacheLogger(); - cacheLogger = testLogger; - addTearDown(() => cacheLogger = originalLogger); + final testLogger = setUpTestLogger(); var config = createTestConfig(); when( @@ -113,22 +104,14 @@ void main() { expect(result2, isNull); verify(config.mockRepo.get(any)).called(2); - expect( - testLogger.logs.any( - (l) => l.contains('Failed to read cache metadata'), - ), - isTrue, - ); + await testLogger.waitForLog('Failed to read cache metadata'); }, ); test( 'Store cleanup error should be handled without unhandled async error', () async { - final originalLogger = cacheLogger; - final testLogger = TestCacheLogger(); - cacheLogger = testLogger; - addTearDown(() => cacheLogger = originalLogger); + final testLogger = setUpTestLogger(); var config = createTestConfig(); var store = CacheStore(config); @@ -145,11 +128,7 @@ void main() { await untilCalled(config.mockRepo.getObjectsOverCapacity(any)); verify(config.mockRepo.getObjectsOverCapacity(any)).called(1); - await Future.delayed(const Duration(milliseconds: 10)); - expect( - testLogger.logs.any((l) => l.contains('Failed to cleanup cache')), - isTrue, - ); + await testLogger.waitForLog('Failed to cleanup cache'); }, ); @@ -566,9 +545,34 @@ Future createDir() async { class TestCacheLogger extends CacheLogger { final logs = []; + Completer? _nextLogCompleter; + + Future waitForLog([Pattern? pattern]) async { + while (true) { + if (pattern != null) { + final existing = logs.where((l) => l.contains(pattern)).firstOrNull; + if (existing != null) return existing; + } else if (logs.isNotEmpty) { + return logs.first; + } + _nextLogCompleter ??= Completer(); + await _nextLogCompleter!.future; + } + } @override void log(String message, CacheManagerLogLevel level) { logs.add(message); + final c = _nextLogCompleter; + _nextLogCompleter = null; + c?.complete(message); } } + +TestCacheLogger setUpTestLogger() { + final originalLogger = cacheLogger; + final testLogger = TestCacheLogger(); + cacheLogger = testLogger; + addTearDown(() => cacheLogger = originalLogger); + return testLogger; +} diff --git a/flutter_cache_manager/test/web_helper_test.dart b/flutter_cache_manager/test/web_helper_test.dart index 9bc655a8..5761ccb7 100644 --- a/flutter_cache_manager/test/web_helper_test.dart +++ b/flutter_cache_manager/test/web_helper_test.dart @@ -289,6 +289,50 @@ void main() { await download; }); + test( + 'downloadFile cleans up downloaded file if store persist fails', + () async { + const imageUrl = 'baseflow.com/testimage'; + + final config = createTestConfig(); + final store = CacheStore(config); + + when( + config.mockRepo.updateOrInsert(any), + ).thenAnswer((_) => Future.error(Exception('Persist failed'))); + + final fileService = MockFileService(); + when( + fileService.get(imageUrl, headers: anyNamed('headers')), + ).thenAnswer((_) { + return Future.value( + MockFileFetcherResponse( + Stream.value([0, 1, 2, 3, 4, 5]), + 6, + 'testv1', + '.jpg', + 200, + DateTime.now(), + ), + ); + }); + + final webHelper = WebHelper(store, fileService); + await expectLater( + () => webHelper.downloadFile(imageUrl).drain(), + throwsA(isA()), + ); + + final testFileSystem = config.fileSystem as TestFileSystem; + final dir = await testFileSystem.directoryFuture; + expect( + dir.listSync().isEmpty, + isTrue, + reason: 'Downloaded file should be deleted on putFile failure', + ); + }, + ); + test('File should be removed if extension changed', () async { const imageUrl = 'baseflow.com/testimage'; var imageName = 'image.png'; From 39b22368f8fe04e31e58fc41abfeb7b8a4b52391 Mon Sep 17 00:00:00 2001 From: mehmetbahadirgungor Date: Tue, 27 Oct 2026 10:10:32 +0300 Subject: [PATCH 4/4] fix: revert memory cache on putFile database failure --- .../lib/src/cache_store.dart | 13 ++++- .../test/cache_store_test.dart | 48 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/flutter_cache_manager/lib/src/cache_store.dart b/flutter_cache_manager/lib/src/cache_store.dart index e4b8889f..eaf4d189 100644 --- a/flutter_cache_manager/lib/src/cache_store.dart +++ b/flutter_cache_manager/lib/src/cache_store.dart @@ -55,8 +55,19 @@ class CacheStore { } Future putFile(CacheObject cacheObject) async { + final previousCacheObject = _memCache[cacheObject.key]; _memCache[cacheObject.key] = cacheObject; - final dynamic out = await _updateCacheDataInDatabase(cacheObject); + final dynamic out; + try { + out = await _updateCacheDataInDatabase(cacheObject); + } catch (e) { + if (previousCacheObject != null) { + _memCache[cacheObject.key] = previousCacheObject; + } else { + _memCache.remove(cacheObject.key); + } + rethrow; + } // We update the cache object with the id if returned by the repository if (out is CacheObject && out.id != null) { diff --git a/flutter_cache_manager/test/cache_store_test.dart b/flutter_cache_manager/test/cache_store_test.dart index 02a18320..91bdf864 100644 --- a/flutter_cache_manager/test/cache_store_test.dart +++ b/flutter_cache_manager/test/cache_store_test.dart @@ -289,6 +289,54 @@ void main() { expect(result!.id, isNotNull); }, ); + + test( + 'putFile restores or removes memory cache entry when updateOrInsert fails', + () async { + var config = createTestConfig(); + var store = CacheStore(config); + + var initialObject = CacheObject( + 'baseflow.com/test.png', + relativePath: 'testimage_old.png', + validTill: clock.now().add(const Duration(days: 7)), + ); + var newObject = CacheObject( + 'baseflow.com/test.png', + relativePath: 'testimage_new.png', + validTill: clock.now().add(const Duration(days: 7)), + ); + + // Case 1: Nothing was in memory cache before, putFile fails + when( + config.mockRepo.updateOrInsert(newObject), + ).thenAnswer((_) => Future.error(Exception('DB error'))); + + await expectLater( + () => store.putFile(newObject), + throwsA(isA()), + ); + expect(store.memoryCacheContainsKey(newObject.key), isFalse); + + // Case 2: initialObject succeeded, then updating to newObject fails + when( + config.mockRepo.updateOrInsert(initialObject), + ).thenAnswer((_) => Future.value(initialObject)); + await store.putFile(initialObject); + expect(store.memoryCacheContainsKey(initialObject.key), isTrue); + + await expectLater( + () => store.putFile(newObject), + throwsA(isA()), + ); + // Should have restored initialObject, not left pointing at newObject + expect(store.memoryCacheContainsKey(initialObject.key), isTrue); + expect( + (await store.getFileFromMemory(initialObject.key))?.file.path, + contains('testimage_old.png'), + ); + }, + ); }); group('Removing files in store', () {