use delta queries for calendar events (#2154)

## Description

Hacks the beta version call into the events api
when iterating through items, allowing us to run
delta-based queries for calendar events.

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

- [x]  No 

## Type of change

- [x] 🌻 Feature

## Issue(s)

* #2022

## Test Plan

- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Keepers 2023-01-18 18:30:56 -07:00 committed by GitHub
parent 81625532bb
commit 51e29f2975
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 120 additions and 36 deletions

View File

@ -68,7 +68,7 @@ func (suite *ConnectorDataCollectionIntegrationSuite) TestExchangeDataCollection
getSelector func(t *testing.T) selectors.Selector getSelector func(t *testing.T) selectors.Selector
}{ }{
{ {
name: suite.user + " Email", name: "Email",
getSelector: func(t *testing.T) selectors.Selector { getSelector: func(t *testing.T) selectors.Selector {
sel := selectors.NewExchangeBackup(selUsers) sel := selectors.NewExchangeBackup(selUsers)
sel.Include(sel.MailFolders([]string{exchange.DefaultMailFolder}, selectors.PrefixMatch())) sel.Include(sel.MailFolders([]string{exchange.DefaultMailFolder}, selectors.PrefixMatch()))
@ -77,7 +77,7 @@ func (suite *ConnectorDataCollectionIntegrationSuite) TestExchangeDataCollection
}, },
}, },
{ {
name: suite.user + " Contacts", name: "Contacts",
getSelector: func(t *testing.T) selectors.Selector { getSelector: func(t *testing.T) selectors.Selector {
sel := selectors.NewExchangeBackup(selUsers) sel := selectors.NewExchangeBackup(selUsers)
sel.Include(sel.ContactFolders([]string{exchange.DefaultContactFolder}, selectors.PrefixMatch())) sel.Include(sel.ContactFolders([]string{exchange.DefaultContactFolder}, selectors.PrefixMatch()))
@ -86,7 +86,7 @@ func (suite *ConnectorDataCollectionIntegrationSuite) TestExchangeDataCollection
}, },
}, },
// { // {
// name: suite.user + " Events", // name: "Events",
// getSelector: func(t *testing.T) selectors.Selector { // getSelector: func(t *testing.T) selectors.Selector {
// sel := selectors.NewExchangeBackup(selUsers) // sel := selectors.NewExchangeBackup(selUsers)
// sel.Include(sel.EventCalendars([]string{exchange.DefaultCalendar}, selectors.PrefixMatch())) // sel.Include(sel.EventCalendars([]string{exchange.DefaultCalendar}, selectors.PrefixMatch()))

View File

@ -144,29 +144,25 @@ func (c Events) EnumerateContainers(
// item pager // item pager
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
type eventWrapper struct {
models.EventCollectionResponseable
}
func (ew eventWrapper) GetOdataDeltaLink() *string {
return nil
}
var _ itemPager = &eventPager{} var _ itemPager = &eventPager{}
const (
eventBetaDeltaURLTemplate = "https://graph.microsoft.com/beta/users/%s/calendars/%s/events/delta"
)
type eventPager struct { type eventPager struct {
gs graph.Servicer gs graph.Servicer
builder *users.ItemCalendarsItemEventsRequestBuilder builder *users.ItemCalendarsItemEventsDeltaRequestBuilder
options *users.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration options *users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration
} }
func (p *eventPager) getPage(ctx context.Context) (pageLinker, error) { func (p *eventPager) getPage(ctx context.Context) (pageLinker, error) {
resp, err := p.builder.Get(ctx, p.options) resp, err := p.builder.Get(ctx, p.options)
return eventWrapper{resp}, err return resp, err
} }
func (p *eventPager) setNext(nextLink string) { func (p *eventPager) setNext(nextLink string) {
p.builder = users.NewItemCalendarsItemEventsRequestBuilder(nextLink, p.gs.Adapter()) p.builder = users.NewItemCalendarsItemEventsDeltaRequestBuilder(nextLink, p.gs.Adapter())
} }
func (p *eventPager) valuesIn(pl pageLinker) ([]getIDAndAddtler, error) { func (p *eventPager) valuesIn(pl pageLinker) ([]getIDAndAddtler, error) {
@ -182,23 +178,54 @@ func (c Events) GetAddedAndRemovedItemIDs(
return nil, nil, DeltaUpdate{}, err return nil, nil, DeltaUpdate{}, err
} }
var errs *multierror.Error var (
resetDelta bool
errs *multierror.Error
)
options, err := optionsForEventsByCalendar([]string{"id"}) options, err := optionsForEventsByCalendarDelta([]string{"id"})
if err != nil { if err != nil {
return nil, nil, DeltaUpdate{}, err return nil, nil, DeltaUpdate{}, err
} }
builder := service.Client().UsersById(user).CalendarsById(calendarID).Events() if len(oldDelta) > 0 {
builder := users.NewItemCalendarsItemEventsDeltaRequestBuilder(oldDelta, service.Adapter())
pgr := &eventPager{service, builder, options}
added, removed, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr)
// note: happy path, not the error condition
if err == nil {
return added, removed, DeltaUpdate{deltaURL, false}, errs.ErrorOrNil()
}
// only return on error if it is NOT a delta issue.
// on bad deltas we retry the call with the regular builder
if graph.IsErrInvalidDelta(err) == nil {
return nil, nil, DeltaUpdate{}, err
}
resetDelta = true
errs = nil
}
// Graph SDK only supports delta queries against events on the beta version, so we're
// manufacturing use of the beta version url to make the call instead.
// See: https://learn.microsoft.com/ko-kr/graph/api/event-delta?view=graph-rest-beta&tabs=http
// Note that the delta item body is skeletal compared to the actual event struct. Lucky
// for us, we only need the item ID. As a result, even though we hacked the version, the
// response body parses properly into the v1.0 structs and complies with our wanted interfaces.
// Likewise, the NextLink and DeltaLink odata tags carry our hack forward, so the rest of the code
// works as intended (until, at least, we want to _not_ call the beta anymore).
rawURL := fmt.Sprintf(eventBetaDeltaURLTemplate, user, calendarID)
builder := users.NewItemCalendarsItemEventsDeltaRequestBuilder(rawURL, service.Adapter())
pgr := &eventPager{service, builder, options} pgr := &eventPager{service, builder, options}
added, _, _, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) added, removed, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr)
if err != nil { if err != nil {
return nil, nil, DeltaUpdate{}, err return nil, nil, DeltaUpdate{}, err
} }
// Events don't have a delta endpoint so just return an empty string. // Events don't have a delta endpoint so just return an empty string.
return added, nil, DeltaUpdate{}, errs.ErrorOrNil() return added, removed, DeltaUpdate{deltaURL, resetDelta}, errs.ErrorOrNil()
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -214,19 +214,19 @@ func optionsForContactFoldersItemDelta(
} }
// optionsForEvents ensures a valid option inputs for `exchange.Events` when selected from within a Calendar // optionsForEvents ensures a valid option inputs for `exchange.Events` when selected from within a Calendar
func optionsForEventsByCalendar( func optionsForEventsByCalendarDelta(
moreOps []string, moreOps []string,
) (*users.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration, error) { ) (*users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration, error) {
selecting, err := buildOptions(moreOps, fieldsForEvents) selecting, err := buildOptions(moreOps, fieldsForEvents)
if err != nil { if err != nil {
return nil, err return nil, err
} }
requestParameters := &users.ItemCalendarsItemEventsRequestBuilderGetQueryParameters{ requestParameters := &users.ItemCalendarsItemEventsDeltaRequestBuilderGetQueryParameters{
Select: selecting, Select: selecting,
} }
options := &users.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration{ options := &users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration{
QueryParameters: requestParameters, QueryParameters: requestParameters,
} }

View File

@ -313,6 +313,13 @@ func (suite *DataCollectionsIntegrationSuite) TestDelta() {
selectors.PrefixMatch(), selectors.PrefixMatch(),
)[0], )[0],
}, },
{
name: "Events",
scope: selectors.NewExchangeBackup(users).EventCalendars(
[]string{DefaultCalendar},
selectors.PrefixMatch(),
)[0],
},
} }
for _, test := range tests { for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {

View File

@ -7,7 +7,7 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
msuser "github.com/microsoftgraph/msgraph-sdk-go/users" "github.com/microsoftgraph/msgraph-sdk-go/users"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -507,7 +507,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchange() {
ctx, flush := tester.NewContext() ctx, flush := tester.NewContext()
defer flush() defer flush()
users := []string{suite.user} owners := []string{suite.user}
tests := []struct { tests := []struct {
name string name string
@ -520,7 +520,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchange() {
{ {
name: "Mail", name: "Mail",
selector: func() *selectors.ExchangeBackup { selector: func() *selectors.ExchangeBackup {
sel := selectors.NewExchangeBackup(users) sel := selectors.NewExchangeBackup(owners)
sel.Include(sel.MailFolders([]string{exchange.DefaultMailFolder}, selectors.PrefixMatch())) sel.Include(sel.MailFolders([]string{exchange.DefaultMailFolder}, selectors.PrefixMatch()))
sel.DiscreteOwner = suite.user sel.DiscreteOwner = suite.user
@ -534,7 +534,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchange() {
{ {
name: "Contacts", name: "Contacts",
selector: func() *selectors.ExchangeBackup { selector: func() *selectors.ExchangeBackup {
sel := selectors.NewExchangeBackup(users) sel := selectors.NewExchangeBackup(owners)
sel.Include(sel.ContactFolders([]string{exchange.DefaultContactFolder}, selectors.PrefixMatch())) sel.Include(sel.ContactFolders([]string{exchange.DefaultContactFolder}, selectors.PrefixMatch()))
return sel return sel
}, },
@ -546,7 +546,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchange() {
{ {
name: "Calendar Events", name: "Calendar Events",
selector: func() *selectors.ExchangeBackup { selector: func() *selectors.ExchangeBackup {
sel := selectors.NewExchangeBackup(users) sel := selectors.NewExchangeBackup(owners)
sel.Include(sel.EventCalendars([]string{exchange.DefaultCalendar}, selectors.PrefixMatch())) sel.Include(sel.EventCalendars([]string{exchange.DefaultCalendar}, selectors.PrefixMatch()))
return sel return sel
}, },
@ -639,10 +639,12 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {
ffs = control.Toggles{} ffs = control.Toggles{}
mb = evmock.NewBus() mb = evmock.NewBus()
now = common.Now() now = common.Now()
users = []string{suite.user} owners = []string{suite.user}
categories = map[path.CategoryType][]string{ categories = map[path.CategoryType][]string{
path.EmailCategory: exchange.MetadataFileNames(path.EmailCategory), path.EmailCategory: exchange.MetadataFileNames(path.EmailCategory),
path.ContactsCategory: exchange.MetadataFileNames(path.ContactsCategory), path.ContactsCategory: exchange.MetadataFileNames(path.ContactsCategory),
// TODO: not currently functioning; cannot retrieve generated calendars
// path.EventsCategory: exchange.MetadataFileNames(path.EventsCategory),
} }
container1 = fmt.Sprintf("%s%d_%s", incrementalsDestContainerPrefix, 1, now) container1 = fmt.Sprintf("%s%d_%s", incrementalsDestContainerPrefix, 1, now)
container2 = fmt.Sprintf("%s%d_%s", incrementalsDestContainerPrefix, 2, now) container2 = fmt.Sprintf("%s%d_%s", incrementalsDestContainerPrefix, 2, now)
@ -688,6 +690,13 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {
) )
} }
eventDBF := func(id, timeStamp, subject, body string) []byte {
return mockconnector.GetMockEventWith(
suite.user, subject, body, body,
now, now, false)
}
// test data set
dataset := map[path.CategoryType]struct { dataset := map[path.CategoryType]struct {
dbf dataBuilderFunc dbf dataBuilderFunc
dests map[string]contDeets dests map[string]contDeets
@ -706,8 +715,17 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {
container2: {}, container2: {},
}, },
}, },
// TODO: not currently functioning; cannot retrieve generated calendars
// path.EventsCategory: {
// dbf: eventDBF,
// dests: map[string]contDeets{
// container1: {},
// container2: {},
// },
// },
} }
// populate initial test data
for category, gen := range dataset { for category, gen := range dataset {
for destName := range gen.dests { for destName := range gen.dests {
deets := generateContainerOfItems( deets := generateContainerOfItems(
@ -717,7 +735,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {
path.ExchangeService, path.ExchangeService,
acct, acct,
category, category,
selectors.NewExchangeRestore(users).Selector, selectors.NewExchangeRestore(owners).Selector,
m365.AzureTenantID, suite.user, destName, m365.AzureTenantID, suite.user, destName,
2, 2,
gen.dbf) gen.dbf)
@ -726,6 +744,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {
} }
} }
// verify test data was populated, and track it for comparisons
for category, gen := range dataset { for category, gen := range dataset {
qp := graph.QueryParams{ qp := graph.QueryParams{
Category: category, Category: category,
@ -752,7 +771,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {
// later on during the tests. Putting their identifiers into the selector // later on during the tests. Putting their identifiers into the selector
// at this point is harmless. // at this point is harmless.
containers := []string{container1, container2, container3, containerRename} containers := []string{container1, container2, container3, containerRename}
sel := selectors.NewExchangeBackup(users) sel := selectors.NewExchangeBackup(owners)
sel.Include( sel.Include(
sel.MailFolders(containers, selectors.PrefixMatch()), sel.MailFolders(containers, selectors.PrefixMatch()),
sel.ContactFolders(containers, selectors.PrefixMatch()), sel.ContactFolders(containers, selectors.PrefixMatch()),
@ -788,7 +807,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {
toContainer := dataset[path.EmailCategory].dests[container1].containerID toContainer := dataset[path.EmailCategory].dests[container1].containerID
fromContainer := dataset[path.EmailCategory].dests[container2].containerID fromContainer := dataset[path.EmailCategory].dests[container2].containerID
body := msuser.NewItemMailFoldersItemMovePostRequestBody() body := users.NewItemMailFoldersItemMovePostRequestBody()
body.SetDestinationId(&toContainer) body.SetDestinationId(&toContainer)
_, err := gc.Service. _, err := gc.Service.
@ -820,6 +839,11 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {
t, t,
cli.ContactFoldersById(containerID).Delete(ctx, nil), cli.ContactFoldersById(containerID).Delete(ctx, nil),
"deleting a contacts folder") "deleting a contacts folder")
case path.EventsCategory:
require.NoError(
t,
cli.CalendarsById(containerID).Delete(ctx, nil),
"deleting a calendar")
} }
} }
}, },
@ -837,7 +861,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {
path.ExchangeService, path.ExchangeService,
acct, acct,
category, category,
selectors.NewExchangeRestore(users).Selector, selectors.NewExchangeRestore(owners).Selector,
m365.AzureTenantID, suite.user, container3, m365.AzureTenantID, suite.user, container3,
2, 2,
gen.dbf) gen.dbf)
@ -897,6 +921,16 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {
body.SetDisplayName(&containerRename) body.SetDisplayName(&containerRename)
_, err = ccf.Patch(ctx, body, nil) _, err = ccf.Patch(ctx, body, nil)
require.NoError(t, err, "updating contact folder name") require.NoError(t, err, "updating contact folder name")
case path.EventsCategory:
ccf := cli.CalendarsById(containerID)
body, err := ccf.Get(ctx, nil)
require.NoError(t, err, "getting calendar")
body.SetName(&containerRename)
_, err = ccf.Patch(ctx, body, nil)
require.NoError(t, err, "updating calendar name")
} }
} }
}, },
@ -926,6 +960,14 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {
_, err = cli.ContactFoldersById(containerID).Contacts().Post(ctx, body, nil) _, err = cli.ContactFoldersById(containerID).Contacts().Post(ctx, body, nil)
require.NoError(t, err, "posting contact item") require.NoError(t, err, "posting contact item")
case path.EventsCategory:
_, itemData := generateItemData(t, category, suite.user, eventDBF)
body, err := support.CreateEventFromBytes(itemData)
require.NoError(t, err, "transforming event bytes to eventable")
_, err = cli.CalendarsById(containerID).Events().Post(ctx, body, nil)
require.NoError(t, err, "posting events item")
} }
} }
}, },
@ -955,6 +997,14 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {
err = cli.ContactsById(ids[0]).Delete(ctx, nil) err = cli.ContactsById(ids[0]).Delete(ctx, nil)
require.NoError(t, err, "deleting contact item: %s", support.ConnectorStackErrorTrace(err)) require.NoError(t, err, "deleting contact item: %s", support.ConnectorStackErrorTrace(err))
case path.EventsCategory:
ids, _, _, err := ac.Events().GetAddedAndRemovedItemIDs(ctx, suite.user, containerID, "")
require.NoError(t, err, "getting event ids")
require.NotEmpty(t, ids, "event ids in folder")
err = cli.CalendarsById(ids[0]).Delete(ctx, nil)
require.NoError(t, err, "deleting calendar: %s", support.ConnectorStackErrorTrace(err))
} }
} }
}, },

View File

@ -198,9 +198,9 @@ func (suite *RestoreOpIntegrationSuite) SetupSuite() {
require.NotEmpty(t, bo.Results.BackupID) require.NotEmpty(t, bo.Results.BackupID)
suite.backupID = bo.Results.BackupID suite.backupID = bo.Results.BackupID
// Discount metadata files (3 paths, 2 deltas) as // Discount metadata files (3 paths, 3 deltas) as
// they are not part of the data restored. // they are not part of the data restored.
suite.numItems = bo.Results.ItemsWritten - 5 suite.numItems = bo.Results.ItemsWritten - 6
} }
func (suite *RestoreOpIntegrationSuite) TearDownSuite() { func (suite *RestoreOpIntegrationSuite) TearDownSuite() {