Remove other event fields ignored by server (#4888)

Update the set of ignored fields for event restores. Most important inclusion is the `iCalUId_v2` field which will cause failures if it's not removed.

---

#### Does this PR need a docs update or release note?

- [x]  Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [ ]  No

#### Type of change

- [ ] 🌻 Feature
- [x] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Test Plan

- [x] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
ashmrtn 2023-12-19 16:26:10 -08:00 committed by GitHub
parent 5aa9515d67
commit 5669619a8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 188 additions and 9 deletions

View File

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Handle the case where an email cannot be retrieved from Exchange due to an `ErrorInvalidRecipients` error. In - Handle the case where an email cannot be retrieved from Exchange due to an `ErrorInvalidRecipients` error. In
this case, Corso will skip over the item but report this in the backup summary. this case, Corso will skip over the item but report this in the backup summary.
- Guarantee Exchange email restoration when restoring multiple attachments. Some previous restores were failing with `ErrorItemNotFound`. - Guarantee Exchange email restoration when restoring multiple attachments. Some previous restores were failing with `ErrorItemNotFound`.
- Avoid Graph SDK `Requests must contain extension changes exclusively.` errors by removing server-populated field from restored event items.
## [v0.17.0] (beta) - 2023-12-11 ## [v0.17.0] (beta) - 2023-12-11

View File

@ -58,6 +58,28 @@ func toMessage(orig models.Messageable) models.Messageable {
return CloneMessageableFields(orig, message) return CloneMessageableFields(orig, message)
} }
// eventUnsupportedAdditionalData lists the set of additionalData keys that are
// not needed for backup completion and may cause errors in Graph API when
// restoring items.
var eventUnsupportedAdditionalData = []string{
// Will cause errors about needing to put extension data in their own requests
// if not removed.
"iCalUId_v2",
// Appears to be duplicate of the iCalUId.
"uid",
// Navigation links from the calendar to the event itself.
"calendar@odata.associationLink",
"calendar@odata.navigationLink",
// Seems like info about the request that generated the data response.
"@odata.context",
// Appears to be similar to the change tag.
"@odata.etag",
// Remove exceptions for recurring events. These will be present in objects
// once we start using the API that is currently in beta.
"cancelledOccurrences",
"exceptionOccurrences",
}
// ToEventSimplified transforms an event to simplified restore format // ToEventSimplified transforms an event to simplified restore format
// To overcome some of the MS Graph API challenges, the event object is modified in the following ways: // To overcome some of the MS Graph API challenges, the event object is modified in the following ways:
// - Instead of adding attendees and generating spurious notifications, // - Instead of adding attendees and generating spurious notifications,
@ -79,6 +101,18 @@ func toEventSimplified(orig models.Eventable) models.Eventable {
orig.SetWebLink(nil) orig.SetWebLink(nil)
orig.SetICalUId(nil) orig.SetICalUId(nil)
orig.SetId(nil) orig.SetId(nil)
orig.SetOdataType(nil)
orig.SetChangeKey(nil)
// Additional fields that don't have API support but are ignored by the
// server.
additionalData := orig.GetAdditionalData()
for _, key := range eventUnsupportedAdditionalData {
delete(additionalData, key)
}
orig.SetAdditionalData(additionalData)
// Sanitize recurrence timezone. // Sanitize recurrence timezone.
if orig.GetRecurrence() != nil { if orig.GetRecurrence() != nil {
@ -88,14 +122,6 @@ func toEventSimplified(orig models.Eventable) models.Eventable {
} }
} }
// Remove exceptions for recurring events
// These will be present in objects once we start using the API
// that is currently in beta
additionalData := orig.GetAdditionalData()
delete(additionalData, "cancelledOccurrences")
delete(additionalData, "exceptionOccurrences")
orig.SetAdditionalData(additionalData)
return orig return orig
} }

View File

@ -58,6 +58,32 @@ func (suite *TransformUnitTest) TestToEventSimplified_attendees() {
} }
} }
func (suite *TransformUnitTest) TestToEventSimplified_noAdditionalRemovedFields() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
bytes := exchMock.EventWithRemovedFields("M365 Event Support Test")
event, err := api.BytesToEventable(bytes)
require.NoError(t, err, clues.ToCore(err))
newEvent := toEventSimplified(event)
serializedBytes, err := api.Client{}.Events().Serialize(
ctx,
newEvent,
"",
"")
require.NoError(t, err, clues.ToCore(err))
serializedString := string(serializedBytes)
for _, key := range eventUnsupportedAdditionalData {
assert.NotContains(t, serializedString, key)
}
}
func (suite *TransformUnitTest) TestToEventSimplified_recurrence() { func (suite *TransformUnitTest) TestToEventSimplified_recurrence() {
var ( var (
t = suite.T() t = suite.T()

View File

@ -95,6 +95,84 @@ var (
"attendees":%s "attendees":%s
}` }`
eventWithRemovedFieldsTmpl = `{
"iCalUId":"abce",
"iCalUId_v2":"fghi",
"uid":"fghi",
"calendar@odata.associationLink":"some-link",
"calendar@odata.navigationLink":"some-link",
"@odata.context":"some-data",
"@odata.etag":"foo",
"categories":[],
"changeKey":"0hATW1CAfUS+njw3hdxSGAAAJIxNug==",
"createdDateTime":"2022-03-28T03:42:03Z",
"lastModifiedDateTime":"2022-05-26T19:25:58Z",
"allowNewTimeProposals":true,
"body":{
"content":"<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"></head><body>` +
`<p>%s</p></body></html>",
"contentType":"html"
},
"bodyPreview":"%s",
"end":{
"dateTime":"%s",
"timeZone":"UTC"
},
"hideAttendees":false,
"importance":"normal",
"isAllDay":false,
"isCancelled":false,
"isDraft":false,
"isOnlineMeeting":false,
"isOrganizer":false,
"isReminderOn":true,
"location":{
"displayName":"Umi Sake House (2230 1st Ave, Seattle, WA 98121 US)",
"locationType":"default",
"uniqueId":"Umi Sake House (2230 1st Ave, Seattle, WA 98121 US)",
"uniqueIdType":"private"
},
"locations":[
{
"displayName":"Umi Sake House (2230 1st Ave, Seattle, WA 98121 US)",
"locationType":"default",
"uniqueId":"",
"uniqueIdType":"unknown"
}
],
"onlineMeetingProvider":"unknown",
"organizer":{
"emailAddress":{
"address":"%s",
"name":"Anu Pierson"
}
},
%s
"originalEndTimeZone":"UTC",
"originalStartTimeZone":"UTC",
"reminderMinutesBeforeStart":15,
"responseRequested":true,
"responseStatus":{
"response":"notResponded",
"time":"0001-01-01T00:00:00Z"
},
"sensitivity":"normal",
"showAs":"tentative",
"start":{
"dateTime":"%s",
"timeZone":"UTC"
},
"subject":"%s",
"type":"%s",
"hasAttachments":%v,
%s
"webLink":"https://outlook.office365.com/owa/?itemid=AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwBGAAAAAADCNgjhM9QmQYWNcI7hCpPrBwDSEBNbUIB9RL6ePDeF3FIYAAAAAAENAADSEBNbUIB9RL6ePDeF3FIYAAAAAG76AAA%%3D&exvsurl=1&path=/calendar/item",
"recurrence":%s,
%s
%s
"attendees":%s
}`
defaultEventBody = "This meeting is to review the latest Tailspin Toys project proposal.<br>\\r\\nBut why not eat some sushi while were at it? :)" defaultEventBody = "This meeting is to review the latest Tailspin Toys project proposal.<br>\\r\\nBut why not eat some sushi while were at it? :)"
defaultEventBodyPreview = "This meeting is to review the latest Tailspin Toys project proposal.\\r\\nBut why not eat some sushi while were at it? :)" defaultEventBodyPreview = "This meeting is to review the latest Tailspin Toys project proposal.\\r\\nBut why not eat some sushi while were at it? :)"
defaultEventOrganizer = "foobar@8qzvrj.onmicrosoft.com" defaultEventOrganizer = "foobar@8qzvrj.onmicrosoft.com"
@ -395,6 +473,17 @@ func EventWith(
organizer, subject, body, bodyPreview, organizer, subject, body, bodyPreview,
originalStartDate, startDateTime, endDateTime, recurrence, attendees, originalStartDate, startDateTime, endDateTime, recurrence, attendees,
attachments, cancelledOccurrences, exceptionOccurrences string, attachments, cancelledOccurrences, exceptionOccurrences string,
) []byte {
return eventWith(
eventTmpl, organizer, subject, body, bodyPreview,
originalStartDate, startDateTime, endDateTime, recurrence, attendees,
attachments, cancelledOccurrences, exceptionOccurrences)
}
func eventWith(
tmpl, organizer, subject, body, bodyPreview,
originalStartDate, startDateTime, endDateTime, recurrence, attendees,
attachments, cancelledOccurrences, exceptionOccurrences string,
) []byte { ) []byte {
hasAttachments := len(attachments) > 0 hasAttachments := len(attachments) > 0
startDateTime = strings.TrimSuffix(startDateTime, "Z") startDateTime = strings.TrimSuffix(startDateTime, "Z")
@ -414,7 +503,7 @@ func EventWith(
} }
return []byte(fmt.Sprintf( return []byte(fmt.Sprintf(
eventTmpl, tmpl,
body, body,
bodyPreview, bodyPreview,
endDateTime, endDateTime,
@ -430,3 +519,40 @@ func EventWith(
exceptionOccurrences, exceptionOccurrences,
attendees)) attendees))
} }
func EventWithRemovedFields(subject string) []byte {
var (
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 = dttm.Format(at)
timeSlice = strings.Split(atTime, "T")
newTime = dttm.Format(tomorrow.AddDate(0, 0, 1))
originalStartDate = dttm.FormatTo(at, dttm.TabularOutput)
nextYear = tomorrow.AddDate(1, 0, 0)
)
recurrence := string(fmt.Sprintf(
recurrenceTmpl,
strconv.Itoa(int(at.Month())),
strconv.Itoa(at.Day()),
timeSlice[0],
`"UTC"`))
cancelledInstances := []string{fmt.Sprintf(cancelledOccurrenceInstanceFormat, dttm.FormatTo(nextYear, dttm.DateOnly))}
cancelledOccurrences := fmt.Sprintf(cancelledOccurrencesFormat, strings.Join(cancelledInstances, ","))
exceptionEvent := EventWith(
defaultEventOrganizer, subject+"(modified)",
defaultEventBody, defaultEventBodyPreview,
fmt.Sprintf(originalStartDateFormat, originalStartDate),
newTime, newTime, NoRecurrence, attendeesTmpl,
NoAttachments, NoCancelledOccurrences, NoExceptionOccurrences)
exceptionOccurrences := fmt.Sprintf(exceptionOccurrencesFormat, exceptionEvent)
return eventWith(
eventWithRemovedFieldsTmpl,
defaultEventOrganizer, subject,
defaultEventBody, defaultEventBodyPreview,
NoOriginalStartDate, atTime, atTime, recurrence, attendeesTmpl,
defaultEventAttachments, cancelledOccurrences, exceptionOccurrences)
}