feat: scaffold app skeleton with config, ULID, health endpoints, compose stack (phase 1)
- 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>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user