From f4847404c4d4cf339c7af86446e89649e6f05eea Mon Sep 17 00:00:00 2001 From: Keepers Date: Thu, 22 Jun 2023 14:54:06 -0600 Subject: [PATCH] add per-container item collision enumeration (#3629) populates the item collision cache generators for all exchange data types. Next we'll use these to compare the items being restored for collision before making post requests, by populating this cache at the top of each collection restore process. --- #### Does this PR need a docs update or release note? - [x] :no_entry: No #### Type of change - [x] :sunflower: Feature #### Issue(s) * #3562 #### Test Plan - [x] :zap: Unit test - [x] :green_heart: E2E --- src/internal/m365/onedrive/mock/handlers.go | 2 +- src/pkg/services/m365/api/config.go | 9 ++- src/pkg/services/m365/api/contacts.go | 14 ++++ src/pkg/services/m365/api/contacts_pager.go | 70 +++++++++++++---- .../services/m365/api/contacts_pager_test.go | 73 ++++++++++++++++++ src/pkg/services/m365/api/events.go | 14 ++++ src/pkg/services/m365/api/events_pager.go | 70 +++++++++++++---- .../services/m365/api/events_pager_test.go | 73 ++++++++++++++++++ src/pkg/services/m365/api/helper_test.go | 34 +++++++++ src/pkg/services/m365/api/item_pager.go | 13 +--- src/pkg/services/m365/api/item_pager_test.go | 42 ++++------- src/pkg/services/m365/api/mail.go | 14 ++++ src/pkg/services/m365/api/mail_pager.go | 75 ++++++++++++++----- src/pkg/services/m365/api/mail_pager_test.go | 73 ++++++++++++++++++ 14 files changed, 490 insertions(+), 86 deletions(-) create mode 100644 src/pkg/services/m365/api/contacts_pager_test.go create mode 100644 src/pkg/services/m365/api/events_pager_test.go create mode 100644 src/pkg/services/m365/api/helper_test.go create mode 100644 src/pkg/services/m365/api/mail_pager_test.go diff --git a/src/internal/m365/onedrive/mock/handlers.go b/src/internal/m365/onedrive/mock/handlers.go index 23ef8a4d5..83da2dee9 100644 --- a/src/internal/m365/onedrive/mock/handlers.go +++ b/src/internal/m365/onedrive/mock/handlers.go @@ -228,7 +228,7 @@ func (m GetsItemPermission) GetItemPermission( // --------------------------------------------------------------------------- // Restore Handler -// --------------------------------------------------------------------------- +// -------------------------------------------------------------------------- type RestoreHandler struct { ItemInfo details.ItemInfo diff --git a/src/pkg/services/m365/api/config.go b/src/pkg/services/m365/api/config.go index 1e3a1ce04..3f02505db 100644 --- a/src/pkg/services/m365/api/config.go +++ b/src/pkg/services/m365/api/config.go @@ -17,8 +17,15 @@ const ( // get easily misspelled. // eg: we don't need a const for "id" const ( - parentFolderID = "parentFolderId" + attendees = "attendees" + bccRecipients = "bccRecipients" + ccRecipients = "ccRecipients" + createdDateTime = "createdDateTime" displayName = "displayName" + givenName = "givenName" + parentFolderID = "parentFolderId" + surname = "surname" + toRecipients = "toRecipients" userPrincipalName = "userPrincipalName" ) diff --git a/src/pkg/services/m365/api/contacts.go b/src/pkg/services/m365/api/contacts.go index c253212cd..80f4d583e 100644 --- a/src/pkg/services/m365/api/contacts.go +++ b/src/pkg/services/m365/api/contacts.go @@ -265,3 +265,17 @@ func ContactInfo(contact models.Contactable) *details.ExchangeInfo { Modified: ptr.OrNow(contact.GetLastModifiedDateTime()), } } + +func contactCollisionKeyProps() []string { + return idAnd(givenName) +} + +// ContactCollisionKey constructs a key from the contactable's creation time and either displayName or given+surname. +// collision keys are used to identify duplicate item conflicts for handling advanced restoration config. +func ContactCollisionKey(item models.Contactable) string { + if item == nil { + return "" + } + + return ptr.Val(item.GetId()) +} diff --git a/src/pkg/services/m365/api/contacts_pager.go b/src/pkg/services/m365/api/contacts_pager.go index a57873fe8..0c607ad8c 100644 --- a/src/pkg/services/m365/api/contacts_pager.go +++ b/src/pkg/services/m365/api/contacts_pager.go @@ -90,32 +90,74 @@ func (c Contacts) EnumerateContainers( // item pager // --------------------------------------------------------------------------- -var _ itemPager[models.Contactable] = &contactsPager{} +var _ itemPager[models.Contactable] = &contactsPageCtrl{} -type contactsPager struct { - // TODO(rkeeprs) +type contactsPageCtrl struct { + gs graph.Servicer + builder *users.ItemContactFoldersItemContactsRequestBuilder + options *users.ItemContactFoldersItemContactsRequestBuilderGetRequestConfiguration } -func (c Contacts) NewContactsPager() itemPager[models.Contactable] { - // TODO(rkeepers) - return nil +func (c Contacts) NewContactsPager( + userID, containerID string, + selectProps ...string, +) itemPager[models.Contactable] { + options := &users.ItemContactFoldersItemContactsRequestBuilderGetRequestConfiguration{ + Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize)), + } + + if len(selectProps) > 0 { + options.QueryParameters = &users.ItemContactFoldersItemContactsRequestBuilderGetQueryParameters{ + Select: selectProps, + } + } + + builder := c.Stable. + Client(). + Users(). + ByUserId(userID). + ContactFolders(). + ByContactFolderId(containerID). + Contacts() + + return &contactsPageCtrl{c.Stable, builder, options} } //lint:ignore U1000 False Positive -func (p *contactsPager) getPage(ctx context.Context) (PageLinker, error) { - // TODO(rkeepers) - return nil, nil +func (p *contactsPageCtrl) getPage(ctx context.Context) (PageLinkValuer[models.Contactable], error) { + resp, err := p.builder.Get(ctx, p.options) + if err != nil { + return nil, graph.Stack(ctx, err) + } + + return EmptyDeltaLinker[models.Contactable]{PageLinkValuer: resp}, nil } //lint:ignore U1000 False Positive -func (p *contactsPager) setNext(nextLink string) { - // TODO(rkeepers) +func (p *contactsPageCtrl) setNext(nextLink string) { + p.builder = users.NewItemContactFoldersItemContactsRequestBuilder(nextLink, p.gs.Adapter()) } //lint:ignore U1000 False Positive -func (p *contactsPager) valuesIn(pl PageLinker) ([]models.Contactable, error) { - // TODO(rkeepers) - return nil, nil +func (c Contacts) GetItemsInContainerByCollisionKey( + ctx context.Context, + userID, containerID string, +) (map[string]string, error) { + ctx = clues.Add(ctx, "container_id", containerID) + pager := c.NewContactsPager(userID, containerID, contactCollisionKeyProps()...) + + items, err := enumerateItems(ctx, pager) + if err != nil { + return nil, graph.Wrap(ctx, err, "enumerating contacts") + } + + m := map[string]string{} + + for _, item := range items { + m[ContactCollisionKey(item)] = ptr.Val(item.GetId()) + } + + return m, nil } // --------------------------------------------------------------------------- diff --git a/src/pkg/services/m365/api/contacts_pager_test.go b/src/pkg/services/m365/api/contacts_pager_test.go new file mode 100644 index 000000000..d29be16c9 --- /dev/null +++ b/src/pkg/services/m365/api/contacts_pager_test.go @@ -0,0 +1,73 @@ +package api_test + +import ( + "testing" + + "github.com/alcionai/clues" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/src/internal/common/ptr" + "github.com/alcionai/corso/src/internal/tester" + "github.com/alcionai/corso/src/pkg/services/m365/api" +) + +type ContactsPagerIntgSuite struct { + tester.Suite + cts clientTesterSetup +} + +func TestContactsPagerIntgSuite(t *testing.T) { + suite.Run(t, &ContactsPagerIntgSuite{ + Suite: tester.NewIntegrationSuite( + t, + [][]string{tester.M365AcctCredEnvs}), + }) +} + +func (suite *ContactsPagerIntgSuite) SetupSuite() { + suite.cts = newClientTesterSetup(suite.T()) +} + +func (suite *ContactsPagerIntgSuite) TestGetItemsInContainerByCollisionKey() { + t := suite.T() + ac := suite.cts.ac.Contacts() + + ctx, flush := tester.NewContext(t) + defer flush() + + container, err := ac.GetContainerByID(ctx, suite.cts.userID, "contacts") + require.NoError(t, err, clues.ToCore(err)) + + conts, err := ac.Stable. + Client(). + Users(). + ByUserId(suite.cts.userID). + ContactFolders(). + ByContactFolderId(ptr.Val(container.GetId())). + Contacts(). + Get(ctx, nil) + require.NoError(t, err, clues.ToCore(err)) + + cs := conts.GetValue() + expect := make([]string, 0, len(cs)) + + for _, c := range cs { + expect = append(expect, api.ContactCollisionKey(c)) + } + + results, err := ac.GetItemsInContainerByCollisionKey(ctx, suite.cts.userID, "contacts") + require.NoError(t, err, clues.ToCore(err)) + require.Less(t, 0, len(results), "requires at least one result") + + for k, v := range results { + assert.NotEmpty(t, k, "all keys should be populated") + assert.NotEmpty(t, v, "all values should be populated") + } + + for _, e := range expect { + _, ok := results[e] + assert.Truef(t, ok, "expected results to contain collision key: %s", e) + } +} diff --git a/src/pkg/services/m365/api/events.go b/src/pkg/services/m365/api/events.go index 5882df306..c57d4c078 100644 --- a/src/pkg/services/m365/api/events.go +++ b/src/pkg/services/m365/api/events.go @@ -698,3 +698,17 @@ func EventFromMap(ev map[string]any) (models.Eventable, error) { return body, nil } + +func eventCollisionKeyProps() []string { + return idAnd("subject") +} + +// EventCollisionKey constructs a key from the eventable's creation time, subject, and organizer. +// collision keys are used to identify duplicate item conflicts for handling advanced restoration config. +func EventCollisionKey(item models.Eventable) string { + if item == nil { + return "" + } + + return ptr.Val(item.GetSubject()) +} diff --git a/src/pkg/services/m365/api/events_pager.go b/src/pkg/services/m365/api/events_pager.go index fb0354cde..6b09bfa35 100644 --- a/src/pkg/services/m365/api/events_pager.go +++ b/src/pkg/services/m365/api/events_pager.go @@ -98,32 +98,74 @@ func (c Events) EnumerateContainers( // item pager // --------------------------------------------------------------------------- -var _ itemPager[models.Eventable] = &eventsPager{} +var _ itemPager[models.Eventable] = &eventsPageCtrl{} -type eventsPager struct { - // TODO(rkeeprs) +type eventsPageCtrl struct { + gs graph.Servicer + builder *users.ItemCalendarsItemEventsRequestBuilder + options *users.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration } -func (c Events) NewEventsPager() itemPager[models.Eventable] { - // TODO(rkeepers) - return nil +func (c Events) NewEventsPager( + userID, containerID string, + selectProps ...string, +) itemPager[models.Eventable] { + options := &users.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration{ + Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize)), + } + + if len(selectProps) > 0 { + options.QueryParameters = &users.ItemCalendarsItemEventsRequestBuilderGetQueryParameters{ + Select: selectProps, + } + } + + builder := c.Stable. + Client(). + Users(). + ByUserId(userID). + Calendars(). + ByCalendarId(containerID). + Events() + + return &eventsPageCtrl{c.Stable, builder, options} } //lint:ignore U1000 False Positive -func (p *eventsPager) getPage(ctx context.Context) (PageLinker, error) { - // TODO(rkeepers) - return nil, nil +func (p *eventsPageCtrl) getPage(ctx context.Context) (PageLinkValuer[models.Eventable], error) { + resp, err := p.builder.Get(ctx, p.options) + if err != nil { + return nil, graph.Stack(ctx, err) + } + + return resp, nil } //lint:ignore U1000 False Positive -func (p *eventsPager) setNext(nextLink string) { - // TODO(rkeepers) +func (p *eventsPageCtrl) setNext(nextLink string) { + p.builder = users.NewItemCalendarsItemEventsRequestBuilder(nextLink, p.gs.Adapter()) } //lint:ignore U1000 False Positive -func (p *eventsPager) valuesIn(pl PageLinker) ([]models.Eventable, error) { - // TODO(rkeepers) - return nil, nil +func (c Events) GetItemsInContainerByCollisionKey( + ctx context.Context, + userID, containerID string, +) (map[string]string, error) { + ctx = clues.Add(ctx, "container_id", containerID) + pager := c.NewEventsPager(userID, containerID, eventCollisionKeyProps()...) + + items, err := enumerateItems(ctx, pager) + if err != nil { + return nil, graph.Wrap(ctx, err, "enumerating events") + } + + m := map[string]string{} + + for _, item := range items { + m[EventCollisionKey(item)] = ptr.Val(item.GetId()) + } + + return m, nil } // --------------------------------------------------------------------------- diff --git a/src/pkg/services/m365/api/events_pager_test.go b/src/pkg/services/m365/api/events_pager_test.go new file mode 100644 index 000000000..e95f933d8 --- /dev/null +++ b/src/pkg/services/m365/api/events_pager_test.go @@ -0,0 +1,73 @@ +package api_test + +import ( + "testing" + + "github.com/alcionai/clues" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/src/internal/common/ptr" + "github.com/alcionai/corso/src/internal/tester" + "github.com/alcionai/corso/src/pkg/services/m365/api" +) + +type EventsPagerIntgSuite struct { + tester.Suite + cts clientTesterSetup +} + +func TestEventsPagerIntgSuite(t *testing.T) { + suite.Run(t, &EventsPagerIntgSuite{ + Suite: tester.NewIntegrationSuite( + t, + [][]string{tester.M365AcctCredEnvs}), + }) +} + +func (suite *EventsPagerIntgSuite) SetupSuite() { + suite.cts = newClientTesterSetup(suite.T()) +} + +func (suite *EventsPagerIntgSuite) TestGetItemsInContainerByCollisionKey() { + t := suite.T() + ac := suite.cts.ac.Events() + + ctx, flush := tester.NewContext(t) + defer flush() + + container, err := ac.GetContainerByID(ctx, suite.cts.userID, "calendar") + require.NoError(t, err, clues.ToCore(err)) + + evts, err := ac.Stable. + Client(). + Users(). + ByUserId(suite.cts.userID). + Calendars(). + ByCalendarId(ptr.Val(container.GetId())). + Events(). + Get(ctx, nil) + require.NoError(t, err, clues.ToCore(err)) + + es := evts.GetValue() + expect := make([]string, 0, len(es)) + + for _, e := range es { + expect = append(expect, api.EventCollisionKey(e)) + } + + results, err := ac.GetItemsInContainerByCollisionKey(ctx, suite.cts.userID, "calendar") + require.NoError(t, err, clues.ToCore(err)) + require.Less(t, 0, len(results), "requires at least one result") + + for k, v := range results { + assert.NotEmpty(t, k, "all keys should be populated") + assert.NotEmpty(t, v, "all values should be populated") + } + + for _, e := range expect { + _, ok := results[e] + assert.Truef(t, ok, "expected results to contain collision key: %s", e) + } +} diff --git a/src/pkg/services/m365/api/helper_test.go b/src/pkg/services/m365/api/helper_test.go new file mode 100644 index 000000000..0d82db8be --- /dev/null +++ b/src/pkg/services/m365/api/helper_test.go @@ -0,0 +1,34 @@ +package api_test + +import ( + "testing" + + "github.com/alcionai/clues" + "github.com/stretchr/testify/require" + + "github.com/alcionai/corso/src/internal/tester" + "github.com/alcionai/corso/src/pkg/services/m365/api" +) + +type clientTesterSetup struct { + ac api.Client + userID string +} + +func newClientTesterSetup(t *testing.T) clientTesterSetup { + cts := clientTesterSetup{} + + ctx, flush := tester.NewContext(t) + defer flush() + + a := tester.NewM365Account(t) + creds, err := a.M365Config() + require.NoError(t, err, clues.ToCore(err)) + + cts.ac, err = api.NewClient(creds) + require.NoError(t, err, clues.ToCore(err)) + + cts.userID = tester.GetM365UserID(ctx) + + return cts +} diff --git a/src/pkg/services/m365/api/item_pager.go b/src/pkg/services/m365/api/item_pager.go index fca130c56..ef54b1a3d 100644 --- a/src/pkg/services/m365/api/item_pager.go +++ b/src/pkg/services/m365/api/item_pager.go @@ -66,11 +66,9 @@ func (e EmptyDeltaLinker[T]) GetValue() []T { type itemPager[T any] interface { // getPage get a page with the specified options from graph - getPage(context.Context) (PageLinker, error) + getPage(context.Context) (PageLinkValuer[T], error) // setNext is used to pass in the next url got from graph setNext(string) - // valuesIn gets us the values in a page - valuesIn(PageLinker) ([]T, error) } func enumerateItems[T any]( @@ -90,14 +88,7 @@ func enumerateItems[T any]( return nil, graph.Stack(ctx, err) } - // each category type responds with a different interface, but all - // of them comply with GetValue, which is where we'll get our item data. - items, err := pager.valuesIn(resp) - if err != nil { - return nil, graph.Stack(ctx, err) - } - - result = append(result, items...) + result = append(result, resp.GetValue()...) nextLink = NextLink(resp) pager.setNext(nextLink) diff --git a/src/pkg/services/m365/api/item_pager_test.go b/src/pkg/services/m365/api/item_pager_test.go index 0f935e46a..0ba312b51 100644 --- a/src/pkg/services/m365/api/item_pager_test.go +++ b/src/pkg/services/m365/api/item_pager_test.go @@ -57,7 +57,9 @@ func (v testPagerValue) GetAdditionalData() map[string]any { // mock page -type testPage struct{} +type testPage struct { + values []any +} func (p testPage) GetOdataNextLink() *string { // no next, just one page @@ -69,30 +71,28 @@ func (p testPage) GetOdataDeltaLink() *string { return ptr.To("") } +func (p testPage) GetValue() []any { + return p.values +} + // mock item pager var _ itemPager[any] = &testPager{} type testPager struct { - t *testing.T - items []any - pageErr error - valuesErr error + t *testing.T + pager testPage + pageErr error } //lint:ignore U1000 False Positive -func (p *testPager) getPage(ctx context.Context) (PageLinker, error) { - return testPage{}, p.pageErr +func (p *testPager) getPage(ctx context.Context) (PageLinkValuer[any], error) { + return p.pager, p.pageErr } //lint:ignore U1000 False Positive func (p *testPager) setNext(nextLink string) {} -//lint:ignore U1000 False Positive -func (p *testPager) valuesIn(pl PageLinker) ([]any, error) { - return p.items, p.valuesErr -} - // mock id pager var _ itemIDPager = &testIDsPager{} @@ -169,26 +169,12 @@ func (suite *ItemPagerUnitSuite) TestEnumerateItems() { ) itemPager[any] { return &testPager{ t: t, - items: []any{"foo", "bar"}, + pager: testPage{[]any{"foo", "bar"}}, } }, expect: []any{"foo", "bar"}, expectErr: require.NoError, }, - { - name: "get values err", - getPager: func( - t *testing.T, - ctx context.Context, - ) itemPager[any] { - return &testPager{ - t: t, - valuesErr: assert.AnError, - } - }, - expect: nil, - expectErr: require.Error, - }, { name: "next page err", getPager: func( @@ -212,7 +198,7 @@ func (suite *ItemPagerUnitSuite) TestEnumerateItems() { ctx, flush := tester.NewContext(t) defer flush() - result, err := enumerateItems[any](ctx, test.getPager(t, ctx)) + result, err := enumerateItems(ctx, test.getPager(t, ctx)) test.expectErr(t, err, clues.ToCore(err)) require.EqualValues(t, test.expect, result) diff --git a/src/pkg/services/m365/api/mail.go b/src/pkg/services/m365/api/mail.go index 8073a6659..ab371074b 100644 --- a/src/pkg/services/m365/api/mail.go +++ b/src/pkg/services/m365/api/mail.go @@ -540,3 +540,17 @@ func UnwrapEmailAddress(contact models.Recipientable) string { return ptr.Val(contact.GetEmailAddress().GetAddress()) } + +func mailCollisionKeyProps() []string { + return idAnd("subject") +} + +// MailCollisionKey constructs a key from the messageable's subject, sender, and recipients (to, cc, bcc). +// collision keys are used to identify duplicate item conflicts for handling advanced restoration config. +func MailCollisionKey(item models.Messageable) string { + if item == nil { + return "" + } + + return ptr.Val(item.GetSubject()) +} diff --git a/src/pkg/services/m365/api/mail_pager.go b/src/pkg/services/m365/api/mail_pager.go index 9dc2c470c..6b8033c94 100644 --- a/src/pkg/services/m365/api/mail_pager.go +++ b/src/pkg/services/m365/api/mail_pager.go @@ -121,32 +121,52 @@ func (c Mail) EnumerateContainers( // item pager // --------------------------------------------------------------------------- -var _ itemPager[models.Messageable] = &mailPager{} +var _ itemPager[models.Messageable] = &mailPageCtrl{} -type mailPager struct { - // TODO(rkeeprs) +type mailPageCtrl struct { + gs graph.Servicer + builder *users.ItemMailFoldersItemMessagesRequestBuilder + options *users.ItemMailFoldersItemMessagesRequestBuilderGetRequestConfiguration } -func (c Mail) NewMailPager() itemPager[models.Messageable] { - // TODO(rkeepers) - return nil +func (c Mail) NewMailPager( + userID, containerID string, + selectProps ...string, +) itemPager[models.Messageable] { + options := &users.ItemMailFoldersItemMessagesRequestBuilderGetRequestConfiguration{ + Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize)), + } + + if len(selectProps) > 0 { + options.QueryParameters = &users.ItemMailFoldersItemMessagesRequestBuilderGetQueryParameters{ + Select: selectProps, + } + } + + builder := c.Stable. + Client(). + Users(). + ByUserId(userID). + MailFolders(). + ByMailFolderId(containerID). + Messages() + + return &mailPageCtrl{c.Stable, builder, options} } //lint:ignore U1000 False Positive -func (p *mailPager) getPage(ctx context.Context) (PageLinker, error) { - // TODO(rkeepers) - return nil, nil +func (p *mailPageCtrl) getPage(ctx context.Context) (PageLinkValuer[models.Messageable], error) { + page, err := p.builder.Get(ctx, p.options) + if err != nil { + return nil, graph.Stack(ctx, err) + } + + return EmptyDeltaLinker[models.Messageable]{PageLinkValuer: page}, nil } //lint:ignore U1000 False Positive -func (p *mailPager) setNext(nextLink string) { - // TODO(rkeepers) -} - -//lint:ignore U1000 False Positive -func (p *mailPager) valuesIn(pl PageLinker) ([]models.Messageable, error) { - // TODO(rkeepers) - return nil, nil +func (p *mailPageCtrl) setNext(nextLink string) { + p.builder = users.NewItemMailFoldersItemMessagesRequestBuilder(nextLink, p.gs.Adapter()) } // --------------------------------------------------------------------------- @@ -204,6 +224,27 @@ func (p *mailIDPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { return toValues[models.Messageable](pl) } +func (c Mail) GetItemsInContainerByCollisionKey( + ctx context.Context, + userID, containerID string, +) (map[string]string, error) { + ctx = clues.Add(ctx, "container_id", containerID) + pager := c.NewMailPager(userID, containerID, mailCollisionKeyProps()...) + + items, err := enumerateItems(ctx, pager) + if err != nil { + return nil, graph.Wrap(ctx, err, "enumerating mail") + } + + m := map[string]string{} + + for _, item := range items { + m[MailCollisionKey(item)] = ptr.Val(item.GetId()) + } + + return m, nil +} + // --------------------------------------------------------------------------- // delta item ID pager // --------------------------------------------------------------------------- diff --git a/src/pkg/services/m365/api/mail_pager_test.go b/src/pkg/services/m365/api/mail_pager_test.go new file mode 100644 index 000000000..0fde70163 --- /dev/null +++ b/src/pkg/services/m365/api/mail_pager_test.go @@ -0,0 +1,73 @@ +package api_test + +import ( + "testing" + + "github.com/alcionai/clues" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/src/internal/common/ptr" + "github.com/alcionai/corso/src/internal/tester" + "github.com/alcionai/corso/src/pkg/services/m365/api" +) + +type MailPagerIntgSuite struct { + tester.Suite + cts clientTesterSetup +} + +func TestMailPagerIntgSuite(t *testing.T) { + suite.Run(t, &MailPagerIntgSuite{ + Suite: tester.NewIntegrationSuite( + t, + [][]string{tester.M365AcctCredEnvs}), + }) +} + +func (suite *MailPagerIntgSuite) SetupSuite() { + suite.cts = newClientTesterSetup(suite.T()) +} + +func (suite *MailPagerIntgSuite) TestGetItemsInContainerByCollisionKey() { + t := suite.T() + ac := suite.cts.ac.Mail() + + ctx, flush := tester.NewContext(t) + defer flush() + + container, err := ac.GetContainerByID(ctx, suite.cts.userID, "inbox") + require.NoError(t, err, clues.ToCore(err)) + + msgs, err := ac.Stable. + Client(). + Users(). + ByUserId(suite.cts.userID). + MailFolders(). + ByMailFolderId(ptr.Val(container.GetId())). + Messages(). + Get(ctx, nil) + require.NoError(t, err, clues.ToCore(err)) + + ms := msgs.GetValue() + expect := make([]string, 0, len(ms)) + + for _, m := range ms { + expect = append(expect, api.MailCollisionKey(m)) + } + + results, err := ac.GetItemsInContainerByCollisionKey(ctx, suite.cts.userID, "inbox") + require.NoError(t, err, clues.ToCore(err)) + require.Less(t, 0, len(results), "requires at least one result") + + for k, v := range results { + assert.NotEmpty(t, k, "all keys should be populated") + assert.NotEmpty(t, v, "all values should be populated") + } + + for _, e := range expect { + _, ok := results[e] + assert.Truef(t, ok, "expected results to contain collision key: %s", e) + } +}