feat : new injection logic
test : some file :) chore : upgrade android gradle
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
abstract class IHttpResponse<T> {
|
||||
T? get data;
|
||||
int get statusCode;
|
||||
Map<String, dynamic>? get headers;
|
||||
bool get isSuccessful;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
abstract class IRemote<T>{
|
||||
Future<T> init();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user