GC: Restore Contact Interface (#642)

Functionality added to restore `exchange.Contact` to M365 back store. Unit tests added to support proper testing of new feature.
This commit is contained in:
Danny 2022-08-30 13:46:47 -04:00 committed by GitHub
parent ae06b36e7a
commit 0b3f345727
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 138 additions and 24 deletions

View File

@ -1,17 +1,23 @@
package exchange package exchange
import ( import (
"context"
"testing" "testing"
"time"
absser "github.com/microsoft/kiota-abstractions-go/serialization" absser "github.com/microsoft/kiota-abstractions-go/serialization"
msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core" msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "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/internal/tester"
"github.com/alcionai/corso/pkg/account" "github.com/alcionai/corso/pkg/account"
"github.com/alcionai/corso/pkg/control"
"github.com/alcionai/corso/pkg/selectors" "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()),
)
}
}
})
}
}

View File

@ -262,11 +262,11 @@ func SetupExchangeCollectionVars(scope selectors.ExchangeScope) (
return nil, nil, nil, errors.New("exchange scope option not supported") 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 // an unique folder for the restore process
// @param category: input from fullPath()[2] // @param category: input from fullPath()[2]
// that defines the application the folder is created in. // that defines the application the folder is created in.
func GetCopyRestoreFolder( func GetRestoreFolder(
service graph.Service, service graph.Service,
user, category string, user, category string,
) (string, error) { ) (string, error) {
@ -325,20 +325,48 @@ func RestoreExchangeObject(
default: default:
return fmt.Errorf("type: %s not supported for exchange restore", category) 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 { switch setting {
case messages: case messages:
switch policy { return RestoreMailMessage(ctx, bits, service, control.Copy, destination, user)
case control.Copy: case contacts:
return RestoreMailMessage(ctx, bits, service, control.Copy, destination, user) return RestoreExchangeContact(ctx, bits, service, control.Copy, destination, user)
default:
return fmt.Errorf("restore policy: %s not supported", policy)
}
default: default:
return fmt.Errorf("type: %s not supported for exchange restore", category) 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 // RestoreMailMessage utility function to place an exchange.Mail
// message into the user's M365 Exchange account. // message into the user's M365 Exchange account.
// @param bits - byte array representation of exchange.Message from Corso backstore // @param bits - byte array representation of exchange.Message from Corso backstore

View File

@ -24,10 +24,6 @@ import (
"github.com/alcionai/corso/pkg/selectors" "github.com/alcionai/corso/pkg/selectors"
) )
const (
mailCategory = "mail"
)
// GraphConnector is a struct used to wrap the GraphServiceClient and // GraphConnector is a struct used to wrap the GraphServiceClient and
// GraphRequestAdapter from the msgraph-sdk-go. Additional fields are for // GraphRequestAdapter from the msgraph-sdk-go. Additional fields are for
// bookkeeping and interfacing with other component. // bookkeeping and interfacing with other component.
@ -221,10 +217,13 @@ func (gc *GraphConnector) ExchangeDataCollection(
return collections, errs return collections, errs
} }
// RestoreMessages: Utility function to connect to M365 backstore // RestoreExchangeDataCollection: Utility function to connect to M365 backstore
// and upload messages from DataCollection. // and upload messages from DataCollection.
// FullPath: tenantId, userId, <mailCategory>, FolderId // FullPath: tenantId, userId, <collectionCategory>, FolderId
func (gc *GraphConnector) RestoreMessages(ctx context.Context, dcs []data.Collection) error { func (gc *GraphConnector) RestoreExchangeDataCollection(
ctx context.Context,
dcs []data.Collection,
) error {
var ( var (
pathCounter = map[string]bool{} pathCounter = map[string]bool{}
attempts, successes int attempts, successes int
@ -241,7 +240,7 @@ func (gc *GraphConnector) RestoreMessages(ctx context.Context, dcs []data.Collec
if _, ok := pathCounter[directory]; !ok { if _, ok := pathCounter[directory]; !ok {
pathCounter[directory] = true pathCounter[directory] = true
if policy == control.Copy { if policy == control.Copy {
folderID, errs = exchange.GetCopyRestoreFolder(&gc.graphService, user, category) folderID, errs = exchange.GetRestoreFolder(&gc.graphService, user, category)
if errs != nil { if errs != nil {
return errs return errs
} }

View File

@ -225,24 +225,23 @@ func (suite *GraphConnectorIntegrationSuite) TestEventsSerializationRegression()
suite.Equal(status.ObjectCount, status.Successful) suite.Equal(status.ObjectCount, status.Successful)
} }
// Restore Functions
// TestRestoreMessages uses mock data to ensure GraphConnector // TestRestoreMessages uses mock data to ensure GraphConnector
// is able to restore a several messageable item to a Mailbox. // is able to restore a several messageable item to a Mailbox.
// The result should be all successful items restored within the same folder. // The result should be all successful items restored within the same folder.
func (suite *GraphConnectorIntegrationSuite) TestRestoreMessages() { func (suite *GraphConnectorIntegrationSuite) TestRestoreMessages() {
t := suite.T() t := suite.T()
category := "mail"
connector := loadConnector(t) 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) collection := make([]data.Collection, 0)
for i := 0; i < 3; i++ { 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) collection = append(collection, mdc)
} }
err := connector.RestoreMessages(context.Background(), collection) err := connector.RestoreExchangeDataCollection(context.Background(), collection)
assert.NoError(suite.T(), err) assert.NoError(suite.T(), err)
status := connector.AwaitStatus() status := connector.AwaitStatus()
assert.NotNil(t, status) assert.NotNil(t, status)

View File

@ -139,7 +139,7 @@ func (op *RestoreOperation) Run(ctx context.Context) (err error) {
return err return err
} }
err = gc.RestoreMessages(ctx, dcs) err = gc.RestoreExchangeDataCollection(ctx, dcs)
if err != nil { if err != nil {
err = errors.Wrap(err, "restoring service data") err = errors.Wrap(err, "restoring service data")
opStats.writeErr = err opStats.writeErr = err