import 'package:dio/dio.dart'; /// Mock Dio client for testing API interactions. /// /// Allows registering canned responses for specific endpoints. class MockApiClient { MockApiClient() { _dio = Dio(BaseOptions(baseUrl: 'https://test.api')) ..interceptors.add(_MockInterceptor(this)); } late final Dio _dio; final Map _responses = {}; final List _requests = []; /// Get the mock Dio instance. Dio get dio => _dio; /// Get all recorded requests. List get requests => List.unmodifiable(_requests); /// Get the last request made. RequestOptions? get lastRequest => _requests.isEmpty ? null : _requests.last; /// Clear all recorded requests. void clearRequests() => _requests.clear(); /// Register a mock response for an endpoint. /// /// [method] is the HTTP method (GET, POST, PUT, DELETE). /// [path] is the endpoint path (can include wildcards like /users/*). /// [response] is the response data. /// [statusCode] is the HTTP status code (default 200). void when({ required String method, required String path, required dynamic response, int statusCode = 200, Map? headers, }) { final key = '${method.toUpperCase()}:$path'; _responses[key] = _MockResponse( data: response, statusCode: statusCode, headers: headers ?? {}, ); } /// Register a GET response. void whenGet(String path, dynamic response, {int statusCode = 200}) { when(method: 'GET', path: path, response: response, statusCode: statusCode); } /// Register a POST response. void whenPost(String path, dynamic response, {int statusCode = 200}) { when(method: 'POST', path: path, response: response, statusCode: statusCode); } /// Register a PUT response. void whenPut(String path, dynamic response, {int statusCode = 200}) { when(method: 'PUT', path: path, response: response, statusCode: statusCode); } /// Register a DELETE response. void whenDelete(String path, dynamic response, {int statusCode = 200}) { when(method: 'DELETE', path: path, response: response, statusCode: statusCode); } /// Register an error response. void whenError({ required String method, required String path, required int statusCode, String? message, }) { when( method: method, path: path, response: {'error': message ?? 'Mock error'}, statusCode: statusCode, ); } /// Find a mock response for the given request. _MockResponse? _findResponse(String method, String path) { // Try exact match first final exactKey = '${method.toUpperCase()}:$path'; if (_responses.containsKey(exactKey)) { return _responses[exactKey]; } // Try wildcard matches for (final entry in _responses.entries) { final pattern = entry.key; if (_matchesPattern(pattern, exactKey)) { return entry.value; } } return null; } bool _matchesPattern(String pattern, String key) { // Simple wildcard matching: /users/* matches /users/123 if (!pattern.contains('*')) return false; final patternParts = pattern.split('/'); final keyParts = key.split('/'); if (patternParts.length != keyParts.length) return false; for (var i = 0; i < patternParts.length; i++) { if (patternParts[i] == '*') continue; if (patternParts[i] != keyParts[i]) return false; } return true; } /// Reset all mock responses and requests. void reset() { _responses.clear(); _requests.clear(); } } class _MockResponse { _MockResponse({ required this.data, required this.statusCode, required this.headers, }); final dynamic data; final int statusCode; final Map headers; } class _MockInterceptor extends Interceptor { _MockInterceptor(this._client); final MockApiClient _client; @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { _client._requests.add(options); final response = _client._findResponse(options.method, options.path); if (response != null) { if (response.statusCode >= 200 && response.statusCode < 300) { handler.resolve(Response( requestOptions: options, data: response.data, statusCode: response.statusCode, headers: Headers.fromMap( response.headers.map((k, v) => MapEntry(k, [v.toString()])), ), )); } else { handler.reject(DioException( requestOptions: options, response: Response( requestOptions: options, data: response.data, statusCode: response.statusCode, ), type: DioExceptionType.badResponse, )); } } else { // No mock registered - return 404 handler.reject(DioException( requestOptions: options, response: Response( requestOptions: options, data: {'error': 'No mock registered for ${options.method} ${options.path}'}, statusCode: 404, ), type: DioExceptionType.badResponse, message: 'No mock registered for ${options.method} ${options.path}', )); } } }