we've been failing on folder name collision, particularly in this package. Probably because it's using second-granularity folder names. --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🐛 Bugfix - [x] 🤖 Supportability/Tests #### Test Plan - [x] ⚡ Unit test - [x] 💚 E2E
500 lines
13 KiB
Go
500 lines
13 KiB
Go
package operations
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/alcionai/clues"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
|
|
"github.com/alcionai/corso/src/internal/common"
|
|
inMock "github.com/alcionai/corso/src/internal/common/idname/mock"
|
|
"github.com/alcionai/corso/src/internal/connector"
|
|
"github.com/alcionai/corso/src/internal/connector/exchange"
|
|
exchMock "github.com/alcionai/corso/src/internal/connector/exchange/mock"
|
|
"github.com/alcionai/corso/src/internal/connector/graph"
|
|
"github.com/alcionai/corso/src/internal/connector/mock"
|
|
"github.com/alcionai/corso/src/internal/connector/onedrive/api"
|
|
"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/operations/inject"
|
|
"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{}
|
|
gc = &mock.GraphConnector{}
|
|
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: &exchMock.DataCollection{},
|
|
},
|
|
},
|
|
gc: &data.CollectionStats{
|
|
Objects: 1,
|
|
Successes: 1,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
expectStatus: Failed,
|
|
expectErr: assert.Error,
|
|
fail: assert.AnError,
|
|
stats: restoreStats{
|
|
bytesRead: &stats.ByteCounter{},
|
|
gc: &data.CollectionStats{},
|
|
},
|
|
},
|
|
{
|
|
expectStatus: NoData,
|
|
expectErr: assert.NoError,
|
|
stats: restoreStats{
|
|
bytesRead: &stats.ByteCounter{},
|
|
cs: []data.RestoreCollection{},
|
|
gc: &data.CollectionStats{},
|
|
},
|
|
},
|
|
}
|
|
for _, test := range table {
|
|
suite.Run(test.expectStatus.String(), func() {
|
|
t := suite.T()
|
|
|
|
op, err := NewRestoreOperation(
|
|
ctx,
|
|
control.Defaults(),
|
|
kw,
|
|
sw,
|
|
gc,
|
|
acct,
|
|
"foo",
|
|
selectors.Selector{DiscreteOwner: "test"},
|
|
dest,
|
|
evmock.NewBus())
|
|
require.NoError(t, err, clues.ToCore(err))
|
|
|
|
op.Errors.Fail(test.fail)
|
|
|
|
err = op.persistResults(ctx, now, &test.stats)
|
|
test.expectErr(t, err, clues.ToCore(err))
|
|
|
|
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.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 bupResults struct {
|
|
selectorResourceOwners []string
|
|
backupID model.StableID
|
|
items int
|
|
gc *connector.GraphConnector
|
|
}
|
|
|
|
type RestoreOpIntegrationSuite struct {
|
|
tester.Suite
|
|
|
|
kopiaCloser func(ctx context.Context)
|
|
acct account.Account
|
|
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}),
|
|
})
|
|
}
|
|
|
|
func (suite *RestoreOpIntegrationSuite) SetupSuite() {
|
|
ctx, flush := tester.NewContext()
|
|
defer flush()
|
|
|
|
var (
|
|
t = suite.T()
|
|
st = tester.NewPrefixedS3Storage(t)
|
|
k = kopia.NewConn(st)
|
|
)
|
|
|
|
suite.acct = tester.NewM365Account(t)
|
|
|
|
err := k.Initialize(ctx)
|
|
require.NoError(t, err, clues.ToCore(err))
|
|
|
|
suite.kopiaCloser = func(ctx context.Context) {
|
|
k.Close(ctx)
|
|
}
|
|
|
|
kw, err := kopia.NewWrapper(k)
|
|
require.NoError(t, err, clues.ToCore(err))
|
|
|
|
suite.kw = kw
|
|
|
|
ms, err := kopia.NewModelStore(k)
|
|
require.NoError(t, err, clues.ToCore(err))
|
|
|
|
suite.ms = ms
|
|
|
|
sw := store.NewKopiaStore(ms)
|
|
suite.sw = sw
|
|
}
|
|
|
|
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() {
|
|
var (
|
|
kw = &kopia.Wrapper{}
|
|
sw = &store.Wrapper{}
|
|
gc = &mock.GraphConnector{}
|
|
acct = tester.NewM365Account(suite.T())
|
|
dest = tester.DefaultTestRestoreDestination()
|
|
opts = control.Defaults()
|
|
)
|
|
|
|
table := []struct {
|
|
name string
|
|
kw *kopia.Wrapper
|
|
sw *store.Wrapper
|
|
rc inject.RestoreConsumer
|
|
acct account.Account
|
|
targets []string
|
|
errCheck assert.ErrorAssertionFunc
|
|
}{
|
|
{"good", kw, sw, gc, acct, nil, assert.NoError},
|
|
{"missing kopia", nil, sw, gc, acct, nil, assert.Error},
|
|
{"missing modelstore", kw, nil, gc, acct, nil, assert.Error},
|
|
{"missing restore consumer", kw, sw, nil, acct, nil, assert.Error},
|
|
}
|
|
for _, test := range table {
|
|
suite.Run(test.name, func() {
|
|
ctx, flush := tester.NewContext()
|
|
defer flush()
|
|
|
|
_, err := NewRestoreOperation(
|
|
ctx,
|
|
opts,
|
|
test.kw,
|
|
test.sw,
|
|
test.rc,
|
|
test.acct,
|
|
"backup-id",
|
|
selectors.Selector{DiscreteOwner: "test"},
|
|
dest,
|
|
evmock.NewBus())
|
|
test.errCheck(suite.T(), err, clues.ToCore(err))
|
|
})
|
|
}
|
|
}
|
|
|
|
func setupExchangeBackup(
|
|
t *testing.T,
|
|
kw *kopia.Wrapper,
|
|
sw *store.Wrapper,
|
|
acct account.Account,
|
|
owner string,
|
|
) bupResults {
|
|
ctx, flush := tester.NewContext()
|
|
defer flush()
|
|
|
|
var (
|
|
users = []string{owner}
|
|
sel = selectors.NewExchangeBackup(users)
|
|
)
|
|
|
|
sel.DiscreteOwner = owner
|
|
sel.Include(
|
|
sel.MailFolders([]string{exchange.DefaultMailFolder}, selectors.PrefixMatch()),
|
|
sel.ContactFolders([]string{exchange.DefaultContactFolder}, selectors.PrefixMatch()),
|
|
sel.EventCalendars([]string{exchange.DefaultCalendar}, selectors.PrefixMatch()))
|
|
|
|
gc := GCWithSelector(t, ctx, acct, connector.Users, sel.Selector, nil, nil)
|
|
|
|
bo, err := NewBackupOperation(
|
|
ctx,
|
|
control.Defaults(),
|
|
kw,
|
|
sw,
|
|
gc,
|
|
acct,
|
|
sel.Selector,
|
|
inMock.NewProvider(owner, owner),
|
|
evmock.NewBus())
|
|
require.NoError(t, err, clues.ToCore(err))
|
|
|
|
err = bo.Run(ctx)
|
|
require.NoError(t, err, clues.ToCore(err))
|
|
require.NotEmpty(t, bo.Results.BackupID)
|
|
|
|
return bupResults{
|
|
selectorResourceOwners: users,
|
|
backupID: bo.Results.BackupID,
|
|
// Discount metadata collection files (1 delta and one prev path for each category).
|
|
// These meta files are used to aid restore, but are not themselves
|
|
// restored (ie: counted as writes).
|
|
items: bo.Results.ItemsWritten - 6,
|
|
gc: gc,
|
|
}
|
|
}
|
|
|
|
func setupSharePointBackup(
|
|
t *testing.T,
|
|
kw *kopia.Wrapper,
|
|
sw *store.Wrapper,
|
|
acct account.Account,
|
|
owner string,
|
|
) bupResults {
|
|
ctx, flush := tester.NewContext()
|
|
defer flush()
|
|
|
|
var (
|
|
sites = []string{owner}
|
|
sel = selectors.NewSharePointBackup(sites)
|
|
)
|
|
|
|
// assume a folder name "test" exists in the drive.
|
|
// this is brittle, and requires us to backfill anytime
|
|
// the site under test changes, but also prevents explosive
|
|
// growth from re-backup/restore of restored files.
|
|
sel.Include(sel.LibraryFolders([]string{"test"}, selectors.PrefixMatch()))
|
|
sel.DiscreteOwner = owner
|
|
|
|
gc := GCWithSelector(t, ctx, acct, connector.Sites, sel.Selector, nil, nil)
|
|
|
|
bo, err := NewBackupOperation(
|
|
ctx,
|
|
control.Defaults(),
|
|
kw,
|
|
sw,
|
|
gc,
|
|
acct,
|
|
sel.Selector,
|
|
inMock.NewProvider(owner, owner),
|
|
evmock.NewBus())
|
|
require.NoError(t, err, clues.ToCore(err))
|
|
|
|
// get the count of drives
|
|
m365, err := acct.M365Config()
|
|
require.NoError(t, err, clues.ToCore(err))
|
|
|
|
adpt, err := graph.CreateAdapter(
|
|
m365.AzureTenantID,
|
|
m365.AzureClientID,
|
|
m365.AzureClientSecret)
|
|
require.NoError(t, err, clues.ToCore(err))
|
|
|
|
service := graph.NewService(adpt)
|
|
spPgr := api.NewSiteDrivePager(service, owner, []string{"id", "name"})
|
|
|
|
drives, err := api.GetAllDrives(ctx, spPgr, true, 3)
|
|
require.NoError(t, err, clues.ToCore(err))
|
|
|
|
err = bo.Run(ctx)
|
|
require.NoError(t, err, clues.ToCore(err))
|
|
require.NotEmpty(t, bo.Results.BackupID)
|
|
|
|
return bupResults{
|
|
selectorResourceOwners: sites,
|
|
backupID: bo.Results.BackupID,
|
|
// Discount metadata files (1 delta, 1 prev path)
|
|
// assume only one folder, and therefore 1 dirmeta per drive
|
|
// assume only one file in each folder, and therefore 1 meta per drive.
|
|
// These meta files are used to aid restore, but are not themselves
|
|
// restored (ie: counted as writes).
|
|
items: bo.Results.ItemsWritten - 2 - len(drives) - len(drives),
|
|
gc: gc,
|
|
}
|
|
}
|
|
|
|
func (suite *RestoreOpIntegrationSuite) TestRestore_Run() {
|
|
ctx, flush := tester.NewContext()
|
|
defer flush()
|
|
|
|
tables := []struct {
|
|
name string
|
|
owner string
|
|
dest control.RestoreDestination
|
|
getSelector func(t *testing.T, owners []string) selectors.Selector
|
|
setup func(t *testing.T, kw *kopia.Wrapper, sw *store.Wrapper, acct account.Account, owner string) bupResults
|
|
}{
|
|
{
|
|
name: "Exchange_Restore",
|
|
owner: tester.M365UserID(suite.T()),
|
|
dest: tester.DefaultTestRestoreDestination(),
|
|
getSelector: func(t *testing.T, owners []string) selectors.Selector {
|
|
rsel := selectors.NewExchangeRestore(owners)
|
|
rsel.Include(rsel.AllData())
|
|
|
|
return rsel.Selector
|
|
},
|
|
setup: setupExchangeBackup,
|
|
},
|
|
{
|
|
name: "SharePoint_Restore",
|
|
owner: tester.M365SiteID(suite.T()),
|
|
dest: control.DefaultRestoreDestination(common.SimpleTimeTesting),
|
|
getSelector: func(t *testing.T, owners []string) selectors.Selector {
|
|
rsel := selectors.NewSharePointRestore(owners)
|
|
rsel.Include(rsel.AllData())
|
|
|
|
return rsel.Selector
|
|
},
|
|
setup: setupSharePointBackup,
|
|
},
|
|
}
|
|
|
|
for _, test := range tables {
|
|
suite.Run(test.name, func() {
|
|
var (
|
|
t = suite.T()
|
|
mb = evmock.NewBus()
|
|
bup = test.setup(t, suite.kw, suite.sw, suite.acct, test.owner)
|
|
)
|
|
|
|
require.NotZero(t, bup.items)
|
|
require.NotEmpty(t, bup.backupID)
|
|
|
|
ro, err := NewRestoreOperation(
|
|
ctx,
|
|
control.Options{FailureHandling: control.FailFast},
|
|
suite.kw,
|
|
suite.sw,
|
|
bup.gc,
|
|
tester.NewM365Account(t),
|
|
bup.backupID,
|
|
test.getSelector(t, bup.selectorResourceOwners),
|
|
test.dest,
|
|
mb)
|
|
require.NoError(t, err, clues.ToCore(err))
|
|
|
|
ds, err := ro.Run(ctx)
|
|
|
|
require.NoError(t, err, "restoreOp.Run() %+v", clues.ToCore(err))
|
|
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.Items()), "item write count matches len 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", clues.ToCore(ro.Errors.Failure()))
|
|
assert.Empty(t, ro.Errors.Recovered(), "recoverable errors")
|
|
assert.Equal(t, bup.items, 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")
|
|
})
|
|
}
|
|
}
|
|
|
|
func (suite *RestoreOpIntegrationSuite) TestRestore_Run_errorNoResults() {
|
|
ctx, flush := tester.NewContext()
|
|
defer flush()
|
|
|
|
var (
|
|
t = suite.T()
|
|
dest = tester.DefaultTestRestoreDestination()
|
|
mb = evmock.NewBus()
|
|
)
|
|
|
|
rsel := selectors.NewExchangeRestore(selectors.None())
|
|
rsel.Include(rsel.AllData())
|
|
|
|
gc, err := connector.NewGraphConnector(
|
|
ctx,
|
|
suite.acct,
|
|
connector.Users)
|
|
require.NoError(t, err, clues.ToCore(err))
|
|
|
|
ro, err := NewRestoreOperation(
|
|
ctx,
|
|
control.Defaults(),
|
|
suite.kw,
|
|
suite.sw,
|
|
gc,
|
|
tester.NewM365Account(t),
|
|
"backupID",
|
|
rsel.Selector,
|
|
dest,
|
|
mb)
|
|
require.NoError(t, err, clues.ToCore(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.Zero(t, mb.TimesCalled[events.RestoreStart], "restore-start events")
|
|
assert.Zero(t, mb.TimesCalled[events.RestoreEnd], "restore-end events")
|
|
}
|