import * as Calendar from 'expo-calendar'; import { ensureCalendarPermission, fetchNativeEvents } from '../services/calendar'; // Mock expo-calendar jest.mock('expo-calendar', () => ({ requestCalendarPermissionsAsync: jest.fn(), isAvailableAsync: jest.fn(), getCalendarsAsync: jest.fn(), getEventsAsync: jest.fn(), EntityTypes: { EVENTS: 'EVENTS', }, })); const mockCalendar = Calendar as jest.Mocked; describe('calendar service', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('ensureCalendarPermission', () => { it('returns true when permission granted and calendar available', async () => { mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'granted' as Calendar.PermissionStatus, granted: true, expires: 'never' as const, canAskAgain: true }); mockCalendar.isAvailableAsync.mockResolvedValue(true); const result = await ensureCalendarPermission(); expect(result).toBe(true); }); it('returns false when permission denied', async () => { mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'denied' as Calendar.PermissionStatus, granted: false, expires: 'never' as const, canAskAgain: true }); const result = await ensureCalendarPermission(); expect(result).toBe(false); expect(mockCalendar.isAvailableAsync).not.toHaveBeenCalled(); }); it('returns false when calendar not available', async () => { mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'granted' as Calendar.PermissionStatus, granted: true, expires: 'never' as const, canAskAgain: true }); mockCalendar.isAvailableAsync.mockResolvedValue(false); const result = await ensureCalendarPermission(); expect(result).toBe(false); }); }); describe('fetchNativeEvents', () => { it('returns empty array when no permission', async () => { mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'denied' as Calendar.PermissionStatus, granted: false, expires: 'never' as const, canAskAgain: true }); const result = await fetchNativeEvents(new Date(), new Date()); expect(result).toEqual([]); }); it('returns empty array when no calendars', async () => { mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'granted' as Calendar.PermissionStatus, granted: true, expires: 'never' as const, canAskAgain: true }); mockCalendar.isAvailableAsync.mockResolvedValue(true); mockCalendar.getCalendarsAsync.mockResolvedValue([]); const result = await fetchNativeEvents(new Date(), new Date()); expect(result).toEqual([]); }); it('returns mapped events from native calendar', async () => { const startDate = new Date('2025-01-01'); const endDate = new Date('2025-01-31'); mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'granted' as Calendar.PermissionStatus, granted: true, expires: 'never' as const, canAskAgain: true }); mockCalendar.isAvailableAsync.mockResolvedValue(true); mockCalendar.getCalendarsAsync.mockResolvedValue([ { id: 'cal1', title: 'Work' }, { id: 'cal2', title: 'Personal' }, ] as Calendar.Calendar[]); mockCalendar.getEventsAsync.mockResolvedValue([ { id: 'evt1', calendarId: 'cal1', title: 'Team Meeting', location: 'Berlin', startDate: new Date('2025-01-15T10:00:00'), }, { id: 'evt2', calendarId: 'cal2', title: 'Dentist', location: null, startDate: new Date('2025-01-20T14:00:00'), }, ] as Calendar.Event[]); const result = await fetchNativeEvents(startDate, endDate); // Only the event with a location is returned; events without a location are filtered out expect(result).toHaveLength(1); expect(result[0]).toEqual({ id: 'evt1', title: 'Team Meeting', destination: 'Berlin', eventTime: new Date('2025-01-15T10:00:00'), source: 'native:cal1', }); expect(mockCalendar.getEventsAsync).toHaveBeenCalledWith( ['cal1', 'cal2'], startDate, endDate, ); }); it('filters out events with no location', async () => { mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'granted' as Calendar.PermissionStatus, granted: true, expires: 'never' as const, canAskAgain: true }); mockCalendar.isAvailableAsync.mockResolvedValue(true); mockCalendar.getCalendarsAsync.mockResolvedValue([{ id: 'cal1', title: 'Default' }] as Calendar.Calendar[]); mockCalendar.getEventsAsync.mockResolvedValue([ { id: 'evt1', calendarId: 'cal1', title: 'No Location Event', location: null, startDate: new Date('2025-01-15T10:00:00'), }, { id: 'evt2', calendarId: 'cal1', title: 'Empty Location Event', location: ' ', startDate: new Date('2025-01-16T10:00:00'), }, ] as Calendar.Event[]); const result = await fetchNativeEvents(new Date(), new Date()); expect(result).toHaveLength(0); }); it('handles events with missing title', async () => { mockCalendar.requestCalendarPermissionsAsync.mockResolvedValue({ status: 'granted' as Calendar.PermissionStatus, granted: true, expires: 'never' as const, canAskAgain: true }); mockCalendar.isAvailableAsync.mockResolvedValue(true); mockCalendar.getCalendarsAsync.mockResolvedValue([{ id: 'cal1', title: 'Default' }] as Calendar.Calendar[]); mockCalendar.getEventsAsync.mockResolvedValue([ { id: 'evt1', calendarId: 'cal1', title: null as unknown as string, location: 'Wien Hbf', startDate: null as unknown as string | Date, }, ] as Calendar.Event[]); const result = await fetchNativeEvents(new Date(), new Date()); expect(result).toHaveLength(1); expect(result[0].title).toBe('Untitled Event'); expect(result[0].destination).toBe('Wien Hbf'); }); }); });