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:
|
||||
detailsSize = itemInfo.OneDrive.Size
|
||||
case path.GroupsService:
|
||||
// FIXME: needs update for message.
|
||||
detailsSize = itemInfo.Groups.Size
|
||||
default:
|
||||
assert.Fail(t, "unrecognized data type")
|
||||
|
||||
@ -42,8 +42,11 @@ type GroupsInfo struct {
|
||||
Modified time.Time `json:"modified,omitempty"`
|
||||
|
||||
// Channels Specific
|
||||
Message ChannelMessageInfo `json:"message"`
|
||||
LastReply ChannelMessageInfo `json:"lastReply"`
|
||||
Message ChannelMessageInfo `json:"message,omitempty"`
|
||||
LastReply ChannelMessageInfo `json:"lastReply,omitempty"`
|
||||
|
||||
// Conversations Specific
|
||||
Post ConversationPostInfo `json:"post,omitempty"`
|
||||
|
||||
// SharePoint specific
|
||||
Created time.Time `json:"created,omitempty"`
|
||||
@ -57,6 +60,14 @@ type GroupsInfo struct {
|
||||
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 {
|
||||
AttachmentNames []string `json:"attachmentNames,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
|
||||
@ -39,7 +39,8 @@ const (
|
||||
FolderItem ItemType = 306
|
||||
|
||||
// Groups/Teams(40x)
|
||||
GroupsChannelMessage ItemType = 401
|
||||
GroupsChannelMessage ItemType = 401
|
||||
GroupsConversationPost ItemType = 402
|
||||
)
|
||||
|
||||
func UpdateItem(item *ItemInfo, newLocPath *path.Builder) {
|
||||
|
||||
@ -52,11 +52,8 @@ func (c Channels) GetChannel(
|
||||
Channels().
|
||||
ByChannelId(containerID).
|
||||
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
|
||||
|
||||
@ -76,7 +76,6 @@ func (c Channels) NewChannelMessagePager(
|
||||
}
|
||||
|
||||
// GetChannelMessages fetches a delta of all messages in the channel.
|
||||
// returns two maps: addedItems, deletedItems
|
||||
func (c Channels) GetChannelMessages(
|
||||
ctx context.Context,
|
||||
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")
|
||||
|
||||
// Getting size just to log in case of error
|
||||
attachConfig.QueryParameters.Select = []string{"id", "size"}
|
||||
attachConfig.QueryParameters.Select = idAnd("size")
|
||||
|
||||
attachments, err := c.LargeItem.
|
||||
Client().
|
||||
@ -348,6 +348,11 @@ func (c Mail) GetItem(
|
||||
Headers: newPreferHeaders(preferImmutableIDs(immutableIDs)),
|
||||
}
|
||||
|
||||
ictx := clues.Add(
|
||||
ctx,
|
||||
"attachment_id", ptr.Val(a.GetId()),
|
||||
"attachment_size", ptr.Val(a.GetSize()))
|
||||
|
||||
att, err := c.Stable.
|
||||
Client().
|
||||
Users().
|
||||
@ -356,17 +361,13 @@ func (c Mail) GetItem(
|
||||
ByMessageId(itemID).
|
||||
Attachments().
|
||||
ByAttachmentId(ptr.Val(a.GetId())).
|
||||
Get(ctx, attachConfig)
|
||||
Get(ictx, attachConfig)
|
||||
if err != nil {
|
||||
// CannotOpenFileAttachment errors are not transient and
|
||||
// happens possibly from the original item somehow getting
|
||||
// deleted from M365 and so we can skip these
|
||||
if graph.IsErrCannotOpenFileAttachment(err) {
|
||||
logger.CtxErr(ctx, err).
|
||||
With(
|
||||
"attachment_id", ptr.Val(a.GetId()),
|
||||
"attachment_size", ptr.Val(a.GetSize())).
|
||||
Info("attachment not found")
|
||||
logger.CtxErr(ictx, err).Info("attachment not found")
|
||||
// TODO This should use a `AddSkip` once we have
|
||||
// figured out the semantics for skipping
|
||||
// subcomponents of an item
|
||||
@ -374,8 +375,7 @@ func (c Mail) GetItem(
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, nil, graph.Wrap(ctx, err, "getting mail attachment").
|
||||
With("attachment_id", ptr.Val(a.GetId()), "attachment_size", ptr.Val(a.GetSize()))
|
||||
return nil, nil, graph.Wrap(ictx, err, "getting mail attachment")
|
||||
}
|
||||
|
||||
atts = append(atts, att)
|
||||
|
||||
@ -204,7 +204,7 @@ func (suite *MailAPIIntgSuite) SetupSuite() {
|
||||
suite.its = newIntegrationTesterSetup(suite.T())
|
||||
}
|
||||
|
||||
func (suite *MailAPIIntgSuite) TestHugeAttachmentListDownload() {
|
||||
func (suite *MailAPIIntgSuite) TestMail_attachmentListDownload() {
|
||||
mid := "fake-message-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()
|
||||
|
||||
ctx, flush := tester.NewContext(t)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user