first commit
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
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}) {}
|
||||
}
|
||||
Reference in New Issue
Block a user