From 7e81ff7d5d8f3f36d31dfbbc2485c79ce5f273bd Mon Sep 17 00:00:00 2001 From: Joshua Rich Date: Sun, 22 Dec 2024 15:34:44 +1000 Subject: [PATCH] test(hass): :white_check_mark: add tests for new options pattern --- .golangci.yaml | 2 + internal/hass/sensor/entities_test.go | 410 ++++++++++++++++++++++++++ internal/hass/sensor/requests.go | 42 +-- internal/hass/sensor/requests_test.go | 101 +++++++ 4 files changed, 536 insertions(+), 19 deletions(-) create mode 100644 internal/hass/sensor/entities_test.go create mode 100644 internal/hass/sensor/requests_test.go diff --git a/.golangci.yaml b/.golangci.yaml index 55942a35f..9ee8ba1ae 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -126,6 +126,8 @@ issues: source: "^//go:generate " - path: '(.+)_test\.go' text: "copies lock" + - path: '(.+)_test\.go' + text: "unused-parameter" - path: '(.+)_test\.go' linters: - funlen diff --git a/internal/hass/sensor/entities_test.go b/internal/hass/sensor/entities_test.go new file mode 100644 index 000000000..685162d88 --- /dev/null +++ b/internal/hass/sensor/entities_test.go @@ -0,0 +1,410 @@ +// Copyright 2024 Joshua Rich . +// SPDX-License-Identifier: MIT + +//revive:disable:unused-receiver +package sensor + +import ( + "reflect" + "testing" + + "github.com/joshuar/go-hass-agent/internal/hass/sensor/types" +) + +func TestState_UpdateValue(t *testing.T) { + type fields struct { + Value any + Attributes map[string]any + Icon string + } + type args struct { + value any + } + tests := []struct { + name string + fields fields + args args + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &State{ + Value: tt.fields.Value, + Attributes: tt.fields.Attributes, + Icon: tt.fields.Icon, + } + s.UpdateValue(tt.args.value) + }) + } +} + +func TestState_UpdateIcon(t *testing.T) { + type fields struct { + Value any + Attributes map[string]any + Icon string + } + type args struct { + icon string + } + tests := []struct { + name string + fields fields + args args + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &State{ + Value: tt.fields.Value, + Attributes: tt.fields.Attributes, + Icon: tt.fields.Icon, + } + s.UpdateIcon(tt.args.icon) + }) + } +} + +func TestState_UpdateAttribute(t *testing.T) { + type fields struct { + Value any + Attributes map[string]any + Icon string + } + type args struct { + key string + value any + } + tests := []struct { + name string + fields fields + args args + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &State{ + Value: tt.fields.Value, + Attributes: tt.fields.Attributes, + Icon: tt.fields.Icon, + } + s.UpdateAttribute(tt.args.key, tt.args.value) + }) + } +} + +func TestState_Validate(t *testing.T) { + type fields struct { + Value any + Attributes map[string]any + Icon string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &State{ + Value: tt.fields.Value, + Attributes: tt.fields.Attributes, + Icon: tt.fields.Icon, + } + if err := s.Validate(); (err != nil) != tt.wantErr { + t.Errorf("State.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestNewSensor(t *testing.T) { + type args struct { + options []Option[Entity] + } + tests := []struct { + name string + args args + want Entity + }{ + { + name: "sensor", + args: args{ + []Option[Entity]{ + WithName("Mock Entity"), + WithID("mock_entity"), + AsTypeSensor(), + WithUnits("units"), + AsDiagnostic(), + WithState( + WithValue("value"), + WithIcon("mdi:icon"), + ), + }, + }, + want: Entity{ + Name: "Mock Entity", + ID: "mock_entity", + EntityType: types.Sensor, + Units: "units", + Category: types.CategoryDiagnostic, + State: &State{ + Value: "value", + Icon: "mdi:icon", + }, + }, + }, + { + name: "binary_sensor", + args: args{ + []Option[Entity]{ + WithName("Mock Entity"), + WithID("mock_entity"), + AsTypeBinarySensor(), + WithUnits("units"), + AsDiagnostic(), + WithState( + WithValue("value"), + WithIcon("mdi:icon"), + ), + }, + }, + want: Entity{ + Name: "Mock Entity", + ID: "mock_entity", + EntityType: types.BinarySensor, + Units: "units", + Category: types.CategoryDiagnostic, + State: &State{ + Value: "value", + Icon: "mdi:icon", + }, + }, + }, + { + name: "set_single_attribute", + args: args{ + []Option[Entity]{ + WithName("Mock Entity"), + WithID("mock_entity"), + WithState( + WithValue("value"), + WithIcon("mdi:icon"), + WithAttribute("attr", "attr_value"), + ), + }, + }, + want: Entity{ + Name: "Mock Entity", + ID: "mock_entity", + State: &State{ + Value: "value", + Icon: "mdi:icon", + Attributes: map[string]any{"attr": "attr_value"}, + }, + }, + }, + { + name: "set_multiple_attributes", + args: args{ + []Option[Entity]{ + WithName("Mock Entity"), + WithID("mock_entity"), + WithState( + WithValue("value"), + WithIcon("mdi:icon"), + WithAttributes(map[string]any{"attr1": "attr_value", "attr2": "attr_value"}), + ), + }, + }, + want: Entity{ + Name: "Mock Entity", + ID: "mock_entity", + State: &State{ + Value: "value", + Icon: "mdi:icon", + Attributes: map[string]any{"attr1": "attr_value", "attr2": "attr_value"}, + }, + }, + }, + { + name: "set_data_source", + args: args{ + []Option[Entity]{ + WithName("Mock Entity"), + WithID("mock_entity"), + WithState( + WithValue("value"), + WithIcon("mdi:icon"), + WithDataSourceAttribute("source"), + WithAttributes(map[string]any{"attr1": "attr_value", "attr2": "attr_value"}), + ), + }, + }, + want: Entity{ + Name: "Mock Entity", + ID: "mock_entity", + State: &State{ + Value: "value", + Icon: "mdi:icon", + Attributes: map[string]any{"data_source": "source", "attr1": "attr_value", "attr2": "attr_value"}, + }, + }, + }, + { + name: "retryable", + args: args{ + []Option[Entity]{ + WithName("Mock Entity"), + WithID("mock_entity"), + AsTypeSensor(), + WithUnits("units"), + AsDiagnostic(), + WithState( + WithValue("value"), + WithIcon("mdi:icon"), + ), + WithRequestRetry(true), + }, + }, + want: Entity{ + Name: "Mock Entity", + ID: "mock_entity", + EntityType: types.Sensor, + Units: "units", + Category: types.CategoryDiagnostic, + State: &State{ + Value: "value", + Icon: "mdi:icon", + }, + requestMetadata: requestMetadata{RetryRequest: true}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewSensor(tt.args.options...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewSensor() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEntity_UpdateState(t *testing.T) { + type fields struct { + State *State + requestMetadata requestMetadata + ID string + Name string + Units string + EntityType types.SensorType + DeviceClass types.DeviceClass + StateClass types.StateClass + Category types.Category + } + type args struct { + options []Option[State] + } + tests := []struct { + name string + fields fields + args args + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Entity{ + State: tt.fields.State, + requestMetadata: tt.fields.requestMetadata, + ID: tt.fields.ID, + Name: tt.fields.Name, + Units: tt.fields.Units, + EntityType: tt.fields.EntityType, + DeviceClass: tt.fields.DeviceClass, + StateClass: tt.fields.StateClass, + Category: tt.fields.Category, + } + e.UpdateState(tt.args.options...) + }) + } +} + +func TestEntity_Validate(t *testing.T) { + type fields struct { + State *State + requestMetadata requestMetadata + ID string + Name string + Units string + EntityType types.SensorType + DeviceClass types.DeviceClass + StateClass types.StateClass + Category types.Category + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Entity{ + State: tt.fields.State, + requestMetadata: tt.fields.requestMetadata, + ID: tt.fields.ID, + Name: tt.fields.Name, + Units: tt.fields.Units, + EntityType: tt.fields.EntityType, + DeviceClass: tt.fields.DeviceClass, + StateClass: tt.fields.StateClass, + Category: tt.fields.Category, + } + if err := e.Validate(); (err != nil) != tt.wantErr { + t.Errorf("Entity.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestLocation_Validate(t *testing.T) { + type fields struct { + Gps []float64 + GpsAccuracy int + Battery int + Speed int + Altitude int + Course int + VerticalAccuracy int + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := &Location{ + Gps: tt.fields.Gps, + GpsAccuracy: tt.fields.GpsAccuracy, + Battery: tt.fields.Battery, + Speed: tt.fields.Speed, + Altitude: tt.fields.Altitude, + Course: tt.fields.Course, + VerticalAccuracy: tt.fields.VerticalAccuracy, + } + if err := l.Validate(); (err != nil) != tt.wantErr { + t.Errorf("Location.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/hass/sensor/requests.go b/internal/hass/sensor/requests.go index 3298385f9..9dc716f63 100644 --- a/internal/hass/sensor/requests.go +++ b/internal/hass/sensor/requests.go @@ -9,6 +9,27 @@ const ( requestTypeLocation = "update_location" ) +type stateRequestBody struct { + State any `json:"state" validate:"required"` + Attributes map[string]any `json:"attributes,omitempty" validate:"omitempty"` + Icon string `json:"icon,omitempty" validate:"omitempty,startswith=mdi:"` + ID string `json:"unique_id" validate:"required"` + EntityType string `json:"type" validate:"omitempty"` +} + +type registrationRequestBody struct { + State any `json:"state" validate:"required"` + Attributes map[string]any `json:"attributes,omitempty" validate:"omitempty"` + Icon string `json:"icon,omitempty" validate:"omitempty,startswith=mdi:"` + ID string `json:"unique_id" validate:"required"` + EntityType string `json:"type" validate:"omitempty"` + Name string `json:"name" validate:"required"` + Units string `json:"unit_of_measurement,omitempty" validate:"omitempty"` + DeviceClass string `json:"device_class,omitempty" validate:"omitempty"` + StateClass string `json:"state_class,omitempty" validate:"omitempty"` + Category string `json:"entity_category,omitempty" validate:"omitempty"` +} + // Request represents a sensor request, either a registration, update or // location update. type Request struct { @@ -29,13 +50,7 @@ func (r *Request) Retry() bool { func AsSensorUpdate(entity Entity) Option[Request] { return func(request Request) Request { request.RequestType = requestTypeUpdateSensor - request.Data = &struct { - State any `json:"state" validate:"required"` - Attributes map[string]any `json:"attributes,omitempty" validate:"omitempty"` - Icon string `json:"icon,omitempty" validate:"omitempty,startswith=mdi:"` - ID string `json:"unique_id" validate:"required"` - EntityType string `json:"type" validate:"omitempty"` - }{ + request.Data = &stateRequestBody{ State: entity.Value, Attributes: entity.Attributes, Icon: entity.Icon, @@ -52,18 +67,7 @@ func AsSensorUpdate(entity Entity) Option[Request] { func AsSensorRegistration(entity Entity) Option[Request] { return func(request Request) Request { request.RequestType = requestTypeRegisterSensor - request.Data = &struct { - State any `json:"state" validate:"required"` - Attributes map[string]any `json:"attributes,omitempty" validate:"omitempty"` - Icon string `json:"icon,omitempty" validate:"omitempty,startswith=mdi:"` - ID string `json:"unique_id" validate:"required"` - EntityType string `json:"type" validate:"omitempty"` - Name string `json:"name" validate:"required"` - Units string `json:"unit_of_measurement,omitempty" validate:"omitempty"` - DeviceClass string `json:"device_class,omitempty" validate:"omitempty"` - StateClass string `json:"state_class,omitempty" validate:"omitempty"` - Category string `json:"entity_category,omitempty" validate:"omitempty"` - }{ + request.Data = ®istrationRequestBody{ State: entity.Value, Attributes: entity.Attributes, Icon: entity.Icon, diff --git a/internal/hass/sensor/requests_test.go b/internal/hass/sensor/requests_test.go new file mode 100644 index 000000000..979aabe89 --- /dev/null +++ b/internal/hass/sensor/requests_test.go @@ -0,0 +1,101 @@ +// Copyright 2024 Joshua Rich . +// SPDX-License-Identifier: MIT + +package sensor + +import ( + "reflect" + "testing" +) + +func requestsEqual(t *testing.T, got, want *Request) bool { + t.Helper() + switch { + case !reflect.DeepEqual(got.RequestType, want.RequestType): + t.Error("request type does not match") + return false + case !reflect.DeepEqual(got.Data, want.Data): + t.Error("request data does not match") + return false + } + return true +} + +func TestNewRequest(t *testing.T) { + mockEntity := NewSensor( + WithName("Mock Entity"), + WithID("mock_entity"), + WithState( + WithValue("value"), + ), + ) + + mockLocation := Location{ + Gps: []float64{0.1, 0.2}, + } + + type args struct { + options []Option[Request] + } + tests := []struct { + name string + args args + want *Request + }{ + { + name: "registration", + args: args{options: []Option[Request]{AsSensorRegistration(mockEntity)}}, + want: &Request{ + RequestType: requestTypeRegisterSensor, + Data: ®istrationRequestBody{ + State: "value", + ID: "mock_entity", + Name: "Mock Entity", + EntityType: "sensor", + }, + }, + }, + { + name: "update", + args: args{options: []Option[Request]{AsSensorUpdate(mockEntity)}}, + want: &Request{ + RequestType: requestTypeUpdateSensor, + Data: &stateRequestBody{ + State: "value", + ID: "mock_entity", + EntityType: "sensor", + }, + }, + }, + { + name: "location", + args: args{options: []Option[Request]{AsLocationUpdate(mockLocation)}}, + want: &Request{ + RequestType: requestTypeLocation, + Data: &Location{ + Gps: []float64{0.1, 0.2}, + }, + }, + }, + { + name: "retryable", + args: args{options: []Option[Request]{AsSensorUpdate(mockEntity), AsRetryable(true)}}, + want: &Request{ + RequestType: requestTypeUpdateSensor, + Data: &stateRequestBody{ + State: "value", + ID: "mock_entity", + EntityType: "sensor", + }, + retryable: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewRequest(tt.args.options...); !requestsEqual(t, got, tt.want) { + t.Errorf("NewRequest() = %v, want %v", got, tt.want) + } + }) + } +}