From 9307f75fe9df82fbf9fa8cabdef20cce71e9ae16 Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Tue, 22 Oct 2019 14:38:08 -0400 Subject: [PATCH] Add some group plugin APIs (#12180) * Add group store GetByUser * Add group store GetByName * Add group plugin APIs * Minor naming fixes * Add minimum version comments --- app/group.go | 8 +++ app/plugin_api.go | 12 ++++ plugin/api.go | 15 ++++ plugin/client_rpc_generated.go | 87 +++++++++++++++++++++++ plugin/plugintest/api.go | 75 ++++++++++++++++++++ store/sqlstore/group_store.go | 32 +++++++++ store/store.go | 2 + store/storetest/group_store.go | 104 ++++++++++++++++++++++++++++ store/storetest/mocks/GroupStore.go | 50 +++++++++++++ store/timer_layer.go | 34 +++++++++ 10 files changed, 419 insertions(+) diff --git a/app/group.go b/app/group.go index e84cd34d9e..690e5bf582 100644 --- a/app/group.go +++ b/app/group.go @@ -11,6 +11,10 @@ func (a *App) GetGroup(id string) (*model.Group, *model.AppError) { return a.Srv.Store.Group().Get(id) } +func (a *App) GetGroupByName(name string) (*model.Group, *model.AppError) { + return a.Srv.Store.Group().GetByName(name) +} + func (a *App) GetGroupByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError) { return a.Srv.Store.Group().GetByRemoteID(remoteID, groupSource) } @@ -19,6 +23,10 @@ func (a *App) GetGroupsBySource(groupSource model.GroupSource) ([]*model.Group, return a.Srv.Store.Group().GetAllBySource(groupSource) } +func (a *App) GetGroupsByUserId(userId string) ([]*model.Group, *model.AppError) { + return a.Srv.Store.Group().GetByUser(userId) +} + func (a *App) CreateGroup(group *model.Group) (*model.Group, *model.AppError) { return a.Srv.Store.Group().Create(group) } diff --git a/app/plugin_api.go b/app/plugin_api.go index dda5e1ae10..d228a88d0d 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -445,6 +445,18 @@ func (api *PluginAPI) DeleteChannelMember(channelId, userId string) *model.AppEr return api.app.LeaveChannel(channelId, userId) } +func (api *PluginAPI) GetGroup(groupId string) (*model.Group, *model.AppError) { + return api.app.GetGroup(groupId) +} + +func (api *PluginAPI) GetGroupByName(name string) (*model.Group, *model.AppError) { + return api.app.GetGroupByName(name) +} + +func (api *PluginAPI) GetGroupsForUser(userId string) ([]*model.Group, *model.AppError) { + return api.app.GetGroupsByUserId(userId) +} + func (api *PluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) { return api.app.CreatePostMissingChannel(post, true) } diff --git a/plugin/api.go b/plugin/api.go index 1afadc2b89..49ac0ebf8f 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -378,6 +378,21 @@ type API interface { // Minimum server version: 5.2 UpdateChannelMemberNotifications(channelId, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) + // GetGroup gets a group by ID. + // + // Minimum server version: 5.18 + GetGroup(groupId string) (*model.Group, *model.AppError) + + // GetGroupByName gets a group by name. + // + // Minimum server version: 5.18 + GetGroupByName(name string) (*model.Group, *model.AppError) + + // GetGroupsForUser gets the groups a user is in. + // + // Minimum server version: 5.18 + GetGroupsForUser(userId string) ([]*model.Group, *model.AppError) + // DeleteChannelMember deletes a channel membership for a user. // // Minimum server version: 5.2 diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index c50730fbef..d91ae36db6 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -2498,6 +2498,93 @@ func (s *apiRPCServer) UpdateChannelMemberNotifications(args *Z_UpdateChannelMem return nil } +type Z_GetGroupArgs struct { + A string +} + +type Z_GetGroupReturns struct { + A *model.Group + B *model.AppError +} + +func (g *apiRPCClient) GetGroup(groupId string) (*model.Group, *model.AppError) { + _args := &Z_GetGroupArgs{groupId} + _returns := &Z_GetGroupReturns{} + if err := g.client.Call("Plugin.GetGroup", _args, _returns); err != nil { + log.Printf("RPC call to GetGroup API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) GetGroup(args *Z_GetGroupArgs, returns *Z_GetGroupReturns) error { + if hook, ok := s.impl.(interface { + GetGroup(groupId string) (*model.Group, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetGroup(args.A) + } else { + return encodableError(fmt.Errorf("API GetGroup called but not implemented.")) + } + return nil +} + +type Z_GetGroupByNameArgs struct { + A string +} + +type Z_GetGroupByNameReturns struct { + A *model.Group + B *model.AppError +} + +func (g *apiRPCClient) GetGroupByName(name string) (*model.Group, *model.AppError) { + _args := &Z_GetGroupByNameArgs{name} + _returns := &Z_GetGroupByNameReturns{} + if err := g.client.Call("Plugin.GetGroupByName", _args, _returns); err != nil { + log.Printf("RPC call to GetGroupByName API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) GetGroupByName(args *Z_GetGroupByNameArgs, returns *Z_GetGroupByNameReturns) error { + if hook, ok := s.impl.(interface { + GetGroupByName(name string) (*model.Group, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetGroupByName(args.A) + } else { + return encodableError(fmt.Errorf("API GetGroupByName called but not implemented.")) + } + return nil +} + +type Z_GetGroupsForUserArgs struct { + A string +} + +type Z_GetGroupsForUserReturns struct { + A []*model.Group + B *model.AppError +} + +func (g *apiRPCClient) GetGroupsForUser(userId string) ([]*model.Group, *model.AppError) { + _args := &Z_GetGroupsForUserArgs{userId} + _returns := &Z_GetGroupsForUserReturns{} + if err := g.client.Call("Plugin.GetGroupsForUser", _args, _returns); err != nil { + log.Printf("RPC call to GetGroupsForUser API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) GetGroupsForUser(args *Z_GetGroupsForUserArgs, returns *Z_GetGroupsForUserReturns) error { + if hook, ok := s.impl.(interface { + GetGroupsForUser(userId string) ([]*model.Group, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetGroupsForUser(args.A) + } else { + return encodableError(fmt.Errorf("API GetGroupsForUser called but not implemented.")) + } + return nil +} + type Z_DeleteChannelMemberArgs struct { A string B string diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go index f308746374..4db7d5e5e2 100644 --- a/plugin/plugintest/api.go +++ b/plugin/plugintest/api.go @@ -996,6 +996,56 @@ func (_m *API) GetFileLink(fileId string) (string, *model.AppError) { return r0, r1 } +// GetGroup provides a mock function with given fields: groupId +func (_m *API) GetGroup(groupId string) (*model.Group, *model.AppError) { + ret := _m.Called(groupId) + + var r0 *model.Group + if rf, ok := ret.Get(0).(func(string) *model.Group); ok { + r0 = rf(groupId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Group) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { + r1 = rf(groupId) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +// GetGroupByName provides a mock function with given fields: name +func (_m *API) GetGroupByName(name string) (*model.Group, *model.AppError) { + ret := _m.Called(name) + + var r0 *model.Group + if rf, ok := ret.Get(0).(func(string) *model.Group); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Group) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { + r1 = rf(name) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // GetGroupChannel provides a mock function with given fields: userIds func (_m *API) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) { ret := _m.Called(userIds) @@ -1021,6 +1071,31 @@ func (_m *API) GetGroupChannel(userIds []string) (*model.Channel, *model.AppErro return r0, r1 } +// GetGroupsForUser provides a mock function with given fields: userId +func (_m *API) GetGroupsForUser(userId string) ([]*model.Group, *model.AppError) { + ret := _m.Called(userId) + + var r0 []*model.Group + if rf, ok := ret.Get(0).(func(string) []*model.Group); ok { + r0 = rf(userId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Group) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { + r1 = rf(userId) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // GetLDAPUserAttributes provides a mock function with given fields: userId, attributes func (_m *API) GetLDAPUserAttributes(userId string, attributes []string) (map[string]string, *model.AppError) { ret := _m.Called(userId, attributes) diff --git a/store/sqlstore/group_store.go b/store/sqlstore/group_store.go index 0895ef0ae9..7ce65b23c4 100644 --- a/store/sqlstore/group_store.go +++ b/store/sqlstore/group_store.go @@ -123,6 +123,18 @@ func (s *SqlGroupStore) Get(groupId string) (*model.Group, *model.AppError) { return group, nil } +func (s *SqlGroupStore) GetByName(name string) (*model.Group, *model.AppError) { + var group *model.Group + if err := s.GetReplica().SelectOne(&group, "SELECT * from UserGroups WHERE Name = :Name", map[string]interface{}{"Name": name}); err != nil { + if err == sql.ErrNoRows { + return nil, model.NewAppError("SqlGroupStore.GroupGetByName", "store.sql_group.no_rows", nil, err.Error(), http.StatusNotFound) + } + return nil, model.NewAppError("SqlGroupStore.GroupGetByName", "store.select_error", nil, err.Error(), http.StatusInternalServerError) + } + + return group, nil +} + func (s *SqlGroupStore) GetByIDs(groupIDs []string) ([]*model.Group, *model.AppError) { var groups []*model.Group query := s.getQueryBuilder().Select("*").From("UserGroups").Where(sq.Eq{"Id": groupIDs}) @@ -158,6 +170,26 @@ func (s *SqlGroupStore) GetAllBySource(groupSource model.GroupSource) ([]*model. return groups, nil } +func (s *SqlGroupStore) GetByUser(userId string) ([]*model.Group, *model.AppError) { + var groups []*model.Group + + query := ` + SELECT + UserGroups.* + FROM + GroupMembers + JOIN UserGroups ON UserGroups.Id = GroupMembers.GroupId + WHERE + GroupMembers.DeleteAt = 0 + AND UserId = :UserId` + + if _, err := s.GetReplica().Select(&groups, query, map[string]interface{}{"UserId": userId}); err != nil { + return nil, model.NewAppError("SqlGroupStore.GetByUser", "store.select_error", nil, err.Error(), http.StatusInternalServerError) + } + + return groups, nil +} + func (s *SqlGroupStore) Update(group *model.Group) (*model.Group, *model.AppError) { var retrievedGroup *model.Group if err := s.GetMaster().SelectOne(&retrievedGroup, "SELECT * FROM UserGroups WHERE Id = :Id", map[string]interface{}{"Id": group.Id}); err != nil { diff --git a/store/store.go b/store/store.go index 9ad3324a02..ab28c0f588 100644 --- a/store/store.go +++ b/store/store.go @@ -569,9 +569,11 @@ type UserTermsOfServiceStore interface { type GroupStore interface { Create(group *model.Group) (*model.Group, *model.AppError) Get(groupID string) (*model.Group, *model.AppError) + GetByName(name string) (*model.Group, *model.AppError) GetByIDs(groupIDs []string) ([]*model.Group, *model.AppError) GetByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError) GetAllBySource(groupSource model.GroupSource) ([]*model.Group, *model.AppError) + GetByUser(userId string) ([]*model.Group, *model.AppError) Update(group *model.Group) (*model.Group, *model.AppError) Delete(groupID string) (*model.Group, *model.AppError) diff --git a/store/storetest/group_store.go b/store/storetest/group_store.go index 11afeb3fe8..0a289cc3ef 100644 --- a/store/storetest/group_store.go +++ b/store/storetest/group_store.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-server/model" @@ -20,9 +21,11 @@ import ( func TestGroupStore(t *testing.T, ss store.Store) { t.Run("Create", func(t *testing.T) { testGroupStoreCreate(t, ss) }) t.Run("Get", func(t *testing.T) { testGroupStoreGet(t, ss) }) + t.Run("GetByName", func(t *testing.T) { testGroupStoreGetByName(t, ss) }) t.Run("GetByIDs", func(t *testing.T) { testGroupStoreGetByIDs(t, ss) }) t.Run("GetByRemoteID", func(t *testing.T) { testGroupStoreGetByRemoteID(t, ss) }) t.Run("GetAllBySource", func(t *testing.T) { testGroupStoreGetAllByType(t, ss) }) + t.Run("GetByUser", func(t *testing.T) { testGroupStoreGetByUser(t, ss) }) t.Run("Update", func(t *testing.T) { testGroupStoreUpdate(t, ss) }) t.Run("Delete", func(t *testing.T) { testGroupStoreDelete(t, ss) }) @@ -181,6 +184,37 @@ func testGroupStoreGet(t *testing.T, ss store.Store) { require.Equal(t, err.Id, "store.sql_group.no_rows") } +func testGroupStoreGetByName(t *testing.T, ss store.Store) { + // Create a group + g1 := &model.Group{ + Name: model.NewId(), + DisplayName: model.NewId(), + Description: model.NewId(), + Source: model.GroupSourceLdap, + RemoteId: model.NewId(), + } + d1, err := ss.Group().Create(g1) + require.Nil(t, err) + require.Len(t, d1.Id, 26) + + // Get the group + d2, err := ss.Group().GetByName(d1.Name) + require.Nil(t, err) + require.Equal(t, d1.Id, d2.Id) + require.Equal(t, d1.Name, d2.Name) + require.Equal(t, d1.DisplayName, d2.DisplayName) + require.Equal(t, d1.Description, d2.Description) + require.Equal(t, d1.RemoteId, d2.RemoteId) + require.Equal(t, d1.CreateAt, d2.CreateAt) + require.Equal(t, d1.UpdateAt, d2.UpdateAt) + require.Equal(t, d1.DeleteAt, d2.DeleteAt) + + // Get an invalid group + _, err = ss.Group().GetByName(model.NewId()) + require.NotNil(t, err) + require.Equal(t, err.Id, "store.sql_group.no_rows") +} + func testGroupStoreGetByIDs(t *testing.T, ss store.Store) { var group1 *model.Group var group2 *model.Group @@ -280,6 +314,76 @@ func testGroupStoreGetAllByType(t *testing.T, ss store.Store) { } } +func testGroupStoreGetByUser(t *testing.T, ss store.Store) { + // Save a group + g1 := &model.Group{ + Name: model.NewId(), + DisplayName: model.NewId(), + Description: model.NewId(), + Source: model.GroupSourceLdap, + RemoteId: model.NewId(), + } + g1, err := ss.Group().Create(g1) + require.Nil(t, err) + + g2 := &model.Group{ + Name: model.NewId(), + DisplayName: model.NewId(), + Description: model.NewId(), + Source: model.GroupSourceLdap, + RemoteId: model.NewId(), + } + g2, err = ss.Group().Create(g2) + require.Nil(t, err) + + u1 := &model.User{ + Email: MakeEmail(), + Username: model.NewId(), + } + u1, err = ss.User().Save(u1) + require.Nil(t, err) + + _, err = ss.Group().UpsertMember(g1.Id, u1.Id) + require.Nil(t, err) + _, err = ss.Group().UpsertMember(g2.Id, u1.Id) + require.Nil(t, err) + + u2 := &model.User{ + Email: MakeEmail(), + Username: model.NewId(), + } + u2, err = ss.User().Save(u2) + require.Nil(t, err) + + _, err = ss.Group().UpsertMember(g2.Id, u2.Id) + require.Nil(t, err) + + groups, err := ss.Group().GetByUser(u1.Id) + require.Nil(t, err) + assert.Equal(t, 2, len(groups)) + found1 := false + found2 := false + for _, g := range groups { + if g.Id == g1.Id { + found1 = true + } + if g.Id == g2.Id { + found2 = true + } + } + assert.True(t, found1) + assert.True(t, found2) + + groups, err = ss.Group().GetByUser(u2.Id) + require.Nil(t, err) + require.Equal(t, 1, len(groups)) + assert.Equal(t, g2.Id, groups[0].Id) + + groups, err = ss.Group().GetByUser(model.NewId()) + require.Nil(t, err) + assert.Equal(t, 0, len(groups)) +} + func testGroupStoreUpdate(t *testing.T, ss store.Store) { // Save a new group g1 := &model.Group{ diff --git a/store/storetest/mocks/GroupStore.go b/store/storetest/mocks/GroupStore.go index f630d27581..e5328e4e50 100644 --- a/store/storetest/mocks/GroupStore.go +++ b/store/storetest/mocks/GroupStore.go @@ -406,6 +406,31 @@ func (_m *GroupStore) GetByIDs(groupIDs []string) ([]*model.Group, *model.AppErr return r0, r1 } +// GetByName provides a mock function with given fields: name +func (_m *GroupStore) GetByName(name string) (*model.Group, *model.AppError) { + ret := _m.Called(name) + + var r0 *model.Group + if rf, ok := ret.Get(0).(func(string) *model.Group); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Group) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { + r1 = rf(name) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // GetByRemoteID provides a mock function with given fields: remoteID, groupSource func (_m *GroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError) { ret := _m.Called(remoteID, groupSource) @@ -431,6 +456,31 @@ func (_m *GroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSour return r0, r1 } +// GetByUser provides a mock function with given fields: userId +func (_m *GroupStore) GetByUser(userId string) ([]*model.Group, *model.AppError) { + ret := _m.Called(userId) + + var r0 []*model.Group + if rf, ok := ret.Get(0).(func(string) []*model.Group); ok { + r0 = rf(userId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Group) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { + r1 = rf(userId) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // GetGroupSyncable provides a mock function with given fields: groupID, syncableID, syncableType func (_m *GroupStore) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) { ret := _m.Called(groupID, syncableID, syncableType) diff --git a/store/timer_layer.go b/store/timer_layer.go index 3632de3d81..366150b79a 100644 --- a/store/timer_layer.go +++ b/store/timer_layer.go @@ -2776,6 +2776,23 @@ func (s *TimerLayerGroupStore) GetByIDs(groupIDs []string) ([]*model.Group, *mod return resultVar0, resultVar1 } +func (s *TimerLayerGroupStore) GetByName(name string) (*model.Group, *model.AppError) { + start := timemodule.Now() + + resultVar0, resultVar1 := s.GroupStore.GetByName(name) + + t := timemodule.Now() + elapsed := t.Sub(start) + if s.Root.Metrics != nil { + success := "false" + if resultVar1 == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetByName", success, float64(elapsed)) + } + return resultVar0, resultVar1 +} + func (s *TimerLayerGroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError) { start := timemodule.Now() @@ -2792,6 +2809,23 @@ func (s *TimerLayerGroupStore) GetByRemoteID(remoteID string, groupSource model. return resultVar0, resultVar1 } +func (s *TimerLayerGroupStore) GetByUser(userId string) ([]*model.Group, *model.AppError) { + start := timemodule.Now() + + resultVar0, resultVar1 := s.GroupStore.GetByUser(userId) + + t := timemodule.Now() + elapsed := t.Sub(start) + if s.Root.Metrics != nil { + success := "false" + if resultVar1 == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetByUser", success, float64(elapsed)) + } + return resultVar0, resultVar1 +} + func (s *TimerLayerGroupStore) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) { start := timemodule.Now()