diff --git a/src/internal/common/time.go b/src/internal/common/time.go index 968355d0b..de430cf8b 100644 --- a/src/internal/common/time.go +++ b/src/internal/common/time.go @@ -38,6 +38,10 @@ const ( // SimpleTimeTesting is used for testing restore destination folders. // Microsecond granularity prevents collisions in parallel package or workflow runs. SimpleTimeTesting TimeFormat = SimpleDateTimeOneDrive + ".000000" + + // M365dateTimeTimeZoneTimeFormat is the format used by M365 for datetimetimezone resource + // https://learn.microsoft.com/en-us/graph/api/resources/datetimetimezone?view=graph-rest-1.0 + M365DateTimeTimeZone TimeFormat = "2006-01-02T15:04:05.000000" ) // these regexes are used to extract time formats from strings. Their primary purpose is to diff --git a/src/internal/connector/exchange/event.go b/src/internal/connector/exchange/event.go index e8eac0759..bb97b1d7a 100644 --- a/src/internal/connector/exchange/event.go +++ b/src/internal/connector/exchange/event.go @@ -15,6 +15,7 @@ func EventInfo(evt models.Eventable) *details.ExchangeInfo { organizer, subject string recurs bool start = time.Time{} + end = time.Time{} ) if evt.GetOrganizer() != nil && @@ -37,19 +38,32 @@ func EventInfo(evt models.Eventable) *details.ExchangeInfo { evt.GetStart().GetDateTime() != nil { // timeString has 'Z' literal added to ensure the stored // DateTime is not: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC) - timeString := *evt.GetStart().GetDateTime() + "Z" + startTime := *evt.GetStart().GetDateTime() + "Z" - output, err := common.ParseTime(timeString) + output, err := common.ParseTime(startTime) if err == nil { start = output } } + if evt.GetEnd() != nil && + evt.GetEnd().GetDateTime() != nil { + // timeString has 'Z' literal added to ensure the stored + // DateTime is not: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC) + endTime := *evt.GetEnd().GetDateTime() + "Z" + + output, err := common.ParseTime(endTime) + if err == nil { + end = output + } + } + return &details.ExchangeInfo{ ItemType: details.ExchangeEvent, Organizer: organizer, Subject: subject, EventStart: start, + EventEnd: end, EventRecurs: recurs, } } diff --git a/src/internal/connector/exchange/event_test.go b/src/internal/connector/exchange/event_test.go index 3feb723a1..4f532150a 100644 --- a/src/internal/connector/exchange/event_test.go +++ b/src/internal/connector/exchange/event_test.go @@ -27,7 +27,7 @@ func TestEventSuite(t *testing.T) { // can be properly retrieved from a models.Eventable object func (suite *EventSuite) TestEventInfo() { initial := time.Now() - now := common.FormatTime(initial) + now := common.FormatTimeWith(initial, common.M365DateTimeTimeZone) suite.T().Logf("Initial: %v\nFormatted: %v\n", initial, now) @@ -47,19 +47,41 @@ func (suite *EventSuite) TestEventInfo() { name: "Start time only", evtAndRP: func() (models.Eventable, *details.ExchangeInfo) { var ( - event = models.NewEvent() - dateTime = models.NewDateTimeTimeZone() - full, err = common.ParseTime(now) + event = models.NewEvent() + dateTime = models.NewDateTimeTimeZone() ) - require.NoError(suite.T(), err) - dateTime.SetDateTime(&now) event.SetStart(dateTime) return event, &details.ExchangeInfo{ - ItemType: details.ExchangeEvent, - Received: full, + ItemType: details.ExchangeEvent, + Received: initial, + EventStart: initial, + } + }, + }, + { + name: "Start and end time only", + evtAndRP: func() (models.Eventable, *details.ExchangeInfo) { + var ( + event = models.NewEvent() + startTime = models.NewDateTimeTimeZone() + endTime = models.NewDateTimeTimeZone() + ) + + startTime.SetDateTime(&now) + event.SetStart(startTime) + + nowp30m := common.FormatTimeWith(initial.Add(30*time.Minute), common.M365DateTimeTimeZone) + endTime.SetDateTime(&nowp30m) + event.SetEnd(endTime) + + return event, &details.ExchangeInfo{ + ItemType: details.ExchangeEvent, + Received: initial, + EventStart: initial, + EventEnd: initial.Add(30 * time.Minute), } }, }, @@ -83,12 +105,13 @@ func (suite *EventSuite) TestEventInfo() { name: "Using mockable", evtAndRP: func() (models.Eventable, *details.ExchangeInfo) { var ( - organizer = "foobar3@8qzvrj.onmicrosoft.com" - subject = " Test Mock Review + Lunch" - bytes = mockconnector.GetDefaultMockEventBytes("Test Mock") - future = time.Now().UTC().AddDate(0, 0, 1) - eventTime = time.Date(future.Year(), future.Month(), future.Day(), future.Hour(), 0, 0, 0, time.UTC) - event, err = support.CreateEventFromBytes(bytes) + organizer = "foobar3@8qzvrj.onmicrosoft.com" + subject = " Test Mock Review + Lunch" + bytes = mockconnector.GetDefaultMockEventBytes("Test Mock") + future = time.Now().UTC().AddDate(0, 0, 1) + eventTime = time.Date(future.Year(), future.Month(), future.Day(), future.Hour(), 0, 0, 0, time.UTC) + eventEndTime = eventTime.Add(30 * time.Minute) + event, err = support.CreateEventFromBytes(bytes) ) require.NoError(suite.T(), err) @@ -98,6 +121,7 @@ func (suite *EventSuite) TestEventInfo() { Subject: subject, Organizer: organizer, EventStart: eventTime, + EventEnd: eventEndTime, } }, }, @@ -110,16 +134,27 @@ func (suite *EventSuite) TestEventInfo() { assert.Equal(t, expected.Subject, result.Subject, "subject") assert.Equal(t, expected.Sender, result.Sender, "sender") - expYear, expMonth, _ := expected.EventStart.Date() // Day not used at certain times of the day - expHr, expMin, expSec := expected.EventStart.Clock() - recvYear, recvMonth, _ := result.EventStart.Date() - recvHr, recvMin, recvSec := result.EventStart.Clock() + expStartYear, expStartMonth, _ := expected.EventStart.Date() // Day not used at certain times of the day + expStartHr, expStartMin, expStartSec := expected.EventStart.Clock() + recvStartYear, recvStartMonth, _ := result.EventStart.Date() + recvStartHr, recvStartMin, recvStartSec := result.EventStart.Clock() - assert.Equal(t, expYear, recvYear, "year") - assert.Equal(t, expMonth, recvMonth, "month") - assert.Equal(t, expHr, recvHr, "hour") - assert.Equal(t, expMin, recvMin, "minute") - assert.Equal(t, expSec, recvSec, "second") + assert.Equal(t, expStartYear, recvStartYear, "year") + assert.Equal(t, expStartMonth, recvStartMonth, "month") + assert.Equal(t, expStartHr, recvStartHr, "hour") + assert.Equal(t, expStartMin, recvStartMin, "minute") + assert.Equal(t, expStartSec, recvStartSec, "second") + + expEndYear, expEndMonth, _ := expected.EventEnd.Date() // Day not used at certain times of the day + expEndHr, expEndMin, expEndSec := expected.EventEnd.Clock() + recvEndYear, recvEndMonth, _ := result.EventEnd.Date() + recvEndHr, recvEndMin, recvEndSec := result.EventEnd.Clock() + + assert.Equal(t, expEndYear, recvEndYear, "year") + assert.Equal(t, expEndMonth, recvEndMonth, "month") + assert.Equal(t, expEndHr, recvEndHr, "hour") + assert.Equal(t, expEndMin, recvEndMin, "minute") + assert.Equal(t, expEndSec, recvEndSec, "second") }) } } diff --git a/src/internal/connector/mockconnector/mock_data_event.go b/src/internal/connector/mockconnector/mock_data_event.go index 0327c89c7..d45bcbff7 100644 --- a/src/internal/connector/mockconnector/mock_data_event.go +++ b/src/internal/connector/mockconnector/mock_data_event.go @@ -176,11 +176,12 @@ func GetMockEventWithSubjectBytes(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) + endTime := common.FormatTime(at.Add(30 * time.Minute)) return GetMockEventWith( defaultEventOrganizer, subject, defaultEventBody, defaultEventBodyPreview, - atTime, atTime, false, + atTime, endTime, false, ) } diff --git a/src/pkg/backup/details/details.go b/src/pkg/backup/details/details.go index c245b6acf..bc3e96a83 100644 --- a/src/pkg/backup/details/details.go +++ b/src/pkg/backup/details/details.go @@ -298,6 +298,7 @@ type ExchangeInfo struct { Subject string `json:"subject,omitempty"` Received time.Time `json:"received,omitempty"` EventStart time.Time `json:"eventStart,omitempty"` + EventEnd time.Time `json:"eventEnd,omitempty"` Organizer string `json:"organizer,omitempty"` ContactName string `json:"contactName,omitempty"` EventRecurs bool `json:"eventRecurs,omitempty"` @@ -308,7 +309,7 @@ type ExchangeInfo struct { func (i ExchangeInfo) Headers() []string { switch i.ItemType { case ExchangeEvent: - return []string{"Organizer", "Subject", "Starts", "Recurring"} + return []string{"Organizer", "Subject", "Starts", "Ends", "Recurring"} case ExchangeContact: return []string{"Contact Name"} @@ -329,6 +330,7 @@ func (i ExchangeInfo) Values() []string { i.Organizer, i.Subject, common.FormatTabularDisplayTime(i.EventStart), + common.FormatTabularDisplayTime(i.EventEnd), strconv.FormatBool(i.EventRecurs), } diff --git a/src/pkg/backup/details/details_test.go b/src/pkg/backup/details/details_test.go index 9463c0053..a86a0e27e 100644 --- a/src/pkg/backup/details/details_test.go +++ b/src/pkg/backup/details/details_test.go @@ -52,14 +52,15 @@ func (suite *DetailsUnitSuite) TestDetailsEntry_HeadersValues() { Exchange: &details.ExchangeInfo{ ItemType: details.ExchangeEvent, EventStart: now, + EventEnd: now, Organizer: "organizer", EventRecurs: true, Subject: "subject", }, }, }, - expectHs: []string{"ID", "Organizer", "Subject", "Starts", "Recurring"}, - expectVs: []string{"deadbeef", "organizer", "subject", nowStr, "true"}, + expectHs: []string{"ID", "Organizer", "Subject", "Starts", "Ends", "Recurring"}, + expectVs: []string{"deadbeef", "organizer", "subject", nowStr, nowStr, "true"}, }, { name: "exchange contact info",