mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 16:42:41 +00:00
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>
256 lines
6.9 KiB
Go
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")
|
|
}
|
|
}
|