mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-12 16:15:24 +00:00
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
262 lines
6.6 KiB
Go
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")
|
|
}
|
|
}
|