diff --git a/src/cli/utils/sharepoint.go b/src/cli/utils/sharepoint.go index e75f750bc..291858b30 100644 --- a/src/cli/utils/sharepoint.go +++ b/src/cli/utils/sharepoint.go @@ -11,6 +11,8 @@ const ( LibraryFN = "library" ListItemFN = "list-item" ListFN = "list" + PageFN = "page" + PageItemFN = "page-item" WebURLFN = "web-url" ) @@ -19,6 +21,8 @@ type SharePointOpts struct { LibraryPaths []string ListItems []string ListPaths []string + PageFolders []string + Pages []string Sites []string WebURLs []string @@ -60,6 +64,7 @@ func IncludeSharePointRestoreDataSelectors(opts SharePointOpts) *selectors.Share lp, li := len(opts.LibraryPaths), len(opts.LibraryItems) ls, lwu := len(opts.Sites), len(opts.WebURLs) slp, sli := len(opts.ListPaths), len(opts.ListItems) + pf, pi := len(opts.PageFolders), len(opts.Pages) if ls == 0 { sites = selectors.Any() @@ -67,7 +72,7 @@ func IncludeSharePointRestoreDataSelectors(opts SharePointOpts) *selectors.Share sel := selectors.NewSharePointRestore(sites) - if lp+li+lwu+slp+sli == 0 { + if lp+li+lwu+slp+sli+pf+pi == 0 { sel.Include(sel.AllData()) return sel } @@ -106,6 +111,23 @@ func IncludeSharePointRestoreDataSelectors(opts SharePointOpts) *selectors.Share } } + if pf+pi > 0 { + if pi == 0 { + opts.Pages = selectors.Any() + } + + opts.PageFolders = trimFolderSlash(opts.PageFolders) + containsFolders, prefixFolders := splitFoldersIntoContainsAndPrefix(opts.PageFolders) + + if len(containsFolders) > 0 { + sel.Include(sel.PageItems(containsFolders, opts.Pages)) + } + + if len(prefixFolders) > 0 { + sel.Include(sel.PageItems(prefixFolders, opts.Pages, selectors.PrefixMatch())) + } + } + if lwu > 0 { opts.WebURLs = trimFolderSlash(opts.WebURLs) containsURLs, suffixURLs := splitFoldersIntoContainsAndPrefix(opts.WebURLs) diff --git a/src/cli/utils/sharepoint_test.go b/src/cli/utils/sharepoint_test.go index de2048d93..7d5d4974b 100644 --- a/src/cli/utils/sharepoint_test.go +++ b/src/cli/utils/sharepoint_test.go @@ -17,6 +17,8 @@ func TestSharePointUtilsSuite(t *testing.T) { suite.Run(t, new(SharePointUtilsSuite)) } +// Tests selector build for SharePoint properly +// differentiates between the 3 categories: Pages, Libraries and Lists CLI func (suite *SharePointUtilsSuite) TestIncludeSharePointRestoreDataSelectors() { var ( empty = []string{} @@ -33,14 +35,9 @@ func (suite *SharePointUtilsSuite) TestIncludeSharePointRestoreDataSelectors() { expectIncludeLen int }{ { - name: "no inputs", - opts: utils.SharePointOpts{ - LibraryItems: empty, - LibraryPaths: empty, - Sites: empty, - WebURLs: empty, - }, - expectIncludeLen: 2, + name: "no inputs", + opts: utils.SharePointOpts{}, + expectIncludeLen: 3, }, { name: "single inputs", @@ -50,7 +47,7 @@ func (suite *SharePointUtilsSuite) TestIncludeSharePointRestoreDataSelectors() { Sites: single, WebURLs: single, }, - expectIncludeLen: 3, + expectIncludeLen: 4, }, { name: "single extended", @@ -62,7 +59,7 @@ func (suite *SharePointUtilsSuite) TestIncludeSharePointRestoreDataSelectors() { Sites: single, WebURLs: single, }, - expectIncludeLen: 4, + expectIncludeLen: 5, }, { name: "multi inputs", @@ -72,7 +69,7 @@ func (suite *SharePointUtilsSuite) TestIncludeSharePointRestoreDataSelectors() { Sites: multi, WebURLs: multi, }, - expectIncludeLen: 3, + expectIncludeLen: 4, }, { name: "library contains", @@ -138,7 +135,7 @@ func (suite *SharePointUtilsSuite) TestIncludeSharePointRestoreDataSelectors() { Sites: empty, WebURLs: containsOnly, }, - expectIncludeLen: 2, + expectIncludeLen: 3, }, { name: "library suffixes", @@ -148,7 +145,7 @@ func (suite *SharePointUtilsSuite) TestIncludeSharePointRestoreDataSelectors() { Sites: empty, WebURLs: prefixOnly, // prefix pattern matches suffix pattern }, - expectIncludeLen: 2, + expectIncludeLen: 3, }, { name: "library suffixes and contains", @@ -158,7 +155,29 @@ func (suite *SharePointUtilsSuite) TestIncludeSharePointRestoreDataSelectors() { Sites: empty, WebURLs: containsAndPrefix, // prefix pattern matches suffix pattern }, - expectIncludeLen: 4, + expectIncludeLen: 6, + }, + { + name: "Page Folder", + opts: utils.SharePointOpts{ + PageFolders: single, + }, + expectIncludeLen: 1, + }, + { + name: "Site Page ", + opts: utils.SharePointOpts{ + Pages: single, + }, + expectIncludeLen: 1, + }, + { + name: "Page & Library", + opts: utils.SharePointOpts{ + PageFolders: single, + LibraryItems: multi, + }, + expectIncludeLen: 2, }, } for _, test := range table { diff --git a/src/pkg/selectors/selectors_test.go b/src/pkg/selectors/selectors_test.go index 08da905d5..a60863dd9 100644 --- a/src/pkg/selectors/selectors_test.go +++ b/src/pkg/selectors/selectors_test.go @@ -239,3 +239,130 @@ func (suite *SelectorSuite) TestSplitByResourceOnwer() { }) } } + +// TestPathCategories verifies that no scope produces a `path.UnknownCategory` +func (suite *SelectorSuite) TestPathCategories_includes() { + users := []string{"someuser@onmicrosoft.com"} + + table := []struct { + name string + getSelector func(t *testing.T) *Selector + isErr assert.ErrorAssertionFunc + }{ + { + name: "empty", + isErr: assert.Error, + getSelector: func(t *testing.T) *Selector { + return &Selector{} + }, + }, + { + name: "Mail_B", + isErr: assert.NoError, + getSelector: func(t *testing.T) *Selector { + sel := NewExchangeBackup(users) + sel.Include(sel.MailFolders([]string{"MailFolder"}, PrefixMatch())) + sel.Mails([]string{"MailFolder2"}, []string{"Mail"}) + return &sel.Selector + }, + }, + { + name: "Mail_R", + isErr: assert.NoError, + getSelector: func(t *testing.T) *Selector { + sel := NewExchangeRestore(users) + sel.Include(sel.MailFolders([]string{"MailFolder"}, PrefixMatch())) + + return &sel.Selector + }, + }, + { + name: "Contacts", + isErr: assert.NoError, + getSelector: func(t *testing.T) *Selector { + sel := NewExchangeBackup(users) + sel.Include(sel.ContactFolders([]string{"Contact Folder"}, PrefixMatch())) + return &sel.Selector + }, + }, + { + name: "Contacts_R", + isErr: assert.NoError, + getSelector: func(t *testing.T) *Selector { + sel := NewExchangeRestore(users) + sel.Include(sel.ContactFolders([]string{"Contact Folder"}, PrefixMatch())) + return &sel.Selector + }, + }, + { + name: "Events", + isErr: assert.NoError, + getSelector: func(t *testing.T) *Selector { + sel := NewExchangeBackup(users) + sel.Include(sel.EventCalendars([]string{"July"}, PrefixMatch())) + return &sel.Selector + }, + }, + { + name: "Events_R", + isErr: assert.NoError, + getSelector: func(t *testing.T) *Selector { + sel := NewExchangeRestore(users) + sel.Include(sel.EventCalendars([]string{"July"}, PrefixMatch())) + sel.EventCalendars([]string{"Independence Day EventID"}) + return &sel.Selector + }, + }, + { + name: "SharePoint Pages", + isErr: assert.NoError, + getSelector: func(t *testing.T) *Selector { + sel := NewSharePointBackup(users) + sel.Include(sel.Pages([]string{"Something"}, SuffixMatch())) + sel.PageItems([]string{"Home Directory"}, []string{"Event Page"}) + + return &sel.Selector + }, + }, + { + name: "SharePoint Lists", + isErr: assert.NoError, + getSelector: func(t *testing.T) *Selector { + sel := NewSharePointBackup(users) + sel.Include(sel.Lists([]string{"Lists from website"}, SuffixMatch())) + + return &sel.Selector + }, + }, + { + name: "SharePoint Libraries", + isErr: assert.NoError, + getSelector: func(t *testing.T) *Selector { + sel := NewSharePointBackup(users) + sel.Include(sel.Libraries([]string{"A directory"}, SuffixMatch())) + + return &sel.Selector + }, + }, + { + name: "OneDrive", + isErr: assert.NoError, + getSelector: func(t *testing.T) *Selector { + sel := NewOneDriveBackup(users) + sel.Include(sel.Folders([]string{"Single Folder"}, PrefixMatch())) + + return &sel.Selector + }, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + obj := test.getSelector(t) + cats, err := obj.PathCategories() + for _, entry := range cats.Includes { + assert.NotEqual(t, entry, path.UnknownCategory) + } + test.isErr(t, err) + }) + } +} diff --git a/src/pkg/selectors/sharepoint.go b/src/pkg/selectors/sharepoint.go index 0e6cb6260..3c47c847b 100644 --- a/src/pkg/selectors/sharepoint.go +++ b/src/pkg/selectors/sharepoint.go @@ -202,6 +202,11 @@ func (s *SharePointRestore) WebURL(urlSuffixes []string, opts ...option) []Share SharePointWebURL, urlSuffixes, pathFilterFactory(opts...)), + makeFilterScope[SharePointScope]( + SharePointPage, + SharePointWebURL, + urlSuffixes, + pathFilterFactory(opts...)), ) return scopes @@ -219,6 +224,7 @@ func (s *sharePoint) AllData() []SharePointScope { scopes, makeScope[SharePointScope](SharePointLibrary, Any()), makeScope[SharePointScope](SharePointList, Any()), + makeScope[SharePointScope](SharePointPage, Any()), ) return scopes @@ -291,6 +297,38 @@ func (s *sharePoint) LibraryItems(libraries, items []string, opts ...option) []S return scopes } +// Pages produces one or more SharePoint page scopes. +// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] +// If any slice contains selectors.None, that slice is reduced to [selectors.None] +// If any slice is empty, it defaults to [selectors.None] +func (s *sharePoint) Pages(pages []string, opts ...option) []SharePointScope { + var ( + scopes = []SharePointScope{} + os = append([]option{pathComparator()}, opts...) + ) + + scopes = append(scopes, makeScope[SharePointScope](SharePointPageFolder, pages, os...)) + + return scopes +} + +// PageItems produces one or more SharePoint page item scopes. +// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] +// If any slice contains selectors.None, that slice is reduced to [selectors.None] +// If any slice is empty, it defaults to [selectors.None] +// options are only applied to the page scopes. +func (s *sharePoint) PageItems(pages, items []string, opts ...option) []SharePointScope { + scopes := []SharePointScope{} + + scopes = append( + scopes, + makeScope[SharePointScope](SharePointPage, items). + set(SharePointPage, pages, opts...), + ) + + return scopes +} + // ------------------- // Filter Factories @@ -315,6 +353,8 @@ const ( SharePointListItem sharePointCategory = "SharePointListItem" SharePointLibrary sharePointCategory = "SharePointLibrary" SharePointLibraryItem sharePointCategory = "SharePointLibraryItem" + SharePointPageFolder sharePointCategory = "SharePointPageFolder" + SharePointPage sharePointCategory = "SharePointPage" // filterable topics identified by SharePoint ) @@ -325,14 +365,18 @@ var sharePointLeafProperties = map[categorizer]leafProperty{ pathKeys: []categorizer{SharePointLibrary, SharePointLibraryItem}, pathType: path.LibrariesCategory, }, + SharePointListItem: { + pathKeys: []categorizer{SharePointList, SharePointListItem}, + pathType: path.ListsCategory, + }, + SharePointPage: { + pathKeys: []categorizer{SharePointPageFolder, SharePointPage}, + pathType: path.PagesCategory, + }, SharePointSite: { // the root category must be represented, even though it isn't a leaf pathKeys: []categorizer{SharePointSite}, pathType: path.UnknownCategory, }, - SharePointListItem: { - pathKeys: []categorizer{SharePointSite, SharePointList, SharePointListItem}, - pathType: path.ListsCategory, - }, } func (c sharePointCategory) String() string { @@ -350,6 +394,8 @@ func (c sharePointCategory) leafCat() categorizer { return SharePointLibraryItem case SharePointList, SharePointListItem: return SharePointListItem + case SharePointPage, SharePointPageFolder: + return SharePointPage } return c @@ -389,6 +435,10 @@ func (c sharePointCategory) pathValues(p path.Path) map[categorizer]string { folderCat, itemCat = SharePointLibrary, SharePointLibraryItem case SharePointList, SharePointListItem: folderCat, itemCat = SharePointList, SharePointListItem + case SharePointPage, SharePointPageFolder: + folderCat, itemCat = SharePointPageFolder, SharePointPage + default: + return map[categorizer]string{} } return map[categorizer]string{ @@ -429,6 +479,12 @@ func (s SharePointScope) categorizer() categorizer { return s.Category() } +// Matches returns true if the category is included in the scope's +// data type, and the target string matches that category's comparator. +func (s SharePointScope) Matches(cat sharePointCategory, target string) bool { + return matches(s, cat, target) +} + // FilterCategory returns the category enum of the scope filter. // If the scope is not a filter type, returns SharePointUnknownCategory. func (s SharePointScope) FilterCategory() sharePointCategory { @@ -443,12 +499,6 @@ func (s SharePointScope) IncludesCategory(cat sharePointCategory) bool { return categoryMatches(s.Category(), cat) } -// Matches returns true if the category is included in the scope's -// data type, and the target string matches that category's comparator. -func (s SharePointScope) Matches(cat sharePointCategory, target string) bool { - return matches(s, cat, target) -} - // returns true if the category is included in the scope's data type, // and the value is set to Any(). func (s SharePointScope) IsAny(cat sharePointCategory) bool { @@ -467,7 +517,7 @@ func (s SharePointScope) set(cat sharePointCategory, v []string, opts ...option) os := []option{} switch cat { - case SharePointLibrary, SharePointList: + case SharePointLibrary, SharePointList, SharePointPage: os = append(os, pathComparator()) } @@ -482,10 +532,14 @@ func (s SharePointScope) setDefaults() { s[SharePointLibraryItem.String()] = passAny s[SharePointList.String()] = passAny s[SharePointListItem.String()] = passAny + s[SharePointPageFolder.String()] = passAny + s[SharePointPage.String()] = passAny case SharePointLibrary: s[SharePointLibraryItem.String()] = passAny case SharePointList: s[SharePointListItem.String()] = passAny + case SharePointPageFolder: + s[SharePointPage.String()] = passAny } } @@ -509,6 +563,7 @@ func (s sharePoint) Reduce(ctx context.Context, deets *details.Details) *details map[path.CategoryType]sharePointCategory{ path.LibrariesCategory: SharePointLibraryItem, path.ListsCategory: SharePointListItem, + path.PagesCategory: SharePointPage, }, ) } diff --git a/src/pkg/selectors/sharepoint_test.go b/src/pkg/selectors/sharepoint_test.go index fc0b3d5c1..b9334a492 100644 --- a/src/pkg/selectors/sharepoint_test.go +++ b/src/pkg/selectors/sharepoint_test.go @@ -61,7 +61,7 @@ func (suite *SharePointSelectorSuite) TestSharePointSelector_AllData() { {"Filter Scopes", sel.Filters}, } for _, test := range table { - require.Len(t, test.scopesToCheck, 2) + require.Len(t, test.scopesToCheck, 3) for _, scope := range test.scopesToCheck { var ( @@ -106,7 +106,7 @@ func (suite *SharePointSelectorSuite) TestSharePointSelector_Include_WebURLs() { sel := NewSharePointRestore([]string{s1, s2}) sel.Include(sel.WebURL([]string{s1, s2})) scopes := sel.Includes - require.Len(t, scopes, 2) + require.Len(t, scopes, 3) for _, sc := range scopes { scopeMustHave( @@ -139,7 +139,7 @@ func (suite *SharePointSelectorSuite) TestSharePointSelector_Include_WebURLs_any sel := NewSharePointRestore(Any()) sel.Include(sel.WebURL(test.in)) scopes := sel.Includes - require.Len(t, scopes, 2) + require.Len(t, scopes, 3) for _, sc := range scopes { scopeMustHave( @@ -163,7 +163,7 @@ func (suite *SharePointSelectorSuite) TestSharePointSelector_Exclude_WebURLs() { sel := NewSharePointRestore([]string{s1, s2}) sel.Exclude(sel.WebURL([]string{s1, s2})) scopes := sel.Excludes - require.Len(t, scopes, 2) + require.Len(t, scopes, 3) for _, sc := range scopes { scopeMustHave(