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") } } // 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) } } }