Files
hidalyx-drluiscastro/test/features/auth_refresh_test.dart
T
2026-09-21 16:33:51 -04:00

186 lines
5.9 KiB
Dart

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<ResponseBody> fetch(
RequestOptions options,
Stream<Uint8List>? requestStream,
Future<void>? 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: '[email protected]',
);
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: '[email protected]',
);
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: '[email protected]',
);
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': '[email protected]'});
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<ResponseBody> fetch(RequestOptions options, Stream<Uint8List>? requestStream, Future<void>? cancelFuture) async {
return handler(options);
}
@override
void close({bool force = false}) {}
}