- Implemented initialization for the Setting Registry during the Load process. - Added reload functionality to refresh the Setting Registry as needed. - Enhanced error handling to capture and report issues during initialization and reloading.
358 lines
8.6 KiB
Go
358 lines
8.6 KiB
Go
package setting_test
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/yaoapp/gou/store"
|
|
"github.com/yaoapp/yao/config"
|
|
"github.com/yaoapp/yao/setting"
|
|
"github.com/yaoapp/yao/test"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
test.Prepare(nil, config.Conf)
|
|
defer test.Clean()
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func setupRegistry(t *testing.T) *setting.Registry {
|
|
t.Helper()
|
|
test.Prepare(t, config.Conf)
|
|
|
|
err := setting.Init()
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
s, _ := store.Get("__yao.store")
|
|
if s != nil {
|
|
s.Del("setting:*")
|
|
}
|
|
c, _ := store.Get("__yao.cache")
|
|
if c != nil {
|
|
c.Del("setting:*")
|
|
}
|
|
test.Clean()
|
|
})
|
|
|
|
return setting.Global
|
|
}
|
|
|
|
var systemScope = setting.ScopeID{Scope: setting.ScopeSystem}
|
|
var teamScope = setting.ScopeID{Scope: setting.ScopeTeam, TeamID: "99"}
|
|
var userScope = setting.ScopeID{Scope: setting.ScopeUser, UserID: "42"}
|
|
|
|
func TestSetAndGet(t *testing.T) {
|
|
r := setupRegistry(t)
|
|
|
|
data := map[string]interface{}{
|
|
"theme": "dark",
|
|
"language": "zh-CN",
|
|
"fontSize": float64(14),
|
|
}
|
|
entry, err := r.Set(systemScope, "preferences", data)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "preferences", entry.Namespace)
|
|
assert.Equal(t, systemScope, entry.Scope)
|
|
assert.NotEmpty(t, entry.UpdatedAt)
|
|
|
|
got, err := r.Get(systemScope, "preferences")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "dark", got["theme"])
|
|
assert.Equal(t, "zh-CN", got["language"])
|
|
assert.Equal(t, float64(14), got["fontSize"])
|
|
}
|
|
|
|
func TestGetWithBind(t *testing.T) {
|
|
r := setupRegistry(t)
|
|
|
|
data := map[string]interface{}{
|
|
"default_chat": "gpt-4o",
|
|
"vision_model": "gpt-4o",
|
|
"embedding_enabled": true,
|
|
}
|
|
_, err := r.Set(systemScope, "models", data)
|
|
require.NoError(t, err)
|
|
|
|
type ModelsConfig struct {
|
|
DefaultChat string `json:"default_chat"`
|
|
VisionModel string `json:"vision_model"`
|
|
EmbeddingEnabled bool `json:"embedding_enabled"`
|
|
}
|
|
|
|
var cfg ModelsConfig
|
|
raw, err := r.Get(systemScope, "models", &cfg)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "gpt-4o", raw["default_chat"])
|
|
assert.Equal(t, "gpt-4o", cfg.DefaultChat)
|
|
assert.Equal(t, "gpt-4o", cfg.VisionModel)
|
|
assert.True(t, cfg.EmbeddingEnabled)
|
|
}
|
|
|
|
func TestGetMergedWithBind(t *testing.T) {
|
|
r := setupRegistry(t)
|
|
|
|
_, err := r.Set(systemScope, "prefs", map[string]interface{}{
|
|
"theme": "dark", "lang": "zh-CN", "font_size": float64(14),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = r.Set(teamScope, "prefs", map[string]interface{}{
|
|
"lang": "en-US",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = r.Set(userScope, "prefs", map[string]interface{}{
|
|
"theme": "light",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
type Prefs struct {
|
|
Theme string `json:"theme"`
|
|
Lang string `json:"lang"`
|
|
FontSize float64 `json:"font_size"`
|
|
}
|
|
|
|
var p Prefs
|
|
_, err = r.GetMerged("42", "99", "prefs", &p)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "light", p.Theme)
|
|
assert.Equal(t, "en-US", p.Lang)
|
|
assert.Equal(t, float64(14), p.FontSize)
|
|
}
|
|
|
|
func TestGetNotFound(t *testing.T) {
|
|
r := setupRegistry(t)
|
|
_, err := r.Get(systemScope, "nonexistent")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not found")
|
|
}
|
|
|
|
func TestGetMerged(t *testing.T) {
|
|
r := setupRegistry(t)
|
|
|
|
_, err := r.Set(systemScope, "theme", map[string]interface{}{
|
|
"primary": "blue", "dark_mode": true, "font": "inter",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = r.Set(
|
|
setting.ScopeID{Scope: setting.ScopeTeam, TeamID: "t1"},
|
|
"theme",
|
|
map[string]interface{}{"dark_mode": false},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
_, err = r.Set(
|
|
setting.ScopeID{Scope: setting.ScopeUser, UserID: "u1"},
|
|
"theme",
|
|
map[string]interface{}{"primary": "red"},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
merged, err := r.GetMerged("u1", "t1", "theme")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "red", merged["primary"])
|
|
assert.Equal(t, false, merged["dark_mode"])
|
|
assert.Equal(t, "inter", merged["font"])
|
|
}
|
|
|
|
func TestGetMergedPartial(t *testing.T) {
|
|
r := setupRegistry(t)
|
|
|
|
_, err := r.Set(systemScope, "partial", map[string]interface{}{"a": "1", "b": "2"})
|
|
require.NoError(t, err)
|
|
|
|
// Only system + user, no team data
|
|
_, err = r.Set(userScope, "partial", map[string]interface{}{"b": "override"})
|
|
require.NoError(t, err)
|
|
|
|
merged, err := r.GetMerged("42", "", "partial")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "1", merged["a"])
|
|
assert.Equal(t, "override", merged["b"])
|
|
}
|
|
|
|
func TestGetMergedNoData(t *testing.T) {
|
|
r := setupRegistry(t)
|
|
_, err := r.GetMerged("42", "99", "nothing")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "no data found")
|
|
}
|
|
|
|
func TestSetOverwrite(t *testing.T) {
|
|
r := setupRegistry(t)
|
|
|
|
_, err := r.Set(systemScope, "overwrite", map[string]interface{}{"a": "1"})
|
|
require.NoError(t, err)
|
|
|
|
_, err = r.Set(systemScope, "overwrite", map[string]interface{}{"a": "2", "b": "3"})
|
|
require.NoError(t, err)
|
|
|
|
got, err := r.Get(systemScope, "overwrite")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "2", got["a"])
|
|
assert.Equal(t, "3", got["b"])
|
|
}
|
|
|
|
func TestSetEmptyNamespace(t *testing.T) {
|
|
r := setupRegistry(t)
|
|
_, err := r.Set(systemScope, "", map[string]interface{}{"a": "1"})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "namespace is required")
|
|
}
|
|
|
|
func TestDelete(t *testing.T) {
|
|
r := setupRegistry(t)
|
|
|
|
_, err := r.Set(systemScope, "to-delete", map[string]interface{}{"x": "y"})
|
|
require.NoError(t, err)
|
|
|
|
err = r.Delete(systemScope, "to-delete")
|
|
require.NoError(t, err)
|
|
|
|
_, err = r.Get(systemScope, "to-delete")
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestDeleteNotFound(t *testing.T) {
|
|
r := setupRegistry(t)
|
|
err := r.Delete(systemScope, "no-such-ns")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not found")
|
|
}
|
|
|
|
func TestListNamespaces(t *testing.T) {
|
|
r := setupRegistry(t)
|
|
|
|
_, err := r.Set(systemScope, "ns-a", map[string]interface{}{"v": 1})
|
|
require.NoError(t, err)
|
|
_, err = r.Set(systemScope, "ns-b", map[string]interface{}{"v": 2})
|
|
require.NoError(t, err)
|
|
_, err = r.Set(teamScope, "ns-c", map[string]interface{}{"v": 3})
|
|
require.NoError(t, err)
|
|
|
|
sysNS, err := r.ListNamespaces(systemScope)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, sysNS, "ns-a")
|
|
assert.Contains(t, sysNS, "ns-b")
|
|
assert.NotContains(t, sysNS, "ns-c")
|
|
|
|
teamNS, err := r.ListNamespaces(teamScope)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, teamNS, "ns-c")
|
|
}
|
|
|
|
func TestMultipleNamespaces(t *testing.T) {
|
|
r := setupRegistry(t)
|
|
|
|
_, err := r.Set(userScope, "alpha", map[string]interface{}{"color": "red"})
|
|
require.NoError(t, err)
|
|
_, err = r.Set(userScope, "beta", map[string]interface{}{"color": "blue"})
|
|
require.NoError(t, err)
|
|
|
|
a, err := r.Get(userScope, "alpha")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "red", a["color"])
|
|
|
|
b, err := r.Get(userScope, "beta")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "blue", b["color"])
|
|
}
|
|
|
|
func TestScopeIsolation(t *testing.T) {
|
|
r := setupRegistry(t)
|
|
|
|
_, err := r.Set(systemScope, "shared", map[string]interface{}{"level": "system"})
|
|
require.NoError(t, err)
|
|
_, err = r.Set(teamScope, "shared", map[string]interface{}{"level": "team"})
|
|
require.NoError(t, err)
|
|
_, err = r.Set(userScope, "shared", map[string]interface{}{"level": "user"})
|
|
require.NoError(t, err)
|
|
|
|
sys, err := r.Get(systemScope, "shared")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "system", sys["level"])
|
|
|
|
team, err := r.Get(teamScope, "shared")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "team", team["level"])
|
|
|
|
user, err := r.Get(userScope, "shared")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "user", user["level"])
|
|
}
|
|
|
|
func TestReload(t *testing.T) {
|
|
r := setupRegistry(t)
|
|
|
|
_, err := r.Set(systemScope, "reload-test", map[string]interface{}{"k": "v"})
|
|
require.NoError(t, err)
|
|
|
|
c, _ := store.Get("__yao.cache")
|
|
if c != nil {
|
|
c.Del("setting:*")
|
|
}
|
|
|
|
err = r.Reload()
|
|
require.NoError(t, err)
|
|
|
|
got, err := r.Get(systemScope, "reload-test")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "v", got["k"])
|
|
}
|
|
|
|
func TestConcurrency(t *testing.T) {
|
|
r := setupRegistry(t)
|
|
|
|
var wg sync.WaitGroup
|
|
errCh := make(chan error, 30)
|
|
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
ns := fmt.Sprintf("conc-%d", idx)
|
|
_, err := r.Set(systemScope, ns, map[string]interface{}{"idx": idx})
|
|
if err != nil {
|
|
errCh <- err
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
ns := fmt.Sprintf("conc-%d", idx)
|
|
_, err := r.Get(systemScope, ns)
|
|
if err != nil {
|
|
errCh <- err
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
ns := fmt.Sprintf("conc-%d", idx)
|
|
if err := r.Delete(systemScope, ns); err != nil {
|
|
errCh <- err
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
close(errCh)
|
|
|
|
for err := range errCh {
|
|
t.Errorf("concurrent operation error: %v", err)
|
|
}
|
|
}
|