interface implementation

This commit is contained in:
neha-Gupta1 2023-08-22 18:29:23 +05:30
commit 4d0d66bec0
7 changed files with 702 additions and 18 deletions

View File

@ -11,8 +11,8 @@ import (
type BackupMessagesHandler interface { type BackupMessagesHandler interface {
GetMessageByID(ctx context.Context, teamID, channelID, itemID string) (models.ChatMessageable, error) GetMessageByID(ctx context.Context, teamID, channelID, itemID string) (models.ChatMessageable, error)
NewMessagePager(teamID, channelID string) api.MessageItemDeltaEnumerator NewMessagePager(teamID, channelID string) api.ChannelMessageDeltaEnumerator
GetChannelByID(ctx context.Context, teamID, channelID string) (models.Channelable, error) GetChannelByID(ctx context.Context, teamID, channelID string) (models.Channelable, error)
NewChannelPager(teamID, channelID string) api.ChannelItemDeltaEnumerator NewChannelPager(teamID, channelID string) api.ChannelDeltaEnumerator
GetReplyByID(ctx context.Context, teamID, channelID, messageID string) (serialization.Parsable, error) GetReplyByID(ctx context.Context, teamID, channelID, messageID string) (serialization.Parsable, error)
} }

View File

@ -28,6 +28,7 @@ const (
TestCfgSiteURL = "m365siteurl" TestCfgSiteURL = "m365siteurl"
TestCfgTeamID = "m365teamid" TestCfgTeamID = "m365teamid"
TestCfgGroupID = "m365groupid" TestCfgGroupID = "m365groupid"
TestCfgChannelID = "m365channelid"
TestCfgUserID = "m365userid" TestCfgUserID = "m365userid"
TestCfgSecondaryUserID = "secondarym365userid" TestCfgSecondaryUserID = "secondarym365userid"
TestCfgSecondaryGroupID = "secondarym365groupid" TestCfgSecondaryGroupID = "secondarym365groupid"
@ -46,6 +47,7 @@ const (
EnvCorsoM365TestSiteURL = "CORSO_M365_TEST_SITE_URL" EnvCorsoM365TestSiteURL = "CORSO_M365_TEST_SITE_URL"
EnvCorsoM365TestTeamID = "CORSO_M365_TEST_TEAM_ID" EnvCorsoM365TestTeamID = "CORSO_M365_TEST_TEAM_ID"
EnvCorsoM365TestGroupID = "CORSO_M365_TEST_GROUP_ID" EnvCorsoM365TestGroupID = "CORSO_M365_TEST_GROUP_ID"
EnvCorsoM365TestChannelID = "CORSO_M365_TEST_CHANNEL_ID"
EnvCorsoM365TestUserID = "CORSO_M365_TEST_USER_ID" EnvCorsoM365TestUserID = "CORSO_M365_TEST_USER_ID"
EnvCorsoSecondaryM365TestSiteID = "CORSO_SECONDARY_M365_TEST_SITE_ID" EnvCorsoSecondaryM365TestSiteID = "CORSO_SECONDARY_M365_TEST_SITE_ID"
EnvCorsoSecondaryM365TestUserID = "CORSO_SECONDARY_M365_TEST_USER_ID" EnvCorsoSecondaryM365TestUserID = "CORSO_SECONDARY_M365_TEST_USER_ID"
@ -167,6 +169,12 @@ func ReadTestConfig() (map[string]string, error) {
os.Getenv(EnvCorsoM365TestGroupID), os.Getenv(EnvCorsoM365TestGroupID),
vpr.GetString(TestCfgGroupID), vpr.GetString(TestCfgGroupID),
"6f24b40d-b13d-4752-980f-f5fb9fba7aa0") "6f24b40d-b13d-4752-980f-f5fb9fba7aa0")
fallbackTo(
testEnv,
TestCfgChannelID,
os.Getenv(EnvCorsoM365TestChannelID),
vpr.GetString(TestCfgChannelID),
"19:nY6QHZ3hnHJ6ylarBxPjCOLRJNvrL3oKI5iW15QxTPA1@thread.tacv2")
fallbackTo( fallbackTo(
testEnv, testEnv,
TestCfgSiteURL, TestCfgSiteURL,

View File

@ -246,3 +246,14 @@ func M365GroupID(t *testing.T) string {
return strings.ToLower(cfg[TestCfgTeamID]) return strings.ToLower(cfg[TestCfgTeamID])
} }
// M365ChannelID returns a channelID string representing the m365TeamsID described
// by either the env var CORSO_M365_TEST_CHANNEL_ID, the corso_test.toml config
// file or the default value (in that order of priority). The default is a
// last-attempt fallback that will only work on alcion's testing org.
func M365ChannelID(t *testing.T) string {
cfg, err := ReadTestConfig()
require.NoError(t, err, "retrieving m365 channel id from test configuration: %+v", clues.ToCore(err))
return cfg[TestCfgChannelID]
}

View File

@ -37,6 +37,9 @@ const (
// Folder Management(30x) // Folder Management(30x)
FolderItem ItemType = 306 FolderItem ItemType = 306
// GroupChannel(40x)
GroupChannel ItemType = 407
) )
func UpdateItem(item *ItemInfo, newLocPath *path.Builder) { func UpdateItem(item *ItemInfo, newLocPath *path.Builder) {

View File

@ -1 +1,380 @@
package api package api
import (
"context"
"fmt"
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/m365/graph"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/logger"
"github.com/microsoft/kiota-abstractions-go/serialization"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/microsoftgraph/msgraph-sdk-go/teams"
)
// ---------------------------------------------------------------------------
// controller
// ---------------------------------------------------------------------------
func (c Client) Channels() Channels {
return Channels{c}
}
// Channels is an interface-compliant provider of the client.
type Channels struct {
Client
}
// ---------------------------------------------------------------------------
// containers
// ---------------------------------------------------------------------------
// CreateContainer makes an channels with the name in the team
func (c Channels) CreateChannel(
ctx context.Context,
teamID, containerName string,
) (graph.Container, error) {
body := models.NewChannel()
body.SetDisplayName(&containerName)
container, err := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
Post(ctx, body, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "creating channel")
}
return ChannelsDisplayable{Channelable: container}, nil
}
// DeleteChannel removes a channel from user's M365 account
func (c Channels) DeleteChannel(
ctx context.Context,
teamID, containerID string,
) error {
// deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707
srv, err := NewService(c.Credentials)
if err != nil {
return graph.Stack(ctx, err)
}
err = srv.Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Delete(ctx, nil)
if err != nil {
return graph.Stack(ctx, err)
}
return nil
}
func (c Channels) GetChannel(
ctx context.Context,
teamID, containerID string,
) (models.Channelable, error) {
config := &teams.ItemChannelsChannelItemRequestBuilderGetRequestConfiguration{
QueryParameters: &teams.ItemChannelsChannelItemRequestBuilderGetQueryParameters{
Select: idAnd("displayName"),
},
}
resp, err := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Get(ctx, config)
if err != nil {
return nil, graph.Stack(ctx, err)
}
return resp, nil
}
func (c Channels) GetChannelByID(
ctx context.Context,
teamID, containerID string,
) (graph.Container, error) {
channel, err := c.GetChannel(ctx, teamID, containerID)
if err != nil {
return nil, err
}
return ChannelsDisplayable{Channelable: channel}, nil
}
// GetChannelByName fetches a channel by name
func (c Channels) GetChannelByName(
ctx context.Context,
teamID, containerName string,
) (graph.Container, error) {
ctx = clues.Add(ctx, "channel_name", containerName)
filter := fmt.Sprintf("displayName eq '%s'", containerName)
options := &teams.ItemChannelsRequestBuilderGetRequestConfiguration{
QueryParameters: &teams.ItemChannelsRequestBuilderGetQueryParameters{
Filter: &filter,
},
}
resp, err := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
Get(ctx, options)
if err != nil {
return nil, graph.Stack(ctx, err).WithClues(ctx)
}
gv := resp.GetValue()
if len(gv) == 0 {
return nil, clues.New("channel not found").WithClues(ctx)
}
// We only allow the api to match one channel with the provided name.
// If we match multiples, we'll eagerly return the first one.
logger.Ctx(ctx).Debugw("channels matched the name search")
// Sanity check ID and name
cal := gv[0]
container := ChannelsDisplayable{Channelable: cal}
if err := graph.CheckIDAndName(container); err != nil {
return nil, clues.Stack(err).WithClues(ctx)
}
return container, nil
}
func (c Channels) PatchChannel(
ctx context.Context,
teamID, containerID string,
body models.Channelable,
) error {
_, err := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Patch(ctx, body, nil)
if err != nil {
return graph.Wrap(ctx, err, "patching channel")
}
return nil
}
// ---------------------------------------------------------------------------
// message
// ---------------------------------------------------------------------------
// GetItem retrieves a Messageable item.
func (c Channels) GetMessage(
ctx context.Context,
teamID, channelID, itemID string,
immutableIDs bool,
errs *fault.Bus,
) (serialization.Parsable, *details.GroupsInfo, error) {
var (
size int64
)
// is preferImmutableIDs headers required here
message, err := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(channelID).
Messages().
ByChatMessageId(itemID).
Get(ctx, nil)
if err != nil {
return nil, nil, graph.Stack(ctx, err)
}
return message, MessageInfo(message, size), nil
}
func (c Channels) PostMessage(
ctx context.Context,
teamID, containerID string,
body models.ChatMessageable,
) (models.ChatMessageable, error) {
itm, err := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Messages().
Post(ctx, body, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "creating mail message")
}
if itm == nil {
return nil, clues.New("nil response mail message creation").WithClues(ctx)
}
return itm, nil
}
func (c Channels) DeleteMessage(
ctx context.Context,
teamID, itemID, containerID string,
) error {
// deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707
srv, err := NewService(c.Credentials)
if err != nil {
return graph.Stack(ctx, err)
}
err = srv.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Messages().
ByChatMessageId(itemID).
Delete(ctx, nil)
if err != nil {
return graph.Wrap(ctx, err, "deleting mail message")
}
return nil
}
// ---------------------------------------------------------------------------
// replies
// ---------------------------------------------------------------------------
// GetReplies retrieves a Messageable item.
func (c Channels) GetReplies(
ctx context.Context,
teamID, channelID, itemID string,
) (serialization.Parsable, error) {
replies, err := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(channelID).
Messages().
ByChatMessageId(itemID).
Replies().
Get(ctx, nil)
if err != nil {
return nil, graph.Stack(ctx, err)
}
return replies, nil
}
func (c Channels) PostReply(
ctx context.Context,
teamID, containerID, messageID string,
body models.ChatMessageable,
) (models.ChatMessageable, error) {
itm, err := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Messages().
ByChatMessageId(messageID).
Replies().
Post(ctx, body, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "creating reply message")
}
if itm == nil {
return nil, clues.New("nil response reply to message creation").WithClues(ctx)
}
return itm, nil
}
func (c Channels) DeleteReply(
ctx context.Context,
teamID, itemID, containerID, replyID string,
) error {
// deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707
srv, err := NewService(c.Credentials)
if err != nil {
return graph.Stack(ctx, err)
}
err = srv.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Messages().
ByChatMessageId(itemID).
Replies().
ByChatMessageId1(replyID).
Delete(ctx, nil)
if err != nil {
return graph.Wrap(ctx, err, "deleting mail message")
}
return nil
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
func MessageInfo(msg models.ChatMessageable, size int64) *details.GroupsInfo {
var (
created = ptr.Val(msg.GetCreatedDateTime())
)
return &details.GroupsInfo{
ItemType: details.GroupChannel,
Size: size,
Created: created,
Modified: ptr.OrNow(msg.GetLastModifiedDateTime()),
}
}
// ---------------------------------------------------------------------------
// helper funcs
// ---------------------------------------------------------------------------
// ChannelsDisplayable is a wrapper that complies with the
// models.Channelable interface with the graph.Container
// interfaces.
type ChannelsDisplayable struct {
models.Channelable
}
// GetParentFolderId returns the default channe name address
//nolint:revive
func (c ChannelsDisplayable) GetParentFolderId() *string {
return nil
}

View File

@ -1,6 +1,14 @@
package api package api
import "github.com/microsoftgraph/msgraph-sdk-go/models" import (
"context"
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/m365/graph"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/microsoftgraph/msgraph-sdk-go/teams"
)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// item pager // item pager
@ -12,18 +20,66 @@ type ChannelMessageDeltaEnumerator interface {
SetNextLinker SetNextLinker
} }
// TODO: implement var _ ChannelMessageDeltaEnumerator = &MessagePageCtrl{}
// var _ ChannelMessageDeltaEnumerator = &messagePageCtrl{}
// type messagePageCtrl struct { type MessagePageCtrl struct {
// gs graph.Servicer gs graph.Servicer
// builder *teams.ItemChannelsItemMessagesRequestBuilder builder *teams.ItemChannelsItemMessagesDeltaRequestBuilder
// options *teams.ItemChannelsItemMessagesRequestBuilderGetRequestConfiguration options *teams.ItemChannelsItemMessagesDeltaRequestBuilderGetRequestConfiguration
// } }
// --------------------------------------------------------------------------- func (c Channels) NewMessagePager(
// channel pager teamID,
// --------------------------------------------------------------------------- channelID string,
fields []string,
) *MessagePageCtrl {
requestConfig := &teams.ItemChannelsItemMessagesDeltaRequestBuilderGetRequestConfiguration{
QueryParameters: &teams.ItemChannelsItemMessagesDeltaRequestBuilderGetQueryParameters{
Select: fields,
},
}
res := &MessagePageCtrl{
gs: c.Stable,
options: requestConfig,
builder: c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(channelID).
Messages().
Delta(),
}
return res
}
func (p *MessagePageCtrl) SetNext(nextLink string) {
p.builder = teams.NewItemChannelsItemMessagesDeltaRequestBuilder(nextLink, p.gs.Adapter())
}
func (p *MessagePageCtrl) GetPage(ctx context.Context) (DeltaPageLinker, error) {
var (
resp DeltaPageLinker
err error
)
resp, err = p.builder.Get(ctx, p.options)
if err != nil {
return nil, graph.Stack(ctx, err)
}
return resp, nil
}
func (p *MessagePageCtrl) ValuesIn(l PageLinker) ([]models.ChatMessageable, error) {
return getValues[models.ChatMessageable](l)
}
type MessageItemIDType struct {
ItemID string
}
type ChannelDeltaEnumerator interface { type ChannelDeltaEnumerator interface {
DeltaGetPager DeltaGetPager
@ -33,9 +89,69 @@ type ChannelDeltaEnumerator interface {
// TODO: implement // TODO: implement
// var _ ChannelDeltaEnumerator = &channelsPageCtrl{} // var _ ChannelDeltaEnumerator = &channelsPageCtrl{}
type channelItemPageCtrl struct {
gs graph.Servicer
builder *teams.ItemChannelsItemMessagesRequestBuilder
options *teams.ItemChannelsItemMessagesRequestBuilderGetRequestConfiguration
}
// type channelsPageCtrl struct { func (c Channels) GetItemIDsInContainer(
// gs graph.Servicer ctx context.Context,
// builder *teams.ItemChannelsChannelItemRequestBuilder teamID, channelID string,
// options *teams.ItemChannelsChannelItemRequestBuilderGetRequestConfiguration ) (map[string]MessageItemIDType, error) {
// } ctx = clues.Add(ctx, "channel_id", channelID)
pager := c.NewChannelItemPager(teamID, channelID)
items, err := enumerateItems(ctx, pager)
if err != nil {
return nil, graph.Wrap(ctx, err, "enumerating contacts")
}
m := map[string]MessageItemIDType{}
for _, item := range items {
m[ptr.Val(item.GetId())] = MessageItemIDType{
ItemID: ptr.Val(item.GetId()),
}
}
return m, nil
}
func (c Channels) NewChannelItemPager(
teamID, containerID string,
selectProps ...string,
) itemPager[models.ChatMessageable] {
options := &teams.ItemChannelsItemMessagesRequestBuilderGetRequestConfiguration{
QueryParameters: &teams.ItemChannelsItemMessagesRequestBuilderGetQueryParameters{},
}
if len(selectProps) > 0 {
options.QueryParameters.Select = selectProps
}
builder := c.Stable.
Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(containerID).
Messages()
return &channelItemPageCtrl{c.Stable, builder, options}
}
//lint:ignore U1000 False Positive
func (p *channelItemPageCtrl) getPage(ctx context.Context) (PageLinkValuer[models.ChatMessageable], error) {
page, err := p.builder.Get(ctx, p.options)
if err != nil {
return nil, graph.Stack(ctx, err)
}
return EmptyDeltaLinker[models.ChatMessageable]{PageLinkValuer: page}, nil
}
//lint:ignore U1000 False Positive
func (p *channelItemPageCtrl) setNext(nextLink string) {
p.builder = teams.NewItemChannelsItemMessagesRequestBuilder(nextLink, p.gs.Adapter())
}

View File

@ -0,0 +1,167 @@
package api_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/alcionai/clues"
"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/account"
"github.com/alcionai/corso/src/pkg/services/m365/api"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type ChannelPagerIntgSuite struct {
tester.Suite
its intgTesterSetup
}
func TestChannelPagerIntgSuite(t *testing.T) {
suite.Run(t, &ChannelPagerIntgSuite{
Suite: tester.NewIntegrationSuite(
t,
[][]string{tconfig.M365AcctCredEnvs}),
})
}
func (suite *ChannelPagerIntgSuite) SetupSuite() {
suite.its = newIntegrationTesterSetup(suite.T())
}
func (suite *ChannelPagerIntgSuite) TestChannels_GetPage() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
teamID := tconfig.M365TeamID(t)
channelID := tconfig.M365ChannelID(t)
pager := suite.its.ac.Channels().NewMessagePager(teamID, channelID, []string{})
a, err := pager.GetPage(ctx)
assert.NoError(t, err, clues.ToCore(err))
assert.NotNil(t, a)
}
func (suite *ChannelPagerIntgSuite) TestChannels_CreateGetAndDelete() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
var (
yy, mm, dd = time.Now().Date()
hh = time.Now().Hour()
min = time.Now().Minute()
ss = time.Now().Second()
containerName = fmt.Sprintf("testChannel%d%d%d%d%d%d", yy, mm, dd, hh, min, ss)
teamID = tconfig.M365TeamID(t)
credentials = suite.its.ac.Credentials
chanClient = suite.its.ac.Channels()
)
// GET channel - should be not found
_, err := suite.its.ac.Channels().GetChannelByName(ctx, teamID, containerName)
assert.Error(t, err, clues.ToCore(err))
// POST channel
channelPost, err := suite.its.ac.Channels().CreateChannel(ctx, teamID, containerName)
assert.NoError(t, err, clues.ToCore(err))
postChannelID := ptr.Val(channelPost.GetId())
// DELETE channel
defer func() {
_, err := chanClient.GetChannelByID(ctx, teamID, postChannelID)
if err != nil {
fmt.Println("could not find channel: ", err)
} else {
deleteChannel(ctx, credentials, teamID, postChannelID)
}
}()
// GET channel -should be found
channel, err := chanClient.GetChannelByName(ctx, teamID, containerName)
assert.NoError(t, err, clues.ToCore(err))
assert.Equal(t, ptr.Val(channel.GetDisplayName()), containerName)
// PATCH channel
patchBody := models.NewChannel()
patchName := fmt.Sprintf("othername%d%d%d%d%d%d", yy, mm, dd, hh, min, ss)
patchBody.SetDisplayName(ptr.To(patchName))
err = chanClient.PatchChannel(ctx, teamID, postChannelID, patchBody)
assert.NoError(t, err, clues.ToCore(err))
assert.Equal(t, ptr.Val(channel.GetDisplayName()), containerName)
// GET channel -should not be found with old name
_, err = chanClient.GetChannelByName(ctx, teamID, containerName)
assert.Error(t, err, clues.ToCore(err))
// GET channel -should be found with new name
channel, err = chanClient.GetChannelByName(ctx, teamID, patchName)
assert.NoError(t, err, clues.ToCore(err))
assert.Equal(t, ptr.Val(channel.GetDisplayName()), patchName)
assert.Equal(t, ptr.Val(channel.GetId()), postChannelID)
// GET channel -should not be found with old name
err = chanClient.DeleteChannel(ctx, teamID, postChannelID)
assert.NoError(t, err, clues.ToCore(err))
// GET channel -should not be found anymore
_, err = chanClient.GetChannel(ctx, teamID, postChannelID)
assert.Error(t, err, clues.ToCore(err))
}
func deleteChannel(ctx context.Context, credentials account.M365Config, teamID, postChannelID string) {
srv, err := api.NewService(credentials)
if err != nil {
fmt.Println("Error found in getting creds")
}
if err != nil {
fmt.Println("Error found in getting creds")
}
err = srv.Client().
Teams().
ByTeamId(teamID).
Channels().
ByChannelId(postChannelID).
Delete(ctx, nil)
if err != nil {
fmt.Println("channel could not be delete in defer")
}
}
// func (suite *ChannelPagerIntgSuite) TestMessages_CreateGetAndDelete() {
// t := suite.T()
// ctx, flush := tester.NewContext(t)
// defer flush()
// var (
// teamID = tconfig.M365TeamsID(t)
// channelID = tconfig.M365ChannelID(t)
// credentials = suite.its.ac.Credentials
// chanClient = suite.its.ac.Channels()
// )
// // GET channel - should be not found
// message, _, err := chanClient.GetMessage(ctx, teamID, channelID, "", "")
// assert.Error(t, err, clues.ToCore(err))
// // POST channel
// // patchBody := models.NewChatMessage()
// // body := models.NewItemBody()
// // content := "Hello World"
// // body.SetContent(&content)
// // patchBody.SetBody(body)
// // _, := suite.its.ac.Channels().PostMessage(ctx, teamID, channelID, patchBody)
// // assert.NoError(t, err, clues.ToCore(err))
// }