diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index f6a834455e..d9e936af4d 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -284,7 +284,7 @@ class MailboxController extends BaseMailboxController mailboxDashBoardController.updateRefreshAllMailboxState(Right(RefreshAllMailboxSuccess())); _handleCreateDefaultFolderIfMissing(mailboxDashBoardController.mapDefaultMailboxIdByRole); _handleDataFromNavigationRouter(); - mailboxDashBoardController.getSpamReportBanner(); + mailboxDashBoardController.refreshSpamReportBanner(); if (PlatformInfo.isIOS) { _updateMailboxIdsBlockNotificationToKeychain(success.mailboxList); } diff --git a/lib/features/mailbox_dashboard/data/datasource/spam_report_datasource.dart b/lib/features/mailbox_dashboard/data/datasource/spam_report_datasource.dart index bb443a74ac..210564ac6e 100644 --- a/lib/features/mailbox_dashboard/data/datasource/spam_report_datasource.dart +++ b/lib/features/mailbox_dashboard/data/datasource/spam_report_datasource.dart @@ -10,7 +10,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/unread_spa abstract class SpamReportDataSource { Future storeLastTimeDismissedSpamReported(DateTime lastTimeDismissedSpamReported); - Future getLastTimeDismissedSpamReported(); + Future getLastTimeDismissedSpamReportedMilliseconds(); Future deleteLastTimeDismissedSpamReported(); diff --git a/lib/features/mailbox_dashboard/data/datasource_impl/hive_spam_report_datasource_impl.dart b/lib/features/mailbox_dashboard/data/datasource_impl/hive_spam_report_datasource_impl.dart index 1a074d6f31..502d822b8b 100644 --- a/lib/features/mailbox_dashboard/data/datasource_impl/hive_spam_report_datasource_impl.dart +++ b/lib/features/mailbox_dashboard/data/datasource_impl/hive_spam_report_datasource_impl.dart @@ -28,7 +28,7 @@ class HiveSpamReportDataSourceImpl extends SpamReportDataSource { } @override - Future getLastTimeDismissedSpamReported() { + Future getLastTimeDismissedSpamReportedMilliseconds() { throw UnimplementedError(); } diff --git a/lib/features/mailbox_dashboard/data/datasource_impl/local_spam_report_datasource_impl.dart b/lib/features/mailbox_dashboard/data/datasource_impl/local_spam_report_datasource_impl.dart index d10c769511..c522c7e7df 100644 --- a/lib/features/mailbox_dashboard/data/datasource_impl/local_spam_report_datasource_impl.dart +++ b/lib/features/mailbox_dashboard/data/datasource_impl/local_spam_report_datasource_impl.dart @@ -21,12 +21,10 @@ class LocalSpamReportDataSourceImpl extends SpamReportDataSource { ); @override - Future getLastTimeDismissedSpamReported() async { + Future getLastTimeDismissedSpamReportedMilliseconds() async { return Future.sync(() async { final spamReportConfig = await _preferencesSettingManager.getSpamReportConfig(); - return DateTime.fromMillisecondsSinceEpoch( - spamReportConfig.lastTimeDismissedMilliseconds, - ); + return spamReportConfig.lastTimeDismissedMilliseconds; }).catchError(_exceptionThrower.throwException); } diff --git a/lib/features/mailbox_dashboard/data/repository/spam_report_repository_impl.dart b/lib/features/mailbox_dashboard/data/repository/spam_report_repository_impl.dart index aad9696f01..1eb6be8391 100644 --- a/lib/features/mailbox_dashboard/data/repository/spam_report_repository_impl.dart +++ b/lib/features/mailbox_dashboard/data/repository/spam_report_repository_impl.dart @@ -12,8 +12,8 @@ class SpamReportRepositoryImpl extends SpamReportRepository { SpamReportRepositoryImpl(this.mapDataSource); @override - Future getLastTimeDismissedSpamReported() async { - return await mapDataSource[DataSourceType.local]!.getLastTimeDismissedSpamReported(); + Future getLastTimeDismissedSpamReportedMilliseconds() async { + return await mapDataSource[DataSourceType.local]!.getLastTimeDismissedSpamReportedMilliseconds(); } @override diff --git a/lib/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart b/lib/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart index a361d9bc46..e334583527 100644 --- a/lib/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart +++ b/lib/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart @@ -1,10 +1,10 @@ import 'package:core/domain/exceptions/app_base_exception.dart'; -class NotFoundLastTimeDismissedSpamReportException extends AppBaseException { - NotFoundLastTimeDismissedSpamReportException([super.message]); +class SpamDismissCooldownActiveException extends AppBaseException { + SpamDismissCooldownActiveException([super.message]); @override - String get exceptionName => 'NotFoundLastTimeDismissedSpamReportException'; + String get exceptionName => 'SpamDismissCooldownActiveException'; } class NotFoundSpamMailboxCachedException extends AppBaseException { @@ -20,3 +20,10 @@ class NotFoundSpamMailboxException extends AppBaseException { @override String get exceptionName => 'NotFoundSpamMailboxException'; } + +class NoUnreadSpamEmailsException extends AppBaseException { + NoUnreadSpamEmailsException([super.message]); + + @override + String get exceptionName => 'NoUnreadSpamEmailsException'; +} diff --git a/lib/features/mailbox_dashboard/domain/repository/spam_report_repository.dart b/lib/features/mailbox_dashboard/domain/repository/spam_report_repository.dart index 39e37628ce..61064156af 100644 --- a/lib/features/mailbox_dashboard/domain/repository/spam_report_repository.dart +++ b/lib/features/mailbox_dashboard/domain/repository/spam_report_repository.dart @@ -6,7 +6,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/spam_repor abstract class SpamReportRepository { Future storeLastTimeDismissedSpamReported(DateTime lastTimeDismissedSpamReported); - Future getLastTimeDismissedSpamReported(); + Future getLastTimeDismissedSpamReportedMilliseconds(); Future deleteLastTimeDismissedSpamReported(); diff --git a/lib/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart b/lib/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart index f256506a19..99dd8c3189 100644 --- a/lib/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart @@ -18,5 +18,3 @@ class GetSpamMailboxCachedFailure extends FeatureFailure { GetSpamMailboxCachedFailure(exception) : super(exception: exception); } - -class InvalidSpamReportCondition extends FeatureFailure {} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart b/lib/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart index 89fb998578..f10187f838 100644 --- a/lib/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart +++ b/lib/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart @@ -1,15 +1,15 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/spam_report_repository.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart'; class GetSpamMailboxCachedInteractor { - static const int spamReportBannerDisplayIntervalInHour = 12; + static const int spamReportBannerDisplayIntervalInHours = 24; final SpamReportRepository _spamReportRepository; @@ -19,15 +19,19 @@ class GetSpamMailboxCachedInteractor { try { yield Right(GetSpamMailboxCachedLoading()); if (await _validateIntervalToShowBanner()) { - final spamMailbox = await _spamReportRepository.getSpamMailboxCached(accountId, userName); + final spamMailbox = await _spamReportRepository.getSpamMailboxCached(accountId, userName); final countUnreadSpamMailbox = spamMailbox.unreadEmails?.value.value.toInt() ?? 0; if (countUnreadSpamMailbox > 0) { yield Right(GetSpamMailboxCachedSuccess(spamMailbox)); } else { - yield Left(InvalidSpamReportCondition()); + yield Left( + GetSpamMailboxCachedFailure(NoUnreadSpamEmailsException()), + ); } } else { - yield Left(InvalidSpamReportCondition()); + yield Left( + GetSpamMailboxCachedFailure(SpamDismissCooldownActiveException()), + ); } } catch (e) { yield Left(GetSpamMailboxCachedFailure(e)); @@ -35,9 +39,23 @@ class GetSpamMailboxCachedInteractor { } Future _validateIntervalToShowBanner() async { - final lastTimeDismissedSpamReported = await _spamReportRepository.getLastTimeDismissedSpamReported(); - final currentTime = DateTime.now().difference(lastTimeDismissedSpamReported); - log('GetSpamMailboxCachedInteractor::_compareSpamReportTime:lastTimeDismissedSpamReported: $lastTimeDismissedSpamReported | currentTime: $currentTime'); - return currentTime.inHours > spamReportBannerDisplayIntervalInHour; + final lastTimeDismissedMs = await _spamReportRepository + .getLastTimeDismissedSpamReportedMilliseconds(); + + if (lastTimeDismissedMs <= 0) { + return true; + } + + final lastTime = DateTime.fromMillisecondsSinceEpoch(lastTimeDismissedMs); + final now = DateTime.now(); + final elapsed = now.difference(lastTime); + if (elapsed.isNegative) { + if (elapsed.abs() < const Duration(days: 1)) return false; + return true; + } + final isIntervalElapsed = + elapsed.inHours >= spamReportBannerDisplayIntervalInHours; + + return isIntervalElapsed; } -} \ No newline at end of file +} diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 5608f3ab0f..9858e0cdc1 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -2525,24 +2525,6 @@ class MailboxDashBoardController extends ReloadableController bool get enableSpamReport => spamReportController.enableSpamReport; - void getSpamReportBanner() { - if (enableSpamReport) { - final spamId = spamMailboxId; - if (spamId == null) { - spamReportController.setSpamPresentationMailbox(null); - return; - } - - final spamMailbox = mapMailboxById[spamId]; - final unreadEmails = spamMailbox?.unreadEmails?.value.value ?? 0; - if (unreadEmails > 0) { - spamReportController.setSpamPresentationMailbox(spamMailbox); - } else { - spamReportController.setSpamPresentationMailbox(null); - } - } - } - void refreshSpamReportBanner() { if (enableSpamReport && sessionCurrent != null && accountId.value != null) { spamReportController.getSpamMailboxCached(accountId.value!, sessionCurrent!.username); diff --git a/lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart index de635206de..cb2705d4aa 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart @@ -9,6 +9,7 @@ import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/spam_report_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_spam_report_state.dart'; @@ -75,7 +76,7 @@ class SpamReportController extends BaseController { @override void handleFailureViewState(Failure failure) { if (failure is GetSpamMailboxCachedFailure) { - presentationSpamMailbox.value = null; + _validateSpamMailboxChanged(failure); } else if (failure is GetSpamReportStateFailure) { _spamReportLoaderStatus = LoaderStatus.completed; } else { @@ -147,6 +148,16 @@ class SpamReportController extends BaseController { presentationSpamMailbox.value = spamMailbox; } + void _validateSpamMailboxChanged(GetSpamMailboxCachedFailure failure) { + if (failure.exception is NoUnreadSpamEmailsException) { + final currentSpamMailbox = presentationSpamMailbox.value; + if (currentSpamMailbox != null && currentSpamMailbox.countUnreadEmails > 0) { + _storeLastTimeDismissedSpamReportedAction(); + } + } + setSpamPresentationMailbox(null); + } + @override void onClose() { _appLifecycleListener?.dispose(); diff --git a/test/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor_test.dart b/test/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor_test.dart new file mode 100644 index 0000000000..f531cbe39c --- /dev/null +++ b/test/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor_test.dart @@ -0,0 +1,246 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/spam_report_repository.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart'; + +import 'get_spam_mailbox_cached_interactor_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + final accountId = AccountId(Id('account-1')); + final userName = UserName('user@example.com'); + + late MockSpamReportRepository spamReportRepository; + late GetSpamMailboxCachedInteractor interactor; + + setUp(() { + spamReportRepository = MockSpamReportRepository(); + interactor = GetSpamMailboxCachedInteractor(spamReportRepository); + }); + + Mailbox makeSpamMailbox({required int unreadCount}) => Mailbox( + id: MailboxId(Id('spam-id')), + unreadEmails: + unreadCount > 0 ? UnreadEmails(UnsignedInt(unreadCount)) : null, + ); + + int msAgo(int hours) => + DateTime.now().subtract(Duration(hours: hours)).millisecondsSinceEpoch; + + // Predicate matchers for Left states — EquatableMixin compares exception by value, + // so isA<>() inside Left(...) breaks equality. Use predicate instead. + Matcher leftWithException() => predicate>( + (either) => either.fold( + (f) => f is GetSpamMailboxCachedFailure && f.exception is E, + (_) => false, + ), + 'Left(GetSpamMailboxCachedFailure($E))', + ); + + group('GetSpamMailboxCachedInteractor', () { + group('first-time user (no stored dismiss timestamp)', () { + test('shows banner when there are unread spam emails', () { + final spamMailbox = makeSpamMailbox(unreadCount: 5); + when(spamReportRepository + .getLastTimeDismissedSpamReportedMilliseconds()) + .thenAnswer((_) async => 0); + when(spamReportRepository.getSpamMailboxCached(accountId, userName)) + .thenAnswer((_) async => spamMailbox); + + expect( + interactor.execute(accountId, userName), + emitsInOrder([ + Right(GetSpamMailboxCachedLoading()), + Right(GetSpamMailboxCachedSuccess(spamMailbox)), + ]), + ); + }); + + test('does not show banner when spam folder has no unread emails', () { + final spamMailbox = makeSpamMailbox(unreadCount: 0); + when(spamReportRepository + .getLastTimeDismissedSpamReportedMilliseconds()) + .thenAnswer((_) async => 0); + when(spamReportRepository.getSpamMailboxCached(accountId, userName)) + .thenAnswer((_) async => spamMailbox); + + expect( + interactor.execute(accountId, userName), + emitsInOrder([ + Right(GetSpamMailboxCachedLoading()), + leftWithException(), + ]), + ); + }); + }); + + group('cooldown active (dismissed less than 24h ago)', () { + test('does not show banner when dismissed 13 hours ago', () { + when(spamReportRepository + .getLastTimeDismissedSpamReportedMilliseconds()) + .thenAnswer((_) async => msAgo(13)); + + expect( + interactor.execute(accountId, userName), + emitsInOrder([ + Right(GetSpamMailboxCachedLoading()), + leftWithException(), + ]), + ); + }); + + test('does not show banner when dismissed 1 hour ago', () { + when(spamReportRepository + .getLastTimeDismissedSpamReportedMilliseconds()) + .thenAnswer((_) async => msAgo(1)); + + expect( + interactor.execute(accountId, userName), + emitsInOrder([ + Right(GetSpamMailboxCachedLoading()), + leftWithException(), + ]), + ); + }); + }); + + group('cooldown expired (dismissed 24h ago)', () { + test('shows banner when dismissed exactly 24 hours ago and unread > 0', + () { + final spamMailbox = makeSpamMailbox(unreadCount: 3); + when(spamReportRepository + .getLastTimeDismissedSpamReportedMilliseconds()) + .thenAnswer((_) async => msAgo(24)); + when(spamReportRepository.getSpamMailboxCached(accountId, userName)) + .thenAnswer((_) async => spamMailbox); + + expect( + interactor.execute(accountId, userName), + emitsInOrder([ + Right(GetSpamMailboxCachedLoading()), + Right(GetSpamMailboxCachedSuccess(spamMailbox)), + ]), + ); + }); + + test('shows banner when dismissed 25 hours ago and unread > 0', () { + final spamMailbox = makeSpamMailbox(unreadCount: 3); + when(spamReportRepository + .getLastTimeDismissedSpamReportedMilliseconds()) + .thenAnswer((_) async => msAgo(25)); + when(spamReportRepository.getSpamMailboxCached(accountId, userName)) + .thenAnswer((_) async => spamMailbox); + + expect( + interactor.execute(accountId, userName), + emitsInOrder([ + Right(GetSpamMailboxCachedLoading()), + Right(GetSpamMailboxCachedSuccess(spamMailbox)), + ]), + ); + }); + + test('does not show banner when dismissed 25 hours ago but unread = 0', + () { + final spamMailbox = makeSpamMailbox(unreadCount: 0); + when(spamReportRepository + .getLastTimeDismissedSpamReportedMilliseconds()) + .thenAnswer((_) async => msAgo(25)); + when(spamReportRepository.getSpamMailboxCached(accountId, userName)) + .thenAnswer((_) async => spamMailbox); + + expect( + interactor.execute(accountId, userName), + emitsInOrder([ + Right(GetSpamMailboxCachedLoading()), + leftWithException(), + ]), + ); + }); + }); + + group('clock-skew (future timestamp)', () { + test( + 'does not show banner when stored timestamp is 12 hours in the future (< 1 day clock skew)', + () { + when(spamReportRepository + .getLastTimeDismissedSpamReportedMilliseconds()) + .thenAnswer((_) async => msAgo(-12)); + + expect( + interactor.execute(accountId, userName), + emitsInOrder([ + Right(GetSpamMailboxCachedLoading()), + leftWithException(), + ]), + ); + }); + + test( + 'shows banner when stored timestamp is 25 hours in the future (>= 1 day clock skew, likely clock reset)', + () { + final spamMailbox = makeSpamMailbox(unreadCount: 3); + when(spamReportRepository + .getLastTimeDismissedSpamReportedMilliseconds()) + .thenAnswer((_) async => msAgo(-25)); + when(spamReportRepository.getSpamMailboxCached(accountId, userName)) + .thenAnswer((_) async => spamMailbox); + + expect( + interactor.execute(accountId, userName), + emitsInOrder([ + Right(GetSpamMailboxCachedLoading()), + Right(GetSpamMailboxCachedSuccess(spamMailbox)), + ]), + ); + }); + }); + + group('error handling', () { + test('wraps repository exception in GetSpamMailboxCachedFailure', () { + final exception = Exception('cache error'); + when(spamReportRepository + .getLastTimeDismissedSpamReportedMilliseconds()) + .thenThrow(exception); + + expect( + interactor.execute(accountId, userName), + emitsInOrder([ + Right(GetSpamMailboxCachedLoading()), + Left(GetSpamMailboxCachedFailure(exception)), + ]), + ); + }); + + test( + 'wraps getSpamMailboxCached exception in GetSpamMailboxCachedFailure', + () { + final exception = Exception('mailbox not found'); + when(spamReportRepository + .getLastTimeDismissedSpamReportedMilliseconds()) + .thenAnswer((_) async => 0); + when(spamReportRepository.getSpamMailboxCached(accountId, userName)) + .thenThrow(exception); + + expect( + interactor.execute(accountId, userName), + emitsInOrder([ + Right(GetSpamMailboxCachedLoading()), + Left(GetSpamMailboxCachedFailure(exception)), + ]), + ); + }); + }); + }); +} diff --git a/test/features/mailbox_dashboard/presentation/controller/spam_report_controller_test.dart b/test/features/mailbox_dashboard/presentation/controller/spam_report_controller_test.dart new file mode 100644 index 0000000000..6cac50d73b --- /dev/null +++ b/test/features/mailbox_dashboard/presentation/controller/spam_report_controller_test.dart @@ -0,0 +1,196 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/app_toast.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart' as jmap_mailbox; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/caching/caching_manager.dart'; +import 'package:core/data/network/config/dynamic_url_interceptors.dart'; +import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_spam_report_state_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/store_last_time_dismissed_spam_reported_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/store_spam_report_state_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart'; +import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; +import 'package:tmail_ui_user/main/utils/toast_manager.dart'; +import 'package:tmail_ui_user/main/utils/twake_app_manager.dart'; +import 'package:uuid/uuid.dart'; + +import 'spam_report_controller_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + // BaseController dependencies + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +void main() { + late MockStoreSpamReportInteractor storeSpamReportInteractor; + late MockStoreSpamReportStateInteractor storeSpamReportStateInteractor; + late MockGetSpamReportStateInteractor getSpamReportStateInteractor; + late MockGetSpamMailboxCachedInteractor getSpamMailboxCachedInteractor; + late SpamReportController controller; + + PresentationMailbox makeMailboxWithUnread(int count) => PresentationMailbox( + jmap_mailbox.MailboxId(Id('spam')), + unreadEmails: jmap_mailbox.UnreadEmails(UnsignedInt(count)), + ); + + void putBaseControllerDependencies() { + Get.put(MockCachingManager()); + Get.put(MockLanguageCacheManager()); + final authInterceptors = MockAuthorizationInterceptors(); + Get.put(authInterceptors); + Get.put(authInterceptors, tag: BindingTag.isolateTag); + Get.put(MockDynamicUrlInterceptors()); + Get.put(MockDeleteCredentialInteractor()); + Get.put(MockLogoutOidcInteractor()); + Get.put(MockDeleteAuthorityOidcInteractor()); + Get.put(MockAppToast()); + Get.put(MockImagePaths()); + Get.put(MockResponsiveUtils()); + Get.put(const Uuid()); + Get.put(MockToastManager()); + Get.put(MockTwakeAppManager()); + } + + setUp(() { + storeSpamReportInteractor = MockStoreSpamReportInteractor(); + storeSpamReportStateInteractor = MockStoreSpamReportStateInteractor(); + getSpamReportStateInteractor = MockGetSpamReportStateInteractor(); + getSpamMailboxCachedInteractor = MockGetSpamMailboxCachedInteractor(); + + when(storeSpamReportInteractor.execute(any)) + .thenAnswer((_) => const Stream.empty()); + when(getSpamReportStateInteractor.execute()) + .thenAnswer((_) => const Stream.empty()); + + putBaseControllerDependencies(); + + controller = SpamReportController( + storeSpamReportInteractor, + storeSpamReportStateInteractor, + getSpamReportStateInteractor, + getSpamMailboxCachedInteractor, + ); + Get.put(controller); + }); + + tearDown(Get.deleteAll); + + Stream> failureStream(Exception exception) => + Stream.fromIterable([ + Right(GetSpamMailboxCachedLoading()), + Left(GetSpamMailboxCachedFailure(exception)), + ]); + + final testAccountId = AccountId(Id('acc')); + final testUserName = UserName('user@example.com'); + + group('SpamReportController._validateSpamMailboxChanged', () { + group('NoUnreadSpamEmailsException', () { + test('stores dismissal time when banner was showing with unread > 0', () async { + controller.setSpamPresentationMailbox(makeMailboxWithUnread(3)); + + when(getSpamMailboxCachedInteractor.execute(any, any)) + .thenAnswer((_) => failureStream(NoUnreadSpamEmailsException())); + + controller.getSpamMailboxCached(testAccountId, testUserName); + await untilCalled(storeSpamReportInteractor.execute(any)); + + verify(storeSpamReportInteractor.execute(any)).called(1); + expect(controller.presentationSpamMailbox.value, isNull); + }); + + test('does not store dismissal when banner was not showing', () async { + controller.setSpamPresentationMailbox(null); + + when(getSpamMailboxCachedInteractor.execute(any, any)) + .thenAnswer((_) => failureStream(NoUnreadSpamEmailsException())); + + controller.getSpamMailboxCached(testAccountId, testUserName); + await pumpEventQueue(); + + verifyNever(storeSpamReportInteractor.execute(any)); + expect(controller.presentationSpamMailbox.value, isNull); + }); + + test('does not store dismissal when banner mailbox has 0 unread', () async { + controller.setSpamPresentationMailbox(makeMailboxWithUnread(0)); + + when(getSpamMailboxCachedInteractor.execute(any, any)) + .thenAnswer((_) => failureStream(NoUnreadSpamEmailsException())); + + controller.getSpamMailboxCached(testAccountId, testUserName); + await pumpEventQueue(); + + verifyNever(storeSpamReportInteractor.execute(any)); + expect(controller.presentationSpamMailbox.value, isNull); + }); + }); + + group('SpamDismissCooldownActiveException', () { + test('hides banner without storing dismissal time', () async { + controller.setSpamPresentationMailbox(makeMailboxWithUnread(5)); + + when(getSpamMailboxCachedInteractor.execute(any, any)) + .thenAnswer((_) => failureStream(SpamDismissCooldownActiveException())); + + controller.getSpamMailboxCached(testAccountId, testUserName); + await pumpEventQueue(); + + verifyNever(storeSpamReportInteractor.execute(any)); + expect(controller.presentationSpamMailbox.value, isNull); + }); + }); + + group('GetSpamMailboxCachedSuccess', () { + test('shows banner with the returned mailbox', () async { + final domainMailbox = jmap_mailbox.Mailbox( + id: jmap_mailbox.MailboxId(Id('spam')), + unreadEmails: jmap_mailbox.UnreadEmails(UnsignedInt(7)), + ); + when(getSpamMailboxCachedInteractor.execute(any, any)).thenAnswer((_) => + Stream.fromIterable([ + Right(GetSpamMailboxCachedLoading()), + Right(GetSpamMailboxCachedSuccess(domainMailbox)), + ])); + + controller.getSpamMailboxCached(testAccountId, testUserName); + await pumpEventQueue(); + + expect(controller.presentationSpamMailbox.value, isNotNull); + }); + }); + }); +}