diff --git a/go.mod b/go.mod index 26d51c0b..a3013b38 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/grandcat/zeroconf v1.0.0 github.com/iancoleman/strcase v0.3.0 github.com/joshuar/go-hass-anything/v12 v12.1.0 + github.com/knadh/koanf/providers/structs v0.1.0 github.com/lthibault/jitterbug/v2 v2.2.2 github.com/lxzan/gws v1.8.8 github.com/magefile/mage v1.15.0 @@ -25,6 +26,7 @@ require ( golang.org/x/text v0.21.0 golang.org/x/tools v0.28.0 gopkg.in/yaml.v3 v3.0.1 + ) require ( @@ -47,6 +49,7 @@ require ( github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/dolthub/maphash v0.1.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/fatih/structs v1.1.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-git/go-git/v5 v5.12.0 // indirect @@ -134,7 +137,6 @@ require ( github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/knadh/koanf/parsers/toml v0.1.0 - github.com/knadh/koanf/providers/env v1.0.0 github.com/knadh/koanf/providers/file v1.1.2 github.com/knadh/koanf/v2 v2.1.2 github.com/leodido/go-urn v1.4.0 // indirect diff --git a/go.sum b/go.sum index 97035ed0..90ebc028 100644 --- a/go.sum +++ b/go.sum @@ -146,6 +146,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -374,10 +376,10 @@ github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NI github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI= github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18= -github.com/knadh/koanf/providers/env v1.0.0 h1:ufePaI9BnWH+ajuxGGiJ8pdTG0uLEUWC7/HDDPGLah0= -github.com/knadh/koanf/providers/env v1.0.0/go.mod h1:mzFyRZueYhb37oPmC1HAv/oGEEuyvJDA98r3XAa8Gak= github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w= github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= +github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSdntfdyIbbCzEyE0= +github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmBzWn1h8Nt9O6EP/91MkcWE= github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ= github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= diff --git a/internal/preferences/hass.go b/internal/preferences/hass.go index f7211569..fa13c4b2 100644 --- a/internal/preferences/hass.go +++ b/internal/preferences/hass.go @@ -35,7 +35,7 @@ type Hass struct { Secret string `toml:"secret,omitempty" json:"secret" validate:"omitempty,ascii"` WebhookID string `toml:"webhook_id" json:"webhook_id" validate:"required,ascii"` RestAPIURL string `toml:"apiurl,omitempty" json:"-" validate:"required_without=CloudhookURL RemoteUIURL,http_url"` - WebsocketURL string `toml:"websocketurl" json:"-" validate:"required,url"` + WebsocketURL string `toml:"websocketurl" json:"-" validate:"required,uri"` } var ( diff --git a/internal/preferences/prefs.go b/internal/preferences/prefs.go index 8a97fc80..7c69892f 100644 --- a/internal/preferences/prefs.go +++ b/internal/preferences/prefs.go @@ -9,13 +9,12 @@ import ( "log/slog" "os" "path/filepath" - "strings" "sync" "github.com/adrg/xdg" "github.com/knadh/koanf/parsers/toml" - "github.com/knadh/koanf/providers/env" "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/providers/structs" "github.com/knadh/koanf/v2" "github.com/joshuar/go-hass-agent/internal/validation" @@ -100,14 +99,9 @@ var Load = func() error { // Load config file if err := prefsSrc.Load(file.Provider(preferencesFile), toml.Parser()); err != nil { slog.Warn("No preferences found, using defaults.", slog.Any("error", err)) - // return fmt.Errorf("%w: %w", ErrLoadPreferences, err) - } - // Merge config with any environment variables. - if err := prefsSrc.Load(env.Provider(prefsEnvPrefix, ".", func(s string) string { - return strings.Replace(strings.ToLower( - strings.TrimPrefix(s, prefsEnvPrefix)), "_", ".", -1) - }), nil); err != nil { - return fmt.Errorf("%w: %w", ErrLoadPreferences, err) + if err := prefsSrc.Load(structs.Provider(defaultAgentPreferences, "toml"), nil); err != nil { + return fmt.Errorf("%w: %w", ErrLoadPreferences, err) + } } // Unmarshal config, overwriting defaults. if err := prefsSrc.UnmarshalWithConf("", defaultAgentPreferences, koanf.UnmarshalConf{Tag: "toml"}); err != nil { @@ -147,11 +141,6 @@ func validate() error { return fmt.Errorf("%w: %w", ErrLoadPreferences, err) } - // Ensure device preferences are valid. - if err := validation.Validate.Struct(currentPreferences.Device); err != nil { - return fmt.Errorf("%w: %s", ErrValidatePreferences, validation.ParseValidationErrors(err)) - } - // Ensure hass preferences are valid. if err := validation.Validate.Struct(currentPreferences.Hass); err != nil { return fmt.Errorf("%w: %s", ErrValidatePreferences, validation.ParseValidationErrors(err)) diff --git a/internal/preferences/testing/data/go-hass-agent-test/preferences.toml b/internal/preferences/testing/data/go-hass-agent-test/preferences.toml index 83a11b7c..94c6cb26 100644 --- a/internal/preferences/testing/data/go-hass-agent-test/preferences.toml +++ b/internal/preferences/testing/data/go-hass-agent-test/preferences.toml @@ -15,6 +15,7 @@ version = "Unknown" [mqtt] enabled = false + topic_prefix = "homeassistant" [registration] server = "http://localhost:8123" diff --git a/internal/preferences/worker.go b/internal/preferences/worker.go index 495d1e1b..0bd6d926 100644 --- a/internal/preferences/worker.go +++ b/internal/preferences/worker.go @@ -1,6 +1,7 @@ // Copyright 2024 Joshua Rich . // SPDX-License-Identifier: MIT +//go:generate go run github.com/matryer/moq -out worker_mocks_test.go . Worker package preferences import ( diff --git a/internal/preferences/worker_mocks_test.go b/internal/preferences/worker_mocks_test.go new file mode 100644 index 00000000..26f5cce3 --- /dev/null +++ b/internal/preferences/worker_mocks_test.go @@ -0,0 +1,104 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package preferences + +import ( + "sync" +) + +// Ensure, that WorkerMock does implement Worker. +// If this is not the case, regenerate this file with moq. +var _ Worker[any] = &WorkerMock[any]{} + +// WorkerMock is a mock implementation of Worker. +// +// func TestSomethingThatUsesWorker(t *testing.T) { +// +// // make and configure a mocked Worker +// mockedWorker := &WorkerMock{ +// DefaultPreferencesFunc: func() T { +// panic("mock out the DefaultPreferences method") +// }, +// PreferencesIDFunc: func() string { +// panic("mock out the PreferencesID method") +// }, +// } +// +// // use mockedWorker in code that requires Worker +// // and then make assertions. +// +// } +type WorkerMock[T any] struct { + // DefaultPreferencesFunc mocks the DefaultPreferences method. + DefaultPreferencesFunc func() T + + // PreferencesIDFunc mocks the PreferencesID method. + PreferencesIDFunc func() string + + // calls tracks calls to the methods. + calls struct { + // DefaultPreferences holds details about calls to the DefaultPreferences method. + DefaultPreferences []struct { + } + // PreferencesID holds details about calls to the PreferencesID method. + PreferencesID []struct { + } + } + lockDefaultPreferences sync.RWMutex + lockPreferencesID sync.RWMutex +} + +// DefaultPreferences calls DefaultPreferencesFunc. +func (mock *WorkerMock[T]) DefaultPreferences() T { + if mock.DefaultPreferencesFunc == nil { + panic("WorkerMock.DefaultPreferencesFunc: method is nil but Worker.DefaultPreferences was just called") + } + callInfo := struct { + }{} + mock.lockDefaultPreferences.Lock() + mock.calls.DefaultPreferences = append(mock.calls.DefaultPreferences, callInfo) + mock.lockDefaultPreferences.Unlock() + return mock.DefaultPreferencesFunc() +} + +// DefaultPreferencesCalls gets all the calls that were made to DefaultPreferences. +// Check the length with: +// +// len(mockedWorker.DefaultPreferencesCalls()) +func (mock *WorkerMock[T]) DefaultPreferencesCalls() []struct { +} { + var calls []struct { + } + mock.lockDefaultPreferences.RLock() + calls = mock.calls.DefaultPreferences + mock.lockDefaultPreferences.RUnlock() + return calls +} + +// PreferencesID calls PreferencesIDFunc. +func (mock *WorkerMock[T]) PreferencesID() string { + if mock.PreferencesIDFunc == nil { + panic("WorkerMock.PreferencesIDFunc: method is nil but Worker.PreferencesID was just called") + } + callInfo := struct { + }{} + mock.lockPreferencesID.Lock() + mock.calls.PreferencesID = append(mock.calls.PreferencesID, callInfo) + mock.lockPreferencesID.Unlock() + return mock.PreferencesIDFunc() +} + +// PreferencesIDCalls gets all the calls that were made to PreferencesID. +// Check the length with: +// +// len(mockedWorker.PreferencesIDCalls()) +func (mock *WorkerMock[T]) PreferencesIDCalls() []struct { +} { + var calls []struct { + } + mock.lockPreferencesID.RLock() + calls = mock.calls.PreferencesID + mock.lockPreferencesID.RUnlock() + return calls +}