Skip to content

Commit

Permalink
test(agent): ✅ add tests for custom MQTT commands package
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuar committed Jul 3, 2024
1 parent 35713fd commit ff8c483
Show file tree
Hide file tree
Showing 6 changed files with 307 additions and 13 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-resty/resty/v2 v2.13.1
github.com/go-test/deep v1.1.1
github.com/go-text/render v0.1.0 // indirect
github.com/go-text/typesetting v0.1.0 // indirect
github.com/gofrs/uuid/v5 v5.2.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-text/render v0.1.0 h1:osrmVDZNHuP1RSu3pNG7Z77Sd2xSbcb/xWytAj9kyVs=
github.com/go-text/render v0.1.0/go.mod h1:jqEuNMenrmj6QRnkdpeaP0oKGFLDNhDkVKwGjsWWYU4=
github.com/go-text/typesetting v0.1.0 h1:vioSaLPYcHwPEPLT7gsjCGDCoYSbljxoHJzMnKwVvHw=
Expand Down
22 changes: 9 additions & 13 deletions internal/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
// ErrNoCommands indicates there were no commands to configure.
var (
ErrNoCommands = errors.New("no commands")
ErrUnknownSwitchState = errors.New("could not parse current state of switch")
ErrUnknownSwitchState = errors.New("could not parse switch state")
)

// Command represents a Command to run by a button or switch.
Expand Down Expand Up @@ -134,7 +134,7 @@ func (d *Controller) Setup(_ context.Context) error {
// the user.
//
//nolint:exhaustruct
func NewCommandsController(ctx context.Context, commandsFile string, device *mqtthass.Device) (*Controller, error) {
func NewCommandsController(_ context.Context, commandsFile string, device *mqtthass.Device) (*Controller, error) {
if _, err := os.Stat(commandsFile); errors.Is(err, os.ErrNotExist) {
return nil, ErrNoCommands
}
Expand All @@ -150,20 +150,12 @@ func NewCommandsController(ctx context.Context, commandsFile string, device *mqt
return nil, fmt.Errorf("could not parse commands file: %w", err)
}

controller := newController(ctx, device, cmds)

return controller, nil
}

// newController creates a new MQTT controller to manage a bunch of buttons and
// switches a user has defined.
func newController(_ context.Context, device *mqtthass.Device, commands *CommandList) *Controller {
controller := &Controller{
buttons: generateButtons(commands.Buttons, device),
switches: generateSwitches(commands.Switches, device),
buttons: generateButtons(cmds.Buttons, device),
switches: generateSwitches(cmds.Switches, device),
}

return controller
return controller, nil
}

// generateButtons will create MQTT entities for buttons defined by the
Expand Down Expand Up @@ -261,6 +253,10 @@ func generateSwitches(switchCmds []Command, device *mqtthass.Device) []*mqtthass
// expected to accept any input, or produce any consumable output, so only the
// return value is checked.
func switchCmd(command, state string) error {
if state == "" {
return ErrUnknownSwitchState
}

cmdElems := strings.Split(command, " ")
cmdElems = append(cmdElems, state)

Expand Down
287 changes: 287 additions & 0 deletions internal/commands/commands_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
// Copyright (c) 2024 Joshua Rich <[email protected]>
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

//nolint:containedctx,dupl,exhaustruct,nlreturn,paralleltest,wsl
//revive:disable:unused-receiver
package commands

import (
"context"
"encoding/json"
"os/exec"
"path/filepath"
"reflect"
"testing"

"github.com/eclipse/paho.golang/paho"
"github.com/go-test/deep"
mqtthass "github.com/joshuar/go-hass-anything/v9/pkg/hass"
mqttapi "github.com/joshuar/go-hass-anything/v9/pkg/mqtt"
"github.com/stretchr/testify/require"
)

var mockCommandCallback = func(_ *paho.Publish) {}

var mockButton = mqtthass.AsButton(
mqtthass.NewEntity("test", "test button", "test_button").
WithOriginInfo(&mqtthass.Origin{}).
WithDeviceInfo(&mqtthass.Device{}).
WithIcon("mdi:test").
WithCommandCallback(mockCommandCallback))

var mockSwitch = mqtthass.AsSwitch(
mqtthass.NewEntity("test", "test switch", "test_switch").
WithOriginInfo(&mqtthass.Origin{}).
WithDeviceInfo(&mqtthass.Device{}).
WithIcon("mdi:test").
WithCommandCallback(mockCommandCallback), true)

func TestController_Subscriptions(t *testing.T) {
var mockButtonSubscription, mockSwitchSubscription *mqttapi.Subscription
var err error

mockButtonSubscription, err = mockButton.MarshalSubscription()
require.NoError(t, err)
mockSwitchSubscription, err = mockSwitch.MarshalSubscription()
require.NoError(t, err)

type fields struct {
buttons []*mqtthass.ButtonEntity
switches []*mqtthass.SwitchEntity
}
tests := []struct {
name string
fields fields
want []*mqttapi.Subscription
}{
{
name: "with subscriptions",
fields: fields{buttons: []*mqtthass.ButtonEntity{mockButton}, switches: []*mqtthass.SwitchEntity{mockSwitch}},
want: []*mqttapi.Subscription{mockButtonSubscription, mockSwitchSubscription},
},
{
name: "without subscriptions",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := &Controller{
buttons: tt.fields.buttons,
switches: tt.fields.switches,
}
got := d.Subscriptions()
if diff := deep.Equal(got, tt.want); diff != nil {
t.Error(diff)
}
})
}
}

func TestController_Configs(t *testing.T) {
var mockButtonConfig, mockSwitchConfig *mqttapi.Msg
var err error

mockButtonConfig, err = mockButton.MarshalConfig()
require.NoError(t, err)
mockSwitchConfig, err = mockSwitch.MarshalConfig()
require.NoError(t, err)

type fields struct {
buttons []*mqtthass.ButtonEntity
switches []*mqtthass.SwitchEntity
}
tests := []struct {
name string
fields fields
want []*mqttapi.Msg
}{
{
name: "with configs",
fields: fields{buttons: []*mqtthass.ButtonEntity{mockButton}, switches: []*mqtthass.SwitchEntity{mockSwitch}},
want: []*mqttapi.Msg{mockButtonConfig, mockSwitchConfig},
},
{
name: "without configs",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := &Controller{
buttons: tt.fields.buttons,
switches: tt.fields.switches,
}
got := d.Configs()
if diff := deep.Equal(got, tt.want); diff != nil {
t.Error(diff)
}
})
}
}

func TestNewCommandsController(t *testing.T) {
// valid commands file
validCommandsFile, err := filepath.Abs("testdata/commands.toml")
require.NoError(t, err)

// invalid commands file
invalidCommandsFile, err := filepath.Abs("testdata/invalidcommands.toml")
require.NoError(t, err)

// unreadable commands file
unreadableCommandsFile := filepath.Join(t.TempDir(), "unreadable.toml")
_, err = exec.Command("touch", unreadableCommandsFile).Output()
require.NoError(t, err)
_, err = exec.Command("chmod", "a-r", unreadableCommandsFile).Output()
require.NoError(t, err)

mockDevice := &mqtthass.Device{}

type args struct {
ctx context.Context
device *mqtthass.Device
commandsFile string
}
tests := []struct {
want *Controller
args args
name string
wantErr bool
}{
{
name: "no commands file",
wantErr: true,
},
{
name: "unreadable commands file",
wantErr: true,
args: args{ctx: context.TODO(), commandsFile: unreadableCommandsFile, device: mockDevice},
},
{
name: "invalid commands file",
wantErr: true,
args: args{ctx: context.TODO(), commandsFile: invalidCommandsFile, device: mockDevice},
},
{
name: "valid commands file",
args: args{ctx: context.TODO(), commandsFile: validCommandsFile, device: mockDevice},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewCommandsController(tt.args.ctx, tt.args.commandsFile, tt.args.device)
if (err != nil) != tt.wantErr {
t.Errorf("NewCommandsController() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}

func Test_buttonCmd(t *testing.T) {
type args struct {
command string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "successful command",
args: args{command: "true"},
},
{
name: "unsuccessful command",
args: args{command: "false"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := buttonCmd(tt.args.command); (err != nil) != tt.wantErr {
t.Errorf("buttonCmd() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func Test_switchCmd(t *testing.T) {
type args struct {
command string
state string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "successful command",
args: args{command: "true", state: "someState"},
},
{
name: "unsuccessful command",
args: args{command: "false", state: "someState"},
wantErr: true,
},
{
name: "no state",
args: args{command: "true"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := switchCmd(tt.args.command, tt.args.state); (err != nil) != tt.wantErr {
t.Errorf("switchCmd() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func Test_switchState(t *testing.T) {
type args struct {
command string
}
tests := []struct {
name string
args args
want json.RawMessage
wantErr bool
}{
{
name: "switch ON",
args: args{command: "echo ON"},
want: json.RawMessage(`ON`),
},
{
name: "switch OFF",
args: args{command: "echo OFF"},
want: json.RawMessage(`OFF`),
},
{
name: "unsuccessful",
args: args{command: "false"},
wantErr: true,
},
{
name: "unknown output",
args: args{command: "echo SOMETHING"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := switchState(tt.args.command)
if (err != nil) != tt.wantErr {
t.Errorf("switchState() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("switchState() = %v, want %v", got, tt.want)
}
})
}
}
4 changes: 4 additions & 0 deletions internal/commands/testdata/commands.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[[button]]
name = "notify-send"
exec = 'notify-send "hello"'
icon = "mdi:chat"
4 changes: 4 additions & 0 deletions internal/commands/testdata/invalidcommands.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[[button
name "notify-send"
exec = 'notify-send "hello"'
icon = "mdi:chat"

0 comments on commit ff8c483

Please sign in to comment.