From 2300727b0950d9a79b9329d67d5a21228e6a706f Mon Sep 17 00:00:00 2001 From: Abhishek Pandey Date: Mon, 10 Apr 2023 13:17:53 -0700 Subject: [PATCH] Sanitize recurrenceTimezone prior to restoring calendar event (#3064) * Do not specify recurrenceTimeZone if it's not set in the item being restored. "" is not a valid value --- #### Does this PR need a docs update or release note? - [x] :no_entry: No #### Type of change - [x] :bug: Bugfix #### Issue(s) * COR-53 #### Test Plan - [x] :green_heart: E2E --- src/cmd/factory/impl/exchange.go | 2 +- .../connector/exchange/restore_test.go | 50 ++++++-- .../mockconnector/mock_data_event.go | 120 ++++++++++++------ .../connector/support/m365Transform.go | 8 ++ .../connector/support/m365Transform_test.go | 76 ++++++++++- .../operations/backup_integration_test.go | 2 +- 6 files changed, 206 insertions(+), 52 deletions(-) diff --git a/src/cmd/factory/impl/exchange.go b/src/cmd/factory/impl/exchange.go index c412ec253..d88f60eaa 100644 --- a/src/cmd/factory/impl/exchange.go +++ b/src/cmd/factory/impl/exchange.go @@ -115,7 +115,7 @@ func handleExchangeCalendarEventFactory(cmd *cobra.Command, args []string) error func(id, now, subject, body string) []byte { return mockconnector.GetMockEventWith( User, subject, body, body, - now, now, false) + now, now, mockconnector.NoRecurrence, mockconnector.NoAttendees, false) }, control.Options{}, errs) diff --git a/src/internal/connector/exchange/restore_test.go b/src/internal/connector/exchange/restore_test.go index 619720e75..ac28bf3f2 100644 --- a/src/internal/connector/exchange/restore_test.go +++ b/src/internal/connector/exchange/restore_test.go @@ -100,12 +100,12 @@ func (suite *ExchangeRestoreSuite) TestRestoreEvent() { defer flush() var ( - t = suite.T() - userID = tester.M365UserID(t) - name = "TestRestoreEvent: " + common.FormatSimpleDateTime(time.Now()) + t = suite.T() + userID = tester.M365UserID(t) + subject = "TestRestoreEvent: " + common.FormatSimpleDateTime(time.Now()) ) - calendar, err := suite.ac.Events().CreateCalendar(ctx, userID, name) + calendar, err := suite.ac.Events().CreateCalendar(ctx, userID, subject) require.NoError(t, err, clues.ToCore(err)) calendarID := ptr.Val(calendar.GetId()) @@ -116,15 +116,39 @@ func (suite *ExchangeRestoreSuite) TestRestoreEvent() { assert.NoError(t, err, clues.ToCore(err)) }() - info, err := RestoreExchangeEvent(ctx, - mockconnector.GetMockEventWithAttendeesBytes(name), - suite.gs, - control.Copy, - calendarID, - userID, - fault.New(true)) - assert.NoError(t, err, clues.ToCore(err)) - assert.NotNil(t, info, "event item info") + tests := []struct { + name string + bytes []byte + }{ + { + name: "Test Event With Attendees", + bytes: mockconnector.GetMockEventWithAttendeesBytes(subject), + }, + { + name: "Test recurrenceTimeZone: Empty", + bytes: mockconnector.GetMockEventWithRecurrenceBytes(subject, `""`), + }, + } + + for _, test := range tests { + suite.Run(test.name, func() { + t := suite.T() + + ctx, flush := tester.NewContext() + defer flush() + + info, err := RestoreExchangeEvent( + ctx, + test.bytes, + suite.gs, + control.Copy, + calendarID, + userID, + fault.New(true)) + assert.NoError(t, err, clues.ToCore(err)) + assert.NotNil(t, info, "event item info") + }) + } } type containerDeleter interface { diff --git a/src/internal/connector/mockconnector/mock_data_event.go b/src/internal/connector/mockconnector/mock_data_event.go index 237355cb5..e6ab44954 100644 --- a/src/internal/connector/mockconnector/mock_data_event.go +++ b/src/internal/connector/mockconnector/mock_data_event.go @@ -19,7 +19,9 @@ import ( // 6. subject // 7. hasAttachments // 8. attachments -// +// 9. recurrence +// 10. attendees + //nolint:lll const ( eventTmpl = `{ @@ -33,7 +35,6 @@ const ( "createdDateTime":"2022-03-28T03:42:03Z", "lastModifiedDateTime":"2022-05-26T19:25:58Z", "allowNewTimeProposals":true, - "attendees":[], "body":{ "content":"` + `

%s

", @@ -92,7 +93,9 @@ const ( "type":"singleInstance", "hasAttachments":%v, %s - "webLink":"https://outlook.office365.com/owa/?itemid=AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwBGAAAAAADCNgjhM9QmQYWNcI7hCpPrBwDSEBNbUIB9RL6ePDeF3FIYAAAAAAENAADSEBNbUIB9RL6ePDeF3FIYAAAAAG76AAA%%3D&exvsurl=1&path=/calendar/item" + "webLink":"https://outlook.office365.com/owa/?itemid=AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwBGAAAAAADCNgjhM9QmQYWNcI7hCpPrBwDSEBNbUIB9RL6ePDeF3FIYAAAAAAENAADSEBNbUIB9RL6ePDeF3FIYAAAAAG76AAA%%3D&exvsurl=1&path=/calendar/item", + "recurrence":%s, + "attendees":%s }` defaultEventBody = "This meeting is to review the latest Tailspin Toys project proposal.
\\r\\nBut why not eat some sushi while we’re at it? :)" @@ -147,6 +150,48 @@ const ( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}]," + + NoRecurrence = `null` + recurrenceTmpl = `{ + "pattern": { + "type": "absoluteYearly", + "interval": 1, + "month": 1, + "dayOfMonth": 1, + "firstDayOfWeek": "sunday", + "index": "first" + }, + "range": { + "type": "noEnd", + "startDate": "%s", + "endDate": "0001-01-01", + "numberOfOccurrences": 0, + "recurrenceTimeZone": %s + } + }` + + NoAttendees = `[]` + attendeesTmpl = `[{ + "emailAddress": { + "address": "george.martinez@8qzvrj.onmicrosoft.com", + "name": "George Martinez" + }, + "type": "required", + "status": { + "response": "none", + "time": "0001-01-01T00:00:00Z" + } + }, { + "emailAddress": { + "address": "LeeG@8qzvrj.onmicrosoft.com", + "name": "Lee Gu" + }, + "type": "required", + "status": { + "response": "none", + "time": "0001-01-01T00:00:00Z" + } + }]` ) // generatePhoneNumber creates a random phone number @@ -182,7 +227,7 @@ func GetMockEventWithSubjectBytes(subject string) []byte { return GetMockEventWith( defaultEventOrganizer, subject, defaultEventBody, defaultEventBodyPreview, - atTime, endTime, false, + atTime, endTime, NoRecurrence, NoAttendees, false, ) } @@ -194,19 +239,49 @@ func GetMockEventWithAttachment(subject string) []byte { return GetMockEventWith( defaultEventOrganizer, subject, defaultEventBody, defaultEventBodyPreview, - atTime, atTime, true, + atTime, atTime, NoRecurrence, NoAttendees, true, + ) +} + +func GetMockEventWithRecurrenceBytes(subject, recurrenceTimeZone string) []byte { + tomorrow := time.Now().UTC().AddDate(0, 0, 1) + at := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), tomorrow.Hour(), 0, 0, 0, time.UTC) + atTime := common.FormatTime(at) + timeSlice := strings.Split(atTime, "T") + + recurrence := string(fmt.Sprintf( + recurrenceTmpl, + timeSlice[0], + recurrenceTimeZone, + )) + + return GetMockEventWith( + defaultEventOrganizer, subject, + defaultEventBody, defaultEventBodyPreview, + atTime, atTime, recurrence, attendeesTmpl, true, + ) +} + +func GetMockEventWithAttendeesBytes(subject string) []byte { + tomorrow := time.Now().UTC().AddDate(0, 0, 1) + at := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), tomorrow.Hour(), 0, 0, 0, time.UTC) + atTime := common.FormatTime(at) + + return GetMockEventWith( + defaultEventOrganizer, subject, + defaultEventBody, defaultEventBodyPreview, + atTime, atTime, NoRecurrence, attendeesTmpl, true, ) } // GetMockEventWith returns bytes for an Eventable item. -// The event has no attendees. // start and end times should be in the format 2006-01-02T15:04:05.0000000Z. // The timezone (Z) will be automatically stripped. A non-utc timezone may // produce unexpected results. // Body must contain a well-formatted string, consumable in a json payload. IE: no unescaped newlines. func GetMockEventWith( organizer, subject, body, bodyPreview, - startDateTime, endDateTime string, + startDateTime, endDateTime, recurrence, attendees string, hasAttachments bool, ) []byte { var attachments string @@ -235,34 +310,7 @@ func GetMockEventWith( subject, hasAttachments, attachments, + recurrence, + attendees, )) } - -func GetMockEventWithAttendeesBytes(subject string) []byte { - newTime := time.Now().AddDate(0, 0, 1) - conversion := common.FormatTime(newTime) - timeSlice := strings.Split(conversion, "T") - - //nolint:lll - event := "{\"id\":\"AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwBGAAAAAADCNgjhM9QmQYWNcI7hCpPrBwDSEBNbUIB9RL6ePDeF3FIYAAAAAAENAADSEBNbUIB9RL6ePDeF3FIYAABU_FdvAAA=\",\"@odata.etag\":\"W/\\\"0hATW1CAfUS+njw3hdxSGAAAVK7j9A==\\\"\"," + - "\"calendar@odata.associationLink\":\"https://graph.microsoft.com/v1.0/users('a4a472f8-ccb0-43ec-bf52-3697a91b926c')/calendars('AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwAuAAAAAADCNgjhM9QmQYWNcI7hCpPrAQDSEBNbUIB9RL6ePDeF3FIYAAAAAAENAAA=')/$ref\"," + - "\"calendar@odata.navigationLink\":\"https://graph.microsoft.com/v1.0/users('a4a472f8-ccb0-43ec-bf52-3697a91b926c')/calendars('AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwAuAAAAAADCNgjhM9QmQYWNcI7hCpPrAQDSEBNbUIB9RL6ePDeF3FIYAAAAAAENAAA=')\"," + - "\"@odata.context\":\"https://graph.microsoft.com/v1.0/$metadata#users('a4a472f8-ccb0-43ec-bf52-3697a91b926c')/events/$entity\",\"categories\":[],\"changeKey\":\"0hATW1CAfUS+njw3hdxSGAAAVK7j9A==\",\"createdDateTime\":\"2022-08-06T12:47:56Z\",\"lastModifiedDateTime\":\"2022-08-06T12:49:59Z\",\"allowNewTimeProposals\":true," + - "\"attendees\":[{\"emailAddress\":{\"address\":\"george.martinez@8qzvrj.onmicrosoft.com\",\"name\":\"George Martinez\"},\"type\":\"required\",\"status\":{\"response\":\"none\",\"time\":\"0001-01-01T00:00:00Z\"}},{\"emailAddress\":{\"address\":\"LeeG@8qzvrj.onmicrosoft.com\",\"name\":\"Lee Gu\"},\"type\":\"required\",\"status\":{\"response\":\"none\",\"time\":\"0001-01-01T00:00:00Z\"}}]," + - "\"body\":{\"content\":\"\\n\\n\\n\\n\\n
\\nDiscuss matters concerning stock options and early release of quarterly earnings.
\\n
" + - "\\n
________________________________________________________________________________\\n
\\n
" + - "\\n
Microsoft Teams meeting\\n
\\n
\\n
Join on your computer or mobile app" + - "\\n
\\nClick\\n here to join the meeting
" + - "\\n
\\n
Meeting ID:\\n292 784 521 247
\\nPasscode: SzBkfK\\n" + - "\\n
Download\\n Teams | " + - "\\nJoin on the web
\\n
\\n
\\n
Learn more" + - "\\n | " + - "\\nMeeting options
\\n
\\n
\\n
\\n
\\n
\\n
________________________________________________________________________________" + - "\\n
\\n\\n\\n\",\"contentType\":\"html\"},\"bodyPreview\":\"Discuss matters concerning stock options and early release of quarterly earnings.\\n\\n\", " + - "\"end\":{\"dateTime\":\"" + timeSlice[0] + "T16:00:00.0000000\",\"timeZone\":\"UTC\"},\"hasAttachments\":false,\"hideAttendees\":false,\"iCalUId\":\"040000008200E00074C5B7101A82E0080000000010A45EC092A9D801000000000000000010000000999C7C6281C2B24A91D5502392B8EF38\",\"importance\":\"normal\",\"isAllDay\":false,\"isCancelled\":false,\"isDraft\":false,\"isOnlineMeeting\":true,\"isOrganizer\":true,\"isReminderOn\":true," + - "\"location\":{\"address\":{},\"coordinates\":{},\"displayName\":\"\",\"locationType\":\"default\",\"uniqueIdType\":\"unknown\"},\"locations\":[],\"onlineMeeting\":{\"joinUrl\":\"https://teams.microsoft.com/l/meetup-join/19%3ameeting_YWNhMzAxZjItMzE2My00ZGQzLTkzMDUtNjQ3NTY0NjNjMTZi%40thread.v2/0?context=%7b%22Tid%22%3a%224d603060-18d6-4764-b9be-4cb794d32b69%22%2c%22Oid%22%3a%22a4a472f8-ccb0-43ec-bf52-3697a91b926c%22%7d\"},\"onlineMeetingProvider\":\"teamsForBusiness\"," + - "\"organizer\":{\"emailAddress\":{\"address\":\"LidiaH@8qzvrj.onmicrosoft.com\",\"name\":\"Lidia Holloway\"}},\"originalEndTimeZone\":\"Eastern Standard Time\",\"originalStartTimeZone\":\"Eastern Standard Time\",\"reminderMinutesBeforeStart\":15,\"responseRequested\":true,\"responseStatus\":{\"response\":\"organizer\",\"time\":\"0001-01-01T00:00:00Z\"},\"sensitivity\":\"normal\",\"showAs\":\"busy\"," + - "\"start\":{\"dateTime\":\"" + timeSlice[0] + "T15:30:00.0000000\",\"timeZone\":\"UTC\"},\"subject\":\"Board " + subject + " Meeting\",\"transactionId\":\"28b36295-6cd3-952f-d8f5-deb313444a51\",\"type\":\"singleInstance\",\"webLink\":\"https://outlook.office365.com/owa/?itemid=AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwBGAAAAAADCNgjhM9QmQYWNcI7hCpPrBwDSEBNbUIB9RL6ePDeF3FIYAAAAAAENAADSEBNbUIB9RL6ePDeF3FIYAABU%2BFdvAAA%3D&exvsurl=1&path=/calendar/item\"}" - - return []byte(event) -} diff --git a/src/internal/connector/support/m365Transform.go b/src/internal/connector/support/m365Transform.go index c5f73fe8f..3c6a7981e 100644 --- a/src/internal/connector/support/m365Transform.go +++ b/src/internal/connector/support/m365Transform.go @@ -88,6 +88,14 @@ func ToEventSimplified(orig models.Eventable) models.Eventable { orig.SetICalUId(nil) orig.SetId(nil) + // Sanitize recurrence timezone. + if orig.GetRecurrence() != nil { + recurrenceTimezone := ptr.Val(orig.GetRecurrence().GetRange().GetRecurrenceTimeZone()) + if len(recurrenceTimezone) == 0 { + orig.GetRecurrence().GetRange().SetRecurrenceTimeZone(nil) + } + } + return orig } diff --git a/src/internal/connector/support/m365Transform_test.go b/src/internal/connector/support/m365Transform_test.go index 355959843..a50c75f44 100644 --- a/src/internal/connector/support/m365Transform_test.go +++ b/src/internal/connector/support/m365Transform_test.go @@ -37,7 +37,7 @@ func (suite *SupportTestSuite) TestToMessage() { assert.NotEqual(t, message.GetId(), clone.GetId()) } -func (suite *SupportTestSuite) TestToEventSimplified() { +func (suite *SupportTestSuite) TestToEventSimplified_attendees() { t := suite.T() bytes := mockconnector.GetMockEventWithAttendeesBytes("M365 Event Support Test") event, err := CreateEventFromBytes(bytes) @@ -57,6 +57,80 @@ func (suite *SupportTestSuite) TestToEventSimplified() { } } +func (suite *SupportTestSuite) TestToEventSimplified_recurrence() { + var ( + t = suite.T() + subject = "M365 Event Support Test" + ) + + tests := []struct { + name string + event func() models.Eventable + validateOutput func(e models.Eventable) bool + }{ + { + name: "Test recurrence: Unspecified", + event: func() models.Eventable { + bytes := mockconnector.GetMockEventWithSubjectBytes(subject) + e, err := CreateEventFromBytes(bytes) + require.NoError(t, err, clues.ToCore(err)) + return e + }, + + validateOutput: func(e models.Eventable) bool { + return e.GetRecurrence() == nil + }, + }, + { + name: "Test recurrenceTimeZone: Unspecified", + event: func() models.Eventable { + bytes := mockconnector.GetMockEventWithRecurrenceBytes(subject, `null`) + e, err := CreateEventFromBytes(bytes) + require.NoError(t, err, clues.ToCore(err)) + return e + }, + + validateOutput: func(e models.Eventable) bool { + return e.GetRecurrence().GetRange().GetRecurrenceTimeZone() == nil + }, + }, + { + name: "Test recurrenceTimeZone: Empty", + event: func() models.Eventable { + bytes := mockconnector.GetMockEventWithRecurrenceBytes(subject, `""`) + event, err := CreateEventFromBytes(bytes) + require.NoError(t, err, clues.ToCore(err)) + return event + }, + + validateOutput: func(e models.Eventable) bool { + return e.GetRecurrence().GetRange().GetRecurrenceTimeZone() == nil + }, + }, + { + name: "Test recurrenceTimeZone: Valid", + event: func() models.Eventable { + bytes := mockconnector.GetMockEventWithRecurrenceBytes(subject, `"Pacific Standard Time"`) + event, err := CreateEventFromBytes(bytes) + require.NoError(t, err, clues.ToCore(err)) + return event + }, + + validateOutput: func(e models.Eventable) bool { + return ptr.Val(e.GetRecurrence().GetRange().GetRecurrenceTimeZone()) == "Pacific Standard Time" + }, + }, + } + + for _, test := range tests { + suite.Run(test.name, func() { + event := test.event() + newEvent := ToEventSimplified(event) + assert.True(t, test.validateOutput(newEvent), test.name) + }) + } +} + type mockContenter struct { content *string contentType *models.BodyType diff --git a/src/internal/operations/backup_integration_test.go b/src/internal/operations/backup_integration_test.go index 202a83929..d4560033c 100644 --- a/src/internal/operations/backup_integration_test.go +++ b/src/internal/operations/backup_integration_test.go @@ -790,7 +790,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() { eventDBF := func(id, timeStamp, subject, body string) []byte { return mockconnector.GetMockEventWith( suite.user, subject, body, body, - now, now, false) + now, now, mockconnector.NoRecurrence, mockconnector.NoAttendees, false) } // test data set