import 'dart:convert'; import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hidalyx_drluiscastro/core/network/api_client.dart'; import 'package:hidalyx_drluiscastro/features/auth/data/models/auth_session_model.dart'; import 'package:hidalyx_drluiscastro/features/auth/data/services/auth_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; class MockAdapter implements HttpClientAdapter { int attempts = 0; @override Future fetch( RequestOptions options, Stream? requestStream, Future? cancelFuture, ) async { attempts++; if (options.headers['Authorization'] == 'Bearer refreshed_token') { return ResponseBody.fromString( '{"status":"success"}', 200, headers: { Headers.contentTypeHeader: [Headers.jsonContentType], }, ); } return ResponseBody.fromString( '{"errors":[{"message":"Token expired."}]}', 401, headers: { Headers.contentTypeHeader: [Headers.jsonContentType], }, ); } @override void close({bool force = false}) {} } void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { SharedPreferences.setMockInitialValues({}); }); group('AuthSessionModel copyWith', () { test('updates accessToken and preserves other fields', () { const session = AuthSessionModel( accessToken: 'old_access', refreshToken: 'old_refresh', userId: 'usr_1', firstName: 'Luis', lastName: 'Castro', email: 'castro@hidalyx.com', ); final updated = session.copyWith( accessToken: 'new_access', refreshToken: 'new_refresh', ); expect(updated.accessToken, 'new_access'); expect(updated.refreshToken, 'new_refresh'); expect(updated.userId, 'usr_1'); expect(updated.fullName, 'Luis Castro'); }); }); group('AuthService Token Refresh flow', () { test('refreshToken updates SharedPreferences and ApiClient when refresh succeeds', () async { final dio = Dio(); final apiClient = ApiClient.withDio(dio); final authService = AuthService(client: apiClient); // Pre-guardar sesión con refresh token final prefs = await SharedPreferences.getInstance(); const initialSession = AuthSessionModel( accessToken: 'expired_access_token', refreshToken: 'valid_mock_refresh_token', userId: 'u1', firstName: 'Luis', lastName: 'Castro', email: 'castroluis01@hotmail.com', ); await prefs.setString('hidalyx_med_auth_session', jsonEncode(initialSession.toJson())); expect(authService.apiClient.onTokenRefresh, isNotNull); }); test('loadSavedSession returns null when session contains empty tokens', () async { final authService = AuthService(); final prefs = await SharedPreferences.getInstance(); const emptySession = AuthSessionModel( accessToken: '', refreshToken: null, userId: 'u1', firstName: 'Luis', lastName: 'Castro', email: 'test@example.com', ); await prefs.setString('hidalyx_med_auth_session', jsonEncode(emptySession.toJson())); final result = await authService.loadSavedSession(); expect(result, isNull); }); test('ApiClient QueuedInterceptorsWrapper intercepts 401 and retries with refreshed token', () async { final dio = Dio(BaseOptions(baseUrl: 'https://mock.api')); final mockAdapter = MockAdapter(); dio.httpClientAdapter = mockAdapter; final client = ApiClient.withDio(dio); bool refreshCalled = false; // Configurar callback de refresco client.onTokenRefresh = () async { refreshCalled = true; return 'refreshed_token'; }; // Realizar una petición protegida final response = await client.dio.get('/items/pacientes'); expect(response.statusCode, 200); expect(response.data['status'], 'success'); expect(refreshCalled, isTrue); expect(mockAdapter.attempts, 2); expect(client.dio.options.headers['Authorization'], 'Bearer refreshed_token'); }); test('ApiClient strips Authorization header on /auth/login request', () async { final dio = Dio(BaseOptions(baseUrl: 'https://mock.api')); RequestOptions? interceptedOptions; dio.httpClientAdapter = _CustomAdapter((options) { interceptedOptions = options; return ResponseBody.fromString('{"data":{"access_token":"token_abc"}}', 200); }); final client = ApiClient.withDio(dio); client.setAuthToken('stale_expired_token'); // POST to /auth/login await client.dio.post('/auth/login', data: {'email': 'test@test.com'}); expect(interceptedOptions, isNotNull); expect(interceptedOptions!.headers.containsKey('Authorization'), isFalse); }); test('ApiClient does NOT trigger onTokenRefresh when /auth/logout fails with 401', () async { final dio = Dio(BaseOptions(baseUrl: 'https://mock.api')); dio.httpClientAdapter = _CustomAdapter((options) { return ResponseBody.fromString('{"errors":[{"message":"Token expired."}]}', 401); }); final client = ApiClient.withDio(dio); bool refreshCalled = false; client.onTokenRefresh = () async { refreshCalled = true; return 'should_not_be_called'; }; try { await client.dio.post('/auth/logout', data: {'refresh_token': 'expired_rf'}); } catch (_) {} expect(refreshCalled, isFalse); }); }); } class _CustomAdapter implements HttpClientAdapter { final ResponseBody Function(RequestOptions options) handler; _CustomAdapter(this.handler); @override Future fetch(RequestOptions options, Stream? requestStream, Future? cancelFuture) async { return handler(options); } @override void close({bool force = false}) {} }