Remove references to the kopia package from `pkg/store` package so that kopia can import that package itself. Do this by using interfaces where needed in `pkg/store` instead of concrete struct types These changes will make cleaning up incomplete backups a little neater since that code will need to lookup both manifests and backup models This PR is just minor renaming and fixups, no logic changes --- #### Does this PR need a docs update or release note? - [ ] ✅ Yes, it's included - [ ] 🕐 Yes, but in a later PR - [x] ⛔ No #### Type of change - [ ] 🌻 Feature - [ ] 🐛 Bugfix - [ ] 🗺️ Documentation - [ ] 🤖 Supportability/Tests - [ ] 💻 CI/Deployment - [x] 🧹 Tech Debt/Cleanup #### Issue(s) * #3217 #### Test Plan - [ ] 💪 Manual - [x] ⚡ Unit test - [x] 💚 E2E
375 lines
9.6 KiB
Go
375 lines
9.6 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/dttm"
|
|
"github.com/alcionai/corso/src/internal/common/idname"
|
|
"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/m365"
|
|
"github.com/alcionai/corso/src/internal/m365/graph"
|
|
"github.com/alcionai/corso/src/internal/m365/mock"
|
|
"github.com/alcionai/corso/src/internal/m365/resource"
|
|
exchMock "github.com/alcionai/corso/src/internal/m365/service/exchange/mock"
|
|
"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/internal/tester/tconfig"
|
|
"github.com/alcionai/corso/src/pkg/account"
|
|
"github.com/alcionai/corso/src/pkg/control"
|
|
"github.com/alcionai/corso/src/pkg/control/repository"
|
|
"github.com/alcionai/corso/src/pkg/control/testdata"
|
|
"github.com/alcionai/corso/src/pkg/count"
|
|
"github.com/alcionai/corso/src/pkg/selectors"
|
|
storeTD "github.com/alcionai/corso/src/pkg/storage/testdata"
|
|
"github.com/alcionai/corso/src/pkg/store"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// unit
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type RestoreOpUnitSuite struct {
|
|
tester.Suite
|
|
}
|
|
|
|
func TestRestoreOpUnitSuite(t *testing.T) {
|
|
suite.Run(t, &RestoreOpUnitSuite{Suite: tester.NewUnitSuite(t)})
|
|
}
|
|
|
|
func (suite *RestoreOpUnitSuite) TestRestoreOperation_PersistResults() {
|
|
var (
|
|
kw = &kopia.Wrapper{}
|
|
sw = store.NewWrapper(&kopia.ModelStore{})
|
|
ctrl = &mock.Controller{}
|
|
now = time.Now()
|
|
restoreCfg = testdata.DefaultRestoreConfig("")
|
|
)
|
|
|
|
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.NoFetchRestoreCollection{
|
|
Collection: &exchMock.DataCollection{},
|
|
},
|
|
},
|
|
ctrl: &data.CollectionStats{
|
|
Objects: 1,
|
|
Successes: 1,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
expectStatus: Failed,
|
|
expectErr: assert.Error,
|
|
fail: assert.AnError,
|
|
stats: restoreStats{
|
|
bytesRead: &stats.ByteCounter{},
|
|
ctrl: &data.CollectionStats{},
|
|
},
|
|
},
|
|
{
|
|
expectStatus: NoData,
|
|
expectErr: assert.NoError,
|
|
stats: restoreStats{
|
|
bytesRead: &stats.ByteCounter{},
|
|
cs: []data.RestoreCollection{},
|
|
ctrl: &data.CollectionStats{},
|
|
},
|
|
},
|
|
}
|
|
for _, test := range table {
|
|
suite.Run(test.expectStatus.String(), func() {
|
|
t := suite.T()
|
|
|
|
ctx, flush := tester.NewContext(t)
|
|
defer flush()
|
|
|
|
op, err := NewRestoreOperation(
|
|
ctx,
|
|
control.DefaultOptions(),
|
|
kw,
|
|
sw,
|
|
ctrl,
|
|
account.Account{},
|
|
"foo",
|
|
selectors.Selector{DiscreteOwner: "test"},
|
|
restoreCfg,
|
|
evmock.NewBus(),
|
|
count.New())
|
|
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.ctrl.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")
|
|
})
|
|
}
|
|
}
|
|
|
|
func (suite *RestoreOpUnitSuite) TestChooseRestoreResource() {
|
|
var (
|
|
id = "id"
|
|
name = "name"
|
|
cfgWithPR = control.DefaultRestoreConfig(dttm.HumanReadable)
|
|
)
|
|
|
|
cfgWithPR.ProtectedResource = "cfgid"
|
|
|
|
table := []struct {
|
|
name string
|
|
cfg control.RestoreConfig
|
|
ctrl *mock.Controller
|
|
orig idname.Provider
|
|
expectErr assert.ErrorAssertionFunc
|
|
expectProvider assert.ValueAssertionFunc
|
|
expectID string
|
|
expectName string
|
|
}{
|
|
{
|
|
name: "use original",
|
|
cfg: control.DefaultRestoreConfig(dttm.HumanReadable),
|
|
ctrl: &mock.Controller{
|
|
ProtectedResourceID: id,
|
|
ProtectedResourceName: name,
|
|
},
|
|
orig: idname.NewProvider("oid", "oname"),
|
|
expectErr: assert.NoError,
|
|
expectID: "oid",
|
|
expectName: "oname",
|
|
},
|
|
{
|
|
name: "look up resource with iface",
|
|
cfg: cfgWithPR,
|
|
ctrl: &mock.Controller{
|
|
ProtectedResourceID: id,
|
|
ProtectedResourceName: name,
|
|
},
|
|
orig: idname.NewProvider("oid", "oname"),
|
|
expectErr: assert.NoError,
|
|
expectID: id,
|
|
expectName: name,
|
|
},
|
|
{
|
|
name: "error looking up protected resource",
|
|
cfg: cfgWithPR,
|
|
ctrl: &mock.Controller{
|
|
ProtectedResourceErr: assert.AnError,
|
|
},
|
|
orig: idname.NewProvider("oid", "oname"),
|
|
expectErr: assert.Error,
|
|
},
|
|
}
|
|
for _, test := range table {
|
|
suite.Run(test.name, func() {
|
|
t := suite.T()
|
|
|
|
ctx, flush := tester.NewContext(t)
|
|
defer flush()
|
|
|
|
result, err := chooseRestoreResource(ctx, test.ctrl, test.cfg, test.orig)
|
|
test.expectErr(t, err, clues.ToCore(err))
|
|
require.NotNil(t, result)
|
|
assert.Equal(t, test.expectID, result.ID())
|
|
assert.Equal(t, test.expectName, result.Name())
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// integration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type RestoreOpIntegrationSuite struct {
|
|
tester.Suite
|
|
|
|
kopiaCloser func(ctx context.Context)
|
|
acct account.Account
|
|
kw *kopia.Wrapper
|
|
sw store.BackupStorer
|
|
ms *kopia.ModelStore
|
|
}
|
|
|
|
func TestRestoreOpIntegrationSuite(t *testing.T) {
|
|
suite.Run(t, &RestoreOpIntegrationSuite{
|
|
Suite: tester.NewIntegrationSuite(
|
|
t,
|
|
[][]string{storeTD.AWSStorageCredEnvs, tconfig.M365AcctCredEnvs}),
|
|
})
|
|
}
|
|
|
|
func (suite *RestoreOpIntegrationSuite) SetupSuite() {
|
|
t := suite.T()
|
|
|
|
ctx, flush := tester.NewContext(t)
|
|
defer flush()
|
|
|
|
graph.InitializeConcurrencyLimiter(ctx, true, 4)
|
|
|
|
var (
|
|
st = storeTD.NewPrefixedS3Storage(t)
|
|
k = kopia.NewConn(st)
|
|
)
|
|
|
|
suite.acct = tconfig.NewM365Account(t)
|
|
|
|
err := k.Initialize(ctx, repository.Options{}, repository.Retention{})
|
|
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.NewWrapper(ms)
|
|
suite.sw = sw
|
|
}
|
|
|
|
func (suite *RestoreOpIntegrationSuite) TearDownSuite() {
|
|
ctx, flush := tester.NewContext(suite.T())
|
|
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.NewWrapper(&kopia.ModelStore{})
|
|
ctrl = &mock.Controller{}
|
|
restoreCfg = testdata.DefaultRestoreConfig("")
|
|
opts = control.DefaultOptions()
|
|
)
|
|
|
|
table := []struct {
|
|
name string
|
|
kw *kopia.Wrapper
|
|
sw store.BackupStorer
|
|
rc inject.RestoreConsumer
|
|
targets []string
|
|
errCheck assert.ErrorAssertionFunc
|
|
}{
|
|
{"good", kw, sw, ctrl, nil, assert.NoError},
|
|
{"missing kopia", nil, sw, ctrl, nil, assert.Error},
|
|
{"missing modelstore", kw, nil, ctrl, nil, assert.Error},
|
|
{"missing restore consumer", kw, sw, nil, nil, assert.Error},
|
|
}
|
|
for _, test := range table {
|
|
suite.Run(test.name, func() {
|
|
t := suite.T()
|
|
|
|
ctx, flush := tester.NewContext(t)
|
|
defer flush()
|
|
|
|
_, err := NewRestoreOperation(
|
|
ctx,
|
|
opts,
|
|
test.kw,
|
|
test.sw,
|
|
test.rc,
|
|
tconfig.NewM365Account(t),
|
|
"backup-id",
|
|
selectors.Selector{DiscreteOwner: "test"},
|
|
restoreCfg,
|
|
evmock.NewBus(),
|
|
count.New())
|
|
test.errCheck(t, err, clues.ToCore(err))
|
|
})
|
|
}
|
|
}
|
|
|
|
func (suite *RestoreOpIntegrationSuite) TestRestore_Run_errorNoBackup() {
|
|
t := suite.T()
|
|
|
|
ctx, flush := tester.NewContext(t)
|
|
defer flush()
|
|
|
|
var (
|
|
restoreCfg = testdata.DefaultRestoreConfig("")
|
|
mb = evmock.NewBus()
|
|
)
|
|
|
|
rsel := selectors.NewExchangeRestore(selectors.None())
|
|
rsel.Include(rsel.AllData())
|
|
|
|
ctrl, err := m365.NewController(
|
|
ctx,
|
|
suite.acct,
|
|
resource.Users,
|
|
rsel.PathService(),
|
|
control.DefaultOptions())
|
|
require.NoError(t, err, clues.ToCore(err))
|
|
|
|
ro, err := NewRestoreOperation(
|
|
ctx,
|
|
control.DefaultOptions(),
|
|
suite.kw,
|
|
suite.sw,
|
|
ctrl,
|
|
tconfig.NewM365Account(t),
|
|
"backupID",
|
|
rsel.Selector,
|
|
restoreCfg,
|
|
mb,
|
|
count.New())
|
|
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")
|
|
// no restore start, because we'd need to find the backup first.
|
|
assert.Equal(t, 0, mb.TimesCalled[events.RestoreStart], "restore-start events")
|
|
assert.Equal(t, 1, mb.TimesCalled[events.RestoreEnd], "restore-end events")
|
|
}
|