158 lines
5.2 KiB
Go
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, ", "))
|
|
}
|
|
}
|