check / check (push) Successful in 2m47s
With METRICS_USERNAME and METRICS_PASSWORD both set, the backend records request metrics through go-http-metrics in a registry of its own, with Go's runtime and process metrics, and serves them at GET /metrics behind basic auth with those credentials; nginx passes /metrics to it. With neither set there is no such route; one alone stops the start with an error naming both, and a METRICS_USERNAME containing ":" stops it with an error naming that. Only requests that reach the health check or POST /api/v1/reports are recorded, not every request as the conventions show: the labels are path and method, which clients can make up without end. So POST /api/v1/reports is registered by its full path, not inside a route group. Deviation: go get and go mod tidy ran directly; no entrypoint adds a Go dependency yet (#45). Model: opus-5-5
148 lines
4.1 KiB
Go
148 lines
4.1 KiB
Go
package config_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/netwatch/internal/config"
|
|
"sneak.berlin/go/netwatch/internal/globals"
|
|
"sneak.berlin/go/netwatch/internal/logger"
|
|
|
|
"go.uber.org/fx"
|
|
)
|
|
|
|
// requireConfigError builds the config as main does and fails the
|
|
// test unless that fails with an error naming setting. It uses
|
|
// fx.New, because fxtest.New fails the test itself on an error.
|
|
func requireConfigError(t *testing.T, setting string) {
|
|
t.Helper()
|
|
|
|
app := fx.New(
|
|
fx.NopLogger,
|
|
fx.Provide(globals.New, logger.New, config.New),
|
|
fx.Invoke(func(*config.Config) {}),
|
|
)
|
|
|
|
err := app.Err()
|
|
if err == nil || !strings.Contains(err.Error(), setting) {
|
|
t.Fatalf("config error = %v, want one naming %s", err, setting)
|
|
}
|
|
}
|
|
|
|
// TestSettingsLoadAsGiven: valid values pass the checks and are used
|
|
// as given. bin/entrypoint.sh starts the server with these
|
|
// BIND_ADDRESS and PORT values.
|
|
func TestSettingsLoadAsGiven(t *testing.T) {
|
|
t.Setenv("BIND_ADDRESS", "127.0.0.1")
|
|
t.Setenv("PORT", "8081")
|
|
t.Setenv("DEBUG", "true")
|
|
|
|
var cfg *config.Config
|
|
|
|
app := fx.New(
|
|
fx.NopLogger,
|
|
fx.Provide(globals.New, logger.New, config.New),
|
|
fx.Populate(&cfg),
|
|
)
|
|
|
|
err := app.Err()
|
|
if err != nil {
|
|
t.Fatalf("config error = %v", err)
|
|
}
|
|
|
|
if cfg.BindAddress != "127.0.0.1" || cfg.Port != 8081 || !cfg.Debug {
|
|
t.Fatalf("BindAddress, Port, Debug = %q, %d, %t; "+
|
|
"want \"127.0.0.1\", 8081, true",
|
|
cfg.BindAddress, cfg.Port, cfg.Debug)
|
|
}
|
|
}
|
|
|
|
// TestPortMustBeAPortNumber: viper reads a value that is not a number
|
|
// as 0, on which the server would listen on a random port.
|
|
func TestPortMustBeAPortNumber(t *testing.T) {
|
|
for _, value := range []string{"abc", "0", "65536", "8080.5"} {
|
|
t.Run(value, func(t *testing.T) {
|
|
t.Setenv("PORT", value)
|
|
|
|
requireConfigError(t, "PORT")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDebugMustBeTrueOrFalse: viper reads any other value, such as
|
|
// "yes", as false.
|
|
func TestDebugMustBeTrueOrFalse(t *testing.T) {
|
|
t.Setenv("DEBUG", "yes")
|
|
|
|
requireConfigError(t, "DEBUG")
|
|
}
|
|
|
|
// TestBindAddressMustBeAnIPAddress: a host name would be looked up
|
|
// only once the server starts listening, and a mistyped one would stop
|
|
// it then with an error that does not name the setting.
|
|
func TestBindAddressMustBeAnIPAddress(t *testing.T) {
|
|
t.Setenv("BIND_ADDRESS", "localhost")
|
|
|
|
requireConfigError(t, "BIND_ADDRESS")
|
|
}
|
|
|
|
// TestReportsPerMinuteMustBePositive: unchecked, zero would panic
|
|
// when the routes are built, and a negative rate would lift the
|
|
// limit.
|
|
func TestReportsPerMinuteMustBePositive(t *testing.T) {
|
|
t.Setenv("REPORTS_PER_MINUTE", "0")
|
|
|
|
requireConfigError(t, "REPORTS_PER_MINUTE")
|
|
}
|
|
|
|
// TestDataDirMaxBytesMustBeANumber: viper reads a value that is not
|
|
// a number, such as "1GB", as 0, which would refuse every report.
|
|
func TestDataDirMaxBytesMustBeANumber(t *testing.T) {
|
|
t.Setenv("DATA_DIR_MAX_BYTES", "1GB")
|
|
|
|
requireConfigError(t, "DATA_DIR_MAX_BYTES")
|
|
}
|
|
|
|
// TestMetricsCredentialsGoTogether: with only one of the two set, the
|
|
// server would quietly serve no metrics, so the start fails, naming
|
|
// both.
|
|
func TestMetricsCredentialsGoTogether(t *testing.T) {
|
|
for _, set := range []string{"METRICS_USERNAME", "METRICS_PASSWORD"} {
|
|
t.Run(set, func(t *testing.T) {
|
|
t.Setenv("METRICS_USERNAME", "")
|
|
t.Setenv("METRICS_PASSWORD", "")
|
|
t.Setenv(set, "prometheus")
|
|
|
|
requireConfigError(t, "METRICS_USERNAME")
|
|
requireConfigError(t, "METRICS_PASSWORD")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMetricsUsernameMustNotContainColon: basic auth splits the
|
|
// credentials at the first ":", so such a user name would get 401 on
|
|
// every request to /metrics.
|
|
func TestMetricsUsernameMustNotContainColon(t *testing.T) {
|
|
t.Setenv("METRICS_USERNAME", "prom:etheus")
|
|
t.Setenv("METRICS_PASSWORD", "secret")
|
|
|
|
requireConfigError(t, "METRICS_USERNAME")
|
|
}
|
|
|
|
// TestCORSAllowedOriginsMustBeOrigins: "*" would let every origin in,
|
|
// and an entry that is not a plain origin would match no page.
|
|
func TestCORSAllowedOriginsMustBeOrigins(t *testing.T) {
|
|
for _, entry := range []string{
|
|
"*",
|
|
"https://*.netwatch.example",
|
|
"netwatch.example",
|
|
"https://netwatch.example/",
|
|
} {
|
|
t.Run(entry, func(t *testing.T) {
|
|
t.Setenv("CORS_ALLOWED_ORIGINS", entry)
|
|
|
|
requireConfigError(t, "CORS_ALLOWED_ORIGINS")
|
|
})
|
|
}
|
|
}
|