231 lines
9.6 KiB
Go
231 lines
9.6 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type referenceConfig struct {
|
|
Port int `json:"port"`
|
|
BatchSize int `json:"batchSize"`
|
|
Enabled bool `json:"enabled"`
|
|
Password string `json:"password"`
|
|
Address string `json:"address"`
|
|
Timeout time.Duration `json:"timeout"`
|
|
Optional *int `json:"optional"`
|
|
Labels map[string]string `json:"labels"`
|
|
Numbers []int64 `json:"numbers"`
|
|
Dynamic any `json:"dynamic"`
|
|
}
|
|
|
|
func (c referenceConfig) Validate() error {
|
|
if c.Port < 0 {
|
|
return Invalid("port", "must not be negative")
|
|
}
|
|
return nil
|
|
}
|
|
func TestExplicitReferencesAcrossFormats(t *testing.T) {
|
|
// Delimiters, quotes and newlines belong to the credential, never file syntax.
|
|
secret := " DDSFLKJlsf:saf:-123\"\n${DO_NOT_READ}&<>\\ "
|
|
sources := map[string]string{
|
|
"json": `{"port":"${PORT:-3306}","enabled":"${ENABLED:-true}","password":"${PASSWORD}","address":"https://${HOST}:${PORT:-3306}/v1","timeout":"${TIMEOUT:-3s}","optional":"${ZERO}","numbers":["${BIG}"],"dynamic":"${ZERO}","labels":{"literal":"$${NOT_READ}","empty":"${OPTIONAL:-}","dollars":"$$cost"}}`,
|
|
"yaml": "port: \"${PORT:-3306}\"\nenabled: \"${ENABLED:-true}\"\npassword: \"${PASSWORD}\"\naddress: \"https://${HOST}:${PORT:-3306}/v1\"\ntimeout: \"${TIMEOUT:-3s}\"\noptional: \"${ZERO}\"\nnumbers: [\"${BIG}\"]\ndynamic: \"${ZERO}\"\nlabels:\n literal: \"$${NOT_READ}\"\n empty: \"${OPTIONAL:-}\"\n dollars: \"$$cost\"\n",
|
|
"xml": `<config><port>${PORT:-3306}</port><enabled>${ENABLED:-true}</enabled><password>${PASSWORD}</password><address>https://${HOST}:${PORT:-3306}/v1</address><timeout>${TIMEOUT:-3s}</timeout><optional>${ZERO}</optional><numbers type="array"><item>${BIG}</item></numbers><dynamic>${ZERO}</dynamic><labels><literal>$${NOT_READ}</literal><empty>${OPTIONAL:-}</empty><dollars>$$cost</dollars></labels></config>`,
|
|
}
|
|
for ext, source := range sources {
|
|
t.Run(ext, func(t *testing.T) {
|
|
calls := map[string]int{}
|
|
values := map[string]string{"PORT": "0", "ENABLED": "false", "PASSWORD": secret, "HOST": "example.test", "ZERO": "0", "BIG": "9007199254740993"}
|
|
l := testLoader(func(name string) (string, bool) { calls[name]++; v, ok := values[name]; return v, ok })
|
|
got, err := LoadProject(configFile(t, ext, source), referenceConfig{Port: 77, BatchSize: 100, Enabled: true, Password: "old"}, l)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Port != 0 || got.BatchSize != 100 || got.Enabled || got.Password != secret || got.Address != "https://example.test:0/v1" || got.Timeout != 3*time.Second || got.Optional == nil || *got.Optional != 0 || got.Numbers[0] != 9007199254740993 || got.Dynamic != "0" {
|
|
t.Fatal("binding changed values")
|
|
}
|
|
if got.Labels["literal"] != "${NOT_READ}" || got.Labels["empty"] != "" || got.Labels["dollars"] != "$cost" {
|
|
t.Fatal(got.Labels)
|
|
}
|
|
for name, n := range calls {
|
|
if n != 1 {
|
|
t.Fatal("not cached", name, n)
|
|
}
|
|
}
|
|
if calls["DO_NOT_READ"] != 0 || calls["NOT_READ"] != 0 {
|
|
t.Fatal("recursive expansion")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
func TestReferenceFallbackAndFixedValues(t *testing.T) {
|
|
for _, present := range []bool{false, true} {
|
|
l := testLoader(func(string) (string, bool) { return "", present })
|
|
got, err := LoadProject(configFile(t, "yaml", "port: \"${PORT:-3306}\"\npassword: \"${PASSWORD:-DDSFLKJlsf:saf:-123}\""), referenceConfig{}, l)
|
|
if err != nil || got.Port != 3306 || got.Password != "DDSFLKJlsf:saf:-123" {
|
|
t.Fatal(got, err)
|
|
}
|
|
for _, ext := range []string{"json", "yaml", "xml"} {
|
|
raw := `{"password":"${PASSWORD}"}`
|
|
if ext == "yaml" {
|
|
raw = "password: '${PASSWORD}'"
|
|
}
|
|
if ext == "xml" {
|
|
raw = "<config><password>${PASSWORD}</password></config>"
|
|
}
|
|
path := configFile(t, ext, raw)
|
|
_, err = LoadProject(path, referenceConfig{Password: "initial"}, l)
|
|
var d *Error
|
|
if !errors.As(err, &d) || d.Env != "PASSWORD" || d.File != path || d.Field != "password" || d.Line < 1 {
|
|
t.Fatal("missing required-reference diagnostic", err)
|
|
}
|
|
}
|
|
}
|
|
calls := 0
|
|
l := testLoader(func(string) (string, bool) { calls++; return "999", true })
|
|
got, err := LoadProject(configFile(t, "json", `{"port":0,"enabled":false,"password":""}`), referenceConfig{Port: 77, BatchSize: 100, Enabled: true, Password: "old"}, l)
|
|
if err != nil || got.Port != 0 || got.BatchSize != 100 || got.Enabled || got.Password != "" || calls != 0 {
|
|
t.Fatal(got, err, calls)
|
|
}
|
|
got, err = LoadProject(configFile(t, "yaml", "password: '${VALUE}'"), referenceConfig{}, testLoader(func(string) (string, bool) { return " ", true }))
|
|
if err != nil || got.Password != " " {
|
|
t.Fatal("trimmed credential", err)
|
|
}
|
|
}
|
|
func TestReferenceDiagnosticsAndScalarLimits(t *testing.T) {
|
|
for _, value := range []string{"private-not-int", "92233720368547758080", "false", "null", "-1"} {
|
|
path := configFile(t, "yaml", "port: '${PORT}'")
|
|
_, err := LoadProject(path, referenceConfig{}, testLoader(func(string) (string, bool) { return value, true }))
|
|
var d *Error
|
|
if !errors.As(err, &d) || d.File != path || d.Env != "PORT" || d.Field != "port" || d.Line != 1 || d.Column < 1 || strings.Contains(err.Error(), value) {
|
|
t.Fatalf("invalid diagnostic: %v", err)
|
|
}
|
|
}
|
|
for _, raw := range []string{"${BAD-123}", "${}", "${X", "${1BAD}", "${BAD NAME}", "${X:-${Y}}"} {
|
|
encoded, _ := json.Marshal(map[string]string{"password": raw})
|
|
if _, err := LoadProject(configFile(t, "json", string(encoded)), referenceConfig{}, testLoader(noEnvironment)); err == nil {
|
|
t.Fatal("invalid syntax accepted", raw)
|
|
}
|
|
}
|
|
for _, value := range []string{string([]byte{0xff}), strings.Repeat("x", maxConfigBytes+1)} {
|
|
if _, err := LoadProject(configFile(t, "yaml", "password: '${SECRET}'"), referenceConfig{}, testLoader(func(string) (string, bool) { return value, true })); err == nil {
|
|
t.Fatal("unbounded/invalid environment")
|
|
}
|
|
}
|
|
path := configFile(t, "yaml", "labels:\n a: '${BIG}'\n b: '${BIG}'\n c: '${BIG}'\n d: '${BIG}'\n e: '${BIG}'")
|
|
if _, err := LoadProject(path, referenceConfig{}, testLoader(func(string) (string, bool) { return strings.Repeat("x", maxConfigBytes), true })); err == nil {
|
|
t.Fatal("expansion budget bypassed")
|
|
}
|
|
// An environment value cannot inject an array/object or coerce dynamic null.
|
|
for _, raw := range []string{"[1,2]", "{\"x\":1}", "null"} {
|
|
_, err := LoadProject(configFile(t, "yaml", "numbers: '${VALUE}'"), referenceConfig{}, testLoader(func(string) (string, bool) { return raw, true }))
|
|
if err == nil {
|
|
t.Fatal("structure injected")
|
|
}
|
|
}
|
|
}
|
|
func TestReferenceCacheIsolationAndRemovedRules(t *testing.T) {
|
|
path := configFile(t, "yaml", "password: '${TOKEN}'")
|
|
for i := 0; i < 24; i++ {
|
|
t.Run(fmt.Sprint(i), func(t *testing.T) {
|
|
t.Parallel()
|
|
v := fmt.Sprint(i)
|
|
l := testLoader(func(string) (string, bool) { return v, true })
|
|
for n := 0; n < 4; n++ {
|
|
got, err := LoadProject(path, referenceConfig{}, l)
|
|
if err != nil || got.Password != v {
|
|
t.Fatal("load leaked", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
func TestReferenceLookupPerLoadAndConcurrentLoader(t *testing.T) {
|
|
var mu sync.Mutex
|
|
reads := 0
|
|
l := testLoader(func(string) (string, bool) { mu.Lock(); defer mu.Unlock(); reads++; return "secret", true })
|
|
path := configFile(t, "yaml", "password: '${TOKEN}${TOKEN}'")
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 24; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
got, err := LoadProject(path, referenceConfig{}, l)
|
|
if err != nil || got.Password != "secretsecret" {
|
|
t.Error(err)
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
if reads != 24 {
|
|
t.Fatal(reads)
|
|
}
|
|
}
|
|
func TestRemovedEnvTagsAndFieldSuggestion(t *testing.T) {
|
|
type old struct {
|
|
Value int `json:"value" envDefault:"9"`
|
|
}
|
|
if _, err := LoadProject("", old{}); err == nil || !strings.Contains(err.Error(), "removed") {
|
|
t.Fatal(err)
|
|
}
|
|
type oldSecret struct {
|
|
Token string `json:"-" env:"TOKEN"`
|
|
}
|
|
if _, err := DecodeFile(configFile(t, "json", `{}`), oldSecret{}); err == nil {
|
|
t.Fatal("old env tag ignored")
|
|
}
|
|
_, err := LoadProject(configFile(t, "yaml", "batchSiz: 10"), referenceConfig{})
|
|
if err == nil || !strings.Contains(err.Error(), `did you mean "batchSize"`) {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("GO_OMNI_BATCH_SIZE", "999")
|
|
got, err := LoadProject("", referenceConfig{BatchSize: 100})
|
|
if err != nil || got.BatchSize != 100 {
|
|
t.Fatal("implicit overlay", err)
|
|
}
|
|
}
|
|
func FuzzConfigurationReferences(f *testing.F) {
|
|
for _, s := range []string{"${X}", "$${X}", "${X:-DDS:abc:-123}", "${X:-}", "https://${X}/${X}", "${X:-${Y}}"} {
|
|
f.Add(s, " secret${NO}\n\" ")
|
|
}
|
|
f.Fuzz(func(t *testing.T, raw, value string) {
|
|
if len(raw) > 8192 || len(value) > 8192 {
|
|
return
|
|
}
|
|
doc := Document{Value: map[string]any{"password": raw}}
|
|
resolved, err := resolveDocument(doc, func(string) (string, bool) { return value, true })
|
|
if err != nil {
|
|
return
|
|
}
|
|
_, _ = bindDocument(resolved, referenceConfig{})
|
|
})
|
|
}
|
|
func BenchmarkExplicitConfiguration(b *testing.B) {
|
|
for _, refs := range []bool{false, true} {
|
|
b.Run(fmt.Sprint(refs), func(b *testing.B) {
|
|
source := `{"port":3306,"enabled":false,"password":"fixed","batchSize":0}`
|
|
if refs {
|
|
source = `{"port":"${PORT:-3306}","enabled":"${ENABLED:-false}","password":"${PASSWORD}","batchSize":0}`
|
|
}
|
|
path := b.TempDir() + "/config.json"
|
|
if err := os.WriteFile(path, []byte(source), 0600); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
l := testLoader(func(k string) (string, bool) { return "private", k == "PASSWORD" })
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
if _, err := LoadProject(path, referenceConfig{BatchSize: 100}, l); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|