package config_test import ( "os" "strings" "testing" "github.com/netsynth/netsynth/classify" "github.com/netsynth/netsynth/config" "github.com/netsynth/netsynth/synth" ) // writeTOML creates a temp TOML file with the given content and returns its path. func writeTOML(t *testing.T, content string) string { t.Helper() f, err := os.CreateTemp(t.TempDir(), "*.toml") if err != nil { t.Fatalf("CreateTemp: %v", err) } if _, err := f.WriteString(content); err != nil { t.Fatalf("WriteString: %v", err) } if err := f.Close(); err != nil { t.Fatalf("Close: %v", err) } return f.Name() } // TestLoadPartialOverrideFrequency: setting only frequency for ICMP overrides BaseHz, // leaves WaveformType unchanged (WaveformCustom), and leaves other classes unchanged. func TestLoadPartialOverrideFrequency(t *testing.T) { path := writeTOML(t, "[sounds.ICMP]\nfrequency = 100.0\n") result, err := config.Load(path) if err != nil { t.Fatalf("Load: %v", err) } cfgs := result.FreqCfgs if cfgs[classify.ClassICMP].BaseHz != 100.0 { t.Errorf("ICMP BaseHz: got %v, want 100.0", cfgs[classify.ClassICMP].BaseHz) } if cfgs[classify.ClassICMP].WaveformType != synth.WaveformCustom { t.Errorf("ICMP WaveformType: got %v, want WaveformCustom (0)", cfgs[classify.ClassICMP].WaveformType) } // DNS should be unchanged want := synth.ClassFreqConfigs[classify.ClassDNS].BaseHz if cfgs[classify.ClassDNS].BaseHz != want { t.Errorf("DNS BaseHz: got %v, want %v (default)", cfgs[classify.ClassDNS].BaseHz, want) } } // TestLoadPartialOverrideWaveform: setting only waveform for ICMP changes WaveformType, // leaves BaseHz unchanged, and regenerates Harmonics. func TestLoadPartialOverrideWaveform(t *testing.T) { path := writeTOML(t, "[sounds.ICMP]\nwaveform = \"square\"\n") result, err := config.Load(path) if err != nil { t.Fatalf("Load: %v", err) } cfgs := result.FreqCfgs if cfgs[classify.ClassICMP].WaveformType != synth.WaveformSquare { t.Errorf("ICMP WaveformType: got %v, want WaveformSquare", cfgs[classify.ClassICMP].WaveformType) } // BaseHz should be unchanged (default is 65.0) if cfgs[classify.ClassICMP].BaseHz != 65.0 { t.Errorf("ICMP BaseHz: got %v, want 65.0 (default)", cfgs[classify.ClassICMP].BaseHz) } // Harmonics should be regenerated (non-empty) if len(cfgs[classify.ClassICMP].Harmonics) == 0 { t.Error("ICMP Harmonics: got empty slice, expected regenerated harmonics for WaveformSquare") } } // TestLoadBothOverrides: setting both frequency and waveform applies both. func TestLoadBothOverrides(t *testing.T) { path := writeTOML(t, "[sounds.ICMP]\nfrequency = 100.0\nwaveform = \"square\"\n") result, err := config.Load(path) if err != nil { t.Fatalf("Load: %v", err) } cfgs := result.FreqCfgs if cfgs[classify.ClassICMP].BaseHz != 100.0 { t.Errorf("ICMP BaseHz: got %v, want 100.0", cfgs[classify.ClassICMP].BaseHz) } if cfgs[classify.ClassICMP].WaveformType != synth.WaveformSquare { t.Errorf("ICMP WaveformType: got %v, want WaveformSquare", cfgs[classify.ClassICMP].WaveformType) } } // TestLoadUnknownKey: a typo'd field name produces an error naming the bad key. func TestLoadUnknownKey(t *testing.T) { path := writeTOML(t, "[sounds.ICMP]\nfrequncy = 440\n") _, err := config.Load(path) if err == nil { t.Fatal("expected error for unknown key 'frequncy', got nil") } if !strings.Contains(err.Error(), "frequncy") { t.Errorf("error should name the bad key 'frequncy', got: %v", err) } } // TestLoadNoConfig: Load("") in a directory with no netsynth.toml returns defaults with no error. func TestLoadNoConfig(t *testing.T) { // Chdir to a temp dir that has no netsynth.toml t.Chdir(t.TempDir()) result, err := config.Load("") if err != nil { t.Fatalf("Load with no config: %v", err) } cfgs := result.FreqCfgs if len(cfgs) != 14 { t.Errorf("result map size: got %d, want 14", len(cfgs)) } // ICMP should be at its default BaseHz (65.0) if cfgs[classify.ClassICMP].BaseHz != 65.0 { t.Errorf("ICMP BaseHz: got %v, want 65.0 (default)", cfgs[classify.ClassICMP].BaseHz) } } // TestLoadExplicitMissing: an explicit path that doesn't exist returns an error containing "not found". func TestLoadExplicitMissing(t *testing.T) { _, err := config.Load("/nonexistent/path/config.toml") if err == nil { t.Fatal("expected error for missing explicit file, got nil") } if !strings.Contains(err.Error(), "not found") { t.Errorf("error should contain 'not found', got: %v", err) } } // TestLoadUnknownClass: unknown class name produces no error (warning only), result has 14 entries. func TestLoadUnknownClass(t *testing.T) { path := writeTOML(t, "[sounds.BOGUS]\nfrequency = 100.0\n") result, err := config.Load(path) if err != nil { t.Fatalf("Load with unknown class: %v", err) } cfgs := result.FreqCfgs if len(cfgs) != 14 { t.Errorf("result map size: got %d, want 14 (BOGUS should not appear)", len(cfgs)) } // Confirm BOGUS is NOT in the map if _, ok := cfgs["BOGUS"]; ok { t.Error("BOGUS class should not be present in result map") } } // TestLoadInvalidWaveform: an invalid waveform string produces an error containing "invalid waveform". func TestLoadInvalidWaveform(t *testing.T) { path := writeTOML(t, "[sounds.ICMP]\nwaveform = \"invalid\"\n") _, err := config.Load(path) if err == nil { t.Fatal("expected error for invalid waveform, got nil") } if !strings.Contains(err.Error(), "invalid waveform") { t.Errorf("error should contain 'invalid waveform', got: %v", err) } } // TestLoadAllDefaultsPresent: regardless of overrides, all 14 default classes are in the result map. func TestLoadAllDefaultsPresent(t *testing.T) { path := writeTOML(t, "[sounds.ICMP]\nfrequency = 200.0\n") result, err := config.Load(path) if err != nil { t.Fatalf("Load: %v", err) } cfgs := result.FreqCfgs if len(cfgs) != 14 { t.Errorf("result map size: got %d, want 14", len(cfgs)) } for _, class := range classify.AllClasses() { if _, ok := cfgs[class]; !ok { t.Errorf("class %q missing from result map", class) } } } // --- New tests for Phase 7 Plan 01 --- // TestLoadCustomRules: TOML with [[rules]] block (port=8080, protocol="tcp", class="MyApp") // plus [sounds.MyApp] (frequency=300.0) parses successfully. func TestLoadCustomRules(t *testing.T) { toml := ` [[rules]] port = 8080 protocol = "tcp" class = "MyApp" [sounds.MyApp] frequency = 300.0 ` path := writeTOML(t, toml) result, err := config.Load(path) if err != nil { t.Fatalf("Load: %v", err) } if len(result.UserRules) != 1 { t.Fatalf("UserRules len: got %d, want 1", len(result.UserRules)) } rule := result.UserRules[0] if rule.Protocol != "tcp" { t.Errorf("UserRules[0].Protocol: got %q, want %q", rule.Protocol, "tcp") } if rule.DstPort != 8080 { t.Errorf("UserRules[0].DstPort: got %d, want 8080", rule.DstPort) } if rule.Class != "MyApp" { t.Errorf("UserRules[0].Class: got %q, want %q", rule.Class, "MyApp") } if result.FreqCfgs["MyApp"].BaseHz != 300.0 { t.Errorf("FreqCfgs[MyApp].BaseHz: got %v, want 300.0", result.FreqCfgs["MyApp"].BaseHz) } } // TestLoadCustomRuleNoPort: TOML with [[rules]] (protocol="udp", class="AllUDP", no port field) // parses; UserRules[0].DstPort == 0. func TestLoadCustomRuleNoPort(t *testing.T) { toml := ` [[rules]] protocol = "udp" class = "AllUDP" ` path := writeTOML(t, toml) result, err := config.Load(path) if err != nil { t.Fatalf("Load: %v", err) } if len(result.UserRules) != 1 { t.Fatalf("UserRules len: got %d, want 1", len(result.UserRules)) } if result.UserRules[0].DstPort != 0 { t.Errorf("UserRules[0].DstPort: got %d, want 0", result.UserRules[0].DstPort) } } // TestLoadCustomRuleMissingProtocol: [[rules]] with class="X" but no protocol -> error containing "protocol is required". func TestLoadCustomRuleMissingProtocol(t *testing.T) { toml := ` [[rules]] class = "X" ` path := writeTOML(t, toml) _, err := config.Load(path) if err == nil { t.Fatal("expected error for missing protocol, got nil") } if !strings.Contains(err.Error(), "protocol is required") { t.Errorf("error should contain 'protocol is required', got: %v", err) } } // TestLoadCustomRuleMissingClass: [[rules]] with protocol="tcp" but no class -> error containing "class is required". func TestLoadCustomRuleMissingClass(t *testing.T) { toml := ` [[rules]] protocol = "tcp" ` path := writeTOML(t, toml) _, err := config.Load(path) if err == nil { t.Fatal("expected error for missing class, got nil") } if !strings.Contains(err.Error(), "class is required") { t.Errorf("error should contain 'class is required', got: %v", err) } } // TestLoadCustomRuleInvalidProtocol: [[rules]] with protocol="ftp" -> error containing "invalid protocol". func TestLoadCustomRuleInvalidProtocol(t *testing.T) { toml := ` [[rules]] protocol = "ftp" class = "FTPTraffic" ` path := writeTOML(t, toml) _, err := config.Load(path) if err == nil { t.Fatal("expected error for invalid protocol 'ftp', got nil") } if !strings.Contains(err.Error(), "invalid protocol") { t.Errorf("error should contain 'invalid protocol', got: %v", err) } } // TestLoadCustomRuleUnknownField: [[rules]] with typo_field="bad" -> error containing "typo_field". func TestLoadCustomRuleUnknownField(t *testing.T) { toml := ` [[rules]] protocol = "tcp" class = "SomeClass" typo_field = "bad" ` path := writeTOML(t, toml) _, err := config.Load(path) if err == nil { t.Fatal("expected error for unknown field 'typo_field', got nil") } if !strings.Contains(err.Error(), "typo_field") { t.Errorf("error should contain 'typo_field', got: %v", err) } } // TestUserRulesPrepend: Load returns UserRules separately from FreqCfgs; // caller can do append(result.UserRules, classify.DefaultRules...) to get user rules first. func TestUserRulesPrepend(t *testing.T) { toml := ` [[rules]] protocol = "tcp" port = 9000 class = "MyService" ` path := writeTOML(t, toml) result, err := config.Load(path) if err != nil { t.Fatalf("Load: %v", err) } combined := append(result.UserRules, classify.DefaultRules...) if len(combined) != len(classify.DefaultRules)+1 { t.Errorf("combined rules len: got %d, want %d", len(combined), len(classify.DefaultRules)+1) } // User rule should be first if combined[0].Class != "MyService" { t.Errorf("first rule should be user rule 'MyService', got %q", combined[0].Class) } } // TestAutoFreqAssignment: TOML with [[rules]] (class="GameServer", protocol="tcp") and // NO [sounds.GameServer] -> FreqCfgs contains "GameServer" entry with BaseHz in [1200, 2350] // and WaveformType == WaveformSine. func TestAutoFreqAssignment(t *testing.T) { toml := ` [[rules]] protocol = "tcp" class = "GameServer" ` path := writeTOML(t, toml) result, err := config.Load(path) if err != nil { t.Fatalf("Load: %v", err) } cfg, ok := result.FreqCfgs["GameServer"] if !ok { t.Fatal("FreqCfgs should contain 'GameServer' entry from auto-freq assignment") } if cfg.BaseHz < 1200.0 || cfg.BaseHz > 2350.0 { t.Errorf("GameServer BaseHz: got %v, want in [1200, 2350]", cfg.BaseHz) } if cfg.WaveformType != synth.WaveformSine { t.Errorf("GameServer WaveformType: got %v, want WaveformSine", cfg.WaveformType) } } // TestAutoFreqDeterministic: Two Load() calls with same class name produce same BaseHz. func TestAutoFreqDeterministic(t *testing.T) { toml := ` [[rules]] protocol = "tcp" class = "MyDeterministicClass" ` path := writeTOML(t, toml) result1, err := config.Load(path) if err != nil { t.Fatalf("Load (1): %v", err) } result2, err := config.Load(path) if err != nil { t.Fatalf("Load (2): %v", err) } hz1 := result1.FreqCfgs["MyDeterministicClass"].BaseHz hz2 := result2.FreqCfgs["MyDeterministicClass"].BaseHz if hz1 != hz2 { t.Errorf("auto-freq not deterministic: first=%v, second=%v", hz1, hz2) } } // TestAutoFreqSkipsBuiltins: TOML with [[rules]] (class="HTTPS", protocol="tcp", port=443) // -> FreqCfgs["HTTPS"].BaseHz == 175.0 (the default), NOT an auto-assigned value. func TestAutoFreqSkipsBuiltins(t *testing.T) { toml := ` [[rules]] protocol = "tcp" port = 443 class = "HTTPS" ` path := writeTOML(t, toml) result, err := config.Load(path) if err != nil { t.Fatalf("Load: %v", err) } if result.FreqCfgs[classify.ClassHTTPS].BaseHz != 175.0 { t.Errorf("HTTPS BaseHz: got %v, want 175.0 (default, not auto-assigned)", result.FreqCfgs[classify.ClassHTTPS].BaseHz) } } // TestLoadResultConfigPath: Load(explicit_path) -> LoadResult.ConfigPath == explicit_path; // Load("") with no file -> LoadResult.ConfigPath == "". func TestLoadResultConfigPath(t *testing.T) { // explicit path path := writeTOML(t, "[sounds.ICMP]\nfrequency = 100.0\n") result, err := config.Load(path) if err != nil { t.Fatalf("Load: %v", err) } if result.ConfigPath != path { t.Errorf("ConfigPath: got %q, want %q", result.ConfigPath, path) } // empty path with no config file t.Chdir(t.TempDir()) result2, err := config.Load("") if err != nil { t.Fatalf("Load with no config: %v", err) } if result2.ConfigPath != "" { t.Errorf("ConfigPath for no-config: got %q, want %q", result2.ConfigPath, "") } } // TestLoadNoConfigReturnsLoadResult: Load("") in empty dir returns LoadResult with // len(FreqCfgs)==14, len(UserRules)==0, ConfigPath=="". func TestLoadNoConfigReturnsLoadResult(t *testing.T) { t.Chdir(t.TempDir()) result, err := config.Load("") if err != nil { t.Fatalf("Load: %v", err) } if len(result.FreqCfgs) != 14 { t.Errorf("FreqCfgs len: got %d, want 14", len(result.FreqCfgs)) } if len(result.UserRules) != 0 { t.Errorf("UserRules len: got %d, want 0", len(result.UserRules)) } if result.ConfigPath != "" { t.Errorf("ConfigPath: got %q, want %q", result.ConfigPath, "") } }