textmachine/platform/internal/config/effective_test.go

192 lines
7.2 KiB
Go

package config
import (
"bytes"
"encoding/json"
"log/slog"
"os"
"regexp"
"sort"
"strings"
"testing"
"time"
"textmachine/platform/internal/books"
)
// PD-114. An environment-only configuration leaves no artifact to read back, so the boot line IS the
// artifact — and the two things it must never carry are a secret and a sum of money.
func effective(t *testing.T) (map[string]Setting, string) {
t.Helper()
c, err := Load()
if err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
c.LogEffective(slog.New(slog.NewJSONHandler(&buf, nil)))
by := map[string]Setting{}
for _, s := range c.Settings {
if _, dup := by[s.Key]; dup {
t.Errorf("%s is printed twice", s.Key)
}
by[s.Key] = s
}
return by, buf.String()
}
// The completeness gate, and the reason it is written against the SOURCE rather than a list: a print
// that covers most of the configuration is worse than none, because the variable an operator is
// hunting is exactly the one nobody remembered to record. A setting added without a record fails
// here, in the same commit that adds it.
func TestEverySettingThisServiceReadsIsPrinted(t *testing.T) {
src, err := os.ReadFile("config.go")
if err != nil {
t.Fatal(err)
}
want := map[string]bool{}
for _, m := range regexp.MustCompile(`TM_PLATFORM_[A-Z0-9_]+`).FindAllString(string(src), -1) {
// A *_FILE variable is the alternative SOURCE of the key beside it, not a setting of its own:
// it is recorded as `source=file` on that key, and its path is never printed.
want[strings.TrimSuffix(m, "_FILE")] = true
}
got, _ := effective(t)
var missing []string
for k := range want {
if _, ok := got[k]; !ok {
missing = append(missing, k)
}
}
sort.Strings(missing)
if len(missing) != 0 {
t.Fatalf("read but never printed: %v", missing)
}
if len(got) != len(want) {
t.Fatalf("%d settings printed, %d read from the environment", len(got), len(want))
}
}
// A DSN carries a password and a client secret is a client secret. What the operator gets is the
// fact and the SOURCE, which is what answers "did my file get picked up".
func TestASecretIsNamedAndNeverPrinted(t *testing.T) {
path := t.TempDir() + "/dsn"
if err := os.WriteFile(path, []byte("postgres://u:hunter2@db/x\n"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("TM_PLATFORM_DSN_FILE", path)
// The whole OIDC quartet, because half a login configuration is refused at boot.
t.Setenv("TM_PLATFORM_OIDC_CLIENT_SECRET", "s3cret")
t.Setenv("TM_PLATFORM_OIDC_ISSUER", "https://accounts.example.org")
t.Setenv("TM_PLATFORM_OIDC_CLIENT_ID", "client")
t.Setenv("TM_PLATFORM_OIDC_REDIRECT_URL", "https://app.example.org/auth/callback")
got, line := effective(t)
if got["TM_PLATFORM_DSN"].Source != sourceFile || got["TM_PLATFORM_DSN"].Value != valueSet {
t.Errorf("DSN printed as %+v", got["TM_PLATFORM_DSN"])
}
if got["TM_PLATFORM_OIDC_CLIENT_SECRET"].Source != sourceEnv {
t.Errorf("client secret printed as %+v", got["TM_PLATFORM_OIDC_CLIENT_SECRET"])
}
for _, leaked := range []string{"hunter2", "s3cret", path} {
if strings.Contains(line, leaked) {
t.Errorf("the boot line carries %q", leaked)
}
}
// And the value really was read, not lost by being redacted.
c, err := Load()
if err != nil {
t.Fatal(err)
}
if !strings.Contains(c.DSN, "hunter2") || c.OIDCClientSecret != "s3cret" {
t.Fatal("the secret was redacted out of the configuration itself, not only out of the log")
}
}
// Money does not reach an INFO log in ANY form (D39.84, PD-99) — including the amounts an operator
// configured. The source is what they need, and the source is all they get.
func TestAConfiguredAmountIsNeverPrinted(t *testing.T) {
t.Setenv("TM_PLATFORM_SIGNUP_GRANT_USD", "7.50")
t.Setenv("TM_PLATFORM_USD_PER_CHAPTER", "0.0425")
got, line := effective(t)
for _, k := range []string{"TM_PLATFORM_SIGNUP_GRANT_USD", "TM_PLATFORM_USD_PER_CHAPTER"} {
if got[k].Source != sourceEnv {
t.Errorf("%s: source %q, want the environment it came from", k, got[k].Source)
}
if got[k].Value != valueAmount {
t.Errorf("%s: value %q, want it withheld", k, got[k].Value)
}
}
for _, amount := range []string{"7.50", "7500000", "0.0425", "42500"} {
if strings.Contains(line, amount) {
t.Errorf("the boot line carries the amount %q", amount)
}
}
c, err := Load()
if err != nil {
t.Fatal(err)
}
if c.SignupGrantMicroUSD != 7_500_000 || c.Runner.PerChapterMicroUSD != 42_500 {
t.Fatalf("the amounts did not apply: %d and %d", c.SignupGrantMicroUSD, c.Runner.PerChapterMicroUSD)
}
}
// A default and an environment variable holding the same string are the same line without the
// source, and telling them apart is the whole question ("did my setting take effect").
func TestTheSourceOfEveryValueIsPrintedBesideIt(t *testing.T) {
t.Setenv("TM_PLATFORM_ADDR", "127.0.0.1:8080") // exactly the default
got, line := effective(t)
if got["TM_PLATFORM_ADDR"].Source != sourceEnv {
t.Errorf("a value set to the default reads as %q", got["TM_PLATFORM_ADDR"].Source)
}
if got["TM_PLATFORM_AFTER_LOGIN"].Source != sourceDefault {
t.Errorf("an unset value reads as %q", got["TM_PLATFORM_AFTER_LOGIN"].Source)
}
// One line per setting, as structured fields rather than a rendered sentence.
var first map[string]any
if err := json.Unmarshal([]byte(strings.SplitN(line, "\n", 2)[0]), &first); err != nil {
t.Fatal(err)
}
for _, k := range []string{"key", "value", "source"} {
if _, ok := first[k]; !ok {
t.Errorf("the boot line has no %q field: %v", k, first)
}
}
}
// An intake root that is not absolute names a different directory to every process that reads a
// book's workdir out of the database — the same class as PD-149 on the state directory, refused in
// the same place.
func TestARelativeBooksDirectoryIsRefusedAtBoot(t *testing.T) {
t.Setenv("TM_PLATFORM_BOOKS_DIR", "books")
if _, err := Load(); err == nil {
t.Fatal("a relative TM_PLATFORM_BOOKS_DIR was accepted")
}
t.Setenv("TM_PLATFORM_BOOKS_DIR", "/srv/tm/books")
c, err := Load()
if err != nil {
t.Fatal(err)
}
if c.IntakeEnabled() {
t.Fatal("intake is enabled without an engine binary: a deployment that cannot parse would take uploads it can only reject")
}
t.Setenv("TM_PLATFORM_ENGINE_BIN", "/opt/engine/2026.08.01/tmctl")
if c, err = Load(); err != nil || !c.IntakeEnabled() {
t.Fatalf("intake stayed off with a root and an engine: %v %+v", err, c.Intake)
}
}
// The upload deadline and the intake sweep's grace are a pair: the sweep treats a book that has been
// `uploading` longer than its grace as an upload whose request is gone, which is only true while one
// request cannot legitimately take that long. Longer, and the sweep deletes the row and the directory
// out from under a request still writing into them — so the boot refuses the combination.
func TestAnUploadDeadlineLongerThanTheSweepsGraceIsRefused(t *testing.T) {
t.Setenv("TM_PLATFORM_BOOKS_DIR", "/srv/tm/books")
t.Setenv("TM_PLATFORM_ENGINE_BIN", "/opt/engine/2026.08.01/tmctl")
t.Setenv("TM_PLATFORM_UPLOAD_DEADLINE", (books.UploadGrace + time.Minute).String())
if _, err := Load(); err == nil {
t.Fatal("an upload deadline longer than the sweep's grace was accepted")
}
t.Setenv("TM_PLATFORM_UPLOAD_DEADLINE", (books.UploadGrace - time.Minute).String())
if _, err := Load(); err != nil {
t.Fatalf("a deadline inside the grace was refused: %v", err)
}
}