package shorewall import ( "os" "path/filepath" "testing" ) func TestParseFile_Basic(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "zones") content := `# This is a comment fw firewall net ipv4 loc ipv4 ` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatal(err) } rows, err := ParseFile(path) if err != nil { t.Fatalf("ParseFile: %v", err) } if len(rows) != 3 { t.Fatalf("expected 3 rows, got %d", len(rows)) } if rows[0][0] != "fw" || rows[0][1] != "firewall" { t.Errorf("row 0 = %v, want [fw firewall]", rows[0]) } if rows[1][0] != "net" || rows[1][1] != "ipv4" { t.Errorf("row 1 = %v, want [net ipv4]", rows[1]) } } func TestParseFile_BlankLinesAndComments(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test") content := ` # full line comment # indented comment field1 field2 ` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatal(err) } rows, err := ParseFile(path) if err != nil { t.Fatalf("ParseFile: %v", err) } if len(rows) != 1 { t.Fatalf("expected 1 row, got %d", len(rows)) } if rows[0][0] != "field1" || rows[0][1] != "field2" { t.Errorf("row 0 = %v, want [field1 field2]", rows[0]) } } func TestParseFile_Continuation(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test") content := `first \ second third ` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatal(err) } rows, err := ParseFile(path) if err != nil { t.Fatalf("ParseFile: %v", err) } if len(rows) != 1 { t.Fatalf("expected 1 row, got %d", len(rows)) } if len(rows[0]) != 3 { t.Fatalf("expected 3 fields, got %d: %v", len(rows[0]), rows[0]) } if rows[0][0] != "first" || rows[0][1] != "second" || rows[0][2] != "third" { t.Errorf("row 0 = %v, want [first second third]", rows[0]) } } func TestParseFile_QuestionDirective(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test") content := `?COMMENT this is a comment directive field1 field2 ` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatal(err) } rows, err := ParseFile(path) if err != nil { t.Fatalf("ParseFile: %v", err) } // ?COMMENT lines should be skipped (starts with ?) if len(rows) != 1 { t.Fatalf("expected 1 row, got %d: %v", len(rows), rows) } if rows[0][0] != "field1" { t.Errorf("expected field1, got %s", rows[0][0]) } } func TestParseFile_SectionMarker(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test") // Note: ?SECTION lines are caught by the generic "?" prefix handler // before the SECTION check, so only bare SECTION lines produce markers. content := `SECTION NEW ACCEPT net fw tcp 22 SECTION ESTABLISHED ACCEPT all all ` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatal(err) } rows, err := ParseFile(path) if err != nil { t.Fatalf("ParseFile: %v", err) } if len(rows) != 4 { t.Fatalf("expected 4 rows, got %d: %v", len(rows), rows) } // First row should be the SECTION marker if rows[0][0] != "?SECTION" || rows[0][1] != "NEW" { t.Errorf("row 0 = %v, want [?SECTION NEW]", rows[0]) } // Second row is a regular rule if rows[1][0] != "ACCEPT" { t.Errorf("row 1[0] = %s, want ACCEPT", rows[1][0]) } // Third row is SECTION ESTABLISHED if rows[2][0] != "?SECTION" || rows[2][1] != "ESTABLISHED" { t.Errorf("row 2 = %v, want [?SECTION ESTABLISHED]", rows[2]) } // Fourth row is the rule if rows[3][0] != "ACCEPT" { t.Errorf("row 3[0] = %s, want ACCEPT", rows[3][0]) } } func TestParseFile_NotExist(t *testing.T) { rows, err := ParseFile("/nonexistent/path/zones") if err != nil { t.Fatalf("expected nil error for nonexistent file, got %v", err) } if rows != nil { t.Fatalf("expected nil rows, got %v", rows) } } func TestParseConf(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "shorewall.conf") content := `# Shorewall config IP_FORWARDING=Yes LOG_LEVEL=info STARTUP_ENABLED=Yes QUOTED_VALUE="some value" SINGLE_QUOTED='another' ` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatal(err) } conf, err := ParseConf(path) if err != nil { t.Fatalf("ParseConf: %v", err) } tests := map[string]string{ "IP_FORWARDING": "Yes", "LOG_LEVEL": "info", "STARTUP_ENABLED": "Yes", "QUOTED_VALUE": "some value", "SINGLE_QUOTED": "another", } for k, want := range tests { got, ok := conf[k] if !ok { t.Errorf("key %q not found in conf", k) continue } if got != want { t.Errorf("conf[%q] = %q, want %q", k, got, want) } } } func TestParseConf_CommentsAndBlanks(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "conf") content := ` # comment KEY1=val1 # another comment KEY2=val2 ` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatal(err) } conf, err := ParseConf(path) if err != nil { t.Fatalf("ParseConf: %v", err) } if len(conf) != 2 { t.Fatalf("expected 2 entries, got %d", len(conf)) } } func TestParseConf_NoEquals(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "conf") content := `NOEQUALS KEY=val ` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatal(err) } conf, err := ParseConf(path) if err != nil { t.Fatalf("ParseConf: %v", err) } if len(conf) != 1 { t.Fatalf("expected 1 entry (lines without = skipped), got %d", len(conf)) } if conf["KEY"] != "val" { t.Errorf("conf[KEY] = %q, want %q", conf["KEY"], "val") } } func TestParseParams(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "params") content := `# params file NET_IF=eth0 LOC_IF=eth1 NET_ADDR=192.168.1.0/24 ` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatal(err) } params, err := ParseParams(path) if err != nil { t.Fatalf("ParseParams: %v", err) } if params["NET_IF"] != "eth0" { t.Errorf("NET_IF = %q, want %q", params["NET_IF"], "eth0") } if params["LOC_IF"] != "eth1" { t.Errorf("LOC_IF = %q, want %q", params["LOC_IF"], "eth1") } if params["NET_ADDR"] != "192.168.1.0/24" { t.Errorf("NET_ADDR = %q, want %q", params["NET_ADDR"], "192.168.1.0/24") } } func TestParseParams_NotExist(t *testing.T) { params, err := ParseParams("/nonexistent/params") if err != nil { t.Fatalf("expected nil error for nonexistent file, got %v", err) } if params != nil { t.Fatalf("expected nil params, got %v", params) } } func TestParseParams_SkipsShellSyntax(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "params") content := `GOOD_VAR=value $(bad)=nope KEY WITH SPACES=no ALSO_GOOD=yes ` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatal(err) } params, err := ParseParams(path) if err != nil { t.Fatalf("ParseParams: %v", err) } if _, ok := params["$(bad)"]; ok { t.Error("should skip key with shell metacharacters") } if params["GOOD_VAR"] != "value" { t.Errorf("GOOD_VAR = %q, want %q", params["GOOD_VAR"], "value") } if params["ALSO_GOOD"] != "yes" { t.Errorf("ALSO_GOOD = %q, want %q", params["ALSO_GOOD"], "yes") } } func TestSplitFields(t *testing.T) { tests := []struct { input string want []string }{ {"ACCEPT net fw tcp 22", []string{"ACCEPT", "net", "fw", "tcp", "22"}}, {"ACCEPT net fw # inline comment", []string{"ACCEPT", "net", "fw"}}, {"ACCEPT net fw #comment", []string{"ACCEPT", "net", "fw"}}, {"single", []string{"single"}}, {" spaced out ", []string{"spaced", "out"}}, } for _, tt := range tests { got := splitFields(tt.input) if len(got) != len(tt.want) { t.Errorf("splitFields(%q) = %v (len %d), want %v (len %d)", tt.input, got, len(got), tt.want, len(tt.want)) continue } for i := range got { if got[i] != tt.want[i] { t.Errorf("splitFields(%q)[%d] = %q, want %q", tt.input, i, got[i], tt.want[i]) } } } } func TestIsDash(t *testing.T) { tests := []struct { input string want bool }{ {"-", true}, {"", true}, {"eth0", false}, {"tcp", false}, {"--", false}, } for _, tt := range tests { got := isDash(tt.input) if got != tt.want { t.Errorf("isDash(%q) = %v, want %v", tt.input, got, tt.want) } } } func TestDirExists(t *testing.T) { t.Run("with zones file", func(t *testing.T) { dir := t.TempDir() zonesPath := filepath.Join(dir, "zones") if err := os.WriteFile(zonesPath, []byte("fw firewall\n"), 0644); err != nil { t.Fatal(err) } if !DirExists(dir) { t.Error("DirExists should return true when zones file exists") } }) t.Run("with shorewall.conf only", func(t *testing.T) { dir := t.TempDir() confPath := filepath.Join(dir, "shorewall.conf") if err := os.WriteFile(confPath, []byte("IP_FORWARDING=Yes\n"), 0644); err != nil { t.Fatal(err) } if !DirExists(dir) { t.Error("DirExists should return true when shorewall.conf exists") } }) t.Run("empty directory", func(t *testing.T) { dir := t.TempDir() if DirExists(dir) { t.Error("DirExists should return false for empty directory") } }) t.Run("zones is a directory not a file", func(t *testing.T) { dir := t.TempDir() zonesDir := filepath.Join(dir, "zones") if err := os.Mkdir(zonesDir, 0755); err != nil { t.Fatal(err) } // zones exists but is a directory, and no shorewall.conf if DirExists(dir) { t.Error("DirExists should return false when zones is a directory and no shorewall.conf") } }) }