diff options
Diffstat (limited to 'internal/config')
| -rw-r--r-- | internal/config/config.go | 13 | ||||
| -rw-r--r-- | internal/config/correct_empty_config_test.go | 50 | ||||
| -rw-r--r-- | internal/config/hardening_config_known_fields_test.go | 66 |
3 files changed, 128 insertions, 1 deletions
diff --git a/internal/config/config.go b/internal/config/config.go index 362c9c1..852a6d6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,6 +3,9 @@ package config import ( + "bytes" + "errors" + "io" "os" "gopkg.in/yaml.v3" @@ -242,7 +245,15 @@ func Load(path string) (*Config, error) { return nil, err } cfg := Default() - if err := yaml.Unmarshal(data, cfg); err != nil { + // KnownFields makes a misspelled key (e.g. telnet_listen) a hard error + // instead of silently leaving the default in place. ColorsConfig is a map, + // so arbitrary color keys remain allowed. + dec := yaml.NewDecoder(bytes.NewReader(data)) + dec.KnownFields(true) + // An empty or comment-only file yields io.EOF, which means "no keys set" + // rather than a broken config: the defaults above stay in effect. A real + // syntax/unknown-field error is still returned. + if err := dec.Decode(cfg); err != nil && !errors.Is(err, io.EOF) { return nil, err } return cfg, nil diff --git a/internal/config/correct_empty_config_test.go b/internal/config/correct_empty_config_test.go new file mode 100644 index 0000000..27d39da --- /dev/null +++ b/internal/config/correct_empty_config_test.go @@ -0,0 +1,50 @@ +package config + +import ( + "os" + "path/filepath" + "testing" +) + +// Regression: Load used yaml.Decoder.Decode for KnownFields, which returns +// io.EOF for an empty or comment-only config.yaml. cmd/thoi treats any non- +// IsNotExist error as fatal, so an empty config aborted startup instead of +// booting on defaults (yaml.Unmarshal had returned nil). + +func writeEmptyConfig(t *testing.T, body string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "config.yaml") + if err := os.WriteFile(path, []byte(body), 0o644); err != nil { + t.Fatal(err) + } + return path +} + +func TestLoadEmptyFileBootsOnDefaults(t *testing.T) { + cfg, err := Load(writeEmptyConfig(t, "")) + if err != nil { + t.Fatalf("empty config.yaml must load on defaults, got: %v", err) + } + if cfg.TickLength != Default().TickLength { + t.Errorf("TickLength = %d, want default %d", cfg.TickLength, Default().TickLength) + } + if cfg.Telnet.Port != Default().Telnet.Port { + t.Errorf("Telnet.Port = %d, want default %d", cfg.Telnet.Port, Default().Telnet.Port) + } +} + +func TestLoadCommentOnlyFileBootsOnDefaults(t *testing.T) { + cfg, err := Load(writeEmptyConfig(t, "# nothing configured yet\n\n# ports come from defaults\n")) + if err != nil { + t.Fatalf("comment-only config.yaml must load on defaults, got: %v", err) + } + if cfg.StartingRoom != Default().StartingRoom { + t.Errorf("StartingRoom = %d, want default %d", cfg.StartingRoom, Default().StartingRoom) + } +} + +func TestLoadEmptyFileStillRejectsUnknownKey(t *testing.T) { + if _, err := Load(writeEmptyConfig(t, "not_a_real_key: 1\n")); err == nil { + t.Fatal("unknown key must still be rejected after the io.EOF fix") + } +} diff --git a/internal/config/hardening_config_known_fields_test.go b/internal/config/hardening_config_known_fields_test.go new file mode 100644 index 0000000..be00aa9 --- /dev/null +++ b/internal/config/hardening_config_known_fields_test.go @@ -0,0 +1,66 @@ +package config + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// Regression: config.Load used plain yaml.Unmarshal, so an unknown/misspelled +// key (e.g. telnet_listen) was silently discarded and the default applied. +// It now uses KnownFields(true), matching the data loaders, while +// SaveDefault's output must still round-trip through Load. + +func writeConfig(t *testing.T, body string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "config.yaml") + if err := os.WriteFile(path, []byte(body), 0o644); err != nil { + t.Fatal(err) + } + return path +} + +func TestLoadRejectsUnknownKey(t *testing.T) { + path := writeConfig(t, "tick_length: 700\ntelnet_listen: 4000\n") + if _, err := Load(path); err == nil { + t.Fatal("expected an error for unknown key telnet_listen, got nil") + } else if !strings.Contains(err.Error(), "telnet_listen") { + t.Fatalf("error should name the offending key, got: %v", err) + } +} + +func TestLoadKnownKeysOverrideDefaults(t *testing.T) { + path := writeConfig(t, "tick_length: 700\nstarting_room: 2002\nstartup_validation:\n root_rooms: [2002]\n") + cfg, err := Load(path) + if err != nil { + t.Fatalf("load: %v", err) + } + if cfg.TickLength != 700 { + t.Errorf("TickLength = %d, want 700", cfg.TickLength) + } + if cfg.StartingRoom != 2002 { + t.Errorf("StartingRoom = %d, want 2002", cfg.StartingRoom) + } + if len(cfg.StartupValidation.RootRooms) != 1 || cfg.StartupValidation.RootRooms[0] != 2002 { + t.Errorf("RootRooms = %v, want [2002]", cfg.StartupValidation.RootRooms) + } + // Unset values keep their defaults. + if cfg.Telnet.Port != Default().Telnet.Port { + t.Errorf("unset Telnet.Port = %d, want default %d", cfg.Telnet.Port, Default().Telnet.Port) + } +} + +func TestSaveDefaultRoundTrips(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.yaml") + if err := SaveDefault(path); err != nil { + t.Fatalf("SaveDefault: %v", err) + } + cfg, err := Load(path) + if err != nil { + t.Fatalf("Load(SaveDefault output): %v", err) + } + if cfg.TickLength != Default().TickLength { + t.Errorf("TickLength = %d, want %d", cfg.TickLength, Default().TickLength) + } +} |
