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>
1651 lines
52 KiB
Go
1651 lines
52 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package storetest
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"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 TestPropertyValueStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
|
|
t.Run("CreatePropertyValue", func(t *testing.T) { testCreatePropertyValue(t, rctx, ss) })
|
|
t.Run("CreateManyPropertyValues", func(t *testing.T) { testCreateManyPropertyValues(t, rctx, ss) })
|
|
t.Run("CreatePropertyValueWithArray", func(t *testing.T) { testCreatePropertyValueWithArray(t, rctx, ss) })
|
|
t.Run("GetPropertyValue", func(t *testing.T) { testGetPropertyValue(t, rctx, ss) })
|
|
t.Run("GetManyPropertyValues", func(t *testing.T) { testGetManyPropertyValues(t, rctx, ss) })
|
|
t.Run("UpdatePropertyValue", func(t *testing.T) { testUpdatePropertyValue(t, rctx, ss) })
|
|
t.Run("UpsertPropertyValue", func(t *testing.T) { testUpsertPropertyValue(t, rctx, ss) })
|
|
t.Run("DeletePropertyValue", func(t *testing.T) { testDeletePropertyValue(t, rctx, ss) })
|
|
t.Run("SearchPropertyValues", func(t *testing.T) { testSearchPropertyValues(t, rctx, ss) })
|
|
t.Run("SearchPropertyValuesSince", func(t *testing.T) { testSearchPropertyValuesSince(t, rctx, ss) })
|
|
t.Run("DeleteForField", func(t *testing.T) { testDeleteForField(t, rctx, ss) })
|
|
t.Run("DeleteForTarget", func(t *testing.T) { testDeleteForTarget(t, rctx, ss) })
|
|
}
|
|
|
|
func testCreatePropertyValue(t *testing.T, _ request.CTX, ss store.Store) {
|
|
t.Run("should fail if the property value already has an ID set", func(t *testing.T) {
|
|
newValue := &model.PropertyValue{ID: "sampleid"}
|
|
value, err := ss.PropertyValue().Create(newValue)
|
|
require.Zero(t, value)
|
|
var eii *store.ErrInvalidInput
|
|
require.ErrorAs(t, err, &eii)
|
|
})
|
|
|
|
t.Run("should fail if the property value is not valid", func(t *testing.T) {
|
|
newValue := &model.PropertyValue{TargetID: ""}
|
|
value, err := ss.PropertyValue().Create(newValue)
|
|
require.Zero(t, value)
|
|
require.ErrorContains(t, err, "model.property_value.is_valid.app_error")
|
|
|
|
newValue = &model.PropertyValue{TargetID: model.NewId(), TargetType: ""}
|
|
value, err = ss.PropertyValue().Create(newValue)
|
|
require.Zero(t, value)
|
|
require.ErrorContains(t, err, "model.property_value.is_valid.app_error")
|
|
})
|
|
|
|
newValue := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"test value"`),
|
|
}
|
|
|
|
t.Run("should be able to create a property value", func(t *testing.T) {
|
|
value, err := ss.PropertyValue().Create(newValue)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, value.ID)
|
|
require.NotZero(t, value.CreateAt)
|
|
require.NotZero(t, value.UpdateAt)
|
|
require.Zero(t, value.DeleteAt)
|
|
})
|
|
|
|
t.Run("should enforce the value's uniqueness", func(t *testing.T) {
|
|
newValue.ID = ""
|
|
value, err := ss.PropertyValue().Create(newValue)
|
|
require.Error(t, err)
|
|
require.Zero(t, value)
|
|
})
|
|
}
|
|
|
|
func testCreateManyPropertyValues(t *testing.T, _ request.CTX, ss store.Store) {
|
|
t.Run("should return nil when given empty slice", func(t *testing.T) {
|
|
values, err := ss.PropertyValue().CreateMany([]*model.PropertyValue{})
|
|
require.NoError(t, err)
|
|
require.Nil(t, values)
|
|
})
|
|
|
|
t.Run("should fail if any property value is not valid", func(t *testing.T) {
|
|
validValue := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"valid value"`),
|
|
}
|
|
|
|
invalidValue := &model.PropertyValue{
|
|
TargetID: "", // Invalid: empty target ID
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"invalid value"`),
|
|
}
|
|
|
|
values, err := ss.PropertyValue().CreateMany([]*model.PropertyValue{validValue, invalidValue})
|
|
require.Zero(t, values)
|
|
require.ErrorContains(t, err, "model.property_value.is_valid.app_error")
|
|
|
|
// Verify no values were created
|
|
results, err := ss.PropertyValue().SearchPropertyValues(model.PropertyValueSearchOpts{
|
|
TargetIDs: []string{validValue.TargetID},
|
|
PerPage: 10,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Empty(t, results)
|
|
})
|
|
|
|
t.Run("should be able to create multiple property values", func(t *testing.T) {
|
|
value1 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value 1"`),
|
|
}
|
|
|
|
value2 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value 2"`),
|
|
}
|
|
|
|
value3 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value 3"`),
|
|
}
|
|
|
|
values, err := ss.PropertyValue().CreateMany([]*model.PropertyValue{value1, value2, value3})
|
|
require.NoError(t, err)
|
|
require.Len(t, values, 3)
|
|
|
|
// Verify all values have IDs and timestamps set
|
|
for i, value := range values {
|
|
require.NotZero(t, value.ID)
|
|
require.NotZero(t, value.CreateAt)
|
|
require.NotZero(t, value.UpdateAt)
|
|
require.Zero(t, value.DeleteAt)
|
|
require.Equal(t, json.RawMessage(fmt.Sprintf(`"value %d"`, i+1)), value.Value)
|
|
}
|
|
|
|
// Verify values can be retrieved from database
|
|
retrievedValues, err := ss.PropertyValue().GetMany("", []string{value1.ID, value2.ID, value3.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, retrievedValues, 3)
|
|
})
|
|
|
|
t.Run("should handle array values", func(t *testing.T) {
|
|
value1 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`["option1", "option2"]`),
|
|
}
|
|
|
|
value2 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`["option3", "option4", "option5"]`),
|
|
}
|
|
|
|
values, err := ss.PropertyValue().CreateMany([]*model.PropertyValue{value1, value2})
|
|
require.NoError(t, err)
|
|
require.Len(t, values, 2)
|
|
|
|
// Verify array values are preserved
|
|
var arrayValues1 []string
|
|
require.NoError(t, json.Unmarshal(values[0].Value, &arrayValues1))
|
|
require.Equal(t, []string{"option1", "option2"}, arrayValues1)
|
|
|
|
var arrayValues2 []string
|
|
require.NoError(t, json.Unmarshal(values[1].Value, &arrayValues2))
|
|
require.Equal(t, []string{"option3", "option4", "option5"}, arrayValues2)
|
|
})
|
|
|
|
t.Run("should enforce uniqueness constraints", func(t *testing.T) {
|
|
groupID := model.NewId()
|
|
targetID := model.NewId()
|
|
fieldID := model.NewId()
|
|
|
|
// Create first value
|
|
value1 := &model.PropertyValue{
|
|
TargetID: targetID,
|
|
TargetType: "test_type",
|
|
GroupID: groupID,
|
|
FieldID: fieldID,
|
|
Value: json.RawMessage(`"first value"`),
|
|
}
|
|
|
|
_, err := ss.PropertyValue().Create(value1)
|
|
require.NoError(t, err)
|
|
|
|
// Try to create duplicate value
|
|
value2 := &model.PropertyValue{
|
|
TargetID: targetID,
|
|
TargetType: "test_type",
|
|
GroupID: groupID,
|
|
FieldID: fieldID,
|
|
Value: json.RawMessage(`"duplicate value"`),
|
|
}
|
|
|
|
value3 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"unique value"`),
|
|
}
|
|
|
|
values, err := ss.PropertyValue().CreateMany([]*model.PropertyValue{value2, value3})
|
|
require.Error(t, err)
|
|
require.Zero(t, values)
|
|
|
|
// Verify the unique value was not created due to transaction rollback
|
|
results, err := ss.PropertyValue().SearchPropertyValues(model.PropertyValueSearchOpts{
|
|
TargetIDs: []string{value3.TargetID},
|
|
PerPage: 10,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Empty(t, results)
|
|
})
|
|
|
|
t.Run("should create values with same field but different targets", func(t *testing.T) {
|
|
groupID := model.NewId()
|
|
fieldID := model.NewId()
|
|
|
|
value1 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID,
|
|
FieldID: fieldID,
|
|
Value: json.RawMessage(`"target 1 value"`),
|
|
}
|
|
|
|
value2 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID,
|
|
FieldID: fieldID,
|
|
Value: json.RawMessage(`"target 2 value"`),
|
|
}
|
|
|
|
values, err := ss.PropertyValue().CreateMany([]*model.PropertyValue{value1, value2})
|
|
require.NoError(t, err)
|
|
require.Len(t, values, 2)
|
|
|
|
// Verify both values were created successfully
|
|
for _, value := range values {
|
|
require.NotZero(t, value.ID)
|
|
require.Equal(t, groupID, value.GroupID)
|
|
require.Equal(t, fieldID, value.FieldID)
|
|
}
|
|
})
|
|
|
|
t.Run("should create values across different groups", func(t *testing.T) {
|
|
value1 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"group 1 value"`),
|
|
}
|
|
|
|
value2 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"group 2 value"`),
|
|
}
|
|
|
|
values, err := ss.PropertyValue().CreateMany([]*model.PropertyValue{value1, value2})
|
|
require.NoError(t, err)
|
|
require.Len(t, values, 2)
|
|
|
|
// Verify values are in different groups
|
|
require.NotEqual(t, values[0].GroupID, values[1].GroupID)
|
|
require.Contains(t, string(values[0].Value), "group 1")
|
|
require.Contains(t, string(values[1].Value), "group 2")
|
|
})
|
|
}
|
|
|
|
func testGetPropertyValue(t *testing.T, _ request.CTX, ss store.Store) {
|
|
t.Run("should fail on nonexisting value", func(t *testing.T) {
|
|
value, err := ss.PropertyValue().Get("", model.NewId())
|
|
require.Zero(t, value)
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
})
|
|
|
|
groupID := model.NewId()
|
|
newValue := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID,
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"test value"`),
|
|
}
|
|
_, err := ss.PropertyValue().Create(newValue)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, newValue.ID)
|
|
|
|
t.Run("should be able to retrieve an existing property value", func(t *testing.T) {
|
|
value, err := ss.PropertyValue().Get(groupID, newValue.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, newValue.ID, value.ID)
|
|
require.Equal(t, newValue.Value, value.Value)
|
|
|
|
// should work without specifying the group ID as well
|
|
value, err = ss.PropertyValue().Get("", newValue.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, newValue.ID, value.ID)
|
|
require.Equal(t, newValue.Value, value.Value)
|
|
})
|
|
|
|
t.Run("should not be able to retrieve an existing value when specifying a different group ID", func(t *testing.T) {
|
|
value, err := ss.PropertyValue().Get(model.NewId(), newValue.ID)
|
|
require.Zero(t, value)
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
})
|
|
|
|
t.Run("should be able to retrieve an existing property value with matching groupID", func(t *testing.T) {
|
|
groupID := model.NewId()
|
|
newValue := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID,
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"test value with group"`),
|
|
}
|
|
_, err := ss.PropertyValue().Create(newValue)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, newValue.ID)
|
|
|
|
value, err := ss.PropertyValue().Get(groupID, newValue.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, newValue.ID, value.ID)
|
|
require.Equal(t, newValue.Value, value.Value)
|
|
})
|
|
|
|
t.Run("should fail when retrieving a value with non-matching groupID", func(t *testing.T) {
|
|
newValue := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"test value with specific group"`),
|
|
}
|
|
_, err := ss.PropertyValue().Create(newValue)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, newValue.ID)
|
|
|
|
// Try to get the value with a different group ID
|
|
value, err := ss.PropertyValue().Get(model.NewId(), newValue.ID)
|
|
require.Zero(t, value)
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
})
|
|
}
|
|
|
|
func testGetManyPropertyValues(t *testing.T, _ request.CTX, ss store.Store) {
|
|
t.Run("should fail on nonexisting values", func(t *testing.T) {
|
|
values, err := ss.PropertyValue().GetMany("", []string{model.NewId(), model.NewId()})
|
|
require.Empty(t, values)
|
|
require.ErrorContains(t, err, "missmatch results")
|
|
})
|
|
|
|
groupID := model.NewId()
|
|
newValues := []*model.PropertyValue{}
|
|
for i := range 3 {
|
|
newValue := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID,
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(fmt.Sprintf(`"test value %d"`, i)),
|
|
}
|
|
_, err := ss.PropertyValue().Create(newValue)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, newValue.ID)
|
|
|
|
newValues = append(newValues, newValue)
|
|
}
|
|
|
|
newValueOutsideGroup := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value outside the groupID"`),
|
|
}
|
|
_, err := ss.PropertyValue().Create(newValueOutsideGroup)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, newValueOutsideGroup.ID)
|
|
|
|
t.Run("should fail if at least one of the ids is nonexistent", func(t *testing.T) {
|
|
values, err := ss.PropertyValue().GetMany(groupID, []string{newValues[0].ID, newValues[1].ID, model.NewId()})
|
|
require.Empty(t, values)
|
|
require.ErrorContains(t, err, "missmatch results")
|
|
})
|
|
|
|
t.Run("should be able to retrieve existing property values", func(t *testing.T) {
|
|
values, err := ss.PropertyValue().GetMany(groupID, []string{newValues[0].ID, newValues[1].ID, newValues[2].ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, values, 3)
|
|
require.ElementsMatch(t, newValues, values)
|
|
})
|
|
|
|
t.Run("should fail if asked for valid IDs but outside the group", func(t *testing.T) {
|
|
values, err := ss.PropertyValue().GetMany(groupID, []string{newValues[0].ID, newValueOutsideGroup.ID})
|
|
require.Empty(t, values)
|
|
require.ErrorContains(t, err, "missmatch results")
|
|
})
|
|
|
|
t.Run("should be able to retrieve existing property values from multiple groups", func(t *testing.T) {
|
|
fields, err := ss.PropertyValue().GetMany("", []string{newValues[0].ID, newValueOutsideGroup.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, fields, 2)
|
|
})
|
|
}
|
|
|
|
func testUpdatePropertyValue(t *testing.T, _ request.CTX, ss store.Store) {
|
|
t.Run("should fail on nonexisting value", func(t *testing.T) {
|
|
value := &model.PropertyValue{
|
|
ID: model.NewId(),
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"test value"`),
|
|
CreateAt: model.GetMillis(),
|
|
}
|
|
updatedValue, err := ss.PropertyValue().Update("", []*model.PropertyValue{value})
|
|
require.Zero(t, updatedValue)
|
|
require.ErrorContains(t, err, "failed to update, some property values were not found, got 0 of 1")
|
|
})
|
|
|
|
t.Run("should fail if the property value is not valid", func(t *testing.T) {
|
|
value := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"test value"`),
|
|
}
|
|
_, err := ss.PropertyValue().Create(value)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, value.ID)
|
|
|
|
value.TargetID = ""
|
|
updatedValue, err := ss.PropertyValue().Update("", []*model.PropertyValue{value})
|
|
require.Zero(t, updatedValue)
|
|
require.ErrorContains(t, err, "model.property_value.is_valid.app_error")
|
|
|
|
value.TargetID = model.NewId()
|
|
value.GroupID = ""
|
|
updatedValue, err = ss.PropertyValue().Update("", []*model.PropertyValue{value})
|
|
require.Zero(t, updatedValue)
|
|
require.ErrorContains(t, err, "model.property_value.is_valid.app_error")
|
|
})
|
|
|
|
t.Run("should be able to update multiple property values", func(t *testing.T) {
|
|
value1 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value 1"`),
|
|
}
|
|
|
|
value2 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value 2"`),
|
|
}
|
|
|
|
for _, value := range []*model.PropertyValue{value1, value2} {
|
|
_, err := ss.PropertyValue().Create(value)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, value.ID)
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
value1.Value = json.RawMessage(`"updated value 1"`)
|
|
value2.Value = json.RawMessage(`"updated value 2"`)
|
|
|
|
_, err := ss.PropertyValue().Update("", []*model.PropertyValue{value1, value2})
|
|
require.NoError(t, err)
|
|
|
|
// Verify first value
|
|
updated1, err := ss.PropertyValue().Get("", value1.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, json.RawMessage(`"updated value 1"`), updated1.Value)
|
|
require.Greater(t, updated1.UpdateAt, updated1.CreateAt)
|
|
|
|
// Verify second value
|
|
updated2, err := ss.PropertyValue().Get("", value2.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, json.RawMessage(`"updated value 2"`), updated2.Value)
|
|
require.Greater(t, updated2.UpdateAt, updated2.CreateAt)
|
|
})
|
|
|
|
t.Run("should not update any values if one update is invalid", func(t *testing.T) {
|
|
// Create two valid values
|
|
groupID := model.NewId()
|
|
value1 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID,
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"Value 1"`),
|
|
}
|
|
|
|
value2 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID,
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"Value 2"`),
|
|
}
|
|
|
|
for _, value := range []*model.PropertyValue{value1, value2} {
|
|
_, err := ss.PropertyValue().Create(value)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
originalUpdateAt1 := value1.UpdateAt
|
|
originalUpdateAt2 := value2.UpdateAt
|
|
|
|
// Try to update both value, but make one invalid
|
|
value1.Value = json.RawMessage(`"Valid update"`)
|
|
value2.GroupID = "Invalid ID"
|
|
|
|
_, err := ss.PropertyValue().Update("", []*model.PropertyValue{value1, value2})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "model.property_value.is_valid.app_error")
|
|
|
|
// Check that values were not updated
|
|
updated1, err := ss.PropertyValue().Get("", value1.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, json.RawMessage(`"Value 1"`), updated1.Value)
|
|
require.Equal(t, originalUpdateAt1, updated1.UpdateAt)
|
|
|
|
updated2, err := ss.PropertyValue().Get("", value2.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, groupID, updated2.GroupID)
|
|
require.Equal(t, originalUpdateAt2, updated2.UpdateAt)
|
|
})
|
|
|
|
t.Run("should not update any values if one update points to a nonexisting one", func(t *testing.T) {
|
|
// Create a valid value
|
|
value1 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"Value 1"`),
|
|
}
|
|
|
|
_, err := ss.PropertyValue().Create(value1)
|
|
require.NoError(t, err)
|
|
|
|
originalUpdateAt := value1.UpdateAt
|
|
|
|
// Try to update both the valid value and a nonexistent one
|
|
value2 := &model.PropertyValue{
|
|
ID: model.NewId(),
|
|
TargetID: model.NewId(),
|
|
CreateAt: 1,
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"Value 2"`),
|
|
}
|
|
|
|
value1.Value = json.RawMessage(`"Updated Value 1"`)
|
|
|
|
_, err = ss.PropertyValue().Update("", []*model.PropertyValue{value1, value2})
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "failed to update, some property values were not found")
|
|
|
|
// Check that the valid value was not updated
|
|
updated1, err := ss.PropertyValue().Get("", value1.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, json.RawMessage(`"Value 1"`), updated1.Value)
|
|
require.Equal(t, originalUpdateAt, updated1.UpdateAt)
|
|
})
|
|
|
|
t.Run("should update values with matching groupID", func(t *testing.T) {
|
|
// Create values with the same groupID
|
|
groupID := model.NewId()
|
|
value1 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID,
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"Group Value 1"`),
|
|
}
|
|
value2 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID,
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"Group Value 2"`),
|
|
}
|
|
|
|
for _, value := range []*model.PropertyValue{value1, value2} {
|
|
_, err := ss.PropertyValue().Create(value)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Update the values with the matching groupID
|
|
value1.Value = json.RawMessage(`"Updated Group Value 1"`)
|
|
value2.Value = json.RawMessage(`"Updated Group Value 2"`)
|
|
|
|
updatedValues, err := ss.PropertyValue().Update(groupID, []*model.PropertyValue{value1, value2})
|
|
require.NoError(t, err)
|
|
require.Len(t, updatedValues, 2)
|
|
|
|
// Verify the values were updated
|
|
for _, value := range []*model.PropertyValue{value1, value2} {
|
|
updated, err := ss.PropertyValue().Get("", value.ID)
|
|
require.NoError(t, err)
|
|
require.Contains(t, string(updated.Value), "Updated Group Value")
|
|
}
|
|
})
|
|
|
|
t.Run("should not update values with non-matching groupID", func(t *testing.T) {
|
|
// Create values with different groupIDs
|
|
groupID1 := model.NewId()
|
|
groupID2 := model.NewId()
|
|
|
|
value1 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID1,
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"Value in Group 1"`),
|
|
}
|
|
value2 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID2,
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"Value in Group 2"`),
|
|
}
|
|
|
|
for _, value := range []*model.PropertyValue{value1, value2} {
|
|
_, err := ss.PropertyValue().Create(value)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
originalValue1 := string(value1.Value)
|
|
originalValue2 := string(value2.Value)
|
|
|
|
// Try to update both values but filter by groupID1
|
|
value1.Value = json.RawMessage(`"Updated Value in Group 1"`)
|
|
value2.Value = json.RawMessage(`"Updated Value in Group 2"`)
|
|
|
|
_, err := ss.PropertyValue().Update(groupID1, []*model.PropertyValue{value1, value2})
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "failed to update, some property values were not found")
|
|
|
|
// Verify neither value was updated due to transaction rollback
|
|
updated1, err := ss.PropertyValue().Get("", value1.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, originalValue1, string(updated1.Value))
|
|
|
|
updated2, err := ss.PropertyValue().Get("", value2.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, originalValue2, string(updated2.Value))
|
|
})
|
|
}
|
|
|
|
func testUpsertPropertyValue(t *testing.T, _ request.CTX, ss store.Store) {
|
|
t.Run("should fail if the property value is not valid", func(t *testing.T) {
|
|
value := &model.PropertyValue{
|
|
TargetID: "",
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"test value"`),
|
|
}
|
|
updatedValue, err := ss.PropertyValue().Upsert([]*model.PropertyValue{value})
|
|
require.Zero(t, updatedValue)
|
|
require.ErrorContains(t, err, "model.property_value.is_valid.app_error")
|
|
|
|
value.TargetID = model.NewId()
|
|
value.GroupID = ""
|
|
updatedValue, err = ss.PropertyValue().Upsert([]*model.PropertyValue{value})
|
|
require.Zero(t, updatedValue)
|
|
require.ErrorContains(t, err, "model.property_value.is_valid.app_error")
|
|
})
|
|
|
|
t.Run("should be able to insert new property values", func(t *testing.T) {
|
|
value1 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value 1"`),
|
|
}
|
|
|
|
value2 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value 2"`),
|
|
}
|
|
|
|
values, err := ss.PropertyValue().Upsert([]*model.PropertyValue{value1, value2})
|
|
require.NoError(t, err)
|
|
require.Len(t, values, 2)
|
|
require.NotEmpty(t, values[0].ID)
|
|
require.NotEmpty(t, values[1].ID)
|
|
require.NotZero(t, values[0].CreateAt)
|
|
require.NotZero(t, values[1].CreateAt)
|
|
|
|
valuesFromStore, err := ss.PropertyValue().GetMany("", []string{values[0].ID, values[1].ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, valuesFromStore, 2)
|
|
})
|
|
|
|
t.Run("should be able to update existing property values", func(t *testing.T) {
|
|
// Create initial value
|
|
value := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"initial value"`),
|
|
}
|
|
_, err := ss.PropertyValue().Create(value)
|
|
require.NoError(t, err)
|
|
valueID := value.ID
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
// Update via upsert
|
|
value.ID = ""
|
|
value.Value = json.RawMessage(`"updated value"`)
|
|
values, err := ss.PropertyValue().Upsert([]*model.PropertyValue{value})
|
|
require.NoError(t, err)
|
|
require.Len(t, values, 1)
|
|
require.Equal(t, valueID, values[0].ID)
|
|
require.Equal(t, json.RawMessage(`"updated value"`), values[0].Value)
|
|
require.Greater(t, values[0].UpdateAt, values[0].CreateAt)
|
|
|
|
// Verify in database
|
|
updated, err := ss.PropertyValue().Get("", valueID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, json.RawMessage(`"updated value"`), updated.Value)
|
|
require.Greater(t, updated.UpdateAt, updated.CreateAt)
|
|
})
|
|
|
|
t.Run("should handle mixed insert and update operations", func(t *testing.T) {
|
|
// Create first value
|
|
existingValue := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"existing value"`),
|
|
}
|
|
_, err := ss.PropertyValue().Create(existingValue)
|
|
require.NoError(t, err)
|
|
|
|
// Prepare new value
|
|
newValue := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"new value"`),
|
|
}
|
|
|
|
// Update existing and insert new via upsert
|
|
existingValue.Value = json.RawMessage(`"updated existing"`)
|
|
values, err := ss.PropertyValue().Upsert([]*model.PropertyValue{existingValue, newValue})
|
|
require.NoError(t, err)
|
|
require.Len(t, values, 2)
|
|
|
|
// Verify both values
|
|
newValueUpserted, err := ss.PropertyValue().Get("", newValue.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, json.RawMessage(`"new value"`), newValueUpserted.Value)
|
|
existingValueUpserted, err := ss.PropertyValue().Get("", existingValue.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, json.RawMessage(`"updated existing"`), existingValueUpserted.Value)
|
|
})
|
|
|
|
t.Run("should not perform any operation if one of the fields is invalid", func(t *testing.T) {
|
|
// Create initial valid value
|
|
existingValue := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"existing value"`),
|
|
}
|
|
_, err := ss.PropertyValue().Create(existingValue)
|
|
require.NoError(t, err)
|
|
|
|
originalValue := *existingValue
|
|
|
|
// Prepare an invalid value
|
|
invalidValue := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: "", // Invalid: empty group ID
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"new value"`),
|
|
}
|
|
|
|
// Try to update existing and insert invalid via upsert
|
|
existingValue.Value = json.RawMessage(`"should not update"`)
|
|
_, err = ss.PropertyValue().Upsert([]*model.PropertyValue{existingValue, invalidValue})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "model.property_value.is_valid.app_error")
|
|
|
|
// Verify the existing value was not changed
|
|
retrieved, err := ss.PropertyValue().Get("", existingValue.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, originalValue.Value, retrieved.Value)
|
|
require.Equal(t, originalValue.UpdateAt, retrieved.UpdateAt)
|
|
|
|
// Verify the invalid value was not inserted
|
|
results, err := ss.PropertyValue().SearchPropertyValues(model.PropertyValueSearchOpts{
|
|
TargetIDs: []string{invalidValue.TargetID},
|
|
PerPage: 10,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Empty(t, results)
|
|
})
|
|
}
|
|
|
|
func testDeletePropertyValue(t *testing.T, _ request.CTX, ss store.Store) {
|
|
t.Run("should fail on nonexisting value", func(t *testing.T) {
|
|
err := ss.PropertyValue().Delete("", model.NewId())
|
|
var enf *store.ErrNotFound
|
|
require.ErrorAs(t, err, &enf)
|
|
})
|
|
|
|
t.Run("should be able to delete an existing property value", func(t *testing.T) {
|
|
newValue := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"test value"`),
|
|
}
|
|
value, err := ss.PropertyValue().Create(newValue)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, value.ID)
|
|
|
|
err = ss.PropertyValue().Delete("", value.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the value was soft-deleted
|
|
deletedValue, err := ss.PropertyValue().Get("", value.ID)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, deletedValue.DeleteAt)
|
|
})
|
|
|
|
t.Run("should be able to create a new value with the same details as the deleted one", func(t *testing.T) {
|
|
sameDetailsValue := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"test value"`),
|
|
}
|
|
value, err := ss.PropertyValue().Create(sameDetailsValue)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, value.ID)
|
|
require.Equal(t, sameDetailsValue.Value, value.Value)
|
|
})
|
|
|
|
t.Run("should be able to delete a value with matching groupID", func(t *testing.T) {
|
|
groupID := model.NewId()
|
|
value := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID,
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value with specific group"`),
|
|
}
|
|
_, err := ss.PropertyValue().Create(value)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, value.ID)
|
|
|
|
// Delete with matching groupID
|
|
err = ss.PropertyValue().Delete(groupID, value.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the value was soft-deleted
|
|
deletedValue, err := ss.PropertyValue().Get(groupID, value.ID)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, deletedValue.DeleteAt)
|
|
})
|
|
|
|
t.Run("should fail when deleting with non-matching groupID", func(t *testing.T) {
|
|
groupID := model.NewId()
|
|
value := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID,
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"another value with specific group"`),
|
|
}
|
|
_, err := ss.PropertyValue().Create(value)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, value.ID)
|
|
|
|
// Try to delete with wrong groupID
|
|
err = ss.PropertyValue().Delete(model.NewId(), value.ID)
|
|
require.Error(t, err)
|
|
var enf *store.ErrNotFound
|
|
require.ErrorAs(t, err, &enf)
|
|
|
|
// Verify the value was not deleted
|
|
nonDeletedValue, err := ss.PropertyValue().Get(groupID, value.ID)
|
|
require.NoError(t, err)
|
|
require.Zero(t, nonDeletedValue.DeleteAt)
|
|
})
|
|
}
|
|
|
|
func testSearchPropertyValues(t *testing.T, _ request.CTX, ss store.Store) {
|
|
groupID := model.NewId()
|
|
targetID := model.NewId()
|
|
fieldID := model.NewId()
|
|
|
|
// Define test property values
|
|
value1 := &model.PropertyValue{
|
|
GroupID: groupID,
|
|
TargetID: targetID,
|
|
TargetType: "test_type",
|
|
FieldID: fieldID,
|
|
Value: json.RawMessage(`"value 1"`),
|
|
}
|
|
|
|
value2 := &model.PropertyValue{
|
|
GroupID: groupID,
|
|
TargetID: targetID,
|
|
TargetType: "other_type",
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value 2"`),
|
|
}
|
|
|
|
value3 := &model.PropertyValue{
|
|
GroupID: model.NewId(),
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value 3"`),
|
|
}
|
|
|
|
value4 := &model.PropertyValue{
|
|
GroupID: groupID,
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
FieldID: fieldID,
|
|
Value: json.RawMessage(`"value 4"`),
|
|
}
|
|
|
|
for _, value := range []*model.PropertyValue{value1, value2, value3, value4} {
|
|
_, err := ss.PropertyValue().Create(value)
|
|
require.NoError(t, err)
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
|
|
// Delete one value for deletion tests
|
|
require.NoError(t, ss.PropertyValue().Delete("", value4.ID))
|
|
|
|
tests := []struct {
|
|
name string
|
|
opts model.PropertyValueSearchOpts
|
|
expectedError bool
|
|
expectedIDs []string
|
|
}{
|
|
{
|
|
name: "negative per_page",
|
|
opts: model.PropertyValueSearchOpts{
|
|
PerPage: -1,
|
|
},
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "filter by group_id",
|
|
opts: model.PropertyValueSearchOpts{
|
|
GroupID: groupID,
|
|
PerPage: 10,
|
|
},
|
|
expectedIDs: []string{value1.ID, value2.ID},
|
|
},
|
|
{
|
|
name: "filter by group_id and target_type",
|
|
opts: model.PropertyValueSearchOpts{
|
|
GroupID: groupID,
|
|
TargetType: "test_type",
|
|
PerPage: 10,
|
|
},
|
|
expectedIDs: []string{value1.ID},
|
|
},
|
|
{
|
|
name: "filter by group_id and target_type including deleted",
|
|
opts: model.PropertyValueSearchOpts{
|
|
GroupID: groupID,
|
|
TargetType: "test_type",
|
|
IncludeDeleted: true,
|
|
PerPage: 10,
|
|
},
|
|
expectedIDs: []string{value1.ID, value4.ID},
|
|
},
|
|
{
|
|
name: "filter by target_id",
|
|
opts: model.PropertyValueSearchOpts{
|
|
TargetIDs: []string{targetID},
|
|
PerPage: 10,
|
|
},
|
|
expectedIDs: []string{value1.ID, value2.ID},
|
|
},
|
|
{
|
|
name: "filter by group_id and target_id",
|
|
opts: model.PropertyValueSearchOpts{
|
|
GroupID: groupID,
|
|
TargetIDs: []string{targetID},
|
|
PerPage: 10,
|
|
},
|
|
expectedIDs: []string{value1.ID, value2.ID},
|
|
},
|
|
{
|
|
name: "filter by field_id",
|
|
opts: model.PropertyValueSearchOpts{
|
|
FieldID: fieldID,
|
|
PerPage: 10,
|
|
},
|
|
expectedIDs: []string{value1.ID},
|
|
},
|
|
{
|
|
name: "filter by field_id including deleted",
|
|
opts: model.PropertyValueSearchOpts{
|
|
FieldID: fieldID,
|
|
IncludeDeleted: true,
|
|
PerPage: 10,
|
|
},
|
|
expectedIDs: []string{value1.ID, value4.ID},
|
|
},
|
|
{
|
|
name: "pagination page 0",
|
|
opts: model.PropertyValueSearchOpts{
|
|
GroupID: groupID,
|
|
PerPage: 1,
|
|
},
|
|
expectedIDs: []string{value1.ID},
|
|
},
|
|
{
|
|
name: "pagination page 1",
|
|
opts: model.PropertyValueSearchOpts{
|
|
GroupID: groupID,
|
|
Cursor: model.PropertyValueSearchCursor{
|
|
CreateAt: value1.CreateAt,
|
|
PropertyValueID: value1.ID,
|
|
},
|
|
PerPage: 1,
|
|
},
|
|
expectedIDs: []string{value2.ID},
|
|
},
|
|
{
|
|
name: "filter by multiple target_ids",
|
|
opts: model.PropertyValueSearchOpts{
|
|
TargetIDs: []string{targetID, value4.TargetID},
|
|
PerPage: 10,
|
|
},
|
|
expectedIDs: []string{value1.ID, value2.ID},
|
|
},
|
|
{
|
|
name: "filter by multiple target_ids with group filter",
|
|
opts: model.PropertyValueSearchOpts{
|
|
GroupID: groupID,
|
|
TargetIDs: []string{targetID, value4.TargetID},
|
|
PerPage: 10,
|
|
},
|
|
expectedIDs: []string{value1.ID, value2.ID},
|
|
},
|
|
{
|
|
name: "filter by SinceUpdateAt timestamp - no results before",
|
|
opts: model.PropertyValueSearchOpts{
|
|
SinceUpdateAt: value3.UpdateAt, // After all existing values
|
|
PerPage: 10,
|
|
},
|
|
expectedIDs: []string{},
|
|
},
|
|
{
|
|
name: "filter by SinceUpdateAt timestamp - get values after specific time",
|
|
opts: model.PropertyValueSearchOpts{
|
|
SinceUpdateAt: value1.UpdateAt, // After value1, should get value2 and value3
|
|
PerPage: 10,
|
|
},
|
|
expectedIDs: []string{value2.ID, value3.ID},
|
|
},
|
|
{
|
|
name: "filter by SinceUpdateAt timestamp with group filter",
|
|
opts: model.PropertyValueSearchOpts{
|
|
GroupID: groupID,
|
|
SinceUpdateAt: value1.UpdateAt, // After value1, should only get value2 from same group
|
|
PerPage: 10,
|
|
},
|
|
expectedIDs: []string{value2.ID},
|
|
},
|
|
{
|
|
name: "filter by SinceUpdateAt timestamp including deleted",
|
|
opts: model.PropertyValueSearchOpts{
|
|
SinceUpdateAt: value3.UpdateAt, // After value3, should get value4 (deleted)
|
|
IncludeDeleted: true,
|
|
PerPage: 10,
|
|
},
|
|
expectedIDs: []string{value4.ID},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
results, err := ss.PropertyValue().SearchPropertyValues(tc.opts)
|
|
if tc.expectedError {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
ids := make([]string, len(results))
|
|
for i, value := range results {
|
|
ids[i] = value.ID
|
|
}
|
|
require.ElementsMatch(t, tc.expectedIDs, ids)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testSearchPropertyValuesSince(t *testing.T, _ request.CTX, ss store.Store) {
|
|
// Create values with controlled timestamps for precise testing
|
|
groupID := model.NewId()
|
|
|
|
// Create value 1 (will remain unchanged)
|
|
value1, err := ss.PropertyValue().Create(&model.PropertyValue{
|
|
GroupID: groupID,
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value1"`),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
time.Sleep(10 * time.Millisecond) // Ensure different timestamps
|
|
|
|
// Create value 2 (will be updated later)
|
|
value2, err := ss.PropertyValue().Create(&model.PropertyValue{
|
|
GroupID: groupID,
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value2"`),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
// Create value 3 (will remain unchanged)
|
|
value3, err := ss.PropertyValue().Create(&model.PropertyValue{
|
|
GroupID: groupID,
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value3"`),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Update value2 to change its UpdateAt timestamp
|
|
time.Sleep(10 * time.Millisecond)
|
|
value2.Value = json.RawMessage(`"value2_updated"`)
|
|
updatedValues, err := ss.PropertyValue().Update("", []*model.PropertyValue{value2})
|
|
require.NoError(t, err)
|
|
require.Len(t, updatedValues, 1)
|
|
updatedValue2 := updatedValues[0]
|
|
|
|
t.Run("SinceUpdateAt filters correctly by UpdateAt", func(t *testing.T) {
|
|
// Get values updated after value1 (should get value2 and value3)
|
|
results, err := ss.PropertyValue().SearchPropertyValues(model.PropertyValueSearchOpts{
|
|
GroupID: groupID,
|
|
SinceUpdateAt: value1.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{value2.ID, value3.ID}, resultIDs)
|
|
})
|
|
|
|
t.Run("SinceUpdateAt with boundary condition", func(t *testing.T) {
|
|
// Get values updated after value3's timestamp
|
|
// Should get both value2 (updated) and value3, so expect 2 results
|
|
results, err := ss.PropertyValue().SearchPropertyValues(model.PropertyValueSearchOpts{
|
|
GroupID: groupID,
|
|
SinceUpdateAt: value3.UpdateAt - 1, // Slightly before value3'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 value2 (updated with new timestamp) and value3
|
|
require.ElementsMatch(t, []string{value2.ID, value3.ID}, resultIDs)
|
|
})
|
|
|
|
t.Run("SinceUpdateAt after all updates", func(t *testing.T) {
|
|
// Get values updated after the most recent update
|
|
results, err := ss.PropertyValue().SearchPropertyValues(model.PropertyValueSearchOpts{
|
|
GroupID: groupID,
|
|
SinceUpdateAt: updatedValue2.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 values updated since current time
|
|
results, err := ss.PropertyValue().SearchPropertyValues(model.PropertyValueSearchOpts{
|
|
GroupID: groupID,
|
|
SinceUpdateAt: model.GetMillis(),
|
|
PerPage: 10,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, results, 0)
|
|
})
|
|
}
|
|
|
|
func testCreatePropertyValueWithArray(t *testing.T, _ request.CTX, ss store.Store) {
|
|
t.Run("should create a property value with array", func(t *testing.T) {
|
|
newValue := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`["option1", "option2", "option3"]`),
|
|
}
|
|
|
|
value, err := ss.PropertyValue().Create(newValue)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, value.ID)
|
|
require.NotZero(t, value.CreateAt)
|
|
require.NotZero(t, value.UpdateAt)
|
|
require.Zero(t, value.DeleteAt)
|
|
|
|
// Verify array values
|
|
var arrayValues []string
|
|
require.NoError(t, json.Unmarshal(value.Value, &arrayValues))
|
|
require.Equal(t, []string{"option1", "option2", "option3"}, arrayValues)
|
|
})
|
|
|
|
t.Run("should update array values", func(t *testing.T) {
|
|
value := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: model.NewId(),
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`["initial1", "initial2"]`),
|
|
}
|
|
|
|
created, err := ss.PropertyValue().Create(value)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, created.ID)
|
|
|
|
created.Value = json.RawMessage(`["updated1", "updated2", "updated3"]`)
|
|
updated, err := ss.PropertyValue().Update("", []*model.PropertyValue{created})
|
|
require.NoError(t, err)
|
|
require.NotZero(t, updated)
|
|
|
|
// Verify updated array values
|
|
retrieved, err := ss.PropertyValue().Get("", created.ID)
|
|
require.NoError(t, err)
|
|
var arrayValues []string
|
|
require.NoError(t, json.Unmarshal(retrieved.Value, &arrayValues))
|
|
require.Equal(t, []string{"updated1", "updated2", "updated3"}, arrayValues)
|
|
})
|
|
}
|
|
|
|
func testDeleteForField(t *testing.T, _ request.CTX, ss store.Store) {
|
|
t.Run("should delete values with matching fieldID and groupID", func(t *testing.T) {
|
|
fieldID := model.NewId()
|
|
groupID := model.NewId()
|
|
|
|
// Create test values
|
|
value1 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID,
|
|
FieldID: fieldID,
|
|
Value: json.RawMessage(`"value 1"`),
|
|
}
|
|
|
|
value2 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID,
|
|
FieldID: fieldID,
|
|
Value: json.RawMessage(`"value 2"`),
|
|
}
|
|
|
|
value3 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID,
|
|
FieldID: model.NewId(), // Different field ID
|
|
Value: json.RawMessage(`"value 3"`),
|
|
}
|
|
|
|
for _, value := range []*model.PropertyValue{value1, value2, value3} {
|
|
_, err := ss.PropertyValue().Create(value)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Delete values for the field
|
|
err := ss.PropertyValue().DeleteForField(groupID, fieldID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify values were soft-deleted
|
|
deletedValues, err := ss.PropertyValue().GetMany(groupID, []string{value1.ID, value2.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, deletedValues, 2)
|
|
require.NotZero(t, deletedValues[0].DeleteAt)
|
|
require.NotZero(t, deletedValues[1].DeleteAt)
|
|
|
|
// Verify value with different field ID was not deleted
|
|
nonDeletedValue, err := ss.PropertyValue().Get(groupID, value3.ID)
|
|
require.NoError(t, err)
|
|
require.Zero(t, nonDeletedValue.DeleteAt)
|
|
})
|
|
|
|
t.Run("should not delete values with non-matching groupID", func(t *testing.T) {
|
|
fieldID := model.NewId()
|
|
groupID1 := model.NewId()
|
|
groupID2 := model.NewId()
|
|
|
|
// Create values with same fieldID but different groupIDs
|
|
value1 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID1,
|
|
FieldID: fieldID,
|
|
Value: json.RawMessage(`"group1 value"`),
|
|
}
|
|
|
|
value2 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID2,
|
|
FieldID: fieldID,
|
|
Value: json.RawMessage(`"group2 value"`),
|
|
}
|
|
|
|
for _, value := range []*model.PropertyValue{value1, value2} {
|
|
_, err := ss.PropertyValue().Create(value)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Delete values for the field but only in groupID1
|
|
err := ss.PropertyValue().DeleteForField(groupID1, fieldID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify value1 was deleted
|
|
deletedValue, err := ss.PropertyValue().Get(groupID1, value1.ID)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, deletedValue.DeleteAt)
|
|
|
|
// Verify value2 was not deleted (different group)
|
|
nonDeletedValue, err := ss.PropertyValue().Get(groupID2, value2.ID)
|
|
require.NoError(t, err)
|
|
require.Zero(t, nonDeletedValue.DeleteAt)
|
|
})
|
|
|
|
t.Run("should delete values with empty groupID", func(t *testing.T) {
|
|
fieldID := model.NewId()
|
|
groupID1 := model.NewId()
|
|
groupID2 := model.NewId()
|
|
|
|
// Create values with same fieldID but different groupIDs
|
|
value1 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID1,
|
|
FieldID: fieldID,
|
|
Value: json.RawMessage(`"group1 value 2"`),
|
|
}
|
|
|
|
value2 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID2,
|
|
FieldID: fieldID,
|
|
Value: json.RawMessage(`"group2 value 2"`),
|
|
}
|
|
|
|
for _, value := range []*model.PropertyValue{value1, value2} {
|
|
_, err := ss.PropertyValue().Create(value)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Delete values for the field across all groups (empty groupID)
|
|
err := ss.PropertyValue().DeleteForField("", fieldID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify both values were deleted
|
|
opts := model.PropertyValueSearchOpts{
|
|
FieldID: fieldID,
|
|
IncludeDeleted: true,
|
|
PerPage: 10,
|
|
}
|
|
|
|
values, err := ss.PropertyValue().SearchPropertyValues(opts)
|
|
require.NoError(t, err)
|
|
|
|
// Find our two values within the results
|
|
var foundValues []*model.PropertyValue
|
|
for _, v := range values {
|
|
if v.ID == value1.ID || v.ID == value2.ID {
|
|
foundValues = append(foundValues, v)
|
|
}
|
|
}
|
|
|
|
require.Len(t, foundValues, 2)
|
|
for _, v := range foundValues {
|
|
require.NotZero(t, v.DeleteAt, "Value should be deleted")
|
|
}
|
|
})
|
|
|
|
t.Run("should work with multiple calls targeting different groups", func(t *testing.T) {
|
|
fieldID := model.NewId()
|
|
groupID1 := model.NewId()
|
|
groupID2 := model.NewId()
|
|
groupID3 := model.NewId()
|
|
|
|
// Create values with same fieldID across multiple groups
|
|
value1 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID1,
|
|
FieldID: fieldID,
|
|
Value: json.RawMessage(`"multi-group test 1"`),
|
|
}
|
|
|
|
value2 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID2,
|
|
FieldID: fieldID,
|
|
Value: json.RawMessage(`"multi-group test 2"`),
|
|
}
|
|
|
|
value3 := &model.PropertyValue{
|
|
TargetID: model.NewId(),
|
|
TargetType: "test_type",
|
|
GroupID: groupID3,
|
|
FieldID: fieldID,
|
|
Value: json.RawMessage(`"multi-group test 3"`),
|
|
}
|
|
|
|
for _, value := range []*model.PropertyValue{value1, value2, value3} {
|
|
_, err := ss.PropertyValue().Create(value)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Delete values for the field but only in groupID1
|
|
err := ss.PropertyValue().DeleteForField(groupID1, fieldID)
|
|
require.NoError(t, err)
|
|
|
|
// Delete values for the field but only in groupID2
|
|
err = ss.PropertyValue().DeleteForField(groupID2, fieldID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify value1 was deleted
|
|
value1Result, err := ss.PropertyValue().Get(groupID1, value1.ID)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, value1Result.DeleteAt)
|
|
|
|
// Verify value2 was deleted
|
|
value2Result, err := ss.PropertyValue().Get(groupID2, value2.ID)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, value2Result.DeleteAt)
|
|
|
|
// Verify value3 was not deleted
|
|
value3Result, err := ss.PropertyValue().Get(groupID3, value3.ID)
|
|
require.NoError(t, err)
|
|
require.Zero(t, value3Result.DeleteAt)
|
|
})
|
|
}
|
|
|
|
func testDeleteForTarget(t *testing.T, _ request.CTX, ss store.Store) {
|
|
groupID1 := model.NewId()
|
|
groupID2 := model.NewId()
|
|
groupID3 := model.NewId()
|
|
targetID1 := model.NewId()
|
|
targetID2 := model.NewId()
|
|
targetType := "test_type"
|
|
|
|
// Create test values for first group and first target
|
|
value1 := &model.PropertyValue{
|
|
TargetID: targetID1,
|
|
TargetType: targetType,
|
|
GroupID: groupID1,
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value 1"`),
|
|
}
|
|
|
|
value2 := &model.PropertyValue{
|
|
TargetID: targetID1,
|
|
TargetType: targetType,
|
|
GroupID: groupID1,
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value 2"`),
|
|
}
|
|
|
|
// Create test value for second group but same target
|
|
value3 := &model.PropertyValue{
|
|
TargetID: targetID1,
|
|
TargetType: targetType,
|
|
GroupID: groupID2,
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value 3"`),
|
|
}
|
|
|
|
// Create test value for first group but different target
|
|
value4 := &model.PropertyValue{
|
|
TargetID: targetID2,
|
|
TargetType: targetType,
|
|
GroupID: groupID1,
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value 4"`),
|
|
}
|
|
|
|
// Create test value with different target type
|
|
value5 := &model.PropertyValue{
|
|
TargetID: targetID1,
|
|
TargetType: "other_type",
|
|
GroupID: groupID1,
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value 5"`),
|
|
}
|
|
|
|
// Create test value for third group and first target
|
|
value6 := &model.PropertyValue{
|
|
TargetID: targetID1,
|
|
TargetType: targetType,
|
|
GroupID: groupID3,
|
|
FieldID: model.NewId(),
|
|
Value: json.RawMessage(`"value 6"`),
|
|
}
|
|
|
|
for _, value := range []*model.PropertyValue{value1, value2, value3, value4, value5, value6} {
|
|
_, err := ss.PropertyValue().Create(value)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
t.Run("should return error if targetType or targetID is empty", func(t *testing.T) {
|
|
err := ss.PropertyValue().DeleteForTarget(groupID1, "", targetID1)
|
|
require.Error(t, err)
|
|
var eii *store.ErrInvalidInput
|
|
require.ErrorAs(t, err, &eii)
|
|
|
|
err = ss.PropertyValue().DeleteForTarget(groupID1, targetType, "")
|
|
require.Error(t, err)
|
|
require.ErrorAs(t, err, &eii)
|
|
|
|
// Verify values were not deleted
|
|
values, err := ss.PropertyValue().GetMany("", []string{value1.ID, value2.ID})
|
|
require.NoError(t, err)
|
|
require.NotZero(t, values)
|
|
require.Len(t, values, 2)
|
|
})
|
|
|
|
t.Run("should delete only property values for a target within the specified group", func(t *testing.T) {
|
|
// Delete values for the first target in first group
|
|
err := ss.PropertyValue().DeleteForTarget(groupID1, targetType, targetID1)
|
|
require.NoError(t, err)
|
|
|
|
// Verify values from first group and first target were hard-deleted
|
|
deletedValues, err := ss.PropertyValue().GetMany("", []string{value1.ID, value2.ID})
|
|
require.Error(t, err)
|
|
require.Zero(t, deletedValues)
|
|
|
|
// Verify value from second group was not deleted
|
|
nonDeletedGroupValue, err := ss.PropertyValue().Get("", value3.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, nonDeletedGroupValue)
|
|
|
|
// Verify value from first group but different target was not deleted
|
|
nonDeletedTargetValue, err := ss.PropertyValue().Get("", value4.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, nonDeletedTargetValue)
|
|
|
|
// Verify value with different target type was not deleted
|
|
nonDeletedTypeValue, err := ss.PropertyValue().Get("", value5.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, nonDeletedTypeValue)
|
|
})
|
|
|
|
t.Run("should delete all values for a target regardless of group", func(t *testing.T) {
|
|
err := ss.PropertyValue().DeleteForTarget("", targetType, targetID1)
|
|
require.NoError(t, err)
|
|
|
|
// Verify values from other groups with targetID1 were deleted
|
|
deletedValues, err := ss.PropertyValue().GetMany("", []string{value3.ID, value6.ID})
|
|
require.Error(t, err)
|
|
require.Zero(t, deletedValues)
|
|
|
|
// Verify value with different target ID was not deleted
|
|
nonDeletedValue, err := ss.PropertyValue().Get("", value4.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, nonDeletedValue)
|
|
|
|
// Verify value with different target type was not deleted
|
|
nonDeletedTypeValue, err := ss.PropertyValue().Get("", value5.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, nonDeletedTypeValue)
|
|
})
|
|
}
|