Skip to content

Commit c9e8335

Browse files
committed
Fixed 4 critical bugs found during code review:
- Added missing await keywords in addOrUpdate() switch statement - Changed int.parse() to int.tryParse() in secure storage - Fixed null pointer when reading non-existent List<Map> from preferences - Fixed typo: enalbeLogging → enableLogging
1 parent 2c884b9 commit c9e8335

8 files changed

+163
-13
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 0.0.6
2+
3+
* **CRITICAL BUG FIX**: Added missing `await` keywords in `addOrUpdate()` switch statement - ensures async operations complete before returning
4+
* **CRITICAL BUG FIX**: Changed `int.parse()` to `int.tryParse()` in secure storage to prevent exceptions on missing keys
5+
* **CRITICAL BUG FIX**: Fixed null pointer exception when reading non-existent `List<Map<String, dynamic>>` from preferences
6+
* Fixed typo: renamed `enalbeLogging` parameter to `enableLogging` in factory method
7+
18
## 0.0.5
29

310
* Added support for web platform using shared preferences

lib/flutter_easy_cache.dart

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ class FlutterEasyCache {
4141
_preferences = preferences;
4242

4343
factory FlutterEasyCache.create(SharedPreferencesWithCache preferences, FlutterSecureStorage secureStorage,
44-
{bool enalbeLogging = false}) {
44+
{bool enableLogging = false}) {
4545
return FlutterEasyCache._internal(
46-
preferences: preferences, secureStorage: secureStorage, loggingEnabled: enalbeLogging);
46+
preferences: preferences, secureStorage: secureStorage, loggingEnabled: enableLogging);
4747
}
4848

4949
/// Add a value to cache, replacing any existing value
@@ -57,11 +57,11 @@ class FlutterEasyCache {
5757

5858
switch (policy) {
5959
case CachePolicy.appSession:
60-
_addOrUpdateAppSession<T>(key: key, value: value);
60+
await _addOrUpdateAppSession<T>(key: key, value: value);
6161
case CachePolicy.appInstall:
62-
_addOrUpdateAppInstall<T>(key: key, value: value);
62+
await _addOrUpdateAppInstall<T>(key: key, value: value);
6363
case CachePolicy.secure:
64-
_addOrUpdateSecureStorage<T>(key: key, value: value);
64+
await _addOrUpdateSecureStorage<T>(key: key, value: value);
6565
break;
6666
}
6767
}
@@ -195,8 +195,10 @@ class FlutterEasyCache {
195195
} else if (T == List<String>) {
196196
value = _preferences?.getStringList(key) as T?;
197197
} else if (T == List<Map<String, dynamic>>) {
198-
final stringList = _preferences?.getStringList(key) as List<String>;
199-
value = stringList.map((e) => jsonDecode(e) as Map<String, dynamic>).toList() as T?;
198+
final stringList = _preferences?.getStringList(key);
199+
if (stringList != null) {
200+
value = stringList.map((e) => jsonDecode(e) as Map<String, dynamic>).toList() as T?;
201+
}
200202
}
201203
} catch (e) {
202204
_consolePrint('WARN: EasyCache error getting "$key": "$e"');
@@ -224,7 +226,7 @@ class FlutterEasyCache {
224226
value = bool.tryParse(stringValue ?? '', caseSensitive: false) as T?;
225227
} else if (T == int) {
226228
final stringValue = await _secureStorage?.read(key: key);
227-
value = int.parse(stringValue ?? '') as T?;
229+
value = int.tryParse(stringValue ?? '') as T?;
228230
} else if (T == double) {
229231
final stringValue = await _secureStorage?.read(key: key);
230232
value = double.tryParse(stringValue ?? '') as T?;

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: flutter_easy_cache
22
description: "A don't overthink it, easy-to-use caching layer for Flutter."
3-
version: 0.0.5
3+
version: 0.0.6
44
repository: https://github.com/chadpavliska/flutter_easy_cache
55

66
environment:

test/bug_reproduction_test.dart

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:flutter_easy_cache/flutter_easy_cache.dart';
3+
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
4+
import 'package:shared_preferences/shared_preferences.dart';
5+
6+
/// These tests are designed to expose specific bugs found in code review
7+
void main() {
8+
TestWidgetsFlutterBinding.ensureInitialized();
9+
10+
late FlutterEasyCache cache;
11+
SharedPreferencesWithCache? sharedPreferences;
12+
FlutterSecureStorage? secureStorage;
13+
14+
setUp(() async {
15+
FlutterEasyCache.setMockInitialValues();
16+
secureStorage ??= const FlutterSecureStorage();
17+
sharedPreferences ??=
18+
await SharedPreferencesWithCache.create(cacheOptions: const SharedPreferencesWithCacheOptions());
19+
cache = FlutterEasyCache.create(sharedPreferences!, secureStorage!, enableLogging: true);
20+
});
21+
22+
tearDown(() async {
23+
await FlutterEasyCache.resetStatic();
24+
});
25+
26+
group('Bug Reproduction Tests', () {
27+
// Bug #2: int.parse() should use tryParse() to avoid throwing
28+
test('BUG #2: Reading non-existent int from secure storage should not throw FormatException', () async {
29+
// First, add and then remove an int to ensure secure storage is initialized
30+
await cache.addOrUpdate(key: 'tempInt', value: 42, policy: CachePolicy.secure);
31+
await cache.remove(key: 'tempInt');
32+
33+
// Now try to read a key that was never set
34+
// With int.parse(), this will throw FormatException: Invalid radix-10 number (at character 1)
35+
// With int.tryParse(), this will return null gracefully
36+
37+
int? retrievedValue;
38+
bool didThrow = false;
39+
40+
try {
41+
retrievedValue = await cache.getValueOrNull<int>(key: 'neverSetIntKey');
42+
} catch (e) {
43+
didThrow = true;
44+
// Exception caught: $e
45+
}
46+
47+
expect(didThrow, false, reason: 'Should use int.tryParse() instead of int.parse() to avoid throwing');
48+
expect(retrievedValue, null);
49+
});
50+
51+
// Bug #3: Null pointer when reading non-existent List<Map>
52+
test('BUG #3: Reading non-existent List<Map> from preferences should not throw', () async {
53+
// Initialize preferences
54+
await cache.addOrUpdate(key: 'dummy', value: 'value', policy: CachePolicy.appInstall);
55+
56+
// Try to read a List<Map> that doesn't exist
57+
// Line 198: final stringList = _preferences?.getStringList(key) as List<String>;
58+
// This will throw TypeError: Null check operator used on a null value
59+
60+
List<Map<String, dynamic>>? retrievedValue;
61+
bool didThrow = false;
62+
63+
try {
64+
retrievedValue = await cache.getValueOrNull<List<Map<String, dynamic>>>(key: 'neverSetListMapKey');
65+
} catch (e) {
66+
didThrow = true;
67+
// Exception caught: $e
68+
}
69+
70+
expect(didThrow, false, reason: 'Should check for null before casting');
71+
expect(retrievedValue, null);
72+
});
73+
74+
// Bug #1: Missing await in addOrUpdate switch statement
75+
test('BUG #1: addOrUpdate should await async operations in switch statement', () async {
76+
// This test tries to verify that the Future completes only after write finishes
77+
// In practice, this bug might not be caught by tests due to fast execution
78+
// But it violates the async contract
79+
80+
const value = 'test-value';
81+
82+
// Create a list to track execution order
83+
final executionOrder = <String>[];
84+
85+
cache.addOrUpdate(key: 'testKey', value: value, policy: CachePolicy.appInstall).then((_) {
86+
executionOrder.add('addOrUpdate completed');
87+
});
88+
89+
// Small delay to let async operations settle
90+
await Future.delayed(const Duration(milliseconds: 10));
91+
executionOrder.add('after delay');
92+
93+
final retrievedValue = await cache.getValueOrNull<String>(key: 'testKey');
94+
95+
expect(retrievedValue, value);
96+
expect(executionOrder, contains('addOrUpdate completed'));
97+
});
98+
});
99+
}

test/flutter_easy_cache_app_install_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ void main() {
2121
secureStorage ??= const FlutterSecureStorage();
2222

2323
// sut
24-
cache = FlutterEasyCache.create(sharedPreferences!, secureStorage!, enalbeLogging: false);
24+
cache = FlutterEasyCache.create(sharedPreferences!, secureStorage!, enableLogging: false);
2525
});
2626

2727
tearDown(() async {

test/flutter_easy_cache_app_session_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ void main() {
2121
await SharedPreferencesWithCache.create(cacheOptions: const SharedPreferencesWithCacheOptions());
2222

2323
// sut
24-
cache = FlutterEasyCache.create(sharedPreferences!, secureStorage!, enalbeLogging: false);
24+
cache = FlutterEasyCache.create(sharedPreferences!, secureStorage!, enableLogging: false);
2525
});
2626

2727
tearDown(() async {

test/flutter_easy_cache_secure_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ void main() {
2121
await SharedPreferencesWithCache.create(cacheOptions: const SharedPreferencesWithCacheOptions());
2222

2323
// sut
24-
cache = FlutterEasyCache.create(sharedPreferences!, secureStorage!, enalbeLogging: false);
24+
cache = FlutterEasyCache.create(sharedPreferences!, secureStorage!, enableLogging: false);
2525
});
2626

2727
tearDown(() async {

test/flutter_easy_cache_test.dart

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ void main() {
2121
await SharedPreferencesWithCache.create(cacheOptions: const SharedPreferencesWithCacheOptions());
2222

2323
// sut
24-
cache = FlutterEasyCache.create(sharedPreferences!, secureStorage!, enalbeLogging: false);
24+
cache = FlutterEasyCache.create(sharedPreferences!, secureStorage!, enableLogging: false);
2525
});
2626

2727
tearDown(() async {
@@ -176,5 +176,47 @@ void main() {
176176
throwsA(isA<TypeError>()),
177177
);
178178
});
179+
180+
// TDD: Test for Bug #1 - Missing await in addOrUpdate()
181+
test('addOrUpdate completes only after value is written to storage', () async {
182+
const value = 'test-value';
183+
184+
// Write to appInstall (requires async disk write)
185+
await cache.addOrUpdate(key: 'testKey', value: value, policy: CachePolicy.appInstall);
186+
187+
// If await is missing in the switch statement, this might read before write completes
188+
final retrievedValue = await cache.getValueOrNull<String>(key: 'testKey');
189+
190+
expect(retrievedValue, value, reason: 'Value should be available immediately after addOrUpdate completes');
191+
});
192+
193+
// TDD: Test for Bug #2 - int.parse() throws instead of returning null
194+
test('Reading non-existent int from secure storage returns null (not throw)', () async {
195+
// This should NOT throw - should return null gracefully
196+
expect(
197+
() async => await cache.getValueOrNull<int>(key: 'nonExistentIntKey'),
198+
returnsNormally,
199+
reason: 'Should not throw when reading non-existent int from secure storage',
200+
);
201+
202+
final value = await cache.getValueOrNull<int>(key: 'nonExistentIntKey');
203+
expect(value, null);
204+
});
205+
206+
// TDD: Test for Bug #3 - Null pointer in List<Map> deserialization
207+
test('Reading non-existent List<Map> from preferences returns null (not throw)', () async {
208+
// First write to appInstall to ensure preferences is initialized
209+
await cache.addOrUpdate(key: 'dummyKey', value: 'dummy', policy: CachePolicy.appInstall);
210+
211+
// This should NOT throw - should return null gracefully
212+
expect(
213+
() async => await cache.getValueOrNull<List<Map<String, dynamic>>>(key: 'nonExistentListMapKey'),
214+
returnsNormally,
215+
reason: 'Should not throw when reading non-existent List<Map> from preferences',
216+
);
217+
218+
final value = await cache.getValueOrNull<List<Map<String, dynamic>>>(key: 'nonExistentListMapKey');
219+
expect(value, null);
220+
});
179221
});
180222
}

0 commit comments

Comments
 (0)