@@ -22,6 +22,26 @@ void main() {
2222 expect (await dataSource.getToken (), token);
2323 });
2424
25+ test (
26+ 'does not reread secure storage after the token cache is warm' ,
27+ () async {
28+ const token = TokenEntity (
29+ accessToken: 'access-token' ,
30+ refreshToken: 'refresh-token' ,
31+ );
32+ final storage = _CountingSecureStorage ({
33+ 'accessToken' : token.accessToken,
34+ 'refreshToken' : token.refreshToken,
35+ });
36+ dataSource = TokenLocalDataSourceImpl .withStorage (storage);
37+
38+ expect (await dataSource.getToken (), token);
39+ expect (await dataSource.getToken (), token);
40+
41+ expect (storage.readsByKey, {'accessToken' : 1 , 'refreshToken' : 1 });
42+ },
43+ );
44+
2545 test ('auth token write only updates the access token slot' , () async {
2646 await dataSource.storeTokens (
2747 const TokenEntity (
@@ -41,6 +61,54 @@ void main() {
4161 );
4262 });
4363
64+ test (
65+ 'auth token write warms the cache when refresh token is persisted' ,
66+ () async {
67+ final storage = _CountingSecureStorage ({'refreshToken' : 'refresh-token' });
68+ dataSource = TokenLocalDataSourceImpl .withStorage (storage);
69+
70+ await dataSource.storeAuthToken ('new-access' );
71+ storage.readsByKey.clear ();
72+
73+ expect (
74+ await dataSource.getToken (),
75+ const TokenEntity (
76+ accessToken: 'new-access' ,
77+ refreshToken: 'refresh-token' ,
78+ ),
79+ );
80+ expect (storage.readsByKey, isEmpty);
81+ },
82+ );
83+
84+ test (
85+ 'legacy token load migrates to current storage and warms cache' ,
86+ () async {
87+ const legacyToken = TokenEntity (
88+ accessToken: 'legacy-access' ,
89+ refreshToken: 'legacy-refresh' ,
90+ );
91+ final storage = _LegacyTokenSecureStorage (
92+ currentValues: {},
93+ legacyValues: {
94+ 'accessToken' : legacyToken.accessToken,
95+ 'refreshToken' : legacyToken.refreshToken,
96+ },
97+ );
98+ dataSource = TokenLocalDataSourceImpl .withStorage (storage);
99+
100+ expect (await dataSource.getToken (), legacyToken);
101+ storage.readsByKey.clear ();
102+
103+ expect (await dataSource.getToken (), legacyToken);
104+ expect (storage.currentValues, {
105+ 'accessToken' : legacyToken.accessToken,
106+ 'refreshToken' : legacyToken.refreshToken,
107+ });
108+ expect (storage.readsByKey, isEmpty);
109+ },
110+ );
111+
44112 test (
45113 'delete removes both token values and missing tokens fail clearly' ,
46114 () async {
@@ -66,3 +134,97 @@ void main() {
66134 },
67135 );
68136}
137+
138+ class _CountingSecureStorage extends FlutterSecureStorage {
139+ _CountingSecureStorage (this .values);
140+
141+ final Map <String , String ?> values;
142+ final readsByKey = < String , int > {};
143+
144+ @override
145+ Future <String ?> read ({
146+ required String key,
147+ AppleOptions ? iOptions,
148+ AndroidOptions ? aOptions,
149+ LinuxOptions ? lOptions,
150+ WebOptions ? webOptions,
151+ AppleOptions ? mOptions,
152+ WindowsOptions ? wOptions,
153+ }) async {
154+ readsByKey[key] = (readsByKey[key] ?? 0 ) + 1 ;
155+ return values[key];
156+ }
157+
158+ @override
159+ Future <void > write ({
160+ required String key,
161+ required String ? value,
162+ AppleOptions ? iOptions,
163+ AndroidOptions ? aOptions,
164+ LinuxOptions ? lOptions,
165+ WebOptions ? webOptions,
166+ AppleOptions ? mOptions,
167+ WindowsOptions ? wOptions,
168+ }) async {
169+ values[key] = value;
170+ }
171+
172+ @override
173+ Future <void > delete ({
174+ required String key,
175+ AppleOptions ? iOptions,
176+ AndroidOptions ? aOptions,
177+ LinuxOptions ? lOptions,
178+ WebOptions ? webOptions,
179+ AppleOptions ? mOptions,
180+ WindowsOptions ? wOptions,
181+ }) async {
182+ values.remove (key);
183+ }
184+ }
185+
186+ class _LegacyTokenSecureStorage extends FlutterSecureStorage {
187+ _LegacyTokenSecureStorage ({
188+ required this .currentValues,
189+ required this .legacyValues,
190+ });
191+
192+ final Map <String , String ?> currentValues;
193+ final Map <String , String ?> legacyValues;
194+ final readsByKey = < String , int > {};
195+
196+ @override
197+ Future <String ?> read ({
198+ required String key,
199+ AppleOptions ? iOptions,
200+ AndroidOptions ? aOptions,
201+ LinuxOptions ? lOptions,
202+ WebOptions ? webOptions,
203+ AppleOptions ? mOptions,
204+ WindowsOptions ? wOptions,
205+ }) async {
206+ readsByKey[key] = (readsByKey[key] ?? 0 ) + 1 ;
207+ return _valuesFor (iOptions)[key];
208+ }
209+
210+ @override
211+ Future <void > write ({
212+ required String key,
213+ required String ? value,
214+ AppleOptions ? iOptions,
215+ AndroidOptions ? aOptions,
216+ LinuxOptions ? lOptions,
217+ WebOptions ? webOptions,
218+ AppleOptions ? mOptions,
219+ WindowsOptions ? wOptions,
220+ }) async {
221+ _valuesFor (iOptions)[key] = value;
222+ }
223+
224+ Map <String , String ?> _valuesFor (AppleOptions ? iOptions) {
225+ return iOptions? .accessibility ==
226+ KeychainAccessibility .first_unlock_this_device
227+ ? currentValues
228+ : legacyValues;
229+ }
230+ }
0 commit comments