Files
gogobee/internal/util/parser_test.go
prosolis e459b6e78d 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>
2026-04-08 17:04:40 -07:00

256 lines
6.9 KiB
Go

package util
import (
"testing"
)
// ── XPForLevel ─────────────────────────────────────────────────────────────
func TestXPForLevel_Zero(t *testing.T) {
if got := XPForLevel(0); got != 0 {
t.Errorf("XPForLevel(0) = %d, want 0", got)
}
}
func TestXPForLevel_KnownValues(t *testing.T) {
cases := []struct {
level int
want int
}{
{1, 100},
{2, 400},
{5, 2500},
{10, 10000},
{20, 40000},
{50, 250000},
}
for _, tc := range cases {
if got := XPForLevel(tc.level); got != tc.want {
t.Errorf("XPForLevel(%d) = %d, want %d", tc.level, got, tc.want)
}
}
}
// ── LevelFromXP ────────────────────────────────────────────────────────────
func TestLevelFromXP_Zero(t *testing.T) {
if got := LevelFromXP(0); got != 0 {
t.Errorf("LevelFromXP(0) = %d, want 0", got)
}
}
func TestLevelFromXP_Negative(t *testing.T) {
if got := LevelFromXP(-100); got != 0 {
t.Errorf("LevelFromXP(-100) = %d, want 0", got)
}
}
func TestLevelFromXP_ExactBoundaries(t *testing.T) {
// At exactly the XP threshold for a level, you should be that level.
cases := []struct {
xp int
want int
}{
{99, 0},
{100, 1},
{399, 1},
{400, 2},
{2500, 5},
{10000, 10},
}
for _, tc := range cases {
if got := LevelFromXP(tc.xp); got != tc.want {
t.Errorf("LevelFromXP(%d) = %d, want %d", tc.xp, got, tc.want)
}
}
}
func TestLevelFromXP_RoundTrip(t *testing.T) {
for level := 0; level <= 100; level++ {
xp := XPForLevel(level)
got := LevelFromXP(xp)
if got != level {
t.Errorf("LevelFromXP(XPForLevel(%d)) = %d, want %d", level, got, level)
}
}
}
// ── ProgressBar ────────────────────────────────────────────────────────────
func TestProgressBar_Empty(t *testing.T) {
bar := ProgressBar(0, 100, 10)
want := "[----------] 0%"
if bar != want {
t.Errorf("ProgressBar(0,100,10) = %q, want %q", bar, want)
}
}
func TestProgressBar_Full(t *testing.T) {
bar := ProgressBar(100, 100, 10)
want := "[##########] 100%"
if bar != want {
t.Errorf("ProgressBar(100,100,10) = %q, want %q", bar, want)
}
}
func TestProgressBar_Half(t *testing.T) {
bar := ProgressBar(50, 100, 10)
want := "[#####-----] 50%"
if bar != want {
t.Errorf("ProgressBar(50,100,10) = %q, want %q", bar, want)
}
}
func TestProgressBar_Overflow(t *testing.T) {
bar := ProgressBar(200, 100, 10)
want := "[##########] 100%"
if bar != want {
t.Errorf("ProgressBar(200,100,10) = %q, want %q", bar, want)
}
}
func TestProgressBar_ZeroMax(t *testing.T) {
bar := ProgressBar(50, 0, 10)
want := "----------"
if bar != want {
t.Errorf("ProgressBar(50,0,10) = %q, want %q", bar, want)
}
}
// ── IsCommand ──────────────────────────────────────────────────────────────
func TestIsCommand_ExactMatch(t *testing.T) {
if !IsCommand("!rank", "!", "rank") {
t.Error("IsCommand should match exact command")
}
}
func TestIsCommand_WithArgs(t *testing.T) {
if !IsCommand("!rank @someone", "!", "rank") {
t.Error("IsCommand should match command with args")
}
}
func TestIsCommand_CaseInsensitive(t *testing.T) {
if !IsCommand("!RANK", "!", "rank") {
t.Error("IsCommand should be case-insensitive")
}
}
func TestIsCommand_NoMatch(t *testing.T) {
if IsCommand("!leaderboard", "!", "rank") {
t.Error("IsCommand should not match different command")
}
}
func TestIsCommand_PartialNoMatch(t *testing.T) {
if IsCommand("!rankings", "!", "rank") {
t.Error("IsCommand should not match partial prefix without space")
}
}
func TestIsCommand_LeadingWhitespace(t *testing.T) {
if !IsCommand(" !rank", "!", "rank") {
t.Error("IsCommand should handle leading whitespace")
}
}
// ── GetArgs ────────────────────────────────────────────────────────────────
func TestGetArgs_NoArgs(t *testing.T) {
if got := GetArgs("!rank", "!", "rank"); got != "" {
t.Errorf("GetArgs should return empty for no args, got %q", got)
}
}
func TestGetArgs_WithArgs(t *testing.T) {
if got := GetArgs("!rank @someone", "!", "rank"); got != "@someone" {
t.Errorf("GetArgs = %q, want %q", got, "@someone")
}
}
func TestGetArgs_MultipleArgs(t *testing.T) {
got := GetArgs("!calc 2 + 2", "!", "calc")
if got != "2 + 2" {
t.Errorf("GetArgs = %q, want %q", got, "2 + 2")
}
}
func TestGetArgs_WrongCommand(t *testing.T) {
if got := GetArgs("!other stuff", "!", "rank"); got != "" {
t.Errorf("GetArgs for wrong command should return empty, got %q", got)
}
}
// ── HasNonASCII ────────────────────────────────────────────────────────────
func TestHasNonASCII_ASCII(t *testing.T) {
if HasNonASCII("hello world 123") {
t.Error("pure ASCII should return false")
}
}
func TestHasNonASCII_Emoji(t *testing.T) {
if !HasNonASCII("hello 🌍") {
t.Error("emoji should return true")
}
}
func TestHasNonASCII_Unicode(t *testing.T) {
if !HasNonASCII("café") {
t.Error("accented chars should return true")
}
}
func TestHasNonASCII_Empty(t *testing.T) {
if HasNonASCII("") {
t.Error("empty string should return false")
}
}
// ── ParseMessage ───────────────────────────────────────────────────────────
func TestParseMessage_Plain(t *testing.T) {
stats := ParseMessage("hello world")
if stats.Words != 2 {
t.Errorf("Words = %d, want 2", stats.Words)
}
if stats.Chars != 11 {
t.Errorf("Chars = %d, want 11", stats.Chars)
}
}
func TestParseMessage_WithLinks(t *testing.T) {
stats := ParseMessage("check https://example.com and https://other.org")
if stats.Links != 2 {
t.Errorf("Links = %d, want 2", stats.Links)
}
}
func TestParseMessage_WithImages(t *testing.T) {
stats := ParseMessage("look https://img.example.com/cat.png here")
if stats.Images != 1 {
t.Errorf("Images = %d, want 1", stats.Images)
}
if stats.Links != 1 {
t.Errorf("Links = %d, want 1", stats.Links)
}
}
func TestParseMessage_Questions(t *testing.T) {
stats := ParseMessage("what? really? yes!")
if stats.Questions != 2 {
t.Errorf("Questions = %d, want 2", stats.Questions)
}
if stats.Exclamations != 1 {
t.Errorf("Exclamations = %d, want 1", stats.Exclamations)
}
}
func TestParseMessage_Empty(t *testing.T) {
stats := ParseMessage("")
if stats.Words != 0 || stats.Chars != 0 {
t.Errorf("empty message should have 0 words and 0 chars")
}
}