diff --git a/plugin/client.go b/plugin/client.go index e445f9e1e0..d74663d1ec 100644 --- a/plugin/client.go +++ b/plugin/client.go @@ -10,6 +10,7 @@ import ( const ( INTERNAL_KEY_PREFIX = "mmi_" BOT_USER_KEY = INTERNAL_KEY_PREFIX + "botid" + CHANNEL_KEY = INTERNAL_KEY_PREFIX + "channelid" ) // Starts the serving of a Mattermost plugin over net/rpc. gRPC is not yet supported. diff --git a/plugin/helpers_channels.go b/plugin/helpers_channels.go new file mode 100644 index 0000000000..710b022303 --- /dev/null +++ b/plugin/helpers_channels.go @@ -0,0 +1,111 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package plugin + +import ( + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/utils" + "github.com/pkg/errors" +) + +func (p *HelpersImpl) EnsureChannel(channel *model.Channel) (retChannelId string, retErr error) { + // Must provide a channel with a name and teadId + if channel == nil || len(channel.Name) < 1 || len(channel.TeamId) < 1 { + return "", errors.New("passed a bad channel, nil or no name or no team id") + } + + // If we fail for any reason, this could be a race between creation of channel and + // retrieval from another EnsureChannel. Just try the basic retrieve existing again. + defer func() { + if retChannelId == "" || retErr != nil { + var err error + var channelIdBytes []byte + + err = utils.ProgressiveRetry(func() error { + channelIdBytes, err = p.API.KVGet(CHANNEL_KEY) + if err != nil { + return err + } + return nil + }) + + if err == nil && channelIdBytes != nil { + retChannelId = string(channelIdBytes) + retErr = nil + } + } + }() + + // Fetch channel ID from key value store + channelIdBytes, kvGetErr := p.API.KVGet(CHANNEL_KEY) + if kvGetErr != nil { + // Failed to retrive the value of channel + return "", errors.Wrap(kvGetErr, "failed to get channel ID") + } + + var existingChannel *model.Channel + var channelGetErr *model.AppError + + // If channel ID exists, get existing channel by ID else get it by Name + if channelIdBytes != nil { + existingChannel, channelGetErr = p.API.GetChannel(string(channelIdBytes)) + if channelGetErr != nil { + return "", errors.Wrap(channelGetErr, "failed to get channel by ID") + } + } else { + existingChannel, channelGetErr = p.API.GetChannelByName(channel.TeamId, channel.Name, false) + if channelGetErr != nil { + return "", errors.Wrap(channelGetErr, "failed to get channel by name") + } + } + + // If channel exists, update the metadata + if existingChannel != nil { + return updateChannel(p, existingChannel, channel) + } + + // Create a new channel + createdChannel, createChannelErr := p.API.CreateChannel(channel) + if createChannelErr != nil { + return "", errors.Wrap(createChannelErr, "failed to create channel") + } + + // Set the new channel id in key value store + if kvSetErr := p.API.KVSet(CHANNEL_KEY, []byte(createdChannel.Id)); kvSetErr != nil { + p.API.LogWarn("Failed to set created channel id.", "channelid", createdChannel.Id, "err", kvSetErr) + } + + return createdChannel.Id, nil +} + +func updateChannel(p *HelpersImpl, existing *model.Channel, new *model.Channel) (string, error) { + // Update metadata of the channel + if updateErr := updateChannelMeta(existing, new); updateErr != nil { + return "", errors.Wrap(updateErr, "Failed to update the metadata of existing channel") + } + + // Send the updates to API + updatedChannel, channelUpdateErr := p.API.UpdateChannel(existing) + if channelUpdateErr != nil { + return "", errors.Wrap(channelUpdateErr, "Failed to update the existing channel") + } + + // Channel exists! + return updatedChannel.Id, nil +} + +func updateChannelMeta(existing *model.Channel, new *model.Channel) error { + // Check if channels are of different types + if existing.Type != new.Type { + return errors.New("Channel type cannot be updated") + } + + // Update metadata of channel + existing.Name = new.Name + existing.DisplayName = new.DisplayName + existing.Purpose = new.Purpose + existing.Header = new.Header + + return nil +} diff --git a/plugin/helpers_channels_test.go b/plugin/helpers_channels_test.go new file mode 100644 index 0000000000..691466ed08 --- /dev/null +++ b/plugin/helpers_channels_test.go @@ -0,0 +1,297 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package plugin_test + +import ( + "testing" + + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/plugin" + "github.com/mattermost/mattermost-server/plugin/plugintest" + "github.com/stretchr/testify/assert" +) + +func TestEnsureChannel(t *testing.T) { + setupAPI := func() *plugintest.API { + return &plugintest.API{} + } + + testChannel := &model.Channel{ + Id: model.NewId(), + TeamId: model.NewId(), + Type: "public", + Name: "test_channel", + DisplayName: "Test Channel", + Purpose: "Testing EnsureChannel", + Header: "Testing EnsureChannel", + } + + t.Run("bad parameters", func(t *testing.T) { + t.Run("no channel", func(t *testing.T) { + p := &plugin.HelpersImpl{} + channelId, err := p.EnsureChannel(nil) + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("empty name", func(t *testing.T) { + p := &plugin.HelpersImpl{} + channelId, err := p.EnsureChannel(&model.Channel{ + Name: "", + }) + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("name without teamId", func(t *testing.T) { + p := &plugin.HelpersImpl{} + channelId, err := p.EnsureChannel(&model.Channel{ + Name: "test_channel", + }) + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("teamId without name", func(t *testing.T) { + p := &plugin.HelpersImpl{} + channelId, err := p.EnsureChannel(&model.Channel{ + TeamId: model.NewId(), + }) + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("teamId with empty name", func(t *testing.T) { + p := &plugin.HelpersImpl{} + channelId, err := p.EnsureChannel(&model.Channel{ + TeamId: model.NewId(), + }) + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + }) + + t.Run("if channel already exists in Key Value store", func(t *testing.T) { + t.Run("should return an error if unable to get channel id", func(t *testing.T) { + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return(nil, &model.AppError{}) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("should return an error if unable to get channel", func(t *testing.T) { + expectedChannelId := model.NewId() + + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return([]byte(expectedChannelId), nil) + api.On("GetChannel", expectedChannelId).Return(nil, &model.AppError{}) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("should return an error if unable to update channel", func(t *testing.T) { + expectedChannelId := model.NewId() + + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return([]byte(expectedChannelId), nil) + api.On("GetChannel", expectedChannelId).Return(testChannel, nil) + api.On("UpdateChannel", testChannel).Return(nil, &model.AppError{}) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("should return the Id of existing channel if metadata is same", func(t *testing.T) { + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return([]byte(testChannel.Id), nil) + api.On("GetChannel", testChannel.Id).Return(testChannel, nil) + api.On("UpdateChannel", testChannel).Return(testChannel, nil) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, testChannel.Id, channelId) + assert.Nil(t, err) + }) + t.Run("should return error if channel type is different from existing one", func(t *testing.T) { + privChannel := &model.Channel{ + Id: model.NewId(), + Type: "private", + TeamId: testChannel.TeamId, + Name: testChannel.Name, + } + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return([]byte(testChannel.Id), nil) + api.On("GetChannel", testChannel.Id).Return(privChannel, nil) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("should return the Id of updated channel if metadata is different", func(t *testing.T) { + updatedChannel := &model.Channel{ + Id: model.NewId(), + TeamId: testChannel.TeamId, + Name: testChannel.Name, + } + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return([]byte(testChannel.Id), nil) + api.On("GetChannel", testChannel.Id).Return(testChannel, nil) + api.On("UpdateChannel", testChannel).Return(updatedChannel, nil) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, updatedChannel.Id, channelId) + assert.Nil(t, err) + }) + }) + + t.Run("if channel is not in Key Value store but already exists", func(t *testing.T) { + t.Run("should return an error if unable to get channel", func(t *testing.T) { + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return(nil, nil) + api.On("GetChannelByName", testChannel.TeamId, testChannel.Name, false).Return(nil, &model.AppError{}) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("should return the Id of existing channel if metadata is same", func(t *testing.T) { + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return(nil, nil) + api.On("GetChannelByName", testChannel.TeamId, testChannel.Name, false).Return(testChannel, nil) + api.On("UpdateChannel", testChannel).Return(testChannel, nil) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, testChannel.Id, channelId) + assert.Nil(t, err) + }) + t.Run("should return error if failed to update the channel", func(t *testing.T) { + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return(nil, nil) + api.On("GetChannelByName", testChannel.TeamId, testChannel.Name, false).Return(testChannel, nil) + api.On("UpdateChannel", testChannel).Return(nil, &model.AppError{}) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("should return the Id of updated channel if metadata is different", func(t *testing.T) { + updatedChannel := &model.Channel{ + Id: model.NewId(), + TeamId: testChannel.TeamId, + Name: testChannel.Name, + } + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return(nil, nil) + api.On("GetChannelByName", testChannel.TeamId, testChannel.Name, false).Return(testChannel, nil) + api.On("UpdateChannel", testChannel).Return(updatedChannel, nil) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, updatedChannel.Id, channelId) + assert.Nil(t, err) + }) + t.Run("should return error if channel type is different from existing one", func(t *testing.T) { + privChannel := &model.Channel{ + Id: model.NewId(), + Type: "private", + TeamId: testChannel.TeamId, + Name: testChannel.Name, + } + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return(nil, nil) + api.On("GetChannelByName", testChannel.TeamId, testChannel.Name, false).Return(privChannel, nil) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + }) + + t.Run("if channel does not exist", func(t *testing.T) { + t.Run("should create new channel and return the Id", func(t *testing.T) { + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return(nil, nil) + api.On("GetChannelByName", testChannel.TeamId, testChannel.Name, false).Return(nil, nil) + api.On("CreateChannel", testChannel).Return(testChannel, nil) + api.On("KVSet", plugin.CHANNEL_KEY, []byte(testChannel.Id)).Return(nil) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, testChannel.Id, channelId) + assert.Nil(t, err) + }) + t.Run("should return error if unable to create new channel", func(t *testing.T) { + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return(nil, nil) + api.On("GetChannelByName", testChannel.TeamId, testChannel.Name, false).Return(nil, nil) + api.On("CreateChannel", testChannel).Return(nil, &model.AppError{}) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("should log and return id if unable to write to Key Value store", func(t *testing.T) { + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return(nil, nil) + api.On("GetChannelByName", testChannel.TeamId, testChannel.Name, false).Return(nil, nil) + api.On("CreateChannel", testChannel).Return(testChannel, nil) + api.On("KVSet", plugin.CHANNEL_KEY, []byte(testChannel.Id)).Return(&model.AppError{}) + api.On("LogWarn", "Failed to set created channel id.", "channelid", testChannel.Id, "err", &model.AppError{}) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, testChannel.Id, channelId) + assert.Nil(t, err) + }) + }) +}