527 lines
18 KiB
Dart
527 lines
18 KiB
Dart
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<void> 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<bool> requestPermission() async {
|
|
if (!_isInitialized) await initialize();
|
|
|
|
// iOS에서 권한 요청
|
|
final bool? result = await mockNotificationsPlugin
|
|
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
|
|
?.requestPermissions(
|
|
alert: true,
|
|
badge: true,
|
|
sound: true,
|
|
);
|
|
|
|
return result ?? false;
|
|
}
|
|
|
|
// 즉시 알림 표시
|
|
Future<void> 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<void> 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<void> 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<void> 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<void> cancelEventNotifications(String eventId) async {
|
|
await mockNotificationsPlugin.cancel(eventId.hashCode);
|
|
await mockNotificationsPlugin.cancel(eventId.hashCode + 1);
|
|
}
|
|
|
|
// 모든 알림 취소
|
|
Future<void> cancelAllNotifications() async {
|
|
await mockNotificationsPlugin.cancelAll();
|
|
}
|
|
}
|