adds the getPostIDs func to ensure conversations complies with standard data paging patterns --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🌻 Feature #### Issue(s) * #4536 #### Test Plan - [x] ⚡ Unit test - [x] 💚 E2E
569 lines
17 KiB
Go
569 lines
17 KiB
Go
package selectors
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/alcionai/clues"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
"golang.org/x/exp/slices"
|
|
|
|
"github.com/alcionai/corso/src/internal/common/dttm"
|
|
odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts"
|
|
"github.com/alcionai/corso/src/internal/tester"
|
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
|
"github.com/alcionai/corso/src/pkg/fault"
|
|
"github.com/alcionai/corso/src/pkg/path"
|
|
)
|
|
|
|
type GroupsSelectorSuite struct {
|
|
tester.Suite
|
|
}
|
|
|
|
func TestGroupsSelectorSuite(t *testing.T) {
|
|
suite.Run(t, &GroupsSelectorSuite{Suite: tester.NewUnitSuite(t)})
|
|
}
|
|
|
|
func (suite *GroupsSelectorSuite) TestNewGroupsBackup() {
|
|
t := suite.T()
|
|
ob := NewGroupsBackup(nil)
|
|
assert.Equal(t, ob.Service, ServiceGroups)
|
|
assert.NotZero(t, ob.Scopes())
|
|
}
|
|
|
|
func (suite *GroupsSelectorSuite) TestToGroupsBackup() {
|
|
t := suite.T()
|
|
ob := NewGroupsBackup(nil)
|
|
s := ob.Selector
|
|
ob, err := s.ToGroupsBackup()
|
|
require.NoError(t, err, clues.ToCore(err))
|
|
assert.Equal(t, ob.Service, ServiceGroups)
|
|
assert.NotZero(t, ob.Scopes())
|
|
}
|
|
|
|
func (suite *GroupsSelectorSuite) TestNewGroupsRestore() {
|
|
t := suite.T()
|
|
or := NewGroupsRestore(nil)
|
|
assert.Equal(t, or.Service, ServiceGroups)
|
|
assert.NotZero(t, or.Scopes())
|
|
}
|
|
|
|
func (suite *GroupsSelectorSuite) TestToGroupsRestore() {
|
|
t := suite.T()
|
|
eb := NewGroupsRestore(nil)
|
|
s := eb.Selector
|
|
or, err := s.ToGroupsRestore()
|
|
require.NoError(t, err, clues.ToCore(err))
|
|
assert.Equal(t, or.Service, ServiceGroups)
|
|
assert.NotZero(t, or.Scopes())
|
|
}
|
|
|
|
func (suite *GroupsSelectorSuite) TestGroupsRestore_Reduce() {
|
|
toRR := func(cat path.CategoryType, midID string, folders []string, item string) string {
|
|
var (
|
|
folderElems = make([]string, 0, len(folders))
|
|
isDrive = cat == path.LibrariesCategory
|
|
)
|
|
|
|
for _, f := range folders {
|
|
if isDrive {
|
|
f = f + ".d"
|
|
}
|
|
|
|
folderElems = append(folderElems, f)
|
|
}
|
|
|
|
return stubRepoRef(
|
|
path.GroupsService,
|
|
cat,
|
|
midID,
|
|
strings.Join(folderElems, "/"),
|
|
item)
|
|
}
|
|
|
|
var (
|
|
drivePrefixElems = []string{
|
|
odConsts.DrivesPathDir,
|
|
"drive!id",
|
|
odConsts.RootPathDir,
|
|
}
|
|
itemElems1 = []string{"folderA", "folderB"}
|
|
itemElems2 = []string{"folderA", "folderC"}
|
|
itemElems3 = []string{"folderD", "folderE"}
|
|
pairAC = "folderA/folderC"
|
|
libItem = toRR(
|
|
path.LibrariesCategory,
|
|
"sid",
|
|
append(slices.Clone(drivePrefixElems), itemElems1...),
|
|
"item")
|
|
libItem2 = toRR(
|
|
path.LibrariesCategory,
|
|
"sid",
|
|
append(slices.Clone(drivePrefixElems), itemElems2...),
|
|
"item2")
|
|
libItem3 = toRR(
|
|
path.LibrariesCategory,
|
|
"sid",
|
|
append(slices.Clone(drivePrefixElems), itemElems3...),
|
|
"item3")
|
|
chanItem = toRR(path.ChannelMessagesCategory, "gid", slices.Clone(itemElems1), "chitem")
|
|
chanItem2 = toRR(path.ChannelMessagesCategory, "gid", slices.Clone(itemElems2), "chitem2")
|
|
chanItem3 = toRR(path.ChannelMessagesCategory, "gid", slices.Clone(itemElems3), "chitem3")
|
|
convItem = toRR(path.ConversationPostsCategory, "gid", slices.Clone(itemElems1), "convitem")
|
|
convItem2 = toRR(path.ConversationPostsCategory, "gid", slices.Clone(itemElems2), "convitem2")
|
|
convItem3 = toRR(path.ConversationPostsCategory, "gid", slices.Clone(itemElems3), "convitem3")
|
|
)
|
|
|
|
deets := &details.Details{
|
|
DetailsModel: details.DetailsModel{
|
|
Entries: []details.Entry{
|
|
{
|
|
RepoRef: libItem,
|
|
ItemRef: "item",
|
|
LocationRef: strings.Join(append([]string{odConsts.RootPathDir}, itemElems1...), "/"),
|
|
ItemInfo: details.ItemInfo{
|
|
Groups: &details.GroupsInfo{
|
|
ItemType: details.SharePointLibrary,
|
|
ItemName: "itemName",
|
|
ParentPath: strings.Join(itemElems1, "/"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
RepoRef: libItem2,
|
|
LocationRef: strings.Join(append([]string{odConsts.RootPathDir}, itemElems2...), "/"),
|
|
// ItemRef intentionally blank to test fallback case
|
|
ItemInfo: details.ItemInfo{
|
|
Groups: &details.GroupsInfo{
|
|
ItemType: details.SharePointLibrary,
|
|
ItemName: "itemName2",
|
|
ParentPath: strings.Join(itemElems2, "/"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
RepoRef: libItem3,
|
|
ItemRef: "item3",
|
|
LocationRef: strings.Join(append([]string{odConsts.RootPathDir}, itemElems3...), "/"),
|
|
ItemInfo: details.ItemInfo{
|
|
Groups: &details.GroupsInfo{
|
|
ItemType: details.SharePointLibrary,
|
|
ItemName: "itemName3",
|
|
ParentPath: strings.Join(itemElems3, "/"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
RepoRef: chanItem,
|
|
ItemRef: "citem",
|
|
LocationRef: strings.Join(itemElems1, "/"),
|
|
ItemInfo: details.ItemInfo{
|
|
Groups: &details.GroupsInfo{
|
|
ItemType: details.GroupsChannelMessage,
|
|
ParentPath: strings.Join(itemElems1, "/"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
RepoRef: chanItem2,
|
|
LocationRef: strings.Join(itemElems2, "/"),
|
|
// ItemRef intentionally blank to test fallback case
|
|
ItemInfo: details.ItemInfo{
|
|
Groups: &details.GroupsInfo{
|
|
ItemType: details.GroupsChannelMessage,
|
|
ParentPath: strings.Join(itemElems2, "/"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
RepoRef: chanItem3,
|
|
ItemRef: "citem3",
|
|
LocationRef: strings.Join(itemElems3, "/"),
|
|
ItemInfo: details.ItemInfo{
|
|
Groups: &details.GroupsInfo{
|
|
ItemType: details.GroupsChannelMessage,
|
|
ParentPath: strings.Join(itemElems3, "/"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
RepoRef: convItem,
|
|
ItemRef: "convitem",
|
|
LocationRef: strings.Join(itemElems1, "/"),
|
|
ItemInfo: details.ItemInfo{
|
|
Groups: &details.GroupsInfo{
|
|
ItemType: details.GroupsConversationPost,
|
|
ParentPath: strings.Join(itemElems1, "/"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
RepoRef: convItem2,
|
|
LocationRef: strings.Join(itemElems2, "/"),
|
|
// ItemRef intentionally blank to test fallback case
|
|
ItemInfo: details.ItemInfo{
|
|
Groups: &details.GroupsInfo{
|
|
ItemType: details.GroupsConversationPost,
|
|
ParentPath: strings.Join(itemElems2, "/"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
RepoRef: convItem3,
|
|
ItemRef: "convitem3",
|
|
LocationRef: strings.Join(itemElems3, "/"),
|
|
ItemInfo: details.ItemInfo{
|
|
Groups: &details.GroupsInfo{
|
|
ItemType: details.GroupsConversationPost,
|
|
ParentPath: strings.Join(itemElems3, "/"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
arr := func(s ...string) []string {
|
|
return s
|
|
}
|
|
|
|
table := []struct {
|
|
name string
|
|
makeSelector func() *GroupsRestore
|
|
expect []string
|
|
cfg Config
|
|
}{
|
|
{
|
|
name: "all",
|
|
makeSelector: func() *GroupsRestore {
|
|
sel := NewGroupsRestore(Any())
|
|
sel.Include(sel.AllData())
|
|
return sel
|
|
},
|
|
expect: arr(
|
|
libItem, libItem2, libItem3,
|
|
chanItem, chanItem2, chanItem3,
|
|
convItem, convItem2, convItem3),
|
|
},
|
|
{
|
|
name: "only match library item",
|
|
makeSelector: func() *GroupsRestore {
|
|
sel := NewGroupsRestore(Any())
|
|
sel.Include(sel.LibraryItems(Any(), []string{"item2"}))
|
|
return sel
|
|
},
|
|
expect: arr(libItem2),
|
|
},
|
|
{
|
|
name: "library id doesn't match name",
|
|
makeSelector: func() *GroupsRestore {
|
|
sel := NewGroupsRestore(Any())
|
|
sel.Include(sel.LibraryItems(Any(), []string{"item2"}))
|
|
return sel
|
|
},
|
|
expect: []string{},
|
|
cfg: Config{OnlyMatchItemNames: true},
|
|
},
|
|
{
|
|
name: "library only match item name",
|
|
makeSelector: func() *GroupsRestore {
|
|
sel := NewGroupsRestore(Any())
|
|
sel.Include(sel.LibraryItems(Any(), []string{"itemName2"}))
|
|
return sel
|
|
},
|
|
expect: arr(libItem2),
|
|
cfg: Config{OnlyMatchItemNames: true},
|
|
},
|
|
{
|
|
name: "name doesn't match",
|
|
makeSelector: func() *GroupsRestore {
|
|
sel := NewGroupsRestore(Any())
|
|
sel.Include(sel.LibraryItems(Any(), []string{"itemName2"}))
|
|
return sel
|
|
},
|
|
expect: []string{},
|
|
},
|
|
{
|
|
name: "only match folder",
|
|
makeSelector: func() *GroupsRestore {
|
|
sel := NewGroupsRestore([]string{"sid"})
|
|
sel.Include(sel.LibraryFolders([]string{"folderA/folderB", pairAC}))
|
|
return sel
|
|
},
|
|
expect: arr(libItem, libItem2),
|
|
},
|
|
{
|
|
name: "only match channel item",
|
|
makeSelector: func() *GroupsRestore {
|
|
sel := NewGroupsRestore(Any())
|
|
sel.Include(sel.ChannelMessages(Any(), []string{"chitem2"}))
|
|
return sel
|
|
},
|
|
expect: arr(chanItem2),
|
|
},
|
|
{
|
|
name: "channel id doesn't match name",
|
|
makeSelector: func() *GroupsRestore {
|
|
sel := NewGroupsRestore(Any())
|
|
sel.Include(sel.ChannelMessages(Any(), []string{"item2"}))
|
|
return sel
|
|
},
|
|
expect: []string{},
|
|
cfg: Config{OnlyMatchItemNames: true},
|
|
},
|
|
{
|
|
name: "only match conversation item",
|
|
makeSelector: func() *GroupsRestore {
|
|
sel := NewGroupsRestore(Any())
|
|
sel.Include(sel.ConversationPosts(Any(), []string{"convitem2"}))
|
|
return sel
|
|
},
|
|
expect: arr(convItem2),
|
|
},
|
|
{
|
|
name: "conversation id doesn't match name",
|
|
makeSelector: func() *GroupsRestore {
|
|
sel := NewGroupsRestore(Any())
|
|
sel.Include(sel.ConversationPosts(Any(), []string{"item2"}))
|
|
return sel
|
|
},
|
|
expect: []string{},
|
|
cfg: Config{OnlyMatchItemNames: true},
|
|
},
|
|
}
|
|
for _, test := range table {
|
|
suite.Run(test.name, func() {
|
|
t := suite.T()
|
|
|
|
ctx, flush := tester.NewContext(t)
|
|
defer flush()
|
|
|
|
sel := test.makeSelector()
|
|
sel.Configure(test.cfg)
|
|
results := sel.Reduce(ctx, deets, fault.New(true))
|
|
paths := results.Paths()
|
|
assert.Equal(t, test.expect, paths)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (suite *GroupsSelectorSuite) TestGroupsCategory_PathValues() {
|
|
var (
|
|
itemName = "item"
|
|
itemID = "item-id"
|
|
shortRef = "short"
|
|
elems = []string{itemID}
|
|
)
|
|
|
|
table := []struct {
|
|
name string
|
|
sc groupsCategory
|
|
pathElems []string
|
|
locRef string
|
|
parentPath string
|
|
expected map[categorizer][]string
|
|
cfg Config
|
|
}{
|
|
{
|
|
name: "Groups Channel Messages",
|
|
sc: GroupsChannelMessage,
|
|
pathElems: elems,
|
|
locRef: "",
|
|
expected: map[categorizer][]string{
|
|
GroupsChannel: {""},
|
|
GroupsChannelMessage: {itemID, shortRef},
|
|
},
|
|
cfg: Config{},
|
|
},
|
|
{
|
|
name: "Groups Conversation Posts",
|
|
sc: GroupsConversationPost,
|
|
pathElems: elems,
|
|
locRef: "",
|
|
expected: map[categorizer][]string{
|
|
GroupsConversation: {""},
|
|
GroupsConversationPost: {itemID, shortRef},
|
|
},
|
|
cfg: Config{},
|
|
},
|
|
}
|
|
|
|
for _, test := range table {
|
|
suite.Run(test.name, func() {
|
|
t := suite.T()
|
|
|
|
itemPath, err := path.Build(
|
|
"tenant",
|
|
"site",
|
|
path.GroupsService,
|
|
test.sc.PathType(),
|
|
true,
|
|
test.pathElems...)
|
|
require.NoError(t, err, clues.ToCore(err))
|
|
|
|
ent := details.Entry{
|
|
RepoRef: itemPath.String(),
|
|
ShortRef: shortRef,
|
|
ItemRef: itemPath.Item(),
|
|
LocationRef: test.locRef,
|
|
ItemInfo: details.ItemInfo{
|
|
Groups: &details.GroupsInfo{
|
|
ItemName: itemName,
|
|
ParentPath: test.parentPath,
|
|
},
|
|
},
|
|
}
|
|
|
|
pv, err := test.sc.pathValues(itemPath, ent, test.cfg)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, test.expected, pv)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (suite *GroupsSelectorSuite) TestGroupsScope_MatchesInfo() {
|
|
var (
|
|
sel = NewGroupsRestore(Any())
|
|
user = "user@mail.com"
|
|
host = "www.website.com"
|
|
// pth = "/foo"
|
|
// url = host + pth
|
|
epoch = time.Time{}
|
|
now = time.Now()
|
|
mod = now.Add(15 * time.Minute)
|
|
future = now.Add(45 * time.Minute)
|
|
dgcm = details.GroupsChannelMessage
|
|
dspl = details.SharePointLibrary
|
|
)
|
|
|
|
type expectation func(t assert.TestingT, value bool, msg string, args ...any) bool
|
|
|
|
table := []struct {
|
|
name string
|
|
itemType details.ItemType
|
|
creator string
|
|
scope []GroupsScope
|
|
expect expectation
|
|
}{
|
|
{"file create after the epoch", dspl, user, sel.CreatedAfter(dttm.Format(epoch)), assert.Truef},
|
|
{"file create after the epoch wrong type", dgcm, user, sel.CreatedAfter(dttm.Format(epoch)), assert.Falsef},
|
|
{"file create after now", dspl, user, sel.CreatedAfter(dttm.Format(now)), assert.Falsef},
|
|
{"file create after later", dspl, user, sel.CreatedAfter(dttm.Format(future)), assert.Falsef},
|
|
{"file create before future", dspl, user, sel.CreatedBefore(dttm.Format(future)), assert.Truef},
|
|
{"file create before future wrong type", dgcm, user, sel.CreatedBefore(dttm.Format(future)), assert.Falsef},
|
|
{"file create before now", dspl, user, sel.CreatedBefore(dttm.Format(now)), assert.Falsef},
|
|
{"file create before modification", dspl, user, sel.CreatedBefore(dttm.Format(mod)), assert.Truef},
|
|
{"file create before epoch", dspl, user, sel.CreatedBefore(dttm.Format(now)), assert.Falsef},
|
|
{"file modified after the epoch", dspl, user, sel.ModifiedAfter(dttm.Format(epoch)), assert.Truef},
|
|
{"file modified after now", dspl, user, sel.ModifiedAfter(dttm.Format(now)), assert.Truef},
|
|
{"file modified after later", dspl, user, sel.ModifiedAfter(dttm.Format(future)), assert.Falsef},
|
|
{"file modified before future", dspl, user, sel.ModifiedBefore(dttm.Format(future)), assert.Truef},
|
|
{"file modified before now", dspl, user, sel.ModifiedBefore(dttm.Format(now)), assert.Falsef},
|
|
{"file modified before epoch", dspl, user, sel.ModifiedBefore(dttm.Format(now)), assert.Falsef},
|
|
{"in library", dspl, user, sel.Library("included-library"), assert.Truef},
|
|
{"not in library", dspl, user, sel.Library("not-included-library"), assert.Falsef},
|
|
{"site id", dspl, user, sel.Site("site1"), assert.Truef},
|
|
{"web url", dspl, user, sel.Site(user), assert.Truef},
|
|
{"library id", dspl, user, sel.Library("1234"), assert.Truef},
|
|
{"not library id", dspl, user, sel.Library("abcd"), assert.Falsef},
|
|
|
|
{"channel message created by", dgcm, user, sel.MessageCreator(user), assert.Truef},
|
|
{"channel message not created by", dgcm, user, sel.MessageCreator(host), assert.Falsef},
|
|
{"chan msg create after the epoch", dgcm, user, sel.MessageCreatedAfter(dttm.Format(epoch)), assert.Truef},
|
|
{
|
|
"chan msg create after the epoch wrong type",
|
|
dspl,
|
|
user,
|
|
sel.MessageCreatedAfter(dttm.Format(epoch)),
|
|
assert.Falsef,
|
|
},
|
|
{"chan msg create after now", dgcm, user, sel.MessageCreatedAfter(dttm.Format(now)), assert.Falsef},
|
|
{"chan msg create after later", dgcm, user, sel.MessageCreatedAfter(dttm.Format(future)), assert.Falsef},
|
|
{"chan msg create before future", dgcm, user, sel.MessageCreatedBefore(dttm.Format(future)), assert.Truef},
|
|
{
|
|
"chan msg create before future wrong type",
|
|
dspl,
|
|
user,
|
|
sel.MessageCreatedBefore(dttm.Format(future)),
|
|
assert.Falsef,
|
|
},
|
|
{"chan msg create before now", dgcm, user, sel.MessageCreatedBefore(dttm.Format(now)), assert.Falsef},
|
|
{"chan msg create before reply", dgcm, user, sel.MessageCreatedBefore(dttm.Format(mod)), assert.Truef},
|
|
{"chan msg create before reply wrong type", dspl, user, sel.MessageCreatedBefore(dttm.Format(mod)), assert.Falsef},
|
|
{"chan msg create before epoch", dgcm, user, sel.MessageCreatedBefore(dttm.Format(now)), assert.Falsef},
|
|
{"chan msg last reply after the epoch", dgcm, user, sel.MessageLastReplyAfter(dttm.Format(epoch)), assert.Truef},
|
|
{"chan msg last reply after now", dgcm, user, sel.MessageLastReplyAfter(dttm.Format(now)), assert.Truef},
|
|
{"chan msg last reply after later", dgcm, user, sel.MessageLastReplyAfter(dttm.Format(future)), assert.Falsef},
|
|
{"chan msg last reply before future", dgcm, user, sel.MessageLastReplyBefore(dttm.Format(future)), assert.Truef},
|
|
{"chan msg last reply before now", dgcm, user, sel.MessageLastReplyBefore(dttm.Format(now)), assert.Falsef},
|
|
{"chan msg last reply before epoch", dgcm, user, sel.MessageLastReplyBefore(dttm.Format(now)), assert.Falsef},
|
|
}
|
|
for _, test := range table {
|
|
suite.Run(test.name, func() {
|
|
t := suite.T()
|
|
|
|
itemInfo := details.ItemInfo{
|
|
Groups: &details.GroupsInfo{
|
|
ItemType: test.itemType,
|
|
Created: now,
|
|
WebURL: test.creator,
|
|
Modified: mod,
|
|
DriveName: "included-library",
|
|
DriveID: "1234",
|
|
SiteID: "site1",
|
|
Message: details.ChannelMessageInfo{
|
|
Creator: test.creator,
|
|
CreatedAt: now,
|
|
},
|
|
LastReply: details.ChannelMessageInfo{
|
|
CreatedAt: mod,
|
|
},
|
|
},
|
|
}
|
|
|
|
scopes := setScopesToDefault(test.scope)
|
|
for _, scope := range scopes {
|
|
test.expect(
|
|
t,
|
|
scope.matchesInfo(itemInfo),
|
|
"not matching:\nscope:\n\t%+v\ninfo:\n\t%+v",
|
|
scope,
|
|
itemInfo.Groups)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func (suite *GroupsSelectorSuite) TestCategory_PathType() {
|
|
table := []struct {
|
|
cat groupsCategory
|
|
pathType path.CategoryType
|
|
}{
|
|
{GroupsCategoryUnknown, path.UnknownCategory},
|
|
{GroupsChannel, path.ChannelMessagesCategory},
|
|
{GroupsChannelMessage, path.ChannelMessagesCategory},
|
|
{GroupsConversation, path.ConversationPostsCategory},
|
|
{GroupsConversationPost, path.ConversationPostsCategory},
|
|
{GroupsInfoChannelMessageCreator, path.ChannelMessagesCategory},
|
|
{GroupsInfoChannelMessageCreatedAfter, path.ChannelMessagesCategory},
|
|
{GroupsInfoChannelMessageCreatedBefore, path.ChannelMessagesCategory},
|
|
{GroupsInfoChannelMessageLastReplyAfter, path.ChannelMessagesCategory},
|
|
{GroupsInfoChannelMessageLastReplyBefore, path.ChannelMessagesCategory},
|
|
{GroupsLibraryFolder, path.LibrariesCategory},
|
|
{GroupsLibraryItem, path.LibrariesCategory},
|
|
{GroupsInfoSiteLibraryDrive, path.LibrariesCategory},
|
|
{GroupsInfoSite, path.LibrariesCategory},
|
|
}
|
|
for _, test := range table {
|
|
suite.Run(test.cat.String(), func() {
|
|
assert.Equal(
|
|
suite.T(),
|
|
test.pathType.String(),
|
|
test.cat.PathType().String())
|
|
})
|
|
}
|
|
}
|