boilerplate introduction of the conversations api (#4570)
adds basic get requests for the conversations api set to the api package. Plust some other minor change for formatting, correctness, or necessity. --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🌻 Feature #### Issue(s) * #4536 #### Test Plan - [x] 💪 Manual - [x] ⚡ Unit test - [x] 💚 E2E
This commit is contained in:
parent
dddd8ac722
commit
3261eefda2
@ -738,6 +738,7 @@ func verifyExtensionData(
|
|||||||
case path.OneDriveService:
|
case path.OneDriveService:
|
||||||
detailsSize = itemInfo.OneDrive.Size
|
detailsSize = itemInfo.OneDrive.Size
|
||||||
case path.GroupsService:
|
case path.GroupsService:
|
||||||
|
// FIXME: needs update for message.
|
||||||
detailsSize = itemInfo.Groups.Size
|
detailsSize = itemInfo.Groups.Size
|
||||||
default:
|
default:
|
||||||
assert.Fail(t, "unrecognized data type")
|
assert.Fail(t, "unrecognized data type")
|
||||||
|
|||||||
@ -42,8 +42,11 @@ type GroupsInfo struct {
|
|||||||
Modified time.Time `json:"modified,omitempty"`
|
Modified time.Time `json:"modified,omitempty"`
|
||||||
|
|
||||||
// Channels Specific
|
// Channels Specific
|
||||||
Message ChannelMessageInfo `json:"message"`
|
Message ChannelMessageInfo `json:"message,omitempty"`
|
||||||
LastReply ChannelMessageInfo `json:"lastReply"`
|
LastReply ChannelMessageInfo `json:"lastReply,omitempty"`
|
||||||
|
|
||||||
|
// Conversations Specific
|
||||||
|
Post ConversationPostInfo `json:"post,omitempty"`
|
||||||
|
|
||||||
// SharePoint specific
|
// SharePoint specific
|
||||||
Created time.Time `json:"created,omitempty"`
|
Created time.Time `json:"created,omitempty"`
|
||||||
@ -57,6 +60,14 @@ type GroupsInfo struct {
|
|||||||
WebURL string `json:"webURL,omitempty"`
|
WebURL string `json:"webURL,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConversationPostInfo struct {
|
||||||
|
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||||
|
Creator string `json:"creator,omitempty"`
|
||||||
|
Preview string `json:"preview,omitempty"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
Subject string `json:"subject,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type ChannelMessageInfo struct {
|
type ChannelMessageInfo struct {
|
||||||
AttachmentNames []string `json:"attachmentNames,omitempty"`
|
AttachmentNames []string `json:"attachmentNames,omitempty"`
|
||||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||||
|
|||||||
@ -39,7 +39,8 @@ const (
|
|||||||
FolderItem ItemType = 306
|
FolderItem ItemType = 306
|
||||||
|
|
||||||
// Groups/Teams(40x)
|
// Groups/Teams(40x)
|
||||||
GroupsChannelMessage ItemType = 401
|
GroupsChannelMessage ItemType = 401
|
||||||
|
GroupsConversationPost ItemType = 402
|
||||||
)
|
)
|
||||||
|
|
||||||
func UpdateItem(item *ItemInfo, newLocPath *path.Builder) {
|
func UpdateItem(item *ItemInfo, newLocPath *path.Builder) {
|
||||||
|
|||||||
@ -52,11 +52,8 @@ func (c Channels) GetChannel(
|
|||||||
Channels().
|
Channels().
|
||||||
ByChannelId(containerID).
|
ByChannelId(containerID).
|
||||||
Get(ctx, config)
|
Get(ctx, config)
|
||||||
if err != nil {
|
|
||||||
return nil, graph.Stack(ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, nil
|
return resp, graph.Stack(ctx, err).OrNil()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetChannelByName fetches a channel by name
|
// GetChannelByName fetches a channel by name
|
||||||
|
|||||||
@ -76,7 +76,6 @@ func (c Channels) NewChannelMessagePager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetChannelMessages fetches a delta of all messages in the channel.
|
// GetChannelMessages fetches a delta of all messages in the channel.
|
||||||
// returns two maps: addedItems, deletedItems
|
|
||||||
func (c Channels) GetChannelMessages(
|
func (c Channels) GetChannelMessages(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
teamID, channelID string,
|
teamID, channelID string,
|
||||||
|
|||||||
130
src/pkg/services/m365/api/conversations.go
Normal file
130
src/pkg/services/m365/api/conversations.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/jaytaylor/html2text"
|
||||||
|
"github.com/microsoftgraph/msgraph-sdk-go/groups"
|
||||||
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
|
"github.com/alcionai/corso/src/internal/common/str"
|
||||||
|
"github.com/alcionai/corso/src/internal/m365/graph"
|
||||||
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// controller
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func (c Client) Conversations() Conversations {
|
||||||
|
return Conversations{c}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversations is an interface-compliant provider of the client.
|
||||||
|
type Conversations struct {
|
||||||
|
Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Item (conversation thread post)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func (c Conversations) GetConversationPost(
|
||||||
|
ctx context.Context,
|
||||||
|
groupID, conversationID, threadID, postID string,
|
||||||
|
cc CallConfig,
|
||||||
|
) (models.Postable, *details.GroupsInfo, error) {
|
||||||
|
config := &groups.ItemConversationsItemThreadsItemPostsPostItemRequestBuilderGetRequestConfiguration{
|
||||||
|
QueryParameters: &groups.ItemConversationsItemThreadsItemPostsPostItemRequestBuilderGetQueryParameters{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cc.Select) > 0 {
|
||||||
|
config.QueryParameters.Select = cc.Select
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cc.Expand) > 0 {
|
||||||
|
config.QueryParameters.Expand = append(config.QueryParameters.Expand, cc.Expand...)
|
||||||
|
}
|
||||||
|
|
||||||
|
post, err := c.Stable.
|
||||||
|
Client().
|
||||||
|
Groups().
|
||||||
|
ByGroupId(groupID).
|
||||||
|
Conversations().
|
||||||
|
ByConversationId(conversationID).
|
||||||
|
Threads().
|
||||||
|
ByConversationThreadId(threadID).
|
||||||
|
Posts().
|
||||||
|
ByPostId(postID).
|
||||||
|
Get(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, graph.Stack(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return post, conversationPostInfo(post), graph.Stack(ctx, err).OrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func conversationPostInfo(
|
||||||
|
post models.Postable,
|
||||||
|
) *details.GroupsInfo {
|
||||||
|
if post == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
preview, contentLen, err := getConversationPostContentPreview(post)
|
||||||
|
if err != nil {
|
||||||
|
preview = "malformed or unparseable html" + preview
|
||||||
|
}
|
||||||
|
|
||||||
|
var sender string
|
||||||
|
if post.GetSender() != nil && post.GetSender().GetEmailAddress() != nil {
|
||||||
|
sender = ptr.Val(post.GetSender().GetEmailAddress().GetAddress())
|
||||||
|
}
|
||||||
|
|
||||||
|
size := contentLen
|
||||||
|
|
||||||
|
for _, a := range post.GetAttachments() {
|
||||||
|
size += int64(ptr.Val(a.GetSize()))
|
||||||
|
}
|
||||||
|
|
||||||
|
cpi := details.ConversationPostInfo{
|
||||||
|
CreatedAt: ptr.Val(post.GetCreatedDateTime()),
|
||||||
|
Creator: sender,
|
||||||
|
Preview: preview,
|
||||||
|
Size: size,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &details.GroupsInfo{
|
||||||
|
ItemType: details.GroupsConversationPost,
|
||||||
|
Modified: ptr.Val(post.GetLastModifiedDateTime()),
|
||||||
|
Post: cpi,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConversationPostContentPreview(post models.Postable) (string, int64, error) {
|
||||||
|
content, origSize, err := stripConversationPostHTML(post)
|
||||||
|
return str.Preview(content, 128), origSize, clues.Stack(err).OrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripConversationPostHTML(post models.Postable) (string, int64, error) {
|
||||||
|
var (
|
||||||
|
content string
|
||||||
|
origSize int64
|
||||||
|
)
|
||||||
|
|
||||||
|
if post.GetBody() != nil {
|
||||||
|
content = ptr.Val(post.GetBody().GetContent())
|
||||||
|
}
|
||||||
|
|
||||||
|
origSize = int64(len(content))
|
||||||
|
|
||||||
|
content, err := html2text.FromString(content)
|
||||||
|
|
||||||
|
return content, origSize, clues.Stack(err).OrNil()
|
||||||
|
}
|
||||||
232
src/pkg/services/m365/api/conversations_pager.go
Normal file
232
src/pkg/services/m365/api/conversations_pager.go
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/microsoftgraph/msgraph-sdk-go/groups"
|
||||||
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/m365/graph"
|
||||||
|
"github.com/alcionai/corso/src/pkg/services/m365/api/pagers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// conversation pager
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
var _ pagers.NonDeltaHandler[models.Conversationable] = &conversationsPageCtrl{}
|
||||||
|
|
||||||
|
type conversationsPageCtrl struct {
|
||||||
|
resourceID string
|
||||||
|
gs graph.Servicer
|
||||||
|
builder *groups.ItemConversationsRequestBuilder
|
||||||
|
options *groups.ItemConversationsRequestBuilderGetRequestConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *conversationsPageCtrl) SetNextLink(nextLink string) {
|
||||||
|
p.builder = groups.NewItemConversationsRequestBuilder(nextLink, p.gs.Adapter())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *conversationsPageCtrl) GetPage(
|
||||||
|
ctx context.Context,
|
||||||
|
) (pagers.NextLinkValuer[models.Conversationable], error) {
|
||||||
|
resp, err := p.builder.Get(ctx, p.options)
|
||||||
|
return resp, graph.Stack(ctx, err).OrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *conversationsPageCtrl) ValidModTimes() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Conversations) NewConversationsPager(
|
||||||
|
groupID string,
|
||||||
|
cc CallConfig,
|
||||||
|
) *conversationsPageCtrl {
|
||||||
|
builder := c.Stable.
|
||||||
|
Client().
|
||||||
|
Groups().
|
||||||
|
ByGroupId(groupID).
|
||||||
|
Conversations()
|
||||||
|
|
||||||
|
options := &groups.ItemConversationsRequestBuilderGetRequestConfiguration{
|
||||||
|
QueryParameters: &groups.ItemConversationsRequestBuilderGetQueryParameters{},
|
||||||
|
Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cc.Select) > 0 {
|
||||||
|
options.QueryParameters.Select = cc.Select
|
||||||
|
}
|
||||||
|
|
||||||
|
return &conversationsPageCtrl{
|
||||||
|
resourceID: groupID,
|
||||||
|
builder: builder,
|
||||||
|
gs: c.Stable,
|
||||||
|
options: options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConversations fetches all conversations in the group.
|
||||||
|
func (c Conversations) GetConversations(
|
||||||
|
ctx context.Context,
|
||||||
|
groupID string,
|
||||||
|
cc CallConfig,
|
||||||
|
) ([]models.Conversationable, error) {
|
||||||
|
pager := c.NewConversationsPager(groupID, cc)
|
||||||
|
items, err := pagers.BatchEnumerateItems[models.Conversationable](ctx, pager)
|
||||||
|
|
||||||
|
return items, graph.Stack(ctx, err).OrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// conversation thread pager
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
var _ pagers.NonDeltaHandler[models.ConversationThreadable] = &conversationThreadsPageCtrl{}
|
||||||
|
|
||||||
|
type conversationThreadsPageCtrl struct {
|
||||||
|
resourceID, conversationID string
|
||||||
|
gs graph.Servicer
|
||||||
|
builder *groups.ItemConversationsItemThreadsRequestBuilder
|
||||||
|
options *groups.ItemConversationsItemThreadsRequestBuilderGetRequestConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *conversationThreadsPageCtrl) SetNextLink(nextLink string) {
|
||||||
|
p.builder = groups.NewItemConversationsItemThreadsRequestBuilder(nextLink, p.gs.Adapter())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *conversationThreadsPageCtrl) GetPage(
|
||||||
|
ctx context.Context,
|
||||||
|
) (pagers.NextLinkValuer[models.ConversationThreadable], error) {
|
||||||
|
resp, err := p.builder.Get(ctx, p.options)
|
||||||
|
return resp, graph.Stack(ctx, err).OrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *conversationThreadsPageCtrl) ValidModTimes() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Conversations) NewConversationThreadsPager(
|
||||||
|
groupID, conversationID string,
|
||||||
|
cc CallConfig,
|
||||||
|
) *conversationThreadsPageCtrl {
|
||||||
|
builder := c.Stable.
|
||||||
|
Client().
|
||||||
|
Groups().
|
||||||
|
ByGroupId(groupID).
|
||||||
|
Conversations().
|
||||||
|
ByConversationId(conversationID).
|
||||||
|
Threads()
|
||||||
|
|
||||||
|
options := &groups.ItemConversationsItemThreadsRequestBuilderGetRequestConfiguration{
|
||||||
|
QueryParameters: &groups.ItemConversationsItemThreadsRequestBuilderGetQueryParameters{},
|
||||||
|
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 &conversationThreadsPageCtrl{
|
||||||
|
resourceID: groupID,
|
||||||
|
conversationID: conversationID,
|
||||||
|
builder: builder,
|
||||||
|
gs: c.Stable,
|
||||||
|
options: options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConversations fetches all conversations in the group.
|
||||||
|
func (c Conversations) GetConversationThreads(
|
||||||
|
ctx context.Context,
|
||||||
|
groupID, conversationID string,
|
||||||
|
cc CallConfig,
|
||||||
|
) ([]models.ConversationThreadable, error) {
|
||||||
|
ctx = clues.Add(ctx, "conversation_id", conversationID)
|
||||||
|
pager := c.NewConversationThreadsPager(groupID, conversationID, cc)
|
||||||
|
items, err := pagers.BatchEnumerateItems[models.ConversationThreadable](ctx, pager)
|
||||||
|
|
||||||
|
return items, graph.Stack(ctx, err).OrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// conversation thread post pager
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
var _ pagers.NonDeltaHandler[models.Postable] = &conversationThreadPostsPageCtrl{}
|
||||||
|
|
||||||
|
type conversationThreadPostsPageCtrl struct {
|
||||||
|
resourceID, conversationID, threadID string
|
||||||
|
gs graph.Servicer
|
||||||
|
builder *groups.ItemConversationsItemThreadsItemPostsRequestBuilder
|
||||||
|
options *groups.ItemConversationsItemThreadsItemPostsRequestBuilderGetRequestConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *conversationThreadPostsPageCtrl) SetNextLink(nextLink string) {
|
||||||
|
p.builder = groups.NewItemConversationsItemThreadsItemPostsRequestBuilder(nextLink, p.gs.Adapter())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *conversationThreadPostsPageCtrl) GetPage(
|
||||||
|
ctx context.Context,
|
||||||
|
) (pagers.NextLinkValuer[models.Postable], error) {
|
||||||
|
resp, err := p.builder.Get(ctx, p.options)
|
||||||
|
return resp, graph.Stack(ctx, err).OrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *conversationThreadPostsPageCtrl) ValidModTimes() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Conversations) NewConversationThreadPostsPager(
|
||||||
|
groupID, conversationID, threadID string,
|
||||||
|
cc CallConfig,
|
||||||
|
) *conversationThreadPostsPageCtrl {
|
||||||
|
builder := c.Stable.
|
||||||
|
Client().
|
||||||
|
Groups().
|
||||||
|
ByGroupId(groupID).
|
||||||
|
Conversations().
|
||||||
|
ByConversationId(conversationID).
|
||||||
|
Threads().
|
||||||
|
ByConversationThreadId(threadID).
|
||||||
|
Posts()
|
||||||
|
|
||||||
|
options := &groups.ItemConversationsItemThreadsItemPostsRequestBuilderGetRequestConfiguration{
|
||||||
|
QueryParameters: &groups.ItemConversationsItemThreadsItemPostsRequestBuilderGetQueryParameters{},
|
||||||
|
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 &conversationThreadPostsPageCtrl{
|
||||||
|
resourceID: groupID,
|
||||||
|
conversationID: conversationID,
|
||||||
|
threadID: threadID,
|
||||||
|
builder: builder,
|
||||||
|
gs: c.Stable,
|
||||||
|
options: options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConversations fetches all conversations in the group.
|
||||||
|
func (c Conversations) GetConversationThreadPosts(
|
||||||
|
ctx context.Context,
|
||||||
|
groupID, conversationID, threadID string,
|
||||||
|
cc CallConfig,
|
||||||
|
) ([]models.Postable, error) {
|
||||||
|
ctx = clues.Add(ctx, "conversation_id", conversationID)
|
||||||
|
pager := c.NewConversationThreadPostsPager(groupID, conversationID, threadID, cc)
|
||||||
|
items, err := pagers.BatchEnumerateItems[models.Postable](ctx, pager)
|
||||||
|
|
||||||
|
return items, graph.Stack(ctx, err).OrNil()
|
||||||
|
}
|
||||||
120
src/pkg/services/m365/api/conversations_pager_test.go
Normal file
120
src/pkg/services/m365/api/conversations_pager_test.go
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
|
"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 ConversationsPagerIntgSuite struct {
|
||||||
|
tester.Suite
|
||||||
|
its intgTesterSetup
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConversationsPagerIntgSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &ConversationsPagerIntgSuite{
|
||||||
|
Suite: tester.NewIntegrationSuite(
|
||||||
|
t,
|
||||||
|
[][]string{tconfig.M365AcctCredEnvs}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConversationsPagerIntgSuite) SetupSuite() {
|
||||||
|
suite.its = newIntegrationTesterSetup(suite.T())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConversationsPagerIntgSuite) TestEnumerateConversations_withThreadsAndPosts() {
|
||||||
|
var (
|
||||||
|
t = suite.T()
|
||||||
|
ac = suite.its.ac.Conversations()
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
convs, err := ac.GetConversations(ctx, suite.its.group.id, CallConfig{})
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
require.NotEmpty(t, convs)
|
||||||
|
|
||||||
|
for _, conv := range convs {
|
||||||
|
threads := testEnumerateConvThreads(suite, conv)
|
||||||
|
|
||||||
|
for _, thread := range threads {
|
||||||
|
posts := testEnumerateConvPosts(suite, conv, thread)
|
||||||
|
|
||||||
|
for _, post := range posts {
|
||||||
|
testGetPostByID(suite, conv, thread, post)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEnumerateConvThreads(
|
||||||
|
suite *ConversationsPagerIntgSuite,
|
||||||
|
conv models.Conversationable,
|
||||||
|
) []models.ConversationThreadable {
|
||||||
|
var threads []models.ConversationThreadable
|
||||||
|
|
||||||
|
suite.Run("threads", func() {
|
||||||
|
var (
|
||||||
|
t = suite.T()
|
||||||
|
ac = suite.its.ac.Conversations()
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
threads, err = ac.GetConversationThreads(
|
||||||
|
ctx,
|
||||||
|
suite.its.group.id,
|
||||||
|
ptr.Val(conv.GetId()),
|
||||||
|
CallConfig{})
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
// to the best of our knowledge, there's only ever one
|
||||||
|
// thread per conversation. Even trying to create a new
|
||||||
|
// thread within a conversation will create an entirely
|
||||||
|
// new conversation. We want this test to fail as a potential
|
||||||
|
// identifier if that changes on us.
|
||||||
|
require.Equal(t, 1, len(threads))
|
||||||
|
})
|
||||||
|
|
||||||
|
return threads
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEnumerateConvPosts(
|
||||||
|
suite *ConversationsPagerIntgSuite,
|
||||||
|
conv models.Conversationable,
|
||||||
|
thread models.ConversationThreadable,
|
||||||
|
) []models.Postable {
|
||||||
|
var posts []models.Postable
|
||||||
|
|
||||||
|
suite.Run("posts", func() {
|
||||||
|
var (
|
||||||
|
t = suite.T()
|
||||||
|
ac = suite.its.ac.Conversations()
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
posts, err = ac.GetConversationThreadPosts(
|
||||||
|
ctx,
|
||||||
|
suite.its.group.id,
|
||||||
|
ptr.Val(conv.GetId()),
|
||||||
|
ptr.Val(thread.GetId()),
|
||||||
|
CallConfig{})
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
require.NotEmpty(t, posts)
|
||||||
|
})
|
||||||
|
|
||||||
|
return posts
|
||||||
|
}
|
||||||
267
src/pkg/services/m365/api/conversations_test.go
Normal file
267
src/pkg/services/m365/api/conversations_test.go
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/h2non/gock"
|
||||||
|
"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"
|
||||||
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||||
|
)
|
||||||
|
|
||||||
|
// called by the pager test, since it is already enumerating
|
||||||
|
// posts.
|
||||||
|
func testGetPostByID(
|
||||||
|
suite *ConversationsPagerIntgSuite,
|
||||||
|
conv models.Conversationable,
|
||||||
|
thread models.ConversationThreadable,
|
||||||
|
post models.Postable,
|
||||||
|
) {
|
||||||
|
suite.Run("post_by_id", func() {
|
||||||
|
var (
|
||||||
|
t = suite.T()
|
||||||
|
ac = suite.its.ac.Conversations()
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
resp, _, err := ac.GetConversationPost(
|
||||||
|
ctx,
|
||||||
|
suite.its.group.id,
|
||||||
|
ptr.Val(conv.GetId()),
|
||||||
|
ptr.Val(thread.GetId()),
|
||||||
|
ptr.Val(post.GetId()),
|
||||||
|
CallConfig{})
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
require.Equal(t, ptr.Val(post.GetId()), ptr.Val(resp.GetId()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConversationsAPIUnitSuite struct {
|
||||||
|
tester.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConversationsAPIUnitSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &ConversationsAPIUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConversationsAPIUnitSuite) TestConversationPostInfo() {
|
||||||
|
var (
|
||||||
|
now = time.Now()
|
||||||
|
content = "content"
|
||||||
|
body = models.NewItemBody()
|
||||||
|
)
|
||||||
|
|
||||||
|
body.SetContent(ptr.To(content))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
postAndInfo func() (models.Postable, *details.GroupsInfo)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "No body",
|
||||||
|
postAndInfo: func() (models.Postable, *details.GroupsInfo) {
|
||||||
|
post := models.NewPost()
|
||||||
|
post.SetCreatedDateTime(&now)
|
||||||
|
post.SetLastModifiedDateTime(&now)
|
||||||
|
|
||||||
|
sender := "foo@bar.com"
|
||||||
|
sea := models.NewEmailAddress()
|
||||||
|
sea.SetAddress(&sender)
|
||||||
|
|
||||||
|
recipient := models.NewRecipient()
|
||||||
|
recipient.SetEmailAddress(sea)
|
||||||
|
|
||||||
|
post.SetSender(recipient)
|
||||||
|
|
||||||
|
iden := models.NewIdentity()
|
||||||
|
iden.SetDisplayName(ptr.To("user"))
|
||||||
|
|
||||||
|
i := &details.GroupsInfo{
|
||||||
|
ItemType: details.GroupsConversationPost,
|
||||||
|
Modified: now,
|
||||||
|
Post: details.ConversationPostInfo{
|
||||||
|
CreatedAt: now,
|
||||||
|
Creator: "foo@bar.com",
|
||||||
|
Preview: "",
|
||||||
|
Size: 0,
|
||||||
|
// TODO: feed the subject in from the conversation
|
||||||
|
Subject: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return post, i
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
chMsg, expected := test.postAndInfo()
|
||||||
|
result := conversationPostInfo(chMsg)
|
||||||
|
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConversationAPIIntgSuite struct {
|
||||||
|
tester.Suite
|
||||||
|
its intgTesterSetup
|
||||||
|
}
|
||||||
|
|
||||||
|
// We do end up mocking the actual request, but creating the rest
|
||||||
|
// similar to full integration tests.
|
||||||
|
func TestConversationAPIIntgSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &ConversationAPIIntgSuite{
|
||||||
|
Suite: tester.NewIntegrationSuite(
|
||||||
|
t,
|
||||||
|
[][]string{tconfig.M365AcctCredEnvs}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConversationAPIIntgSuite) SetupSuite() {
|
||||||
|
suite.its = newIntegrationTesterSetup(suite.T())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConversationAPIIntgSuite) TestConversations_attachmentListDownload() {
|
||||||
|
pid := "fake-post-id"
|
||||||
|
aid := "fake-attachment-id"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupf func()
|
||||||
|
attachmentCount int
|
||||||
|
size int64
|
||||||
|
expect assert.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no attachments",
|
||||||
|
setupf: func() {
|
||||||
|
itm := models.NewPost()
|
||||||
|
itm.SetId(&pid)
|
||||||
|
|
||||||
|
interceptV1Path(
|
||||||
|
"groups",
|
||||||
|
"group",
|
||||||
|
"conversations",
|
||||||
|
"conv",
|
||||||
|
"threads",
|
||||||
|
"thread",
|
||||||
|
"posts",
|
||||||
|
pid).
|
||||||
|
Reply(200).
|
||||||
|
JSON(requireParseableToMap(suite.T(), itm))
|
||||||
|
},
|
||||||
|
expect: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fetch with attachment",
|
||||||
|
setupf: func() {
|
||||||
|
itm := models.NewPost()
|
||||||
|
itm.SetId(&pid)
|
||||||
|
itm.SetHasAttachments(ptr.To(true))
|
||||||
|
|
||||||
|
attch := models.NewAttachment()
|
||||||
|
attch.SetSize(ptr.To[int32](50))
|
||||||
|
|
||||||
|
itm.SetAttachments([]models.Attachmentable{attch})
|
||||||
|
|
||||||
|
interceptV1Path(
|
||||||
|
"groups",
|
||||||
|
"group",
|
||||||
|
"conversations",
|
||||||
|
"conv",
|
||||||
|
"threads",
|
||||||
|
"thread",
|
||||||
|
"posts",
|
||||||
|
pid).
|
||||||
|
Reply(200).
|
||||||
|
JSON(requireParseableToMap(suite.T(), itm))
|
||||||
|
},
|
||||||
|
attachmentCount: 1,
|
||||||
|
size: 50,
|
||||||
|
expect: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fetch multiple individual attachments",
|
||||||
|
setupf: func() {
|
||||||
|
truthy := true
|
||||||
|
itm := models.NewPost()
|
||||||
|
itm.SetId(&pid)
|
||||||
|
itm.SetHasAttachments(&truthy)
|
||||||
|
|
||||||
|
attch := models.NewAttachment()
|
||||||
|
attch.SetId(&aid)
|
||||||
|
attch.SetSize(ptr.To[int32](200))
|
||||||
|
|
||||||
|
itm.SetAttachments([]models.Attachmentable{attch, attch, attch, attch, attch})
|
||||||
|
|
||||||
|
interceptV1Path(
|
||||||
|
"groups",
|
||||||
|
"group",
|
||||||
|
"conversations",
|
||||||
|
"conv",
|
||||||
|
"threads",
|
||||||
|
"thread",
|
||||||
|
"posts",
|
||||||
|
pid).
|
||||||
|
Reply(200).
|
||||||
|
JSON(requireParseableToMap(suite.T(), itm))
|
||||||
|
},
|
||||||
|
attachmentCount: 5,
|
||||||
|
size: 1000,
|
||||||
|
expect: assert.NoError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
defer gock.Off()
|
||||||
|
test.setupf()
|
||||||
|
|
||||||
|
item, _, err := suite.its.gockAC.
|
||||||
|
Conversations().
|
||||||
|
GetConversationPost(
|
||||||
|
ctx,
|
||||||
|
"group",
|
||||||
|
"conv",
|
||||||
|
"thread",
|
||||||
|
pid,
|
||||||
|
CallConfig{})
|
||||||
|
test.expect(t, err)
|
||||||
|
|
||||||
|
var size int64
|
||||||
|
|
||||||
|
if item.GetBody() != nil {
|
||||||
|
content := ptr.Val(item.GetBody().GetContent())
|
||||||
|
size = int64(len(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments := item.GetAttachments()
|
||||||
|
for _, attachment := range attachments {
|
||||||
|
size += int64(*attachment.GetSize())
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, *item.GetId(), pid)
|
||||||
|
assert.Equal(t, test.attachmentCount, len(attachments), "attachment count")
|
||||||
|
assert.Equal(t, test.size, size, "item size")
|
||||||
|
assert.True(t, gock.IsDone(), "made all requests")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -324,7 +324,7 @@ func (c Mail) GetItem(
|
|||||||
logger.CtxErr(ctx, err).Info("fetching all attachments by id")
|
logger.CtxErr(ctx, err).Info("fetching all attachments by id")
|
||||||
|
|
||||||
// Getting size just to log in case of error
|
// Getting size just to log in case of error
|
||||||
attachConfig.QueryParameters.Select = []string{"id", "size"}
|
attachConfig.QueryParameters.Select = idAnd("size")
|
||||||
|
|
||||||
attachments, err := c.LargeItem.
|
attachments, err := c.LargeItem.
|
||||||
Client().
|
Client().
|
||||||
@ -348,6 +348,11 @@ func (c Mail) GetItem(
|
|||||||
Headers: newPreferHeaders(preferImmutableIDs(immutableIDs)),
|
Headers: newPreferHeaders(preferImmutableIDs(immutableIDs)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ictx := clues.Add(
|
||||||
|
ctx,
|
||||||
|
"attachment_id", ptr.Val(a.GetId()),
|
||||||
|
"attachment_size", ptr.Val(a.GetSize()))
|
||||||
|
|
||||||
att, err := c.Stable.
|
att, err := c.Stable.
|
||||||
Client().
|
Client().
|
||||||
Users().
|
Users().
|
||||||
@ -356,17 +361,13 @@ func (c Mail) GetItem(
|
|||||||
ByMessageId(itemID).
|
ByMessageId(itemID).
|
||||||
Attachments().
|
Attachments().
|
||||||
ByAttachmentId(ptr.Val(a.GetId())).
|
ByAttachmentId(ptr.Val(a.GetId())).
|
||||||
Get(ctx, attachConfig)
|
Get(ictx, attachConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// CannotOpenFileAttachment errors are not transient and
|
// CannotOpenFileAttachment errors are not transient and
|
||||||
// happens possibly from the original item somehow getting
|
// happens possibly from the original item somehow getting
|
||||||
// deleted from M365 and so we can skip these
|
// deleted from M365 and so we can skip these
|
||||||
if graph.IsErrCannotOpenFileAttachment(err) {
|
if graph.IsErrCannotOpenFileAttachment(err) {
|
||||||
logger.CtxErr(ctx, err).
|
logger.CtxErr(ictx, err).Info("attachment not found")
|
||||||
With(
|
|
||||||
"attachment_id", ptr.Val(a.GetId()),
|
|
||||||
"attachment_size", ptr.Val(a.GetSize())).
|
|
||||||
Info("attachment not found")
|
|
||||||
// TODO This should use a `AddSkip` once we have
|
// TODO This should use a `AddSkip` once we have
|
||||||
// figured out the semantics for skipping
|
// figured out the semantics for skipping
|
||||||
// subcomponents of an item
|
// subcomponents of an item
|
||||||
@ -374,8 +375,7 @@ func (c Mail) GetItem(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil, graph.Wrap(ctx, err, "getting mail attachment").
|
return nil, nil, graph.Wrap(ictx, err, "getting mail attachment")
|
||||||
With("attachment_id", ptr.Val(a.GetId()), "attachment_size", ptr.Val(a.GetSize()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
atts = append(atts, att)
|
atts = append(atts, att)
|
||||||
|
|||||||
@ -204,7 +204,7 @@ func (suite *MailAPIIntgSuite) SetupSuite() {
|
|||||||
suite.its = newIntegrationTesterSetup(suite.T())
|
suite.its = newIntegrationTesterSetup(suite.T())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *MailAPIIntgSuite) TestHugeAttachmentListDownload() {
|
func (suite *MailAPIIntgSuite) TestMail_attachmentListDownload() {
|
||||||
mid := "fake-message-id"
|
mid := "fake-message-id"
|
||||||
aid := "fake-attachment-id"
|
aid := "fake-attachment-id"
|
||||||
|
|
||||||
@ -369,7 +369,7 @@ func (suite *MailAPIIntgSuite) TestHugeAttachmentListDownload() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *MailAPIIntgSuite) TestMail_RestoreLargeAttachment() {
|
func (suite *MailAPIIntgSuite) TestMail_PostLargeAttachment() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
|
|
||||||
ctx, flush := tester.NewContext(t)
|
ctx, flush := tester.NewContext(t)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user