lutron-control/config_test.go
James Coleman b39600f64a
Some checks failed
Go package / build (push) Has been cancelled
Add scene-select channels, MQTT override, and release tooling; bump to 0.1.1
Restore uncommitted work from earlier sessions: DMX scene-select channels,
MQTT override handling, configurable zone refresh interval, OSC additions,
and matching tests and docs.

Add versioning following nginx-cache-purge: VERSION file, ldflags-stamped
build identifiers in info.go, Makefile, GoReleaser config, GitHub workflows,
and a -version flag using the standard flag package.

Claude-Session: https://claude.ai/code/session_01C5ZxhELATettecwYcNiXVN
2026-08-25 21:15:33 -05:00

140 lines
4.3 KiB
Go

package main
import (
"testing"
"github.com/kkyr/fig"
)
// TestExampleConfigValidates ensures the shipped config.example.yaml parses and
// passes cross-field validation, so the documented options stay in sync with the
// config structs.
func TestExampleConfigValidates(t *testing.T) {
cfg := &Config{Log: &LogConfig{}}
if err := fig.Load(cfg, fig.File("config.example.yaml"), fig.Dirs(".")); err != nil {
t.Fatalf("parse config.example.yaml: %s", err)
}
if err := cfg.Validate(); err != nil {
t.Fatalf("validate config.example.yaml: %s", err)
}
}
// baseConfig returns a minimal valid configuration with one serial device and an
// MQTT source, ready for per-test mutation.
func baseConfig() *Config {
return &Config{
Devices: []*DeviceConfig{{
Name: "grafik-eye",
Transport: "serial",
Serial: SerialConfig{Device: "/dev/ttyUSB0"},
Zones: 6,
}},
Sources: []*SourceConfig{{
Name: "home-assistant",
Type: "mqtt",
Device: "grafik-eye",
MQTT: MQTTConfig{Broker: "127.0.0.1", Topic: "lutron/qse-nwk"},
}},
}
}
// TestValidateMQTTLights verifies a light controlling zones 1-6 validates and that
// a zone outside the device's range is rejected.
func TestValidateMQTTLights(t *testing.T) {
c := baseConfig()
c.Sources[0].MQTT.Lights = []MQTTLightConfig{
{Name: "All Zones", Topic: "lutron/qse-nwk/all", Zones: []int{1, 2, 3, 4, 5, 6}},
}
if err := c.Validate(); err != nil {
t.Fatalf("valid lights config rejected: %s", err)
}
// A zone beyond the device's 6 zones must be rejected.
c.Sources[0].MQTT.Lights[0].Zones = []int{6, 7}
if err := c.Validate(); err == nil {
t.Error("expected error for out-of-range zone")
}
}
// TestValidateMQTTLightRequirements verifies per-light topic, zone-set, and
// duplicate-topic constraints.
func TestValidateMQTTLightRequirements(t *testing.T) {
// A light without a topic is rejected.
c := baseConfig()
c.Sources[0].MQTT.Lights = []MQTTLightConfig{{Zones: []int{1}}}
if err := c.Validate(); err == nil {
t.Error("expected error for missing light topic")
}
// A light with no zones is rejected.
c = baseConfig()
c.Sources[0].MQTT.Lights = []MQTTLightConfig{{Topic: "lutron/a"}}
if err := c.Validate(); err == nil {
t.Error("expected error for light with no zones")
}
// Two lights sharing a topic are rejected.
c = baseConfig()
c.Sources[0].MQTT.Lights = []MQTTLightConfig{
{Topic: "lutron/a", Zones: []int{1}},
{Topic: "lutron/a", Zones: []int{2}},
}
if err := c.Validate(); err == nil {
t.Error("expected error for duplicate light topic")
}
}
// TestValidatePriorityOverride verifies the temporary override must raise the
// source above its normal priority and that a negative window is rejected.
func TestValidatePriorityOverride(t *testing.T) {
c := baseConfig()
c.Sources[0].Priority = 1
c.Sources[0].OverrideSec = 60
c.Sources[0].OverridePriority = 1000
if err := c.Validate(); err != nil {
t.Fatalf("valid override config rejected: %s", err)
}
// An override that doesn't outrank the source's own priority takes control
// from nobody.
c.Sources[0].OverridePriority = 1
if err := c.Validate(); err == nil {
t.Error("expected error for override_priority not above priority")
}
c = baseConfig()
c.Sources[0].OverrideSec = -1
if err := c.Validate(); err == nil {
t.Error("expected error for negative override_sec")
}
}
// TestNewMQTTSourceSynthesizesLight verifies that, with no lights configured, a
// single light over every zone is synthesized from the base topic.
func TestNewMQTTSourceSynthesizesLight(t *testing.T) {
cfg := &SourceConfig{
Name: "home-assistant",
Type: "mqtt",
MQTT: MQTTConfig{Broker: "127.0.0.1", Topic: "lutron/qse-nwk", DeviceName: "Lutron"},
}
dev := NewDevice(&DeviceConfig{Name: "grafik-eye", Zones: 6})
binding := dev.RegisterSource(cfg.Name, cfg.Priority, 0, cfg.Fade)
s := newMQTTSource(cfg, dev, binding)
if len(s.lights) != 1 {
t.Fatalf("expected 1 synthesized light, got %d", len(s.lights))
}
l := s.lights[0]
if l.topic != "lutron/qse-nwk" || l.topicSet != "lutron/qse-nwk/set" {
t.Errorf("unexpected light topics: %q / %q", l.topic, l.topicSet)
}
want := []int{1, 2, 3, 4, 5, 6}
if len(l.zones) != len(want) {
t.Fatalf("zones = %v, want %v", l.zones, want)
}
for i, z := range want {
if l.zones[i] != z {
t.Errorf("zones[%d] = %d, want %d", i, l.zones[i], z)
}
}
}