976f5238f8
- internal .env loader (real env wins) + strictly validated config struct - ULID package: 48-bit ms timestamp + 80-bit crypto randomness, sortable - HTTP server with recovery/request-id/trusted-proxy/access-log middleware - /healthz, /readyz (pluggable checkers), /metricsz (counter registry) - multi-stage Dockerfile (alpine, non-root) + compose with healthchecks, app bound to 127.0.0.1:8787, mongo unpublished, graceful 15s shutdown Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
138 lines
3.7 KiB
Go
138 lines
3.7 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// validEnv sets the minimum required variables for a passing Load.
|
|
func validEnv(t *testing.T) {
|
|
t.Helper()
|
|
t.Setenv("SESSION_SECRET", "0123456789abcdef0123456789abcdef")
|
|
t.Setenv("CREDENTIALS_ENC_KEY", base64.StdEncoding.EncodeToString(make([]byte, 32)))
|
|
}
|
|
|
|
func TestLoadDefaults(t *testing.T) {
|
|
validEnv(t)
|
|
c, err := Load()
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if c.AppPort != 8787 {
|
|
t.Errorf("AppPort = %d, want 8787", c.AppPort)
|
|
}
|
|
if c.MongoDB != "bountyboard" {
|
|
t.Errorf("MongoDB = %q", c.MongoDB)
|
|
}
|
|
if c.AtomizerTimeout.Seconds() != 120 {
|
|
t.Errorf("AtomizerTimeout = %v", c.AtomizerTimeout)
|
|
}
|
|
if c.CookieSecure() {
|
|
t.Error("CookieSecure should be false for http base URL in auto mode")
|
|
}
|
|
if len(c.CredentialsEncKey) != 32 {
|
|
t.Errorf("CredentialsEncKey len = %d", len(c.CredentialsEncKey))
|
|
}
|
|
}
|
|
|
|
func TestLoadValidationErrors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key string
|
|
value string
|
|
wantSub string
|
|
}{
|
|
{"bad port", "APP_PORT", "99999", "APP_PORT"},
|
|
{"non-numeric port", "APP_PORT", "abc", "APP_PORT"},
|
|
{"short session secret", "SESSION_SECRET", "short", "SESSION_SECRET"},
|
|
{"bad enc key", "CREDENTIALS_ENC_KEY", "not-base64!!", "CREDENTIALS_ENC_KEY"},
|
|
{"wrong size enc key", "CREDENTIALS_ENC_KEY", base64.StdEncoding.EncodeToString(make([]byte, 16)), "CREDENTIALS_ENC_KEY"},
|
|
{"bad base url", "APP_BASE_URL", "not a url", "APP_BASE_URL"},
|
|
{"bad atomizer url", "ATOMIZER_BASE_URL", "::::", "ATOMIZER_BASE_URL"},
|
|
{"bad cidr", "TRUSTED_PROXY_CIDRS", "10.0.0.0/8,banana", "TRUSTED_PROXY_CIDRS"},
|
|
{"bad cookie mode", "COOKIE_SECURE", "maybe", "COOKIE_SECURE"},
|
|
{"bad upload size", "MAX_UPLOAD_MB", "0", "MAX_UPLOAD_MB"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
validEnv(t)
|
|
t.Setenv(tt.key, tt.value)
|
|
_, err := Load()
|
|
if err == nil {
|
|
t.Fatal("want error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), tt.wantSub) {
|
|
t.Errorf("error %q does not mention %q", err, tt.wantSub)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadMissingEncKey(t *testing.T) {
|
|
t.Setenv("SESSION_SECRET", "0123456789abcdef0123456789abcdef")
|
|
t.Setenv("CREDENTIALS_ENC_KEY", "")
|
|
if _, err := Load(); err == nil {
|
|
t.Fatal("want error for missing CREDENTIALS_ENC_KEY")
|
|
}
|
|
}
|
|
|
|
func TestTrustedProxyCIDRs(t *testing.T) {
|
|
validEnv(t)
|
|
t.Setenv("TRUSTED_PROXY_CIDRS", "172.16.0.0/12, 10.0.0.0/8")
|
|
c, err := Load()
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if len(c.TrustedProxyCIDRs) != 2 {
|
|
t.Fatalf("got %d prefixes, want 2", len(c.TrustedProxyCIDRs))
|
|
}
|
|
}
|
|
|
|
func TestCookieSecureModes(t *testing.T) {
|
|
tests := []struct {
|
|
mode, baseURL string
|
|
want bool
|
|
}{
|
|
{"auto", "http://localhost:8787", false},
|
|
{"auto", "https://bounty.example.com", true},
|
|
{"true", "http://localhost:8787", true},
|
|
{"false", "https://bounty.example.com", false},
|
|
}
|
|
for _, tt := range tests {
|
|
validEnv(t)
|
|
t.Setenv("COOKIE_SECURE", tt.mode)
|
|
t.Setenv("APP_BASE_URL", tt.baseURL)
|
|
c, err := Load()
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if got := c.CookieSecure(); got != tt.want {
|
|
t.Errorf("mode=%s url=%s: CookieSecure()=%v, want %v", tt.mode, tt.baseURL, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOIDCAndSMTPEnabled(t *testing.T) {
|
|
validEnv(t)
|
|
c, err := Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.OIDC.Enabled() || c.SMTP.Enabled() {
|
|
t.Error("OIDC and SMTP must be disabled when unset")
|
|
}
|
|
|
|
t.Setenv("OIDC_ISSUER_URL", "https://issuer.example.com")
|
|
t.Setenv("OIDC_CLIENT_ID", "id")
|
|
t.Setenv("OIDC_CLIENT_SECRET", "secret")
|
|
t.Setenv("SMTP_HOST", "smtp.example.com")
|
|
c, err = Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !c.OIDC.Enabled() || !c.SMTP.Enabled() {
|
|
t.Error("OIDC and SMTP must be enabled when set")
|
|
}
|
|
}
|