mattermost-community-enterp.../channels/store/storetest/property_field_store.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
Full Mattermost server source with integrated Community Enterprise features.
Includes vendor directory for offline/air-gapped builds.

Structure:
- enterprise-impl/: Enterprise feature implementations
- enterprise-community/: Init files that register implementations
- enterprise/: Bridge imports (community_imports.go)
- vendor/: All dependencies for offline builds

Build (online):
  go build ./cmd/mattermost

Build (offline/air-gapped):
  go build -mod=vendor ./cmd/mattermost

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 23:59:07 +09:00

1064 lines
34 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"database/sql"
"fmt"
"testing"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/stretchr/testify/require"
)
func TestPropertyFieldStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
t.Run("CreatePropertyField", func(t *testing.T) { testCreatePropertyField(t, rctx, ss) })
t.Run("GetPropertyField", func(t *testing.T) { testGetPropertyField(t, rctx, ss) })
t.Run("GetManyPropertyFields", func(t *testing.T) { testGetManyPropertyFields(t, rctx, ss) })
t.Run("GetFieldByName", func(t *testing.T) { testGetFieldByName(t, rctx, ss) })
t.Run("UpdatePropertyField", func(t *testing.T) { testUpdatePropertyField(t, rctx, ss) })
t.Run("DeletePropertyField", func(t *testing.T) { testDeletePropertyField(t, rctx, ss) })
t.Run("SearchPropertyFields", func(t *testing.T) { testSearchPropertyFields(t, rctx, ss) })
t.Run("SearchPropertyFieldsSince", func(t *testing.T) { testSearchPropertyFieldsSince(t, rctx, ss) })
t.Run("CountForGroup", func(t *testing.T) { testCountForGroup(t, rctx, ss) })
}
func testCreatePropertyField(t *testing.T, _ request.CTX, ss store.Store) {
t.Run("should fail if the property field already has an ID set", func(t *testing.T) {
newField := &model.PropertyField{ID: "sampleid"}
field, err := ss.PropertyField().Create(newField)
require.Zero(t, field)
var eii *store.ErrInvalidInput
require.ErrorAs(t, err, &eii)
})
t.Run("should fail if the property field is not valid", func(t *testing.T) {
newField := &model.PropertyField{GroupID: ""}
field, err := ss.PropertyField().Create(newField)
require.Zero(t, field)
require.ErrorContains(t, err, "model.property_field.is_valid.app_error")
newField = &model.PropertyField{GroupID: model.NewId(), Name: ""}
field, err = ss.PropertyField().Create(newField)
require.Zero(t, field)
require.ErrorContains(t, err, "model.property_field.is_valid.app_error")
})
newField := &model.PropertyField{
GroupID: model.NewId(),
Name: "My new property field",
Type: model.PropertyFieldTypeText,
Attrs: map[string]any{
"locked": true,
"special": "value",
},
}
t.Run("should be able to create a property field", func(t *testing.T) {
field, err := ss.PropertyField().Create(newField)
require.NoError(t, err)
require.NotZero(t, field.ID)
require.NotZero(t, field.CreateAt)
require.NotZero(t, field.UpdateAt)
require.Zero(t, field.DeleteAt)
})
t.Run("should enforce the field's uniqueness", func(t *testing.T) {
newField.ID = ""
field, err := ss.PropertyField().Create(newField)
require.Error(t, err)
require.Empty(t, field)
})
}
func testGetPropertyField(t *testing.T, _ request.CTX, ss store.Store) {
t.Run("should fail on nonexisting field", func(t *testing.T) {
field, err := ss.PropertyField().Get("", model.NewId())
require.Zero(t, field)
require.ErrorIs(t, err, sql.ErrNoRows)
})
groupID := model.NewId()
newField := &model.PropertyField{
GroupID: groupID,
Name: "My new property field",
Type: model.PropertyFieldTypeText,
Attrs: map[string]any{
"locked": true,
"special": "value",
},
}
_, err := ss.PropertyField().Create(newField)
require.NoError(t, err)
require.NotZero(t, newField.ID)
t.Run("should be able to retrieve an existing property field", func(t *testing.T) {
field, err := ss.PropertyField().Get(groupID, newField.ID)
require.NoError(t, err)
require.Equal(t, newField.ID, field.ID)
require.True(t, field.Attrs["locked"].(bool))
require.Equal(t, "value", field.Attrs["special"])
// should work without specifying the group ID as well
field, err = ss.PropertyField().Get("", newField.ID)
require.NoError(t, err)
require.Equal(t, newField.ID, field.ID)
require.True(t, field.Attrs["locked"].(bool))
require.Equal(t, "value", field.Attrs["special"])
})
t.Run("should not be able to retrieve an existing field when specifying a different group ID", func(t *testing.T) {
field, err := ss.PropertyField().Get(model.NewId(), newField.ID)
require.Zero(t, field)
require.ErrorIs(t, err, sql.ErrNoRows)
})
}
func testGetManyPropertyFields(t *testing.T, _ request.CTX, ss store.Store) {
t.Run("should fail on nonexisting fields", func(t *testing.T) {
fields, err := ss.PropertyField().GetMany("", []string{model.NewId(), model.NewId()})
require.Empty(t, fields)
require.ErrorContains(t, err, "missmatch results")
})
groupID := model.NewId()
newFields := []*model.PropertyField{}
for _, fieldName := range []string{"field1", "field2", "field3"} {
newField := &model.PropertyField{
GroupID: groupID,
Name: fieldName,
Type: model.PropertyFieldTypeText,
}
_, err := ss.PropertyField().Create(newField)
require.NoError(t, err)
require.NotZero(t, newField.ID)
newFields = append(newFields, newField)
}
newFieldOutsideGroup := &model.PropertyField{
GroupID: model.NewId(),
Name: "field outside the groupID",
Type: model.PropertyFieldTypeText,
}
_, err := ss.PropertyField().Create(newFieldOutsideGroup)
require.NoError(t, err)
require.NotZero(t, newFieldOutsideGroup.ID)
t.Run("should fail if at least one of the ids is nonexistent", func(t *testing.T) {
fields, err := ss.PropertyField().GetMany(groupID, []string{newFields[0].ID, newFields[1].ID, model.NewId()})
require.Empty(t, fields)
require.ErrorContains(t, err, "missmatch results")
})
t.Run("should be able to retrieve existing property fields", func(t *testing.T) {
fields, err := ss.PropertyField().GetMany(groupID, []string{newFields[0].ID, newFields[1].ID, newFields[2].ID})
require.NoError(t, err)
require.Len(t, fields, 3)
require.ElementsMatch(t, newFields, fields)
})
t.Run("should fail if asked for valid IDs but outside the group", func(t *testing.T) {
fields, err := ss.PropertyField().GetMany(groupID, []string{newFields[0].ID, newFieldOutsideGroup.ID})
require.Empty(t, fields)
require.ErrorContains(t, err, "missmatch results")
})
t.Run("should be able to retrieve existing property fields from multiple groups", func(t *testing.T) {
fields, err := ss.PropertyField().GetMany("", []string{newFields[0].ID, newFieldOutsideGroup.ID})
require.NoError(t, err)
require.Len(t, fields, 2)
})
}
func testGetFieldByName(t *testing.T, _ request.CTX, ss store.Store) {
t.Run("should fail on nonexisting field", func(t *testing.T) {
field, err := ss.PropertyField().GetFieldByName("", "", "nonexistent-field-name")
require.Zero(t, field)
require.ErrorIs(t, err, sql.ErrNoRows)
})
groupID := model.NewId()
targetID := model.NewId()
newField := &model.PropertyField{
GroupID: groupID,
TargetID: targetID,
Name: "unique-field-name",
Type: model.PropertyFieldTypeText,
Attrs: map[string]any{
"locked": true,
"special": "value",
},
}
_, cErr := ss.PropertyField().Create(newField)
require.NoError(t, cErr)
require.NotZero(t, newField.ID)
t.Run("should be able to retrieve an existing property field by name", func(t *testing.T) {
field, err := ss.PropertyField().GetFieldByName(groupID, targetID, "unique-field-name")
require.NoError(t, err)
require.Equal(t, newField.ID, field.ID)
require.Equal(t, "unique-field-name", field.Name)
require.True(t, field.Attrs["locked"].(bool))
require.Equal(t, "value", field.Attrs["special"])
})
t.Run("should not be able to retrieve an existing field when specifying a different group ID", func(t *testing.T) {
field, err := ss.PropertyField().GetFieldByName(model.NewId(), targetID, "unique-field-name")
require.Zero(t, field)
require.ErrorIs(t, err, sql.ErrNoRows)
})
t.Run("should not be able to retrieve an existing field when specifying a different target ID", func(t *testing.T) {
field, err := ss.PropertyField().GetFieldByName(groupID, model.NewId(), "unique-field-name")
require.Zero(t, field)
require.ErrorIs(t, err, sql.ErrNoRows)
})
// Test with multiple fields with the same name but different groups
anotherGroupID := model.NewId()
duplicateNameField := &model.PropertyField{
GroupID: anotherGroupID,
TargetID: targetID,
Name: "unique-field-name", // Same name as the first field
Type: model.PropertyFieldTypeSelect,
Attrs: map[string]any{
"options": []string{"a", "b", "c"},
},
}
_, cErr = ss.PropertyField().Create(duplicateNameField)
require.NoError(t, cErr)
require.NotZero(t, duplicateNameField.ID)
t.Run("should retrieve the correct field when multiple fields have the same name but different groups", func(t *testing.T) {
// Get the field from the first group
field, err := ss.PropertyField().GetFieldByName(groupID, targetID, "unique-field-name")
require.NoError(t, err)
require.Equal(t, newField.ID, field.ID)
require.Equal(t, model.PropertyFieldTypeText, field.Type)
// Get the field from the second group
field, err = ss.PropertyField().GetFieldByName(anotherGroupID, targetID, "unique-field-name")
require.NoError(t, err)
require.Equal(t, duplicateNameField.ID, field.ID)
require.Equal(t, model.PropertyFieldTypeSelect, field.Type)
})
// Test with multiple fields with the same name and same group but different target IDs
anotherTargetID := model.NewId()
sameGroupDifferentTargetField := &model.PropertyField{
GroupID: groupID,
TargetID: anotherTargetID,
Name: "unique-field-name", // Same name as the first field
Type: model.PropertyFieldTypeText,
Attrs: map[string]any{
"min": 1,
"max": 100,
},
}
_, cErr = ss.PropertyField().Create(sameGroupDifferentTargetField)
require.NoError(t, cErr)
require.NotZero(t, sameGroupDifferentTargetField.ID)
t.Run("should retrieve the correct field when multiple fields have the same name and group but different target IDs", func(t *testing.T) {
// Get the field with the first target ID
field, err := ss.PropertyField().GetFieldByName(groupID, targetID, "unique-field-name")
require.NoError(t, err)
require.Equal(t, newField.ID, field.ID)
require.Equal(t, model.PropertyFieldTypeText, field.Type)
// Get the field with the second target ID
field, err = ss.PropertyField().GetFieldByName(groupID, anotherTargetID, "unique-field-name")
require.NoError(t, err)
require.Equal(t, sameGroupDifferentTargetField.ID, field.ID)
require.Equal(t, model.PropertyFieldTypeText, field.Type)
})
// Test with a deleted field
t.Run("should not retrieve deleted fields", func(t *testing.T) {
// Create another field with a unique name
deletedField := &model.PropertyField{
GroupID: groupID,
TargetID: targetID,
Name: "to-be-deleted-field",
Type: model.PropertyFieldTypeText,
}
_, cErr := ss.PropertyField().Create(deletedField)
require.NoError(t, cErr)
require.NotZero(t, deletedField.ID)
// Verify it can be retrieved before deletion
field, err := ss.PropertyField().GetFieldByName(groupID, targetID, "to-be-deleted-field")
require.NoError(t, err)
require.Equal(t, deletedField.ID, field.ID)
// Delete the field
err = ss.PropertyField().Delete("", deletedField.ID)
require.NoError(t, err)
// Verify it can't be retrieved after deletion
field, err = ss.PropertyField().GetFieldByName(groupID, targetID, "to-be-deleted-field")
require.Zero(t, field)
require.ErrorIs(t, err, sql.ErrNoRows)
})
t.Run("should not retrieve fields with matching name but different DeleteAt status", func(t *testing.T) {
// Create a field with the same name/group/target as the deleted one
replacementField := &model.PropertyField{
GroupID: groupID,
TargetID: targetID,
Name: "to-be-deleted-field", // Same name as the deleted field
Type: model.PropertyFieldTypeText,
Attrs: map[string]any{
"min": 0,
"max": 10,
},
}
_, cErr := ss.PropertyField().Create(replacementField)
require.NoError(t, cErr)
require.NotZero(t, replacementField.ID)
// Verify only the non-deleted field is retrieved
field, err := ss.PropertyField().GetFieldByName(groupID, targetID, "to-be-deleted-field")
require.NoError(t, err)
require.Equal(t, replacementField.ID, field.ID)
require.Equal(t, model.PropertyFieldTypeText, field.Type)
require.Zero(t, field.DeleteAt)
})
}
func testUpdatePropertyField(t *testing.T, _ request.CTX, ss store.Store) {
t.Run("should fail on nonexisting field", func(t *testing.T) {
field := &model.PropertyField{
ID: model.NewId(),
GroupID: model.NewId(),
Name: "My property field",
Type: model.PropertyFieldTypeText,
CreateAt: model.GetMillis(),
}
updatedField, err := ss.PropertyField().Update("", []*model.PropertyField{field})
require.Zero(t, updatedField)
require.ErrorContains(t, err, "failed to update, some property fields were not found, got 0 of 1")
})
t.Run("should fail if the property field is not valid", func(t *testing.T) {
field := &model.PropertyField{
GroupID: model.NewId(),
Name: "My property field",
Type: model.PropertyFieldTypeText,
}
_, err := ss.PropertyField().Create(field)
require.NoError(t, err)
require.NotZero(t, field.ID)
field.GroupID = ""
updatedField, err := ss.PropertyField().Update("", []*model.PropertyField{field})
require.Zero(t, updatedField)
require.ErrorContains(t, err, "model.property_field.is_valid.app_error")
field.GroupID = model.NewId()
field.Name = ""
updatedField, err = ss.PropertyField().Update("", []*model.PropertyField{field})
require.Zero(t, updatedField)
require.ErrorContains(t, err, "model.property_field.is_valid.app_error")
})
t.Run("should be able to update multiple property fields", func(t *testing.T) {
field1 := &model.PropertyField{
GroupID: model.NewId(),
Name: "First field",
Type: model.PropertyFieldTypeText,
Attrs: map[string]any{
"locked": true,
"special": "value",
},
}
field2 := &model.PropertyField{
GroupID: model.NewId(),
Name: "Second field",
Type: model.PropertyFieldTypeSelect,
Attrs: map[string]any{
"options": []string{"a", "b"},
},
}
for _, field := range []*model.PropertyField{field1, field2} {
_, err := ss.PropertyField().Create(field)
require.NoError(t, err)
require.NotZero(t, field.ID)
}
time.Sleep(10 * time.Millisecond)
field1.Name = "Updated first"
field1.Type = model.PropertyFieldTypeSelect
field1.Attrs = map[string]any{
"locked": false,
"new_field": "new_value",
}
field2.Name = "Updated second"
field2.Attrs = map[string]any{
"options": []string{"x", "y", "z"},
}
_, err := ss.PropertyField().Update("", []*model.PropertyField{field1, field2})
require.NoError(t, err)
// Verify first field
updated1, err := ss.PropertyField().Get("", field1.ID)
require.NoError(t, err)
require.Equal(t, "Updated first", updated1.Name)
require.Equal(t, model.PropertyFieldTypeSelect, updated1.Type)
require.False(t, updated1.Attrs["locked"].(bool))
require.NotContains(t, updated1.Attrs, "special")
require.Equal(t, "new_value", updated1.Attrs["new_field"])
require.Greater(t, updated1.UpdateAt, updated1.CreateAt)
// Verify second field
updated2, err := ss.PropertyField().Get("", field2.ID)
require.NoError(t, err)
require.Equal(t, "Updated second", updated2.Name)
require.Equal(t, model.PropertyFieldTypeSelect, updated2.Type)
require.ElementsMatch(t, []string{"x", "y", "z"}, updated2.Attrs["options"])
require.Greater(t, updated2.UpdateAt, updated2.CreateAt)
})
t.Run("should not update any fields if one update is invalid", func(t *testing.T) {
// Create two valid fields
groupID := model.NewId()
field1 := &model.PropertyField{
GroupID: groupID,
Name: "Field 1",
Type: model.PropertyFieldTypeText,
Attrs: map[string]any{
"key": "value",
},
}
field2 := &model.PropertyField{
GroupID: groupID,
Name: "Field 2",
Type: model.PropertyFieldTypeText,
Attrs: map[string]any{
"key": "value",
},
}
for _, field := range []*model.PropertyField{field1, field2} {
_, err := ss.PropertyField().Create(field)
require.NoError(t, err)
}
originalUpdateAt1 := field1.UpdateAt
originalUpdateAt2 := field2.UpdateAt
// Try to update both fields, but make one invalid
field1.Name = "Valid update"
field2.GroupID = "Invalid ID"
_, err := ss.PropertyField().Update("", []*model.PropertyField{field1, field2})
require.ErrorContains(t, err, "model.property_field.is_valid.app_error")
// Check that fields were not updated
updated1, err := ss.PropertyField().Get("", field1.ID)
require.NoError(t, err)
require.Equal(t, "Field 1", updated1.Name)
require.Equal(t, originalUpdateAt1, updated1.UpdateAt)
updated2, err := ss.PropertyField().Get("", field2.ID)
require.NoError(t, err)
require.Equal(t, groupID, updated2.GroupID)
require.Equal(t, originalUpdateAt2, updated2.UpdateAt)
})
t.Run("should not update any fields if one update points to a nonexisting one", func(t *testing.T) {
// Create a valid field
field1 := &model.PropertyField{
GroupID: model.NewId(),
Name: "First field",
Type: model.PropertyFieldTypeText,
}
_, err := ss.PropertyField().Create(field1)
require.NoError(t, err)
originalUpdateAt := field1.UpdateAt
// Try to update both the valid field and a nonexistent one
field2 := &model.PropertyField{
ID: model.NewId(),
GroupID: model.NewId(),
Name: "Second field",
Type: model.PropertyFieldTypeText,
TargetID: model.NewId(),
TargetType: "test_type",
CreateAt: 1,
Attrs: map[string]any{
"key": "value",
},
}
field1.Name = "Updated First"
_, err = ss.PropertyField().Update("", []*model.PropertyField{field1, field2})
require.Error(t, err)
require.ErrorContains(t, err, "failed to update, some property fields were not found")
// Check that the valid field was not updated
updated1, err := ss.PropertyField().Get("", field1.ID)
require.NoError(t, err)
require.Equal(t, "First field", updated1.Name)
require.Equal(t, originalUpdateAt, updated1.UpdateAt)
})
t.Run("should update fields with matching groupID", func(t *testing.T) {
// Create fields with the same groupID
groupID := model.NewId()
field1 := &model.PropertyField{
GroupID: groupID,
Name: "Group Field 1",
Type: model.PropertyFieldTypeText,
}
field2 := &model.PropertyField{
GroupID: groupID,
Name: "Group Field 2",
Type: model.PropertyFieldTypeText,
}
for _, field := range []*model.PropertyField{field1, field2} {
_, err := ss.PropertyField().Create(field)
require.NoError(t, err)
}
// Update the fields with the matching groupID
field1.Name = "Updated Group Field 1"
field2.Name = "Updated Group Field 2"
updatedFields, err := ss.PropertyField().Update(groupID, []*model.PropertyField{field1, field2})
require.NoError(t, err)
require.Len(t, updatedFields, 2)
// Verify the fields were updated
for _, field := range []*model.PropertyField{field1, field2} {
updated, err := ss.PropertyField().Get("", field.ID)
require.NoError(t, err)
require.Contains(t, updated.Name, "Updated Group Field")
}
})
t.Run("should not update fields with non-matching groupID", func(t *testing.T) {
// Create fields with different groupIDs
groupID1 := model.NewId()
groupID2 := model.NewId()
field1 := &model.PropertyField{
GroupID: groupID1,
Name: "Field in Group 1",
Type: model.PropertyFieldTypeText,
}
field2 := &model.PropertyField{
GroupID: groupID2,
Name: "Field in Group 2",
Type: model.PropertyFieldTypeText,
}
for _, field := range []*model.PropertyField{field1, field2} {
_, err := ss.PropertyField().Create(field)
require.NoError(t, err)
}
originalName1 := field1.Name
originalName2 := field2.Name
// Try to update both fields but filter by groupID1
field1.Name = "Updated Field in Group 1"
field2.Name = "Updated Field in Group 2"
_, err := ss.PropertyField().Update(groupID1, []*model.PropertyField{field1, field2})
require.Error(t, err)
require.ErrorContains(t, err, "failed to update, some property fields were not found")
// Verify neither field was updated due to transaction rollback
updated1, err := ss.PropertyField().Get("", field1.ID)
require.NoError(t, err)
require.Equal(t, originalName1, updated1.Name)
updated2, err := ss.PropertyField().Get("", field2.ID)
require.NoError(t, err)
require.Equal(t, originalName2, updated2.Name)
})
}
func testDeletePropertyField(t *testing.T, _ request.CTX, ss store.Store) {
t.Run("should fail on nonexisting field", func(t *testing.T) {
err := ss.PropertyField().Delete("", model.NewId())
var enf *store.ErrNotFound
require.ErrorAs(t, err, &enf)
})
newField := &model.PropertyField{
GroupID: model.NewId(),
Name: "My property field",
Type: model.PropertyFieldTypeText,
}
t.Run("should be able to delete an existing property field", func(t *testing.T) {
field, err := ss.PropertyField().Create(newField)
require.NoError(t, err)
require.NotEmpty(t, field.ID)
err = ss.PropertyField().Delete("", field.ID)
require.NoError(t, err)
// Verify the field was soft-deleted
deletedField, err := ss.PropertyField().Get("", field.ID)
require.NoError(t, err)
require.NotZero(t, deletedField.DeleteAt)
})
t.Run("should be able to create a new field with the same details as the deleted one", func(t *testing.T) {
newField.ID = ""
field, err := ss.PropertyField().Create(newField)
require.NoError(t, err)
require.NotEmpty(t, field.ID)
})
t.Run("should be able to delete a field with matching groupID", func(t *testing.T) {
groupID := model.NewId()
field := &model.PropertyField{
GroupID: groupID,
Name: "Field with specific group",
Type: model.PropertyFieldTypeText,
}
_, err := ss.PropertyField().Create(field)
require.NoError(t, err)
require.NotZero(t, field.ID)
err = ss.PropertyField().Delete(groupID, field.ID)
require.NoError(t, err)
// Verify the field was soft-deleted
deletedField, err := ss.PropertyField().Get(groupID, field.ID)
require.NoError(t, err)
require.NotZero(t, deletedField.DeleteAt)
})
t.Run("should fail when deleting with non-matching groupID", func(t *testing.T) {
groupID := model.NewId()
field := &model.PropertyField{
GroupID: groupID,
Name: "Another field with specific group",
Type: model.PropertyFieldTypeText,
}
_, err := ss.PropertyField().Create(field)
require.NoError(t, err)
require.NotZero(t, field.ID)
// Try to delete with wrong groupID
err = ss.PropertyField().Delete(model.NewId(), field.ID)
require.Error(t, err)
var enf *store.ErrNotFound
require.ErrorAs(t, err, &enf)
// Verify the field was not deleted
nonDeletedField, err := ss.PropertyField().Get(groupID, field.ID)
require.NoError(t, err)
require.Zero(t, nonDeletedField.DeleteAt)
})
}
func testCountForGroup(t *testing.T, _ request.CTX, ss store.Store) {
t.Run("should return 0 for group with no properties", func(t *testing.T) {
count, err := ss.PropertyField().CountForGroup(model.NewId(), false)
require.NoError(t, err)
require.Equal(t, int64(0), count)
})
t.Run("should return correct count for group with properties", func(t *testing.T) {
groupID := model.NewId()
// Create 5 property fields
for i := range 5 {
field := &model.PropertyField{
GroupID: groupID,
Name: fmt.Sprintf("Field %d", i),
Type: model.PropertyFieldTypeText,
}
_, err := ss.PropertyField().Create(field)
require.NoError(t, err)
}
count, err := ss.PropertyField().CountForGroup(groupID, false)
require.NoError(t, err)
require.Equal(t, int64(5), count)
})
t.Run("should not count deleted properties when includeDeleted is false", func(t *testing.T) {
groupID := model.NewId()
// Create 5 property fields
for i := range 5 {
field := &model.PropertyField{
GroupID: groupID,
Name: fmt.Sprintf("Field %d", i),
Type: model.PropertyFieldTypeText,
}
_, err := ss.PropertyField().Create(field)
require.NoError(t, err)
}
// Create one more and delete it
deletedField := &model.PropertyField{
GroupID: groupID,
Name: "To be deleted",
Type: model.PropertyFieldTypeText,
}
_, err := ss.PropertyField().Create(deletedField)
require.NoError(t, err)
err = ss.PropertyField().Delete("", deletedField.ID)
require.NoError(t, err)
// Count should be 5 since the deleted field shouldn't be counted
count, err := ss.PropertyField().CountForGroup(groupID, false)
require.NoError(t, err)
require.Equal(t, int64(5), count)
})
t.Run("should count deleted properties when includeDeleted is true", func(t *testing.T) {
groupID := model.NewId()
// Create 5 property fields
for i := range 5 {
field := &model.PropertyField{
GroupID: groupID,
Name: fmt.Sprintf("Field %d", i),
Type: model.PropertyFieldTypeText,
}
_, err := ss.PropertyField().Create(field)
require.NoError(t, err)
}
// Create one more and delete it
deletedField := &model.PropertyField{
GroupID: groupID,
Name: "To be deleted",
Type: model.PropertyFieldTypeText,
}
_, err := ss.PropertyField().Create(deletedField)
require.NoError(t, err)
err = ss.PropertyField().Delete("", deletedField.ID)
require.NoError(t, err)
// Count should be 6 since we're including deleted fields
count, err := ss.PropertyField().CountForGroup(groupID, true)
require.NoError(t, err)
require.Equal(t, int64(6), count)
})
}
func testSearchPropertyFields(t *testing.T, _ request.CTX, ss store.Store) {
groupID := model.NewId()
targetID := model.NewId()
// Define test property fields
field1 := &model.PropertyField{
GroupID: groupID,
Name: "Field 1",
Type: model.PropertyFieldTypeText,
TargetID: targetID,
TargetType: "test_type",
}
field2 := &model.PropertyField{
GroupID: groupID,
Name: "Field 2",
Type: model.PropertyFieldTypeSelect,
TargetID: targetID,
TargetType: "other_type",
}
field3 := &model.PropertyField{
GroupID: model.NewId(),
Name: "Field 3",
Type: model.PropertyFieldTypeText,
TargetType: "test_type",
}
targetID2 := model.NewId()
field4 := &model.PropertyField{
GroupID: groupID,
Name: "Field 4",
Type: model.PropertyFieldTypeText,
TargetID: targetID2,
TargetType: "test_type",
}
for _, field := range []*model.PropertyField{field1, field2, field3, field4} {
_, err := ss.PropertyField().Create(field)
require.NoError(t, err)
time.Sleep(10 * time.Millisecond)
}
// Delete one field for deletion tests
require.NoError(t, ss.PropertyField().Delete("", field4.ID))
tests := []struct {
name string
opts model.PropertyFieldSearchOpts
expectedError bool
expectedIDs []string
}{
{
name: "negative per_page",
opts: model.PropertyFieldSearchOpts{
PerPage: -1,
},
expectedError: true,
},
{
name: "filter by group_id",
opts: model.PropertyFieldSearchOpts{
GroupID: groupID,
PerPage: 10,
},
expectedIDs: []string{field1.ID, field2.ID},
},
{
name: "filter by group_id including deleted",
opts: model.PropertyFieldSearchOpts{
GroupID: groupID,
PerPage: 10,
IncludeDeleted: true,
},
expectedIDs: []string{field1.ID, field2.ID, field4.ID},
},
{
name: "filter by target_type",
opts: model.PropertyFieldSearchOpts{
TargetType: "test_type",
PerPage: 10,
},
expectedIDs: []string{field1.ID, field3.ID},
},
{
name: "filter by target_id",
opts: model.PropertyFieldSearchOpts{
TargetIDs: []string{targetID},
PerPage: 10,
},
expectedIDs: []string{field1.ID, field2.ID},
},
{
name: "pagination page 0",
opts: model.PropertyFieldSearchOpts{
GroupID: groupID,
PerPage: 2,
IncludeDeleted: true,
},
expectedIDs: []string{field1.ID, field2.ID},
},
{
name: "pagination page 1",
opts: model.PropertyFieldSearchOpts{
GroupID: groupID,
Cursor: model.PropertyFieldSearchCursor{
CreateAt: field2.CreateAt,
PropertyFieldID: field2.ID,
},
PerPage: 2,
IncludeDeleted: true,
},
expectedIDs: []string{field4.ID},
},
{
name: "filter by multiple target_ids",
opts: model.PropertyFieldSearchOpts{
TargetIDs: []string{targetID, targetID2},
PerPage: 10,
},
expectedIDs: []string{field1.ID, field2.ID},
},
{
name: "filter by multiple target_ids including deleted",
opts: model.PropertyFieldSearchOpts{
TargetIDs: []string{targetID, targetID2},
IncludeDeleted: true,
PerPage: 10,
},
expectedIDs: []string{field1.ID, field2.ID, field4.ID},
},
{
name: "filter by multiple target_ids with group filter",
opts: model.PropertyFieldSearchOpts{
GroupID: groupID,
TargetIDs: []string{targetID, targetID2},
PerPage: 10,
},
expectedIDs: []string{field1.ID, field2.ID},
},
{
name: "filter by SinceUpdateAt timestamp - no results before",
opts: model.PropertyFieldSearchOpts{
SinceUpdateAt: field3.UpdateAt, // After all existing fields
PerPage: 10,
},
expectedIDs: []string{},
},
{
name: "filter by SinceUpdateAt timestamp - get fields after specific time",
opts: model.PropertyFieldSearchOpts{
SinceUpdateAt: field1.UpdateAt, // After field1, should get field2 and field3
PerPage: 10,
},
expectedIDs: []string{field2.ID, field3.ID},
},
{
name: "filter by SinceUpdateAt timestamp with group filter",
opts: model.PropertyFieldSearchOpts{
GroupID: groupID,
SinceUpdateAt: field1.UpdateAt, // After field1, should only get field2 from same group
PerPage: 10,
},
expectedIDs: []string{field2.ID},
},
{
name: "filter by SinceUpdateAt timestamp including deleted",
opts: model.PropertyFieldSearchOpts{
SinceUpdateAt: field3.UpdateAt, // After field3, should get field4 (deleted)
IncludeDeleted: true,
PerPage: 10,
},
expectedIDs: []string{field4.ID},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
results, err := ss.PropertyField().SearchPropertyFields(tc.opts)
if tc.expectedError {
require.Error(t, err)
return
}
require.NoError(t, err)
ids := make([]string, len(results))
for i, field := range results {
ids[i] = field.ID
}
require.ElementsMatch(t, tc.expectedIDs, ids)
})
}
}
func testSearchPropertyFieldsSince(t *testing.T, _ request.CTX, ss store.Store) {
// Create fields with controlled timestamps for precise testing
groupID := model.NewId()
// Create field 1 (will remain unchanged)
field1, err := ss.PropertyField().Create(&model.PropertyField{
GroupID: groupID,
Name: "Field 1",
Type: model.PropertyFieldTypeText,
TargetID: model.NewId(),
TargetType: "test_type",
})
require.NoError(t, err)
time.Sleep(10 * time.Millisecond) // Ensure different timestamps
// Create field 2 (will be updated later)
field2, err := ss.PropertyField().Create(&model.PropertyField{
GroupID: groupID,
Name: "Field 2",
Type: model.PropertyFieldTypeText,
TargetID: model.NewId(),
TargetType: "test_type",
})
require.NoError(t, err)
time.Sleep(10 * time.Millisecond)
// Create field 3 (will remain unchanged)
field3, err := ss.PropertyField().Create(&model.PropertyField{
GroupID: groupID,
Name: "Field 3",
Type: model.PropertyFieldTypeText,
TargetID: model.NewId(),
TargetType: "test_type",
})
require.NoError(t, err)
// Update field2 to change its UpdateAt timestamp
time.Sleep(10 * time.Millisecond)
field2.Name = "Field 2 Updated"
updatedFields, err := ss.PropertyField().Update("", []*model.PropertyField{field2})
require.NoError(t, err)
require.Len(t, updatedFields, 1)
updatedField2 := updatedFields[0]
t.Run("SinceUpdateAt filters correctly by UpdateAt", func(t *testing.T) {
// Get fields updated after field1 (should get field2 and field3)
results, err := ss.PropertyField().SearchPropertyFields(model.PropertyFieldSearchOpts{
GroupID: groupID,
SinceUpdateAt: field1.UpdateAt,
PerPage: 10,
})
require.NoError(t, err)
require.Len(t, results, 2)
resultIDs := make([]string, len(results))
for i, result := range results {
resultIDs[i] = result.ID
}
require.ElementsMatch(t, []string{field2.ID, field3.ID}, resultIDs)
})
t.Run("SinceUpdateAt with boundary condition", func(t *testing.T) {
// Get fields updated after just before field3's timestamp
// Should get both field3 and field2 (which was updated last and now has the most recent UpdateAt), so expect 2 results
results, err := ss.PropertyField().SearchPropertyFields(model.PropertyFieldSearchOpts{
GroupID: groupID,
SinceUpdateAt: field3.UpdateAt - 1, // Slightly before field3's timestamp
PerPage: 10,
})
require.NoError(t, err)
require.Len(t, results, 2)
resultIDs := make([]string, len(results))
for i, result := range results {
resultIDs[i] = result.ID
}
// Should get both field2 (updated with new timestamp) and field3
require.ElementsMatch(t, []string{field2.ID, field3.ID}, resultIDs)
})
t.Run("SinceUpdateAt after all updates", func(t *testing.T) {
// Get fields updated after the most recent update
results, err := ss.PropertyField().SearchPropertyFields(model.PropertyFieldSearchOpts{
GroupID: groupID,
SinceUpdateAt: updatedField2.UpdateAt, // After the update
PerPage: 10,
})
require.NoError(t, err)
require.Len(t, results, 0) // Should be empty
})
t.Run("SinceUpdateAt with very recent timestamp", func(t *testing.T) {
// Get fields updated since current time
results, err := ss.PropertyField().SearchPropertyFields(model.PropertyFieldSearchOpts{
GroupID: groupID,
SinceUpdateAt: model.GetMillis(),
PerPage: 10,
})
require.NoError(t, err)
require.Len(t, results, 0)
})
}