Full codebase audit: 21 security/robustness fixes, 328 tests across 4 packages

Security & economy:
- Credit()/Debit() reject non-positive amounts (closes infinite-money exploit)
- Lottery ticket purchase uses in-transaction count check (closes TOCTOU race)
- Arena bail channel ownership prevents double-close panic
- Entry.ID validated before exec.Command in fetch-esteemed
- Internal errors no longer leaked to users (forex, esteemed)

Robustness:
- safeGo() panic recovery added to 17 goroutine launch sites across 14 plugins
- db.Close() added and called on shutdown
- Miniflux mutex pattern fixed (snapshot-under-lock)
- Silently ignored DB/JSON errors now logged (lottery_db)

Performance:
- Added indexes: idx_arena_runs_user(user_id, status), idx_euro_bal_user(user_id)

UX:
- Empty !buy args shows usage instead of "No item matching ''"
- "Failed to load character" now suggests !adventure

Test coverage:
- internal/util/parser_test.go (19 tests: XP, levels, progress bar, commands, parsing)
- internal/crypto/crypto_test.go (12 tests: encrypt/decrypt, HMAC, ParseKey)
- internal/plugin/helpers_test.go (30 tests: formatNumber, calc, lottery, chat perks)
- internal/plugin/audit_fixes_test.go (25 tests: safeGo, bail channels, error leaks, overflow)
- internal/db/db_test.go (4 tests: Init, Close, schema indexes)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-04-08 17:04:40 -07:00
parent 68b2f8b7a5
commit e459b6e78d
34 changed files with 2443 additions and 486 deletions

View File

@@ -0,0 +1,165 @@
package crypto
import (
"bytes"
"encoding/base64"
"testing"
)
func testKey() []byte {
// 32 bytes for AES-256
return []byte("01234567890123456789012345678901")
}
// ── Encrypt / Decrypt Round-Trip ───────────────────────────────────────────
func TestEncryptDecrypt_RoundTrip(t *testing.T) {
key := testKey()
plaintext := []byte("hello, arena champion")
ciphertext, err := Encrypt(key, plaintext)
if err != nil {
t.Fatalf("Encrypt: %v", err)
}
got, err := Decrypt(key, ciphertext)
if err != nil {
t.Fatalf("Decrypt: %v", err)
}
if !bytes.Equal(got, plaintext) {
t.Errorf("round-trip failed: got %q, want %q", got, plaintext)
}
}
func TestEncryptDecrypt_EmptyPlaintext(t *testing.T) {
key := testKey()
ciphertext, err := Encrypt(key, []byte{})
if err != nil {
t.Fatalf("Encrypt empty: %v", err)
}
got, err := Decrypt(key, ciphertext)
if err != nil {
t.Fatalf("Decrypt empty: %v", err)
}
if len(got) != 0 {
t.Errorf("expected empty plaintext, got %d bytes", len(got))
}
}
func TestEncrypt_DifferentNonces(t *testing.T) {
key := testKey()
plaintext := []byte("same input")
ct1, _ := Encrypt(key, plaintext)
ct2, _ := Encrypt(key, plaintext)
if bytes.Equal(ct1, ct2) {
t.Error("two encryptions of the same plaintext should produce different ciphertexts (random nonces)")
}
}
func TestDecrypt_WrongKey(t *testing.T) {
key := testKey()
plaintext := []byte("secret data")
ciphertext, err := Encrypt(key, plaintext)
if err != nil {
t.Fatalf("Encrypt: %v", err)
}
wrongKey := []byte("99999999999999999999999999999999")
_, err = Decrypt(wrongKey, ciphertext)
if err == nil {
t.Error("Decrypt with wrong key should fail")
}
}
func TestDecrypt_TruncatedCiphertext(t *testing.T) {
key := testKey()
_, err := Decrypt(key, []byte{1, 2, 3})
if err == nil {
t.Error("Decrypt with too-short ciphertext should fail")
}
}
// ── HMAC ───────────────────────────────────────────────────────────────────
func TestHMAC_Deterministic(t *testing.T) {
key := testKey()
data := []byte("test data")
h1 := HMAC(key, data)
h2 := HMAC(key, data)
if h1 != h2 {
t.Error("HMAC should be deterministic")
}
}
func TestHMAC_DifferentData(t *testing.T) {
key := testKey()
h1 := HMAC(key, []byte("data1"))
h2 := HMAC(key, []byte("data2"))
if h1 == h2 {
t.Error("HMAC of different data should differ")
}
}
func TestHMAC_DifferentKeys(t *testing.T) {
data := []byte("same data")
h1 := HMAC([]byte("key1key1key1key1key1key1key1key1"), data)
h2 := HMAC([]byte("key2key2key2key2key2key2key2key2"), data)
if h1 == h2 {
t.Error("HMAC with different keys should differ")
}
}
func TestHMAC_Length(t *testing.T) {
h := HMAC(testKey(), []byte("x"))
// SHA-256 = 32 bytes = 64 hex chars
if len(h) != 64 {
t.Errorf("HMAC hex length = %d, want 64", len(h))
}
}
// ── ParseKey ───────────────────────────────────────────────────────────────
func TestParseKey_Valid(t *testing.T) {
raw := make([]byte, 32)
for i := range raw {
raw[i] = byte(i)
}
b64 := base64.StdEncoding.EncodeToString(raw)
key, err := ParseKey(b64)
if err != nil {
t.Fatalf("ParseKey: %v", err)
}
if !bytes.Equal(key, raw) {
t.Error("ParseKey returned wrong bytes")
}
}
func TestParseKey_WrongLength(t *testing.T) {
raw := make([]byte, 16) // too short
b64 := base64.StdEncoding.EncodeToString(raw)
_, err := ParseKey(b64)
if err == nil {
t.Error("ParseKey should reject non-32-byte keys")
}
}
func TestParseKey_InvalidBase64(t *testing.T) {
_, err := ParseKey("not!valid!base64!!!")
if err == nil {
t.Error("ParseKey should reject invalid base64")
}
}