package nginx import ( "fmt" "strings" "testing" "time" "rua.plus/lolly/internal/config" ) // helper: parse nginx config string and convert. func convertString(t *testing.T, input string) (*ConvertResult, error) { t.Helper() cfg, err := ParseString(input) if err != nil { t.Fatalf("parse error: %v", err) } return Convert(cfg) } // helper: check if any warning contains substring. func hasWarningContaining(warnings []Warning, substr string) bool { for _, w := range warnings { if strings.Contains(w.Message, substr) { return true } } return false } func TestConvertServerBlock(t *testing.T) { input := ` http { server { listen 8080; server_name example.com www.example.com; } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } if len(result.Config.Servers) != 1 { t.Fatalf("expected 1 server, got %d", len(result.Config.Servers)) } s := result.Config.Servers[0] if s.Listen != ":8080" { t.Errorf("expected listen :8080, got %s", s.Listen) } if s.Name != "example.com" { t.Errorf("expected name example.com, got %s", s.Name) } if len(s.ServerNames) != 2 { t.Fatalf("expected 2 server_names, got %d", len(s.ServerNames)) } if s.ServerNames[0] != "example.com" || s.ServerNames[1] != "www.example.com" { t.Errorf("expected [example.com www.example.com], got %v", s.ServerNames) } } func TestConvertLocationProxyPass(t *testing.T) { input := ` http { server { listen 80; location /api/ { proxy_pass http://backend:8080; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if len(s.Proxy) != 1 { t.Fatalf("expected 1 proxy, got %d", len(s.Proxy)) } p := s.Proxy[0] if p.Path != "/api/" { t.Errorf("expected path /api/, got %s", p.Path) } if len(p.Targets) != 1 { t.Fatalf("expected 1 target, got %d", len(p.Targets)) } if p.Targets[0].URL != "http://backend:8080" { t.Errorf("expected target URL http://backend:8080, got %s", p.Targets[0].URL) } } func TestConvertLocationRoot(t *testing.T) { input := ` http { server { listen 80; location /static/ { root /var/www/html; index index.html index.htm; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if len(s.Static) != 1 { t.Fatalf("expected 1 static, got %d", len(s.Static)) } st := s.Static[0] if st.Path != "/static/" { t.Errorf("expected path /static/, got %s", st.Path) } if st.Root != "/var/www/html" { t.Errorf("expected root /var/www/html, got %s", st.Root) } if len(st.Index) != 2 || st.Index[0] != "index.html" || st.Index[1] != "index.htm" { t.Errorf("expected [index.html index.htm], got %v", st.Index) } } func TestConvertServerLevelRoot(t *testing.T) { input := ` http { server { listen 80; root /var/www/html; index index.html index.htm index.php; } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if len(s.Static) != 1 { t.Fatalf("expected 1 static, got %d", len(s.Static)) } st := s.Static[0] if st.Path != "/" { t.Errorf("expected path /, got %s", st.Path) } if st.Root != "/var/www/html" { t.Errorf("expected root /var/www/html, got %s", st.Root) } if len(st.Index) != 3 || st.Index[0] != "index.html" || st.Index[1] != "index.htm" || st.Index[2] != "index.php" { t.Errorf("expected [index.html index.htm index.php], got %v", st.Index) } } func TestConvertServerLevelRootWithLocation(t *testing.T) { // When both server-level root and location /static exist, both should be converted input := ` http { server { listen 80; root /var/www/html; index index.html; location /static/ { root /var/www/static; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if len(s.Static) != 2 { t.Fatalf("expected 2 static, got %d", len(s.Static)) } // Find the root location var rootStatic, staticLoc *config.StaticConfig for i := range s.Static { if s.Static[i].Path == "/" { rootStatic = &s.Static[i] } else if s.Static[i].Path == "/static/" { staticLoc = &s.Static[i] } } if rootStatic == nil { t.Error("expected root static config at path /") } else if rootStatic.Root != "/var/www/html" { t.Errorf("expected root /var/www/html, got %s", rootStatic.Root) } if staticLoc == nil { t.Error("expected static config at path /static/") } else if staticLoc.Root != "/var/www/static" { t.Errorf("expected root /var/www/static, got %s", staticLoc.Root) } } func TestConvertServerLevelRootNotCreatedForProxy(t *testing.T) { // When location / is a proxy, server-level root should NOT create a static config input := ` http { server { listen 80; root /var/www/html; location / { proxy_pass http://backend; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if len(s.Static) != 0 { t.Errorf("expected 0 static (location / is proxy), got %d", len(s.Static)) } if len(s.Proxy) != 1 { t.Errorf("expected 1 proxy, got %d", len(s.Proxy)) } } func TestConvertUpstream(t *testing.T) { input := ` upstream backend { server 10.0.0.1:8080 weight=3; server 10.0.0.2:8080 weight=1 max_fails=3 fail_timeout=30s; server 10.0.0.3:8080 backup; } http { server { listen 80; location / { proxy_pass http://backend; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if len(s.Proxy) != 1 { t.Fatalf("expected 1 proxy, got %d", len(s.Proxy)) } p := s.Proxy[0] if len(p.Targets) != 3 { t.Fatalf("expected 3 targets, got %d", len(p.Targets)) } if p.Targets[0].URL != "http://10.0.0.1:8080" { t.Errorf("target[0] URL = %s, want http://10.0.0.1:8080", p.Targets[0].URL) } if p.Targets[0].Weight != 3 { t.Errorf("target[0] Weight = %d, want 3", p.Targets[0].Weight) } if p.Targets[1].MaxFails != 3 { t.Errorf("target[1] MaxFails = %d, want 3", p.Targets[1].MaxFails) } if p.Targets[1].FailTimeout != 30*time.Second { t.Errorf("target[1] FailTimeout = %v, want 30s", p.Targets[1].FailTimeout) } if !p.Targets[2].Backup { t.Error("target[2] Backup = false, want true") } } func TestConvertLocationModifiers(t *testing.T) { tests := []struct { name string input string wantType string }{ { name: "exact", input: ` http { server { listen 80; location = /exact { proxy_pass http://backend; } } }`, wantType: "exact", }, { name: "prefix_priority", input: ` http { server { listen 80; location ^~ /prefix { proxy_pass http://backend; } } }`, wantType: "prefix_priority", }, { name: "regex", input: ` http { server { listen 80; location ~ \.php$ { proxy_pass http://backend; } } }`, wantType: "regex", }, { name: "regex_caseless", input: ` http { server { listen 80; location ~* \.php$ { proxy_pass http://backend; } } }`, wantType: "regex_caseless", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := convertString(t, tt.input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if len(s.Proxy) != 1 { t.Fatalf("expected 1 proxy, got %d", len(s.Proxy)) } if s.Proxy[0].LocationType != tt.wantType { t.Errorf("LocationType = %s, want %s", s.Proxy[0].LocationType, tt.wantType) } }) } } func TestConvertGzipConfig(t *testing.T) { input := ` http { server { listen 80; gzip on; gzip_types text/plain text/css application/json; gzip_min_length 1024; } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if s.Compression.Type != "gzip" { t.Errorf("Compression.Type = %s, want gzip", s.Compression.Type) } if len(s.Compression.Types) != 3 { t.Fatalf("Compression.Types length = %d, want 3", len(s.Compression.Types)) } if s.Compression.Types[0] != "text/plain" { t.Errorf("Compression.Types[0] = %s, want text/plain", s.Compression.Types[0]) } if s.Compression.MinSize != 1024 { t.Errorf("Compression.MinSize = %d, want 1024", s.Compression.MinSize) } } func TestConvertRewrite(t *testing.T) { input := ` http { server { listen 80; rewrite ^/old/(.*)$ /new/$1 last; } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if len(s.Rewrite) != 1 { t.Fatalf("expected 1 rewrite rule, got %d", len(s.Rewrite)) } r := s.Rewrite[0] if r.Pattern != "^/old/(.*)$" { t.Errorf("Pattern = %s, want ^/old/(.*)$", r.Pattern) } if r.Replacement != "/new/$1" { t.Errorf("Replacement = %s, want /new/$1", r.Replacement) } if r.Flag != "last" { t.Errorf("Flag = %s, want last", r.Flag) } } func TestConvertReturn301(t *testing.T) { input := ` http { server { listen 80; location /old { return 301 https://example.com/new; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if len(s.Rewrite) != 1 { t.Fatalf("expected 1 rewrite rule, got %d", len(s.Rewrite)) } r := s.Rewrite[0] if r.Pattern != "^/old$" { t.Errorf("Pattern = %s, want ^/old$", r.Pattern) } if r.Replacement != "https://example.com/new" { t.Errorf("Replacement = %s, want https://example.com/new", r.Replacement) } if r.Flag != "permanent" { t.Errorf("Flag = %s, want permanent", r.Flag) } if !hasWarningContaining(result.Warnings, "return 301") { t.Error("expected warning about return 301 conversion") } } func TestConvertReturn302(t *testing.T) { input := ` http { server { listen 80; location /temp { return 302 https://example.com/temp-new; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if len(s.Rewrite) != 1 { t.Fatalf("expected 1 rewrite rule, got %d", len(s.Rewrite)) } r := s.Rewrite[0] if r.Flag != "redirect" { t.Errorf("Flag = %s, want redirect", r.Flag) } if !hasWarningContaining(result.Warnings, "return 302") { t.Error("expected warning about return 302 conversion") } } func TestConvertSSLConfig(t *testing.T) { input := ` http { server { listen 443 ssl; ssl_certificate /etc/ssl/server.crt; ssl_certificate_key /etc/ssl/server.key; } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if s.SSL.Cert != "/etc/ssl/server.crt" { t.Errorf("SSL.Cert = %s, want /etc/ssl/server.crt", s.SSL.Cert) } if s.SSL.Key != "/etc/ssl/server.key" { t.Errorf("SSL.Key = %s, want /etc/ssl/server.key", s.SSL.Key) } if s.Listen != ":443" { t.Errorf("Listen = %s, want :443", s.Listen) } } func TestConvertProxyAddXForwardedFor(t *testing.T) { input := ` http { server { listen 80; location / { proxy_pass http://backend; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if len(s.Proxy) != 1 { t.Fatalf("expected 1 proxy, got %d", len(s.Proxy)) } val := s.Proxy[0].Headers.SetRequest["X-Forwarded-For"] if val != "$remote_addr" { t.Errorf("X-Forwarded-For = %s, want $remote_addr", val) } if !hasWarningContaining(result.Warnings, "$proxy_add_x_forwarded_for") { t.Error("expected warning about $proxy_add_x_forwarded_for replacement") } } func TestConvertUnsupportedDirective(t *testing.T) { input := ` http { server { listen 80; if ($host = example.com) { return 301 https://example.com; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } if !hasWarningContaining(result.Warnings, "'if' directive") { t.Error("expected warning about unsupported 'if' directive") } } func TestConvertEmptyLocation(t *testing.T) { input := ` http { server { listen 80; location /empty { } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } if !hasWarningContaining(result.Warnings, "no content") { t.Error("expected warning about empty location") } } func TestConvertConflictingLocation(t *testing.T) { input := ` http { server { listen 80; location /conflict { proxy_pass http://backend; root /var/www; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] // Should be classified as proxy, not static. if len(s.Proxy) != 1 { t.Fatalf("expected 1 proxy, got %d", len(s.Proxy)) } if len(s.Static) != 0 { t.Errorf("expected 0 static, got %d", len(s.Static)) } if !hasWarningContaining(result.Warnings, "proxy_pass takes priority") { t.Error("expected warning about proxy_pass taking priority over root/alias") } } func TestConvertAliasDirective(t *testing.T) { input := ` http { server { listen 80; location /images/ { alias /data/photos/; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if len(s.Static) != 1 { t.Fatalf("expected 1 static, got %d", len(s.Static)) } st := s.Static[0] if st.Alias != "/data/photos/" { t.Errorf("Alias = %s, want /data/photos/", st.Alias) } // alias 和 root 互斥,root 应为空 if st.Root != "" { t.Errorf("Root = %s, want empty (alias is set)", st.Root) } } func TestConvertReturnNonRedirect(t *testing.T) { input := ` http { server { listen 80; location /health { return 200 "OK"; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } if len(result.Warnings) == 0 { t.Fatal("expected at least one warning, got none") } if !hasWarningContaining(result.Warnings, "'return' directive") && !hasWarningContaining(result.Warnings, "return") { t.Errorf("expected warning about non-redirect return, got warnings: %v", result.Warnings) } } func TestConvertServerLevelReturn(t *testing.T) { input := ` http { server { listen 80; return 301 https://example.com; } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if len(s.Rewrite) != 1 { t.Fatalf("expected 1 rewrite rule, got %d", len(s.Rewrite)) } r := s.Rewrite[0] if r.Pattern != "^/" { t.Errorf("Pattern = %s, want ^/", r.Pattern) } if r.Replacement != "https://example.com" { t.Errorf("Replacement = %s, want https://example.com", r.Replacement) } if r.Flag != "permanent" { t.Errorf("Flag = %s, want permanent", r.Flag) } } func TestConvertProxySetHeader(t *testing.T) { input := ` http { server { listen 80; location / { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] p := s.Proxy[0] if p.Headers.SetRequest["Host"] != "$host" { t.Errorf("Host = %s, want $host", p.Headers.SetRequest["Host"]) } if p.Headers.SetRequest["X-Real-IP"] != "$remote_addr" { t.Errorf("X-Real-IP = %s, want $remote_addr", p.Headers.SetRequest["X-Real-IP"]) } if p.Headers.SetRequest["X-Forwarded-Proto"] != "$scheme" { t.Errorf("X-Forwarded-Proto = %s, want $scheme", p.Headers.SetRequest["X-Forwarded-Proto"]) } } func TestConvertProxyTimeout(t *testing.T) { input := ` http { server { listen 80; location / { proxy_pass http://backend; proxy_connect_timeout 5s; proxy_read_timeout 30s; proxy_send_timeout 15s; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] p := s.Proxy[0] if p.Timeout.Connect != 5*time.Second { t.Errorf("Connect = %v, want 5s", p.Timeout.Connect) } if p.Timeout.Read != 30*time.Second { t.Errorf("Read = %v, want 30s", p.Timeout.Read) } if p.Timeout.Write != 15*time.Second { t.Errorf("Write = %v, want 15s", p.Timeout.Write) } } func TestConvertClientMaxBodySize(t *testing.T) { input := ` http { server { listen 80; client_max_body_size 10m; } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if s.ClientMaxBodySize != "10m" { t.Errorf("ClientMaxBodySize = %s, want 10m", s.ClientMaxBodySize) } } func TestConvertMultipleServerBlocks(t *testing.T) { input := ` http { server { listen 80; server_name example.com; } server { listen 443 ssl; server_name secure.example.com; ssl_certificate /etc/ssl/cert.pem; ssl_certificate_key /etc/ssl/key.pem; } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } if len(result.Config.Servers) != 2 { t.Fatalf("expected 2 servers, got %d", len(result.Config.Servers)) } if result.Config.Servers[0].Listen != ":80" { t.Errorf("server[0] Listen = %s, want :80", result.Config.Servers[0].Listen) } if result.Config.Servers[1].Listen != ":443" { t.Errorf("server[1] Listen = %s, want :443", result.Config.Servers[1].Listen) } if result.Config.Servers[1].Name != "secure.example.com" { t.Errorf("server[1] Name = %s, want secure.example.com", result.Config.Servers[1].Name) } } func TestConvertDefaultServer(t *testing.T) { input := ` http { server { listen 80 default_server; server_name _; } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if !s.Default { t.Error("Default = false, want true") } if s.Listen != ":80" { t.Errorf("Listen = %s, want :80", s.Listen) } } func TestConvertProxyRedirect(t *testing.T) { tests := []struct { name string input string mode string rules int }{ { name: "off", input: ` http { server { listen 80; location / { proxy_pass http://backend; proxy_redirect off; } } }`, mode: "off", rules: 0, }, { name: "default", input: ` http { server { listen 80; location / { proxy_pass http://backend; proxy_redirect default; } } }`, mode: "default", rules: 0, }, { name: "custom", input: ` http { server { listen 80; location / { proxy_pass http://backend; proxy_redirect http://backend/ /; } } }`, mode: "custom", rules: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := convertString(t, tt.input) if err != nil { t.Fatalf("convert error: %v", err) } p := result.Config.Servers[0].Proxy[0] if p.RedirectRewrite == nil { t.Fatal("RedirectRewrite is nil") } if p.RedirectRewrite.Mode != tt.mode { t.Errorf("Mode = %s, want %s", p.RedirectRewrite.Mode, tt.mode) } if len(p.RedirectRewrite.Rules) != tt.rules { t.Errorf("Rules count = %d, want %d", len(p.RedirectRewrite.Rules), tt.rules) } }) } } func TestConvertErrorPage(t *testing.T) { input := ` http { server { listen 80; error_page 404 /404.html; error_page 500 502 503 /50x.html; } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] pages := s.Security.ErrorPage.Pages if pages[404] != "/404.html" { t.Errorf("error_page[404] = %s, want /404.html", pages[404]) } if pages[500] != "/50x.html" { t.Errorf("error_page[500] = %s, want /50x.html", pages[500]) } if pages[502] != "/50x.html" { t.Errorf("error_page[502] = %s, want /50x.html", pages[502]) } if pages[503] != "/50x.html" { t.Errorf("error_page[503] = %s, want /50x.html", pages[503]) } } func TestConvertAuthBasic(t *testing.T) { input := ` http { server { listen 80; auth_basic "Restricted Area"; auth_basic_user_file /etc/nginx/.htpasswd; } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if s.Security.Auth.Type != "basic" { t.Errorf("Auth.Type = %s, want basic", s.Security.Auth.Type) } if s.Security.Auth.Realm != "Restricted Area" { t.Errorf("Auth.Realm = %s, want Restricted Area", s.Security.Auth.Realm) } if !hasWarningContaining(result.Warnings, "auth_basic_user_file") { t.Error("expected warning about auth_basic_user_file migration") } } func TestConvertTopLevelServer(t *testing.T) { // Server block without http wrapper. input := ` server { listen 8080; server_name direct.example.com; } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } if len(result.Config.Servers) != 1 { t.Fatalf("expected 1 server, got %d", len(result.Config.Servers)) } s := result.Config.Servers[0] if s.Listen != ":8080" { t.Errorf("Listen = %s, want :8080", s.Listen) } if s.Name != "direct.example.com" { t.Errorf("Name = %s, want direct.example.com", s.Name) } } func TestConvertLoggingConfig(t *testing.T) { input := ` http { server { listen 80; access_log /var/log/nginx/access.log combined; error_log /var/log/nginx/error.log warn; } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } if result.Config.Logging.Access.Path != "/var/log/nginx/access.log" { t.Errorf("Access.Path = %s, want /var/log/nginx/access.log", result.Config.Logging.Access.Path) } if result.Config.Logging.Access.Format != "combined" { t.Errorf("Access.Format = %s, want combined", result.Config.Logging.Access.Format) } if result.Config.Logging.Error.Path != "/var/log/nginx/error.log" { t.Errorf("Error.Path = %s, want /var/log/nginx/error.log", result.Config.Logging.Error.Path) } if result.Config.Logging.Error.Level != "warn" { t.Errorf("Error.Level = %s, want warn", result.Config.Logging.Error.Level) } } func TestConvertServerTokens(t *testing.T) { input := ` http { server { listen 80; server_tokens off; } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if s.ServerTokens { t.Error("ServerTokens = true, want false") } } func TestConvertUpstreamLoadBalance(t *testing.T) { tests := []struct { name string upstream string wantLB string }{ { name: "least_conn", upstream: "least_conn;", wantLB: "least_conn", }, { name: "ip_hash", upstream: "ip_hash;", wantLB: "ip_hash", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { input := fmt.Sprintf(` upstream backend { server 10.0.0.1:8080; %s } http { server { listen 80; location / { proxy_pass http://backend; } } } `, tt.upstream) result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } p := result.Config.Servers[0].Proxy[0] if p.LoadBalance != tt.wantLB { t.Errorf("LoadBalance = %s, want %s", p.LoadBalance, tt.wantLB) } }) } } func TestConvertTryFiles(t *testing.T) { input := ` http { server { listen 80; location / { root /var/www; try_files $uri $uri/ /index.html; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] st := s.Static[0] if len(st.TryFiles) != 3 { t.Fatalf("TryFiles length = %d, want 3", len(st.TryFiles)) } if st.TryFiles[0] != "$uri" || st.TryFiles[1] != "$uri/" || st.TryFiles[2] != "/index.html" { t.Errorf("TryFiles = %v, want [$uri $uri/ /index.html]", st.TryFiles) } } func TestConvertProxyHidePassHeaders(t *testing.T) { input := ` http { server { listen 80; location / { proxy_pass http://backend; proxy_hide_header X-Powered-By; proxy_pass_header X-My-Custom; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } p := result.Config.Servers[0].Proxy[0] if len(p.Headers.HideResponse) != 1 || p.Headers.HideResponse[0] != "X-Powered-By" { t.Errorf("HideResponse = %v, want [X-Powered-By]", p.Headers.HideResponse) } if len(p.Headers.PassResponse) != 1 || p.Headers.PassResponse[0] != "X-My-Custom" { t.Errorf("PassResponse = %v, want [X-My-Custom]", p.Headers.PassResponse) } } func TestConvertProxyCache(t *testing.T) { input := ` http { server { listen 80; location / { proxy_pass http://backend; proxy_cache my_cache; proxy_cache_valid 200 10m; proxy_cache_valid 404 1m; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } p := result.Config.Servers[0].Proxy[0] if !p.Cache.Enabled { t.Error("Cache.Enabled = false, want true") } if p.CacheValid == nil { t.Fatal("CacheValid is nil") } if p.CacheValid.OK != 10*time.Minute { t.Errorf("CacheValid.OK = %v, want 10m", p.CacheValid.OK) } if p.CacheValid.NotFound != time.Minute { t.Errorf("CacheValid.NotFound = %v, want 1m", p.CacheValid.NotFound) } } func TestConvertNamedLocation(t *testing.T) { input := ` http { server { listen 80; location @fallback { proxy_pass http://fallback-backend; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } p := result.Config.Servers[0].Proxy[0] if p.LocationType != "named" { t.Errorf("LocationType = %s, want named", p.LocationType) } if p.LocationName != "fallback" { t.Errorf("LocationName = %s, want fallback", p.LocationName) } } func TestConvertWarningString(t *testing.T) { w := Warning{ Directive: "if", Line: 10, File: "test.conf", Message: "unsupported", } s := w.String() if !strings.Contains(s, "test.conf:10") { t.Errorf("Warning.String() = %s, should contain test.conf:10", s) } if !strings.Contains(s, "unsupported") { t.Errorf("Warning.String() = %s, should contain 'unsupported'", s) } } func TestConvertEmptyConfig(t *testing.T) { input := `` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } if len(result.Config.Servers) != 0 { t.Errorf("expected 0 servers, got %d", len(result.Config.Servers)) } } func TestConvertServerTokensOn(t *testing.T) { input := ` http { server { listen 80; server_tokens on; } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if !s.ServerTokens { t.Error("ServerTokens = false, want true") } } func TestConvertMapDirectiveWarning(t *testing.T) { input := ` http { map $host $backend { default backend1; } server { listen 80; } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } if !hasWarningContaining(result.Warnings, "'map' directive") { t.Error("expected warning about unsupported 'map' directive") } } func TestConvertUpstreamDownServer(t *testing.T) { input := ` upstream backend { server 10.0.0.1:8080; server 10.0.0.2:8080 down; } http { server { listen 80; location / { proxy_pass http://backend; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } p := result.Config.Servers[0].Proxy[0] if len(p.Targets) != 2 { t.Fatalf("expected 2 targets, got %d", len(p.Targets)) } if !p.Targets[1].Down { t.Error("target[1].Down = false, want true") } } func TestConvertUpstreamInsideHttpBlock(t *testing.T) { // Upstream blocks inside http block should be found. input := ` http { upstream backend { server 10.0.0.1:8080; server 10.0.0.2:8080; } server { listen 80; location / { proxy_pass http://backend; } } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if len(s.Proxy) != 1 { t.Fatalf("expected 1 proxy, got %d", len(s.Proxy)) } p := s.Proxy[0] if len(p.Targets) != 2 { t.Fatalf("expected 2 targets from upstream inside http block, got %d", len(p.Targets)) } if p.Targets[0].URL != "http://10.0.0.1:8080" { t.Errorf("target[0] URL = %s, want http://10.0.0.1:8080", p.Targets[0].URL) } if p.Targets[1].URL != "http://10.0.0.2:8080" { t.Errorf("target[1] URL = %s, want http://10.0.0.2:8080", p.Targets[1].URL) } } func TestConvertSSLWithoutCertKey(t *testing.T) { // SSL enabled via listen directive but no ssl_certificate/key should produce a warning. input := ` http { server { listen 443 ssl; } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if s.SSL.Cert != "" { t.Errorf("SSL.Cert = %s, want empty string (no auto)", s.SSL.Cert) } if s.SSL.Key != "" { t.Errorf("SSL.Key = %s, want empty string (no auto)", s.SSL.Key) } if !hasWarningContaining(result.Warnings, "ssl_certificate") { t.Error("expected warning about missing ssl_certificate/ssl_certificate_key") } } func TestConvertSSLWithCertKeyNoWarning(t *testing.T) { // SSL enabled with both cert and key should NOT produce a warning. input := ` http { server { listen 443 ssl; ssl_certificate /etc/ssl/server.crt; ssl_certificate_key /etc/ssl/server.key; } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if s.SSL.Cert != "/etc/ssl/server.crt" { t.Errorf("SSL.Cert = %s, want /etc/ssl/server.crt", s.SSL.Cert) } if s.SSL.Key != "/etc/ssl/server.key" { t.Errorf("SSL.Key = %s, want /etc/ssl/server.key", s.SSL.Key) } if hasWarningContaining(result.Warnings, "ssl_certificate") { t.Error("unexpected warning about missing ssl_certificate when both are provided") } } func TestConvertServerNoListenDefault(t *testing.T) { // Server block without listen directive should get default 0.0.0.0:80. input := ` http { server { server_name example.com; } } ` result, err := convertString(t, input) if err != nil { t.Fatalf("convert error: %v", err) } s := result.Config.Servers[0] if s.Listen != "0.0.0.0:80" { t.Errorf("Listen = %s, want 0.0.0.0:80 when no listen directive", s.Listen) } }