From 1470776f3c26665101902b491f0d6c057236ada0 Mon Sep 17 00:00:00 2001 From: Keepers Date: Thu, 26 Oct 2023 12:03:44 -0600 Subject: [PATCH] expose additional channel metadata (#4539) builds out more details for channel messages and replies --- #### Does this PR need a docs update or release note? - [x] :no_entry: No #### Type of change - [ ] :sunflower: Feature #### Issue(s) * #3988 #### Test Plan - [x] :zap: Unit test - [x] :green_heart: E2E --- CHANGELOG.md | 3 + src/internal/m365/collection/groups/export.go | 6 + src/pkg/backup/details/groups.go | 53 ++- src/pkg/backup/details/groups_test.go | 58 +++ src/pkg/selectors/groups.go | 9 +- src/pkg/selectors/groups_test.go | 124 +++-- src/pkg/services/m365/api/channels.go | 78 +++- .../services/m365/api/channels_pager_test.go | 28 +- src/pkg/services/m365/api/channels_test.go | 427 ++++++++++++++++-- 9 files changed, 633 insertions(+), 153 deletions(-) create mode 100644 src/pkg/backup/details/groups_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index fff7464bf..448481df5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - SharePoint backup would fail if any site had an empty display name - Fix a bug with exports hanging post completion +### Changed +- Item Details formatting in Groups and Teams backups. Pre-release users will need to run new backups to avoid data corruption. + ## [v0.14.2] (beta) - 2023-10-17 ### Added diff --git a/src/internal/m365/collection/groups/export.go b/src/internal/m365/collection/groups/export.go index 590bacd48..4418fc9d5 100644 --- a/src/internal/m365/collection/groups/export.go +++ b/src/internal/m365/collection/groups/export.go @@ -90,10 +90,14 @@ func streamItems( type ( minimumChannelMessage struct { + // TODO(keepers): remove attachmentNames when better formatting + // of attachments within the content body is implemented. + AttachmentNames []string `json:"attachmentNames"` Content string `json:"content"` CreatedDateTime time.Time `json:"createdDateTime"` From string `json:"from"` LastModifiedDateTime time.Time `json:"lastModifiedDateTime"` + Subject string `json:"subject"` } minimumChannelMessageAndReplies struct { @@ -155,9 +159,11 @@ func makeMinimumChannelMesasge(item models.ChatMessageable) minimumChannelMessag } return minimumChannelMessage{ + AttachmentNames: api.GetChatMessageAttachmentNames(item), Content: content, CreatedDateTime: ptr.Val(item.GetCreatedDateTime()), From: api.GetChatMessageFrom(item), LastModifiedDateTime: ptr.Val(item.GetLastModifiedDateTime()), + Subject: ptr.Val(item.GetSubject()), } } diff --git a/src/pkg/backup/details/groups.go b/src/pkg/backup/details/groups.go index cf56a5edf..8d7867e9a 100644 --- a/src/pkg/backup/details/groups.go +++ b/src/pkg/backup/details/groups.go @@ -37,25 +37,33 @@ func NewGroupsLocationIDer( // GroupsInfo describes a groups item type GroupsInfo struct { - Created time.Time `json:"created,omitempty"` - ItemName string `json:"itemName,omitempty"` - ItemType ItemType `json:"itemType,omitempty"` - Modified time.Time `json:"modified,omitempty"` - Owner string `json:"owner,omitempty"` - ParentPath string `json:"parentPath,omitempty"` - Size int64 `json:"size,omitempty"` + ItemType ItemType `json:"itemType,omitempty"` + Modified time.Time `json:"modified,omitempty"` // Channels Specific - LastReplyAt time.Time `json:"lastResponseAt,omitempty"` - MessageCreator string `json:"messageCreator,omitempty"` - MessagePreview string `json:"messagePreview,omitempty"` - ReplyCount int `json:"replyCount,omitempty"` + Message ChannelMessageInfo `json:"message"` + LastReply ChannelMessageInfo `json:"lastReply"` // SharePoint specific - DriveName string `json:"driveName,omitempty"` - DriveID string `json:"driveID,omitempty"` - SiteID string `json:"siteID,omitempty"` - WebURL string `json:"webURL,omitempty"` + Created time.Time `json:"created,omitempty"` + DriveName string `json:"driveName,omitempty"` + DriveID string `json:"driveID,omitempty"` + ItemName string `json:"itemName,omitempty"` + Owner string `json:"owner,omitempty"` + ParentPath string `json:"parentPath,omitempty"` + SiteID string `json:"siteID,omitempty"` + Size int64 `json:"size,omitempty"` + WebURL string `json:"webURL,omitempty"` +} + +type ChannelMessageInfo struct { + AttachmentNames []string `json:"attachmentNames,omitempty"` + CreatedAt time.Time `json:"createdAt,omitempty"` + Creator string `json:"creator,omitempty"` + Preview string `json:"preview,omitempty"` + ReplyCount int `json:"replyCount"` + Size int64 `json:"size,omitempty"` + Subject string `json:"subject,omitempty"` } // Headers returns the human-readable names of properties in a SharePointInfo @@ -65,7 +73,7 @@ func (i GroupsInfo) Headers() []string { case SharePointLibrary: return []string{"ItemName", "Library", "ParentPath", "Size", "Owner", "Created", "Modified"} case GroupsChannelMessage: - return []string{"Message", "Channel", "Replies", "Creator", "Created", "Last Reply"} + return []string{"Message", "Channel", "Subject", "Replies", "Creator", "Created", "Last Reply"} } return []string{} @@ -86,17 +94,18 @@ func (i GroupsInfo) Values() []string { dttm.FormatToTabularDisplay(i.Modified), } case GroupsChannelMessage: - lastReply := dttm.FormatToTabularDisplay(i.LastReplyAt) - if i.LastReplyAt.Equal(time.Time{}) { + lastReply := dttm.FormatToTabularDisplay(i.LastReply.CreatedAt) + if i.LastReply.CreatedAt.IsZero() { lastReply = "" } return []string{ - i.MessagePreview, + i.Message.Preview, i.ParentPath, - strconv.Itoa(i.ReplyCount), - i.MessageCreator, - dttm.FormatToTabularDisplay(i.Created), + i.Message.Subject, + strconv.Itoa(i.Message.ReplyCount), + i.Message.Creator, + dttm.FormatToTabularDisplay(i.Message.CreatedAt), lastReply, } } diff --git a/src/pkg/backup/details/groups_test.go b/src/pkg/backup/details/groups_test.go new file mode 100644 index 000000000..33ab0acf5 --- /dev/null +++ b/src/pkg/backup/details/groups_test.go @@ -0,0 +1,58 @@ +package details_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/src/internal/common/dttm" + "github.com/alcionai/corso/src/internal/tester" + "github.com/alcionai/corso/src/pkg/backup/details" +) + +type GroupsUnitSuite struct { + tester.Suite +} + +func TestGroupsUnitSuite(t *testing.T) { + suite.Run(t, &GroupsUnitSuite{Suite: tester.NewUnitSuite(t)}) +} + +func (suite *GroupsUnitSuite) TestGroupsPrintable() { + t := suite.T() + now := time.Now() + then := now.Add(time.Minute) + + gi := details.GroupsInfo{ + ItemType: details.GroupsChannelMessage, + ParentPath: "parentPath", + Message: details.ChannelMessageInfo{ + Preview: "preview", + ReplyCount: 1, + Creator: "creator", + CreatedAt: now, + Subject: "subject", + }, + LastReply: details.ChannelMessageInfo{ + CreatedAt: then, + }, + } + + expectVs := []string{ + "preview", + "parentPath", + "subject", + "1", + "creator", + dttm.FormatToTabularDisplay(now), + dttm.FormatToTabularDisplay(then), + } + + hs := gi.Headers() + vs := gi.Values() + + assert.Equal(t, len(hs), len(vs)) + assert.Equal(t, expectVs, vs) +} diff --git a/src/pkg/selectors/groups.go b/src/pkg/selectors/groups.go index 6a2614149..d5aae480a 100644 --- a/src/pkg/selectors/groups.go +++ b/src/pkg/selectors/groups.go @@ -3,7 +3,6 @@ package selectors import ( "context" "fmt" - "time" "github.com/alcionai/clues" @@ -826,15 +825,15 @@ func (s GroupsScope) matchesInfo(dii details.ItemInfo) bool { case GroupsInfoLibraryItemModifiedAfter, GroupsInfoLibraryItemModifiedBefore: i = dttm.Format(info.Modified) case GroupsInfoChannelMessageCreator: - i = info.MessageCreator + i = info.Message.Creator case GroupsInfoChannelMessageCreatedAfter, GroupsInfoChannelMessageCreatedBefore: - i = dttm.Format(info.Created) + i = dttm.Format(info.Message.CreatedAt) case GroupsInfoChannelMessageLastReplyAfter, GroupsInfoChannelMessageLastReplyBefore: - if info.LastReplyAt.Equal(time.Time{}) { + if info.LastReply.CreatedAt.IsZero() { return false } - i = dttm.Format(info.LastReplyAt) + i = dttm.Format(info.LastReply.CreatedAt) } return s.Matches(infoCat, i) && int(info.ItemType) == acceptableItemType diff --git a/src/pkg/selectors/groups_test.go b/src/pkg/selectors/groups_test.go index e2074354e..a403ae2f7 100644 --- a/src/pkg/selectors/groups_test.go +++ b/src/pkg/selectors/groups_test.go @@ -370,53 +370,67 @@ func (suite *GroupsSelectorSuite) TestGroupsScope_MatchesInfo() { dspl = details.SharePointLibrary ) + type expectation func(t assert.TestingT, value bool, msg string, args ...any) bool + table := []struct { name string itemType details.ItemType creator string scope []GroupsScope - expect assert.BoolAssertionFunc + expect expectation }{ - {"file create after the epoch", dspl, user, sel.CreatedAfter(dttm.Format(epoch)), assert.True}, - {"file create after the epoch wrong type", dgcm, user, sel.CreatedAfter(dttm.Format(epoch)), assert.False}, - {"file create after now", dspl, user, sel.CreatedAfter(dttm.Format(now)), assert.False}, - {"file create after later", dspl, user, sel.CreatedAfter(dttm.Format(future)), assert.False}, - {"file create before future", dspl, user, sel.CreatedBefore(dttm.Format(future)), assert.True}, - {"file create before future wrong type", dgcm, user, sel.CreatedBefore(dttm.Format(future)), assert.False}, - {"file create before now", dspl, user, sel.CreatedBefore(dttm.Format(now)), assert.False}, - {"file create before modification", dspl, user, sel.CreatedBefore(dttm.Format(mod)), assert.True}, - {"file create before epoch", dspl, user, sel.CreatedBefore(dttm.Format(now)), assert.False}, - {"file modified after the epoch", dspl, user, sel.ModifiedAfter(dttm.Format(epoch)), assert.True}, - {"file modified after now", dspl, user, sel.ModifiedAfter(dttm.Format(now)), assert.True}, - {"file modified after later", dspl, user, sel.ModifiedAfter(dttm.Format(future)), assert.False}, - {"file modified before future", dspl, user, sel.ModifiedBefore(dttm.Format(future)), assert.True}, - {"file modified before now", dspl, user, sel.ModifiedBefore(dttm.Format(now)), assert.False}, - {"file modified before epoch", dspl, user, sel.ModifiedBefore(dttm.Format(now)), assert.False}, - {"in library", dspl, user, sel.Library("included-library"), assert.True}, - {"not in library", dspl, user, sel.Library("not-included-library"), assert.False}, - {"site id", dspl, user, sel.Site("site1"), assert.True}, - {"web url", dspl, user, sel.Site(user), assert.True}, - {"library id", dspl, user, sel.Library("1234"), assert.True}, - {"not library id", dspl, user, sel.Library("abcd"), assert.False}, + {"file create after the epoch", dspl, user, sel.CreatedAfter(dttm.Format(epoch)), assert.Truef}, + {"file create after the epoch wrong type", dgcm, user, sel.CreatedAfter(dttm.Format(epoch)), assert.Falsef}, + {"file create after now", dspl, user, sel.CreatedAfter(dttm.Format(now)), assert.Falsef}, + {"file create after later", dspl, user, sel.CreatedAfter(dttm.Format(future)), assert.Falsef}, + {"file create before future", dspl, user, sel.CreatedBefore(dttm.Format(future)), assert.Truef}, + {"file create before future wrong type", dgcm, user, sel.CreatedBefore(dttm.Format(future)), assert.Falsef}, + {"file create before now", dspl, user, sel.CreatedBefore(dttm.Format(now)), assert.Falsef}, + {"file create before modification", dspl, user, sel.CreatedBefore(dttm.Format(mod)), assert.Truef}, + {"file create before epoch", dspl, user, sel.CreatedBefore(dttm.Format(now)), assert.Falsef}, + {"file modified after the epoch", dspl, user, sel.ModifiedAfter(dttm.Format(epoch)), assert.Truef}, + {"file modified after now", dspl, user, sel.ModifiedAfter(dttm.Format(now)), assert.Truef}, + {"file modified after later", dspl, user, sel.ModifiedAfter(dttm.Format(future)), assert.Falsef}, + {"file modified before future", dspl, user, sel.ModifiedBefore(dttm.Format(future)), assert.Truef}, + {"file modified before now", dspl, user, sel.ModifiedBefore(dttm.Format(now)), assert.Falsef}, + {"file modified before epoch", dspl, user, sel.ModifiedBefore(dttm.Format(now)), assert.Falsef}, + {"in library", dspl, user, sel.Library("included-library"), assert.Truef}, + {"not in library", dspl, user, sel.Library("not-included-library"), assert.Falsef}, + {"site id", dspl, user, sel.Site("site1"), assert.Truef}, + {"web url", dspl, user, sel.Site(user), assert.Truef}, + {"library id", dspl, user, sel.Library("1234"), assert.Truef}, + {"not library id", dspl, user, sel.Library("abcd"), assert.Falsef}, - {"channel message created by", dgcm, user, sel.MessageCreator(user), assert.True}, - {"channel message not created by", dgcm, user, sel.MessageCreator(host), assert.False}, - {"chan msg create after the epoch", dgcm, user, sel.MessageCreatedAfter(dttm.Format(epoch)), assert.True}, - {"chan msg create after the epoch wrong type", dspl, user, sel.MessageCreatedAfter(dttm.Format(epoch)), assert.False}, - {"chan msg create after now", dgcm, user, sel.MessageCreatedAfter(dttm.Format(now)), assert.False}, - {"chan msg create after later", dgcm, user, sel.MessageCreatedAfter(dttm.Format(future)), assert.False}, - {"chan msg create before future", dgcm, user, sel.MessageCreatedBefore(dttm.Format(future)), assert.True}, - {"chan msg create before future wrong type", dspl, user, sel.MessageCreatedBefore(dttm.Format(future)), assert.False}, - {"chan msg create before now", dgcm, user, sel.MessageCreatedBefore(dttm.Format(now)), assert.False}, - {"chan msg create before reply", dgcm, user, sel.MessageCreatedBefore(dttm.Format(mod)), assert.True}, - {"chan msg create before reply wrong type", dspl, user, sel.MessageCreatedBefore(dttm.Format(mod)), assert.False}, - {"chan msg create before epoch", dgcm, user, sel.MessageCreatedBefore(dttm.Format(now)), assert.False}, - {"chan msg last reply after the epoch", dgcm, user, sel.MessageLastReplyAfter(dttm.Format(epoch)), assert.True}, - {"chan msg last reply after now", dgcm, user, sel.MessageLastReplyAfter(dttm.Format(now)), assert.True}, - {"chan msg last reply after later", dgcm, user, sel.MessageLastReplyAfter(dttm.Format(future)), assert.False}, - {"chan msg last reply before future", dgcm, user, sel.MessageLastReplyBefore(dttm.Format(future)), assert.True}, - {"chan msg last reply before now", dgcm, user, sel.MessageLastReplyBefore(dttm.Format(now)), assert.False}, - {"chan msg last reply before epoch", dgcm, user, sel.MessageLastReplyBefore(dttm.Format(now)), assert.False}, + {"channel message created by", dgcm, user, sel.MessageCreator(user), assert.Truef}, + {"channel message not created by", dgcm, user, sel.MessageCreator(host), assert.Falsef}, + {"chan msg create after the epoch", dgcm, user, sel.MessageCreatedAfter(dttm.Format(epoch)), assert.Truef}, + { + "chan msg create after the epoch wrong type", + dspl, + user, + sel.MessageCreatedAfter(dttm.Format(epoch)), + assert.Falsef, + }, + {"chan msg create after now", dgcm, user, sel.MessageCreatedAfter(dttm.Format(now)), assert.Falsef}, + {"chan msg create after later", dgcm, user, sel.MessageCreatedAfter(dttm.Format(future)), assert.Falsef}, + {"chan msg create before future", dgcm, user, sel.MessageCreatedBefore(dttm.Format(future)), assert.Truef}, + { + "chan msg create before future wrong type", + dspl, + user, + sel.MessageCreatedBefore(dttm.Format(future)), + assert.Falsef, + }, + {"chan msg create before now", dgcm, user, sel.MessageCreatedBefore(dttm.Format(now)), assert.Falsef}, + {"chan msg create before reply", dgcm, user, sel.MessageCreatedBefore(dttm.Format(mod)), assert.Truef}, + {"chan msg create before reply wrong type", dspl, user, sel.MessageCreatedBefore(dttm.Format(mod)), assert.Falsef}, + {"chan msg create before epoch", dgcm, user, sel.MessageCreatedBefore(dttm.Format(now)), assert.Falsef}, + {"chan msg last reply after the epoch", dgcm, user, sel.MessageLastReplyAfter(dttm.Format(epoch)), assert.Truef}, + {"chan msg last reply after now", dgcm, user, sel.MessageLastReplyAfter(dttm.Format(now)), assert.Truef}, + {"chan msg last reply after later", dgcm, user, sel.MessageLastReplyAfter(dttm.Format(future)), assert.Falsef}, + {"chan msg last reply before future", dgcm, user, sel.MessageLastReplyBefore(dttm.Format(future)), assert.Truef}, + {"chan msg last reply before now", dgcm, user, sel.MessageLastReplyBefore(dttm.Format(now)), assert.Falsef}, + {"chan msg last reply before epoch", dgcm, user, sel.MessageLastReplyBefore(dttm.Format(now)), assert.Falsef}, } for _, test := range table { suite.Run(test.name, func() { @@ -424,21 +438,31 @@ func (suite *GroupsSelectorSuite) TestGroupsScope_MatchesInfo() { itemInfo := details.ItemInfo{ Groups: &details.GroupsInfo{ - ItemType: test.itemType, - WebURL: test.creator, - MessageCreator: test.creator, - Created: now, - Modified: mod, - LastReplyAt: mod, - DriveName: "included-library", - DriveID: "1234", - SiteID: "site1", + ItemType: test.itemType, + Created: now, + WebURL: test.creator, + Modified: mod, + DriveName: "included-library", + DriveID: "1234", + SiteID: "site1", + Message: details.ChannelMessageInfo{ + Creator: test.creator, + CreatedAt: now, + }, + LastReply: details.ChannelMessageInfo{ + CreatedAt: mod, + }, }, } scopes := setScopesToDefault(test.scope) for _, scope := range scopes { - test.expect(t, scope.matchesInfo(itemInfo)) + test.expect( + t, + scope.matchesInfo(itemInfo), + "not matching:\nscope:\n\t%+v\ninfo:\n\t%+v", + scope, + itemInfo.Groups) } }) } diff --git a/src/pkg/services/m365/api/channels.go b/src/pkg/services/m365/api/channels.go index 44cf0d6aa..c2ab8f8bb 100644 --- a/src/pkg/services/m365/api/channels.go +++ b/src/pkg/services/m365/api/channels.go @@ -141,38 +141,58 @@ func channelMessageInfo( msg models.ChatMessageable, ) *details.GroupsInfo { var ( - lastReply time.Time - modTime = ptr.OrNow(msg.GetLastModifiedDateTime()) - content string + lastReply models.ChatMessageable + lastReplyAt time.Time + modTime = ptr.OrNow(msg.GetLastModifiedDateTime()) ) - for _, r := range msg.GetReplies() { + replies := msg.GetReplies() + + for _, r := range replies { cdt := ptr.Val(r.GetCreatedDateTime()) - if cdt.After(lastReply) { - lastReply = cdt + if cdt.After(lastReplyAt) { + lastReply = r + lastReplyAt = ptr.Val(r.GetCreatedDateTime()) } } // if the message hasn't been modified since before the most recent // reply, set the modified time to the most recent reply. This ensures // we update the message contents to match changes in replies. - if modTime.Before(lastReply) { - modTime = lastReply + if modTime.Before(lastReplyAt) { + modTime = lastReplyAt } - if msg.GetBody() != nil { - content = ptr.Val(msg.GetBody().GetContent()) + preview, contentLen := GetChatMessageContentPreview(msg) + + message := details.ChannelMessageInfo{ + AttachmentNames: GetChatMessageAttachmentNames(msg), + CreatedAt: ptr.Val(msg.GetCreatedDateTime()), + Creator: GetChatMessageFrom(msg), + Preview: preview, + ReplyCount: len(replies), + Size: contentLen, + Subject: ptr.Val(msg.GetSubject()), + } + + var lr details.ChannelMessageInfo + + if lastReply != nil { + preview, contentLen = GetChatMessageContentPreview(lastReply) + lr = details.ChannelMessageInfo{ + AttachmentNames: GetChatMessageAttachmentNames(lastReply), + CreatedAt: ptr.Val(lastReply.GetCreatedDateTime()), + Creator: GetChatMessageFrom(lastReply), + Preview: preview, + Size: contentLen, + } } return &details.GroupsInfo{ - ItemType: details.GroupsChannelMessage, - Created: ptr.Val(msg.GetCreatedDateTime()), - LastReplyAt: lastReply, - Modified: modTime, - MessageCreator: GetChatMessageFrom(msg), - MessagePreview: str.Preview(content, 128), - ReplyCount: len(msg.GetReplies()), - Size: int64(len(content)), + ItemType: details.GroupsChannelMessage, + Modified: modTime, + Message: message, + LastReply: lr, } } @@ -212,3 +232,25 @@ func GetChatMessageFrom(msg models.ChatMessageable) string { return "" } + +func GetChatMessageContentPreview(msg models.ChatMessageable) (string, int64) { + var content string + + if msg.GetBody() != nil { + content = ptr.Val(msg.GetBody().GetContent()) + } + + return str.Preview(content, 128), int64(len(content)) +} + +func GetChatMessageAttachmentNames(msg models.ChatMessageable) []string { + names := make([]string, 0, len(msg.GetAttachments())) + + for _, a := range msg.GetAttachments() { + if name := ptr.Val(a.GetName()); len(name) > 0 { + names = append(names, name) + } + } + + return names +} diff --git a/src/pkg/services/m365/api/channels_pager_test.go b/src/pkg/services/m365/api/channels_pager_test.go index d83eeb75b..ea118a350 100644 --- a/src/pkg/services/m365/api/channels_pager_test.go +++ b/src/pkg/services/m365/api/channels_pager_test.go @@ -106,14 +106,16 @@ func testEnumerateChannelMessageReplies( require.NoError(t, err, clues.ToCore(err)) var ( - lastReply time.Time - replyIDs = map[string]struct{}{} + lastReply models.ChatMessageable + lastReplyAt time.Time + replyIDs = map[string]struct{}{} ) for _, r := range replies { cdt := ptr.Val(r.GetCreatedDateTime()) - if cdt.After(lastReply) { - lastReply = cdt + if cdt.After(lastReplyAt) { + lastReply = r + lastReplyAt = cdt } replyIDs[ptr.Val(r.GetId())] = struct{}{} @@ -122,10 +124,20 @@ func testEnumerateChannelMessageReplies( assert.Equal(t, messageID, ptr.Val(msg.GetId())) assert.Equal(t, channelID, ptr.Val(msg.GetChannelIdentity().GetChannelId())) assert.Equal(t, groupID, ptr.Val(msg.GetChannelIdentity().GetTeamId())) - assert.Equal(t, len(replies), info.ReplyCount) - assert.Equal(t, msg.GetFrom().GetUser().GetDisplayName(), info.MessageCreator) - assert.Equal(t, lastReply, info.LastReplyAt) - assert.Equal(t, str.Preview(ptr.Val(msg.GetBody().GetContent()), 128), info.MessagePreview) + // message + assert.Equal(t, len(msg.GetAttachments()), len(info.Message.AttachmentNames)) + assert.Equal(t, len(replies), info.Message.ReplyCount) + assert.Equal(t, lastReplyAt, info.Message.CreatedAt) + assert.Equal(t, msg.GetFrom().GetUser().GetDisplayName(), info.Message.Creator) + assert.Equal(t, str.Preview(ptr.Val(msg.GetBody().GetContent()), 128), info.Message.Preview) + assert.Equal(t, len(ptr.Val(msg.GetBody().GetContent())), info.Message.Size) + // last reply + assert.Equal(t, len(lastReply.GetAttachments()), len(info.LastReply.AttachmentNames)) + assert.Zero(t, info.LastReply.ReplyCount) + assert.Equal(t, lastReplyAt, info.LastReply.CreatedAt) + assert.Equal(t, lastReply.GetFrom().GetUser().GetDisplayName(), info.LastReply.Creator) + assert.Equal(t, str.Preview(ptr.Val(lastReply.GetBody().GetContent()), 128), info.LastReply.Preview) + assert.Equal(t, len(ptr.Val(lastReply.GetBody().GetContent())), info.LastReply.Size) msgReplyIDs := map[string]struct{}{} diff --git a/src/pkg/services/m365/api/channels_test.go b/src/pkg/services/m365/api/channels_test.go index 851360f2c..9a46c052d 100644 --- a/src/pkg/services/m365/api/channels_test.go +++ b/src/pkg/services/m365/api/channels_test.go @@ -17,7 +17,7 @@ type ChannelsAPIUnitSuite struct { tester.Suite } -func TestChannelsAPIUnitSuitee(t *testing.T) { +func TestChannelsAPIUnitSuite(t *testing.T) { suite.Run(t, &ChannelsAPIUnitSuite{Suite: tester.NewUnitSuite(t)}) } @@ -26,12 +26,36 @@ func (suite *ChannelsAPIUnitSuite) TestChannelMessageInfo() { initial = time.Now().Add(-24 * time.Hour) mid = time.Now().Add(-1 * time.Hour) curr = time.Now() + ) - content = "content" - body = models.NewItemBody() + var ( + content = "content" + body = models.NewItemBody() + replyContent = "replycontent" + replyBody = models.NewItemBody() ) body.SetContent(ptr.To(content)) + replyBody.SetContent(ptr.To(replyContent)) + + var ( + attach1 = models.NewChatMessageAttachment() + attach2 = models.NewChatMessageAttachment() + replyAttach1 = models.NewChatMessageAttachment() + replyAttach2 = models.NewChatMessageAttachment() + ) + + attach1.SetName(ptr.To("attach1.ment")) + attach2.SetName(ptr.To("attach2.ment")) + replyAttach1.SetName(ptr.To("replyattach1.ment")) + replyAttach2.SetName(ptr.To("replyattach2.ment")) + + var ( + attachments = []models.ChatMessageAttachmentable{attach1, attach2} + replyAttachments = []models.ChatMessageAttachmentable{replyAttach1, replyAttach2} + expectAttachNames = []string{"attach1.ment", "attach2.ment"} + expectReplyAttachNames = []string{"replyattach1.ment", "replyattach2.ment"} + ) tests := []struct { name string @@ -43,6 +67,7 @@ func (suite *ChannelsAPIUnitSuite) TestChannelMessageInfo() { msg := models.NewChatMessage() msg.SetCreatedDateTime(&initial) msg.SetLastModifiedDateTime(&initial) + msg.SetSubject(ptr.To("subject")) iden := models.NewIdentity() iden.SetDisplayName(ptr.To("user")) @@ -53,12 +78,52 @@ func (suite *ChannelsAPIUnitSuite) TestChannelMessageInfo() { msg.SetFrom(from) i := &details.GroupsInfo{ - ItemType: details.GroupsChannelMessage, - Created: initial, - Modified: initial, - LastReplyAt: time.Time{}, - ReplyCount: 0, - MessageCreator: "user", + ItemType: details.GroupsChannelMessage, + Modified: initial, + LastReply: details.ChannelMessageInfo{}, + Message: details.ChannelMessageInfo{ + AttachmentNames: []string{}, + CreatedAt: initial, + Creator: "user", + ReplyCount: 0, + Preview: "", + Size: 0, + Subject: "subject", + }, + } + + return msg, i + }, + }, + { + name: "No Subject", + msgAndInfo: func() (models.ChatMessageable, *details.GroupsInfo) { + msg := models.NewChatMessage() + msg.SetCreatedDateTime(&initial) + msg.SetLastModifiedDateTime(&initial) + msg.SetBody(body) + + iden := models.NewIdentity() + iden.SetDisplayName(ptr.To("user")) + + from := models.NewChatMessageFromIdentitySet() + from.SetUser(iden) + + msg.SetFrom(from) + + i := &details.GroupsInfo{ + ItemType: details.GroupsChannelMessage, + Modified: initial, + LastReply: details.ChannelMessageInfo{}, + Message: details.ChannelMessageInfo{ + AttachmentNames: []string{}, + CreatedAt: initial, + Creator: "user", + ReplyCount: 0, + Preview: content, + Size: int64(len(content)), + Subject: "", + }, } return msg, i @@ -71,6 +136,7 @@ func (suite *ChannelsAPIUnitSuite) TestChannelMessageInfo() { msg.SetCreatedDateTime(&initial) msg.SetLastModifiedDateTime(&initial) msg.SetBody(body) + msg.SetSubject(ptr.To("subject")) iden := models.NewIdentity() iden.SetDisplayName(ptr.To("user")) @@ -81,14 +147,18 @@ func (suite *ChannelsAPIUnitSuite) TestChannelMessageInfo() { msg.SetFrom(from) i := &details.GroupsInfo{ - ItemType: details.GroupsChannelMessage, - Created: initial, - Modified: initial, - LastReplyAt: time.Time{}, - ReplyCount: 0, - MessageCreator: "user", - Size: int64(len(content)), - MessagePreview: content, + ItemType: details.GroupsChannelMessage, + Modified: initial, + LastReply: details.ChannelMessageInfo{}, + Message: details.ChannelMessageInfo{ + AttachmentNames: []string{}, + CreatedAt: initial, + Creator: "user", + ReplyCount: 0, + Preview: content, + Size: int64(len(content)), + Subject: "subject", + }, } return msg, i @@ -101,6 +171,7 @@ func (suite *ChannelsAPIUnitSuite) TestChannelMessageInfo() { msg.SetCreatedDateTime(&initial) msg.SetLastModifiedDateTime(&initial) msg.SetBody(body) + msg.SetSubject(ptr.To("subject")) iden := models.NewIdentity() iden.SetDisplayName(ptr.To("app")) @@ -111,14 +182,18 @@ func (suite *ChannelsAPIUnitSuite) TestChannelMessageInfo() { msg.SetFrom(from) i := &details.GroupsInfo{ - ItemType: details.GroupsChannelMessage, - Created: initial, - Modified: initial, - LastReplyAt: time.Time{}, - ReplyCount: 0, - MessageCreator: "app", - Size: int64(len(content)), - MessagePreview: content, + ItemType: details.GroupsChannelMessage, + Modified: initial, + LastReply: details.ChannelMessageInfo{}, + Message: details.ChannelMessageInfo{ + AttachmentNames: []string{}, + CreatedAt: initial, + Creator: "app", + ReplyCount: 0, + Preview: content, + Size: int64(len(content)), + Subject: "subject", + }, } return msg, i @@ -131,6 +206,7 @@ func (suite *ChannelsAPIUnitSuite) TestChannelMessageInfo() { msg.SetCreatedDateTime(&initial) msg.SetLastModifiedDateTime(&initial) msg.SetBody(body) + msg.SetSubject(ptr.To("subject")) iden := models.NewIdentity() iden.SetDisplayName(ptr.To("device")) @@ -141,14 +217,54 @@ func (suite *ChannelsAPIUnitSuite) TestChannelMessageInfo() { msg.SetFrom(from) i := &details.GroupsInfo{ - ItemType: details.GroupsChannelMessage, - Created: initial, - Modified: initial, - LastReplyAt: time.Time{}, - ReplyCount: 0, - MessageCreator: "device", - Size: int64(len(content)), - MessagePreview: content, + ItemType: details.GroupsChannelMessage, + Modified: initial, + LastReply: details.ChannelMessageInfo{}, + Message: details.ChannelMessageInfo{ + AttachmentNames: []string{}, + CreatedAt: initial, + Creator: "device", + ReplyCount: 0, + Preview: content, + Size: int64(len(content)), + Subject: "subject", + }, + } + + return msg, i + }, + }, + { + name: "No Replies - with attachments", + msgAndInfo: func() (models.ChatMessageable, *details.GroupsInfo) { + msg := models.NewChatMessage() + msg.SetCreatedDateTime(&initial) + msg.SetLastModifiedDateTime(&initial) + msg.SetBody(body) + msg.SetSubject(ptr.To("subject")) + msg.SetAttachments(attachments) + + iden := models.NewIdentity() + iden.SetDisplayName(ptr.To("user")) + + from := models.NewChatMessageFromIdentitySet() + from.SetUser(iden) + + msg.SetFrom(from) + + i := &details.GroupsInfo{ + ItemType: details.GroupsChannelMessage, + Modified: initial, + LastReply: details.ChannelMessageInfo{}, + Message: details.ChannelMessageInfo{ + AttachmentNames: expectAttachNames, + CreatedAt: initial, + Creator: "user", + ReplyCount: 0, + Preview: content, + Size: int64(len(content)), + Subject: "subject", + }, } return msg, i @@ -161,6 +277,7 @@ func (suite *ChannelsAPIUnitSuite) TestChannelMessageInfo() { msg.SetCreatedDateTime(&initial) msg.SetLastModifiedDateTime(&initial) msg.SetBody(body) + msg.SetSubject(ptr.To("subject")) iden := models.NewIdentity() iden.SetDisplayName(ptr.To("user")) @@ -170,21 +287,42 @@ func (suite *ChannelsAPIUnitSuite) TestChannelMessageInfo() { msg.SetFrom(from) + // reply + + iden = models.NewIdentity() + iden.SetDisplayName(ptr.To("replyuser")) + + from = models.NewChatMessageFromIdentitySet() + from.SetUser(iden) + reply := models.NewChatMessage() reply.SetCreatedDateTime(&curr) reply.SetLastModifiedDateTime(&curr) + reply.SetFrom(from) + reply.SetBody(replyBody) msg.SetReplies([]models.ChatMessageable{reply}) i := &details.GroupsInfo{ - ItemType: details.GroupsChannelMessage, - Created: initial, - Modified: curr, - LastReplyAt: curr, - ReplyCount: 1, - MessageCreator: "user", - Size: int64(len(content)), - MessagePreview: content, + ItemType: details.GroupsChannelMessage, + Modified: curr, + LastReply: details.ChannelMessageInfo{ + AttachmentNames: []string{}, + CreatedAt: curr, + Creator: "replyuser", + ReplyCount: 0, + Preview: replyContent, + Size: int64(len(replyContent)), + }, + Message: details.ChannelMessageInfo{ + AttachmentNames: []string{}, + CreatedAt: initial, + Creator: "user", + ReplyCount: 1, + Preview: content, + Size: int64(len(content)), + Subject: "subject", + }, } return msg, i @@ -197,6 +335,7 @@ func (suite *ChannelsAPIUnitSuite) TestChannelMessageInfo() { msg.SetCreatedDateTime(&initial) msg.SetLastModifiedDateTime(&initial) msg.SetBody(body) + msg.SetSubject(ptr.To("subject")) iden := models.NewIdentity() iden.SetDisplayName(ptr.To("user")) @@ -206,25 +345,197 @@ func (suite *ChannelsAPIUnitSuite) TestChannelMessageInfo() { msg.SetFrom(from) + // replies + + iden = models.NewIdentity() + iden.SetDisplayName(ptr.To("reply1user")) + + from = models.NewChatMessageFromIdentitySet() + from.SetUser(iden) + reply1 := models.NewChatMessage() reply1.SetCreatedDateTime(&mid) reply1.SetLastModifiedDateTime(&mid) + reply1.SetFrom(from) + reply1.SetBody(replyBody) + + iden = models.NewIdentity() + iden.SetDisplayName(ptr.To("reply2user")) + + from = models.NewChatMessageFromIdentitySet() + from.SetUser(iden) reply2 := models.NewChatMessage() reply2.SetCreatedDateTime(&curr) reply2.SetLastModifiedDateTime(&curr) + reply2.SetFrom(from) + reply2.SetBody(replyBody) msg.SetReplies([]models.ChatMessageable{reply1, reply2}) i := &details.GroupsInfo{ - ItemType: details.GroupsChannelMessage, - Created: initial, - Modified: curr, - LastReplyAt: curr, - ReplyCount: 2, - MessageCreator: "user", - Size: int64(len(content)), - MessagePreview: content, + ItemType: details.GroupsChannelMessage, + Modified: curr, + LastReply: details.ChannelMessageInfo{ + AttachmentNames: []string{}, + CreatedAt: curr, + Creator: "reply2user", + ReplyCount: 0, + Preview: replyContent, + Size: int64(len(replyContent)), + }, + Message: details.ChannelMessageInfo{ + AttachmentNames: []string{}, + CreatedAt: initial, + Creator: "user", + ReplyCount: 2, + Preview: content, + Size: int64(len(content)), + Subject: "subject", + }, + } + + return msg, i + }, + }, + { + name: "Many Replies - not last has attachments", + msgAndInfo: func() (models.ChatMessageable, *details.GroupsInfo) { + msg := models.NewChatMessage() + msg.SetCreatedDateTime(&initial) + msg.SetLastModifiedDateTime(&initial) + msg.SetBody(body) + msg.SetSubject(ptr.To("subject")) + + iden := models.NewIdentity() + iden.SetDisplayName(ptr.To("user")) + + from := models.NewChatMessageFromIdentitySet() + from.SetUser(iden) + + msg.SetFrom(from) + + // replies + + iden = models.NewIdentity() + iden.SetDisplayName(ptr.To("reply1user")) + + from = models.NewChatMessageFromIdentitySet() + from.SetUser(iden) + + reply1 := models.NewChatMessage() + reply1.SetCreatedDateTime(&mid) + reply1.SetLastModifiedDateTime(&mid) + reply1.SetFrom(from) + reply1.SetBody(replyBody) + reply1.SetAttachments(replyAttachments) + + iden = models.NewIdentity() + iden.SetDisplayName(ptr.To("reply2user")) + + from = models.NewChatMessageFromIdentitySet() + from.SetUser(iden) + + reply2 := models.NewChatMessage() + reply2.SetCreatedDateTime(&curr) + reply2.SetLastModifiedDateTime(&curr) + reply2.SetFrom(from) + reply2.SetBody(replyBody) + + msg.SetReplies([]models.ChatMessageable{reply1, reply2}) + + i := &details.GroupsInfo{ + ItemType: details.GroupsChannelMessage, + Modified: curr, + LastReply: details.ChannelMessageInfo{ + AttachmentNames: []string{}, + CreatedAt: curr, + Creator: "reply2user", + ReplyCount: 0, + Preview: replyContent, + Size: int64(len(replyContent)), + }, + Message: details.ChannelMessageInfo{ + AttachmentNames: []string{}, + CreatedAt: initial, + Creator: "user", + ReplyCount: 2, + Preview: content, + Size: int64(len(content)), + Subject: "subject", + }, + } + + return msg, i + }, + }, + { + name: "Many Replies - last has attachments", + msgAndInfo: func() (models.ChatMessageable, *details.GroupsInfo) { + msg := models.NewChatMessage() + msg.SetCreatedDateTime(&initial) + msg.SetLastModifiedDateTime(&initial) + msg.SetBody(body) + msg.SetSubject(ptr.To("subject")) + msg.SetAttachments(attachments) + + iden := models.NewIdentity() + iden.SetDisplayName(ptr.To("user")) + + from := models.NewChatMessageFromIdentitySet() + from.SetUser(iden) + + msg.SetFrom(from) + + // replies + + iden = models.NewIdentity() + iden.SetDisplayName(ptr.To("reply1user")) + + from = models.NewChatMessageFromIdentitySet() + from.SetUser(iden) + + reply1 := models.NewChatMessage() + reply1.SetCreatedDateTime(&mid) + reply1.SetLastModifiedDateTime(&mid) + reply1.SetFrom(from) + reply1.SetBody(replyBody) + + iden = models.NewIdentity() + iden.SetDisplayName(ptr.To("reply2user")) + + from = models.NewChatMessageFromIdentitySet() + from.SetUser(iden) + + reply2 := models.NewChatMessage() + reply2.SetCreatedDateTime(&curr) + reply2.SetLastModifiedDateTime(&curr) + reply2.SetFrom(from) + reply2.SetBody(replyBody) + reply2.SetAttachments(replyAttachments) + + msg.SetReplies([]models.ChatMessageable{reply1, reply2}) + + i := &details.GroupsInfo{ + ItemType: details.GroupsChannelMessage, + Modified: curr, + LastReply: details.ChannelMessageInfo{ + AttachmentNames: expectReplyAttachNames, + CreatedAt: curr, + Creator: "reply2user", + ReplyCount: 0, + Preview: replyContent, + Size: int64(len(replyContent)), + }, + Message: details.ChannelMessageInfo{ + AttachmentNames: expectAttachNames, + CreatedAt: initial, + Creator: "user", + ReplyCount: 2, + Preview: content, + Size: int64(len(content)), + Subject: "subject", + }, } return msg, i @@ -233,8 +544,24 @@ func (suite *ChannelsAPIUnitSuite) TestChannelMessageInfo() { } for _, test := range tests { suite.Run(test.name, func() { + t := suite.T() + chMsg, expected := test.msgAndInfo() - assert.Equal(suite.T(), expected, channelMessageInfo(chMsg)) + result := channelMessageInfo(chMsg) + + ma := result.Message.AttachmentNames + result.Message.AttachmentNames = nil + ema := expected.Message.AttachmentNames + expected.Message.AttachmentNames = nil + + lra := result.LastReply.AttachmentNames + result.LastReply.AttachmentNames = nil + elra := expected.LastReply.AttachmentNames + expected.LastReply.AttachmentNames = nil + + assert.Equal(t, expected, result) + assert.ElementsMatch(t, ema, ma) + assert.ElementsMatch(t, elra, lra) }) } }