prevent cross-contamination on filter reduce (#898)
## Description Over-restrictive scope correlation caused the reduce processor to only include filters which matched the data type of the scope. Ironically, this allowed a superset of information to match, by evading the _all-match_ expectations of filter scopes. Also replaces the data category consts inside /details with the path category types, since those are acting as better canonical owners of data type identification throughout the app. ## Type of change - [x] 🐛 Bugfix ## Issue(s) * #890 ## Test Plan - [x] 💪 Manual - [x] ⚡ Unit test
This commit is contained in:
parent
b2d3330db7
commit
41bb3ee6f9
@ -39,7 +39,7 @@ func (dm DetailsModel) PrintEntries(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printTable(ctx context.Context, dm DetailsModel) {
|
func printTable(ctx context.Context, dm DetailsModel) {
|
||||||
perType := map[itemType][]print.Printable{}
|
perType := map[ItemType][]print.Printable{}
|
||||||
|
|
||||||
for _, de := range dm.Entries {
|
for _, de := range dm.Entries {
|
||||||
it := de.infoType()
|
it := de.infoType()
|
||||||
@ -226,21 +226,21 @@ func (de DetailsEntry) Values() []string {
|
|||||||
return vs
|
return vs
|
||||||
}
|
}
|
||||||
|
|
||||||
type itemType int
|
type ItemType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UnknownType itemType = iota
|
UnknownType ItemType = iota
|
||||||
|
|
||||||
// separate each service by a factor of 100 for padding
|
// separate each service by a factor of 100 for padding
|
||||||
ExchangeContact
|
ExchangeContact
|
||||||
ExchangeEvent
|
ExchangeEvent
|
||||||
ExchangeMail
|
ExchangeMail
|
||||||
|
|
||||||
SharepointItem itemType = iota + 100
|
SharepointItem ItemType = iota + 100
|
||||||
|
|
||||||
OneDriveItem itemType = iota + 200
|
OneDriveItem ItemType = iota + 200
|
||||||
|
|
||||||
FolderItem itemType = iota + 300
|
FolderItem ItemType = iota + 300
|
||||||
)
|
)
|
||||||
|
|
||||||
// ItemInfo is a oneOf that contains service specific
|
// ItemInfo is a oneOf that contains service specific
|
||||||
@ -258,7 +258,7 @@ type ItemInfo struct {
|
|||||||
// infoType provides internal categorization for collecting like-typed ItemInfos.
|
// infoType provides internal categorization for collecting like-typed ItemInfos.
|
||||||
// It should return the most granular value type (ex: "event" for an exchange
|
// It should return the most granular value type (ex: "event" for an exchange
|
||||||
// calendar event).
|
// calendar event).
|
||||||
func (i ItemInfo) infoType() itemType {
|
func (i ItemInfo) infoType() ItemType {
|
||||||
switch {
|
switch {
|
||||||
case i.Folder != nil:
|
case i.Folder != nil:
|
||||||
return i.Folder.ItemType
|
return i.Folder.ItemType
|
||||||
@ -277,8 +277,8 @@ func (i ItemInfo) infoType() itemType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FolderInfo struct {
|
type FolderInfo struct {
|
||||||
ItemType itemType
|
ItemType ItemType `json:"itemType,omitempty"`
|
||||||
DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i FolderInfo) Headers() []string {
|
func (i FolderInfo) Headers() []string {
|
||||||
@ -291,7 +291,7 @@ func (i FolderInfo) Values() []string {
|
|||||||
|
|
||||||
// ExchangeInfo describes an exchange item
|
// ExchangeInfo describes an exchange item
|
||||||
type ExchangeInfo struct {
|
type ExchangeInfo struct {
|
||||||
ItemType itemType
|
ItemType ItemType `json:"itemType,omitempty"`
|
||||||
Sender string `json:"sender,omitempty"`
|
Sender string `json:"sender,omitempty"`
|
||||||
Subject string `json:"subject,omitempty"`
|
Subject string `json:"subject,omitempty"`
|
||||||
Received time.Time `json:"received,omitempty"`
|
Received time.Time `json:"received,omitempty"`
|
||||||
@ -344,7 +344,7 @@ func (i ExchangeInfo) Values() []string {
|
|||||||
// TODO: Implement this. This is currently here
|
// TODO: Implement this. This is currently here
|
||||||
// just to illustrate usage
|
// just to illustrate usage
|
||||||
type SharepointInfo struct {
|
type SharepointInfo struct {
|
||||||
ItemType itemType
|
ItemType ItemType `json:"itemType,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Headers returns the human-readable names of properties in a SharepointInfo
|
// Headers returns the human-readable names of properties in a SharepointInfo
|
||||||
@ -361,9 +361,9 @@ func (i SharepointInfo) Values() []string {
|
|||||||
|
|
||||||
// OneDriveInfo describes a oneDrive item
|
// OneDriveInfo describes a oneDrive item
|
||||||
type OneDriveInfo struct {
|
type OneDriveInfo struct {
|
||||||
ItemType itemType
|
ItemType ItemType `json:"itemType,omitempty"`
|
||||||
ParentPath string `json:"parentPath"`
|
ParentPath string `json:"parentPath"`
|
||||||
ItemName string `json:"itemName"`
|
ItemName string `json:"itemName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Headers returns the human-readable names of properties in a OneDriveInfo
|
// Headers returns the human-readable names of properties in a OneDriveInfo
|
||||||
|
|||||||
@ -333,8 +333,8 @@ func (sr *ExchangeRestore) EventRecurs(recurs string) []ExchangeScope {
|
|||||||
func (sr *ExchangeRestore) EventStartsAfter(timeStrings string) []ExchangeScope {
|
func (sr *ExchangeRestore) EventStartsAfter(timeStrings string) []ExchangeScope {
|
||||||
return []ExchangeScope{
|
return []ExchangeScope{
|
||||||
makeFilterScope[ExchangeScope](
|
makeFilterScope[ExchangeScope](
|
||||||
ExchangeMail,
|
ExchangeEvent,
|
||||||
ExchangeFilterMailReceivedAfter,
|
ExchangeFilterEventStartsAfter,
|
||||||
[]string{timeStrings},
|
[]string{timeStrings},
|
||||||
wrapFilter(filters.Less)),
|
wrapFilter(filters.Less)),
|
||||||
}
|
}
|
||||||
@ -347,8 +347,8 @@ func (sr *ExchangeRestore) EventStartsAfter(timeStrings string) []ExchangeScope
|
|||||||
func (sr *ExchangeRestore) EventStartsBefore(timeStrings string) []ExchangeScope {
|
func (sr *ExchangeRestore) EventStartsBefore(timeStrings string) []ExchangeScope {
|
||||||
return []ExchangeScope{
|
return []ExchangeScope{
|
||||||
makeFilterScope[ExchangeScope](
|
makeFilterScope[ExchangeScope](
|
||||||
ExchangeMail,
|
ExchangeEvent,
|
||||||
ExchangeFilterMailReceivedBefore,
|
ExchangeFilterEventStartsBefore,
|
||||||
[]string{timeStrings},
|
[]string{timeStrings},
|
||||||
wrapFilter(filters.Greater)),
|
wrapFilter(filters.Greater)),
|
||||||
}
|
}
|
||||||
@ -690,25 +690,21 @@ func (s exchange) Reduce(ctx context.Context, deets *details.Details) *details.D
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// matchesEntry returns true if either the path or the info in the exchangeEntry matches the scope details.
|
|
||||||
func (s ExchangeScope) matchesEntry(
|
|
||||||
cat categorizer,
|
|
||||||
pathValues map[categorizer]string,
|
|
||||||
entry details.DetailsEntry,
|
|
||||||
) bool {
|
|
||||||
// matchesPathValues can be handled generically, thanks to SCIENCE.
|
|
||||||
return matchesPathValues(s, cat.(exchangeCategory), pathValues, entry.ShortRef) || s.matchesInfo(entry.Exchange)
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchesInfo handles the standard behavior when comparing a scope and an ExchangeFilter
|
// matchesInfo handles the standard behavior when comparing a scope and an ExchangeFilter
|
||||||
// returns true if the scope and info match for the provided category.
|
// returns true if the scope and info match for the provided category.
|
||||||
func (s ExchangeScope) matchesInfo(info *details.ExchangeInfo) bool {
|
func (s ExchangeScope) matchesInfo(dii details.ItemInfo) bool {
|
||||||
// we need values to match against
|
info := dii.Exchange
|
||||||
if info == nil {
|
if info == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
filterCat := s.FilterCategory()
|
filterCat := s.FilterCategory()
|
||||||
|
|
||||||
|
cfpc := categoryFromItemType(info.ItemType)
|
||||||
|
if !typeAndCategoryMatches(filterCat, cfpc) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
i := ""
|
i := ""
|
||||||
|
|
||||||
switch filterCat {
|
switch filterCat {
|
||||||
@ -732,3 +728,19 @@ func (s ExchangeScope) matchesInfo(info *details.ExchangeInfo) bool {
|
|||||||
|
|
||||||
return s.Matches(filterCat, i)
|
return s.Matches(filterCat, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// categoryFromItemType interprets the category represented by the ExchangeInfo
|
||||||
|
// struct. Since every ExchangeInfo can hold all exchange data info, the exact
|
||||||
|
// type that the struct represents must be compared using its ItemType prop.
|
||||||
|
func categoryFromItemType(pct details.ItemType) exchangeCategory {
|
||||||
|
switch pct {
|
||||||
|
case details.ExchangeContact:
|
||||||
|
return ExchangeContact
|
||||||
|
case details.ExchangeMail:
|
||||||
|
return ExchangeMail
|
||||||
|
case details.ExchangeEvent:
|
||||||
|
return ExchangeEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExchangeCategoryUnknown
|
||||||
|
}
|
||||||
|
|||||||
@ -710,62 +710,99 @@ func (suite *ExchangeSelectorSuite) TestExchangeScope_MatchesInfo() {
|
|||||||
epoch = time.Time{}
|
epoch = time.Time{}
|
||||||
now = time.Now()
|
now = time.Now()
|
||||||
future = now.Add(1 * time.Minute)
|
future = now.Add(1 * time.Minute)
|
||||||
info = &details.ExchangeInfo{
|
|
||||||
ContactName: name,
|
|
||||||
EventRecurs: true,
|
|
||||||
EventStart: now,
|
|
||||||
Organizer: organizer,
|
|
||||||
Sender: sender,
|
|
||||||
Subject: subject,
|
|
||||||
Received: now,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
infoWith := func(itype details.ItemType) details.ItemInfo {
|
||||||
|
return details.ItemInfo{
|
||||||
|
Exchange: &details.ExchangeInfo{
|
||||||
|
ItemType: itype,
|
||||||
|
ContactName: name,
|
||||||
|
EventRecurs: true,
|
||||||
|
EventStart: now,
|
||||||
|
Organizer: organizer,
|
||||||
|
Sender: sender,
|
||||||
|
Subject: subject,
|
||||||
|
Received: now,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
|
itype details.ItemType
|
||||||
scope []ExchangeScope
|
scope []ExchangeScope
|
||||||
expect assert.BoolAssertionFunc
|
expect assert.BoolAssertionFunc
|
||||||
}{
|
}{
|
||||||
{"any mail with a sender", es.MailSender(AnyTgt), assert.True},
|
{"any mail with a sender", details.ExchangeMail, es.MailSender(AnyTgt), assert.True},
|
||||||
{"no mail, regardless of sender", es.MailSender(NoneTgt), assert.False},
|
{"no mail, regardless of sender", details.ExchangeMail, es.MailSender(NoneTgt), assert.False},
|
||||||
{"mail from a different sender", es.MailSender("magoo@ma.goo"), assert.False},
|
{"mail from a different sender", details.ExchangeMail, es.MailSender("magoo@ma.goo"), assert.False},
|
||||||
{"mail from the matching sender", es.MailSender(sender), assert.True},
|
{"mail from the matching sender", details.ExchangeMail, es.MailSender(sender), assert.True},
|
||||||
{"mail with any subject", es.MailSubject(AnyTgt), assert.True},
|
{"mail with any subject", details.ExchangeMail, es.MailSubject(AnyTgt), assert.True},
|
||||||
{"mail with none subject", es.MailSubject(NoneTgt), assert.False},
|
{"mail with none subject", details.ExchangeMail, es.MailSubject(NoneTgt), assert.False},
|
||||||
{"mail with a different subject", es.MailSubject("fancy"), assert.False},
|
{"mail with a different subject", details.ExchangeMail, es.MailSubject("fancy"), assert.False},
|
||||||
{"mail with the matching subject", es.MailSubject(subject), assert.True},
|
{"mail with the matching subject", details.ExchangeMail, es.MailSubject(subject), assert.True},
|
||||||
{"mail with a substring subject match", es.MailSubject(subject[5:9]), assert.True},
|
{"mail with a substring subject match", details.ExchangeMail, es.MailSubject(subject[5:9]), assert.True},
|
||||||
{"mail received after the epoch", es.MailReceivedAfter(common.FormatTime(epoch)), assert.True},
|
{"mail received after the epoch", details.ExchangeMail, es.MailReceivedAfter(common.FormatTime(epoch)), assert.True},
|
||||||
{"mail received after now", es.MailReceivedAfter(common.FormatTime(now)), assert.False},
|
{"mail received after now", details.ExchangeMail, es.MailReceivedAfter(common.FormatTime(now)), assert.False},
|
||||||
{"mail received after sometime later", es.MailReceivedAfter(common.FormatTime(future)), assert.False},
|
{
|
||||||
{"mail received before the epoch", es.MailReceivedBefore(common.FormatTime(epoch)), assert.False},
|
"mail received after sometime later",
|
||||||
{"mail received before now", es.MailReceivedBefore(common.FormatTime(now)), assert.False},
|
details.ExchangeMail,
|
||||||
{"mail received before sometime later", es.MailReceivedBefore(common.FormatTime(future)), assert.True},
|
es.MailReceivedAfter(common.FormatTime(future)),
|
||||||
{"event with any organizer", es.EventOrganizer(AnyTgt), assert.True},
|
assert.False,
|
||||||
{"event with none organizer", es.EventOrganizer(NoneTgt), assert.False},
|
},
|
||||||
{"event with a different organizer", es.EventOrganizer("fancy"), assert.False},
|
{
|
||||||
{"event with the matching organizer", es.EventOrganizer(organizer), assert.True},
|
"mail received before the epoch",
|
||||||
{"event that recurs", es.EventRecurs("true"), assert.True},
|
details.ExchangeMail,
|
||||||
{"event that does not recur", es.EventRecurs("false"), assert.False},
|
es.MailReceivedBefore(common.FormatTime(epoch)),
|
||||||
{"event starting after the epoch", es.EventStartsAfter(common.FormatTime(epoch)), assert.True},
|
assert.False,
|
||||||
{"event starting after now", es.EventStartsAfter(common.FormatTime(now)), assert.False},
|
},
|
||||||
{"event starting after sometime later", es.EventStartsAfter(common.FormatTime(future)), assert.False},
|
{"mail received before now", details.ExchangeMail, es.MailReceivedBefore(common.FormatTime(now)), assert.False},
|
||||||
{"event starting before the epoch", es.EventStartsBefore(common.FormatTime(epoch)), assert.False},
|
{
|
||||||
{"event starting before now", es.EventStartsBefore(common.FormatTime(now)), assert.False},
|
"mail received before sometime later",
|
||||||
{"event starting before sometime later", es.EventStartsBefore(common.FormatTime(future)), assert.True},
|
details.ExchangeMail,
|
||||||
{"event with any subject", es.EventSubject(AnyTgt), assert.True},
|
es.MailReceivedBefore(common.FormatTime(future)),
|
||||||
{"event with none subject", es.EventSubject(NoneTgt), assert.False},
|
assert.True,
|
||||||
{"event with a different subject", es.EventSubject("fancy"), assert.False},
|
},
|
||||||
{"event with the matching subject", es.EventSubject(subject), assert.True},
|
{"event with any organizer", details.ExchangeEvent, es.EventOrganizer(AnyTgt), assert.True},
|
||||||
{"contact with a different name", es.ContactName("blarps"), assert.False},
|
{"event with none organizer", details.ExchangeEvent, es.EventOrganizer(NoneTgt), assert.False},
|
||||||
{"contact with the same name", es.ContactName(name), assert.True},
|
{"event with a different organizer", details.ExchangeEvent, es.EventOrganizer("fancy"), assert.False},
|
||||||
{"contact with a subname search", es.ContactName(name[2:5]), assert.True},
|
{"event with the matching organizer", details.ExchangeEvent, es.EventOrganizer(organizer), assert.True},
|
||||||
|
{"event that recurs", details.ExchangeEvent, es.EventRecurs("true"), assert.True},
|
||||||
|
{"event that does not recur", details.ExchangeEvent, es.EventRecurs("false"), assert.False},
|
||||||
|
{"event starting after the epoch", details.ExchangeEvent, es.EventStartsAfter(common.FormatTime(epoch)), assert.True},
|
||||||
|
{"event starting after now", details.ExchangeEvent, es.EventStartsAfter(common.FormatTime(now)), assert.False},
|
||||||
|
{
|
||||||
|
"event starting after sometime later",
|
||||||
|
details.ExchangeEvent,
|
||||||
|
es.EventStartsAfter(common.FormatTime(future)),
|
||||||
|
assert.False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"event starting before the epoch",
|
||||||
|
details.ExchangeEvent,
|
||||||
|
es.EventStartsBefore(common.FormatTime(epoch)),
|
||||||
|
assert.False,
|
||||||
|
},
|
||||||
|
{"event starting before now", details.ExchangeEvent, es.EventStartsBefore(common.FormatTime(now)), assert.False},
|
||||||
|
{
|
||||||
|
"event starting before sometime later",
|
||||||
|
details.ExchangeEvent,
|
||||||
|
es.EventStartsBefore(common.FormatTime(future)),
|
||||||
|
assert.True,
|
||||||
|
},
|
||||||
|
{"event with any subject", details.ExchangeEvent, es.EventSubject(AnyTgt), assert.True},
|
||||||
|
{"event with none subject", details.ExchangeEvent, es.EventSubject(NoneTgt), assert.False},
|
||||||
|
{"event with a different subject", details.ExchangeEvent, es.EventSubject("fancy"), assert.False},
|
||||||
|
{"event with the matching subject", details.ExchangeEvent, es.EventSubject(subject), assert.True},
|
||||||
|
{"contact with a different name", details.ExchangeContact, es.ContactName("blarps"), assert.False},
|
||||||
|
{"contact with the same name", details.ExchangeContact, es.ContactName(name), assert.True},
|
||||||
|
{"contact with a subname search", details.ExchangeContact, es.ContactName(name[2:5]), 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) {
|
||||||
scopes := setScopesToDefault(test.scope)
|
scopes := setScopesToDefault(test.scope)
|
||||||
for _, scope := range scopes {
|
for _, scope := range scopes {
|
||||||
test.expect(t, scope.matchesInfo(info))
|
test.expect(t, scope.matchesInfo(infoWith(test.itype)))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -864,6 +901,12 @@ func (suite *ExchangeSelectorSuite) TestIdPath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ExchangeSelectorSuite) TestExchangeRestore_Reduce() {
|
func (suite *ExchangeSelectorSuite) TestExchangeRestore_Reduce() {
|
||||||
|
var (
|
||||||
|
contact = stubRepoRef(path.ExchangeService, path.ContactsCategory, "uid", "cfld", "cid")
|
||||||
|
event = stubRepoRef(path.ExchangeService, path.EventsCategory, "uid", "ecld", "eid")
|
||||||
|
mail = stubRepoRef(path.ExchangeService, path.EmailCategory, "uid", "mfld", "mid")
|
||||||
|
)
|
||||||
|
|
||||||
makeDeets := func(refs ...string) *details.Details {
|
makeDeets := func(refs ...string) *details.Details {
|
||||||
deets := &details.Details{
|
deets := &details.Details{
|
||||||
DetailsModel: details.DetailsModel{
|
DetailsModel: details.DetailsModel{
|
||||||
@ -872,20 +915,30 @@ func (suite *ExchangeSelectorSuite) TestExchangeRestore_Reduce() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range refs {
|
for _, r := range refs {
|
||||||
|
itype := details.UnknownType
|
||||||
|
|
||||||
|
switch r {
|
||||||
|
case contact:
|
||||||
|
itype = details.ExchangeContact
|
||||||
|
case event:
|
||||||
|
itype = details.ExchangeEvent
|
||||||
|
case mail:
|
||||||
|
itype = details.ExchangeMail
|
||||||
|
}
|
||||||
|
|
||||||
deets.Entries = append(deets.Entries, details.DetailsEntry{
|
deets.Entries = append(deets.Entries, details.DetailsEntry{
|
||||||
RepoRef: r,
|
RepoRef: r,
|
||||||
|
ItemInfo: details.ItemInfo{
|
||||||
|
Exchange: &details.ExchangeInfo{
|
||||||
|
ItemType: itype,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return deets
|
return deets
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
contact = stubRepoRef(path.ExchangeService, path.ContactsCategory, "uid", "cfld", "cid")
|
|
||||||
event = stubRepoRef(path.ExchangeService, path.EventsCategory, "uid", "ecld", "eid")
|
|
||||||
mail = stubRepoRef(path.ExchangeService, path.EmailCategory, "uid", "mfld", "mid")
|
|
||||||
)
|
|
||||||
|
|
||||||
arr := func(s ...string) []string {
|
arr := func(s ...string) []string {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
@ -1009,6 +1062,44 @@ func (suite *ExchangeSelectorSuite) TestExchangeRestore_Reduce() {
|
|||||||
},
|
},
|
||||||
arr(contact, event),
|
arr(contact, event),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"filter on mail subject",
|
||||||
|
func() *details.Details {
|
||||||
|
ds := makeDeets(mail)
|
||||||
|
for i := range ds.Entries {
|
||||||
|
ds.Entries[i].Exchange.Subject = "has a subject"
|
||||||
|
}
|
||||||
|
return ds
|
||||||
|
}(),
|
||||||
|
func() *ExchangeRestore {
|
||||||
|
er := NewExchangeRestore()
|
||||||
|
er.Include(er.Users(Any()))
|
||||||
|
er.Filter(er.MailSubject("subj"))
|
||||||
|
return er
|
||||||
|
},
|
||||||
|
arr(mail),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filter on mail subject multiple input categories",
|
||||||
|
func() *details.Details {
|
||||||
|
mds := makeDeets(mail)
|
||||||
|
for i := range mds.Entries {
|
||||||
|
mds.Entries[i].Exchange.Subject = "has a subject"
|
||||||
|
}
|
||||||
|
|
||||||
|
ds := makeDeets(contact, event)
|
||||||
|
ds.Entries = append(ds.Entries, mds.Entries...)
|
||||||
|
|
||||||
|
return ds
|
||||||
|
}(),
|
||||||
|
func() *ExchangeRestore {
|
||||||
|
er := NewExchangeRestore()
|
||||||
|
er.Include(er.Users(Any()))
|
||||||
|
er.Filter(er.MailSubject("subj"))
|
||||||
|
return er
|
||||||
|
},
|
||||||
|
arr(mail),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
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) {
|
||||||
@ -1067,7 +1158,7 @@ func (suite *ExchangeSelectorSuite) TestScopesByCategory() {
|
|||||||
}
|
}
|
||||||
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 := scopesByCategory[ExchangeScope](test.scopes, cats)
|
result := scopesByCategory[ExchangeScope](test.scopes, cats, false)
|
||||||
assert.Len(t, result[ExchangeContact], test.expect.contact)
|
assert.Len(t, result[ExchangeContact], test.expect.contact)
|
||||||
assert.Len(t, result[ExchangeEvent], test.expect.event)
|
assert.Len(t, result[ExchangeEvent], test.expect.event)
|
||||||
assert.Len(t, result[ExchangeMail], test.expect.mail)
|
assert.Len(t, result[ExchangeMail], test.expect.mail)
|
||||||
@ -1288,3 +1379,38 @@ func (suite *ExchangeSelectorSuite) TestExchangeCategory_PathKeys() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *ExchangeSelectorSuite) TestCategoryFromItemType() {
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
input details.ItemType
|
||||||
|
expect exchangeCategory
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "contact",
|
||||||
|
input: details.ExchangeContact,
|
||||||
|
expect: ExchangeContact,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "event",
|
||||||
|
input: details.ExchangeEvent,
|
||||||
|
expect: ExchangeEvent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mail",
|
||||||
|
input: details.ExchangeMail,
|
||||||
|
expect: ExchangeMail,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown",
|
||||||
|
input: details.UnknownType,
|
||||||
|
expect: ExchangeCategoryUnknown,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.T().Run(test.name, func(t *testing.T) {
|
||||||
|
result := categoryFromItemType(test.input)
|
||||||
|
assert.Equal(t, test.expect, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -52,7 +52,10 @@ func (mc mockCategorizer) isLeaf() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (mc mockCategorizer) pathValues(pth path.Path) map[categorizer]string {
|
func (mc mockCategorizer) pathValues(pth path.Path) map[categorizer]string {
|
||||||
return map[categorizer]string{rootCatStub: "stub"}
|
return map[categorizer]string{
|
||||||
|
rootCatStub: "root",
|
||||||
|
leafCatStub: "leaf",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mc mockCategorizer) pathKeys() []categorizer {
|
func (mc mockCategorizer) pathKeys() []categorizer {
|
||||||
@ -86,11 +89,7 @@ func (ms mockScope) categorizer() categorizer {
|
|||||||
return unknownCatStub
|
return unknownCatStub
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ms mockScope) matchesEntry(
|
func (ms mockScope) matchesInfo(dii details.ItemInfo) bool {
|
||||||
cat categorizer,
|
|
||||||
pathValues map[categorizer]string,
|
|
||||||
entry details.DetailsEntry,
|
|
||||||
) bool {
|
|
||||||
return ms[shouldMatch].Target == "true"
|
return ms[shouldMatch].Target == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,14 +106,27 @@ func stubScope(match string) mockScope {
|
|||||||
sm = match
|
sm = match
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filt := passAny
|
||||||
|
if match == "none" {
|
||||||
|
filt = failAny
|
||||||
|
}
|
||||||
|
|
||||||
return mockScope{
|
return mockScope{
|
||||||
rootCatStub.String(): passAny,
|
rootCatStub.String(): filt,
|
||||||
|
leafCatStub.String(): filt,
|
||||||
scopeKeyCategory: filters.Identity(rootCatStub.String()),
|
scopeKeyCategory: filters.Identity(rootCatStub.String()),
|
||||||
scopeKeyDataType: filters.Identity(rootCatStub.String()),
|
scopeKeyDataType: filters.Identity(rootCatStub.String()),
|
||||||
shouldMatch: filters.Identity(sm),
|
shouldMatch: filters.Identity(sm),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stubInfoScope(match string) mockScope {
|
||||||
|
sc := stubScope(match)
|
||||||
|
sc[scopeKeyInfoFilter] = filters.Identity("true")
|
||||||
|
|
||||||
|
return sc
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// selectors
|
// selectors
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -299,23 +299,14 @@ func (s OneDriveScope) setDefaults() {
|
|||||||
// no-op while no child scope types below user are identified
|
// no-op while no child scope types below user are identified
|
||||||
}
|
}
|
||||||
|
|
||||||
// matchesEntry returns true if either the path or the info in the oneDriveEntry matches the scope details.
|
|
||||||
func (s OneDriveScope) matchesEntry(
|
|
||||||
cat categorizer,
|
|
||||||
pathValues map[categorizer]string,
|
|
||||||
entry details.DetailsEntry,
|
|
||||||
) bool {
|
|
||||||
// matchesPathValues can be handled generically, thanks to SCIENCE.
|
|
||||||
return matchesPathValues(s, cat.(oneDriveCategory), pathValues, entry.ShortRef) || s.matchesInfo(entry.OneDrive)
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchesInfo handles the standard behavior when comparing a scope and an oneDriveInfo
|
// matchesInfo handles the standard behavior when comparing a scope and an oneDriveInfo
|
||||||
// returns true if the scope and info match for the provided category.
|
// returns true if the scope and info match for the provided category.
|
||||||
func (s OneDriveScope) matchesInfo(info *details.OneDriveInfo) bool {
|
func (s OneDriveScope) matchesInfo(dii details.ItemInfo) bool {
|
||||||
// we need values to match against
|
info := dii.OneDrive
|
||||||
if info == nil {
|
if info == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// the scope must define targets to match on
|
// the scope must define targets to match on
|
||||||
filterCat := s.FilterCategory()
|
filterCat := s.FilterCategory()
|
||||||
targets := s.Get(filterCat)
|
targets := s.Get(filterCat)
|
||||||
|
|||||||
@ -93,19 +93,14 @@ type (
|
|||||||
// internal to scopes.go can utilize the scope's category without the service context.
|
// internal to scopes.go can utilize the scope's category without the service context.
|
||||||
categorizer() categorizer
|
categorizer() categorizer
|
||||||
|
|
||||||
// matchesEntry is used to determine if the scope values match with either the pathValues,
|
// matchesInfo is used to determine if the scope values match a specific DetailsEntry
|
||||||
// or the DetailsEntry for the given category.
|
// ItemInfo filter. Unlike path filtering, the entry comparison requires service-specific
|
||||||
// The path comparison (using cat and pathValues) can be handled generically within
|
// context in order for the scope to extract the correct serviceInfo in the entry.
|
||||||
// scopes.go. However, the entry comparison requires service-specific context in order
|
|
||||||
// for the scope to extract the correct serviceInfo in the entry.
|
|
||||||
//
|
//
|
||||||
// Params:
|
// Params:
|
||||||
// cat - the category type expressed in the Path. Not the category of the Scope. If the
|
// info - the details entry itemInfo containing extended service info that a filter may
|
||||||
// scope does not align with this parameter, the result is automatically false.
|
// compare. Identification of the correct entry Info service is left up to the fulfiller.
|
||||||
// pathValues - the result of categorizer.pathValues() for the Path being checked.
|
matchesInfo(info details.ItemInfo) bool
|
||||||
// entry - the details entry containing extended service info for the item that a filter may
|
|
||||||
// compare. Identification of the correct entry Info service is left up to the scope.
|
|
||||||
matchesEntry(cat categorizer, pathValues map[categorizer]string, entry details.DetailsEntry) bool
|
|
||||||
|
|
||||||
// setDefaults populates default values for certain scope categories.
|
// setDefaults populates default values for certain scope categories.
|
||||||
// Primarily to ensure that root- or mid-tier scopes (such as folders)
|
// Primarily to ensure that root- or mid-tier scopes (such as folders)
|
||||||
@ -220,9 +215,9 @@ func reduce[T scopeT, C categoryT](
|
|||||||
}
|
}
|
||||||
|
|
||||||
// aggregate each scope type by category for easier isolation in future processing.
|
// aggregate each scope type by category for easier isolation in future processing.
|
||||||
excls := scopesByCategory[T](s.Excludes, dataCategories)
|
excls := scopesByCategory[T](s.Excludes, dataCategories, false)
|
||||||
filts := scopesByCategory[T](s.Filters, dataCategories)
|
filts := scopesByCategory[T](s.Filters, dataCategories, true)
|
||||||
incls := scopesByCategory[T](s.Includes, dataCategories)
|
incls := scopesByCategory[T](s.Includes, dataCategories, false)
|
||||||
|
|
||||||
ents := []details.DetailsEntry{}
|
ents := []details.DetailsEntry{}
|
||||||
|
|
||||||
@ -262,9 +257,12 @@ func reduce[T scopeT, C categoryT](
|
|||||||
// ex: a slice containing the scopes [mail1, mail2, event1]
|
// ex: a slice containing the scopes [mail1, mail2, event1]
|
||||||
// would produce a map like { mail: [1, 2], event: [1] }
|
// would produce a map like { mail: [1, 2], event: [1] }
|
||||||
// so long as "mail" and "event" are contained in cats.
|
// so long as "mail" and "event" are contained in cats.
|
||||||
|
// For ALL-mach requirements, scopes used as filters should force inclusion using
|
||||||
|
// includeAll=true, independent of the category.
|
||||||
func scopesByCategory[T scopeT, C categoryT](
|
func scopesByCategory[T scopeT, C categoryT](
|
||||||
scopes []scope,
|
scopes []scope,
|
||||||
cats map[path.CategoryType]C,
|
cats map[path.CategoryType]C,
|
||||||
|
includeAll bool,
|
||||||
) map[C][]T {
|
) map[C][]T {
|
||||||
m := map[C][]T{}
|
m := map[C][]T{}
|
||||||
for _, cat := range cats {
|
for _, cat := range cats {
|
||||||
@ -274,7 +272,8 @@ func scopesByCategory[T scopeT, C categoryT](
|
|||||||
for _, sc := range scopes {
|
for _, sc := range scopes {
|
||||||
for _, cat := range cats {
|
for _, cat := range cats {
|
||||||
t := T(sc)
|
t := T(sc)
|
||||||
if typeAndCategoryMatches(cat, t.categorizer()) {
|
// include a scope if the data category matches, or the caller forces inclusion.
|
||||||
|
if includeAll || typeAndCategoryMatches(cat, t.categorizer()) {
|
||||||
m[cat] = append(m[cat], t)
|
m[cat] = append(m[cat], t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -285,8 +284,8 @@ func scopesByCategory[T scopeT, C categoryT](
|
|||||||
|
|
||||||
// passes compares each path to the included and excluded exchange scopes. Returns true
|
// passes compares each path to the included and excluded exchange scopes. Returns true
|
||||||
// if the path is included, passes filters, and not excluded.
|
// if the path is included, passes filters, and not excluded.
|
||||||
func passes[T scopeT](
|
func passes[T scopeT, C categoryT](
|
||||||
cat categorizer,
|
cat C,
|
||||||
pathValues map[categorizer]string,
|
pathValues map[categorizer]string,
|
||||||
entry details.DetailsEntry,
|
entry details.DetailsEntry,
|
||||||
excs, filts, incs []T,
|
excs, filts, incs []T,
|
||||||
@ -303,7 +302,7 @@ func passes[T scopeT](
|
|||||||
var included bool
|
var included bool
|
||||||
|
|
||||||
for _, inc := range incs {
|
for _, inc := range incs {
|
||||||
if inc.matchesEntry(cat, pathValues, entry) {
|
if matchesEntry(inc, cat, pathValues, entry) {
|
||||||
included = true
|
included = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -316,14 +315,14 @@ func passes[T scopeT](
|
|||||||
|
|
||||||
// all filters must pass
|
// all filters must pass
|
||||||
for _, filt := range filts {
|
for _, filt := range filts {
|
||||||
if !filt.matchesEntry(cat, pathValues, entry) {
|
if !matchesEntry(filt, cat, pathValues, entry) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// any matching exclusion means failure
|
// any matching exclusion means failure
|
||||||
for _, exc := range excs {
|
for _, exc := range excs {
|
||||||
if exc.matchesEntry(cat, pathValues, entry) {
|
if matchesEntry(exc, cat, pathValues, entry) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -331,6 +330,22 @@ func passes[T scopeT](
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// matchesEntry determines whether the category and scope require a path
|
||||||
|
// comparison or an entry info comparison.
|
||||||
|
func matchesEntry[T scopeT, C categoryT](
|
||||||
|
sc T,
|
||||||
|
cat C,
|
||||||
|
pathValues map[categorizer]string,
|
||||||
|
entry details.DetailsEntry,
|
||||||
|
) bool {
|
||||||
|
// filterCategory requires matching against service-specific info values
|
||||||
|
if len(getFilterCategory(sc)) > 0 {
|
||||||
|
return sc.matchesInfo(entry.ItemInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchesPathValues(sc, cat, pathValues, entry.ShortRef)
|
||||||
|
}
|
||||||
|
|
||||||
// matchesPathValues will check whether the pathValues have matching entries
|
// matchesPathValues will check whether the pathValues have matching entries
|
||||||
// in the scope. The keys of the values to match against are identified by
|
// in the scope. The keys of the values to match against are identified by
|
||||||
// the categorizer.
|
// the categorizer.
|
||||||
@ -342,27 +357,25 @@ func matchesPathValues[T scopeT, C categoryT](
|
|||||||
pathValues map[categorizer]string,
|
pathValues map[categorizer]string,
|
||||||
shortRef string,
|
shortRef string,
|
||||||
) bool {
|
) bool {
|
||||||
// if scope specifies a filter category,
|
|
||||||
// path checking is automatically skipped.
|
|
||||||
if len(getFilterCategory(sc)) > 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range cat.pathKeys() {
|
for _, c := range cat.pathKeys() {
|
||||||
scopeVals := getCatValue(sc, c)
|
scopeVals := getCatValue(sc, c)
|
||||||
|
|
||||||
// the scope must define the targets to match on
|
// the scope must define the targets to match on
|
||||||
if len(scopeVals) == 0 {
|
if len(scopeVals) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// None() fails all matches
|
// None() fails all matches
|
||||||
if scopeVals[0] == NoneTgt {
|
if scopeVals[0] == NoneTgt {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// the path must contain a value to match against
|
|
||||||
|
// the pathValues must have an entry for the given categorizer
|
||||||
pathVal, ok := pathValues[c]
|
pathVal, ok := pathValues[c]
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// all parts of the scope must match
|
// all parts of the scope must match
|
||||||
cc := c.(C)
|
cc := c.(C)
|
||||||
if !isAnyTarget(sc, cc) {
|
if !isAnyTarget(sc, cc) {
|
||||||
|
|||||||
@ -103,19 +103,29 @@ func (suite *SelectorScopesSuite) TestContains() {
|
|||||||
|
|
||||||
func (suite *SelectorScopesSuite) TestGetCatValue() {
|
func (suite *SelectorScopesSuite) TestGetCatValue() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
|
|
||||||
stub := stubScope("")
|
stub := stubScope("")
|
||||||
stub[rootCatStub.String()] = filterize(rootCatStub.String())
|
stub[rootCatStub.String()] = filterize(rootCatStub.String())
|
||||||
|
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
[]string{rootCatStub.String()},
|
[]string{rootCatStub.String()},
|
||||||
getCatValue(stub, rootCatStub))
|
getCatValue(stub, rootCatStub))
|
||||||
assert.Equal(t, None(), getCatValue(stub, leafCatStub))
|
assert.Equal(t,
|
||||||
|
None(),
|
||||||
|
getCatValue(stub, mockCategorizer("foo")))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *SelectorScopesSuite) TestIsAnyTarget() {
|
func (suite *SelectorScopesSuite) TestIsAnyTarget() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
stub := stubScope("")
|
stub := stubScope("")
|
||||||
assert.True(t, isAnyTarget(stub, rootCatStub))
|
assert.True(t, isAnyTarget(stub, rootCatStub))
|
||||||
|
assert.True(t, isAnyTarget(stub, leafCatStub))
|
||||||
|
assert.False(t, isAnyTarget(stub, mockCategorizer("smarf")))
|
||||||
|
|
||||||
|
stub = stubScope("none")
|
||||||
|
assert.False(t, isAnyTarget(stub, rootCatStub))
|
||||||
assert.False(t, isAnyTarget(stub, leafCatStub))
|
assert.False(t, isAnyTarget(stub, leafCatStub))
|
||||||
|
assert.False(t, isAnyTarget(stub, mockCategorizer("smarf")))
|
||||||
}
|
}
|
||||||
|
|
||||||
var reduceTestTable = []struct {
|
var reduceTestTable = []struct {
|
||||||
@ -161,7 +171,7 @@ var reduceTestTable = []struct {
|
|||||||
name: "include all filter none",
|
name: "include all filter none",
|
||||||
sel: func() mockSel {
|
sel: func() mockSel {
|
||||||
sel := stubSelector()
|
sel := stubSelector()
|
||||||
sel.Filters[0] = scope(stubScope("none"))
|
sel.Filters[0] = scope(stubInfoScope("none"))
|
||||||
sel.Excludes = nil
|
sel.Excludes = nil
|
||||||
return sel
|
return sel
|
||||||
},
|
},
|
||||||
@ -257,7 +267,8 @@ func (suite *SelectorScopesSuite) TestScopesByCategory() {
|
|||||||
[]scope{scope(s1), scope(s2)},
|
[]scope{scope(s1), scope(s2)},
|
||||||
map[path.CategoryType]mockCategorizer{
|
map[path.CategoryType]mockCategorizer{
|
||||||
path.UnknownCategory: rootCatStub,
|
path.UnknownCategory: rootCatStub,
|
||||||
})
|
},
|
||||||
|
false)
|
||||||
assert.Len(t, result, 1)
|
assert.Len(t, result, 1)
|
||||||
assert.Len(t, result[rootCatStub], 1)
|
assert.Len(t, result[rootCatStub], 1)
|
||||||
assert.Empty(t, result[leafCatStub])
|
assert.Empty(t, result[leafCatStub])
|
||||||
@ -265,7 +276,8 @@ func (suite *SelectorScopesSuite) TestScopesByCategory() {
|
|||||||
|
|
||||||
func (suite *SelectorScopesSuite) TestPasses() {
|
func (suite *SelectorScopesSuite) TestPasses() {
|
||||||
cat := rootCatStub
|
cat := rootCatStub
|
||||||
pathVals := map[categorizer]string{}
|
pth := stubPath(suite.T(), "uid", []string{"fld"}, path.EventsCategory)
|
||||||
|
pathVals := cat.pathValues(pth)
|
||||||
entry := details.DetailsEntry{}
|
entry := details.DetailsEntry{}
|
||||||
|
|
||||||
for _, test := range reduceTestTable {
|
for _, test := range reduceTestTable {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user