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

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();
}