diff --git a/src/internal/connector/exchange/container_resolver.go b/src/internal/connector/exchange/container_resolver.go index d1d8e2a35..772fe1f13 100644 --- a/src/internal/connector/exchange/container_resolver.go +++ b/src/internal/connector/exchange/container_resolver.go @@ -62,9 +62,9 @@ func (cr *containerResolver) idToPath( return fullPath, nil } -// PathInCache utility function to return m365ID of folder if the pathString -// matches the path of a container within the cache. A boolean function -// accompanies the call to indicate whether the lookup was successful. +// PathInCache utility function to return m365ID of folder if the path.Folders +// matches the directory of a container within the cache. A boolean result +// is provided to indicate whether the lookup was successful. func (cr *containerResolver) PathInCache(pathString string) (string, bool) { if len(pathString) == 0 || cr == nil { return "", false diff --git a/src/internal/connector/exchange/data_collections.go b/src/internal/connector/exchange/data_collections.go index e587b0934..feccb1757 100644 --- a/src/internal/connector/exchange/data_collections.go +++ b/src/internal/connector/exchange/data_collections.go @@ -236,7 +236,7 @@ func createCollections( defer closer() defer close(foldersComplete) - resolver, err := populateExchangeContainerResolver(ctx, qp) + resolver, err := PopulateExchangeContainerResolver(ctx, qp) if err != nil { return nil, errors.Wrap(err, "getting folder cache") } diff --git a/src/internal/connector/exchange/exchange_service_test.go b/src/internal/connector/exchange/exchange_service_test.go index 214651f98..ab5d734ce 100644 --- a/src/internal/connector/exchange/exchange_service_test.go +++ b/src/internal/connector/exchange/exchange_service_test.go @@ -560,26 +560,24 @@ func (suite *ExchangeServiceSuite) TestGetContainerIDFromCache() { for _, test := range tests { suite.T().Run(test.name, func(t *testing.T) { - folderID, err := GetContainerIDFromCache( + folderID, err := CreateContainerDestinaion( ctx, connector, test.pathFunc1(t), folderName, - directoryCaches, - ) + directoryCaches) require.NoError(t, err) resolver := directoryCaches[test.category] _, err = resolver.IDToPath(ctx, folderID) assert.NoError(t, err) - secondID, err := GetContainerIDFromCache( + secondID, err := CreateContainerDestinaion( ctx, connector, test.pathFunc2(t), folderName, - directoryCaches, - ) + directoryCaches) require.NoError(t, err) _, err = resolver.IDToPath(ctx, secondID) diff --git a/src/internal/connector/exchange/service_functions.go b/src/internal/connector/exchange/service_functions.go index 4b59bd9c1..c92be996d 100644 --- a/src/internal/connector/exchange/service_functions.go +++ b/src/internal/connector/exchange/service_functions.go @@ -125,11 +125,11 @@ func DeleteContactFolder(ctx context.Context, gs graph.Servicer, user, folderID return gs.Client().UsersById(user).ContactFoldersById(folderID).Delete(ctx, nil) } -// populateExchangeContainerResolver gets a folder resolver if one is available for +// PopulateExchangeContainerResolver gets a folder resolver if one is available for // this category of data. If one is not available, returns nil so that other // logic in the caller can complete as long as they check if the resolver is not // nil. If an error occurs populating the resolver, returns an error. -func populateExchangeContainerResolver( +func PopulateExchangeContainerResolver( ctx context.Context, qp graph.QueryParams, ) (graph.ContainerResolver, error) { diff --git a/src/internal/connector/exchange/service_restore.go b/src/internal/connector/exchange/service_restore.go index 7905a41f3..e5efb2fe7 100644 --- a/src/internal/connector/exchange/service_restore.go +++ b/src/internal/connector/exchange/service_restore.go @@ -312,7 +312,7 @@ func RestoreExchangeDataCollections( userCaches = directoryCaches[userID] } - containerID, err := GetContainerIDFromCache( + containerID, err := CreateContainerDestinaion( ctx, gs, dc.FullPath(), @@ -425,10 +425,12 @@ func restoreCollection( } } -// generateRestoreContainerFunc utility function that holds logic for creating -// Root Directory or necessary functions based on path.CategoryType -// Assumption: collisionPolicy == COPY -func GetContainerIDFromCache( +// CreateContainerDestinaion builds the destination into the container +// at the provided path. As a precondition, the destination cannot +// already exist. If it does then an error is returned. The provided +// containerResolver is updated with the new destination. +// @ returns the container ID of the new destination container. +func CreateContainerDestinaion( ctx context.Context, gs graph.Servicer, directory path.Path, diff --git a/src/internal/operations/backup_integration_test.go b/src/internal/operations/backup_integration_test.go index 1f408c12a..f57830e22 100644 --- a/src/internal/operations/backup_integration_test.go +++ b/src/internal/operations/backup_integration_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/google/uuid" + msuser "github.com/microsoftgraph/msgraph-sdk-go/users" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,6 +17,7 @@ import ( "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/connector" "github.com/alcionai/corso/src/internal/connector/exchange" + "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/mockconnector" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/events" @@ -631,6 +633,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() { } folder1 = fmt.Sprintf("%s%d_%s", incrementalsDestFolderPrefix, 1, now) folder2 = fmt.Sprintf("%s%d_%s", incrementalsDestFolderPrefix, 2, now) + folder3 = fmt.Sprintf("%s%d_%s", incrementalsDestFolderPrefix, 3, now) ) m365, err := acct.M365Config() @@ -639,14 +642,20 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() { gc, err := connector.NewGraphConnector(ctx, acct, connector.Users) require.NoError(t, err) - // generate 2 new folders with two items each. + // generate 3 new folders with two items each. + // Only the first two folders will be part of the initial backup and + // incrementals. The third folder will be introduced partway through + // the changes. // This should be enough to cover most delta actions, since moving one // folder into another generates a delta for both addition and deletion. - // TODO: get the folder IDs somehow, so that we can call mutations on - // the folders by ID. + type contDeets struct { + containerID string + deets *details.Details + } + dataset := map[path.CategoryType]struct { dbf dataBuilderFunc - dests map[string]*details.Details + dests map[string]contDeets }{ path.EmailCategory: { dbf: func(id, timeStamp, subject, body string) []byte { @@ -657,9 +666,10 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() { subject, body, body, now, now, now, now) }, - dests: map[string]*details.Details{ - folder1: nil, - folder2: nil, + dests: map[string]contDeets{ + folder1: {}, + folder2: {}, + folder3: {}, }, }, path.ContactsCategory: { @@ -673,25 +683,50 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() { "123-456-7890", ) }, - dests: map[string]*details.Details{ - folder1: nil, - folder2: nil, + dests: map[string]contDeets{ + folder1: {}, + folder2: {}, + folder3: {}, }, }, } for category, gen := range dataset { - for dest := range gen.dests { - dataset[category].dests[dest] = generateContainerOfItems( + for destName := range gen.dests { + deets := generateContainerOfItems( t, ctx, gc, path.ExchangeService, category, selectors.NewExchangeRestore(users).Selector, - m365.AzureTenantID, suite.user, dest, + m365.AzureTenantID, suite.user, destName, 2, gen.dbf) + + dataset[category].dests[destName] = contDeets{"", deets} + } + } + + for category, gen := range dataset { + qp := graph.QueryParams{ + Category: category, + ResourceOwner: suite.user, + Credentials: m365, + } + cr, err := exchange.PopulateExchangeContainerResolver(ctx, qp) + require.NoError(t, err, "populating %s container resolver", category) + + for destName, dest := range gen.dests { + p, err := path.FromDataLayerPath(dest.deets.Entries[0].RepoRef, true) + require.NoError(t, err) + + id, ok := cr.PathInCache(p.Folder()) + require.True(t, ok, "dir %s found in %s cache", p.Folder(), category) + + d := dataset[category].dests[destName] + d.containerID = id + dataset[category].dests[destName] = d } } @@ -713,7 +748,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() { // [ ] remove an item from an existing folder // [ ] add a new folder // [ ] rename a folder - // [ ] relocate one folder into another + // [x] relocate one folder into another // [ ] remove a folder table := []struct { @@ -729,6 +764,27 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() { itemsRead: 0, itemsWritten: 0, }, + { + name: "move an email folder to a subfolder", + updateUserData: func(t *testing.T) { + // contacts cannot be sufoldered; this is an email-only change + toFolder := dataset[path.EmailCategory].dests[folder1].containerID + fromFolder := dataset[path.EmailCategory].dests[folder2].containerID + + body := msuser.NewItemMailFoldersItemMovePostRequestBody() + body.SetDestinationId(&toFolder) + + _, err := gc.Service. + Client(). + UsersById(suite.user). + MailFoldersById(fromFolder). + Move(). + Post(ctx, body, nil) + require.NoError(t, err) + }, + itemsRead: 0, // zero because we don't count container reads + itemsWritten: 2, + }, } for _, test := range table { suite.T().Run(test.name, func(t *testing.T) { @@ -753,7 +809,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() { ) // do some additional checks to ensure the incremental dealt with fewer items. - // +4 on read/writes to account for metadata + // +4 on read/writes to account for metadata: 1 delta and 1 path for each type. assert.Equal(t, test.itemsWritten+4, incBO.Results.ItemsWritten, "incremental items written") assert.Equal(t, test.itemsRead+4, incBO.Results.ItemsRead, "incremental items read") assert.NoError(t, incBO.Results.ReadErrors, "incremental read errors")