From 288181e9436217f88c48b61d756d59135af4eb78 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Sun, 28 Jun 2026 14:51:26 +0900 Subject: [PATCH] refactor: inject core service dependencies --- lib/core/dio/app_dio.dart | 39 ++++-- .../dio/interceptors/token_interceptor.dart | 20 +-- .../token_session_invalidator.dart | 3 + lib/core/services/notification_service.dart | 57 ++++---- .../services/notification_tap_router.dart | 43 ++++++ .../notification_token_registrar.dart | 10 ++ .../services/device_fcm_token_registrar.dart | 27 ++++ .../token_local_session_invalidator.dart | 15 +++ lib/main.dart | 11 +- .../interceptors/token_interceptor_test.dart | 105 +++------------ .../logging_transformer_test.dart | 18 +-- .../services/notification_service_test.dart | 123 +++++------------- .../notification_tap_router_test.dart | 47 +++++++ .../device_fcm_token_registrar_test.dart | 43 ++++++ 14 files changed, 321 insertions(+), 240 deletions(-) create mode 100644 lib/core/dio/interceptors/token_session_invalidator.dart create mode 100644 lib/core/services/notification_tap_router.dart create mode 100644 lib/core/services/notification_token_registrar.dart create mode 100644 lib/data/services/device_fcm_token_registrar.dart create mode 100644 lib/data/services/token_local_session_invalidator.dart create mode 100644 test/core/services/notification_tap_router_test.dart create mode 100644 test/data/services/device_fcm_token_registrar_test.dart diff --git a/lib/core/dio/app_dio.dart b/lib/core/dio/app_dio.dart index 2369a3b1..f02ea1a2 100644 --- a/lib/core/dio/app_dio.dart +++ b/lib/core/dio/app_dio.dart @@ -4,26 +4,39 @@ import 'package:on_time_front/core/constants/environment_variable.dart'; import 'package:on_time_front/core/dio/adapters/shared.dart'; import 'package:on_time_front/core/dio/interceptors/logger_interceptor.dart'; import 'package:on_time_front/core/dio/interceptors/token_interceptor.dart'; +import 'package:on_time_front/core/dio/interceptors/token_session_invalidator.dart'; import 'package:on_time_front/core/dio/transformers/logging_transformer.dart'; +import 'package:on_time_front/data/data_sources/token_local_data_source.dart'; @Injectable(as: Dio) class AppDio with DioMixin implements Dio { - AppDio() { + AppDio( + TokenLocalDataSource tokenLocalDataSource, + TokenSessionInvalidator sessionInvalidator, + ) { httpClientAdapter = getAdapter(); transformer = LoggingTransformer(inner: BackgroundTransformer()); options = BaseOptions( - contentType: Headers.jsonContentType, - baseUrl: EnvironmentVariable.restApiUrl, - connectTimeout: const Duration(milliseconds: 30000), - receiveTimeout: const Duration(milliseconds: 30000), - sendTimeout: const Duration(milliseconds: 30000), - receiveDataWhenStatusError: true, - followRedirects: false, - headers: { - "Accept": "application/json", - "Authorization": EnvironmentVariable.restAuthToken - }); + contentType: Headers.jsonContentType, + baseUrl: EnvironmentVariable.restApiUrl, + connectTimeout: const Duration(milliseconds: 30000), + receiveTimeout: const Duration(milliseconds: 30000), + sendTimeout: const Duration(milliseconds: 30000), + receiveDataWhenStatusError: true, + followRedirects: false, + headers: { + "Accept": "application/json", + "Authorization": EnvironmentVariable.restAuthToken, + }, + ); - interceptors.addAll([TokenInterceptor(this), LoggerInterceptor()]); + interceptors.addAll([ + TokenInterceptor( + this, + tokenLocalDataSource: tokenLocalDataSource, + sessionInvalidator: sessionInvalidator, + ), + LoggerInterceptor(), + ]); } } diff --git a/lib/core/dio/interceptors/token_interceptor.dart b/lib/core/dio/interceptors/token_interceptor.dart index 1ab69826..7b3df3a6 100644 --- a/lib/core/dio/interceptors/token_interceptor.dart +++ b/lib/core/dio/interceptors/token_interceptor.dart @@ -1,10 +1,9 @@ import 'package:dio/dio.dart'; import 'package:on_time_front/core/constants/endpoint.dart'; -import 'package:on_time_front/core/di/di_setup.dart'; +import 'package:on_time_front/core/dio/interceptors/token_session_invalidator.dart'; import 'package:on_time_front/core/logging/app_logger.dart'; import 'package:on_time_front/data/data_sources/token_local_data_source.dart'; import 'package:on_time_front/domain/entities/token_entity.dart'; -import 'package:on_time_front/domain/repositories/user_repository.dart'; class TokenInterceptor implements InterceptorsWrapper { static const _refreshTokenPath = '/refresh-token'; @@ -13,9 +12,14 @@ class TokenInterceptor implements InterceptorsWrapper { 'Authentication token is unavailable for a protected request'; final Dio dio; - TokenInterceptor(this.dio); - final TokenLocalDataSource tokenLocalDataSource = getIt - .get(); + final TokenLocalDataSource tokenLocalDataSource; + final TokenSessionInvalidator _sessionInvalidator; + + TokenInterceptor( + this.dio, { + required this.tokenLocalDataSource, + required TokenSessionInvalidator sessionInvalidator, + }) : _sessionInvalidator = sessionInvalidator; // when accessToken is expired & having multiple requests call // this variable to lock others request to make sure only trigger call refresh token 01 times @@ -186,11 +190,7 @@ class TokenInterceptor implements InterceptorsWrapper { } Future _signOutLocally() async { - try { - await getIt.get().signOut(); - } catch (_) { - await tokenLocalDataSource.deleteToken(); - } + await _sessionInvalidator.signOutLocally(); } @override diff --git a/lib/core/dio/interceptors/token_session_invalidator.dart b/lib/core/dio/interceptors/token_session_invalidator.dart new file mode 100644 index 00000000..1e07debc --- /dev/null +++ b/lib/core/dio/interceptors/token_session_invalidator.dart @@ -0,0 +1,3 @@ +abstract interface class TokenSessionInvalidator { + Future signOutLocally(); +} diff --git a/lib/core/services/notification_service.dart b/lib/core/services/notification_service.dart index 8f65c2c9..67c3dd93 100644 --- a/lib/core/services/notification_service.dart +++ b/lib/core/services/notification_service.dart @@ -8,16 +8,13 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:on_time_front/core/di/di_setup.dart'; import 'package:on_time_front/core/logging/app_logger.dart'; import 'package:on_time_front/core/services/js_interop_service.dart'; -import 'package:on_time_front/core/services/navigation_service.dart'; import 'package:on_time_front/core/services/notification_content.dart'; import 'package:on_time_front/core/services/notification_routing.dart'; -import 'package:on_time_front/data/data_sources/notification_remote_data_source.dart'; -import 'package:on_time_front/data/models/fcm_token_register_request_model.dart'; +import 'package:on_time_front/core/services/notification_tap_router.dart'; +import 'package:on_time_front/core/services/notification_token_registrar.dart'; import 'package:on_time_front/domain/entities/alarm_entities.dart'; -import 'package:on_time_front/domain/repositories/alarm_repository.dart'; import 'package:permission_handler/permission_handler.dart' as permission_handler; import 'package:timezone/data/latest.dart' as tz_data; @@ -43,11 +40,16 @@ class NotificationService { FlutterLocalNotificationsPlugin? localNotifications, String Function()? localeProvider, bool? isIOSOverride, + FcmTokenRegistrar? fcmTokenRegistrar, + NotificationTapRouter? notificationTapRouter, }) : _messaging = messaging ?? FirebaseMessaging.instance, _localNotifications = localNotifications ?? FlutterLocalNotificationsPlugin(), _localeProvider = localeProvider, - _isIOSOverride = isIOSOverride; + _isIOSOverride = isIOSOverride, + _fcmTokenRegistrar = fcmTokenRegistrar ?? const NoopFcmTokenRegistrar(), + _notificationTapRouter = + notificationTapRouter ?? const NoopNotificationTapRouter(); @visibleForTesting NotificationService.test({ @@ -57,10 +59,15 @@ class NotificationService { bool isFlutterLocalNotificationsInitialized = false, bool isTimezoneInitialized = false, bool? isIOSOverride, + FcmTokenRegistrar? fcmTokenRegistrar, + NotificationTapRouter? notificationTapRouter, }) : _messaging = messaging, _localNotifications = localNotifications, _localeProvider = localeProvider, _isIOSOverride = isIOSOverride, + _fcmTokenRegistrar = fcmTokenRegistrar ?? const NoopFcmTokenRegistrar(), + _notificationTapRouter = + notificationTapRouter ?? const NoopNotificationTapRouter(), _isFlutterLocalNotificationsInitialized = isFlutterLocalNotificationsInitialized, _isTimezoneInitialized = isTimezoneInitialized; @@ -74,6 +81,8 @@ class NotificationService { final FlutterLocalNotificationsPlugin _localNotifications; final String Function()? _localeProvider; final bool? _isIOSOverride; + FcmTokenRegistrar _fcmTokenRegistrar; + NotificationTapRouter _notificationTapRouter; bool _isFlutterLocalNotificationsInitialized = false; bool _isTimezoneInitialized = false; @@ -92,6 +101,14 @@ class NotificationService { } } + void configureDelegates({ + required FcmTokenRegistrar fcmTokenRegistrar, + required NotificationTapRouter notificationTapRouter, + }) { + _fcmTokenRegistrar = fcmTokenRegistrar; + _notificationTapRouter = notificationTapRouter; + } + Future initialize() async { try { FirebaseMessaging.onBackgroundMessage( @@ -216,13 +233,7 @@ class NotificationService { if (token != null) { try { - final deviceId = await getIt.get().getDeviceId(); - await getIt.get().fcmTokenRegister( - FcmTokenRegisterRequestModel( - firebaseToken: token, - deviceId: deviceId, - ), - ); + await _fcmTokenRegistrar.registerToken(token); AppLogger.debug('[FCM] FCM Token 서버 등록 완료'); } catch (e) { AppLogger.debug('[FCM] FCM Token 서버 등록 실패: $e'); @@ -233,12 +244,9 @@ class NotificationService { AppLogger.debug( '[FCM] token refreshed token=${AppLogger.redactToken(newToken)}', ); - getIt.get().getDeviceId().then((deviceId) { - getIt.get().fcmTokenRegister( - FcmTokenRegisterRequestModel( - firebaseToken: newToken, - deviceId: deviceId, - ), + _fcmTokenRegistrar.registerToken(newToken).catchError((e) { + AppLogger.debug( + '[FCM] refreshed token server registration failed: $e', ); }); }); @@ -631,18 +639,11 @@ class NotificationService { void _handleLocalNotificationTap(String? payload) { AppLogger.debug('[FCM] 알림 탭'); - final target = notificationRouteForPayloadString(payload); - if (target != null) { - getIt.get().push(target.path, extra: target.extra); - } + _notificationTapRouter.routeLocalNotificationTap(payload); } Future _handleBackgroundMessage(RemoteMessage message) async { AppLogger.debug('[FCM] 백그라운드 메시지 처리'); - - final target = notificationRouteForData(message.data); - if (target != null) { - getIt.get().push(target.path, extra: target.extra); - } + _notificationTapRouter.routeRemoteNotificationData(message.data); } } diff --git a/lib/core/services/notification_tap_router.dart b/lib/core/services/notification_tap_router.dart new file mode 100644 index 00000000..176bee4b --- /dev/null +++ b/lib/core/services/notification_tap_router.dart @@ -0,0 +1,43 @@ +import 'package:injectable/injectable.dart'; +import 'package:on_time_front/core/services/navigation_service.dart'; +import 'package:on_time_front/core/services/notification_routing.dart'; + +abstract interface class NotificationTapRouter { + void routeLocalNotificationTap(String? payload); + + void routeRemoteNotificationData(Map data); +} + +class NoopNotificationTapRouter implements NotificationTapRouter { + const NoopNotificationTapRouter(); + + @override + void routeLocalNotificationTap(String? payload) {} + + @override + void routeRemoteNotificationData(Map data) {} +} + +@Singleton(as: NotificationTapRouter) +class NavigationNotificationTapRouter implements NotificationTapRouter { + NavigationNotificationTapRouter(this._navigationService); + + final NavigationService _navigationService; + + @override + void routeLocalNotificationTap(String? payload) { + final target = notificationRouteForPayloadString(payload); + _pushTarget(target); + } + + @override + void routeRemoteNotificationData(Map data) { + final target = notificationRouteForData(data); + _pushTarget(target); + } + + void _pushTarget(NotificationRouteTarget? target) { + if (target == null) return; + _navigationService.push(target.path, extra: target.extra); + } +} diff --git a/lib/core/services/notification_token_registrar.dart b/lib/core/services/notification_token_registrar.dart new file mode 100644 index 00000000..40acb189 --- /dev/null +++ b/lib/core/services/notification_token_registrar.dart @@ -0,0 +1,10 @@ +abstract interface class FcmTokenRegistrar { + Future registerToken(String firebaseToken); +} + +class NoopFcmTokenRegistrar implements FcmTokenRegistrar { + const NoopFcmTokenRegistrar(); + + @override + Future registerToken(String firebaseToken) async {} +} diff --git a/lib/data/services/device_fcm_token_registrar.dart b/lib/data/services/device_fcm_token_registrar.dart new file mode 100644 index 00000000..8729d777 --- /dev/null +++ b/lib/data/services/device_fcm_token_registrar.dart @@ -0,0 +1,27 @@ +import 'package:injectable/injectable.dart'; +import 'package:on_time_front/core/services/notification_token_registrar.dart'; +import 'package:on_time_front/data/data_sources/notification_remote_data_source.dart'; +import 'package:on_time_front/data/models/fcm_token_register_request_model.dart'; +import 'package:on_time_front/domain/repositories/alarm_repository.dart'; + +@Singleton(as: FcmTokenRegistrar) +class DeviceFcmTokenRegistrar implements FcmTokenRegistrar { + DeviceFcmTokenRegistrar( + this._alarmRepository, + this._notificationRemoteDataSource, + ); + + final AlarmRepository _alarmRepository; + final NotificationRemoteDataSource _notificationRemoteDataSource; + + @override + Future registerToken(String firebaseToken) async { + final deviceId = await _alarmRepository.getDeviceId(); + await _notificationRemoteDataSource.fcmTokenRegister( + FcmTokenRegisterRequestModel( + firebaseToken: firebaseToken, + deviceId: deviceId, + ), + ); + } +} diff --git a/lib/data/services/token_local_session_invalidator.dart b/lib/data/services/token_local_session_invalidator.dart new file mode 100644 index 00000000..f1ab9090 --- /dev/null +++ b/lib/data/services/token_local_session_invalidator.dart @@ -0,0 +1,15 @@ +import 'package:injectable/injectable.dart'; +import 'package:on_time_front/core/dio/interceptors/token_session_invalidator.dart'; +import 'package:on_time_front/data/data_sources/token_local_data_source.dart'; + +@Singleton(as: TokenSessionInvalidator) +class TokenLocalSessionInvalidator implements TokenSessionInvalidator { + TokenLocalSessionInvalidator(this._tokenLocalDataSource); + + final TokenLocalDataSource _tokenLocalDataSource; + + @override + Future signOutLocally() { + return _tokenLocalDataSource.deleteToken(); + } +} diff --git a/lib/main.dart b/lib/main.dart index f0d57fbd..bf4c7c16 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,9 @@ import 'package:intl/date_symbol_data_local.dart'; import 'package:on_time_front/core/di/di_setup.dart'; import 'package:on_time_front/core/logging/app_logger.dart'; import 'package:on_time_front/core/services/device_info_service/shared.dart'; +import 'package:on_time_front/core/services/notification_service.dart'; +import 'package:on_time_front/core/services/notification_tap_router.dart'; +import 'package:on_time_front/core/services/notification_token_registrar.dart'; import 'package:on_time_front/firebase_options.dart'; import 'package:on_time_front/presentation/app/screens/app.dart'; @@ -13,11 +16,13 @@ void main() async { AppLogger.configureFlutterDebugPrint(); await HardwareKeyboard.instance.syncKeyboardState().catchError((_) {}); await initializeDateFormatting(); - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); AppLogger.debug('[FCM Main] Firebase initialized'); configureDependencies(); + NotificationService.instance.configureDelegates( + fcmTokenRegistrar: getIt.get(), + notificationTapRouter: getIt.get(), + ); AppLogger.debug( 'Device standalone mode=${DeviceInfoService.isInStandaloneMode}', diff --git a/test/core/dio/interceptors/token_interceptor_test.dart b/test/core/dio/interceptors/token_interceptor_test.dart index 33148b45..e9acd022 100644 --- a/test/core/dio/interceptors/token_interceptor_test.dart +++ b/test/core/dio/interceptors/token_interceptor_test.dart @@ -3,30 +3,21 @@ import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:get_it/get_it.dart'; -import 'package:google_sign_in/google_sign_in.dart'; import 'package:on_time_front/core/constants/endpoint.dart'; import 'package:on_time_front/core/dio/interceptors/token_interceptor.dart'; +import 'package:on_time_front/core/dio/interceptors/token_session_invalidator.dart'; import 'package:on_time_front/data/data_sources/token_local_data_source.dart'; import 'package:on_time_front/domain/entities/token_entity.dart'; -import 'package:on_time_front/domain/entities/user_entity.dart'; -import 'package:on_time_front/domain/repositories/user_repository.dart'; void main() { - final getIt = GetIt.instance; - late Dio dio; late _FakeTokenLocalDataSource tokenLocalDataSource; - late _FakeUserRepository userRepository; + late _FakeTokenSessionInvalidator sessionInvalidator; late _TokenRefreshAdapter adapter; setUp(() async { - await getIt.reset(); - tokenLocalDataSource = _FakeTokenLocalDataSource(); - userRepository = _FakeUserRepository(tokenLocalDataSource); - getIt.registerSingleton(tokenLocalDataSource); - getIt.registerSingleton(userRepository); + sessionInvalidator = _FakeTokenSessionInvalidator(tokenLocalDataSource); adapter = _TokenRefreshAdapter(); dio = Dio( @@ -35,11 +26,13 @@ void main() { receiveDataWhenStatusError: true, ), )..httpClientAdapter = adapter; - dio.interceptors.add(TokenInterceptor(dio)); - }); - - tearDown(() async { - await getIt.reset(); + dio.interceptors.add( + TokenInterceptor( + dio, + tokenLocalDataSource: tokenLocalDataSource, + sessionInvalidator: sessionInvalidator, + ), + ); }); test( @@ -105,7 +98,7 @@ void main() { ); expect(adapter.refreshRequests, 1); - expect(userRepository.signOutCalled, isFalse); + expect(sessionInvalidator.signOutCalled, isFalse); }); test('locally signs out when refresh token request returns 401', () async { @@ -122,7 +115,7 @@ void main() { containsAll(['/protected', '/refresh-token']), ); expect(adapter.refreshRequests, 1); - expect(userRepository.signOutCalled, isTrue); + expect(sessionInvalidator.signOutCalled, isTrue); expect(tokenLocalDataSource.deleteTokenCalled, isTrue); }); @@ -155,7 +148,7 @@ void main() { ); expect(adapter.requestedPaths, isEmpty); - expect(userRepository.signOutCalled, isFalse); + expect(sessionInvalidator.signOutCalled, isFalse); expect(tokenLocalDataSource.deleteTokenCalled, isFalse); }, ); @@ -172,7 +165,7 @@ void main() { ); expect(adapter.refreshRequests, 1); - expect(userRepository.signOutCalled, isTrue); + expect(sessionInvalidator.signOutCalled, isTrue); expect(tokenLocalDataSource.deleteTokenCalled, isTrue); }, ); @@ -295,79 +288,15 @@ class _FakeTokenLocalDataSource implements TokenLocalDataSource { } } -class _FakeUserRepository implements UserRepository { - _FakeUserRepository(this._tokenLocalDataSource); +class _FakeTokenSessionInvalidator implements TokenSessionInvalidator { + _FakeTokenSessionInvalidator(this._tokenLocalDataSource); final TokenLocalDataSource _tokenLocalDataSource; bool signOutCalled = false; @override - Future signOut() async { + Future signOutLocally() async { signOutCalled = true; await _tokenLocalDataSource.deleteToken(); } - - @override - Stream get userStream => const Stream.empty(); - - @override - Stream get googleAuthenticationEvents => - const Stream.empty(); - - @override - bool get supportsGoogleAuthenticate => false; - - @override - Future authenticateWithGoogle() => - throw UnimplementedError(); - - @override - Future initializeGoogleSignIn() async {} - - @override - Future deleteAppleUser({String? feedbackMessage}) => - throw UnimplementedError(); - - @override - Future deleteGoogleUser({String? feedbackMessage}) => - throw UnimplementedError(); - - @override - Future deleteUser({String? feedbackMessage}) => - throw UnimplementedError(); - - @override - Future disconnectGoogleSignIn() => throw UnimplementedError(); - - @override - Future getUser() => throw UnimplementedError(); - - @override - Future getUserSocialType() => throw UnimplementedError(); - - @override - Future postFeedback(String message) => throw UnimplementedError(); - - @override - Future signIn({required String email, required String password}) => - throw UnimplementedError(); - - @override - Future signInWithApple({ - required String idToken, - required String authCode, - required String fullName, - String? email, - }) => throw UnimplementedError(); - - @override - Future signInWithGoogle(GoogleSignInAccount account) => - throw UnimplementedError(); - - @override - Future signUp({ - required String email, - required String password, - required String name, - }) => throw UnimplementedError(); } diff --git a/test/core/dio/transformers/logging_transformer_test.dart b/test/core/dio/transformers/logging_transformer_test.dart index d22469e9..25a99bd3 100644 --- a/test/core/dio/transformers/logging_transformer_test.dart +++ b/test/core/dio/transformers/logging_transformer_test.dart @@ -1,18 +1,14 @@ import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:on_time_front/core/di/di_setup.dart'; import 'package:on_time_front/core/dio/app_dio.dart'; import 'package:on_time_front/core/dio/interceptors/logger_interceptor.dart'; import 'package:on_time_front/core/dio/interceptors/token_interceptor.dart'; +import 'package:on_time_front/core/dio/interceptors/token_session_invalidator.dart'; import 'package:on_time_front/core/dio/transformers/logging_transformer.dart'; import 'package:on_time_front/data/data_sources/token_local_data_source.dart'; import 'package:on_time_front/domain/entities/token_entity.dart'; void main() { - tearDown(() async { - await getIt.reset(); - }); - test( 'logging transformer preserves serialized request and response body', () async { @@ -29,9 +25,10 @@ void main() { ); test('AppDio configures JSON defaults, logging, and auth interceptors', () { - getIt.registerSingleton(_FakeTokenLocalDataSource()); - - final dio = AppDio(); + final dio = AppDio( + _FakeTokenLocalDataSource(), + _FakeTokenSessionInvalidator(), + ); expect(dio.options.contentType, Headers.jsonContentType); expect(dio.options.receiveDataWhenStatusError, isTrue); @@ -72,3 +69,8 @@ class _FakeTokenLocalDataSource implements TokenLocalDataSource { @override Future deleteToken() async {} } + +class _FakeTokenSessionInvalidator implements TokenSessionInvalidator { + @override + Future signOutLocally() async {} +} diff --git a/test/core/services/notification_service_test.dart b/test/core/services/notification_service_test.dart index 3b298399..d33fb4f9 100644 --- a/test/core/services/notification_service_test.dart +++ b/test/core/services/notification_service_test.dart @@ -5,27 +5,15 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:on_time_front/core/di/di_setup.dart'; -import 'package:on_time_front/core/services/navigation_service.dart'; import 'package:on_time_front/core/services/notification_service.dart'; -import 'package:on_time_front/data/data_sources/notification_remote_data_source.dart'; -import 'package:on_time_front/data/models/fcm_token_register_request_model.dart'; +import 'package:on_time_front/core/services/notification_tap_router.dart'; +import 'package:on_time_front/core/services/notification_token_registrar.dart'; import 'package:on_time_front/domain/entities/alarm_entities.dart'; -import 'package:on_time_front/domain/entities/schedule_with_preparation_entity.dart'; -import 'package:on_time_front/domain/repositories/alarm_repository.dart'; import 'package:timezone/timezone.dart' as tz; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - setUp(() async { - await getIt.reset(); - }); - - tearDown(() async { - await getIt.reset(); - }); - test( 'hasNotificationPermission accepts authorized and provisional states', () async { @@ -111,7 +99,7 @@ void main() { data: {'type': 'preparation_step', 'scheduleId': 'schedule-1'}, ); final localNotifications = _RecordingLocalNotifications(); - final navigationService = _FakeNavigationService(); + final tapRouter = _FakeNotificationTapRouter(); const firebaseMessagingChannel = MethodChannel( 'plugins.flutter.io/firebase_messaging', ); @@ -124,10 +112,10 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(firebaseMessagingChannel, null); }); - getIt.registerSingleton(navigationService); final service = NotificationService.test( messaging: messaging, localNotifications: localNotifications, + notificationTapRouter: tapRouter, ); await service.initialize(); @@ -136,7 +124,7 @@ void main() { expect(messaging.getTokenCount, 1); expect(messaging.getInitialMessageCount, 1); expect(localNotifications.initializeCount, 1); - expect(navigationService.pushedRoutes, ['/alarmScreen']); + expect(tapRouter.remoteMessageData.single['scheduleId'], 'schedule-1'); }, ); @@ -167,23 +155,20 @@ void main() { expect(messaging.tokenRefreshListened, isTrue); }); - test('requestNotificationToken registers FCM token with device id', () async { + test('requestNotificationToken delegates FCM token registration', () async { final messaging = _FakeFirebaseMessaging(AuthorizationStatus.authorized) ..token = 'fcm-token'; - final remoteDataSource = _FakeNotificationRemoteDataSource(); - getIt - ..registerSingleton(_FakeAlarmRepository()) - ..registerSingleton(remoteDataSource); + final tokenRegistrar = _FakeFcmTokenRegistrar(); final service = NotificationService.test( messaging: messaging, localNotifications: _RecordingLocalNotifications(), isFlutterLocalNotificationsInitialized: true, + fcmTokenRegistrar: tokenRegistrar, ); await service.requestNotificationToken(); - expect(remoteDataSource.registeredTokens.single.firebaseToken, 'fcm-token'); - expect(remoteDataSource.registeredTokens.single.deviceId, 'device-1'); + expect(tokenRegistrar.registeredTokens, ['fcm-token']); }); test( @@ -191,24 +176,22 @@ void main() { () async { final messaging = _FakeFirebaseMessaging(AuthorizationStatus.authorized) ..token = 'initial-token'; - final remoteDataSource = _FakeNotificationRemoteDataSource(); - getIt - ..registerSingleton(_FakeAlarmRepository()) - ..registerSingleton(remoteDataSource); + final tokenRegistrar = _FakeFcmTokenRegistrar(); final service = NotificationService.test( messaging: messaging, localNotifications: _RecordingLocalNotifications(), isFlutterLocalNotificationsInitialized: true, + fcmTokenRegistrar: tokenRegistrar, ); await service.requestNotificationToken(); messaging.emitTokenRefresh('refreshed-token'); await pumpEventQueue(); - expect( - remoteDataSource.registeredTokens.map((token) => token.firebaseToken), - ['initial-token', 'refreshed-token'], - ); + expect(tokenRegistrar.registeredTokens, [ + 'initial-token', + 'refreshed-token', + ]); }, ); @@ -229,14 +212,14 @@ void main() { ); test( - 'local notification taps route decoded payloads through navigation service', + 'local notification taps are delegated to the notification tap router', () async { final localNotifications = _RecordingLocalNotifications(); - final navigationService = _FakeNavigationService(); - getIt.registerSingleton(navigationService); + final tapRouter = _FakeNotificationTapRouter(); final service = NotificationService.test( messaging: _FakeFirebaseMessaging(AuthorizationStatus.authorized), localNotifications: localNotifications, + notificationTapRouter: tapRouter, ); await service.setupFlutterNotifications(); @@ -245,7 +228,10 @@ void main() { ); localNotifications.tapPayload('not-json'); - expect(navigationService.pushedRoutes, ['/alarmScreen']); + expect(tapRouter.localPayloads, [ + '{"type":"preparation_step","scheduleId":"schedule-1"}', + 'not-json', + ]); }, ); @@ -758,69 +744,26 @@ class _FakeIOSLocalNotificationsPlugin } } -class _FakeNavigationService implements NavigationService { - final pushedRoutes = []; - final pushedExtras = []; +class _FakeFcmTokenRegistrar implements FcmTokenRegistrar { + final registeredTokens = []; @override - void push(String routeName, {Object? extra}) { - pushedRoutes.add(routeName); - pushedExtras.add(extra); + Future registerToken(String firebaseToken) async { + registeredTokens.add(firebaseToken); } - - @override - dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } -class _FakeAlarmRepository implements AlarmRepository { - @override - Future getDeviceId() async => 'device-1'; - - @override - Future buildCurrentDeviceInfo() { - throw UnimplementedError(); - } +class _FakeNotificationTapRouter implements NotificationTapRouter { + final localPayloads = []; + final remoteMessageData = >[]; @override - Future getAlarmSettings() { - throw UnimplementedError(); + void routeLocalNotificationTap(String? payload) { + localPayloads.add(payload); } @override - Future updateAlarmSettings({required bool alarmsEnabled}) { - throw UnimplementedError(); - } - - @override - Future registerCurrentDevice(AlarmDeviceInfo deviceInfo) { - throw UnimplementedError(); - } - - @override - Future unregisterCurrentDevice(String deviceId) { - throw UnimplementedError(); - } - - @override - Future> getAlarmWindow( - DateTime startDate, - DateTime endDate, - ) { - throw UnimplementedError(); - } - - @override - Future postAlarmStatus(AlarmStatusReport report) { - throw UnimplementedError(); - } -} - -class _FakeNotificationRemoteDataSource - implements NotificationRemoteDataSource { - final registeredTokens = []; - - @override - Future fcmTokenRegister(FcmTokenRegisterRequestModel model) async { - registeredTokens.add(model); + void routeRemoteNotificationData(Map data) { + remoteMessageData.add(data); } } diff --git a/test/core/services/notification_tap_router_test.dart b/test/core/services/notification_tap_router_test.dart new file mode 100644 index 00000000..5d051cad --- /dev/null +++ b/test/core/services/notification_tap_router_test.dart @@ -0,0 +1,47 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:on_time_front/core/services/navigation_service.dart'; +import 'package:on_time_front/core/services/notification_tap_router.dart'; + +void main() { + test('routes local notification payloads through navigation service', () { + final navigationService = _FakeNavigationService(); + final router = NavigationNotificationTapRouter(navigationService); + + router.routeLocalNotificationTap( + '{"type":"preparation_step","scheduleId":"schedule-1"}', + ); + router.routeLocalNotificationTap('not-json'); + + expect(navigationService.pushedRoutes, ['/alarmScreen']); + }); + + test('routes remote schedule alarm data with launch payload', () { + final navigationService = _FakeNavigationService(); + final router = NavigationNotificationTapRouter(navigationService); + + router.routeRemoteNotificationData({ + 'type': 'schedule_alarm', + 'scheduleId': 'schedule-1', + }); + + expect(navigationService.pushedRoutes, ['/scheduleStart']); + expect(navigationService.pushedExtras.single, { + 'type': 'schedule_alarm', + 'scheduleId': 'schedule-1', + }); + }); +} + +class _FakeNavigationService implements NavigationService { + final pushedRoutes = []; + final pushedExtras = []; + + @override + void push(String routeName, {Object? extra}) { + pushedRoutes.add(routeName); + pushedExtras.add(extra); + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} diff --git a/test/data/services/device_fcm_token_registrar_test.dart b/test/data/services/device_fcm_token_registrar_test.dart new file mode 100644 index 00000000..da18f3f7 --- /dev/null +++ b/test/data/services/device_fcm_token_registrar_test.dart @@ -0,0 +1,43 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:on_time_front/data/data_sources/notification_remote_data_source.dart'; +import 'package:on_time_front/data/models/fcm_token_register_request_model.dart'; +import 'package:on_time_front/data/services/device_fcm_token_registrar.dart'; +import 'package:on_time_front/domain/repositories/alarm_repository.dart'; + +void main() { + test('registers FCM token for the current alarm device', () async { + final alarmRepository = _FakeAlarmRepository(deviceId: 'device-1'); + final remoteDataSource = _FakeNotificationRemoteDataSource(); + final registrar = DeviceFcmTokenRegistrar( + alarmRepository, + remoteDataSource, + ); + + await registrar.registerToken('fcm-token'); + + expect(remoteDataSource.registeredTokens.single.firebaseToken, 'fcm-token'); + expect(remoteDataSource.registeredTokens.single.deviceId, 'device-1'); + }); +} + +class _FakeAlarmRepository implements AlarmRepository { + _FakeAlarmRepository({required this.deviceId}); + + final String deviceId; + + @override + Future getDeviceId() async => deviceId; + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeNotificationRemoteDataSource + implements NotificationRemoteDataSource { + final registeredTokens = []; + + @override + Future fcmTokenRegister(FcmTokenRegisterRequestModel model) async { + registeredTokens.add(model); + } +}