// internal/appconfig/materialize_test.go package appconfig import ( "strings" "testing" ) func TestRenderEnvMergesPlainAndDecryptedSecrets(t *testing.T) { priv, pub, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } pubKey, err := ParsePublicKey(pub, "test") if err != nil { t.Fatalf("ParsePublicKey: %v", err) } envelope, err := Seal(pubKey, Binding{Account: "bab", App: "keymail"}, []byte("s3cr3t")) if err != nil { t.Fatalf("Seal: %v", err) } m := &Manifest{Schema: 1, App: "keymail", Vars: map[string]Var{ "LOG_LEVEL": {Kind: "env", Value: "debug"}, "SES_SECRET": {Kind: "secret", Envelope: envelope}, }} got, err := RenderEnv(m, privKey, Binding{Account: "bab", App: "keymail"}) if err != nil { t.Fatalf("RenderEnv: %v", err) } // Sorted by key, so the diff Sync computes against what's on disk is // deterministic — an unrelated key added or removed elsewhere in the // map must not reorder every line and force a spurious restart. want := "LOG_LEVEL=debug\nSES_SECRET=s3cr3t\n" if string(got) != want { t.Fatalf("RenderEnv = %q, want %q", got, want) } } func TestRenderEnvRejectsValuesSystemdWouldMangle(t *testing.T) { // systemd's EnvironmentFile parser is shell-like: it eats backslash // escapes and quotes, and drops a line outright on an unbalanced quote. // A value carrying any of those would be silently corrupted on the host, // so reject it here, where an operator still sees the error. for name, val := range map[string]string{ "newline": "line1\nline2", "nul": "a\x00b", "backslash": `C:\Users\svc`, "singlequote": "it's", "doublequote": `say "hi"`, "unbalanced": `p4ss"word`, "mixed": `a\'b"c`, } { m := &Manifest{Schema: 1, App: "keymail", Vars: map[string]Var{ "BAD": {Kind: "env", Value: val}, }} _, err := RenderEnv(m, nil, Binding{Account: "bab", App: "keymail"}) if err == nil { t.Errorf("%s: RenderEnv accepted %q, want an error", name, val) continue } if !strings.Contains(err.Error(), "BAD") { t.Errorf("%s: error %q does not name the offending var", name, err) } } } func TestRenderEnvAcceptsOrdinaryPunctuation(t *testing.T) { // The rejection must stay narrow: everything systemd passes through // verbatim still renders, including the characters common in URLs and // base64 payloads. val := "postgres://u:p%40ss@host:5432/db?sslmode=require&x=a+b/c==" m := &Manifest{Schema: 1, App: "keymail", Vars: map[string]Var{ "DATABASE_URL": {Kind: "env", Value: val}, }} got, err := RenderEnv(m, nil, Binding{Account: "bab", App: "keymail"}) if err != nil { t.Fatalf("RenderEnv: %v", err) } if string(got) != "DATABASE_URL="+val+"\n" { t.Fatalf("RenderEnv = %q, want DATABASE_URL=%s", got, val) } }