diff --git a/src/internal/connector/exchange/exchange_service_test.go b/src/internal/connector/exchange/exchange_service_test.go index 01d545d12..0d015241f 100644 --- a/src/internal/connector/exchange/exchange_service_test.go +++ b/src/internal/connector/exchange/exchange_service_test.go @@ -1,17 +1,23 @@ package exchange import ( + "context" "testing" + "time" absser "github.com/microsoft/kiota-abstractions-go/serialization" msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core" "github.com/microsoftgraph/msgraph-sdk-go/models" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/alcionai/corso/internal/common" + "github.com/alcionai/corso/internal/connector/mockconnector" "github.com/alcionai/corso/internal/tester" "github.com/alcionai/corso/pkg/account" + "github.com/alcionai/corso/pkg/control" "github.com/alcionai/corso/pkg/selectors" ) @@ -420,3 +426,85 @@ func (suite *ExchangeServiceSuite) TestIterativeFunctions() { }) } } + +// TestRestoreContact ensures contact object can be created, placed into +// the Corso Folder. The function handles test clean-up. +func (suite *ExchangeServiceSuite) TestRestoreContact() { + t := suite.T() + userID := tester.M365UserID(suite.T()) + now := time.Now() + + folderName := "TestRestoreContact: " + common.FormatSimpleDateTime(now) + aFolder, err := CreateContactFolder(suite.es, userID, folderName) + require.NoError(t, err) + folderID := *aFolder.GetId() + err = RestoreExchangeContact(context.Background(), + mockconnector.GetMockContactBytes("Corso TestContact"), + suite.es, + control.Copy, + folderID, + userID) + assert.NoError(t, err) + // Removes folder containing contact prior to exiting test + err = DeleteContactFolder(suite.es, userID, folderID) + assert.NoError(t, err) +} + +// TestEstablishFolder checks the ability to Create a "container" for the +// GraphConnector's Restore Workflow based on OptionIdentifier. +func (suite *ExchangeServiceSuite) TestEstablishFolder() { + tests := []struct { + name string + option optionIdentifier + checkError assert.ErrorAssertionFunc + }{ + { + name: "Establish User Restore Folder", + option: users, + checkError: assert.Error, + }, + { + name: "Establish Event Restore Location", + option: events, + checkError: assert.Error, + }, + { + name: "Establish Restore Folder for Unknown", + option: unknown, + checkError: assert.Error, + }, + { + name: "Establish Restore folder for Mail", + option: messages, + checkError: assert.NoError, + }, + { + name: "Establish Restore folder for Contacts", + option: contacts, + checkError: assert.NoError, + }, + } + now := time.Now() + folderName := "CorsoEstablishFolder" + common.FormatSimpleDateTime(now) + userID := tester.M365UserID(suite.T()) + for _, test := range tests { + suite.T().Run(test.name, func(t *testing.T) { + folderID, err := establishFolder(suite.es, folderName, userID, test.option) + require.True(t, test.checkError(t, err)) + if folderID != "" { + switch test.option { + case messages: + err = DeleteMailFolder(suite.es, userID, folderID) + assert.NoError(t, err) + case contacts: + err = DeleteContactFolder(suite.es, userID, folderID) + assert.NoError(t, err) + default: + assert.NoError(t, + errors.New("unsupported type received folderID: "+test.option.String()), + ) + } + } + }) + } +} diff --git a/src/internal/connector/exchange/service_functions.go b/src/internal/connector/exchange/service_functions.go index 4419578c6..dca85cec5 100644 --- a/src/internal/connector/exchange/service_functions.go +++ b/src/internal/connector/exchange/service_functions.go @@ -262,11 +262,11 @@ func SetupExchangeCollectionVars(scope selectors.ExchangeScope) ( return nil, nil, nil, errors.New("exchange scope option not supported") } -// GetCopyRestoreFolder utility function to create +// GetRestoreFolder utility function to create // an unique folder for the restore process // @param category: input from fullPath()[2] // that defines the application the folder is created in. -func GetCopyRestoreFolder( +func GetRestoreFolder( service graph.Service, user, category string, ) (string, error) { @@ -325,20 +325,48 @@ func RestoreExchangeObject( default: return fmt.Errorf("type: %s not supported for exchange restore", category) } + if policy != control.Copy { + return fmt.Errorf("restore policy: %s not supported", policy) + } switch setting { case messages: - switch policy { - case control.Copy: - return RestoreMailMessage(ctx, bits, service, control.Copy, destination, user) - default: - return fmt.Errorf("restore policy: %s not supported", policy) - } + return RestoreMailMessage(ctx, bits, service, control.Copy, destination, user) + case contacts: + return RestoreExchangeContact(ctx, bits, service, control.Copy, destination, user) default: return fmt.Errorf("type: %s not supported for exchange restore", category) } } +// RestoreExchangeContact restores a contact to the @bits byte +// representation of M365 contact object. +// @destination M365 ID representing a M365 Contact_Folder +// Returns an error if the input bits do not parse into a models.Contactable object +// or if an error is encountered sending data to the M365 account. +// Post details: https://docs.microsoft.com/en-us/graph/api/user-post-contacts?view=graph-rest-1.0&tabs=go +func RestoreExchangeContact( + ctx context.Context, + bits []byte, + service graph.Service, + cp control.CollisionPolicy, + destination, user string, +) error { + contact, err := support.CreateContactFromBytes(bits) + if err != nil { + return err + } + + response, err := service.Client().UsersById(user).ContactFoldersById(destination).Contacts().Post(contact) + if err != nil { + return errors.Wrap(err, support.ConnectorStackErrorTrace(err)) + } + if response == nil { + return errors.New("msgraph contact post fail: REST response not received") + } + return nil +} + // RestoreMailMessage utility function to place an exchange.Mail // message into the user's M365 Exchange account. // @param bits - byte array representation of exchange.Message from Corso backstore diff --git a/src/internal/connector/graph_connector.go b/src/internal/connector/graph_connector.go index e5e2b4e56..d8a513d79 100644 --- a/src/internal/connector/graph_connector.go +++ b/src/internal/connector/graph_connector.go @@ -24,10 +24,6 @@ import ( "github.com/alcionai/corso/pkg/selectors" ) -const ( - mailCategory = "mail" -) - // GraphConnector is a struct used to wrap the GraphServiceClient and // GraphRequestAdapter from the msgraph-sdk-go. Additional fields are for // bookkeeping and interfacing with other component. @@ -221,10 +217,13 @@ func (gc *GraphConnector) ExchangeDataCollection( return collections, errs } -// RestoreMessages: Utility function to connect to M365 backstore +// RestoreExchangeDataCollection: Utility function to connect to M365 backstore // and upload messages from DataCollection. -// FullPath: tenantId, userId, , FolderId -func (gc *GraphConnector) RestoreMessages(ctx context.Context, dcs []data.Collection) error { +// FullPath: tenantId, userId, , FolderId +func (gc *GraphConnector) RestoreExchangeDataCollection( + ctx context.Context, + dcs []data.Collection, +) error { var ( pathCounter = map[string]bool{} attempts, successes int @@ -241,7 +240,7 @@ func (gc *GraphConnector) RestoreMessages(ctx context.Context, dcs []data.Collec if _, ok := pathCounter[directory]; !ok { pathCounter[directory] = true if policy == control.Copy { - folderID, errs = exchange.GetCopyRestoreFolder(&gc.graphService, user, category) + folderID, errs = exchange.GetRestoreFolder(&gc.graphService, user, category) if errs != nil { return errs } diff --git a/src/internal/connector/graph_connector_test.go b/src/internal/connector/graph_connector_test.go index 2d1eea276..24a2e6fd8 100644 --- a/src/internal/connector/graph_connector_test.go +++ b/src/internal/connector/graph_connector_test.go @@ -225,24 +225,23 @@ func (suite *GraphConnectorIntegrationSuite) TestEventsSerializationRegression() suite.Equal(status.ObjectCount, status.Successful) } +// Restore Functions // TestRestoreMessages uses mock data to ensure GraphConnector // is able to restore a several messageable item to a Mailbox. // The result should be all successful items restored within the same folder. func (suite *GraphConnectorIntegrationSuite) TestRestoreMessages() { t := suite.T() + category := "mail" connector := loadConnector(t) - user := tester.M365UserID(t) - if len(user) == 0 { - suite.T().Skip("Environment not configured: missing m365 test user") - } - collection := make([]data.Collection, 0) for i := 0; i < 3; i++ { - mdc := mockconnector.NewMockExchangeCollection([]string{"tenant", user, mailCategory, "Inbox"}, 1) + mdc := mockconnector.NewMockExchangeCollection( + []string{"tenant", suite.user, category, "Inbox"}, + 1) collection = append(collection, mdc) } - err := connector.RestoreMessages(context.Background(), collection) + err := connector.RestoreExchangeDataCollection(context.Background(), collection) assert.NoError(suite.T(), err) status := connector.AwaitStatus() assert.NotNil(t, status) diff --git a/src/internal/operations/restore.go b/src/internal/operations/restore.go index aa07e51af..2b666a9a2 100644 --- a/src/internal/operations/restore.go +++ b/src/internal/operations/restore.go @@ -139,7 +139,7 @@ func (op *RestoreOperation) Run(ctx context.Context) (err error) { return err } - err = gc.RestoreMessages(ctx, dcs) + err = gc.RestoreExchangeDataCollection(ctx, dcs) if err != nil { err = errors.Wrap(err, "restoring service data") opStats.writeErr = err