package channels import ( "strings" "testing" "github.com/Molecule-AI/molecule-monorepo/platform/internal/crypto" ) // withTestEncryptionKey installs a deterministic 32-byte key for the // duration of a test, then restores the previous state. Without this // the tests would depend on ambient SECRETS_ENCRYPTION_KEY. func withTestEncryptionKey(t *testing.T) { t.Helper() // Base64 of 32 zero bytes = "AAAA..." (44 chars). Matches the loader's // base64 path — the raw 32-byte path requires a string that is not // decodable as base64, which is surprisingly hard to construct. t.Setenv("SECRETS_ENCRYPTION_KEY", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") crypto.ResetForTesting() crypto.Init() t.Cleanup(func() { crypto.ResetForTesting() }) } func TestEncryptSensitiveFields_RoundTrip(t *testing.T) { withTestEncryptionKey(t) cfg := map[string]interface{}{ "bot_token": "123456:telegram-bot-token", "chat_id": "-100999", // non-sensitive, untouched "webhook_secret": "hmac-shared-key", // second known-sensitive field } if err := EncryptSensitiveFields(cfg); err != nil { t.Fatalf("encrypt: %v", err) } if tok, _ := cfg["bot_token"].(string); !strings.HasPrefix(tok, ciphertextPrefix) { t.Errorf("bot_token not encrypted: got %q", tok) } if sec, _ := cfg["webhook_secret"].(string); !strings.HasPrefix(sec, ciphertextPrefix) { t.Errorf("webhook_secret not encrypted: got %q", sec) } if chat, _ := cfg["chat_id"].(string); chat != "-100999" { t.Errorf("chat_id modified: got %q", chat) } if err := DecryptSensitiveFields(cfg); err != nil { t.Fatalf("decrypt: %v", err) } if got, _ := cfg["bot_token"].(string); got != "123456:telegram-bot-token" { t.Errorf("bot_token round-trip mismatch: got %q", got) } if got, _ := cfg["webhook_secret"].(string); got != "hmac-shared-key" { t.Errorf("webhook_secret round-trip mismatch: got %q", got) } } func TestEncryptSensitiveFields_Idempotent(t *testing.T) { withTestEncryptionKey(t) cfg := map[string]interface{}{"bot_token": "abc"} if err := EncryptSensitiveFields(cfg); err != nil { t.Fatalf("first encrypt: %v", err) } first, _ := cfg["bot_token"].(string) if err := EncryptSensitiveFields(cfg); err != nil { t.Fatalf("second encrypt: %v", err) } second, _ := cfg["bot_token"].(string) if first != second { t.Errorf("idempotent encrypt should not re-wrap: first=%q second=%q", first, second) } } func TestDecryptSensitiveFields_LegacyPlaintextPassesThrough(t *testing.T) { // Legacy row predates #319 — no ciphertext prefix. withTestEncryptionKey(t) cfg := map[string]interface{}{"bot_token": "legacy-plaintext-value"} if err := DecryptSensitiveFields(cfg); err != nil { t.Fatalf("decrypt: %v", err) } if got, _ := cfg["bot_token"].(string); got != "legacy-plaintext-value" { t.Errorf("legacy plaintext was mangled: got %q", got) } } func TestEncryptSensitiveFields_DevFallback_NoKey(t *testing.T) { // No key set — dev behaviour matches workspace_secrets: store plaintext. t.Setenv("SECRETS_ENCRYPTION_KEY", "") crypto.ResetForTesting() crypto.Init() t.Cleanup(crypto.ResetForTesting) cfg := map[string]interface{}{"bot_token": "dev-token"} if err := EncryptSensitiveFields(cfg); err != nil { t.Fatalf("encrypt: %v", err) } if got, _ := cfg["bot_token"].(string); got != "dev-token" { t.Errorf("dev fallback should leave plaintext: got %q", got) } } func TestEncryptSensitiveFields_SkipsEmptyAndNonString(t *testing.T) { withTestEncryptionKey(t) cfg := map[string]interface{}{ "bot_token": "", // empty "webhook_secret": 12345, // non-string "unrelated": "ignore", // not in sensitiveFields } if err := EncryptSensitiveFields(cfg); err != nil { t.Fatalf("encrypt: %v", err) } if got, _ := cfg["bot_token"].(string); got != "" { t.Errorf("empty bot_token should stay empty: got %q", got) } if got, _ := cfg["webhook_secret"].(int); got != 12345 { t.Errorf("non-string webhook_secret should be untouched: got %v", cfg["webhook_secret"]) } if got, _ := cfg["unrelated"].(string); got != "ignore" { t.Errorf("unrelated field should be untouched: got %q", got) } } func TestEncryptSensitiveFields_NilConfig(t *testing.T) { if err := EncryptSensitiveFields(nil); err != nil { t.Errorf("nil config: expected no error, got %v", err) } if err := DecryptSensitiveFields(nil); err != nil { t.Errorf("nil config: expected no error, got %v", err) } }