add chats service and category to paths (#5065)
introduces the Chats service and Chats category --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🌻 Feature #### Issue(s) * #5061 #### Test Plan - [x] ⚡ Unit test - [x] 💚 E2E
This commit is contained in:
parent
8ac7e6caa2
commit
8e6a47b103
@ -112,8 +112,6 @@ func (i *ExchangeInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *ExchangeInfo) updateFolder(f *FolderInfo) error {
|
func (i *ExchangeInfo) updateFolder(f *FolderInfo) error {
|
||||||
// Use a switch instead of a rather large if-statement. Just make sure it's an
|
|
||||||
// Exchange type. If it's not return an error.
|
|
||||||
switch i.ItemType {
|
switch i.ItemType {
|
||||||
case ExchangeContact, ExchangeEvent, ExchangeMail:
|
case ExchangeContact, ExchangeEvent, ExchangeMail:
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -79,7 +79,7 @@ type ChannelMessageInfo struct {
|
|||||||
Subject string `json:"subject,omitempty"`
|
Subject string `json:"subject,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Headers returns the human-readable names of properties in a SharePointInfo
|
// Headers returns the human-readable names of properties in a groupsInfo
|
||||||
// for printing out to a terminal in a columnar display.
|
// for printing out to a terminal in a columnar display.
|
||||||
func (i GroupsInfo) Headers() []string {
|
func (i GroupsInfo) Headers() []string {
|
||||||
switch i.ItemType {
|
switch i.ItemType {
|
||||||
|
|||||||
@ -41,6 +41,9 @@ const (
|
|||||||
// Groups/Teams(40x)
|
// Groups/Teams(40x)
|
||||||
GroupsChannelMessage ItemType = 401
|
GroupsChannelMessage ItemType = 401
|
||||||
GroupsConversationPost ItemType = 402
|
GroupsConversationPost ItemType = 402
|
||||||
|
|
||||||
|
// Teams Chat
|
||||||
|
TeamsChat ItemType = 501
|
||||||
)
|
)
|
||||||
|
|
||||||
func UpdateItem(item *ItemInfo, newLocPath *path.Builder) {
|
func UpdateItem(item *ItemInfo, newLocPath *path.Builder) {
|
||||||
@ -73,6 +76,7 @@ type ItemInfo struct {
|
|||||||
SharePoint *SharePointInfo `json:"sharePoint,omitempty"`
|
SharePoint *SharePointInfo `json:"sharePoint,omitempty"`
|
||||||
OneDrive *OneDriveInfo `json:"oneDrive,omitempty"`
|
OneDrive *OneDriveInfo `json:"oneDrive,omitempty"`
|
||||||
Groups *GroupsInfo `json:"groups,omitempty"`
|
Groups *GroupsInfo `json:"groups,omitempty"`
|
||||||
|
TeamsChats *TeamsChatsInfo `json:"teamsChats,omitempty"`
|
||||||
// Optional item extension data
|
// Optional item extension data
|
||||||
Extension *ExtensionData `json:"extension,omitempty"`
|
Extension *ExtensionData `json:"extension,omitempty"`
|
||||||
}
|
}
|
||||||
@ -99,6 +103,9 @@ func (i ItemInfo) infoType() ItemType {
|
|||||||
|
|
||||||
case i.Groups != nil:
|
case i.Groups != nil:
|
||||||
return i.Groups.ItemType
|
return i.Groups.ItemType
|
||||||
|
|
||||||
|
case i.TeamsChats != nil:
|
||||||
|
return i.TeamsChats.ItemType
|
||||||
}
|
}
|
||||||
|
|
||||||
return UnknownType
|
return UnknownType
|
||||||
@ -120,6 +127,9 @@ func (i ItemInfo) size() int64 {
|
|||||||
|
|
||||||
case i.Folder != nil:
|
case i.Folder != nil:
|
||||||
return i.Folder.Size
|
return i.Folder.Size
|
||||||
|
|
||||||
|
case i.TeamsChats != nil:
|
||||||
|
return int64(i.TeamsChats.Chat.MessageCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
@ -141,6 +151,9 @@ func (i ItemInfo) Modified() time.Time {
|
|||||||
|
|
||||||
case i.Folder != nil:
|
case i.Folder != nil:
|
||||||
return i.Folder.Modified
|
return i.Folder.Modified
|
||||||
|
|
||||||
|
case i.TeamsChats != nil:
|
||||||
|
return i.TeamsChats.Modified
|
||||||
}
|
}
|
||||||
|
|
||||||
return time.Time{}
|
return time.Time{}
|
||||||
@ -160,6 +173,9 @@ func (i ItemInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) {
|
|||||||
case i.Groups != nil:
|
case i.Groups != nil:
|
||||||
return i.Groups.uniqueLocation(baseLoc)
|
return i.Groups.uniqueLocation(baseLoc)
|
||||||
|
|
||||||
|
case i.TeamsChats != nil:
|
||||||
|
return i.TeamsChats.uniqueLocation(baseLoc)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, clues.New("unsupported type")
|
return nil, clues.New("unsupported type")
|
||||||
}
|
}
|
||||||
@ -179,6 +195,9 @@ func (i ItemInfo) updateFolder(f *FolderInfo) error {
|
|||||||
case i.Groups != nil:
|
case i.Groups != nil:
|
||||||
return i.Groups.updateFolder(f)
|
return i.Groups.updateFolder(f)
|
||||||
|
|
||||||
|
case i.TeamsChats != nil:
|
||||||
|
return i.TeamsChats.updateFolder(f)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return clues.New("unsupported type")
|
return clues.New("unsupported type")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,12 +71,21 @@ func (suite *ItemInfoUnitSuite) TestItemInfo_IsDriveItem() {
|
|||||||
{
|
{
|
||||||
name: "exchange anything",
|
name: "exchange anything",
|
||||||
ii: ItemInfo{
|
ii: ItemInfo{
|
||||||
Groups: &GroupsInfo{
|
Exchange: &ExchangeInfo{
|
||||||
ItemType: ExchangeMail,
|
ItemType: ExchangeMail,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expect: assert.False,
|
expect: assert.False,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "teams chat",
|
||||||
|
ii: ItemInfo{
|
||||||
|
TeamsChats: &TeamsChatsInfo{
|
||||||
|
ItemType: TeamsChat,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: assert.False,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
suite.Run(test.name, func() {
|
suite.Run(test.name, func() {
|
||||||
|
|||||||
118
src/pkg/backup/details/teamsChats.go
Normal file
118
src/pkg/backup/details/teamsChats.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package details
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/pkg/dttm"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewChatsLocationIDer builds a LocationIDer for the chats.
|
||||||
|
func NewChatsLocationIDer(
|
||||||
|
category path.CategoryType,
|
||||||
|
escapedFolders ...string,
|
||||||
|
) (uniqueLoc, error) {
|
||||||
|
if err := path.ValidateServiceAndCategory(path.TeamsChatsService, category); err != nil {
|
||||||
|
return uniqueLoc{}, clues.Wrap(err, "making chats LocationIDer")
|
||||||
|
}
|
||||||
|
|
||||||
|
pb := path.Builder{}.Append(category.String()).Append(escapedFolders...)
|
||||||
|
|
||||||
|
return uniqueLoc{
|
||||||
|
pb: pb,
|
||||||
|
prefixElems: 1,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamsChatsInfo describes a chat within teams chats.
|
||||||
|
type TeamsChatsInfo struct {
|
||||||
|
ItemType ItemType `json:"itemType,omitempty"`
|
||||||
|
Modified time.Time `json:"modified,omitempty"`
|
||||||
|
ParentPath string `json:"parentPath,omitempty"`
|
||||||
|
|
||||||
|
Chat ChatInfo `json:"chat,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatInfo struct {
|
||||||
|
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||||
|
HasExternalMembers bool `json:"hasExternalMembers,omitempty"`
|
||||||
|
LastMessageAt time.Time `json:"lastMessageAt,omitempty"`
|
||||||
|
LastMessagePreview string `json:"preview,omitempty"`
|
||||||
|
Members []string `json:"members,omitempty"`
|
||||||
|
MessageCount int `json:"messageCount,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers returns the human-readable names of properties in a ChatsInfo
|
||||||
|
// for printing out to a terminal in a columnar display.
|
||||||
|
func (i TeamsChatsInfo) Headers() []string {
|
||||||
|
switch i.ItemType {
|
||||||
|
case TeamsChat:
|
||||||
|
return []string{"Name", "Last message", "Last message at", "Message count", "Created", "Members"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns the values matching the Headers list for printing
|
||||||
|
// out to a terminal in a columnar display.
|
||||||
|
func (i TeamsChatsInfo) Values() []string {
|
||||||
|
switch i.ItemType {
|
||||||
|
case TeamsChat:
|
||||||
|
members := ""
|
||||||
|
icmLen := len(i.Chat.Members)
|
||||||
|
|
||||||
|
if icmLen > 0 {
|
||||||
|
members = i.Chat.Members[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if icmLen > 1 {
|
||||||
|
members = fmt.Sprintf("%s, and %d more", members, icmLen-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{
|
||||||
|
i.Chat.Name,
|
||||||
|
i.Chat.LastMessagePreview,
|
||||||
|
dttm.FormatToTabularDisplay(i.Chat.LastMessageAt),
|
||||||
|
strconv.Itoa(i.Chat.MessageCount),
|
||||||
|
dttm.FormatToTabularDisplay(i.Chat.CreatedAt),
|
||||||
|
members,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *TeamsChatsInfo) UpdateParentPath(newLocPath *path.Builder) {
|
||||||
|
i.ParentPath = newLocPath.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *TeamsChatsInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) {
|
||||||
|
var category path.CategoryType
|
||||||
|
|
||||||
|
switch i.ItemType {
|
||||||
|
case TeamsChat:
|
||||||
|
category = path.ChatsCategory
|
||||||
|
}
|
||||||
|
|
||||||
|
loc, err := NewChatsLocationIDer(category, baseLoc.Elements()...)
|
||||||
|
|
||||||
|
return &loc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *TeamsChatsInfo) updateFolder(f *FolderInfo) error {
|
||||||
|
switch i.ItemType {
|
||||||
|
case TeamsChat:
|
||||||
|
default:
|
||||||
|
return clues.New("unsupported non-Chats ItemType").
|
||||||
|
With("item_type", i.ItemType)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.DataType = i.ItemType
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
71
src/pkg/backup/details/teamsChats_test.go
Normal file
71
src/pkg/backup/details/teamsChats_test.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package details_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"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/dttm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChatsUnitSuite struct {
|
||||||
|
tester.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChatsUnitSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &ChatsUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ChatsUnitSuite) TestChatsPrintable() {
|
||||||
|
now := time.Now()
|
||||||
|
then := now.Add(time.Minute)
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
info details.TeamsChatsInfo
|
||||||
|
expectHs []string
|
||||||
|
expectVs []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "channel message",
|
||||||
|
info: details.TeamsChatsInfo{
|
||||||
|
ItemType: details.TeamsChat,
|
||||||
|
ParentPath: "parentpath",
|
||||||
|
Chat: details.ChatInfo{
|
||||||
|
CreatedAt: now,
|
||||||
|
HasExternalMembers: true,
|
||||||
|
LastMessageAt: then,
|
||||||
|
LastMessagePreview: "last message preview",
|
||||||
|
Members: []string{"foo@bar.baz", "fnords@smarf.zoomba"},
|
||||||
|
MessageCount: 42,
|
||||||
|
Name: "chat name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectHs: []string{"Name", "Last message", "Last message at", "Message count", "Created", "Members"},
|
||||||
|
expectVs: []string{
|
||||||
|
"chat name",
|
||||||
|
"last message preview",
|
||||||
|
dttm.FormatToTabularDisplay(then),
|
||||||
|
"42",
|
||||||
|
dttm.FormatToTabularDisplay(now),
|
||||||
|
"foo@bar.baz, and 1 more",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
hs := test.info.Headers()
|
||||||
|
vs := test.info.Values()
|
||||||
|
|
||||||
|
assert.Equal(t, len(hs), len(vs))
|
||||||
|
assert.Equal(t, test.expectHs, hs)
|
||||||
|
assert.Equal(t, test.expectVs, vs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -28,6 +28,7 @@ const (
|
|||||||
DetailsCategory CategoryType = 8 // details
|
DetailsCategory CategoryType = 8 // details
|
||||||
ChannelMessagesCategory CategoryType = 9 // channelMessages
|
ChannelMessagesCategory CategoryType = 9 // channelMessages
|
||||||
ConversationPostsCategory CategoryType = 10 // conversationPosts
|
ConversationPostsCategory CategoryType = 10 // conversationPosts
|
||||||
|
ChatsCategory CategoryType = 11 // chats
|
||||||
)
|
)
|
||||||
|
|
||||||
var strToCat = map[string]CategoryType{
|
var strToCat = map[string]CategoryType{
|
||||||
@ -41,6 +42,7 @@ var strToCat = map[string]CategoryType{
|
|||||||
strings.ToLower(DetailsCategory.String()): DetailsCategory,
|
strings.ToLower(DetailsCategory.String()): DetailsCategory,
|
||||||
strings.ToLower(ChannelMessagesCategory.String()): ChannelMessagesCategory,
|
strings.ToLower(ChannelMessagesCategory.String()): ChannelMessagesCategory,
|
||||||
strings.ToLower(ConversationPostsCategory.String()): ConversationPostsCategory,
|
strings.ToLower(ConversationPostsCategory.String()): ConversationPostsCategory,
|
||||||
|
strings.ToLower(ChatsCategory.String()): ChatsCategory,
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToCategoryType(s string) CategoryType {
|
func ToCategoryType(s string) CategoryType {
|
||||||
@ -63,6 +65,7 @@ var catToHuman = map[CategoryType]string{
|
|||||||
DetailsCategory: "Details",
|
DetailsCategory: "Details",
|
||||||
ChannelMessagesCategory: "Messages",
|
ChannelMessagesCategory: "Messages",
|
||||||
ConversationPostsCategory: "Posts",
|
ConversationPostsCategory: "Posts",
|
||||||
|
ChatsCategory: "Chats",
|
||||||
}
|
}
|
||||||
|
|
||||||
// HumanString produces a more human-readable string version of the category.
|
// HumanString produces a more human-readable string version of the category.
|
||||||
@ -100,6 +103,9 @@ var serviceCategories = map[ServiceType]map[CategoryType]struct{}{
|
|||||||
ConversationPostsCategory: {},
|
ConversationPostsCategory: {},
|
||||||
LibrariesCategory: {},
|
LibrariesCategory: {},
|
||||||
},
|
},
|
||||||
|
TeamsChatsService: {
|
||||||
|
ChatsCategory: {},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateServiceAndCategoryStrings(s, c string) (ServiceType, CategoryType, error) {
|
func validateServiceAndCategoryStrings(s, c string) (ServiceType, CategoryType, error) {
|
||||||
|
|||||||
@ -35,6 +35,7 @@ func (suite *CategoryTypeUnitSuite) TestToCategoryType() {
|
|||||||
{input: "details", expect: 8},
|
{input: "details", expect: 8},
|
||||||
{input: "channelmessages", expect: 9},
|
{input: "channelmessages", expect: 9},
|
||||||
{input: "conversationposts", expect: 10},
|
{input: "conversationposts", expect: 10},
|
||||||
|
{input: "chats", expect: 11},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
suite.Run(test.input, func() {
|
suite.Run(test.input, func() {
|
||||||
@ -62,6 +63,7 @@ func (suite *CategoryTypeUnitSuite) TestHumanString() {
|
|||||||
{input: 8, expect: "Details"},
|
{input: 8, expect: "Details"},
|
||||||
{input: 9, expect: "Messages"},
|
{input: 9, expect: "Messages"},
|
||||||
{input: 10, expect: "Posts"},
|
{input: 10, expect: "Posts"},
|
||||||
|
{input: 11, expect: "Chats"},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
suite.Run(test.input.String(), func() {
|
suite.Run(test.input.String(), func() {
|
||||||
|
|||||||
@ -19,11 +19,12 @@ func _() {
|
|||||||
_ = x[DetailsCategory-8]
|
_ = x[DetailsCategory-8]
|
||||||
_ = x[ChannelMessagesCategory-9]
|
_ = x[ChannelMessagesCategory-9]
|
||||||
_ = x[ConversationPostsCategory-10]
|
_ = x[ConversationPostsCategory-10]
|
||||||
|
_ = x[ChatsCategory-11]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _CategoryType_name = "UnknownCategoryemailcontactseventsfileslistslibrariespagesdetailschannelMessagesconversationPosts"
|
const _CategoryType_name = "UnknownCategoryemailcontactseventsfileslistslibrariespagesdetailschannelMessagesconversationPostschats"
|
||||||
|
|
||||||
var _CategoryType_index = [...]uint8{0, 15, 20, 28, 34, 39, 44, 53, 58, 65, 80, 97}
|
var _CategoryType_index = [...]uint8{0, 15, 20, 28, 34, 39, 44, 53, 58, 65, 80, 97, 102}
|
||||||
|
|
||||||
func (i CategoryType) String() string {
|
func (i CategoryType) String() string {
|
||||||
if i < 0 || i >= CategoryType(len(_CategoryType_index)-1) {
|
if i < 0 || i >= CategoryType(len(_CategoryType_index)-1) {
|
||||||
|
|||||||
@ -118,6 +118,14 @@ func (suite *ServiceCategoryUnitSuite) TestValidateServiceAndCategory() {
|
|||||||
expectedCategory: LibrariesCategory,
|
expectedCategory: LibrariesCategory,
|
||||||
check: assert.NoError,
|
check: assert.NoError,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ChatsChats",
|
||||||
|
service: TeamsChatsService.String(),
|
||||||
|
category: ChatsCategory.String(),
|
||||||
|
expectedService: TeamsChatsService,
|
||||||
|
expectedCategory: ChatsCategory,
|
||||||
|
check: assert.NoError,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
suite.Run(test.name, func() {
|
suite.Run(test.name, func() {
|
||||||
|
|||||||
@ -23,39 +23,38 @@ type ServiceType int
|
|||||||
//go:generate stringer -type=ServiceType -linecomment
|
//go:generate stringer -type=ServiceType -linecomment
|
||||||
const (
|
const (
|
||||||
UnknownService ServiceType = 0
|
UnknownService ServiceType = 0
|
||||||
ExchangeService ServiceType = 1 // exchange
|
ExchangeService ServiceType = 1 // exchange
|
||||||
OneDriveService ServiceType = 2 // onedrive
|
OneDriveService ServiceType = 2 // onedrive
|
||||||
SharePointService ServiceType = 3 // sharepoint
|
SharePointService ServiceType = 3 // sharepoint
|
||||||
ExchangeMetadataService ServiceType = 4 // exchangeMetadata
|
ExchangeMetadataService ServiceType = 4 // exchangeMetadata
|
||||||
OneDriveMetadataService ServiceType = 5 // onedriveMetadata
|
OneDriveMetadataService ServiceType = 5 // onedriveMetadata
|
||||||
SharePointMetadataService ServiceType = 6 // sharepointMetadata
|
SharePointMetadataService ServiceType = 6 // sharepointMetadata
|
||||||
GroupsService ServiceType = 7 // groups
|
GroupsService ServiceType = 7 // groups
|
||||||
GroupsMetadataService ServiceType = 8 // groupsMetadata
|
GroupsMetadataService ServiceType = 8 // groupsMetadata
|
||||||
|
TeamsChatsService ServiceType = 9 // teamsChats
|
||||||
|
TeamsChatsMetadataService ServiceType = 10 // teamsChatsMetadata
|
||||||
)
|
)
|
||||||
|
|
||||||
func ToServiceType(service string) ServiceType {
|
var strToSvc = map[string]ServiceType{
|
||||||
s := strings.ToLower(service)
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
switch s {
|
func ToServiceType(service string) ServiceType {
|
||||||
case strings.ToLower(ExchangeService.String()):
|
st, ok := strToSvc[strings.ToLower(service)]
|
||||||
return ExchangeService
|
if !ok {
|
||||||
case strings.ToLower(OneDriveService.String()):
|
st = UnknownService
|
||||||
return OneDriveService
|
|
||||||
case strings.ToLower(SharePointService.String()):
|
|
||||||
return SharePointService
|
|
||||||
case strings.ToLower(GroupsService.String()):
|
|
||||||
return GroupsService
|
|
||||||
case strings.ToLower(ExchangeMetadataService.String()):
|
|
||||||
return ExchangeMetadataService
|
|
||||||
case strings.ToLower(OneDriveMetadataService.String()):
|
|
||||||
return OneDriveMetadataService
|
|
||||||
case strings.ToLower(SharePointMetadataService.String()):
|
|
||||||
return SharePointMetadataService
|
|
||||||
case strings.ToLower(GroupsMetadataService.String()):
|
|
||||||
return GroupsMetadataService
|
|
||||||
default:
|
|
||||||
return UnknownService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return st
|
||||||
}
|
}
|
||||||
|
|
||||||
var serviceToHuman = map[ServiceType]string{
|
var serviceToHuman = map[ServiceType]string{
|
||||||
@ -63,6 +62,7 @@ var serviceToHuman = map[ServiceType]string{
|
|||||||
OneDriveService: "OneDrive",
|
OneDriveService: "OneDrive",
|
||||||
SharePointService: "SharePoint",
|
SharePointService: "SharePoint",
|
||||||
GroupsService: "Groups",
|
GroupsService: "Groups",
|
||||||
|
TeamsChatsService: "Chats",
|
||||||
}
|
}
|
||||||
|
|
||||||
// HumanString produces a more human-readable string version of the service.
|
// HumanString produces a more human-readable string version of the service.
|
||||||
@ -78,26 +78,19 @@ func (svc ServiceType) HumanString() string {
|
|||||||
func (svc ServiceType) ToMetadata() ServiceType {
|
func (svc ServiceType) ToMetadata() ServiceType {
|
||||||
//exhaustive:enforce
|
//exhaustive:enforce
|
||||||
switch svc {
|
switch svc {
|
||||||
case ExchangeService:
|
case ExchangeService, ExchangeMetadataService:
|
||||||
return ExchangeMetadataService
|
return ExchangeMetadataService
|
||||||
case OneDriveService:
|
case OneDriveService, OneDriveMetadataService:
|
||||||
return OneDriveMetadataService
|
return OneDriveMetadataService
|
||||||
case SharePointService:
|
case SharePointService, SharePointMetadataService:
|
||||||
return SharePointMetadataService
|
return SharePointMetadataService
|
||||||
case GroupsService:
|
case GroupsService, GroupsMetadataService:
|
||||||
return GroupsMetadataService
|
return GroupsMetadataService
|
||||||
|
case TeamsChatsService, TeamsChatsMetadataService:
|
||||||
case ExchangeMetadataService:
|
return TeamsChatsMetadataService
|
||||||
fallthrough
|
|
||||||
case OneDriveMetadataService:
|
|
||||||
fallthrough
|
|
||||||
case SharePointMetadataService:
|
|
||||||
fallthrough
|
|
||||||
case GroupsMetadataService:
|
|
||||||
fallthrough
|
|
||||||
case UnknownService:
|
case UnknownService:
|
||||||
return svc
|
fallthrough
|
||||||
|
default:
|
||||||
|
return UnknownService
|
||||||
}
|
}
|
||||||
|
|
||||||
return UnknownService
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,11 +17,13 @@ func _() {
|
|||||||
_ = x[SharePointMetadataService-6]
|
_ = x[SharePointMetadataService-6]
|
||||||
_ = x[GroupsService-7]
|
_ = x[GroupsService-7]
|
||||||
_ = x[GroupsMetadataService-8]
|
_ = x[GroupsMetadataService-8]
|
||||||
|
_ = x[TeamsChatsService-9]
|
||||||
|
_ = x[TeamsChatsMetadataService-10]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _ServiceType_name = "UnknownServiceexchangeonedrivesharepointexchangeMetadataonedriveMetadatasharepointMetadatagroupsgroupsMetadata"
|
const _ServiceType_name = "UnknownServiceexchangeonedrivesharepointexchangeMetadataonedriveMetadatasharepointMetadatagroupsgroupsMetadatachatschatsMetadata"
|
||||||
|
|
||||||
var _ServiceType_index = [...]uint8{0, 14, 22, 30, 40, 56, 72, 90, 96, 110}
|
var _ServiceType_index = [...]uint8{0, 14, 22, 30, 40, 56, 72, 90, 96, 110, 115, 128}
|
||||||
|
|
||||||
func (i ServiceType) String() string {
|
func (i ServiceType) String() string {
|
||||||
if i < 0 || i >= ServiceType(len(_ServiceType_index)-1) {
|
if i < 0 || i >= ServiceType(len(_ServiceType_index)-1) {
|
||||||
|
|||||||
@ -591,7 +591,7 @@ func (ec exchangeCategory) isLeaf() bool {
|
|||||||
// pathValues transforms the two paths to maps of identified properties.
|
// pathValues transforms the two paths to maps of identified properties.
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
// [tenantID, service, userPN, category, mailFolder, mailID]
|
// [tenantID, service, userID, category, mailFolder, mailID]
|
||||||
// => {exchMailFolder: mailFolder, exchMail: mailID}
|
// => {exchMailFolder: mailFolder, exchMail: mailID}
|
||||||
func (ec exchangeCategory) pathValues(
|
func (ec exchangeCategory) pathValues(
|
||||||
repo path.Path,
|
repo path.Path,
|
||||||
@ -772,7 +772,7 @@ func (s ExchangeScope) matchesInfo(dii details.ItemInfo) bool {
|
|||||||
|
|
||||||
infoCat := s.InfoCategory()
|
infoCat := s.InfoCategory()
|
||||||
|
|
||||||
cfpc := categoryFromItemType(info.ItemType)
|
cfpc := exchangeCategoryFromItemType(info.ItemType)
|
||||||
if !typeAndCategoryMatches(infoCat, cfpc) {
|
if !typeAndCategoryMatches(infoCat, cfpc) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -801,10 +801,10 @@ func (s ExchangeScope) matchesInfo(dii details.ItemInfo) bool {
|
|||||||
return s.Matches(infoCat, i)
|
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
|
// struct. Since every ExchangeInfo can hold all exchange data info, the exact
|
||||||
// type that the struct represents must be compared using its ItemType prop.
|
// 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 {
|
switch pct {
|
||||||
case details.ExchangeContact:
|
case details.ExchangeContact:
|
||||||
return ExchangeContact
|
return ExchangeContact
|
||||||
|
|||||||
@ -1602,7 +1602,7 @@ func (suite *ExchangeSelectorSuite) TestCategoryFromItemType() {
|
|||||||
suite.Run(test.name, func() {
|
suite.Run(test.name, func() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
|
|
||||||
result := categoryFromItemType(test.input)
|
result := exchangeCategoryFromItemType(test.input)
|
||||||
assert.Equal(t, test.expect, result)
|
assert.Equal(t, test.expect, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ const (
|
|||||||
ServiceOneDrive service = 2 // OneDrive
|
ServiceOneDrive service = 2 // OneDrive
|
||||||
ServiceSharePoint service = 3 // SharePoint
|
ServiceSharePoint service = 3 // SharePoint
|
||||||
ServiceGroups service = 4 // Groups
|
ServiceGroups service = 4 // Groups
|
||||||
|
ServiceTeamsChats service = 5 // TeamsChats
|
||||||
)
|
)
|
||||||
|
|
||||||
var serviceToPathType = map[service]path.ServiceType{
|
var serviceToPathType = map[service]path.ServiceType{
|
||||||
@ -34,6 +35,7 @@ var serviceToPathType = map[service]path.ServiceType{
|
|||||||
ServiceOneDrive: path.OneDriveService,
|
ServiceOneDrive: path.OneDriveService,
|
||||||
ServiceSharePoint: path.SharePointService,
|
ServiceSharePoint: path.SharePointService,
|
||||||
ServiceGroups: path.GroupsService,
|
ServiceGroups: path.GroupsService,
|
||||||
|
ServiceTeamsChats: path.TeamsChatsService,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
525
src/pkg/selectors/teamsChats.go
Normal file
525
src/pkg/selectors/teamsChats.go
Normal file
@ -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
|
||||||
|
}
|
||||||
843
src/pkg/selectors/teamsChats_test.go
Normal file
843
src/pkg/selectors/teamsChats_test.go
Normal file
@ -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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
89
src/pkg/services/m365/api/teamdChats_test.go
Normal file
89
src/pkg/services/m365/api/teamdChats_test.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChatsAPIUnitSuite struct {
|
||||||
|
tester.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChatsAPIUnitSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &ChatsAPIUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ChatsAPIUnitSuite) TestChatsInfo() {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
chatAndExpected func() (models.Chatable, *details.TeamsChatsInfo)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Empty chat",
|
||||||
|
chatAndExpected: func() (models.Chatable, *details.TeamsChatsInfo) {
|
||||||
|
chat := models.NewChat()
|
||||||
|
|
||||||
|
i := &details.TeamsChatsInfo{
|
||||||
|
ItemType: details.TeamsChat,
|
||||||
|
Chat: details.ChatInfo{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return chat, i
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "All fields",
|
||||||
|
chatAndExpected: func() (models.Chatable, *details.TeamsChatsInfo) {
|
||||||
|
now := time.Now()
|
||||||
|
then := now.Add(1 * time.Hour)
|
||||||
|
|
||||||
|
chat := models.NewChat()
|
||||||
|
chat.SetTopic(ptr.To("Hello world"))
|
||||||
|
chat.SetCreatedDateTime(&now)
|
||||||
|
chat.SetLastUpdatedDateTime(&then)
|
||||||
|
|
||||||
|
i := &details.TeamsChatsInfo{
|
||||||
|
ItemType: details.TeamsChat,
|
||||||
|
Chat: details.ChatInfo{
|
||||||
|
Name: "Hello world",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return chat, i
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
chat, expected := test.chatAndExpected()
|
||||||
|
result := TeamsChatInfo(chat)
|
||||||
|
|
||||||
|
assert.Equal(t, expected.Chat.Name, result.Chat.Name)
|
||||||
|
|
||||||
|
expectLastUpdated := chat.GetLastUpdatedDateTime()
|
||||||
|
if expectLastUpdated != nil {
|
||||||
|
assert.Equal(t, ptr.Val(expectLastUpdated), result.Modified)
|
||||||
|
} else {
|
||||||
|
assert.True(t, result.Modified.After(start))
|
||||||
|
}
|
||||||
|
|
||||||
|
expectCreated := chat.GetCreatedDateTime()
|
||||||
|
if expectCreated != nil {
|
||||||
|
assert.Equal(t, ptr.Val(expectCreated), result.Chat.CreatedAt)
|
||||||
|
} else {
|
||||||
|
assert.True(t, result.Chat.CreatedAt.After(start))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
74
src/pkg/services/m365/api/teamsChats.go
Normal file
74
src/pkg/services/m365/api/teamsChats.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/microsoftgraph/msgraph-sdk-go/chats"
|
||||||
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||||
|
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// controller
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func (c Client) Chats() Chats {
|
||||||
|
return Chats{c}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chats is an interface-compliant provider of the client.
|
||||||
|
type Chats struct {
|
||||||
|
Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Chats
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func (c Chats) GetChatByID(
|
||||||
|
ctx context.Context,
|
||||||
|
chatID string,
|
||||||
|
cc CallConfig,
|
||||||
|
) (models.Chatable, *details.TeamsChatsInfo, error) {
|
||||||
|
config := &chats.ChatItemRequestBuilderGetRequestConfiguration{
|
||||||
|
QueryParameters: &chats.ChatItemRequestBuilderGetQueryParameters{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cc.Select) > 0 {
|
||||||
|
config.QueryParameters.Select = cc.Select
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cc.Expand) > 0 {
|
||||||
|
config.QueryParameters.Expand = cc.Expand
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.Stable.
|
||||||
|
Client().
|
||||||
|
Chats().
|
||||||
|
ByChatId(chatID).
|
||||||
|
Get(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, graph.Stack(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, TeamsChatInfo(resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TeamsChatInfo(chat models.Chatable) *details.TeamsChatsInfo {
|
||||||
|
return &details.TeamsChatsInfo{
|
||||||
|
ItemType: details.TeamsChat,
|
||||||
|
Modified: ptr.OrNow(chat.GetLastUpdatedDateTime()),
|
||||||
|
|
||||||
|
Chat: details.ChatInfo{
|
||||||
|
CreatedAt: ptr.OrNow(chat.GetCreatedDateTime()),
|
||||||
|
Name: ptr.Val(chat.GetTopic()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
172
src/pkg/services/m365/api/teamsChats_pager.go
Normal file
172
src/pkg/services/m365/api/teamsChats_pager.go
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/microsoftgraph/msgraph-sdk-go/chats"
|
||||||
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
|
"github.com/microsoftgraph/msgraph-sdk-go/users"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
||||||
|
"github.com/alcionai/corso/src/pkg/services/m365/api/pagers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// chat message pager
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// delta queries are not supported
|
||||||
|
var _ pagers.NonDeltaHandler[models.ChatMessageable] = &chatMessagePageCtrl{}
|
||||||
|
|
||||||
|
type chatMessagePageCtrl struct {
|
||||||
|
chatID string
|
||||||
|
gs graph.Servicer
|
||||||
|
builder *chats.ItemMessagesRequestBuilder
|
||||||
|
options *chats.ItemMessagesRequestBuilderGetRequestConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *chatMessagePageCtrl) SetNextLink(nextLink string) {
|
||||||
|
p.builder = chats.NewItemMessagesRequestBuilder(nextLink, p.gs.Adapter())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *chatMessagePageCtrl) GetPage(
|
||||||
|
ctx context.Context,
|
||||||
|
) (pagers.NextLinkValuer[models.ChatMessageable], error) {
|
||||||
|
resp, err := p.builder.Get(ctx, p.options)
|
||||||
|
return resp, graph.Stack(ctx, err).OrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *chatMessagePageCtrl) ValidModTimes() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chats) NewChatMessagePager(
|
||||||
|
chatID string,
|
||||||
|
cc CallConfig,
|
||||||
|
) *chatMessagePageCtrl {
|
||||||
|
builder := c.Stable.
|
||||||
|
Client().
|
||||||
|
Chats().
|
||||||
|
ByChatId(chatID).
|
||||||
|
Messages()
|
||||||
|
|
||||||
|
options := &chats.ItemMessagesRequestBuilderGetRequestConfiguration{
|
||||||
|
QueryParameters: &chats.ItemMessagesRequestBuilderGetQueryParameters{},
|
||||||
|
Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cc.Select) > 0 {
|
||||||
|
options.QueryParameters.Select = cc.Select
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cc.Expand) > 0 {
|
||||||
|
options.QueryParameters.Expand = cc.Expand
|
||||||
|
}
|
||||||
|
|
||||||
|
return &chatMessagePageCtrl{
|
||||||
|
chatID: chatID,
|
||||||
|
builder: builder,
|
||||||
|
gs: c.Stable,
|
||||||
|
options: options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChatMessages fetches a delta of all messages in the chat.
|
||||||
|
func (c Chats) GetChatMessages(
|
||||||
|
ctx context.Context,
|
||||||
|
chatID string,
|
||||||
|
cc CallConfig,
|
||||||
|
) ([]models.ChatMessageable, error) {
|
||||||
|
ctx = clues.Add(ctx, "chat_id", chatID)
|
||||||
|
pager := c.NewChatMessagePager(chatID, cc)
|
||||||
|
items, err := pagers.BatchEnumerateItems[models.ChatMessageable](ctx, pager)
|
||||||
|
|
||||||
|
return items, graph.Stack(ctx, err).OrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChatMessageIDs fetches a delta of all messages in the chat.
|
||||||
|
// returns two maps: addedItems, deletedItems
|
||||||
|
func (c Chats) GetChatMessageIDs(
|
||||||
|
ctx context.Context,
|
||||||
|
chatID string,
|
||||||
|
cc CallConfig,
|
||||||
|
) (pagers.AddedAndRemoved, error) {
|
||||||
|
aar, err := pagers.GetAddedAndRemovedItemIDs[models.ChatMessageable](
|
||||||
|
ctx,
|
||||||
|
c.NewChatMessagePager(chatID, CallConfig{}),
|
||||||
|
nil,
|
||||||
|
"",
|
||||||
|
false, // delta queries are not supported
|
||||||
|
0,
|
||||||
|
pagers.AddedAndRemovedByDeletedDateTime[models.ChatMessageable],
|
||||||
|
IsNotSystemMessage)
|
||||||
|
|
||||||
|
return aar, clues.Stack(err).OrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// chat pager
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
var _ pagers.NonDeltaHandler[models.Chatable] = &chatPageCtrl{}
|
||||||
|
|
||||||
|
type chatPageCtrl struct {
|
||||||
|
gs graph.Servicer
|
||||||
|
builder *users.ItemChatsRequestBuilder
|
||||||
|
options *users.ItemChatsRequestBuilderGetRequestConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *chatPageCtrl) SetNextLink(nextLink string) {
|
||||||
|
p.builder = users.NewItemChatsRequestBuilder(nextLink, p.gs.Adapter())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *chatPageCtrl) GetPage(
|
||||||
|
ctx context.Context,
|
||||||
|
) (pagers.NextLinkValuer[models.Chatable], error) {
|
||||||
|
resp, err := p.builder.Get(ctx, p.options)
|
||||||
|
return resp, graph.Stack(ctx, err).OrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *chatPageCtrl) ValidModTimes() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chats) NewChatPager(
|
||||||
|
userID string,
|
||||||
|
cc CallConfig,
|
||||||
|
) *chatPageCtrl {
|
||||||
|
options := &users.ItemChatsRequestBuilderGetRequestConfiguration{
|
||||||
|
QueryParameters: &users.ItemChatsRequestBuilderGetQueryParameters{},
|
||||||
|
Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cc.Select) > 0 {
|
||||||
|
options.QueryParameters.Select = cc.Select
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cc.Expand) > 0 {
|
||||||
|
options.QueryParameters.Expand = cc.Expand
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &chatPageCtrl{
|
||||||
|
gs: c.Stable,
|
||||||
|
options: options,
|
||||||
|
builder: c.Stable.
|
||||||
|
Client().
|
||||||
|
Users().
|
||||||
|
ByUserId(userID).
|
||||||
|
Chats(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChats fetches all chats in the team.
|
||||||
|
func (c Chats) GetChats(
|
||||||
|
ctx context.Context,
|
||||||
|
userID string,
|
||||||
|
cc CallConfig,
|
||||||
|
) ([]models.Chatable, error) {
|
||||||
|
return pagers.BatchEnumerateItems[models.Chatable](ctx, c.NewChatPager(userID, cc))
|
||||||
|
}
|
||||||
125
src/pkg/services/m365/api/teamsChats_pager_test.go
Normal file
125
src/pkg/services/m365/api/teamsChats_pager_test.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
|
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChatsPagerIntgSuite struct {
|
||||||
|
tester.Suite
|
||||||
|
its intgTesterSetup
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChatsPagerIntgSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &ChatsPagerIntgSuite{
|
||||||
|
Suite: tester.NewIntegrationSuite(
|
||||||
|
t,
|
||||||
|
[][]string{tconfig.M365AcctCredEnvs}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ChatsPagerIntgSuite) SetupSuite() {
|
||||||
|
suite.its = newIntegrationTesterSetup(suite.T())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ChatsPagerIntgSuite) TestEnumerateChats() {
|
||||||
|
var (
|
||||||
|
t = suite.T()
|
||||||
|
ac = suite.its.ac.Chats()
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
cc := CallConfig{
|
||||||
|
Expand: []string{"lastMessagePreview"},
|
||||||
|
}
|
||||||
|
|
||||||
|
chats, err := ac.GetChats(ctx, suite.its.user.id, cc)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
require.NotEmpty(t, chats)
|
||||||
|
|
||||||
|
for _, chat := range chats {
|
||||||
|
chatID := ptr.Val(chat.GetId())
|
||||||
|
|
||||||
|
suite.Run("chat_"+chatID, func() {
|
||||||
|
testGetChatByID(suite.T(), ac, chatID)
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.Run("chat_messages_"+chatID, func() {
|
||||||
|
testEnumerateChatMessages(
|
||||||
|
suite.T(),
|
||||||
|
ac,
|
||||||
|
chatID,
|
||||||
|
chat.GetLastMessagePreview())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetChatByID(
|
||||||
|
t *testing.T,
|
||||||
|
ac Chats,
|
||||||
|
chatID string,
|
||||||
|
) {
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
cc := CallConfig{}
|
||||||
|
|
||||||
|
chat, _, err := ac.GetChatByID(ctx, chatID, cc)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
require.NotNil(t, chat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEnumerateChatMessages(
|
||||||
|
t *testing.T,
|
||||||
|
ac Chats,
|
||||||
|
chatID string,
|
||||||
|
lastMessagePreview models.ChatMessageInfoable,
|
||||||
|
) {
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
cc := CallConfig{}
|
||||||
|
|
||||||
|
messages, err := ac.GetChatMessages(ctx, chatID, cc)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
var lastID string
|
||||||
|
if lastMessagePreview != nil {
|
||||||
|
lastID = ptr.Val(lastMessagePreview.GetId())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, msg := range messages {
|
||||||
|
msgID := ptr.Val(msg.GetId())
|
||||||
|
|
||||||
|
assert.Equal(
|
||||||
|
t,
|
||||||
|
chatID,
|
||||||
|
ptr.Val(msg.GetChatId()),
|
||||||
|
"message:",
|
||||||
|
msgID)
|
||||||
|
|
||||||
|
if msgID == lastID {
|
||||||
|
previewContent := ptr.Val(lastMessagePreview.GetBody().GetContent())
|
||||||
|
msgContent := ptr.Val(msg.GetBody().GetContent())
|
||||||
|
|
||||||
|
previewContent = replaceAttachmentMarkup(previewContent, nil)
|
||||||
|
msgContent = replaceAttachmentMarkup(msgContent, nil)
|
||||||
|
|
||||||
|
assert.Equal(
|
||||||
|
t,
|
||||||
|
previewContent,
|
||||||
|
msgContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user