- 新增 internal/converter/nginx 解析器和转换器 - main.go 添加 --import/-i 参数支持 nginx 配置导入 - app_test.go 添加导入功能相关测试 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1329 lines
29 KiB
Go
1329 lines
29 KiB
Go
package nginx
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// helper: parse nginx config string and convert.
|
|
func convertString(t *testing.T, input string) (*ConvertResult, error) {
|
|
t.Helper()
|
|
cfg, err := Parse(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 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 != "10.0.0.1:8080" {
|
|
t.Errorf("target[0] URL = %s, want 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.Root != "/data/photos/" {
|
|
t.Errorf("Root = %s, want /data/photos/", st.Root)
|
|
}
|
|
|
|
if !hasWarningContaining(result.Warnings, "alias") {
|
|
t.Error("expected warning about alias conversion to 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 != "10.0.0.1:8080" {
|
|
t.Errorf("target[0] URL = %s, want 10.0.0.1:8080", p.Targets[0].URL)
|
|
}
|
|
if p.Targets[1].URL != "10.0.0.2:8080" {
|
|
t.Errorf("target[1] URL = %s, want 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)
|
|
}
|
|
}
|
|
|