forked from eatvio/eatvio_app
chore(version): update app version to 1.3.5+85 and improve test coverage reporting in CI
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/interceptors/phone_verify_error_interceptor.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockProfileRepository extends Mock implements ProfileRepository {}
|
||||
|
||||
class MockErrorInterceptorHandler extends Mock implements ErrorInterceptorHandler {}
|
||||
|
||||
class FakeDioException extends Fake implements DioException {}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
group('PhoneVerifyErrorInterceptor', () {
|
||||
late PhoneVerifyErrorInterceptor interceptor;
|
||||
late MockProfileRepository mockProfileRepository;
|
||||
|
||||
setUpAll(() {
|
||||
// Регистрируем fallback значения для mocktail
|
||||
registerFallbackValue(FakeDioException());
|
||||
// Регистрируем mock в GetIt
|
||||
if (sl.isRegistered<ProfileRepository>()) {
|
||||
sl.unregister<ProfileRepository>();
|
||||
}
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
mockProfileRepository = MockProfileRepository();
|
||||
sl.registerSingleton<ProfileRepository>(mockProfileRepository);
|
||||
interceptor = PhoneVerifyErrorInterceptor();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
if (sl.isRegistered<ProfileRepository>()) {
|
||||
sl.unregister<ProfileRepository>();
|
||||
}
|
||||
});
|
||||
|
||||
test('onError - обрабатывает 405 с unverificated_phone', () async {
|
||||
const phone = '+1234567890';
|
||||
const passKey = 'test-pass-key';
|
||||
|
||||
when(() => mockProfileRepository.getMyPhone()).thenAnswer((_) async => phone);
|
||||
when(
|
||||
() => mockProfileRepository.getPassKey(
|
||||
routeName: any(named: 'routeName'),
|
||||
routeParams: any(named: 'routeParams'),
|
||||
),
|
||||
).thenAnswer((_) async => passKey);
|
||||
|
||||
final requestOptions = RequestOptions(path: '/test');
|
||||
final response = Response(requestOptions: requestOptions, statusCode: 405, data: {'type': 'unverificated_phone'});
|
||||
final error = DioException(requestOptions: requestOptions, response: response, type: DioExceptionType.badResponse);
|
||||
|
||||
final handler = ErrorInterceptorHandler();
|
||||
|
||||
// AppRouter.navigatorKey.currentContext может быть null в тестах
|
||||
// Метод выбросит исключение из-за null check operator на строке 17
|
||||
expect(() => interceptor.onError(error, handler), throwsA(isA<Error>()));
|
||||
});
|
||||
|
||||
test('onError - пропускает другие ошибки', () async {
|
||||
final requestOptions = RequestOptions(path: '/', method: 'GET', baseUrl: 'http://localhost');
|
||||
final response = Response(requestOptions: requestOptions, statusCode: 404, data: {});
|
||||
final error = DioException(requestOptions: requestOptions, response: response, type: DioExceptionType.badResponse);
|
||||
|
||||
final handler = MockErrorInterceptorHandler();
|
||||
when(() => handler.next(any())).thenReturn(null);
|
||||
|
||||
// Метод onError объявлен как void, но использует async внутри
|
||||
// В данном случае он просто вызывает handler.next(error)
|
||||
interceptor.onError(error, handler);
|
||||
|
||||
// Ждем завершения асинхронных операций
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
// Проверяем, что handler.next был вызван с правильной ошибкой
|
||||
verify(() => handler.next(error)).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
161
test/core/interceptors/token_interceptor_test.dart
Normal file
161
test/core/interceptors/token_interceptor_test.dart
Normal file
@@ -0,0 +1,161 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class MockRequestOptions extends Mock implements RequestOptions {}
|
||||
|
||||
class MockRequestInterceptorHandler extends Mock implements RequestInterceptorHandler {}
|
||||
|
||||
class MockErrorInterceptorHandler extends Mock implements ErrorInterceptorHandler {}
|
||||
|
||||
class MockResponse extends Mock implements Response {}
|
||||
|
||||
class MockDioException extends Mock implements DioException {}
|
||||
|
||||
class FakeRequestOptions extends Fake implements RequestOptions {}
|
||||
|
||||
class FakeDioException extends Fake implements DioException {}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(FakeRequestOptions());
|
||||
registerFallbackValue(FakeDioException());
|
||||
});
|
||||
group('TokenInterceptor', () {
|
||||
late TokenInterceptor interceptor;
|
||||
|
||||
setUp(() async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
interceptor = TokenInterceptor();
|
||||
// Сбрасываем токен перед каждым тестом
|
||||
try {
|
||||
await TokenStorage.removeTokenInStorage();
|
||||
} catch (e) {
|
||||
// Игнорируем ошибки при очистке
|
||||
}
|
||||
});
|
||||
|
||||
group('onRequest', () {
|
||||
test('добавляет Authorization header когда токен есть', () async {
|
||||
const token = 'test-token-123';
|
||||
await TokenStorage.saveTokenInStorage(token);
|
||||
|
||||
final headers = <String, dynamic>{};
|
||||
final options = MockRequestOptions();
|
||||
when(() => options.headers).thenReturn(headers);
|
||||
final handler = MockRequestInterceptorHandler();
|
||||
|
||||
when(() => handler.next(any())).thenReturn(null);
|
||||
|
||||
await interceptor.onRequest(options, handler);
|
||||
|
||||
expect(headers['Authorization'], 'Bearer $token');
|
||||
verify(() => handler.next(options)).called(1);
|
||||
});
|
||||
|
||||
test('не добавляет Authorization header когда токена нет', () async {
|
||||
final headers = <String, dynamic>{};
|
||||
final options = MockRequestOptions();
|
||||
when(() => options.headers).thenReturn(headers);
|
||||
final handler = MockRequestInterceptorHandler();
|
||||
|
||||
when(() => handler.next(any())).thenReturn(null);
|
||||
|
||||
await interceptor.onRequest(options, handler);
|
||||
|
||||
expect(headers.containsKey('Authorization'), false);
|
||||
verify(() => handler.next(options)).called(1);
|
||||
});
|
||||
|
||||
test('не добавляет Authorization header когда токен пустой', () async {
|
||||
await TokenStorage.saveTokenInStorage('');
|
||||
|
||||
final headers = <String, dynamic>{};
|
||||
final options = MockRequestOptions();
|
||||
when(() => options.headers).thenReturn(headers);
|
||||
final handler = MockRequestInterceptorHandler();
|
||||
|
||||
when(() => handler.next(any())).thenReturn(null);
|
||||
|
||||
await interceptor.onRequest(options, handler);
|
||||
|
||||
expect(headers.containsKey('Authorization'), false);
|
||||
verify(() => handler.next(options)).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('onError', () {
|
||||
test('удаляет токен и перенаправляет на auth при 401', () async {
|
||||
const token = 'test-token';
|
||||
await TokenStorage.saveTokenInStorage(token);
|
||||
|
||||
final options = MockRequestOptions();
|
||||
final response = MockResponse();
|
||||
when(() => response.statusCode).thenReturn(401);
|
||||
|
||||
final error = MockDioException();
|
||||
when(() => error.response).thenReturn(response);
|
||||
when(() => error.requestOptions).thenReturn(options);
|
||||
|
||||
final handler = MockErrorInterceptorHandler();
|
||||
when(() => handler.next(any())).thenReturn(null);
|
||||
|
||||
// Проверяем, что токен был удален
|
||||
final tokenBefore = await TokenStorage.getTokenFromStorage();
|
||||
expect(tokenBefore, token);
|
||||
|
||||
await interceptor.onError(error, handler);
|
||||
|
||||
final tokenAfter = await TokenStorage.getTokenFromStorage();
|
||||
expect(tokenAfter, null);
|
||||
verify(() => handler.next(error)).called(1);
|
||||
});
|
||||
|
||||
test('не удаляет токен при других статусах', () async {
|
||||
const token = 'test-token';
|
||||
await TokenStorage.saveTokenInStorage(token);
|
||||
|
||||
final options = MockRequestOptions();
|
||||
final response = MockResponse();
|
||||
when(() => response.statusCode).thenReturn(404);
|
||||
|
||||
final error = MockDioException();
|
||||
when(() => error.response).thenReturn(response);
|
||||
when(() => error.requestOptions).thenReturn(options);
|
||||
|
||||
final handler = MockErrorInterceptorHandler();
|
||||
when(() => handler.next(any())).thenReturn(null);
|
||||
|
||||
await interceptor.onError(error, handler);
|
||||
|
||||
final tokenAfter = await TokenStorage.getTokenFromStorage();
|
||||
expect(tokenAfter, token);
|
||||
verify(() => handler.next(error)).called(1);
|
||||
});
|
||||
|
||||
test('не удаляет токен когда response null', () async {
|
||||
const token = 'test-token';
|
||||
await TokenStorage.saveTokenInStorage(token);
|
||||
|
||||
final options = MockRequestOptions();
|
||||
final error = MockDioException();
|
||||
when(() => error.response).thenReturn(null);
|
||||
when(() => error.requestOptions).thenReturn(options);
|
||||
|
||||
final handler = MockErrorInterceptorHandler();
|
||||
when(() => handler.next(any())).thenReturn(null);
|
||||
|
||||
await interceptor.onError(error, handler);
|
||||
|
||||
final tokenAfter = await TokenStorage.getTokenFromStorage();
|
||||
expect(tokenAfter, token);
|
||||
verify(() => handler.next(error)).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user