Files
1Panel/agent/app/service/nginx_http_config_test.go
Kadxy 37170ae81a fix(openresty): report brotli as pending when the http.d include can be added (#14285)
Installs that enabled ngx_brotli before v2.3.0 have no http.d include in
nginx.conf, because the include is only inserted when a module is toggled
or built. The settings page reported this as "cannot write the managed
configuration" and skipped the brotli save, so the save path that inserts
the include was never reached, even on a stock nginx.conf.

The settings response now tells the two cases apart with an in-memory
insertion check that writes nothing:

- include missing but insertable, or managed file missing: managedPending,
  the page explains the values take effect after saving and saves normally
- no safe insertion point: managedUnavailable, unchanged

The save path now inserts the include through ensureNginxHTTPIncludeActive,
the same function module activation uses, so both produce identical
results. A sentinel error keeps "no insertion point" mapped to
ErrBrotliUnsupported while I/O failures are still returned as-is.
2026-11-03 18:58:24 +08:00

184 lines
5.9 KiB
Go

package service
import (
"errors"
"strings"
"testing"
)
const plainNginxConf = `user root;
worker_processes auto;
include /usr/local/openresty/nginx/conf/modules-enabled/*.conf;
events {
use epoll;
}
http {
include mime.types;
default_type application/octet-stream;
gzip on;
gzip_comp_level 5;
limit_conn_zone $binary_remote_addr zone=perip:10m;
include /usr/local/openresty/nginx/conf/conf.d/*.conf;
include /usr/local/openresty/nginx/conf/default/*.conf;
}
`
func TestInsertNginxHTTPIncludeBeforeConfD(t *testing.T) {
got, err := insertNginxHTTPInclude(plainNginxConf)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(got, " "+nginxHTTPIncludeDirective) {
t.Fatalf("include not inserted with matching indent:\n%s", got)
}
// Ordering is the point of the insertion site: panel defaults must be
// evaluated before per-site configuration.
httpIdx := strings.Index(got, "conf/http.d/*.conf")
confDIdx := strings.Index(got, "conf/conf.d/*.conf")
if httpIdx < 0 || confDIdx < 0 || httpIdx > confDIdx {
t.Fatalf("http.d must be included before conf.d (http.d=%d conf.d=%d)", httpIdx, confDIdx)
}
// The rest of the file must be untouched.
stripped := strings.Replace(got, " "+nginxHTTPIncludeDirective+"\n", "", 1)
if stripped != plainNginxConf {
t.Fatal("insertion altered content outside the inserted line")
}
}
func TestInsertNginxHTTPIncludeIsIdempotent(t *testing.T) {
once, err := insertNginxHTTPInclude(plainNginxConf)
if err != nil {
t.Fatal(err)
}
twice, err := insertNginxHTTPInclude(once)
if err != nil {
t.Fatal(err)
}
if twice != once {
t.Fatal("a second insertion must be a no-op")
}
}
func TestInsertNginxHTTPIncludeFallsBackToHTTPBlock(t *testing.T) {
content := strings.Replace(plainNginxConf,
" include /usr/local/openresty/nginx/conf/conf.d/*.conf;\n", "", 1)
got, err := insertNginxHTTPInclude(content)
if err != nil {
t.Fatal(err)
}
httpIdx := strings.Index(got, "http {")
incIdx := strings.Index(got, nginxHTTPIncludeDirective)
if incIdx < 0 || incIdx < httpIdx {
t.Fatalf("include should land inside the http block:\n%s", got)
}
// Indented one level deeper than the http keyword.
if !strings.Contains(got, " "+nginxHTTPIncludeDirective) {
t.Errorf("fallback indentation is wrong:\n%s", got)
}
}
func TestInsertNginxHTTPIncludeRejectsConfigWithoutHTTPBlock(t *testing.T) {
_, err := insertNginxHTTPInclude("events {}\n")
if err == nil {
t.Fatal("a config without an http block must be rejected so callers can degrade")
}
// The save path maps this error to an actionable message and must be able
// to tell it apart from an I/O failure.
if !errors.Is(err, errNginxHTTPIncludeNoInsertionPoint) {
t.Fatalf("unexpected error: %v", err)
}
}
func TestInsertNginxHTTPIncludeIgnoresCommentedIncludes(t *testing.T) {
commented := strings.Replace(plainNginxConf,
" include /usr/local/openresty/nginx/conf/conf.d/*.conf;",
" # include /usr/local/openresty/nginx/conf/conf.d/*.conf;", 1)
got, err := insertNginxHTTPInclude(commented)
if err != nil {
t.Fatal(err)
}
// The commented conf.d line is not a valid anchor; the fallback must win.
if strings.Index(got, nginxHTTPIncludeDirective) < strings.Index(got, "http {") {
t.Fatal("a commented include must not be used as the anchor")
}
}
func TestInsertNginxHTTPIncludeHandlesCRLF(t *testing.T) {
crlf := strings.ReplaceAll(plainNginxConf, "\n", "\r\n")
got, err := insertNginxHTTPInclude(crlf)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(got, nginxHTTPIncludeDirective) {
t.Fatal("include missing on a CRLF file")
}
}
func TestNginxHTTPIncludeRe(t *testing.T) {
cases := []struct {
name string
content string
want bool
}{
{"present", plainNginxConf + " " + nginxHTTPIncludeDirective + "\n", true},
{"absent", plainNginxConf, false},
{"commented out", "# " + nginxHTTPIncludeDirective, false},
// nginx accepts quoted paths, and a hand-written or legacy installer
// may use them; a quoted include must count as present.
{"quoted absolute path", `include "/usr/local/openresty/nginx/conf/http.d/*.conf";`, true},
{"quoted with extra whitespace", ` include "/usr/local/openresty/nginx/conf/http.d/*.conf" ;`, true},
{"relative path form", ` include http.d/*.conf;`, true},
{"a different directory does not count", ` include /usr/local/openresty/nginx/conf/conf.d/*.conf;`, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := nginxHTTPIncludeRe.MatchString(tc.content); got != tc.want {
t.Fatalf("expected %v, got %v", tc.want, got)
}
})
}
}
// The anchor for insertion must tolerate the same variants, or a config with
// a quoted conf.d include would take the http-block fallback for no reason.
func TestInsertNginxHTTPIncludeWithQuotedConfDAnchor(t *testing.T) {
quoted := strings.Replace(plainNginxConf,
" include /usr/local/openresty/nginx/conf/conf.d/*.conf;",
` include "/usr/local/openresty/nginx/conf/conf.d/*.conf";`, 1)
got, err := insertNginxHTTPInclude(quoted)
if err != nil {
t.Fatal(err)
}
httpIdx := strings.Index(got, "conf/http.d/*.conf")
confDIdx := strings.Index(got, "conf/conf.d/*.conf")
if httpIdx < 0 || confDIdx < 0 || httpIdx > confDIdx {
t.Fatalf("quoted anchor not used; include misplaced:\n%s", got)
}
}
// A CRLF file must keep its own line endings after insertion.
func TestInsertNginxHTTPIncludeKeepsCRLFStyle(t *testing.T) {
crlf := strings.ReplaceAll(plainNginxConf, "\n", "\r\n")
got, err := insertNginxHTTPInclude(crlf)
if err != nil {
t.Fatal(err)
}
idx := strings.Index(got, nginxHTTPIncludeDirective)
if idx < 0 {
t.Fatal("include missing")
}
if got[idx-1] == '\n' || (idx >= 2 && got[idx-2:idx] != "\r\n" && got[idx-1] != ' ') {
// The inserted line must end with \r\n like the rest of the file.
}
end := idx + len(nginxHTTPIncludeDirective)
if end+2 > len(got) || got[end:end+2] != "\r\n" {
t.Fatalf("inserted line does not end with CRLF: %q", got[end:end+4])
}
}