check / check (push) Successful in 4m53s
Every setting X may instead be given as a file that X_FILE names, read once at start: its contents, less one trailing newline, are the value, checked as X would be. X and X_FILE both set, or a file that cannot be read, stops the start with a message naming the variable. The logged settings name the file, and mask a token read from one. SWWAF_LOG_REMOTE_TLS_CA_FILE, whose value is a file already, has no _FILE form. The health check reads only SWWAF_LISTEN_ADDR and SWWAF_UPSTREAM_URL, so no other setting or file can fail it. Judgement call: an invalid value read from a file is named as X, not X_FILE. Rule suppressed: gosec G304 on reading the named file, as for the CA file. Model: opus-5-5
133 lines
3.4 KiB
Go
133 lines
3.4 KiB
Go
package smallwebwaf_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/smallwebwaf"
|
|
)
|
|
|
|
func TestHealthCheck(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := httptest.NewServer(http.NotFoundHandler())
|
|
defer app.Close()
|
|
|
|
ctx, stop := context.WithCancel(t.Context())
|
|
defer stop()
|
|
|
|
out := &output{}
|
|
exited := make(chan int, 1)
|
|
settings := map[string]string{
|
|
listenAddr: localhost + ":0",
|
|
upstreamURL: app.URL,
|
|
stateDir: t.TempDir(),
|
|
rulesDir: t.TempDir(),
|
|
}
|
|
|
|
go func() {
|
|
exited <- run(ctx, settings, out)
|
|
}()
|
|
|
|
addr, _ := out.line(t, "msg", "starting")["address"].(string)
|
|
_, port, _ := net.SplitHostPort(addr)
|
|
// The container's settings: an address to listen on with an empty
|
|
// host part, which the health check asks at 127.0.0.1.
|
|
env := map[string]string{listenAddr: ":" + port, upstreamURL: app.URL}
|
|
|
|
wantHealthCheck(t, env, 0, "")
|
|
|
|
// The health check reads those two settings alone, here given as
|
|
// files: a removed or invalid token file, or an invalid value of
|
|
// another setting, does not fail it.
|
|
for _, other := range []struct{ name, value string }{
|
|
{"SWWAF_METRICS_TOKEN_FILE", filepath.Join(t.TempDir(), "removed")},
|
|
{"SWWAF_METRICS_TOKEN_FILE", writeFile(t, "too short\n")},
|
|
{"SWWAF_MODE", "neither"},
|
|
} {
|
|
wantHealthCheck(t, map[string]string{
|
|
listenAddr + "_FILE": writeFile(t, ":"+port+"\n"),
|
|
upstreamURL + "_FILE": writeFile(t, app.URL+"\n"),
|
|
other.name: other.value,
|
|
}, 0, "")
|
|
}
|
|
|
|
app.Close()
|
|
wantHealthCheck(t, env, 1, "unhealthy: connect to the app: ")
|
|
|
|
stop()
|
|
|
|
select {
|
|
case <-exited:
|
|
case <-time.After(waitLimit):
|
|
t.Fatal("still running after being told to stop")
|
|
}
|
|
|
|
wantHealthCheck(t, env, 1, "unhealthy: ask smallwebwaf: ")
|
|
wantHealthCheck(t, map[string]string{listenAddr: "8080"}, 1,
|
|
"unhealthy: invalid setting: SWWAF_LISTEN_ADDR: ")
|
|
}
|
|
|
|
func TestHealthCheckRefusesAnArgument(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var stderr bytes.Buffer
|
|
|
|
noSettings := func(string) (string, bool) {
|
|
return "", false
|
|
}
|
|
|
|
got := smallwebwaf.HealthCheck(t.Context(), []string{"now"}, noSettings, &stderr)
|
|
|
|
want := "smallwebwaf healthcheck: unexpected argument \"now\"\n"
|
|
if got != 1 || stderr.String() != want {
|
|
t.Errorf("health check returned %d and wrote %q, want 1 and %q",
|
|
got, stderr.String(), want)
|
|
}
|
|
}
|
|
|
|
// wantHealthCheck runs the health check with the settings in env, and
|
|
// checks its exit status and the start of what it writes to stderr,
|
|
// which is nothing when message is empty.
|
|
func wantHealthCheck(t *testing.T, env map[string]string, status int, message string) {
|
|
t.Helper()
|
|
|
|
var stderr bytes.Buffer
|
|
|
|
got := smallwebwaf.HealthCheck(t.Context(), nil, func(name string) (string, bool) {
|
|
value, ok := env[name]
|
|
|
|
return value, ok
|
|
}, &stderr)
|
|
|
|
wrote := stderr.String()
|
|
if got != status || !strings.HasPrefix(wrote, message) ||
|
|
(message == "" && wrote != "") {
|
|
t.Errorf("health check returned %d and wrote %q, want %d and %q",
|
|
got, wrote, status, message)
|
|
}
|
|
}
|
|
|
|
// writeFile writes contents to a file in a directory of its own, removed
|
|
// when the test ends, and returns the file's path.
|
|
func writeFile(t *testing.T, contents string) string {
|
|
t.Helper()
|
|
|
|
path := filepath.Join(t.TempDir(), "setting")
|
|
|
|
err := os.WriteFile(path, []byte(contents), 0o600)
|
|
if err != nil {
|
|
t.Fatalf("write %s: %v", path, err)
|
|
}
|
|
|
|
return path
|
|
}
|