diff --git a/src/internal/connector/onedrive/api/drive.go b/src/internal/connector/onedrive/api/drive.go index 8f9471cef..3e65e502b 100644 --- a/src/internal/connector/onedrive/api/drive.go +++ b/src/internal/connector/onedrive/api/drive.go @@ -10,6 +10,7 @@ import ( mssites "github.com/microsoftgraph/msgraph-sdk-go/sites" msusers "github.com/microsoftgraph/msgraph-sdk-go/users" + "github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph/api" ) @@ -137,6 +138,11 @@ type siteDrivePager struct { options *mssites.ItemDrivesRequestBuilderGetRequestConfiguration } +// NewSiteDrivePager is a constructor for creating a siteDrivePager +// fields are the associated site drive fields that are desired to be returned +// in a query. NOTE: Fields are case-sensitive. Incorrect field settings will +// cause errors during later paging. +// Available fields: https://learn.microsoft.com/en-us/graph/api/resources/drive?view=graph-rest-1.0 func NewSiteDrivePager( gs graph.Servicer, siteID string, @@ -178,3 +184,72 @@ func (p *siteDrivePager) SetNext(link string) { func (p *siteDrivePager) ValuesIn(l api.PageLinker) ([]models.Driveable, error) { return getValues[models.Driveable](l) } + +// GetDriveIDByName is a helper function to retrieve the M365ID of a site drive. +// Returns "" if the folder is not within the drive. +// Dependency: Requires "name" and "id" to be part of the given options +func (p *siteDrivePager) GetDriveIDByName(ctx context.Context, driveName string) (string, error) { + var empty string + + for { + resp, err := p.builder.Get(ctx, p.options) + if err != nil { + return empty, clues.Stack(err).WithClues(ctx).With(graph.ErrData(err)...) + } + + for _, entry := range resp.GetValue() { + if ptr.Val(entry.GetName()) == driveName { + return ptr.Val(entry.GetId()), nil + } + } + + link, ok := ptr.ValOK(resp.GetOdataNextLink()) + if !ok { + break + } + + p.builder = mssites.NewItemDrivesRequestBuilder(link, p.gs.Adapter()) + } + + return empty, nil +} + +// GetFolderIDByName is a helper function to retrieve the M365ID of a folder within a site document library. +// Returns "" if the folder is not within the drive +func (p *siteDrivePager) GetFolderIDByName(ctx context.Context, driveID, folderName string) (string, error) { + var empty string + + // *msdrives.ItemRootChildrenRequestBuilder + builder := p.gs.Client().DrivesById(driveID).Root().Children() + option := &msdrives.ItemRootChildrenRequestBuilderGetRequestConfiguration{ + QueryParameters: &msdrives.ItemRootChildrenRequestBuilderGetQueryParameters{ + Select: []string{"id", "name", "folder"}, + }, + } + + for { + resp, err := builder.Get(ctx, option) + if err != nil { + return empty, clues.Stack(err).WithClues(ctx).With(graph.ErrData(err)...) + } + + for _, entry := range resp.GetValue() { + if entry.GetFolder() == nil { + continue + } + + if ptr.Val(entry.GetName()) == folderName { + return ptr.Val(entry.GetId()), nil + } + } + + link, ok := ptr.ValOK(resp.GetOdataNextLink()) + if !ok { + break + } + + builder = msdrives.NewItemRootChildrenRequestBuilder(link, p.gs.Adapter()) + } + + return empty, nil +} diff --git a/src/internal/connector/onedrive/api/drive_test.go b/src/internal/connector/onedrive/api/drive_test.go new file mode 100644 index 000000000..ce11ee27c --- /dev/null +++ b/src/internal/connector/onedrive/api/drive_test.go @@ -0,0 +1,81 @@ +package api_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/src/internal/connector/graph" + "github.com/alcionai/corso/src/internal/connector/onedrive/api" + "github.com/alcionai/corso/src/internal/tester" + "github.com/alcionai/corso/src/pkg/account" +) + +type OneDriveAPISuite struct { + tester.Suite + creds account.M365Config + service graph.Servicer +} + +func (suite *OneDriveAPISuite) SetupSuite() { + t := suite.T() + a := tester.NewM365Account(t) + m365, err := a.M365Config() + require.NoError(t, err) + + suite.creds = m365 + adpt, err := graph.CreateAdapter(m365.AzureTenantID, m365.AzureClientID, m365.AzureClientSecret) + require.NoError(t, err) + + suite.service = graph.NewService(adpt) +} + +func TestOneDriveAPIs(t *testing.T) { + suite.Run(t, &OneDriveAPISuite{ + Suite: tester.NewIntegrationSuite( + t, + [][]string{tester.M365AcctCredEnvs}, + tester.CorsoGraphConnectorOneDriveTests), + }) +} + +func (suite *OneDriveAPISuite) TestCreatePagerAndGetPage() { + ctx, flush := tester.NewContext() + defer flush() + + t := suite.T() + siteID := tester.M365SiteID(t) + pager := api.NewSiteDrivePager(suite.service, siteID, []string{"name"}) + a, err := pager.GetPage(ctx) + assert.NoError(t, err) + assert.NotNil(t, a) +} + +func (suite *OneDriveAPISuite) TestGetDriveIDByName() { + ctx, flush := tester.NewContext() + defer flush() + + t := suite.T() + siteID := tester.M365SiteID(t) + pager := api.NewSiteDrivePager(suite.service, siteID, []string{"id", "name"}) + id, err := pager.GetDriveIDByName(ctx, "Documents") + assert.NoError(t, err) + assert.NotEmpty(t, id) +} + +func (suite *OneDriveAPISuite) TestGetDriveFolderByName() { + ctx, flush := tester.NewContext() + defer flush() + + t := suite.T() + siteID := tester.M365SiteID(t) + pager := api.NewSiteDrivePager(suite.service, siteID, []string{"id", "name"}) + id, err := pager.GetDriveIDByName(ctx, "Documents") + require.NoError(t, err) + require.NotEmpty(t, id) + + _, err = pager.GetFolderIDByName(ctx, id, "folder") + assert.NoError(t, err) +} diff --git a/src/internal/operations/backup_integration_test.go b/src/internal/operations/backup_integration_test.go index d31a7d2e2..3c3d3e84c 100644 --- a/src/internal/operations/backup_integration_test.go +++ b/src/internal/operations/backup_integration_test.go @@ -1107,10 +1107,11 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_sharePoint() { sel = selectors.NewSharePointBackup([]string{suite.site}) ) - sel.Include(sel.AllData()) + sel.Include(sel.Libraries(selectors.Any())) - bo, _, _, _, closer := prepNewTestBackupOp(t, ctx, mb, sel.Selector, control.Toggles{}) + bo, _, kw, _, closer := prepNewTestBackupOp(t, ctx, mb, sel.Selector, control.Toggles{}) defer closer() runAndCheckBackup(t, ctx, &bo, mb) + checkBackupIsInManifests(t, ctx, kw, &bo, sel.Selector, suite.site, path.LibrariesCategory) } diff --git a/src/internal/operations/restore_test.go b/src/internal/operations/restore_test.go index 8bf695d7e..736d3c20f 100644 --- a/src/internal/operations/restore_test.go +++ b/src/internal/operations/restore_test.go @@ -9,8 +9,12 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/alcionai/corso/src/internal/common" "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/connector/onedrive" + "github.com/alcionai/corso/src/internal/connector/onedrive/api" "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/events" @@ -133,12 +137,14 @@ func (suite *RestoreOpSuite) TestRestoreOperation_PersistResults() { type RestoreOpIntegrationSuite struct { tester.Suite - backupID model.StableID - numItems int - kopiaCloser func(ctx context.Context) - kw *kopia.Wrapper - sw *store.Wrapper - ms *kopia.ModelStore + backupID model.StableID + sharepointID model.StableID + shareItems int + numItems int + kopiaCloser func(ctx context.Context) + kw *kopia.Wrapper + sw *store.Wrapper + ms *kopia.ModelStore } func TestRestoreOpIntegrationSuite(t *testing.T) { @@ -208,6 +214,30 @@ func (suite *RestoreOpIntegrationSuite) SetupSuite() { // Discount metadata files (3 paths, 3 deltas) as // they are not part of the data restored. suite.numItems = bo.Results.ItemsWritten - 6 + + siteID := tester.M365SiteID(t) + sites := []string{siteID} + csel := selectors.NewSharePointBackup(sites) + csel.DiscreteOwner = siteID + csel.Include( + csel.Libraries(selectors.Any()), + ) + + bo, err = NewBackupOperation( + ctx, + control.Options{}, + kw, + sw, + acct, + csel.Selector, + evmock.NewBus(), + ) + require.NoError(t, err) + require.NoError(t, bo.Run(ctx)) + require.NotEmpty(t, bo.Results.BackupID) + suite.sharepointID = bo.Results.BackupID + // Discount MetaData files (1 path, 1 delta) + suite.shareItems = bo.Results.ItemsWritten - 2 } func (suite *RestoreOpIntegrationSuite) TearDownSuite() { @@ -266,49 +296,106 @@ func (suite *RestoreOpIntegrationSuite) TestNewRestoreOperation() { } } +//nolint:lll func (suite *RestoreOpIntegrationSuite) TestRestore_Run() { ctx, flush := tester.NewContext() defer flush() - t := suite.T() - users := []string{tester.M365UserID(t)} + tables := []struct { + name string + bID model.StableID + expectedItems int + dest control.RestoreDestination + getSelector func(t *testing.T) selectors.Selector + cleanup func(t *testing.T, dest string) + }{ + { + name: "Exchange_Restore", + bID: suite.backupID, + expectedItems: suite.numItems, + dest: tester.DefaultTestRestoreDestination(), + getSelector: func(t *testing.T) selectors.Selector { + users := []string{tester.M365UserID(t)} + rsel := selectors.NewExchangeRestore(users) + rsel.Include(rsel.AllData()) - rsel := selectors.NewExchangeRestore(users) - rsel.Include(rsel.AllData()) + return rsel.Selector + }, + }, + { + name: "SharePoint_Restore", + bID: suite.sharepointID, + expectedItems: suite.shareItems, + dest: control.DefaultRestoreDestination(common.SimpleDateTimeOneDrive), + getSelector: func(t *testing.T) selectors.Selector { + bsel := selectors.NewSharePointRestore([]string{tester.M365SiteID(t)}) + bsel.Include(bsel.AllData()) - dest := tester.DefaultTestRestoreDestination() - mb := evmock.NewBus() + return bsel.Selector + }, + cleanup: func(t *testing.T, dest string) { + act := tester.NewM365Account(t) + m365, err := act.M365Config() + require.NoError(t, err) - ro, err := NewRestoreOperation( - ctx, - control.Options{}, - suite.kw, - suite.sw, - tester.NewM365Account(t), - suite.backupID, - rsel.Selector, - dest, - mb) - require.NoError(t, err) + adpt, err := graph.CreateAdapter(m365.AzureTenantID, m365.AzureClientID, m365.AzureClientSecret) + require.NoError(t, err) + service := graph.NewService(adpt) + pager := api.NewSiteDrivePager(service, tester.M365SiteID(t), []string{"id", "name"}) + driveID, err := pager.GetDriveIDByName(ctx, "Documents") + require.NoError(t, err) + require.NotEmpty(t, driveID) - ds, err := ro.Run(ctx) + folderID, err := pager.GetFolderIDByName(ctx, driveID, dest) + require.NoError(t, err) + require.NotEmpty(t, folderID) - require.NoError(t, err, "restoreOp.Run()") - require.NotEmpty(t, ro.Results, "restoreOp results") - require.NotNil(t, ds, "restored details") - assert.Equal(t, ro.Status, Completed, "restoreOp status") - assert.Equal(t, ro.Results.ItemsWritten, len(ds.Entries), "count of items written matches restored entries in details") - assert.Less(t, 0, ro.Results.ItemsRead, "restore items read") - assert.Less(t, 0, ro.Results.ItemsWritten, "restored items written") - assert.Less(t, int64(0), ro.Results.BytesRead, "bytes read") - assert.Equal(t, 1, ro.Results.ResourceOwners, "resource Owners") - assert.NoError(t, ro.Errors.Failure(), "non-recoverable error") - assert.Empty(t, ro.Errors.Recovered(), "recoverable errors") - assert.NoError(t, ro.Results.ReadErrors, "errors while reading restore data") - assert.NoError(t, ro.Results.WriteErrors, "errors while writing restore data") - assert.Equal(t, suite.numItems, ro.Results.ItemsWritten, "backup and restore wrote the same num of items") - assert.Equal(t, 1, mb.TimesCalled[events.RestoreStart], "restore-start events") - assert.Equal(t, 1, mb.TimesCalled[events.RestoreEnd], "restore-end events") + err = onedrive.DeleteItem(ctx, service, driveID, folderID) + assert.NoError(t, err, "failed to delete restore folder: operations_SharePoint_Restore") + }, + }, + } + + for _, test := range tables { + suite.T().Run(test.name, func(t *testing.T) { + mb := evmock.NewBus() + ro, err := NewRestoreOperation( + ctx, + control.Options{FailFast: true}, + suite.kw, + suite.sw, + tester.NewM365Account(t), + test.bID, + test.getSelector(t), + test.dest, + mb) + require.NoError(t, err) + + ds, err := ro.Run(ctx) + + require.NoError(t, err, "restoreOp.Run()") + require.NotEmpty(t, ro.Results, "restoreOp results") + require.NotNil(t, ds, "restored details") + assert.Equal(t, ro.Status, Completed, "restoreOp status") + assert.Equal(t, ro.Results.ItemsWritten, len(ds.Entries), "count of items written matches restored entries in details") + assert.Less(t, 0, ro.Results.ItemsRead, "restore items read") + assert.Less(t, 0, ro.Results.ItemsWritten, "restored items written") + assert.Less(t, int64(0), ro.Results.BytesRead, "bytes read") + assert.Equal(t, 1, ro.Results.ResourceOwners, "resource Owners") + assert.NoError(t, ro.Errors.Failure(), "non-recoverable error") + assert.Empty(t, ro.Errors.Recovered(), "recoverable errors") + assert.NoError(t, ro.Results.ReadErrors, "errors while reading restore data") + assert.NoError(t, ro.Results.WriteErrors, "errors while writing restore data") + assert.Equal(t, test.expectedItems, ro.Results.ItemsWritten, "backup and restore wrote the same num of items") + assert.Equal(t, 1, mb.TimesCalled[events.RestoreStart], "restore-start events") + assert.Equal(t, 1, mb.TimesCalled[events.RestoreEnd], "restore-end events") + + // clean up + if test.cleanup != nil { + test.cleanup(t, test.dest.ContainerName) + } + }) + } } func (suite *RestoreOpIntegrationSuite) TestRestore_Run_ErrorNoResults() {