corso/src/internal/operations/restore_test.go
Keepers 635e4f6bc4
rename libraries -> libraryFolder (#2774)
The selector moniker "libraries" is incorrect.  It should refer to the library folder, since it only
matches on directory structures within a given
drive.  The 'library' is analogous to the drive
itself, and will need a separate selector of its
own.

---

#### Does this PR need a docs update or release note?

- [x]  No

#### Type of change

- [x] 🐛 Bugfix

#### Issue(s)

* #2757

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
2023-03-14 01:38:54 +00:00

428 lines
12 KiB
Go

package operations
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"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"
evmock "github.com/alcionai/corso/src/internal/events/mock"
"github.com/alcionai/corso/src/internal/kopia"
"github.com/alcionai/corso/src/internal/model"
"github.com/alcionai/corso/src/internal/stats"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/selectors"
"github.com/alcionai/corso/src/pkg/store"
)
// ---------------------------------------------------------------------------
// unit
// ---------------------------------------------------------------------------
type RestoreOpSuite struct {
tester.Suite
}
func TestRestoreOpSuite(t *testing.T) {
suite.Run(t, &RestoreOpSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *RestoreOpSuite) TestRestoreOperation_PersistResults() {
ctx, flush := tester.NewContext()
defer flush()
var (
kw = &kopia.Wrapper{}
sw = &store.Wrapper{}
acct = account.Account{}
now = time.Now()
dest = tester.DefaultTestRestoreDestination()
)
table := []struct {
expectStatus opStatus
expectErr assert.ErrorAssertionFunc
stats restoreStats
fail error
}{
{
expectStatus: Completed,
expectErr: assert.NoError,
stats: restoreStats{
resourceCount: 1,
bytesRead: &stats.ByteCounter{
NumBytes: 42,
},
cs: []data.RestoreCollection{
data.NotFoundRestoreCollection{
Collection: &mockconnector.MockExchangeDataCollection{},
},
},
gc: &support.ConnectorOperationStatus{
Metrics: support.CollectionMetrics{
Objects: 1,
Successes: 1,
},
},
},
},
{
expectStatus: Failed,
expectErr: assert.Error,
fail: assert.AnError,
stats: restoreStats{
bytesRead: &stats.ByteCounter{},
gc: &support.ConnectorOperationStatus{},
},
},
{
expectStatus: NoData,
expectErr: assert.NoError,
stats: restoreStats{
bytesRead: &stats.ByteCounter{},
cs: []data.RestoreCollection{},
gc: &support.ConnectorOperationStatus{},
},
},
}
for _, test := range table {
suite.Run(test.expectStatus.String(), func() {
t := suite.T()
op, err := NewRestoreOperation(
ctx,
control.Options{},
kw,
sw,
acct,
"foo",
selectors.Selector{DiscreteOwner: "test"},
dest,
evmock.NewBus())
require.NoError(t, err)
op.Errors.Fail(test.fail)
test.expectErr(t, op.persistResults(ctx, now, &test.stats))
assert.Equal(t, test.expectStatus.String(), op.Status.String(), "status")
assert.Equal(t, len(test.stats.cs), op.Results.ItemsRead, "items read")
assert.Equal(t, test.stats.gc.Metrics.Successes, op.Results.ItemsWritten, "items written")
assert.Equal(t, test.stats.bytesRead.NumBytes, op.Results.BytesRead, "resource owners")
assert.Equal(t, test.stats.resourceCount, op.Results.ResourceOwners, "resource owners")
assert.Equal(t, now, op.Results.StartedAt, "started at")
assert.Less(t, now, op.Results.CompletedAt, "completed at")
})
}
}
// ---------------------------------------------------------------------------
// integration
// ---------------------------------------------------------------------------
type RestoreOpIntegrationSuite struct {
tester.Suite
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) {
suite.Run(t, &RestoreOpIntegrationSuite{
Suite: tester.NewIntegrationSuite(
t,
[][]string{tester.AWSStorageCredEnvs, tester.M365AcctCredEnvs},
tester.CorsoOperationTests),
})
}
func (suite *RestoreOpIntegrationSuite) SetupSuite() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
m365UserID := tester.M365UserID(t)
acct := tester.NewM365Account(t)
// need to initialize the repository before we can test connecting to it.
st := tester.NewPrefixedS3Storage(t)
k := kopia.NewConn(st)
require.NoError(t, k.Initialize(ctx))
suite.kopiaCloser = func(ctx context.Context) {
k.Close(ctx)
}
kw, err := kopia.NewWrapper(k)
require.NoError(t, err)
suite.kw = kw
ms, err := kopia.NewModelStore(k)
require.NoError(t, err)
suite.ms = ms
sw := store.NewKopiaStore(ms)
suite.sw = sw
users := []string{m365UserID}
bsel := selectors.NewExchangeBackup(users)
bsel.DiscreteOwner = m365UserID
bsel.Include(
bsel.MailFolders([]string{exchange.DefaultMailFolder}, selectors.PrefixMatch()),
bsel.ContactFolders([]string{exchange.DefaultContactFolder}, selectors.PrefixMatch()),
bsel.EventCalendars([]string{exchange.DefaultCalendar}, selectors.PrefixMatch()),
)
bo, err := NewBackupOperation(
ctx,
control.Options{},
kw,
sw,
acct,
bsel.Selector,
evmock.NewBus())
require.NoError(t, err)
require.NoError(t, bo.Run(ctx))
require.NotEmpty(t, bo.Results.BackupID)
suite.backupID = bo.Results.BackupID
// 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.LibraryFolders(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() {
ctx, flush := tester.NewContext()
defer flush()
if suite.ms != nil {
suite.ms.Close(ctx)
}
if suite.kw != nil {
suite.kw.Close(ctx)
}
if suite.kopiaCloser != nil {
suite.kopiaCloser(ctx)
}
}
func (suite *RestoreOpIntegrationSuite) TestNewRestoreOperation() {
kw := &kopia.Wrapper{}
sw := &store.Wrapper{}
acct := tester.NewM365Account(suite.T())
dest := tester.DefaultTestRestoreDestination()
table := []struct {
name string
opts control.Options
kw *kopia.Wrapper
sw *store.Wrapper
acct account.Account
targets []string
errCheck assert.ErrorAssertionFunc
}{
{"good", control.Options{}, kw, sw, acct, nil, assert.NoError},
{"missing kopia", control.Options{}, nil, sw, acct, nil, assert.Error},
{"missing modelstore", control.Options{}, kw, nil, acct, nil, assert.Error},
}
for _, test := range table {
suite.Run(test.name, func() {
ctx, flush := tester.NewContext()
defer flush()
_, err := NewRestoreOperation(
ctx,
test.opts,
test.kw,
test.sw,
test.acct,
"backup-id",
selectors.Selector{DiscreteOwner: "test"},
dest,
evmock.NewBus())
test.errCheck(suite.T(), err)
})
}
}
//nolint:lll
func (suite *RestoreOpIntegrationSuite) TestRestore_Run() {
ctx, flush := tester.NewContext()
defer flush()
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())
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())
return bsel.Selector
},
cleanup: func(t *testing.T, dest string) {
act := tester.NewM365Account(t)
m365, err := act.M365Config()
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)
folderID, err := pager.GetFolderIDByName(ctx, driveID, dest)
require.NoError(t, err)
require.NotEmpty(t, folderID)
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, 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.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() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
rsel := selectors.NewExchangeRestore(selectors.None())
rsel.Include(rsel.AllData())
dest := tester.DefaultTestRestoreDestination()
mb := evmock.NewBus()
ro, err := NewRestoreOperation(
ctx,
control.Options{},
suite.kw,
suite.sw,
tester.NewM365Account(t),
suite.backupID,
rsel.Selector,
dest,
mb)
require.NoError(t, err)
ds, err := ro.Run(ctx)
require.Error(t, err, "restoreOp.Run() should have errored")
require.Nil(t, ds, "restoreOp.Run() should not produce details")
assert.Zero(t, ro.Results.ResourceOwners, "resource owners")
assert.Zero(t, ro.Results.BytesRead, "bytes read")
assert.Equal(t, 1, mb.TimesCalled[events.RestoreStart], "restore-start events")
assert.Zero(t, mb.TimesCalled[events.RestoreEnd], "restore-end events")
}