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
\\n
Meeting ID:\\n292 784 521 247 \\n
Passcode: SzBkfK\\n" +
- "\\n
\\n
\\n
\\n
\\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