diff --git a/cmd/brightness.go b/cmd/brightness.go index 3313720..1d2e454 100644 --- a/cmd/brightness.go +++ b/cmd/brightness.go @@ -16,6 +16,7 @@ package cmd import ( "fmt" + "io" "slices" "github.com/rs/zerolog/log" @@ -30,7 +31,7 @@ var ( brightnessRange = append([]string{"+", "-", "min", "mid", "max"}, util.MakeRangeString(1, 99)...) ) -func newBrightnessCmd(h *pkg.Hctl) *cobra.Command { +func newBrightnessCmd(h *pkg.Hctl, out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "brightness [+|-|min|max|1-99]", Short: "Change brightness", @@ -57,9 +58,9 @@ func newBrightnessCmd(h *pkg.Hctl) *cobra.Command { c := h.GetRest() obj, state, sub, err := c.TurnLightOnBrightness(args[0], args[1]) if err != nil { - o.PrintError(err) + o.FprintError(out, err) } else { - o.PrintSuccessAction(obj, state) + o.FprintSuccess(out, fmt.Sprintf("%s brightness set to %s%%", obj, args[1])) } log.Debug().Caller().Msgf("Result: %s(%s) to %s", obj, sub, state) }, diff --git a/cmd/brightness_test.go b/cmd/brightness_test.go new file mode 100644 index 0000000..249b162 --- /dev/null +++ b/cmd/brightness_test.go @@ -0,0 +1,39 @@ +// Copyright 2024 Fabian `xx4h` Sylvester +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "testing" + + "github.com/xx4h/hctl/pkg/hctltest" +) + +func Test_newCmdBrightness(t *testing.T) { + ms := hctltest.MockServer(t) + h := newTestingHctl(t) + if err := h.SetConfigValue("hub.url", ms.URL); err != nil { + t.Error(err) + } + + var tests = map[string]cmdTest{ + "set brightness": { + "brightness light.livingroom_other 20", + "(?m)^.*livingroom_other brightness set to 20%", + "", + }, + } + + testCmd(t, h, tests) +} diff --git a/cmd/completion_test.go b/cmd/completion_test.go index 955d0f7..b40a43c 100644 --- a/cmd/completion_test.go +++ b/cmd/completion_test.go @@ -17,16 +17,13 @@ package cmd import ( "testing" - "github.com/xx4h/hctl/pkg" "github.com/xx4h/hctl/pkg/hctltest" ) func Test_compListStates(t *testing.T) { ms := hctltest.MockServer(t) - h, err := pkg.NewHctl(false) - if err != nil { - t.Errorf("Error createing new Hctl instance: %+v", err) - } + h := newTestingHctl(t) + if err := h.SetConfigValue("hub.url", ms.URL); err != nil { t.Errorf("Could not set hub.url to %s: %+v", ms.URL, err) } @@ -47,42 +44,42 @@ func Test_compListStates(t *testing.T) { nil, []string{"brightness"}, "", - 5, + 6, }, "serviceCap turn_on": { nil, []string{"turn_on"}, nil, "", - 10, + 11, }, "serviceCap turn_on + state off": { nil, []string{"turn_on"}, nil, "off", - 4, + 5, }, "serviceCap turn_off + state on": { nil, []string{"turn_off"}, nil, "on", - 6, + 7, }, "serviceCap play_media + attrib device_class": { nil, []string{"play_media"}, []string{"device_class"}, "", - 1, + 2, }, "serviceCap play_media + attrib device_class or video_out": { nil, []string{"play_media"}, []string{"device_class", "video_out"}, "", - 2, + 3, }, } diff --git a/cmd/config_get.go b/cmd/config_get.go index 343deaa..dfd5fc5 100644 --- a/cmd/config_get.go +++ b/cmd/config_get.go @@ -36,7 +36,7 @@ const ( // editorconfig-checker-enable ) -func newConfigGetCmd(h *pkg.Hctl, _ io.Writer) *cobra.Command { +func newConfigGetCmd(h *pkg.Hctl, out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "get [PATH]", Short: "Get configuration parameters", @@ -55,14 +55,22 @@ func newConfigGetCmd(h *pkg.Hctl, _ io.Writer) *cobra.Command { a, _ := compListConfig("", []string{}, h) slices.Sort(a) for _, b := range a { - l := append([]any{}, b, h.GetConfigValue(b)) + v, err := h.GetConfigValue(b) + l := []any{} + if err == nil { + l = append(l, b, v) + } clist = append(clist, l) } } else { - l := append([]any{}, args[0], h.GetConfigValue(args[0])) + v, err := h.GetConfigValue(args[0]) + if err != nil { + o.FprintError(out, err) + } + l := append([]any{}, args[0], v) clist = append(clist, l) } - o.PrintSuccessListWithHeader(header, clist) + o.FprintSuccessListWithHeader(out, header, clist) }, } diff --git a/cmd/config_get_test.go b/cmd/config_get_test.go new file mode 100644 index 0000000..e107b8c --- /dev/null +++ b/cmd/config_get_test.go @@ -0,0 +1,33 @@ +// Copyright 2024 Fabian `xx4h` Sylvester +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "testing" +) + +func Test_newCmdConfigGet(t *testing.T) { + h := newTestingHctl(t) + + var tests = map[string]cmdTest{ + "get existing option": { + "config get completion.short_names", + "(?m)^OPTION\\s+VALUE$\n^completion.short_names\\s+true", + "", + }, + } + + testCmd(t, h, tests) +} diff --git a/cmd/config_rem.go b/cmd/config_rem.go index a8bf3ff..e979163 100644 --- a/cmd/config_rem.go +++ b/cmd/config_rem.go @@ -33,7 +33,7 @@ const ( // editorconfig-checker-enable ) -func newConfigRemCmd(h *pkg.Hctl, _ io.Writer) *cobra.Command { +func newConfigRemCmd(h *pkg.Hctl, out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "remove PATH", Short: "Set config variables", @@ -45,9 +45,9 @@ func newConfigRemCmd(h *pkg.Hctl, _ io.Writer) *cobra.Command { }, Run: func(_ *cobra.Command, args []string) { if err := h.RemoveConfigOptionWrite(args[0]); err != nil { - o.PrintError(err) + o.FprintError(out, err) } - o.PrintSuccess(fmt.Sprintf("Option `%s` successfully removed.", args[0])) + o.FprintSuccess(out, fmt.Sprintf("Option `%s` successfully removed.", args[0])) }, } diff --git a/cmd/config_rem_test.go b/cmd/config_rem_test.go new file mode 100644 index 0000000..d4c9426 --- /dev/null +++ b/cmd/config_rem_test.go @@ -0,0 +1,33 @@ +// Copyright 2024 Fabian `xx4h` Sylvester +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "testing" +) + +func Test_newCmdConfigRem(t *testing.T) { + h := newTestingHctl(t) + + var tests = map[string]cmdTest{ + "rem completion.short_names": { + "config remove device_map.g", + "(?m)^.*Option `device_map.g` successfully removed", + "", + }, + } + + testCmd(t, h, tests) +} diff --git a/cmd/config_set.go b/cmd/config_set.go index 7f9dd9c..a328f2c 100644 --- a/cmd/config_set.go +++ b/cmd/config_set.go @@ -33,7 +33,7 @@ const ( // editorconfig-checker-enable ) -func newConfigSetCmd(h *pkg.Hctl, _ io.Writer) *cobra.Command { +func newConfigSetCmd(h *pkg.Hctl, out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "set PATH VALUE", Short: "Set config variables", @@ -48,9 +48,9 @@ func newConfigSetCmd(h *pkg.Hctl, _ io.Writer) *cobra.Command { }, Run: func(_ *cobra.Command, args []string) { if err := h.SetConfigValueWrite(args[0], args[1]); err != nil { - o.PrintError(err) + o.FprintError(out, err) } - o.PrintSuccess(fmt.Sprintf("Option `%s` successfully set to `%s`.", args[0], args[1])) + o.FprintSuccess(out, fmt.Sprintf("Option `%s` successfully set to `%s`.", args[0], args[1])) }, } diff --git a/cmd/config_set_test.go b/cmd/config_set_test.go new file mode 100644 index 0000000..011852b --- /dev/null +++ b/cmd/config_set_test.go @@ -0,0 +1,33 @@ +// Copyright 2024 Fabian `xx4h` Sylvester +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "testing" +) + +func Test_newCmdConfigSet(t *testing.T) { + h := newTestingHctl(t) + + var tests = map[string]cmdTest{ + "set completion.short_names option": { + "config set completion.short_names false", + "(?m)^.*Option `completion.short_names` successfully set to `false`", + "", + }, + } + + testCmd(t, h, tests) +} diff --git a/cmd/list.go b/cmd/list.go index a312a18..79c9622 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -15,13 +15,15 @@ package cmd import ( + "io" + "github.com/spf13/cobra" "github.com/xx4h/hctl/pkg" ) // listCmd represents the list command -func newListCmd(h *pkg.Hctl) *cobra.Command { +func newListCmd(h *pkg.Hctl, out io.Writer) *cobra.Command { var domains []string var services []string @@ -33,9 +35,9 @@ func newListCmd(h *pkg.Hctl) *cobra.Command { ValidArgs: []string{"entities", "services"}, RunE: func(_ *cobra.Command, args []string) error { if len(args) == 0 || args[0] == "entities" { - h.DumpStates(domains) + h.DumpStates(out, domains) } else if args[0] == "services" { - h.DumpServices(domains, services) + h.DumpServices(out, domains, services) } return nil }, diff --git a/cmd/list_test.go b/cmd/list_test.go new file mode 100644 index 0000000..27cfbec --- /dev/null +++ b/cmd/list_test.go @@ -0,0 +1,54 @@ +// Copyright 2024 Fabian `xx4h` Sylvester +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "testing" + + "github.com/xx4h/hctl/pkg/hctltest" +) + +func Test_newCmdList(t *testing.T) { + ms := hctltest.MockServer(t) + h := newTestingHctl(t) + if err := h.SetConfigValue("hub.url", ms.URL); err != nil { + t.Error(err) + } + + var tests = map[string]cmdTest{ + "list services": { + "list services", + `^.*Services`, + "", + }, + "list services with play_media": { + "list services -s play_media", + `(?m)^.*Services.*\n.*media_player.*\n.*play_media`, + "", + }, + "list entities": { + "list entities", + `^.*States`, + "", + }, + "list entities of domain media_player": { + "list entities -d media_player", + `^.*States.*\n.*media_player.*\n.*player1.*\n.*player2`, + "", + }, + } + + testCmd(t, h, tests) +} diff --git a/cmd/off.go b/cmd/off.go index ff72528..f167963 100644 --- a/cmd/off.go +++ b/cmd/off.go @@ -24,7 +24,7 @@ import ( o "github.com/xx4h/hctl/pkg/output" ) -func newOffCmd(h *pkg.Hctl, _ io.Writer) *cobra.Command { +func newOffCmd(h *pkg.Hctl, out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "off", @@ -40,9 +40,9 @@ func newOffCmd(h *pkg.Hctl, _ io.Writer) *cobra.Command { c := h.GetRest() obj, state, sub, err := c.TurnOff(args[0]) if err != nil { - o.PrintError(err) + o.FprintError(out, err) } else { - o.PrintSuccessAction(obj, state) + o.FprintSuccessAction(out, obj, state) } log.Debug().Caller().Msgf("Result: %s(%s) to %s", obj, sub, state) }, diff --git a/cmd/off_test.go b/cmd/off_test.go new file mode 100644 index 0000000..322b07a --- /dev/null +++ b/cmd/off_test.go @@ -0,0 +1,39 @@ +// Copyright 2024 Fabian `xx4h` Sylvester +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "testing" + + "github.com/xx4h/hctl/pkg/hctltest" +) + +func Test_newCmdOff(t *testing.T) { + ms := hctltest.MockServer(t) + h := newTestingHctl(t) + if err := h.SetConfigValue("hub.url", ms.URL); err != nil { + t.Error(err) + } + + var tests = map[string]cmdTest{ + "turn off": { + "off light.bedroom_other", + "(?m)^.*bedroom_other off", + "", + }, + } + + testCmd(t, h, tests) +} diff --git a/cmd/on.go b/cmd/on.go index df81a84..2f0ba72 100644 --- a/cmd/on.go +++ b/cmd/on.go @@ -15,6 +15,8 @@ package cmd import ( + "io" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -22,7 +24,7 @@ import ( o "github.com/xx4h/hctl/pkg/output" ) -func newOnCmd(h *pkg.Hctl) *cobra.Command { +func newOnCmd(h *pkg.Hctl, out io.Writer) *cobra.Command { var brightness string cmd := &cobra.Command{ @@ -57,9 +59,9 @@ func newOnCmd(h *pkg.Hctl) *cobra.Command { obj, state, sub, err = c.TurnOn(args[0]) } if err != nil { - o.PrintError(err) + o.FprintError(out, err) } else { - o.PrintSuccessAction(obj, state) + o.FprintSuccessAction(out, obj, state) } log.Debug().Caller().Msgf("Result: %s(%s) to %s", obj, sub, state) }, diff --git a/cmd/on_test.go b/cmd/on_test.go new file mode 100644 index 0000000..3dda875 --- /dev/null +++ b/cmd/on_test.go @@ -0,0 +1,39 @@ +// Copyright 2024 Fabian `xx4h` Sylvester +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "testing" + + "github.com/xx4h/hctl/pkg/hctltest" +) + +func Test_newCmdOn(t *testing.T) { + ms := hctltest.MockServer(t) + h := newTestingHctl(t) + if err := h.SetConfigValue("hub.url", ms.URL); err != nil { + t.Error(err) + } + + var tests = map[string]cmdTest{ + "turn on": { + "on light.bedroom_main", + "(?m)^.*bedroom_main on", + "", + }, + } + + testCmd(t, h, tests) +} diff --git a/cmd/play.go b/cmd/play.go index 5811f75..3ce369b 100644 --- a/cmd/play.go +++ b/cmd/play.go @@ -23,7 +23,7 @@ import ( ) // toggleCmd represents the toggle command -func newPlayCmd(h *pkg.Hctl, _ io.Writer) *cobra.Command { +func newPlayCmd(h *pkg.Hctl, out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "play", Short: "Play music from url on media player", @@ -38,7 +38,7 @@ func newPlayCmd(h *pkg.Hctl, _ io.Writer) *cobra.Command { return nil, cobra.ShellCompDirectiveNoFileComp }, Run: func(_ *cobra.Command, args []string) { - h.PlayMusic(args[0], args[1]) + h.PlayMusic(out, args[0], args[1]) }, } diff --git a/cmd/play_test.go b/cmd/play_test.go new file mode 100644 index 0000000..9baf190 --- /dev/null +++ b/cmd/play_test.go @@ -0,0 +1,39 @@ +// Copyright 2024 Fabian `xx4h` Sylvester +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "testing" + + "github.com/xx4h/hctl/pkg/hctltest" +) + +func Test_newCmdPlay(t *testing.T) { + ms := hctltest.MockServer(t) + h := newTestingHctl(t) + if err := h.SetConfigValue("hub.url", ms.URL); err != nil { + t.Error(err) + } + + var tests = map[string]cmdTest{ + "play mp3": { + "play player1 testdata/test.fake.mp3", + "(?m)^.*player1 playing test.fake.mp3", + "", + }, + } + + testCmd(t, h, tests) +} diff --git a/cmd/root.go b/cmd/root.go index f214ed7..d0a5924 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -62,13 +62,13 @@ func newRootCmd(h *pkg.Hctl, out io.Writer, _ []string) *cobra.Command { } cmd.AddCommand( - newBrightnessCmd(h), + newBrightnessCmd(h, out), newCompletionCmd(), newConfigCmd(h, out), newInitCmd(h), - newListCmd(h), + newListCmd(h, out), newOffCmd(h, out), - newOnCmd(h), + newOnCmd(h, out), newPlayCmd(h, out), newToggleCmd(h, out), newVersionCmd(out), @@ -87,7 +87,7 @@ func RunCmd() { return strings.ToUpper(fmt.Sprintf("| %-6s|", i)) } log.Logger = log.Output(output) - h, err := pkg.NewHctl(true) + h, err := pkg.NewHctl(false) if err != nil { log.Fatal().Caller().Msgf("Error: %v", err) } diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 0000000..bf82afc --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,94 @@ +// Copyright 2024 Fabian `xx4h` Sylvester +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "bytes" + "io" + "os" + "path" + "regexp" + "strings" + "testing" + + "github.com/xx4h/hctl/pkg" +) + +type cmdTest struct { + input string + rexOut string + rexErr string +} + +func newTestingHctl(t *testing.T) *pkg.Hctl { + t.Helper() + h, err := pkg.NewHctl(true) + if err != nil { + t.Error(err) + } + + // create tempdir for config + tmpDir := t.TempDir() + testdata, err := os.Open("testdata/hctl.yaml") + if err != nil { + t.Error(err) + } + config, err := os.Create(path.Join(tmpDir, "hctl.yaml")) + if err != nil { + t.Error(err) + } + if _, err := io.Copy(config, testdata); err != nil { + t.Error(err) + } + + if err := h.LoadConfig(path.Join(tmpDir, "hctl.yaml")); err != nil { + t.Error(err) + } + return h +} + +func testCmd(t *testing.T, h *pkg.Hctl, tests map[string]cmdTest) { + t.Helper() + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + out := new(bytes.Buffer) + errout := new(bytes.Buffer) + in := strings.Split(tt.input, " ") + rootCmd = newRootCmd(h, out, in) + rootCmd.SetOut(out) + rootCmd.SetErr(errout) + rootCmd.SetArgs(in) + if err := rootCmd.Execute(); err != nil { + t.Error(err) + } + e := errout.String() + o := out.String() + rex, err := regexp.Compile(tt.rexErr) + if err != nil { + t.Error(err) + } + if ok := rex.MatchString(e); !ok { + t.Errorf("got %s, want %s", o, tt.rexErr) + } + ok, err := regexp.MatchString(tt.rexOut, o) + if err != nil { + t.Error(err) + } + if !ok { + t.Errorf("got %s, want %s", o, tt.rexOut) + } + }) + } +} diff --git a/cmd/testdata/hctl.yaml b/cmd/testdata/hctl.yaml new file mode 100644 index 0000000..4c21744 --- /dev/null +++ b/cmd/testdata/hctl.yaml @@ -0,0 +1,8 @@ +hub: + type: hass + url: http://127.1.33.7/api + token: "test" +device_map: + a: "media_player.player1" +media_map: + party_horn: /path/to/part_horn.mp3 diff --git a/cmd/testdata/test.fake.mp3 b/cmd/testdata/test.fake.mp3 new file mode 100644 index 0000000..9b3d121 --- /dev/null +++ b/cmd/testdata/test.fake.mp3 @@ -0,0 +1 @@ +# this is not a mp3 diff --git a/cmd/toggle.go b/cmd/toggle.go index dd09c64..a6d1501 100644 --- a/cmd/toggle.go +++ b/cmd/toggle.go @@ -25,7 +25,7 @@ import ( ) // toggleCmd represents the toggle command -func newToggleCmd(h *pkg.Hctl, _ io.Writer) *cobra.Command { +func newToggleCmd(h *pkg.Hctl, out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "toggle", Short: "Toggle on/off a light or switch", @@ -41,9 +41,9 @@ func newToggleCmd(h *pkg.Hctl, _ io.Writer) *cobra.Command { c := h.GetRest() obj, state, sub, err := c.Toggle(args[0]) if err != nil { - o.PrintError(err) + o.FprintError(out, err) } else { - o.PrintSuccessAction(obj, state) + o.FprintSuccessAction(out, obj, state) } log.Debug().Caller().Msgf("Result: %s(%s) to %s", obj, sub, state) }, diff --git a/cmd/toggle_test.go b/cmd/toggle_test.go new file mode 100644 index 0000000..a6a1d9a --- /dev/null +++ b/cmd/toggle_test.go @@ -0,0 +1,39 @@ +// Copyright 2024 Fabian `xx4h` Sylvester +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "testing" + + "github.com/xx4h/hctl/pkg/hctltest" +) + +func Test_newCmdToggle(t *testing.T) { + ms := hctltest.MockServer(t) + h := newTestingHctl(t) + if err := h.SetConfigValue("hub.url", ms.URL); err != nil { + t.Error(err) + } + + var tests = map[string]cmdTest{ + "toggle light": { + "toggle bedroom_main", + "(?m)^.*bedroom_main toggle", + "", + }, + } + + testCmd(t, h, tests) +} diff --git a/cmd/volume.go b/cmd/volume.go index 12ca145..701294e 100644 --- a/cmd/volume.go +++ b/cmd/volume.go @@ -27,7 +27,7 @@ import ( ) // toggleCmd represents the toggle command -func newVolumeCmd(h *pkg.Hctl, _ io.Writer) *cobra.Command { +func newVolumeCmd(h *pkg.Hctl, out io.Writer) *cobra.Command { volRange := util.MakeRangeString(0, 100) cmd := &cobra.Command{ Use: "volume", @@ -44,9 +44,13 @@ func newVolumeCmd(h *pkg.Hctl, _ io.Writer) *cobra.Command { }, Run: func(_ *cobra.Command, args []string) { if !slices.Contains(volRange, args[1]) { - o.PrintError(fmt.Errorf("volume needs to be 1-100")) + o.FprintError(out, fmt.Errorf("volume needs to be 1-100")) } - h.VolumeSet(args[0], args[1]) + obj, state, err := h.VolumeSet(args[0], args[1]) + if err != nil { + o.FprintError(out, err) + } + o.FprintSuccessAction(out, obj, state) }, } diff --git a/cmd/volume_test.go b/cmd/volume_test.go new file mode 100644 index 0000000..0631719 --- /dev/null +++ b/cmd/volume_test.go @@ -0,0 +1,39 @@ +// Copyright 2024 Fabian `xx4h` Sylvester +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "testing" + + "github.com/xx4h/hctl/pkg/hctltest" +) + +func Test_newCmdVolume(t *testing.T) { + ms := hctltest.MockServer(t) + h := newTestingHctl(t) + if err := h.SetConfigValue("hub.url", ms.URL); err != nil { + t.Error(err) + } + + var tests = map[string]cmdTest{ + "set volume": { + "volume media_player.player1 10", + "(?m)^.*player1 volume set to 10%", + "", + }, + } + + testCmd(t, h, tests) +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 74b3afb..774efa7 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -87,6 +87,7 @@ func NewViper() (*viper.Viper, error) { v.AddConfigPath(".") v.AddConfigPath(path.Join(userDir, ".config/hctl")) v.AddConfigPath(execDir) + return v, nil } @@ -122,7 +123,10 @@ func NewConfig() (*Config, error) { return cfg, nil } -func (c *Config) LoadConfig() error { +func (c *Config) LoadConfig(configPath string) error { + if configPath != "" { + c.Viper.SetConfigFile(configPath) + } if err := c.Viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { diff --git a/pkg/hctl.go b/pkg/hctl.go index f58d348..17a85a5 100644 --- a/pkg/hctl.go +++ b/pkg/hctl.go @@ -16,6 +16,7 @@ package pkg import ( "fmt" + "io" "os" "strconv" "time" @@ -37,13 +38,13 @@ type Hctl struct { // log *zerolog.Logger } -func NewHctl(loadCfg bool) (*Hctl, error) { +func NewHctl(testing bool) (*Hctl, error) { cfg, err := config.NewConfig() if err != nil { return nil, err } - if loadCfg { - err := cfg.LoadConfig() + if !testing { + err := cfg.LoadConfig("") if err != nil { return nil, err } @@ -54,6 +55,14 @@ func NewHctl(loadCfg bool) (*Hctl, error) { }, nil } +func (h *Hctl) LoadConfig(configPath string) error { + err := h.cfg.LoadConfig(configPath) + if err != nil { + return err + } + return nil +} + func (h *Hctl) InitializeConfig(path string) { if err := i.InitializeConfig(h.cfg, path); err != nil { log.Debug().Caller().Msgf("Error: %+v", err) @@ -67,13 +76,13 @@ func (h *Hctl) CompletionShortNamesEnabled() bool { return h.cfg.Completion.ShortNames } -func (h *Hctl) GetConfigValue(p string) any { +func (h *Hctl) GetConfigValue(p string) (any, error) { v, err := h.cfg.GetValueByPath(p) if err != nil { log.Debug().Caller().Msgf("Error: %+v", err) - o.PrintError(err) + return nil, err } - return v + return v, nil } func (h *Hctl) SetConfigValue(p string, v string) error { @@ -151,24 +160,24 @@ func (h *Hctl) GetFilteredStatesMap(domains []string) (map[string][]string, erro return h.GetRest().GetFilteredStatesMap(domains) } -func (h *Hctl) DumpServices(domains []string, services []string) { +func (h *Hctl) DumpServices(out io.Writer, domains []string, services []string) { t := h.GetFilteredServicesMap(domains, services) - if err := o.PrintThreeLevelFlatTree("Services", t); err != nil { + if err := o.PrintThreeLevelFlatTree(out, "Services", t); err != nil { log.Error().Msgf("Error: %+v", err) } } -func (h *Hctl) DumpStates(domains []string) { +func (h *Hctl) DumpStates(out io.Writer, domains []string) { t, err := h.GetFilteredStatesMap(domains) if err != nil { - o.PrintError(err) + o.FprintError(out, err) } - if err := o.PrintThreeLevelFlatTree("States", t); err != nil { + if err := o.PrintThreeLevelFlatTree(out, "States", t); err != nil { log.Error().Msgf("Error: %+v", err) } } -func (h *Hctl) PlayMusic(obj string, mediaURL string) { +func (h *Hctl) PlayMusic(out io.Writer, obj string, mediaURL string) { if mapURL, ok := h.cfg.MediaMap[mediaURL]; ok { mediaURL = mapURL } @@ -180,7 +189,7 @@ func (h *Hctl) PlayMusic(obj string, mediaURL string) { obj, state, sub, err := h.GetRest().PlayMusic(obj, mediaURL, mediaURL) if err != nil { log.Debug().Caller().Msgf("Error: %+v", err) - o.PrintError(err) + o.FprintError(out, err) } o.PrintSuccessAction(obj, state) @@ -193,7 +202,7 @@ func (h *Hctl) PlayMusic(obj string, mediaURL string) { _, err := os.Stat(mediaURL) if err != nil { log.Debug().Caller().Msgf("Error: %+v", err) - o.PrintError(err) + o.FprintError(out, err) } // get new Media instance @@ -208,10 +217,10 @@ func (h *Hctl) PlayMusic(obj string, mediaURL string) { obj, state, sub, err := h.GetRest().PlayMusic(obj, s.GetURL(), s.GetMediaName()) if err != nil { log.Debug().Caller().Msgf("Error: %+v", err) - o.PrintError(err) + o.FprintError(out, err) } - o.PrintSuccessAction(obj, state) + o.FprintSuccessAction(out, obj, state) log.Debug().Caller().Msgf("Result: %s(%s) to %s", obj, sub, state) // TODO: find better way to ensure we don't close the server before file has been served // -> RaceCondition @@ -224,19 +233,19 @@ func (h *Hctl) PlayMusic(obj string, mediaURL string) { } } -func (h *Hctl) VolumeSet(obj string, volume string) { +func (h *Hctl) VolumeSet(obj string, volume string) (string, string, error) { vint, err := strconv.Atoi(volume) if err != nil { log.Debug().Caller().Msgf("Error: %+v", err) - o.PrintError(err) + return "", "", err } obj, state, sub, err := h.GetRest().VolumeSet(obj, vint) if err != nil { log.Debug().Caller().Msgf("Error: %+v", err) - o.PrintError(err) + return "", "", err } - o.PrintSuccessAction(obj, state) log.Debug().Caller().Msgf("Result: %s(%s) to %s", obj, sub, state) + return obj, state, nil } func (h *Hctl) SetLogging(level string) error { diff --git a/pkg/hctltest/http.go b/pkg/hctltest/http.go index 07df29e..f125ce0 100644 --- a/pkg/hctltest/http.go +++ b/pkg/hctltest/http.go @@ -32,12 +32,16 @@ func MockServer(t testing.TB) *httptest.Server { testdir := filepath.Dir(filename) t.Helper() mux := http.NewServeMux() + + // handle default entry point and return API running mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) if _, err := w.Write([]byte(`{"message": "API running."}`)); err != nil { t.Errorf("Error writing data: %v", err) } }) + + // get all services mux.HandleFunc("/services", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) data, err := os.ReadFile(fmt.Sprintf("%s/testdata/services.json", testdir)) @@ -48,6 +52,8 @@ func MockServer(t testing.TB) *httptest.Server { t.Errorf("Error writing data: %v", err) } }) + + // get all states mux.HandleFunc("/states", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) data, err := os.ReadFile(fmt.Sprintf("%s/testdata/states.json", testdir)) @@ -58,6 +64,8 @@ func MockServer(t testing.TB) *httptest.Server { t.Errorf("Error writing data: %v", err) } }) + + // Update entity in domain/service mux.HandleFunc("POST /services/{domain}/{service}", func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() body, err := io.ReadAll(r.Body) diff --git a/pkg/hctltest/testdata/bedroom_main_light_turn_on_response.json b/pkg/hctltest/testdata/bedroom_main_light_turn_on_response.json new file mode 100644 index 0000000..4fd278e --- /dev/null +++ b/pkg/hctltest/testdata/bedroom_main_light_turn_on_response.json @@ -0,0 +1,21 @@ +[ + { + "entity_id": "light.bedroom_main", + "state": "on", + "attributes": { + "supported_color_modes": ["onoff"], + "color_mode": null, + "all_on": false, + "friendly_name": "Bedroom Main", + "supported_features": 0 + }, + "last_changed": "2024-10-12T18:25:14.915796+00:00", + "last_reported": "2024-10-12T18:25:14.915796+00:00", + "last_updated": "2024-10-12T18:25:14.915796+00:00", + "context": { + "id": "ABDCDEFGHIJKLMNOPQRSTUVW05", + "parent_id": null, + "user_id": "someuseridhash" + } + } +] diff --git a/pkg/hctltest/testdata/bedroom_other_light_turn_off_response.json b/pkg/hctltest/testdata/bedroom_other_light_turn_off_response.json new file mode 100644 index 0000000..4da424c --- /dev/null +++ b/pkg/hctltest/testdata/bedroom_other_light_turn_off_response.json @@ -0,0 +1,32 @@ +[ + { + "entity_id": "light.bedroom_other", + "state": "off", + "attributes": { + "min_color_temp_kelvin": 1538, + "max_color_temp_kelvin": 7142, + "min_mireds": 140, + "max_mireds": 650, + "effect_list": ["colorloop"], + "supported_color_modes": ["color_temp", "hs", "xy"], + "effect": null, + "color_mode": "xy", + "brightness": 70, + "color_temp_kelvin": null, + "color_temp": null, + "hs_color": [345.198, 89.02], + "rgb_color": [255, 28, 84], + "xy_color": [0.6447, 0.279], + "friendly_name": "Bedroom Other", + "supported_features": 44 + }, + "last_changed": "2024-10-19T09:06:48.406200+00:00", + "last_reported": "2024-10-19T09:06:48.406200+00:00", + "last_updated": "2024-10-19T09:06:48.406200+00:00", + "context": { + "id": "ABDCDEFGHIJKLMNOPQRSTUVW06", + "parent_id": null, + "user_id": "someuseridhash" + } + } +] diff --git a/pkg/hctltest/testdata/livingroom_other_light_turn_on_response.json b/pkg/hctltest/testdata/livingroom_other_light_turn_on_response.json new file mode 100644 index 0000000..91d8faf --- /dev/null +++ b/pkg/hctltest/testdata/livingroom_other_light_turn_on_response.json @@ -0,0 +1,32 @@ +[ + { + "entity_id": "light.livingroom_other", + "state": "on", + "attributes": { + "min_color_temp_kelvin": 1538, + "max_color_temp_kelvin": 7142, + "min_mireds": 140, + "max_mireds": 650, + "effect_list": ["colorloop"], + "supported_color_modes": ["color_temp", "hs", "xy"], + "effect": null, + "color_mode": "xy", + "brightness": 70, + "color_temp_kelvin": null, + "color_temp": null, + "hs_color": [345.198, 89.02], + "rgb_color": [255, 28, 84], + "xy_color": [0.6447, 0.279], + "friendly_name": "Living Other", + "supported_features": 44 + }, + "last_changed": "2024-10-19T09:06:48.406200+00:00", + "last_reported": "2024-10-19T09:06:48.406200+00:00", + "last_updated": "2024-10-19T09:06:48.406200+00:00", + "context": { + "id": "ABDCDEFGHIJKLMNOPQRSTUVW04", + "parent_id": null, + "user_id": "someuseridhash" + } + } +] diff --git a/pkg/hctltest/testdata/player1_media_player_volume_set_response.json b/pkg/hctltest/testdata/player1_media_player_volume_set_response.json new file mode 100644 index 0000000..79dea56 --- /dev/null +++ b/pkg/hctltest/testdata/player1_media_player_volume_set_response.json @@ -0,0 +1,56 @@ +[ + { + "entity_id": "media_player.player1", + "state": "idle", + "attributes": { + "volume_level": 0.30000001192092896, + "is_volume_muted": false, + "media_content_id": "http://10.10.10.10:1337/fake.mp3", + "media_content_type": "music", + "media_duration": 3.160816, + "media_position": 0, + "media_position_updated_at": "2024-10-12T20:57:22.888305+00:00", + "app_id": "ABCDE123", + "app_name": "Default Media Receiver", + "entity_picture_local": null, + "device_class": "speaker", + "friendly_name": "Player 1", + "supported_features": 152463 + }, + "last_changed": "2024-10-12T20:56:54.692016+00:00", + "last_reported": "2024-10-12T20:57:22.888625+00:00", + "last_updated": "2024-10-12T20:57:22.888625+00:00", + "context": { + "id": "ABDCDEFGHIJKLMNOPQRSTUVW01", + "parent_id": null, + "user_id": "someuseridhash" + } + }, + { + "entity_id": "media_player.player1", + "state": "playing", + "attributes": { + "volume_level": 0.30000001192092896, + "is_volume_muted": false, + "media_content_id": "http://10.10.10.10:1337/fake.mp3", + "media_content_type": "music", + "media_duration": 3.160816, + "media_position": 0, + "media_position_updated_at": "2024-10-12T20:57:23.012272+00:00", + "app_id": "ABCDE123", + "app_name": "Default Media Receiver", + "entity_picture_local": null, + "device_class": "speaker", + "friendly_name": "Player 1", + "supported_features": 152463 + }, + "last_changed": "2024-10-12T20:57:23.012695+00:00", + "last_reported": "2024-10-12T20:57:23.012695+00:00", + "last_updated": "2024-10-12T20:57:23.012695+00:00", + "context": { + "id": "ABDCDEFGHIJKLMNOPQRSTUVW01", + "parent_id": null, + "user_id": "someuseridhash" + } + } +] diff --git a/pkg/output/output.go b/pkg/output/output.go index ef2c28b..386a108 100644 --- a/pkg/output/output.go +++ b/pkg/output/output.go @@ -16,6 +16,7 @@ package output import ( "fmt" + "io" "os" "sort" @@ -30,21 +31,42 @@ func GetBanner() (string, error) { putils.LettersFromStringWithStyle("Ctl", pterm.FgWhite.ToStyle())).Srender() } +func FprintSuccess(out io.Writer, str string) { + pterm.Fprint(out, pterm.Success.Sprintln(str)) +} + func PrintSuccess(str string) { pterm.Success.Println(str) } +func FprintSuccessAction(out io.Writer, obj string, state string) { + pterm.Fprint(out, pterm.Success.Sprintfln("%s %s", obj, state)) +} + func PrintSuccessAction(obj string, state string) { pterm.Success.Printfln("%s %s", obj, state) } -func PrintSuccessListWithHeader(header []interface{}, list [][]interface{}) { +func ListWithHeader(header []interface{}, list [][]interface{}) *uitable.Table { table := uitable.New() table.AddRow(header...) for _, entry := range list { table.AddRow(entry...) } - fmt.Println(table) + return table +} + +func FprintSuccessListWithHeader(out io.Writer, header []interface{}, list [][]interface{}) { + fmt.Fprintln(out, ListWithHeader(header, list)) +} + +func PrintSuccessListWithHeader(header []interface{}, list [][]interface{}) { + fmt.Println(ListWithHeader(header, list)) +} + +func FprintError(out io.Writer, err error) { + pterm.Fprint(out, pterm.Error.Sprintln(err)) + os.Exit(1) } func PrintError(err error) { @@ -52,7 +74,7 @@ func PrintError(err error) { os.Exit(1) } -func PrintThreeLevelFlatTree(name string, tree map[string][]string) error { +func PrintThreeLevelFlatTree(out io.Writer, name string, tree map[string][]string) error { t := pterm.TreeNode{ Text: name, Children: []pterm.TreeNode{}, @@ -80,5 +102,11 @@ func PrintThreeLevelFlatTree(name string, tree map[string][]string) error { t.Children = append(t.Children, g) } - return pterm.DefaultTree.WithRoot(t).Render() + treeout, err := pterm.DefaultTree.WithRoot(t).Srender() + if err != nil { + return err + } + + fmt.Fprintln(out, treeout) + return nil }