import 'package:dio/dio.dart'; import 'package:drift/native.dart'; import 'package:fintracker_app/core/cache/cache_interceptor.dart'; import 'package:fintracker_app/core/cache/response_cache.dart'; import 'package:flutter_test/flutter_test.dart'; /// `onResponse`/`onError` complete synchronously in `CacheInterceptor` — no /// real Dio transport needed, same approach as `date_query_interceptor_test.dart`. class _ResponseCapture extends ResponseInterceptorHandler { Response? passed; @override void next(Response response) => passed = response; } class _ErrorCapture extends ErrorInterceptorHandler { Response? resolvedResponse; DioException? forwarded; @override void resolve(Response response) => resolvedResponse = response; @override void next(DioException error) => forwarded = error; } RequestOptions _request(String method, String path, [Map query = const {}]) => RequestOptions(path: path, method: method, queryParameters: query); DioException _offline(RequestOptions options) => DioException(requestOptions: options, type: DioExceptionType.connectionError); void main() { late ResponseCacheDatabase db; late CacheInterceptor interceptor; setUp(() { db = ResponseCacheDatabase.forTesting(NativeDatabase.memory()); interceptor = CacheInterceptor(db); }); tearDown(() => db.close()); test('a successful GET is written to the cache', () async { final options = _request('GET', '/networth/breakdown'); interceptor.onResponse( Response(requestOptions: options, statusCode: 200, data: {'totalRub': '100'}), _ResponseCapture(), ); await pumpEventQueue(); // the write is fire-and-forget final cached = await db.get(CacheInterceptor.requestKey(options)); expect(cached?.body, {'totalRub': '100'}); }); test('a connection error is answered from the cache, with fetchedAt set', () async { final options = _request('GET', '/networth/breakdown'); await db.put(CacheInterceptor.requestKey(options), {'totalRub': '100'}); final handler = _ErrorCapture(); await interceptor.onError(_offline(options), handler); expect(handler.resolvedResponse?.data, {'totalRub': '100'}); expect(handler.resolvedResponse?.extra['fetchedAt'], isA()); expect(handler.forwarded, isNull); }); test('a connection error with nothing cached still fails', () async { final options = _request('GET', '/networth/breakdown', {'never': 'asked'}); final failure = _offline(options); final handler = _ErrorCapture(); await interceptor.onError(failure, handler); expect(handler.resolvedResponse, isNull); expect(handler.forwarded, same(failure)); }); test('a real server error is never masked by a cached answer', () async { final options = _request('GET', '/networth/breakdown'); await db.put(CacheInterceptor.requestKey(options), {'totalRub': '100'}); final serverError = DioException( requestOptions: options, type: DioExceptionType.badResponse, response: Response(requestOptions: options, statusCode: 500), ); final handler = _ErrorCapture(); await interceptor.onError(serverError, handler); expect(handler.resolvedResponse, isNull); expect(handler.forwarded, same(serverError)); }); test('a failed mutation is never served from the cache', () async { final options = _request('POST', '/links'); final failure = _offline(options); final handler = _ErrorCapture(); await interceptor.onError(failure, handler); expect(handler.resolvedResponse, isNull); expect(handler.forwarded, same(failure)); }); test('query parameters are part of the cache key', () async { final page1 = _request('GET', '/events', {'page': 1}); final page2 = _request('GET', '/events', {'page': 2}); await db.put(CacheInterceptor.requestKey(page1), 'page1'); final handler = _ErrorCapture(); await interceptor.onError(_offline(page2), handler); expect(handler.resolvedResponse, isNull); // page=2 was never cached }); test('the key ignores query parameter order', () { final a = _request('GET', '/events', {'a': 1, 'b': 2}); final b = _request('GET', '/events', {'b': 2, 'a': 1}); expect(CacheInterceptor.requestKey(a), CacheInterceptor.requestKey(b)); }); }