diff --git a/src/cli/backup/teamschats.go b/src/cli/backup/teamschats.go index 1901893809..487cc41e47 100644 --- a/src/cli/backup/teamschats.go +++ b/src/cli/backup/teamschats.go @@ -213,7 +213,6 @@ func runDetailsTeamsChatsCmd(cmd *cobra.Command) error { opts := utils.MakeTeamsChatsOpts(cmd) sel := utils.IncludeTeamsChatsRestoreDataSelectors(ctx, opts) - sel.Configure(selectors.Config{OnlyMatchItemNames: true}) utils.FilterTeamsChatsRestoreInfoSelectors(sel, opts) ds, err := genericDetailsCommand(cmd, flags.BackupIDFV, sel.Selector) diff --git a/src/cli/utils/teamschats.go b/src/cli/utils/teamschats.go index 1467949a33..f91609bbc9 100644 --- a/src/cli/utils/teamschats.go +++ b/src/cli/utils/teamschats.go @@ -89,7 +89,10 @@ func IncludeTeamsChatsRestoreDataSelectors(ctx context.Context, opts TeamsChatsO users = selectors.Any() } - return selectors.NewTeamsChatsRestore(users) + sel := selectors.NewTeamsChatsRestore(users) + sel.Include(sel.Chats(selectors.Any())) + + return sel } // FilterTeamsChatsRestoreInfoSelectors builds the common info-selector filters. diff --git a/src/internal/m365/collection/teamschats/backup.go b/src/internal/m365/collection/teamschats/backup.go index 732d32286a..f554540ebb 100644 --- a/src/internal/m365/collection/teamschats/backup.go +++ b/src/internal/m365/collection/teamschats/backup.go @@ -115,11 +115,8 @@ func populateCollection[I chatsItemer]( ) ctx = clues.AddLabelCounter(ctx, cl.PlainAdder()) - cc := api.CallConfig{ - CanMakeDeltaQueries: false, - } - items, err := bh.getItemIDs(ctx, cc) + items, err := bh.getItemIDs(ctx) if err != nil { errs.AddRecoverable(ctx, clues.Stack(err)) return collection, clues.Stack(errs.Failure()).OrNil() diff --git a/src/internal/m365/collection/teamschats/backup_test.go b/src/internal/m365/collection/teamschats/backup_test.go index bbb8ee0d56..3bf22a776f 100644 --- a/src/internal/m365/collection/teamschats/backup_test.go +++ b/src/internal/m365/collection/teamschats/backup_test.go @@ -49,14 +49,6 @@ type mockBackupHandler struct { doNotInclude bool } -//lint:ignore U1000 false linter issue due to generics -func (bh mockBackupHandler) augmentItemInfo( - *details.TeamsChatsInfo, - models.Chatable, -) { - // no-op -} - func (bh mockBackupHandler) container() container[models.Chatable] { return chatContainer() } @@ -71,7 +63,6 @@ func (bh mockBackupHandler) getContainer( func (bh mockBackupHandler) getItemIDs( _ context.Context, - _ api.CallConfig, ) ([]models.Chatable, error) { return bh.chats, bh.chatsErr } @@ -96,15 +87,13 @@ func (bh mockBackupHandler) CanonicalPath() (path.Path, error) { func (bh mockBackupHandler) getItem( _ context.Context, _ string, - itemID string, + chat models.Chatable, ) (models.Chatable, *details.TeamsChatsInfo, error) { - chat := models.NewChat() + chatID := ptr.Val(chat.GetId()) - chat.SetId(ptr.To(itemID)) - chat.SetTopic(ptr.To(itemID)) - chat.SetMessages(bh.chatMessages[itemID]) + chat.SetMessages(bh.chatMessages[chatID]) - return chat, bh.info[itemID], bh.getMessageErr[itemID] + return chat, bh.info[chatID], bh.getMessageErr[chatID] } // --------------------------------------------------------------------------- diff --git a/src/internal/m365/collection/teamschats/chat_handler.go b/src/internal/m365/collection/teamschats/chat_handler.go index 93ef8316a7..adbd1103be 100644 --- a/src/internal/m365/collection/teamschats/chat_handler.go +++ b/src/internal/m365/collection/teamschats/chat_handler.go @@ -8,6 +8,7 @@ import ( "github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/pkg/backup/details" + "github.com/alcionai/corso/src/pkg/errs/core" "github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/services/m365/api" @@ -45,8 +46,11 @@ func (bh usersChatsBackupHandler) getContainer( //lint:ignore U1000 required for interface compliance func (bh usersChatsBackupHandler) getItemIDs( ctx context.Context, - cc api.CallConfig, ) ([]models.Chatable, error) { + cc := api.CallConfig{ + Expand: []string{"lastMessagePreview"}, + } + return bh.ac.GetChats( ctx, bh.protectedResourceID, @@ -80,18 +84,29 @@ func (bh usersChatsBackupHandler) CanonicalPath() (path.Path, error) { func (bh usersChatsBackupHandler) getItem( ctx context.Context, userID string, - chatID string, + chat models.Chatable, ) (models.Chatable, *details.TeamsChatsInfo, error) { - // FIXME: should retrieve and populate all messages in the chat. - return nil, nil, clues.New("not implemented") -} + if chat == nil { + return nil, nil, clues.Stack(core.ErrNotFound) + } -//lint:ignore U1000 false linter issue due to generics -func (bh usersChatsBackupHandler) augmentItemInfo( - dgi *details.TeamsChatsInfo, - c models.Chatable, -) { - // no-op + chatID := ptr.Val(chat.GetId()) + + msgs, err := bh.ac.GetChatMessages(ctx, chatID, api.CallConfig{}) + if err != nil { + return nil, nil, clues.Stack(err) + } + + chat.SetMessages(msgs) + + members, err := bh.ac.GetChatMembers(ctx, chatID, api.CallConfig{}) + if err != nil { + return nil, nil, clues.Stack(err) + } + + chat.SetMembers(members) + + return chat, api.TeamsChatInfo(chat), nil } func chatContainer() container[models.Chatable] { diff --git a/src/internal/m365/collection/teamschats/collection.go b/src/internal/m365/collection/teamschats/collection.go index a7f25c6011..e2fe532db0 100644 --- a/src/internal/m365/collection/teamschats/collection.go +++ b/src/internal/m365/collection/teamschats/collection.go @@ -66,7 +66,7 @@ func updateStatus( // or notMoved (if they match). func NewCollection[I chatsItemer]( baseCol data.BaseCollection, - getAndAugment getItemAndAugmentInfoer[I], + getter getItemer[I], protectedResource string, items []I, contains container[I], @@ -76,7 +76,7 @@ func NewCollection[I chatsItemer]( BaseCollection: baseCol, items: items, contains: contains, - getAndAugment: getAndAugment, + getter: getter, statusUpdater: statusUpdater, stream: make(chan data.Item, collectionChannelBufferSize), protectedResource: protectedResource, @@ -96,7 +96,7 @@ type lazyFetchCollection[I chatsItemer] struct { items []I - getAndAugment getItemAndAugmentInfoer[I] + getter getItemer[I] statusUpdater support.StatusUpdater } @@ -152,33 +152,30 @@ func (col *lazyFetchCollection[I]) streamItems(ctx context.Context, errs *fault. break } - itemID := ptr.Val(item.GetId()) modTime := ptr.Val(item.GetLastUpdatedDateTime()) wg.Add(1) semaphoreCh <- struct{}{} - go func(id string, modTime time.Time) { + go func(item I, modTime time.Time) { defer wg.Done() defer func() { <-semaphoreCh }() - ictx := clues.Add( - ctx, - "item_id", id, - "parent_path", path.LoggableDir(col.LocationPath().String())) + itemID := ptr.Val(item.GetId()) + ictx := clues.Add(ctx, "item_id", itemID) col.stream <- data.NewLazyItemWithInfo( ictx, &lazyItemGetter[I]{ - modTime: modTime, - getAndAugment: col.getAndAugment, - resourceID: col.protectedResource, - itemID: id, - containerIDs: col.FullPath().Folders(), - contains: col.contains, - parentPath: col.LocationPath().String(), + modTime: modTime, + getter: col.getter, + resourceID: col.protectedResource, + item: item, + containerIDs: col.FullPath().Folders(), + contains: col.contains, + parentPath: col.LocationPath().String(), }, - id, + itemID, modTime, col.Counter, el) @@ -188,20 +185,20 @@ func (col *lazyFetchCollection[I]) streamItems(ctx context.Context, errs *fault. if progressMessage != nil { progressMessage <- struct{}{} } - }(itemID, modTime) + }(item, modTime) } wg.Wait() } type lazyItemGetter[I chatsItemer] struct { - getAndAugment getItemAndAugmentInfoer[I] - resourceID string - itemID string - parentPath string - containerIDs path.Elements - modTime time.Time - contains container[I] + getter getItemer[I] + resourceID string + item I + parentPath string + containerIDs path.Elements + modTime time.Time + contains container[I] } func (lig *lazyItemGetter[I]) GetData( @@ -211,10 +208,10 @@ func (lig *lazyItemGetter[I]) GetData( writer := kjson.NewJsonSerializationWriter() defer writer.Close() - item, info, err := lig.getAndAugment.getItem( + item, info, err := lig.getter.getItem( ctx, lig.resourceID, - lig.itemID) + lig.item) if err != nil { // For items that were deleted in flight, add the skip label so that // they don't lead to recoverable failures during backup. @@ -232,8 +229,6 @@ func (lig *lazyItemGetter[I]) GetData( return nil, nil, false, err } - lig.getAndAugment.augmentItemInfo(info, lig.contains.container) - if err := writer.WriteObjectValue("", item); err != nil { err = clues.WrapWC(ctx, err, "writing item to serializer").Label(fault.LabelForceNoBackupCreation) errs.AddRecoverable(ctx, err) diff --git a/src/internal/m365/collection/teamschats/collection_test.go b/src/internal/m365/collection/teamschats/collection_test.go index 15b0dac778..b1296e40f4 100644 --- a/src/internal/m365/collection/teamschats/collection_test.go +++ b/src/internal/m365/collection/teamschats/collection_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/alcionai/clues" + "github.com/google/uuid" "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -155,20 +156,13 @@ type getAndAugmentChat struct { func (m getAndAugmentChat) getItem( _ context.Context, _ string, - itemID string, + chat models.Chatable, ) (models.Chatable, *details.TeamsChatsInfo, error) { - chat := models.NewChat() - chat.SetId(ptr.To(itemID)) - chat.SetTopic(ptr.To(itemID)) + chat.SetTopic(chat.GetId()) return chat, &details.TeamsChatsInfo{}, m.err } -//lint:ignore U1000 false linter issue due to generics -func (getAndAugmentChat) augmentItemInfo(*details.TeamsChatsInfo, models.Chatable) { - // no-op -} - func (suite *CollectionUnitSuite) TestLazyFetchCollection_Items_LazyFetch() { var ( t = suite.T() @@ -226,7 +220,7 @@ func (suite *CollectionUnitSuite) TestLazyFetchCollection_Items_LazyFetch() { count.New()), items: test.items, contains: container[models.Chatable]{}, - getAndAugment: getterAugmenter, + getter: getterAugmenter, stream: make(chan data.Item), statusUpdater: statusUpdater, } @@ -277,6 +271,8 @@ func (suite *CollectionUnitSuite) TestLazyItem_GetDataErrors() { ctx, flush := tester.NewContext(t) defer flush() + chat := testdata.StubChats(uuid.NewString())[0] + m := getAndAugmentChat{ err: test.getErr, } @@ -284,13 +280,13 @@ func (suite *CollectionUnitSuite) TestLazyItem_GetDataErrors() { li := data.NewLazyItemWithInfo( ctx, &lazyItemGetter[models.Chatable]{ - resourceID: "resourceID", - itemID: "itemID", - getAndAugment: &m, - modTime: now, - parentPath: parentPath, + resourceID: "resourceID", + item: chat, + getter: &m, + modTime: now, + parentPath: parentPath, }, - "itemID", + ptr.Val(chat.GetId()), now, count.New(), fault.New(true)) @@ -319,6 +315,8 @@ func (suite *CollectionUnitSuite) TestLazyItem_ReturnsEmptyReaderOnDeletedInFlig ctx, flush := tester.NewContext(t) defer flush() + chat := testdata.StubChats(uuid.NewString())[0] + m := getAndAugmentChat{ err: core.ErrNotFound, } @@ -326,13 +324,13 @@ func (suite *CollectionUnitSuite) TestLazyItem_ReturnsEmptyReaderOnDeletedInFlig li := data.NewLazyItemWithInfo( ctx, &lazyItemGetter[models.Chatable]{ - resourceID: "resourceID", - itemID: "itemID", - getAndAugment: &m, - modTime: now, - parentPath: parentPath, + resourceID: "resourceID", + item: chat, + getter: &m, + modTime: now, + parentPath: parentPath, }, - "itemID", + ptr.Val(chat.GetId()), now, count.New(), fault.New(true)) @@ -359,18 +357,19 @@ func (suite *CollectionUnitSuite) TestLazyItem() { ctx, flush := tester.NewContext(t) defer flush() + chat := testdata.StubChats(uuid.NewString())[0] m := getAndAugmentChat{} li := data.NewLazyItemWithInfo( ctx, &lazyItemGetter[models.Chatable]{ - resourceID: "resourceID", - itemID: "itemID", - getAndAugment: &m, - modTime: now, - parentPath: parentPath, + resourceID: "resourceID", + item: chat, + getter: &m, + modTime: now, + parentPath: parentPath, }, - "itemID", + ptr.Val(chat.GetId()), now, count.New(), fault.New(true)) diff --git a/src/internal/m365/collection/teamschats/handlers.go b/src/internal/m365/collection/teamschats/handlers.go index f1db7d32b1..7865de4891 100644 --- a/src/internal/m365/collection/teamschats/handlers.go +++ b/src/internal/m365/collection/teamschats/handlers.go @@ -22,7 +22,7 @@ type chatsItemer interface { type backupHandler[I chatsItemer] interface { getContainerer[I] - getItemAndAugmentInfoer[I] + getItemer[I] getItemer[I] getItemIDser[I] includeItemer[I] @@ -39,22 +39,10 @@ type getContainerer[I chatsItemer] interface { ) (container[I], error) } -type getItemAndAugmentInfoer[I chatsItemer] interface { - getItemer[I] - augmentItemInfoer[I] -} - -type augmentItemInfoer[I chatsItemer] interface { - // augmentItemInfo completes the teamChatsInfo population with any data - // owned by the container and not accessible to the item. - augmentItemInfo(*details.TeamsChatsInfo, I) -} - // gets all item IDs in the container type getItemIDser[I chatsItemer] interface { getItemIDs( ctx context.Context, - cc api.CallConfig, ) ([]I, error) } @@ -62,7 +50,7 @@ type getItemer[I chatsItemer] interface { getItem( ctx context.Context, protectedResource string, - itemID string, + i I, ) (I, *details.TeamsChatsInfo, error) } diff --git a/src/internal/m365/collection/teamschats/testdata/chats.go b/src/internal/m365/collection/teamschats/testdata/chats.go index 9d28009107..775f3cc6ff 100644 --- a/src/internal/m365/collection/teamschats/testdata/chats.go +++ b/src/internal/m365/collection/teamschats/testdata/chats.go @@ -11,11 +11,21 @@ func StubChats(ids ...string) []models.Chatable { sl := make([]models.Chatable, 0, len(ids)) for _, id := range ids { - ch := models.NewChat() - ch.SetTopic(ptr.To(id)) - ch.SetId(ptr.To(id)) + chat := models.NewChat() + chat.SetTopic(ptr.To(id)) + chat.SetId(ptr.To(id)) - sl = append(sl, ch) + // we should expect to get the latest message preview by default + lastMsgPrv := models.NewChatMessageInfo() + lastMsgPrv.SetId(ptr.To(uuid.NewString())) + + body := models.NewItemBody() + body.SetContent(ptr.To(id)) + lastMsgPrv.SetBody(body) + + chat.SetLastMessagePreview(lastMsgPrv) + + sl = append(sl, chat) } return sl @@ -24,17 +34,24 @@ func StubChats(ids ...string) []models.Chatable { func StubChatMessages(ids ...string) []models.ChatMessageable { sl := make([]models.ChatMessageable, 0, len(ids)) + var lastMsg models.ChatMessageable + for _, id := range ids { - cm := models.NewChatMessage() - cm.SetId(ptr.To(uuid.NewString())) + msg := models.NewChatMessage() + msg.SetId(ptr.To(uuid.NewString())) body := models.NewItemBody() body.SetContent(ptr.To(id)) - cm.SetBody(body) + msg.SetBody(body) - sl = append(sl, cm) + sl = append(sl, msg) + lastMsg = msg } + lastMsgPrv := models.NewChatMessageInfo() + lastMsgPrv.SetId(lastMsg.GetId()) + lastMsgPrv.SetBody(lastMsg.GetBody()) + return sl } diff --git a/src/pkg/backup/details/entry.go b/src/pkg/backup/details/entry.go index 0d040eb672..a020598d1f 100644 --- a/src/pkg/backup/details/entry.go +++ b/src/pkg/backup/details/entry.go @@ -103,6 +103,12 @@ func (de Entry) ToLocationIDer(backupVersion int) (LocationIDer, error) { } baseLoc = path.Builder{}.Append(p.Root).Append(p.Folders...) + + case TeamsChat: + baseLoc = &path.Builder{} + + default: + return nil, clues.New("undentified item type").With("item_type", de.ItemInfo.infoType()) } if baseLoc == nil { @@ -141,26 +147,23 @@ func (de Entry) MinimumPrintable() any { // Headers returns the human-readable names of properties in a DetailsEntry // for printing out to a terminal in a columnar display. func (de Entry) Headers(skipID bool) []string { - hs := []string{} + var hs []string - if de.ItemInfo.Folder != nil { + switch { + case de.ItemInfo.Folder != nil: hs = de.ItemInfo.Folder.Headers() - } - - if de.ItemInfo.Exchange != nil { + case de.ItemInfo.Exchange != nil: hs = de.ItemInfo.Exchange.Headers() - } - - if de.ItemInfo.SharePoint != nil { + case de.ItemInfo.SharePoint != nil: hs = de.ItemInfo.SharePoint.Headers() - } - - if de.ItemInfo.OneDrive != nil { + case de.ItemInfo.OneDrive != nil: hs = de.ItemInfo.OneDrive.Headers() - } - - if de.ItemInfo.Groups != nil { + case de.ItemInfo.Groups != nil: hs = de.ItemInfo.Groups.Headers() + case de.ItemInfo.TeamsChats != nil: + hs = de.ItemInfo.TeamsChats.Headers() + default: + hs = []string{"ERROR - Service not recognized"} } if skipID { @@ -172,26 +175,23 @@ func (de Entry) Headers(skipID bool) []string { // Values returns the values matching the Headers list. func (de Entry) Values(skipID bool) []string { - vs := []string{} + var vs []string - if de.ItemInfo.Folder != nil { + switch { + case de.ItemInfo.Folder != nil: vs = de.ItemInfo.Folder.Values() - } - - if de.ItemInfo.Exchange != nil { + case de.ItemInfo.Exchange != nil: vs = de.ItemInfo.Exchange.Values() - } - - if de.ItemInfo.SharePoint != nil { + case de.ItemInfo.SharePoint != nil: vs = de.ItemInfo.SharePoint.Values() - } - - if de.ItemInfo.OneDrive != nil { + case de.ItemInfo.OneDrive != nil: vs = de.ItemInfo.OneDrive.Values() - } - - if de.ItemInfo.Groups != nil { + case de.ItemInfo.Groups != nil: vs = de.ItemInfo.Groups.Values() + case de.ItemInfo.TeamsChats != nil: + vs = de.ItemInfo.TeamsChats.Values() + default: + vs = []string{"ERROR - Service not recognized"} } if skipID { diff --git a/src/pkg/backup/details/iteminfo.go b/src/pkg/backup/details/iteminfo.go index 22391d2d85..656d7d92a8 100644 --- a/src/pkg/backup/details/iteminfo.go +++ b/src/pkg/backup/details/iteminfo.go @@ -91,19 +91,14 @@ func (i ItemInfo) infoType() ItemType { switch { case i.Folder != nil: return i.Folder.ItemType - case i.Exchange != nil: return i.Exchange.ItemType - case i.SharePoint != nil: return i.SharePoint.ItemType - case i.OneDrive != nil: return i.OneDrive.ItemType - case i.Groups != nil: return i.Groups.ItemType - case i.TeamsChats != nil: return i.TeamsChats.ItemType } @@ -115,19 +110,14 @@ func (i ItemInfo) size() int64 { switch { case i.Exchange != nil: return i.Exchange.Size - case i.OneDrive != nil: return i.OneDrive.Size - case i.SharePoint != nil: return i.SharePoint.Size - case i.Groups != nil: return i.Groups.Size - case i.Folder != nil: return i.Folder.Size - case i.TeamsChats != nil: return int64(i.TeamsChats.Chat.MessageCount) } @@ -139,19 +129,14 @@ func (i ItemInfo) Modified() time.Time { switch { case i.Exchange != nil: return i.Exchange.Modified - case i.OneDrive != nil: return i.OneDrive.Modified - case i.SharePoint != nil: return i.SharePoint.Modified - case i.Groups != nil: return i.Groups.Modified - case i.Folder != nil: return i.Folder.Modified - case i.TeamsChats != nil: return i.TeamsChats.Modified } @@ -163,19 +148,14 @@ func (i ItemInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) { switch { case i.Exchange != nil: return i.Exchange.uniqueLocation(baseLoc) - case i.OneDrive != nil: return i.OneDrive.uniqueLocation(baseLoc) - case i.SharePoint != nil: return i.SharePoint.uniqueLocation(baseLoc) - case i.Groups != nil: return i.Groups.uniqueLocation(baseLoc) - case i.TeamsChats != nil: return i.TeamsChats.uniqueLocation(baseLoc) - default: return nil, clues.New("unsupported type") } @@ -185,19 +165,14 @@ func (i ItemInfo) updateFolder(f *FolderInfo) error { switch { case i.Exchange != nil: return i.Exchange.updateFolder(f) - case i.OneDrive != nil: return i.OneDrive.updateFolder(f) - case i.SharePoint != nil: return i.SharePoint.updateFolder(f) - case i.Groups != nil: return i.Groups.updateFolder(f) - case i.TeamsChats != nil: return i.TeamsChats.updateFolder(f) - default: return clues.New("unsupported type") } diff --git a/src/pkg/backup/details/teamsChats.go b/src/pkg/backup/details/teamsChats.go index 0974941a31..4cd1594921 100644 --- a/src/pkg/backup/details/teamsChats.go +++ b/src/pkg/backup/details/teamsChats.go @@ -44,7 +44,7 @@ type ChatInfo struct { LastMessagePreview string `json:"preview,omitempty"` Members []string `json:"members,omitempty"` MessageCount int `json:"messageCount,omitempty"` - Name string `json:"name,omitempty"` + Topic string `json:"topic,omitempty"` } // Headers returns the human-readable names of properties in a ChatsInfo @@ -52,7 +52,7 @@ type ChatInfo struct { func (i TeamsChatsInfo) Headers() []string { switch i.ItemType { case TeamsChat: - return []string{"Name", "Last message", "Last message at", "Message count", "Created", "Members"} + return []string{"Topic", "Last message", "Last message at", "Message count", "Created", "Members"} } return []string{} @@ -75,7 +75,7 @@ func (i TeamsChatsInfo) Values() []string { } return []string{ - i.Chat.Name, + i.Chat.Topic, i.Chat.LastMessagePreview, dttm.FormatToTabularDisplay(i.Chat.LastMessageAt), strconv.Itoa(i.Chat.MessageCount), diff --git a/src/pkg/backup/details/teamsChats_test.go b/src/pkg/backup/details/teamsChats_test.go index 476a323418..d3f646f1d0 100644 --- a/src/pkg/backup/details/teamsChats_test.go +++ b/src/pkg/backup/details/teamsChats_test.go @@ -42,7 +42,7 @@ func (suite *ChatsUnitSuite) TestChatsPrintable() { LastMessagePreview: "last message preview", Members: []string{"foo@bar.baz", "fnords@smarf.zoomba"}, MessageCount: 42, - Name: "chat name", + Topic: "chat name", }, }, expectHs: []string{"Name", "Last message", "Last message at", "Message count", "Created", "Members"}, diff --git a/src/pkg/selectors/teamsChats.go b/src/pkg/selectors/teamsChats.go index 60df1a08b6..3e749c7e20 100644 --- a/src/pkg/selectors/teamsChats.go +++ b/src/pkg/selectors/teamsChats.go @@ -9,6 +9,7 @@ import ( "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/identity" + "github.com/alcionai/corso/src/pkg/dttm" "github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/filters" "github.com/alcionai/corso/src/pkg/path" @@ -254,21 +255,81 @@ func (sr *TeamsChatsRestore) ChatMember(memberID string) []TeamsChatsScope { } } -// ChatName produces one or more teamsChats chat name info scopes. +// ChatTopic 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 { +func (sr *TeamsChatsRestore) ChatTopic(topic string) []TeamsChatsScope { return []TeamsChatsScope{ makeInfoScope[TeamsChatsScope]( TeamsChatsChat, - TeamsChatsInfoChatName, - []string{memberID}, + TeamsChatsInfoChatTopic, + []string{topic}, filters.In), } } +// ChatCreatedBefore produces one or more teamsChats chat name info scopes. +// Matches any chat whose creation datetime is before the given datetime. +// 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) ChatCreatedBefore(datetime string) []TeamsChatsScope { + return []TeamsChatsScope{ + makeInfoScope[TeamsChatsScope]( + TeamsChatsChat, + TeamsChatsInfoChatCreatedBefore, + []string{datetime}, + filters.Greater), + } +} + +// ChatCreatedBefore produces one or more teamsChats chat name info scopes. +// Matches any chat whose creation datetime is after the given datetime. +// 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) ChatCreatedAfter(datetime string) []TeamsChatsScope { + return []TeamsChatsScope{ + makeInfoScope[TeamsChatsScope]( + TeamsChatsChat, + TeamsChatsInfoChatCreatedAfter, + []string{datetime}, + filters.Less), + } +} + +// ChatLastMessageBefore produces one or more teamsChats chat name info scopes. +// Matches any chat whose most recent message (if it has messages) is before the given datetime. +// 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) ChatLastMessageBefore(datetime string) []TeamsChatsScope { + return []TeamsChatsScope{ + makeInfoScope[TeamsChatsScope]( + TeamsChatsChat, + TeamsChatsInfoChatLastMessageBefore, + []string{datetime}, + filters.Greater), + } +} + +// ChatCreatedBefore produces one or more teamsChats chat name info scopes. +// Matches any chat whose most recent message (if it has messages) is after the given datetime. +// 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) ChatLastMessageAfter(datetime string) []TeamsChatsScope { + return []TeamsChatsScope{ + makeInfoScope[TeamsChatsScope]( + TeamsChatsChat, + TeamsChatsInfoChatLastMesasgeAfter, + []string{datetime}, + filters.Less), + } +} + // --------------------------------------------------------------------------- // Categories // --------------------------------------------------------------------------- @@ -288,8 +349,12 @@ const ( TeamsChatsChat teamsChatsCategory = "TeamsChatsChat" // data contained within details.ItemInfo - TeamsChatsInfoChatMember teamsChatsCategory = "TeamsChatsInfoChatMember" - TeamsChatsInfoChatName teamsChatsCategory = "TeamsChatsInfoChatName" + TeamsChatsInfoChatCreatedBefore teamsChatsCategory = "TeamsChatsInfoChatCreatedBefore" + TeamsChatsInfoChatCreatedAfter teamsChatsCategory = "TeamsChatsInfoChatCreatedAfter" + TeamsChatsInfoChatLastMessageBefore teamsChatsCategory = "TeamsChatsInfoChatLastMessageBefore" + TeamsChatsInfoChatLastMesasgeAfter teamsChatsCategory = "TeamsChatsInfoChatLastMesasgeAfter" + TeamsChatsInfoChatMember teamsChatsCategory = "TeamsChatsInfoChatMember" + TeamsChatsInfoChatTopic teamsChatsCategory = "TeamsChatsInfoChatName" ) // teamsChatsLeafProperties describes common metadata of the leaf categories @@ -317,7 +382,9 @@ func (ec teamsChatsCategory) String() string { // Ex: TeamsChatsUser.leafCat() => TeamsChatsUser func (ec teamsChatsCategory) leafCat() categorizer { switch ec { - case TeamsChatsChat, TeamsChatsInfoChatMember, TeamsChatsInfoChatName: + case TeamsChatsChat, TeamsChatsInfoChatMember, TeamsChatsInfoChatTopic, + TeamsChatsInfoChatCreatedBefore, TeamsChatsInfoChatCreatedAfter, + TeamsChatsInfoChatLastMessageBefore, TeamsChatsInfoChatLastMesasgeAfter: return TeamsChatsChat } @@ -505,8 +572,16 @@ func (s TeamsChatsScope) matchesInfo(dii details.ItemInfo) bool { switch infoCat { case TeamsChatsInfoChatMember: i = strings.Join(info.Chat.Members, ",") - case TeamsChatsInfoChatName: - i = info.Chat.Name + case TeamsChatsInfoChatTopic: + i = info.Chat.Topic + case TeamsChatsInfoChatCreatedBefore, TeamsChatsInfoChatCreatedAfter: + i = dttm.Format(info.Chat.CreatedAt) + case TeamsChatsInfoChatLastMessageBefore, TeamsChatsInfoChatLastMesasgeAfter: + if info.Chat.MessageCount < 1 { + return false + } + + i = dttm.Format(info.Chat.LastMessageAt) } return s.Matches(infoCat, i) diff --git a/src/pkg/selectors/teamsChats_test.go b/src/pkg/selectors/teamsChats_test.go index f3b695494e..cc7f737275 100644 --- a/src/pkg/selectors/teamsChats_test.go +++ b/src/pkg/selectors/teamsChats_test.go @@ -12,6 +12,7 @@ import ( "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/backup/details" + "github.com/alcionai/corso/src/pkg/dttm" "github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/filters" "github.com/alcionai/corso/src/pkg/path" @@ -252,14 +253,16 @@ func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_MatchesInfo() { cs := NewTeamsChatsRestore(Any()) const ( - name = "smarf mcfnords" + topic = "smarf mcfnords" member = "cooks@2many.smarf" subject = "I have seen the fnords!" + dtype = details.TeamsChat ) var ( now = time.Now() - future = now.Add(1 * time.Minute) + past = dttm.Format(now.Add(-1 * time.Minute)) + future = dttm.Format(now.Add(1 * time.Minute)) ) infoWith := func(itype details.ItemType) details.ItemInfo { @@ -269,11 +272,11 @@ func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_MatchesInfo() { Chat: details.ChatInfo{ CreatedAt: now, HasExternalMembers: false, - LastMessageAt: future, + LastMessageAt: now, LastMessagePreview: "preview", Members: []string{member}, MessageCount: 1, - Name: name, + Topic: topic, }, }, } @@ -285,12 +288,20 @@ func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_MatchesInfo() { 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}, + {"chat with a different member", dtype, cs.ChatMember("blarps"), assert.False}, + {"chat with the same member", dtype, cs.ChatMember(member), assert.True}, + {"chat with a member submatch search", dtype, cs.ChatMember(member[2:5]), assert.True}, + {"chat with a different topic", dtype, cs.ChatTopic("blarps"), assert.False}, + {"chat with the same topic", dtype, cs.ChatTopic(topic), assert.True}, + {"chat with a subtopic search", dtype, cs.ChatTopic(topic[2:5]), assert.True}, + {"chat created after", dtype, cs.ChatCreatedAfter(past), assert.True}, + {"chat not created after", dtype, cs.ChatCreatedAfter(future), assert.False}, + {"chat created before", dtype, cs.ChatCreatedBefore(future), assert.True}, + {"chat not created before", dtype, cs.ChatCreatedBefore(past), assert.False}, + {"chat last message after", dtype, cs.ChatLastMessageAfter(past), assert.True}, + {"chat last message not after", dtype, cs.ChatLastMessageAfter(future), assert.False}, + {"chat last message before", dtype, cs.ChatLastMessageBefore(future), assert.True}, + {"chat last message not before", dtype, cs.ChatLastMessageBefore(past), assert.False}, } for _, test := range table { suite.Run(test.name, func() { diff --git a/src/pkg/services/m365/api/channels.go b/src/pkg/services/m365/api/channels.go index 89ec6505b3..5a6b8a2d46 100644 --- a/src/pkg/services/m365/api/channels.go +++ b/src/pkg/services/m365/api/channels.go @@ -162,7 +162,7 @@ func channelMessageInfo( modTime = lastReplyAt } - preview, contentLen, err := getChatMessageContentPreview(msg) + preview, contentLen, err := getChatMessageContentPreview(msg, msg) if err != nil { preview = "malformed or unparseable html" + preview } @@ -180,7 +180,7 @@ func channelMessageInfo( var lr details.ChannelMessageInfo if lastReply != nil { - preview, contentLen, err = getChatMessageContentPreview(lastReply) + preview, contentLen, err = getChatMessageContentPreview(lastReply, lastReply) if err != nil { preview = "malformed or unparseable html: " + preview } @@ -239,12 +239,28 @@ func GetChatMessageFrom(msg models.ChatMessageable) string { return "" } -func getChatMessageContentPreview(msg models.ChatMessageable) (string, int64, error) { - content, origSize, err := stripChatMessageHTML(msg) +// a hack for fulfilling getAttachmentser when the model doesn't +// provide GetAttachments() +type noAttachments struct{} + +func (noAttachments) GetAttachments() []models.ChatMessageAttachmentable { + return []models.ChatMessageAttachmentable{} +} + +type getBodyer interface { + GetBody() models.ItemBodyable +} + +type getAttachmentser interface { + GetAttachments() []models.ChatMessageAttachmentable +} + +func getChatMessageContentPreview(msg getBodyer, atts getAttachmentser) (string, int64, error) { + content, origSize, err := stripChatMessageHTML(msg, atts) return str.Preview(content, 128), origSize, clues.Stack(err).OrNil() } -func stripChatMessageHTML(msg models.ChatMessageable) (string, int64, error) { +func stripChatMessageHTML(msg getBodyer, atts getAttachmentser) (string, int64, error) { var ( content string origSize int64 @@ -256,7 +272,7 @@ func stripChatMessageHTML(msg models.ChatMessageable) (string, int64, error) { origSize = int64(len(content)) - content = replaceAttachmentMarkup(content, msg.GetAttachments()) + content = replaceAttachmentMarkup(content, atts.GetAttachments()) content, err := html2text.FromString(content) return content, origSize, clues.Stack(err).OrNil() diff --git a/src/pkg/services/m365/api/channels_test.go b/src/pkg/services/m365/api/channels_test.go index 8483d305ce..55e5e05255 100644 --- a/src/pkg/services/m365/api/channels_test.go +++ b/src/pkg/services/m365/api/channels_test.go @@ -712,7 +712,7 @@ func (suite *ChannelsAPIUnitSuite) TestStripChatMessageContent() { msg.SetAttachments(test.attachments) // not testing len; it's effectively covered by the content assertion - result, _, err := stripChatMessageHTML(msg) + result, _, err := stripChatMessageHTML(msg, msg) assert.Equal(t, test.expect, result) test.expectErr(t, err, clues.ToCore(err)) }) diff --git a/src/pkg/services/m365/api/teamdChats_test.go b/src/pkg/services/m365/api/teamdChats_test.go deleted file mode 100644 index db079420ed..0000000000 --- a/src/pkg/services/m365/api/teamdChats_test.go +++ /dev/null @@ -1,89 +0,0 @@ -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)) - } - }) - } -} diff --git a/src/pkg/services/m365/api/teamsChats.go b/src/pkg/services/m365/api/teamsChats.go index 1d3b7d0c6d..04e1b74590 100644 --- a/src/pkg/services/m365/api/teamsChats.go +++ b/src/pkg/services/m365/api/teamsChats.go @@ -2,6 +2,7 @@ package api import ( "context" + "time" "github.com/microsoftgraph/msgraph-sdk-go/chats" "github.com/microsoftgraph/msgraph-sdk-go/models" @@ -62,13 +63,47 @@ func (c Chats) GetChatByID( // --------------------------------------------------------------------------- func TeamsChatInfo(chat models.Chatable) *details.TeamsChatsInfo { + var ( + // in case of an empty chat, we want to use Val instead of OrNow + lastModTime = ptr.Val(chat.GetLastUpdatedDateTime()) + lastMsgPreview = chat.GetLastMessagePreview() + lastMsgCreatedAt time.Time + members = chat.GetMembers() + memberNames = []string{} + msgs = chat.GetMessages() + preview string + err error + ) + + if lastMsgPreview != nil { + preview, _, err = getChatMessageContentPreview(lastMsgPreview, noAttachments{}) + if err != nil { + preview = "malformed or unparseable html" + preview + } + + // in case of an empty mod time, we want to use the chat's mod time + // therefore Val instaed of OrNow + lastMsgCreatedAt = ptr.Val(lastMsgPreview.GetCreatedDateTime()) + if lastModTime.Before(lastMsgCreatedAt) { + lastModTime = lastMsgCreatedAt + } + } + + for _, m := range members { + memberNames = append(memberNames, ptr.Val(m.GetDisplayName())) + } + return &details.TeamsChatsInfo{ ItemType: details.TeamsChat, - Modified: ptr.OrNow(chat.GetLastUpdatedDateTime()), + Modified: lastModTime, Chat: details.ChatInfo{ - CreatedAt: ptr.OrNow(chat.GetCreatedDateTime()), - Name: ptr.Val(chat.GetTopic()), + CreatedAt: ptr.OrNow(chat.GetCreatedDateTime()), + LastMessageAt: lastMsgCreatedAt, + LastMessagePreview: preview, + Members: memberNames, + MessageCount: len(msgs), + Topic: ptr.Val(chat.GetTopic()), }, } } diff --git a/src/pkg/services/m365/api/teamsChats_pager.go b/src/pkg/services/m365/api/teamsChats_pager.go index 0268530eec..77bee30e58 100644 --- a/src/pkg/services/m365/api/teamsChats_pager.go +++ b/src/pkg/services/m365/api/teamsChats_pager.go @@ -12,6 +12,79 @@ import ( "github.com/alcionai/corso/src/pkg/services/m365/api/pagers" ) +// --------------------------------------------------------------------------- +// chat members pager +// --------------------------------------------------------------------------- + +// delta queries are not supported +var _ pagers.NonDeltaHandler[models.ConversationMemberable] = &chatMembersPageCtrl{} + +type chatMembersPageCtrl struct { + chatID string + gs graph.Servicer + builder *chats.ItemMembersRequestBuilder + options *chats.ItemMembersRequestBuilderGetRequestConfiguration +} + +func (p *chatMembersPageCtrl) SetNextLink(nextLink string) { + p.builder = chats.NewItemMembersRequestBuilder(nextLink, p.gs.Adapter()) +} + +func (p *chatMembersPageCtrl) GetPage( + ctx context.Context, +) (pagers.NextLinkValuer[models.ConversationMemberable], error) { + resp, err := p.builder.Get(ctx, p.options) + return resp, graph.Stack(ctx, err).OrNil() +} + +func (p *chatMembersPageCtrl) ValidModTimes() bool { + return true +} + +func (c Chats) NewChatMembersPager( + chatID string, + cc CallConfig, +) *chatMembersPageCtrl { + builder := c.Stable. + Client(). + Chats(). + ByChatId(chatID). + Members() + + options := &chats.ItemMembersRequestBuilderGetRequestConfiguration{ + QueryParameters: &chats.ItemMembersRequestBuilderGetQueryParameters{}, + 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 &chatMembersPageCtrl{ + chatID: chatID, + builder: builder, + gs: c.Stable, + options: options, + } +} + +// GetChatMembers fetches a delta of all members in the chat. +func (c Chats) GetChatMembers( + ctx context.Context, + chatID string, + cc CallConfig, +) ([]models.ConversationMemberable, error) { + ctx = clues.Add(ctx, "chat_id", chatID) + pager := c.NewChatMembersPager(chatID, cc) + items, err := pagers.BatchEnumerateItems[models.ConversationMemberable](ctx, pager) + + return items, graph.Stack(ctx, err).OrNil() +} + // --------------------------------------------------------------------------- // chat message pager // --------------------------------------------------------------------------- @@ -85,26 +158,6 @@ func (c Chats) GetChatMessages( 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 // --------------------------------------------------------------------------- diff --git a/src/pkg/services/m365/api/teamsChats_pager_test.go b/src/pkg/services/m365/api/teamsChats_pager_test.go index c74d5f733a..b276b245ec 100644 --- a/src/pkg/services/m365/api/teamsChats_pager_test.go +++ b/src/pkg/services/m365/api/teamsChats_pager_test.go @@ -61,6 +61,11 @@ func (suite *ChatsPagerIntgSuite) TestEnumerateChats() { ac, chatID, chat.GetLastMessagePreview()) + + testEnumerateChatMembers( + suite.T(), + ac, + chatID) }) } } @@ -123,3 +128,22 @@ func testEnumerateChatMessages( } } } + +func testEnumerateChatMembers( + t *testing.T, + ac Chats, + chatID string, +) { + ctx, flush := tester.NewContext(t) + defer flush() + + cc := CallConfig{} + + members, err := ac.GetChatMembers(ctx, chatID, cc) + require.NoError(t, err, clues.ToCore(err)) + + // no good way to test members right now. Even though + // the graph api response contains the `userID` and `email` + // properties, we can't access them in the sdk model + assert.NotEmpty(t, members) +} diff --git a/src/pkg/services/m365/api/teamsChats_test.go b/src/pkg/services/m365/api/teamsChats_test.go new file mode 100644 index 0000000000..2f17cff924 --- /dev/null +++ b/src/pkg/services/m365/api/teamsChats_test.go @@ -0,0 +1,158 @@ +package api + +import ( + "testing" + "time" + + "github.com/google/uuid" + "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/m365/collection/teamschats/testdata" + "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 + expected func() (models.Chatable, *details.TeamsChatsInfo) + }{ + { + name: "Empty chat", + expected: func() (models.Chatable, *details.TeamsChatsInfo) { + chat := models.NewChat() + + i := &details.TeamsChatsInfo{ + ItemType: details.TeamsChat, + Modified: ptr.Val(chat.GetLastUpdatedDateTime()), + Chat: details.ChatInfo{}, + } + + return chat, i + }, + }, + { + name: "All fields", + expected: func() (models.Chatable, *details.TeamsChatsInfo) { + now := time.Now() + then := now.Add(1 * time.Hour) + id := uuid.NewString() + + chat := testdata.StubChats(id)[0] + chat.SetTopic(ptr.To("Hello world")) + chat.SetCreatedDateTime(&now) + chat.SetLastUpdatedDateTime(&now) + chat.GetLastMessagePreview().SetCreatedDateTime(&then) + + msgs := testdata.StubChatMessages(ptr.Val(chat.GetLastMessagePreview().GetId())) + chat.SetMessages(msgs) + + i := &details.TeamsChatsInfo{ + ItemType: details.TeamsChat, + Modified: then, + Chat: details.ChatInfo{ + Topic: "Hello world", + LastMessageAt: then, + LastMessagePreview: id, + Members: []string{}, + MessageCount: 1, + }, + } + + return chat, i + }, + }, + { + name: "last message preview, but no messages", + expected: func() (models.Chatable, *details.TeamsChatsInfo) { + now := time.Now() + then := now.Add(1 * time.Hour) + id := uuid.NewString() + + chat := testdata.StubChats(id)[0] + chat.SetTopic(ptr.To("Hello world")) + chat.SetCreatedDateTime(&now) + chat.SetLastUpdatedDateTime(&now) + chat.GetLastMessagePreview().SetCreatedDateTime(&then) + + i := &details.TeamsChatsInfo{ + ItemType: details.TeamsChat, + Modified: then, + Chat: details.ChatInfo{ + Topic: "Hello world", + LastMessageAt: then, + LastMessagePreview: id, + Members: []string{}, + MessageCount: 0, + }, + } + + return chat, i + }, + }, + { + name: "chat only, no messages", + expected: func() (models.Chatable, *details.TeamsChatsInfo) { + now := time.Now() + then := now.Add(1 * time.Hour) + + chat := testdata.StubChats(uuid.NewString())[0] + chat.SetTopic(ptr.To("Hello world")) + chat.SetCreatedDateTime(&now) + chat.SetLastUpdatedDateTime(&then) + chat.SetLastMessagePreview(nil) + chat.SetMessages(nil) + + i := &details.TeamsChatsInfo{ + ItemType: details.TeamsChat, + Modified: then, + Chat: details.ChatInfo{ + Topic: "Hello world", + LastMessageAt: time.Time{}, + LastMessagePreview: "", + Members: []string{}, + MessageCount: 0, + }, + } + + return chat, i + }, + }, + } + for _, test := range tests { + suite.Run(test.name, func() { + t := suite.T() + chat, expected := test.expected() + result := TeamsChatInfo(chat) + + assert.Equal(t, expected.Chat.Topic, result.Chat.Topic) + + expectCreated := chat.GetCreatedDateTime() + if expectCreated != nil { + assert.Equal(t, ptr.Val(expectCreated), result.Chat.CreatedAt) + } else { + assert.True(t, result.Chat.CreatedAt.After(start)) + } + + assert.Truef( + t, + expected.Modified.Equal(result.Modified), + "modified time doesn't match\nexpected %v\ngot %v", + expected.Modified, + result.Modified) + }) + } +}