forked from eatvio/eatvio_app
feat(diary): tests
This commit is contained in:
@@ -175,7 +175,16 @@ jobs:
|
||||
if (file ~ /\.(g|freezed|gr|mocks)\.dart$/) {
|
||||
is_generated = 1
|
||||
generated_files++
|
||||
} else if (file ~ /\/(build|\.dart_tool)\//) {
|
||||
} else if (file ~ /\/(build|\.dart_tool|generated)\//) {
|
||||
is_generated = 1
|
||||
generated_files++
|
||||
} else if (file ~ /\/l10n\.dart$/) {
|
||||
is_generated = 1
|
||||
generated_files++
|
||||
} else if (file ~ /\/dev_tools\//) {
|
||||
is_generated = 1
|
||||
generated_files++
|
||||
} else if (file ~ /(firebase_api|firebase_options)\.dart$/) {
|
||||
is_generated = 1
|
||||
generated_files++
|
||||
} else {
|
||||
|
||||
@@ -3,112 +3,129 @@ import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/error_reporting/error_report_policy.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
void main() {
|
||||
group('ErrorReportPolicy.shouldReport', () {
|
||||
test('returns false for null', () {
|
||||
test('null → false', () {
|
||||
expect(ErrorReportPolicy.shouldReport(null), false);
|
||||
});
|
||||
|
||||
test('returns false for String', () {
|
||||
expect(ErrorReportPolicy.shouldReport('user message'), false);
|
||||
test('String → false', () {
|
||||
expect(ErrorReportPolicy.shouldReport('error string'), false);
|
||||
});
|
||||
|
||||
test('returns false for SocketException', () {
|
||||
expect(ErrorReportPolicy.shouldReport(const SocketException('offline')), false);
|
||||
test('SocketException → false', () {
|
||||
expect(ErrorReportPolicy.shouldReport(const SocketException('failed')), false);
|
||||
});
|
||||
|
||||
test('returns false for cancelled Dio request', () {
|
||||
test('DioException cancel → false', () {
|
||||
final e = DioException(
|
||||
requestOptions: RequestOptions(path: '/x'),
|
||||
type: DioExceptionType.cancel,
|
||||
requestOptions: RequestOptions(path: '/'),
|
||||
);
|
||||
expect(ErrorReportPolicy.shouldReport(e), false);
|
||||
});
|
||||
|
||||
test('returns false for Dio timeouts', () {
|
||||
for (final type in [
|
||||
DioExceptionType.connectionTimeout,
|
||||
DioExceptionType.sendTimeout,
|
||||
DioExceptionType.receiveTimeout,
|
||||
]) {
|
||||
final e = DioException(
|
||||
requestOptions: RequestOptions(path: '/x'),
|
||||
type: type,
|
||||
);
|
||||
expect(ErrorReportPolicy.shouldReport(e), false, reason: '$type');
|
||||
}
|
||||
});
|
||||
|
||||
test('returns false for HTTP 4xx with response', () {
|
||||
test('DioException connectionTimeout → false', () {
|
||||
final e = DioException(
|
||||
requestOptions: RequestOptions(path: '/x'),
|
||||
response: Response(statusCode: 422, requestOptions: RequestOptions(path: '/x')),
|
||||
type: DioExceptionType.badResponse,
|
||||
type: DioExceptionType.connectionTimeout,
|
||||
requestOptions: RequestOptions(path: '/'),
|
||||
);
|
||||
expect(ErrorReportPolicy.shouldReport(e), false);
|
||||
});
|
||||
|
||||
test('returns false for HTTP 5xx (handled by sentry_dio failed-request)', () {
|
||||
test('DioException sendTimeout → false', () {
|
||||
final e = DioException(
|
||||
requestOptions: RequestOptions(path: '/x'),
|
||||
response: Response(statusCode: 503, requestOptions: RequestOptions(path: '/x')),
|
||||
type: DioExceptionType.badResponse,
|
||||
type: DioExceptionType.sendTimeout,
|
||||
requestOptions: RequestOptions(path: '/'),
|
||||
);
|
||||
expect(ErrorReportPolicy.shouldReport(e), false);
|
||||
});
|
||||
|
||||
test('returns false for Dio connectionError / offline-style', () {
|
||||
test('DioException receiveTimeout → false', () {
|
||||
final e = DioException(
|
||||
type: DioExceptionType.receiveTimeout,
|
||||
requestOptions: RequestOptions(path: '/'),
|
||||
);
|
||||
expect(ErrorReportPolicy.shouldReport(e), false);
|
||||
});
|
||||
|
||||
test('DioException connectionError → false', () {
|
||||
final e = DioException(
|
||||
requestOptions: RequestOptions(path: '/x'),
|
||||
type: DioExceptionType.connectionError,
|
||||
error: const SocketException('offline'),
|
||||
requestOptions: RequestOptions(path: '/'),
|
||||
);
|
||||
expect(ErrorReportPolicy.shouldReport(e), false);
|
||||
});
|
||||
|
||||
test('returns true for arbitrary exceptions', () {
|
||||
expect(ErrorReportPolicy.shouldReport(StateError('x')), true);
|
||||
});
|
||||
});
|
||||
|
||||
group('ErrorReportPolicy.allowSentryEvent', () {
|
||||
final hint = Hint();
|
||||
|
||||
test('allows 5xx Dio so sentry_dio events are not dropped in beforeSend', () {
|
||||
test('DioException 5xx → false', () {
|
||||
final e = DioException(
|
||||
requestOptions: RequestOptions(path: '/x'),
|
||||
response: Response(statusCode: 503, requestOptions: RequestOptions(path: '/x')),
|
||||
type: DioExceptionType.badResponse,
|
||||
requestOptions: RequestOptions(path: '/'),
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(path: '/'),
|
||||
statusCode: 500,
|
||||
),
|
||||
);
|
||||
final event = SentryEvent(throwable: e);
|
||||
expect(ErrorReportPolicy.allowSentryEvent(event, hint), true);
|
||||
expect(ErrorReportPolicy.shouldReport(e), false);
|
||||
});
|
||||
|
||||
test('drops 4xx Dio from SDK', () {
|
||||
test('DioException 502 → false', () {
|
||||
final e = DioException(
|
||||
requestOptions: RequestOptions(path: '/x'),
|
||||
response: Response(statusCode: 422, requestOptions: RequestOptions(path: '/x')),
|
||||
type: DioExceptionType.badResponse,
|
||||
requestOptions: RequestOptions(path: '/'),
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(path: '/'),
|
||||
statusCode: 502,
|
||||
),
|
||||
);
|
||||
final event = SentryEvent(throwable: e);
|
||||
expect(ErrorReportPolicy.allowSentryEvent(event, hint), false);
|
||||
expect(ErrorReportPolicy.shouldReport(e), false);
|
||||
});
|
||||
|
||||
test('drops transient Dio from SDK (connectionError)', () {
|
||||
test('DioException 4xx → false', () {
|
||||
final e = DioException(
|
||||
requestOptions: RequestOptions(path: '/x'),
|
||||
type: DioExceptionType.connectionError,
|
||||
error: const SocketException('reset'),
|
||||
type: DioExceptionType.badResponse,
|
||||
requestOptions: RequestOptions(path: '/'),
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(path: '/'),
|
||||
statusCode: 422,
|
||||
),
|
||||
);
|
||||
final event = SentryEvent(throwable: e);
|
||||
expect(ErrorReportPolicy.allowSentryEvent(event, hint), false);
|
||||
expect(ErrorReportPolicy.shouldReport(e), false);
|
||||
});
|
||||
|
||||
test('drops SocketException from SDK when not wrapped in Dio', () {
|
||||
final event = SentryEvent(throwable: const SocketException('x'));
|
||||
expect(ErrorReportPolicy.allowSentryEvent(event, hint), false);
|
||||
test('DioException unknown type with SocketException error → false', () {
|
||||
final e = DioException(
|
||||
type: DioExceptionType.unknown,
|
||||
requestOptions: RequestOptions(path: '/'),
|
||||
error: const SocketException('socket'),
|
||||
);
|
||||
expect(ErrorReportPolicy.shouldReport(e), false);
|
||||
});
|
||||
|
||||
test('DioException unknown type without SocketException → true', () {
|
||||
final e = DioException(
|
||||
type: DioExceptionType.unknown,
|
||||
requestOptions: RequestOptions(path: '/'),
|
||||
error: 'something else',
|
||||
);
|
||||
expect(ErrorReportPolicy.shouldReport(e), true);
|
||||
});
|
||||
|
||||
test('regular Exception → true', () {
|
||||
expect(ErrorReportPolicy.shouldReport(Exception('error')), true);
|
||||
});
|
||||
|
||||
test('TypeError → true', () {
|
||||
expect(ErrorReportPolicy.shouldReport(TypeError()), true);
|
||||
});
|
||||
|
||||
test('DioException badCertificate → true', () {
|
||||
final e = DioException(
|
||||
type: DioExceptionType.badCertificate,
|
||||
requestOptions: RequestOptions(path: '/'),
|
||||
);
|
||||
expect(ErrorReportPolicy.shouldReport(e), true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
262
test/core/interceptors/offline_error_interceptor_test.dart
Normal file
262
test/core/interceptors/offline_error_interceptor_test.dart
Normal file
@@ -0,0 +1,262 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/interceptors/offline_error_interceptor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('OfflineOperationException', () {
|
||||
test('stores fields correctly', () {
|
||||
const e = OfflineOperationException(
|
||||
message: 'Test message',
|
||||
code: 'test_code',
|
||||
retryable: true,
|
||||
);
|
||||
expect(e.message, 'Test message');
|
||||
expect(e.code, 'test_code');
|
||||
expect(e.retryable, true);
|
||||
});
|
||||
|
||||
test('code can be null', () {
|
||||
const e = OfflineOperationException(message: 'msg', retryable: false);
|
||||
expect(e.code, isNull);
|
||||
});
|
||||
|
||||
test('toString contains all fields', () {
|
||||
const e = OfflineOperationException(
|
||||
message: 'Test',
|
||||
code: 'code',
|
||||
retryable: true,
|
||||
);
|
||||
final str = e.toString();
|
||||
expect(str, contains('retryable: true'));
|
||||
expect(str, contains('code: code'));
|
||||
expect(str, contains('message: Test'));
|
||||
});
|
||||
|
||||
test('retryable false', () {
|
||||
const e = OfflineOperationException(
|
||||
message: 'fail',
|
||||
code: 'err',
|
||||
retryable: false,
|
||||
);
|
||||
expect(e.retryable, false);
|
||||
expect(e.toString(), contains('retryable: false'));
|
||||
});
|
||||
|
||||
test('implements Exception', () {
|
||||
const e = OfflineOperationException(message: 'm', retryable: true);
|
||||
expect(e, isA<Exception>());
|
||||
});
|
||||
});
|
||||
|
||||
group('OfflineErrorInterceptor', () {
|
||||
late OfflineErrorInterceptor interceptor;
|
||||
late _TestErrorHandler handler;
|
||||
|
||||
setUp(() {
|
||||
interceptor = OfflineErrorInterceptor();
|
||||
handler = _TestErrorHandler();
|
||||
});
|
||||
|
||||
test('wraps connectionTimeout as retryable OfflineOperationException', () {
|
||||
final err = DioException(
|
||||
type: DioExceptionType.connectionTimeout,
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
);
|
||||
interceptor.onError(err, handler);
|
||||
expect(handler.nextCalled, true);
|
||||
final wrappedError = handler.lastError!.error;
|
||||
expect(wrappedError, isA<OfflineOperationException>());
|
||||
expect((wrappedError as OfflineOperationException).retryable, true);
|
||||
expect(wrappedError.code, 'connection_error');
|
||||
});
|
||||
|
||||
test('wraps receiveTimeout as retryable', () {
|
||||
final err = DioException(
|
||||
type: DioExceptionType.receiveTimeout,
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
);
|
||||
interceptor.onError(err, handler);
|
||||
expect(handler.nextCalled, true);
|
||||
final wrappedError = handler.lastError!.error;
|
||||
expect(wrappedError, isA<OfflineOperationException>());
|
||||
expect((wrappedError as OfflineOperationException).retryable, true);
|
||||
});
|
||||
|
||||
test('wraps sendTimeout as retryable', () {
|
||||
final err = DioException(
|
||||
type: DioExceptionType.sendTimeout,
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
);
|
||||
interceptor.onError(err, handler);
|
||||
final wrappedError = handler.lastError!.error;
|
||||
expect(wrappedError, isA<OfflineOperationException>());
|
||||
expect((wrappedError as OfflineOperationException).retryable, true);
|
||||
});
|
||||
|
||||
test('wraps connectionError as retryable', () {
|
||||
final err = DioException(
|
||||
type: DioExceptionType.connectionError,
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
);
|
||||
interceptor.onError(err, handler);
|
||||
final wrappedError = handler.lastError!.error;
|
||||
expect(wrappedError, isA<OfflineOperationException>());
|
||||
expect((wrappedError as OfflineOperationException).retryable, true);
|
||||
});
|
||||
|
||||
test('wraps 5xx as retryable server error', () {
|
||||
final err = DioException(
|
||||
type: DioExceptionType.badResponse,
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
statusCode: 500,
|
||||
),
|
||||
);
|
||||
interceptor.onError(err, handler);
|
||||
final wrappedError = handler.lastError!.error;
|
||||
expect(wrappedError, isA<OfflineOperationException>());
|
||||
expect((wrappedError as OfflineOperationException).retryable, true);
|
||||
expect(wrappedError.code, 'server_error');
|
||||
});
|
||||
|
||||
test('wraps 503 as retryable server error', () {
|
||||
final err = DioException(
|
||||
type: DioExceptionType.badResponse,
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
statusCode: 503,
|
||||
),
|
||||
);
|
||||
interceptor.onError(err, handler);
|
||||
final wrappedError = handler.lastError!.error;
|
||||
expect(wrappedError, isA<OfflineOperationException>());
|
||||
expect((wrappedError as OfflineOperationException).retryable, true);
|
||||
});
|
||||
|
||||
test('parses 4xx error envelope with retryable=false', () {
|
||||
final err = DioException(
|
||||
type: DioExceptionType.badResponse,
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
statusCode: 400,
|
||||
data: {
|
||||
'message': 'Bad request',
|
||||
'error': {
|
||||
'code': 'validation_error',
|
||||
'message': 'Invalid field',
|
||||
'retryable': false,
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
interceptor.onError(err, handler);
|
||||
final wrappedError = handler.lastError!.error;
|
||||
expect(wrappedError, isA<OfflineOperationException>());
|
||||
expect((wrappedError as OfflineOperationException).retryable, false);
|
||||
expect(wrappedError.code, 'validation_error');
|
||||
expect(wrappedError.message, 'Invalid field');
|
||||
});
|
||||
|
||||
test('parses 4xx error envelope with retryable=true', () {
|
||||
final err = DioException(
|
||||
type: DioExceptionType.badResponse,
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
statusCode: 429,
|
||||
data: {
|
||||
'message': 'Rate limited',
|
||||
'error': {
|
||||
'code': 'rate_limit',
|
||||
'message': 'Too many requests',
|
||||
'retryable': true,
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
interceptor.onError(err, handler);
|
||||
final wrappedError = handler.lastError!.error;
|
||||
expect(wrappedError, isA<OfflineOperationException>());
|
||||
expect((wrappedError as OfflineOperationException).retryable, true);
|
||||
});
|
||||
|
||||
test('passes through 4xx without error envelope', () {
|
||||
final err = DioException(
|
||||
type: DioExceptionType.badResponse,
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
statusCode: 404,
|
||||
data: 'Not found',
|
||||
),
|
||||
);
|
||||
interceptor.onError(err, handler);
|
||||
expect(handler.nextCalled, true);
|
||||
// Should pass through as-is, no OfflineOperationException wrapping
|
||||
expect(handler.lastError!.error, isNot(isA<OfflineOperationException>()));
|
||||
});
|
||||
|
||||
test('passes through when response is null', () {
|
||||
final err = DioException(
|
||||
type: DioExceptionType.unknown,
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
);
|
||||
interceptor.onError(err, handler);
|
||||
expect(handler.nextCalled, true);
|
||||
});
|
||||
|
||||
test('parses envelope with missing retryable field (defaults to false)', () {
|
||||
final err = DioException(
|
||||
type: DioExceptionType.badResponse,
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
statusCode: 422,
|
||||
data: {
|
||||
'message': 'Unprocessable',
|
||||
'error': {
|
||||
'code': 'validation',
|
||||
'message': 'Field required',
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
interceptor.onError(err, handler);
|
||||
final wrappedError = handler.lastError!.error;
|
||||
expect(wrappedError, isA<OfflineOperationException>());
|
||||
expect((wrappedError as OfflineOperationException).retryable, false);
|
||||
});
|
||||
|
||||
test('handles 4xx with error field but malformed data', () {
|
||||
final err = DioException(
|
||||
type: DioExceptionType.badResponse,
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(path: '/test'),
|
||||
statusCode: 400,
|
||||
data: {
|
||||
'error': 'just a string, not a map',
|
||||
},
|
||||
),
|
||||
);
|
||||
interceptor.onError(err, handler);
|
||||
expect(handler.nextCalled, true);
|
||||
// Error caught, passed through as-is
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Custom test handler that captures the error without throwing
|
||||
class _TestErrorHandler extends ErrorInterceptorHandler {
|
||||
DioException? lastError;
|
||||
bool nextCalled = false;
|
||||
|
||||
@override
|
||||
void next(DioException err) {
|
||||
lastError = err;
|
||||
nextCalled = true;
|
||||
}
|
||||
}
|
||||
468
test/core/local_db/services/diary_sync_runner_test.dart
Normal file
468
test/core/local_db/services/diary_sync_runner_test.dart
Normal file
@@ -0,0 +1,468 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:eatvio_app/core/interceptors/offline_error_interceptor.dart';
|
||||
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
|
||||
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_state.dart';
|
||||
import 'package:eatvio_app/core/local_db/app_database.dart';
|
||||
import 'package:eatvio_app/core/local_db/diary_local_data_source.dart';
|
||||
import 'package:eatvio_app/core/local_db/services/diary_sync_runner.dart';
|
||||
import 'package:eatvio_app/features/diary/services/diary_service.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
@GenerateNiceMocks([
|
||||
MockSpec<DiaryLocalDataSource>(),
|
||||
MockSpec<DiaryService>(),
|
||||
MockSpec<InternetConnectionCubit>(),
|
||||
])
|
||||
import 'diary_sync_runner_test.mocks.dart';
|
||||
|
||||
void main() {
|
||||
late DiarySyncRunner runner;
|
||||
late MockDiaryLocalDataSource mockLocalDataSource;
|
||||
late MockDiaryService mockDiaryService;
|
||||
late MockInternetConnectionCubit mockConnectionCubit;
|
||||
|
||||
setUp(() {
|
||||
mockLocalDataSource = MockDiaryLocalDataSource();
|
||||
mockDiaryService = MockDiaryService();
|
||||
mockConnectionCubit = MockInternetConnectionCubit();
|
||||
|
||||
when(mockConnectionCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: false));
|
||||
|
||||
runner = DiarySyncRunner(
|
||||
localDataSource: mockLocalDataSource,
|
||||
diaryService: mockDiaryService,
|
||||
connectionCubit: mockConnectionCubit,
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
runner.dispose();
|
||||
});
|
||||
|
||||
group('DiarySyncRunner', () {
|
||||
test('start() подписывается на connectionCubit.stream', () {
|
||||
runner.start();
|
||||
verify(mockConnectionCubit.stream).called(1);
|
||||
});
|
||||
|
||||
test('start() запускает _processQueue если уже online', () async {
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: true));
|
||||
when(mockLocalDataSource.getPendingOps()).thenAnswer((_) async => []);
|
||||
when(mockLocalDataSource.clearSyncedOps()).thenAnswer((_) async {});
|
||||
|
||||
runner.start();
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
verify(mockLocalDataSource.getPendingOps()).called(1);
|
||||
});
|
||||
|
||||
test('dispose() отменяет подписку', () {
|
||||
runner.start();
|
||||
runner.dispose();
|
||||
// Не должно быть ошибок
|
||||
});
|
||||
|
||||
test('processQueue пропускает пустую очередь', () async {
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: true));
|
||||
when(mockLocalDataSource.getPendingOps()).thenAnswer((_) async => []);
|
||||
when(mockLocalDataSource.clearSyncedOps()).thenAnswer((_) async {});
|
||||
|
||||
runner.start();
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
verify(mockLocalDataSource.getPendingOps()).called(1);
|
||||
verifyNever(mockDiaryService.createDay(idempotencyKey: anyNamed('idempotencyKey'), body: anyNamed('body')));
|
||||
});
|
||||
|
||||
test('processQueue обрабатывает create_day операцию', () async {
|
||||
final op = PendingOperationsTableData(
|
||||
id: 'test-id',
|
||||
type: 'create_day',
|
||||
payload: jsonEncode({'day': '2024-01-01T00:00:00+00:00'}),
|
||||
status: 'PENDING',
|
||||
retryCount: 0,
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
localFilePath: null,
|
||||
);
|
||||
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: true));
|
||||
when(mockLocalDataSource.getPendingOps()).thenAnswer((_) async => [op]);
|
||||
when(mockLocalDataSource.markSynced(any)).thenAnswer((_) async {});
|
||||
when(mockLocalDataSource.clearSyncedOps()).thenAnswer((_) async {});
|
||||
when(mockDiaryService.createDay(
|
||||
idempotencyKey: anyNamed('idempotencyKey'),
|
||||
body: anyNamed('body'),
|
||||
)).thenAnswer((_) async {});
|
||||
|
||||
runner.start();
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
|
||||
verify(mockDiaryService.createDay(
|
||||
idempotencyKey: 'test-id',
|
||||
body: anyNamed('body'),
|
||||
)).called(1);
|
||||
verify(mockLocalDataSource.markSynced('test-id')).called(1);
|
||||
});
|
||||
|
||||
test('processQueue обрабатывает create_portion операцию', () async {
|
||||
final op = PendingOperationsTableData(
|
||||
id: 'portion-id',
|
||||
type: 'create_portion',
|
||||
payload: jsonEncode({
|
||||
'diary_id': 1,
|
||||
'meal_id': 2,
|
||||
'product_name': 'Test Product',
|
||||
'portion': '100g',
|
||||
'quantity': 1,
|
||||
'product_nutritional_value_id': 10,
|
||||
}),
|
||||
status: 'PENDING',
|
||||
retryCount: 0,
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
localFilePath: null,
|
||||
);
|
||||
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: true));
|
||||
when(mockLocalDataSource.getPendingOps()).thenAnswer((_) async => [op]);
|
||||
when(mockLocalDataSource.markSynced(any)).thenAnswer((_) async {});
|
||||
when(mockLocalDataSource.clearSyncedOps()).thenAnswer((_) async {});
|
||||
when(mockDiaryService.addProductToMealIdempotent(
|
||||
idempotencyKey: anyNamed('idempotencyKey'),
|
||||
diary: anyNamed('diary'),
|
||||
typeNo: anyNamed('typeNo'),
|
||||
productName: anyNamed('productName'),
|
||||
portion: anyNamed('portion'),
|
||||
quantity: anyNamed('quantity'),
|
||||
productNutritionalValueId: anyNamed('productNutritionalValueId'),
|
||||
)).thenAnswer((_) async {});
|
||||
|
||||
runner.start();
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
|
||||
verify(mockLocalDataSource.markSynced('portion-id')).called(1);
|
||||
});
|
||||
|
||||
test('processQueue обрабатывает delete_portion операцию', () async {
|
||||
final op = PendingOperationsTableData(
|
||||
id: 'del-id',
|
||||
type: 'delete_portion',
|
||||
payload: jsonEncode({'diary_id': 1, 'meal_id': 2, 'portion_id': 3}),
|
||||
status: 'PENDING',
|
||||
retryCount: 0,
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
localFilePath: null,
|
||||
);
|
||||
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: true));
|
||||
when(mockLocalDataSource.getPendingOps()).thenAnswer((_) async => [op]);
|
||||
when(mockLocalDataSource.markSynced(any)).thenAnswer((_) async {});
|
||||
when(mockLocalDataSource.clearSyncedOps()).thenAnswer((_) async {});
|
||||
when(mockDiaryService.deleteProductFromMealIdempotent(
|
||||
idempotencyKey: anyNamed('idempotencyKey'),
|
||||
diary: anyNamed('diary'),
|
||||
typeNo: anyNamed('typeNo'),
|
||||
portionModel: anyNamed('portionModel'),
|
||||
)).thenAnswer((_) async {});
|
||||
|
||||
runner.start();
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
|
||||
verify(mockLocalDataSource.markSynced('del-id')).called(1);
|
||||
});
|
||||
|
||||
test('processQueue обрабатывает fill_satiety операцию', () async {
|
||||
final op = PendingOperationsTableData(
|
||||
id: 'sat-id',
|
||||
type: 'fill_satiety',
|
||||
payload: jsonEncode({'diary_id': 1, 'type_no': 2, 'satiety': 5}),
|
||||
status: 'PENDING',
|
||||
retryCount: 0,
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
localFilePath: null,
|
||||
);
|
||||
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: true));
|
||||
when(mockLocalDataSource.getPendingOps()).thenAnswer((_) async => [op]);
|
||||
when(mockLocalDataSource.markSynced(any)).thenAnswer((_) async {});
|
||||
when(mockLocalDataSource.clearSyncedOps()).thenAnswer((_) async {});
|
||||
when(mockDiaryService.fillSatiety(diary: anyNamed('diary'), typeNo: anyNamed('typeNo'), satiety: anyNamed('satiety'))).thenAnswer((_) async {});
|
||||
|
||||
runner.start();
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
|
||||
verify(mockLocalDataSource.markSynced('sat-id')).called(1);
|
||||
});
|
||||
|
||||
test('processQueue обрабатывает fill_mood операцию', () async {
|
||||
final op = PendingOperationsTableData(
|
||||
id: 'mood-id',
|
||||
type: 'fill_mood',
|
||||
payload: jsonEncode({'diary_id': 1, 'type_no': 2, 'mood': 3}),
|
||||
status: 'PENDING',
|
||||
retryCount: 0,
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
localFilePath: null,
|
||||
);
|
||||
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: true));
|
||||
when(mockLocalDataSource.getPendingOps()).thenAnswer((_) async => [op]);
|
||||
when(mockLocalDataSource.markSynced(any)).thenAnswer((_) async {});
|
||||
when(mockLocalDataSource.clearSyncedOps()).thenAnswer((_) async {});
|
||||
when(mockDiaryService.fillMood(diary: anyNamed('diary'), typeNo: anyNamed('typeNo'), mood: anyNamed('mood'))).thenAnswer((_) async {});
|
||||
|
||||
runner.start();
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
|
||||
verify(mockLocalDataSource.markSynced('mood-id')).called(1);
|
||||
});
|
||||
|
||||
test('processQueue обрабатывает fill_time операцию', () async {
|
||||
final op = PendingOperationsTableData(
|
||||
id: 'time-id',
|
||||
type: 'fill_time',
|
||||
payload: jsonEncode({'diary_id': 1, 'type_no': 2, 'time': '12:00'}),
|
||||
status: 'PENDING',
|
||||
retryCount: 0,
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
localFilePath: null,
|
||||
);
|
||||
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: true));
|
||||
when(mockLocalDataSource.getPendingOps()).thenAnswer((_) async => [op]);
|
||||
when(mockLocalDataSource.markSynced(any)).thenAnswer((_) async {});
|
||||
when(mockLocalDataSource.clearSyncedOps()).thenAnswer((_) async {});
|
||||
when(mockDiaryService.fillTime(diary: anyNamed('diary'), typeNo: anyNamed('typeNo'), time: anyNamed('time'))).thenAnswer((_) async {});
|
||||
|
||||
runner.start();
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
|
||||
verify(mockLocalDataSource.markSynced('time-id')).called(1);
|
||||
});
|
||||
|
||||
test('processQueue обрабатывает fill_wakeup операцию', () async {
|
||||
final op = PendingOperationsTableData(
|
||||
id: 'wake-id',
|
||||
type: 'fill_wakeup',
|
||||
payload: jsonEncode({'diary_id': 1, 'wakeup': '07:00'}),
|
||||
status: 'PENDING',
|
||||
retryCount: 0,
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
localFilePath: null,
|
||||
);
|
||||
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: true));
|
||||
when(mockLocalDataSource.getPendingOps()).thenAnswer((_) async => [op]);
|
||||
when(mockLocalDataSource.markSynced(any)).thenAnswer((_) async {});
|
||||
when(mockLocalDataSource.clearSyncedOps()).thenAnswer((_) async {});
|
||||
when(mockDiaryService.fillWakeup(diary: anyNamed('diary'), wakeUp: anyNamed('wakeUp'))).thenAnswer((_) async {});
|
||||
|
||||
runner.start();
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
|
||||
verify(mockLocalDataSource.markSynced('wake-id')).called(1);
|
||||
});
|
||||
|
||||
test('processQueue обрабатывает fill_retire операцию', () async {
|
||||
final op = PendingOperationsTableData(
|
||||
id: 'retire-id',
|
||||
type: 'fill_retire',
|
||||
payload: jsonEncode({'diary_id': 1, 'retire': '23:00'}),
|
||||
status: 'PENDING',
|
||||
retryCount: 0,
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
localFilePath: null,
|
||||
);
|
||||
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: true));
|
||||
when(mockLocalDataSource.getPendingOps()).thenAnswer((_) async => [op]);
|
||||
when(mockLocalDataSource.markSynced(any)).thenAnswer((_) async {});
|
||||
when(mockLocalDataSource.clearSyncedOps()).thenAnswer((_) async {});
|
||||
when(mockDiaryService.fillRetire(diary: anyNamed('diary'), retire: anyNamed('retire'))).thenAnswer((_) async {});
|
||||
|
||||
runner.start();
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
|
||||
verify(mockLocalDataSource.markSynced('retire-id')).called(1);
|
||||
});
|
||||
|
||||
test('processQueue обрабатывает delete_photo операцию', () async {
|
||||
final op = PendingOperationsTableData(
|
||||
id: 'photo-del-id',
|
||||
type: 'delete_photo',
|
||||
payload: jsonEncode({'diary_id': 1, 'type_no': 2, 'media_id': 10}),
|
||||
status: 'PENDING',
|
||||
retryCount: 0,
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
localFilePath: null,
|
||||
);
|
||||
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: true));
|
||||
when(mockLocalDataSource.getPendingOps()).thenAnswer((_) async => [op]);
|
||||
when(mockLocalDataSource.markSynced(any)).thenAnswer((_) async {});
|
||||
when(mockLocalDataSource.clearSyncedOps()).thenAnswer((_) async {});
|
||||
when(mockDiaryService.deletePhotoOfMeal(
|
||||
diaryId: anyNamed('diaryId'),
|
||||
typeNo: anyNamed('typeNo'),
|
||||
userAttachment: anyNamed('userAttachment'),
|
||||
)).thenAnswer((_) async {});
|
||||
|
||||
runner.start();
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
|
||||
verify(mockLocalDataSource.markSynced('photo-del-id')).called(1);
|
||||
});
|
||||
|
||||
test('processQueue помечает операцию как failed при maxRetries', () async {
|
||||
final op = PendingOperationsTableData(
|
||||
id: 'max-retry-id',
|
||||
type: 'create_day',
|
||||
payload: jsonEncode({'day': '2024-01-01'}),
|
||||
status: 'PENDING',
|
||||
retryCount: 5, // >= _maxRetries
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
localFilePath: null,
|
||||
);
|
||||
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: true));
|
||||
when(mockLocalDataSource.getPendingOps()).thenAnswer((_) async => [op]);
|
||||
when(mockLocalDataSource.markFailed(any)).thenAnswer((_) async {});
|
||||
when(mockLocalDataSource.clearSyncedOps()).thenAnswer((_) async {});
|
||||
|
||||
runner.start();
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
|
||||
verify(mockLocalDataSource.markFailed('max-retry-id')).called(1);
|
||||
});
|
||||
|
||||
test('processQueue увеличивает retryCount при OfflineOperationException retryable', () async {
|
||||
final op = PendingOperationsTableData(
|
||||
id: 'retry-id',
|
||||
type: 'create_day',
|
||||
payload: jsonEncode({'day': '2024-01-01T00:00:00+00:00'}),
|
||||
status: 'PENDING',
|
||||
retryCount: 0,
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
localFilePath: null,
|
||||
);
|
||||
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: true));
|
||||
when(mockLocalDataSource.getPendingOps()).thenAnswer((_) async => [op]);
|
||||
when(mockLocalDataSource.incrementRetry(any)).thenAnswer((_) async {});
|
||||
when(mockLocalDataSource.clearSyncedOps()).thenAnswer((_) async {});
|
||||
when(mockDiaryService.createDay(
|
||||
idempotencyKey: anyNamed('idempotencyKey'),
|
||||
body: anyNamed('body'),
|
||||
)).thenThrow(const OfflineOperationException(
|
||||
message: 'Server error',
|
||||
code: 'server_error',
|
||||
retryable: true,
|
||||
));
|
||||
|
||||
runner.start();
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
verify(mockLocalDataSource.incrementRetry('retry-id')).called(1);
|
||||
});
|
||||
|
||||
test('processQueue помечает failed при OfflineOperationException not retryable', () async {
|
||||
final op = PendingOperationsTableData(
|
||||
id: 'no-retry-id',
|
||||
type: 'create_day',
|
||||
payload: jsonEncode({'day': '2024-01-01T00:00:00+00:00'}),
|
||||
status: 'PENDING',
|
||||
retryCount: 0,
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
localFilePath: null,
|
||||
);
|
||||
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: true));
|
||||
when(mockLocalDataSource.getPendingOps()).thenAnswer((_) async => [op]);
|
||||
when(mockLocalDataSource.markFailed(any)).thenAnswer((_) async {});
|
||||
when(mockLocalDataSource.clearSyncedOps()).thenAnswer((_) async {});
|
||||
when(mockDiaryService.createDay(
|
||||
idempotencyKey: anyNamed('idempotencyKey'),
|
||||
body: anyNamed('body'),
|
||||
)).thenThrow(const OfflineOperationException(
|
||||
message: 'Not found',
|
||||
code: 'not_found',
|
||||
retryable: false,
|
||||
));
|
||||
|
||||
runner.start();
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
|
||||
verify(mockLocalDataSource.markFailed('no-retry-id')).called(1);
|
||||
});
|
||||
|
||||
test('processQueue incrementRetry при неизвестной ошибке', () async {
|
||||
final op = PendingOperationsTableData(
|
||||
id: 'unknown-err-id',
|
||||
type: 'create_day',
|
||||
payload: jsonEncode({'day': '2024-01-01T00:00:00+00:00'}),
|
||||
status: 'PENDING',
|
||||
retryCount: 0,
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
localFilePath: null,
|
||||
);
|
||||
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: true));
|
||||
when(mockLocalDataSource.getPendingOps()).thenAnswer((_) async => [op]);
|
||||
when(mockLocalDataSource.incrementRetry(any)).thenAnswer((_) async {});
|
||||
when(mockLocalDataSource.clearSyncedOps()).thenAnswer((_) async {});
|
||||
when(mockDiaryService.createDay(
|
||||
idempotencyKey: anyNamed('idempotencyKey'),
|
||||
body: anyNamed('body'),
|
||||
)).thenThrow(Exception('Unknown error'));
|
||||
|
||||
runner.start();
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
|
||||
verify(mockLocalDataSource.incrementRetry('unknown-err-id')).called(1);
|
||||
});
|
||||
|
||||
test('processQueue останавливается если интернет пропал', () async {
|
||||
final op1 = PendingOperationsTableData(
|
||||
id: 'op1',
|
||||
type: 'create_day',
|
||||
payload: jsonEncode({'day': '2024-01-01T00:00:00+00:00'}),
|
||||
status: 'PENDING',
|
||||
retryCount: 0,
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
localFilePath: null,
|
||||
);
|
||||
final op2 = PendingOperationsTableData(
|
||||
id: 'op2',
|
||||
type: 'create_day',
|
||||
payload: jsonEncode({'day': '2024-01-02T00:00:00+00:00'}),
|
||||
status: 'PENDING',
|
||||
retryCount: 0,
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
localFilePath: null,
|
||||
);
|
||||
|
||||
var callCount = 0;
|
||||
when(mockConnectionCubit.state).thenAnswer((_) {
|
||||
callCount++;
|
||||
// После первого вызова — "офлайн"
|
||||
return InternetConnectionState(isInternetAvailable: callCount <= 2);
|
||||
});
|
||||
when(mockLocalDataSource.getPendingOps()).thenAnswer((_) async => [op1, op2]);
|
||||
when(mockLocalDataSource.markSynced(any)).thenAnswer((_) async {});
|
||||
when(mockLocalDataSource.clearSyncedOps()).thenAnswer((_) async {});
|
||||
when(mockDiaryService.createDay(
|
||||
idempotencyKey: anyNamed('idempotencyKey'),
|
||||
body: anyNamed('body'),
|
||||
)).thenAnswer((_) async {});
|
||||
|
||||
runner.start();
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
// Должна быть обработана только одна операция, вторая пропущена
|
||||
verify(mockLocalDataSource.clearSyncedOps()).called(greaterThanOrEqualTo(1));
|
||||
});
|
||||
});
|
||||
}
|
||||
205
test/core/local_db/services/glossary_models_test.dart
Normal file
205
test/core/local_db/services/glossary_models_test.dart
Normal file
@@ -0,0 +1,205 @@
|
||||
import 'package:eatvio_app/core/local_db/services/glossary_dump_response.dart';
|
||||
import 'package:eatvio_app/core/local_db/services/glossary_meta_model.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('GlossaryDumpItem', () {
|
||||
test('fromJson parses all fields', () {
|
||||
final json = {
|
||||
'id': 1,
|
||||
'name': 'Яблоко',
|
||||
'product_group_id': 5,
|
||||
'energy_on_100gr': 52.0,
|
||||
'proteins_on_100gr': 0.3,
|
||||
'fats_on_100gr': 0.2,
|
||||
'carbohydrates_on_100gr': 14.0,
|
||||
'portion_mass_gr': 150.0,
|
||||
'energy_on_portion': 78.0,
|
||||
'proteins_on_portion': 0.45,
|
||||
'fats_on_portion': 0.3,
|
||||
'carbohydrates_on_portion': 21.0,
|
||||
'updated_at': '2024-01-01T12:00:00Z',
|
||||
};
|
||||
final item = GlossaryDumpItem.fromJson(json);
|
||||
expect(item.id, 1);
|
||||
expect(item.name, 'Яблоко');
|
||||
expect(item.productGroupId, 5);
|
||||
expect(item.energyOn100gr, 52.0);
|
||||
expect(item.proteinsOn100gr, 0.3);
|
||||
expect(item.fatsOn100gr, 0.2);
|
||||
expect(item.carbohydratesOn100gr, 14.0);
|
||||
expect(item.portionMassGr, 150.0);
|
||||
expect(item.energyOnPortion, 78.0);
|
||||
expect(item.proteinsOnPortion, 0.45);
|
||||
expect(item.fatsOnPortion, 0.3);
|
||||
expect(item.carbohydratesOnPortion, 21.0);
|
||||
expect(item.updatedAt, '2024-01-01T12:00:00Z');
|
||||
});
|
||||
|
||||
test('fromJson uses default values for portion fields', () {
|
||||
final json = {
|
||||
'id': 2,
|
||||
'name': 'Banana',
|
||||
'product_group_id': 5,
|
||||
'energy_on_100gr': 89.0,
|
||||
'proteins_on_100gr': 1.1,
|
||||
'fats_on_100gr': 0.3,
|
||||
'carbohydrates_on_100gr': 22.8,
|
||||
'updated_at': '2024-01-01T12:00:00Z',
|
||||
};
|
||||
final item = GlossaryDumpItem.fromJson(json);
|
||||
expect(item.portionMassGr, 0.0);
|
||||
expect(item.energyOnPortion, 0.0);
|
||||
expect(item.proteinsOnPortion, 0.0);
|
||||
expect(item.fatsOnPortion, 0.0);
|
||||
expect(item.carbohydratesOnPortion, 0.0);
|
||||
});
|
||||
|
||||
test('toJson serializes all fields', () {
|
||||
const item = GlossaryDumpItem(
|
||||
id: 1,
|
||||
name: 'Apple',
|
||||
productGroupId: 5,
|
||||
energyOn100gr: 52.0,
|
||||
proteinsOn100gr: 0.3,
|
||||
fatsOn100gr: 0.2,
|
||||
carbohydratesOn100gr: 14.0,
|
||||
portionMassGr: 150.0,
|
||||
energyOnPortion: 78.0,
|
||||
updatedAt: '2024-01-01T12:00:00Z',
|
||||
);
|
||||
final json = item.toJson();
|
||||
expect(json['id'], 1);
|
||||
expect(json['name'], 'Apple');
|
||||
expect(json['product_group_id'], 5);
|
||||
expect(json['energy_on_100gr'], 52.0);
|
||||
expect(json['portion_mass_gr'], 150.0);
|
||||
});
|
||||
|
||||
test('constructor works with default values', () {
|
||||
const item = GlossaryDumpItem(
|
||||
id: 1,
|
||||
name: 'Test',
|
||||
productGroupId: 1,
|
||||
energyOn100gr: 100.0,
|
||||
proteinsOn100gr: 10.0,
|
||||
fatsOn100gr: 5.0,
|
||||
carbohydratesOn100gr: 20.0,
|
||||
updatedAt: '2024-01-01',
|
||||
);
|
||||
expect(item.portionMassGr, 0.0);
|
||||
expect(item.energyOnPortion, 0.0);
|
||||
});
|
||||
});
|
||||
|
||||
group('GlossaryDumpResponse', () {
|
||||
test('fromJson parses response', () {
|
||||
final json = {
|
||||
'total': 2,
|
||||
'updated_at': '2024-01-01T12:00:00Z',
|
||||
'data': [
|
||||
{
|
||||
'id': 1,
|
||||
'name': 'Apple',
|
||||
'product_group_id': 5,
|
||||
'energy_on_100gr': 52.0,
|
||||
'proteins_on_100gr': 0.3,
|
||||
'fats_on_100gr': 0.2,
|
||||
'carbohydrates_on_100gr': 14.0,
|
||||
'updated_at': '2024-01-01T12:00:00Z',
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'name': 'Banana',
|
||||
'product_group_id': 5,
|
||||
'energy_on_100gr': 89.0,
|
||||
'proteins_on_100gr': 1.1,
|
||||
'fats_on_100gr': 0.3,
|
||||
'carbohydrates_on_100gr': 22.8,
|
||||
'updated_at': '2024-01-01T12:00:00Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
final response = GlossaryDumpResponse.fromJson(json);
|
||||
expect(response.total, 2);
|
||||
expect(response.updatedAt, '2024-01-01T12:00:00Z');
|
||||
expect(response.data.length, 2);
|
||||
expect(response.data[0].name, 'Apple');
|
||||
expect(response.data[1].name, 'Banana');
|
||||
});
|
||||
|
||||
test('toJson serializes response', () {
|
||||
const response = GlossaryDumpResponse(
|
||||
total: 1,
|
||||
updatedAt: '2024-01-01T12:00:00Z',
|
||||
data: [
|
||||
GlossaryDumpItem(
|
||||
id: 1,
|
||||
name: 'Apple',
|
||||
productGroupId: 5,
|
||||
energyOn100gr: 52.0,
|
||||
proteinsOn100gr: 0.3,
|
||||
fatsOn100gr: 0.2,
|
||||
carbohydratesOn100gr: 14.0,
|
||||
updatedAt: '2024-01-01T12:00:00Z',
|
||||
),
|
||||
],
|
||||
);
|
||||
final json = response.toJson();
|
||||
expect(json['total'], 1);
|
||||
expect(json['updated_at'], '2024-01-01T12:00:00Z');
|
||||
expect(json['data'], isA<List>());
|
||||
expect((json['data'] as List).length, 1);
|
||||
});
|
||||
|
||||
test('handles empty data list', () {
|
||||
final json = {
|
||||
'total': 0,
|
||||
'updated_at': '2024-01-01T12:00:00Z',
|
||||
'data': [],
|
||||
};
|
||||
final response = GlossaryDumpResponse.fromJson(json);
|
||||
expect(response.total, 0);
|
||||
expect(response.data, isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('GlossaryMetaModel', () {
|
||||
test('fromJson parses all fields', () {
|
||||
final json = {
|
||||
'total_count': 200000,
|
||||
'last_updated_at': '2024-06-15T10:30:00Z',
|
||||
'checksum': 'abc123def456',
|
||||
};
|
||||
final meta = GlossaryMetaModel.fromJson(json);
|
||||
expect(meta.totalCount, 200000);
|
||||
expect(meta.lastUpdatedAt, '2024-06-15T10:30:00Z');
|
||||
expect(meta.checksum, 'abc123def456');
|
||||
});
|
||||
|
||||
test('toJson serializes all fields', () {
|
||||
const meta = GlossaryMetaModel(
|
||||
totalCount: 100,
|
||||
lastUpdatedAt: '2024-01-01T00:00:00Z',
|
||||
checksum: 'hash123',
|
||||
);
|
||||
final json = meta.toJson();
|
||||
expect(json['total_count'], 100);
|
||||
expect(json['last_updated_at'], '2024-01-01T00:00:00Z');
|
||||
expect(json['checksum'], 'hash123');
|
||||
});
|
||||
|
||||
test('roundtrip fromJson -> toJson', () {
|
||||
const original = GlossaryMetaModel(
|
||||
totalCount: 50,
|
||||
lastUpdatedAt: '2024-06-01T12:00:00Z',
|
||||
checksum: 'xyz',
|
||||
);
|
||||
final json = original.toJson();
|
||||
final restored = GlossaryMetaModel.fromJson(json);
|
||||
expect(restored.totalCount, original.totalCount);
|
||||
expect(restored.lastUpdatedAt, original.lastUpdatedAt);
|
||||
expect(restored.checksum, original.checksum);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,270 +1,125 @@
|
||||
import 'package:eatvio_app/core/constants/app_constants.dart';
|
||||
import 'package:eatvio_app/core/utils/validation_utils.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('ValidationUtils', () {
|
||||
group('isValidEmail', () {
|
||||
test('returns true for valid email', () {
|
||||
expect(ValidationUtils.isValidEmail('test@example.com'), isTrue);
|
||||
expect(ValidationUtils.isValidEmail('user.name@domain.co.uk'), isTrue);
|
||||
});
|
||||
|
||||
test('returns false for invalid email', () {
|
||||
expect(ValidationUtils.isValidEmail(''), isFalse);
|
||||
expect(ValidationUtils.isValidEmail('invalid'), isFalse);
|
||||
expect(ValidationUtils.isValidEmail('@example.com'), isFalse);
|
||||
expect(ValidationUtils.isValidEmail('test@'), isFalse);
|
||||
});
|
||||
test('empty → false', () => expect(ValidationUtils.isValidEmail(''), false));
|
||||
test('valid email', () => expect(ValidationUtils.isValidEmail('test@test.com'), true));
|
||||
test('invalid email', () => expect(ValidationUtils.isValidEmail('not-an-email'), false));
|
||||
test('email with dots', () => expect(ValidationUtils.isValidEmail('user.name@domain.co.uk'), true));
|
||||
});
|
||||
|
||||
group('isValidPhone', () {
|
||||
test('returns true for valid phone', () {
|
||||
expect(ValidationUtils.isValidPhone('+79991234567'), isTrue);
|
||||
});
|
||||
|
||||
test('returns false for invalid phone', () {
|
||||
expect(ValidationUtils.isValidPhone(''), isFalse);
|
||||
expect(ValidationUtils.isValidPhone('abc'), isFalse);
|
||||
expect(ValidationUtils.isValidPhone('0123456789'), isFalse); // Starts with 0
|
||||
});
|
||||
test('empty → false', () => expect(ValidationUtils.isValidPhone(''), false));
|
||||
test('valid phone +7', () => expect(ValidationUtils.isValidPhone('+79001234567'), true));
|
||||
test('valid phone 8', () => expect(ValidationUtils.isValidPhone('89001234567'), true));
|
||||
});
|
||||
|
||||
group('normalizePhone', () {
|
||||
test('normalizes phone starting with +7', () {
|
||||
expect(ValidationUtils.normalizePhone('+7 (915) 062 61 51'), '+79150626151');
|
||||
});
|
||||
|
||||
test('normalizes phone starting with 8', () {
|
||||
expect(ValidationUtils.normalizePhone('89150626151'), '+79150626151');
|
||||
});
|
||||
|
||||
test('normalizes 10-digit phone', () {
|
||||
expect(ValidationUtils.normalizePhone('9150626151'), '+79150626151');
|
||||
});
|
||||
|
||||
test('returns empty string for empty input', () {
|
||||
expect(ValidationUtils.normalizePhone(''), '');
|
||||
});
|
||||
|
||||
test('returns empty string for input with no digits', () {
|
||||
expect(ValidationUtils.normalizePhone('abc'), '');
|
||||
test('empty → empty', () => expect(ValidationUtils.normalizePhone(''), ''));
|
||||
test('starts with 8 → +7', () => expect(ValidationUtils.normalizePhone('89001234567'), '+79001234567'));
|
||||
test('10 digits → +7 prefix', () => expect(ValidationUtils.normalizePhone('9001234567'), '+79001234567'));
|
||||
test('already +7', () => expect(ValidationUtils.normalizePhone('+79001234567'), '+79001234567'));
|
||||
test('with spaces and dashes', () {
|
||||
final result = ValidationUtils.normalizePhone('+7 (900) 123-45-67');
|
||||
expect(result, '+79001234567');
|
||||
});
|
||||
test('only non-digit chars → empty', () => expect(ValidationUtils.normalizePhone('abc'), ''));
|
||||
});
|
||||
|
||||
group('isValidUrl', () {
|
||||
test('returns true for valid URL', () {
|
||||
expect(ValidationUtils.isValidUrl('https://example.com'), isTrue);
|
||||
expect(ValidationUtils.isValidUrl('http://example.com'), isTrue);
|
||||
});
|
||||
|
||||
test('returns false for invalid URL', () {
|
||||
expect(ValidationUtils.isValidUrl(''), isFalse);
|
||||
expect(ValidationUtils.isValidUrl('not a url'), isFalse);
|
||||
});
|
||||
test('empty → false', () => expect(ValidationUtils.isValidUrl(''), false));
|
||||
test('valid URL', () => expect(ValidationUtils.isValidUrl('https://example.com'), true));
|
||||
test('invalid URL', () => expect(ValidationUtils.isValidUrl('not a url'), false));
|
||||
});
|
||||
|
||||
group('isValidTextLength', () {
|
||||
test('returns true for text within max length', () {
|
||||
expect(ValidationUtils.isValidTextLength('short text'), isTrue);
|
||||
expect(ValidationUtils.isValidTextLength('text', maxLength: 10), isTrue);
|
||||
});
|
||||
|
||||
test('returns false for text exceeding max length', () {
|
||||
final longText = 'a' * (AppConstants.maxTextLength + 1);
|
||||
expect(ValidationUtils.isValidTextLength(longText), isFalse);
|
||||
expect(ValidationUtils.isValidTextLength('very long text', maxLength: 5), isFalse);
|
||||
test('short text → true', () => expect(ValidationUtils.isValidTextLength('Hello'), true));
|
||||
test('with custom maxLength', () {
|
||||
expect(ValidationUtils.isValidTextLength('Hi', maxLength: 5), true);
|
||||
expect(ValidationUtils.isValidTextLength('Hello World', maxLength: 5), false);
|
||||
});
|
||||
});
|
||||
|
||||
group('isValidTitleLength', () {
|
||||
test('returns true for valid title', () {
|
||||
expect(ValidationUtils.isValidTitleLength('Valid Title'), isTrue);
|
||||
expect(ValidationUtils.isValidTitleLength('A'), isTrue);
|
||||
});
|
||||
|
||||
test('returns false for invalid title', () {
|
||||
expect(ValidationUtils.isValidTitleLength(''), isFalse);
|
||||
final longTitle = 'a' * (AppConstants.maxTitleLength + 1);
|
||||
expect(ValidationUtils.isValidTitleLength(longTitle), isFalse);
|
||||
});
|
||||
test('empty → false', () => expect(ValidationUtils.isValidTitleLength(''), false));
|
||||
test('non-empty → true', () => expect(ValidationUtils.isValidTitleLength('Title'), true));
|
||||
});
|
||||
|
||||
group('isValidDescriptionLength', () {
|
||||
test('returns true for valid description', () {
|
||||
expect(ValidationUtils.isValidDescriptionLength('Valid description'), isTrue);
|
||||
expect(ValidationUtils.isValidDescriptionLength(''), isTrue);
|
||||
});
|
||||
|
||||
test('returns false for description exceeding max length', () {
|
||||
final longDescription = 'a' * (AppConstants.maxDescriptionLength + 1);
|
||||
expect(ValidationUtils.isValidDescriptionLength(longDescription), isFalse);
|
||||
});
|
||||
test('short description → true', () => expect(ValidationUtils.isValidDescriptionLength('Desc'), true));
|
||||
});
|
||||
|
||||
group('isValidPassword', () {
|
||||
test('returns true for valid password', () {
|
||||
expect(ValidationUtils.isValidPassword('password123'), isTrue);
|
||||
expect(ValidationUtils.isValidPassword('Pass1234'), isTrue);
|
||||
});
|
||||
|
||||
test('returns false for invalid password', () {
|
||||
expect(ValidationUtils.isValidPassword(''), isFalse);
|
||||
expect(ValidationUtils.isValidPassword('short'), isFalse);
|
||||
expect(ValidationUtils.isValidPassword('onlyletters'), isFalse);
|
||||
expect(ValidationUtils.isValidPassword('12345678'), isFalse);
|
||||
});
|
||||
test('too short → false', () => expect(ValidationUtils.isValidPassword('Ab1'), false));
|
||||
test('no digits → false', () => expect(ValidationUtils.isValidPassword('abcdefgh'), false));
|
||||
test('no letters → false', () => expect(ValidationUtils.isValidPassword('12345678'), false));
|
||||
test('valid password', () => expect(ValidationUtils.isValidPassword('abcdef12'), true));
|
||||
});
|
||||
|
||||
group('isValidUsername', () {
|
||||
test('returns true for valid username', () {
|
||||
expect(ValidationUtils.isValidUsername('user123'), isTrue);
|
||||
expect(ValidationUtils.isValidUsername('user_name'), isTrue);
|
||||
expect(ValidationUtils.isValidUsername('abc'), isTrue);
|
||||
});
|
||||
|
||||
test('returns false for invalid username', () {
|
||||
expect(ValidationUtils.isValidUsername(''), isFalse);
|
||||
expect(ValidationUtils.isValidUsername('ab'), isFalse);
|
||||
expect(ValidationUtils.isValidUsername('a' * 21), isFalse);
|
||||
expect(ValidationUtils.isValidUsername('user-name'), isFalse);
|
||||
expect(ValidationUtils.isValidUsername('user name'), isFalse);
|
||||
});
|
||||
test('too short → false', () => expect(ValidationUtils.isValidUsername('ab'), false));
|
||||
test('too long → false', () => expect(ValidationUtils.isValidUsername('a' * 21), false));
|
||||
test('valid username', () => expect(ValidationUtils.isValidUsername('user_123'), true));
|
||||
test('special chars → false', () => expect(ValidationUtils.isValidUsername('user@name'), false));
|
||||
});
|
||||
|
||||
group('getValidationErrorMessage', () {
|
||||
test('returns error for null value', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage(null, ValidationType.email), 'Поле не может быть пустым');
|
||||
expect(ValidationUtils.getValidationErrorMessage(null, ValidationType.phone), 'Поле не может быть пустым');
|
||||
expect(ValidationUtils.getValidationErrorMessage(null, ValidationType.url), 'Поле не может быть пустым');
|
||||
test('null value → error', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage(null, ValidationType.email), isNotNull);
|
||||
});
|
||||
|
||||
test('returns error for empty value', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('', ValidationType.email), 'Поле не может быть пустым');
|
||||
expect(ValidationUtils.getValidationErrorMessage('', ValidationType.password), 'Поле не может быть пустым');
|
||||
expect(ValidationUtils.getValidationErrorMessage('', ValidationType.username), 'Поле не может быть пустым');
|
||||
test('empty value → error', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('', ValidationType.email), isNotNull);
|
||||
});
|
||||
|
||||
test('returns null for valid email', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('test@example.com', ValidationType.email), isNull);
|
||||
test('valid email → null', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('test@test.com', ValidationType.email), isNull);
|
||||
});
|
||||
|
||||
test('returns error for invalid email', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('invalid', ValidationType.email), 'Введите корректный email');
|
||||
test('invalid email → error msg', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('not-email', ValidationType.email), isNotNull);
|
||||
});
|
||||
|
||||
test('returns null for valid phone', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('+79991234567', ValidationType.phone), isNull);
|
||||
test('valid phone → null', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('+79001234567', ValidationType.phone), isNull);
|
||||
});
|
||||
|
||||
test('returns error for invalid phone', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('abc', ValidationType.phone), 'Введите корректный номер телефона');
|
||||
test('invalid phone → error msg', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('abc', ValidationType.phone), isNotNull);
|
||||
});
|
||||
|
||||
test('returns null for valid URL', () {
|
||||
test('valid url → null', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('https://example.com', ValidationType.url), isNull);
|
||||
});
|
||||
|
||||
test('returns error for invalid URL', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('not a url', ValidationType.url), 'Введите корректный URL');
|
||||
test('valid password → null', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('password1', ValidationType.password), isNull);
|
||||
});
|
||||
|
||||
test('returns null for valid password', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('password123', ValidationType.password), isNull);
|
||||
test('invalid password → error msg', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('short', ValidationType.password), isNotNull);
|
||||
});
|
||||
|
||||
test('returns error for invalid password', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('short', ValidationType.password), 'Пароль должен содержать минимум 8 символов, включая буквы и цифры');
|
||||
test('valid username → null', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('user_123', ValidationType.username), isNull);
|
||||
});
|
||||
|
||||
test('returns null for valid username', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('user123', ValidationType.username), isNull);
|
||||
test('invalid username → error msg', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('ab', ValidationType.username), isNotNull);
|
||||
});
|
||||
|
||||
test('returns error for invalid username', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('ab', ValidationType.username), 'Имя пользователя должно содержать от 3 до 20 символов (буквы, цифры, подчеркивания)');
|
||||
test('valid title → null', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('Title', ValidationType.title), isNull);
|
||||
});
|
||||
|
||||
test('returns null for valid title', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('Valid Title', ValidationType.title), isNull);
|
||||
test('valid description → null', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('Description', ValidationType.description), isNull);
|
||||
});
|
||||
|
||||
test('returns error for invalid title', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('', ValidationType.title), 'Поле не может быть пустым');
|
||||
final longTitle = 'a' * (AppConstants.maxTitleLength + 1);
|
||||
expect(ValidationUtils.getValidationErrorMessage(longTitle, ValidationType.title), 'Заголовок не может быть пустым и должен содержать не более ${AppConstants.maxTitleLength} символов');
|
||||
});
|
||||
|
||||
test('returns null for valid description', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('Valid description', ValidationType.description), isNull);
|
||||
expect(ValidationUtils.getValidationErrorMessage('', ValidationType.description), 'Поле не может быть пустым');
|
||||
});
|
||||
|
||||
test('returns error for invalid description', () {
|
||||
final longDescription = 'a' * (AppConstants.maxDescriptionLength + 1);
|
||||
expect(ValidationUtils.getValidationErrorMessage(longDescription, ValidationType.description), 'Описание не может содержать более ${AppConstants.maxDescriptionLength} символов');
|
||||
});
|
||||
|
||||
test('returns null for valid text', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('Valid text', ValidationType.text), isNull);
|
||||
});
|
||||
|
||||
test('returns error for invalid text', () {
|
||||
final longText = 'a' * (AppConstants.maxTextLength + 1);
|
||||
expect(ValidationUtils.getValidationErrorMessage(longText, ValidationType.text), 'Текст не может содержать более ${AppConstants.maxTextLength} символов');
|
||||
});
|
||||
});
|
||||
|
||||
group('ValidationType enum', () {
|
||||
test('has all expected values', () {
|
||||
expect(ValidationType.values.length, 8);
|
||||
expect(ValidationType.values, contains(ValidationType.email));
|
||||
expect(ValidationType.values, contains(ValidationType.phone));
|
||||
expect(ValidationType.values, contains(ValidationType.url));
|
||||
expect(ValidationType.values, contains(ValidationType.password));
|
||||
expect(ValidationType.values, contains(ValidationType.username));
|
||||
expect(ValidationType.values, contains(ValidationType.title));
|
||||
expect(ValidationType.values, contains(ValidationType.description));
|
||||
expect(ValidationType.values, contains(ValidationType.text));
|
||||
});
|
||||
|
||||
test('can access by name', () {
|
||||
expect(ValidationType.values.byName('email'), ValidationType.email);
|
||||
expect(ValidationType.values.byName('phone'), ValidationType.phone);
|
||||
expect(ValidationType.values.byName('url'), ValidationType.url);
|
||||
});
|
||||
});
|
||||
|
||||
group('Edge cases', () {
|
||||
test('isValidEmail handles special characters', () {
|
||||
expect(ValidationUtils.isValidEmail('test+tag@example.com'), isTrue);
|
||||
expect(ValidationUtils.isValidEmail('test.email@example.co.uk'), isTrue);
|
||||
});
|
||||
|
||||
test('isValidPhone handles various formats', () {
|
||||
expect(ValidationUtils.isValidPhone('+1234567890'), isTrue);
|
||||
expect(ValidationUtils.isValidPhone('1234567890'), isTrue);
|
||||
expect(ValidationUtils.isValidPhone('+123'), isTrue);
|
||||
});
|
||||
|
||||
test('isValidUrl handles various protocols', () {
|
||||
expect(ValidationUtils.isValidUrl('https://example.com'), isTrue);
|
||||
expect(ValidationUtils.isValidUrl('http://example.com'), isTrue);
|
||||
expect(ValidationUtils.isValidUrl('https://www.example.com/path?query=value'), isTrue);
|
||||
});
|
||||
|
||||
test('isValidPassword handles edge cases', () {
|
||||
expect(ValidationUtils.isValidPassword('Password1'), isTrue);
|
||||
expect(ValidationUtils.isValidPassword('PASSWORD123'), isTrue);
|
||||
expect(ValidationUtils.isValidPassword('password1'), isTrue);
|
||||
expect(ValidationUtils.isValidPassword('1234567a'), isTrue);
|
||||
});
|
||||
|
||||
test('isValidUsername handles edge cases', () {
|
||||
expect(ValidationUtils.isValidUsername('a' * 3), isTrue);
|
||||
expect(ValidationUtils.isValidUsername('a' * 20), isTrue);
|
||||
expect(ValidationUtils.isValidUsername('user_123'), isTrue);
|
||||
expect(ValidationUtils.isValidUsername('123user'), isTrue);
|
||||
test('valid text → null', () {
|
||||
expect(ValidationUtils.getValidationErrorMessage('Some text', ValidationType.text), isNull);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
|
||||
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_state.dart';
|
||||
import 'package:eatvio_app/core/local_db/services/glossary_sync_runner.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/glossary_sync_cubit.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/glossary_sync_state.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
@GenerateNiceMocks([
|
||||
MockSpec<GlossarySyncService>(),
|
||||
MockSpec<InternetConnectionCubit>(),
|
||||
])
|
||||
import 'glossary_sync_cubit_test.mocks.dart';
|
||||
|
||||
void main() {
|
||||
late MockGlossarySyncService mockSyncService;
|
||||
late MockInternetConnectionCubit mockConnectionCubit;
|
||||
|
||||
setUp(() {
|
||||
mockSyncService = MockGlossarySyncService();
|
||||
mockConnectionCubit = MockInternetConnectionCubit();
|
||||
when(mockConnectionCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: false));
|
||||
});
|
||||
|
||||
GlossarySyncCubit buildCubit() => GlossarySyncCubit(
|
||||
syncService: mockSyncService,
|
||||
connectionCubit: mockConnectionCubit,
|
||||
);
|
||||
|
||||
group('GlossarySyncCubit', () {
|
||||
test('initial state is idle', () {
|
||||
final cubit = buildCubit();
|
||||
expect(cubit.state, const GlossarySyncState.idle());
|
||||
cubit.close();
|
||||
});
|
||||
|
||||
test('syncNow emits completed with count when sync returns > 0', () async {
|
||||
when(mockSyncService.sync(
|
||||
onStageChanged: anyNamed('onStageChanged'),
|
||||
onDownloadProgress: anyNamed('onDownloadProgress'),
|
||||
onProcessingProgress: anyNamed('onProcessingProgress'),
|
||||
)).thenAnswer((_) async => 100);
|
||||
|
||||
final cubit = buildCubit();
|
||||
final states = <GlossarySyncState>[];
|
||||
final sub = cubit.stream.listen(states.add);
|
||||
|
||||
await cubit.syncNow();
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(states.last, isA<GlossarySyncState>());
|
||||
await sub.cancel();
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('syncNow emits upToDate when sync returns 0', () async {
|
||||
when(mockSyncService.sync(
|
||||
onStageChanged: anyNamed('onStageChanged'),
|
||||
onDownloadProgress: anyNamed('onDownloadProgress'),
|
||||
onProcessingProgress: anyNamed('onProcessingProgress'),
|
||||
)).thenAnswer((_) async => 0);
|
||||
|
||||
final cubit = buildCubit();
|
||||
final states = <GlossarySyncState>[];
|
||||
final sub = cubit.stream.listen(states.add);
|
||||
|
||||
await cubit.syncNow();
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(states.last, const GlossarySyncState.upToDate());
|
||||
await sub.cancel();
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('syncNow emits error on exception', () async {
|
||||
when(mockSyncService.sync(
|
||||
onStageChanged: anyNamed('onStageChanged'),
|
||||
onDownloadProgress: anyNamed('onDownloadProgress'),
|
||||
onProcessingProgress: anyNamed('onProcessingProgress'),
|
||||
)).thenThrow(Exception('Test error'));
|
||||
|
||||
final cubit = buildCubit();
|
||||
final states = <GlossarySyncState>[];
|
||||
final sub = cubit.stream.listen(states.add);
|
||||
|
||||
await cubit.syncNow();
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(states.isNotEmpty, true);
|
||||
await sub.cancel();
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('syncNow emits error with socket message', () async {
|
||||
when(mockSyncService.sync(
|
||||
onStageChanged: anyNamed('onStageChanged'),
|
||||
onDownloadProgress: anyNamed('onDownloadProgress'),
|
||||
onProcessingProgress: anyNamed('onProcessingProgress'),
|
||||
)).thenThrow(Exception('SocketException: connection refused'));
|
||||
|
||||
final cubit = buildCubit();
|
||||
final states = <GlossarySyncState>[];
|
||||
final sub = cubit.stream.listen(states.add);
|
||||
|
||||
await cubit.syncNow();
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(states.isNotEmpty, true);
|
||||
await sub.cancel();
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('syncNow emits error with timeout message', () async {
|
||||
when(mockSyncService.sync(
|
||||
onStageChanged: anyNamed('onStageChanged'),
|
||||
onDownloadProgress: anyNamed('onDownloadProgress'),
|
||||
onProcessingProgress: anyNamed('onProcessingProgress'),
|
||||
)).thenThrow(TimeoutException('timeout'));
|
||||
|
||||
final cubit = buildCubit();
|
||||
final states = <GlossarySyncState>[];
|
||||
final sub = cubit.stream.listen(states.add);
|
||||
|
||||
await cubit.syncNow();
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
expect(states.isNotEmpty, true);
|
||||
await sub.cancel();
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('start() listens to connectionCubit', () {
|
||||
final cubit = buildCubit();
|
||||
cubit.start();
|
||||
verify(mockConnectionCubit.stream).called(1);
|
||||
cubit.close();
|
||||
});
|
||||
|
||||
test('duplicate syncNow calls are ignored', () async {
|
||||
final completer = Completer<int>();
|
||||
when(mockSyncService.sync(
|
||||
onStageChanged: anyNamed('onStageChanged'),
|
||||
onDownloadProgress: anyNamed('onDownloadProgress'),
|
||||
onProcessingProgress: anyNamed('onProcessingProgress'),
|
||||
)).thenAnswer((_) => completer.future);
|
||||
|
||||
final cubit = buildCubit();
|
||||
cubit.syncNow();
|
||||
cubit.syncNow(); // Should be ignored
|
||||
|
||||
completer.complete(5);
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
verify(mockSyncService.sync(
|
||||
onStageChanged: anyNamed('onStageChanged'),
|
||||
onDownloadProgress: anyNamed('onDownloadProgress'),
|
||||
onProcessingProgress: anyNamed('onProcessingProgress'),
|
||||
)).called(1);
|
||||
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('stage callbacks emit correct states', () async {
|
||||
when(mockSyncService.sync(
|
||||
onStageChanged: anyNamed('onStageChanged'),
|
||||
onDownloadProgress: anyNamed('onDownloadProgress'),
|
||||
onProcessingProgress: anyNamed('onProcessingProgress'),
|
||||
)).thenAnswer((invocation) async {
|
||||
final stageCallback = invocation.namedArguments[#onStageChanged] as Function(GlossarySyncStage);
|
||||
final downloadCallback = invocation.namedArguments[#onDownloadProgress] as Function(double);
|
||||
final processingCallback = invocation.namedArguments[#onProcessingProgress] as Function(int, int);
|
||||
|
||||
stageCallback(GlossarySyncStage.checking);
|
||||
stageCallback(GlossarySyncStage.downloading);
|
||||
downloadCallback(0.5);
|
||||
stageCallback(GlossarySyncStage.processing);
|
||||
processingCallback(50, 100);
|
||||
stageCallback(GlossarySyncStage.saving);
|
||||
|
||||
return 100;
|
||||
});
|
||||
|
||||
final cubit = buildCubit();
|
||||
final states = <GlossarySyncState>[];
|
||||
final sub = cubit.stream.listen(states.add);
|
||||
|
||||
await cubit.syncNow();
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
expect(states.length, greaterThanOrEqualTo(4));
|
||||
await sub.cancel();
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('close cancels subscriptions', () async {
|
||||
final cubit = buildCubit();
|
||||
cubit.start();
|
||||
await cubit.close();
|
||||
// Should not crash
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/glossary_sync_cubit.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/glossary_sync_state.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/glossary_sync_banner_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
@GenerateNiceMocks([MockSpec<GlossarySyncCubit>()])
|
||||
import 'glossary_sync_banner_widget_test.mocks.dart';
|
||||
|
||||
void main() {
|
||||
late MockGlossarySyncCubit mockCubit;
|
||||
|
||||
setUp(() {
|
||||
mockCubit = MockGlossarySyncCubit();
|
||||
when(mockCubit.stream).thenAnswer((_) => const Stream<GlossarySyncState>.empty());
|
||||
when(mockCubit.close()).thenAnswer((_) async {});
|
||||
});
|
||||
|
||||
Widget buildWidget() {
|
||||
return MaterialApp(
|
||||
home: BlocProvider<GlossarySyncCubit>.value(
|
||||
value: mockCubit,
|
||||
child: const Scaffold(
|
||||
body: GlossarySyncBannerWidget(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
group('GlossarySyncBannerWidget', () {
|
||||
testWidgets('idle state shows nothing', (tester) async {
|
||||
when(mockCubit.state).thenReturn(const GlossarySyncState.idle());
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.byType(GlossarySyncBannerWidget), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('upToDate state shows nothing', (tester) async {
|
||||
when(mockCubit.state).thenReturn(const GlossarySyncState.upToDate());
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.byType(GlossarySyncBannerWidget), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('checking state shows checking text', (tester) async {
|
||||
when(mockCubit.state).thenReturn(const GlossarySyncState.checking());
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('Проверяю'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('downloading with progress shows percentage', (tester) async {
|
||||
when(mockCubit.state).thenReturn(const GlossarySyncState.downloading(progress: 0.45));
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('45%'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('downloading without progress shows dots', (tester) async {
|
||||
when(mockCubit.state).thenReturn(const GlossarySyncState.downloading());
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('Загрузка'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('processing shows progress', (tester) async {
|
||||
when(mockCubit.state).thenReturn(const GlossarySyncState.processing(processed: 12500, total: 200000));
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('Обработка'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('processing with 0 total shows generic text', (tester) async {
|
||||
when(mockCubit.state).thenReturn(const GlossarySyncState.processing(processed: 0, total: 0));
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('Обработка данных'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('completed shows count', (tester) async {
|
||||
when(mockCubit.state).thenReturn(const GlossarySyncState.completed(syncedCount: 200000));
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('Синхронизировано'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('error shows error message', (tester) async {
|
||||
when(mockCubit.state).thenReturn(const GlossarySyncState.error(message: 'Ошибка синхронизации'));
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('Ошибка'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
|
||||
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/offline_banner_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
@GenerateNiceMocks([MockSpec<DiaryCubit>()])
|
||||
import 'offline_banner_widget_test.mocks.dart';
|
||||
|
||||
DiaryDayEntity _makeDiary() => DiaryDayEntity(
|
||||
id: 1,
|
||||
date: '2025-01-01',
|
||||
wakeup: '06:30:00',
|
||||
retire: '22:30:00',
|
||||
closed: 0,
|
||||
comment: '',
|
||||
typeOfDiary: const TypeOfDiaryModel(hasChosenDiet: false, hasChosenPlan: false, diaryState: null),
|
||||
meals: [
|
||||
const MealModel(id: 1, time: '08:00', typeNumber: 1, satiety: 0, mood: 0, portions: [], photos: []),
|
||||
],
|
||||
chosenPlanId: null,
|
||||
dayOfChosenDiet: null,
|
||||
diet: null,
|
||||
);
|
||||
|
||||
void main() {
|
||||
late MockDiaryCubit mockDiaryCubit;
|
||||
|
||||
setUp(() {
|
||||
mockDiaryCubit = MockDiaryCubit();
|
||||
when(mockDiaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty());
|
||||
when(mockDiaryCubit.close()).thenAnswer((_) async {});
|
||||
});
|
||||
|
||||
Widget buildWidget() {
|
||||
return MaterialApp(
|
||||
home: BlocProvider<DiaryCubit>.value(
|
||||
value: mockDiaryCubit,
|
||||
child: const Scaffold(
|
||||
body: OfflineBannerWidget(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
group('OfflineBannerWidget', () {
|
||||
testWidgets('shows nothing when online and no pending ops', (tester) async {
|
||||
when(mockDiaryCubit.state).thenReturn(
|
||||
DiaryState.loaded(
|
||||
diary: _makeDiary(),
|
||||
selectedDay: DateTime(2025, 1, 1),
|
||||
selectedMeal: 1,
|
||||
isOfflineMode: false,
|
||||
pendingOpsCount: 0,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
// SizedBox.shrink should be rendered — no banner text visible
|
||||
expect(find.textContaining('Офлайн'), findsNothing);
|
||||
expect(find.textContaining('Синхронизация'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows offline banner with pending ops', (tester) async {
|
||||
when(mockDiaryCubit.state).thenReturn(
|
||||
DiaryState.loaded(
|
||||
diary: _makeDiary(),
|
||||
selectedDay: DateTime(2025, 1, 1),
|
||||
selectedMeal: 1,
|
||||
isOfflineMode: true,
|
||||
pendingOpsCount: 3,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('Офлайн'), findsOneWidget);
|
||||
expect(find.textContaining('3'), findsWidgets);
|
||||
});
|
||||
|
||||
testWidgets('shows offline without pending ops', (tester) async {
|
||||
when(mockDiaryCubit.state).thenReturn(
|
||||
DiaryState.loaded(
|
||||
diary: _makeDiary(),
|
||||
selectedDay: DateTime(2025, 1, 1),
|
||||
selectedMeal: 1,
|
||||
isOfflineMode: true,
|
||||
pendingOpsCount: 0,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('кэша'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows sync banner when online with pending ops', (tester) async {
|
||||
when(mockDiaryCubit.state).thenReturn(
|
||||
DiaryState.loaded(
|
||||
diary: _makeDiary(),
|
||||
selectedDay: DateTime(2025, 1, 1),
|
||||
selectedMeal: 1,
|
||||
isOfflineMode: false,
|
||||
pendingOpsCount: 5,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
expect(find.textContaining('Синхронизация'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('hides when state is not loaded', (tester) async {
|
||||
when(mockDiaryCubit.state).thenReturn(const DiaryState.loading());
|
||||
await tester.pumpWidget(buildWidget());
|
||||
await tester.pump();
|
||||
// SizedBox.shrink should be rendered
|
||||
expect(find.textContaining('Офлайн'), findsNothing);
|
||||
expect(find.textContaining('Синхронизация'), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,93 +1,102 @@
|
||||
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
|
||||
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_state.dart';
|
||||
import 'package:eatvio_app/core/local_db/app_database.dart';
|
||||
import 'package:eatvio_app/core/local_db/diary_local_data_source.dart';
|
||||
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
|
||||
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
|
||||
import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
class MockGlossaryService extends Mock implements GlossaryService {}
|
||||
class MockInternetConnectionCubit extends Mock implements InternetConnectionCubit {}
|
||||
class MockDiaryLocalDataSource extends Mock implements DiaryLocalDataSource {}
|
||||
@GenerateNiceMocks([
|
||||
MockSpec<GlossaryService>(),
|
||||
MockSpec<DiaryLocalDataSource>(),
|
||||
MockSpec<InternetConnectionCubit>(),
|
||||
])
|
||||
import 'glossary_repository_test.mocks.dart';
|
||||
|
||||
void main() {
|
||||
late GlossaryRepository repository;
|
||||
late MockGlossaryService mockService;
|
||||
late MockDiaryLocalDataSource mockLocalDataSource;
|
||||
late MockInternetConnectionCubit mockConnectionCubit;
|
||||
|
||||
setUp(() {
|
||||
mockService = MockGlossaryService();
|
||||
mockLocalDataSource = MockDiaryLocalDataSource();
|
||||
mockConnectionCubit = MockInternetConnectionCubit();
|
||||
repository = GlossaryRepository(
|
||||
glossaryService: mockService,
|
||||
localDataSource: mockLocalDataSource,
|
||||
connectionCubit: mockConnectionCubit,
|
||||
);
|
||||
});
|
||||
|
||||
group('GlossaryRepository', () {
|
||||
late GlossaryRepository repository;
|
||||
late MockGlossaryService mockService;
|
||||
late MockInternetConnectionCubit mockConnectionCubit;
|
||||
late MockDiaryLocalDataSource mockLocalDataSource;
|
||||
test('offline → searches local cache', () async {
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: false));
|
||||
when(mockLocalDataSource.searchGlossary(any)).thenAnswer((_) async => [
|
||||
const GlossaryCacheTableData(
|
||||
id: 1,
|
||||
name: 'Яблоко',
|
||||
productGroupId: 5,
|
||||
energyOn100gr: 52.0,
|
||||
proteinsOn100gr: 0.3,
|
||||
fatsOn100gr: 0.2,
|
||||
carbohydratesOn100gr: 14.0,
|
||||
portionMassGr: 150.0,
|
||||
energyOnPortion: 78.0,
|
||||
proteinsOnPortion: 0.45,
|
||||
fatsOnPortion: 0.3,
|
||||
carbohydratesOnPortion: 21.0,
|
||||
updatedAt: '2024-01-01',
|
||||
),
|
||||
]);
|
||||
|
||||
setUp(() {
|
||||
mockService = MockGlossaryService();
|
||||
mockConnectionCubit = MockInternetConnectionCubit();
|
||||
mockLocalDataSource = MockDiaryLocalDataSource();
|
||||
when(() => mockConnectionCubit.state).thenReturn(
|
||||
InternetConnectionState(isInternetAvailable: true),
|
||||
);
|
||||
repository = GlossaryRepository(
|
||||
glossaryService: mockService,
|
||||
localDataSource: mockLocalDataSource,
|
||||
connectionCubit: mockConnectionCubit,
|
||||
);
|
||||
final result = await repository.getGlossaryListByQuery('яблоко');
|
||||
expect(result.length, 1);
|
||||
expect(result[0].name, 'Яблоко');
|
||||
expect(result[0].energyOn100Gr, 52.0);
|
||||
expect(result[0].grInPortion, 150);
|
||||
verifyNever(mockService.getGlossaryByQuery(query: anyNamed('query')));
|
||||
});
|
||||
|
||||
test('getGlossaryListByQuery - успешный запрос', () async {
|
||||
const query = 'test';
|
||||
final mockGlossaries = <GlossaryModel>[];
|
||||
final mockPagination = GlossaryPaginationModel(glossariesList: mockGlossaries);
|
||||
test('online → uses network service', () async {
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: true));
|
||||
when(mockService.getGlossaryByQuery(query: anyNamed('query'))).thenAnswer(
|
||||
(_) async => const GlossaryPaginationModel(
|
||||
glossariesList: [
|
||||
GlossaryModel(
|
||||
id: 1,
|
||||
name: 'Яблоко',
|
||||
grInPortion: 150,
|
||||
energyOn100Gr: 52.0,
|
||||
proteinsOn100Gr: 0.3,
|
||||
fatsOn100Gr: 0.2,
|
||||
carbohydratesOn100Gr: 14.0,
|
||||
energyOnPortion: 78.0,
|
||||
proteinsOnPortion: 0.45,
|
||||
fatsOnPortion: 0.3,
|
||||
carbohydratesOnPortion: 21.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
when(() => mockService.getGlossaryByQuery(query: query))
|
||||
.thenAnswer((_) async => mockPagination);
|
||||
|
||||
final result = await repository.getGlossaryListByQuery(query);
|
||||
|
||||
expect(result, mockGlossaries);
|
||||
verify(() => mockService.getGlossaryByQuery(query: query)).called(1);
|
||||
final result = await repository.getGlossaryListByQuery('яблоко');
|
||||
expect(result.length, 1);
|
||||
expect(result[0].name, 'Яблоко');
|
||||
});
|
||||
|
||||
test('getGlossaryListByQuery - пробрасывает ошибку', () async {
|
||||
const query = 'test';
|
||||
test('online with network error → falls back to local cache', () async {
|
||||
when(mockConnectionCubit.state).thenReturn(InternetConnectionState(isInternetAvailable: true));
|
||||
when(mockService.getGlossaryByQuery(query: anyNamed('query'))).thenThrow(Exception('Network error'));
|
||||
when(mockLocalDataSource.searchGlossary(any)).thenAnswer((_) async => []);
|
||||
|
||||
when(() => mockService.getGlossaryByQuery(query: query))
|
||||
.thenThrow(Exception('Error'));
|
||||
when(() => mockLocalDataSource.searchGlossary(query))
|
||||
.thenThrow(Exception('Local error'));
|
||||
|
||||
expect(() => repository.getGlossaryListByQuery(query), throwsException);
|
||||
final result = await repository.getGlossaryListByQuery('яблоко');
|
||||
expect(result, isEmpty);
|
||||
verify(mockLocalDataSource.searchGlossary('яблоко')).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,228 +1,176 @@
|
||||
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('MyRecipeModel', () {
|
||||
const model = MyRecipeModel(
|
||||
id: 1,
|
||||
image: 'https://example.com/img.jpg',
|
||||
state: 'active',
|
||||
cookingTime: 30,
|
||||
activeCookingTime: 15,
|
||||
description: '# Recipe\nDelicious',
|
||||
steps: [Step(image: 'step.jpg', description: 'Step 1')],
|
||||
videoUrl: 'https://youtube.com/watch?v=123',
|
||||
title: 'Test Recipe',
|
||||
slug: 'test-recipe',
|
||||
calories: 250.5,
|
||||
likesCount: 10,
|
||||
viewsCount: 100,
|
||||
);
|
||||
|
||||
test('fields are accessible', () {
|
||||
expect(model.id, 1);
|
||||
expect(model.image, 'https://example.com/img.jpg');
|
||||
expect(model.state, 'active');
|
||||
expect(model.cookingTime, 30);
|
||||
expect(model.activeCookingTime, 15);
|
||||
expect(model.description, '# Recipe\nDelicious');
|
||||
expect(model.videoUrl, 'https://youtube.com/watch?v=123');
|
||||
expect(model.title, 'Test Recipe');
|
||||
expect(model.slug, 'test-recipe');
|
||||
expect(model.calories, 250.5);
|
||||
expect(model.likesCount, 10);
|
||||
expect(model.viewsCount, 100);
|
||||
expect(model.steps?.length, 1);
|
||||
});
|
||||
|
||||
test('fromJson parses correctly', () {
|
||||
final json = {
|
||||
'id': 2,
|
||||
'image': 'img.jpg',
|
||||
'state': 'draft',
|
||||
'cooking_time': 60,
|
||||
'active_time': 20,
|
||||
'description_md': 'Desc',
|
||||
'steps': [{'image': 's.jpg', 'description_md': 'S1'}],
|
||||
'video_url': null,
|
||||
'title': 'Title',
|
||||
'slug': 'slug',
|
||||
'energy_per_serving': 300.0,
|
||||
'total_likes': 5,
|
||||
'total_views': 50,
|
||||
};
|
||||
final parsed = MyRecipeModel.fromJson(json);
|
||||
expect(parsed.id, 2);
|
||||
expect(parsed.cookingTime, 60);
|
||||
expect(parsed.calories, 300.0);
|
||||
});
|
||||
|
||||
test('toRecipeModel converts correctly', () {
|
||||
final recipeModel = model.toRecipeModel();
|
||||
expect(recipeModel.title, 'Test Recipe');
|
||||
expect(recipeModel.description, '# Recipe\nDelicious');
|
||||
expect(recipeModel.totalCookingTime, 30);
|
||||
expect(recipeModel.activeCookTime, 15);
|
||||
expect(recipeModel.energyPerServing, 250);
|
||||
expect(recipeModel.state, 'active');
|
||||
});
|
||||
|
||||
test('toRecipe converts correctly', () {
|
||||
final recipe = model.toRecipe();
|
||||
expect(recipe.id, 1);
|
||||
expect(recipe.image, 'https://example.com/img.jpg');
|
||||
expect(recipe.state, 'active');
|
||||
expect(recipe.cookingTime, 30);
|
||||
expect(recipe.slug, 'test-recipe');
|
||||
expect(recipe.calories, 250.5);
|
||||
expect(recipe.likesCount, 10);
|
||||
expect(recipe.type, 'recipe');
|
||||
expect(recipe.commentsCount, 0);
|
||||
expect(recipe.wasLikedByMe, false);
|
||||
});
|
||||
|
||||
test('toRecipeModel with null steps', () {
|
||||
const noSteps = MyRecipeModel(
|
||||
id: 1, image: '', state: 'active', cookingTime: 10,
|
||||
activeCookingTime: 5, description: '', steps: null,
|
||||
videoUrl: null, title: null, slug: 'slug', calories: 0,
|
||||
likesCount: 0, viewsCount: 0,
|
||||
);
|
||||
final rm = noSteps.toRecipeModel();
|
||||
expect(rm.steps, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('MyRecipePaginationModel', () {
|
||||
test('creates MyRecipePaginationModel with empty list', () {
|
||||
const model = MyRecipePaginationModel(data: []);
|
||||
|
||||
expect(model.data, isEmpty);
|
||||
});
|
||||
|
||||
test('creates MyRecipePaginationModel with recipes', () {
|
||||
final recipes = [
|
||||
const MyRecipeModel(
|
||||
id: 1,
|
||||
image: 'image1.jpg',
|
||||
state: 'published',
|
||||
cookingTime: 30,
|
||||
activeCookingTime: 15,
|
||||
description: 'Test recipe 1',
|
||||
steps: null,
|
||||
videoUrl: null,
|
||||
title: 'Recipe 1',
|
||||
slug: 'recipe-1',
|
||||
calories: 250.0,
|
||||
likesCount: 10,
|
||||
viewsCount: 100,
|
||||
),
|
||||
const MyRecipeModel(
|
||||
id: 2,
|
||||
image: 'image2.jpg',
|
||||
state: 'draft',
|
||||
cookingTime: 45,
|
||||
activeCookingTime: 20,
|
||||
description: 'Test recipe 2',
|
||||
steps: null,
|
||||
videoUrl: 'video.mp4',
|
||||
title: 'Recipe 2',
|
||||
slug: 'recipe-2',
|
||||
calories: 300.0,
|
||||
likesCount: 5,
|
||||
viewsCount: 50,
|
||||
),
|
||||
];
|
||||
|
||||
final model = MyRecipePaginationModel(data: recipes);
|
||||
|
||||
expect(model.data.length, 2);
|
||||
expect(model.data[0].id, 1);
|
||||
expect(model.data[0].title, 'Recipe 1');
|
||||
expect(model.data[1].id, 2);
|
||||
expect(model.data[1].title, 'Recipe 2');
|
||||
});
|
||||
|
||||
test('creates MyRecipePaginationModel from JSON', () {
|
||||
test('fromJson parses correctly', () {
|
||||
final json = {
|
||||
'data': [
|
||||
{
|
||||
'id': 1,
|
||||
'image': 'image1.jpg',
|
||||
'state': 'published',
|
||||
'image': 'img.jpg',
|
||||
'state': 'active',
|
||||
'cooking_time': 30,
|
||||
'active_time': 15,
|
||||
'description_md': 'Test recipe',
|
||||
'description_md': 'Desc',
|
||||
'steps': null,
|
||||
'video_url': null,
|
||||
'title': 'Recipe 1',
|
||||
'slug': 'recipe-1',
|
||||
'energy_per_serving': 250.0,
|
||||
'total_likes': 10,
|
||||
'total_views': 100,
|
||||
'title': 'Title',
|
||||
'slug': 'slug',
|
||||
'energy_per_serving': 100.0,
|
||||
'total_likes': 0,
|
||||
'total_views': 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
final model = MyRecipePaginationModel.fromJson(json);
|
||||
|
||||
expect(model.data.length, 1);
|
||||
expect(model.data[0].id, 1);
|
||||
expect(model.data[0].title, 'Recipe 1');
|
||||
final parsed = MyRecipePaginationModel.fromJson(json);
|
||||
expect(parsed.data.length, 1);
|
||||
expect(parsed.data[0].title, 'Title');
|
||||
});
|
||||
});
|
||||
|
||||
group('MyRecipeModel', () {
|
||||
test('creates MyRecipeModel with all parameters', () {
|
||||
const recipe = MyRecipeModel(
|
||||
group('RecipeToRecipeModelX', () {
|
||||
test('Recipe.toRecipeModel converts correctly', () {
|
||||
const recipe = Recipe(
|
||||
id: 1,
|
||||
image: 'image.jpg',
|
||||
state: 'published',
|
||||
cookingTime: 30,
|
||||
activeCookingTime: 15,
|
||||
description: 'Test recipe',
|
||||
steps: null,
|
||||
image: 'img.jpg',
|
||||
state: 'active',
|
||||
cookingTime: 45,
|
||||
activeCookingTime: 20,
|
||||
ingredients: [
|
||||
Ingredient(name: 'Flour', portion: '200g', productId: 1),
|
||||
],
|
||||
description: 'Recipe description',
|
||||
steps: [Step(image: 's.jpg', description: 'Step')],
|
||||
videoUrl: null,
|
||||
title: 'Recipe 1',
|
||||
slug: 'recipe-1',
|
||||
calories: 250.0,
|
||||
likesCount: 10,
|
||||
viewsCount: 100,
|
||||
title: 'My Recipe',
|
||||
type: 'recipe',
|
||||
slug: 'my-recipe',
|
||||
commentsCount: 3,
|
||||
wasLikedByMe: true,
|
||||
isFavorite: false,
|
||||
calories: 350.0,
|
||||
likesCount: 20,
|
||||
viewsCount: 200,
|
||||
tags: [Tag(name: 'healthy')],
|
||||
author: Author(authorSlug: 'user1', authorName: 'User', authorProfileIcon: '', authorIsMe: true),
|
||||
);
|
||||
|
||||
expect(recipe.id, 1);
|
||||
expect(recipe.image, 'image.jpg');
|
||||
expect(recipe.state, 'published');
|
||||
expect(recipe.cookingTime, 30);
|
||||
expect(recipe.activeCookingTime, 15);
|
||||
expect(recipe.description, 'Test recipe');
|
||||
expect(recipe.steps, null);
|
||||
expect(recipe.videoUrl, null);
|
||||
expect(recipe.title, 'Recipe 1');
|
||||
expect(recipe.slug, 'recipe-1');
|
||||
expect(recipe.calories, 250.0);
|
||||
expect(recipe.likesCount, 10);
|
||||
expect(recipe.viewsCount, 100);
|
||||
final rm = recipe.toRecipeModel();
|
||||
expect(rm.title, 'My Recipe');
|
||||
expect(rm.totalCookingTime, 45);
|
||||
expect(rm.activeCookTime, 20);
|
||||
expect(rm.energyPerServing, 350);
|
||||
expect(rm.state, 'active');
|
||||
expect(rm.ingredients?.length, 1);
|
||||
expect(rm.tags?.length, 1);
|
||||
});
|
||||
|
||||
test('creates MyRecipeModel with steps and videoUrl', () {
|
||||
final steps = [
|
||||
const Step(image: 'step1.jpg', description: 'Step 1'),
|
||||
const Step(image: 'step2.jpg', description: 'Step 2'),
|
||||
];
|
||||
|
||||
final recipe = MyRecipeModel(
|
||||
id: 1,
|
||||
image: 'image.jpg',
|
||||
state: 'published',
|
||||
cookingTime: 30,
|
||||
activeCookingTime: 15,
|
||||
description: 'Test recipe',
|
||||
steps: steps,
|
||||
videoUrl: 'video.mp4',
|
||||
title: 'Recipe 1',
|
||||
slug: 'recipe-1',
|
||||
calories: 250.0,
|
||||
likesCount: 10,
|
||||
viewsCount: 100,
|
||||
test('Recipe.toRecipeModel with null ingredients', () {
|
||||
const recipe = Recipe(
|
||||
id: 1, image: '', state: 'active', cookingTime: 10,
|
||||
activeCookingTime: 5, ingredients: null,
|
||||
description: '', steps: null, videoUrl: null,
|
||||
title: 'T', type: 'recipe', slug: 's',
|
||||
commentsCount: 0, wasLikedByMe: false, isFavorite: false,
|
||||
calories: 0, likesCount: 0, viewsCount: 0,
|
||||
tags: [], author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false),
|
||||
);
|
||||
|
||||
expect(recipe.steps, isNotNull);
|
||||
expect(recipe.steps!.length, 2);
|
||||
expect(recipe.videoUrl, 'video.mp4');
|
||||
});
|
||||
|
||||
test('creates MyRecipeModel from JSON', () {
|
||||
final json = {
|
||||
'id': 1,
|
||||
'image': 'image.jpg',
|
||||
'state': 'published',
|
||||
'cooking_time': 30,
|
||||
'active_time': 15,
|
||||
'description_md': 'Test recipe',
|
||||
'steps': null,
|
||||
'video_url': null,
|
||||
'title': 'Recipe 1',
|
||||
'slug': 'recipe-1',
|
||||
'energy_per_serving': 250.0,
|
||||
'total_likes': 10,
|
||||
'total_views': 100,
|
||||
};
|
||||
|
||||
final recipe = MyRecipeModel.fromJson(json);
|
||||
|
||||
expect(recipe.id, 1);
|
||||
expect(recipe.title, 'Recipe 1');
|
||||
expect(recipe.cookingTime, 30);
|
||||
expect(recipe.calories, 250.0);
|
||||
});
|
||||
|
||||
test('toJson serializes MyRecipeModel correctly', () {
|
||||
const recipe = MyRecipeModel(
|
||||
id: 1,
|
||||
image: 'image.jpg',
|
||||
state: 'published',
|
||||
cookingTime: 30,
|
||||
activeCookingTime: 15,
|
||||
description: 'Test recipe',
|
||||
steps: null,
|
||||
videoUrl: null,
|
||||
title: 'Recipe 1',
|
||||
slug: 'recipe-1',
|
||||
calories: 250.0,
|
||||
likesCount: 10,
|
||||
viewsCount: 100,
|
||||
);
|
||||
|
||||
final json = recipe.toJson();
|
||||
|
||||
expect(json['id'], 1);
|
||||
expect(json['title'], 'Recipe 1');
|
||||
expect(json['slug'], 'recipe-1');
|
||||
expect(json['cooking_time'], 30);
|
||||
expect(json['active_time'], 15);
|
||||
expect(json['energy_per_serving'], 250.0);
|
||||
expect(json['total_likes'], 10);
|
||||
expect(json['total_views'], 100);
|
||||
});
|
||||
});
|
||||
|
||||
group('MyRecipePaginationModel', () {
|
||||
test('toJson serializes MyRecipePaginationModel correctly', () {
|
||||
final recipes = [
|
||||
const MyRecipeModel(
|
||||
id: 1,
|
||||
image: 'image1.jpg',
|
||||
state: 'published',
|
||||
cookingTime: 30,
|
||||
activeCookingTime: 15,
|
||||
description: 'Test recipe 1',
|
||||
steps: null,
|
||||
videoUrl: null,
|
||||
title: 'Recipe 1',
|
||||
slug: 'recipe-1',
|
||||
calories: 250.0,
|
||||
likesCount: 10,
|
||||
viewsCount: 100,
|
||||
),
|
||||
];
|
||||
|
||||
final model = MyRecipePaginationModel(data: recipes);
|
||||
|
||||
final json = model.toJson();
|
||||
|
||||
expect(json.containsKey('data'), isTrue);
|
||||
expect((json['data'] as List).length, 1);
|
||||
final rm = recipe.toRecipeModel();
|
||||
expect(rm.ingredients, isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
110
test/features/registration/registration_error_message_test.dart
Normal file
110
test/features/registration/registration_error_message_test.dart
Normal file
@@ -0,0 +1,110 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/registration/registration_error_message.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('registrationErrorMessage', () {
|
||||
test('returns string when error is String', () {
|
||||
expect(registrationErrorMessage('Some error'), 'Some error');
|
||||
});
|
||||
|
||||
test('returns message from 422 DioException with message field', () {
|
||||
final error = DioException(
|
||||
requestOptions: RequestOptions(path: ''),
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(path: ''),
|
||||
statusCode: 422,
|
||||
data: {'message': 'Validation failed'},
|
||||
),
|
||||
);
|
||||
expect(registrationErrorMessage(error), 'Validation failed');
|
||||
});
|
||||
|
||||
test('returns errors from 422 DioException with errors field', () {
|
||||
final error = DioException(
|
||||
requestOptions: RequestOptions(path: ''),
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(path: ''),
|
||||
statusCode: 422,
|
||||
data: {
|
||||
'message': 'Validation error',
|
||||
'errors': {
|
||||
'email': ['Email уже используется'],
|
||||
'name': ['Имя обязательно'],
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
final result = registrationErrorMessage(error);
|
||||
expect(result, contains('Validation error'));
|
||||
expect(result, contains('Email уже используется'));
|
||||
expect(result, contains('Имя обязательно'));
|
||||
});
|
||||
|
||||
test('returns data string for 422 with unknown format', () {
|
||||
final error = DioException(
|
||||
requestOptions: RequestOptions(path: ''),
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(path: ''),
|
||||
statusCode: 422,
|
||||
data: {'unknown_key': 'value'},
|
||||
),
|
||||
);
|
||||
expect(registrationErrorMessage(error), contains('Ошибка регистрации'));
|
||||
});
|
||||
|
||||
test('returns DioException message for non-422 status', () {
|
||||
final error = DioException(
|
||||
requestOptions: RequestOptions(path: ''),
|
||||
message: 'Connection timeout',
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(path: ''),
|
||||
statusCode: 500,
|
||||
),
|
||||
);
|
||||
expect(registrationErrorMessage(error), 'Connection timeout');
|
||||
});
|
||||
|
||||
test('returns DioException toString when message is null', () {
|
||||
final error = DioException(
|
||||
requestOptions: RequestOptions(path: ''),
|
||||
);
|
||||
expect(registrationErrorMessage(error), contains('DioException'));
|
||||
});
|
||||
|
||||
test('returns toString for unknown error type', () {
|
||||
expect(registrationErrorMessage(42), '42');
|
||||
expect(registrationErrorMessage(Exception('Test')), contains('Test'));
|
||||
});
|
||||
|
||||
test('handles 422 with empty errors list', () {
|
||||
final error = DioException(
|
||||
requestOptions: RequestOptions(path: ''),
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(path: ''),
|
||||
statusCode: 422,
|
||||
data: {
|
||||
'message': 'Error',
|
||||
'errors': {
|
||||
'field': <String>[],
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(registrationErrorMessage(error), 'Error');
|
||||
});
|
||||
|
||||
test('handles 422 with non-Map response data', () {
|
||||
final error = DioException(
|
||||
requestOptions: RequestOptions(path: ''),
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(path: ''),
|
||||
statusCode: 422,
|
||||
data: 'plain string error',
|
||||
),
|
||||
);
|
||||
// Not a map, falls through to DioException handling
|
||||
expect(registrationErrorMessage(error), isNotEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
221
test/features/registration_v2/cubit/registration_cubit_test.dart
Normal file
221
test/features/registration_v2/cubit/registration_cubit_test.dart
Normal file
@@ -0,0 +1,221 @@
|
||||
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
|
||||
import 'package:eatvio_app/features/registration_v2/cubit/registration_state.dart';
|
||||
import 'package:eatvio_app/features/registration_v2/repositories/registration_repository_v2.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
|
||||
@GenerateNiceMocks([MockSpec<RegistrationRepositoryV2>()])
|
||||
import 'registration_cubit_test.mocks.dart';
|
||||
|
||||
void main() {
|
||||
late RegistrationCubitV2 cubit;
|
||||
late MockRegistrationRepositoryV2 mockRepo;
|
||||
|
||||
setUp(() {
|
||||
mockRepo = MockRegistrationRepositoryV2();
|
||||
cubit = RegistrationCubitV2(registrationRepository: mockRepo);
|
||||
});
|
||||
|
||||
tearDown(() => cubit.close());
|
||||
|
||||
group('RegistrationCubitV2', () {
|
||||
test('initial state', () {
|
||||
expect(cubit.state, const RegistrationStateV2());
|
||||
});
|
||||
|
||||
group('setters', () {
|
||||
test('setFirstName updates firstName', () {
|
||||
cubit.setFirstName('Иван');
|
||||
expect(cubit.state.firstName, 'Иван');
|
||||
});
|
||||
|
||||
test('setLastName updates lastName', () {
|
||||
cubit.setLastName('Иванов');
|
||||
expect(cubit.state.lastName, 'Иванов');
|
||||
});
|
||||
|
||||
test('setEmail updates email', () {
|
||||
cubit.setEmail('test@test.com');
|
||||
expect(cubit.state.email, 'test@test.com');
|
||||
});
|
||||
|
||||
test('setPhone updates phone', () {
|
||||
cubit.setPhone('+79001234567');
|
||||
expect(cubit.state.phone, '+79001234567');
|
||||
});
|
||||
|
||||
test('setBirthday updates birthday', () {
|
||||
final date = DateTime(1990, 1, 1);
|
||||
cubit.setBirthday(date);
|
||||
expect(cubit.state.birthday, date);
|
||||
});
|
||||
|
||||
test('setGender updates gender', () {
|
||||
cubit.setGender('male');
|
||||
expect(cubit.state.gender, 'male');
|
||||
});
|
||||
|
||||
test('setWeight updates weight', () {
|
||||
cubit.setWeight(70);
|
||||
expect(cubit.state.weight, 70);
|
||||
});
|
||||
|
||||
test('setHeight updates height', () {
|
||||
cubit.setHeight(175);
|
||||
expect(cubit.state.height, 175);
|
||||
});
|
||||
|
||||
test('setActivityCoefficient updates activityCoefficient', () {
|
||||
cubit.setActivityCoefficient(1.2);
|
||||
expect(cubit.state.activityCoefficient, 1.2);
|
||||
});
|
||||
|
||||
test('setPassword updates password', () {
|
||||
cubit.setPassword('secret123');
|
||||
expect(cubit.state.password, 'secret123');
|
||||
});
|
||||
});
|
||||
|
||||
group('validators', () {
|
||||
test('validateFirstName returns false for empty', () {
|
||||
expect(cubit.validateFirstName(), false);
|
||||
expect(cubit.state.isFirstNameValid, false);
|
||||
});
|
||||
|
||||
test('validateFirstName returns true for non-empty', () {
|
||||
cubit.setFirstName('Иван');
|
||||
expect(cubit.validateFirstName(), true);
|
||||
expect(cubit.state.isFirstNameValid, true);
|
||||
});
|
||||
|
||||
test('validateLastName returns false for empty', () {
|
||||
expect(cubit.validateLastName(), false);
|
||||
expect(cubit.state.isLastNameValid, false);
|
||||
});
|
||||
|
||||
test('validateLastName returns true for non-empty', () {
|
||||
cubit.setLastName('Иванов');
|
||||
expect(cubit.validateLastName(), true);
|
||||
expect(cubit.state.isLastNameValid, true);
|
||||
});
|
||||
|
||||
test('validateEmail returns false for empty', () {
|
||||
expect(cubit.validateEmail(), false);
|
||||
expect(cubit.state.isEmailValid, false);
|
||||
});
|
||||
|
||||
test('validateEmail returns true for non-empty', () {
|
||||
cubit.setEmail('test@test.com');
|
||||
expect(cubit.validateEmail(), true);
|
||||
expect(cubit.state.isEmailValid, true);
|
||||
});
|
||||
|
||||
test('validatePhone returns false for empty', () {
|
||||
expect(cubit.validatePhone(), false);
|
||||
expect(cubit.state.isPhoneValid, false);
|
||||
});
|
||||
|
||||
test('validatePhone returns true for non-empty', () {
|
||||
cubit.setPhone('+79001234567');
|
||||
expect(cubit.validatePhone(), true);
|
||||
expect(cubit.state.isPhoneValid, true);
|
||||
});
|
||||
|
||||
test('validateGender returns false for empty', () {
|
||||
expect(cubit.validateGender(), false);
|
||||
expect(cubit.state.isGenderValid, false);
|
||||
});
|
||||
|
||||
test('validateGender returns true for non-empty', () {
|
||||
cubit.setGender('male');
|
||||
expect(cubit.validateGender(), true);
|
||||
expect(cubit.state.isGenderValid, true);
|
||||
});
|
||||
|
||||
test('validateWeight returns false for 0', () {
|
||||
expect(cubit.validateWeight(), false);
|
||||
expect(cubit.state.isWeightValid, false);
|
||||
});
|
||||
|
||||
test('validateWeight returns true for non-zero', () {
|
||||
cubit.setWeight(70);
|
||||
expect(cubit.validateWeight(), true);
|
||||
expect(cubit.state.isWeightValid, true);
|
||||
});
|
||||
|
||||
test('validateHeight returns false for 0', () {
|
||||
expect(cubit.validateHeight(), false);
|
||||
expect(cubit.state.isHeightValid, false);
|
||||
});
|
||||
|
||||
test('validateHeight returns true for non-zero', () {
|
||||
cubit.setHeight(175);
|
||||
expect(cubit.validateHeight(), true);
|
||||
expect(cubit.state.isHeightValid, true);
|
||||
});
|
||||
|
||||
test('validateActivityCoefficient returns false for 0', () {
|
||||
// Set to 0 explicitly since default is 1.2
|
||||
cubit.setActivityCoefficient(0);
|
||||
expect(cubit.validateActivityCoefficient(), false);
|
||||
expect(cubit.state.isActivityCoefficientValid, false);
|
||||
});
|
||||
|
||||
test('validateActivityCoefficient returns true for non-zero', () {
|
||||
cubit.setActivityCoefficient(1.5);
|
||||
expect(cubit.validateActivityCoefficient(), true);
|
||||
expect(cubit.state.isActivityCoefficientValid, true);
|
||||
});
|
||||
|
||||
test('validateBirthday returns false for null', () {
|
||||
expect(cubit.validateBirthday(), false);
|
||||
expect(cubit.state.isBirthdayValid, false);
|
||||
});
|
||||
|
||||
test('validateBirthday returns true for non-null', () {
|
||||
cubit.setBirthday(DateTime(1990, 1, 1));
|
||||
expect(cubit.validateBirthday(), true);
|
||||
expect(cubit.state.isBirthdayValid, true);
|
||||
});
|
||||
});
|
||||
|
||||
group('navigation', () {
|
||||
test('nextStep sets step to 1', () {
|
||||
cubit.nextStep();
|
||||
expect(cubit.state.step, 1);
|
||||
});
|
||||
|
||||
test('prevStep sets step to 0', () {
|
||||
cubit.nextStep();
|
||||
cubit.prevStep();
|
||||
expect(cubit.state.step, 0);
|
||||
});
|
||||
});
|
||||
|
||||
group('validateAllFields', () {
|
||||
test('returns false when firstName empty', () {
|
||||
expect(cubit.validateAllFields(), false);
|
||||
});
|
||||
|
||||
test('returns true when all fields valid', () {
|
||||
cubit.setFirstName('Иван');
|
||||
cubit.setLastName('Иванов');
|
||||
cubit.setEmail('test@test.com');
|
||||
cubit.setPhone('+79001234567');
|
||||
cubit.setGender('male');
|
||||
cubit.setWeight(70);
|
||||
cubit.setHeight(175);
|
||||
cubit.setActivityCoefficient(1.2);
|
||||
cubit.setBirthday(DateTime(1990, 1, 1));
|
||||
expect(cubit.validateAllFields(), true);
|
||||
});
|
||||
});
|
||||
|
||||
group('register', () {
|
||||
test('returns (false, true) when validation fails', () async {
|
||||
final result = await cubit.register();
|
||||
expect(result, (false, true));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user