import { AccessTab } from '@/components/Calendar/AccessTab' import { CalendarAccessRights, UserWithAccess } from '@/components/Calendar/CalendarAccessRights' import CalendarPopover from '@/components/Calendar/CalendarModal' import { updateDelegationCalendar } from '@/features/Calendars/api/updateDelegationCalendar' import { AccessRight, Calendar } from '@/features/Calendars/CalendarTypes' import * as eventThunks from '@/features/Calendars/services' import * as delegationThunks from '@/features/Calendars/services/updateDelegationCalendarAsync' import { getUserDetails } from '@/features/User/userAPI' import { accessRightToDavProp } from '@/utils/accessRightToDavProp' import { api } from '@/utils/apiUtils' import { fireEvent, screen, waitFor } from '@testing-library/react' import { renderWithProviders } from '../../utils/Renderwithproviders' jest.mock('@/utils/apiUtils', () => ({ api: { post: jest.fn() } })) jest.mock('@/features/User/userAPI', () => ({ getUserDetails: jest.fn() })) jest.mock('@/features/Calendars/CalendarApi', () => ({ getSecretLink: jest.fn().mockReturnValue(''), exportCalendar: jest.fn() })) const mockThunkWithUnwrap = (resolvedValue: unknown = {}) => jest.fn().mockImplementation(() => { const result = Object.assign(Promise.resolve(resolvedValue), { unwrap: () => Promise.resolve(resolvedValue) }) return jest.fn().mockReturnValue(result) }) describe('accessRightToDavProp', () => { it('maps 5 (ADMIN) → dav:administration', () => { expect(accessRightToDavProp(5)).toBe('dav:administration') }) it('maps 3 (EDITOR) → dav:read-write', () => { expect(accessRightToDavProp(3)).toBe('dav:read-write') }) it('maps 2 (VIEW) → dav:read', () => { expect(accessRightToDavProp(2)).toBe('dav:read') }) it('defaults to dav:read for unknown values (covered by default case)', () => { expect(accessRightToDavProp(999 as AccessRight)).toBe('dav:read') }) }) describe('updateDelegationCalendar', () => { beforeEach(() => jest.clearAllMocks()) it('posts to the correct DAV endpoint with the share body', async () => { ;(api.post as jest.Mock).mockResolvedValue({ ok: true }) const share = { set: [{ 'dav:href': 'mailto:alice@example.com', 'dav:read': true }], remove: [] } await updateDelegationCalendar('/calendars/user/cal1.json', share) await waitFor(() => expect(api.post).toHaveBeenCalledWith( 'dav/calendars/user/cal1.json', expect.objectContaining({ body: JSON.stringify({ share }) }) ) ) }) }) const baseCalendar: Calendar = { id: 'user1/cal1', name: 'My Calendar', description: '', color: { color: '#0062FF', dark: '#FFF' }, link: '/calendars/user1/cal1.json', visibility: 'public', events: {}, invite: [], owner: { emails: ['user1@example.com'] }, access: { write: true } as any, delegated: true } describe('CalendarAccessRights', () => { const mockOnChange = jest.fn() const mockOnInvitesLoaded = jest.fn() beforeEach(() => jest.clearAllMocks()) it('renders the grant access rights section', () => { renderWithProviders( , { ...userState } ) expect( screen.getByText('calendarPopover.access.grantAccessRights') ).toBeInTheDocument() }) it('shows a list of users already passed in via value prop', () => { const users: UserWithAccess[] = [ { openpaasId: 'user1', displayName: 'Alice', email: 'alice@example.com', accessRight: 2 } ] renderWithProviders( ) expect(screen.getByText('Alice')).toBeInTheDocument() expect(screen.getByText('alice@example.com')).toBeInTheDocument() }) it('calls onChange with user removed when remove button is clicked', () => { const users: UserWithAccess[] = [ { openpaasId: 'user1', displayName: 'Alice', email: 'alice@example.com', accessRight: 5 } ] renderWithProviders( , { ...userState } ) fireEvent.click(screen.getByLabelText(/remove/i)) expect(mockOnChange).toHaveBeenCalledWith([]) }) it('loads invited users from calendar.invite on mount', async () => { const calendarWithInvite: Calendar = { ...baseCalendar, invite: [ { href: 'mailto:bob@example.com', principal: '/principals/users/bob123', access: 3, inviteStatus: 1 } ] } ;(getUserDetails as jest.Mock).mockResolvedValue({ preferredEmail: 'bob@example.com', firstname: 'Bob', lastname: 'Smith', emails: ['bob@example.com'] }) renderWithProviders( , { ...userState } ) await waitFor(() => expect(getUserDetails).toHaveBeenCalledWith('bob123')) await waitFor(() => expect(mockOnInvitesLoaded).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining({ email: 'bob@example.com', accessRight: 3 }) ]) ) ) }) it('skips invite entries where getUserDetails throws', async () => { const calendarWithInvite: Calendar = { ...baseCalendar, invite: [ { href: 'mailto:ghost@example.com', principal: '/principals/users/ghost', access: 2, inviteStatus: 1 } ] } ;(getUserDetails as jest.Mock).mockRejectedValue(new Error('Not found')) renderWithProviders( , { ...userState } ) await waitFor(() => expect(mockOnInvitesLoaded).toHaveBeenCalledWith([])) }) it('shows a loading spinner while invite users are being fetched', async () => { const calendarWithInvite: Calendar = { ...baseCalendar, invite: [ { href: 'mailto:carol@example.com', principal: '/principals/users/carol', access: 2, inviteStatus: 1 } ] } // Never resolves during the assertion window ;(getUserDetails as jest.Mock).mockImplementation( () => new Promise(() => {}) ) renderWithProviders( , { ...userState } ) expect(document.querySelector('[role="progressbar"]')).toBeInTheDocument() }) it('displays the calendar owner information', () => { renderWithProviders( , { ...userState } ) expect(screen.getAllByText('user1@example.com').length).toBeGreaterThan(0) expect(screen.getByText('calendarPopover.access.owner')).toBeInTheDocument() }) it('fetches and displays resource administrators for resource calendars', async () => { const resourceCalendar: any = { ...baseCalendar, owner: { _id: 'resource1', resource: true, emails: ['resource1@example.com'], administrators: [ { id: 'admin1' }, { id: 'admin2' }, { id: 'resource1' } // Owner shouldn't be loaded as an admin ] } } ;(getUserDetails as jest.Mock).mockImplementation((id: string) => { if (id === 'admin1') { return Promise.resolve({ preferredEmail: 'admin1@example.com', firstname: 'Admin', lastname: 'One' }) } if (id === 'admin2') { return Promise.resolve({ preferredEmail: 'admin2@example.com', firstname: 'Admin', lastname: 'Two' }) } return Promise.resolve(null) }) renderWithProviders( , { ...userState } ) await waitFor(() => { expect(getUserDetails).toHaveBeenCalledWith('admin1') expect(getUserDetails).toHaveBeenCalledWith('admin2') }) expect(getUserDetails).not.toHaveBeenCalledWith('resource1') expect(await screen.findByText('Admin One')).toBeInTheDocument() expect(screen.getByText('admin1@example.com')).toBeInTheDocument() expect(screen.getByText('Admin Two')).toBeInTheDocument() expect(screen.getByText('admin2@example.com')).toBeInTheDocument() expect( screen.getAllByText('calendarPopover.access.administrator') ).toHaveLength(2) }) }) const userState = { user: { userData: { openpaasId: 'user1', email: 'user1@example.com' } } } describe('AccessTab – conditional rendering of CalendarAccessRights', () => { const noop = jest.fn() afterEach(() => jest.clearAllMocks()) it('shows CalendarAccessRights when the current user owns the calendar', () => { renderWithProviders( , { ...userState, calendars: { list: { 'user1/cal1': baseCalendar } } } ) expect( screen.getByText('calendarPopover.access.grantAccessRights') ).toBeInTheDocument() }) it('hides CalendarAccessRights textfield for a non-owner without admin delegation', () => { const foreignCalendar: Calendar = { ...baseCalendar, id: 'otherUser/cal1', invite: [] } renderWithProviders( , { ...userState, calendars: { list: { 'otherUser/cal1': foreignCalendar } } } ) expect(screen.queryByText('peopleSearch.label')).not.toBeInTheDocument() expect( screen.getByText('calendarPopover.access.accessRights') ).toBeInTheDocument() }) it('shows CalendarAccessRights when the user has access=5 (admin) delegation', () => { const delegatedCalendar: Calendar = { ...baseCalendar, id: 'otherUser/cal1', invite: [ { href: 'mailto:user1@example.com', principal: '/principals/users/user1', access: 5, inviteStatus: 1 } ] } renderWithProviders( , { ...userState, calendars: { list: { 'otherUser/cal1': delegatedCalendar } } } ) expect( screen.getByText('calendarPopover.access.grantAccessRights') ).toBeInTheDocument() }) }) const existingCalendar: Calendar = { id: 'user1/cal1', name: 'Existing Cal', description: 'Desc', color: { color: '#0062FF', dark: '#FFF' }, link: '/calendars/user/cal1', visibility: 'public', events: {}, invite: [], owner: { emails: ['user1@example.com'] } } describe('CalendarModal – updateDelegationCalendarAsync integration', () => { const mockOnClose = jest.fn() beforeEach(() => { jest.clearAllMocks() jest .spyOn(eventThunks, 'patchCalendarAsync') .mockImplementation(mockThunkWithUnwrap()) jest .spyOn(eventThunks, 'patchACLCalendarAsync') .mockImplementation(mockThunkWithUnwrap()) jest .spyOn(delegationThunks, 'updateDelegationCalendarAsync') .mockImplementation(mockThunkWithUnwrap()) }) it('does NOT call updateDelegationCalendarAsync when no users are added or removed', async () => { renderWithProviders( , { ...userState, calendars: { list: { 'user1/cal1': existingCalendar } } } ) fireEvent.click(screen.getByRole('button', { name: /save/i })) await waitFor(() => expect(mockOnClose).toHaveBeenCalled()) expect( delegationThunks.updateDelegationCalendarAsync ).not.toHaveBeenCalled() }) }) describe('CalendarModal – cancel button', () => { const mockOnClose = jest.fn() beforeEach(() => { jest.clearAllMocks() jest .spyOn(delegationThunks, 'updateDelegationCalendarAsync') .mockImplementation(mockThunkWithUnwrap()) jest .spyOn(eventThunks, 'patchCalendarAsync') .mockImplementation(mockThunkWithUnwrap()) }) it('calls onClose without saving when Cancel is clicked', async () => { renderWithProviders( , { ...userState } ) fireEvent.click(screen.getByRole('button', { name: /cancel/i })) await waitFor(() => expect(mockOnClose).toHaveBeenCalled()) expect( delegationThunks.updateDelegationCalendarAsync ).not.toHaveBeenCalled() expect(eventThunks.patchCalendarAsync).not.toHaveBeenCalled() }) })