ics - decoder_test.go
1 package ics
2
3 import (
4 "bytes"
5 "fmt"
6 "io"
7 "net/url"
8 "reflect"
9 "strings"
10 "testing"
11 "time"
12 )
13
14 func (c Calendar) Error() string {
15 var buf bytes.Buffer
16 buf.WriteString("Calendar: ")
17 indentWrite(reflect.ValueOf(&c), 1, &buf)
18 return buf.String()
19 }
20
21 func indentWrite(v reflect.Value, indentLevel int, w writer) {
22 w.WriteString(v.Type().String())
23 w.WriteString(" ")
24 if v.Type().Kind() == reflect.Ptr {
25 if v.IsNil() {
26 w.WriteString("<nil>\n")
27 return
28 }
29 v = v.Elem()
30 }
31 if s, ok := v.Interface().(interface {
32 String() string
33 }); ok {
34 w.WriteString(s.String())
35 } else {
36 switch v.Type().Kind() {
37 case reflect.Struct:
38 w.WriteString("{\n")
39 for i := 0; i < v.Type().NumField(); i++ {
40 indent(w, indentLevel)
41 w.WriteString(v.Type().Field(i).Name)
42 w.WriteString(": ")
43 indentWrite(v.Field(i), indentLevel+1, w)
44 }
45 indent(w, indentLevel-1)
46 w.WriteString("}")
47 case reflect.String:
48 fmt.Fprintf(w, "%q", v.String())
49 case reflect.Slice:
50 w.WriteString("{")
51 if v.Len() > 0 {
52 w.WriteString("\n")
53 for i := 0; i < v.Len(); i++ {
54 indent(w, indentLevel)
55 fmt.Fprintf(w, "%d", i)
56 w.WriteString(": ")
57 indentWrite(v.Index(i), indentLevel+1, w)
58 }
59 indent(w, indentLevel-1)
60 }
61 w.WriteString("}")
62 case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
63 w.WriteString(v.Kind().String())
64 w.WriteString(" <")
65 fmt.Fprintf(w, "%d", v.Int())
66 w.WriteString(">")
67 case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
68 w.WriteString(v.Kind().String())
69 w.WriteString(" <")
70 fmt.Fprintf(w, "%d", v.Uint())
71 w.WriteString(">")
72 case reflect.Bool:
73 if v.Bool() {
74 w.WriteString("True")
75 } else {
76 w.WriteString("False")
77 }
78 default:
79 w.WriteString(v.Kind().String())
80 }
81 }
82 w.WriteString("\n")
83 }
84
85 func indent(w io.Writer, indentLevel int) {
86 in := [1]byte{' '}
87 for i := 0; i < indentLevel; i++ {
88 w.Write(in[:])
89 }
90 }
91
92 func TestDecode(t *testing.T) {
93 tzny, err := time.LoadLocation("America/New_York")
94 if err != nil {
95 t.Errorf("unexpected error: %s", err)
96 return
97 }
98 tests := []struct {
99 Input string
100 Output *Calendar
101 Error error
102 }{
103 {
104 Error: io.ErrUnexpectedEOF,
105 },
106 {
107 Input: "BEGIN:VCALENDAR\r\nPRODID:TestDecode\r\nVERSION:2.0\r\nEND:VCALENDAR\r\n",
108 Output: &Calendar{
109 ProductID: "TestDecode",
110 Version: "2.0",
111 },
112 },
113 {
114 Input: "BEGIN:VCALENDAR\r\n" +
115 "PRODID:-//xyz Corp//NONSGML PDA Calendar Version 1.0//EN\r\n" +
116 "VERSION:2.0\r\n" +
117 "BEGIN:VEVENT\r\n" +
118 "DTSTAMP:19960704T120000Z\r\n" +
119 "UID:uid1@example.com\r\n" +
120 "ORGANIZER:mailto:jsmith@example.com\r\n" +
121 "DTSTART:19960918T143000Z\r\n" +
122 "DTEND:19960920T220000Z\r\n" +
123 "STATUS:CONFIRMED\r\n" +
124 "CATEGORIES:CONFERENCE\r\n" +
125 "SUMMARY:Networld+Interop Conference\r\n" +
126 "DESCRIPTION:Networld+Interop Conference\r\n and Exhibit\\nAtlanta World Congress Center\\n\r\n Atlanta\\, Georgia\r\n" +
127 "END:VEVENT\r\n" +
128 "END:VCALENDAR\r\n",
129 Output: &Calendar{
130 ProductID: "-//xyz Corp//NONSGML PDA Calendar Version 1.0//EN",
131 Version: "2.0",
132 Event: []Event{
133 {
134 DateTimeStamp: PropDateTimeStamp{
135 Time: time.Date(1996, 7, 4, 12, 0, 0, 0, time.UTC),
136 },
137 UID: "uid1@example.com",
138 Organizer: &PropOrganizer{
139 CalendarAddress: CalendarAddress{
140 URL: url.URL{
141 Scheme: "mailto",
142 Opaque: "jsmith@example.com",
143 },
144 },
145 },
146 DateTimeStart: &PropDateTimeStart{
147 DateTime: &DateTime{
148 Time: time.Date(1996, 9, 18, 14, 30, 0, 0, time.UTC),
149 },
150 },
151 DateTimeEnd: &PropDateTimeEnd{
152 DateTime: &DateTime{
153 Time: time.Date(1996, 9, 20, 22, 0, 0, 0, time.UTC),
154 },
155 },
156 Status: StatusConfirmed.New(),
157 Categories: []PropCategories{
158 {
159 MText: MText{"CONFERENCE"},
160 },
161 },
162 Summary: &PropSummary{
163 Text: "Networld+Interop Conference",
164 },
165 Description: &PropDescription{
166 Text: "Networld+Interop Conference and Exhibit\nAtlanta World Congress Center\nAtlanta, Georgia",
167 },
168 },
169 },
170 },
171 },
172 {
173 Input: "BEGIN:VCALENDAR\r\n" +
174 "PRODID:-//RDU Software//NONSGML HandCal//EN\r\n" +
175 "VERSION:2.0\r\n" +
176 "BEGIN:VTIMEZONE\r\n" +
177 "TZID:America/New_York\r\n" +
178 "BEGIN:STANDARD\r\n" +
179 "DTSTART:19981025T020000\r\n" +
180 "TZOFFSETFROM:-0400\r\n" +
181 "TZOFFSETTO:-0500\r\n" +
182 "TZNAME:EST\r\n" +
183 "END:STANDARD\r\n" +
184 "BEGIN:DAYLIGHT\r\n" +
185 "DTSTART:19990404T020000\r\n" +
186 "TZOFFSETFROM:-0500\r\n" +
187 "TZOFFSETTO:-0400\r\n" +
188 "TZNAME:EDT\r\n" +
189 "END:DAYLIGHT\r\n" +
190 "END:VTIMEZONE\r\n" +
191 "BEGIN:VEVENT\r\n" +
192 "DTSTAMP:19980309T231000Z\r\n" +
193 "UID:guid-1.example.com\r\n" +
194 "ORGANIZER:mailto:mrbig@example.com\r\n" +
195 "ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT;CUTYPE=GROUP:\r\n" +
196 " mailto:employee-A@example.com\r\n" +
197 "DESCRIPTION:Project XYZ Review Meeting\r\n" +
198 "CATEGORIES:MEETING\r\n" +
199 "CLASS:PUBLIC\r\n" +
200 "CREATED:19980309T130000Z\r\n" +
201 "SUMMARY:XYZ Project Review\r\n" +
202 "DTSTART;TZID=America/New_York:19980312T083000\r\n" +
203 "DTEND;TZID=America/New_York:19980312T093000\r\n" +
204 "LOCATION:1CP Conference Room 4350\r\n" +
205 "END:VEVENT\r\n" +
206 "END:VCALENDAR\r\n",
207 Output: &Calendar{
208 ProductID: "-//RDU Software//NONSGML HandCal//EN",
209 Version: "2.0",
210 Event: []Event{
211 {
212 DateTimeStamp: PropDateTimeStamp{
213 Time: time.Date(1998, 3, 9, 23, 10, 0, 0, time.UTC),
214 },
215 UID: "guid-1.example.com",
216 Organizer: &PropOrganizer{
217 CalendarAddress: CalendarAddress{
218 URL: url.URL{
219 Scheme: "mailto",
220 Opaque: "mrbig@example.com",
221 },
222 },
223 },
224 Attendee: []PropAttendee{
225 {
226 CalendarUserType: CalendarUserTypeGroup.New(),
227 RSVP: NewRSVP(true),
228 ParticipationRole: ParticipationRoleRequiredParticipant.New(),
229 CalendarAddress: CalendarAddress{
230 URL: url.URL{
231 Scheme: "mailto",
232 Opaque: "employee-A@example.com",
233 },
234 },
235 },
236 },
237 Description: &PropDescription{
238 Text: "Project XYZ Review Meeting",
239 },
240 Categories: []PropCategories{
241 {
242 MText: MText{"MEETING"},
243 },
244 },
245 Class: ClassPublic.New(),
246 Created: &PropCreated{
247 Time: time.Date(1998, 3, 9, 13, 0, 0, 0, time.UTC),
248 },
249 DateTimeStart: &PropDateTimeStart{
250 DateTime: &DateTime{
251 Time: time.Date(1998, 3, 12, 8, 30, 0, 0, tzny),
252 },
253 },
254 DateTimeEnd: &PropDateTimeEnd{
255 DateTime: &DateTime{
256 Time: time.Date(1998, 3, 12, 9, 30, 0, 0, tzny),
257 },
258 },
259 Location: &PropLocation{
260 Text: "1CP Conference Room 4350",
261 },
262 Summary: &PropSummary{
263 Text: "XYZ Project Review",
264 },
265 },
266 },
267 Timezone: []Timezone{
268 {
269 TimezoneID: "America/New_York",
270 Standard: []Standard{
271 {
272 DateTimeStart: PropDateTimeStart{
273 DateTime: &DateTime{
274 Time: time.Date(1998, 10, 25, 2, 0, 0, 0, time.Local),
275 },
276 },
277 TimezoneOffsetFrom: -4 * 3600,
278 TimezoneOffsetTo: -5 * 3600,
279 TimezoneName: []PropTimezoneName{
280 {
281 Text: "EST",
282 },
283 },
284 },
285 },
286 Daylight: []Daylight{
287 {
288 DateTimeStart: PropDateTimeStart{
289 DateTime: &DateTime{
290 Time: time.Date(1999, 4, 4, 2, 0, 0, 0, time.Local),
291 },
292 },
293 TimezoneOffsetFrom: -5 * 3600,
294 TimezoneOffsetTo: -4 * 3600,
295 TimezoneName: []PropTimezoneName{
296 {
297 Text: "EDT",
298 },
299 },
300 },
301 },
302 },
303 },
304 },
305 },
306 {
307 Input: "BEGIN:VCALENDAR\r\n" +
308 "VERSION:2.0\r\n" +
309 "PRODID:-//ABC Corporation//NONSGML My Product//EN\r\n" +
310 "BEGIN:VTODO\r\n" +
311 "DTSTAMP:19980130T134500Z\r\n" +
312 "SEQUENCE:2\r\n" +
313 "UID:uid4@example.com\r\n" +
314 "ORGANIZER:mailto:unclesam@example.com\r\n" +
315 "ATTENDEE;PARTSTAT=ACCEPTED:mailto:jqpublic@example.com\r\n" +
316 "DUE:19980415T000000\r\n" +
317 "STATUS:NEEDS-ACTION\r\n" +
318 "SUMMARY:Submit Income Taxes\r\n" +
319 "BEGIN:VALARM\r\n" +
320 "ACTION:AUDIO\r\n" +
321 "TRIGGER;VALUE=DATE-TIME:19980403T120000Z\r\n" +
322 "ATTACH;FMTTYPE=audio/basic:http://example.com/pub/audio-\r\n" +
323 " files/ssbanner.aud\r\n" +
324 "REPEAT:4\r\n" +
325 "DURATION:PT1H\r\n" +
326 "END:VALARM\r\n" +
327 "END:VTODO\r\n" +
328 "END:VCALENDAR\r\n",
329 Output: &Calendar{
330 Version: "2.0",
331 ProductID: "-//ABC Corporation//NONSGML My Product//EN",
332 Todo: []Todo{
333 {
334 DateTimeStamp: PropDateTimeStamp{
335 Time: time.Date(1998, 1, 30, 13, 45, 0, 0, time.UTC),
336 },
337 Sequence: NewSequence(2),
338 UID: "uid4@example.com",
339 Organizer: &PropOrganizer{
340 CalendarAddress: CalendarAddress{
341 URL: url.URL{
342 Scheme: "mailto",
343 Opaque: "unclesam@example.com",
344 },
345 },
346 },
347 Attendee: []PropAttendee{
348 {
349 ParticipationStatus: ParticipationStatusAccepted.New(),
350 CalendarAddress: CalendarAddress{
351 URL: url.URL{
352 Scheme: "mailto",
353 Opaque: "jqpublic@example.com",
354 },
355 },
356 },
357 },
358 Due: &PropDue{
359 DateTime: &DateTime{
360 Time: time.Date(1998, 4, 15, 0, 0, 0, 0, time.Local),
361 },
362 },
363 Status: StatusNeedsAction.New(),
364 Summary: &PropSummary{
365 Text: "Submit Income Taxes",
366 },
367 Alarm: []Alarm{
368 {
369 AlarmType: &AlarmAudio{
370 Trigger: PropTrigger{
371 DateTime: &DateTime{
372 Time: time.Date(1998, 4, 3, 12, 0, 0, 0, time.UTC),
373 },
374 },
375 Attachment: []PropAttachment{
376 {
377 FormatType: NewFormatType("audio/basic"),
378 URI: &URI{
379 URL: url.URL{
380 Scheme: "http",
381 Host: "example.com",
382 Path: "/pub/audio-files/ssbanner.aud",
383 },
384 },
385 },
386 },
387 Repeat: NewRepeat(4),
388 Duration: &PropDuration{
389 Hours: 1,
390 },
391 },
392 },
393 },
394 },
395 },
396 },
397 },
398 {
399 Input: "BEGIN:VCALENDAR\r\n" +
400 "VERSION:2.0\r\n" +
401 "PRODID:-//ABC Corporation//NONSGML My Product//EN\r\n" +
402 "BEGIN:VJOURNAL\r\n" +
403 "DTSTAMP:19970324T120000Z\r\n" +
404 "UID:uid5@example.com\r\n" +
405 "ORGANIZER:mailto:jsmith@example.com\r\n" +
406 "STATUS:DRAFT\r\n" +
407 "CLASS:PUBLIC\r\n" +
408 "CATEGORIES:Project Report,XYZ,Weekly Meeting\r\n" +
409 "DESCRIPTION:Project xyz Review Meeting Minutes\\n\r\n" +
410 " Agenda\\n1. Review of project version 1.0 requirements.\\n2.\r\n" +
411 " Definition of project processes.\\n3. Review of project schedule.\\n\r\n" +
412 " Participants: John Smith\\, Jane Doe\\, Jim Dandy\\n-It was\r\n" +
413 " decided that the requirements need to be signed off by\r\n" +
414 " product marketing.\\n-Project processes were accepted.\\n\r\n" +
415 " -Project schedule needs to account for scheduled holidays\r\n" +
416 " and employee vacation time. Check with HR for specific\r\n" +
417 " dates.\\n-New schedule will be distributed by Friday.\\n-\r\n" +
418 " Next weeks meeting is cancelled. No meeting until 3/23.\r\n" +
419 "END:VJOURNAL\r\n" +
420 "END:VCALENDAR\r\n",
421 Output: &Calendar{
422 Version: "2.0",
423 ProductID: "-//ABC Corporation//NONSGML My Product//EN",
424 Journal: []Journal{
425 {
426 DateTimeStamp: PropDateTimeStamp{
427 Time: time.Date(1997, 3, 24, 12, 0, 0, 0, time.UTC),
428 },
429 UID: "uid5@example.com",
430 Organizer: &PropOrganizer{
431 CalendarAddress: CalendarAddress{
432 URL: url.URL{
433 Scheme: "mailto",
434 Opaque: "jsmith@example.com",
435 },
436 },
437 },
438 Status: StatusDraft.New(),
439 Class: ClassPublic.New(),
440 Categories: []PropCategories{
441 {
442 MText: MText{
443 "Project Report",
444 "XYZ",
445 "Weekly Meeting",
446 },
447 },
448 },
449 Description: []PropDescription{
450 {
451 Text: "Project xyz Review Meeting Minutes\n" +
452 "Agenda\n" +
453 "1. Review of project version 1.0 requirements.\n" +
454 "2. Definition of project processes.\n" +
455 "3. Review of project schedule.\n" +
456 "Participants: John Smith, Jane Doe, Jim Dandy\n" +
457 "-It was decided that the requirements need to be signed off by product marketing.\n" +
458 "-Project processes were accepted.\n" +
459 "-Project schedule needs to account for scheduled holidays and employee vacation time. Check with HR for specific dates.\n" +
460 "-New schedule will be distributed by Friday.\n" +
461 "-Next weeks meeting is cancelled. No meeting until 3/23.",
462 },
463 },
464 },
465 },
466 },
467 },
468 {
469 Input: "BEGIN:VCALENDAR\r\n" +
470 "VERSION:2.0\r\n" +
471 "PRODID:-//RDU Software//NONSGML HandCal//EN\r\n" +
472 "BEGIN:VFREEBUSY\r\n" +
473 "DTSTAMP:19970324T120000Z\r\n" +
474 "UID:uid5@example.com\r\n" +
475 "ORGANIZER:mailto:jsmith@example.com\r\n" +
476 "DTSTART:19980313T141711Z\r\n" +
477 "DTEND:19980410T141711Z\r\n" +
478 "FREEBUSY:19980314T233000Z/19980315T003000Z\r\n" +
479 "FREEBUSY:19980316T153000Z/19980316T163000Z\r\n" +
480 "FREEBUSY:19980318T030000Z/19980318T040000Z\r\n" +
481 "URL:http://www.example.com/calendar/busytime/jsmith.ifb\r\n" +
482 "END:VFREEBUSY\r\n" +
483 "END:VCALENDAR\r\n",
484 Output: &Calendar{
485 Version: "2.0",
486 ProductID: "-//RDU Software//NONSGML HandCal//EN",
487 FreeBusy: []FreeBusy{
488 {
489 DateTimeStamp: PropDateTimeStamp{
490 Time: time.Date(1997, 3, 24, 12, 0, 0, 0, time.UTC),
491 },
492 UID: "uid5@example.com",
493 Organizer: &PropOrganizer{
494 CalendarAddress: CalendarAddress{
495 URL: url.URL{
496 Scheme: "mailto",
497 Opaque: "jsmith@example.com",
498 },
499 },
500 },
501 DateTimeStart: &PropDateTimeStart{
502 DateTime: &DateTime{
503 Time: time.Date(1998, 3, 13, 14, 17, 11, 0, time.UTC),
504 },
505 },
506 DateTimeEnd: &PropDateTimeEnd{
507 DateTime: &DateTime{
508 Time: time.Date(1998, 4, 10, 14, 17, 11, 0, time.UTC),
509 },
510 },
511 FreeBusy: []PropFreeBusy{
512 {
513 Period: Period{
514 Start: DateTime{
515 Time: time.Date(1998, 3, 14, 23, 30, 0, 0, time.UTC),
516 },
517 End: DateTime{
518 Time: time.Date(1998, 3, 15, 0, 30, 0, 0, time.UTC),
519 },
520 },
521 },
522 {
523 Period: Period{
524 Start: DateTime{
525 Time: time.Date(1998, 3, 16, 15, 30, 0, 0, time.UTC),
526 },
527 End: DateTime{
528 Time: time.Date(1998, 3, 16, 16, 30, 0, 0, time.UTC),
529 },
530 },
531 },
532 {
533 Period: Period{
534 Start: DateTime{
535 Time: time.Date(1998, 3, 18, 3, 0, 0, 0, time.UTC),
536 },
537 End: DateTime{
538 Time: time.Date(1998, 3, 18, 4, 0, 0, 0, time.UTC),
539 },
540 },
541 },
542 },
543 URL: &PropURL{
544 URL: url.URL{
545 Scheme: "http",
546 Host: "www.example.com",
547 Path: "/calendar/busytime/jsmith.ifb",
548 },
549 },
550 },
551 },
552 },
553 },
554 }
555
556 for n, test := range tests {
557 c, err := Decode(strings.NewReader(test.Input))
558 if err != test.Error {
559 t.Errorf("test %d: expecting error %q, got %q", n+1, test.Error, err)
560 } else if !reflect.DeepEqual(c, test.Output) {
561 t.Errorf("test %d: expecting calendar %s, got %s", n+1, test.Output, c)
562 }
563 }
564 }