fix(payment): store provider config as plaintext JSON with legacy ciphertext fallback

Without TOTP_ENCRYPTION_KEY, saved payment configs were lost on restart because
the AES round-trip failed silently. Write new records as plaintext JSON; read
path tries JSON first, falls back to legacy AES decrypt when a key is present,
and treats unreadable values as empty so admins can re-enter them via the UI.
This commit is contained in:
erio
2026-04-17 17:00:29 +08:00
parent 6cfdf4ec05
commit fd0c9a1305
4 changed files with 151 additions and 25 deletions

View File

@@ -452,6 +452,103 @@ func TestStartOfDay(t *testing.T) {
}
}
func TestDecryptConfig_PlaintextAndLegacyCompat(t *testing.T) {
t.Parallel()
key := make([]byte, AES256KeySize)
for i := range key {
key[i] = byte(i + 1)
}
wrongKey := make([]byte, AES256KeySize)
for i := range wrongKey {
wrongKey[i] = byte(0xFF - i)
}
plaintextJSON := `{"appId":"app-123","secret":"sec-xyz"}`
legacyEncrypted, err := Encrypt(plaintextJSON, key)
if err != nil {
t.Fatalf("seed Encrypt: %v", err)
}
tests := []struct {
name string
stored string
key []byte
want map[string]string
}{
{
name: "empty stored returns nil map",
stored: "",
key: key,
want: nil,
},
{
name: "plaintext JSON parses directly",
stored: plaintextJSON,
key: nil,
want: map[string]string{"appId": "app-123", "secret": "sec-xyz"},
},
{
name: "plaintext JSON works even with key present",
stored: plaintextJSON,
key: key,
want: map[string]string{"appId": "app-123", "secret": "sec-xyz"},
},
{
name: "legacy ciphertext with correct key decrypts",
stored: legacyEncrypted,
key: key,
want: map[string]string{"appId": "app-123", "secret": "sec-xyz"},
},
{
name: "legacy ciphertext with no key treated as empty",
stored: legacyEncrypted,
key: nil,
want: nil,
},
{
name: "legacy ciphertext with wrong key treated as empty",
stored: legacyEncrypted,
key: wrongKey,
want: nil,
},
{
name: "garbage data treated as empty",
stored: "not-json-and-not-ciphertext",
key: key,
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
lb := NewDefaultLoadBalancer(nil, tt.key)
got, err := lb.decryptConfig(tt.stored)
if err != nil {
t.Fatalf("decryptConfig unexpected error: %v", err)
}
if !stringMapEqual(got, tt.want) {
t.Fatalf("decryptConfig = %v, want %v", got, tt.want)
}
})
}
}
// stringMapEqual compares two map[string]string values; nil and empty are equal.
func stringMapEqual(a, b map[string]string) bool {
if len(a) != len(b) {
return false
}
for k, v := range a {
if bv, ok := b[k]; !ok || bv != v {
return false
}
}
return true
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------