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.
@@ -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
- });