The config command group manages the config file: config init - write default config (moved from top-level init) config edit - open the config in $EDITOR (falls back to vi) config get - print a value by dotted YAML path (s3.bucket) config set - set a scalar value by dotted YAML path get/set operate on the yaml.Node tree so comments and formatting in the config file are preserved across edits. set creates intermediate maps as needed.
146 lines
3.5 KiB
Go
146 lines
3.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.eeqj.de/sneak/vaultik/internal/config"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// TestDefaultConfigTemplateParses ensures the init template is valid YAML
|
|
// that unmarshals into the Config struct with the expected snapshots.
|
|
func TestDefaultConfigTemplateParses(t *testing.T) {
|
|
var cfg config.Config
|
|
if err := yaml.Unmarshal([]byte(defaultConfigTemplate), &cfg); err != nil {
|
|
t.Fatalf("default config template is not valid YAML: %v", err)
|
|
}
|
|
|
|
if len(cfg.AgeRecipients) != 1 {
|
|
t.Errorf("expected 1 placeholder age recipient, got %d", len(cfg.AgeRecipients))
|
|
}
|
|
|
|
home, ok := cfg.Snapshots["home"]
|
|
if !ok {
|
|
t.Fatal("expected 'home' snapshot in default config")
|
|
}
|
|
if len(home.Paths) == 0 {
|
|
t.Error("home snapshot should have at least one path")
|
|
}
|
|
if len(home.Exclude) == 0 {
|
|
t.Error("home snapshot should have exclude patterns")
|
|
}
|
|
|
|
apps, ok := cfg.Snapshots["apps"]
|
|
if !ok {
|
|
t.Fatal("expected 'apps' snapshot in default config")
|
|
}
|
|
if len(apps.Paths) != 1 || apps.Paths[0] != "/Applications" {
|
|
t.Errorf("apps snapshot should back up /Applications, got %v", apps.Paths)
|
|
}
|
|
if len(apps.Exclude) == 0 {
|
|
t.Error("apps snapshot should have exclude patterns")
|
|
}
|
|
}
|
|
|
|
const testYAML = `# top comment
|
|
compression_level: 3
|
|
s3:
|
|
bucket: oldbucket # inline comment
|
|
region: us-east-1
|
|
snapshots:
|
|
home:
|
|
paths:
|
|
- "~"
|
|
`
|
|
|
|
func parseTestYAML(t *testing.T) *yaml.Node {
|
|
t.Helper()
|
|
var root yaml.Node
|
|
if err := yaml.Unmarshal([]byte(testYAML), &root); err != nil {
|
|
t.Fatalf("parsing test yaml: %v", err)
|
|
}
|
|
return &root
|
|
}
|
|
|
|
func TestYAMLPathGet(t *testing.T) {
|
|
root := parseTestYAML(t)
|
|
|
|
tests := []struct {
|
|
path string
|
|
want string
|
|
err bool
|
|
}{
|
|
{"compression_level", "3", false},
|
|
{"s3.bucket", "oldbucket", false},
|
|
{"s3.region", "us-east-1", false},
|
|
{"s3.nonexistent", "", true},
|
|
{"nonexistent", "", true},
|
|
{"compression_level.sub", "", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.path, func(t *testing.T) {
|
|
node, err := yamlPathGet(root, splitPath(tt.path))
|
|
if tt.err {
|
|
if err == nil {
|
|
t.Fatalf("expected error for %q", tt.path)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if node.Value != tt.want {
|
|
t.Errorf("get %q = %q, want %q", tt.path, node.Value, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestYAMLPathSet(t *testing.T) {
|
|
root := parseTestYAML(t)
|
|
|
|
// Overwrite existing nested value
|
|
if err := yamlPathSet(root, splitPath("s3.bucket"), "newbucket"); err != nil {
|
|
t.Fatalf("set s3.bucket: %v", err)
|
|
}
|
|
|
|
// Create new nested key with intermediate map
|
|
if err := yamlPathSet(root, splitPath("s3.endpoint"), "s3.example.com"); err != nil {
|
|
t.Fatalf("set s3.endpoint: %v", err)
|
|
}
|
|
if err := yamlPathSet(root, splitPath("newmap.newkey"), "val"); err != nil {
|
|
t.Fatalf("set newmap.newkey: %v", err)
|
|
}
|
|
|
|
// Round-trip and verify values + comment preservation
|
|
out, err := yaml.Marshal(root)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
text := string(out)
|
|
|
|
for _, want := range []string{"newbucket", "s3.example.com", "newkey: val", "# top comment", "# inline comment"} {
|
|
if !contains(text, want) {
|
|
t.Errorf("round-tripped YAML missing %q:\n%s", want, text)
|
|
}
|
|
}
|
|
|
|
got, err := yamlPathGet(root, splitPath("s3.bucket"))
|
|
if err != nil {
|
|
t.Fatalf("get after set: %v", err)
|
|
}
|
|
if got.Value != "newbucket" {
|
|
t.Errorf("s3.bucket = %q after set, want newbucket", got.Value)
|
|
}
|
|
}
|
|
|
|
func splitPath(s string) []string {
|
|
return strings.Split(s, ".")
|
|
}
|
|
|
|
func contains(haystack, needle string) bool {
|
|
return strings.Contains(haystack, needle)
|
|
}
|