mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 23:56:37 +08:00
fix(config): decode *bool so trailing-slash redirect binds from yaml/env
This commit is contained in:
@@ -180,6 +180,13 @@ func formatEntryValue(value any, secret bool) string {
|
|||||||
if value == nil {
|
if value == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
rv := reflect.ValueOf(value)
|
||||||
|
if rv.Kind() == reflect.Ptr {
|
||||||
|
if rv.IsNil() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprint(rv.Elem().Interface())
|
||||||
|
}
|
||||||
return fmt.Sprint(value)
|
return fmt.Sprint(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -224,6 +224,54 @@ func TestEntriesRedactSecretsAndReportDefaults(t *testing.T) {
|
|||||||
assert.Equal(t, "86400", entries["app.session_age"].Value)
|
assert.Equal(t, "86400", entries["app.session_age"].Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type optionalBoolConfig struct {
|
||||||
|
RedirectTrailingSlash *bool `config:"redirect_trailing_slash" env:"APP_REDIRECT_TRAILING_SLASH"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindBoolPointerFromFileAndEnv(t *testing.T) {
|
||||||
|
t.Run("absent stays nil", func(t *testing.T) {
|
||||||
|
r := extpoints.NewConfigRegistry(newFakeSource())
|
||||||
|
require.NoError(t, r.Declare("driver_http", extpoints.ConfigBinding{Prefix: "app", Target: &optionalBoolConfig{}}))
|
||||||
|
require.NoError(t, r.Resolve())
|
||||||
|
|
||||||
|
var got optionalBoolConfig
|
||||||
|
require.NoError(t, r.Bind("app", &got))
|
||||||
|
assert.Nil(t, got.RedirectTrailingSlash)
|
||||||
|
assert.Equal(t, "", r.Origin("app.redirect_trailing_slash"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("file false", func(t *testing.T) {
|
||||||
|
src := newFakeSource()
|
||||||
|
src.values["app.redirect_trailing_slash"] = false
|
||||||
|
|
||||||
|
r := extpoints.NewConfigRegistry(src)
|
||||||
|
require.NoError(t, r.Declare("driver_http", extpoints.ConfigBinding{Prefix: "app", Target: &optionalBoolConfig{}}))
|
||||||
|
require.NoError(t, r.Resolve())
|
||||||
|
|
||||||
|
var got optionalBoolConfig
|
||||||
|
require.NoError(t, r.Bind("app", &got))
|
||||||
|
require.NotNil(t, got.RedirectTrailingSlash)
|
||||||
|
assert.False(t, *got.RedirectTrailingSlash)
|
||||||
|
assert.Equal(t, extpoints.OriginFile, r.Origin("app.redirect_trailing_slash"))
|
||||||
|
assert.False(t, r.Bool("app.redirect_trailing_slash", true))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("env false", func(t *testing.T) {
|
||||||
|
src := newFakeSource()
|
||||||
|
src.env["APP_REDIRECT_TRAILING_SLASH"] = "false"
|
||||||
|
|
||||||
|
r := extpoints.NewConfigRegistry(src)
|
||||||
|
require.NoError(t, r.Declare("driver_http", extpoints.ConfigBinding{Prefix: "app", Target: &optionalBoolConfig{}}))
|
||||||
|
require.NoError(t, r.Resolve())
|
||||||
|
|
||||||
|
var got optionalBoolConfig
|
||||||
|
require.NoError(t, r.Bind("app", &got))
|
||||||
|
require.NotNil(t, got.RedirectTrailingSlash)
|
||||||
|
assert.False(t, *got.RedirectTrailingSlash)
|
||||||
|
assert.Equal(t, extpoints.OriginEnv, r.Origin("app.redirect_trailing_slash"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestBindRejectsReadsBeforeSourceIsRegistered(t *testing.T) {
|
func TestBindRejectsReadsBeforeSourceIsRegistered(t *testing.T) {
|
||||||
r := extpoints.NewConfigRegistry(nil)
|
r := extpoints.NewConfigRegistry(nil)
|
||||||
require.NoError(t, r.Declare("cache", extpoints.ConfigBinding{Prefix: "redis", Target: &redisConfig{}}))
|
require.NoError(t, r.Declare("cache", extpoints.ConfigBinding{Prefix: "redis", Target: &redisConfig{}}))
|
||||||
|
|||||||
@@ -33,15 +33,38 @@ func convertValue(raw any, typ reflect.Type) (any, error) {
|
|||||||
return convertSlice(raw, typ)
|
return convertSlice(raw, typ)
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
return convertStruct(raw, typ)
|
return convertStruct(raw, typ)
|
||||||
|
case reflect.Ptr:
|
||||||
|
return convertPointer(raw, typ)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("%w: %s is not a supported configuration type", ErrConfigType, typ)
|
return nil, fmt.Errorf("%w: %s is not a supported configuration type", ErrConfigType, typ)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convertPointer decodes into the element type and returns a non-nil pointer to it.
|
||||||
|
// Nested pointers are rejected so configuration tags stay one level deep.
|
||||||
|
func convertPointer(raw any, typ reflect.Type) (any, error) {
|
||||||
|
elemType := typ.Elem()
|
||||||
|
if elemType.Kind() == reflect.Ptr {
|
||||||
|
return nil, fmt.Errorf("%w: %s is not a supported configuration type", ErrConfigType, typ)
|
||||||
|
}
|
||||||
|
elem, err := convertValue(raw, elemType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ptr := reflect.New(elemType)
|
||||||
|
ptr.Elem().Set(reflect.ValueOf(elem))
|
||||||
|
return ptr.Interface(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func convertBool(raw any) (any, error) {
|
func convertBool(raw any) (any, error) {
|
||||||
switch v := raw.(type) {
|
switch v := raw.(type) {
|
||||||
case bool:
|
case bool:
|
||||||
return v, nil
|
return v, nil
|
||||||
|
case *bool:
|
||||||
|
if v == nil {
|
||||||
|
return nil, fmt.Errorf("%w: nil *bool is not a boolean", ErrConfigType)
|
||||||
|
}
|
||||||
|
return *v, nil
|
||||||
case string:
|
case string:
|
||||||
parsed, err := strconv.ParseBool(strings.TrimSpace(v))
|
parsed, err := strconv.ParseBool(strings.TrimSpace(v))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,7 +3,11 @@
|
|||||||
|
|
||||||
package driver_http
|
package driver_http
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"Wavelet/core/extpoints"
|
||||||
|
)
|
||||||
|
|
||||||
func TestBuildEngineDefaultRedirectsTrailingSlash(t *testing.T) {
|
func TestBuildEngineDefaultRedirectsTrailingSlash(t *testing.T) {
|
||||||
eng, err := BuildEngineWithConfig(httpAppConfig{}, httpRedisConfig{})
|
eng, err := BuildEngineWithConfig(httpAppConfig{}, httpRedisConfig{})
|
||||||
@@ -25,4 +29,85 @@ func TestBuildEngineCanDisableRedirectTrailingSlash(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBindAppConfigDefaultKeepsTrailingSlashRedirect(t *testing.T) {
|
||||||
|
cfg := bindAppConfig(t, map[string]any{}, map[string]string{})
|
||||||
|
if cfg.RedirectTrailingSlash != nil {
|
||||||
|
t.Fatal("absent redirect_trailing_slash must leave *bool nil")
|
||||||
|
}
|
||||||
|
eng, err := BuildEngineWithConfig(cfg, httpRedisConfig{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !eng.RedirectTrailingSlash {
|
||||||
|
t.Fatal("default RedirectTrailingSlash must be true after Bind")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindAppConfigCanDisableTrailingSlashRedirect(t *testing.T) {
|
||||||
|
cfg := bindAppConfig(t, map[string]any{"app.redirect_trailing_slash": false}, nil)
|
||||||
|
if cfg.RedirectTrailingSlash == nil || *cfg.RedirectTrailingSlash {
|
||||||
|
t.Fatal("yaml false must bind *false")
|
||||||
|
}
|
||||||
|
eng, err := BuildEngineWithConfig(cfg, httpRedisConfig{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if eng.RedirectTrailingSlash {
|
||||||
|
t.Fatal("RedirectTrailingSlash must honor bound false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindAppConfigEnvCanDisableTrailingSlashRedirect(t *testing.T) {
|
||||||
|
cfg := bindAppConfig(t, nil, map[string]string{"APP_REDIRECT_TRAILING_SLASH": "false"})
|
||||||
|
if cfg.RedirectTrailingSlash == nil || *cfg.RedirectTrailingSlash {
|
||||||
|
t.Fatal("env false must bind *false")
|
||||||
|
}
|
||||||
|
eng, err := BuildEngineWithConfig(cfg, httpRedisConfig{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if eng.RedirectTrailingSlash {
|
||||||
|
t.Fatal("RedirectTrailingSlash must honor env-bound false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type slashConfigSource struct {
|
||||||
|
values map[string]any
|
||||||
|
env map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s slashConfigSource) Lookup(path string) (any, bool) {
|
||||||
|
v, ok := s.values[path]
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s slashConfigSource) LookupEnv(name string) (string, bool) {
|
||||||
|
v, ok := s.env[name]
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s slashConfigSource) Describe() string { return "slash-test" }
|
||||||
|
|
||||||
|
func bindAppConfig(t *testing.T, values map[string]any, env map[string]string) httpAppConfig {
|
||||||
|
t.Helper()
|
||||||
|
if values == nil {
|
||||||
|
values = map[string]any{}
|
||||||
|
}
|
||||||
|
if env == nil {
|
||||||
|
env = map[string]string{}
|
||||||
|
}
|
||||||
|
r := extpoints.NewConfigRegistry(slashConfigSource{values: values, env: env})
|
||||||
|
if err := r.Declare("driver_http", extpoints.ConfigBinding{Prefix: "app", Target: &httpAppConfig{}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := r.Resolve(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var cfg httpAppConfig
|
||||||
|
if err := r.Bind("app", &cfg); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
func boolPtr(v bool) *bool { return &v }
|
func boolPtr(v bool) *bool { return &v }
|
||||||
|
|||||||
Reference in New Issue
Block a user