package config import ( "os" "path/filepath" "strings" "testing" ) func TestDefaultValidate(t *testing.T) { if err := Default().Validate(); err != nil { t.Fatalf("default invalid: %v", err) } } func TestLoadAndValidate(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.json") os.WriteFile(path, []byte(`{"audio":{"toneLeftHz":900,"toneRightHz":1700,"toneAmplitude":0.3},"fm":{"frequencyMHz":99.9},"backend":{"kind":"file","outputPath":"out.f32"},"control":{"listenAddress":"127.0.0.1:8088"}}`), 0o644) cfg, err := Load(path) if err != nil { t.Fatalf("load: %v", err) } if cfg.Audio.ToneLeftHz != 900 { t.Fatalf("unexpected left tone: %v", cfg.Audio.ToneLeftHz) } } func TestValidateRejectsBadFrequency(t *testing.T) { cfg := Default() cfg.FM.FrequencyMHz = 200 if err := cfg.Validate(); err == nil { t.Fatal("expected error") } } func TestValidateRejectsBadPreEmphasis(t *testing.T) { cfg := Default() cfg.FM.PreEmphasisTauUS = 150 if err := cfg.Validate(); err == nil { t.Fatal("expected error") } } func TestDefaultPreEmphasis(t *testing.T) { if Default().FM.PreEmphasisTauUS != 50 { t.Fatal("expected 50") } } func TestDefaultFMModulation(t *testing.T) { cfg := Default() if !cfg.FM.FMModulationEnabled { t.Fatal("expected true") } if cfg.FM.MaxDeviationHz != 75000 { t.Fatal("expected 75000") } } func TestParsePI(t *testing.T) { tests := []struct { in string want uint16 ok bool }{ {"1234", 0x1234, true}, {"0xBEEF", 0xBEEF, true}, {"0XCAFE", 0xCAFE, true}, {" 0x2345 ", 0x2345, true}, {"", 0, false}, {"nope", 0, false}, } for _, tt := range tests { got, err := ParsePI(tt.in) if tt.ok && err != nil { t.Fatalf("ParsePI(%q): %v", tt.in, err) } if !tt.ok && err == nil { t.Fatalf("ParsePI(%q): expected error", tt.in) } if tt.ok && got != tt.want { t.Fatalf("ParsePI(%q): got %x want %x", tt.in, got, tt.want) } } } func TestValidateRejectsInvalidPI(t *testing.T) { cfg := Default() cfg.RDS.PI = "nope" if err := cfg.Validate(); err == nil { t.Fatal("expected error") } } func TestValidateRejectsEmptyPI(t *testing.T) { cfg := Default() cfg.RDS.PI = "" if err := cfg.Validate(); err == nil { t.Fatal("expected error") } } func TestValidateRejectsLongPS(t *testing.T) { cfg := Default() cfg.RDS.PS = "TOO_LONG_PS" if err := cfg.Validate(); err == nil { t.Fatal("expected error for PS longer than 8 characters") } } func TestValidateRejectsLongRadioText(t *testing.T) { cfg := Default() cfg.RDS.RadioText = strings.Repeat("x", 65) if err := cfg.Validate(); err == nil { t.Fatal("expected error for RadioText longer than 64 characters") } } func TestEffectiveDeviceRate(t *testing.T) { cfg := Default() if cfg.EffectiveDeviceRate() != float64(cfg.FM.CompositeRateHz) { t.Fatal("expected composite rate") } cfg.Backend.DeviceSampleRateHz = 912000 if cfg.EffectiveDeviceRate() != 912000 { t.Fatal("expected 912000") } } func TestValidateRejectsUnsupportedIngestKind(t *testing.T) { cfg := Default() cfg.Ingest.Kind = "unsupported" if err := cfg.Validate(); err == nil { t.Fatal("expected error") } } func TestValidateRejectsInvalidSRTConfig(t *testing.T) { cfg := Default() cfg.Ingest.Kind = "srt" cfg.Ingest.SRT.URL = "" if err := cfg.Validate(); err == nil { t.Fatal("expected srt url error") } cfg = Default() cfg.Ingest.Kind = "srt" cfg.Ingest.SRT.URL = "srt://127.0.0.1:9000" cfg.Ingest.SRT.Mode = "invalid" if err := cfg.Validate(); err == nil { t.Fatal("expected srt mode error") } cfg = Default() cfg.Ingest.Kind = "srt" cfg.Ingest.SRT.URL = "srt://127.0.0.1:9000" cfg.Ingest.SRT.SampleRateHz = 0 if err := cfg.Validate(); err == nil { t.Fatal("expected srt sample rate error") } cfg = Default() cfg.Ingest.Kind = "srt" cfg.Ingest.SRT.URL = "srt://127.0.0.1:9000" cfg.Ingest.SRT.Channels = 3 if err := cfg.Validate(); err == nil { t.Fatal("expected srt channels error") } } func TestValidateRejectsInvalidAES67Config(t *testing.T) { cfg := Default() cfg.Ingest.Kind = "aes67" cfg.Ingest.AES67.MulticastGroup = "" if err := cfg.Validate(); err == nil { t.Fatal("expected aes67 multicast group error") } cfg = Default() cfg.Ingest.Kind = "aes67" cfg.Ingest.AES67.MulticastGroup = "239.10.20.30" cfg.Ingest.AES67.Port = 5004 cfg.Ingest.AES67.Encoding = "L16" if err := cfg.Validate(); err == nil { t.Fatal("expected aes67 encoding error") } cfg = Default() cfg.Ingest.Kind = "aes67" cfg.Ingest.AES67.MulticastGroup = "239.10.20.30" cfg.Ingest.AES67.Port = 5004 cfg.Ingest.AES67.SDP = "v=0" cfg.Ingest.AES67.SDPPath = "stream.sdp" if err := cfg.Validate(); err == nil { t.Fatal("expected mutually exclusive sdp/sdpPath error") } } func TestValidateAcceptsAES67WithSDPOnly(t *testing.T) { cfg := Default() cfg.Ingest.Kind = "aes67" cfg.Ingest.AES67.MulticastGroup = "" cfg.Ingest.AES67.SDP = "v=0\r\ns=demo\r\nc=IN IP4 239.10.20.30\r\nm=audio 5004 RTP/AVP 97\r\na=rtpmap:97 L24/48000/2\r\n" if err := cfg.Validate(); err != nil { t.Fatalf("expected aes67 with SDP to validate: %v", err) } } func TestValidateAcceptsAES67WithDiscoveryOnly(t *testing.T) { cfg := Default() cfg.Ingest.Kind = "aes67" cfg.Ingest.AES67.MulticastGroup = "" cfg.Ingest.AES67.Port = 0 cfg.Ingest.AES67.Discovery.StreamName = "AES67-MAIN" if err := cfg.Validate(); err != nil { t.Fatalf("expected aes67 discovery config to validate: %v", err) } } func TestValidateRejectsAES67DiscoveryWithoutStreamName(t *testing.T) { cfg := Default() cfg.Ingest.Kind = "aes67" cfg.Ingest.AES67.MulticastGroup = "" cfg.Ingest.AES67.Port = 0 cfg.Ingest.AES67.Discovery.Enabled = true cfg.Ingest.AES67.Discovery.StreamName = "" if err := cfg.Validate(); err == nil { t.Fatal("expected discovery streamName validation error") } } func TestValidateRejectsAES67DiscoverySAPPortOutOfRange(t *testing.T) { cfg := Default() cfg.Ingest.Kind = "aes67" cfg.Ingest.AES67.MulticastGroup = "" cfg.Ingest.AES67.Port = 0 cfg.Ingest.AES67.Discovery.StreamName = "AES67-MAIN" cfg.Ingest.AES67.Discovery.SAPPort = 70000 if err := cfg.Validate(); err == nil { t.Fatal("expected discovery sapPort validation error") } } func TestValidateRejectsUnsupportedIngestPCMShape(t *testing.T) { cfg := Default() cfg.Ingest.Stdin.SampleRateHz = 0 if err := cfg.Validate(); err == nil { t.Fatal("expected sampleRate error") } cfg = Default() cfg.Ingest.HTTPRaw.Channels = 6 if err := cfg.Validate(); err == nil { t.Fatal("expected channels error") } cfg = Default() cfg.Ingest.Stdin.Format = "f32le" if err := cfg.Validate(); err == nil { t.Fatal("expected format error") } } func TestValidateRejectsUnsupportedIcecastDecoder(t *testing.T) { cfg := Default() cfg.Ingest.Icecast.Decoder = "mystery" if err := cfg.Validate(); err == nil { t.Fatal("expected decoder error") } } func TestValidateAcceptsIcecastDecoderFallbackAlias(t *testing.T) { cfg := Default() cfg.Ingest.Icecast.Decoder = "fallback" if err := cfg.Validate(); err != nil { t.Fatalf("expected fallback alias to be accepted: %v", err) } } func TestValidateRejectsIcecastRadioTextMaxLenOutOfRange(t *testing.T) { cfg := Default() cfg.Ingest.Icecast.RadioText.MaxLen = 65 if err := cfg.Validate(); err == nil { t.Fatal("expected maxLen error") } } func TestValidateRejectsReconnectWithMissingBackoff(t *testing.T) { cfg := Default() cfg.Ingest.Reconnect.Enabled = true cfg.Ingest.Reconnect.InitialBackoffMs = 0 if err := cfg.Validate(); err == nil { t.Fatal("expected reconnect backoff error") } } func TestValidateRejectsZeroMpxGain(t *testing.T) { cfg := Default() cfg.FM.MpxGain = 0 if err := cfg.Validate(); err == nil { t.Fatal("expected mpxGain error") } } func TestValidateRejectsInvalidStereoMode(t *testing.T) { cfg := Default() cfg.FM.StereoMode = "weird" if err := cfg.Validate(); err == nil { t.Fatal("expected stereoMode validation error") } } func TestValidateAcceptsStereoModes(t *testing.T) { for _, mode := range []string{"DSB", "SSB", "VSB"} { cfg := Default() cfg.FM.StereoMode = mode if err := cfg.Validate(); err != nil { t.Fatalf("mode %s should validate: %v", mode, err) } } }