add e2e backup-restore integration test (#311)
* add e2e backup-restore integration test Adds an e2e integration test that starts by backing up data, and ends with restoring it. Also makes various amendments to other code where necessary to facilitate this exercise.
This commit is contained in:
parent
76a5d6bba3
commit
105fd7383a
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/alcionai/corso/cli/utils"
|
"github.com/alcionai/corso/cli/utils"
|
||||||
"github.com/alcionai/corso/pkg/logger"
|
"github.com/alcionai/corso/pkg/logger"
|
||||||
"github.com/alcionai/corso/pkg/repository"
|
"github.com/alcionai/corso/pkg/repository"
|
||||||
|
"github.com/alcionai/corso/pkg/selectors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// exchange bucket info from flags
|
// exchange bucket info from flags
|
||||||
@ -85,7 +86,7 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
defer utils.CloseRepo(ctx, r)
|
defer utils.CloseRepo(ctx, r)
|
||||||
|
|
||||||
ro, err := r.NewRestore(ctx, backupID, []string{m365.TenantID, user, "mail", folder, mail})
|
ro, err := r.NewRestore(ctx, backupID, exchangeRestoreSelectors(user, folder, mail))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Failed to initialize Exchange restore")
|
return errors.Wrap(err, "Failed to initialize Exchange restore")
|
||||||
}
|
}
|
||||||
@ -98,9 +99,32 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateRestoreFlags(u, f, m, bID string) error {
|
func exchangeRestoreSelectors(u, f, m string) selectors.Selector {
|
||||||
if len(bID) == 0 {
|
sel := selectors.NewExchangeRestore()
|
||||||
return errors.New("a backup ID is requried")
|
if u == "*" {
|
||||||
|
u = selectors.All
|
||||||
|
}
|
||||||
|
if f == "*" {
|
||||||
|
f = selectors.All
|
||||||
|
}
|
||||||
|
if m == "*" {
|
||||||
|
m = selectors.All
|
||||||
|
}
|
||||||
|
if len(m) > 0 {
|
||||||
|
sel.Include(sel.Mails(u, f, m))
|
||||||
|
}
|
||||||
|
if len(f) > 0 && len(m) == 0 {
|
||||||
|
sel.Include(sel.MailFolders(u, f))
|
||||||
|
}
|
||||||
|
if len(f) == 0 && len(m) == 0 {
|
||||||
|
sel.Include(sel.Users(u))
|
||||||
|
}
|
||||||
|
return sel.Selector
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateRestoreFlags(u, f, m, rpid string) error {
|
||||||
|
if len(rpid) == 0 {
|
||||||
|
return errors.New("a restore point ID is requried")
|
||||||
}
|
}
|
||||||
lu, lf, lm := len(u), len(f), len(m)
|
lu, lf, lm := len(u), len(f), len(m)
|
||||||
if (lu == 0 || u == "*") && (lf+lm > 0) {
|
if (lu == 0 || u == "*") && (lf+lm > 0) {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ require (
|
|||||||
github.com/kopia/kopia v0.11.1
|
github.com/kopia/kopia v0.11.1
|
||||||
github.com/microsoft/kiota-abstractions-go v0.8.1
|
github.com/microsoft/kiota-abstractions-go v0.8.1
|
||||||
github.com/microsoft/kiota-authentication-azure-go v0.3.0
|
github.com/microsoft/kiota-authentication-azure-go v0.3.0
|
||||||
github.com/microsoft/kiota-serialization-json-go v0.5.4
|
github.com/microsoft/kiota-serialization-json-go v0.5.5
|
||||||
github.com/microsoftgraph/msgraph-sdk-go v0.28.0
|
github.com/microsoftgraph/msgraph-sdk-go v0.28.0
|
||||||
github.com/microsoftgraph/msgraph-sdk-go-core v0.26.1
|
github.com/microsoftgraph/msgraph-sdk-go-core v0.26.1
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
@ -45,7 +45,7 @@ require (
|
|||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
github.com/chmduquesne/rollinghash v4.0.0+incompatible // indirect
|
github.com/chmduquesne/rollinghash v4.0.0+incompatible // indirect
|
||||||
github.com/cjlapao/common-go v0.0.21 // indirect
|
github.com/cjlapao/common-go v0.0.22 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||||
github.com/edsrzf/mmap-go v1.1.0 // indirect
|
github.com/edsrzf/mmap-go v1.1.0 // indirect
|
||||||
|
|||||||
@ -69,8 +69,8 @@ github.com/chmduquesne/rollinghash v4.0.0+incompatible/go.mod h1:Uc2I36RRfTAf7Dg
|
|||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
github.com/cjlapao/common-go v0.0.21 h1:z4PDFLQG4pJxHEmM8ecmnjDgTcR0Xr/30WZiNZF2oYM=
|
github.com/cjlapao/common-go v0.0.22 h1:hQQ4mMupPp47eZRb5D8mxjrp0VyRSWNk/FOld7wxMsQ=
|
||||||
github.com/cjlapao/common-go v0.0.21/go.mod h1:QHUcl8KX3RgNVonFJ1WpW4mlr9NyWOHmzqxaRbwooPo=
|
github.com/cjlapao/common-go v0.0.22/go.mod h1:RZuwsymEIdwSubzUBpNYmmGfeITqHDV5iTgnr6zYwSc=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
@ -256,8 +256,8 @@ github.com/microsoft/kiota-authentication-azure-go v0.3.0 h1:iLyy5qldAjBiYMGMk1r
|
|||||||
github.com/microsoft/kiota-authentication-azure-go v0.3.0/go.mod h1:qyZWSCug2eG1zrRnCSacyFHGsgQa4aSCWn3EOkY9Z1M=
|
github.com/microsoft/kiota-authentication-azure-go v0.3.0/go.mod h1:qyZWSCug2eG1zrRnCSacyFHGsgQa4aSCWn3EOkY9Z1M=
|
||||||
github.com/microsoft/kiota-http-go v0.5.2 h1:BS/bK2xHLT8TT+p0uZKxwu+lkXDAPByugYP2n1nV0Uo=
|
github.com/microsoft/kiota-http-go v0.5.2 h1:BS/bK2xHLT8TT+p0uZKxwu+lkXDAPByugYP2n1nV0Uo=
|
||||||
github.com/microsoft/kiota-http-go v0.5.2/go.mod h1:WqEFNw3rMEatymG4Xh3rLSTxaKq80rJdQ/CSSh7m6jI=
|
github.com/microsoft/kiota-http-go v0.5.2/go.mod h1:WqEFNw3rMEatymG4Xh3rLSTxaKq80rJdQ/CSSh7m6jI=
|
||||||
github.com/microsoft/kiota-serialization-json-go v0.5.4 h1:BpkTYq1AeZPCnSsp3zpzfNL9hx3xb1/LPFteV6tbhMQ=
|
github.com/microsoft/kiota-serialization-json-go v0.5.5 h1:B0iKBKOdi+9NKFlormLRqduQ1+77MPGRsZ7xnd74EqQ=
|
||||||
github.com/microsoft/kiota-serialization-json-go v0.5.4/go.mod h1:GI9vrssO1EvqzDtvMKuhjALn40phZOWkeeaMgtCk6xE=
|
github.com/microsoft/kiota-serialization-json-go v0.5.5/go.mod h1:GI9vrssO1EvqzDtvMKuhjALn40phZOWkeeaMgtCk6xE=
|
||||||
github.com/microsoft/kiota-serialization-text-go v0.4.1 h1:6QPH7+geUPCpaSZkKCQw0Scngx2IF0vKodrvvWWiu2A=
|
github.com/microsoft/kiota-serialization-text-go v0.4.1 h1:6QPH7+geUPCpaSZkKCQw0Scngx2IF0vKodrvvWWiu2A=
|
||||||
github.com/microsoft/kiota-serialization-text-go v0.4.1/go.mod h1:DsriFnVBDCc4D84qxG3j8q/1Sxu16JILfhxMZm3kdfw=
|
github.com/microsoft/kiota-serialization-text-go v0.4.1/go.mod h1:DsriFnVBDCc4D84qxG3j8q/1Sxu16JILfhxMZm3kdfw=
|
||||||
github.com/microsoftgraph/msgraph-sdk-go v0.28.0 h1:BolP/vNW7gsNXivg/qikcdftOicLMgMm3Z/6PpSFDvU=
|
github.com/microsoftgraph/msgraph-sdk-go v0.28.0 h1:BolP/vNW7gsNXivg/qikcdftOicLMgMm3Z/6PpSFDvU=
|
||||||
|
|||||||
@ -5,6 +5,7 @@ package connector
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
az "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
az "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||||
ka "github.com/microsoft/kiota-authentication-azure-go"
|
ka "github.com/microsoft/kiota-authentication-azure-go"
|
||||||
@ -187,20 +188,30 @@ func (gc *GraphConnector) ExchangeDataCollection(ctx context.Context, selector s
|
|||||||
// RestoreMessages: Utility function to connect to M365 backstore
|
// RestoreMessages: Utility function to connect to M365 backstore
|
||||||
// and upload messages from DataCollection.
|
// and upload messages from DataCollection.
|
||||||
// FullPath: tenantId, userId, <mailCategory>, FolderId
|
// FullPath: tenantId, userId, <mailCategory>, FolderId
|
||||||
func (gc *GraphConnector) RestoreMessages(ctx context.Context, dc DataCollection) error {
|
func (gc *GraphConnector) RestoreMessages(ctx context.Context, dcs []DataCollection) error {
|
||||||
var errs error
|
var (
|
||||||
|
pathCounter = map[string]bool{}
|
||||||
|
attempts, successes int
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, dc := range dcs {
|
||||||
// must be user.GetId(), PrimaryName no longer works 6-15-2022
|
// must be user.GetId(), PrimaryName no longer works 6-15-2022
|
||||||
user := dc.FullPath()[1]
|
user := dc.FullPath()[1]
|
||||||
items := dc.Items()
|
items := dc.Items()
|
||||||
|
pathCounter[strings.Join(dc.FullPath(), "")] = true
|
||||||
|
|
||||||
for {
|
var exit bool
|
||||||
|
for !exit {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return support.WrapAndAppend("context cancelled", ctx.Err(), errs)
|
return support.WrapAndAppend("context cancelled", ctx.Err(), errs)
|
||||||
case data, ok := <-items:
|
case data, ok := <-items:
|
||||||
if !ok {
|
if !ok {
|
||||||
return errs
|
exit = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
attempts++
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
_, err := buf.ReadFrom(data.ToReader())
|
_, err := buf.ReadFrom(data.ToReader())
|
||||||
@ -215,7 +226,6 @@ func (gc *GraphConnector) RestoreMessages(ctx context.Context, dc DataCollection
|
|||||||
}
|
}
|
||||||
clone := support.ToMessage(message)
|
clone := support.ToMessage(message)
|
||||||
address := dc.FullPath()[3]
|
address := dc.FullPath()[3]
|
||||||
// details on valueId settings: https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxprops/77844470-22ca-43fb-993d-c53e96cf9cd6
|
|
||||||
valueId := "Integer 0x0E07"
|
valueId := "Integer 0x0E07"
|
||||||
enableValue := "4"
|
enableValue := "4"
|
||||||
sv := models.NewSingleValueLegacyExtendedProperty()
|
sv := models.NewSingleValueLegacyExtendedProperty()
|
||||||
@ -227,19 +237,28 @@ func (gc *GraphConnector) RestoreMessages(ctx context.Context, dc DataCollection
|
|||||||
clone.SetIsDraft(&draft)
|
clone.SetIsDraft(&draft)
|
||||||
sentMessage, err := gc.client.UsersById(user).MailFoldersById(address).Messages().Post(clone)
|
sentMessage, err := gc.client.UsersById(user).MailFoldersById(address).Messages().Post(clone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = support.WrapAndAppend(data.UUID()+": "+
|
errs = support.WrapAndAppend(
|
||||||
support.ConnectorStackErrorTrace(err), err, errs)
|
data.UUID()+": "+support.ConnectorStackErrorTrace(err),
|
||||||
|
err, errs)
|
||||||
continue
|
continue
|
||||||
// TODO: Add to retry Handler for the for failure
|
// TODO: Add to retry Handler for the for failure
|
||||||
}
|
}
|
||||||
|
|
||||||
if sentMessage == nil && err == nil {
|
if sentMessage == nil && err == nil {
|
||||||
errs = support.WrapAndAppend(data.UUID(), errors.New("Message not Sent: Blocked by server"), errs)
|
errs = support.WrapAndAppend(data.UUID(), errors.New("Message not Sent: Blocked by server"), errs)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
successes++
|
||||||
}
|
}
|
||||||
// This completes the restore loop for a message..
|
// This completes the restore loop for a message..
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status := support.CreateStatus(ctx, support.Restore, attempts, successes, len(pathCounter), errs)
|
||||||
|
gc.SetStatus(*status)
|
||||||
|
logger.Ctx(ctx).Debug(gc.PrintableStatus())
|
||||||
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
// serializeMessages: Temp Function as place Holder until Collections have been added
|
// serializeMessages: Temp Function as place Holder until Collections have been added
|
||||||
@ -299,11 +318,9 @@ func (gc *GraphConnector) serializeMessages(ctx context.Context, user string) ([
|
|||||||
success += edc.Length()
|
success += edc.Length()
|
||||||
collections = append(collections, &edc)
|
collections = append(collections, &edc)
|
||||||
}
|
}
|
||||||
status, err := support.CreateStatus(support.Backup, attemptedItems, success, len(tasklist), errs)
|
status := support.CreateStatus(ctx, support.Backup, attemptedItems, success, len(tasklist), errs)
|
||||||
if err == nil {
|
|
||||||
gc.SetStatus(*status)
|
gc.SetStatus(*status)
|
||||||
logger.Ctx(ctx).Debugw(gc.PrintableStatus())
|
logger.Ctx(ctx).Debugw(gc.PrintableStatus())
|
||||||
}
|
|
||||||
return collections, errs
|
return collections, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -83,7 +83,7 @@ func (suite *GraphConnectorIntegrationSuite) TestGraphConnector_restoreMessages(
|
|||||||
edc := NewExchangeDataCollection("tenant", []string{"tenantId", evs[user], mailCategory, "Inbox"})
|
edc := NewExchangeDataCollection("tenant", []string{"tenantId", evs[user], mailCategory, "Inbox"})
|
||||||
edc.PopulateCollection(&ds)
|
edc.PopulateCollection(&ds)
|
||||||
edc.FinishPopulation()
|
edc.FinishPopulation()
|
||||||
err = suite.connector.RestoreMessages(context.Background(), &edc)
|
err = suite.connector.RestoreMessages(context.Background(), []DataCollection{&edc})
|
||||||
assert.NoError(suite.T(), err)
|
assert.NoError(suite.T(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,9 +165,11 @@ func (suite *DisconnectedGraphConnectorSuite) TestInterfaceAlignment() {
|
|||||||
func (suite *DisconnectedGraphConnectorSuite) TestGraphConnector_Status() {
|
func (suite *DisconnectedGraphConnectorSuite) TestGraphConnector_Status() {
|
||||||
gc := GraphConnector{}
|
gc := GraphConnector{}
|
||||||
suite.Equal(len(gc.PrintableStatus()), 0)
|
suite.Equal(len(gc.PrintableStatus()), 0)
|
||||||
status, err := support.CreateStatus(support.Restore, 12, 9, 8,
|
status := support.CreateStatus(
|
||||||
|
context.Background(),
|
||||||
|
support.Restore,
|
||||||
|
12, 9, 8,
|
||||||
support.WrapAndAppend("tres", errors.New("three"), support.WrapAndAppend("arc376", errors.New("one"), errors.New("two"))))
|
support.WrapAndAppend("tres", errors.New("three"), support.WrapAndAppend("arc376", errors.New("one"), errors.New("two"))))
|
||||||
assert.NoError(suite.T(), err)
|
|
||||||
gc.SetStatus(*status)
|
gc.SetStatus(*status)
|
||||||
suite.Greater(len(gc.PrintableStatus()), 0)
|
suite.Greater(len(gc.PrintableStatus()), 0)
|
||||||
suite.Greater(gc.Status().ObjectCount, 0)
|
suite.Greater(gc.Status().ObjectCount, 0)
|
||||||
|
|||||||
@ -15,6 +15,8 @@ import (
|
|||||||
type MockExchangeDataCollection struct {
|
type MockExchangeDataCollection struct {
|
||||||
fullPath []string
|
fullPath []string
|
||||||
messageCount int
|
messageCount int
|
||||||
|
Data [][]byte
|
||||||
|
Names []string
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -26,11 +28,19 @@ var (
|
|||||||
// NewMockExchangeDataCollection creates an data collection that will return the specified number of
|
// NewMockExchangeDataCollection creates an data collection that will return the specified number of
|
||||||
// mock messages when iterated
|
// mock messages when iterated
|
||||||
func NewMockExchangeDataCollection(pathRepresentation []string, numMessagesToReturn int) *MockExchangeDataCollection {
|
func NewMockExchangeDataCollection(pathRepresentation []string, numMessagesToReturn int) *MockExchangeDataCollection {
|
||||||
collection := &MockExchangeDataCollection{
|
c := &MockExchangeDataCollection{
|
||||||
fullPath: pathRepresentation,
|
fullPath: pathRepresentation,
|
||||||
messageCount: numMessagesToReturn,
|
messageCount: numMessagesToReturn,
|
||||||
|
Data: [][]byte{},
|
||||||
|
Names: []string{},
|
||||||
}
|
}
|
||||||
return collection
|
|
||||||
|
for i := 0; i < c.messageCount; i++ {
|
||||||
|
// We can plug in whatever data we want here (can be an io.Reader to a test data file if needed)
|
||||||
|
c.Data = append(c.Data, []byte("test message"))
|
||||||
|
c.Names = append(c.Names, uuid.NewString())
|
||||||
|
}
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (medc *MockExchangeDataCollection) FullPath() []string {
|
func (medc *MockExchangeDataCollection) FullPath() []string {
|
||||||
@ -44,11 +54,11 @@ func (medc *MockExchangeDataCollection) Items() <-chan connector.DataStream {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer close(res)
|
defer close(res)
|
||||||
|
|
||||||
for i := 0; i < medc.messageCount; i++ {
|
for i := 0; i < medc.messageCount; i++ {
|
||||||
// We can plug in whatever data we want here (can be an io.Reader to a test data file if needed)
|
res <- &MockExchangeData{
|
||||||
m := []byte("test message")
|
medc.Names[i],
|
||||||
res <- &MockExchangeData{uuid.NewString(), io.NopCloser(bytes.NewReader(m))}
|
io.NopCloser(bytes.NewReader(medc.Data[i])),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
package support
|
package support
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConnectorOperationStatus struct {
|
type ConnectorOperationStatus struct {
|
||||||
@ -25,25 +27,30 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Constructor for ConnectorOperationStatus. If the counts do not agree, an error is returned.
|
// Constructor for ConnectorOperationStatus. If the counts do not agree, an error is returned.
|
||||||
func CreateStatus(op Operation, objects, success, folders int, err error) (*ConnectorOperationStatus, error) {
|
func CreateStatus(ctx context.Context, op Operation, objects, success, folders int, err error) *ConnectorOperationStatus {
|
||||||
hasErrors := err != nil
|
hasErrors := err != nil
|
||||||
var reason string
|
var reason string
|
||||||
if err != nil {
|
if err != nil {
|
||||||
reason = err.Error()
|
reason = err.Error()
|
||||||
}
|
}
|
||||||
|
numErr := GetNumberOfErrors(err)
|
||||||
status := ConnectorOperationStatus{
|
status := ConnectorOperationStatus{
|
||||||
lastOperation: op,
|
lastOperation: op,
|
||||||
ObjectCount: objects,
|
ObjectCount: objects,
|
||||||
folderCount: folders,
|
folderCount: folders,
|
||||||
successful: success,
|
successful: success,
|
||||||
errorCount: GetNumberOfErrors(err),
|
errorCount: numErr,
|
||||||
incomplete: hasErrors,
|
incomplete: hasErrors,
|
||||||
incompleteReason: reason,
|
incompleteReason: reason,
|
||||||
}
|
}
|
||||||
if status.ObjectCount != status.errorCount+status.successful {
|
if status.ObjectCount != status.errorCount+status.successful {
|
||||||
return nil, errors.New("incorrect total on initialization")
|
logger.Ctx(ctx).DPanicw(
|
||||||
|
"status object count does not match errors + successes",
|
||||||
|
"objects", objects,
|
||||||
|
"successes", success,
|
||||||
|
"errors", numErr)
|
||||||
}
|
}
|
||||||
return &status, nil
|
return &status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cos *ConnectorOperationStatus) String() string {
|
func (cos *ConnectorOperationStatus) String() string {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package support
|
package support
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -30,36 +31,35 @@ func (suite *GCStatusTestSuite) TestCreateStatus() {
|
|||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
params statusParams
|
params statusParams
|
||||||
expected bool
|
expect assert.BoolAssertionFunc
|
||||||
checkError assert.ValueAssertionFunc
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Test: Status Success",
|
name: "Test: Status Success",
|
||||||
params: statusParams{Backup, 12, 12, 3, nil},
|
params: statusParams{Backup, 12, 12, 3, nil},
|
||||||
expected: false,
|
expect: assert.False,
|
||||||
checkError: assert.Nil,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Test: Status Failed",
|
name: "Test: Status Failed",
|
||||||
params: statusParams{Restore, 12, 9, 8, WrapAndAppend("tres", errors.New("three"), WrapAndAppend("arc376", errors.New("one"), errors.New("two")))},
|
params: statusParams{Restore, 12, 9, 8, WrapAndAppend("tres", errors.New("three"), WrapAndAppend("arc376", errors.New("one"), errors.New("two")))},
|
||||||
expected: true,
|
expect: assert.True,
|
||||||
checkError: assert.Nil,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Invalid status",
|
name: "Invalid status",
|
||||||
params: statusParams{Backup, 9, 3, 12, errors.New("invalidcl")},
|
// todo: expect panic once logger.DPanicw identifies dev mode.
|
||||||
expected: false,
|
params: statusParams{Backup, 9, 3, 13, errors.New("invalidcl")},
|
||||||
checkError: assert.NotNil,
|
expect: assert.True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
suite.T().Run(test.name, func(t *testing.T) {
|
suite.T().Run(test.name, func(t *testing.T) {
|
||||||
result, err := CreateStatus(test.params.operationType, test.params.objects,
|
result := CreateStatus(
|
||||||
test.params.success, test.params.folders, test.params.err)
|
context.Background(),
|
||||||
test.checkError(t, err)
|
test.params.operationType,
|
||||||
if err == nil {
|
test.params.objects,
|
||||||
suite.Equal(result.incomplete, test.expected)
|
test.params.success,
|
||||||
}
|
test.params.folders,
|
||||||
|
test.params.err)
|
||||||
|
test.expect(t, result.incomplete, "status is incomplete")
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -321,13 +321,12 @@ func (w Wrapper) getEntry(
|
|||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectItems is a generic helper function that pulls data from kopia for the
|
// CollectItems pulls data from kopia for the given items in the snapshot with
|
||||||
// given item in the snapshot with ID snapshotID. If isDirectory is true, it
|
// ID snapshotID. If isDirectory is true, it returns a slice of DataCollections
|
||||||
// returns a slice of DataCollections with data from directories in the subtree
|
// with data from directories in the subtree rooted at itemPath. If isDirectory
|
||||||
// rooted at itemPath. If isDirectory is false it returns a DataCollection (in a
|
// is false it returns a DataCollection (in a slice) with a single item for each
|
||||||
// slice) with a single item corresponding to the requested item. If the item
|
// requested item. If the item does not exist or a file is found when a directory
|
||||||
// does not exist or a file is found when a directory is expected (or the
|
// is expected (or the opposite) it returns an error.
|
||||||
// opposite) it returns an error.
|
|
||||||
func (w Wrapper) collectItems(
|
func (w Wrapper) collectItems(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
snapshotID string,
|
snapshotID string,
|
||||||
@ -528,3 +527,30 @@ func (w Wrapper) RestoreDirectory(
|
|||||||
) ([]connector.DataCollection, error) {
|
) ([]connector.DataCollection, error) {
|
||||||
return w.collectItems(ctx, snapshotID, basePath, true)
|
return w.collectItems(ctx, snapshotID, basePath, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RestoreSingleItem looks up all paths- assuming each is an item declaration,
|
||||||
|
// not a directory- in the snapshot with id snapshotID. The path should be the
|
||||||
|
// full path of the item from the root. Returns the results as a slice of single-
|
||||||
|
// item DataCollections, where the DataCollection.FullPath() matches the path.
|
||||||
|
// If the item does not exist in kopia or is not a file an error is returned.
|
||||||
|
// The UUID of the returned DataStreams will be the name of the kopia file the
|
||||||
|
// data is sourced from.
|
||||||
|
func (w Wrapper) RestoreMultipleItems(
|
||||||
|
ctx context.Context,
|
||||||
|
snapshotID string,
|
||||||
|
paths [][]string,
|
||||||
|
) ([]connector.DataCollection, error) {
|
||||||
|
var (
|
||||||
|
dcs = []connector.DataCollection{}
|
||||||
|
errs *multierror.Error
|
||||||
|
)
|
||||||
|
for _, path := range paths {
|
||||||
|
dc, err := w.RestoreSingleItem(ctx, snapshotID, path)
|
||||||
|
if err != nil {
|
||||||
|
errs = multierror.Append(errs, err)
|
||||||
|
} else {
|
||||||
|
dcs = append(dcs, dc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dcs, errs.ErrorOrNil()
|
||||||
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/kopia/kopia/fs"
|
"github.com/kopia/kopia/fs"
|
||||||
"github.com/kopia/kopia/fs/virtualfs"
|
"github.com/kopia/kopia/fs/virtualfs"
|
||||||
"github.com/kopia/kopia/repo/manifest"
|
"github.com/kopia/kopia/repo/manifest"
|
||||||
@ -72,17 +73,12 @@ func testForFiles(
|
|||||||
fullPath := path.Join(append(c.FullPath(), s.UUID())...)
|
fullPath := path.Join(append(c.FullPath(), s.UUID())...)
|
||||||
|
|
||||||
expected, ok := expected[fullPath]
|
expected, ok := expected[fullPath]
|
||||||
require.True(
|
require.True(t, ok, "unexpected file with path %q", fullPath)
|
||||||
t,
|
|
||||||
ok,
|
|
||||||
"unexpected file with path %q",
|
|
||||||
path.Join(append(c.FullPath(), fullPath)...),
|
|
||||||
)
|
|
||||||
|
|
||||||
buf, err := ioutil.ReadAll(s.ToReader())
|
buf, err := ioutil.ReadAll(s.ToReader())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err, "reading collection item: %s", fullPath)
|
||||||
|
|
||||||
assert.Equal(t, expected, buf)
|
assert.Equal(t, expected, buf, "comparing collection item: %s", fullPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -674,3 +670,79 @@ func (suite *KopiaSimpleRepoIntegrationSuite) TestBackupRestoreDirectory_Errors(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *KopiaSimpleRepoIntegrationSuite) TestRestoreMultipleItems() {
|
||||||
|
t := suite.T()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
k, err := openKopiaRepo(t, ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
w := &Wrapper{k}
|
||||||
|
|
||||||
|
tid := uuid.NewString()
|
||||||
|
p1 := []string{tid, "uid", "emails", "fid"}
|
||||||
|
p2 := []string{tid, "uid2", "emails", "fid"}
|
||||||
|
dc1 := mockconnector.NewMockExchangeDataCollection(p1, 1)
|
||||||
|
dc2 := mockconnector.NewMockExchangeDataCollection(p2, 1)
|
||||||
|
fp1 := append(p1, dc1.Names[0])
|
||||||
|
fp2 := append(p2, dc2.Names[0])
|
||||||
|
|
||||||
|
stats, _, err := w.BackupCollections(ctx, []connector.DataCollection{dc1, dc2})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := map[string][]byte{
|
||||||
|
path.Join(fp1...): dc1.Data[0],
|
||||||
|
path.Join(fp2...): dc2.Data[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := w.RestoreMultipleItems(
|
||||||
|
ctx,
|
||||||
|
string(stats.SnapshotID),
|
||||||
|
[][]string{fp1, fp2})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 2, len(result))
|
||||||
|
|
||||||
|
testForFiles(t, expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *KopiaSimpleRepoIntegrationSuite) TestRestoreMultipleItems_Errors() {
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
snapshotID string
|
||||||
|
paths [][]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"EmptyPaths",
|
||||||
|
string(suite.snapshotID),
|
||||||
|
[][]string{{}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NoSnapshot",
|
||||||
|
"foo",
|
||||||
|
[][]string{append(testPath, testFileName)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"TargetNotAFile",
|
||||||
|
string(suite.snapshotID),
|
||||||
|
[][]string{testPath[:2]},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NonExistentFile",
|
||||||
|
string(suite.snapshotID),
|
||||||
|
[][]string{append(testPath, "subdir", "foo")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range table {
|
||||||
|
suite.T().Run(test.name, func(t *testing.T) {
|
||||||
|
_, err := suite.w.RestoreMultipleItems(
|
||||||
|
suite.ctx,
|
||||||
|
test.snapshotID,
|
||||||
|
test.paths,
|
||||||
|
)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/alcionai/corso/internal/connector"
|
"github.com/alcionai/corso/internal/connector"
|
||||||
"github.com/alcionai/corso/internal/connector/support"
|
"github.com/alcionai/corso/internal/connector/support"
|
||||||
"github.com/alcionai/corso/internal/kopia"
|
"github.com/alcionai/corso/internal/kopia"
|
||||||
|
"github.com/alcionai/corso/internal/model"
|
||||||
"github.com/alcionai/corso/pkg/account"
|
"github.com/alcionai/corso/pkg/account"
|
||||||
"github.com/alcionai/corso/pkg/backup"
|
"github.com/alcionai/corso/pkg/backup"
|
||||||
"github.com/alcionai/corso/pkg/selectors"
|
"github.com/alcionai/corso/pkg/selectors"
|
||||||
@ -29,7 +30,7 @@ type BackupOperation struct {
|
|||||||
type BackupResults struct {
|
type BackupResults struct {
|
||||||
summary
|
summary
|
||||||
metrics
|
metrics
|
||||||
// todo: Backup ID
|
BackupID model.ID `json:"backupID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBackupOperation constructs and validates a backup operation.
|
// NewBackupOperation constructs and validates a backup operation.
|
||||||
@ -113,10 +114,15 @@ func (op *BackupOperation) createBackupModels(ctx context.Context, snapID string
|
|||||||
return errors.Wrap(err, "creating backupdetails model")
|
return errors.Wrap(err, "creating backupdetails model")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = op.modelStore.Put(ctx, kopia.BackupModel, backup.New(snapID, string(details.ModelStoreID)))
|
rp := backup.New(snapID, string(details.ModelStoreID))
|
||||||
|
|
||||||
|
err = op.modelStore.Put(ctx, kopia.BackupModel, rp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "creating backup model")
|
return errors.Wrap(err, "creating backup model")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
op.Results.BackupID = rp.StableID
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -75,7 +75,10 @@ type BackupOpIntegrationSuite struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBackupOpIntegrationSuite(t *testing.T) {
|
func TestBackupOpIntegrationSuite(t *testing.T) {
|
||||||
if err := ctesting.RunOnAny(ctesting.CorsoCITests); err != nil {
|
if err := ctesting.RunOnAny(
|
||||||
|
ctesting.CorsoCITests,
|
||||||
|
ctesting.CorsoOperationTests,
|
||||||
|
); err != nil {
|
||||||
t.Skip(err)
|
t.Skip(err)
|
||||||
}
|
}
|
||||||
suite.Run(t, new(BackupOpIntegrationSuite))
|
suite.Run(t, new(BackupOpIntegrationSuite))
|
||||||
@ -128,10 +131,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run() {
|
|||||||
t := suite.T()
|
t := suite.T()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// m365User := "lidiah@8qzvrj.onmicrosoft.com"
|
m365User := "lidiah@8qzvrj.onmicrosoft.com"
|
||||||
// not the user we want to use, but all the others are
|
|
||||||
// suffering from JsonParseNode syndrome
|
|
||||||
m365User := "george.martinez@8qzvrj.onmicrosoft.com"
|
|
||||||
acct, err := ctesting.NewM365Account()
|
acct, err := ctesting.NewM365Account()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -168,6 +168,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run() {
|
|||||||
|
|
||||||
require.NoError(t, bo.Run(ctx))
|
require.NoError(t, bo.Run(ctx))
|
||||||
require.NotEmpty(t, bo.Results)
|
require.NotEmpty(t, bo.Results)
|
||||||
|
require.NotEmpty(t, bo.Results.BackupID)
|
||||||
assert.Equal(t, bo.Status, Successful)
|
assert.Equal(t, bo.Status, Successful)
|
||||||
assert.Greater(t, bo.Results.ItemsRead, 0)
|
assert.Greater(t, bo.Results.ItemsRead, 0)
|
||||||
assert.Greater(t, bo.Results.ItemsWritten, 0)
|
assert.Greater(t, bo.Results.ItemsWritten, 0)
|
||||||
|
|||||||
@ -4,22 +4,26 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/kopia/kopia/repo/manifest"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/alcionai/corso/internal/connector"
|
"github.com/alcionai/corso/internal/connector"
|
||||||
"github.com/alcionai/corso/internal/connector/support"
|
"github.com/alcionai/corso/internal/connector/support"
|
||||||
"github.com/alcionai/corso/internal/kopia"
|
"github.com/alcionai/corso/internal/kopia"
|
||||||
|
"github.com/alcionai/corso/internal/model"
|
||||||
"github.com/alcionai/corso/pkg/account"
|
"github.com/alcionai/corso/pkg/account"
|
||||||
|
"github.com/alcionai/corso/pkg/backup"
|
||||||
|
"github.com/alcionai/corso/pkg/selectors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RestoreOperation wraps an operation with restore-specific props.
|
// RestoreOperation wraps an operation with restore-specific props.
|
||||||
type RestoreOperation struct {
|
type RestoreOperation struct {
|
||||||
operation
|
operation
|
||||||
|
|
||||||
BackupID string `json:"backupID"`
|
BackupID model.ID `json:"backupID"`
|
||||||
Results RestoreResults `json:"results"`
|
Results RestoreResults `json:"results"`
|
||||||
Targets []string `json:"selectors"` // todo: replace with Selectors
|
Selectors selectors.Selector `json:"selectors"` // todo: replace with Selectors
|
||||||
Version string `json:"bersion"`
|
Version string `json:"version"`
|
||||||
|
|
||||||
account account.Account
|
account account.Account
|
||||||
}
|
}
|
||||||
@ -37,13 +41,13 @@ func NewRestoreOperation(
|
|||||||
kw *kopia.Wrapper,
|
kw *kopia.Wrapper,
|
||||||
ms *kopia.ModelStore,
|
ms *kopia.ModelStore,
|
||||||
acct account.Account,
|
acct account.Account,
|
||||||
backupID string,
|
backupID model.ID,
|
||||||
targets []string,
|
sel selectors.Selector,
|
||||||
) (RestoreOperation, error) {
|
) (RestoreOperation, error) {
|
||||||
op := RestoreOperation{
|
op := RestoreOperation{
|
||||||
operation: newOperation(opts, kw, ms),
|
operation: newOperation(opts, kw, ms),
|
||||||
BackupID: backupID,
|
BackupID: backupID,
|
||||||
Targets: targets,
|
Selectors: sel,
|
||||||
Version: "v0",
|
Version: "v0",
|
||||||
account: acct,
|
account: acct,
|
||||||
}
|
}
|
||||||
@ -77,23 +81,48 @@ func (op *RestoreOperation) Run(ctx context.Context) error {
|
|||||||
stats := restoreStats{}
|
stats := restoreStats{}
|
||||||
defer op.persistResults(time.Now(), &stats)
|
defer op.persistResults(time.Now(), &stats)
|
||||||
|
|
||||||
dc, err := op.kopia.RestoreSingleItem(ctx, op.BackupID, op.Targets)
|
// retrieve the restore point details
|
||||||
|
rp := backup.Backup{}
|
||||||
|
err := op.modelStore.Get(ctx, kopia.BackupModel, op.BackupID, &rp)
|
||||||
|
if err != nil {
|
||||||
|
stats.readErr = errors.Wrap(err, "retrieving restore point")
|
||||||
|
return stats.readErr
|
||||||
|
}
|
||||||
|
|
||||||
|
rpd := backup.Details{}
|
||||||
|
err = op.modelStore.GetWithModelStoreID(ctx, kopia.BackupDetailsModel, manifest.ID(rp.DetailsID), &rpd)
|
||||||
|
if err != nil {
|
||||||
|
stats.readErr = errors.Wrap(err, "retrieving restore point details")
|
||||||
|
return stats.readErr
|
||||||
|
}
|
||||||
|
|
||||||
|
er, err := op.Selectors.ToExchangeRestore()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stats.readErr = err
|
stats.readErr = err
|
||||||
return errors.Wrap(err, "retrieving service data")
|
return err
|
||||||
}
|
}
|
||||||
stats.cs = []connector.DataCollection{dc}
|
|
||||||
|
|
||||||
|
// format the details and retrieve the items from kopia
|
||||||
|
fds := er.FilterDetails(&rpd)
|
||||||
|
dcs, err := op.kopia.RestoreMultipleItems(ctx, rp.SnapshotID, fds)
|
||||||
|
if err != nil {
|
||||||
|
stats.readErr = errors.Wrap(err, "retrieving service data")
|
||||||
|
return stats.readErr
|
||||||
|
}
|
||||||
|
stats.cs = dcs
|
||||||
|
|
||||||
|
// restore those collections using graph
|
||||||
gc, err := connector.NewGraphConnector(op.account)
|
gc, err := connector.NewGraphConnector(op.account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stats.writeErr = err
|
stats.writeErr = errors.Wrap(err, "connecting to graph api")
|
||||||
return errors.Wrap(err, "connecting to graph api")
|
return stats.writeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gc.RestoreMessages(ctx, dc); err != nil {
|
if err := gc.RestoreMessages(ctx, dcs); err != nil {
|
||||||
stats.writeErr = err
|
stats.writeErr = errors.Wrap(err, "restoring service data")
|
||||||
return errors.Wrap(err, "restoring service data")
|
return stats.writeErr
|
||||||
}
|
}
|
||||||
|
stats.gc = gc.Status()
|
||||||
|
|
||||||
op.Status = Successful
|
op.Status = Successful
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/alcionai/corso/internal/kopia"
|
"github.com/alcionai/corso/internal/kopia"
|
||||||
ctesting "github.com/alcionai/corso/internal/testing"
|
ctesting "github.com/alcionai/corso/internal/testing"
|
||||||
"github.com/alcionai/corso/pkg/account"
|
"github.com/alcionai/corso/pkg/account"
|
||||||
|
"github.com/alcionai/corso/pkg/selectors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -50,7 +51,7 @@ func (suite *RestoreOpSuite) TestRestoreOperation_PersistResults() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
op, err := NewRestoreOperation(ctx, Options{}, kw, ms, acct, "foo", nil)
|
op, err := NewRestoreOperation(ctx, Options{}, kw, ms, acct, "foo", selectors.Selector{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
op.persistResults(now, &stats)
|
op.persistResults(now, &stats)
|
||||||
@ -73,7 +74,10 @@ type RestoreOpIntegrationSuite struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRestoreOpIntegrationSuite(t *testing.T) {
|
func TestRestoreOpIntegrationSuite(t *testing.T) {
|
||||||
if err := ctesting.RunOnAny(ctesting.CorsoCITests); err != nil {
|
if err := ctesting.RunOnAny(
|
||||||
|
ctesting.CorsoCITests,
|
||||||
|
ctesting.CorsoOperationTests,
|
||||||
|
); err != nil {
|
||||||
t.Skip(err)
|
t.Skip(err)
|
||||||
}
|
}
|
||||||
suite.Run(t, new(RestoreOpIntegrationSuite))
|
suite.Run(t, new(RestoreOpIntegrationSuite))
|
||||||
@ -112,8 +116,69 @@ func (suite *RestoreOpIntegrationSuite) TestNewRestoreOperation() {
|
|||||||
test.ms,
|
test.ms,
|
||||||
test.acct,
|
test.acct,
|
||||||
"backup-id",
|
"backup-id",
|
||||||
nil)
|
selectors.Selector{})
|
||||||
test.errCheck(t, err)
|
test.errCheck(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *RestoreOpIntegrationSuite) TestRestore_Run() {
|
||||||
|
t := suite.T()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
m365User := "lidiah@8qzvrj.onmicrosoft.com"
|
||||||
|
acct, err := ctesting.NewM365Account()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// need to initialize the repository before we can test connecting to it.
|
||||||
|
st, err := ctesting.NewPrefixedS3Storage(t)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
k := kopia.NewConn(st)
|
||||||
|
require.NoError(t, k.Initialize(ctx))
|
||||||
|
defer k.Close(ctx)
|
||||||
|
|
||||||
|
w, err := kopia.NewWrapper(k)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer w.Close(ctx)
|
||||||
|
|
||||||
|
ms, err := kopia.NewModelStore(k)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer ms.Close(ctx)
|
||||||
|
|
||||||
|
bsel := selectors.NewExchangeBackup()
|
||||||
|
bsel.Include(bsel.Users(m365User))
|
||||||
|
|
||||||
|
bo, err := NewBackupOperation(
|
||||||
|
ctx,
|
||||||
|
Options{},
|
||||||
|
w,
|
||||||
|
ms,
|
||||||
|
acct,
|
||||||
|
bsel.Selector)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, bo.Run(ctx))
|
||||||
|
require.NotEmpty(t, bo.Results.BackupID)
|
||||||
|
|
||||||
|
rsel := selectors.NewExchangeRestore()
|
||||||
|
rsel.Include(rsel.Users(m365User))
|
||||||
|
|
||||||
|
ro, err := NewRestoreOperation(
|
||||||
|
ctx,
|
||||||
|
Options{},
|
||||||
|
w,
|
||||||
|
ms,
|
||||||
|
acct,
|
||||||
|
bo.Results.BackupID,
|
||||||
|
rsel.Selector)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, ro.Run(ctx), "restoreOp.Run()")
|
||||||
|
require.NotEmpty(t, ro.Results, "restoreOp results")
|
||||||
|
assert.Equal(t, ro.Status, Successful, "restoreOp status")
|
||||||
|
assert.Greater(t, ro.Results.ItemsRead, 0, "restore items read")
|
||||||
|
assert.Greater(t, ro.Results.ItemsWritten, 0, "restored items written")
|
||||||
|
assert.Zero(t, ro.Results.ReadErrors, "errors while reading restore data")
|
||||||
|
assert.Zero(t, ro.Results.WriteErrors, "errors while writing restore data")
|
||||||
|
assert.Equal(t, bo.Results.ItemsWritten, ro.Results.ItemsWritten, "backup and restore wrote the same num of items")
|
||||||
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ const (
|
|||||||
CorsoGraphConnectorTests = "CORSO_GRAPH_CONNECTOR_TESTS"
|
CorsoGraphConnectorTests = "CORSO_GRAPH_CONNECTOR_TESTS"
|
||||||
CorsoKopiaWrapperTests = "CORSO_KOPIA_WRAPPER_TESTS"
|
CorsoKopiaWrapperTests = "CORSO_KOPIA_WRAPPER_TESTS"
|
||||||
CorsoModelStoreTests = "CORSO_MODEL_STORE_TESTS"
|
CorsoModelStoreTests = "CORSO_MODEL_STORE_TESTS"
|
||||||
|
CorsoOperationTests = "CORSO_OPERATION_TESTS"
|
||||||
CorsoRepositoryTests = "CORSO_REPOSITORY_TESTS"
|
CorsoRepositoryTests = "CORSO_REPOSITORY_TESTS"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/alcionai/corso/internal/kopia"
|
"github.com/alcionai/corso/internal/kopia"
|
||||||
|
"github.com/alcionai/corso/internal/model"
|
||||||
"github.com/alcionai/corso/internal/operations"
|
"github.com/alcionai/corso/internal/operations"
|
||||||
"github.com/alcionai/corso/pkg/account"
|
"github.com/alcionai/corso/pkg/account"
|
||||||
"github.com/alcionai/corso/pkg/backup"
|
"github.com/alcionai/corso/pkg/backup"
|
||||||
@ -138,15 +139,15 @@ func (r Repository) NewBackup(ctx context.Context, selector selectors.Selector)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewRestore generates a restoreOperation runner.
|
// NewRestore generates a restoreOperation runner.
|
||||||
func (r Repository) NewRestore(ctx context.Context, backupID string, targets []string) (operations.RestoreOperation, error) {
|
func (r Repository) NewRestore(ctx context.Context, backupID string, sel selectors.Selector) (operations.RestoreOperation, error) {
|
||||||
return operations.NewRestoreOperation(
|
return operations.NewRestoreOperation(
|
||||||
ctx,
|
ctx,
|
||||||
operations.Options{},
|
operations.Options{},
|
||||||
r.dataLayer,
|
r.dataLayer,
|
||||||
r.modelStore,
|
r.modelStore,
|
||||||
r.Account,
|
r.Account,
|
||||||
backupID,
|
model.ID(backupID),
|
||||||
targets)
|
sel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// backups lists backups in a respository
|
// backups lists backups in a respository
|
||||||
|
|||||||
@ -190,7 +190,7 @@ func (suite *RepositoryIntegrationSuite) TestNewRestore() {
|
|||||||
r, err := repository.Initialize(ctx, acct, st)
|
r, err := repository.Initialize(ctx, acct, st)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ro, err := r.NewRestore(ctx, "backup-id", []string{})
|
ro, err := r.NewRestore(ctx, "backup-id", selectors.Selector{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, ro)
|
require.NotNil(t, ro)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,7 +36,7 @@ type (
|
|||||||
func NewExchangeBackup() *ExchangeBackup {
|
func NewExchangeBackup() *ExchangeBackup {
|
||||||
src := ExchangeBackup{
|
src := ExchangeBackup{
|
||||||
exchange{
|
exchange{
|
||||||
newSelector(ServiceExchange, ""),
|
newSelector(ServiceExchange),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return &src
|
return &src
|
||||||
@ -53,10 +53,10 @@ func (s Selector) ToExchangeBackup() (*ExchangeBackup, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewExchangeRestore produces a new Selector with the service set to ServiceExchange.
|
// NewExchangeRestore produces a new Selector with the service set to ServiceExchange.
|
||||||
func NewExchangeRestore(backupID string) *ExchangeRestore {
|
func NewExchangeRestore() *ExchangeRestore {
|
||||||
src := ExchangeRestore{
|
src := ExchangeRestore{
|
||||||
exchange{
|
exchange{
|
||||||
newSelector(ServiceExchange, backupID),
|
newSelector(ServiceExchange),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return &src
|
return &src
|
||||||
@ -406,15 +406,15 @@ func idPath(cat exchangeCategory, path []string) map[exchangeCategory]string {
|
|||||||
|
|
||||||
// FilterDetails reduces the entries in a backupDetails struct to only
|
// FilterDetails reduces the entries in a backupDetails struct to only
|
||||||
// those that match the inclusions and exclusions in the selector.
|
// those that match the inclusions and exclusions in the selector.
|
||||||
func (s *ExchangeRestore) FilterDetails(deets *backup.Details) []string {
|
func (s *ExchangeRestore) FilterDetails(deets *backup.Details) [][]string {
|
||||||
if deets == nil {
|
if deets == nil {
|
||||||
return []string{}
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
entIncs := exchangeScopesByCategory(s.Includes)
|
entIncs := exchangeScopesByCategory(s.Includes)
|
||||||
entExcs := exchangeScopesByCategory(s.Excludes)
|
entExcs := exchangeScopesByCategory(s.Excludes)
|
||||||
|
|
||||||
refs := []string{}
|
refs := [][]string{}
|
||||||
|
|
||||||
for _, ent := range deets.Entries {
|
for _, ent := range deets.Entries {
|
||||||
path := strings.Split(ent.RepoRef, "/")
|
path := strings.Split(ent.RepoRef, "/")
|
||||||
@ -438,7 +438,7 @@ func (s *ExchangeRestore) FilterDetails(deets *backup.Details) []string {
|
|||||||
entIncs[cat.String()],
|
entIncs[cat.String()],
|
||||||
entExcs[cat.String()])
|
entExcs[cat.String()])
|
||||||
if matched {
|
if matched {
|
||||||
refs = append(refs, ent.RepoRef)
|
refs = append(refs, path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package selectors
|
package selectors
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/alcionai/corso/pkg/backup"
|
"github.com/alcionai/corso/pkg/backup"
|
||||||
@ -21,7 +22,6 @@ func (suite *ExchangeSourceSuite) TestNewExchangeBackup() {
|
|||||||
t := suite.T()
|
t := suite.T()
|
||||||
eb := NewExchangeBackup()
|
eb := NewExchangeBackup()
|
||||||
assert.Equal(t, eb.Service, ServiceExchange)
|
assert.Equal(t, eb.Service, ServiceExchange)
|
||||||
assert.Zero(t, eb.BackupID)
|
|
||||||
assert.NotZero(t, eb.Scopes())
|
assert.NotZero(t, eb.Scopes())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,26 +32,23 @@ func (suite *ExchangeSourceSuite) TestToExchangeBackup() {
|
|||||||
eb, err := s.ToExchangeBackup()
|
eb, err := s.ToExchangeBackup()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, eb.Service, ServiceExchange)
|
assert.Equal(t, eb.Service, ServiceExchange)
|
||||||
assert.Zero(t, eb.BackupID)
|
|
||||||
assert.NotZero(t, eb.Scopes())
|
assert.NotZero(t, eb.Scopes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ExchangeSourceSuite) TestNewExchangeRestore() {
|
func (suite *ExchangeSourceSuite) TestNewExchangeRestore() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
er := NewExchangeRestore("backupID")
|
er := NewExchangeRestore()
|
||||||
assert.Equal(t, er.Service, ServiceExchange)
|
assert.Equal(t, er.Service, ServiceExchange)
|
||||||
assert.Equal(t, er.BackupID, "backupID")
|
|
||||||
assert.NotZero(t, er.Scopes())
|
assert.NotZero(t, er.Scopes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ExchangeSourceSuite) TestToExchangeRestore() {
|
func (suite *ExchangeSourceSuite) TestToExchangeRestore() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
eb := NewExchangeRestore("rpid")
|
eb := NewExchangeRestore()
|
||||||
s := eb.Selector
|
s := eb.Selector
|
||||||
eb, err := s.ToExchangeRestore()
|
eb, err := s.ToExchangeRestore()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, eb.Service, ServiceExchange)
|
assert.Equal(t, eb.Service, ServiceExchange)
|
||||||
assert.Equal(t, eb.BackupID, "rpid")
|
|
||||||
assert.NotZero(t, eb.Scopes())
|
assert.NotZero(t, eb.Scopes())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -486,7 +483,7 @@ func (suite *ExchangeSourceSuite) TestExchangeScope_IncludesPath() {
|
|||||||
)
|
)
|
||||||
var (
|
var (
|
||||||
path = []string{"tid", usr, "mail", fld, mail}
|
path = []string{"tid", usr, "mail", fld, mail}
|
||||||
es = NewExchangeRestore("rpid")
|
es = NewExchangeRestore()
|
||||||
)
|
)
|
||||||
|
|
||||||
table := []struct {
|
table := []struct {
|
||||||
@ -526,7 +523,7 @@ func (suite *ExchangeSourceSuite) TestExchangeScope_ExcludesPath() {
|
|||||||
)
|
)
|
||||||
var (
|
var (
|
||||||
path = []string{"tid", usr, "mail", fld, mail}
|
path = []string{"tid", usr, "mail", fld, mail}
|
||||||
es = NewExchangeRestore("rpid")
|
es = NewExchangeRestore()
|
||||||
)
|
)
|
||||||
|
|
||||||
table := []struct {
|
table := []struct {
|
||||||
@ -622,124 +619,131 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() {
|
|||||||
event = "tid/uid/event/eid"
|
event = "tid/uid/event/eid"
|
||||||
mail = "tid/uid/mail/mfld/mid"
|
mail = "tid/uid/mail/mfld/mid"
|
||||||
)
|
)
|
||||||
|
split := func(s ...string) [][]string {
|
||||||
|
r := [][]string{}
|
||||||
|
for _, ss := range s {
|
||||||
|
r = append(r, strings.Split(ss, "/"))
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
deets *backup.Details
|
deets *backup.Details
|
||||||
makeSelector func() *ExchangeRestore
|
makeSelector func() *ExchangeRestore
|
||||||
expect []string
|
expect [][]string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"no refs",
|
"no refs",
|
||||||
makeDeets(),
|
makeDeets(),
|
||||||
func() *ExchangeRestore {
|
func() *ExchangeRestore {
|
||||||
er := NewExchangeRestore("rpid")
|
er := NewExchangeRestore()
|
||||||
er.Include(er.Users(All))
|
er.Include(er.Users(All))
|
||||||
return er
|
return er
|
||||||
},
|
},
|
||||||
[]string{},
|
[][]string{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"contact only",
|
"contact only",
|
||||||
makeDeets(contact),
|
makeDeets(contact),
|
||||||
func() *ExchangeRestore {
|
func() *ExchangeRestore {
|
||||||
er := NewExchangeRestore("rpid")
|
er := NewExchangeRestore()
|
||||||
er.Include(er.Users(All))
|
er.Include(er.Users(All))
|
||||||
return er
|
return er
|
||||||
},
|
},
|
||||||
[]string{contact},
|
split(contact),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"event only",
|
"event only",
|
||||||
makeDeets(event),
|
makeDeets(event),
|
||||||
func() *ExchangeRestore {
|
func() *ExchangeRestore {
|
||||||
er := NewExchangeRestore("rpid")
|
er := NewExchangeRestore()
|
||||||
er.Include(er.Users(All))
|
er.Include(er.Users(All))
|
||||||
return er
|
return er
|
||||||
},
|
},
|
||||||
[]string{event},
|
split(event),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"mail only",
|
"mail only",
|
||||||
makeDeets(mail),
|
makeDeets(mail),
|
||||||
func() *ExchangeRestore {
|
func() *ExchangeRestore {
|
||||||
er := NewExchangeRestore("rpid")
|
er := NewExchangeRestore()
|
||||||
er.Include(er.Users(All))
|
er.Include(er.Users(All))
|
||||||
return er
|
return er
|
||||||
},
|
},
|
||||||
[]string{mail},
|
split(mail),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"all",
|
"all",
|
||||||
makeDeets(contact, event, mail),
|
makeDeets(contact, event, mail),
|
||||||
func() *ExchangeRestore {
|
func() *ExchangeRestore {
|
||||||
er := NewExchangeRestore("rpid")
|
er := NewExchangeRestore()
|
||||||
er.Include(er.Users(All))
|
er.Include(er.Users(All))
|
||||||
return er
|
return er
|
||||||
},
|
},
|
||||||
[]string{contact, event, mail},
|
split(contact, event, mail),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"only match contact",
|
"only match contact",
|
||||||
makeDeets(contact, event, mail),
|
makeDeets(contact, event, mail),
|
||||||
func() *ExchangeRestore {
|
func() *ExchangeRestore {
|
||||||
er := NewExchangeRestore("rpid")
|
er := NewExchangeRestore()
|
||||||
er.Include(er.Contacts("uid", "cfld", "cid"))
|
er.Include(er.Contacts("uid", "cfld", "cid"))
|
||||||
return er
|
return er
|
||||||
},
|
},
|
||||||
[]string{contact},
|
split(contact),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"only match event",
|
"only match event",
|
||||||
makeDeets(contact, event, mail),
|
makeDeets(contact, event, mail),
|
||||||
func() *ExchangeRestore {
|
func() *ExchangeRestore {
|
||||||
er := NewExchangeRestore("rpid")
|
er := NewExchangeRestore()
|
||||||
er.Include(er.Events("uid", "eid"))
|
er.Include(er.Events("uid", "eid"))
|
||||||
return er
|
return er
|
||||||
},
|
},
|
||||||
[]string{event},
|
split(event),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"only match mail",
|
"only match mail",
|
||||||
makeDeets(contact, event, mail),
|
makeDeets(contact, event, mail),
|
||||||
func() *ExchangeRestore {
|
func() *ExchangeRestore {
|
||||||
er := NewExchangeRestore("rpid")
|
er := NewExchangeRestore()
|
||||||
er.Include(er.Mails("uid", "mfld", "mid"))
|
er.Include(er.Mails("uid", "mfld", "mid"))
|
||||||
return er
|
return er
|
||||||
},
|
},
|
||||||
[]string{mail},
|
split(mail),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"exclude contact",
|
"exclude contact",
|
||||||
makeDeets(contact, event, mail),
|
makeDeets(contact, event, mail),
|
||||||
func() *ExchangeRestore {
|
func() *ExchangeRestore {
|
||||||
er := NewExchangeRestore("rpid")
|
er := NewExchangeRestore()
|
||||||
er.Include(er.Users(All))
|
er.Include(er.Users(All))
|
||||||
er.Exclude(er.Contacts("uid", "cfld", "cid"))
|
er.Exclude(er.Contacts("uid", "cfld", "cid"))
|
||||||
return er
|
return er
|
||||||
},
|
},
|
||||||
[]string{event, mail},
|
split(event, mail),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"exclude event",
|
"exclude event",
|
||||||
makeDeets(contact, event, mail),
|
makeDeets(contact, event, mail),
|
||||||
func() *ExchangeRestore {
|
func() *ExchangeRestore {
|
||||||
er := NewExchangeRestore("rpid")
|
er := NewExchangeRestore()
|
||||||
er.Include(er.Users(All))
|
er.Include(er.Users(All))
|
||||||
er.Exclude(er.Events("uid", "eid"))
|
er.Exclude(er.Events("uid", "eid"))
|
||||||
return er
|
return er
|
||||||
},
|
},
|
||||||
[]string{contact, mail},
|
split(contact, mail),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"exclude mail",
|
"exclude mail",
|
||||||
makeDeets(contact, event, mail),
|
makeDeets(contact, event, mail),
|
||||||
func() *ExchangeRestore {
|
func() *ExchangeRestore {
|
||||||
er := NewExchangeRestore("rpid")
|
er := NewExchangeRestore()
|
||||||
er.Include(er.Users(All))
|
er.Include(er.Users(All))
|
||||||
er.Exclude(er.Mails("uid", "mfld", "mid"))
|
er.Exclude(er.Mails("uid", "mfld", "mid"))
|
||||||
return er
|
return er
|
||||||
},
|
},
|
||||||
[]string{contact, event},
|
split(contact, event),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
@ -753,7 +757,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() {
|
|||||||
|
|
||||||
func (suite *ExchangeSourceSuite) TestExchangeScopesByCategory() {
|
func (suite *ExchangeSourceSuite) TestExchangeScopesByCategory() {
|
||||||
var (
|
var (
|
||||||
es = NewExchangeRestore("rpid")
|
es = NewExchangeRestore()
|
||||||
users = es.Users(All)
|
users = es.Users(All)
|
||||||
contacts = es.ContactFolders(All, All)
|
contacts = es.ContactFolders(All, All)
|
||||||
events = es.Events(All, All)
|
events = es.Events(All, All)
|
||||||
@ -798,7 +802,7 @@ func (suite *ExchangeSourceSuite) TestMatchExchangeEntry() {
|
|||||||
return extendExchangeScopeValues(None, exchangeScope(s))
|
return extendExchangeScopeValues(None, exchangeScope(s))
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
es = NewExchangeRestore("rpid")
|
es = NewExchangeRestore()
|
||||||
inAll = include(es.Users(All))
|
inAll = include(es.Users(All))
|
||||||
inNone = include(es.Users(None))
|
inNone = include(es.Users(None))
|
||||||
inMail = include(es.Mails(All, All, mail))
|
inMail = include(es.Mails(All, All, mail))
|
||||||
|
|||||||
@ -44,16 +44,14 @@ const (
|
|||||||
// The core selector. Has no api for setting or retrieving data.
|
// The core selector. Has no api for setting or retrieving data.
|
||||||
// Is only used to pass along more specific selector instances.
|
// Is only used to pass along more specific selector instances.
|
||||||
type Selector struct {
|
type Selector struct {
|
||||||
BackupID string `json:"backupID,omitempty"` // A backup id, used only by restore operations.
|
|
||||||
Service service `json:"service,omitempty"` // The service scope of the data. Exchange, Teams, Sharepoint, etc.
|
Service service `json:"service,omitempty"` // The service scope of the data. Exchange, Teams, Sharepoint, etc.
|
||||||
Excludes []map[string]string `json:"exclusions,omitempty"` // A slice of exclusions. Each exclusion applies to all inclusions.
|
Excludes []map[string]string `json:"exclusions,omitempty"` // A slice of exclusions. Each exclusion applies to all inclusions.
|
||||||
Includes []map[string]string `json:"scopes,omitempty"` // A slice of inclusions. Expected to get cast to a service wrapper within each service handler.
|
Includes []map[string]string `json:"scopes,omitempty"` // A slice of inclusions. Expected to get cast to a service wrapper within each service handler.
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper for specific selector instance constructors.
|
// helper for specific selector instance constructors.
|
||||||
func newSelector(s service, backupID string) Selector {
|
func newSelector(s service) Selector {
|
||||||
return Selector{
|
return Selector{
|
||||||
BackupID: backupID,
|
|
||||||
Service: s,
|
Service: s,
|
||||||
Excludes: []map[string]string{},
|
Excludes: []map[string]string{},
|
||||||
Includes: []map[string]string{},
|
Includes: []map[string]string{},
|
||||||
|
|||||||
@ -17,10 +17,9 @@ func TestSelectorSuite(t *testing.T) {
|
|||||||
|
|
||||||
func (suite *SelectorSuite) TestNewSelector() {
|
func (suite *SelectorSuite) TestNewSelector() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
s := newSelector(ServiceUnknown, "backupID")
|
s := newSelector(ServiceUnknown)
|
||||||
assert.NotNil(t, s)
|
assert.NotNil(t, s)
|
||||||
assert.Equal(t, s.Service, ServiceUnknown)
|
assert.Equal(t, s.Service, ServiceUnknown)
|
||||||
assert.Equal(t, s.BackupID, "backupID")
|
|
||||||
assert.NotNil(t, s.Includes)
|
assert.NotNil(t, s.Includes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user