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
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()),
)
}
}
})
}
}

View File

@ -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)
}
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

View File

@ -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, <mailCategory>, FolderId
func (gc *GraphConnector) RestoreMessages(ctx context.Context, dcs []data.Collection) error {
// FullPath: tenantId, userId, <collectionCategory>, 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
}

View File

@ -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)

View File

@ -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