Neue Domain kann per 301 auf eine andere Domain/URL umgeleitet werden, statt auf ein Backend zu routen (Use-Case: kvs.netcell-it.de → https://zkm.netcell-it.de, inkl. HTTPS). Variante (a): immer auf Ziel-Root (`redirect location`), pfad- unabhängig. - Migration 0043: domains.redirect_to text NOT NULL DEFAULT '' - Model + domains-Service (SELECT/INSERT/UPDATE/scan) - HAProxy-Generator: buildRedirectTo() sanitisiert (nur http(s), kein Whitespace/Quotes → sonst kein Redirect statt kaputter Config); Template emittiert `http-request redirect location <url> code 301 if hdr(host)`. Terminiert vor use_backend → Redirect-Domain routet auf kein Backend. http→https läuft über den vorhandenen :80-Redirect (zwei Hops, inkl. TLS). - UI: Feld „Weiterleitung (301) nach" im Domain-Formular (de/en) - Tests: Render-Zeile + buildRedirectTo-Sanitisierung Hinweis: Die Redirect-Domain braucht weiterhin ein eigenes TLS-Zert (ACME). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
583 lines
19 KiB
Go
583 lines
19 KiB
Go
package haproxy
|
||
|
||
import (
|
||
"bytes"
|
||
"strings"
|
||
"testing"
|
||
|
||
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
||
)
|
||
|
||
func renderView(t *testing.T, v View) string {
|
||
t.Helper()
|
||
var buf bytes.Buffer
|
||
if err := tpl.Execute(&buf, v); err != nil {
|
||
t.Fatalf("template execute: %v", err)
|
||
}
|
||
return buf.String()
|
||
}
|
||
|
||
func mkBackend(id int64, name string, hcp *string) models.Backend {
|
||
return models.Backend{
|
||
ID: id, Name: name, Scheme: "http",
|
||
LBAlgorithm: "roundrobin", Active: true,
|
||
HealthCheckPath: hcp,
|
||
}
|
||
}
|
||
|
||
func mkServer(backendID int64, name, addr string, port int) models.BackendServer {
|
||
return models.BackendServer{
|
||
BackendID: backendID, Name: name, Address: addr, Port: port,
|
||
Weight: 100, Active: true,
|
||
}
|
||
}
|
||
|
||
func TestRender_BaselineHasFrontendsAndApiBackend(t *testing.T) {
|
||
out := renderView(t, View{})
|
||
for _, w := range []string{
|
||
"frontend public_http",
|
||
"frontend public_https",
|
||
"frontend internal_stats",
|
||
"backend api_backend",
|
||
"server api1 127.0.0.1:9443 check",
|
||
"bind :443 ssl crt /etc/edgeguard/tls/",
|
||
// HTTP/3 (QUIC) zusätzlich zum h2/http1.1-Listener.
|
||
"bind quic4@:443 ssl crt /etc/edgeguard/tls/ alpn h3",
|
||
// limited-quic global muss gesetzt sein, sonst weigert sich
|
||
// HAProxy 3.0 das quic4-bind anzunehmen (OpenSSL-Kompat-Layer).
|
||
"limited-quic",
|
||
// Alt-Svc damit Browser auf h3 upgraden.
|
||
`Alt-Svc "h3=\":443\"; ma=86400"`,
|
||
"path_beg /.well-known/acme-challenge/",
|
||
"http-request redirect scheme https",
|
||
// Client-IP-Weiterleitung an Backends — XFF kommt aus
|
||
// defaults (option forwardfor), Proto + RealIP setzen wir
|
||
// pro public-Frontend explizit.
|
||
"option forwardfor",
|
||
"http-request set-header X-Forwarded-Proto https",
|
||
"http-request set-header X-Real-IP %[src]",
|
||
} {
|
||
if !strings.Contains(out, w) {
|
||
t.Errorf("missing %q in baseline output:\n%s", w, out)
|
||
}
|
||
}
|
||
// Globales HSTS auf public_https darf NICHT mehr drin sein —
|
||
// das wird jetzt pro Domain via ACL gesetzt (siehe HSTS-Test).
|
||
// mgmt_https hat aber weiterhin ein globales HSTS.
|
||
publicIdx := strings.Index(out, "frontend public_https")
|
||
mgmtIdx := strings.Index(out, "frontend mgmt_https")
|
||
if publicIdx < 0 || mgmtIdx < 0 || publicIdx >= mgmtIdx {
|
||
t.Fatalf("frontend ordering unexpected:\n%s", out)
|
||
}
|
||
publicBlock := out[publicIdx:mgmtIdx]
|
||
if strings.Contains(publicBlock, "set-header Strict-Transport-Security") {
|
||
t.Errorf("public_https soll KEIN globales HSTS mehr enthalten (pro-Domain ACL):\n%s", publicBlock)
|
||
}
|
||
}
|
||
|
||
func TestRender_HSTSPerDomain(t *testing.T) {
|
||
v := View{
|
||
Domains: []DomainView{
|
||
{
|
||
Domain: models.Domain{
|
||
ID: 1, Name: "a.example.com", Active: true,
|
||
HSTSEnabled: true, HSTSMaxAge: 63072000,
|
||
HSTSSubdomains: true, HSTSPreload: true,
|
||
},
|
||
HSTSHeader: "max-age=63072000; includeSubDomains; preload",
|
||
},
|
||
{
|
||
Domain: models.Domain{
|
||
ID: 2, Name: "b.example.com", Active: true,
|
||
HSTSEnabled: false,
|
||
},
|
||
},
|
||
},
|
||
}
|
||
out := renderView(t, v)
|
||
for _, w := range []string{
|
||
// Erst löschen (gegen Backend-set HSTS), dann setzen.
|
||
`http-response del-header Strict-Transport-Security if { hdr(host) -i a.example.com }`,
|
||
`http-response set-header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" if { hdr(host) -i a.example.com }`,
|
||
} {
|
||
if !strings.Contains(out, w) {
|
||
t.Errorf("missing %q in per-domain HSTS output:\n%s", w, out)
|
||
}
|
||
}
|
||
if strings.Contains(out, "Strict-Transport-Security \"\" if { hdr(host) -i b.example.com }") ||
|
||
strings.Contains(out, "if { hdr(host) -i b.example.com }") && strings.Contains(out, "Strict-Transport-Security") &&
|
||
strings.Contains(out, "b.example.com") && strings.Count(out, "Strict-Transport-Security") > 2 {
|
||
// HSTS soll für Domain ohne HSTSEnabled gar nicht erst gerendert werden.
|
||
// (mgmt_https hat noch eins, plus die eine Zeile von a.example.com → 2 Vorkommen erwartet.)
|
||
}
|
||
}
|
||
|
||
func TestRender_MaintenanceModeBlocksWith503(t *testing.T) {
|
||
msg := `Wartung — bitte später wiederkommen.`
|
||
v := View{
|
||
Domains: []DomainView{
|
||
{
|
||
Domain: models.Domain{
|
||
ID: 1, Name: "down.example.com", Active: true,
|
||
MaintenanceMode: true, MaintenanceMessage: &msg,
|
||
},
|
||
MaintMessage: msg,
|
||
},
|
||
},
|
||
}
|
||
out := renderView(t, v)
|
||
want := `http-request return status 503 content-type "text/plain; charset=utf-8" string "Wartung — bitte später wiederkommen." if { hdr(host) -i down.example.com }`
|
||
if !strings.Contains(out, want) {
|
||
t.Errorf("missing maintenance-503 line:\n%s", out)
|
||
}
|
||
}
|
||
|
||
func TestRender_WWWRedirectToNaked(t *testing.T) {
|
||
v := View{
|
||
Domains: []DomainView{
|
||
{
|
||
Domain: models.Domain{
|
||
ID: 1, Name: "example.com", Active: true,
|
||
WWWRedirect: "to-naked",
|
||
},
|
||
RedirectFromHost: "www.example.com",
|
||
},
|
||
},
|
||
}
|
||
out := renderView(t, v)
|
||
want := `http-request redirect prefix https://example.com code 301 if { hdr(host) -i www.example.com }`
|
||
if !strings.Contains(out, want) {
|
||
t.Errorf("missing www→naked redirect line:\n%s", out)
|
||
}
|
||
}
|
||
|
||
func TestRender_WWWRedirectToWWW(t *testing.T) {
|
||
v := View{
|
||
Domains: []DomainView{
|
||
{
|
||
Domain: models.Domain{
|
||
ID: 1, Name: "www.example.com", Active: true,
|
||
WWWRedirect: "to-www",
|
||
},
|
||
RedirectFromHost: "example.com",
|
||
},
|
||
},
|
||
}
|
||
out := renderView(t, v)
|
||
want := `http-request redirect prefix https://www.example.com code 301 if { hdr(host) -i example.com }`
|
||
if !strings.Contains(out, want) {
|
||
t.Errorf("missing naked→www redirect line:\n%s", out)
|
||
}
|
||
}
|
||
|
||
func TestRender_RedirectTo(t *testing.T) {
|
||
v := View{
|
||
Domains: []DomainView{
|
||
{
|
||
Domain: models.Domain{
|
||
ID: 1, Name: "kvs.netcell-it.de", Active: true,
|
||
RedirectTo: "https://zkm.netcell-it.de",
|
||
},
|
||
RedirectTo: "https://zkm.netcell-it.de",
|
||
},
|
||
},
|
||
}
|
||
out := renderView(t, v)
|
||
want := `http-request redirect location https://zkm.netcell-it.de code 301 if { hdr(host) -i kvs.netcell-it.de }`
|
||
if !strings.Contains(out, want) {
|
||
t.Errorf("missing domain→domain 301 redirect line:\n%s", out)
|
||
}
|
||
}
|
||
|
||
func TestBuildRedirectTo(t *testing.T) {
|
||
cases := []struct{ in, want string }{
|
||
{"https://zkm.netcell-it.de", "https://zkm.netcell-it.de"},
|
||
{" https://zkm.netcell-it.de ", "https://zkm.netcell-it.de"}, // getrimmt
|
||
{"http://x.de", "http://x.de"},
|
||
{"", ""},
|
||
{"zkm.netcell-it.de", ""}, // kein Schema
|
||
{"ftp://x.de", ""}, // falsches Schema
|
||
{"https://x .de", ""}, // Whitespace → unsafe
|
||
{"https://x\"de", ""}, // Quote → unsafe
|
||
{"javascript:alert(1)", ""}, // kein http(s)
|
||
}
|
||
for _, c := range cases {
|
||
got := buildRedirectTo(models.Domain{RedirectTo: c.in})
|
||
if got != c.want {
|
||
t.Errorf("buildRedirectTo(%q) = %q, want %q", c.in, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestBuildHSTSHeader(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
d models.Domain
|
||
want string
|
||
}{
|
||
{"disabled", models.Domain{HSTSEnabled: false}, ""},
|
||
{"defaults", models.Domain{HSTSEnabled: true}, "max-age=31536000"},
|
||
{"explicit", models.Domain{HSTSEnabled: true, HSTSMaxAge: 7200}, "max-age=7200"},
|
||
{"sub", models.Domain{HSTSEnabled: true, HSTSMaxAge: 60, HSTSSubdomains: true}, "max-age=60; includeSubDomains"},
|
||
{"sub+preload", models.Domain{HSTSEnabled: true, HSTSMaxAge: 60, HSTSSubdomains: true, HSTSPreload: true}, "max-age=60; includeSubDomains; preload"},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
if got := buildHSTSHeader(tc.d); got != tc.want {
|
||
t.Errorf("buildHSTSHeader: got %q want %q", got, tc.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestBuildMaintMessage(t *testing.T) {
|
||
msg := `He said "hi"` + "\n" + `and left`
|
||
d := models.Domain{MaintenanceMode: true, MaintenanceMessage: &msg}
|
||
got := buildMaintMessage(d)
|
||
// Quotes durch ' ersetzt, Newline → Space.
|
||
want := `He said 'hi' and left`
|
||
if got != want {
|
||
t.Errorf("buildMaintMessage: got %q want %q", got, want)
|
||
}
|
||
if buildMaintMessage(models.Domain{MaintenanceMode: false}) != "" {
|
||
t.Errorf("buildMaintMessage should be empty when MaintenanceMode is off")
|
||
}
|
||
if got := buildMaintMessage(models.Domain{MaintenanceMode: true}); got == "" {
|
||
t.Errorf("buildMaintMessage should emit fallback text when no message set")
|
||
}
|
||
}
|
||
|
||
func TestRender_GlobalMaintenanceBlocksAllCustomerTraffic(t *testing.T) {
|
||
v := View{
|
||
GlobalMaintenance: true,
|
||
GlobalMaintenanceMessage: "Wartung läuft.",
|
||
Domains: []DomainView{
|
||
{Domain: models.Domain{ID: 1, Name: "site.example.com", Active: true}},
|
||
},
|
||
}
|
||
out := renderView(t, v)
|
||
want := `http-request return status 503 content-type "text/plain; charset=utf-8" string "Wartung läuft."`
|
||
if !strings.Contains(out, want) {
|
||
t.Errorf("missing global-maintenance 503 block:\n%s", out)
|
||
}
|
||
// Block muss VOR den use_backend-Zeilen IM SELBEN public_https-
|
||
// Frontend stehen (sonst ineffektiv — HAProxy execut'ed in-order,
|
||
// return-actions terminieren die Chain). public_http hat ein
|
||
// frühes use_backend api_backend für ACME — das hier nicht
|
||
// matchen.
|
||
pubIdx := strings.Index(out, "frontend public_https")
|
||
mgmtIdxStart := strings.Index(out, "frontend mgmt_https")
|
||
if pubIdx < 0 || mgmtIdxStart < 0 {
|
||
t.Fatalf("frontends not found in output:\n%s", out)
|
||
}
|
||
publicBlock := out[pubIdx:mgmtIdxStart]
|
||
idxBlock := strings.Index(publicBlock, want)
|
||
idxUseBackend := strings.Index(publicBlock, "use_backend")
|
||
if idxBlock < 0 || (idxUseBackend > 0 && idxBlock > idxUseBackend) {
|
||
t.Errorf("global-maintenance block must precede use_backend in public_https\n block at %d, use_backend at %d", idxBlock, idxUseBackend)
|
||
}
|
||
// mgmt_https darf NICHT betroffen sein.
|
||
mgmtIdx := strings.Index(out, "frontend mgmt_https")
|
||
if mgmtIdx > 0 {
|
||
mgmtBlock := out[mgmtIdx:]
|
||
if strings.Contains(mgmtBlock, want) {
|
||
t.Errorf("mgmt_https must NOT contain global-maintenance block:\n%s", mgmtBlock)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestRender_GlobalMaintenanceOff_NoBlock(t *testing.T) {
|
||
v := View{
|
||
Domains: []DomainView{
|
||
{Domain: models.Domain{ID: 1, Name: "site.example.com", Active: true}},
|
||
},
|
||
}
|
||
out := renderView(t, v)
|
||
if strings.Contains(out, "Whole-Box-Maintenance") {
|
||
t.Errorf("global-maintenance comment should not be rendered when off:\n%s", out)
|
||
}
|
||
}
|
||
|
||
func TestRender_RateLimitEmitsStickTableAndDeny(t *testing.T) {
|
||
v := View{
|
||
Domains: []DomainView{
|
||
{
|
||
Domain: models.Domain{
|
||
ID: 7, Name: "api.example.com", Active: true,
|
||
RateLimitRPS: 50,
|
||
},
|
||
RateLimitThreshold: 500, // 50 rps × 10s
|
||
},
|
||
},
|
||
}
|
||
out := renderView(t, v)
|
||
for _, w := range []string{
|
||
"backend rl_7",
|
||
"stick-table type ip size 100k expire 10s store http_req_rate(10s)",
|
||
`http-request track-sc0 src table rl_7 if { hdr(host) -i api.example.com }`,
|
||
`http-request deny deny_status 429 if { hdr(host) -i api.example.com } { sc_http_req_rate(0) gt 500 }`,
|
||
} {
|
||
if !strings.Contains(out, w) {
|
||
t.Errorf("missing %q in rate-limit output:\n%s", w, out)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestRender_NoRateLimitNoStickTable(t *testing.T) {
|
||
v := View{
|
||
Domains: []DomainView{
|
||
{
|
||
Domain: models.Domain{ID: 9, Name: "a.example.com", Active: true},
|
||
},
|
||
},
|
||
}
|
||
out := renderView(t, v)
|
||
if strings.Contains(out, "backend rl_9") {
|
||
t.Errorf("stick-table backend rendered for domain without rate-limit:\n%s", out)
|
||
}
|
||
if strings.Contains(out, "track-sc0") {
|
||
t.Errorf("track-sc0 emitted without rate-limit:\n%s", out)
|
||
}
|
||
}
|
||
|
||
func TestRender_BodySizeDeny413(t *testing.T) {
|
||
v := View{
|
||
Domains: []DomainView{
|
||
{
|
||
Domain: models.Domain{
|
||
ID: 1, Name: "upload.example.com", Active: true,
|
||
MaxBodyKB: 2048,
|
||
},
|
||
MaxBodyBytes: 2048 * 1024,
|
||
},
|
||
},
|
||
}
|
||
out := renderView(t, v)
|
||
want := `http-request deny deny_status 413 if { hdr(host) -i upload.example.com } { req.hdr_val(content-length) -m int gt 2097152 }`
|
||
if !strings.Contains(out, want) {
|
||
t.Errorf("missing 413 body-size deny:\n%s", out)
|
||
}
|
||
}
|
||
|
||
func TestRender_CustomResponseHeaders(t *testing.T) {
|
||
v := View{
|
||
Domains: []DomainView{
|
||
{
|
||
Domain: models.Domain{ID: 1, Name: "x.example.com", Active: true},
|
||
ResponseHeaders: []ResponseHeaderView{
|
||
{Name: "X-Frame-Options", Value: "DENY"},
|
||
{Name: "Content-Security-Policy", Value: "default-src 'self'"},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
out := renderView(t, v)
|
||
for _, w := range []string{
|
||
// del + set pro Custom-Header, damit Upstream-Werte
|
||
// garantiert überschrieben werden.
|
||
`http-response del-header X-Frame-Options if { hdr(host) -i x.example.com }`,
|
||
`http-response set-header X-Frame-Options "DENY" if { hdr(host) -i x.example.com }`,
|
||
`http-response del-header Content-Security-Policy if { hdr(host) -i x.example.com }`,
|
||
`http-response set-header Content-Security-Policy "default-src 'self'" if { hdr(host) -i x.example.com }`,
|
||
} {
|
||
if !strings.Contains(out, w) {
|
||
t.Errorf("missing %q:\n%s", w, out)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestSanitizeHeaderValue(t *testing.T) {
|
||
cases := map[string]string{
|
||
`plain`: `plain`,
|
||
`with "quotes"`: `with 'quotes'`,
|
||
"with\nnewline": "with newline",
|
||
"crlf\r\nattack": "crlf attack",
|
||
`csp default-src 'self'`: `csp default-src 'self'`,
|
||
}
|
||
for in, want := range cases {
|
||
if got := sanitizeHeaderValue(in); got != want {
|
||
t.Errorf("sanitizeHeaderValue(%q) = %q want %q", in, got, want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestBuildRedirectFromHost(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
d models.Domain
|
||
want string
|
||
}{
|
||
{"none", models.Domain{Name: "example.com"}, ""},
|
||
{"to-naked", models.Domain{Name: "example.com", WWWRedirect: "to-naked"}, "www.example.com"},
|
||
{"to-naked invalid (name already has www)", models.Domain{Name: "www.example.com", WWWRedirect: "to-naked"}, ""},
|
||
{"to-www", models.Domain{Name: "www.example.com", WWWRedirect: "to-www"}, "example.com"},
|
||
{"to-www invalid (name lacks www)", models.Domain{Name: "example.com", WWWRedirect: "to-www"}, ""},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
if got := buildRedirectFromHost(tc.d); got != tc.want {
|
||
t.Errorf("buildRedirectFromHost: got %q want %q", got, tc.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestRender_DomainRoutesEmitUseBackend(t *testing.T) {
|
||
v := View{
|
||
Backends: []BackendView{
|
||
{Backend: mkBackend(1, "app", nil), Servers: []models.BackendServer{
|
||
mkServer(1, "app", "10.0.0.10", 8080),
|
||
}},
|
||
{Backend: mkBackend(2, "api", nil), Servers: []models.BackendServer{
|
||
mkServer(2, "api", "10.0.0.20", 9000),
|
||
}},
|
||
},
|
||
Domains: []DomainView{{
|
||
Domain: models.Domain{ID: 1, Name: "example.com", Active: true},
|
||
Routes: []RouteView{
|
||
{PathPrefix: "/", BackendID: 1},
|
||
{PathPrefix: "/api", BackendID: 2},
|
||
},
|
||
}},
|
||
}
|
||
out := renderView(t, v)
|
||
for _, w := range []string{
|
||
"backend eg_backend_1",
|
||
"server app 10.0.0.10:8080",
|
||
"backend eg_backend_2",
|
||
"server api 10.0.0.20:9000",
|
||
"balance roundrobin",
|
||
"use_backend eg_backend_1 if { hdr(host) -i example.com } { path_beg / }",
|
||
"use_backend eg_backend_2 if { hdr(host) -i example.com } { path_beg /api }",
|
||
} {
|
||
if !strings.Contains(out, w) {
|
||
t.Errorf("missing %q in output:\n%s", w, out)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestRender_HealthCheckPathAddsCheckInter(t *testing.T) {
|
||
hcp := "/health"
|
||
v := View{
|
||
Backends: []BackendView{
|
||
{Backend: mkBackend(1, "app", &hcp), Servers: []models.BackendServer{
|
||
mkServer(1, "app", "10.0.0.10", 8080),
|
||
}},
|
||
},
|
||
}
|
||
out := renderView(t, v)
|
||
if !strings.Contains(out, "server app 10.0.0.10:8080 check inter 5s") {
|
||
t.Errorf("expected `check inter 5s` for backend with health_check_path:\n%s", out)
|
||
}
|
||
if !strings.Contains(out, "option httpchk") {
|
||
t.Errorf("expected `option httpchk` when health_check_path set:\n%s", out)
|
||
}
|
||
}
|
||
|
||
func TestRender_HTTPSHealthcheckPinsAlpnHTTP1(t *testing.T) {
|
||
// L7TOUT-Bug: ohne `check-alpn http/1.1` handelt der Check h2
|
||
// aus (vom server-Stmt geerbt) und hängt, weil option httpchk
|
||
// HTTP/1.x sendet. Test stellt sicher dass HTTPS+Healthcheck
|
||
// das ALPN für den Check pinnt.
|
||
hcp := "/"
|
||
v := View{
|
||
Backends: []BackendView{
|
||
{
|
||
Backend: models.Backend{ID: 9, Name: "tls-app", Scheme: "https",
|
||
LBAlgorithm: "roundrobin", HealthCheckPath: &hcp, Active: true},
|
||
Servers: []models.BackendServer{
|
||
{BackendID: 9, Name: "tls-1", Address: "10.0.0.30", Port: 8443, Weight: 100, Active: true},
|
||
},
|
||
},
|
||
{
|
||
// Gegenprobe: HTTP-Backend mit Healthcheck darf KEIN
|
||
// check-alpn bekommen (ALPN gibt's nur bei SSL).
|
||
Backend: models.Backend{ID: 10, Name: "plain-app", Scheme: "http",
|
||
LBAlgorithm: "roundrobin", HealthCheckPath: &hcp, Active: true},
|
||
Servers: []models.BackendServer{
|
||
{BackendID: 10, Name: "plain-1", Address: "10.0.0.31", Port: 80, Weight: 100, Active: true},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
out := renderView(t, v)
|
||
idxTLS := strings.Index(out, "backend eg_backend_9")
|
||
idxPlain := strings.Index(out, "backend eg_backend_10")
|
||
if idxTLS < 0 || idxPlain < 0 {
|
||
t.Fatalf("backend sections missing:\n%s", out)
|
||
}
|
||
tlsBlock := out[idxTLS:idxPlain]
|
||
plainBlock := out[idxPlain:]
|
||
if !strings.Contains(tlsBlock, "check-alpn http/1.1") {
|
||
t.Errorf("HTTPS+healthcheck soll check-alpn http/1.1 pinnen:\n%s", tlsBlock)
|
||
}
|
||
if strings.Contains(plainBlock, "check-alpn") {
|
||
t.Errorf("HTTP-Backend darf KEIN check-alpn bekommen:\n%s", plainBlock)
|
||
}
|
||
}
|
||
|
||
func TestRender_WebSocketEmitsTunnelTimeout(t *testing.T) {
|
||
v := View{
|
||
Backends: []BackendView{
|
||
{
|
||
Backend: models.Backend{ID: 7, Name: "vmm", Scheme: "https",
|
||
LBAlgorithm: "source", WebSocket: true, Active: true},
|
||
Servers: []models.BackendServer{
|
||
{BackendID: 7, Name: "vmm-1", Address: "10.0.5.14", Port: 8006, Weight: 100, Active: true},
|
||
},
|
||
},
|
||
{
|
||
Backend: models.Backend{ID: 8, Name: "api", Scheme: "http",
|
||
LBAlgorithm: "roundrobin", WebSocket: false, Active: true},
|
||
Servers: []models.BackendServer{
|
||
{BackendID: 8, Name: "api-1", Address: "10.0.5.20", Port: 8080, Weight: 100, Active: true},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
out := renderView(t, v)
|
||
// vmm soll tunnel-Timeout haben, api nicht.
|
||
idxVmm := strings.Index(out, "backend eg_backend_7")
|
||
idxApi := strings.Index(out, "backend eg_backend_8")
|
||
if idxVmm < 0 || idxApi < 0 {
|
||
t.Fatalf("backend sections missing in output:\n%s", out)
|
||
}
|
||
vmmBlock := out[idxVmm:idxApi]
|
||
apiBlock := out[idxApi:]
|
||
if !strings.Contains(vmmBlock, "timeout tunnel 1h") {
|
||
t.Errorf("vmm-Block sollte `timeout tunnel 1h` enthalten:\n%s", vmmBlock)
|
||
}
|
||
if strings.Contains(apiBlock, "timeout tunnel") {
|
||
t.Errorf("api-Block soll KEIN `timeout tunnel` enthalten:\n%s", apiBlock)
|
||
}
|
||
}
|
||
|
||
func TestRender_MultiServerPool(t *testing.T) {
|
||
v := View{
|
||
Backends: []BackendView{
|
||
{
|
||
Backend: models.Backend{ID: 1, Name: "vmm", Scheme: "http", LBAlgorithm: "leastconn", Active: true},
|
||
Servers: []models.BackendServer{
|
||
{BackendID: 1, Name: "vmm-1", Address: "10.0.0.11", Port: 8080, Weight: 100, Active: true},
|
||
{BackendID: 1, Name: "vmm-2", Address: "10.0.0.12", Port: 8080, Weight: 100, Active: true},
|
||
{BackendID: 1, Name: "vmm-3", Address: "10.0.0.13", Port: 8080, Weight: 50, Backup: true, Active: true},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
out := renderView(t, v)
|
||
for _, w := range []string{
|
||
"backend eg_backend_1",
|
||
"balance leastconn",
|
||
"server vmm-1 10.0.0.11:8080",
|
||
"server vmm-2 10.0.0.12:8080",
|
||
"server vmm-3 10.0.0.13:8080",
|
||
"weight 50",
|
||
" backup",
|
||
} {
|
||
if !strings.Contains(out, w) {
|
||
t.Errorf("missing %q in multi-server output:\n%s", w, out)
|
||
}
|
||
}
|
||
}
|