From e90fef4f9393f4025489803da008dfc0b2354c87 Mon Sep 17 00:00:00 2001 From: Keepers Date: Thu, 25 Jan 2024 12:06:09 -0700 Subject: [PATCH] add teams chats selectors boilerplate (#5075) --- .../details/{chats.go => teamsChats.go} | 0 .../{chats_test.go => teamsChats_test.go} | 0 src/pkg/path/service_type.go | 20 +- src/pkg/selectors/exchange.go | 8 +- src/pkg/selectors/exchange_test.go | 2 +- src/pkg/selectors/selectors.go | 2 + src/pkg/selectors/teamsChats.go | 525 +++++++++++ src/pkg/selectors/teamsChats_test.go | 843 ++++++++++++++++++ 8 files changed, 1385 insertions(+), 15 deletions(-) rename src/pkg/backup/details/{chats.go => teamsChats.go} (100%) rename src/pkg/backup/details/{chats_test.go => teamsChats_test.go} (100%) create mode 100644 src/pkg/selectors/teamsChats.go create mode 100644 src/pkg/selectors/teamsChats_test.go diff --git a/src/pkg/backup/details/chats.go b/src/pkg/backup/details/teamsChats.go similarity index 100% rename from src/pkg/backup/details/chats.go rename to src/pkg/backup/details/teamsChats.go diff --git a/src/pkg/backup/details/chats_test.go b/src/pkg/backup/details/teamsChats_test.go similarity index 100% rename from src/pkg/backup/details/chats_test.go rename to src/pkg/backup/details/teamsChats_test.go diff --git a/src/pkg/path/service_type.go b/src/pkg/path/service_type.go index 098fb4ffde..5541e670de 100644 --- a/src/pkg/path/service_type.go +++ b/src/pkg/path/service_type.go @@ -36,16 +36,16 @@ const ( ) var strToSvc = map[string]ServiceType{ - ExchangeService.String(): ExchangeService, - ExchangeMetadataService.String(): ExchangeMetadataService, - OneDriveService.String(): OneDriveService, - OneDriveMetadataService.String(): OneDriveMetadataService, - SharePointService.String(): SharePointService, - SharePointMetadataService.String(): SharePointMetadataService, - GroupsService.String(): GroupsService, - GroupsMetadataService.String(): GroupsMetadataService, - TeamsChatsService.String(): TeamsChatsService, - TeamsChatsMetadataService.String(): TeamsChatsMetadataService, + strings.ToLower(ExchangeService.String()): ExchangeService, + strings.ToLower(ExchangeMetadataService.String()): ExchangeMetadataService, + strings.ToLower(OneDriveService.String()): OneDriveService, + strings.ToLower(OneDriveMetadataService.String()): OneDriveMetadataService, + strings.ToLower(SharePointService.String()): SharePointService, + strings.ToLower(SharePointMetadataService.String()): SharePointMetadataService, + strings.ToLower(GroupsService.String()): GroupsService, + strings.ToLower(GroupsMetadataService.String()): GroupsMetadataService, + strings.ToLower(TeamsChatsService.String()): TeamsChatsService, + strings.ToLower(TeamsChatsMetadataService.String()): TeamsChatsMetadataService, } func ToServiceType(service string) ServiceType { diff --git a/src/pkg/selectors/exchange.go b/src/pkg/selectors/exchange.go index 26af761eae..ba824a89e0 100644 --- a/src/pkg/selectors/exchange.go +++ b/src/pkg/selectors/exchange.go @@ -591,7 +591,7 @@ func (ec exchangeCategory) isLeaf() bool { // pathValues transforms the two paths to maps of identified properties. // // Example: -// [tenantID, service, userPN, category, mailFolder, mailID] +// [tenantID, service, userID, category, mailFolder, mailID] // => {exchMailFolder: mailFolder, exchMail: mailID} func (ec exchangeCategory) pathValues( repo path.Path, @@ -772,7 +772,7 @@ func (s ExchangeScope) matchesInfo(dii details.ItemInfo) bool { infoCat := s.InfoCategory() - cfpc := categoryFromItemType(info.ItemType) + cfpc := exchangeCategoryFromItemType(info.ItemType) if !typeAndCategoryMatches(infoCat, cfpc) { return false } @@ -801,10 +801,10 @@ func (s ExchangeScope) matchesInfo(dii details.ItemInfo) bool { return s.Matches(infoCat, i) } -// categoryFromItemType interprets the category represented by the ExchangeInfo +// exchangeCategoryFromItemType 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 { +func exchangeCategoryFromItemType(pct details.ItemType) exchangeCategory { switch pct { case details.ExchangeContact: return ExchangeContact diff --git a/src/pkg/selectors/exchange_test.go b/src/pkg/selectors/exchange_test.go index 16d2541dac..4dc56c5293 100644 --- a/src/pkg/selectors/exchange_test.go +++ b/src/pkg/selectors/exchange_test.go @@ -1602,7 +1602,7 @@ func (suite *ExchangeSelectorSuite) TestCategoryFromItemType() { suite.Run(test.name, func() { t := suite.T() - result := categoryFromItemType(test.input) + result := exchangeCategoryFromItemType(test.input) assert.Equal(t, test.expect, result) }) } diff --git a/src/pkg/selectors/selectors.go b/src/pkg/selectors/selectors.go index b1fdb64330..4c49f637b2 100644 --- a/src/pkg/selectors/selectors.go +++ b/src/pkg/selectors/selectors.go @@ -26,6 +26,7 @@ const ( ServiceOneDrive service = 2 // OneDrive ServiceSharePoint service = 3 // SharePoint ServiceGroups service = 4 // Groups + ServiceTeamsChats service = 5 // TeamsChats ) var serviceToPathType = map[service]path.ServiceType{ @@ -34,6 +35,7 @@ var serviceToPathType = map[service]path.ServiceType{ ServiceOneDrive: path.OneDriveService, ServiceSharePoint: path.SharePointService, ServiceGroups: path.GroupsService, + ServiceTeamsChats: path.TeamsChatsService, } var ( diff --git a/src/pkg/selectors/teamsChats.go b/src/pkg/selectors/teamsChats.go new file mode 100644 index 0000000000..60df1a08b6 --- /dev/null +++ b/src/pkg/selectors/teamsChats.go @@ -0,0 +1,525 @@ +package selectors + +import ( + "context" + "fmt" + "strings" + + "github.com/alcionai/clues" + + "github.com/alcionai/corso/src/pkg/backup/details" + "github.com/alcionai/corso/src/pkg/backup/identity" + "github.com/alcionai/corso/src/pkg/fault" + "github.com/alcionai/corso/src/pkg/filters" + "github.com/alcionai/corso/src/pkg/path" +) + +// --------------------------------------------------------------------------- +// Selectors +// --------------------------------------------------------------------------- + +type ( + // teamsChats provides an api for selecting + // data scopes applicable to the TeamsChats service. + teamsChats struct { + Selector + } + + // TeamsChatsBackup provides an api for selecting + // data scopes applicable to the TeamsChats service, + // plus backup-specific methods. + TeamsChatsBackup struct { + teamsChats + } + + // TeamsChatsRestore provides an api for selecting + // data scopes applicable to the TeamsChats service, + // plus restore-specific methods. + TeamsChatsRestore struct { + teamsChats + } +) + +var ( + _ Reducer = &TeamsChatsRestore{} + _ pathCategorier = &TeamsChatsRestore{} + _ reasoner = &TeamsChatsRestore{} +) + +// NewTeamsChats produces a new Selector with the service set to ServiceTeamsChats. +func NewTeamsChatsBackup(users []string) *TeamsChatsBackup { + src := TeamsChatsBackup{ + teamsChats{ + newSelector(ServiceTeamsChats, users), + }, + } + + return &src +} + +// ToTeamsChatsBackup transforms the generic selector into an TeamsChatsBackup. +// Errors if the service defined by the selector is not ServiceTeamsChats. +func (s Selector) ToTeamsChatsBackup() (*TeamsChatsBackup, error) { + if s.Service != ServiceTeamsChats { + return nil, badCastErr(ServiceTeamsChats, s.Service) + } + + src := TeamsChatsBackup{teamsChats{s}} + + return &src, nil +} + +func (s TeamsChatsBackup) SplitByResourceOwner(users []string) []TeamsChatsBackup { + sels := splitByProtectedResource[TeamsChatsScope](s.Selector, users, TeamsChatsUser) + + ss := make([]TeamsChatsBackup, 0, len(sels)) + for _, sel := range sels { + ss = append(ss, TeamsChatsBackup{teamsChats{sel}}) + } + + return ss +} + +// NewTeamsChatsRestore produces a new Selector with the service set to ServiceTeamsChats. +func NewTeamsChatsRestore(users []string) *TeamsChatsRestore { + src := TeamsChatsRestore{ + teamsChats{ + newSelector(ServiceTeamsChats, users), + }, + } + + return &src +} + +// ToTeamsChatsRestore transforms the generic selector into an TeamsChatsRestore. +// Errors if the service defined by the selector is not ServiceTeamsChats. +func (s Selector) ToTeamsChatsRestore() (*TeamsChatsRestore, error) { + if s.Service != ServiceTeamsChats { + return nil, badCastErr(ServiceTeamsChats, s.Service) + } + + src := TeamsChatsRestore{teamsChats{s}} + + return &src, nil +} + +func (sr TeamsChatsRestore) SplitByResourceOwner(users []string) []TeamsChatsRestore { + sels := splitByProtectedResource[TeamsChatsScope](sr.Selector, users, TeamsChatsUser) + + ss := make([]TeamsChatsRestore, 0, len(sels)) + for _, sel := range sels { + ss = append(ss, TeamsChatsRestore{teamsChats{sel}}) + } + + return ss +} + +// PathCategories produces the aggregation of discrete users described by each type of scope. +func (s teamsChats) PathCategories() selectorPathCategories { + return selectorPathCategories{ + Excludes: pathCategoriesIn[TeamsChatsScope, teamsChatsCategory](s.Excludes), + Filters: pathCategoriesIn[TeamsChatsScope, teamsChatsCategory](s.Filters), + Includes: pathCategoriesIn[TeamsChatsScope, teamsChatsCategory](s.Includes), + } +} + +// Reasons returns a deduplicated set of the backup reasons produced +// using the selector's discrete owner and each scopes' service and +// category types. +func (s teamsChats) Reasons(tenantID string, useOwnerNameForID bool) []identity.Reasoner { + return reasonsFor(s, tenantID, useOwnerNameForID) +} + +// --------------------------------------------------------------------------- +// Stringers and Concealers +// --------------------------------------------------------------------------- + +func (s TeamsChatsScope) Conceal() string { return conceal(s) } +func (s TeamsChatsScope) Format(fs fmt.State, r rune) { format(s, fs, r) } +func (s TeamsChatsScope) String() string { return conceal(s) } +func (s TeamsChatsScope) PlainString() string { return plainString(s) } + +// ------------------- +// Exclude/Includes + +// Exclude appends the provided scopes to the selector's exclusion set. +// Every Exclusion scope applies globally, affecting all inclusion scopes. +// Data is excluded if it matches ANY exclusion (of the same data category). +// +// All parts of the scope must match for data to be exclucded. +// Ex: Mail(u1, f1, m1) => only excludes mail if it is owned by user u1, +// located in folder f1, and ID'd as m1. MailSender(foo) => only excludes +// mail whose sender is foo. Use selectors.Any() to wildcard a scope value. +// No value will match if selectors.None() is provided. +// +// Group-level scopes will automatically apply the Any() wildcard to +// child properties. +// ex: User(u1) automatically cascades to all chats, +func (s *teamsChats) Exclude(scopes ...[]TeamsChatsScope) { + s.Excludes = appendScopes(s.Excludes, scopes...) +} + +// Filter appends the provided scopes to the selector's filters set. +// A selector with >0 filters and 0 inclusions will include any data +// that passes all filters. +// A selector with >0 filters and >0 inclusions will reduce the +// inclusion set to only the data that passes all filters. +// Data is retained if it passes ALL filters (of the same data category). +// +// All parts of the scope must match for data to pass the filter. +// Ex: Mail(u1, f1, m1) => only passes mail that is owned by user u1, +// located in folder f1, and ID'd as m1. MailSender(foo) => only passes +// mail whose sender is foo. Use selectors.Any() to wildcard a scope value. +// No value will match if selectors.None() is provided. +// +// Group-level scopes will automatically apply the Any() wildcard to +// child properties. +// ex: User(u1) automatically cascades to all chats, +func (s *teamsChats) Filter(scopes ...[]TeamsChatsScope) { + s.Filters = appendScopes(s.Filters, scopes...) +} + +// Include appends the provided scopes to the selector's inclusion set. +// Data is included if it matches ANY inclusion. +// The inclusion set is later filtered (all included data must pass ALL +// filters) and excluded (all included data must not match ANY exclusion). +// Data is included if it matches ANY inclusion (of the same data category). +// +// All parts of the scope must match for data to be included. +// Ex: Mail(u1, f1, m1) => only includes mail if it is owned by user u1, +// located in folder f1, and ID'd as m1. MailSender(foo) => only includes +// mail whose sender is foo. Use selectors.Any() to wildcard a scope value. +// No value will match if selectors.None() is provided. +// +// Group-level scopes will automatically apply the Any() wildcard to +// child properties. +// ex: User(u1) automatically cascades to all chats, +func (s *teamsChats) Include(scopes ...[]TeamsChatsScope) { + s.Includes = appendScopes(s.Includes, scopes...) +} + +// Scopes retrieves the list of teamsChatsScopes in the selector. +func (s *teamsChats) Scopes() []TeamsChatsScope { + return scopes[TeamsChatsScope](s.Selector) +} + +type TeamsChatsItemScopeConstructor func([]string, []string, ...option) []TeamsChatsScope + +// ------------------- +// Scope Factories + +// Chats produces one or more teamsChats 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 folder scopes. +func (s *teamsChats) Chats(chats []string, opts ...option) []TeamsChatsScope { + scopes := []TeamsChatsScope{} + + scopes = append( + scopes, + makeScope[TeamsChatsScope](TeamsChatsChat, chats, defaultItemOptions(s.Cfg)...)) + + return scopes +} + +// Retrieves all teamsChats data. +// Each user id generates a scope for each data type: chats (only one data type at this time). +// 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 *teamsChats) AllData() []TeamsChatsScope { + scopes := []TeamsChatsScope{} + + scopes = append(scopes, makeScope[TeamsChatsScope](TeamsChatsChat, Any())) + + return scopes +} + +// ------------------- +// ItemInfo Factories + +// ChatMember produces one or more teamsChats chat member info scopes. +// Matches any chat member whose email contains the provided string. +// 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 (sr *TeamsChatsRestore) ChatMember(memberID string) []TeamsChatsScope { + return []TeamsChatsScope{ + makeInfoScope[TeamsChatsScope]( + TeamsChatsChat, + TeamsChatsInfoChatMember, + []string{memberID}, + filters.In), + } +} + +// ChatName produces one or more teamsChats chat name info scopes. +// Matches any chat whose name contains the provided string. +// 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 (sr *TeamsChatsRestore) ChatName(memberID string) []TeamsChatsScope { + return []TeamsChatsScope{ + makeInfoScope[TeamsChatsScope]( + TeamsChatsChat, + TeamsChatsInfoChatName, + []string{memberID}, + filters.In), + } +} + +// --------------------------------------------------------------------------- +// Categories +// --------------------------------------------------------------------------- + +// teamsChatsCategory enumerates the type of the lowest level +// of data specified by the scope. +type teamsChatsCategory string + +// interface compliance checks +var _ categorizer = TeamsChatsCategoryUnknown + +const ( + TeamsChatsCategoryUnknown teamsChatsCategory = "" + + // types of data identified by teamsChats + TeamsChatsUser teamsChatsCategory = "TeamsChatsUser" + TeamsChatsChat teamsChatsCategory = "TeamsChatsChat" + + // data contained within details.ItemInfo + TeamsChatsInfoChatMember teamsChatsCategory = "TeamsChatsInfoChatMember" + TeamsChatsInfoChatName teamsChatsCategory = "TeamsChatsInfoChatName" +) + +// teamsChatsLeafProperties describes common metadata of the leaf categories +var teamsChatsLeafProperties = map[categorizer]leafProperty{ + TeamsChatsChat: { + pathKeys: []categorizer{TeamsChatsChat}, + pathType: path.ChatsCategory, + }, + TeamsChatsUser: { // the root category must be represented, even though it isn't a leaf + pathKeys: []categorizer{TeamsChatsUser}, + pathType: path.UnknownCategory, + }, +} + +func (ec teamsChatsCategory) String() string { + return string(ec) +} + +// leafCat returns the leaf category of the receiver. +// If the receiver category has multiple leaves (ex: User) or no leaves, +// (ex: Unknown), the receiver itself is returned. +// If the receiver category is an info type (ex: TeamsChatsInfoChatMember), +// returns the category covered by the info. +// Ex: TeamsChatsChatFolder.leafCat() => TeamsChatsChat +// Ex: TeamsChatsUser.leafCat() => TeamsChatsUser +func (ec teamsChatsCategory) leafCat() categorizer { + switch ec { + case TeamsChatsChat, TeamsChatsInfoChatMember, TeamsChatsInfoChatName: + return TeamsChatsChat + } + + return ec +} + +// rootCat returns the root category type. +func (ec teamsChatsCategory) rootCat() categorizer { + return TeamsChatsUser +} + +// unknownCat returns the unknown category type. +func (ec teamsChatsCategory) unknownCat() categorizer { + return TeamsChatsCategoryUnknown +} + +// isUnion returns true if c is a user +func (ec teamsChatsCategory) isUnion() bool { + return ec == ec.rootCat() +} + +// isLeaf is true if the category is a mail, event, or contact category. +func (ec teamsChatsCategory) isLeaf() bool { + return ec == ec.leafCat() +} + +// pathValues transforms the two paths to maps of identified properties. +// +// Example: +// [tenantID, service, userID, category, chatID] +// => {teamsChat: chatID} +func (ec teamsChatsCategory) pathValues( + repo path.Path, + ent details.Entry, + cfg Config, +) (map[categorizer][]string, error) { + var itemCat categorizer + + switch ec { + case TeamsChatsChat: + itemCat = TeamsChatsChat + + default: + return nil, clues.New("bad Chat Category").With("category", ec) + } + + item := ent.ItemRef + if len(item) == 0 { + item = repo.Item() + } + + items := []string{ent.ShortRef, item} + + // only include the item ID when the user is NOT matching + // item names. TeamsChats data does not contain an item name, + // only an ID, and we don't want to mix up the two. + if cfg.OnlyMatchItemNames { + items = []string{ent.ShortRef} + } + + result := map[categorizer][]string{ + itemCat: items, + } + + return result, nil +} + +// pathKeys returns the path keys recognized by the receiver's leaf type. +func (ec teamsChatsCategory) pathKeys() []categorizer { + return teamsChatsLeafProperties[ec.leafCat()].pathKeys +} + +// PathType converts the category's leaf type into the matching path.CategoryType. +func (ec teamsChatsCategory) PathType() path.CategoryType { + return teamsChatsLeafProperties[ec.leafCat()].pathType +} + +// --------------------------------------------------------------------------- +// Scopes +// --------------------------------------------------------------------------- + +// TeamsChatsScope specifies the data available +// when interfacing with the TeamsChats service. +type TeamsChatsScope scope + +// interface compliance checks +var _ scoper = &TeamsChatsScope{} + +// Category describes the type of the data in scope. +func (s TeamsChatsScope) Category() teamsChatsCategory { + return teamsChatsCategory(getCategory(s)) +} + +// categorizer type is a generic wrapper around Category. +// Primarily used by scopes.go to for abstract comparisons. +func (s TeamsChatsScope) 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 TeamsChatsScope) Matches(cat teamsChatsCategory, target string) bool { + return matches(s, cat, target) +} + +// InfoCategory returns the category enum of the scope info. +// If the scope is not an info type, returns TeamsChatsUnknownCategory. +func (s TeamsChatsScope) InfoCategory() teamsChatsCategory { + return teamsChatsCategory(getInfoCategory(s)) +} + +// IncludeCategory checks whether the scope includes a certain category of data. +// Ex: to check if the scope includes mail data: +// s.IncludesCategory(selector.TeamsChatsMail) +func (s TeamsChatsScope) IncludesCategory(cat teamsChatsCategory) bool { + return categoryMatches(s.Category(), cat) +} + +// returns true if the category is included in the scope's data type, +// and the value is set to Any(). +func (s TeamsChatsScope) IsAny(cat teamsChatsCategory) bool { + return IsAnyTarget(s, cat) +} + +// Get returns the data category in the scope. If the scope +// contains all data types for a user, it'll return the +// TeamsChatsUser category. +func (s TeamsChatsScope) Get(cat teamsChatsCategory) []string { + return getCatValue(s, cat) +} + +// commenting out for now, but leaving in place; it's likely to return when we add filters +// // sets a value by category to the scope. Only intended for internal use. +// func (s TeamsChatsScope) set(cat teamsChatsCategory, v []string, opts ...option) TeamsChatsScope { +// return set(s, cat, v, opts...) +// } + +// setDefaults ensures that contact folder, mail folder, and user category +// scopes all express `AnyTgt` for their child category types. +func (s TeamsChatsScope) setDefaults() { + switch s.Category() { + case TeamsChatsUser: + s[TeamsChatsChat.String()] = passAny + } +} + +// --------------------------------------------------------------------------- +// Backup Details Filtering +// --------------------------------------------------------------------------- + +// Reduce filters the entries in a details struct to only those that match the +// inclusions, filters, and exclusions in the selector. +func (s teamsChats) Reduce( + ctx context.Context, + deets *details.Details, + errs *fault.Bus, +) *details.Details { + return reduce[TeamsChatsScope]( + ctx, + deets, + s.Selector, + map[path.CategoryType]teamsChatsCategory{ + path.ChatsCategory: TeamsChatsChat, + }, + errs) +} + +// matchesInfo handles the standard behavior when comparing a scope and an TeamsChatsInfo +// returns true if the scope and info match for the provided category. +func (s TeamsChatsScope) matchesInfo(dii details.ItemInfo) bool { + info := dii.TeamsChats + if info == nil { + return false + } + + infoCat := s.InfoCategory() + + cfpc := teamsChatsCategoryFromItemType(info.ItemType) + if !typeAndCategoryMatches(infoCat, cfpc) { + return false + } + + i := "" + + switch infoCat { + case TeamsChatsInfoChatMember: + i = strings.Join(info.Chat.Members, ",") + case TeamsChatsInfoChatName: + i = info.Chat.Name + } + + return s.Matches(infoCat, i) +} + +// teamsChatsCategoryFromItemType interprets the category represented by the TeamsChatsInfo +// struct. Since every TeamsChatsInfo can hold all teamsChats data info, the exact +// type that the struct represents must be compared using its ItemType prop. +func teamsChatsCategoryFromItemType(pct details.ItemType) teamsChatsCategory { + switch pct { + case details.TeamsChat: + return TeamsChatsChat + } + + return TeamsChatsCategoryUnknown +} diff --git a/src/pkg/selectors/teamsChats_test.go b/src/pkg/selectors/teamsChats_test.go new file mode 100644 index 0000000000..f3b695494e --- /dev/null +++ b/src/pkg/selectors/teamsChats_test.go @@ -0,0 +1,843 @@ +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" + + "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/filters" + "github.com/alcionai/corso/src/pkg/path" +) + +type TeamsChatsSelectorSuite struct { + tester.Suite +} + +func TestTeamsChatsSelectorSuite(t *testing.T) { + suite.Run(t, &TeamsChatsSelectorSuite{Suite: tester.NewUnitSuite(t)}) +} + +func (suite *TeamsChatsSelectorSuite) TestNewTeamsChatsBackup() { + t := suite.T() + eb := NewTeamsChatsBackup(nil) + assert.Equal(t, eb.Service, ServiceTeamsChats) + assert.NotZero(t, eb.Scopes()) +} + +func (suite *TeamsChatsSelectorSuite) TestToTeamsChatsBackup() { + t := suite.T() + eb := NewTeamsChatsBackup(nil) + s := eb.Selector + eb, err := s.ToTeamsChatsBackup() + require.NoError(t, err, clues.ToCore(err)) + assert.Equal(t, eb.Service, ServiceTeamsChats) + assert.NotZero(t, eb.Scopes()) +} + +func (suite *TeamsChatsSelectorSuite) TestNewTeamsChatsRestore() { + t := suite.T() + er := NewTeamsChatsRestore(nil) + assert.Equal(t, er.Service, ServiceTeamsChats) + assert.NotZero(t, er.Scopes()) +} + +func (suite *TeamsChatsSelectorSuite) TestToTeamsChatsRestore() { + t := suite.T() + eb := NewTeamsChatsRestore(nil) + s := eb.Selector + eb, err := s.ToTeamsChatsRestore() + require.NoError(t, err, clues.ToCore(err)) + assert.Equal(t, eb.Service, ServiceTeamsChats) + assert.NotZero(t, eb.Scopes()) +} + +func (suite *TeamsChatsSelectorSuite) TestTeamsChatsSelector_Exclude_TeamsChats() { + t := suite.T() + + const ( + user = "user" + folder = AnyTgt + c1 = "c1" + c2 = "c2" + ) + + sel := NewTeamsChatsBackup([]string{user}) + sel.Exclude(sel.Chats([]string{c1, c2})) + scopes := sel.Excludes + require.Len(t, scopes, 1) + + scopeMustHave( + t, + TeamsChatsScope(scopes[0]), + map[categorizer][]string{ + TeamsChatsChat: {c1, c2}, + }) +} + +func (suite *TeamsChatsSelectorSuite) TestTeamsChatsSelector_Include_TeamsChats() { + t := suite.T() + + const ( + user = "user" + folder = AnyTgt + c1 = "c1" + c2 = "c2" + ) + + sel := NewTeamsChatsBackup([]string{user}) + sel.Include(sel.Chats([]string{c1, c2})) + scopes := sel.Includes + require.Len(t, scopes, 1) + + scopeMustHave( + t, + TeamsChatsScope(scopes[0]), + map[categorizer][]string{ + TeamsChatsChat: {c1, c2}, + }) + + assert.Equal(t, sel.Scopes()[0].Category(), TeamsChatsChat) +} + +func (suite *TeamsChatsSelectorSuite) TestTeamsChatsSelector_Exclude_AllData() { + t := suite.T() + + const ( + u1 = "u1" + u2 = "u2" + ) + + sel := NewTeamsChatsBackup([]string{u1, u2}) + sel.Exclude(sel.AllData()) + scopes := sel.Excludes + require.Len(t, scopes, 1) + + for _, sc := range scopes { + if sc[scopeKeyCategory].Compare(TeamsChatsChat.String()) { + scopeMustHave( + t, + TeamsChatsScope(sc), + map[categorizer][]string{ + TeamsChatsChat: Any(), + }) + } + } +} + +func (suite *TeamsChatsSelectorSuite) TestTeamsChatsSelector_Include_AllData() { + t := suite.T() + + const ( + u1 = "u1" + u2 = "u2" + ) + + sel := NewTeamsChatsBackup([]string{u1, u2}) + sel.Include(sel.AllData()) + scopes := sel.Includes + require.Len(t, scopes, 1) + + for _, sc := range scopes { + if sc[scopeKeyCategory].Compare(TeamsChatsChat.String()) { + scopeMustHave( + t, + TeamsChatsScope(sc), + map[categorizer][]string{ + TeamsChatsChat: Any(), + }) + } + } +} + +func (suite *TeamsChatsSelectorSuite) TestTeamsChatsBackup_Scopes() { + eb := NewTeamsChatsBackup(Any()) + eb.Include(eb.AllData()) + + scopes := eb.Scopes() + assert.Len(suite.T(), scopes, 1) + + for _, sc := range scopes { + cat := sc.Category() + suite.Run(cat.String(), func() { + t := suite.T() + + switch sc.Category() { + case TeamsChatsChat: + assert.True(t, sc.IsAny(TeamsChatsChat)) + } + }) + } +} + +func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_Category() { + table := []struct { + is teamsChatsCategory + expect teamsChatsCategory + check assert.ComparisonAssertionFunc + }{ + {TeamsChatsCategoryUnknown, TeamsChatsCategoryUnknown, assert.Equal}, + {TeamsChatsCategoryUnknown, TeamsChatsUser, assert.NotEqual}, + {TeamsChatsChat, TeamsChatsChat, assert.Equal}, + {TeamsChatsUser, TeamsChatsUser, assert.Equal}, + {TeamsChatsUser, TeamsChatsCategoryUnknown, assert.NotEqual}, + } + for _, test := range table { + suite.Run(test.is.String()+test.expect.String(), func() { + eb := NewTeamsChatsBackup(Any()) + eb.Includes = []scope{ + {scopeKeyCategory: filters.Identity(test.is.String())}, + } + scope := eb.Scopes()[0] + test.check(suite.T(), test.expect, scope.Category()) + }) + } +} + +func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_IncludesCategory() { + table := []struct { + is teamsChatsCategory + expect teamsChatsCategory + check assert.BoolAssertionFunc + }{ + {TeamsChatsCategoryUnknown, TeamsChatsCategoryUnknown, assert.False}, + {TeamsChatsCategoryUnknown, TeamsChatsUser, assert.True}, + {TeamsChatsUser, TeamsChatsUser, assert.True}, + {TeamsChatsUser, TeamsChatsCategoryUnknown, assert.True}, + } + for _, test := range table { + suite.Run(test.is.String()+test.expect.String(), func() { + eb := NewTeamsChatsBackup(Any()) + eb.Includes = []scope{ + {scopeKeyCategory: filters.Identity(test.is.String())}, + } + scope := eb.Scopes()[0] + test.check(suite.T(), scope.IncludesCategory(test.expect)) + }) + } +} + +func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_Get() { + eb := NewTeamsChatsBackup(Any()) + eb.Include(eb.AllData()) + + scopes := eb.Scopes() + + table := []teamsChatsCategory{ + TeamsChatsChat, + } + for _, test := range table { + suite.Run(test.String(), func() { + t := suite.T() + + for _, sc := range scopes { + switch sc.Category() { + case TeamsChatsChat: + assert.Equal(t, Any(), sc.Get(TeamsChatsChat)) + } + assert.Equal(t, None(), sc.Get(TeamsChatsCategoryUnknown)) + } + }) + } +} + +func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_MatchesInfo() { + cs := NewTeamsChatsRestore(Any()) + + const ( + name = "smarf mcfnords" + member = "cooks@2many.smarf" + subject = "I have seen the fnords!" + ) + + var ( + now = time.Now() + future = now.Add(1 * time.Minute) + ) + + infoWith := func(itype details.ItemType) details.ItemInfo { + return details.ItemInfo{ + TeamsChats: &details.TeamsChatsInfo{ + ItemType: itype, + Chat: details.ChatInfo{ + CreatedAt: now, + HasExternalMembers: false, + LastMessageAt: future, + LastMessagePreview: "preview", + Members: []string{member}, + MessageCount: 1, + Name: name, + }, + }, + } + } + + table := []struct { + name string + itype details.ItemType + scope []TeamsChatsScope + expect assert.BoolAssertionFunc + }{ + {"chat with a different member", details.TeamsChat, cs.ChatMember("blarps"), assert.False}, + {"chat with the same member", details.TeamsChat, cs.ChatMember(member), assert.True}, + {"chat with a member submatch search", details.TeamsChat, cs.ChatMember(member[2:5]), assert.True}, + {"chat with a different name", details.TeamsChat, cs.ChatName("blarps"), assert.False}, + {"chat with the same name", details.TeamsChat, cs.ChatName(name), assert.True}, + {"chat with a subname search", details.TeamsChat, cs.ChatName(name[2:5]), assert.True}, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + scopes := setScopesToDefault(test.scope) + for _, scope := range scopes { + test.expect(t, scope.matchesInfo(infoWith(test.itype))) + } + }) + } +} + +func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_MatchesPath() { + const ( + user = "userID" + chat = "chatID" + ) + + repoRef, err := path.Build("tid", user, path.TeamsChatsService, path.ChatsCategory, true, chat) + require.NoError(suite.T(), err, clues.ToCore(err)) + + var ( + loc = strings.Join([]string{chat}, "/") + short = "thisisahashofsomekind" + cs = NewTeamsChatsRestore(Any()) + ent = details.Entry{ + RepoRef: repoRef.String(), + ShortRef: short, + ItemRef: chat, + LocationRef: loc, + } + ) + + table := []struct { + name string + scope []TeamsChatsScope + shortRef string + expect assert.BoolAssertionFunc + }{ + {"all items", cs.AllData(), "", assert.True}, + {"all chats", cs.Chats(Any()), "", assert.True}, + {"no chats", cs.Chats(None()), "", assert.False}, + {"matching chats", cs.Chats([]string{chat}), "", assert.True}, + {"non-matching chats", cs.Chats([]string{"smarf"}), "", assert.False}, + {"one of multiple chats", cs.Chats([]string{"smarf", chat}), "", assert.True}, + {"chats short ref", cs.Chats([]string{short}), short, assert.True}, + {"non-leaf short ref", cs.Chats([]string{"foo"}), short, assert.False}, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + scopes := setScopesToDefault(test.scope) + var aMatch bool + for _, scope := range scopes { + pvs, err := TeamsChatsChat.pathValues(repoRef, ent, Config{}) + require.NoError(t, err) + + if matchesPathValues(scope, TeamsChatsChat, pvs) { + aMatch = true + break + } + } + test.expect(t, aMatch) + }) + } +} + +func (suite *TeamsChatsSelectorSuite) TestTeamsChatsRestore_Reduce() { + chat, err := path.Build("tid", "uid", path.TeamsChatsService, path.ChatsCategory, true, "cid") + require.NoError(suite.T(), err, clues.ToCore(err)) + + toRR := func(p path.Path) string { + newElems := []string{} + + for _, e := range p.Folders() { + newElems = append(newElems, e+".d") + } + + joinedFldrs := strings.Join(newElems, "/") + + return stubRepoRef(p.Service(), p.Category(), p.ProtectedResource(), joinedFldrs, p.Item()) + } + + makeDeets := func(refs ...path.Path) *details.Details { + deets := &details.Details{ + DetailsModel: details.DetailsModel{ + Entries: []details.Entry{}, + }, + } + + for _, r := range refs { + itype := details.UnknownType + + switch r { + case chat: + itype = details.TeamsChat + } + + deets.Entries = append(deets.Entries, details.Entry{ + RepoRef: toRR(r), + // Don't escape because we assume nice paths. + LocationRef: r.Folder(false), + ItemInfo: details.ItemInfo{ + TeamsChats: &details.TeamsChatsInfo{ + ItemType: itype, + }, + }, + }) + } + + return deets + } + + table := []struct { + name string + deets *details.Details + makeSelector func() *TeamsChatsRestore + expect []string + }{ + { + "no refs", + makeDeets(), + func() *TeamsChatsRestore { + er := NewTeamsChatsRestore(Any()) + er.Include(er.AllData()) + return er + }, + []string{}, + }, + { + "chat only", + makeDeets(chat), + func() *TeamsChatsRestore { + er := NewTeamsChatsRestore(Any()) + er.Include(er.AllData()) + return er + }, + []string{toRR(chat)}, + }, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + defer flush() + + sel := test.makeSelector() + results := sel.Reduce(ctx, test.deets, fault.New(true)) + paths := results.Paths() + assert.Equal(t, test.expect, paths) + }) + } +} + +func (suite *TeamsChatsSelectorSuite) TestTeamsChatsRestore_Reduce_locationRef() { + var ( + chat = stubRepoRef(path.TeamsChatsService, path.ChatsCategory, "uid", "", "cid") + chatLocation = "chatname" + ) + + makeDeets := func(refs ...string) *details.Details { + deets := &details.Details{ + DetailsModel: details.DetailsModel{ + Entries: []details.Entry{}, + }, + } + + for _, r := range refs { + var ( + location string + itype = details.UnknownType + ) + + switch r { + case chat: + itype = details.TeamsChat + location = chatLocation + } + + deets.Entries = append(deets.Entries, details.Entry{ + RepoRef: r, + LocationRef: location, + ItemInfo: details.ItemInfo{ + TeamsChats: &details.TeamsChatsInfo{ + ItemType: itype, + }, + }, + }) + } + + return deets + } + + arr := func(s ...string) []string { + return s + } + + table := []struct { + name string + deets *details.Details + makeSelector func() *TeamsChatsRestore + expect []string + }{ + { + "no refs", + makeDeets(), + func() *TeamsChatsRestore { + er := NewTeamsChatsRestore(Any()) + er.Include(er.AllData()) + return er + }, + []string{}, + }, + { + "chat only", + makeDeets(chat), + func() *TeamsChatsRestore { + er := NewTeamsChatsRestore(Any()) + er.Include(er.AllData()) + return er + }, + arr(chat), + }, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + defer flush() + + sel := test.makeSelector() + results := sel.Reduce(ctx, test.deets, fault.New(true)) + paths := results.Paths() + assert.Equal(t, test.expect, paths) + }) + } +} + +func (suite *TeamsChatsSelectorSuite) TestScopesByCategory() { + var ( + cs = NewTeamsChatsRestore(Any()) + teamsChats = cs.Chats(Any()) + ) + + type expect struct { + chat int + } + + type input []scope + + makeInput := func(cs ...[]TeamsChatsScope) []scope { + mss := []scope{} + + for _, sl := range cs { + for _, s := range sl { + mss = append(mss, scope(s)) + } + } + + return mss + } + cats := map[path.CategoryType]teamsChatsCategory{ + path.ChatsCategory: TeamsChatsChat, + } + + table := []struct { + name string + scopes input + expect expect + }{ + {"teamsChats only", makeInput(teamsChats), expect{1}}, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + result := scopesByCategory[TeamsChatsScope](test.scopes, cats, false) + assert.Len(t, result[TeamsChatsChat], test.expect.chat) + }) + } +} + +func (suite *TeamsChatsSelectorSuite) TestPasses() { + const ( + chatID = "chatID" + cat = TeamsChatsChat + ) + + short := "thisisahashofsomekind" + entry := details.Entry{ + ShortRef: short, + ItemRef: chatID, + } + + repoRef, err := path.Build("tid", "user", path.TeamsChatsService, path.ChatsCategory, true, chatID) + require.NoError(suite.T(), err, clues.ToCore(err)) + + var ( + cs = NewTeamsChatsRestore(Any()) + otherChat = setScopesToDefault(cs.Chats([]string{"smarf"})) + chat = setScopesToDefault(cs.Chats([]string{chatID})) + noChat = setScopesToDefault(cs.Chats(None())) + allChats = setScopesToDefault(cs.Chats(Any())) + ent = details.Entry{ + RepoRef: repoRef.String(), + } + ) + + table := []struct { + name string + excludes, filters, includes []TeamsChatsScope + expect assert.BoolAssertionFunc + }{ + {"empty", nil, nil, nil, assert.False}, + {"in Chat", nil, nil, chat, assert.True}, + {"in Other", nil, nil, otherChat, assert.False}, + {"in no Chat", nil, nil, noChat, assert.False}, + {"ex None filter chat", allChats, chat, nil, assert.False}, + {"ex Chat", chat, nil, allChats, assert.False}, + {"ex Other", otherChat, nil, allChats, assert.True}, + {"in and ex Chat", chat, nil, chat, assert.False}, + {"filter Chat", nil, chat, allChats, assert.True}, + {"filter Other", nil, otherChat, allChats, assert.False}, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + pvs, err := cat.pathValues(repoRef, ent, Config{}) + require.NoError(t, err) + + result := passes( + cat, + pvs, + entry, + test.excludes, + test.filters, + test.includes) + test.expect(t, result) + }) + } +} + +func (suite *TeamsChatsSelectorSuite) TestContains() { + target := "fnords" + + var ( + cs = NewTeamsChatsRestore(Any()) + noChat = setScopesToDefault(cs.Chats(None())) + does = setScopesToDefault(cs.Chats([]string{target})) + doesNot = setScopesToDefault(cs.Chats([]string{"smarf"})) + ) + + table := []struct { + name string + scopes []TeamsChatsScope + expect assert.BoolAssertionFunc + }{ + {"no chat", noChat, assert.False}, + {"does contain", does, assert.True}, + {"does not contain", doesNot, assert.False}, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + var result bool + for _, scope := range test.scopes { + if scope.Matches(TeamsChatsChat, target) { + result = true + break + } + } + test.expect(t, result) + }) + } +} + +func (suite *TeamsChatsSelectorSuite) TestIsAny() { + var ( + cs = NewTeamsChatsRestore(Any()) + specificChat = setScopesToDefault(cs.Chats([]string{"chat"})) + anyChat = setScopesToDefault(cs.Chats(Any())) + ) + + table := []struct { + name string + scopes []TeamsChatsScope + cat teamsChatsCategory + expect assert.BoolAssertionFunc + }{ + {"specific chat", specificChat, TeamsChatsChat, assert.False}, + {"any chat", anyChat, TeamsChatsChat, assert.True}, + {"wrong category", anyChat, TeamsChatsUser, assert.False}, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + var result bool + for _, scope := range test.scopes { + if scope.IsAny(test.cat) { + result = true + break + } + } + test.expect(t, result) + }) + } +} + +func (suite *TeamsChatsSelectorSuite) TestTeamsChatsCategory_leafCat() { + table := []struct { + cat teamsChatsCategory + expect teamsChatsCategory + }{ + {teamsChatsCategory("foo"), teamsChatsCategory("foo")}, + {TeamsChatsCategoryUnknown, TeamsChatsCategoryUnknown}, + {TeamsChatsUser, TeamsChatsUser}, + {TeamsChatsChat, TeamsChatsChat}, + } + for _, test := range table { + suite.Run(test.cat.String(), func() { + assert.Equal(suite.T(), test.expect, test.cat.leafCat(), test.cat.String()) + }) + } +} + +func (suite *TeamsChatsSelectorSuite) TestTeamsChatsCategory_PathValues() { + t := suite.T() + + chatPath, err := path.Build("tid", "u", path.TeamsChatsService, path.ChatsCategory, true, "chatitem.d") + require.NoError(t, err, clues.ToCore(err)) + + chatLoc, err := path.Build("tid", "u", path.TeamsChatsService, path.ChatsCategory, true, "chatitem") + require.NoError(t, err, clues.ToCore(err)) + + var ( + chatMap = map[categorizer][]string{ + TeamsChatsChat: {chatPath.Item(), "chat-short"}, + } + chatOnlyNameMap = map[categorizer][]string{ + TeamsChatsChat: {"chat-short"}, + } + ) + + table := []struct { + cat teamsChatsCategory + path path.Path + loc path.Path + short string + expect map[categorizer][]string + expectOnlyName map[categorizer][]string + }{ + {TeamsChatsChat, chatPath, chatLoc, "chat-short", chatMap, chatOnlyNameMap}, + } + for _, test := range table { + suite.Run(string(test.cat), func() { + t := suite.T() + ent := details.Entry{ + RepoRef: test.path.String(), + ShortRef: test.short, + LocationRef: test.loc.Folder(true), + ItemRef: test.path.Item(), + } + + pvs, err := test.cat.pathValues(test.path, ent, Config{}) + require.NoError(t, err) + + for k := range test.expect { + assert.ElementsMatch(t, test.expect[k], pvs[k]) + } + + pvs, err = test.cat.pathValues(test.path, ent, Config{OnlyMatchItemNames: true}) + require.NoError(t, err) + + for k := range test.expectOnlyName { + assert.ElementsMatch(t, test.expectOnlyName[k], pvs[k], k) + } + }) + } +} + +func (suite *TeamsChatsSelectorSuite) TestTeamsChatsCategory_PathKeys() { + chat := []categorizer{TeamsChatsChat} + user := []categorizer{TeamsChatsUser} + + var empty []categorizer + + table := []struct { + cat teamsChatsCategory + expect []categorizer + }{ + {TeamsChatsCategoryUnknown, empty}, + {TeamsChatsChat, chat}, + {TeamsChatsUser, user}, + } + for _, test := range table { + suite.Run(string(test.cat), func() { + assert.Equal(suite.T(), test.cat.pathKeys(), test.expect) + }) + } +} + +func (suite *TeamsChatsSelectorSuite) TestCategoryFromItemType() { + table := []struct { + name string + input details.ItemType + expect teamsChatsCategory + }{ + { + name: "chat", + input: details.TeamsChat, + expect: TeamsChatsChat, + }, + { + name: "unknown", + input: details.UnknownType, + expect: TeamsChatsCategoryUnknown, + }, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + result := teamsChatsCategoryFromItemType(test.input) + assert.Equal(t, test.expect, result) + }) + } +} + +func (suite *TeamsChatsSelectorSuite) TestCategory_PathType() { + table := []struct { + cat teamsChatsCategory + pathType path.CategoryType + }{ + {TeamsChatsCategoryUnknown, path.UnknownCategory}, + {TeamsChatsChat, path.ChatsCategory}, + {TeamsChatsUser, path.UnknownCategory}, + } + for _, test := range table { + suite.Run(test.cat.String(), func() { + assert.Equal(suite.T(), test.pathType, test.cat.PathType()) + }) + } +}