186 lines
5.9 KiB
Dart
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}) {}
|
|
}
|