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  }