132 lines
4.3 KiB
Go
132 lines
4.3 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"testing"
|
|
"textmachine/platform/internal/money"
|
|
"time"
|
|
)
|
|
|
|
func TestDefaultsAndOverrides(t *testing.T) {
|
|
c, err := Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Addr != "127.0.0.1:8080" {
|
|
t.Fatalf("default addr = %q; a control plane must not bind the world by default", c.Addr)
|
|
}
|
|
if c.Migrate {
|
|
t.Fatal("migrations must be opt-in")
|
|
}
|
|
|
|
t.Setenv("TM_PLATFORM_ADDR", ":9000")
|
|
t.Setenv("TM_PLATFORM_SESSION_IDLE", "30m")
|
|
t.Setenv("TM_PLATFORM_TRUSTED_ORIGINS", "https://app.example.org, https://desktop.example.org ")
|
|
c, err = Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Addr != ":9000" || c.SessionIdleTTL != 30*time.Minute {
|
|
t.Fatalf("config = %+v", c)
|
|
}
|
|
if len(c.TrustedOrigins) != 2 || c.TrustedOrigins[1] != "https://desktop.example.org" {
|
|
t.Fatalf("origins = %q", c.TrustedOrigins)
|
|
}
|
|
}
|
|
|
|
func TestImpossibleSessionWindowIsRefusedAtBoot(t *testing.T) {
|
|
t.Setenv("TM_PLATFORM_SESSION_IDLE", "100h")
|
|
t.Setenv("TM_PLATFORM_SESSION_MAX_AGE", "1h")
|
|
if _, err := Load(); err == nil {
|
|
t.Fatal("an idle window longer than the absolute one must fail at boot, not at 3am")
|
|
}
|
|
}
|
|
|
|
func TestMalformedDurationIsRefused(t *testing.T) {
|
|
t.Setenv("TM_PLATFORM_SESSION_IDLE", "fortnight")
|
|
if _, err := Load(); err == nil {
|
|
t.Fatal("want a parse error")
|
|
}
|
|
}
|
|
|
|
// A secret read from a file never enters the process environment, which is where a credential
|
|
// leaks from first (child processes inherit it, /proc exposes it). systemd hands one over this way.
|
|
func TestSecretsCanComeFromFiles(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "dsn")
|
|
if err := os.WriteFile(path, []byte("postgres://u:p@h/db\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("TM_PLATFORM_DSN_FILE", path)
|
|
c, err := Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.DSN != "postgres://u:p@h/db" {
|
|
t.Fatalf("DSN = %q (trailing newline must be trimmed)", c.DSN)
|
|
}
|
|
|
|
t.Setenv("TM_PLATFORM_DSN_FILE", filepath.Join(t.TempDir(), "absent"))
|
|
if _, err := Load(); err == nil {
|
|
t.Fatal("a named secret file that cannot be read must fail at boot, not at the first query")
|
|
}
|
|
}
|
|
|
|
// Half a login configuration mounts a surface that fails at the first click instead of at boot.
|
|
func TestPartialOIDCConfigurationIsRefused(t *testing.T) {
|
|
t.Setenv("TM_PLATFORM_OIDC_ISSUER", "https://accounts.google.com")
|
|
t.Setenv("TM_PLATFORM_OIDC_CLIENT_ID", "id")
|
|
if _, err := Load(); err == nil {
|
|
t.Fatal("an issuer without a secret and a redirect must be refused")
|
|
}
|
|
t.Setenv("TM_PLATFORM_OIDC_CLIENT_SECRET", "s")
|
|
t.Setenv("TM_PLATFORM_OIDC_REDIRECT_URL", "https://app.example.org/auth/callback")
|
|
c, err := Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !c.LoginEnabled() {
|
|
t.Fatal("a complete configuration must enable sign-in")
|
|
}
|
|
}
|
|
|
|
// The free tier is a number an operator sets, and a bad one must not become a silent zero.
|
|
func TestSignupGrantIsParsedNotGuessed(t *testing.T) {
|
|
if c, err := Load(); err != nil || c.SignupGrantMicroUSD != 5*money.PerUSD {
|
|
t.Fatalf("default grant = %d (%v)", c.SignupGrantMicroUSD, err)
|
|
}
|
|
t.Setenv("TM_PLATFORM_SIGNUP_GRANT_USD", "2.50")
|
|
c, err := Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.SignupGrantMicroUSD != 2_500_000 {
|
|
t.Fatalf("grant = %d", c.SignupGrantMicroUSD)
|
|
}
|
|
t.Setenv("TM_PLATFORM_SIGNUP_GRANT_USD", "five dollars")
|
|
if _, err := Load(); err == nil {
|
|
t.Fatal("an unparseable grant must fail at boot")
|
|
}
|
|
}
|
|
|
|
// PD-58. The session clocks are a declared conformance point, not a preference: ASVS 5.0 7.1.1
|
|
// takes the baseline from NIST SP 800-63B-4, whose AAL1 rule is that the overall reauthentication
|
|
// timeout SHOULD be no more than 30 days. The reasoning lives in STACK_DECISIONS §13; this keeps
|
|
// the defaults from drifting past it without someone changing that document too.
|
|
// Mutation caught: raising either default beyond the norm.
|
|
func TestSessionClocksStayWithinTheDeclaredBaseline(t *testing.T) {
|
|
c, err := Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const aal1Overall = 30 * 24 * time.Hour
|
|
if c.SessionMaxAge > aal1Overall {
|
|
t.Errorf("absolute session lifetime is %s, above the NIST SP 800-63B-4 AAL1 figure of %s: a deviation needs a written justification (ASVS 7.1.1)",
|
|
c.SessionMaxAge, aal1Overall)
|
|
}
|
|
if c.SessionIdleTTL <= 0 || c.SessionIdleTTL > c.SessionMaxAge {
|
|
t.Errorf("idle window is %s against an absolute of %s: an idle window that cannot expire first is not one",
|
|
c.SessionIdleTTL, c.SessionMaxAge)
|
|
}
|
|
}
|