cadfa70e60
* #708 apply strictier linting rules and fix simple eslint bugs * #708 fix eslint errors relate to promise * #708 fix eslint import/no-extraneous-dependencies * #708 fix eslint errors of react-hook * #708 enable eslint check for typescript --------- Co-authored-by: lethemanh <lethemanh@lethemanhs-MacBook-Pro.local>
779 lines
21 KiB
TypeScript
779 lines
21 KiB
TypeScript
import { updateSeries } from '@/features/Events/api/updateSeries'
|
|
import {
|
|
deleteEvent,
|
|
importEventFromFile,
|
|
moveEvent,
|
|
putEvent,
|
|
searchEvent
|
|
} from '@/features/Events/EventApi'
|
|
import { CalendarEvent } from '@/features/Events/EventsTypes'
|
|
import { calendarEventToJCal } from '@/features/Events/utils'
|
|
import { clientConfig } from '@/features/User/oidcAuth'
|
|
import { api } from '@/utils/apiUtils'
|
|
|
|
clientConfig.url = 'https://example.com'
|
|
|
|
jest.mock('@/utils/apiUtils')
|
|
|
|
const day = new Date()
|
|
|
|
const mockEvent = {
|
|
uid: 'event1',
|
|
title: 'Test Event',
|
|
timezone: 'UTC',
|
|
calId: '667037022b752d0026472254/cal1',
|
|
URL: '/calendars/667037022b752d0026472254/667037022b752d0026472254/cal1.ics',
|
|
start: day.toISOString(),
|
|
end: day.toISOString(),
|
|
status: 'PUBLIC',
|
|
organizer: { cn: 'test', cal_address: 'test@test.com' },
|
|
attendee: [
|
|
{
|
|
cn: 'test',
|
|
cal_address: 'test@test.com',
|
|
partstat: 'NEEDS-ACTION',
|
|
rsvp: 'TRUE',
|
|
role: 'REQ-PARTICIPANT',
|
|
cutype: 'INDIVIDUAL'
|
|
},
|
|
{
|
|
cn: 'John',
|
|
cal_address: 'john@test.com',
|
|
partstat: 'NEEDS-ACTION',
|
|
rsvp: 'TRUE',
|
|
role: 'REQ-PARTICIPANT',
|
|
cutype: 'INDIVIDUAL'
|
|
}
|
|
]
|
|
} as CalendarEvent
|
|
|
|
describe('eventApi', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
|
|
it('putEvent sends PUT request with JCal body', async () => {
|
|
const mockResponse = { status: 201, url: '/dav/cals/test.ics' }
|
|
;(api as unknown as jest.Mock).mockReturnValue(mockResponse)
|
|
const result = await putEvent(mockEvent)
|
|
const expectedResult = calendarEventToJCal(mockEvent)
|
|
expect(api).toHaveBeenCalledWith(
|
|
'dav/calendars/667037022b752d0026472254/667037022b752d0026472254/cal1.ics',
|
|
expect.objectContaining({
|
|
method: 'PUT',
|
|
headers: { 'content-type': 'text/calendar; charset=utf-8' },
|
|
body: JSON.stringify(expectedResult)
|
|
})
|
|
)
|
|
expect(result).toBe(mockResponse)
|
|
})
|
|
|
|
it('putEvent logs when status is 201', async () => {
|
|
const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation()
|
|
const mockResponse = { status: 201, url: '/dav/cals/test.ics' }
|
|
;(api as unknown as jest.Mock).mockReturnValue(mockResponse)
|
|
|
|
await putEvent(mockEvent)
|
|
|
|
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
|
'Event created successfully:',
|
|
'/dav/cals/test.ics'
|
|
)
|
|
|
|
consoleInfoSpy.mockRestore()
|
|
})
|
|
|
|
test('moveEvent sends MOVE request with destination header', async () => {
|
|
const mockResponse = { status: 204 }
|
|
;(api as unknown as jest.Mock).mockReturnValue({
|
|
json: jest.fn().mockResolvedValue(mockResponse)
|
|
})
|
|
const result = await moveEvent(mockEvent, 'newurl.ics')
|
|
|
|
expect(api).toHaveBeenCalledWith(
|
|
'dav/calendars/667037022b752d0026472254/667037022b752d0026472254/cal1.ics',
|
|
expect.objectContaining({
|
|
method: 'MOVE',
|
|
headers: {
|
|
destination: 'newurl.ics'
|
|
}
|
|
})
|
|
)
|
|
})
|
|
|
|
it('deleteEvent sends DELETE request and returns json response', async () => {
|
|
const mockResponse = { ok: true }
|
|
;(api as unknown as jest.Mock).mockReturnValue({
|
|
json: jest.fn().mockResolvedValue(mockResponse)
|
|
})
|
|
|
|
const result = await deleteEvent('/calendars/test.ics')
|
|
|
|
expect(api).toHaveBeenCalledWith('dav/calendars/test.ics', {
|
|
method: 'DELETE'
|
|
})
|
|
})
|
|
|
|
it('import event file', async () => {
|
|
const mockResponse = { status: 202 }
|
|
;(api.post as unknown as jest.Mock).mockReturnValue({
|
|
json: jest.fn().mockResolvedValue(mockResponse)
|
|
})
|
|
|
|
await importEventFromFile('123456789', '/calendar/calLink.json')
|
|
|
|
expect(api.post).toHaveBeenCalledWith('api/import', {
|
|
body: JSON.stringify({
|
|
fileId: '123456789',
|
|
target: '/calendar/calLink.json'
|
|
})
|
|
})
|
|
})
|
|
|
|
test('putEvent handles byday field correctly', async () => {
|
|
const mockResponse = { status: 201, url: '/dav/cals/test.ics' }
|
|
;(api as unknown as jest.Mock).mockReturnValue(mockResponse)
|
|
|
|
const eventWithByday = {
|
|
...mockEvent,
|
|
repetition: {
|
|
freq: 'weekly',
|
|
interval: 1,
|
|
byday: ['MO', 'WE', 'FR']
|
|
}
|
|
}
|
|
|
|
await putEvent(eventWithByday)
|
|
const expectedResult = calendarEventToJCal(eventWithByday)
|
|
|
|
expect(api).toHaveBeenCalledWith(
|
|
'dav/calendars/667037022b752d0026472254/667037022b752d0026472254/cal1.ics',
|
|
expect.objectContaining({
|
|
method: 'PUT',
|
|
headers: { 'content-type': 'text/calendar; charset=utf-8' },
|
|
body: JSON.stringify(expectedResult)
|
|
})
|
|
)
|
|
})
|
|
|
|
test('putEvent handles null byday field correctly', async () => {
|
|
const mockResponse = { status: 201, url: '/dav/cals/test.ics' }
|
|
;(api as unknown as jest.Mock).mockReturnValue(mockResponse)
|
|
|
|
const eventWithNullByday = {
|
|
...mockEvent,
|
|
repetition: {
|
|
freq: 'daily',
|
|
interval: 1,
|
|
byday: null
|
|
}
|
|
}
|
|
|
|
await putEvent(eventWithNullByday)
|
|
const expectedResult = calendarEventToJCal(eventWithNullByday)
|
|
|
|
expect(api).toHaveBeenCalledWith(
|
|
'dav/calendars/667037022b752d0026472254/667037022b752d0026472254/cal1.ics',
|
|
expect.objectContaining({
|
|
method: 'PUT',
|
|
headers: { 'content-type': 'text/calendar; charset=utf-8' },
|
|
body: JSON.stringify(expectedResult)
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('searchEvent', () => {
|
|
const mockFilters = {
|
|
searchIn: ['user1/calendar1', 'user2/calendar2'],
|
|
keywords: 'meeting',
|
|
organizers: ['org@example.com'],
|
|
attendees: ['part@example.com']
|
|
}
|
|
|
|
it('should call API with correct parameters', async () => {
|
|
const mockResponse = {
|
|
_total_hits: 5,
|
|
_embedded: { events: [] }
|
|
}
|
|
|
|
;(api.post as jest.Mock).mockReturnValue({
|
|
json: jest.fn().mockResolvedValue(mockResponse)
|
|
})
|
|
|
|
await searchEvent('test', mockFilters)
|
|
|
|
expect(api.post).toHaveBeenCalledWith(
|
|
'calendar/api/events/search?limit=30&offset=0',
|
|
{
|
|
body: JSON.stringify({
|
|
query: 'meeting',
|
|
calendars: [
|
|
{ calendarId: 'calendar1', userId: 'user1' },
|
|
{ calendarId: 'calendar2', userId: 'user2' }
|
|
],
|
|
organizers: ['org@example.com'],
|
|
attendees: ['part@example.com']
|
|
})
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should use query param when keywords is empty', async () => {
|
|
const mockResponse = { _total_hits: 0, _embedded: { events: [] } }
|
|
|
|
;(api.post as jest.Mock).mockReturnValue({
|
|
json: jest.fn().mockResolvedValue(mockResponse)
|
|
})
|
|
|
|
await searchEvent('fallback query', {
|
|
...mockFilters,
|
|
keywords: ''
|
|
})
|
|
|
|
expect(api.post).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
expect.objectContaining({
|
|
body: expect.stringContaining('"query":"fallback query"')
|
|
})
|
|
)
|
|
})
|
|
|
|
it('should omit organizers when empty', async () => {
|
|
const mockResponse = { _total_hits: 0, _embedded: { events: [] } }
|
|
|
|
;(api.post as jest.Mock).mockReturnValue({
|
|
json: jest.fn().mockResolvedValue(mockResponse)
|
|
})
|
|
|
|
await searchEvent('test', {
|
|
...mockFilters,
|
|
organizers: []
|
|
})
|
|
|
|
const callArgs = (api.post as jest.Mock).mock.calls[0][1]
|
|
const body = JSON.parse(callArgs.body)
|
|
expect(body.organizers).toBeUndefined()
|
|
})
|
|
|
|
it('should omit participants when empty', async () => {
|
|
const mockResponse = { _total_hits: 0, _embedded: { events: [] } }
|
|
|
|
;(api.post as jest.Mock).mockReturnValue({
|
|
json: jest.fn().mockResolvedValue(mockResponse)
|
|
})
|
|
|
|
await searchEvent('test', {
|
|
...mockFilters,
|
|
attendees: []
|
|
})
|
|
|
|
const callArgs = (api.post as jest.Mock).mock.calls[0][1]
|
|
const body = JSON.parse(callArgs.body)
|
|
expect(body.participants).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('updateSeries', () => {
|
|
const recurringEventICS = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
PRODID:-//Test//EN
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
DTSTART:20240201T100000Z
|
|
DTEND:20240201T110000Z
|
|
RRULE:FREQ=DAILY
|
|
SUMMARY:Old title
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240202T100000Z
|
|
DTSTART:20240202T100000Z
|
|
DTEND:20240202T110000Z
|
|
SUMMARY:Old title
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
`
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
;(api.get as jest.Mock).mockResolvedValue({
|
|
text: jest.fn().mockResolvedValue(recurringEventICS)
|
|
})
|
|
;(api as jest.Mock).mockResolvedValue({ ok: true })
|
|
})
|
|
|
|
it('propagates summary changes to overrides', async () => {
|
|
await updateSeries(
|
|
{ ...mockEvent, title: 'New title' } as any,
|
|
undefined,
|
|
false
|
|
)
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
|
|
const vevents = body[2].filter(([n]: any) => n === 'vevent')
|
|
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === 'recurrence-id')
|
|
)
|
|
|
|
const summary = override[1].find(([k]: any) => k === 'summary')
|
|
expect(summary[3]).toBe('New title')
|
|
})
|
|
|
|
it('does not remove recurrence-id from overrides', async () => {
|
|
await updateSeries(mockEvent as any, undefined, false)
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
|
|
const vevents = body[2].filter(([n]: any) => n === 'vevent')
|
|
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === 'recurrence-id')
|
|
)
|
|
|
|
expect(override).toBeDefined()
|
|
})
|
|
|
|
it('increments SEQUENCE on overrides when metadata changes', async () => {
|
|
await updateSeries(
|
|
{ ...mockEvent, title: 'Changed title' } as any,
|
|
undefined,
|
|
false
|
|
)
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
|
|
const vevents = body[2].filter(([n]: any) => n === 'vevent')
|
|
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === 'recurrence-id')
|
|
)
|
|
|
|
const sequence = override[1].find(([k]: any) => k === 'sequence')
|
|
expect(sequence[3]).toBe(2)
|
|
})
|
|
|
|
it('propagates description, location, class, and transp changes', async () => {
|
|
const icsWithMultipleFields = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
DTSTART:20240201T100000Z
|
|
DTEND:20240201T110000Z
|
|
RRULE:FREQ=DAILY
|
|
SUMMARY:Title
|
|
DESCRIPTION:Old description
|
|
LOCATION:Old location
|
|
CLASS:PUBLIC
|
|
TRANSP:OPAQUE
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240202T100000Z
|
|
DTSTART:20240202T100000Z
|
|
DTEND:20240202T110000Z
|
|
SUMMARY:Title
|
|
DESCRIPTION:Old description
|
|
LOCATION:Old location
|
|
CLASS:PUBLIC
|
|
TRANSP:OPAQUE
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
`
|
|
|
|
;(api.get as jest.Mock).mockResolvedValueOnce({
|
|
text: jest.fn().mockResolvedValue(icsWithMultipleFields)
|
|
})
|
|
|
|
await updateSeries(
|
|
{
|
|
...mockEvent,
|
|
description: 'New description',
|
|
location: 'New location',
|
|
class: 'PRIVATE',
|
|
transp: 'TRANSPARENT'
|
|
} as any,
|
|
undefined,
|
|
false
|
|
)
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
|
|
const vevents = body[2].filter(([n]: any) => n === 'vevent')
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === 'recurrence-id')
|
|
)
|
|
|
|
const description = override[1].find(([k]: any) => k === 'description')
|
|
const location = override[1].find(([k]: any) => k === 'location')
|
|
const classField = override[1].find(([k]: any) => k === 'class')
|
|
const transp = override[1].find(([k]: any) => k === 'transp')
|
|
|
|
expect(description[3]).toBe('New description')
|
|
expect(location[3]).toBe('New location')
|
|
expect(classField[3]).toBe('PRIVATE')
|
|
expect(transp[3]).toBe('TRANSPARENT')
|
|
})
|
|
|
|
it('propagates organizer changes', async () => {
|
|
const icsWithOrganizer = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
DTSTART:20240201T100000Z
|
|
RRULE:FREQ=DAILY
|
|
SUMMARY:Title
|
|
ORGANIZER;CN=Alice:mailto:alice@example.com
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240202T100000Z
|
|
DTSTART:20240202T100000Z
|
|
SUMMARY:Title
|
|
ORGANIZER;CN=Alice:mailto:alice@example.com
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
`
|
|
|
|
;(api.get as jest.Mock).mockResolvedValueOnce({
|
|
text: jest.fn().mockResolvedValue(icsWithOrganizer)
|
|
})
|
|
|
|
await updateSeries(
|
|
{
|
|
...mockEvent,
|
|
organizer: { cn: 'Bob', cal_address: 'bob@example.com' }
|
|
} as any,
|
|
undefined,
|
|
false
|
|
)
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
|
|
const vevents = body[2].filter(([n]: any) => n === 'vevent')
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === 'recurrence-id')
|
|
)
|
|
|
|
const organizer = override[1].find(([k]: any) => k === 'organizer')
|
|
expect(organizer[3]).toBe('mailto:bob@example.com')
|
|
expect(organizer[1].cn).toBe('Bob')
|
|
})
|
|
|
|
it('propagates attendee changes', async () => {
|
|
const icsWithAttendees = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
DTSTART:20240201T100000Z
|
|
RRULE:FREQ=DAILY
|
|
SUMMARY:Title
|
|
ATTENDEE;CN=Alice:mailto:alice@example.com
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240202T100000Z
|
|
DTSTART:20240202T100000Z
|
|
SUMMARY:Title
|
|
ATTENDEE;CN=Alice:mailto:alice@example.com
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
`
|
|
|
|
;(api.get as jest.Mock).mockResolvedValueOnce({
|
|
text: jest.fn().mockResolvedValue(icsWithAttendees)
|
|
})
|
|
|
|
await updateSeries(
|
|
{
|
|
...mockEvent,
|
|
attendee: [
|
|
{
|
|
cn: 'Bob',
|
|
cal_address: 'bob@example.com',
|
|
partstat: 'NEEDS-ACTION',
|
|
cutype: 'INDIVIDUAL',
|
|
role: 'REQ-PARTICIPANT',
|
|
rsvp: 'TRUE'
|
|
},
|
|
{
|
|
cn: 'Charlie',
|
|
cal_address: 'charlie@example.com',
|
|
partstat: 'NEEDS-ACTION',
|
|
cutype: 'INDIVIDUAL',
|
|
role: 'REQ-PARTICIPANT',
|
|
rsvp: 'TRUE'
|
|
}
|
|
]
|
|
} as any,
|
|
undefined,
|
|
false
|
|
)
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
|
|
const vevents = body[2].filter(([n]: any) => n === 'vevent')
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === 'recurrence-id')
|
|
)
|
|
|
|
const attendees = override[1].filter(([k]: any) => k === 'attendee')
|
|
expect(attendees).toHaveLength(2)
|
|
expect(attendees[0][3]).toBe('mailto:bob@example.com')
|
|
expect(attendees[1][3]).toBe('mailto:charlie@example.com')
|
|
})
|
|
|
|
it('propagates x-openpaas-videoconference changes', async () => {
|
|
const icsWithVideo = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
DTSTART:20240201T100000Z
|
|
RRULE:FREQ=DAILY
|
|
SUMMARY:Title
|
|
X-OPENPAAS-VIDEOCONFERENCE:https://meet.old.com
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240202T100000Z
|
|
DTSTART:20240202T100000Z
|
|
SUMMARY:Title
|
|
X-OPENPAAS-VIDEOCONFERENCE:https://meet.old.com
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
`
|
|
|
|
;(api.get as jest.Mock).mockResolvedValueOnce({
|
|
text: jest.fn().mockResolvedValue(icsWithVideo)
|
|
})
|
|
|
|
await updateSeries(
|
|
{
|
|
...mockEvent,
|
|
x_openpass_videoconference: 'https://meet.new.com'
|
|
} as any,
|
|
undefined,
|
|
false
|
|
)
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
|
|
const vevents = body[2].filter(([n]: any) => n === 'vevent')
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === 'recurrence-id')
|
|
)
|
|
|
|
const videoconf = override[1].find(
|
|
([k]: any) => k === 'x-openpaas-videoconference'
|
|
)
|
|
expect(videoconf[3]).toBe('https://meet.new.com')
|
|
})
|
|
|
|
it('works with multiple override instances', async () => {
|
|
const icsMultipleOverrides = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
DTSTART:20240201T100000Z
|
|
DTEND:20240201T110000Z
|
|
RRULE:FREQ=DAILY
|
|
SUMMARY:Old
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240202T100000Z
|
|
DTSTART:20240202T100000Z
|
|
DTEND:20240202T110000Z
|
|
SUMMARY:Old
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240203T100000Z
|
|
DTSTART:20240203T100000Z
|
|
DTEND:20240203T110000Z
|
|
SUMMARY:Old
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
`
|
|
|
|
;(api.get as jest.Mock).mockResolvedValueOnce({
|
|
text: jest.fn().mockResolvedValue(icsMultipleOverrides)
|
|
})
|
|
|
|
await updateSeries(
|
|
{ ...mockEvent, title: 'New title' } as any,
|
|
undefined,
|
|
false
|
|
)
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
|
|
const vevents = body[2].filter(([n]: any) => n === 'vevent')
|
|
const overrides = vevents.filter(([, props]: any) =>
|
|
props.some(([k]: any) => k === 'recurrence-id')
|
|
)
|
|
|
|
expect(overrides).toHaveLength(2)
|
|
overrides.forEach(override => {
|
|
const summary = override[1].find(([k]: any) => k === 'summary')
|
|
const sequence = override[1].find(([k]: any) => k === 'sequence')
|
|
expect(summary[3]).toBe('New title')
|
|
expect(sequence[3]).toBe(2)
|
|
})
|
|
})
|
|
|
|
it('adds sequence field when missing and metadata changes', async () => {
|
|
const icsNoSequence = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
DTSTART:20240201T100000Z
|
|
DTEND:20240201T110000Z
|
|
RRULE:FREQ=DAILY
|
|
SUMMARY:Old
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240202T100000Z
|
|
DTSTART:20240202T100000Z
|
|
DTEND:20240202T110000Z
|
|
SUMMARY:Old
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
`
|
|
|
|
;(api.get as jest.Mock).mockResolvedValueOnce({
|
|
text: jest.fn().mockResolvedValue(icsNoSequence)
|
|
})
|
|
|
|
await updateSeries(
|
|
{ ...mockEvent, title: 'New title' } as any,
|
|
undefined,
|
|
false
|
|
)
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
|
|
const vevents = body[2].filter(([n]: any) => n === 'vevent')
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === 'recurrence-id')
|
|
)
|
|
|
|
const sequence = override[1].find(([k]: any) => k === 'sequence')
|
|
expect(sequence[3]).toBe(1)
|
|
})
|
|
|
|
it('preserves override-specific fields when propagating metadata', async () => {
|
|
const icsCustomFields = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
DTSTART:20240201T100000Z
|
|
DTEND:20240201T110000Z
|
|
RRULE:FREQ=DAILY
|
|
SUMMARY:Old
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240202T100000Z
|
|
DTSTART:20240202T140000Z
|
|
DTEND:20240202T150000Z
|
|
SUMMARY:Old
|
|
DESCRIPTION:custom-value
|
|
SEQUENCE:1
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
`
|
|
|
|
;(api.get as jest.Mock).mockResolvedValueOnce({
|
|
text: jest.fn().mockResolvedValue(icsCustomFields)
|
|
})
|
|
|
|
await updateSeries(
|
|
{ ...mockEvent, title: 'New title' } as any,
|
|
undefined,
|
|
false
|
|
)
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
|
|
const vevents = body[2].filter(([n]: any) => n === 'vevent')
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === 'recurrence-id')
|
|
)
|
|
|
|
const summary = override[1].find(([k]: any) => k === 'summary')
|
|
const dtstart = override[1].find(([k]: any) => k === 'dtstart')
|
|
const dtend = override[1].find(([k]: any) => k === 'dtend')
|
|
const description = override[1].find(([k]: any) => k === 'description')
|
|
const recurrenceId = override[1].find(([k]: any) => k === 'recurrence-id')
|
|
// Summary should be updated
|
|
expect(summary[3]).toBe('New title')
|
|
// Override-specific time should be preserved
|
|
expect(dtstart[3]).toBe('2024-02-02T14:00:00Z')
|
|
expect(dtend[3]).toBe('2024-02-02T15:00:00Z')
|
|
// Custom field should be preserved
|
|
expect(description[3]).toBe('custom-value')
|
|
// Recurrence-id should be preserved
|
|
expect(recurrenceId[3]).toBe('2024-02-02T10:00:00Z')
|
|
})
|
|
|
|
const recurringWithAlarmICS = `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
DTSTART:20240201T100000Z
|
|
RRULE:FREQ=DAILY
|
|
SUMMARY:Old
|
|
BEGIN:VALARM
|
|
ACTION:DISPLAY
|
|
TRIGGER:-PT15M
|
|
END:VALARM
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
UID:event1
|
|
RECURRENCE-ID:20240202T100000Z
|
|
DTSTART:20240202T100000Z
|
|
SUMMARY:Old
|
|
BEGIN:VALARM
|
|
ACTION:DISPLAY
|
|
TRIGGER:-PT15M
|
|
END:VALARM
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
`
|
|
|
|
it('replaces VALARM on overrides when alarm changes', async () => {
|
|
;(api.get as jest.Mock).mockResolvedValueOnce({
|
|
text: jest.fn().mockResolvedValue(recurringWithAlarmICS)
|
|
})
|
|
|
|
await updateSeries(
|
|
{ ...mockEvent, alarm: { action: 'EMAIL', trigger: '-PT30M' } } as any,
|
|
undefined,
|
|
false
|
|
)
|
|
|
|
const body = JSON.parse((api as jest.Mock).mock.calls[0][1].body)
|
|
const vevents = body[2].filter(([n]: any) => n === 'vevent')
|
|
|
|
const override = vevents.find(([, props]: any) =>
|
|
props.some(([k]: any) => k === 'recurrence-id')
|
|
)
|
|
|
|
const alarms = override[2].filter(([n]: any) => n === 'valarm')
|
|
|
|
expect(alarms).toHaveLength(1)
|
|
const action = alarms[0][1].find(([k]: any) => k === 'action')
|
|
const trigger = alarms[0][1].find(([k]: any) => k === 'trigger')
|
|
expect(action[3]).toBe('EMAIL')
|
|
expect(trigger[3]).toBe('-PT30M')
|
|
})
|
|
})
|
|
})
|