From 5669619a8de76a3c2ea6e190cf27419561aa610b Mon Sep 17 00:00:00 2001 From: ashmrtn <3891298+ashmrtn@users.noreply.github.com> Date: Tue, 19 Dec 2023 16:26:10 -0800 Subject: [PATCH] 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] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [ ] :no_entry: No #### Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Supportability/Tests - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup #### Test Plan - [x] :muscle: Manual - [x] :zap: Unit test - [ ] :green_heart: E2E --- CHANGELOG.md | 1 + .../m365/collection/exchange/transform.go | 42 ++++-- .../collection/exchange/transform_test.go | 26 ++++ .../m365/service/exchange/mock/event.go | 128 +++++++++++++++++- 4 files changed, 188 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1580ef6df..5c1e5a9b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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`. +- 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 diff --git a/src/internal/m365/collection/exchange/transform.go b/src/internal/m365/collection/exchange/transform.go index 5dccbe34f..5b9514874 100644 --- a/src/internal/m365/collection/exchange/transform.go +++ b/src/internal/m365/collection/exchange/transform.go @@ -58,6 +58,28 @@ func toMessage(orig models.Messageable) models.Messageable { 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 // 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, @@ -79,6 +101,18 @@ func toEventSimplified(orig models.Eventable) models.Eventable { orig.SetWebLink(nil) orig.SetICalUId(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. 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 } diff --git a/src/internal/m365/collection/exchange/transform_test.go b/src/internal/m365/collection/exchange/transform_test.go index 020406803..4f088ae56 100644 --- a/src/internal/m365/collection/exchange/transform_test.go +++ b/src/internal/m365/collection/exchange/transform_test.go @@ -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() { var ( t = suite.T() diff --git a/src/internal/m365/service/exchange/mock/event.go b/src/internal/m365/service/exchange/mock/event.go index 4dd3ea9e7..c9910e9f5 100644 --- a/src/internal/m365/service/exchange/mock/event.go +++ b/src/internal/m365/service/exchange/mock/event.go @@ -95,6 +95,84 @@ var ( "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":"
` + + `%s
", + "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.