// 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) }) }