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:
parent
ae06b36e7a
commit
0b3f345727
@ -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()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user