caldav-adapter 9.2.0 → 9.3.1
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/common/xml-encode.js +24 -0
- package/index.js +18 -10
- package/package.json +7 -2
- package/routes/calendar/calendar/calendar-multiget.js +5 -1
- package/routes/calendar/calendar/delete.js +1 -1
- package/routes/calendar/calendar/propfind.js +11 -0
- package/routes/calendar/calendar/put.js +19 -1
- package/routes/calendar/calendar/report.js +4 -1
- package/routes/calendar/calendar/sync-collection.js +18 -1
- package/routes/calendar/calendar.js +45 -18
- package/routes/calendar/scheduling.js +14 -6
- package/routes/calendar/user/propfind.js +10 -0
- package/routes/principal/mkcalendar.js +57 -34
- package/routes/principal/propfind.js +24 -7
- package/routes/principal/report.js +6 -0
- package/.commitlintrc.js +0 -3
- package/.editorconfig +0 -9
- package/.gitattributes +0 -1
- package/.github/workflows/ci.yml +0 -24
- package/.husky/commit-msg +0 -4
- package/.husky/pre-commit +0 -4
- package/.lintstagedrc.js +0 -5
- package/.prettierrc.js +0 -5
- package/.remarkignore +0 -1
- package/.remarkrc.js +0 -3
- package/.xo-config.js +0 -8
- package/test/event-response.test.js +0 -383
- package/test/proppatch.test.js +0 -1254
- package/test/scheduling.test.js +0 -697
- package/test/test.js +0 -41
package/test/scheduling.test.js
DELETED
|
@@ -1,697 +0,0 @@
|
|
|
1
|
-
const test = require('ava');
|
|
2
|
-
|
|
3
|
-
// Mock options for testing
|
|
4
|
-
const createMockOptions = (overrides = {}) => ({
|
|
5
|
-
data: {
|
|
6
|
-
getCalendarId: () => 'test-calendar',
|
|
7
|
-
buildICS: () => 'BEGIN:VCALENDAR\r\nEND:VCALENDAR',
|
|
8
|
-
getETag: () => '"test-etag"',
|
|
9
|
-
...overrides.data
|
|
10
|
-
},
|
|
11
|
-
logEnabled: false,
|
|
12
|
-
...overrides
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
// Mock Koa context
|
|
16
|
-
const createMockCtx = (overrides = {}) => ({
|
|
17
|
-
method: 'GET',
|
|
18
|
-
url: '/cal/user@example.com/',
|
|
19
|
-
status: 200,
|
|
20
|
-
body: '',
|
|
21
|
-
request: {
|
|
22
|
-
body: ''
|
|
23
|
-
},
|
|
24
|
-
state: {
|
|
25
|
-
user: {
|
|
26
|
-
principalId: 'user@example.com',
|
|
27
|
-
principalName: 'user@example.com',
|
|
28
|
-
email: 'user@example.com'
|
|
29
|
-
},
|
|
30
|
-
params: {
|
|
31
|
-
principalId: 'user@example.com'
|
|
32
|
-
},
|
|
33
|
-
calendarHomeUrl: '/cal/user@example.com/',
|
|
34
|
-
principalUrl: '/p/user@example.com/',
|
|
35
|
-
calendarUrl: '/cal/user@example.com/default/'
|
|
36
|
-
},
|
|
37
|
-
set() {},
|
|
38
|
-
...overrides
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// ============================================
|
|
42
|
-
// Tests for common/response.js DAV header
|
|
43
|
-
// ============================================
|
|
44
|
-
|
|
45
|
-
test('DAV header includes calendar-auto-schedule capability', (t) => {
|
|
46
|
-
const response = require('../common/response');
|
|
47
|
-
|
|
48
|
-
// Create a mock context to capture the header
|
|
49
|
-
let davHeader = '';
|
|
50
|
-
const ctx = {
|
|
51
|
-
status: 0,
|
|
52
|
-
body: '',
|
|
53
|
-
set(name, value) {
|
|
54
|
-
if (name === 'DAV') {
|
|
55
|
-
davHeader = value;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
response.setOptions(ctx, ['OPTIONS']);
|
|
61
|
-
|
|
62
|
-
t.true(
|
|
63
|
-
davHeader.includes('calendar-auto-schedule'),
|
|
64
|
-
'DAV header should include calendar-auto-schedule'
|
|
65
|
-
);
|
|
66
|
-
t.true(
|
|
67
|
-
davHeader.includes('calendar-schedule'),
|
|
68
|
-
'DAV header should include calendar-schedule'
|
|
69
|
-
);
|
|
70
|
-
t.true(
|
|
71
|
-
davHeader.includes('calendar-access'),
|
|
72
|
-
'DAV header should include calendar-access'
|
|
73
|
-
);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// ============================================
|
|
77
|
-
// Tests for common/tags.js scheduling properties
|
|
78
|
-
// ============================================
|
|
79
|
-
|
|
80
|
-
test('schedule-inbox-URL returns correct URL when calendarHomeUrl is set', async (t) => {
|
|
81
|
-
const options = createMockOptions();
|
|
82
|
-
const { tags } = require('../common/tags')(options);
|
|
83
|
-
|
|
84
|
-
const scheduleInboxTag =
|
|
85
|
-
tags['urn:ietf:params:xml:ns:caldav']['schedule-inbox-URL'];
|
|
86
|
-
t.is(typeof scheduleInboxTag, 'object');
|
|
87
|
-
t.is(typeof scheduleInboxTag.resp, 'function');
|
|
88
|
-
|
|
89
|
-
const ctx = createMockCtx();
|
|
90
|
-
const result = await scheduleInboxTag.resp({ ctx });
|
|
91
|
-
|
|
92
|
-
t.truthy(result);
|
|
93
|
-
const tagKey = Object.keys(result)[0];
|
|
94
|
-
t.true(tagKey.includes('schedule-inbox-URL'));
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test('schedule-outbox-URL returns correct URL when calendarHomeUrl is set', async (t) => {
|
|
98
|
-
const options = createMockOptions();
|
|
99
|
-
const { tags } = require('../common/tags')(options);
|
|
100
|
-
|
|
101
|
-
const scheduleOutboxTag =
|
|
102
|
-
tags['urn:ietf:params:xml:ns:caldav']['schedule-outbox-URL'];
|
|
103
|
-
t.is(typeof scheduleOutboxTag, 'object');
|
|
104
|
-
t.is(typeof scheduleOutboxTag.resp, 'function');
|
|
105
|
-
|
|
106
|
-
const ctx = createMockCtx();
|
|
107
|
-
const result = await scheduleOutboxTag.resp({ ctx });
|
|
108
|
-
|
|
109
|
-
t.truthy(result);
|
|
110
|
-
const tagKey = Object.keys(result)[0];
|
|
111
|
-
t.true(tagKey.includes('schedule-outbox-URL'));
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test('schedule-default-calendar-URL returns correct URL when calendarUrl is set', async (t) => {
|
|
115
|
-
const options = createMockOptions();
|
|
116
|
-
const { tags } = require('../common/tags')(options);
|
|
117
|
-
|
|
118
|
-
const scheduleDefaultCalTag =
|
|
119
|
-
tags['urn:ietf:params:xml:ns:caldav']['schedule-default-calendar-URL'];
|
|
120
|
-
t.is(typeof scheduleDefaultCalTag, 'object');
|
|
121
|
-
t.is(typeof scheduleDefaultCalTag.resp, 'function');
|
|
122
|
-
|
|
123
|
-
const ctx = createMockCtx();
|
|
124
|
-
const result = await scheduleDefaultCalTag.resp({ ctx });
|
|
125
|
-
|
|
126
|
-
t.truthy(result);
|
|
127
|
-
const tagKey = Object.keys(result)[0];
|
|
128
|
-
t.true(tagKey.includes('schedule-default-calendar-URL'));
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test('schedule-calendar-transp returns opaque by default', async (t) => {
|
|
132
|
-
const options = createMockOptions();
|
|
133
|
-
const { tags } = require('../common/tags')(options);
|
|
134
|
-
|
|
135
|
-
const scheduleTranspTag =
|
|
136
|
-
tags['urn:ietf:params:xml:ns:caldav']['schedule-calendar-transp'];
|
|
137
|
-
t.is(typeof scheduleTranspTag, 'object');
|
|
138
|
-
t.is(typeof scheduleTranspTag.resp, 'function');
|
|
139
|
-
|
|
140
|
-
const calendar = { name: 'Test Calendar' };
|
|
141
|
-
const result = await scheduleTranspTag.resp({
|
|
142
|
-
resource: 'calendar',
|
|
143
|
-
calendar
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
t.truthy(result);
|
|
147
|
-
const tagKey = Object.keys(result)[0];
|
|
148
|
-
t.true(tagKey.includes('schedule-calendar-transp'));
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test('schedule-calendar-transp respects calendar scheduleTransp property', async (t) => {
|
|
152
|
-
const options = createMockOptions();
|
|
153
|
-
const { tags } = require('../common/tags')(options);
|
|
154
|
-
|
|
155
|
-
const scheduleTranspTag =
|
|
156
|
-
tags['urn:ietf:params:xml:ns:caldav']['schedule-calendar-transp'];
|
|
157
|
-
|
|
158
|
-
const calendar = { name: 'Test Calendar', scheduleTransp: 'transparent' };
|
|
159
|
-
const result = await scheduleTranspTag.resp({
|
|
160
|
-
resource: 'calendar',
|
|
161
|
-
calendar
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
t.truthy(result);
|
|
165
|
-
// The result should contain the transparent value
|
|
166
|
-
const resultStr = JSON.stringify(result);
|
|
167
|
-
t.true(resultStr.includes('transparent'));
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test('schedule-tag returns value when event has scheduleTag', async (t) => {
|
|
171
|
-
const options = createMockOptions();
|
|
172
|
-
const { tags } = require('../common/tags')(options);
|
|
173
|
-
|
|
174
|
-
const scheduleTagProp = tags['urn:ietf:params:xml:ns:caldav']['schedule-tag'];
|
|
175
|
-
t.is(typeof scheduleTagProp, 'object');
|
|
176
|
-
t.is(typeof scheduleTagProp.resp, 'function');
|
|
177
|
-
|
|
178
|
-
const event = { scheduleTag: '"schedule-tag-123"' };
|
|
179
|
-
const result = await scheduleTagProp.resp({ resource: 'event', event });
|
|
180
|
-
|
|
181
|
-
t.truthy(result);
|
|
182
|
-
const tagKey = Object.keys(result)[0];
|
|
183
|
-
t.true(tagKey.includes('schedule-tag'));
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test('schedule-tag returns undefined when event has no scheduleTag', async (t) => {
|
|
187
|
-
const options = createMockOptions();
|
|
188
|
-
const { tags } = require('../common/tags')(options);
|
|
189
|
-
|
|
190
|
-
const scheduleTagProp = tags['urn:ietf:params:xml:ns:caldav']['schedule-tag'];
|
|
191
|
-
|
|
192
|
-
const event = { uid: 'test-event' };
|
|
193
|
-
const result = await scheduleTagProp.resp({ resource: 'event', event });
|
|
194
|
-
|
|
195
|
-
t.is(result, undefined);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// ============================================
|
|
199
|
-
// Tests for scheduling.js routes
|
|
200
|
-
// ============================================
|
|
201
|
-
|
|
202
|
-
test('scheduling module exports required functions', (t) => {
|
|
203
|
-
const options = createMockOptions();
|
|
204
|
-
const scheduling = require('../routes/calendar/scheduling')(options);
|
|
205
|
-
|
|
206
|
-
t.is(typeof scheduling.postOutbox, 'function');
|
|
207
|
-
t.is(typeof scheduling.propfindInbox, 'function');
|
|
208
|
-
t.is(typeof scheduling.getInbox, 'function');
|
|
209
|
-
t.is(typeof scheduling.propfindOutbox, 'function');
|
|
210
|
-
t.is(typeof scheduling.route, 'function');
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test('scheduling route handler sets OPTIONS for outbox', async (t) => {
|
|
214
|
-
const options = createMockOptions();
|
|
215
|
-
const scheduling = require('../routes/calendar/scheduling')(options);
|
|
216
|
-
|
|
217
|
-
let allowHeader = '';
|
|
218
|
-
const ctx = createMockCtx({
|
|
219
|
-
method: 'OPTIONS',
|
|
220
|
-
url: '/cal/user@example.com/outbox/',
|
|
221
|
-
set(name, value) {
|
|
222
|
-
if (name === 'Allow') {
|
|
223
|
-
allowHeader = value;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
await scheduling.route(ctx);
|
|
229
|
-
|
|
230
|
-
t.true(allowHeader.includes('POST'));
|
|
231
|
-
t.true(allowHeader.includes('PROPFIND'));
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
test('scheduling route handler sets OPTIONS for inbox', async (t) => {
|
|
235
|
-
const options = createMockOptions();
|
|
236
|
-
const scheduling = require('../routes/calendar/scheduling')(options);
|
|
237
|
-
|
|
238
|
-
let allowHeader = '';
|
|
239
|
-
const ctx = createMockCtx({
|
|
240
|
-
method: 'OPTIONS',
|
|
241
|
-
url: '/cal/user@example.com/inbox/',
|
|
242
|
-
set(name, value) {
|
|
243
|
-
if (name === 'Allow') {
|
|
244
|
-
allowHeader = value;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
await scheduling.route(ctx);
|
|
250
|
-
|
|
251
|
-
t.true(allowHeader.includes('GET'));
|
|
252
|
-
t.true(allowHeader.includes('PROPFIND'));
|
|
253
|
-
t.true(allowHeader.includes('DELETE'));
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
test('propfindInbox returns schedule-inbox resourcetype', async (t) => {
|
|
257
|
-
const options = createMockOptions();
|
|
258
|
-
const scheduling = require('../routes/calendar/scheduling')(options);
|
|
259
|
-
|
|
260
|
-
const ctx = createMockCtx({
|
|
261
|
-
method: 'PROPFIND',
|
|
262
|
-
url: '/cal/user@example.com/inbox/'
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
await scheduling.propfindInbox(ctx);
|
|
266
|
-
|
|
267
|
-
t.is(ctx.status, 207);
|
|
268
|
-
t.truthy(ctx.body);
|
|
269
|
-
t.true(ctx.body.includes('schedule-inbox'));
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
test('propfindOutbox returns schedule-outbox resourcetype', async (t) => {
|
|
273
|
-
const options = createMockOptions();
|
|
274
|
-
const scheduling = require('../routes/calendar/scheduling')(options);
|
|
275
|
-
|
|
276
|
-
const ctx = createMockCtx({
|
|
277
|
-
method: 'PROPFIND',
|
|
278
|
-
url: '/cal/user@example.com/outbox/'
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
await scheduling.propfindOutbox(ctx);
|
|
282
|
-
|
|
283
|
-
t.is(ctx.status, 207);
|
|
284
|
-
t.truthy(ctx.body);
|
|
285
|
-
t.true(ctx.body.includes('schedule-outbox'));
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
test('getInbox returns empty collection by default', async (t) => {
|
|
289
|
-
const options = createMockOptions();
|
|
290
|
-
const scheduling = require('../routes/calendar/scheduling')(options);
|
|
291
|
-
|
|
292
|
-
const ctx = createMockCtx({
|
|
293
|
-
method: 'GET',
|
|
294
|
-
url: '/cal/user@example.com/inbox/'
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
await scheduling.getInbox(ctx);
|
|
298
|
-
|
|
299
|
-
t.is(ctx.status, 207);
|
|
300
|
-
t.truthy(ctx.body);
|
|
301
|
-
t.true(ctx.body.includes('schedule-inbox'));
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
test('getInbox uses options.data.getSchedulingMessages when available', async (t) => {
|
|
305
|
-
const mockMessages = [
|
|
306
|
-
{
|
|
307
|
-
href: '/cal/user@example.com/inbox/msg1.ics',
|
|
308
|
-
etag: '"etag1"',
|
|
309
|
-
icalData: 'BEGIN:VCALENDAR\r\nEND:VCALENDAR'
|
|
310
|
-
}
|
|
311
|
-
];
|
|
312
|
-
|
|
313
|
-
const options = createMockOptions({
|
|
314
|
-
data: {
|
|
315
|
-
getSchedulingMessages: async () => mockMessages
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
const scheduling = require('../routes/calendar/scheduling')(options);
|
|
319
|
-
|
|
320
|
-
const ctx = createMockCtx({
|
|
321
|
-
method: 'GET',
|
|
322
|
-
url: '/cal/user@example.com/inbox/'
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
await scheduling.getInbox(ctx);
|
|
326
|
-
|
|
327
|
-
t.is(ctx.status, 207);
|
|
328
|
-
t.truthy(ctx.body);
|
|
329
|
-
t.true(ctx.body.includes('msg1.ics'));
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
test('postOutbox returns 400 when body is missing', async (t) => {
|
|
333
|
-
const options = createMockOptions();
|
|
334
|
-
const scheduling = require('../routes/calendar/scheduling')(options);
|
|
335
|
-
|
|
336
|
-
const ctx = createMockCtx({
|
|
337
|
-
method: 'POST',
|
|
338
|
-
url: '/cal/user@example.com/outbox/',
|
|
339
|
-
request: { body: null }
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
await scheduling.postOutbox(ctx);
|
|
343
|
-
|
|
344
|
-
t.is(ctx.status, 400);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
test('postOutbox handles free-busy query', async (t) => {
|
|
348
|
-
const options = createMockOptions();
|
|
349
|
-
const scheduling = require('../routes/calendar/scheduling')(options);
|
|
350
|
-
|
|
351
|
-
const freeBusyRequest = [
|
|
352
|
-
'BEGIN:VCALENDAR',
|
|
353
|
-
'VERSION:2.0',
|
|
354
|
-
'METHOD:REQUEST',
|
|
355
|
-
'BEGIN:VFREEBUSY',
|
|
356
|
-
'ORGANIZER:mailto:organizer@example.com',
|
|
357
|
-
'ATTENDEE:mailto:attendee@example.com',
|
|
358
|
-
'DTSTART:20260101T000000Z',
|
|
359
|
-
'DTEND:20260102T000000Z',
|
|
360
|
-
'END:VFREEBUSY',
|
|
361
|
-
'END:VCALENDAR'
|
|
362
|
-
].join('\r\n');
|
|
363
|
-
|
|
364
|
-
const ctx = createMockCtx({
|
|
365
|
-
method: 'POST',
|
|
366
|
-
url: '/cal/user@example.com/outbox/',
|
|
367
|
-
request: { body: freeBusyRequest }
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
await scheduling.postOutbox(ctx);
|
|
371
|
-
|
|
372
|
-
t.is(ctx.status, 207);
|
|
373
|
-
t.truthy(ctx.body);
|
|
374
|
-
t.true(ctx.body.includes('schedule-response'));
|
|
375
|
-
t.true(ctx.body.includes('attendee@example.com'));
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
test('postOutbox handles iTIP REQUEST', async (t) => {
|
|
379
|
-
const options = createMockOptions();
|
|
380
|
-
const scheduling = require('../routes/calendar/scheduling')(options);
|
|
381
|
-
|
|
382
|
-
const itipRequest = [
|
|
383
|
-
'BEGIN:VCALENDAR',
|
|
384
|
-
'VERSION:2.0',
|
|
385
|
-
'METHOD:REQUEST',
|
|
386
|
-
'BEGIN:VEVENT',
|
|
387
|
-
'UID:test-event-123',
|
|
388
|
-
'ORGANIZER:mailto:organizer@example.com',
|
|
389
|
-
'ATTENDEE:mailto:attendee1@example.com',
|
|
390
|
-
'ATTENDEE:mailto:attendee2@example.com',
|
|
391
|
-
'SUMMARY:Test Meeting',
|
|
392
|
-
'DTSTART:20260115T100000Z',
|
|
393
|
-
'DTEND:20260115T110000Z',
|
|
394
|
-
'END:VEVENT',
|
|
395
|
-
'END:VCALENDAR'
|
|
396
|
-
].join('\r\n');
|
|
397
|
-
|
|
398
|
-
const ctx = createMockCtx({
|
|
399
|
-
method: 'POST',
|
|
400
|
-
url: '/cal/user@example.com/outbox/',
|
|
401
|
-
request: { body: itipRequest }
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
await scheduling.postOutbox(ctx);
|
|
405
|
-
|
|
406
|
-
t.is(ctx.status, 207);
|
|
407
|
-
t.truthy(ctx.body);
|
|
408
|
-
t.true(ctx.body.includes('schedule-response'));
|
|
409
|
-
t.true(ctx.body.includes('attendee1@example.com'));
|
|
410
|
-
t.true(ctx.body.includes('attendee2@example.com'));
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
test('postOutbox calls sendSchedulingMessage when available', async (t) => {
|
|
414
|
-
const sentMessages = [];
|
|
415
|
-
|
|
416
|
-
const options = createMockOptions({
|
|
417
|
-
data: {
|
|
418
|
-
async sendSchedulingMessage(ctx, msg) {
|
|
419
|
-
sentMessages.push(msg);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
const scheduling = require('../routes/calendar/scheduling')(options);
|
|
424
|
-
|
|
425
|
-
const itipRequest = [
|
|
426
|
-
'BEGIN:VCALENDAR',
|
|
427
|
-
'VERSION:2.0',
|
|
428
|
-
'METHOD:REQUEST',
|
|
429
|
-
'BEGIN:VEVENT',
|
|
430
|
-
'UID:test-event-456',
|
|
431
|
-
'ORGANIZER:mailto:organizer@example.com',
|
|
432
|
-
'ATTENDEE:mailto:attendee@example.com',
|
|
433
|
-
'SUMMARY:Test Meeting',
|
|
434
|
-
'DTSTART:20260115T100000Z',
|
|
435
|
-
'DTEND:20260115T110000Z',
|
|
436
|
-
'END:VEVENT',
|
|
437
|
-
'END:VCALENDAR'
|
|
438
|
-
].join('\r\n');
|
|
439
|
-
|
|
440
|
-
const ctx = createMockCtx({
|
|
441
|
-
method: 'POST',
|
|
442
|
-
url: '/cal/user@example.com/outbox/',
|
|
443
|
-
request: { body: itipRequest }
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
await scheduling.postOutbox(ctx);
|
|
447
|
-
|
|
448
|
-
t.is(sentMessages.length, 1);
|
|
449
|
-
t.is(sentMessages[0].method, 'REQUEST');
|
|
450
|
-
t.is(sentMessages[0].attendee, 'attendee@example.com');
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
test('postOutbox uses custom getFreeBusy when available', async (t) => {
|
|
454
|
-
const customFreeBusy = [
|
|
455
|
-
'BEGIN:VCALENDAR',
|
|
456
|
-
'VERSION:2.0',
|
|
457
|
-
'METHOD:REPLY',
|
|
458
|
-
'BEGIN:VFREEBUSY',
|
|
459
|
-
'FREEBUSY:20260101T090000Z/20260101T100000Z',
|
|
460
|
-
'END:VFREEBUSY',
|
|
461
|
-
'END:VCALENDAR'
|
|
462
|
-
].join('\r\n');
|
|
463
|
-
|
|
464
|
-
const options = createMockOptions({
|
|
465
|
-
data: {
|
|
466
|
-
getFreeBusy: async () => customFreeBusy
|
|
467
|
-
}
|
|
468
|
-
});
|
|
469
|
-
const scheduling = require('../routes/calendar/scheduling')(options);
|
|
470
|
-
|
|
471
|
-
const freeBusyRequest = [
|
|
472
|
-
'BEGIN:VCALENDAR',
|
|
473
|
-
'VERSION:2.0',
|
|
474
|
-
'METHOD:REQUEST',
|
|
475
|
-
'BEGIN:VFREEBUSY',
|
|
476
|
-
'ATTENDEE:mailto:attendee@example.com',
|
|
477
|
-
'END:VFREEBUSY',
|
|
478
|
-
'END:VCALENDAR'
|
|
479
|
-
].join('\r\n');
|
|
480
|
-
|
|
481
|
-
const ctx = createMockCtx({
|
|
482
|
-
method: 'POST',
|
|
483
|
-
url: '/cal/user@example.com/outbox/',
|
|
484
|
-
request: { body: freeBusyRequest }
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
await scheduling.postOutbox(ctx);
|
|
488
|
-
|
|
489
|
-
t.is(ctx.status, 207);
|
|
490
|
-
t.true(ctx.body.includes('FREEBUSY'));
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
// ============================================
|
|
494
|
-
// Tests for calendar.js routing integration
|
|
495
|
-
// ============================================
|
|
496
|
-
|
|
497
|
-
test('calendar router routes inbox requests to scheduling handler', async (t) => {
|
|
498
|
-
// We need to test that the calendar.js properly routes to scheduling
|
|
499
|
-
// This is an integration test
|
|
500
|
-
const options = createMockOptions({
|
|
501
|
-
data: {
|
|
502
|
-
getCalendar: async () => null
|
|
503
|
-
}
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
const calendarRouter = require('../routes/calendar/calendar')(options);
|
|
507
|
-
|
|
508
|
-
const ctx = createMockCtx({
|
|
509
|
-
method: 'PROPFIND',
|
|
510
|
-
url: '/cal/user@example.com/inbox/'
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
await calendarRouter(ctx);
|
|
514
|
-
|
|
515
|
-
// If routing works, we should get a 207 response with schedule-inbox
|
|
516
|
-
t.is(ctx.status, 207);
|
|
517
|
-
t.true(ctx.body.includes('schedule-inbox'));
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
test('calendar router routes outbox requests to scheduling handler', async (t) => {
|
|
521
|
-
const options = createMockOptions({
|
|
522
|
-
data: {
|
|
523
|
-
getCalendar: async () => null
|
|
524
|
-
}
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
const calendarRouter = require('../routes/calendar/calendar')(options);
|
|
528
|
-
|
|
529
|
-
const ctx = createMockCtx({
|
|
530
|
-
method: 'PROPFIND',
|
|
531
|
-
url: '/cal/user@example.com/outbox/'
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
await calendarRouter(ctx);
|
|
535
|
-
|
|
536
|
-
t.is(ctx.status, 207);
|
|
537
|
-
t.true(ctx.body.includes('schedule-outbox'));
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
// ============================================
|
|
541
|
-
// Tests for edge cases and error handling
|
|
542
|
-
// ============================================
|
|
543
|
-
|
|
544
|
-
test('schedule-inbox-URL returns empty href when ctx is missing', async (t) => {
|
|
545
|
-
const options = createMockOptions();
|
|
546
|
-
const { tags } = require('../common/tags')(options);
|
|
547
|
-
|
|
548
|
-
const scheduleInboxTag =
|
|
549
|
-
tags['urn:ietf:params:xml:ns:caldav']['schedule-inbox-URL'];
|
|
550
|
-
const result = await scheduleInboxTag.resp({});
|
|
551
|
-
|
|
552
|
-
t.truthy(result);
|
|
553
|
-
// Should return empty href
|
|
554
|
-
const resultStr = JSON.stringify(result);
|
|
555
|
-
t.true(resultStr.includes('href'));
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
test('schedule-outbox-URL returns empty href when ctx is missing', async (t) => {
|
|
559
|
-
const options = createMockOptions();
|
|
560
|
-
const { tags } = require('../common/tags')(options);
|
|
561
|
-
|
|
562
|
-
const scheduleOutboxTag =
|
|
563
|
-
tags['urn:ietf:params:xml:ns:caldav']['schedule-outbox-URL'];
|
|
564
|
-
const result = await scheduleOutboxTag.resp({});
|
|
565
|
-
|
|
566
|
-
t.truthy(result);
|
|
567
|
-
const resultStr = JSON.stringify(result);
|
|
568
|
-
t.true(resultStr.includes('href'));
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
test('postOutbox handles errors in getFreeBusy gracefully', async (t) => {
|
|
572
|
-
const options = createMockOptions({
|
|
573
|
-
data: {
|
|
574
|
-
async getFreeBusy() {
|
|
575
|
-
throw new Error('Database error');
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
|
-
const scheduling = require('../routes/calendar/scheduling')(options);
|
|
580
|
-
|
|
581
|
-
const freeBusyRequest = [
|
|
582
|
-
'BEGIN:VCALENDAR',
|
|
583
|
-
'VERSION:2.0',
|
|
584
|
-
'METHOD:REQUEST',
|
|
585
|
-
'BEGIN:VFREEBUSY',
|
|
586
|
-
'ATTENDEE:mailto:attendee@example.com',
|
|
587
|
-
'END:VFREEBUSY',
|
|
588
|
-
'END:VCALENDAR'
|
|
589
|
-
].join('\r\n');
|
|
590
|
-
|
|
591
|
-
const ctx = createMockCtx({
|
|
592
|
-
method: 'POST',
|
|
593
|
-
url: '/cal/user@example.com/outbox/',
|
|
594
|
-
request: { body: freeBusyRequest }
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
await scheduling.postOutbox(ctx);
|
|
598
|
-
|
|
599
|
-
t.is(ctx.status, 207);
|
|
600
|
-
// Should return error status 3.7 for invalid calendar user
|
|
601
|
-
t.true(ctx.body.includes('3.7'));
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
test('postOutbox handles errors in sendSchedulingMessage gracefully', async (t) => {
|
|
605
|
-
const options = createMockOptions({
|
|
606
|
-
data: {
|
|
607
|
-
async sendSchedulingMessage() {
|
|
608
|
-
throw new Error('SMTP error');
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
});
|
|
612
|
-
const scheduling = require('../routes/calendar/scheduling')(options);
|
|
613
|
-
|
|
614
|
-
const itipRequest = [
|
|
615
|
-
'BEGIN:VCALENDAR',
|
|
616
|
-
'VERSION:2.0',
|
|
617
|
-
'METHOD:REQUEST',
|
|
618
|
-
'BEGIN:VEVENT',
|
|
619
|
-
'UID:test-event',
|
|
620
|
-
'ATTENDEE:mailto:attendee@example.com',
|
|
621
|
-
'END:VEVENT',
|
|
622
|
-
'END:VCALENDAR'
|
|
623
|
-
].join('\r\n');
|
|
624
|
-
|
|
625
|
-
const ctx = createMockCtx({
|
|
626
|
-
method: 'POST',
|
|
627
|
-
url: '/cal/user@example.com/outbox/',
|
|
628
|
-
request: { body: itipRequest }
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
await scheduling.postOutbox(ctx);
|
|
632
|
-
|
|
633
|
-
t.is(ctx.status, 207);
|
|
634
|
-
// Should return error status 5.1 for delivery failure
|
|
635
|
-
t.true(ctx.body.includes('5.1'));
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
test('getInbox handles errors in getSchedulingMessages gracefully', async (t) => {
|
|
639
|
-
const options = createMockOptions({
|
|
640
|
-
data: {
|
|
641
|
-
async getSchedulingMessages() {
|
|
642
|
-
throw new Error('Database error');
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
});
|
|
646
|
-
const scheduling = require('../routes/calendar/scheduling')(options);
|
|
647
|
-
|
|
648
|
-
const ctx = createMockCtx({
|
|
649
|
-
method: 'GET',
|
|
650
|
-
url: '/cal/user@example.com/inbox/'
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
await scheduling.getInbox(ctx);
|
|
654
|
-
|
|
655
|
-
// Should still return 207 with empty collection
|
|
656
|
-
t.is(ctx.status, 207);
|
|
657
|
-
t.true(ctx.body.includes('schedule-inbox'));
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
test('scheduling route returns 405 for unsupported methods', async (t) => {
|
|
661
|
-
const options = createMockOptions();
|
|
662
|
-
const scheduling = require('../routes/calendar/scheduling')(options);
|
|
663
|
-
|
|
664
|
-
const ctx = createMockCtx({
|
|
665
|
-
method: 'PUT',
|
|
666
|
-
url: '/cal/user@example.com/outbox/'
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
await scheduling.route(ctx);
|
|
670
|
-
|
|
671
|
-
t.is(ctx.status, 405);
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
test('postOutbox returns 400 when no attendees in free-busy query', async (t) => {
|
|
675
|
-
const options = createMockOptions();
|
|
676
|
-
const scheduling = require('../routes/calendar/scheduling')(options);
|
|
677
|
-
|
|
678
|
-
const freeBusyRequest = [
|
|
679
|
-
'BEGIN:VCALENDAR',
|
|
680
|
-
'VERSION:2.0',
|
|
681
|
-
'METHOD:REQUEST',
|
|
682
|
-
'BEGIN:VFREEBUSY',
|
|
683
|
-
'ORGANIZER:mailto:organizer@example.com',
|
|
684
|
-
'END:VFREEBUSY',
|
|
685
|
-
'END:VCALENDAR'
|
|
686
|
-
].join('\r\n');
|
|
687
|
-
|
|
688
|
-
const ctx = createMockCtx({
|
|
689
|
-
method: 'POST',
|
|
690
|
-
url: '/cal/user@example.com/outbox/',
|
|
691
|
-
request: { body: freeBusyRequest }
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
await scheduling.postOutbox(ctx);
|
|
695
|
-
|
|
696
|
-
t.is(ctx.status, 400);
|
|
697
|
-
});
|