Files
jbergner 6e152a5121
Some checks failed
release-tag / release-image (push) Failing after 1m38s
2.0.2 Update und Anpassungen
2026-07-24 10:08:19 +02:00

158 lines
5.2 KiB
Go

package app
import (
"go/ast"
"go/parser"
"go/token"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"testing"
)
func TestConfigValidationRequiresTrustedProxyCIDRs(t *testing.T) {
cfg := Config{TrustProxy: true}
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "TRUSTED_PROXY_CIDRS") {
t.Fatalf("expected trusted proxy validation error, got %v", err)
}
}
func TestConfigValidationRequiresMetricsToken(t *testing.T) {
cfg := Config{MetricsEnabled: true, MetricsToken: "short"}
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "METRICS_TOKEN") {
t.Fatalf("expected metrics token validation error, got %v", err)
}
}
func TestConfigValidationLegalStrict(t *testing.T) {
cfg := Config{LegalStrict: true}
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "LEGAL_NAME") {
t.Fatalf("expected legal validation error, got %v", err)
}
cfg.LegalName = "Example GmbH"
cfg.LegalAddress = "Example Street 1\n12345 Example"
cfg.LegalEmail = "legal@example.org"
cfg.DataProtectionContact = "privacy@example.org"
cfg.HostingProvider = "Example Hosting GmbH"
cfg.HostingAddress = "Hosting Street 1\n12345 Example"
if err := cfg.Validate(); err != nil {
t.Fatalf("valid strict legal configuration rejected: %v", err)
}
}
func TestMetricsDisabledAndProtected(t *testing.T) {
h := testHandlerConfig(t, Config{ListenAddress: ":0", BaseURL: "https://example.org", PublicName: "Test", DefaultLanguage: "de"})
r := httptest.NewRequest(http.MethodGet, "/metrics", nil)
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
if w.Code != http.StatusNotFound {
t.Fatalf("disabled metrics status %d", w.Code)
}
cfg := Config{ListenAddress: ":0", BaseURL: "https://example.org", PublicName: "Test", DefaultLanguage: "de", MetricsEnabled: true, MetricsToken: strings.Repeat("x", 40)}
h = testHandlerConfig(t, cfg)
r = httptest.NewRequest(http.MethodGet, "/metrics", nil)
w = httptest.NewRecorder()
h.ServeHTTP(w, r)
if w.Code != http.StatusUnauthorized {
t.Fatalf("metrics without token status %d", w.Code)
}
r = httptest.NewRequest(http.MethodGet, "/metrics", nil)
r.Header.Set("Authorization", "Bearer "+cfg.MetricsToken)
w = httptest.NewRecorder()
h.ServeHTTP(w, r)
if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "ai_disclosure_uptime_seconds") {
t.Fatalf("authorized metrics failed: %d %s", w.Code, w.Body.String())
}
}
func TestOperatorPagesAndHSTS(t *testing.T) {
cfg := Config{
ListenAddress: ":0", BaseURL: "https://example.org", PublicName: "Example", DefaultLanguage: "de", EnableHSTS: true,
LegalName: "Example GmbH", LegalAddress: "Example Street 1", LegalEmail: "legal@example.org",
DataProtectionContact: "privacy@example.org", HostingProvider: "Host GmbH", HostingAddress: "Host Street 1",
AccessibilityContact: "access@example.org", AccessibilityStatus: "Teilweise konform getestet",
}
h := testHandlerConfig(t, cfg)
for _, tc := range []struct{ path, want string }{{"/legal?lang=de", "Example GmbH"}, {"/privacy?lang=en", "Privacy information"}, {"/accessibility?lang=fr", "Accessibilité"}} {
r := httptest.NewRequest(http.MethodGet, tc.path, nil)
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), tc.want) {
t.Fatalf("%s: %d, missing %q", tc.path, w.Code, tc.want)
}
if got := w.Header().Get("Strict-Transport-Security"); got == "" {
t.Fatalf("%s missing HSTS", tc.path)
}
}
}
func TestEnvExampleDocumentsRuntimeConfiguration(t *testing.T) {
root := filepath.Join("..", "..")
data, err := os.ReadFile(filepath.Join(root, ".env.example"))
if err != nil {
t.Fatal(err)
}
text := string(data)
keys := map[string]struct{}{}
files := []string{filepath.Join("config.go"), filepath.Join(root, "cmd", "server", "main.go")}
helpers := map[string]bool{
"env": true, "envAllowEmpty": true, "boolEnv": true, "durationEnv": true, "intEnv": true,
"serviceModeEnv": true, "disputeStatusEnv": true, "cidrEnv": true, "secretEnv": true,
}
fset := token.NewFileSet()
for _, filename := range files {
file, err := parser.ParseFile(fset, filename, nil, 0)
if err != nil {
t.Fatalf("parse %s: %v", filename, err)
}
ast.Inspect(file, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok || len(call.Args) == 0 {
return true
}
name := ""
switch fn := call.Fun.(type) {
case *ast.Ident:
if helpers[fn.Name] {
name = fn.Name
}
case *ast.SelectorExpr:
if ident, ok := fn.X.(*ast.Ident); ok && ident.Name == "os" && (fn.Sel.Name == "Getenv" || fn.Sel.Name == "LookupEnv") {
name = fn.Sel.Name
}
}
if name == "" {
return true
}
lit, ok := call.Args[0].(*ast.BasicLit)
if !ok || lit.Kind != token.STRING {
return true
}
key, err := strconv.Unquote(lit.Value)
if err != nil || key == "" {
return true
}
keys[key] = struct{}{}
if name == "secretEnv" {
keys[key+"_FILE"] = struct{}{}
}
return true
})
}
missing := make([]string, 0)
for key := range keys {
if !strings.Contains(text, key+"=") {
missing = append(missing, key)
}
}
sort.Strings(missing)
if len(missing) > 0 {
t.Fatalf(".env.example is missing runtime variables: %s", strings.Join(missing, ", "))
}
}