mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
The server module named github.com/hanzoai/team/server/v8 — a path that resolves to the retired workspace's repository, not this one — while server/public still published github.com/mattermost/mattermost/server/public. Both are github.com/hanzoteam/server/... now, which is where this code actually lives. That second half is what made the binary unbootable. Upstream's public path stayed claimed by upstream, mattermost-plugin-ai imported it, and the build linked public/model twice; public/plugin's gob.Register then ran twice under one name and panicked at init, before main. Reverting the rename traded the brand for a working binary. Rebranding the plugin alongside it needs no such trade: the agents fork is github.com/hanzoteam/agents/v2 against our public module, so no module named mattermost/mattermost is in the graph at all. Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and it is ours.
638 lines
22 KiB
Go
638 lines
22 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package properties
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
"github.com/hanzoteam/server/server/public/shared/request"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// testHook is a configurable PropertyHook implementation for testing hook
|
|
// registration, ordering, chaining, and blocking behavior. It embeds
|
|
// BasePropertyHook for default passthrough behavior and only overrides
|
|
// methods where a test-specific function is set.
|
|
type testHook struct {
|
|
BasePropertyHook
|
|
preCreateFieldFn func(*model.PropertyField) (*model.PropertyField, error)
|
|
preUpdateFieldFn func(string, *model.PropertyField) (*model.PropertyField, error)
|
|
preUpdateFieldsFn func(string, []*model.PropertyField) ([]*model.PropertyField, error)
|
|
preDeleteFieldFn func(string, string) error
|
|
postUpdateFieldsFn func(string, []*model.PropertyField, []*model.PropertyField, []*model.PropertyField) ([]*model.PropertyField, []*model.PropertyField, []string, error)
|
|
postGetFieldFn func(*model.PropertyField) (*model.PropertyField, error)
|
|
postGetFieldsFn func([]*model.PropertyField) ([]*model.PropertyField, error)
|
|
preUpsertValueFn func(*model.PropertyValue) (*model.PropertyValue, error)
|
|
preUpsertValuesFn func([]*model.PropertyValue) ([]*model.PropertyValue, error)
|
|
postGetValueFn func(*model.PropertyValue) (*model.PropertyValue, error)
|
|
postGetValuesFn func([]*model.PropertyValue) ([]*model.PropertyValue, error)
|
|
}
|
|
|
|
func (h *testHook) PreCreatePropertyField(_ request.CTX, field *model.PropertyField) (*model.PropertyField, error) {
|
|
if h.preCreateFieldFn != nil {
|
|
return h.preCreateFieldFn(field)
|
|
}
|
|
return field, nil
|
|
}
|
|
|
|
func (h *testHook) PreUpdatePropertyField(_ request.CTX, groupID string, field *model.PropertyField) (*model.PropertyField, error) {
|
|
if h.preUpdateFieldFn != nil {
|
|
return h.preUpdateFieldFn(groupID, field)
|
|
}
|
|
return field, nil
|
|
}
|
|
|
|
func (h *testHook) PreUpdatePropertyFields(_ request.CTX, groupID string, fields []*model.PropertyField) ([]*model.PropertyField, error) {
|
|
if h.preUpdateFieldsFn != nil {
|
|
return h.preUpdateFieldsFn(groupID, fields)
|
|
}
|
|
return fields, nil
|
|
}
|
|
|
|
func (h *testHook) PreDeletePropertyField(_ request.CTX, groupID string, id string) error {
|
|
if h.preDeleteFieldFn != nil {
|
|
return h.preDeleteFieldFn(groupID, id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *testHook) PostUpdatePropertyFields(_ request.CTX, groupID string, prev, requested, propagated []*model.PropertyField) ([]*model.PropertyField, []*model.PropertyField, []string, error) {
|
|
if h.postUpdateFieldsFn != nil {
|
|
return h.postUpdateFieldsFn(groupID, prev, requested, propagated)
|
|
}
|
|
return requested, propagated, nil, nil
|
|
}
|
|
|
|
func (h *testHook) PostGetPropertyField(_ request.CTX, field *model.PropertyField) (*model.PropertyField, error) {
|
|
if h.postGetFieldFn != nil {
|
|
return h.postGetFieldFn(field)
|
|
}
|
|
return field, nil
|
|
}
|
|
|
|
func (h *testHook) PostGetPropertyFields(_ request.CTX, fields []*model.PropertyField) ([]*model.PropertyField, error) {
|
|
if h.postGetFieldsFn != nil {
|
|
return h.postGetFieldsFn(fields)
|
|
}
|
|
return fields, nil
|
|
}
|
|
|
|
func (h *testHook) PreUpsertPropertyValue(_ request.CTX, value *model.PropertyValue) (*model.PropertyValue, error) {
|
|
if h.preUpsertValueFn != nil {
|
|
return h.preUpsertValueFn(value)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (h *testHook) PreUpsertPropertyValues(_ request.CTX, values []*model.PropertyValue) ([]*model.PropertyValue, error) {
|
|
if h.preUpsertValuesFn != nil {
|
|
return h.preUpsertValuesFn(values)
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
func (h *testHook) PostGetPropertyValue(_ request.CTX, value *model.PropertyValue) (*model.PropertyValue, error) {
|
|
if h.postGetValueFn != nil {
|
|
return h.postGetValueFn(value)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (h *testHook) PostGetPropertyValues(_ request.CTX, values []*model.PropertyValue) ([]*model.PropertyValue, error) {
|
|
if h.postGetValuesFn != nil {
|
|
return h.postGetValuesFn(values)
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
func TestHookRegistration(t *testing.T) {
|
|
th := Setup(t).RegisterCPAPropertyGroup(t)
|
|
|
|
t.Run("service starts with no hooks", func(t *testing.T) {
|
|
service, err := New(ServiceConfig{
|
|
PropertyGroupStore: th.dbStore.PropertyGroup(),
|
|
PropertyFieldStore: th.dbStore.PropertyField(),
|
|
PropertyValueStore: th.dbStore.PropertyValue(),
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, service.hooks)
|
|
})
|
|
|
|
t.Run("AddHook appends hooks in order", func(t *testing.T) {
|
|
service, err := New(ServiceConfig{
|
|
PropertyGroupStore: th.dbStore.PropertyGroup(),
|
|
PropertyFieldStore: th.dbStore.PropertyField(),
|
|
PropertyValueStore: th.dbStore.PropertyValue(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
hook1 := &testHook{}
|
|
hook2 := &testHook{}
|
|
service.AddHook(hook1)
|
|
service.AddHook(hook2)
|
|
assert.Len(t, service.hooks, 2)
|
|
})
|
|
}
|
|
|
|
func TestPreHookBlocking(t *testing.T) {
|
|
th := Setup(t).RegisterCPAPropertyGroup(t)
|
|
rctx := th.Context
|
|
groupID := th.RegisterPropertyGroup(t, model.PropertyGroupVersionV1).ID
|
|
|
|
t.Run("pre-hook error blocks CreatePropertyField", func(t *testing.T) {
|
|
hook := &testHook{
|
|
preCreateFieldFn: func(field *model.PropertyField) (*model.PropertyField, error) {
|
|
return nil, fmt.Errorf("blocked by hook")
|
|
},
|
|
}
|
|
th.service.AddHook(hook)
|
|
defer func() { th.service.hooks = th.service.hooks[:len(th.service.hooks)-1] }()
|
|
|
|
field := &model.PropertyField{
|
|
GroupID: groupID,
|
|
Name: "blocked-field-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
TargetType: "user",
|
|
}
|
|
_, err := th.service.CreatePropertyField(rctx, field)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "blocked by hook")
|
|
})
|
|
|
|
t.Run("pre-hook error blocks DeletePropertyField", func(t *testing.T) {
|
|
hook := &testHook{
|
|
preDeleteFieldFn: func(gid string, id string) error {
|
|
return fmt.Errorf("delete blocked")
|
|
},
|
|
}
|
|
th.service.AddHook(hook)
|
|
defer func() { th.service.hooks = th.service.hooks[:len(th.service.hooks)-1] }()
|
|
|
|
err := th.service.DeletePropertyField(rctx, groupID, model.NewId())
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "delete blocked")
|
|
})
|
|
|
|
t.Run("pre-hook error blocks UpsertPropertyValue", func(t *testing.T) {
|
|
hook := &testHook{
|
|
preUpsertValueFn: func(value *model.PropertyValue) (*model.PropertyValue, error) {
|
|
return nil, fmt.Errorf("upsert blocked")
|
|
},
|
|
}
|
|
th.service.AddHook(hook)
|
|
defer func() { th.service.hooks = th.service.hooks[:len(th.service.hooks)-1] }()
|
|
|
|
value := &model.PropertyValue{
|
|
GroupID: groupID,
|
|
FieldID: model.NewId(),
|
|
}
|
|
_, err := th.service.UpsertPropertyValue(rctx, value)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "upsert blocked")
|
|
})
|
|
}
|
|
|
|
func TestPreHookInputModification(t *testing.T) {
|
|
th := Setup(t).RegisterCPAPropertyGroup(t)
|
|
rctx := th.Context
|
|
groupID := th.RegisterPropertyGroup(t, model.PropertyGroupVersionV1).ID
|
|
|
|
t.Run("pre-hook modifies field before creation", func(t *testing.T) {
|
|
hook := &testHook{
|
|
preCreateFieldFn: func(field *model.PropertyField) (*model.PropertyField, error) {
|
|
// Modify the field name
|
|
field.Name = "modified-" + field.Name
|
|
return field, nil
|
|
},
|
|
}
|
|
th.service.AddHook(hook)
|
|
defer func() { th.service.hooks = th.service.hooks[:len(th.service.hooks)-1] }()
|
|
|
|
field := &model.PropertyField{
|
|
GroupID: groupID,
|
|
Name: "original",
|
|
Type: model.PropertyFieldTypeText,
|
|
TargetType: "user",
|
|
}
|
|
result, err := th.service.CreatePropertyField(rctx, field)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "modified-original", result.Name)
|
|
})
|
|
}
|
|
|
|
func TestPostHookFiltering(t *testing.T) {
|
|
th := Setup(t).RegisterCPAPropertyGroup(t)
|
|
rctx := th.Context
|
|
groupID := th.RegisterPropertyGroup(t, model.PropertyGroupVersionV1).ID
|
|
|
|
t.Run("post-hook returning nil field without error surfaces errNilHookResult", func(t *testing.T) {
|
|
field := th.CreatePropertyFieldDirect(t, &model.PropertyField{
|
|
GroupID: groupID,
|
|
Name: "nil-return-field-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
TargetType: "user",
|
|
})
|
|
|
|
hook := &testHook{
|
|
postGetFieldFn: func(f *model.PropertyField) (*model.PropertyField, error) {
|
|
return nil, nil
|
|
},
|
|
}
|
|
th.service.AddHook(hook)
|
|
defer func() { th.service.hooks = th.service.hooks[:len(th.service.hooks)-1] }()
|
|
|
|
result, err := th.service.GetPropertyField(rctx, groupID, field.ID)
|
|
require.ErrorIs(t, err, errNilHookResult)
|
|
assert.Nil(t, result)
|
|
})
|
|
|
|
t.Run("post-hook that drops fields from list returns error", func(t *testing.T) {
|
|
field1 := th.CreatePropertyFieldDirect(t, &model.PropertyField{
|
|
GroupID: groupID,
|
|
Name: "keep-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
TargetType: "user",
|
|
})
|
|
field2 := th.CreatePropertyFieldDirect(t, &model.PropertyField{
|
|
GroupID: groupID,
|
|
Name: "remove-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
TargetType: "user",
|
|
})
|
|
|
|
hook := &testHook{
|
|
postGetFieldsFn: func(fields []*model.PropertyField) ([]*model.PropertyField, error) {
|
|
filtered := []*model.PropertyField{}
|
|
for _, f := range fields {
|
|
if f.ID == field1.ID {
|
|
filtered = append(filtered, f)
|
|
}
|
|
}
|
|
return filtered, nil
|
|
},
|
|
}
|
|
th.service.AddHook(hook)
|
|
defer func() { th.service.hooks = th.service.hooks[:len(th.service.hooks)-1] }()
|
|
|
|
_, err := th.service.GetPropertyFields(rctx, groupID, []string{field1.ID, field2.ID})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "fewer fields")
|
|
})
|
|
}
|
|
|
|
func TestMultipleHooksChaining(t *testing.T) {
|
|
th := Setup(t).RegisterCPAPropertyGroup(t)
|
|
rctx := th.Context
|
|
groupID := th.RegisterPropertyGroup(t, model.PropertyGroupVersionV1).ID
|
|
|
|
t.Run("multiple pre-hooks chain modifications in order", func(t *testing.T) {
|
|
order := []string{}
|
|
|
|
hook1 := &testHook{
|
|
preCreateFieldFn: func(field *model.PropertyField) (*model.PropertyField, error) {
|
|
order = append(order, "hook1")
|
|
field.Name = field.Name + "-h1"
|
|
return field, nil
|
|
},
|
|
}
|
|
hook2 := &testHook{
|
|
preCreateFieldFn: func(field *model.PropertyField) (*model.PropertyField, error) {
|
|
order = append(order, "hook2")
|
|
field.Name = field.Name + "-h2"
|
|
return field, nil
|
|
},
|
|
}
|
|
th.service.AddHook(hook1)
|
|
th.service.AddHook(hook2)
|
|
defer func() { th.service.hooks = th.service.hooks[:len(th.service.hooks)-2] }()
|
|
|
|
field := &model.PropertyField{
|
|
GroupID: groupID,
|
|
Name: "base",
|
|
Type: model.PropertyFieldTypeText,
|
|
TargetType: "user",
|
|
}
|
|
result, err := th.service.CreatePropertyField(rctx, field)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "base-h1-h2", result.Name)
|
|
assert.Equal(t, []string{"hook1", "hook2"}, order)
|
|
})
|
|
|
|
t.Run("first hook error prevents second hook from running", func(t *testing.T) {
|
|
hook2Called := false
|
|
|
|
hook1 := &testHook{
|
|
preCreateFieldFn: func(field *model.PropertyField) (*model.PropertyField, error) {
|
|
return nil, fmt.Errorf("hook1 blocked")
|
|
},
|
|
}
|
|
hook2 := &testHook{
|
|
preCreateFieldFn: func(field *model.PropertyField) (*model.PropertyField, error) {
|
|
hook2Called = true
|
|
return field, nil
|
|
},
|
|
}
|
|
th.service.AddHook(hook1)
|
|
th.service.AddHook(hook2)
|
|
defer func() { th.service.hooks = th.service.hooks[:len(th.service.hooks)-2] }()
|
|
|
|
field := &model.PropertyField{
|
|
GroupID: groupID,
|
|
Name: "should-fail-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
TargetType: "user",
|
|
}
|
|
_, err := th.service.CreatePropertyField(rctx, field)
|
|
require.Error(t, err)
|
|
assert.False(t, hook2Called, "second hook should not have been called")
|
|
})
|
|
|
|
t.Run("multiple post-hooks chain in order", func(t *testing.T) {
|
|
field := th.CreatePropertyFieldDirect(t, &model.PropertyField{
|
|
GroupID: groupID,
|
|
Name: "chain-post-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
TargetType: "user",
|
|
Attrs: model.StringInterface{"step": "0"},
|
|
})
|
|
|
|
hook1 := &testHook{
|
|
postGetFieldFn: func(f *model.PropertyField) (*model.PropertyField, error) {
|
|
if f.Attrs == nil {
|
|
f.Attrs = make(model.StringInterface)
|
|
}
|
|
f.Attrs["hook1"] = true
|
|
return f, nil
|
|
},
|
|
}
|
|
hook2 := &testHook{
|
|
postGetFieldFn: func(f *model.PropertyField) (*model.PropertyField, error) {
|
|
if f.Attrs == nil {
|
|
f.Attrs = make(model.StringInterface)
|
|
}
|
|
f.Attrs["hook2"] = true
|
|
return f, nil
|
|
},
|
|
}
|
|
th.service.AddHook(hook1)
|
|
th.service.AddHook(hook2)
|
|
defer func() { th.service.hooks = th.service.hooks[:len(th.service.hooks)-2] }()
|
|
|
|
result, err := th.service.GetPropertyField(rctx, groupID, field.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, true, result.Attrs["hook1"])
|
|
assert.Equal(t, true, result.Attrs["hook2"])
|
|
})
|
|
}
|
|
|
|
func TestAccessControlHookGroupScoping(t *testing.T) {
|
|
th := Setup(t).RegisterCPAPropertyGroup(t)
|
|
rctx := th.Context
|
|
|
|
th.service.setPluginCheckerForTests(func(pluginID string) bool {
|
|
return pluginID == "plugin-1"
|
|
})
|
|
|
|
rctxPlugin1 := RequestContextWithCallerID(th.Context, "plugin-1")
|
|
rctxPlugin2 := RequestContextWithCallerID(th.Context, "plugin-2")
|
|
|
|
t.Run("access control enforced for managed group (CPA)", func(t *testing.T) {
|
|
// Create a protected field in the CPA group via the source plugin
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "protected-managed-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "plugin-1", created.Attrs[model.PropertyAttrsSourcePluginID])
|
|
|
|
// Another plugin should NOT be able to update it (protected)
|
|
created.Attrs[model.PropertyAttrsProtected] = true
|
|
updated, _, err := th.service.UpdatePropertyField(rctxPlugin1, th.CPAGroupID, created)
|
|
require.NoError(t, err)
|
|
|
|
updated.Name = "attempt-update"
|
|
_, _, err = th.service.UpdatePropertyField(rctxPlugin2, th.CPAGroupID, updated)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "protected")
|
|
})
|
|
|
|
t.Run("access control NOT enforced for unmanaged group", func(t *testing.T) {
|
|
unmanagedGroup, err := th.service.RegisterPropertyGroup(&model.PropertyGroup{Name: "unmanaged_scoping_test", Version: model.PropertyGroupVersionV1})
|
|
require.NoError(t, err)
|
|
|
|
// Create a protected field in an unmanaged group
|
|
field := &model.PropertyField{
|
|
GroupID: unmanagedGroup.ID,
|
|
Name: "protected-unmanaged-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
TargetType: "user",
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyAttrsSourcePluginID: "plugin-1",
|
|
},
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
|
|
// Another plugin CAN update it (no access control for this group)
|
|
created.Name = "updated-by-plugin2"
|
|
updated, _, err := th.service.UpdatePropertyField(rctxPlugin2, unmanagedGroup.ID, created)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "updated-by-plugin2", updated.Name)
|
|
})
|
|
|
|
t.Run("read filtering applied for managed group", func(t *testing.T) {
|
|
// Create a source-only protected field in the CPA group
|
|
field := &model.PropertyField{
|
|
GroupID: th.CPAGroupID,
|
|
Name: "source-only-managed-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeSelect,
|
|
ObjectType: model.PropertyFieldObjectTypeUser,
|
|
TargetType: string(model.PropertyFieldTargetLevelSystem),
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModeSourceOnly,
|
|
model.PropertyAttrsProtected: true,
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": "opt1", "value": "Option 1"},
|
|
map[string]any{"id": "opt2", "value": "Option 2"},
|
|
},
|
|
},
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
|
|
// Source plugin sees all options
|
|
result, err := th.service.GetPropertyField(rctxPlugin1, th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
opts := result.Attrs[model.PropertyFieldAttributeOptions].([]any)
|
|
assert.Len(t, opts, 2)
|
|
|
|
// Other caller sees empty options
|
|
result2, err := th.service.GetPropertyField(rctx, th.CPAGroupID, created.ID)
|
|
require.NoError(t, err)
|
|
opts2 := result2.Attrs[model.PropertyFieldAttributeOptions].([]any)
|
|
assert.Len(t, opts2, 0)
|
|
})
|
|
|
|
t.Run("read filtering NOT applied for unmanaged group", func(t *testing.T) {
|
|
unmanagedGroup, err := th.service.RegisterPropertyGroup(&model.PropertyGroup{Name: "unmanaged_read_test", Version: model.PropertyGroupVersionV1})
|
|
require.NoError(t, err)
|
|
|
|
// Create a source-only field in an unmanaged group
|
|
field := &model.PropertyField{
|
|
GroupID: unmanagedGroup.ID,
|
|
Name: "source-only-unmanaged-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeSelect,
|
|
TargetType: "user",
|
|
Attrs: model.StringInterface{
|
|
model.PropertyAttrsAccessMode: model.PropertyAccessModeSourceOnly,
|
|
model.PropertyAttrsSourcePluginID: "plugin-1",
|
|
model.PropertyFieldAttributeOptions: []any{
|
|
map[string]any{"id": "opt1", "value": "Option 1"},
|
|
map[string]any{"id": "opt2", "value": "Option 2"},
|
|
},
|
|
},
|
|
}
|
|
created, err := th.service.CreatePropertyField(rctxPlugin1, field)
|
|
require.NoError(t, err)
|
|
|
|
// Non-source caller sees ALL options (no filtering for unmanaged groups)
|
|
result, err := th.service.GetPropertyField(rctx, unmanagedGroup.ID, created.ID)
|
|
require.NoError(t, err)
|
|
opts := result.Attrs[model.PropertyFieldAttributeOptions].([]any)
|
|
assert.Len(t, opts, 2)
|
|
})
|
|
}
|
|
|
|
func TestPreUpdatePropertyFieldsHook(t *testing.T) {
|
|
th := Setup(t).RegisterCPAPropertyGroup(t)
|
|
rctx := th.Context
|
|
groupID := th.RegisterPropertyGroup(t, model.PropertyGroupVersionV1).ID
|
|
|
|
t.Run("pre-hook error blocks batch UpdatePropertyFields", func(t *testing.T) {
|
|
hook := &testHook{
|
|
preUpdateFieldsFn: func(gid string, fields []*model.PropertyField) ([]*model.PropertyField, error) {
|
|
return nil, fmt.Errorf("batch update blocked")
|
|
},
|
|
}
|
|
th.service.AddHook(hook)
|
|
defer func() { th.service.hooks = th.service.hooks[:len(th.service.hooks)-1] }()
|
|
|
|
field := th.CreatePropertyFieldDirect(t, &model.PropertyField{
|
|
GroupID: groupID,
|
|
Name: "batch-block-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
TargetType: "user",
|
|
})
|
|
field.Name = "updated"
|
|
_, _, _, err := th.service.UpdatePropertyFields(rctx, groupID, []*model.PropertyField{field})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "batch update blocked")
|
|
})
|
|
|
|
t.Run("pre-hook modifies fields in batch update", func(t *testing.T) {
|
|
hook := &testHook{
|
|
preUpdateFieldsFn: func(gid string, fields []*model.PropertyField) ([]*model.PropertyField, error) {
|
|
for _, f := range fields {
|
|
f.Name = "modified-" + f.Name
|
|
}
|
|
return fields, nil
|
|
},
|
|
}
|
|
th.service.AddHook(hook)
|
|
defer func() { th.service.hooks = th.service.hooks[:len(th.service.hooks)-1] }()
|
|
|
|
field1 := th.CreatePropertyFieldDirect(t, &model.PropertyField{
|
|
GroupID: groupID,
|
|
Name: "batch-a-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
TargetType: "user",
|
|
})
|
|
field2 := th.CreatePropertyFieldDirect(t, &model.PropertyField{
|
|
GroupID: groupID,
|
|
Name: "batch-b-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
TargetType: "user",
|
|
})
|
|
|
|
field1.Name = "a"
|
|
field2.Name = "b"
|
|
results, _, _, err := th.service.UpdatePropertyFields(rctx, groupID, []*model.PropertyField{field1, field2})
|
|
require.NoError(t, err)
|
|
require.Len(t, results, 2)
|
|
assert.Equal(t, "modified-a", results[0].Name)
|
|
assert.Equal(t, "modified-b", results[1].Name)
|
|
})
|
|
}
|
|
|
|
func TestPostUpdatePropertyFieldsHook(t *testing.T) {
|
|
th := Setup(t).RegisterCPAPropertyGroup(t)
|
|
rctx := th.Context
|
|
groupID := th.RegisterPropertyGroup(t, model.PropertyGroupVersionV1).ID
|
|
|
|
t.Run("post-hook transforms requested attrs and surfaces cleared IDs", func(t *testing.T) {
|
|
hook := &testHook{
|
|
postUpdateFieldsFn: func(_ string, _, requested, propagated []*model.PropertyField) ([]*model.PropertyField, []*model.PropertyField, []string, error) {
|
|
for _, f := range requested {
|
|
if f.Attrs == nil {
|
|
f.Attrs = model.StringInterface{}
|
|
}
|
|
f.Attrs["redacted"] = true
|
|
}
|
|
ids := make([]string, 0, len(requested))
|
|
for _, f := range requested {
|
|
ids = append(ids, f.ID)
|
|
}
|
|
return requested, propagated, ids, nil
|
|
},
|
|
}
|
|
th.service.AddHook(hook)
|
|
defer func() { th.service.hooks = th.service.hooks[:len(th.service.hooks)-1] }()
|
|
|
|
field := th.CreatePropertyFieldDirect(t, &model.PropertyField{
|
|
GroupID: groupID,
|
|
Name: "post-transform-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
TargetType: "user",
|
|
})
|
|
field.Name = "post-transform-renamed-" + model.NewId()
|
|
|
|
results, _, cleared, err := th.service.UpdatePropertyFields(rctx, groupID, []*model.PropertyField{field})
|
|
require.NoError(t, err)
|
|
require.Len(t, results, 1)
|
|
assert.Equal(t, true, results[0].Attrs["redacted"], "post-hook attr transform must reach caller")
|
|
assert.Equal(t, []string{field.ID}, cleared, "cleared IDs from post-hook must be surfaced")
|
|
})
|
|
|
|
t.Run("post-hook returning wrong-length requested slice is skipped", func(t *testing.T) {
|
|
hook := &testHook{
|
|
postUpdateFieldsFn: func(_ string, _, requested, propagated []*model.PropertyField) ([]*model.PropertyField, []*model.PropertyField, []string, error) {
|
|
// Drop a field — cardinality guard must reject this transform.
|
|
return requested[:0], propagated, nil, nil
|
|
},
|
|
}
|
|
th.service.AddHook(hook)
|
|
defer func() { th.service.hooks = th.service.hooks[:len(th.service.hooks)-1] }()
|
|
|
|
field := th.CreatePropertyFieldDirect(t, &model.PropertyField{
|
|
GroupID: groupID,
|
|
Name: "post-cardinality-" + model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
TargetType: "user",
|
|
})
|
|
field.Name = "post-cardinality-renamed-" + model.NewId()
|
|
|
|
results, _, _, err := th.service.UpdatePropertyFields(rctx, groupID, []*model.PropertyField{field})
|
|
require.NoError(t, err)
|
|
assert.Len(t, results, 1, "wrong-length transform must be discarded; original requested must survive")
|
|
})
|
|
}
|