[MM-64683] Implement property field counting functionality in Plugin API (#33438)

* Implement property field limit enforcement and counting functionality in Plugin API

- Added a limit of 20 property fields per group in the CreatePropertyField method.
- Introduced CountPropertyFields method to count active and all property fields, including deleted ones.
- Enhanced tests to validate the new property field limit and counting behavior.
- Updated related API and service methods to support the new functionality.

* Update server/channels/app/properties/property_field.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix vet

* fix lint error

* fix test

* fix tests

* fix test

* count properties + targets

* Update server/channels/app/plugin_api.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* remove test for limit

* fix more tests

* improve testing messages now that the limit is removed

* Apply suggestion from @calebroseland

Co-authored-by: Caleb Roseland <caleb@calebroseland.com>

* Apply suggestion from @calebroseland

Co-authored-by: Caleb Roseland <caleb@calebroseland.com>

* Apply suggestion from @calebroseland

Co-authored-by: Caleb Roseland <caleb@calebroseland.com>

* Apply suggestion from @calebroseland

Co-authored-by: Caleb Roseland <caleb@calebroseland.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Julien Tant <785518+JulienTant@users.noreply.github.com>
Co-authored-by: Caleb Roseland <caleb@calebroseland.com>
This commit is contained in:
Guillermo Vayá
2025-09-11 12:49:14 -07:00
committed by GitHub
co-authored by Copilot Caleb Roseland Mattermost Build Julien Tant
parent 316712522c
commit d15b933888
14 changed files with 901 additions and 18 deletions
+18
View File
@@ -1503,6 +1503,10 @@ func (api *PluginAPI) DeleteGroupConstrainedMemberships() *model.AppError {
}
func (api *PluginAPI) CreatePropertyField(field *model.PropertyField) (*model.PropertyField, error) {
if field == nil {
return nil, fmt.Errorf("invalid input: property field parameter is required")
}
return api.app.PropertyService().CreatePropertyField(field)
}
@@ -1526,6 +1530,20 @@ func (api *PluginAPI) SearchPropertyFields(groupID, targetID string, opts model.
return api.app.PropertyService().SearchPropertyFields(groupID, targetID, opts)
}
func (api *PluginAPI) CountPropertyFields(groupID string, includeDeleted bool) (int64, error) {
if includeDeleted {
return api.app.PropertyService().CountAllPropertyFieldsForGroup(groupID)
}
return api.app.PropertyService().CountActivePropertyFieldsForGroup(groupID)
}
func (api *PluginAPI) CountPropertyFieldsForTarget(groupID, targetType, targetID string, includeDeleted bool) (int64, error) {
if includeDeleted {
return api.app.PropertyService().CountAllPropertyFieldsForTarget(groupID, targetType, targetID)
}
return api.app.PropertyService().CountActivePropertyFieldsForTarget(groupID, targetType, targetID)
}
func (api *PluginAPI) CreatePropertyValue(value *model.PropertyValue) (*model.PropertyValue, error) {
return api.app.PropertyService().CreatePropertyValue(value)
}
+213
View File
@@ -2892,3 +2892,216 @@ func TestPluginServeHTTPCompatibility(t *testing.T) {
})
}
}
func TestPluginAPICreatePropertyField(t *testing.T) {
mainHelper.Parallel(t)
t.Run("should allow creation after deleting fields", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
// Create 20 property fields
groupID := model.NewId()
var createdFields []*model.PropertyField
for i := 1; i <= 20; i++ {
field := &model.PropertyField{
GroupID: groupID,
Name: fmt.Sprintf("field_%d", i),
Type: model.PropertyFieldTypeText,
CreateAt: model.GetMillis(),
UpdateAt: model.GetMillis(),
}
created, err := api.CreatePropertyField(field)
require.NoError(t, err)
createdFields = append(createdFields, created)
}
// Delete one field
err := api.DeletePropertyField(groupID, createdFields[0].ID)
require.NoError(t, err)
// Should now be able to create another field
newField := &model.PropertyField{
GroupID: groupID,
Name: "new_field",
Type: model.PropertyFieldTypeText,
CreateAt: model.GetMillis(),
UpdateAt: model.GetMillis(),
}
created, err := api.CreatePropertyField(newField)
require.NoError(t, err)
assert.Equal(t, newField.Name, created.Name)
})
t.Run("should not count deleted fields", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
groupID := model.NewId()
// Create and delete 5 fields
for i := 1; i <= 5; i++ {
field := &model.PropertyField{
GroupID: groupID,
Name: fmt.Sprintf("deleted_field_%d", i),
Type: model.PropertyFieldTypeText,
CreateAt: model.GetMillis(),
UpdateAt: model.GetMillis(),
}
created, err := api.CreatePropertyField(field)
require.NoError(t, err)
err = api.DeletePropertyField(groupID, created.ID)
require.NoError(t, err)
}
// Should be able to create multiple active fields
for i := 1; i <= 20; i++ {
field := &model.PropertyField{
GroupID: groupID,
Name: fmt.Sprintf("active_field_%d", i),
Type: model.PropertyFieldTypeText,
CreateAt: model.GetMillis(),
UpdateAt: model.GetMillis(),
}
created, err := api.CreatePropertyField(field)
require.NoError(t, err)
assert.Equal(t, field.Name, created.Name)
}
})
t.Run("should reject empty or invalid group ID", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
// Test with empty group ID - should fail validation
field := &model.PropertyField{
GroupID: "",
Name: "test_field",
Type: model.PropertyFieldTypeText,
CreateAt: model.GetMillis(),
UpdateAt: model.GetMillis(),
}
created, err := api.CreatePropertyField(field)
require.Error(t, err) // Should fail due to invalid GroupID
assert.Nil(t, created)
assert.Contains(t, err.Error(), "group_id")
// Test with nil field - should fail gracefully
created, err = api.CreatePropertyField(nil)
require.Error(t, err) // Should fail when given nil input
assert.Nil(t, created)
assert.Contains(t, err.Error(), "invalid input: property field parameter is required")
})
}
func TestPluginAPICountPropertyFields(t *testing.T) {
mainHelper.Parallel(t)
t.Run("should count active property fields only", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
groupID := model.NewId()
// Create 5 fields
var createdFields []*model.PropertyField
for i := 1; i <= 5; i++ {
field := &model.PropertyField{
GroupID: groupID,
Name: fmt.Sprintf("field_%d", i),
Type: model.PropertyFieldTypeText,
CreateAt: model.GetMillis(),
UpdateAt: model.GetMillis(),
}
created, err := api.CreatePropertyField(field)
require.NoError(t, err)
createdFields = append(createdFields, created)
}
// Count active fields
count, err := api.CountPropertyFields(groupID, false)
require.NoError(t, err)
assert.Equal(t, int64(5), count)
// Delete 2 fields
err = api.DeletePropertyField(groupID, createdFields[0].ID)
require.NoError(t, err)
err = api.DeletePropertyField(groupID, createdFields[1].ID)
require.NoError(t, err)
// Count should now be 3
count, err = api.CountPropertyFields(groupID, false)
require.NoError(t, err)
assert.Equal(t, int64(3), count)
})
t.Run("should count all property fields including deleted", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
groupID := model.NewId()
// Create 5 fields
var createdFields []*model.PropertyField
for i := 1; i <= 5; i++ {
field := &model.PropertyField{
GroupID: groupID,
Name: fmt.Sprintf("field_%d", i),
Type: model.PropertyFieldTypeText,
CreateAt: model.GetMillis(),
UpdateAt: model.GetMillis(),
}
created, err := api.CreatePropertyField(field)
require.NoError(t, err)
createdFields = append(createdFields, created)
}
// Count all fields
count, err := api.CountPropertyFields(groupID, true)
require.NoError(t, err)
assert.Equal(t, int64(5), count)
// Delete 2 fields
err = api.DeletePropertyField(groupID, createdFields[0].ID)
require.NoError(t, err)
err = api.DeletePropertyField(groupID, createdFields[1].ID)
require.NoError(t, err)
// Count all should still be 5
count, err = api.CountPropertyFields(groupID, true)
require.NoError(t, err)
assert.Equal(t, int64(5), count)
// Count active should be 3
count, err = api.CountPropertyFields(groupID, false)
require.NoError(t, err)
assert.Equal(t, int64(3), count)
})
t.Run("should return 0 for empty group", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
api := th.SetupPluginAPI()
count, err := api.CountPropertyFields("non-existent-group", false)
require.NoError(t, err)
assert.Equal(t, int64(0), count)
count, err = api.CountPropertyFields("non-existent-group", true)
require.NoError(t, err)
assert.Equal(t, int64(0), count)
})
}
+168 -18
View File
@@ -38,9 +38,10 @@ func TestPluginProperties(t *testing.T) {
}
// Create a property field
fieldName := "Test Field " + model.NewId()
field := &model.PropertyField{
GroupID: group.ID,
Name: "Test Field",
Name: fieldName,
Type: model.PropertyFieldTypeText,
TargetType: "user",
}
@@ -55,8 +56,8 @@ func TestPluginProperties(t *testing.T) {
if err != nil {
return fmt.Errorf("failed to get property field: %w", err)
}
if retrievedField.Name != "Test Field" {
return fmt.Errorf("field name mismatch: expected 'Test Field', got '%s'", retrievedField.Name)
if retrievedField.Name != fieldName {
return fmt.Errorf("field name mismatch: expected '%s', got '%s'", fieldName, retrievedField.Name)
}
// Update the field
@@ -70,7 +71,7 @@ func TestPluginProperties(t *testing.T) {
}
// Search for fields
fields, err := p.API.SearchPropertyFields(group.ID, "", model.PropertyFieldSearchOpts{})
fields, err := p.API.SearchPropertyFields(group.ID, "", model.PropertyFieldSearchOpts{PerPage: 50})
if err != nil {
return fmt.Errorf("failed to search property fields: %w", err)
}
@@ -85,7 +86,7 @@ func TestPluginProperties(t *testing.T) {
}
// Verify deletion
fields, err = p.API.SearchPropertyFields(group.ID, "", model.PropertyFieldSearchOpts{})
fields, err = p.API.SearchPropertyFields(group.ID, "", model.PropertyFieldSearchOpts{PerPage: 50})
if err != nil {
return fmt.Errorf("failed to search property fields after deletion: %w", err)
}
@@ -102,7 +103,7 @@ func TestPluginProperties(t *testing.T) {
`}, th.App, th.NewPluginAPI)
defer tearDown()
require.Len(t, activationErrors, 1)
require.Nil(t, nil, activationErrors[0])
require.NoError(t, activationErrors[0])
// Clean up
err2 := th.App.DisablePlugin(pluginIDs[0])
@@ -134,9 +135,10 @@ func TestPluginProperties(t *testing.T) {
}
// Create a property field
fieldName := "Test Field " + model.NewId()
field := &model.PropertyField{
GroupID: group.ID,
Name: "Test Field",
Name: fieldName,
Type: model.PropertyFieldTypeText,
TargetType: "user",
}
@@ -148,7 +150,7 @@ func TestPluginProperties(t *testing.T) {
// Create a property value
targetId := model.NewId()
valueJson := []byte("test-value")
valueJson := []byte("\"test-value\"")
value := &model.PropertyValue{
GroupID: group.ID,
FieldID: createdField.ID,
@@ -167,22 +169,22 @@ func TestPluginProperties(t *testing.T) {
if err != nil {
return fmt.Errorf("failed to get property value: %w", err)
}
if string(retrievedValue.Value) != "test-value" {
return fmt.Errorf("value mismatch: expected 'test-value', got '%s'", string(retrievedValue.Value))
if string(retrievedValue.Value) != "\"test-value\"" {
return fmt.Errorf("value mismatch: expected '\"test-value\"', got '%s'", string(retrievedValue.Value))
}
// Update the value
retrievedValue.Value = []byte("updated-test-value")
retrievedValue.Value = []byte("\"updated-test-value\"")
updatedValue, err := p.API.UpdatePropertyValue(group.ID, retrievedValue)
if err != nil {
return fmt.Errorf("failed to update property value: %w", err)
}
if string(updatedValue.Value) != "updated-test-value" {
return fmt.Errorf("updated value mismatch: expected 'updated-test-value', got '%s'", string(updatedValue.Value))
if string(updatedValue.Value) != "\"updated-test-value\"" {
return fmt.Errorf("updated value mismatch: expected '\"updated-test-value\"', got '%s'", string(updatedValue.Value))
}
// Upsert the value
upsertValueJson := []byte("upserted-value")
upsertValueJson := []byte("\"upserted-value\"")
upsertValue := &model.PropertyValue{
GroupID: group.ID,
FieldID: createdField.ID,
@@ -197,7 +199,7 @@ func TestPluginProperties(t *testing.T) {
}
// Search for values
values, err := p.API.SearchPropertyValues(group.ID, targetId, model.PropertyValueSearchOpts{})
values, err := p.API.SearchPropertyValues(group.ID, targetId, model.PropertyValueSearchOpts{PerPage: 50})
if err != nil {
return fmt.Errorf("failed to search property values: %w", err)
}
@@ -212,7 +214,7 @@ func TestPluginProperties(t *testing.T) {
}
// Verify deletion
values, err = p.API.SearchPropertyValues(group.ID, targetId, model.PropertyValueSearchOpts{})
values, err = p.API.SearchPropertyValues(group.ID, targetId, model.PropertyValueSearchOpts{PerPage: 50})
if err != nil {
return fmt.Errorf("failed to search property values after deletion: %w", err)
}
@@ -229,7 +231,7 @@ func TestPluginProperties(t *testing.T) {
`}, th.App, th.NewPluginAPI)
defer tearDown()
require.Len(t, activationErrors, 1)
require.Nil(t, nil, activationErrors[0])
require.NoError(t, activationErrors[0])
// Clean up
err2 := th.App.DisablePlugin(pluginIDs[0])
@@ -277,7 +279,155 @@ func TestPluginProperties(t *testing.T) {
`}, th.App, th.NewPluginAPI)
defer tearDown()
require.Len(t, activationErrors, 1)
require.Nil(t, nil, activationErrors[0])
require.NoError(t, activationErrors[0])
// Clean up
err2 := th.App.DisablePlugin(pluginIDs[0])
require.Nil(t, err2)
appErr := th.App.ch.RemovePlugin(pluginIDs[0])
require.Nil(t, appErr)
})
t.Run("test property field counting", func(t *testing.T) {
groupName := model.NewId()
tearDown, pluginIDs, activationErrors := SetAppEnvironmentWithPlugins(t, []string{`
package main
import (
"fmt"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) OnActivate() error {
// Register a property group
group, err := p.API.RegisterPropertyGroup("` + groupName + `")
if err != nil {
return fmt.Errorf("failed to register property group: %w", err)
}
// Create multiple property fields for the same target
targetId := model.NewId()
for i := 1; i <= 20; i++ {
field := &model.PropertyField{
GroupID: group.ID,
Name: fmt.Sprintf("Field %d", i),
Type: model.PropertyFieldTypeText,
TargetType: "user",
TargetID: targetId,
}
_, err := p.API.CreatePropertyField(field)
if err != nil {
return fmt.Errorf("failed to create property field %d: %w", i, err)
}
}
// Count active fields - should be 20
count, err := p.API.CountPropertyFields(group.ID, false)
if err != nil {
return fmt.Errorf("failed to count property fields: %w", err)
}
if count != 20 {
return fmt.Errorf("expected 20 active fields (test creates 20), got %d", count)
}
// Search for fields to get one to delete
fields, err := p.API.SearchPropertyFields(group.ID, "", model.PropertyFieldSearchOpts{PerPage: 1})
if err != nil {
return fmt.Errorf("failed to search property fields: %w", err)
}
if len(fields) == 0 {
return fmt.Errorf("no fields found to delete")
}
// Delete one field
err = p.API.DeletePropertyField(group.ID, fields[0].ID)
if err != nil {
return fmt.Errorf("failed to delete property field: %w", err)
}
// Count active fields - should be 19
count, err = p.API.CountPropertyFields(group.ID, false)
if err != nil {
return fmt.Errorf("failed to count property fields after deletion: %w", err)
}
if count != 19 {
return fmt.Errorf("expected 19 active fields after deletion, got %d", count)
}
// Count all fields including deleted - should be 20
totalCount, err := p.API.CountPropertyFields(group.ID, true)
if err != nil {
return fmt.Errorf("failed to count all property fields: %w", err)
}
if totalCount != 20 {
return fmt.Errorf("expected 20 total fields including deleted (test created 20), got %d", totalCount)
}
// Now creating a new field for the same target should work again
newField := &model.PropertyField{
GroupID: group.ID,
Name: "New Field",
Type: model.PropertyFieldTypeText,
TargetType: "user",
TargetID: targetId,
}
_, err = p.API.CreatePropertyField(newField)
if err != nil {
return fmt.Errorf("failed to create new field after deletion: %w", err)
}
// Count should be back to 20
count, err = p.API.CountPropertyFields(group.ID, false)
if err != nil {
return fmt.Errorf("failed to count property fields after new creation: %w", err)
}
if count != 20 {
return fmt.Errorf("expected 20 active fields after new creation (19 + 1), got %d", count)
}
// Test that we can create fields for a different target
differentTargetId := model.NewId()
for i := 1; i <= 20; i++ {
field := &model.PropertyField{
GroupID: group.ID,
Name: fmt.Sprintf("Different Target Field %d", i),
Type: model.PropertyFieldTypeText,
TargetType: "user",
TargetID: differentTargetId,
}
_, err := p.API.CreatePropertyField(field)
if err != nil {
return fmt.Errorf("failed to create property field %d for different target: %w", i, err)
}
}
// Total count should now be 40 (20 for each target)
totalCount, err = p.API.CountPropertyFields(group.ID, false)
if err != nil {
return fmt.Errorf("failed to count total property fields: %w", err)
}
if totalCount != 40 {
return fmt.Errorf("expected 40 total active fields, got %d", totalCount)
}
return nil
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`}, th.App, th.NewPluginAPI)
defer tearDown()
require.Len(t, activationErrors, 1)
require.NoError(t, activationErrors[0])
// Clean up
err2 := th.App.DisablePlugin(pluginIDs[0])
@@ -29,6 +29,18 @@ func (ps *PropertyService) CountActivePropertyFieldsForGroup(groupID string) (in
return ps.fieldStore.CountForGroup(groupID, false)
}
func (ps *PropertyService) CountAllPropertyFieldsForGroup(groupID string) (int64, error) {
return ps.fieldStore.CountForGroup(groupID, true)
}
func (ps *PropertyService) CountActivePropertyFieldsForTarget(groupID, targetType, targetID string) (int64, error) {
return ps.fieldStore.CountForTarget(groupID, targetType, targetID, false)
}
func (ps *PropertyService) CountAllPropertyFieldsForTarget(groupID, targetType, targetID string) (int64, error) {
return ps.fieldStore.CountForTarget(groupID, targetType, targetID, true)
}
func (ps *PropertyService) SearchPropertyFields(groupID, targetID string, opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error) {
// groupID and targetID are part of the search method signature to
// incentivize the use of the database indexes in searches
@@ -0,0 +1,206 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package properties
import (
"testing"
"github.com/mattermost/mattermost/server/public/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
// mockPropertyFieldStore is a mock implementation of PropertyFieldStore interface
type mockPropertyFieldStore struct {
mock.Mock
}
func (m *mockPropertyFieldStore) Create(field *model.PropertyField) (*model.PropertyField, error) {
args := m.Called(field)
return args.Get(0).(*model.PropertyField), args.Error(1)
}
func (m *mockPropertyFieldStore) Get(groupID, id string) (*model.PropertyField, error) {
args := m.Called(groupID, id)
return args.Get(0).(*model.PropertyField), args.Error(1)
}
func (m *mockPropertyFieldStore) GetMany(groupID string, ids []string) ([]*model.PropertyField, error) {
args := m.Called(groupID, ids)
return args.Get(0).([]*model.PropertyField), args.Error(1)
}
func (m *mockPropertyFieldStore) GetFieldByName(groupID, targetID, name string) (*model.PropertyField, error) {
args := m.Called(groupID, targetID, name)
return args.Get(0).(*model.PropertyField), args.Error(1)
}
func (m *mockPropertyFieldStore) CountForGroup(groupID string, includeDeleted bool) (int64, error) {
args := m.Called(groupID, includeDeleted)
return args.Get(0).(int64), args.Error(1)
}
func (m *mockPropertyFieldStore) CountForTarget(groupID, targetType, targetID string, includeDeleted bool) (int64, error) {
args := m.Called(groupID, targetType, targetID, includeDeleted)
return args.Get(0).(int64), args.Error(1)
}
func (m *mockPropertyFieldStore) SearchPropertyFields(opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error) {
args := m.Called(opts)
return args.Get(0).([]*model.PropertyField), args.Error(1)
}
func (m *mockPropertyFieldStore) Update(groupID string, fields []*model.PropertyField) ([]*model.PropertyField, error) {
args := m.Called(groupID, fields)
return args.Get(0).([]*model.PropertyField), args.Error(1)
}
func (m *mockPropertyFieldStore) Delete(groupID string, id string) error {
args := m.Called(groupID, id)
return args.Error(0)
}
func TestPropertyService_CountActivePropertyFieldsForGroup(t *testing.T) {
t.Run("should return count of active property fields for a group", func(t *testing.T) {
// Create a mock store
mockStore := &mockPropertyFieldStore{}
mockStore.On("CountForGroup", "group1", false).Return(int64(5), nil)
// Create the service
service := &PropertyService{
fieldStore: mockStore,
}
// Call the method
count, err := service.CountActivePropertyFieldsForGroup("group1")
// Verify the results
require.NoError(t, err)
assert.Equal(t, int64(5), count)
mockStore.AssertExpectations(t)
})
t.Run("should return error when store fails", func(t *testing.T) {
// Create a mock store
mockStore := &mockPropertyFieldStore{}
mockStore.On("CountForGroup", "group1", false).Return(int64(0), model.NewAppError("test", "test.error", nil, "", 500))
// Create the service
service := &PropertyService{
fieldStore: mockStore,
}
// Call the method
count, err := service.CountActivePropertyFieldsForGroup("group1")
// Verify the results
require.Error(t, err)
assert.Equal(t, int64(0), count)
mockStore.AssertExpectations(t)
})
t.Run("should return 0 for empty group", func(t *testing.T) {
// Create a mock store
mockStore := &mockPropertyFieldStore{}
mockStore.On("CountForGroup", "empty-group", false).Return(int64(0), nil)
// Create the service
service := &PropertyService{
fieldStore: mockStore,
}
// Call the method
count, err := service.CountActivePropertyFieldsForGroup("empty-group")
// Verify the results
require.NoError(t, err)
assert.Equal(t, int64(0), count)
mockStore.AssertExpectations(t)
})
}
func TestPropertyService_CountAllPropertyFieldsForGroup(t *testing.T) {
t.Run("should return count of all property fields including deleted for a group", func(t *testing.T) {
// Create a mock store
mockStore := &mockPropertyFieldStore{}
mockStore.On("CountForGroup", "group1", true).Return(int64(8), nil)
// Create the service
service := &PropertyService{
fieldStore: mockStore,
}
// Call the method
count, err := service.CountAllPropertyFieldsForGroup("group1")
// Verify the results
require.NoError(t, err)
assert.Equal(t, int64(8), count)
mockStore.AssertExpectations(t)
})
t.Run("should return error when store fails", func(t *testing.T) {
// Create a mock store
mockStore := &mockPropertyFieldStore{}
mockStore.On("CountForGroup", "group1", true).Return(int64(0), model.NewAppError("test", "test.error", nil, "", 500))
// Create the service
service := &PropertyService{
fieldStore: mockStore,
}
// Call the method
count, err := service.CountAllPropertyFieldsForGroup("group1")
// Verify the results
require.Error(t, err)
assert.Equal(t, int64(0), count)
mockStore.AssertExpectations(t)
})
t.Run("should return 0 for empty group", func(t *testing.T) {
// Create a mock store
mockStore := &mockPropertyFieldStore{}
mockStore.On("CountForGroup", "empty-group", true).Return(int64(0), nil)
// Create the service
service := &PropertyService{
fieldStore: mockStore,
}
// Call the method
count, err := service.CountAllPropertyFieldsForGroup("empty-group")
// Verify the results
require.NoError(t, err)
assert.Equal(t, int64(0), count)
mockStore.AssertExpectations(t)
})
t.Run("should return higher count than active fields when there are deleted fields", func(t *testing.T) {
// Create a mock store
mockStore := &mockPropertyFieldStore{}
mockStore.On("CountForGroup", "group1", false).Return(int64(5), nil)
mockStore.On("CountForGroup", "group1", true).Return(int64(8), nil)
// Create the service
service := &PropertyService{
fieldStore: mockStore,
}
// Call both methods
activeCount, err := service.CountActivePropertyFieldsForGroup("group1")
require.NoError(t, err)
allCount, err := service.CountAllPropertyFieldsForGroup("group1")
require.NoError(t, err)
// Verify that all count is higher than active count
assert.Equal(t, int64(5), activeCount)
assert.Equal(t, int64(8), allCount)
assert.True(t, allCount > activeCount)
mockStore.AssertExpectations(t)
})
}
@@ -118,6 +118,25 @@ func (s *SqlPropertyFieldStore) CountForGroup(groupID string, includeDeleted boo
return count, nil
}
func (s *SqlPropertyFieldStore) CountForTarget(groupID, targetType, targetID string, includeDeleted bool) (int64, error) {
var count int64
builder := s.getQueryBuilder().
Select("COUNT(id)").
From("PropertyFields").
Where(sq.Eq{"GroupID": groupID}).
Where(sq.Eq{"TargetType": targetType}).
Where(sq.Eq{"TargetID": targetID})
if !includeDeleted {
builder = builder.Where(sq.Eq{"DeleteAt": 0})
}
if err := s.GetReplica().GetBuilder(&count, builder); err != nil {
return int64(0), errors.Wrap(err, "failed to count property fields for target")
}
return count, nil
}
func (s *SqlPropertyFieldStore) SearchPropertyFields(opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error) {
if err := opts.Cursor.IsValid(); err != nil {
return nil, fmt.Errorf("cursor is invalid: %w", err)
+1
View File
@@ -1108,6 +1108,7 @@ type PropertyFieldStore interface {
GetMany(groupID string, ids []string) ([]*model.PropertyField, error)
GetFieldByName(groupID, targetID, name string) (*model.PropertyField, error)
CountForGroup(groupID string, includeDeleted bool) (int64, error)
CountForTarget(groupID, targetType, targetID string, includeDeleted bool) (int64, error)
SearchPropertyFields(opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error)
Update(groupID string, fields []*model.PropertyField) ([]*model.PropertyField, error)
Delete(groupID string, id string) error
@@ -42,6 +42,34 @@ func (_m *PropertyFieldStore) CountForGroup(groupID string, includeDeleted bool)
return r0, r1
}
// CountForTarget provides a mock function with given fields: groupID, targetType, targetID, includeDeleted
func (_m *PropertyFieldStore) CountForTarget(groupID string, targetType string, targetID string, includeDeleted bool) (int64, error) {
ret := _m.Called(groupID, targetType, targetID, includeDeleted)
if len(ret) == 0 {
panic("no return value specified for CountForTarget")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(string, string, string, bool) (int64, error)); ok {
return rf(groupID, targetType, targetID, includeDeleted)
}
if rf, ok := ret.Get(0).(func(string, string, string, bool) int64); ok {
r0 = rf(groupID, targetType, targetID, includeDeleted)
} else {
r0 = ret.Get(0).(int64)
}
if rf, ok := ret.Get(1).(func(string, string, string, bool) error); ok {
r1 = rf(groupID, targetType, targetID, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Create provides a mock function with given fields: field
func (_m *PropertyFieldStore) Create(field *model.PropertyField) (*model.PropertyField, error) {
ret := _m.Called(field)
+12
View File
@@ -1451,6 +1451,18 @@ type API interface {
// Minimum server version: 10.10
SearchPropertyFields(groupID, targetID string, opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error)
// CountPropertyFields counts property fields for a group.
//
// @tag PropertyField
// Minimum server version: 11.0
CountPropertyFields(groupID string, includeDeleted bool) (int64, error)
// CountPropertyFieldsForTarget counts property fields for a specific target.
//
// @tag PropertyField
// Minimum server version: 11.0
CountPropertyFieldsForTarget(groupID, targetType, targetID string, includeDeleted bool) (int64, error)
// CreatePropertyValue creates a new property value.
//
// @tag PropertyValue
@@ -1541,6 +1541,20 @@ func (api *apiTimerLayer) SearchPropertyFields(groupID, targetID string, opts mo
return _returnsA, _returnsB
}
func (api *apiTimerLayer) CountPropertyFields(groupID string, includeDeleted bool) (int64, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CountPropertyFields(groupID, includeDeleted)
api.recordTime(startTime, "CountPropertyFields", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) CountPropertyFieldsForTarget(groupID, targetType, targetID string, includeDeleted bool) (int64, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CountPropertyFieldsForTarget(groupID, targetType, targetID, includeDeleted)
api.recordTime(startTime, "CountPropertyFieldsForTarget", _returnsB == nil)
return _returnsA, _returnsB
}
func (api *apiTimerLayer) CreatePropertyValue(value *model.PropertyValue) (*model.PropertyValue, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.CreatePropertyValue(value)
@@ -7389,6 +7389,70 @@ func (s *apiRPCServer) SearchPropertyFields(args *Z_SearchPropertyFieldsArgs, re
return nil
}
type Z_CountPropertyFieldsArgs struct {
A string
B bool
}
type Z_CountPropertyFieldsReturns struct {
A int64
B error
}
func (g *apiRPCClient) CountPropertyFields(groupID string, includeDeleted bool) (int64, error) {
_args := &Z_CountPropertyFieldsArgs{groupID, includeDeleted}
_returns := &Z_CountPropertyFieldsReturns{}
if err := g.client.Call("Plugin.CountPropertyFields", _args, _returns); err != nil {
log.Printf("RPC call to CountPropertyFields API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) CountPropertyFields(args *Z_CountPropertyFieldsArgs, returns *Z_CountPropertyFieldsReturns) error {
if hook, ok := s.impl.(interface {
CountPropertyFields(groupID string, includeDeleted bool) (int64, error)
}); ok {
returns.A, returns.B = hook.CountPropertyFields(args.A, args.B)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("API CountPropertyFields called but not implemented."))
}
return nil
}
type Z_CountPropertyFieldsForTargetArgs struct {
A string
B string
C string
D bool
}
type Z_CountPropertyFieldsForTargetReturns struct {
A int64
B error
}
func (g *apiRPCClient) CountPropertyFieldsForTarget(groupID, targetType, targetID string, includeDeleted bool) (int64, error) {
_args := &Z_CountPropertyFieldsForTargetArgs{groupID, targetType, targetID, includeDeleted}
_returns := &Z_CountPropertyFieldsForTargetReturns{}
if err := g.client.Call("Plugin.CountPropertyFieldsForTarget", _args, _returns); err != nil {
log.Printf("RPC call to CountPropertyFieldsForTarget API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) CountPropertyFieldsForTarget(args *Z_CountPropertyFieldsForTargetArgs, returns *Z_CountPropertyFieldsForTargetReturns) error {
if hook, ok := s.impl.(interface {
CountPropertyFieldsForTarget(groupID, targetType, targetID string, includeDeleted bool) (int64, error)
}); ok {
returns.A, returns.B = hook.CountPropertyFieldsForTarget(args.A, args.B, args.C, args.D)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("API CountPropertyFieldsForTarget called but not implemented."))
}
return nil
}
type Z_CreatePropertyValueArgs struct {
A *model.PropertyValue
}
+56
View File
@@ -148,6 +148,62 @@ func (_m *API) CopyFileInfos(userID string, fileIds []string) ([]string, *model.
return r0, r1
}
// CountPropertyFields provides a mock function with given fields: groupID, includeDeleted
func (_m *API) CountPropertyFields(groupID string, includeDeleted bool) (int64, error) {
ret := _m.Called(groupID, includeDeleted)
if len(ret) == 0 {
panic("no return value specified for CountPropertyFields")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(string, bool) (int64, error)); ok {
return rf(groupID, includeDeleted)
}
if rf, ok := ret.Get(0).(func(string, bool) int64); ok {
r0 = rf(groupID, includeDeleted)
} else {
r0 = ret.Get(0).(int64)
}
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(groupID, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountPropertyFieldsForTarget provides a mock function with given fields: groupID, targetType, targetID, includeDeleted
func (_m *API) CountPropertyFieldsForTarget(groupID string, targetType string, targetID string, includeDeleted bool) (int64, error) {
ret := _m.Called(groupID, targetType, targetID, includeDeleted)
if len(ret) == 0 {
panic("no return value specified for CountPropertyFieldsForTarget")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(string, string, string, bool) (int64, error)); ok {
return rf(groupID, targetType, targetID, includeDeleted)
}
if rf, ok := ret.Get(0).(func(string, string, string, bool) int64); ok {
r0 = rf(groupID, targetType, targetID, includeDeleted)
} else {
r0 = ret.Get(0).(int64)
}
if rf, ok := ret.Get(1).(func(string, string, string, bool) error); ok {
r1 = rf(groupID, targetType, targetID, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateBot provides a mock function with given fields: bot
func (_m *API) CreateBot(bot *model.Bot) (*model.Bot, *model.AppError) {
ret := _m.Called(bot)
+14
View File
@@ -52,6 +52,20 @@ func (p *PropertyService) SearchPropertyFields(groupID, targetID string, opts mo
return p.api.SearchPropertyFields(groupID, targetID, opts)
}
// CountPropertyFields counts property fields for a group.
//
// Minimum server version: 11.0
func (p *PropertyService) CountPropertyFields(groupID string, includeDeleted bool) (int64, error) {
return p.api.CountPropertyFields(groupID, includeDeleted)
}
// CountPropertyFieldsForTarget counts property fields for a specific target.
//
// Minimum server version: 11.0
func (p *PropertyService) CountPropertyFieldsForTarget(groupID, targetType, targetID string, includeDeleted bool) (int64, error) {
return p.api.CountPropertyFieldsForTarget(groupID, targetType, targetID, includeDeleted)
}
// CreatePropertyValue creates a new property value.
//
// Minimum server version: 10.10
+76
View File
@@ -172,6 +172,82 @@ func TestPropertyFieldAPI(t *testing.T) {
assert.Equal(t, fields, result)
api.AssertExpectations(t)
})
t.Run("CountPropertyFields", func(t *testing.T) {
// Setup
api := &plugintest.API{}
// Mock the API call for active fields only
api.On("CountPropertyFields", "group1", false).Return(int64(5), nil)
// Create the client
client := NewClient(api, nil)
// Call the method
result, err := client.Property.CountPropertyFields("group1", false)
// Verify the results
require.NoError(t, err)
assert.Equal(t, int64(5), result)
api.AssertExpectations(t)
})
t.Run("CountPropertyFields with deleted", func(t *testing.T) {
// Setup
api := &plugintest.API{}
// Mock the API call for all fields including deleted
api.On("CountPropertyFields", "group1", true).Return(int64(8), nil)
// Create the client
client := NewClient(api, nil)
// Call the method
result, err := client.Property.CountPropertyFields("group1", true)
// Verify the results
require.NoError(t, err)
assert.Equal(t, int64(8), result)
api.AssertExpectations(t)
})
t.Run("CountPropertyFieldsForTarget", func(t *testing.T) {
// Setup
api := &plugintest.API{}
// Mock the API call for active fields for a specific target
api.On("CountPropertyFieldsForTarget", "group1", "user", "target123", false).Return(int64(3), nil)
// Create the client
client := NewClient(api, nil)
// Call the method
result, err := client.Property.CountPropertyFieldsForTarget("group1", "user", "target123", false)
// Verify the results
require.NoError(t, err)
assert.Equal(t, int64(3), result)
api.AssertExpectations(t)
})
t.Run("CountPropertyFieldsForTarget with deleted", func(t *testing.T) {
// Setup
api := &plugintest.API{}
// Mock the API call for all fields including deleted for a specific target
api.On("CountPropertyFieldsForTarget", "group1", "user", "target123", true).Return(int64(5), nil)
// Create the client
client := NewClient(api, nil)
// Call the method
result, err := client.Property.CountPropertyFieldsForTarget("group1", "user", "target123", true)
// Verify the results
require.NoError(t, err)
assert.Equal(t, int64(5), result)
api.AssertExpectations(t)
})
}
func TestPropertyValueAPI(t *testing.T) {