lolly/internal/converter/nginx/parser_test.go
xfy 909bd405d2 feat(converter,app): 添加 nginx 配置导入功能
- 新增 internal/converter/nginx 解析器和转换器
- main.go 添加 --import/-i 参数支持 nginx 配置导入
- app_test.go 添加导入功能相关测试

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 17:12:49 +08:00

431 lines
11 KiB
Go

package nginx
import (
"fmt"
"os"
"path/filepath"
"testing"
)
func TestParseSimpleDirective(t *testing.T) {
cfg, err := Parse("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 := Parse("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 := Parse("# 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 := Parse(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 := Parse(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 := Parse(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 := Parse("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 := Parse("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), 0644); 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, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(incDir, "a.conf"), []byte("listen 80;\n"), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(incDir, "b.conf"), []byte("server_name a.com;\n"), 0644); 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), 0644); 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+";"), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(b, []byte("include "+a+";"), 0644); 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), 0644); 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), 0644); 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), 0644); 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 := Parse(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 := Parse("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 := Parse("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])
}
}
}