Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
package config

import (
"bytes"
"errors"
"flag"
"fmt"
"io"
"os"
"time"

Expand Down Expand Up @@ -286,14 +289,20 @@ func (t Thresholds) validate() error {

// loadYAMLFile merges the YAML file at path into cfg. Only keys present in
// the file override cfg's existing (default) values; absent keys are left
// untouched, since yaml.Unmarshal only writes fields it finds in the
// document.
// untouched, since the decoder only writes fields it finds in the document.
// KnownFields(true) rejects any key that doesn't map to a Config field, so a
// typo (e.g. "api_kay") fails fast at startup instead of silently falling
// back to the default (e.g. no authentication).
func loadYAMLFile(path string, cfg *Config) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read %s: %w", path, err)
}
if err := yaml.Unmarshal(data, cfg); err != nil {
dec := yaml.NewDecoder(bytes.NewReader(data))
dec.KnownFields(true)
// An empty or comment-only file has nothing to decode; Decode reports
// that as io.EOF, but it's not an error here since cfg is left as-is.
if err := dec.Decode(cfg); err != nil && !errors.Is(err, io.EOF) {
return fmt.Errorf("parse %s: %w", path, err)
}
return nil
Expand Down
38 changes: 38 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,44 @@ func TestLoad_MalformedYAML(t *testing.T) {
}
}

func TestLoad_UnknownTopLevelKeyIsError(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.yaml")
// "api_kay" is a typo of "api_key"; it must not be silently ignored,
// since that would leave APIKey at its default (no authentication).
writeFile(t, path, "api_kay: \"secret123\"\n")

_, err := Load([]string{"-config", path})
if err == nil {
t.Fatal("expected error for unknown top-level YAML key")
}
}

func TestLoad_UnknownNestedKeyIsError(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.yaml")
writeFile(t, path, "thresholds:\n temperature_warn_percent: 55\n")

_, err := Load([]string{"-config", path})
if err == nil {
t.Fatal("expected error for unknown nested YAML key")
}
}

func TestLoad_EmptyConfigFileIsNotError(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.yaml")
writeFile(t, path, "# just a comment, no keys\n")

result, err := Load([]string{"-config", path})
if err != nil {
t.Fatalf("Load: %v", err)
}
if !reflect.DeepEqual(result.Config, Default()) {
t.Fatalf("Load with empty/comment-only file = %+v, want defaults %+v", result.Config, Default())
}
}

func TestLoad_VersionFlag(t *testing.T) {
result, err := Load([]string{"-version"})
if err != nil {
Expand Down
Loading