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()
})
})