Files
dokku/plugins/nginx-vhosts/template_test.go
Jose Diaz-Gonzalez 465de6cc71 refactor: consolidate nginx.conf.sigil server blocks
The default nginx template rendered four near-identical server blocks per app, so the same listen, access_log, error_log, ssl_*, error_page, and proxy chain had to be maintained across http, https, grpc, and grpcs branches. Merging http and https into a single branch keyed on an `is_ssl` boolean, and likewise grpc and grpcs, removes the duplicate proxy_set_header chain and error-page locations and brings the structure in line with the openresty proxy template. Output is preserved up to whitespace and the existing `cat -s` pass already squashes the leftover blank lines. Adds plugins/nginx-vhosts/template_test.go exercising the rendered output via sigil as a Go library across HTTP-only, HTTPS, HTTP-to-HTTPS redirect, no-listeners 502 fallback, gRPC, gRPCs, gRPC-without-listeners skip, IPv4 bind, upstream blocks with and without keepalive, the access_log `off` short-circuit, the X-Forwarded-Ssl toggle, the http2 listen-parameter vs directive split, http2_push_preload conditional emission, and the nginx.conf.d/*.conf include in every code path; and tests/unit/nginx-vhosts_16.bats covering deploy plus `nginx -t` end-to-end for both HTTP and HTTPS apps.
2026-04-30 16:36:45 -04:00

292 lines
9.0 KiB
Go

package nginxvhosts
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/gliderlabs/sigil"
_ "github.com/gliderlabs/sigil/builtin"
)
func templatePath(t *testing.T) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
return filepath.Join(filepath.Dir(file), "templates", "nginx.conf.sigil")
}
func defaultVars() map[string]interface{} {
return map[string]interface{}{
"APP": "app",
"DOKKU_ROOT": "/home/dokku",
"DOKKU_LIB_ROOT": "/var/lib/dokku",
"NOSSL_SERVER_NAME": "app.example.com",
"SSL_SERVER_NAME": "",
"SSL_INUSE": "",
"APP_SSL_PATH": "/home/dokku/app/tls",
"DOKKU_APP_WEB_LISTENERS": "127.0.0.1:5000",
"PROXY_PORT_MAP": "http:80:5000",
"PROXY_UPSTREAM_PORTS": "5000",
"PROXY_PORT": "80",
"PROXY_SSL_PORT": "443",
"PROXY_KEEPALIVE": "",
"NGINX_BIND_ADDRESS_IP4": "",
"NGINX_BIND_ADDRESS_IP6": "::",
"NGINX_ACCESS_LOG_PATH": "/var/log/nginx/app-access.log",
"NGINX_ACCESS_LOG_FORMAT": "",
"NGINX_ERROR_LOG_PATH": "/var/log/nginx/app-error.log",
"NGINX_UNDERSCORE_IN_HEADERS": "off",
"CLIENT_BODY_TIMEOUT": "60s",
"CLIENT_HEADER_TIMEOUT": "60s",
"CLIENT_MAX_BODY_SIZE": "1m",
"KEEPALIVE_TIMEOUT": "75s",
"LINGERING_TIMEOUT": "5s",
"SEND_TIMEOUT": "60s",
"PROXY_CONNECT_TIMEOUT": "60s",
"PROXY_READ_TIMEOUT": "60s",
"PROXY_SEND_TIMEOUT": "60s",
"PROXY_BUFFER_SIZE": "4k",
"PROXY_BUFFERING": "on",
"PROXY_BUFFERS": "8 4k",
"PROXY_BUSY_BUFFERS_SIZE": "8k",
"PROXY_X_FORWARDED_FOR": "$remote_addr",
"PROXY_X_FORWARDED_PORT": "$server_port",
"PROXY_X_FORWARDED_PROTO": "$scheme",
"PROXY_X_FORWARDED_SSL": "",
"HTTP2_DIRECTIVE_SUPPORTED": "true",
}
}
func renderTemplate(t *testing.T, vars map[string]interface{}) string {
t.Helper()
data, err := os.ReadFile(templatePath(t))
if err != nil {
t.Fatalf("read template: %v", err)
}
buf, err := sigil.Execute(data, vars, "nginx.conf.sigil")
if err != nil {
t.Fatalf("sigil.Execute: %v", err)
}
return buf.String()
}
func mustContain(t *testing.T, out, needle string) {
t.Helper()
if !strings.Contains(out, needle) {
t.Errorf("expected output to contain %q\n--- output ---\n%s", needle, out)
}
}
func mustNotContain(t *testing.T, out, needle string) {
t.Helper()
if strings.Contains(out, needle) {
t.Errorf("expected output NOT to contain %q\n--- output ---\n%s", needle, out)
}
}
func TestTemplate_HTTPOnlyBasicProxy(t *testing.T) {
out := renderTemplate(t, defaultVars())
mustContain(t, out, "listen [::]:80;")
mustContain(t, out, "proxy_pass http://app-5000;")
mustContain(t, out, "error_page 500 501 502 503")
mustContain(t, out, "server_name app.example.com;")
mustNotContain(t, out, "ssl_certificate")
mustNotContain(t, out, "return 301 https")
mustNotContain(t, out, "http2_push_preload")
}
func TestTemplate_HTTPSEmitsPushPreloadWhenSupported(t *testing.T) {
v := defaultVars()
v["PROXY_PORT_MAP"] = "https:443:5000"
v["SSL_INUSE"] = "true"
v["SSL_SERVER_NAME"] = "app.example.com"
v["HTTP2_PUSH_SUPPORTED"] = "true"
out := renderTemplate(t, v)
mustContain(t, out, "http2_push_preload on;")
}
func TestTemplate_HTTPSOmitsPushPreloadWhenUnsupported(t *testing.T) {
v := defaultVars()
v["PROXY_PORT_MAP"] = "https:443:5000"
v["SSL_INUSE"] = "true"
v["SSL_SERVER_NAME"] = "app.example.com"
v["HTTP2_PUSH_SUPPORTED"] = "false"
out := renderTemplate(t, v)
mustNotContain(t, out, "http2_push_preload")
}
func TestTemplate_HTTPRedirectsToHTTPSWhenSSLInUse(t *testing.T) {
v := defaultVars()
v["PROXY_PORT_MAP"] = "http:80:5000 https:443:5000"
v["SSL_INUSE"] = "true"
v["SSL_SERVER_NAME"] = "app.example.com"
out := renderTemplate(t, v)
port80, _, ok := strings.Cut(out, "listen [::]:443")
if !ok {
t.Fatalf("expected an https server block in output:\n%s", out)
}
mustContain(t, port80, "return 301 https://$host:443$request_uri;")
mustContain(t, port80, "include /home/dokku/app/nginx.conf.d/*.conf;")
mustNotContain(t, port80, "proxy_pass http://app-5000;")
mustContain(t, out, "ssl_certificate /home/dokku/app/tls/server.crt;")
mustContain(t, out, "proxy_pass http://app-5000;")
}
func TestTemplate_HTTPSWithHTTP2Directive(t *testing.T) {
v := defaultVars()
v["PROXY_PORT_MAP"] = "https:443:5000"
v["SSL_INUSE"] = "true"
v["SSL_SERVER_NAME"] = "app.example.com"
v["HTTP2_DIRECTIVE_SUPPORTED"] = "true"
out := renderTemplate(t, v)
mustContain(t, out, "listen [::]:443 ssl;")
mustContain(t, out, "http2 on;")
mustNotContain(t, out, "listen [::]:443 ssl http2;")
}
func TestTemplate_HTTPSWithHTTP2Parameter(t *testing.T) {
v := defaultVars()
v["PROXY_PORT_MAP"] = "https:443:5000"
v["SSL_INUSE"] = "true"
v["SSL_SERVER_NAME"] = "app.example.com"
v["HTTP2_DIRECTIVE_SUPPORTED"] = "false"
out := renderTemplate(t, v)
mustContain(t, out, "listen [::]:443 ssl http2;")
mustNotContain(t, out, "http2 on;")
}
func TestTemplate_NoWebListenersReturns502(t *testing.T) {
v := defaultVars()
v["DOKKU_APP_WEB_LISTENERS"] = ""
out := renderTemplate(t, v)
mustContain(t, out, "return 502;")
mustNotContain(t, out, "proxy_pass http://app-5000;")
}
func TestTemplate_GRPCNoSSL(t *testing.T) {
v := defaultVars()
v["PROXY_PORT_MAP"] = "grpc:50051:50051"
v["PROXY_UPSTREAM_PORTS"] = "50051"
out := renderTemplate(t, v)
mustContain(t, out, "grpc_pass grpc://app-50051;")
mustContain(t, out, "http2")
mustNotContain(t, out, "ssl_certificate")
}
func TestTemplate_GRPCS(t *testing.T) {
v := defaultVars()
v["PROXY_PORT_MAP"] = "grpcs:443:50051"
v["PROXY_UPSTREAM_PORTS"] = "50051"
v["SSL_INUSE"] = "true"
v["SSL_SERVER_NAME"] = "app.example.com"
out := renderTemplate(t, v)
mustContain(t, out, "ssl_certificate /home/dokku/app/tls/server.crt;")
mustContain(t, out, "grpc_pass grpc://app-50051;")
}
func TestTemplate_GRPCSkippedWithoutListeners(t *testing.T) {
v := defaultVars()
v["PROXY_PORT_MAP"] = "grpc:50051:50051"
v["PROXY_UPSTREAM_PORTS"] = "50051"
v["DOKKU_APP_WEB_LISTENERS"] = ""
out := renderTemplate(t, v)
mustNotContain(t, out, "grpc_pass")
mustNotContain(t, out, "server {")
}
func TestTemplate_BindAddressIPv4(t *testing.T) {
v := defaultVars()
v["NGINX_BIND_ADDRESS_IP4"] = "127.0.0.1"
out := renderTemplate(t, v)
mustContain(t, out, "listen 127.0.0.1:80;")
v2 := defaultVars()
out2 := renderTemplate(t, v2)
mustNotContain(t, out2, "listen 127.0.0.1")
mustNotContain(t, out2, ":80;\n listen :80;")
}
func TestTemplate_UpstreamBlock(t *testing.T) {
v := defaultVars()
v["PROXY_PORT_MAP"] = "http:80:5000 http:8080:5001"
v["PROXY_UPSTREAM_PORTS"] = "5000 5001"
v["DOKKU_APP_WEB_LISTENERS"] = "10.0.0.1:5000 10.0.0.2:5000"
out := renderTemplate(t, v)
mustContain(t, out, "upstream app-5000 {")
mustContain(t, out, "upstream app-5001 {")
mustContain(t, out, "server 10.0.0.1:5000;")
mustContain(t, out, "server 10.0.0.2:5000;")
mustContain(t, out, "server 10.0.0.1:5001;")
mustContain(t, out, "server 10.0.0.2:5001;")
}
func TestTemplate_UpstreamWithKeepalive(t *testing.T) {
v := defaultVars()
v["PROXY_KEEPALIVE"] = "16"
out := renderTemplate(t, v)
mustContain(t, out, "keepalive 16;")
v2 := defaultVars()
out2 := renderTemplate(t, v2)
mustNotContain(t, out2, "keepalive 16;")
}
func TestTemplate_AccessLogFormat(t *testing.T) {
v := defaultVars()
v["NGINX_ACCESS_LOG_FORMAT"] = "json"
v["NGINX_ACCESS_LOG_PATH"] = "/var/log/nginx/app-access.log"
out := renderTemplate(t, v)
mustContain(t, out, "access_log /var/log/nginx/app-access.log json;")
v2 := defaultVars()
v2["NGINX_ACCESS_LOG_FORMAT"] = "json"
v2["NGINX_ACCESS_LOG_PATH"] = "off"
out2 := renderTemplate(t, v2)
mustContain(t, out2, "access_log off;")
mustNotContain(t, out2, "access_log off json;")
}
func TestTemplate_XForwardedSSL(t *testing.T) {
v := defaultVars()
v["PROXY_X_FORWARDED_SSL"] = "on"
out := renderTemplate(t, v)
mustContain(t, out, "proxy_set_header X-Forwarded-Ssl on;")
v2 := defaultVars()
out2 := renderTemplate(t, v2)
mustNotContain(t, out2, "X-Forwarded-Ssl")
}
func TestTemplate_NginxConfDIncludeAlways(t *testing.T) {
cases := []struct {
name string
mutate func(map[string]interface{})
}{
{"http", func(v map[string]interface{}) {}},
{"http_redirect", func(v map[string]interface{}) {
v["PROXY_PORT_MAP"] = "http:80:5000 https:443:5000"
v["SSL_INUSE"] = "true"
v["SSL_SERVER_NAME"] = "app.example.com"
}},
{"grpc", func(v map[string]interface{}) {
v["PROXY_PORT_MAP"] = "grpc:50051:50051"
v["PROXY_UPSTREAM_PORTS"] = "50051"
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
v := defaultVars()
tc.mutate(v)
out := renderTemplate(t, v)
mustContain(t, out, "include /home/dokku/app/nginx.conf.d/*.conf;")
})
}
}