applies folder matching behavior to cli input (#1443)

## Description

Allows users to specify whether they want to
select folders by prefix or search behavior.
Search/contains behavior is the default case,
with prefix being an optional deviation if the
folder input is prepended with a '/' character.

Also, propagates the PrefixMatch setting to
all integration tests that rely on selecting only
the default folder in exchange.

## Type of change

- [x] 🌻 Feature

## Issue(s)

* #1224

## Test Plan

- [x] 💪 Manual
- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Keepers 2022-11-08 16:10:40 -07:00 committed by GitHub
parent b20e7af533
commit 20795d3b56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 290 additions and 28 deletions

View File

@ -54,7 +54,7 @@ type ExchangeOpts struct {
func AddExchangeInclude( func AddExchangeInclude(
sel *selectors.ExchangeRestore, sel *selectors.ExchangeRestore,
resource, folders, items []string, resource, folders, items []string,
incl func([]string, []string, []string) []selectors.ExchangeScope, eisc selectors.ExchangeItemScopeConstructor,
) { ) {
lf, li := len(folders), len(items) lf, li := len(folders), len(items)
@ -68,15 +68,19 @@ func AddExchangeInclude(
resource = selectors.Any() resource = selectors.Any()
} }
if lf == 0 {
folders = selectors.Any()
}
if li == 0 { if li == 0 {
items = selectors.Any() items = selectors.Any()
} }
sel.Include(incl(resource, folders, items)) containsFolders, prefixFolders := splitFoldersIntoContainsAndPrefix(folders)
if len(containsFolders) > 0 {
sel.Include(eisc(resource, containsFolders, items))
}
if len(prefixFolders) > 0 {
sel.Include(eisc(resource, prefixFolders, items, selectors.PrefixMatch()))
}
} }
// AddExchangeFilter adds the scope of the provided values to the selector's // AddExchangeFilter adds the scope of the provided values to the selector's

View File

@ -19,7 +19,7 @@ func TestExchangeUtilsSuite(t *testing.T) {
suite.Run(t, new(ExchangeUtilsSuite)) suite.Run(t, new(ExchangeUtilsSuite))
} }
func (suite *ExchangeUtilsSuite) TestValidateBackupDetailFlags() { func (suite *ExchangeUtilsSuite) TestValidateRestoreFlags() {
table := []struct { table := []struct {
name string name string
backupID string backupID string
@ -56,7 +56,7 @@ func (suite *ExchangeUtilsSuite) TestValidateBackupDetailFlags() {
} }
} }
func (suite *ExchangeUtilsSuite) TestIncludeExchangeBackupDetailDataSelectors() { func (suite *ExchangeUtilsSuite) TestIncludeExchangeRestoreDataSelectors() {
stub := []string{"id-stub"} stub := []string{"id-stub"}
many := []string{"fnord", "smarf"} many := []string{"fnord", "smarf"}
a := []string{utils.Wildcard} a := []string{utils.Wildcard}
@ -308,7 +308,76 @@ func (suite *ExchangeUtilsSuite) TestIncludeExchangeBackupDetailDataSelectors()
} }
} }
func (suite *ExchangeUtilsSuite) TestFilterExchangeBackupDetailInfoSelectors() { func (suite *ExchangeUtilsSuite) TestAddExchangeInclude() {
var (
empty = []string{}
single = []string{"single"}
multi = []string{"more", "than", "one"}
containsOnly = []string{"contains"}
prefixOnly = []string{"/prefix"}
containsAndPrefix = []string{"contains", "/prefix"}
eisc = selectors.NewExchangeRestore().Contacts // type independent, just need the func
)
table := []struct {
name string
resources, folders, items []string
expectIncludeLen int
}{
{
name: "no inputs",
resources: empty,
folders: empty,
items: empty,
expectIncludeLen: 0,
},
{
name: "single inputs",
resources: single,
folders: single,
items: single,
expectIncludeLen: 1,
},
{
name: "multi inputs",
resources: multi,
folders: multi,
items: multi,
expectIncludeLen: 1,
},
{
name: "folder contains",
resources: empty,
folders: containsOnly,
items: empty,
expectIncludeLen: 1,
},
{
name: "folder prefixes",
resources: empty,
folders: prefixOnly,
items: empty,
expectIncludeLen: 1,
},
{
name: "folder prefixes and contains",
resources: empty,
folders: containsAndPrefix,
items: empty,
expectIncludeLen: 2,
},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
sel := selectors.NewExchangeRestore()
// no return, mutates sel as a side effect
utils.AddExchangeInclude(sel, test.resources, test.folders, test.items, eisc)
assert.Len(t, sel.Includes, test.expectIncludeLen)
})
}
}
func (suite *ExchangeUtilsSuite) TestFilterExchangeRestoreInfoSelectors() {
stub := "id-stub" stub := "id-stub"
table := []struct { table := []struct {

View File

@ -74,12 +74,18 @@ func IncludeOneDriveRestoreDataSelectors(
sel *selectors.OneDriveRestore, sel *selectors.OneDriveRestore,
opts OneDriveOpts, opts OneDriveOpts,
) { ) {
lp, ln := len(opts.Paths), len(opts.Names)
// only use the inclusion if either a path or item name
// is specified
if lp+ln == 0 {
return
}
if len(opts.Users) == 0 { if len(opts.Users) == 0 {
opts.Users = selectors.Any() opts.Users = selectors.Any()
} }
lp, ln := len(opts.Paths), len(opts.Names)
// either scope the request to a set of users // either scope the request to a set of users
if lp+ln == 0 { if lp+ln == 0 {
sel.Include(sel.Users(opts.Users)) sel.Include(sel.Users(opts.Users))
@ -89,15 +95,19 @@ func IncludeOneDriveRestoreDataSelectors(
opts.Paths = trimFolderSlash(opts.Paths) opts.Paths = trimFolderSlash(opts.Paths)
if lp == 0 {
opts.Paths = selectors.Any()
}
if ln == 0 { if ln == 0 {
opts.Names = selectors.Any() opts.Names = selectors.Any()
} }
sel.Include(sel.Items(opts.Users, opts.Paths, opts.Names)) containsFolders, prefixFolders := splitFoldersIntoContainsAndPrefix(opts.Paths)
if len(containsFolders) > 0 {
sel.Include(sel.Items(opts.Users, containsFolders, opts.Names))
}
if len(prefixFolders) > 0 {
sel.Include(sel.Items(opts.Users, prefixFolders, opts.Names, selectors.PrefixMatch()))
}
} }
// FilterOneDriveRestoreInfoSelectors builds the common info-selector filters. // FilterOneDriveRestoreInfoSelectors builds the common info-selector filters.

View File

@ -0,0 +1,90 @@
package utils_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/pkg/selectors"
)
func (suite *ExchangeUtilsSuite) TestIncludeOneDriveRestoreDataSelectors() {
var (
empty = []string{}
single = []string{"single"}
multi = []string{"more", "than", "one"}
containsOnly = []string{"contains"}
prefixOnly = []string{"/prefix"}
containsAndPrefix = []string{"contains", "/prefix"}
)
table := []struct {
name string
opts utils.OneDriveOpts
expectIncludeLen int
}{
{
name: "no inputs",
opts: utils.OneDriveOpts{
Users: empty,
Paths: empty,
Names: empty,
},
expectIncludeLen: 0,
},
{
name: "single inputs",
opts: utils.OneDriveOpts{
Users: single,
Paths: single,
Names: single,
},
expectIncludeLen: 1,
},
{
name: "multi inputs",
opts: utils.OneDriveOpts{
Users: multi,
Paths: multi,
Names: multi,
},
expectIncludeLen: 1,
},
{
name: "folder contains",
opts: utils.OneDriveOpts{
Users: empty,
Paths: containsOnly,
Names: empty,
},
expectIncludeLen: 1,
},
{
name: "folder prefixes",
opts: utils.OneDriveOpts{
Users: empty,
Paths: prefixOnly,
Names: empty,
},
expectIncludeLen: 1,
},
{
name: "folder prefixes and contains",
opts: utils.OneDriveOpts{
Users: empty,
Paths: containsAndPrefix,
Names: empty,
},
expectIncludeLen: 2,
},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
sel := selectors.NewOneDriveRestore()
// no return, mutates sel as a side effect
utils.IncludeOneDriveRestoreDataSelectors(sel, test.opts)
assert.Len(t, sel.Includes, test.expectIncludeLen)
})
}
}

View File

@ -8,7 +8,9 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/repository" "github.com/alcionai/corso/src/pkg/repository"
"github.com/alcionai/corso/src/pkg/selectors"
) )
// common flag names // common flag names
@ -64,3 +66,34 @@ func AddCommand(parent, c *cobra.Command) (*cobra.Command, *pflag.FlagSet) {
return c, c.Flags() return c, c.Flags()
} }
// separates the provided folders into two sets: folders that use a pathContains
// comparison (the default), and folders that use a pathPrefix comparison.
// Any element beginning with a path.PathSeparator (ie: '/') is moved to the prefix
// comparison set. If folders is nil, returns only containsFolders with the any matcher.
func splitFoldersIntoContainsAndPrefix(folders []string) ([]string, []string) {
var (
containsFolders = []string{}
prefixFolders = []string{}
)
if len(folders) == 0 {
return selectors.Any(), nil
}
// separate folder selection inputs by behavior.
// any input beginning with a '/' character acts as a prefix match.
for _, f := range folders {
if len(f) == 0 {
continue
}
if f[0] == path.PathSeparator {
prefixFolders = append(prefixFolders, f)
} else {
containsFolders = append(containsFolders, f)
}
}
return containsFolders, prefixFolders
}

View File

@ -1,4 +1,4 @@
package utils_test package utils
import ( import (
"testing" "testing"
@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/cli/utils" "github.com/alcionai/corso/src/pkg/selectors"
) )
type CliUtilsSuite struct { type CliUtilsSuite struct {
@ -33,6 +33,52 @@ func (suite *CliUtilsSuite) TestRequireProps() {
}, },
} }
for _, test := range table { for _, test := range table {
test.errCheck(suite.T(), utils.RequireProps(test.props)) test.errCheck(suite.T(), RequireProps(test.props))
}
}
func (suite *CliUtilsSuite) TestSplitFoldersIntoContainsAndPrefix() {
table := []struct {
name string
input []string
expectC []string
expectP []string
}{
{
name: "empty",
expectC: selectors.Any(),
expectP: nil,
},
{
name: "only contains",
input: []string{"a", "b", "c"},
expectC: []string{"a", "b", "c"},
expectP: []string{},
},
{
name: "only leading slash counts as contains",
input: []string{"a/////", "\\/b", "\\//c\\/"},
expectC: []string{"a/////", "\\/b", "\\//c\\/"},
expectP: []string{},
},
{
name: "only prefix",
input: []string{"/a", "/b", "/\\/c"},
expectC: []string{},
expectP: []string{"/a", "/b", "/\\/c"},
},
{
name: "mixed",
input: []string{"/a", "b", "/c"},
expectC: []string{"b"},
expectP: []string{"/a", "/c"},
},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
c, p := splitFoldersIntoContainsAndPrefix(test.input)
assert.ElementsMatch(t, test.expectC, c, "contains set")
assert.ElementsMatch(t, test.expectP, p, "prefix set")
})
} }
} }

View File

@ -277,7 +277,7 @@ func (suite *ServiceFunctionsIntegrationSuite) TestGetAllMailFolders() {
getScope: func(t *testing.T) selectors.ExchangeScope { getScope: func(t *testing.T) selectors.ExchangeScope {
return selectors. return selectors.
NewExchangeBackup(). NewExchangeBackup().
MailFolders([]string{userID}, []string{nonExistantLookup})[0] MailFolders([]string{userID}, []string{nonExistantLookup}, selectors.PrefixMatch())[0]
}, },
}, },
} }

View File

@ -166,6 +166,8 @@ func (s *exchange) DiscreteScopes(userPNs []string) []ExchangeScope {
return discreteScopes[ExchangeScope](s.Selector, ExchangeUser, userPNs) return discreteScopes[ExchangeScope](s.Selector, ExchangeUser, userPNs)
} }
type ExchangeItemScopeConstructor func([]string, []string, []string, ...option) []ExchangeScope
// ------------------- // -------------------
// Scope Factories // Scope Factories
@ -173,13 +175,14 @@ func (s *exchange) DiscreteScopes(userPNs []string) []ExchangeScope {
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // 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 contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
func (s *exchange) Contacts(users, folders, contacts []string) []ExchangeScope { // options are only applied to the folder scopes.
func (s *exchange) Contacts(users, folders, contacts []string, opts ...option) []ExchangeScope {
scopes := []ExchangeScope{} scopes := []ExchangeScope{}
scopes = append( scopes = append(
scopes, scopes,
makeScope[ExchangeScope](ExchangeContact, users, contacts). makeScope[ExchangeScope](ExchangeContact, users, contacts).
set(ExchangeContactFolder, folders), set(ExchangeContactFolder, folders, opts...),
) )
return scopes return scopes
@ -189,6 +192,7 @@ func (s *exchange) Contacts(users, folders, contacts []string) []ExchangeScope {
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // 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 contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
// options are only applied to the folder scopes.
func (s *exchange) ContactFolders(users, folders []string, opts ...option) []ExchangeScope { func (s *exchange) ContactFolders(users, folders []string, opts ...option) []ExchangeScope {
var ( var (
scopes = []ExchangeScope{} scopes = []ExchangeScope{}
@ -207,13 +211,14 @@ func (s *exchange) ContactFolders(users, folders []string, opts ...option) []Exc
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // 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 contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
func (s *exchange) Events(users, calendars, events []string) []ExchangeScope { // options are only applied to the folder scopes.
func (s *exchange) Events(users, calendars, events []string, opts ...option) []ExchangeScope {
scopes := []ExchangeScope{} scopes := []ExchangeScope{}
scopes = append( scopes = append(
scopes, scopes,
makeScope[ExchangeScope](ExchangeEvent, users, events). makeScope[ExchangeScope](ExchangeEvent, users, events).
set(ExchangeEventCalendar, calendars), set(ExchangeEventCalendar, calendars, opts...),
) )
return scopes return scopes
@ -224,6 +229,7 @@ func (s *exchange) Events(users, calendars, events []string) []ExchangeScope {
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // 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 contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
// options are only applied to the folder scopes.
func (s *exchange) EventCalendars(users, events []string, opts ...option) []ExchangeScope { func (s *exchange) EventCalendars(users, events []string, opts ...option) []ExchangeScope {
var ( var (
scopes = []ExchangeScope{} scopes = []ExchangeScope{}
@ -242,13 +248,14 @@ func (s *exchange) EventCalendars(users, events []string, opts ...option) []Exch
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // 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 contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
func (s *exchange) Mails(users, folders, mails []string) []ExchangeScope { // options are only applied to the folder scopes.
func (s *exchange) Mails(users, folders, mails []string, opts ...option) []ExchangeScope {
scopes := []ExchangeScope{} scopes := []ExchangeScope{}
scopes = append( scopes = append(
scopes, scopes,
makeScope[ExchangeScope](ExchangeMail, users, mails). makeScope[ExchangeScope](ExchangeMail, users, mails).
set(ExchangeMailFolder, folders), set(ExchangeMailFolder, folders, opts...),
) )
return scopes return scopes
@ -258,6 +265,7 @@ func (s *exchange) Mails(users, folders, mails []string) []ExchangeScope {
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // 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 contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
// options are only applied to the folder scopes.
func (s *exchange) MailFolders(users, folders []string, opts ...option) []ExchangeScope { func (s *exchange) MailFolders(users, folders []string, opts ...option) []ExchangeScope {
var ( var (
scopes = []ExchangeScope{} scopes = []ExchangeScope{}

View File

@ -179,6 +179,7 @@ func (s *oneDrive) Users(users []string) []OneDriveScope {
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // 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 contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
// options are only applied to the folder scopes.
func (s *oneDrive) Folders(users, folders []string, opts ...option) []OneDriveScope { func (s *oneDrive) Folders(users, folders []string, opts ...option) []OneDriveScope {
var ( var (
scopes = []OneDriveScope{} scopes = []OneDriveScope{}
@ -197,13 +198,14 @@ func (s *oneDrive) Folders(users, folders []string, opts ...option) []OneDriveSc
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // 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 contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
func (s *oneDrive) Items(users, folders, items []string) []OneDriveScope { // options are only applied to the folder scopes.
func (s *oneDrive) Items(users, folders, items []string, opts ...option) []OneDriveScope {
scopes := []OneDriveScope{} scopes := []OneDriveScope{}
scopes = append( scopes = append(
scopes, scopes,
makeScope[OneDriveScope](OneDriveItem, users, items). makeScope[OneDriveScope](OneDriveItem, users, items).
set(OneDriveFolder, folders), set(OneDriveFolder, folders, opts...),
) )
return scopes return scopes