feat : new injection logic

test : some file :)
chore : upgrade android gradle
This commit is contained in:
2025-08-19 11:22:34 +03:30
parent 9b04c0374b
commit 7c3c1280b2
47 changed files with 1139 additions and 377 deletions

33
packages/core/.gitignore vendored Normal file
View File

@@ -0,0 +1,33 @@
# Ignore build output and other generated files
build/
# Dart/Pub related
.dart_tool/
.packages
.pub/
# IDE files
.idea/
*.iml
*.ipr
*.iws
# VS Code
.vscode/
# macOS
.DS_Store
# Other
*.log
*.swp
*.pyc
# Flutter specific
.flutter-plugins
.flutter-plugins-dependencies
pubspec.lock
# Test outputs
test_cache/

View File

@@ -1 +0,0 @@
{"packages/cupertino_icons/assets/CupertinoIcons.ttf":["packages/cupertino_icons/assets/CupertinoIcons.ttf"],"packages/flutter_map/lib/assets/flutter_map_logo.png":["packages/flutter_map/lib/assets/flutter_map_logo.png"],"packages/font_awesome_flutter/lib/fonts/fa-brands-400.ttf":["packages/font_awesome_flutter/lib/fonts/fa-brands-400.ttf"],"packages/font_awesome_flutter/lib/fonts/fa-regular-400.ttf":["packages/font_awesome_flutter/lib/fonts/fa-regular-400.ttf"],"packages/font_awesome_flutter/lib/fonts/fa-solid-900.ttf":["packages/font_awesome_flutter/lib/fonts/fa-solid-900.ttf"]}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -2,6 +2,7 @@ library;
export 'package:android_intent_plus/android_intent.dart';
export 'package:android_intent_plus/flag.dart';
export 'package:connectivity_plus/connectivity_plus.dart';
export 'package:device_info_plus/device_info_plus.dart';
export 'package:dio/dio.dart';
//other packages

View File

@@ -0,0 +1,24 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:get/get.dart';
class NetworkStatus {
NetworkStatus._();
static final NetworkStatus _instance = NetworkStatus._();
factory NetworkStatus() => _instance;
final Connectivity _connectivity = Connectivity();
RxBool isConnected = false.obs;
void startListening() {
_connectivity.onConnectivityChanged.listen((result) {
isConnected.value = !result.contains(ConnectivityResult.none);
});
_connectivity.checkConnectivity().then((result) {
isConnected.value = !result.contains(ConnectivityResult.none);
});
}
}

View File

@@ -0,0 +1,168 @@
import 'dart:async';
import '../../core.dart';
/// Callback to refresh the authentication token.
/// Typically used to request a new token from the server.
typedef RefreshTokenCallback = Future<String?> Function();
/// Callback to save a new authentication token.
typedef SaveTokenCallback = Future<void> Function(String token);
/// Callback to clear the authentication token, e.g., on logout or failure.
typedef ClearTokenCallback = Future<void> Function();
/// Callback invoked when token refresh fails.
/// Typically used to redirect the user to login or show a logout message.
typedef OnRefreshFailedCallback = Future<void> Function();
/// Represents a queued request waiting for token refresh.
class QueuedRequest {
/// The original request options.
final RequestOptions options;
/// Completer used to complete the response once the request is retried.
final Completer<Response> completer;
/// Constructs a queued request.
QueuedRequest(this.options, this.completer);
}
/// An interceptor for automatic token management and refresh handling.
///
/// Features:
/// - Queues requests while a token refresh is in progress.
/// - Saves and clears tokens via provided callbacks.
/// - Calls [OnRefreshFailedCallback] if token refresh fails.
class AppInterceptorN extends Interceptor {
/// Callback to refresh the authentication token.
final RefreshTokenCallback? refreshTokenCallback;
/// Callback to save the new token.
final SaveTokenCallback saveTokenCallback;
/// Callback to clear the token.
final ClearTokenCallback clearTokenCallback;
/// Callback executed when token refresh fails.
final OnRefreshFailedCallback onRefreshFailed;
/// Optional additional arguments for authentication.
final dynamic authArguments;
/// The Dio instance used to send requests.
final Dio dio;
/// Maximum number of retry attempts for failed requests.
final int maxRetries;
/// Whether a token refresh is currently in progress.
bool _isRefreshing = false;
/// Queue of requests waiting for a new token.
final List<QueuedRequest> _queue = [];
/// Current token in use.
String? _currentToken;
/// Constructs the interceptor.
AppInterceptorN({
required this.dio,
required this.saveTokenCallback,
required this.clearTokenCallback,
required this.onRefreshFailed,
this.refreshTokenCallback,
this.authArguments,
this.maxRetries = 3,
});
/// Called before sending a request.
/// If a token refresh is in progress, the request is added to the queue.
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
if (_isRefreshing) {
final completer = Completer<Response>();
_queue.add(QueuedRequest(options, completer));
return handler.resolve(await completer.future);
}
handler.next(options);
}
/// Called when an error occurs during a request.
///
/// - If the error is a 401 (unauthorized) and retry count is below `maxRetries`,
/// the token is refreshed and queued requests are retried.
/// - If the token refresh fails, all queued requests are cancelled and
/// [onRefreshFailed] is executed.
@override
Future<void> onError(DioException err, ErrorInterceptorHandler handler) async {
int currentRetry = err.requestOptions.extra['retryCount'] ?? 0;
if (err.response?.statusCode == 401 &&
err.type != DioExceptionType.cancel &&
currentRetry < maxRetries) {
final completer = Completer<Response>();
final updatedOptions = err.requestOptions.copyWith(
extra: {...err.requestOptions.extra, 'retryCount': currentRetry + 1},
);
_queue.add(QueuedRequest(updatedOptions, completer));
if (!_isRefreshing) {
_isRefreshing = true;
try {
final newToken = await refreshTokenCallback?.call();
if (newToken != null && newToken.isNotEmpty) {
_currentToken = newToken;
await saveTokenCallback(newToken);
for (var req in _queue) {
final newOptions = req.options.copyWith(
headers: {...req.options.headers, 'Authorization': 'Bearer $newToken'},
);
dio
.fetch(newOptions)
.then(req.completer.complete)
.catchError(req.completer.completeError);
}
} else {
await clearTokenCallback();
await _handleRefreshFailure();
for (var req in _queue) {
req.completer.completeError(
DioException(requestOptions: req.options, type: DioExceptionType.cancel),
);
}
}
} catch (e) {
await clearTokenCallback();
await _handleRefreshFailure();
for (var req in _queue) {
req.completer.completeError(e);
}
} finally {
_queue.clear();
_isRefreshing = false;
}
}
return handler.resolve(await completer.future);
}
handler.next(err);
}
/// Handles token refresh failure:
/// - Cancels all ongoing requests via [ApiHandler].
/// - Executes external [onRefreshFailed] callback.
Future<void> _handleRefreshFailure() async {
ApiHandler.cancelAllRequests("Token refresh failed");
await onRefreshFailed.call();
}
@visibleForTesting
set isRefreshingForTest(bool value) => _isRefreshing = value;
@visibleForTesting
List<QueuedRequest> get queue => _queue;
}

View File

@@ -1,15 +1,15 @@
import 'package:get_it/get_it.dart';
import 'package:logger/logger.dart';
import 'package:rasadyar_core/data/services/auth_middelware.dart';
import 'package:rasadyar_core/data/services/network_status.dart';
import 'package:rasadyar_core/infrastructure/local/hive_local_storage.dart';
final diCore = GetIt.instance;
Future<void> setupAllCoreProvider() async {
await _setUpLogger();
await _setupLocalStorage();
await _setupRemote();
diCore.registerSingleton(NetworkStatus()..startListening());
await diCore.allReady();
}
@@ -23,4 +23,4 @@ Future<void> _setupLocalStorage() async {
Future<void> _setupRemote() async {
// diCore.registerSingleton<HiveLocalStorage>(HiveLocalStorage());
}
}

View File

@@ -1,16 +1,21 @@
import 'package:flutter/material.dart';
import '../../core.dart';
/// Handles global API requests management with CancelToken.
class ApiHandler {
// Global CancelToken for all requests.
static CancelToken _globalCancelToken = CancelToken();
/// Returns the current global CancelToken.
static CancelToken get globalCancelToken => _globalCancelToken;
/// Resets the global CancelToken to a new one.
static Future<void> reset() async {
_globalCancelToken = CancelToken();
}
/// Cancels all ongoing requests and resets the CancelToken.
/// [reason] is optional text explaining why requests are canceled.
static void cancelAllRequests(String reason) {
if (!_globalCancelToken.isCancelled) {
_globalCancelToken.cancel(reason);

View File

@@ -77,18 +77,18 @@ packages:
dependency: transitive
description:
name: build
sha256: "7d95cbbb1526ab5ae977df9b4cc660963b9b27f6d1075c0b34653868911385e4"
sha256: "6439a9c71a4e6eca8d9490c1b380a25b02675aa688137dfbe66d2062884a23ac"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "3.0.2"
build_config:
dependency: transitive
description:
name: build_config
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.2.0"
build_daemon:
dependency: transitive
description:
@@ -101,26 +101,26 @@ packages:
dependency: transitive
description:
name: build_resolvers
sha256: "38c9c339333a09b090a638849a4c56e70a404c6bdd3b511493addfbc113b60c2"
sha256: "2b21a125d66a86b9511cc3fb6c668c42e9a1185083922bf60e46d483a81a9712"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "3.0.2"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: b971d4a1c789eba7be3e6fe6ce5e5b50fd3719e3cb485b3fad6d04358304351d
sha256: fd3c09f4bbff7fa6e8d8ef688a0b2e8a6384e6483a25af0dac75fef362bcfe6f
url: "https://pub.dev"
source: hosted
version: "2.6.0"
version: "2.7.0"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: c04e612ca801cd0928ccdb891c263a2b1391cb27940a5ea5afcf9ba894de5d62
sha256: ab27e46c8aa233e610cf6084ee6d8a22c6f873a0a9929241d8855b7a72978ae7
url: "https://pub.dev"
source: hosted
version: "9.2.0"
version: "9.3.0"
built_collection:
dependency: transitive
description:
@@ -153,6 +153,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_config:
dependency: transitive
description:
name: cli_config
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
url: "https://pub.dev"
source: hosted
version: "0.2.0"
clock:
dependency: transitive
description:
@@ -209,6 +217,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
url: "https://pub.dev"
source: hosted
version: "1.15.0"
cross_file:
dependency: transitive
description:
@@ -761,66 +777,66 @@ packages:
dependency: "direct main"
description:
name: image_picker
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.2.0"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "6fae381e6af2bbe0365a5e4ce1db3959462fa0c4d234facf070746024bb80c8d"
sha256: e83b2b05141469c5e19d77e1dfa11096b6b1567d09065b2265d7c6904560050c
url: "https://pub.dev"
source: hosted
version: "0.8.12+24"
version: "0.8.13"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
version: "3.1.0"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e
url: "https://pub.dev"
source: hosted
version: "0.8.12+2"
version: "0.8.13"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9"
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
url: "https://pub.dev"
source: hosted
version: "0.2.1+2"
version: "0.2.2"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04
url: "https://pub.dev"
source: hosted
version: "0.2.1+2"
version: "0.2.2"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665"
url: "https://pub.dev"
source: hosted
version: "2.10.1"
version: "2.11.0"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
version: "0.2.2"
image_size_getter:
dependency: transitive
description:
@@ -889,26 +905,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0"
url: "https://pub.dev"
source: hosted
version: "10.0.9"
version: "11.0.1"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.9"
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
lints:
dependency: transitive
description:
@@ -1021,6 +1037,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.5.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
@@ -1161,10 +1185,10 @@ packages:
dependency: "direct main"
description:
name: persian_datetime_picker
sha256: "7ccbfd3a68dc89d405550f624e9fa590c914fed2aa2d48973c4f4400baab2e06"
sha256: "0ec2879d2bee8390dda088b412739e6316e3a54d77640ec54dc1eeca8c5baa59"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.1.1"
petitparser:
dependency: transitive
description:
@@ -1285,6 +1309,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
@@ -1322,6 +1362,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.6"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: transitive
description:
@@ -1378,14 +1434,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test:
dependency: "direct dev"
description:
name: test
sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb"
url: "https://pub.dev"
source: hosted
version: "1.26.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev"
source: hosted
version: "0.7.4"
version: "0.7.6"
test_core:
dependency: transitive
description:
name: test_core
sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a"
url: "https://pub.dev"
source: hosted
version: "0.6.11"
time:
dependency: transitive
description:
@@ -1454,10 +1526,10 @@ packages:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
vm_service:
dependency: transitive
description:
@@ -1498,6 +1570,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
win32:
dependency: transitive
description:
@@ -1555,5 +1635,5 @@ packages:
source: hosted
version: "2.1.0"
sdks:
dart: ">=3.8.1 <4.0.0"
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.29.0"

View File

@@ -4,7 +4,7 @@ publish_to: none
version: 1.2.0+2
environment:
sdk: ^3.8.1
sdk: ^3.9.0
dependencies:
flutter:
@@ -17,7 +17,7 @@ dependencies:
package_info_plus: ^8.3.1
##image_picker
image_picker: ^1.1.2
image_picker: ^1.2.0
image_cropper: ^9.1.0
#UI
@@ -58,7 +58,7 @@ dependencies:
#other
permission_handler: ^12.0.1
persian_datetime_picker: ^3.1.0
persian_datetime_picker: ^3.1.1
encrypt: ^5.0.3
#L10N tools
@@ -89,10 +89,11 @@ dev_dependencies:
sdk: flutter
flutter_lints: ^6.0.0
##code generation
build_runner: ^2.6.0
build_runner: ^2.7.0
hive_ce_generator: ^1.9.3
freezed: ^3.2.0
json_serializable: ^6.10.0
test: ^1.24.0
##test

View File

@@ -0,0 +1,106 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:rasadyar_core/infrastructure/remote/app_interceptor_n.dart';
class MockDio extends Mock implements Dio {}
class MockResponse extends Mock implements Response {}
class MockRequestOptions extends Mock implements RequestOptions {}
class FakeRequestOptions extends Fake implements RequestOptions {}
void main() {
late MockDio dio;
late AppInterceptorN interceptor;
late MockResponse response;
late MockRequestOptions requestOptions;
late bool saveCalled;
late bool clearCalled;
late bool refreshFailedCalled;
String? savedToken;
setUp(() {
dio = MockDio();
response = MockResponse();
requestOptions = MockRequestOptions();
saveCalled = false;
clearCalled = false;
refreshFailedCalled = false;
savedToken = null;
interceptor = AppInterceptorN(
dio: dio,
saveTokenCallback: (token) async {
savedToken = token;
saveCalled = true;
},
clearTokenCallback: () async {
clearCalled = true;
},
refreshTokenCallback: () async => 'newToken',
onRefreshFailed: () async {
refreshFailedCalled = true;
},
);
});
setUpAll(() {
registerFallbackValue(FakeRequestOptions());
});
test('should refresh token and retry queued requests on 401', () async {
final options = RequestOptions(path: "/test");
final response = Response(requestOptions: options, statusCode: 200, data: 'ok');
when(() => dio.fetch(any())).thenAnswer((_) async => response);
final handler = ErrorInterceptorHandler();
final dioException = DioException(
requestOptions: options,
response: Response(requestOptions: options, statusCode: 401),
type: DioExceptionType.badResponse,
);
await interceptor.onError(dioException, handler);
expect(saveCalled, isTrue);
expect(savedToken, 'newToken');
expect(interceptor.queue.isEmpty, isTrue);
expect(clearCalled, isFalse);
expect(refreshFailedCalled, isFalse);
});
test('should queue request if refreshing', () async {
interceptor.isRefreshingForTest = true;
final options = RequestOptions(path: "/test");
final handler = RequestInterceptorHandler();
final completer = Completer<Response>();
interceptor.queue.add(QueuedRequest(options, completer));
interceptor.onRequest(options, handler);
expect(interceptor.queue.length, 2); // One added in setUp, one here
});
test('should refresh token and retry queued requests on 401', () async {
final options = RequestOptions(path: "/test");
final response = Response(requestOptions: options, statusCode: 200, data: 'ok');
when(() => dio.fetch(any())).thenAnswer((_) async => response);
final handler = ErrorInterceptorHandler();
final dioException = DioException(
requestOptions: options,
response: Response(requestOptions: options, statusCode: 401),
type: DioExceptionType.badResponse,
);
await interceptor.onError(dioException, handler);
expect(saveCalled, isTrue);
expect(savedToken, 'newToken');
expect(interceptor.queue.isEmpty, isTrue);
expect(clearCalled, isFalse);
expect(refreshFailedCalled, isFalse);
});
}

View File

@@ -1,23 +0,0 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'interfaces/i_form_data.dart';
class DioFormData implements IFormData {
final FormData _formData = FormData();
@override
void addFile(String field, Uint8List bytes, String filename) {
_formData.files.add(MapEntry(
field,
MultipartFile.fromBytes(bytes, filename: filename),
));
}
@override
void addField(String key, String value) {
_formData.fields.add(MapEntry(key, value));
}
FormData get raw => _formData;
}

View File

@@ -0,0 +1,55 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:rasadyar_core/core.dart';
void main() {
group('DioFormData', () {
late DioFormData formData;
setUp(() {
formData = DioFormData();
});
test('addField should add a field to FormData', () {
formData.addField('userName', 'mojtaba');
expect(formData.raw.fields.length, 1);
expect(formData.raw.fields.first.key, 'userName');
expect(formData.raw.fields.first.value, 'mojtaba');
});
test('addFile should add a file to FormData', () async {
final bytes = Uint8List.fromList([1, 2, 3, 4]);
formData.addFile('fileField', bytes, 'test.txt');
expect(formData.raw.files.length, 1);
final fileEntry = formData.raw.files.first;
expect(fileEntry.key, 'fileField');
final multipart = fileEntry.value;
expect(multipart.filename, 'test.txt');
final uploadedBytes = await multipart.finalize().toBytes();
expect(uploadedBytes, bytes);
});
test('raw getter should return the internal FormData instance', () {
final tmp = formData.raw;
expect(tmp, isA<FormData>());
});
});
}
extension on Stream<List<int>> {
/// Helper to collect stream into a single Uint8List for comparison
Future<Uint8List> toBytes() async {
final chunks = <int>[];
await for (final chunk in this) {
chunks.addAll(chunk);
}
return Uint8List.fromList(chunks);
}
}

View File

@@ -0,0 +1,201 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:rasadyar_core/core.dart';
class MockDio extends Mock implements Dio {}
class MockInterceptor extends Mock implements AppInterceptor {}
class FakeRequestOptions extends Fake implements RequestOptions {}
void main() {
setUpAll(() {
registerFallbackValue(FakeRequestOptions());
registerFallbackValue(RequestOptions(path: ''));
registerFallbackValue(Options());
registerFallbackValue(CancelToken());
});
group('Dio Remote', () {
late DioRemote dioRemote;
late MockDio mockDio;
setUp(() {
mockDio = MockDio();
dioRemote = DioRemote();
dioRemote.dio = mockDio;
});
test('init sets dio and adds interceptor if provided', () async {
final interceptor = MockInterceptor();
final client = DioRemote(interceptors: interceptor);
await client.init();
expect(client.dio, isA<Dio>());
});
test('get returns DioResponse with raw data', () async {
final response = Response(
requestOptions: RequestOptions(path: '/test'),
statusCode: 200,
data: {'message': 'ok'},
);
when(
() => mockDio.get(
any(),
queryParameters: any(named: 'queryParameters'),
options: any(named: 'options'),
onReceiveProgress: any(named: 'onReceiveProgress'),
cancelToken: any(named: 'cancelToken'),
),
).thenAnswer((_) async => response);
final result = await dioRemote.get<Map<String, dynamic>>('/test');
expect(result, isA<DioResponse<Map<String, dynamic>>>());
expect(result.data, {'message': 'ok'});
expect(result.statusCode, 200);
});
test('get applies fromJson mapper', () async {
final response = Response(
requestOptions: RequestOptions(path: '/user'),
statusCode: 200,
data: {'id': 1, 'name': 'Ali'},
);
when(
() => mockDio.get(
any(),
queryParameters: any(named: 'queryParameters'),
options: any(named: 'options'),
onReceiveProgress: any(named: 'onReceiveProgress'),
cancelToken: any(named: 'cancelToken'),
),
).thenAnswer((_) async => response);
final result = await dioRemote.get<String>(
'/user',
fromJson: (json) => "User: ${json['name']}",
);
expect(result.data, 'User: Ali');
});
test('post applies fromJson correctly', () async {
final response = Response(
requestOptions: RequestOptions(path: '/post'),
statusCode: 200,
data: {'id': 99},
);
when(
() => mockDio.post(
any(),
data: any(named: 'data'),
queryParameters: any(named: 'queryParameters'),
options: any(named: 'options'),
onSendProgress: any(named: 'onSendProgress'),
onReceiveProgress: any(named: 'onReceiveProgress'),
cancelToken: any(named: 'cancelToken'),
),
).thenAnswer((_) async => response);
final result = await dioRemote.post<int>(
'/post',
fromJson: (json) => json['id'],
);
expect(result.data, 99);
});
test('put returns parsed data', () async {
final response = Response(
requestOptions: RequestOptions(path: '/put'),
statusCode: 200,
data: {'value': 'updated'},
);
when(
() => mockDio.put(
any(),
data: any(named: 'data'),
queryParameters: any(named: 'queryParameters'),
options: any(named: 'options'),
onSendProgress: any(named: 'onSendProgress'),
onReceiveProgress: any(named: 'onReceiveProgress'),
cancelToken: any(named: 'cancelToken'),
),
).thenAnswer((_) async => response);
final result = await dioRemote.put<String>('/put', fromJson: (json) => json['value']);
expect(result.data, 'updated');
});
test('delete works with fromJson', () async {
final response = Response(
requestOptions: RequestOptions(path: '/delete'),
statusCode: 200,
data: {'removed': true},
);
when(
() => mockDio.delete(
any(),
data: any(named: 'data'),
queryParameters: any(named: 'queryParameters'),
options: any(named: 'options'),
cancelToken: any(named: 'cancelToken'),
),
).thenAnswer((_) async => response);
final result = await dioRemote.delete<bool>('/delete', fromJson: (json) => json['removed']);
expect(result.data, true);
});
test('download returns DioResponse with bytes', () async {
final response = Response<Uint8List>(
requestOptions: RequestOptions(path: '/download'),
statusCode: 200,
data: Uint8List.fromList([1, 2, 3]),
);
when(
() => mockDio.get<Uint8List>(
any(),
options: any(named: 'options'),
onReceiveProgress: any(named: 'onReceiveProgress'),
cancelToken: any(named: 'cancelToken'),
),
).thenAnswer((_) async => response);
final result = await dioRemote.download('/download');
expect(result.data, isA<Uint8List>());
expect(result.data!.length, 3);
});
test('upload sends DioFormData and returns DioResponse', () async {
final formData = DioFormData();
formData.addField('field', 'value');
final response = Response(
requestOptions: RequestOptions(path: '/upload'),
statusCode: 200,
data: 'uploaded',
);
when(
() => mockDio.post(
any(),
data: any(named: 'data'),
options: any(named: 'options'),
onSendProgress: any(named: 'onSendProgress'),
cancelToken: any(named: 'cancelToken'),
),
).thenAnswer((_) async => response);
final result = await dioRemote.upload<String>('/upload', formData: formData);
expect(result.data, 'uploaded');
});
});
}

View File

@@ -1,20 +0,0 @@
import 'interfaces/i_http_response.dart';
import 'package:dio/dio.dart';
class DioResponse<T> implements IHttpResponse<T> {
final Response<dynamic> _response;
DioResponse(this._response);
@override
T? get data => _response.data;
@override
int get statusCode => _response.statusCode ?? 0;
@override
Map<String, dynamic>? get headers => _response.headers.map;
@override
bool get isSuccessful => statusCode >= 200 && statusCode < 300;
}

View File

@@ -0,0 +1,60 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:rasadyar_core/core.dart';
void main() {
group('DioResponse', () {
test('data should return response data', () {
final response = Response(
requestOptions: RequestOptions(path: "/"),
data: 'hello',
);
final dioResponse = DioResponse<String>(response);
expect(dioResponse.data, 'hello');
});
test('status Code should return 0 if null', () {
final response = Response(requestOptions: RequestOptions(path: "/"), statusCode: null);
final dioResponse = DioResponse(response);
expect(dioResponse.statusCode, 0);
});
test('headers should return response headers map', () {
final headers = Headers.fromMap({
'content-type': ['application/json'],
});
final response = Response(
requestOptions: RequestOptions(path: "/"),
headers: headers,
);
final dioResponse = DioResponse(response);
expect(dioResponse.headers, isA<Map>());
expect(dioResponse.headers, {
'content-type': ['application/json'],
});
});
test('isSuccessful should return true for 2xx codes', () {
final response = Response(requestOptions: RequestOptions(path: "/"), statusCode: 200);
final dioResponse = DioResponse(response);
expect(dioResponse.statusCode, 200);
expect(dioResponse.isSuccessful, true);
});
test('isSuccessful should return false for non-2xx codes', () {
final response = Response(requestOptions: RequestOptions(path: "/"),statusCode: 404);
final dioResponse = DioResponse(response);
expect(dioResponse.statusCode, 404);
expect(dioResponse.isSuccessful, false);
});
});
}

View File

@@ -1,6 +0,0 @@
import 'package:flutter/foundation.dart';
abstract class IFormData{
void addFile(String field, Uint8List bytes, String filename);
void addField(String key, String value);
}

View File

@@ -1,53 +0,0 @@
import 'package:dio/dio.dart';
import 'i_form_data.dart';
import 'i_http_response.dart';
abstract class IHttpClient {
Future<void> init();
Future<IHttpResponse<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Map<String, String>? headers,
ProgressCallback? onReceiveProgress,
});
Future<IHttpResponse<T>> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Map<String, String>? headers,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
});
Future<IHttpResponse<T>> put<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Map<String, String>? headers,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
});
Future<IHttpResponse<T>> delete<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Map<String, String>? headers,
});
Future<IHttpResponse<T>> download<T>(
String url, {
ProgressCallback? onReceiveProgress,
});
Future<IHttpResponse<T>> upload<T>(
String path, {
required IFormData formData,
Map<String, String>? headers,
ProgressCallback? onSendProgress,
});
}

View File

@@ -1,6 +0,0 @@
abstract class IHttpResponse<T> {
T? get data;
int get statusCode;
Map<String, dynamic>? get headers;
bool get isSuccessful;
}

View File

@@ -1,4 +0,0 @@
abstract class IRemote<T>{
Future<T> init();
}