Files
viettranx 90b7396d74 fix(bus,cron): panic recovery + deadlock fix + unit tests for 9 packages
Bug fixes:
- bus: add panic recovery in Broadcast() — panicking subscriber no longer
  crashes entire event bus goroutine
- cron: fix deadlock in RunJob() — recordRun called while holding mutex,
  extracted recordRunLocked for callers already holding lock
- cron: add run log recording to executeJobByID — automatic scheduler
  was not populating run log, only manual RunJob did

New tests (~170 cases across 9 packages):
- crypto: roundtrip, key derivation (hex/b64/raw), nonce uniqueness,
  backward compat, wrong key, corruption
- permissions: role hierarchy, RoleFromScopes, CanAccess, scope precedence
- bus: pub/sub delivery, buffer full, panic recovery, concurrent safety
- providers/retry: IsRetryableError, backoff, jitter, Retry-After, context
  cancellation, hook callback
- sessions: idempotency, concurrent writes, defensive copy, save/load
  roundtrip, metadata accumulation
- scheduler: draining, drop policies, adaptive throttle, stale completion,
  lane concurrency, debounce, interrupt mode
- config: JSON5 parsing, env overrides, FlexibleStringSlice, owner IDs
- cron: schedule validation, computeNextRun, CRUD, job execution, failure
  tracking, persistence roundtrip
- agent: history sanitization edge cases — all-tool history, partial results,
  cross-turn dedup, orphaned tools, 1000-msg performance
2026-03-28 18:15:46 +07:00

262 lines
6.6 KiB
Go

package crypto
import (
"encoding/base64"
"encoding/hex"
"strings"
"testing"
)
// testKey32 is a deterministic 32-byte raw key for tests.
const testKey32 = "01234567890123456789012345678901" // exactly 32 bytes
func testKeyHex() string {
return hex.EncodeToString([]byte(testKey32)) // 64 hex chars
}
func testKeyBase64() string {
return base64.StdEncoding.EncodeToString([]byte(testKey32)) // 44 chars ending with =
}
// --- DeriveKey ---
func TestDeriveKey_Hex64(t *testing.T) {
k, err := DeriveKey(testKeyHex())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(k) != 32 {
t.Fatalf("expected 32 bytes, got %d", len(k))
}
}
func TestDeriveKey_Base64_44(t *testing.T) {
k, err := DeriveKey(testKeyBase64())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(k) != 32 {
t.Fatalf("expected 32 bytes, got %d", len(k))
}
}
func TestDeriveKey_Raw32(t *testing.T) {
k, err := DeriveKey(testKey32)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(k) != testKey32 {
t.Fatalf("raw key mismatch")
}
}
func TestDeriveKey_InvalidLength(t *testing.T) {
tests := []struct {
name string
key string
}{
{"too_short", "abc"},
{"16_bytes", "0123456789abcdef"},
{"48_bytes", strings.Repeat("a", 48)},
{"empty", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := DeriveKey(tt.key)
if err == nil {
t.Fatalf("expected error for key len %d, got nil", len(tt.key))
}
})
}
}
func TestDeriveKey_Hex64_InvalidHex(t *testing.T) {
// 64 chars but not valid hex → should fall through to raw 32 check, then fail
key := strings.Repeat("zz", 32) // 64 chars, invalid hex
_, err := DeriveKey(key)
if err == nil {
t.Fatal("expected error for invalid hex, got nil")
}
}
// --- Encrypt with invalid key ---
func TestEncrypt_InvalidKey(t *testing.T) {
_, err := Encrypt("secret", "too-short-key")
if err == nil {
t.Fatal("expected error for invalid key length")
}
}
// --- Encrypt / Decrypt roundtrip ---
func TestEncryptDecrypt_Roundtrip(t *testing.T) {
tests := []struct {
name string
plaintext string
}{
{"simple_ascii", "my-secret-api-key-12345"},
{"unicode", "你好世界🌍emoji日本語"},
{"special_chars", `key="value"&foo=bar<>'"!@#$%`},
{"long_string", strings.Repeat("abcdefghij", 1000)},
{"single_char", "x"},
{"whitespace", " \t\n "},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
enc, err := Encrypt(tt.plaintext, testKey32)
if err != nil {
t.Fatalf("encrypt failed: %v", err)
}
if !strings.HasPrefix(enc, prefix) {
t.Fatalf("encrypted value missing prefix, got: %s", enc[:20])
}
dec, err := Decrypt(enc, testKey32)
if err != nil {
t.Fatalf("decrypt failed: %v", err)
}
if dec != tt.plaintext {
t.Fatalf("roundtrip mismatch: got %q, want %q", dec, tt.plaintext)
}
})
}
}
func TestEncryptDecrypt_EmptyPlaintext(t *testing.T) {
enc, err := Encrypt("", testKey32)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if enc != "" {
t.Fatalf("expected empty string, got %q", enc)
}
}
func TestEncryptDecrypt_EmptyKey(t *testing.T) {
// Empty key → plaintext returned unchanged for both encrypt and decrypt
enc, err := Encrypt("secret", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if enc != "secret" {
t.Fatalf("expected plaintext passthrough, got %q", enc)
}
dec, err := Decrypt("aes-gcm:someciphertext", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if dec != "aes-gcm:someciphertext" {
t.Fatalf("expected ciphertext passthrough, got %q", dec)
}
}
// --- Nonce uniqueness: same plaintext + key → different ciphertexts ---
func TestEncrypt_NonceUniqueness(t *testing.T) {
const plaintext = "identical-plaintext"
enc1, _ := Encrypt(plaintext, testKey32)
enc2, _ := Encrypt(plaintext, testKey32)
if enc1 == enc2 {
t.Fatal("two encryptions of same plaintext must produce different ciphertext (unique nonce)")
}
}
// --- Backward compatibility: decrypt unencrypted string returns as-is ---
func TestDecrypt_BackwardCompat_PlainText(t *testing.T) {
plain := "sk-ant-not-encrypted-just-raw"
dec, err := Decrypt(plain, testKey32)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if dec != plain {
t.Fatalf("expected passthrough for unencrypted value, got %q", dec)
}
}
// --- Error cases ---
func TestDecrypt_CorruptedCiphertext(t *testing.T) {
// Valid prefix but garbage base64 → should return as-is (line 68: treat as plain text)
garbage := prefix + "!!!not-base64!!!"
dec, err := Decrypt(garbage, testKey32)
if err != nil {
t.Fatalf("unexpected error for garbage base64: %v", err)
}
if dec != garbage {
t.Fatalf("expected passthrough for invalid base64, got %q", dec)
}
}
func TestDecrypt_WrongKey(t *testing.T) {
enc, _ := Encrypt("secret", testKey32)
otherKey := "98765432109876543210987654321098" // different 32-byte key
_, err := Decrypt(enc, otherKey)
if err == nil {
t.Fatal("expected error for wrong key, got nil")
}
if !strings.Contains(err.Error(), "decrypt failed") {
t.Fatalf("expected 'decrypt failed' error, got: %v", err)
}
}
func TestDecrypt_TooShortCiphertext(t *testing.T) {
// Valid prefix + valid base64 but too short for nonce → return as-is
short := prefix + base64.StdEncoding.EncodeToString([]byte("x"))
dec, err := Decrypt(short, testKey32)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if dec != short {
t.Fatalf("expected passthrough for too-short ciphertext, got %q", dec)
}
}
// --- IsEncrypted ---
func TestIsEncrypted(t *testing.T) {
if !IsEncrypted("aes-gcm:abc123") {
t.Fatal("expected true for prefixed value")
}
if IsEncrypted("plain-text-value") {
t.Fatal("expected false for unprefixed value")
}
if IsEncrypted("") {
t.Fatal("expected false for empty string")
}
}
// --- Cross-key-format roundtrip: encrypt with one format, decrypt with another ---
func TestEncryptDecrypt_CrossKeyFormats(t *testing.T) {
// All three key formats represent the same 32-byte key
keyRaw := testKey32
keyHex := testKeyHex()
keyB64 := testKeyBase64()
plaintext := "cross-format-test-value"
// Encrypt with raw, decrypt with hex
enc, _ := Encrypt(plaintext, keyRaw)
dec, err := Decrypt(enc, keyHex)
if err != nil {
t.Fatalf("hex decrypt failed: %v", err)
}
if dec != plaintext {
t.Fatalf("cross-format mismatch: raw→hex")
}
// Encrypt with hex, decrypt with base64
enc, _ = Encrypt(plaintext, keyHex)
dec, err = Decrypt(enc, keyB64)
if err != nil {
t.Fatalf("base64 decrypt failed: %v", err)
}
if dec != plaintext {
t.Fatalf("cross-format mismatch: hex→base64")
}
}