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:
parent
5aa9515d67
commit
5669619a8d
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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 we’re at it? :)"
|
defaultEventBody = "This meeting is to review the latest Tailspin Toys project proposal.<br>\\r\\nBut why not eat some sushi while we’re at it? :)"
|
||||||
defaultEventBodyPreview = "This meeting is to review the latest Tailspin Toys project proposal.\\r\\nBut why not eat some sushi while we’re at it? :)"
|
defaultEventBodyPreview = "This meeting is to review the latest Tailspin Toys project proposal.\\r\\nBut why not eat some sushi while we’re 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)
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user