import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:mockito/annotations.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:timezone/timezone.dart' as tz; import 'package:timezone/data/latest.dart' as tz_data; import 'package:shared_preferences/shared_preferences.dart'; import 'package:lanebow/services/event_service.dart'; import 'package:lanebow/models/event_model.dart'; import 'package:lanebow/services/api_service.dart'; import 'package:lanebow/config/api_config.dart'; // 모킹 클래스 생성 @GenerateMocks([ApiService, FlutterLocalNotificationsPlugin]) import 'notification_service_integration_test.mocks.dart'; void main() { // Flutter 위젯 테스트 바인딩 초기화 TestWidgetsFlutterBinding.ensureInitialized(); // SharedPreferences 모킹 설정 SharedPreferences.setMockInitialValues({ ApiConfig.clubIdKey: 'test_club_id', ApiConfig.tokenKey: 'test_token' }); late TestNotificationService notificationService; late EventService eventService; late MockApiService mockApiService; late MockFlutterLocalNotificationsPlugin mockNotificationsPlugin; // 테스트 전 설정 setUp(() { // 모킹된 API 서비스 및 알림 플러그인 생성 mockApiService = MockApiService(); mockNotificationsPlugin = MockFlutterLocalNotificationsPlugin(); // 테스트용 NotificationService 클래스 생성 notificationService = TestNotificationService(mockNotificationsPlugin); // EventService 생성 및 모킹된 API 서비스 주입 eventService = EventService.forTest(mockApiService); eventService.setClubId('test_club_id'); // 타임존 초기화 tz_data.initializeTimeZones(); // API 서비스 토큰 설정 when(mockApiService.setToken(any)).thenReturn(null); // 알림 플러그인 초기화 모킹 when(mockNotificationsPlugin.initialize( any, onDidReceiveNotificationResponse: anyNamed('onDidReceiveNotificationResponse'), onDidReceiveBackgroundNotificationResponse: anyNamed('onDidReceiveBackgroundNotificationResponse'), )).thenAnswer((_) async => true); }); group('NotificationService 통합 테스트', () { test('이벤트 생성 시 알림이 설정되어야 함', () async { // given const clubId = 'test_club_id'; const token = 'test_token'; // 토큰과 클럽 ID 설정 eventService.setClubId(clubId); when(mockApiService.setToken(token)).thenReturn(null); await eventService.initialize(token); final event = Event( id: 'test_event_id', clubId: 'test_club_id', title: '테스트 이벤트', description: '테스트 설명', startDate: DateTime.now().add(const Duration(hours: 3)), endDate: DateTime.now().add(const Duration(hours: 5)), location: '테스트 장소', registrationDeadline: DateTime.now().add(const Duration(days: 2)), maxParticipants: 10, isActive: true, ); // API 응답 모킹 when(mockApiService.post('${ApiConfig.clubs}/events', data: anyNamed('data'))).thenAnswer((_) async => { 'event': { 'id': event.id, 'clubId': event.clubId, 'title': event.title, 'startDate': event.startDate.toIso8601String(), 'isActive': event.isActive, } }); // 알림 설정 모킹 when(mockNotificationsPlugin.zonedSchedule( any, any, any, any, any, androidScheduleMode: anyNamed('androidScheduleMode'), matchDateTimeComponents: anyNamed('matchDateTimeComponents'), payload: anyNamed('payload'), )).thenAnswer((_) async {}); // when // 이벤트 생성 및 알림 설정 await eventService.createEvent(event.toJson()); await notificationService.scheduleEventStartReminder(event); await notificationService.scheduleRegistrationDeadlineReminder(event); // then // 이벤트 시작 알림 설정 확인 verify(mockNotificationsPlugin.zonedSchedule( event.id.hashCode, '이벤트 시작 알림', '${event.title} 이벤트가 1시간 후에 시작됩니다.', any, any, androidScheduleMode: anyNamed('androidScheduleMode'), matchDateTimeComponents: anyNamed('matchDateTimeComponents'), payload: event.id, )).called(1); // 등록 마감 알림 설정 확인 verify(mockNotificationsPlugin.zonedSchedule( event.id.hashCode + 1, '이벤트 등록 마감 알림', '${event.title} 이벤트의 등록이 내일 마감됩니다.', any, any, androidScheduleMode: anyNamed('androidScheduleMode'), matchDateTimeComponents: anyNamed('matchDateTimeComponents'), payload: event.id, )).called(1); }); test('이벤트 수정 시 알림이 업데이트되어야 함', () async { // given const clubId = 'test_club_id'; const token = 'test_token'; // 토큰과 클럽 ID 설정 eventService.setClubId(clubId); when(mockApiService.setToken(token)).thenReturn(null); await eventService.initialize(token); final originalEvent = Event( id: 'test_event_id', clubId: 'test_club_id', title: '원래 이벤트', startDate: DateTime.now().add(const Duration(hours: 3)), registrationDeadline: DateTime.now().add(const Duration(days: 2)), isActive: true, ); final updatedEvent = Event( id: 'test_event_id', clubId: 'test_club_id', title: '수정된 이벤트', startDate: DateTime.now().add(const Duration(hours: 5)), // 변경된 시작 시간 registrationDeadline: DateTime.now().add(const Duration(days: 3)), // 변경된 마감 시간 isActive: true, ); // API 응답 모킹 when(mockApiService.put('${ApiConfig.clubs}/events/${originalEvent.id}', data: anyNamed('data'))).thenAnswer((_) async => { 'event': { 'id': updatedEvent.id, 'clubId': updatedEvent.clubId, 'title': updatedEvent.title, 'startDate': updatedEvent.startDate.toIso8601String(), 'registrationDeadline': updatedEvent.registrationDeadline?.toIso8601String(), 'isActive': updatedEvent.isActive, } }); // 알림 설정 모킹 when(mockNotificationsPlugin.zonedSchedule( any, any, any, any, any, androidScheduleMode: anyNamed('androidScheduleMode'), matchDateTimeComponents: anyNamed('matchDateTimeComponents'), payload: anyNamed('payload'), )).thenAnswer((_) async {}); // 알림 취소 모킹 when(mockNotificationsPlugin.cancel(any)).thenAnswer((_) async {}); // when // 이벤트 수정 await eventService.updateEvent(updatedEvent.id, updatedEvent.toJson()); // 기존 알림 취소 후 새 알림 설정 await notificationService.cancelEventNotifications(updatedEvent.id); await notificationService.scheduleEventStartReminder(updatedEvent); await notificationService.scheduleRegistrationDeadlineReminder(updatedEvent); // then // 기존 알림 취소 확인 verify(mockNotificationsPlugin.cancel(updatedEvent.id.hashCode)).called(1); verify(mockNotificationsPlugin.cancel(updatedEvent.id.hashCode + 1)).called(1); // 새 알림 설정 확인 verify(mockNotificationsPlugin.zonedSchedule( updatedEvent.id.hashCode, '이벤트 시작 알림', '${updatedEvent.title} 이벤트가 1시간 후에 시작됩니다.', any, any, androidScheduleMode: anyNamed('androidScheduleMode'), matchDateTimeComponents: anyNamed('matchDateTimeComponents'), payload: updatedEvent.id, )).called(1); verify(mockNotificationsPlugin.zonedSchedule( updatedEvent.id.hashCode + 1, '이벤트 등록 마감 알림', '${updatedEvent.title} 이벤트의 등록이 내일 마감됩니다.', any, any, androidScheduleMode: anyNamed('androidScheduleMode'), matchDateTimeComponents: anyNamed('matchDateTimeComponents'), payload: updatedEvent.id, )).called(1); }); test('이벤트 삭제 시 알림이 취소되어야 함', () async { // given const eventId = 'test_event_id'; const clubId = 'test_club_id'; const token = 'test_token'; // 토큰과 클럽 ID 설정 eventService.setClubId(clubId); when(mockApiService.setToken(token)).thenReturn(null); await eventService.initialize(token); // API 응답 모킹 when(mockApiService.delete('${ApiConfig.clubs}/events/$eventId')).thenAnswer((_) async => {'success': true}); // 알림 취소 모킹 when(mockNotificationsPlugin.cancel(any)).thenAnswer((_) async {}); // when // 이벤트 삭제 및 알림 취소 await eventService.deleteEvent(eventId); await notificationService.cancelEventNotifications(eventId); // then // 알림 취소 확인 verify(mockNotificationsPlugin.cancel(eventId.hashCode)).called(1); verify(mockNotificationsPlugin.cancel(eventId.hashCode + 1)).called(1); }); test('앱 시작 시 알림 서비스가 초기화되어야 함', () async { // given when(mockNotificationsPlugin.initialize(any, onDidReceiveNotificationResponse: anyNamed('onDidReceiveNotificationResponse'))) .thenAnswer((_) async => true); // when // 앱 시작 시 알림 서비스 초기화 await notificationService.initialize(); // then // 알림 초기화 확인 verify(mockNotificationsPlugin.initialize(any, onDidReceiveNotificationResponse: anyNamed('onDidReceiveNotificationResponse'))).called(1); }); test('여러 이벤트에 대한 알림이 올바르게 설정되어야 함', () async { // given const clubId = 'test_club_id'; const token = 'test_token'; // 토큰과 클럽 ID 설정 eventService.setClubId(clubId); when(mockApiService.setToken(token)).thenReturn(null); await eventService.initialize(token); final events = [ Event( id: 'event_id_1', clubId: 'test_club_id', title: '이벤트 1', startDate: DateTime.now().add(const Duration(hours: 3)), registrationDeadline: DateTime.now().add(const Duration(days: 2)), isActive: true, ), Event( id: 'event_id_2', clubId: 'test_club_id', title: '이벤트 2', startDate: DateTime.now().add(const Duration(hours: 5)), registrationDeadline: DateTime.now().add(const Duration(days: 3)), isActive: true, ), ]; // API 응답 모킹 when(mockApiService.post('${ApiConfig.clubs}/events', data: anyNamed('data'))).thenAnswer((_) async => { 'events': events.map((e) => { 'id': e.id, 'clubId': e.clubId, 'title': e.title, 'description': e.description, 'startDate': e.startDate.toIso8601String(), 'endDate': e.endDate?.toIso8601String(), 'location': e.location, 'registrationDeadline': e.registrationDeadline?.toIso8601String(), 'maxParticipants': e.maxParticipants, 'isActive': e.isActive, }).toList(), }); // 알림 설정 모킹 when(mockNotificationsPlugin.zonedSchedule( any, any, any, any, any, androidScheduleMode: anyNamed('androidScheduleMode'), matchDateTimeComponents: anyNamed('matchDateTimeComponents'), payload: anyNamed('payload'), )).thenAnswer((_) async {}); // when // 클럽의 모든 이벤트 조회 await eventService.fetchClubEvents(); final fetchedEvents = eventService.events; // 각 이벤트에 대한 알림 설정 for (final event in fetchedEvents) { await notificationService.scheduleEventStartReminder(event); await notificationService.scheduleRegistrationDeadlineReminder(event); } // then // 각 이벤트에 대한 알림 설정 확인 for (final event in events) { verify(mockNotificationsPlugin.zonedSchedule( event.id.hashCode, '이벤트 시작 알림', '${event.title} 이벤트가 1시간 후에 시작됩니다.', any, any, androidScheduleMode: anyNamed('androidScheduleMode'), matchDateTimeComponents: anyNamed('matchDateTimeComponents'), payload: event.id, )).called(1); verify(mockNotificationsPlugin.zonedSchedule( event.id.hashCode + 1, '이벤트 등록 마감 알림', '${event.title} 이벤트의 등록이 내일 마감됩니다.', any, any, androidScheduleMode: anyNamed('androidScheduleMode'), matchDateTimeComponents: anyNamed('matchDateTimeComponents'), payload: event.id, )).called(1); } }); }); } // 테스트를 위한 NotificationService 클래스 class TestNotificationService { final FlutterLocalNotificationsPlugin mockNotificationsPlugin; bool _isInitialized = false; TestNotificationService(this.mockNotificationsPlugin); // 알림 서비스 초기화 Future initialize() async { if (_isInitialized) return; // 타임존 초기화 tz_data.initializeTimeZones(); // 안드로이드 설정 const AndroidInitializationSettings androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); // iOS 설정 const DarwinInitializationSettings iOSSettings = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, ); // 초기화 설정 const InitializationSettings initSettings = InitializationSettings( android: androidSettings, iOS: iOSSettings, ); // 알림 플러그인 초기화 await mockNotificationsPlugin.initialize( initSettings, onDidReceiveNotificationResponse: (NotificationResponse response) { // 알림 클릭 시 처리 debugPrint('알림 클릭: ${response.payload}'); }, ); _isInitialized = true; } // 권한 요청 Future requestPermission() async { if (!_isInitialized) await initialize(); // iOS에서 권한 요청 final bool? result = await mockNotificationsPlugin .resolvePlatformSpecificImplementation() ?.requestPermissions( alert: true, badge: true, sound: true, ); return result ?? false; } // 즉시 알림 표시 Future showNotification({ required int id, required String title, required String body, String? payload, }) async { if (!_isInitialized) await initialize(); const AndroidNotificationDetails androidDetails = AndroidNotificationDetails( 'event_channel', '이벤트 알림', channelDescription: '이벤트 관련 알림을 표시합니다', importance: Importance.high, priority: Priority.high, showWhen: true, ); const DarwinNotificationDetails iOSDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, ); const NotificationDetails notificationDetails = NotificationDetails( android: androidDetails, iOS: iOSDetails, ); await mockNotificationsPlugin.show( id, title, body, notificationDetails, payload: payload, ); } // 예약 알림 설정 Future scheduleNotification({ required int id, required String title, required String body, required DateTime scheduledDate, String? payload, }) async { if (!_isInitialized) await initialize(); final tz.TZDateTime scheduledTZDate = tz.TZDateTime.from( scheduledDate, tz.local, ); const AndroidNotificationDetails androidDetails = AndroidNotificationDetails( 'event_reminder_channel', '이벤트 리마인더', channelDescription: '예정된 이벤트에 대한 알림을 표시합니다', importance: Importance.high, priority: Priority.high, ); const DarwinNotificationDetails iOSDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, ); const NotificationDetails notificationDetails = NotificationDetails( android: androidDetails, iOS: iOSDetails, ); await mockNotificationsPlugin.zonedSchedule( id, title, body, scheduledTZDate, notificationDetails, androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, matchDateTimeComponents: DateTimeComponents.time, payload: payload, ); } // 이벤트 시작 알림 설정 Future scheduleEventStartReminder(Event event) async { if (event.id.isEmpty) return; // 이벤트 시작 1시간 전 알림 final DateTime reminderTime = event.startDate.subtract(const Duration(hours: 1)); // 현재 시간이 알림 시간보다 이후라면 알림을 설정하지 않음 if (reminderTime.isBefore(DateTime.now())) return; await scheduleNotification( id: event.id.hashCode, title: '이벤트 시작 알림', body: '${event.title} 이벤트가 1시간 후에 시작됩니다.', scheduledDate: reminderTime, payload: event.id, ); } // 이벤트 등록 마감 알림 설정 Future scheduleRegistrationDeadlineReminder(Event event) async { if (event.id.isEmpty || event.registrationDeadline == null) return; // 등록 마감 1일 전 알림 final DateTime reminderTime = event.registrationDeadline!.subtract(const Duration(days: 1)); // 현재 시간이 알림 시간보다 이후라면 알림을 설정하지 않음 if (reminderTime.isBefore(DateTime.now())) return; await scheduleNotification( id: event.id.hashCode + 1, // 이벤트 시작 알림과 ID가 겹치지 않도록 함 title: '이벤트 등록 마감 알림', body: '${event.title} 이벤트의 등록이 내일 마감됩니다.', scheduledDate: reminderTime, payload: event.id, ); } // 특정 이벤트의 모든 알림 취소 Future cancelEventNotifications(String eventId) async { await mockNotificationsPlugin.cancel(eventId.hashCode); await mockNotificationsPlugin.cancel(eventId.hashCode + 1); } // 모든 알림 취소 Future cancelAllNotifications() async { await mockNotificationsPlugin.cancelAll(); } }