diff --git a/src/pkg/selectors/exchange.go b/src/pkg/selectors/exchange.go index 5313b8849..9b1dddefd 100644 --- a/src/pkg/selectors/exchange.go +++ b/src/pkg/selectors/exchange.go @@ -36,6 +36,8 @@ type ( } ) +var _ Reducer = &ExchangeRestore{} + // NewExchange produces a new Selector with the service set to ServiceExchange. func NewExchangeBackup() *ExchangeBackup { src := ExchangeBackup{ diff --git a/src/pkg/selectors/selectors.go b/src/pkg/selectors/selectors.go index 97022f3e1..bc88608c8 100644 --- a/src/pkg/selectors/selectors.go +++ b/src/pkg/selectors/selectors.go @@ -1,12 +1,14 @@ package selectors import ( + "context" "encoding/json" "fmt" "strings" "github.com/pkg/errors" + "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/filters" ) @@ -53,6 +55,10 @@ var ( // It is not used aside from printing resources. const All = "All" +type Reducer interface { + Reduce(context.Context, *details.Details) *details.Details +} + // --------------------------------------------------------------------------- // Selector // --------------------------------------------------------------------------- diff --git a/src/pkg/selectors/selectors_reduce_test.go b/src/pkg/selectors/selectors_reduce_test.go new file mode 100644 index 000000000..53e7fd854 --- /dev/null +++ b/src/pkg/selectors/selectors_reduce_test.go @@ -0,0 +1,179 @@ +package selectors_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/src/internal/common" + "github.com/alcionai/corso/src/pkg/backup/details" + "github.com/alcionai/corso/src/pkg/selectors" + "github.com/alcionai/corso/src/pkg/selectors/testdata" +) + +type SelectorReduceSuite struct { + suite.Suite +} + +func TestSelectorReduceSuite(t *testing.T) { + suite.Run(t, new(SelectorReduceSuite)) +} + +func (suite *SelectorReduceSuite) TestReduce() { + ctx := context.Background() + allDetails := testdata.GetDetailsSet() + table := []struct { + name string + selFunc func() selectors.Reducer + expected []details.DetailsEntry + }{ + { + name: "ExchangeAllMail", + selFunc: func() selectors.Reducer { + sel := selectors.NewExchangeRestore() + sel.Include(sel.Mails( + selectors.Any(), + selectors.Any(), + selectors.Any(), + )) + + return sel + }, + expected: testdata.ExchangeEmailItems, + }, + { + name: "ExchangeMailSubject", + selFunc: func() selectors.Reducer { + sel := selectors.NewExchangeRestore() + sel.Filter(sel.MailSubject("foo")) + + return sel + }, + expected: []details.DetailsEntry{testdata.ExchangeEmailItems[0]}, + }, + { + name: "ExchangeMailSubjectExcludeItem", + selFunc: func() selectors.Reducer { + sel := selectors.NewExchangeRestore() + sel.Filter(sel.MailSender("a-person")) + sel.Exclude(sel.Mails( + selectors.Any(), + selectors.Any(), + []string{testdata.ExchangeEmailItemPath2.ShortRef()}, + )) + + return sel + }, + expected: []details.DetailsEntry{testdata.ExchangeEmailItems[0]}, + }, + { + name: "ExchangeMailSender", + selFunc: func() selectors.Reducer { + sel := selectors.NewExchangeRestore() + sel.Filter(sel.MailSender("a-person")) + + return sel + }, + expected: testdata.ExchangeEmailItems, + }, + { + name: "ExchangeMailReceivedTime", + selFunc: func() selectors.Reducer { + sel := selectors.NewExchangeRestore() + sel.Filter(sel.MailReceivedBefore( + common.FormatTime(testdata.Time1.Add(time.Second)), + )) + + return sel + }, + expected: []details.DetailsEntry{testdata.ExchangeEmailItems[0]}, + }, + { + name: "ExchangeMailID", + selFunc: func() selectors.Reducer { + sel := selectors.NewExchangeRestore() + sel.Include(sel.Mails( + selectors.Any(), + selectors.Any(), + []string{testdata.ExchangeEmailItemPath1.Item()}, + )) + + return sel + }, + expected: []details.DetailsEntry{testdata.ExchangeEmailItems[0]}, + }, + { + name: "ExchangeMailShortRef", + selFunc: func() selectors.Reducer { + sel := selectors.NewExchangeRestore() + sel.Include(sel.Mails( + selectors.Any(), + selectors.Any(), + []string{testdata.ExchangeEmailItemPath1.ShortRef()}, + )) + + return sel + }, + expected: []details.DetailsEntry{testdata.ExchangeEmailItems[0]}, + }, + { + name: "ExchangeAllEventsAndMailWithSubject", + selFunc: func() selectors.Reducer { + sel := selectors.NewExchangeRestore() + sel.Include(sel.Events( + selectors.Any(), + selectors.Any(), + selectors.Any(), + )) + sel.Filter(sel.MailSubject("foo")) + + return sel + }, + expected: []details.DetailsEntry{testdata.ExchangeEmailItems[0]}, + }, + { + name: "ExchangeEventsAndMailWithSubject", + selFunc: func() selectors.Reducer { + sel := selectors.NewExchangeRestore() + sel.Filter(sel.EventSubject("foo")) + sel.Filter(sel.MailSubject("foo")) + + return sel + }, + expected: []details.DetailsEntry{}, + }, + { + name: "ExchangeAll", + selFunc: func() selectors.Reducer { + sel := selectors.NewExchangeRestore() + sel.Include(sel.Users( + selectors.Any(), + )) + + return sel + }, + expected: append( + append( + append( + []details.DetailsEntry{}, + testdata.ExchangeEmailItems...), + testdata.ExchangeContactsItems...), + testdata.ExchangeEventsItems..., + ), + }, + } + + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + test := test + + t.Parallel() + + output := test.selFunc().Reduce(ctx, allDetails) + assert.ElementsMatch(t, test.expected, output.Entries) + }) + } +} diff --git a/src/pkg/selectors/testdata/details.go b/src/pkg/selectors/testdata/details.go new file mode 100644 index 000000000..e61114b96 --- /dev/null +++ b/src/pkg/selectors/testdata/details.go @@ -0,0 +1,216 @@ +package testdata + +import ( + stdpath "path" + "time" + + "github.com/alcionai/corso/src/internal/path" + "github.com/alcionai/corso/src/pkg/backup/details" +) + +// mustParsePath takes a string representing a resource path and returns a path +// instance. Panics if the path cannot be parsed. Useful for simple variable +// assignments. +func mustParsePath(ref string, isItem bool) path.Path { + p, err := path.FromDataLayerPath(ref, isItem) + if err != nil { + panic(err) + } + + return p +} + +// mustAppendPath takes a Path, string representing a path element, and whether +// the element is an item and returns a path instance representing the original +// path with the element appended to it. Panics if the path cannot be parsed. +// Useful for simple variable assignments. +func mustAppendPath(p path.Path, newElement string, isItem bool) path.Path { + newP, err := p.Append(newElement, isItem) + if err != nil { + panic(err) + } + + return newP +} + +const ( + ItemName1 = "item1" + ItemName2 = "item2" +) + +var ( + Time1 = time.Date(2022, 9, 21, 10, 0, 0, 0, time.UTC) + Time2 = time.Date(2022, 10, 21, 10, 0, 0, 0, time.UTC) + + ExchangeEmailBasePath = mustParsePath("tenant-id/exchange/user-id/email/Inbox/subfolder", false) + ExchangeEmailItemPath1 = mustAppendPath(ExchangeEmailBasePath, ItemName1, true) + ExchangeEmailItemPath2 = mustAppendPath(ExchangeEmailBasePath, ItemName2, true) + + ExchangeEmailItems = []details.DetailsEntry{ + { + RepoRef: ExchangeEmailItemPath1.String(), + ShortRef: ExchangeEmailItemPath1.ShortRef(), + ParentRef: ExchangeEmailItemPath1.ToBuilder().Dir().ShortRef(), + ItemInfo: details.ItemInfo{ + Exchange: &details.ExchangeInfo{ + ItemType: details.ExchangeMail, + Sender: "a-person", + Subject: "foo", + Received: Time1, + }, + }, + }, + { + RepoRef: ExchangeEmailItemPath2.String(), + ShortRef: ExchangeEmailItemPath2.ShortRef(), + ParentRef: ExchangeEmailItemPath2.ToBuilder().Dir().ShortRef(), + ItemInfo: details.ItemInfo{ + Exchange: &details.ExchangeInfo{ + ItemType: details.ExchangeMail, + Sender: "a-person", + Subject: "bar", + Received: Time2, + }, + }, + }, + } + + ExchangeContactsBasePath = mustParsePath("tenant-id/exchange/user-id/contacts/contacts", false) + ExchangeContactsItemPath1 = mustAppendPath(ExchangeContactsBasePath, ItemName1, true) + ExchangeContactsItemPath2 = mustAppendPath(ExchangeContactsBasePath, ItemName2, true) + + ExchangeContactsItems = []details.DetailsEntry{ + { + RepoRef: ExchangeContactsItemPath1.String(), + ShortRef: ExchangeContactsItemPath1.ShortRef(), + ParentRef: ExchangeContactsItemPath1.ToBuilder().Dir().ShortRef(), + ItemInfo: details.ItemInfo{ + Exchange: &details.ExchangeInfo{ + ItemType: details.ExchangeContact, + ContactName: "a-person", + }, + }, + }, + { + RepoRef: ExchangeContactsItemPath2.String(), + ShortRef: ExchangeContactsItemPath2.ShortRef(), + ParentRef: ExchangeContactsItemPath2.ToBuilder().Dir().ShortRef(), + ItemInfo: details.ItemInfo{ + Exchange: &details.ExchangeInfo{ + ItemType: details.ExchangeContact, + ContactName: "another-person", + }, + }, + }, + } + + ExchangeEventsBasePath = mustParsePath("tenant-id/exchange/user-id/events/holidays", false) + ExchangeEventsItemPath1 = mustAppendPath(ExchangeEventsBasePath, ItemName1, true) + ExchangeEventsItemPath2 = mustAppendPath(ExchangeEventsBasePath, ItemName2, true) + + ExchangeEventsItems = []details.DetailsEntry{ + { + RepoRef: ExchangeEventsItemPath1.String(), + ShortRef: ExchangeEventsItemPath1.ShortRef(), + ParentRef: ExchangeEventsItemPath1.ToBuilder().Dir().ShortRef(), + ItemInfo: details.ItemInfo{ + Exchange: &details.ExchangeInfo{ + ItemType: details.ExchangeEvent, + Organizer: "a-person", + Subject: "foo", + EventStart: Time1, + EventRecurs: false, + }, + }, + }, + { + RepoRef: ExchangeEventsItemPath2.String(), + ShortRef: ExchangeEventsItemPath2.ShortRef(), + ParentRef: ExchangeEventsItemPath2.ToBuilder().Dir().ShortRef(), + ItemInfo: details.ItemInfo{ + Exchange: &details.ExchangeInfo{ + ItemType: details.ExchangeEvent, + Organizer: "a-person", + Subject: "foo", + EventStart: Time2, + EventRecurs: true, + }, + }, + }, + } + + OneDriveBasePath = mustParsePath("tenant-id/onedrive/user-id/files/folder/subfolder", false) + OneDriveItemPath1 = mustAppendPath(OneDriveBasePath, ItemName1, true) + OneDriveItemPath2 = mustAppendPath(OneDriveBasePath, ItemName2, true) + + OneDriveItems = []details.DetailsEntry{ + { + RepoRef: OneDriveItemPath1.String(), + ShortRef: OneDriveItemPath1.ShortRef(), + ParentRef: OneDriveItemPath1.ToBuilder().Dir().ShortRef(), + ItemInfo: details.ItemInfo{ + OneDrive: &details.OneDriveInfo{ + ItemType: details.OneDriveItem, + ParentPath: stdpath.Join( + append( + []string{ + "drives", + "foo", + "root:", + }, + OneDriveItemPath1.Folders()..., + )..., + ), + ItemName: OneDriveItemPath1.Item() + "name", + }, + }, + }, + { + RepoRef: OneDriveItemPath2.String(), + ShortRef: OneDriveItemPath2.ShortRef(), + ParentRef: OneDriveItemPath2.ToBuilder().Dir().ShortRef(), + ItemInfo: details.ItemInfo{ + OneDrive: &details.OneDriveInfo{ + ItemType: details.OneDriveItem, + ParentPath: stdpath.Join( + append( + []string{ + "drives", + "foo", + "root:", + }, + OneDriveItemPath2.Folders()..., + )..., + ), + ItemName: OneDriveItemPath2.Item() + "name", + }, + }, + }, + } +) + +func GetDetailsSet() *details.Details { + entries := []details.DetailsEntry{} + + for _, e := range ExchangeEmailItems { + entries = append(entries, e) + } + + for _, e := range ExchangeContactsItems { + entries = append(entries, e) + } + + for _, e := range ExchangeEventsItems { + entries = append(entries, e) + } + + for _, e := range OneDriveItems { + entries = append(entries, e) + } + + return &details.Details{ + DetailsModel: details.DetailsModel{ + Entries: entries, + }, + } +}