From 874e3c3a50f23d7bad33e9d50d560932a5376269 Mon Sep 17 00:00:00 2001 From: Keepers Date: Thu, 25 May 2023 17:53:11 -0600 Subject: [PATCH] cleanup exchange api code, move out of support (#3455) normalizes exchange api naming and client usage. Also moves serialization and transformation to and from graph client objects out of graph/support and into the api. Some parts of graph/support have been moved into conn/ exchange instead, since they're more oriented around certain forms of object consumption rather than broad object serialization. Except for different uses of api client graph servicers, all changes are movement/renaming. --- #### Does this PR need a docs update or release note? - [x] :no_entry: No #### Type of change - [x] :broom: Tech Debt/Cleanup #### Issue(s) * #1996 #### Test Plan - [x] :zap: Unit test - [x] :green_heart: E2E --- src/internal/connector/exchange/attachment.go | 3 +- .../attendee.go => exchange/attendees.go} | 2 +- .../connector/exchange/data_collections.go | 6 +- .../exchange/data_collections_test.go | 6 +- .../connector/exchange/iterators_test.go | 6 +- .../connector/exchange/mock/mock_test.go | 15 +- .../connector/exchange/service_restore.go | 22 +- .../transform.go} | 171 +------------ .../transform_test.go} | 33 +-- .../connector/graph_connector_helper_test.go | 13 +- .../connector/sharepoint/api/pages.go | 3 +- .../connector/sharepoint/api/serialization.go | 212 +++++++++++++++ .../sharepoint/api/serialization_test.go | 126 +++++++++ .../connector/sharepoint/collection_test.go | 3 +- .../connector/sharepoint/mock/mock_test.go | 8 +- src/internal/connector/sharepoint/restore.go | 6 +- src/internal/connector/support/m365Support.go | 87 ------- .../connector/support/m365Support_test.go | 241 ------------------ .../operations/backup_integration_test.go | 7 +- src/pkg/services/m365/api/client_test.go | 5 +- src/pkg/services/m365/api/contacts.go | 146 +++++------ src/pkg/services/m365/api/contacts_test.go | 39 +++ src/pkg/services/m365/api/events.go | 175 +++++++------ src/pkg/services/m365/api/events_test.go | 40 ++- .../m365/api/item_serialization_test.go | 7 +- src/pkg/services/m365/api/mail.go | 184 ++++++------- src/pkg/services/m365/api/mail_test.go | 32 +++ src/pkg/services/m365/api/serialization.go | 25 ++ 28 files changed, 787 insertions(+), 836 deletions(-) rename src/internal/connector/{support/attendee.go => exchange/attendees.go} (99%) rename src/internal/connector/{support/m365Transform.go => exchange/transform.go} (62%) rename src/internal/connector/{support/m365Transform_test.go => exchange/transform_test.go} (87%) create mode 100644 src/internal/connector/sharepoint/api/serialization.go create mode 100644 src/internal/connector/sharepoint/api/serialization_test.go delete mode 100644 src/internal/connector/support/m365Support.go delete mode 100644 src/internal/connector/support/m365Support_test.go create mode 100644 src/pkg/services/m365/api/serialization.go diff --git a/src/internal/connector/exchange/attachment.go b/src/internal/connector/exchange/attachment.go index 640bb04e7..d09124523 100644 --- a/src/internal/connector/exchange/attachment.go +++ b/src/internal/connector/exchange/attachment.go @@ -8,7 +8,6 @@ import ( "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/alcionai/corso/src/internal/common/ptr" - "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/pkg/logger" ) @@ -85,7 +84,7 @@ func uploadAttachment( // item Attachments to be skipped until the completion of Issue #2353 if attachmentType == models.ITEM_ATTACHMENTTYPE { - a, err := support.ToItemAttachment(attachment) + a, err := toItemAttachment(attachment) if err != nil { logger.CtxErr(ctx, err).Info(fmt.Sprintf("item attachment type not supported: %v", attachmentType)) return nil diff --git a/src/internal/connector/support/attendee.go b/src/internal/connector/exchange/attendees.go similarity index 99% rename from src/internal/connector/support/attendee.go rename to src/internal/connector/exchange/attendees.go index 3fac24920..5eb638e93 100644 --- a/src/internal/connector/support/attendee.go +++ b/src/internal/connector/exchange/attendees.go @@ -1,4 +1,4 @@ -package support +package exchange import ( "fmt" diff --git a/src/internal/connector/exchange/data_collections.go b/src/internal/connector/exchange/data_collections.go index d07ee4300..d9905f3a3 100644 --- a/src/internal/connector/exchange/data_collections.go +++ b/src/internal/connector/exchange/data_collections.go @@ -266,10 +266,14 @@ func createCollections( ) ([]data.BackupCollection, error) { var ( allCollections = make([]data.BackupCollection, 0) - ac = api.Client{Credentials: creds} category = scope.Category().PathType() ) + ac, err := api.NewClient(creds) + if err != nil { + return nil, clues.Wrap(err, "getting api client").WithClues(ctx) + } + ctx = clues.Add(ctx, "category", category) getter, err := getterByType(ac, category) diff --git a/src/internal/connector/exchange/data_collections_test.go b/src/internal/connector/exchange/data_collections_test.go index a1ca35c14..00dde5ebd 100644 --- a/src/internal/connector/exchange/data_collections_test.go +++ b/src/internal/connector/exchange/data_collections_test.go @@ -483,7 +483,7 @@ func (suite *DataCollectionsIntegrationSuite) TestMailSerializationRegression() continue } - message, err := support.CreateMessageFromBytes(buf.Bytes()) + message, err := api.BytesToMessageable(buf.Bytes()) assert.NotNil(t, message) assert.NoError(t, err, clues.ToCore(err)) } @@ -553,7 +553,7 @@ func (suite *DataCollectionsIntegrationSuite) TestContactSerializationRegression continue } - contact, err := support.CreateContactFromBytes(buf.Bytes()) + contact, err := api.BytesToContactable(buf.Bytes()) assert.NotNil(t, contact) assert.NoError(t, err, "converting contact bytes: "+buf.String(), clues.ToCore(err)) count++ @@ -683,7 +683,7 @@ func (suite *DataCollectionsIntegrationSuite) TestEventsSerializationRegression( continue } - event, err := support.CreateEventFromBytes(buf.Bytes()) + event, err := api.BytesToEventable(buf.Bytes()) assert.NotNil(t, event) assert.NoError(t, err, "creating event from bytes: "+buf.String(), clues.ToCore(err)) } diff --git a/src/internal/connector/exchange/iterators_test.go b/src/internal/connector/exchange/iterators_test.go index 58f66fe55..7938dfff8 100644 --- a/src/internal/connector/exchange/iterators_test.go +++ b/src/internal/connector/exchange/iterators_test.go @@ -10,8 +10,8 @@ import ( exchMock "github.com/alcionai/corso/src/internal/connector/exchange/mock" "github.com/alcionai/corso/src/internal/connector/graph" - "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/tester" + "github.com/alcionai/corso/src/pkg/services/m365/api" ) type ExchangeIteratorSuite struct { @@ -25,7 +25,7 @@ func TestExchangeIteratorSuite(t *testing.T) { func (suite *ExchangeIteratorSuite) TestDisplayable() { t := suite.T() bytes := exchMock.ContactBytes("Displayable") - contact, err := support.CreateContactFromBytes(bytes) + contact, err := api.BytesToContactable(bytes) require.NoError(t, err, clues.ToCore(err)) aDisplayable, ok := contact.(graph.Displayable) @@ -37,7 +37,7 @@ func (suite *ExchangeIteratorSuite) TestDisplayable() { func (suite *ExchangeIteratorSuite) TestDescendable() { t := suite.T() bytes := exchMock.MessageBytes("Descendable") - message, err := support.CreateMessageFromBytes(bytes) + message, err := api.BytesToMessageable(bytes) require.NoError(t, err, clues.ToCore(err)) aDescendable, ok := message.(graph.Descendable) diff --git a/src/internal/connector/exchange/mock/mock_test.go b/src/internal/connector/exchange/mock/mock_test.go index d23791ef2..b373790c7 100644 --- a/src/internal/connector/exchange/mock/mock_test.go +++ b/src/internal/connector/exchange/mock/mock_test.go @@ -6,15 +6,14 @@ import ( "testing" "github.com/alcionai/clues" - "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/fault" + "github.com/alcionai/corso/src/pkg/services/m365/api" ) type MockSuite struct { @@ -78,7 +77,7 @@ func (suite *MockSuite) TestMockExchangeCollection_NewExchangeCollectionMail_Hyd assert.NoError(t, err, clues.ToCore(err)) byteArray := buf.Bytes() - something, err := support.CreateFromBytes(byteArray, models.CreateMessageFromDiscriminatorValue) + something, err := api.BytesToMessageable(byteArray) assert.NoError(t, err, clues.ToCore(err)) assert.NotNil(t, something) } @@ -146,7 +145,7 @@ func (suite *MockExchangeDataSuite) TestMockByteHydration() { name: "Message Bytes", transformation: func(t *testing.T) error { bytes := MessageBytes(subject) - _, err := support.CreateMessageFromBytes(bytes) + _, err := api.BytesToMessageable(bytes) return err }, }, @@ -154,7 +153,7 @@ func (suite *MockExchangeDataSuite) TestMockByteHydration() { name: "Event Message Response: Regression", transformation: func(t *testing.T) error { bytes := EventMessageResponse(subject) - _, err := support.CreateMessageFromBytes(bytes) + _, err := api.BytesToEventable(bytes) return err }, }, @@ -162,7 +161,7 @@ func (suite *MockExchangeDataSuite) TestMockByteHydration() { name: "Event Message Request: Regression", transformation: func(t *testing.T) error { bytes := EventMessageRequest(subject) - _, err := support.CreateMessageFromBytes(bytes) + _, err := api.BytesToEventable(bytes) return err }, }, @@ -170,7 +169,7 @@ func (suite *MockExchangeDataSuite) TestMockByteHydration() { name: "Contact Bytes", transformation: func(t *testing.T) error { bytes := ContactBytes(subject) - _, err := support.CreateContactFromBytes(bytes) + _, err := api.BytesToContactable(bytes) return err }, }, @@ -178,7 +177,7 @@ func (suite *MockExchangeDataSuite) TestMockByteHydration() { name: "Event No Attendees Bytes", transformation: func(t *testing.T) error { bytes := EventBytes(subject) - _, err := support.CreateEventFromBytes(bytes) + _, err := api.BytesToEventable(bytes) return err }, }, diff --git a/src/internal/connector/exchange/service_restore.go b/src/internal/connector/exchange/service_restore.go index 5286652fa..f5435249e 100644 --- a/src/internal/connector/exchange/service_restore.go +++ b/src/internal/connector/exchange/service_restore.go @@ -65,12 +65,12 @@ func RestoreItem( // RestoreContact wraps api.Contacts().PostItem() func RestoreContact( ctx context.Context, - bits []byte, + body []byte, cli itemPoster[models.Contactable], cp control.CollisionPolicy, destination, user string, ) (*details.ExchangeInfo, error) { - contact, err := support.CreateContactFromBytes(bits) + contact, err := api.BytesToContactable(body) if err != nil { return nil, graph.Wrap(ctx, err, "creating contact from bytes") } @@ -83,7 +83,7 @@ func RestoreContact( } info := api.ContactInfo(contact) - info.Size = int64(len(bits)) + info.Size = int64(len(body)) return info, nil } @@ -91,7 +91,7 @@ func RestoreContact( // RestoreEvent wraps api.Events().PostItem() func RestoreEvent( ctx context.Context, - bits []byte, + body []byte, itemCli itemPoster[models.Eventable], attachmentCli attachmentPoster, gs graph.Servicer, @@ -99,7 +99,7 @@ func RestoreEvent( destination, user string, errs *fault.Bus, ) (*details.ExchangeInfo, error) { - event, err := support.CreateEventFromBytes(bits) + event, err := api.BytesToEventable(body) if err != nil { return nil, clues.Wrap(err, "creating event from bytes").WithClues(ctx) } @@ -108,7 +108,7 @@ func RestoreEvent( var ( el = errs.Local() - transformedEvent = support.ToEventSimplified(event) + transformedEvent = toEventSimplified(event) attached []models.Attachmentable ) @@ -141,7 +141,7 @@ func RestoreEvent( } info := api.EventInfo(event) - info.Size = int64(len(bits)) + info.Size = int64(len(body)) return info, el.Failure() } @@ -149,7 +149,7 @@ func RestoreEvent( // RestoreMessage wraps api.Mail().PostItem(), handling attachment creation along the way func RestoreMessage( ctx context.Context, - bits []byte, + body []byte, itemCli itemPoster[models.Messageable], attachmentCli attachmentPoster, gs graph.Servicer, @@ -158,7 +158,7 @@ func RestoreMessage( errs *fault.Bus, ) (*details.ExchangeInfo, error) { // Creates messageable object from original bytes - msg, err := support.CreateMessageFromBytes(bits) + msg, err := api.BytesToMessageable(body) if err != nil { return nil, clues.Wrap(err, "creating mail from bytes").WithClues(ctx) } @@ -166,7 +166,7 @@ func RestoreMessage( ctx = clues.Add(ctx, "item_id", ptr.Val(msg.GetId())) var ( - clone = support.ToMessage(msg) + clone = toMessage(msg) valueID = MailRestorePropertyTag enableValue = RestoreCanonicalEnableValue ) @@ -244,7 +244,7 @@ func RestoreMessage( } } - return api.MailInfo(clone, int64(len(bits))), el.Failure() + return api.MailInfo(clone, int64(len(body))), el.Failure() } // RestoreCollections restores M365 objects in data.RestoreCollection to MSFT diff --git a/src/internal/connector/support/m365Transform.go b/src/internal/connector/exchange/transform.go similarity index 62% rename from src/internal/connector/support/m365Transform.go rename to src/internal/connector/exchange/transform.go index fb44f2a6d..99df1805c 100644 --- a/src/internal/connector/support/m365Transform.go +++ b/src/internal/connector/exchange/transform.go @@ -1,4 +1,4 @@ -package support +package exchange import ( "fmt" @@ -53,7 +53,7 @@ func CloneMessageableFields(orig, message models.Messageable) models.Messageable return message } -func ToMessage(orig models.Messageable) models.Messageable { +func toMessage(orig models.Messageable) models.Messageable { message := models.NewMessage() temp := CloneMessageableFields(orig, message) @@ -70,7 +70,7 @@ func ToMessage(orig models.Messageable) models.Messageable { // - 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 { +func toEventSimplified(orig models.Eventable) models.Eventable { attendees := FormatAttendees(orig, ptr.Val(orig.GetBody().GetContentType()) == models.HTML_BODYTYPE) orig.SetAttendees([]models.Attendeeable{}) origBody := orig.GetBody() @@ -144,165 +144,6 @@ func insertStringToBody(body getContenter, newContent string) string { return newContent + content } -// CloneListItem creates a new `SharePoint.ListItem` and stores the original item's -// M365 data into it set fields. -// - https://learn.microsoft.com/en-us/graph/api/resources/listitem?view=graph-rest-1.0 -func CloneListItem(orig models.ListItemable) models.ListItemable { - newItem := models.NewListItem() - newFieldData := retrieveFieldData(orig.GetFields()) - - newItem.SetAdditionalData(orig.GetAdditionalData()) - newItem.SetAnalytics(orig.GetAnalytics()) - newItem.SetContentType(orig.GetContentType()) - newItem.SetCreatedBy(orig.GetCreatedBy()) - newItem.SetCreatedByUser(orig.GetCreatedByUser()) - newItem.SetCreatedDateTime(orig.GetCreatedDateTime()) - newItem.SetDescription(orig.GetDescription()) - // ETag cannot be carried forward - newItem.SetFields(newFieldData) - newItem.SetLastModifiedBy(orig.GetLastModifiedBy()) - newItem.SetLastModifiedByUser(orig.GetLastModifiedByUser()) - newItem.SetLastModifiedDateTime(orig.GetLastModifiedDateTime()) - newItem.SetOdataType(orig.GetOdataType()) - // parentReference and SharePointIDs cause error on upload. - // POST Command will link items to the created list. - newItem.SetVersions(orig.GetVersions()) - - return newItem -} - -// retrieveFieldData utility function to clone raw listItem data from the embedded -// additionalData map -// Further details on FieldValueSets: -// - https://learn.microsoft.com/en-us/graph/api/resources/fieldvalueset?view=graph-rest-1.0 -func retrieveFieldData(orig models.FieldValueSetable) models.FieldValueSetable { - fields := models.NewFieldValueSet() - additionalData := make(map[string]any) - fieldData := orig.GetAdditionalData() - - // M365 Book keeping values removed during new Item Creation - // Removed Values: - // -- Prefixes -> @odata.context : absolute path to previous list - // . -> @odata.etag : Embedded link to Prior M365 ID - // -- String Match: Read-Only Fields - // -> id : previous un - for key, value := range fieldData { - if strings.HasPrefix(key, "_") || strings.HasPrefix(key, "@") || - key == "Edit" || key == "Created" || key == "Modified" || - strings.Contains(key, "LookupId") || strings.Contains(key, "ChildCount") || strings.Contains(key, "LinkTitle") { - continue - } - - additionalData[key] = value - } - - fields.SetAdditionalData(additionalData) - - return fields -} - -// ToListable utility function to encapsulate stored data for restoration. -// New Listable omits trackable fields such as `id` or `ETag` and other read-only -// objects that are prevented upon upload. Additionally, read-Only columns are -// not attached in this method. -// ListItems are not included in creation of new list, and have to be restored -// in separate call. -func ToListable(orig models.Listable, displayName string) models.Listable { - newList := models.NewList() - - newList.SetContentTypes(orig.GetContentTypes()) - newList.SetCreatedBy(orig.GetCreatedBy()) - newList.SetCreatedByUser(orig.GetCreatedByUser()) - newList.SetCreatedDateTime(orig.GetCreatedDateTime()) - newList.SetDescription(orig.GetDescription()) - newList.SetDisplayName(&displayName) - newList.SetLastModifiedBy(orig.GetLastModifiedBy()) - newList.SetLastModifiedByUser(orig.GetLastModifiedByUser()) - newList.SetLastModifiedDateTime(orig.GetLastModifiedDateTime()) - newList.SetList(orig.GetList()) - newList.SetOdataType(orig.GetOdataType()) - newList.SetParentReference(orig.GetParentReference()) - - columns := make([]models.ColumnDefinitionable, 0) - leg := map[string]struct{}{ - "Attachments": {}, - "Edit": {}, - "Content Type": {}, - } - - for _, cd := range orig.GetColumns() { - var ( - displayName string - readOnly bool - ) - - if name, ok := ptr.ValOK(cd.GetDisplayName()); ok { - displayName = name - } - - if ro, ok := ptr.ValOK(cd.GetReadOnly()); ok { - readOnly = ro - } - - _, isLegacy := leg[displayName] - - // Skips columns that cannot be uploaded for models.ColumnDefinitionable: - // - ReadOnly, Title, or Legacy columns: Attachments, Edit, or Content Type - if readOnly || displayName == "Title" || isLegacy { - continue - } - - columns = append(columns, cloneColumnDefinitionable(cd)) - } - - newList.SetColumns(columns) - - return newList -} - -// cloneColumnDefinitionable utility function for encapsulating models.ColumnDefinitionable data -// into new object for upload. -func cloneColumnDefinitionable(orig models.ColumnDefinitionable) models.ColumnDefinitionable { - newColumn := models.NewColumnDefinition() - - newColumn.SetAdditionalData(orig.GetAdditionalData()) - newColumn.SetBoolean(orig.GetBoolean()) - newColumn.SetCalculated(orig.GetCalculated()) - newColumn.SetChoice(orig.GetChoice()) - newColumn.SetColumnGroup(orig.GetColumnGroup()) - newColumn.SetContentApprovalStatus(orig.GetContentApprovalStatus()) - newColumn.SetCurrency(orig.GetCurrency()) - newColumn.SetDateTime(orig.GetDateTime()) - newColumn.SetDefaultValue(orig.GetDefaultValue()) - newColumn.SetDescription(orig.GetDescription()) - newColumn.SetDisplayName(orig.GetDisplayName()) - newColumn.SetEnforceUniqueValues(orig.GetEnforceUniqueValues()) - newColumn.SetGeolocation(orig.GetGeolocation()) - newColumn.SetHidden(orig.GetHidden()) - newColumn.SetHyperlinkOrPicture(orig.GetHyperlinkOrPicture()) - newColumn.SetIndexed(orig.GetIndexed()) - newColumn.SetIsDeletable(orig.GetIsDeletable()) - newColumn.SetIsReorderable(orig.GetIsReorderable()) - newColumn.SetIsSealed(orig.GetIsSealed()) - newColumn.SetLookup(orig.GetLookup()) - newColumn.SetName(orig.GetName()) - newColumn.SetNumber(orig.GetNumber()) - newColumn.SetOdataType(orig.GetOdataType()) - newColumn.SetPersonOrGroup(orig.GetPersonOrGroup()) - newColumn.SetPropagateChanges(orig.GetPropagateChanges()) - newColumn.SetReadOnly(orig.GetReadOnly()) - newColumn.SetRequired(orig.GetRequired()) - newColumn.SetSourceColumn(orig.GetSourceColumn()) - newColumn.SetSourceContentType(orig.GetSourceContentType()) - newColumn.SetTerm(orig.GetTerm()) - newColumn.SetText(orig.GetText()) - newColumn.SetThumbnail(orig.GetThumbnail()) - newColumn.SetType(orig.GetType()) - newColumn.SetValidation(orig.GetValidation()) - - return newColumn -} - // =============================================================================================== // Sanitization section // Set of functions that support ItemAttachemtable object restoration. @@ -329,9 +170,9 @@ const ( contactItemType = "#microsoft.graph.contact" ) -// ToItemAttachment transforms internal item, OutlookItemables, into +// toItemAttachment transforms internal item, OutlookItemables, into // objects that are able to be uploaded into M365. -func ToItemAttachment(orig models.Attachmentable) (models.Attachmentable, error) { +func toItemAttachment(orig models.Attachmentable) (models.Attachmentable, error) { transform, ok := orig.(models.ItemAttachmentable) if !ok { // Shouldn't ever happen return nil, clues.New("transforming attachment to item attachment") @@ -452,7 +293,7 @@ func sanitizeEvent(orig models.Eventable) (models.Eventable, error) { } func sanitizeMessage(orig models.Messageable) (models.Messageable, error) { - message := ToMessage(orig) + message := toMessage(orig) // TODO #2428 (dadam39): re-apply nested attachments for itemAttachments // Upstream: https://github.com/microsoft/kiota-serialization-json-go/issues/61 diff --git a/src/internal/connector/support/m365Transform_test.go b/src/internal/connector/exchange/transform_test.go similarity index 87% rename from src/internal/connector/support/m365Transform_test.go rename to src/internal/connector/exchange/transform_test.go index 924c19bb7..fe1bc1601 100644 --- a/src/internal/connector/support/m365Transform_test.go +++ b/src/internal/connector/exchange/transform_test.go @@ -1,4 +1,4 @@ -package support +package exchange import ( "testing" @@ -12,24 +12,25 @@ import ( "github.com/alcionai/corso/src/internal/common/ptr" exchMock "github.com/alcionai/corso/src/internal/connector/exchange/mock" "github.com/alcionai/corso/src/internal/tester" + "github.com/alcionai/corso/src/pkg/services/m365/api" ) -type SupportTestSuite struct { +type TransformUnitTest struct { tester.Suite } func TestSupportTestSuite(t *testing.T) { - suite.Run(t, &SupportTestSuite{Suite: tester.NewUnitSuite(t)}) + suite.Run(t, &TransformUnitTest{Suite: tester.NewUnitSuite(t)}) } -func (suite *SupportTestSuite) TestToMessage() { +func (suite *TransformUnitTest) TestToMessage() { t := suite.T() bytes := exchMock.MessageBytes("m365 mail support test") - message, err := CreateMessageFromBytes(bytes) + message, err := api.BytesToMessageable(bytes) require.NoError(suite.T(), err, clues.ToCore(err)) - clone := ToMessage(message) + clone := toMessage(message) assert.Equal(t, message.GetBccRecipients(), clone.GetBccRecipients()) assert.Equal(t, message.GetSubject(), clone.GetSubject()) assert.Equal(t, message.GetSender(), clone.GetSender()) @@ -37,14 +38,14 @@ func (suite *SupportTestSuite) TestToMessage() { assert.NotEqual(t, message.GetId(), clone.GetId()) } -func (suite *SupportTestSuite) TestToEventSimplified_attendees() { +func (suite *TransformUnitTest) TestToEventSimplified_attendees() { t := suite.T() bytes := exchMock.EventWithAttendeesBytes("M365 Event Support Test") - event, err := CreateEventFromBytes(bytes) + event, err := api.BytesToEventable(bytes) require.NoError(t, err, clues.ToCore(err)) attendees := event.GetAttendees() - newEvent := ToEventSimplified(event) + newEvent := toEventSimplified(event) assert.Empty(t, newEvent.GetHideAttendees()) assert.Equal(t, ptr.Val(event.GetBody().GetContentType()), ptr.Val(newEvent.GetBody().GetContentType())) @@ -57,7 +58,7 @@ func (suite *SupportTestSuite) TestToEventSimplified_attendees() { } } -func (suite *SupportTestSuite) TestToEventSimplified_recurrence() { +func (suite *TransformUnitTest) TestToEventSimplified_recurrence() { var ( t = suite.T() subject = "M365 Event Support Test" @@ -72,7 +73,7 @@ func (suite *SupportTestSuite) TestToEventSimplified_recurrence() { name: "Test recurrence: Unspecified", event: func() models.Eventable { bytes := exchMock.EventWithSubjectBytes(subject) - e, err := CreateEventFromBytes(bytes) + e, err := api.BytesToEventable(bytes) require.NoError(t, err, clues.ToCore(err)) return e }, @@ -85,7 +86,7 @@ func (suite *SupportTestSuite) TestToEventSimplified_recurrence() { name: "Test recurrenceTimeZone: Unspecified", event: func() models.Eventable { bytes := exchMock.EventWithRecurrenceBytes(subject, `null`) - e, err := CreateEventFromBytes(bytes) + e, err := api.BytesToEventable(bytes) require.NoError(t, err, clues.ToCore(err)) return e }, @@ -98,7 +99,7 @@ func (suite *SupportTestSuite) TestToEventSimplified_recurrence() { name: "Test recurrenceTimeZone: Empty", event: func() models.Eventable { bytes := exchMock.EventWithRecurrenceBytes(subject, `""`) - event, err := CreateEventFromBytes(bytes) + event, err := api.BytesToEventable(bytes) require.NoError(t, err, clues.ToCore(err)) return event }, @@ -111,7 +112,7 @@ func (suite *SupportTestSuite) TestToEventSimplified_recurrence() { name: "Test recurrenceTimeZone: Valid", event: func() models.Eventable { bytes := exchMock.EventWithRecurrenceBytes(subject, `"Pacific Standard Time"`) - event, err := CreateEventFromBytes(bytes) + event, err := api.BytesToEventable(bytes) require.NoError(t, err, clues.ToCore(err)) return event }, @@ -125,7 +126,7 @@ func (suite *SupportTestSuite) TestToEventSimplified_recurrence() { for _, test := range tests { suite.Run(test.name, func() { event := test.event() - newEvent := ToEventSimplified(event) + newEvent := toEventSimplified(event) assert.True(t, test.validateOutput(newEvent), test.name) }) } @@ -148,7 +149,7 @@ func makeMockContent(c string, ct models.BodyType) mockContenter { return mockContenter{&c, &ct} } -func (suite *SupportTestSuite) TestInsertStringToBody() { +func (suite *TransformUnitTest) TestInsertStringToBody() { nilTextContent := makeMockContent("", models.TEXT_BODYTYPE) nilTextContent.content = nil nilHTMLContent := makeMockContent("", models.HTML_BODYTYPE) diff --git a/src/internal/connector/graph_connector_helper_test.go b/src/internal/connector/graph_connector_helper_test.go index a38b5236b..b5907497b 100644 --- a/src/internal/connector/graph_connector_helper_test.go +++ b/src/internal/connector/graph_connector_helper_test.go @@ -18,7 +18,6 @@ import ( "github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/connector/onedrive" "github.com/alcionai/corso/src/internal/connector/onedrive/metadata" - "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/fault" @@ -582,7 +581,7 @@ func compareExchangeEmail( return } - itemMessage, err := support.CreateMessageFromBytes(itemData) + itemMessage, err := api.BytesToMessageable(itemData) if !assert.NoError(t, err, "deserializing backed up message", clues.ToCore(err)) { return } @@ -592,7 +591,7 @@ func compareExchangeEmail( return } - expectedMessage, err := support.CreateMessageFromBytes(expectedBytes) + expectedMessage, err := api.BytesToMessageable(expectedBytes) assert.NoError(t, err, "deserializing source message", clues.ToCore(err)) checkMessage(t, expectedMessage, itemMessage) @@ -609,7 +608,7 @@ func compareExchangeContact( return } - itemContact, err := support.CreateContactFromBytes(itemData) + itemContact, err := api.BytesToContactable(itemData) if !assert.NoError(t, err, "deserializing backed up contact", clues.ToCore(err)) { return } @@ -619,7 +618,7 @@ func compareExchangeContact( return } - expectedContact, err := support.CreateContactFromBytes(expectedBytes) + expectedContact, err := api.BytesToContactable(expectedBytes) if !assert.NoError(t, err, "deserializing source contact") { return } @@ -637,7 +636,7 @@ func compareExchangeEvent( return } - itemEvent, err := support.CreateEventFromBytes(itemData) + itemEvent, err := api.BytesToEventable(itemData) if !assert.NoError(t, err, "deserializing backed up contact", clues.ToCore(err)) { return } @@ -647,7 +646,7 @@ func compareExchangeEvent( return } - expectedEvent, err := support.CreateEventFromBytes(expectedBytes) + expectedEvent, err := api.BytesToEventable(expectedBytes) assert.NoError(t, err, "deserializing source contact", clues.ToCore(err)) checkEvent(t, expectedEvent, itemEvent) diff --git a/src/internal/connector/sharepoint/api/pages.go b/src/internal/connector/sharepoint/api/pages.go index 17e0e69af..f3ee314e5 100644 --- a/src/internal/connector/sharepoint/api/pages.go +++ b/src/internal/connector/sharepoint/api/pages.go @@ -12,7 +12,6 @@ import ( "github.com/alcionai/corso/src/internal/connector/graph" betamodels "github.com/alcionai/corso/src/internal/connector/graph/betasdk/models" betasites "github.com/alcionai/corso/src/internal/connector/graph/betasdk/sites" - "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/diagnostics" "github.com/alcionai/corso/src/pkg/backup/details" @@ -190,7 +189,7 @@ func RestoreSitePage( } // Hydrate Page - page, err := support.CreatePageFromBytes(byteArray) + page, err := CreatePageFromBytes(byteArray) if err != nil { return dii, clues.Wrap(err, "creating Page object").WithClues(ctx) } diff --git a/src/internal/connector/sharepoint/api/serialization.go b/src/internal/connector/sharepoint/api/serialization.go new file mode 100644 index 000000000..e1382ed94 --- /dev/null +++ b/src/internal/connector/sharepoint/api/serialization.go @@ -0,0 +1,212 @@ +package api + +import ( + "strings" + + "github.com/alcionai/clues" + "github.com/microsoft/kiota-abstractions-go/serialization" + kjson "github.com/microsoft/kiota-serialization-json-go" + "github.com/microsoftgraph/msgraph-sdk-go/models" + + "github.com/alcionai/corso/src/internal/common/ptr" + betamodels "github.com/alcionai/corso/src/internal/connector/graph/betasdk/models" +) + +// createFromBytes generates an m365 object form bytes. +func createFromBytes( + bytes []byte, + createFunc serialization.ParsableFactory, +) (serialization.Parsable, error) { + parseNode, err := kjson.NewJsonParseNodeFactory().GetRootParseNode("application/json", bytes) + if err != nil { + return nil, clues.Wrap(err, "deserializing bytes into base m365 object") + } + + v, err := parseNode.GetObjectValue(createFunc) + if err != nil { + return nil, clues.Wrap(err, "parsing m365 object factory") + } + + return v, nil +} + +func CreateListFromBytes(bytes []byte) (models.Listable, error) { + parsable, err := createFromBytes(bytes, models.CreateListFromDiscriminatorValue) + if err != nil { + return nil, clues.Wrap(err, "deserializing bytes to sharepoint list") + } + + list := parsable.(models.Listable) + + return list, nil +} + +func CreatePageFromBytes(bytes []byte) (betamodels.SitePageable, error) { + parsable, err := createFromBytes(bytes, betamodels.CreateSitePageFromDiscriminatorValue) + if err != nil { + return nil, clues.Wrap(err, "deserializing bytes to sharepoint page") + } + + page := parsable.(betamodels.SitePageable) + + return page, nil +} + +// ToListable utility function to encapsulate stored data for restoration. +// New Listable omits trackable fields such as `id` or `ETag` and other read-only +// objects that are prevented upon upload. Additionally, read-Only columns are +// not attached in this method. +// ListItems are not included in creation of new list, and have to be restored +// in separate call. +func ToListable(orig models.Listable, displayName string) models.Listable { + newList := models.NewList() + + newList.SetContentTypes(orig.GetContentTypes()) + newList.SetCreatedBy(orig.GetCreatedBy()) + newList.SetCreatedByUser(orig.GetCreatedByUser()) + newList.SetCreatedDateTime(orig.GetCreatedDateTime()) + newList.SetDescription(orig.GetDescription()) + newList.SetDisplayName(&displayName) + newList.SetLastModifiedBy(orig.GetLastModifiedBy()) + newList.SetLastModifiedByUser(orig.GetLastModifiedByUser()) + newList.SetLastModifiedDateTime(orig.GetLastModifiedDateTime()) + newList.SetList(orig.GetList()) + newList.SetOdataType(orig.GetOdataType()) + newList.SetParentReference(orig.GetParentReference()) + + columns := make([]models.ColumnDefinitionable, 0) + leg := map[string]struct{}{ + "Attachments": {}, + "Edit": {}, + "Content Type": {}, + } + + for _, cd := range orig.GetColumns() { + var ( + displayName string + readOnly bool + ) + + if name, ok := ptr.ValOK(cd.GetDisplayName()); ok { + displayName = name + } + + if ro, ok := ptr.ValOK(cd.GetReadOnly()); ok { + readOnly = ro + } + + _, isLegacy := leg[displayName] + + // Skips columns that cannot be uploaded for models.ColumnDefinitionable: + // - ReadOnly, Title, or Legacy columns: Attachments, Edit, or Content Type + if readOnly || displayName == "Title" || isLegacy { + continue + } + + columns = append(columns, cloneColumnDefinitionable(cd)) + } + + newList.SetColumns(columns) + + return newList +} + +// cloneColumnDefinitionable utility function for encapsulating models.ColumnDefinitionable data +// into new object for upload. +func cloneColumnDefinitionable(orig models.ColumnDefinitionable) models.ColumnDefinitionable { + newColumn := models.NewColumnDefinition() + + newColumn.SetAdditionalData(orig.GetAdditionalData()) + newColumn.SetBoolean(orig.GetBoolean()) + newColumn.SetCalculated(orig.GetCalculated()) + newColumn.SetChoice(orig.GetChoice()) + newColumn.SetColumnGroup(orig.GetColumnGroup()) + newColumn.SetContentApprovalStatus(orig.GetContentApprovalStatus()) + newColumn.SetCurrency(orig.GetCurrency()) + newColumn.SetDateTime(orig.GetDateTime()) + newColumn.SetDefaultValue(orig.GetDefaultValue()) + newColumn.SetDescription(orig.GetDescription()) + newColumn.SetDisplayName(orig.GetDisplayName()) + newColumn.SetEnforceUniqueValues(orig.GetEnforceUniqueValues()) + newColumn.SetGeolocation(orig.GetGeolocation()) + newColumn.SetHidden(orig.GetHidden()) + newColumn.SetHyperlinkOrPicture(orig.GetHyperlinkOrPicture()) + newColumn.SetIndexed(orig.GetIndexed()) + newColumn.SetIsDeletable(orig.GetIsDeletable()) + newColumn.SetIsReorderable(orig.GetIsReorderable()) + newColumn.SetIsSealed(orig.GetIsSealed()) + newColumn.SetLookup(orig.GetLookup()) + newColumn.SetName(orig.GetName()) + newColumn.SetNumber(orig.GetNumber()) + newColumn.SetOdataType(orig.GetOdataType()) + newColumn.SetPersonOrGroup(orig.GetPersonOrGroup()) + newColumn.SetPropagateChanges(orig.GetPropagateChanges()) + newColumn.SetReadOnly(orig.GetReadOnly()) + newColumn.SetRequired(orig.GetRequired()) + newColumn.SetSourceColumn(orig.GetSourceColumn()) + newColumn.SetSourceContentType(orig.GetSourceContentType()) + newColumn.SetTerm(orig.GetTerm()) + newColumn.SetText(orig.GetText()) + newColumn.SetThumbnail(orig.GetThumbnail()) + newColumn.SetType(orig.GetType()) + newColumn.SetValidation(orig.GetValidation()) + + return newColumn +} + +// CloneListItem creates a new `SharePoint.ListItem` and stores the original item's +// M365 data into it set fields. +// - https://learn.microsoft.com/en-us/graph/api/resources/listitem?view=graph-rest-1.0 +func CloneListItem(orig models.ListItemable) models.ListItemable { + newItem := models.NewListItem() + newFieldData := retrieveFieldData(orig.GetFields()) + + newItem.SetAdditionalData(orig.GetAdditionalData()) + newItem.SetAnalytics(orig.GetAnalytics()) + newItem.SetContentType(orig.GetContentType()) + newItem.SetCreatedBy(orig.GetCreatedBy()) + newItem.SetCreatedByUser(orig.GetCreatedByUser()) + newItem.SetCreatedDateTime(orig.GetCreatedDateTime()) + newItem.SetDescription(orig.GetDescription()) + // ETag cannot be carried forward + newItem.SetFields(newFieldData) + newItem.SetLastModifiedBy(orig.GetLastModifiedBy()) + newItem.SetLastModifiedByUser(orig.GetLastModifiedByUser()) + newItem.SetLastModifiedDateTime(orig.GetLastModifiedDateTime()) + newItem.SetOdataType(orig.GetOdataType()) + // parentReference and SharePointIDs cause error on upload. + // POST Command will link items to the created list. + newItem.SetVersions(orig.GetVersions()) + + return newItem +} + +// retrieveFieldData utility function to clone raw listItem data from the embedded +// additionalData map +// Further details on FieldValueSets: +// - https://learn.microsoft.com/en-us/graph/api/resources/fieldvalueset?view=graph-rest-1.0 +func retrieveFieldData(orig models.FieldValueSetable) models.FieldValueSetable { + fields := models.NewFieldValueSet() + additionalData := make(map[string]any) + fieldData := orig.GetAdditionalData() + + // M365 Book keeping values removed during new Item Creation + // Removed Values: + // -- Prefixes -> @odata.context : absolute path to previous list + // . -> @odata.etag : Embedded link to Prior M365 ID + // -- String Match: Read-Only Fields + // -> id : previous un + for key, value := range fieldData { + if strings.HasPrefix(key, "_") || strings.HasPrefix(key, "@") || + key == "Edit" || key == "Created" || key == "Modified" || + strings.Contains(key, "LookupId") || strings.Contains(key, "ChildCount") || strings.Contains(key, "LinkTitle") { + continue + } + + additionalData[key] = value + } + + fields.SetAdditionalData(additionalData) + + return fields +} diff --git a/src/internal/connector/sharepoint/api/serialization_test.go b/src/internal/connector/sharepoint/api/serialization_test.go new file mode 100644 index 000000000..df6389f8d --- /dev/null +++ b/src/internal/connector/sharepoint/api/serialization_test.go @@ -0,0 +1,126 @@ +package api + +import ( + "testing" + + "github.com/alcionai/clues" + kioser "github.com/microsoft/kiota-serialization-json-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + bmodels "github.com/alcionai/corso/src/internal/connector/graph/betasdk/models" + spMock "github.com/alcionai/corso/src/internal/connector/sharepoint/mock" + "github.com/alcionai/corso/src/internal/tester" +) + +type SerializationUnitSuite struct { + tester.Suite +} + +func TestDataSupportSuite(t *testing.T) { + suite.Run(t, &SerializationUnitSuite{Suite: tester.NewUnitSuite(t)}) +} + +func (suite *SerializationUnitSuite) TestCreateListFromBytes() { + listBytes, err := spMock.ListBytes("DataSupportSuite") + require.NoError(suite.T(), err) + + tests := []struct { + name string + byteArray []byte + checkError assert.ErrorAssertionFunc + isNil assert.ValueAssertionFunc + }{ + { + name: "empty bytes", + byteArray: make([]byte, 0), + checkError: assert.Error, + isNil: assert.Nil, + }, + { + name: "invalid bytes", + byteArray: []byte("Invalid byte stream \"subject:\" Not going to work"), + checkError: assert.Error, + isNil: assert.Nil, + }, + { + name: "Valid List", + byteArray: listBytes, + checkError: assert.NoError, + isNil: assert.NotNil, + }, + } + + for _, test := range tests { + suite.Run(test.name, func() { + t := suite.T() + + result, err := CreateListFromBytes(test.byteArray) + test.checkError(t, err, clues.ToCore(err)) + test.isNil(t, result) + }) + } +} + +func (suite *SerializationUnitSuite) TestCreatePageFromBytes() { + tests := []struct { + name string + checkError assert.ErrorAssertionFunc + isNil assert.ValueAssertionFunc + getBytes func(t *testing.T) []byte + }{ + { + "empty bytes", + assert.Error, + assert.Nil, + func(t *testing.T) []byte { + return make([]byte, 0) + }, + }, + { + "invalid bytes", + assert.Error, + assert.Nil, + func(t *testing.T) []byte { + return []byte("snarf") + }, + }, + { + "Valid Page", + assert.NoError, + assert.NotNil, + func(t *testing.T) []byte { + pg := bmodels.NewSitePage() + title := "Tested" + pg.SetTitle(&title) + pg.SetName(&title) + pg.SetWebUrl(&title) + + writer := kioser.NewJsonSerializationWriter() + err := writer.WriteObjectValue("", pg) + require.NoError(t, err, clues.ToCore(err)) + + byteArray, err := writer.GetSerializedContent() + require.NoError(t, err, clues.ToCore(err)) + + return byteArray + }, + }, + } + + for _, test := range tests { + suite.Run(test.name, func() { + t := suite.T() + + result, err := CreatePageFromBytes(test.getBytes(t)) + test.checkError(t, err) + test.isNil(t, result) + if result != nil { + assert.Equal(t, "Tested", *result.GetName(), "name") + assert.Equal(t, "Tested", *result.GetTitle(), "title") + assert.Equal(t, "Tested", *result.GetWebUrl(), "webURL") + } + }) + } +} diff --git a/src/internal/connector/sharepoint/collection_test.go b/src/internal/connector/sharepoint/collection_test.go index 991a55588..a74701037 100644 --- a/src/internal/connector/sharepoint/collection_test.go +++ b/src/internal/connector/sharepoint/collection_test.go @@ -15,7 +15,6 @@ import ( "github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/connector/sharepoint/api" spMock "github.com/alcionai/corso/src/internal/connector/sharepoint/mock" - "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/account" @@ -131,7 +130,7 @@ func (suite *SharePointCollectionSuite) TestCollection_Items() { }, getItem: func(t *testing.T, itemName string) *Item { byteArray := spMock.Page(itemName) - page, err := support.CreatePageFromBytes(byteArray) + page, err := api.CreatePageFromBytes(byteArray) require.NoError(t, err, clues.ToCore(err)) data := &Item{ diff --git a/src/internal/connector/sharepoint/mock/mock_test.go b/src/internal/connector/sharepoint/mock/mock_test.go index 63cd0e82e..01776ea36 100644 --- a/src/internal/connector/sharepoint/mock/mock_test.go +++ b/src/internal/connector/sharepoint/mock/mock_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - "github.com/alcionai/corso/src/internal/connector/support" + "github.com/alcionai/corso/src/internal/connector/sharepoint/api" "github.com/alcionai/corso/src/internal/tester" ) @@ -39,7 +39,7 @@ func (suite *MockSuite) TestMockByteHydration() { bytes, err := writer.GetSerializedContent() require.NoError(t, err, clues.ToCore(err)) - _, err = support.CreateListFromBytes(bytes) + _, err = api.CreateListFromBytes(bytes) return err }, @@ -49,7 +49,7 @@ func (suite *MockSuite) TestMockByteHydration() { transformation: func(t *testing.T) error { bytes, err := ListBytes(subject) require.NoError(t, err, clues.ToCore(err)) - _, err = support.CreateListFromBytes(bytes) + _, err = api.CreateListFromBytes(bytes) return err }, }, @@ -57,7 +57,7 @@ func (suite *MockSuite) TestMockByteHydration() { name: "SharePoint: Page", transformation: func(t *testing.T) error { bytes := Page(subject) - _, err := support.CreatePageFromBytes(bytes) + _, err := api.CreatePageFromBytes(bytes) return err }, diff --git a/src/internal/connector/sharepoint/restore.go b/src/internal/connector/sharepoint/restore.go index 04c6f8083..f889a25b9 100644 --- a/src/internal/connector/sharepoint/restore.go +++ b/src/internal/connector/sharepoint/restore.go @@ -160,7 +160,7 @@ func restoreListItem( return dii, clues.Wrap(err, "reading backup data").WithClues(ctx) } - oldList, err := support.CreateListFromBytes(byteArray) + oldList, err := betaAPI.CreateListFromBytes(byteArray) if err != nil { return dii, clues.Wrap(err, "creating item").WithClues(ctx) } @@ -171,12 +171,12 @@ func restoreListItem( var ( newName = fmt.Sprintf("%s_%s", destName, listName) - newList = support.ToListable(oldList, newName) + newList = betaAPI.ToListable(oldList, newName) contents = make([]models.ListItemable, 0) ) for _, itm := range oldList.GetItems() { - temp := support.CloneListItem(itm) + temp := betaAPI.CloneListItem(itm) contents = append(contents, temp) } diff --git a/src/internal/connector/support/m365Support.go b/src/internal/connector/support/m365Support.go deleted file mode 100644 index 02bbf4c9c..000000000 --- a/src/internal/connector/support/m365Support.go +++ /dev/null @@ -1,87 +0,0 @@ -package support - -import ( - "github.com/alcionai/clues" - "github.com/microsoft/kiota-abstractions-go/serialization" - kjson "github.com/microsoft/kiota-serialization-json-go" - "github.com/microsoftgraph/msgraph-sdk-go/models" - - betamodels "github.com/alcionai/corso/src/internal/connector/graph/betasdk/models" -) - -// CreateFromBytes helper function to initialize m365 object form bytes. -// @param bytes -> source, createFunc -> abstract function for initialization -func CreateFromBytes(bytes []byte, createFunc serialization.ParsableFactory) (serialization.Parsable, error) { - parseNode, err := kjson.NewJsonParseNodeFactory().GetRootParseNode("application/json", bytes) - if err != nil { - return nil, clues.Wrap(err, "deserializing bytes into base m365 object") - } - - anObject, err := parseNode.GetObjectValue(createFunc) - if err != nil { - return nil, clues.Wrap(err, "parsing m365 object factory") - } - - return anObject, nil -} - -// CreateMessageFromBytes function to transform bytes into Messageable object -func CreateMessageFromBytes(bytes []byte) (models.Messageable, error) { - aMessage, err := CreateFromBytes(bytes, models.CreateMessageFromDiscriminatorValue) - if err != nil { - return nil, clues.Wrap(err, "deserializing bytes to exchange message") - } - - message := aMessage.(models.Messageable) - - return message, nil -} - -// CreateContactFromBytes function to transform bytes into Contactable object -// Error returned if ParsableFactory function does not accept given bytes -func CreateContactFromBytes(bytes []byte) (models.Contactable, error) { - parsable, err := CreateFromBytes(bytes, models.CreateContactFromDiscriminatorValue) - if err != nil { - return nil, clues.Wrap(err, "deserializing bytes to exchange contact") - } - - contact := parsable.(models.Contactable) - - return contact, nil -} - -// CreateEventFromBytes transforms given bytes into models.Eventable object -func CreateEventFromBytes(bytes []byte) (models.Eventable, error) { - parsable, err := CreateFromBytes(bytes, models.CreateEventFromDiscriminatorValue) - if err != nil { - return nil, clues.Wrap(err, "deserializing bytes to exchange event") - } - - event := parsable.(models.Eventable) - - return event, nil -} - -// CreateListFromBytes transforms given bytes into models.Listable object -func CreateListFromBytes(bytes []byte) (models.Listable, error) { - parsable, err := CreateFromBytes(bytes, models.CreateListFromDiscriminatorValue) - if err != nil { - return nil, clues.Wrap(err, "deserializing bytes to sharepoint list") - } - - list := parsable.(models.Listable) - - return list, nil -} - -// CreatePageFromBytes transforms given bytes in models.SitePageable object -func CreatePageFromBytes(bytes []byte) (betamodels.SitePageable, error) { - parsable, err := CreateFromBytes(bytes, betamodels.CreateSitePageFromDiscriminatorValue) - if err != nil { - return nil, clues.Wrap(err, "deserializing bytes to sharepoint page") - } - - page := parsable.(betamodels.SitePageable) - - return page, nil -} diff --git a/src/internal/connector/support/m365Support_test.go b/src/internal/connector/support/m365Support_test.go deleted file mode 100644 index f35761781..000000000 --- a/src/internal/connector/support/m365Support_test.go +++ /dev/null @@ -1,241 +0,0 @@ -package support - -import ( - "testing" - - "github.com/alcionai/clues" - kioser "github.com/microsoft/kiota-serialization-json-go" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - - exchMock "github.com/alcionai/corso/src/internal/connector/exchange/mock" - bmodels "github.com/alcionai/corso/src/internal/connector/graph/betasdk/models" - spMock "github.com/alcionai/corso/src/internal/connector/sharepoint/mock" - "github.com/alcionai/corso/src/internal/tester" -) - -type DataSupportSuite struct { - tester.Suite -} - -func TestDataSupportSuite(t *testing.T) { - suite.Run(t, &DataSupportSuite{Suite: tester.NewUnitSuite(t)}) -} - -var ( - empty = "Empty Bytes" - invalid = "Invalid Bytes" -) - -// TestCreateMessageFromBytes verifies approved mockdata bytes can -// be successfully transformed into M365 Message data. -func (suite *DataSupportSuite) TestCreateMessageFromBytes() { - table := []struct { - name string - byteArray []byte - checkError assert.ErrorAssertionFunc - checkObject assert.ValueAssertionFunc - }{ - { - name: "Empty Bytes", - byteArray: make([]byte, 0), - checkError: assert.Error, - checkObject: assert.Nil, - }, - { - name: "aMessage bytes", - byteArray: exchMock.MessageBytes("m365 mail support test"), - checkError: assert.NoError, - checkObject: assert.NotNil, - }, - } - for _, test := range table { - suite.Run(test.name, func() { - t := suite.T() - - result, err := CreateMessageFromBytes(test.byteArray) - test.checkError(t, err, clues.ToCore(err)) - test.checkObject(t, result) - }) - } -} - -// TestCreateContactFromBytes verifies behavior of CreateContactFromBytes -// by ensuring correct error and object output. -func (suite *DataSupportSuite) TestCreateContactFromBytes() { - table := []struct { - name string - byteArray []byte - checkError assert.ErrorAssertionFunc - isNil assert.ValueAssertionFunc - }{ - { - name: empty, - byteArray: make([]byte, 0), - checkError: assert.Error, - isNil: assert.Nil, - }, - { - name: invalid, - byteArray: []byte("A random sentence doesn't make an object"), - checkError: assert.Error, - isNil: assert.Nil, - }, - { - name: "Valid Contact", - byteArray: exchMock.ContactBytes("Support Test"), - checkError: assert.NoError, - isNil: assert.NotNil, - }, - } - for _, test := range table { - suite.Run(test.name, func() { - t := suite.T() - - result, err := CreateContactFromBytes(test.byteArray) - test.checkError(t, err, clues.ToCore(err)) - test.isNil(t, result) - }) - } -} - -func (suite *DataSupportSuite) TestCreateEventFromBytes() { - tests := []struct { - name string - byteArray []byte - checkError assert.ErrorAssertionFunc - isNil assert.ValueAssertionFunc - }{ - { - name: empty, - byteArray: make([]byte, 0), - checkError: assert.Error, - isNil: assert.Nil, - }, - { - name: invalid, - byteArray: []byte("Invalid byte stream \"subject:\" Not going to work"), - checkError: assert.Error, - isNil: assert.Nil, - }, - { - name: "Valid Event", - byteArray: exchMock.EventBytes("Event Test"), - checkError: assert.NoError, - isNil: assert.NotNil, - }, - } - for _, test := range tests { - suite.Run(test.name, func() { - t := suite.T() - - result, err := CreateEventFromBytes(test.byteArray) - test.checkError(t, err, clues.ToCore(err)) - test.isNil(t, result) - }) - } -} - -func (suite *DataSupportSuite) TestCreateListFromBytes() { - listBytes, err := spMock.ListBytes("DataSupportSuite") - require.NoError(suite.T(), err) - - tests := []struct { - name string - byteArray []byte - checkError assert.ErrorAssertionFunc - isNil assert.ValueAssertionFunc - }{ - { - name: empty, - byteArray: make([]byte, 0), - checkError: assert.Error, - isNil: assert.Nil, - }, - { - name: invalid, - byteArray: []byte("Invalid byte stream \"subject:\" Not going to work"), - checkError: assert.Error, - isNil: assert.Nil, - }, - { - name: "Valid List", - byteArray: listBytes, - checkError: assert.NoError, - isNil: assert.NotNil, - }, - } - - for _, test := range tests { - suite.Run(test.name, func() { - t := suite.T() - - result, err := CreateListFromBytes(test.byteArray) - test.checkError(t, err, clues.ToCore(err)) - test.isNil(t, result) - }) - } -} - -func (suite *DataSupportSuite) TestCreatePageFromBytes() { - tests := []struct { - name string - checkError assert.ErrorAssertionFunc - isNil assert.ValueAssertionFunc - getBytes func(t *testing.T) []byte - }{ - { - empty, - assert.Error, - assert.Nil, - func(t *testing.T) []byte { - return make([]byte, 0) - }, - }, - { - invalid, - assert.Error, - assert.Nil, - func(t *testing.T) []byte { - return []byte("snarf") - }, - }, - { - "Valid Page", - assert.NoError, - assert.NotNil, - func(t *testing.T) []byte { - pg := bmodels.NewSitePage() - title := "Tested" - pg.SetTitle(&title) - pg.SetName(&title) - pg.SetWebUrl(&title) - - writer := kioser.NewJsonSerializationWriter() - err := writer.WriteObjectValue("", pg) - require.NoError(t, err, clues.ToCore(err)) - - byteArray, err := writer.GetSerializedContent() - require.NoError(t, err, clues.ToCore(err)) - - return byteArray - }, - }, - } - - for _, test := range tests { - suite.Run(test.name, func() { - t := suite.T() - - result, err := CreatePageFromBytes(test.getBytes(t)) - test.checkError(t, err) - test.isNil(t, result) - if result != nil { - assert.Equal(t, "Tested", *result.GetName(), "name") - assert.Equal(t, "Tested", *result.GetTitle(), "title") - assert.Equal(t, "Tested", *result.GetWebUrl(), "webURL") - } - }) - } -} diff --git a/src/internal/operations/backup_integration_test.go b/src/internal/operations/backup_integration_test.go index 3f8c5f242..df32d3fb5 100644 --- a/src/internal/operations/backup_integration_test.go +++ b/src/internal/operations/backup_integration_test.go @@ -29,7 +29,6 @@ import ( "github.com/alcionai/corso/src/internal/connector/onedrive" odConsts "github.com/alcionai/corso/src/internal/connector/onedrive/consts" "github.com/alcionai/corso/src/internal/connector/onedrive/metadata" - "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/events" evmock "github.com/alcionai/corso/src/internal/events/mock" @@ -1136,7 +1135,7 @@ func testExchangeContinuousBackups(suite *BackupOpIntegrationSuite, toggles cont switch category { case path.EmailCategory: _, itemData := generateItemData(t, category, uidn.ID(), mailDBF) - body, err := support.CreateMessageFromBytes(itemData) + body, err := api.BytesToMessageable(itemData) require.NoErrorf(t, err, "transforming mail bytes to messageable: %+v", clues.ToCore(err)) itm, err := ac.Mail().PostItem(ctx, uidn.ID(), containerID, body) @@ -1149,7 +1148,7 @@ func testExchangeContinuousBackups(suite *BackupOpIntegrationSuite, toggles cont case path.ContactsCategory: _, itemData := generateItemData(t, category, uidn.ID(), contactDBF) - body, err := support.CreateContactFromBytes(itemData) + body, err := api.BytesToContactable(itemData) require.NoErrorf(t, err, "transforming contact bytes to contactable: %+v", clues.ToCore(err)) itm, err := ac.Contacts().PostItem(ctx, uidn.ID(), containerID, body) @@ -1162,7 +1161,7 @@ func testExchangeContinuousBackups(suite *BackupOpIntegrationSuite, toggles cont case path.EventsCategory: _, itemData := generateItemData(t, category, uidn.ID(), eventDBF) - body, err := support.CreateEventFromBytes(itemData) + body, err := api.BytesToEventable(itemData) require.NoErrorf(t, err, "transforming event bytes to eventable: %+v", clues.ToCore(err)) itm, err := ac.Events().PostItem(ctx, uidn.ID(), containerID, body) diff --git a/src/pkg/services/m365/api/client_test.go b/src/pkg/services/m365/api/client_test.go index 196cc2322..6348f12f4 100644 --- a/src/pkg/services/m365/api/client_test.go +++ b/src/pkg/services/m365/api/client_test.go @@ -11,7 +11,6 @@ import ( exchMock "github.com/alcionai/corso/src/internal/connector/exchange/mock" "github.com/alcionai/corso/src/internal/connector/graph" - "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/account" ) @@ -67,7 +66,7 @@ func (suite *ExchangeServiceSuite) TestHasAttachments() { "This is testing", "This is testing", ) - message, err := support.CreateMessageFromBytes(byteArray) + message, err := BytesToMessageable(byteArray) require.NoError(t, err, clues.ToCore(err)) return message.GetBody() }, @@ -77,7 +76,7 @@ func (suite *ExchangeServiceSuite) TestHasAttachments() { hasAttachment: assert.True, getBodyable: func(t *testing.T) models.ItemBodyable { byteArray := exchMock.MessageWithOneDriveAttachment("Test legacy") - message, err := support.CreateMessageFromBytes(byteArray) + message, err := BytesToMessageable(byteArray) require.NoError(t, err, clues.ToCore(err)) return message.GetBody() }, diff --git a/src/pkg/services/m365/api/contacts.go b/src/pkg/services/m365/api/contacts.go index 1bfdad984..8588711f8 100644 --- a/src/pkg/services/m365/api/contacts.go +++ b/src/pkg/services/m365/api/contacts.go @@ -38,13 +38,17 @@ type Contacts struct { // If successful, returns the created folder object. func (c Contacts) CreateContactFolder( ctx context.Context, - user, folderName string, + userID, containerName string, ) (models.ContactFolderable, error) { - requestBody := models.NewContactFolder() - temp := folderName - requestBody.SetDisplayName(&temp) + body := models.NewContactFolder() + body.SetDisplayName(ptr.To(containerName)) - mdl, err := c.Stable.Client().Users().ByUserId(user).ContactFolders().Post(ctx, requestBody, nil) + mdl, err := c.Stable. + Client(). + Users(). + ByUserId(userID). + ContactFolders(). + Post(ctx, body, nil) if err != nil { return nil, graph.Wrap(ctx, err, "creating contact folder") } @@ -55,7 +59,7 @@ func (c Contacts) CreateContactFolder( // DeleteContainer deletes the ContactFolder associated with the M365 ID if permissions are valid. func (c Contacts) DeleteContainer( ctx context.Context, - user, folderID string, + userID, containerID string, ) error { // deletes require unique http clients // https://github.com/alcionai/corso/issues/2707 @@ -64,7 +68,13 @@ func (c Contacts) DeleteContainer( return graph.Stack(ctx, err) } - err = srv.Client().Users().ByUserId(user).ContactFolders().ByContactFolderId(folderID).Delete(ctx, nil) + err = srv. + Client(). + Users(). + ByUserId(userID). + ContactFolders(). + ByContactFolderId(containerID). + Delete(ctx, nil) if err != nil { return graph.Stack(ctx, err) } @@ -79,18 +89,14 @@ func (c Contacts) GetFolder( ctx context.Context, userID, containerID string, ) (models.ContactFolderable, error) { - service, err := c.Service() - if err != nil { - return nil, graph.Stack(ctx, err) - } - config := &users.ItemContactFoldersContactFolderItemRequestBuilderGetRequestConfiguration{ QueryParameters: &users.ItemContactFoldersContactFolderItemRequestBuilderGetQueryParameters{ Select: idAnd(displayName, parentFolderID), }, } - resp, err := service.Client(). + resp, err := c.Stable. + Client(). Users(). ByUserId(userID). ContactFolders(). @@ -106,9 +112,9 @@ func (c Contacts) GetFolder( // interface-compliant wrapper of GetFolder func (c Contacts) GetContainerByID( ctx context.Context, - userID, dirID string, + userID, containerID string, ) (graph.Container, error) { - return c.GetFolder(ctx, userID, dirID) + return c.GetFolder(ctx, userID, containerID) } func (c Contacts) PatchFolder( @@ -116,12 +122,8 @@ func (c Contacts) PatchFolder( userID, containerID string, body models.ContactFolderable, ) error { - service, err := c.Service() - if err != nil { - return graph.Stack(ctx, err) - } - - _, err = service.Client(). + _, err := c.Stable. + Client(). Users(). ByUserId(userID). ContactFolders(). @@ -145,15 +147,10 @@ func (c Contacts) PatchFolder( // not contain historical data. func (c Contacts) EnumerateContainers( ctx context.Context, - userID, baseDirID string, + userID, baseContainerID string, fn func(graph.CachedContainer) error, errs *fault.Bus, ) error { - service, err := c.Service() - if err != nil { - return graph.Stack(ctx, err) - } - config := &users.ItemContactFoldersItemChildFoldersRequestBuilderGetRequestConfiguration{ QueryParameters: &users.ItemContactFoldersItemChildFoldersRequestBuilderGetQueryParameters{ Select: idAnd(displayName, parentFolderID), @@ -161,11 +158,12 @@ func (c Contacts) EnumerateContainers( } el := errs.Local() - builder := service.Client(). + builder := c.Stable. + Client(). Users(). ByUserId(userID). ContactFolders(). - ByContactFolderId(baseDirID). + ByContactFolderId(baseContainerID). ChildFolders() for { @@ -205,7 +203,7 @@ func (c Contacts) EnumerateContainers( break } - builder = users.NewItemContactFoldersItemChildFoldersRequestBuilder(link, service.Adapter()) + builder = users.NewItemContactFoldersItemChildFoldersRequestBuilder(link, c.Stable.Adapter()) } return el.Failure() @@ -218,7 +216,7 @@ func (c Contacts) EnumerateContainers( // GetItem retrieves a Contactable item. func (c Contacts) GetItem( ctx context.Context, - user, itemID string, + userID, itemID string, immutableIDs bool, _ *fault.Bus, // no attachments to iterate over, so this goes unused ) (serialization.Parsable, *details.ExchangeInfo, error) { @@ -226,7 +224,13 @@ func (c Contacts) GetItem( Headers: newPreferHeaders(preferImmutableIDs(immutableIDs)), } - cont, err := c.Stable.Client().Users().ByUserId(user).Contacts().ByContactId(itemID).Get(ctx, options) + cont, err := c.Stable. + Client(). + Users(). + ByUserId(userID). + Contacts(). + ByContactId(itemID). + Get(ctx, options) if err != nil { return nil, nil, graph.Stack(ctx, err) } @@ -239,12 +243,8 @@ func (c Contacts) PostItem( userID, containerID string, body models.Contactable, ) (models.Contactable, error) { - service, err := c.Service() - if err != nil { - return nil, graph.Stack(ctx, err) - } - - itm, err := service.Client(). + itm, err := c.Stable. + Client(). Users(). ByUserId(userID). ContactFolders(). @@ -264,12 +264,13 @@ func (c Contacts) DeleteItem( ) error { // deletes require unique http clients // https://github.com/alcionai/corso/issues/2707 - service, err := c.Service() + srv, err := c.Service() if err != nil { return graph.Stack(ctx, err) } - err = service.Client(). + err = srv. + Client(). Users(). ByUserId(userID). Contacts(). @@ -297,7 +298,7 @@ type contactPager struct { func NewContactPager( ctx context.Context, gs graph.Servicer, - user, directoryID string, + userID, containerID string, immutableIDs bool, ) itemPager { config := &users.ItemContactFoldersItemContactsRequestBuilderGetRequestConfiguration{ @@ -307,11 +308,12 @@ func NewContactPager( Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize), preferImmutableIDs(immutableIDs)), } - builder := gs.Client(). + builder := gs. + Client(). Users(). - ByUserId(user). + ByUserId(userID). ContactFolders(). - ByContactFolderId(directoryID). + ByContactFolderId(containerID). Contacts() return &contactPager{gs, builder, config} @@ -345,8 +347,8 @@ var _ itemPager = &contactDeltaPager{} type contactDeltaPager struct { gs graph.Servicer - user string - directoryID string + userID string + containerID string builder *users.ItemContactFoldersItemContactsDeltaRequestBuilder options *users.ItemContactFoldersItemContactsDeltaRequestBuilderGetRequestConfiguration } @@ -354,18 +356,17 @@ type contactDeltaPager struct { func getContactDeltaBuilder( ctx context.Context, gs graph.Servicer, - user string, - directoryID string, + userID, containerID string, options *users.ItemContactFoldersItemContactsDeltaRequestBuilderGetRequestConfiguration, ) *users.ItemContactFoldersItemContactsDeltaRequestBuilder { - builder := gs.Client().Users().ByUserId(user).ContactFolders().ByContactFolderId(directoryID).Contacts().Delta() + builder := gs.Client().Users().ByUserId(userID).ContactFolders().ByContactFolderId(containerID).Contacts().Delta() return builder } func NewContactDeltaPager( ctx context.Context, gs graph.Servicer, - user, directoryID, deltaURL string, + userID, containerID, oldDelta string, immutableIDs bool, ) itemPager { options := &users.ItemContactFoldersItemContactsDeltaRequestBuilderGetRequestConfiguration{ @@ -376,13 +377,13 @@ func NewContactDeltaPager( } var builder *users.ItemContactFoldersItemContactsDeltaRequestBuilder - if deltaURL != "" { - builder = users.NewItemContactFoldersItemContactsDeltaRequestBuilder(deltaURL, gs.Adapter()) + if oldDelta != "" { + builder = users.NewItemContactFoldersItemContactsDeltaRequestBuilder(oldDelta, gs.Adapter()) } else { - builder = getContactDeltaBuilder(ctx, gs, user, directoryID, options) + builder = getContactDeltaBuilder(ctx, gs, userID, containerID, options) } - return &contactDeltaPager{gs, user, directoryID, builder, options} + return &contactDeltaPager{gs, userID, containerID, builder, options} } func (p *contactDeltaPager) getPage(ctx context.Context) (DeltaPageLinker, error) { @@ -399,7 +400,7 @@ func (p *contactDeltaPager) setNext(nextLink string) { } func (p *contactDeltaPager) reset(ctx context.Context) { - p.builder = getContactDeltaBuilder(ctx, p.gs, p.user, p.directoryID, p.options) + p.builder = getContactDeltaBuilder(ctx, p.gs, p.userID, p.containerID, p.options) } func (p *contactDeltaPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { @@ -408,35 +409,38 @@ func (p *contactDeltaPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { func (c Contacts) GetAddedAndRemovedItemIDs( ctx context.Context, - user, directoryID, oldDelta string, + userID, containerID, oldDelta string, immutableIDs bool, canMakeDeltaQueries bool, ) ([]string, []string, DeltaUpdate, error) { - service, err := c.Service() - if err != nil { - return nil, nil, DeltaUpdate{}, graph.Stack(ctx, err) - } - ctx = clues.Add( ctx, "category", selectors.ExchangeContact, - "container_id", directoryID) + "container_id", containerID) - pager := NewContactPager(ctx, service, user, directoryID, immutableIDs) - deltaPager := NewContactDeltaPager(ctx, service, user, directoryID, oldDelta, immutableIDs) + pager := NewContactPager(ctx, c.Stable, userID, containerID, immutableIDs) + deltaPager := NewContactDeltaPager(ctx, c.Stable, userID, containerID, oldDelta, immutableIDs) - return getAddedAndRemovedItemIDs(ctx, service, pager, deltaPager, oldDelta, canMakeDeltaQueries) + return getAddedAndRemovedItemIDs(ctx, c.Stable, pager, deltaPager, oldDelta, canMakeDeltaQueries) } // --------------------------------------------------------------------------- // Serialization // --------------------------------------------------------------------------- -// Serialize rserializes the item into a byte slice. +func BytesToContactable(bytes []byte) (models.Contactable, error) { + v, err := createFromBytes(bytes, models.CreateContactFromDiscriminatorValue) + if err != nil { + return nil, clues.Wrap(err, "deserializing bytes to contact") + } + + return v.(models.Contactable), nil +} + func (c Contacts) Serialize( ctx context.Context, item serialization.Parsable, - user, itemID string, + userID, itemID string, ) ([]byte, error) { contact, ok := item.(models.Contactable) if !ok { @@ -444,15 +448,11 @@ func (c Contacts) Serialize( } ctx = clues.Add(ctx, "item_id", ptr.Val(contact.GetId())) - - var ( - err error - writer = kjson.NewJsonSerializationWriter() - ) + writer := kjson.NewJsonSerializationWriter() defer writer.Close() - if err = writer.WriteObjectValue("", contact); err != nil { + if err := writer.WriteObjectValue("", contact); err != nil { return nil, graph.Stack(ctx, err) } diff --git a/src/pkg/services/m365/api/contacts_test.go b/src/pkg/services/m365/api/contacts_test.go index 7397f486e..13b5330cf 100644 --- a/src/pkg/services/m365/api/contacts_test.go +++ b/src/pkg/services/m365/api/contacts_test.go @@ -4,10 +4,12 @@ import ( "testing" "time" + "github.com/alcionai/clues" "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + exchMock "github.com/alcionai/corso/src/internal/connector/exchange/mock" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/backup/details" ) @@ -66,3 +68,40 @@ func (suite *ContactsAPIUnitSuite) TestContactInfo() { }) } } + +func (suite *ContactsAPIUnitSuite) TestBytesToContactable() { + table := []struct { + name string + byteArray []byte + checkError assert.ErrorAssertionFunc + isNil assert.ValueAssertionFunc + }{ + { + name: "empty bytes", + byteArray: make([]byte, 0), + checkError: assert.Error, + isNil: assert.Nil, + }, + { + name: "invalid bytes", + byteArray: []byte("A random sentence doesn't make an object"), + checkError: assert.Error, + isNil: assert.Nil, + }, + { + name: "Valid Contact", + byteArray: exchMock.ContactBytes("Support Test"), + checkError: assert.NoError, + isNil: assert.NotNil, + }, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + result, err := BytesToContactable(test.byteArray) + test.checkError(t, err, clues.ToCore(err)) + test.isNil(t, result) + }) + } +} diff --git a/src/pkg/services/m365/api/events.go b/src/pkg/services/m365/api/events.go index f0e749f81..bd63490f8 100644 --- a/src/pkg/services/m365/api/events.go +++ b/src/pkg/services/m365/api/events.go @@ -42,12 +42,17 @@ type Events struct { // Reference: https://docs.microsoft.com/en-us/graph/api/user-post-calendars?view=graph-rest-1.0&tabs=go func (c Events) CreateCalendar( ctx context.Context, - user, calendarName string, + userID, containerName string, ) (models.Calendarable, error) { - requestbody := models.NewCalendar() - requestbody.SetName(&calendarName) + body := models.NewCalendar() + body.SetName(&containerName) - mdl, err := c.Stable.Client().Users().ByUserId(user).Calendars().Post(ctx, requestbody, nil) + mdl, err := c.Stable. + Client(). + Users(). + ByUserId(userID). + Calendars(). + Post(ctx, body, nil) if err != nil { return nil, graph.Wrap(ctx, err, "creating calendar") } @@ -59,7 +64,7 @@ func (c Events) CreateCalendar( // Reference: https://docs.microsoft.com/en-us/graph/api/calendar-delete?view=graph-rest-1.0&tabs=go func (c Events) DeleteContainer( ctx context.Context, - user, calendarID string, + userID, containerID string, ) error { // deletes require unique http clients // https://github.com/alcionai/corso/issues/2707 @@ -68,7 +73,12 @@ func (c Events) DeleteContainer( return graph.Stack(ctx, err) } - err = srv.Client().Users().ByUserId(user).Calendars().ByCalendarId(calendarID).Delete(ctx, nil) + err = srv.Client(). + Users(). + ByUserId(userID). + Calendars(). + ByCalendarId(containerID). + Delete(ctx, nil) if err != nil { return graph.Stack(ctx, err) } @@ -83,18 +93,14 @@ func (c Events) GetCalendar( ctx context.Context, userID, containerID string, ) (models.Calendarable, error) { - service, err := c.Service() - if err != nil { - return nil, graph.Stack(ctx, err) - } - config := &users.ItemCalendarsCalendarItemRequestBuilderGetRequestConfiguration{ QueryParameters: &users.ItemCalendarsCalendarItemRequestBuilderGetQueryParameters{ Select: idAnd("name", "owner"), }, } - resp, err := service.Client(). + resp, err := c.Stable. + Client(). Users(). ByUserId(userID). Calendars(). @@ -110,9 +116,9 @@ func (c Events) GetCalendar( // interface-compliant wrapper of GetCalendar func (c Events) GetContainerByID( ctx context.Context, - userID, dirID string, + userID, containerID string, ) (graph.Container, error) { - cal, err := c.GetCalendar(ctx, userID, dirID) + cal, err := c.GetCalendar(ctx, userID, containerID) if err != nil { return nil, err } @@ -123,18 +129,23 @@ func (c Events) GetContainerByID( // GetContainerByName fetches a calendar by name func (c Events) GetContainerByName( ctx context.Context, - userID, name string, + userID, containerName string, ) (models.Calendarable, error) { - filter := fmt.Sprintf("name eq '%s'", name) + filter := fmt.Sprintf("name eq '%s'", containerName) options := &users.ItemCalendarsRequestBuilderGetRequestConfiguration{ QueryParameters: &users.ItemCalendarsRequestBuilderGetQueryParameters{ Filter: &filter, }, } - ctx = clues.Add(ctx, "calendar_name", name) + ctx = clues.Add(ctx, "calendar_name", containerName) - resp, err := c.Stable.Client().Users().ByUserId(userID).Calendars().Get(ctx, options) + resp, err := c.Stable. + Client(). + Users(). + ByUserId(userID). + Calendars(). + Get(ctx, options) if err != nil { return nil, graph.Stack(ctx, err).WithClues(ctx) } @@ -164,12 +175,8 @@ func (c Events) PatchCalendar( userID, containerID string, body models.Calendarable, ) error { - service, err := c.Service() - if err != nil { - return graph.Stack(ctx, err) - } - - _, err = service.Client(). + _, err := c.Stable. + Client(). Users(). ByUserId(userID). Calendars(). @@ -193,15 +200,10 @@ func (c Events) PatchCalendar( // not contain historical data. func (c Events) EnumerateContainers( ctx context.Context, - userID, baseDirID string, + userID, baseContainerID string, fn func(graph.CachedContainer) error, errs *fault.Bus, ) error { - service, err := c.Service() - if err != nil { - return graph.Stack(ctx, err) - } - var ( el = errs.Local() config = &users.ItemCalendarsRequestBuilderGetRequestConfiguration{ @@ -209,7 +211,8 @@ func (c Events) EnumerateContainers( Select: idAnd("name"), }, } - builder = service.Client(). + builder = c.Stable. + Client(). Users(). ByUserId(userID). Calendars() @@ -256,7 +259,7 @@ func (c Events) EnumerateContainers( break } - builder = users.NewItemCalendarsRequestBuilder(link, service.Adapter()) + builder = users.NewItemCalendarsRequestBuilder(link, c.Stable.Adapter()) } return el.Failure() @@ -273,7 +276,7 @@ const ( // GetItem retrieves an Eventable item. func (c Events) GetItem( ctx context.Context, - user, itemID string, + userID, itemID string, immutableIDs bool, errs *fault.Bus, ) (serialization.Parsable, *details.ExchangeInfo, error) { @@ -285,9 +288,10 @@ func (c Events) GetItem( } ) - event, err = c.Stable.Client(). + event, err = c.Stable. + Client(). Users(). - ByUserId(user). + ByUserId(userID). Events(). ByEventId(itemID). Get(ctx, config) @@ -306,7 +310,7 @@ func (c Events) GetItem( attached, err := c.LargeItem. Client(). Users(). - ByUserId(user). + ByUserId(userID). Events(). ByEventId(itemID). Attachments(). @@ -326,12 +330,8 @@ func (c Events) PostItem( userID, containerID string, body models.Eventable, ) (models.Eventable, error) { - service, err := c.Service() - if err != nil { - return nil, graph.Stack(ctx, err) - } - - itm, err := service.Client(). + itm, err := c.Stable. + Client(). Users(). ByUserId(userID). Calendars(). @@ -351,12 +351,13 @@ func (c Events) DeleteItem( ) error { // deletes require unique http clients // https://github.com/alcionai/corso/issues/2707 - service, err := c.Service() + srv, err := c.Service() if err != nil { return graph.Stack(ctx, err) } - err = service.Client(). + err = srv. + Client(). Users(). ByUserId(userID). Events(). @@ -374,12 +375,8 @@ func (c Events) PostSmallAttachment( userID, containerID, parentItemID string, body models.Attachmentable, ) error { - service, err := c.Service() - if err != nil { - return graph.Stack(ctx, err) - } - - _, err = service.Client(). + _, err := c.Stable. + Client(). Users(). ByUserId(userID). Calendars(). @@ -397,7 +394,7 @@ func (c Events) PostSmallAttachment( func (c Events) PostLargeAttachment( ctx context.Context, - userID, containerID, parentItemID, name string, + userID, containerID, parentItemID, itemName string, size int64, body models.Attachmentable, ) (models.UploadSessionable, error) { @@ -407,7 +404,7 @@ func (c Events) PostLargeAttachment( } session := users.NewItemCalendarEventsItemAttachmentsCreateUploadSessionPostRequestBody() - session.SetAttachmentItem(makeSessionAttachment(name, size)) + session.SetAttachmentItem(makeSessionAttachment(itemName, size)) us, err := c.LargeItem. Client(). @@ -451,14 +448,20 @@ type eventPager struct { func NewEventPager( ctx context.Context, gs graph.Servicer, - user, calendarID string, + userID, containerID string, immutableIDs bool, ) (itemPager, error) { options := &users.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration{ Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize), preferImmutableIDs(immutableIDs)), } - builder := gs.Client().Users().ByUserId(user).Calendars().ByCalendarId(calendarID).Events() + builder := gs. + Client(). + Users(). + ByUserId(userID). + Calendars(). + ByCalendarId(containerID). + Events() return &eventPager{gs, builder, options}, nil } @@ -490,17 +493,17 @@ func (p *eventPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { var _ itemPager = &eventDeltaPager{} type eventDeltaPager struct { - gs graph.Servicer - user string - calendarID string - builder *users.ItemCalendarsItemEventsDeltaRequestBuilder - options *users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration + gs graph.Servicer + userID string + containerID string + builder *users.ItemCalendarsItemEventsDeltaRequestBuilder + options *users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration } func NewEventDeltaPager( ctx context.Context, gs graph.Servicer, - user, calendarID, deltaURL string, + userID, containerID, oldDelta string, immutableIDs bool, ) (itemPager, error) { options := &users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration{ @@ -509,20 +512,19 @@ func NewEventDeltaPager( var builder *users.ItemCalendarsItemEventsDeltaRequestBuilder - if deltaURL == "" { - builder = getEventDeltaBuilder(ctx, gs, user, calendarID, options) + if oldDelta == "" { + builder = getEventDeltaBuilder(ctx, gs, userID, containerID, options) } else { - builder = users.NewItemCalendarsItemEventsDeltaRequestBuilder(deltaURL, gs.Adapter()) + builder = users.NewItemCalendarsItemEventsDeltaRequestBuilder(oldDelta, gs.Adapter()) } - return &eventDeltaPager{gs, user, calendarID, builder, options}, nil + return &eventDeltaPager{gs, userID, containerID, builder, options}, nil } func getEventDeltaBuilder( ctx context.Context, gs graph.Servicer, - user string, - calendarID string, + userID, containerID string, options *users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration, ) *users.ItemCalendarsItemEventsDeltaRequestBuilder { // Graph SDK only supports delta queries against events on the beta version, so we're @@ -533,7 +535,7 @@ func getEventDeltaBuilder( // 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) + rawURL := fmt.Sprintf(eventBetaDeltaURLTemplate, userID, containerID) builder := users.NewItemCalendarsItemEventsDeltaRequestBuilder(rawURL, gs.Adapter()) return builder @@ -553,7 +555,7 @@ func (p *eventDeltaPager) setNext(nextLink string) { } func (p *eventDeltaPager) reset(ctx context.Context) { - p.builder = getEventDeltaBuilder(ctx, p.gs, p.user, p.calendarID, p.options) + p.builder = getEventDeltaBuilder(ctx, p.gs, p.userID, p.containerID, p.options) } func (p *eventDeltaPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { @@ -562,41 +564,42 @@ func (p *eventDeltaPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { func (c Events) GetAddedAndRemovedItemIDs( ctx context.Context, - user, calendarID, oldDelta string, + userID, containerID, oldDelta string, immutableIDs bool, canMakeDeltaQueries bool, ) ([]string, []string, DeltaUpdate, error) { - service, err := c.Service() - if err != nil { - return nil, nil, DeltaUpdate{}, err - } + ctx = clues.Add(ctx, "container_id", containerID) - ctx = clues.Add( - ctx, - "container_id", calendarID) - - pager, err := NewEventPager(ctx, service, user, calendarID, immutableIDs) + pager, err := NewEventPager(ctx, c.Stable, userID, containerID, immutableIDs) if err != nil { return nil, nil, DeltaUpdate{}, graph.Wrap(ctx, err, "creating non-delta pager") } - deltaPager, err := NewEventDeltaPager(ctx, service, user, calendarID, oldDelta, immutableIDs) + deltaPager, err := NewEventDeltaPager(ctx, c.Stable, userID, containerID, oldDelta, immutableIDs) if err != nil { return nil, nil, DeltaUpdate{}, graph.Wrap(ctx, err, "creating delta pager") } - return getAddedAndRemovedItemIDs(ctx, service, pager, deltaPager, oldDelta, canMakeDeltaQueries) + return getAddedAndRemovedItemIDs(ctx, c.Stable, pager, deltaPager, oldDelta, canMakeDeltaQueries) } // --------------------------------------------------------------------------- // Serialization // --------------------------------------------------------------------------- -// Serialize transforms the event into a byte slice. +func BytesToEventable(body []byte) (models.Eventable, error) { + v, err := createFromBytes(body, models.CreateEventFromDiscriminatorValue) + if err != nil { + return nil, clues.Wrap(err, "deserializing bytes to event") + } + + return v.(models.Eventable), nil +} + func (c Events) Serialize( ctx context.Context, item serialization.Parsable, - user, itemID string, + userID, itemID string, ) ([]byte, error) { event, ok := item.(models.Eventable) if !ok { @@ -605,14 +608,10 @@ func (c Events) Serialize( ctx = clues.Add(ctx, "item_id", ptr.Val(event.GetId())) - var ( - err error - writer = kjson.NewJsonSerializationWriter() - ) - + writer := kjson.NewJsonSerializationWriter() defer writer.Close() - if err = writer.WriteObjectValue("", event); err != nil { + if err := writer.WriteObjectValue("", event); err != nil { return nil, graph.Stack(ctx, err) } diff --git a/src/pkg/services/m365/api/events_test.go b/src/pkg/services/m365/api/events_test.go index 6939c67cf..9f185bacc 100644 --- a/src/pkg/services/m365/api/events_test.go +++ b/src/pkg/services/m365/api/events_test.go @@ -12,7 +12,6 @@ import ( "github.com/alcionai/corso/src/internal/common/dttm" exchMock "github.com/alcionai/corso/src/internal/connector/exchange/mock" - "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/backup/details" ) @@ -127,7 +126,7 @@ func (suite *EventsAPIUnitSuite) TestEventInfo() { 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) + event, err = BytesToEventable(bytes) ) require.NoError(suite.T(), err, clues.ToCore(err)) @@ -176,3 +175,40 @@ func (suite *EventsAPIUnitSuite) TestEventInfo() { }) } } + +func (suite *EventsAPIUnitSuite) TestBytesToEventable() { + tests := []struct { + name string + byteArray []byte + checkError assert.ErrorAssertionFunc + isNil assert.ValueAssertionFunc + }{ + { + name: "empty bytes", + byteArray: make([]byte, 0), + checkError: assert.Error, + isNil: assert.Nil, + }, + { + name: "invalid bytes", + byteArray: []byte("Invalid byte stream \"subject:\" Not going to work"), + checkError: assert.Error, + isNil: assert.Nil, + }, + { + name: "Valid Event", + byteArray: exchMock.EventBytes("Event Test"), + checkError: assert.NoError, + isNil: assert.NotNil, + }, + } + for _, test := range tests { + suite.Run(test.name, func() { + t := suite.T() + + result, err := BytesToEventable(test.byteArray) + test.checkError(t, err, clues.ToCore(err)) + test.isNil(t, result) + }) + } +} diff --git a/src/pkg/services/m365/api/item_serialization_test.go b/src/pkg/services/m365/api/item_serialization_test.go index a0f373a0a..ed871d5f3 100644 --- a/src/pkg/services/m365/api/item_serialization_test.go +++ b/src/pkg/services/m365/api/item_serialization_test.go @@ -12,7 +12,6 @@ import ( "github.com/stretchr/testify/suite" "github.com/alcionai/corso/src/internal/common/ptr" - "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/services/m365/api" ) @@ -50,7 +49,7 @@ func (suite *ItemSerializationUnitSuite) TestConcurrentItemSerialization() { return bs }, deserializeAndGetField: func(t *testing.T, bs []byte) string { - item, err := support.CreateMessageFromBytes(bs) + item, err := api.BytesToMessageable(bs) require.NoError( t, err, @@ -75,7 +74,7 @@ func (suite *ItemSerializationUnitSuite) TestConcurrentItemSerialization() { return bs }, deserializeAndGetField: func(t *testing.T, bs []byte) string { - item, err := support.CreateEventFromBytes(bs) + item, err := api.BytesToEventable(bs) require.NoError( t, err, @@ -100,7 +99,7 @@ func (suite *ItemSerializationUnitSuite) TestConcurrentItemSerialization() { return bs }, deserializeAndGetField: func(t *testing.T, bs []byte) string { - item, err := support.CreateContactFromBytes(bs) + item, err := api.BytesToContactable(bs) require.NoError( t, err, diff --git a/src/pkg/services/m365/api/mail.go b/src/pkg/services/m365/api/mail.go index f48ffce4b..ba275e762 100644 --- a/src/pkg/services/m365/api/mail.go +++ b/src/pkg/services/m365/api/mail.go @@ -45,14 +45,18 @@ type Mail struct { // Reference: https://docs.microsoft.com/en-us/graph/api/user-post-mailfolders?view=graph-rest-1.0&tabs=http func (c Mail) CreateMailFolder( ctx context.Context, - user, folder string, + userID, containerName string, ) (models.MailFolderable, error) { isHidden := false - requestBody := models.NewMailFolder() - requestBody.SetDisplayName(&folder) - requestBody.SetIsHidden(&isHidden) + body := models.NewMailFolder() + body.SetDisplayName(&containerName) + body.SetIsHidden(&isHidden) - mdl, err := c.Stable.Client().Users().ByUserId(user).MailFolders().Post(ctx, requestBody, nil) + mdl, err := c.Stable.Client(). + Users(). + ByUserId(userID). + MailFolders(). + Post(ctx, body, nil) if err != nil { return nil, graph.Wrap(ctx, err, "creating mail folder") } @@ -62,26 +66,21 @@ func (c Mail) CreateMailFolder( func (c Mail) CreateMailFolderWithParent( ctx context.Context, - user, folder, parentID string, + userID, containerName, parentContainerID string, ) (models.MailFolderable, error) { - service, err := c.Service() - if err != nil { - return nil, graph.Stack(ctx, err) - } - isHidden := false - requestBody := models.NewMailFolder() - requestBody.SetDisplayName(&folder) - requestBody.SetIsHidden(&isHidden) + body := models.NewMailFolder() + body.SetDisplayName(&containerName) + body.SetIsHidden(&isHidden) - mdl, err := service. + mdl, err := c.Stable. Client(). Users(). - ByUserId(user). + ByUserId(userID). MailFolders(). - ByMailFolderId(parentID). + ByMailFolderId(parentContainerID). ChildFolders(). - Post(ctx, requestBody, nil) + Post(ctx, body, nil) if err != nil { return nil, graph.Wrap(ctx, err, "creating nested mail folder") } @@ -93,7 +92,7 @@ func (c Mail) CreateMailFolderWithParent( // Reference: https://docs.microsoft.com/en-us/graph/api/mailfolder-delete?view=graph-rest-1.0&tabs=http func (c Mail) DeleteContainer( ctx context.Context, - user, folderID string, + userID, containerID string, ) error { // deletes require unique http clients // https://github.com/alcionai/corso/issues/2707 @@ -104,9 +103,9 @@ func (c Mail) DeleteContainer( err = srv.Client(). Users(). - ByUserId(user). + ByUserId(userID). MailFolders(). - ByMailFolderId(folderID). + ByMailFolderId(containerID). Delete(ctx, nil) if err != nil { return graph.Stack(ctx, err) @@ -122,18 +121,14 @@ func (c Mail) GetFolder( ctx context.Context, userID, containerID string, ) (models.MailFolderable, error) { - service, err := c.Service() - if err != nil { - return nil, graph.Stack(ctx, err) - } - config := &users.ItemMailFoldersMailFolderItemRequestBuilderGetRequestConfiguration{ QueryParameters: &users.ItemMailFoldersMailFolderItemRequestBuilderGetQueryParameters{ Select: idAnd(displayName, parentFolderID), }, } - resp, err := service.Client(). + resp, err := c.Stable. + Client(). Users(). ByUserId(userID). MailFolders(). @@ -149,9 +144,9 @@ func (c Mail) GetFolder( // interface-compliant wrapper of GetFolder func (c Mail) GetContainerByID( ctx context.Context, - userID, dirID string, + userID, containerID string, ) (graph.Container, error) { - return c.GetFolder(ctx, userID, dirID) + return c.GetFolder(ctx, userID, containerID) } func (c Mail) MoveContainer( @@ -159,12 +154,7 @@ func (c Mail) MoveContainer( userID, containerID string, body users.ItemMailFoldersItemMovePostRequestBodyable, ) error { - service, err := c.Service() - if err != nil { - return graph.Stack(ctx, err) - } - - _, err = service. + _, err := c.Stable. Client(). Users(). ByUserId(userID). @@ -184,12 +174,8 @@ func (c Mail) PatchFolder( userID, containerID string, body models.MailFolderable, ) error { - service, err := c.Service() - if err != nil { - return graph.Stack(ctx, err) - } - - _, err = service.Client(). + _, err := c.Stable. + Client(). Users(). ByUserId(userID). MailFolders(). @@ -211,9 +197,9 @@ type mailFolderPager struct { builder *users.ItemMailFoldersRequestBuilder } -func NewMailFolderPager(service graph.Servicer, user string) mailFolderPager { +func NewMailFolderPager(service graph.Servicer, userID string) mailFolderPager { // v1.0 non delta /mailFolders endpoint does not return any of the nested folders - rawURL := fmt.Sprintf(mailFoldersBetaURLTemplate, user) + rawURL := fmt.Sprintf(mailFoldersBetaURLTemplate, userID) builder := users.NewItemMailFoldersRequestBuilder(rawURL, service.Adapter()) return mailFolderPager{service, builder} @@ -250,18 +236,12 @@ func (p *mailFolderPager) valuesIn(pl PageLinker) ([]models.MailFolderable, erro // not contain historical data. func (c Mail) EnumerateContainers( ctx context.Context, - userID, baseDirID string, + userID, baseContainerID string, fn func(graph.CachedContainer) error, errs *fault.Bus, ) error { - service, err := c.Service() - if err != nil { - return graph.Stack(ctx, err) - } - el := errs.Local() - - pgr := NewMailFolderPager(service, userID) + pgr := NewMailFolderPager(c.Stable, userID) for { if el.Failure() != nil { @@ -319,7 +299,7 @@ func (c Mail) EnumerateContainers( // attachment is also downloaded. func (c Mail) GetItem( ctx context.Context, - user, itemID string, + userID, itemID string, immutableIDs bool, errs *fault.Bus, ) (serialization.Parsable, *details.ExchangeInfo, error) { @@ -331,9 +311,10 @@ func (c Mail) GetItem( } ) - mail, err := c.Stable.Client(). + mail, err := c.Stable. + Client(). Users(). - ByUserId(user). + ByUserId(userID). Messages(). ByMessageId(itemID). Get(ctx, config) @@ -363,7 +344,7 @@ func (c Mail) GetItem( attached, err := c.LargeItem. Client(). Users(). - ByUserId(user). + ByUserId(userID). Messages(). ByMessageId(itemID). Attachments(). @@ -396,7 +377,7 @@ func (c Mail) GetItem( attachments, err := c.LargeItem. Client(). Users(). - ByUserId(user). + ByUserId(userID). Messages(). ByMessageId(itemID). Attachments(). @@ -418,7 +399,7 @@ func (c Mail) GetItem( att, err := c.Stable. Client(). Users(). - ByUserId(user). + ByUserId(userID). Messages(). ByMessageId(itemID). Attachments(). @@ -444,12 +425,7 @@ func (c Mail) PostItem( userID, containerID string, body models.Messageable, ) (models.Messageable, error) { - service, err := c.Service() - if err != nil { - return nil, graph.Stack(ctx, err) - } - - itm, err := service. + itm, err := c.Stable. Client(). Users(). ByUserId(userID). @@ -474,12 +450,12 @@ func (c Mail) DeleteItem( ) error { // deletes require unique http clients // https://github.com/alcionai/corso/issues/2707 - service, err := c.Service() + srv, err := NewService(c.Credentials) if err != nil { return graph.Stack(ctx, err) } - err = service. + err = srv. Client(). Users(). ByUserId(userID). @@ -498,12 +474,7 @@ func (c Mail) PostSmallAttachment( userID, containerID, parentItemID string, body models.Attachmentable, ) error { - service, err := c.Service() - if err != nil { - return graph.Stack(ctx, err) - } - - _, err = service. + _, err := c.Stable. Client(). Users(). ByUserId(userID). @@ -522,7 +493,7 @@ func (c Mail) PostSmallAttachment( func (c Mail) PostLargeAttachment( ctx context.Context, - userID, containerID, parentItemID, name string, + userID, containerID, parentItemID, itemName string, size int64, body models.Attachmentable, ) (models.UploadSessionable, error) { @@ -532,7 +503,7 @@ func (c Mail) PostLargeAttachment( } session := users.NewItemMailFoldersItemMessagesItemAttachmentsCreateUploadSessionPostRequestBody() - session.SetAttachmentItem(makeSessionAttachment(name, size)) + session.SetAttachmentItem(makeSessionAttachment(itemName, size)) us, err := c.LargeItem. Client(). @@ -576,7 +547,7 @@ type mailPager struct { func NewMailPager( ctx context.Context, gs graph.Servicer, - user, directoryID string, + userID, containerID string, immutableIDs bool, ) itemPager { config := &users.ItemMailFoldersItemMessagesRequestBuilderGetRequestConfiguration{ @@ -586,11 +557,12 @@ func NewMailPager( Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize), preferImmutableIDs(immutableIDs)), } - builder := gs.Client(). + builder := gs. + Client(). Users(). - ByUserId(user). + ByUserId(userID). MailFolders(). - ByMailFolderId(directoryID). + ByMailFolderId(containerID). Messages() return &mailPager{gs, builder, config} @@ -624,8 +596,8 @@ var _ itemPager = &mailDeltaPager{} type mailDeltaPager struct { gs graph.Servicer - user string - directoryID string + userID string + containerID string builder *users.ItemMailFoldersItemMessagesDeltaRequestBuilder options *users.ItemMailFoldersItemMessagesDeltaRequestBuilderGetRequestConfiguration } @@ -633,15 +605,15 @@ type mailDeltaPager struct { func getMailDeltaBuilder( ctx context.Context, gs graph.Servicer, - user string, - directoryID string, + user, containerID string, options *users.ItemMailFoldersItemMessagesDeltaRequestBuilderGetRequestConfiguration, ) *users.ItemMailFoldersItemMessagesDeltaRequestBuilder { - builder := gs.Client(). + builder := gs. + Client(). Users(). ByUserId(user). MailFolders(). - ByMailFolderId(directoryID). + ByMailFolderId(containerID). Messages(). Delta() @@ -651,7 +623,7 @@ func getMailDeltaBuilder( func NewMailDeltaPager( ctx context.Context, gs graph.Servicer, - user, directoryID, oldDelta string, + userID, containerID, oldDelta string, immutableIDs bool, ) itemPager { config := &users.ItemMailFoldersItemMessagesDeltaRequestBuilderGetRequestConfiguration{ @@ -666,10 +638,10 @@ func NewMailDeltaPager( if len(oldDelta) > 0 { builder = users.NewItemMailFoldersItemMessagesDeltaRequestBuilder(oldDelta, gs.Adapter()) } else { - builder = getMailDeltaBuilder(ctx, gs, user, directoryID, config) + builder = getMailDeltaBuilder(ctx, gs, userID, containerID, config) } - return &mailDeltaPager{gs, user, directoryID, builder, config} + return &mailDeltaPager{gs, userID, containerID, builder, config} } func (p *mailDeltaPager) getPage(ctx context.Context) (DeltaPageLinker, error) { @@ -686,11 +658,12 @@ func (p *mailDeltaPager) setNext(nextLink string) { } func (p *mailDeltaPager) reset(ctx context.Context) { - p.builder = p.gs.Client(). + p.builder = p.gs. + Client(). Users(). - ByUserId(p.user). + ByUserId(p.userID). MailFolders(). - ByMailFolderId(p.directoryID). + ByMailFolderId(p.containerID). Messages(). Delta() } @@ -701,31 +674,34 @@ func (p *mailDeltaPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { func (c Mail) GetAddedAndRemovedItemIDs( ctx context.Context, - user, directoryID, oldDelta string, + userID, containerID, oldDelta string, immutableIDs bool, canMakeDeltaQueries bool, ) ([]string, []string, DeltaUpdate, error) { - service, err := c.Service() - if err != nil { - return nil, nil, DeltaUpdate{}, err - } - ctx = clues.Add( ctx, "category", selectors.ExchangeMail, - "container_id", directoryID) + "container_id", containerID) - pager := NewMailPager(ctx, service, user, directoryID, immutableIDs) - deltaPager := NewMailDeltaPager(ctx, service, user, directoryID, oldDelta, immutableIDs) + pager := NewMailPager(ctx, c.Stable, userID, containerID, immutableIDs) + deltaPager := NewMailDeltaPager(ctx, c.Stable, userID, containerID, oldDelta, immutableIDs) - return getAddedAndRemovedItemIDs(ctx, service, pager, deltaPager, oldDelta, canMakeDeltaQueries) + return getAddedAndRemovedItemIDs(ctx, c.Stable, pager, deltaPager, oldDelta, canMakeDeltaQueries) } // --------------------------------------------------------------------------- // Serialization // --------------------------------------------------------------------------- -// Serialize transforms the mail item into a byte slice. +func BytesToMessageable(body []byte) (models.Messageable, error) { + v, err := createFromBytes(body, models.CreateMessageFromDiscriminatorValue) + if err != nil { + return nil, clues.Wrap(err, "deserializing bytes to message") + } + + return v.(models.Messageable), nil +} + func (c Mail) Serialize( ctx context.Context, item serialization.Parsable, @@ -737,15 +713,11 @@ func (c Mail) Serialize( } ctx = clues.Add(ctx, "item_id", ptr.Val(msg.GetId())) - - var ( - err error - writer = kjson.NewJsonSerializationWriter() - ) + writer := kjson.NewJsonSerializationWriter() defer writer.Close() - if err = writer.WriteObjectValue("", msg); err != nil { + if err := writer.WriteObjectValue("", msg); err != nil { return nil, graph.Stack(ctx, err) } diff --git a/src/pkg/services/m365/api/mail_test.go b/src/pkg/services/m365/api/mail_test.go index 016ae11ee..581dcd2b0 100644 --- a/src/pkg/services/m365/api/mail_test.go +++ b/src/pkg/services/m365/api/mail_test.go @@ -15,6 +15,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/alcionai/corso/src/internal/common/ptr" + exchMock "github.com/alcionai/corso/src/internal/connector/exchange/mock" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/backup/details" @@ -157,6 +158,37 @@ func (suite *MailAPIUnitSuite) TestMailInfo() { } } +func (suite *MailAPIUnitSuite) TestBytesToMessagable() { + table := []struct { + name string + byteArray []byte + checkError assert.ErrorAssertionFunc + checkObject assert.ValueAssertionFunc + }{ + { + name: "Empty Bytes", + byteArray: make([]byte, 0), + checkError: assert.Error, + checkObject: assert.Nil, + }, + { + name: "aMessage bytes", + byteArray: exchMock.MessageBytes("m365 mail support test"), + checkError: assert.NoError, + checkObject: assert.NotNil, + }, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + result, err := api.BytesToMessageable(test.byteArray) + test.checkError(t, err, clues.ToCore(err)) + test.checkObject(t, result) + }) + } +} + type MailAPIIntgSuite struct { tester.Suite credentials account.M365Config diff --git a/src/pkg/services/m365/api/serialization.go b/src/pkg/services/m365/api/serialization.go new file mode 100644 index 000000000..2410ca090 --- /dev/null +++ b/src/pkg/services/m365/api/serialization.go @@ -0,0 +1,25 @@ +package api + +import ( + "github.com/alcionai/clues" + "github.com/microsoft/kiota-abstractions-go/serialization" + kjson "github.com/microsoft/kiota-serialization-json-go" +) + +// createFromBytes generates an m365 object form bytes. +func createFromBytes( + bytes []byte, + createFunc serialization.ParsableFactory, +) (serialization.Parsable, error) { + parseNode, err := kjson.NewJsonParseNodeFactory().GetRootParseNode("application/json", bytes) + if err != nil { + return nil, clues.Wrap(err, "deserializing bytes into base m365 object") + } + + v, err := parseNode.GetObjectValue(createFunc) + if err != nil { + return nil, clues.Wrap(err, "parsing m365 object factory") + } + + return v, nil +}