caldav-adapter 8.3.4 → 8.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "caldav-adapter",
|
|
3
3
|
"description": "CalDAV server for Node.js and Koa. Modernized and maintained for Forward Email.",
|
|
4
|
-
"version": "8.3.
|
|
4
|
+
"version": "8.3.5",
|
|
5
5
|
"author": "Sanders DeNardi and Forward Email LLC",
|
|
6
6
|
"contributors": [
|
|
7
7
|
"Sanders DeNardi <sedenardi@gmail.com> (http://www.sandersdenardi.com/)",
|
|
@@ -12,6 +12,21 @@ module.exports = function (options) {
|
|
|
12
12
|
|
|
13
13
|
return async function (ctx, events, calendar, children) {
|
|
14
14
|
const eventActions = _.map(events, async (event) => {
|
|
15
|
+
//
|
|
16
|
+
// For deleted events without href, we cannot reliably construct the
|
|
17
|
+
// correct URL that the client originally used. Returning an incorrect
|
|
18
|
+
// URL causes Apple Calendar to fail with "Couldn't get a calendar item
|
|
19
|
+
// to remove" errors because the URL doesn't match its local cache.
|
|
20
|
+
//
|
|
21
|
+
// Skip these events in sync-collection responses. The client will
|
|
22
|
+
// eventually clean them up through other sync mechanisms.
|
|
23
|
+
//
|
|
24
|
+
// See: https://www.rfc-editor.org/rfc/rfc6578.html (sync-collection)
|
|
25
|
+
//
|
|
26
|
+
if (event.deleted_at && !event.href) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
15
30
|
const misses = [];
|
|
16
31
|
const propActions = _.map(children, async (child) => {
|
|
17
32
|
return tags.getResponse({
|
|
@@ -65,6 +80,7 @@ module.exports = function (options) {
|
|
|
65
80
|
return resp;
|
|
66
81
|
});
|
|
67
82
|
const responses = await Promise.all(eventActions);
|
|
68
|
-
|
|
83
|
+
// Filter out null responses (deleted events without href)
|
|
84
|
+
return { responses: _.compact(responses) };
|
|
69
85
|
};
|
|
70
86
|
};
|
|
@@ -95,15 +95,17 @@ test('event-response uses event.href when available instead of constructing from
|
|
|
95
95
|
t.is(responseObj['D:href'], '/cal/user/calendar/original@event.ics');
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
test('event-response returns 404 status for deleted events', async (t) => {
|
|
98
|
+
test('event-response returns 404 status for deleted events with href', async (t) => {
|
|
99
99
|
const options = createMockOptions();
|
|
100
100
|
const eventResponse = eventResponseFactory(options);
|
|
101
101
|
const ctx = createMockCtx('/cal/user/calendar');
|
|
102
102
|
const calendar = createMockCalendar();
|
|
103
103
|
|
|
104
|
+
// Deleted event WITH href - should be included in response
|
|
104
105
|
const events = [
|
|
105
106
|
{
|
|
106
107
|
eventId: 'deleted-event',
|
|
108
|
+
href: '/cal/user/calendar/deleted-event.ics',
|
|
107
109
|
deleted_at: new Date(),
|
|
108
110
|
ical: 'BEGIN:VCALENDAR\nEND:VCALENDAR'
|
|
109
111
|
}
|
|
@@ -125,6 +127,30 @@ test('event-response returns 404 status for deleted events', async (t) => {
|
|
|
125
127
|
t.true(responseObj['D:status'].includes('404'));
|
|
126
128
|
});
|
|
127
129
|
|
|
130
|
+
test('event-response skips deleted events without href', async (t) => {
|
|
131
|
+
const options = createMockOptions();
|
|
132
|
+
const eventResponse = eventResponseFactory(options);
|
|
133
|
+
const ctx = createMockCtx('/cal/user/calendar');
|
|
134
|
+
const calendar = createMockCalendar();
|
|
135
|
+
|
|
136
|
+
// Deleted event WITHOUT href - should be skipped to prevent Apple sync errors
|
|
137
|
+
const events = [
|
|
138
|
+
{
|
|
139
|
+
eventId: 'deleted-event-no-href',
|
|
140
|
+
deleted_at: new Date(),
|
|
141
|
+
ical: 'BEGIN:VCALENDAR\nEND:VCALENDAR'
|
|
142
|
+
}
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const children = [];
|
|
146
|
+
|
|
147
|
+
const result = await eventResponse(ctx, events, calendar, children);
|
|
148
|
+
|
|
149
|
+
t.truthy(result.responses);
|
|
150
|
+
// Should be empty - deleted events without href are skipped
|
|
151
|
+
t.is(result.responses.length, 0);
|
|
152
|
+
});
|
|
153
|
+
|
|
128
154
|
test('event-response uses href for deleted events when available (critical for sync)', async (t) => {
|
|
129
155
|
const options = createMockOptions();
|
|
130
156
|
const eventResponse = eventResponseFactory(options);
|
|
@@ -199,6 +225,7 @@ test('event-response handles multiple events correctly', async (t) => {
|
|
|
199
225
|
},
|
|
200
226
|
{
|
|
201
227
|
eventId: 'event3',
|
|
228
|
+
href: '/cal/user/calendar/event3.ics',
|
|
202
229
|
deleted_at: new Date(),
|
|
203
230
|
ical: 'BEGIN:VCALENDAR\nEND:VCALENDAR'
|
|
204
231
|
}
|
|
@@ -217,11 +244,55 @@ test('event-response handles multiple events correctly', async (t) => {
|
|
|
217
244
|
// Event 2: uses href
|
|
218
245
|
t.is(result.responses[1]['D:href'], '/cal/user/calendar/custom-path.ics');
|
|
219
246
|
|
|
220
|
-
// Event 3: deleted
|
|
247
|
+
// Event 3: deleted with href
|
|
221
248
|
t.is(result.responses[2]['D:href'], '/cal/user/calendar/event3.ics');
|
|
222
249
|
t.true(result.responses[2]['D:status'].includes('404'));
|
|
223
250
|
});
|
|
224
251
|
|
|
252
|
+
test('event-response handles multiple events with mixed deleted states', async (t) => {
|
|
253
|
+
const options = createMockOptions();
|
|
254
|
+
const eventResponse = eventResponseFactory(options);
|
|
255
|
+
const ctx = createMockCtx('/cal/user/calendar');
|
|
256
|
+
const calendar = createMockCalendar();
|
|
257
|
+
|
|
258
|
+
// Mix of events: active, deleted with href, deleted without href
|
|
259
|
+
const events = [
|
|
260
|
+
{
|
|
261
|
+
eventId: 'active-event',
|
|
262
|
+
ical: 'BEGIN:VCALENDAR\nEND:VCALENDAR'
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
eventId: 'deleted-with-href',
|
|
266
|
+
href: '/cal/user/calendar/deleted-with-href.ics',
|
|
267
|
+
deleted_at: new Date(),
|
|
268
|
+
ical: 'BEGIN:VCALENDAR\nEND:VCALENDAR'
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
eventId: 'deleted-no-href',
|
|
272
|
+
deleted_at: new Date(),
|
|
273
|
+
ical: 'BEGIN:VCALENDAR\nEND:VCALENDAR'
|
|
274
|
+
}
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
const children = [];
|
|
278
|
+
|
|
279
|
+
const result = await eventResponse(ctx, events, calendar, children);
|
|
280
|
+
|
|
281
|
+
t.truthy(result.responses);
|
|
282
|
+
// Only 2 responses - deleted without href is skipped
|
|
283
|
+
t.is(result.responses.length, 2);
|
|
284
|
+
|
|
285
|
+
// Active event
|
|
286
|
+
t.is(result.responses[0]['D:href'], '/cal/user/calendar/active-event.ics');
|
|
287
|
+
|
|
288
|
+
// Deleted event with href
|
|
289
|
+
t.is(
|
|
290
|
+
result.responses[1]['D:href'],
|
|
291
|
+
'/cal/user/calendar/deleted-with-href.ics'
|
|
292
|
+
);
|
|
293
|
+
t.true(result.responses[1]['D:status'].includes('404'));
|
|
294
|
+
});
|
|
295
|
+
|
|
225
296
|
test('event-response handles email-like eventId with @ symbol', async (t) => {
|
|
226
297
|
const options = createMockOptions();
|
|
227
298
|
const eventResponse = eventResponseFactory(options);
|