GC: Restore: Simplified Event Restore (#981)

## Description
Implementation of simplified Restore based on the following [spec](https://www.notion.so/alcion/Event-restore-semantics-061aee5288244629b1c53337e4dea306#6e974540c8804c4fa832218675534e1c)

<!-- Insert PR description-->

## Type of change

<!--- Please check the type of change your PR introduces: --->
- [x] 🌻 Feature


## Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
*closes  #954<issue>

## Test Plan

<!-- How will this be tested prior to merging.-->
- [x] 💪 Manual
This commit is contained in:
Danny 2022-09-29 18:01:12 -04:00 committed by GitHub
parent 23e1db13df
commit 114fec6059
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 260 additions and 4 deletions

View File

@ -426,8 +426,6 @@ func (suite *ExchangeServiceSuite) TestRestoreContact() {
func (suite *ExchangeServiceSuite) TestRestoreEvent() { func (suite *ExchangeServiceSuite) TestRestoreEvent() {
t := suite.T() t := suite.T()
ctx := context.Background() ctx := context.Background()
// TODO: #779 - reinstate when restored events to not generate notifications
t.Skip("#779 - reinstate when restored events to not generate notifications")
userID := tester.M365UserID(t) userID := tester.M365UserID(t)
name := "TestRestoreEvent: " + common.FormatSimpleDateTime(time.Now()) name := "TestRestoreEvent: " + common.FormatSimpleDateTime(time.Now())
calendar, err := CreateCalendar(ctx, suite.es, userID, name) calendar, err := CreateCalendar(ctx, suite.es, userID, name)
@ -435,7 +433,7 @@ func (suite *ExchangeServiceSuite) TestRestoreEvent() {
calendarID := *calendar.GetId() calendarID := *calendar.GetId()
err = RestoreExchangeEvent(context.Background(), err = RestoreExchangeEvent(context.Background(),
mockconnector.GetMockEventBytes("Restore Event "), mockconnector.GetMockEventWithAttendeesBytes(name),
suite.es, suite.es,
control.Copy, control.Copy,
calendarID, calendarID,

View File

@ -150,7 +150,9 @@ func RestoreExchangeEvent(
return err return err
} }
response, err := service.Client().UsersById(user).CalendarsById(destination).Events().Post(ctx, event, nil) transformedEvent := support.ToEventSimplified(event)
response, err := service.Client().UsersById(user).CalendarsById(destination).Events().Post(ctx, transformedEvent, nil)
if err != nil { if err != nil {
return errors.Wrap(err, return errors.Wrap(err,
fmt.Sprintf( fmt.Sprintf(

View File

@ -271,6 +271,35 @@ func GetMockEventBytes(subject string) []byte {
return []byte(event) return []byte(event)
} }
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\":\"<html>\\n<head>\\n<meta http-equiv=\\\"Content-Type\\\" content=\\\"text/html; charset=utf-8\\\">\\n</head>\\n<body>\\n<div class=\\\"elementToProof\\\" style=\\\"font-family:Calibri,Arial,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0); background-color:rgb(255,255,255)\\\">\\nDiscuss matters concerning stock options and early release of quarterly earnings.</div>\\n<br> " +
"\\n<div style=\\\"width:100%; height:20px\\\"><span style=\\\"white-space:nowrap; color:#5F5F5F; opacity:.36\\\">________________________________________________________________________________</span>\\n</div>\\n<div class=\\\"me-email-text\\\" lang=\\\"en-GB\\\" style=\\\"color:#252424; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\\\"> " +
"\\n<div style=\\\"margin-top:24px; margin-bottom:20px\\\"><span style=\\\"font-size:24px; color:#252424\\\">Microsoft Teams meeting</span>\\n</div>\\n<div style=\\\"margin-bottom:20px\\\">\\n<div style=\\\"margin-top:0px; margin-bottom:0px; font-weight:bold\\\"><span style=\\\"font-size:14px; color:#252424\\\">Join on your computer or mobile app</span>" +
"\\n</div>\\n<a href=\\\"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\\\" class=\\\"me-email-headline\\\" style=\\\"font-size:14px; font-family:'Segoe UI Semibold','Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif; text-decoration:underline; color:#6264a7\\\">Click\\n here to join the meeting</a> </div>" +
"\\n<div style=\\\"margin-bottom:20px; margin-top:20px\\\">\\n<div style=\\\"margin-bottom:4px\\\"><span style=\\\"font-size:14px; color:#252424\\\">Meeting ID:\\n<span style=\\\"font-size:16px; color:#252424\\\">292 784 521 247</span> </span><br>\\n<span style=\\\"font-size:14px; color:#252424\\\">Passcode: </span><span style=\\\"font-size:16px; color:#252424\\\">SzBkfK\\n</span>" +
"\\n<div style=\\\"font-size:14px\\\"><a href=\\\"https://www.microsoft.com/en-us/microsoft-teams/download-app\\\" class=\\\"me-email-link\\\" style=\\\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\\\">Download\\n Teams</a> | <a href=\\\"https://www.microsoft.com/microsoft-teams/join-a-meeting\\\" class=\\\"me-email-link\\\" style=\\\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\\\">" +
"\\nJoin on the web</a></div>\\n</div>\\n</div>\\n<div style=\\\"margin-bottom:24px; margin-top:20px\\\"><a href=\\\"https://aka.ms/JoinTeamsMeeting\\\" class=\\\"me-email-link\\\" style=\\\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\\\">Learn more</a>" +
"\\n | <a href=\\\"https://teams.microsoft.com/meetingOptions/?organizerId=a4a472f8-ccb0-43ec-bf52-3697a91b926c&amp;tenantId=4d603060-18d6-4764-b9be-4cb794d32b69&amp;threadId=19_meeting_YWNhMzAxZjItMzE2My00ZGQzLTkzMDUtNjQ3NTY0NjNjMTZi@thread.v2&amp;messageId=0&amp;language=en-GB\\\" class=\\\"me-email-link\\\" style=\\\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\\\">" +
"\\nMeeting options</a> </div>\\n</div>\\n<div style=\\\"font-size:14px; margin-bottom:4px; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\\\">\\n</div>\\n<div style=\\\"font-size:12px\\\"></div>\\n<div></div>\\n<div style=\\\"width:100%; height:20px\\\"><span style=\\\"white-space:nowrap; color:#5F5F5F; opacity:.36\\\">________________________________________________________________________________</span>" +
"\\n</div>\\n</body>\\n</html>\\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)
}
type errReader struct { type errReader struct {
readErr error readErr error
} }

View File

@ -121,3 +121,48 @@ func (suite *MockExchangeDataSuite) TestMockExchangeData() {
}) })
} }
} }
func (suite *MockExchangeDataSuite) TestMockByteHydration() {
subject := "Mock Hydration"
tests := []struct {
name string
transformation func(t *testing.T) error
}{
{
name: "Message Bytes",
transformation: func(t *testing.T) error {
bytes := mockconnector.GetMockMessageBytes(subject)
_, err := support.CreateMessageFromBytes(bytes)
return err
},
}, {
name: "Contact Bytes",
transformation: func(t *testing.T) error {
bytes := mockconnector.GetMockContactBytes(subject)
_, err := support.CreateContactFromBytes(bytes)
return err
},
}, {
name: "Event No Attendees Bytes",
transformation: func(t *testing.T) error {
bytes := mockconnector.GetMockEventBytes(subject)
_, err := support.CreateEventFromBytes(bytes)
return err
},
}, {
name: "Event w/ Attendees Bytes",
transformation: func(t *testing.T) error {
bytes := mockconnector.GetMockEventWithAttendeesBytes(subject)
_, err := support.CreateEventFromBytes(bytes)
return err
},
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
err := test.transformation(t)
assert.NoError(t, err)
})
}
}

View File

@ -0,0 +1,113 @@
package support
import (
"fmt"
"github.com/microsoftgraph/msgraph-sdk-go/models"
)
type attendee struct {
name string
email string
response string
}
// FormatAttendees returns string representation of an attendee
// Return Format: - Name <email@example.com>, Accepted | Declined | Tentative | No Response
func FormatAttendees(event models.Eventable, isHTML bool) string {
var (
failed int
response = event.GetAttendees()
required = make([]attendee, 0)
optional = make([]attendee, 0)
resource = make([]attendee, 0)
)
for _, entry := range response {
if guardCheckForAttendee(entry) {
failed++
continue
}
temp := attendee{
name: *entry.GetEmailAddress().GetName(),
email: *entry.GetEmailAddress().GetAddress(),
response: entry.GetStatus().GetResponse().String(),
}
switch *entry.GetType() {
case models.REQUIRED_ATTENDEETYPE:
required = append(required, temp)
case models.OPTIONAL_ATTENDEETYPE:
optional = append(optional, temp)
case models.RESOURCE_ATTENDEETYPE:
resource = append(resource, temp)
}
}
req := attendeeListToString(required, "Required", isHTML)
opt := attendeeListToString(optional, "Optional", isHTML)
res := attendeeListToString(resource, "Resource", isHTML)
return req + opt + res
}
func attendeeListToString(attendList []attendee, heading string, isHTML bool) string {
var (
message string
lineBreak string
)
if isHTML {
lineBreak = "<br>"
} else {
lineBreak = "\n"
}
if len(attendList) > 0 {
message = heading + ":" + lineBreak
for _, resource := range attendList {
message += "- " + resource.String(isHTML) + " " + lineBreak
}
message += lineBreak + lineBreak
}
return message
}
func guardCheckForAttendee(attendee models.Attendeeable) bool {
if attendee.GetType() == nil ||
attendee.GetStatus() == nil {
return true
}
if attendee.GetStatus().GetResponse() == nil {
return true
}
if attendee.GetEmailAddress() == nil {
return true
}
if attendee.GetEmailAddress().GetName() == nil ||
attendee.GetEmailAddress().GetAddress() == nil {
return true
}
return false
}
// String function to return struct representation of attendee
func (at *attendee) String(isHTML bool) string {
var contents string
if isHTML {
contents = fmt.Sprintf("%s &lt;%s&gt;, %s", at.name, at.email, at.response)
} else {
contents = fmt.Sprintf("%s <%s>, %s", at.name, at.email, at.response)
}
return contents
}

View File

@ -3,6 +3,7 @@ package support
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
kw "github.com/microsoft/kiota-serialization-json-go" kw "github.com/microsoft/kiota-serialization-json-go"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
@ -400,3 +401,50 @@ func SetAdditionalDataToEventMessage(
return newMessage, nil return newMessage, nil
} }
// ToEventSimplified transforms an event to simplifed 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,
// add a summary of attendees at the beginning to the event before the original body content
// * event.attendees is set to an empty list
func ToEventSimplified(orig models.Eventable) models.Eventable {
attendees := FormatAttendees(orig, *orig.GetBody().GetContentType() == models.HTML_BODYTYPE)
orig.SetAttendees([]models.Attendeeable{})
origBody := orig.GetBody()
newContent := insertStringToBody(origBody, attendees)
newBody := models.NewItemBody()
newBody.SetContentType(origBody.GetContentType())
newBody.SetAdditionalData(origBody.GetAdditionalData())
newBody.SetOdataType(origBody.GetOdataType())
newBody.SetContent(&newContent)
orig.SetBody(newBody)
return orig
}
// insertStringToBody helper function to insert text into models.bodyable
// @returns string containing the content string of altered body.
func insertStringToBody(body models.ItemBodyable, newContent string) string {
var prefix, suffix string
if body.GetContent() == nil ||
body.GetContentType() == nil {
return ""
}
content := *body.GetContent()
switch *body.GetContentType() {
case models.TEXT_BODYTYPE:
return newContent + content
case models.HTML_BODYTYPE:
array := strings.Split(content, "<body>")
prefix = array[0] + "<body>"
interior := array[1]
bodyArray := strings.Split(interior, ">")
prefix += bodyArray[0] + ">"
suffix = strings.Join(bodyArray[1:], ">")
}
return prefix + newContent + suffix
}

View File

@ -3,6 +3,7 @@ package support
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -29,3 +30,23 @@ func (suite *SupportTestSuite) TestToMessage() {
suite.Equal(message.GetSentDateTime(), clone.GetSentDateTime()) suite.Equal(message.GetSentDateTime(), clone.GetSentDateTime())
suite.NotEqual(message.GetId(), clone.GetId()) suite.NotEqual(message.GetId(), clone.GetId())
} }
func (suite *SupportTestSuite) TestToEventSimplified() {
t := suite.T()
bytes := mockconnector.GetMockEventWithAttendeesBytes("M365 Event Support Test")
event, err := CreateEventFromBytes(bytes)
require.NoError(t, err)
attendees := event.GetAttendees()
newEvent := ToEventSimplified(event)
assert.Empty(t, newEvent.GetHideAttendees())
assert.Equal(t, *event.GetBody().GetContentType(), *newEvent.GetBody().GetContentType())
assert.Equal(t, event.GetBody().GetAdditionalData(), newEvent.GetBody().GetAdditionalData())
assert.Contains(t, *event.GetBody().GetContent(), "Required:")
for _, member := range attendees {
assert.Contains(t, *event.GetBody().GetContent(), *member.GetEmailAddress().GetName())
assert.Contains(t, *event.GetBody().GetContent(), *member.GetEmailAddress().GetAddress())
}
}