package nginx import ( "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/suite" "github.com/acepanel/panel/pkg/webserver/types" ) type VhostTestSuite struct { suite.Suite vhost *PHPVhost configDir string } func TestVhostTestSuite(t *testing.T) { suite.Run(t, &VhostTestSuite{}) } func (s *VhostTestSuite) SetupTest() { // 创建临时配置目录 configDir, err := os.MkdirTemp("", "nginx-test-*") s.Require().NoError(err) s.configDir = configDir // 创建 site 目录 err = os.MkdirAll(filepath.Join(configDir, "site"), 0755) s.Require().NoError(err) vhost, err := NewPHPVhost(configDir) s.Require().NoError(err) s.Require().NotNil(vhost) s.vhost = vhost } func (s *VhostTestSuite) TearDownTest() { // 清理临时目录 if s.configDir != "" { s.NoError(os.RemoveAll(s.configDir)) } } func (s *VhostTestSuite) TestNewVhost() { s.Equal(s.configDir, s.vhost.configDir) s.NotNil(s.vhost.parser) } func (s *VhostTestSuite) TestEnable() { // 默认应该是启用状态(没有 000-disable.conf) s.True(s.vhost.Enable()) // 禁用网站 s.NoError(s.vhost.SetEnable(false)) s.False(s.vhost.Enable()) // 验证禁用文件存在 disableFile := filepath.Join(s.configDir, "site", DisableConfName) _, err := os.Stat(disableFile) s.NoError(err) // 重新启用 s.NoError(s.vhost.SetEnable(true)) s.True(s.vhost.Enable()) // 验证禁用文件已删除 _, err = os.Stat(disableFile) s.True(os.IsNotExist(err)) } func (s *VhostTestSuite) TestDisableConfigContent() { // 禁用网站 s.NoError(s.vhost.SetEnable(false)) // 读取禁用配置内容 disableFile := filepath.Join(s.configDir, "site", DisableConfName) content, err := os.ReadFile(disableFile) s.NoError(err) // 验证内容包含 503 返回 s.Contains(string(content), "503") s.Contains(string(content), "return") } func (s *VhostTestSuite) TestServerName() { names := []string{"example.com", "www.example.com", "api.example.com"} s.NoError(s.vhost.SetServerName(names)) got := s.vhost.ServerName() s.Len(got, 3) s.Equal("example.com", got[0]) s.Equal("www.example.com", got[1]) s.Equal("api.example.com", got[2]) } func (s *VhostTestSuite) TestServerNameEmpty() { s.NoError(s.vhost.SetServerName([]string{})) } func (s *VhostTestSuite) TestRoot() { root := "/var/www/html" s.NoError(s.vhost.SetRoot(root)) s.Equal(root, s.vhost.Root()) } func (s *VhostTestSuite) TestIndex() { index := []string{"index.html", "index.php", "default.html"} s.NoError(s.vhost.SetIndex(index)) got := s.vhost.Index() s.Len(got, 3) s.Equal(index, got) } func (s *VhostTestSuite) TestIndexEmpty() { s.NoError(s.vhost.SetIndex([]string{})) } func (s *VhostTestSuite) TestListen() { listens := []types.Listen{ {Address: "80"}, {Address: "443", Args: []string{"ssl"}}, } s.NoError(s.vhost.SetListen(listens)) got := s.vhost.Listen() s.Len(got, 2) } func (s *VhostTestSuite) TestListenWithHTTP3() { listens := []types.Listen{ {Address: "443", Args: []string{"quic"}}, } s.NoError(s.vhost.SetListen(listens)) got := s.vhost.Listen() s.Len(got, 1) s.Equal("quic", got[0].Args[0]) } func (s *VhostTestSuite) TestSSL() { s.False(s.vhost.SSL()) s.Nil(s.vhost.SSLConfig()) } func (s *VhostTestSuite) TestSetSSLConfig() { sslConfig := &types.SSLConfig{ Cert: "/etc/ssl/cert.pem", Key: "/etc/ssl/key.pem", Protocols: []string{"TLSv1.2", "TLSv1.3"}, HSTS: true, OCSP: true, } s.NoError(s.vhost.SetSSLConfig(sslConfig)) s.True(s.vhost.SSL()) got := s.vhost.SSLConfig() s.NotNil(got) s.True(got.HSTS) s.True(got.OCSP) } func (s *VhostTestSuite) TestSetSSLConfigNil() { s.Error(s.vhost.SetSSLConfig(nil)) } func (s *VhostTestSuite) TestClearSSL() { sslConfig := &types.SSLConfig{ Cert: "/etc/ssl/cert.pem", Key: "/etc/ssl/key.pem", HSTS: true, } s.NoError(s.vhost.SetSSLConfig(sslConfig)) s.True(s.vhost.SSL()) s.NoError(s.vhost.ClearSSL()) s.False(s.vhost.SSL()) } func (s *VhostTestSuite) TestPHP() { s.Equal(uint(0), s.vhost.PHP()) s.NoError(s.vhost.SetPHP(84)) s.Equal(uint(84), s.vhost.PHP()) s.NoError(s.vhost.SetPHP(0)) s.Equal(uint(0), s.vhost.PHP()) } func (s *VhostTestSuite) TestAccessLog() { accessLog := "/var/log/nginx/access.log" s.NoError(s.vhost.SetAccessLog(accessLog)) s.Equal(accessLog, s.vhost.AccessLog()) } func (s *VhostTestSuite) TestErrorLog() { errorLog := "/var/log/nginx/error.log" s.NoError(s.vhost.SetErrorLog(errorLog)) s.Equal(errorLog, s.vhost.ErrorLog()) } func (s *VhostTestSuite) TestIncludes() { includes := []types.IncludeFile{ {Path: "/etc/nginx/conf.d/ssl.conf"}, {Path: "/etc/nginx/conf.d/php.conf"}, } s.NoError(s.vhost.SetIncludes(includes)) got := s.vhost.Includes() s.Len(got, 2) s.Equal(includes[0].Path, got[0].Path) s.Equal(includes[1].Path, got[1].Path) } func (s *VhostTestSuite) TestBasicAuth() { s.Nil(s.vhost.BasicAuth()) auth := map[string]string{ "realm": "Test Realm", "user_file": "/etc/nginx/htpasswd", } s.NoError(s.vhost.SetBasicAuth(auth)) got := s.vhost.BasicAuth() s.NotNil(got) s.Equal(auth["user_file"], got["user_file"]) s.NoError(s.vhost.ClearBasicAuth()) s.Nil(s.vhost.BasicAuth()) } func (s *VhostTestSuite) TestRateLimit() { s.Nil(s.vhost.RateLimit()) limit := &types.RateLimit{ Rate: "512k", Zone: map[string]string{ "perip": "10", }, } s.NoError(s.vhost.SetRateLimit(limit)) got := s.vhost.RateLimit() s.NotNil(got) s.Equal("512k", got.Rate) s.NoError(s.vhost.ClearRateLimit()) s.Nil(s.vhost.RateLimit()) } func (s *VhostTestSuite) TestReset() { s.NoError(s.vhost.SetServerName([]string{"modified.com"})) s.NoError(s.vhost.SetRoot("/modified/path")) s.NoError(s.vhost.Reset()) names := s.vhost.ServerName() s.NotContains(names, "modified.com") } func (s *VhostTestSuite) TestSave() { // 设置配置文件路径 configFile := filepath.Join(s.configDir, "nginx.conf") s.vhost.parser.SetConfigPath(configFile) s.NoError(s.vhost.SetServerName([]string{"save-test.com"})) s.NoError(s.vhost.Save()) // 验证配置文件已保存 content, err := os.ReadFile(configFile) s.NoError(err) s.Contains(string(content), "save-test.com") } func (s *VhostTestSuite) TestDump() { s.NoError(s.vhost.SetServerName([]string{"dump-test.com"})) s.NoError(s.vhost.SetRoot("/var/www/dump-test")) content := s.vhost.parser.Dump() s.NotEmpty(content) s.Contains(content, "dump-test.com") s.Contains(content, "/var/www/dump-test") s.Contains(content, "server") } func (s *VhostTestSuite) TestDumpWithSSL() { sslConfig := &types.SSLConfig{ Cert: "/etc/ssl/cert.pem", Key: "/etc/ssl/key.pem", Protocols: []string{"TLSv1.2", "TLSv1.3"}, } s.NoError(s.vhost.SetSSLConfig(sslConfig)) content := s.vhost.parser.Dump() s.Contains(content, "ssl_certificate") s.Contains(content, "ssl_certificate_key") } func (s *VhostTestSuite) TestHTTPSRedirect() { sslConfig := &types.SSLConfig{ Cert: "/etc/ssl/cert.pem", Key: "/etc/ssl/key.pem", HTTPRedirect: true, } s.NoError(s.vhost.SetSSLConfig(sslConfig)) got := s.vhost.SSLConfig() s.NotNil(got) s.True(got.HTTPRedirect) } func (s *VhostTestSuite) TestAltSvc() { sslConfig := &types.SSLConfig{ Cert: "/etc/ssl/cert.pem", Key: "/etc/ssl/key.pem", AltSvc: `h3=":$server_port"; ma=2592000`, } s.NoError(s.vhost.SetSSLConfig(sslConfig)) got := s.vhost.SSLConfig() s.NotNil(got) s.Contains(got.AltSvc, "h3=") } func (s *VhostTestSuite) TestDefaultConfIncludesServerD() { // 验证默认配置包含 site 的 include s.Contains(DefaultConf, "site") s.Contains(DefaultConf, "include") } func (s *VhostTestSuite) TestRedirects() { // 初始应该没有重定向 s.Empty(s.vhost.Redirects()) // 设置重定向 redirects := []types.Redirect{ { Type: types.RedirectTypeURL, From: "/old", To: "/new", StatusCode: 301, }, { Type: types.RedirectTypeHost, From: "old.example.com", To: "https://new.example.com", KeepURI: true, StatusCode: 308, }, } s.NoError(s.vhost.SetRedirects(redirects)) // 验证重定向文件已创建 siteDir := filepath.Join(s.configDir, "site") entries, err := os.ReadDir(siteDir) s.NoError(err) redirectCount := 0 for _, entry := range entries { if strings.HasPrefix(entry.Name(), "1") && strings.HasSuffix(entry.Name(), "-redirect.conf") { redirectCount++ } } s.Equal(2, redirectCount) // 验证可以读取回来 got := s.vhost.Redirects() s.Len(got, 2) } func (s *VhostTestSuite) TestRedirectURL() { redirects := []types.Redirect{ { Type: types.RedirectTypeURL, From: "/old-page", To: "/new-page", StatusCode: 301, }, } s.NoError(s.vhost.SetRedirects(redirects)) // 读取配置文件内容 siteDir := filepath.Join(s.configDir, "site") content, err := os.ReadFile(filepath.Join(siteDir, "100-redirect.conf")) s.NoError(err) s.Contains(string(content), "location = /old-page") s.Contains(string(content), "return 301") s.Contains(string(content), "/new-page") } func (s *VhostTestSuite) TestRedirectHost() { redirects := []types.Redirect{ { Type: types.RedirectTypeHost, From: "old.example.com", To: "https://new.example.com", KeepURI: true, StatusCode: 308, }, } s.NoError(s.vhost.SetRedirects(redirects)) // 读取配置文件内容 siteDir := filepath.Join(s.configDir, "site") content, err := os.ReadFile(filepath.Join(siteDir, "100-redirect.conf")) s.NoError(err) s.Contains(string(content), "$host") s.Contains(string(content), "old.example.com") s.Contains(string(content), "return 308") s.Contains(string(content), "$request_uri") } func (s *VhostTestSuite) TestRedirect404() { redirects := []types.Redirect{ { Type: types.RedirectType404, To: "/custom-404.html", StatusCode: 308, }, } s.NoError(s.vhost.SetRedirects(redirects)) // 读取配置文件内容 siteDir := filepath.Join(s.configDir, "site") content, err := os.ReadFile(filepath.Join(siteDir, "100-redirect.conf")) s.NoError(err) s.Contains(string(content), "error_page 404") s.Contains(string(content), "@redirect_404") } // ProxyVhost 测试套件 type ProxyVhostTestSuite struct { suite.Suite vhost *ProxyVhost configDir string } func TestProxyVhostTestSuite(t *testing.T) { suite.Run(t, &ProxyVhostTestSuite{}) } func (s *ProxyVhostTestSuite) SetupTest() { configDir, err := os.MkdirTemp("", "nginx-proxy-test-*") s.Require().NoError(err) s.configDir = configDir // 创建 site 和 shared 目录 s.NoError(os.MkdirAll(filepath.Join(configDir, "site"), 0755)) s.NoError(os.MkdirAll(filepath.Join(configDir, "shared"), 0755)) vhost, err := NewProxyVhost(configDir) s.Require().NoError(err) s.vhost = vhost } func (s *ProxyVhostTestSuite) TearDownTest() { if s.configDir != "" { s.NoError(os.RemoveAll(s.configDir)) } } func (s *ProxyVhostTestSuite) TestProxies() { // 初始应该没有代理配置 s.Empty(s.vhost.Proxies()) // 设置代理配置 proxies := []types.Proxy{ { Location: "/", Pass: "http://backend", Host: "example.com", }, { Location: "/api", Pass: "http://api-backend:8080", Buffering: true, }, } s.NoError(s.vhost.SetProxies(proxies)) // 验证代理文件已创建 siteDir := filepath.Join(s.configDir, "site") entries, err := os.ReadDir(siteDir) s.NoError(err) proxyCount := 0 for _, entry := range entries { if strings.HasPrefix(entry.Name(), "2") && strings.HasSuffix(entry.Name(), "-proxy.conf") { proxyCount++ } } s.Equal(2, proxyCount) // 验证可以读取回来 got := s.vhost.Proxies() s.Len(got, 2) } func (s *ProxyVhostTestSuite) TestProxyConfig() { proxies := []types.Proxy{ { Location: "/", Pass: "http://backend", Host: "example.com", SNI: "example.com", Buffering: true, }, } s.NoError(s.vhost.SetProxies(proxies)) // 读取配置文件内容 siteDir := filepath.Join(s.configDir, "site") content, err := os.ReadFile(filepath.Join(siteDir, "200-proxy.conf")) s.NoError(err) s.Contains(string(content), "location /") s.Contains(string(content), "proxy_pass http://backend") s.Contains(string(content), "proxy_set_header Host") s.Contains(string(content), "example.com") s.Contains(string(content), "proxy_ssl_name") s.Contains(string(content), "proxy_buffering on") } func (s *ProxyVhostTestSuite) TestClearProxies() { proxies := []types.Proxy{ {Location: "/", Pass: "http://backend"}, } s.NoError(s.vhost.SetProxies(proxies)) s.Len(s.vhost.Proxies(), 1) s.NoError(s.vhost.ClearProxies()) s.Empty(s.vhost.Proxies()) } func (s *ProxyVhostTestSuite) TestUpstreams() { // 初始应该没有上游服务器配置 s.Empty(s.vhost.Upstreams()) // 设置上游服务器 upstreams := map[string]types.Upstream{ "backend": { Servers: map[string]string{ "127.0.0.1:8080": "weight=5", "127.0.0.1:8081": "weight=3", }, Algo: "least_conn", Keepalive: 32, }, } s.NoError(s.vhost.SetUpstreams(upstreams)) // 验证 upstream 文件已创建 sharedDir := filepath.Join(s.configDir, "shared") entries, err := os.ReadDir(sharedDir) s.NoError(err) s.NotEmpty(entries) // 验证可以读取回来 got := s.vhost.Upstreams() s.Len(got, 1) s.Contains(got, "backend") s.Equal("least_conn", got["backend"].Algo) s.Equal(32, got["backend"].Keepalive) } func (s *ProxyVhostTestSuite) TestUpstreamConfig() { upstreams := map[string]types.Upstream{ "mybackend": { Servers: map[string]string{ "127.0.0.1:8080": "weight=5", }, Algo: "ip_hash", Keepalive: 16, }, } s.NoError(s.vhost.SetUpstreams(upstreams)) // 读取配置文件内容 sharedDir := filepath.Join(s.configDir, "shared") entries, err := os.ReadDir(sharedDir) s.NoError(err) s.Require().NotEmpty(entries) content, err := os.ReadFile(filepath.Join(sharedDir, entries[0].Name())) s.NoError(err) s.Contains(string(content), "upstream mybackend") s.Contains(string(content), "ip_hash") s.Contains(string(content), "server 127.0.0.1:8080") s.Contains(string(content), "weight=5") s.Contains(string(content), "keepalive 16") } func (s *ProxyVhostTestSuite) TestClearUpstreams() { upstreams := map[string]types.Upstream{ "backend": { Servers: map[string]string{"127.0.0.1:8080": ""}, }, } s.NoError(s.vhost.SetUpstreams(upstreams)) s.Len(s.vhost.Upstreams(), 1) s.NoError(s.vhost.ClearUpstreams()) s.Empty(s.vhost.Upstreams()) } func (s *ProxyVhostTestSuite) TestProxyWithUpstream() { // 先创建 upstream upstreams := map[string]types.Upstream{ "api-servers": { Servers: map[string]string{ "127.0.0.1:3000": "", "127.0.0.1:3001": "", }, Algo: "least_conn", }, } s.NoError(s.vhost.SetUpstreams(upstreams)) // 然后创建引用 upstream 的 proxy proxies := []types.Proxy{ { Location: "/api", Pass: "http://api-servers", }, } s.NoError(s.vhost.SetProxies(proxies)) // 验证两者都存在 s.Len(s.vhost.Upstreams(), 1) s.Len(s.vhost.Proxies(), 1) // 验证 proxy 配置中引用了 upstream siteDir := filepath.Join(s.configDir, "site") content, err := os.ReadFile(filepath.Join(siteDir, "200-proxy.conf")) s.NoError(err) s.Contains(string(content), "http://api-servers") }