Refuse config keys that no setting has (closes #419)
check / check (push) Successful in 14m3s
check / check (push) Successful in 14m3s
A misspelled or unknown config key was accepted silently, so a typo in exclude backed up everything it meant to leave out. Loading now refuses a key that no setting has, at any depth, naming its dotted path and, when it is in the file, its line; keys brought in by ${...} references are checked too. config set refuses an unknown key. A top-level env: section stays accepted, since the config library uses it. Anchors that contain themselves fail with an error.
Model: opus-5-5
This commit was merged in pull request #443.
This commit is contained in:
1 parent
028a166256
commit
d8d13d1c8b
6 files changed
+462
-47
No files matched your search
@@ -0,0 +1,163 @@
|
||||
package config //nolint:testpackage // checks the unexported errUnknownKey
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// keysTestConfig loads; each case below adds one line to it.
|
||||
const keysTestConfig = "storage_url: file:///tmp/vaultik-test-storage\n" +
|
||||
"s3:\n" +
|
||||
" region: us-east-1\n" +
|
||||
"snapshots:\n" +
|
||||
" test:\n" +
|
||||
" paths: [/tmp/vaultik-test-source]\n"
|
||||
|
||||
// TestParseRefusesUnknownKey checks that a key no setting has, at any
|
||||
// depth, fails the parse with an error naming the key and its line. A key
|
||||
// that a ${...} reference brings in is not in the file, so its error gives
|
||||
// no line.
|
||||
func TestParseRefusesUnknownKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
snapshotPath := filepath.Join(t.TempDir(), "snapshot.yml")
|
||||
|
||||
err := os.WriteFile(snapshotPath, []byte("other:\n"+
|
||||
" paths: [/tmp/vaultik-test-source]\n"+
|
||||
" exlude: [/tmp/cache]\n"), 0o600)
|
||||
if err != nil {
|
||||
t.Fatalf("write snapshot: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
yaml string
|
||||
want string
|
||||
}{
|
||||
{"top level", keysTestConfig + "compression_lvl: 9\n",
|
||||
`unknown config key "compression_lvl" on line 7`},
|
||||
{"in a snapshot", keysTestConfig + " exlude: [/tmp/cache]\n",
|
||||
`unknown config key "snapshots.test.exlude" on line 7`},
|
||||
{"in s3", strings.Replace(keysTestConfig, " region:",
|
||||
" bukcet: b\n region:", 1),
|
||||
`unknown config key "s3.bukcet" on line 3`},
|
||||
{"from a reference",
|
||||
keysTestConfig + " other: ${YAML:" + snapshotPath + ":other}\n",
|
||||
`unknown config key "snapshots.other.exlude"`},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := Parse([]byte(tt.yaml))
|
||||
if !errors.Is(err, errUnknownKey) {
|
||||
t.Fatalf("Parse() error = %v, want errUnknownKey", err)
|
||||
}
|
||||
|
||||
if err.Error() != tt.want {
|
||||
t.Errorf("Parse() error = %q, want %q", err.Error(), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseRefusesAnchorThatContainsItself checks that a YAML merge key (<<)
|
||||
// naming the anchor of the map it sits in fails the parse with an error.
|
||||
func TestParseRefusesAnchorThatContainsItself(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := strings.Replace(keysTestConfig, " test:", " test: &test", 1) +
|
||||
" <<: *test\n"
|
||||
|
||||
_, err := Parse([]byte(data))
|
||||
if !errors.Is(err, errAnchorContainsItself) {
|
||||
t.Fatalf("Parse() error = %v, want errAnchorContainsItself", err)
|
||||
}
|
||||
|
||||
want := `anchor contains itself: "test" on line 7`
|
||||
if err.Error() != want {
|
||||
t.Errorf("Parse() error = %q, want %q", err.Error(), want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseRefusesExcessiveAliasing checks that Parse returns an error for
|
||||
// a file whose merge keys (<<) fan out. Each of eight snapshots merges the
|
||||
// one before it ten times. A key check that followed every alias would walk
|
||||
// the first snapshot over 10^8 times and run past the package's test
|
||||
// timeout.
|
||||
func TestParseRefusesExcessiveAliasing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const snapshots, merges = 8, 10
|
||||
|
||||
var data strings.Builder
|
||||
|
||||
data.WriteString(strings.Replace(keysTestConfig, " test:", " test: &s0", 1))
|
||||
|
||||
for i := 1; i <= snapshots; i++ {
|
||||
aliases := slices.Repeat([]string{"*s" + strconv.Itoa(i-1)}, merges)
|
||||
fmt.Fprintf(&data, " s%d: &s%d\n <<: [%s]\n",
|
||||
i, i, strings.Join(aliases, ", "))
|
||||
}
|
||||
|
||||
_, err := Parse([]byte(data.String()))
|
||||
if err == nil {
|
||||
t.Fatal("Parse() returned no error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseAcceptsKnownKeys checks that a config with a ${...} reference
|
||||
// and a YAML merge key (<<) still parses.
|
||||
func TestParseAcceptsKnownKeys(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
levelPath := filepath.Join(t.TempDir(), "level")
|
||||
|
||||
err := os.WriteFile(levelPath, []byte("9"), 0o600)
|
||||
if err != nil {
|
||||
t.Fatalf("write level: %v", err)
|
||||
}
|
||||
|
||||
data := strings.Replace(keysTestConfig, " test:", " test: &test", 1) +
|
||||
" other:\n" +
|
||||
" <<: *test\n" +
|
||||
" exclude: [/tmp/vaultik-test-source/cache]\n" +
|
||||
"compression_level: ${FILE:" + levelPath + "}\n"
|
||||
|
||||
cfg, err := Parse([]byte(data))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse() unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.CompressionLevel != 9 {
|
||||
t.Errorf("compression_level = %d, want 9", cfg.CompressionLevel)
|
||||
}
|
||||
|
||||
other := cfg.Snapshots["other"]
|
||||
if len(other.Paths) != 1 || len(other.Exclude) != 1 {
|
||||
t.Errorf("snapshot other = %+v, want the paths of test and one exclude",
|
||||
other)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseAcceptsEnvSection checks that a config with a top-level env
|
||||
// section still parses. smartconfig sets the environment variables it
|
||||
// lists, so its keys are not settings.
|
||||
func TestParseAcceptsEnvSection(t *testing.T) {
|
||||
const name = "VAULTIK_TEST_ENV_SECTION"
|
||||
|
||||
// Parse sets the variable; t.Setenv restores it after the test.
|
||||
t.Setenv(name, "")
|
||||
|
||||
_, err := Parse([]byte(keysTestConfig + "env:\n " + name + ": set\n"))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse() unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user