package nginx import ( "fmt" "os" "path/filepath" "testing" ) func TestParseSimpleDirective(t *testing.T) { cfg, err := ParseString("listen 80;") if err != nil { t.Fatalf("unexpected error: %v", err) } if len(cfg.Directives) != 1 { t.Fatalf("expected 1 directive, got %d", len(cfg.Directives)) } d := cfg.Directives[0] if d.Name != "listen" { t.Errorf("expected name %q, got %q", "listen", d.Name) } if len(d.Args) != 1 || d.Args[0] != "80" { t.Errorf("expected args [80], got %v", d.Args) } } func TestParseBlockDirective(t *testing.T) { cfg, err := ParseString("server { listen 80; }") if err != nil { t.Fatalf("unexpected error: %v", err) } if len(cfg.Directives) != 1 { t.Fatalf("expected 1 directive, got %d", len(cfg.Directives)) } d := cfg.Directives[0] if d.Name != "server" { t.Errorf("expected name %q, got %q", "server", d.Name) } if len(d.Block) != 1 { t.Fatalf("expected 1 child, got %d", len(d.Block)) } if d.Block[0].Name != "listen" { t.Errorf("expected child name %q, got %q", "listen", d.Block[0].Name) } if len(d.Block[0].Args) != 1 || d.Block[0].Args[0] != "80" { t.Errorf("expected child args [80], got %v", d.Block[0].Args) } } func TestParseComment(t *testing.T) { cfg, err := ParseString("# this is a comment\nlisten 80;") if err != nil { t.Fatalf("unexpected error: %v", err) } if len(cfg.Directives) != 1 { t.Fatalf("expected 1 directive, got %d", len(cfg.Directives)) } if cfg.Directives[0].Name != "listen" { t.Errorf("expected name %q, got %q", "listen", cfg.Directives[0].Name) } } func TestParseQuotedString(t *testing.T) { tests := []struct { name string input string dirName string expected []string }{ { name: "double quoted", input: `proxy_set_header Host "example.com";`, dirName: "proxy_set_header", expected: []string{"Host", "example.com"}, }, { name: "single quoted", input: `proxy_set_header Host 'example.com';`, dirName: "proxy_set_header", expected: []string{"Host", "example.com"}, }, { name: "escaped quote inside double", input: `set $x "hello\"world";`, dirName: "set", expected: []string{"$x", `hello"world`}, }, { name: "escaped quote inside single", input: `set $x 'hello\'world';`, dirName: "set", expected: []string{"$x", "hello'world"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg, err := ParseString(tt.input) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(cfg.Directives) != 1 { t.Fatalf("expected 1 directive, got %d", len(cfg.Directives)) } d := cfg.Directives[0] if d.Name != tt.dirName { t.Errorf("expected name %q, got %q", tt.dirName, d.Name) } if len(d.Args) != len(tt.expected) { t.Fatalf("expected %d args, got %d", len(tt.expected), len(d.Args)) } for i, want := range tt.expected { if d.Args[i] != want { t.Errorf("arg[%d]: expected %q, got %q", i, want, d.Args[i]) } } }) } } func TestParseMultipleDirectives(t *testing.T) { input := `listen 80; server_name example.com;` cfg, err := ParseString(input) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(cfg.Directives) != 2 { t.Fatalf("expected 2 directives, got %d", len(cfg.Directives)) } if cfg.Directives[0].Name != "listen" { t.Errorf("directive[0]: expected %q, got %q", "listen", cfg.Directives[0].Name) } if cfg.Directives[1].Name != "server_name" { t.Errorf("directive[1]: expected %q, got %q", "server_name", cfg.Directives[1].Name) } } func TestParseNestedBlocks(t *testing.T) { input := `http { server { location / { root /var/www; } } }` cfg, err := ParseString(input) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(cfg.Directives) != 1 { t.Fatalf("expected 1 top-level directive, got %d", len(cfg.Directives)) } http := cfg.Directives[0] if http.Name != "http" || len(http.Block) != 1 { t.Fatalf("expected http with 1 child") } srv := http.Block[0] if srv.Name != "server" || len(srv.Block) != 1 { t.Fatalf("expected server with 1 child") } loc := srv.Block[0] if loc.Name != "location" || len(loc.Block) != 1 { t.Fatalf("expected location with 1 child") } if loc.Block[0].Name != "root" { t.Errorf("expected root, got %q", loc.Block[0].Name) } } func TestParseUnclosedBlock(t *testing.T) { _, err := ParseString("server { listen 80;") if err == nil { t.Fatal("expected error for unclosed block") } pe, ok := err.(*ParseError) if !ok { t.Fatalf("expected *ParseError, got %T", err) } if pe.Line == 0 { t.Error("expected non-zero line number") } } func TestParseMissingSemicolon(t *testing.T) { _, err := ParseString("listen 80") if err == nil { t.Fatal("expected error for missing semicolon") } pe, ok := err.(*ParseError) if !ok { t.Fatalf("expected *ParseError, got %T", err) } if pe.Line == 0 { t.Error("expected non-zero line number") } } func TestParseFile(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "nginx.conf") content := "listen 80;\nserver_name example.com;" if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatalf("write file: %v", err) } cfg, err := ParseFile(path) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(cfg.Directives) != 2 { t.Fatalf("expected 2 directives, got %d", len(cfg.Directives)) } if cfg.Directives[0].Name != "listen" { t.Errorf("directive[0]: expected %q, got %q", "listen", cfg.Directives[0].Name) } if cfg.Directives[1].Name != "server_name" { t.Errorf("directive[1]: expected %q, got %q", "server_name", cfg.Directives[1].Name) } } func TestParseIncludeGlob(t *testing.T) { dir := t.TempDir() // Create a subdirectory for included files to avoid matching nginx.conf itself. incDir := filepath.Join(dir, "includes") if err := os.Mkdir(incDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(incDir, "a.conf"), []byte("listen 80;\n"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(incDir, "b.conf"), []byte("server_name a.com;\n"), 0o644); err != nil { t.Fatal(err) } // Create main config with include. main := filepath.Join(dir, "nginx.conf") content := "include " + incDir + "/*.conf;" if err := os.WriteFile(main, []byte(content), 0o644); err != nil { t.Fatal(err) } cfg, err := ParseFile(main) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(cfg.Directives) != 2 { t.Fatalf("expected 2 directives from included files, got %d", len(cfg.Directives)) } names := map[string]bool{} for _, d := range cfg.Directives { names[d.Name] = true } if !names["listen"] || !names["server_name"] { t.Errorf("expected listen and server_name, got %v", cfg.Directives) } } func TestParseIncludeCircular(t *testing.T) { dir := t.TempDir() a := filepath.Join(dir, "a.conf") b := filepath.Join(dir, "b.conf") if err := os.WriteFile(a, []byte("include "+b+";"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(b, []byte("include "+a+";"), 0o644); err != nil { t.Fatal(err) } _, err := ParseFile(a) if err == nil { t.Fatal("expected error for circular include") } pe, ok := err.(*ParseError) if !ok { t.Fatalf("expected *ParseError, got %T", err) } if pe.Message == "" { t.Error("expected non-empty error message") } } func TestParseIncludeMaxDepth(t *testing.T) { dir := t.TempDir() // Create a chain of includes: 0.conf includes 1.conf, 1.conf includes 2.conf, etc. for i := 0; i <= maxDepth+1; i++ { path := filepath.Join(dir, fmt.Sprintf("%d.conf", i)) var content string if i < maxDepth+1 { next := filepath.Join(dir, fmt.Sprintf("%d.conf", i+1)) content = "include " + next + ";" } else { content = "listen 80;" } if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatal(err) } } _, err := ParseFile(filepath.Join(dir, "0.conf")) if err == nil { t.Fatal("expected error for max include depth") } if _, ok := err.(*ParseError); !ok { t.Fatalf("expected *ParseError, got %T", err) } } func TestParseIncludeNotFound(t *testing.T) { dir := t.TempDir() main := filepath.Join(dir, "nginx.conf") content := "include /nonexistent/path.conf;" if err := os.WriteFile(main, []byte(content), 0o644); err != nil { t.Fatal(err) } _, err := ParseFile(main) if err == nil { t.Fatal("expected error for include of nonexistent file") } } func TestParseIncludeGlobNoMatch(t *testing.T) { dir := t.TempDir() main := filepath.Join(dir, "nginx.conf") content := "include " + dir + "/nonexistent/*.conf;" if err := os.WriteFile(main, []byte(content), 0o644); err != nil { t.Fatal(err) } cfg, err := ParseFile(main) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(cfg.Directives) != 0 { t.Fatalf("expected 0 directives for glob with no matches, got %d", len(cfg.Directives)) } } func TestParseLocationModifiers(t *testing.T) { tests := []struct { name string input string dirName string args []string }{ { name: "exact match", input: "location = /path {}", dirName: "location", args: []string{"=", "/path"}, }, { name: "regex", input: `location ~ \.php$ {}`, dirName: "location", args: []string{"~", `\.php$`}, }, { name: "case insensitive regex", input: `location ~* \.jpg$ {}`, dirName: "location", args: []string{"~*", `\.jpg$`}, }, { name: "prefix with continuation", input: "location ^~ /images/ {}", dirName: "location", args: []string{"^~", "/images/"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg, err := ParseString(tt.input) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(cfg.Directives) != 1 { t.Fatalf("expected 1 directive, got %d", len(cfg.Directives)) } d := cfg.Directives[0] if d.Name != tt.dirName { t.Errorf("expected name %q, got %q", tt.dirName, d.Name) } if len(d.Args) != len(tt.args) { t.Fatalf("expected %d args, got %d: %v", len(tt.args), len(d.Args), d.Args) } for i, want := range tt.args { if d.Args[i] != want { t.Errorf("arg[%d]: expected %q, got %q", i, want, d.Args[i]) } } }) } } func TestParseEmptyBlock(t *testing.T) { cfg, err := ParseString("server {}") if err != nil { t.Fatalf("unexpected error: %v", err) } if len(cfg.Directives) != 1 { t.Fatalf("expected 1 directive, got %d", len(cfg.Directives)) } d := cfg.Directives[0] if d.Name != "server" { t.Errorf("expected name %q, got %q", "server", d.Name) } if len(d.Block) != 0 { t.Errorf("expected empty block, got %d children", len(d.Block)) } } func TestParseMultipleArgs(t *testing.T) { cfg, err := ParseString("return 301 https://example.com;") if err != nil { t.Fatalf("unexpected error: %v", err) } if len(cfg.Directives) != 1 { t.Fatalf("expected 1 directive, got %d", len(cfg.Directives)) } d := cfg.Directives[0] if d.Name != "return" { t.Errorf("expected name %q, got %q", "return", d.Name) } expected := []string{"301", "https://example.com"} if len(d.Args) != len(expected) { t.Fatalf("expected %d args, got %d", len(expected), len(d.Args)) } for i, want := range expected { if d.Args[i] != want { t.Errorf("arg[%d]: expected %q, got %q", i, want, d.Args[i]) } } }