- GameRepo: EnsureCategoryAchievement, RemoveCategoryAchievementIfNotEarned, ListCategoriesWithBudget, GetMonthlySpendByCategory - GameService: OnCategoryBudgetSet, CheckCategoryBudgets - ListAchievements filtra conquistas por categorias do perfil (sem vazamento entre usuários) - NotifyAction chama CheckCategoryBudgets em transaction_created e import_confirmed - CategoryHandler injeta GameService e chama OnCategoryBudgetSet após Create/Update - 5 novos testes unitários cobrindo todos os cenários de AC
174 lines
5.6 KiB
Go
174 lines
5.6 KiB
Go
package service_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"financeiro-carvalho/internal/model"
|
|
"financeiro-carvalho/internal/service"
|
|
)
|
|
|
|
type mockGameRepo struct {
|
|
profile model.PlayerProfile
|
|
achievements map[string]bool
|
|
ensuredCats map[int]string
|
|
removedCats []int
|
|
catBudgets []model.CategoryBudgetInfo
|
|
catSpend map[int]float64
|
|
}
|
|
|
|
func newMockGameRepo() *mockGameRepo {
|
|
return &mockGameRepo{
|
|
profile: model.PlayerProfile{ID: 1, Level: 1, XP: 0, XPToNext: 100},
|
|
achievements: map[string]bool{},
|
|
ensuredCats: map[int]string{},
|
|
catSpend: map[int]float64{},
|
|
}
|
|
}
|
|
|
|
func (m *mockGameRepo) GetProfile(_ context.Context) (*model.PlayerProfile, error) {
|
|
cp := m.profile
|
|
return &cp, nil
|
|
}
|
|
func (m *mockGameRepo) AddXP(_ context.Context, _ string, amount int, _ string) (*model.PlayerProfile, error) {
|
|
m.profile.XP += amount
|
|
for m.profile.XP >= m.profile.XPToNext {
|
|
m.profile.XP -= m.profile.XPToNext
|
|
m.profile.Level++
|
|
m.profile.XPToNext = m.profile.Level * m.profile.Level * 100
|
|
}
|
|
cp := m.profile
|
|
return &cp, nil
|
|
}
|
|
func (m *mockGameRepo) ListActiveQuests(_ context.Context) ([]model.PlayerQuest, error) {
|
|
return nil, nil
|
|
}
|
|
func (m *mockGameRepo) EnsurePlayerQuest(_ context.Context, _ int, _ string) (int, error) { return 1, nil }
|
|
func (m *mockGameRepo) IncrementQuestCount(_ context.Context, _, _ string, _ int) error { return nil }
|
|
func (m *mockGameRepo) UpdateQuestPct(_ context.Context, _ string, _ float64) error { return nil }
|
|
func (m *mockGameRepo) ClaimQuest(_ context.Context, _ int) (int, error) { return 50, nil }
|
|
func (m *mockGameRepo) ListAchievements(_ context.Context) ([]model.Achievement, error) { return nil, nil }
|
|
func (m *mockGameRepo) IsAchievementUnlocked(_ context.Context, c string) (bool, error) {
|
|
return m.achievements[c], nil
|
|
}
|
|
func (m *mockGameRepo) UnlockAchievement(_ context.Context, c string) (int, error) {
|
|
m.achievements[c] = true
|
|
return 100, nil
|
|
}
|
|
func (m *mockGameRepo) ListCosmetics(_ context.Context) ([]model.Cosmetic, error) { return nil, nil }
|
|
func (m *mockGameRepo) EquipCosmetic(_ context.Context, _ int) error { return nil }
|
|
func (m *mockGameRepo) UnlockCosmeticsForLevel(_ context.Context, _ int) error { return nil }
|
|
func (m *mockGameRepo) EnsureCategoryAchievement(_ context.Context, catID int, name string) error {
|
|
m.ensuredCats[catID] = name
|
|
return nil
|
|
}
|
|
func (m *mockGameRepo) RemoveCategoryAchievementIfNotEarned(_ context.Context, catID int) error {
|
|
m.removedCats = append(m.removedCats, catID)
|
|
return nil
|
|
}
|
|
func (m *mockGameRepo) ListCategoriesWithBudget(_ context.Context) ([]model.CategoryBudgetInfo, error) {
|
|
return m.catBudgets, nil
|
|
}
|
|
func (m *mockGameRepo) GetMonthlySpendByCategory(_ context.Context, _ string, catID int) (float64, error) {
|
|
return m.catSpend[catID], nil
|
|
}
|
|
|
|
func TestGame_XPAccumulates(t *testing.T) {
|
|
repo := newMockGameRepo()
|
|
svc := service.NewGameService(repo)
|
|
|
|
p, err := svc.AwardXP(context.Background(), "test", 50, "test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p.XP != 50 {
|
|
t.Errorf("expected xp=50, got %d", p.XP)
|
|
}
|
|
}
|
|
|
|
func TestGame_LevelUpAtThreshold(t *testing.T) {
|
|
repo := newMockGameRepo()
|
|
svc := service.NewGameService(repo)
|
|
|
|
// level 1 threshold is 100 XP
|
|
p, err := svc.AwardXP(context.Background(), "test", 100, "test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p.Level != 2 {
|
|
t.Errorf("expected level=2 after 100 XP, got %d", p.Level)
|
|
}
|
|
if p.XP != 0 {
|
|
t.Errorf("expected remaining xp=0, got %d", p.XP)
|
|
}
|
|
}
|
|
|
|
func TestGame_AchievementNotDuplicated(t *testing.T) {
|
|
repo := newMockGameRepo()
|
|
svc := service.NewGameService(repo)
|
|
|
|
svc.CheckAndUnlockAchievement(context.Background(), "first_transaction")
|
|
svc.CheckAndUnlockAchievement(context.Background(), "first_transaction")
|
|
|
|
// second call should be a no-op — XP only awarded once
|
|
// repo tracks it as unlocked after first call
|
|
if !repo.achievements["first_transaction"] {
|
|
t.Error("achievement should be marked unlocked")
|
|
}
|
|
}
|
|
|
|
func TestGame_EnsureCategoryAchievementOnBudgetSet(t *testing.T) {
|
|
repo := newMockGameRepo()
|
|
svc := service.NewGameService(repo)
|
|
|
|
budget := 800.0
|
|
svc.OnCategoryBudgetSet(context.Background(), 5, "Alimentação", &budget)
|
|
|
|
if repo.ensuredCats[5] != "Alimentação" {
|
|
t.Errorf("expected EnsureCategoryAchievement called for cat 5, got %v", repo.ensuredCats)
|
|
}
|
|
if len(repo.removedCats) != 0 {
|
|
t.Error("expected no removal when budget is set")
|
|
}
|
|
}
|
|
|
|
func TestGame_RemoveCategoryAchievementOnBudgetNull(t *testing.T) {
|
|
repo := newMockGameRepo()
|
|
svc := service.NewGameService(repo)
|
|
|
|
svc.OnCategoryBudgetSet(context.Background(), 5, "Alimentação", nil)
|
|
|
|
if len(repo.removedCats) == 0 || repo.removedCats[0] != 5 {
|
|
t.Errorf("expected RemoveCategoryAchievementIfNotEarned for cat 5, got %v", repo.removedCats)
|
|
}
|
|
if len(repo.ensuredCats) != 0 {
|
|
t.Error("expected no ensure when budget is nil")
|
|
}
|
|
}
|
|
|
|
func TestGame_CategoryBudgetUnlock(t *testing.T) {
|
|
repo := newMockGameRepo()
|
|
repo.catBudgets = []model.CategoryBudgetInfo{{CategoryID: 5, MonthlyBudget: 800}}
|
|
repo.catSpend[5] = 750
|
|
svc := service.NewGameService(repo)
|
|
|
|
svc.CheckCategoryBudgets(context.Background(), "2026-05")
|
|
|
|
if !repo.achievements["budget_cat_5"] {
|
|
t.Error("expected budget_cat_5 unlocked when spend 750 <= budget 800")
|
|
}
|
|
}
|
|
|
|
func TestGame_CategoryBudgetNoUnlockWhenOverBudget(t *testing.T) {
|
|
repo := newMockGameRepo()
|
|
repo.catBudgets = []model.CategoryBudgetInfo{{CategoryID: 5, MonthlyBudget: 800}}
|
|
repo.catSpend[5] = 850
|
|
svc := service.NewGameService(repo)
|
|
|
|
svc.CheckCategoryBudgets(context.Background(), "2026-05")
|
|
|
|
if repo.achievements["budget_cat_5"] {
|
|
t.Error("expected budget_cat_5 NOT unlocked when spend 850 > budget 800")
|
|
}
|
|
}
|