Files
Erupe/server/channelserver/svc_gacha_test.go
Houmgaor 1e9de7920d refactor(gacha): extract gacha logic into GachaService
Move payment processing, reward selection, stepup state management,
and box gacha tracking from handlers into a dedicated service layer.
Handlers now delegate to GachaService methods and only handle
protocol serialization.
2026-02-23 23:57:54 +01:00

317 lines
7.7 KiB
Go

package channelserver
import (
"database/sql"
"errors"
"testing"
"time"
"go.uber.org/zap"
)
func newTestGachaService(gr GachaRepo, ur UserRepo, cr CharacterRepo) *GachaService {
logger, _ := zap.NewDevelopment()
return NewGachaService(gr, ur, cr, logger, 100000)
}
func TestGachaService_PlayNormalGacha(t *testing.T) {
tests := []struct {
name string
txErr error
poolErr error
txRolls int
pool []GachaEntry
items map[uint32][]GachaItem
wantErr bool
wantCount int
}{
{
name: "transact error",
txErr: errors.New("tx fail"),
wantErr: true,
},
{
name: "reward pool error",
txRolls: 1,
poolErr: errors.New("pool fail"),
wantErr: true,
},
{
name: "success single roll",
txRolls: 1,
pool: []GachaEntry{{ID: 10, Weight: 100, Rarity: 3}},
items: map[uint32][]GachaItem{
10: {{ItemType: 1, ItemID: 500, Quantity: 1}},
},
wantCount: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gr := &mockGachaRepo{
txRolls: tt.txRolls,
txErr: tt.txErr,
rewardPool: tt.pool,
rewardPoolErr: tt.poolErr,
entryItems: tt.items,
}
cr := newMockCharacterRepo()
svc := newTestGachaService(gr, &mockUserRepoGacha{}, cr)
result, err := svc.PlayNormalGacha(1, 1, 1, 0)
if tt.wantErr {
if err == nil {
t.Fatal("Expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(result.Rewards) != tt.wantCount {
t.Errorf("Rewards count = %d, want %d", len(result.Rewards), tt.wantCount)
}
// Verify items were saved
if tt.wantCount > 0 && cr.columns["gacha_items"] == nil {
t.Error("Expected gacha items to be saved")
}
})
}
}
func TestGachaService_PlayStepupGacha(t *testing.T) {
tests := []struct {
name string
txErr error
poolErr error
txRolls int
pool []GachaEntry
items map[uint32][]GachaItem
guaranteed []GachaItem
wantErr bool
wantRandomCount int
wantGuaranteeCount int
}{
{
name: "transact error",
txErr: errors.New("tx fail"),
wantErr: true,
},
{
name: "reward pool error",
txRolls: 1,
poolErr: errors.New("pool fail"),
wantErr: true,
},
{
name: "success with guaranteed",
txRolls: 1,
pool: []GachaEntry{{ID: 10, Weight: 100, Rarity: 2}},
items: map[uint32][]GachaItem{
10: {{ItemType: 1, ItemID: 600, Quantity: 2}},
},
guaranteed: []GachaItem{{ItemType: 1, ItemID: 700, Quantity: 1}},
wantRandomCount: 1,
wantGuaranteeCount: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gr := &mockGachaRepo{
txRolls: tt.txRolls,
txErr: tt.txErr,
rewardPool: tt.pool,
rewardPoolErr: tt.poolErr,
entryItems: tt.items,
guaranteedItems: tt.guaranteed,
}
cr := newMockCharacterRepo()
svc := newTestGachaService(gr, &mockUserRepoGacha{}, cr)
result, err := svc.PlayStepupGacha(1, 1, 1, 0)
if tt.wantErr {
if err == nil {
t.Fatal("Expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(result.RandomRewards) != tt.wantRandomCount {
t.Errorf("RandomRewards count = %d, want %d", len(result.RandomRewards), tt.wantRandomCount)
}
if len(result.GuaranteedRewards) != tt.wantGuaranteeCount {
t.Errorf("GuaranteedRewards count = %d, want %d", len(result.GuaranteedRewards), tt.wantGuaranteeCount)
}
if !gr.deletedStepup {
t.Error("Expected stepup to be deleted")
}
if gr.insertedStep != 1 {
t.Errorf("Expected insertedStep=1, got %d", gr.insertedStep)
}
})
}
}
func TestGachaService_PlayBoxGacha(t *testing.T) {
gr := &mockGachaRepo{
txRolls: 1,
rewardPool: []GachaEntry{
{ID: 10, Weight: 100, Rarity: 1},
},
entryItems: map[uint32][]GachaItem{
10: {{ItemType: 1, ItemID: 800, Quantity: 1}},
},
}
cr := newMockCharacterRepo()
svc := newTestGachaService(gr, &mockUserRepoGacha{}, cr)
result, err := svc.PlayBoxGacha(1, 1, 1, 0)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(result.Rewards) != 1 {
t.Errorf("Rewards count = %d, want 1", len(result.Rewards))
}
if len(gr.insertedBoxIDs) == 0 {
t.Error("Expected box entry to be inserted")
}
}
func TestGachaService_GetStepupStatus(t *testing.T) {
now := time.Date(2025, 6, 15, 15, 0, 0, 0, time.UTC) // 3 PM
midday := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC)
tests := []struct {
name string
step uint8
createdAt time.Time
stepupErr error
hasEntry bool
wantStep uint8
wantDeleted bool
}{
{
name: "no rows",
stepupErr: sql.ErrNoRows,
wantStep: 0,
},
{
name: "fresh with entry",
step: 2,
createdAt: now, // after midday
hasEntry: true,
wantStep: 2,
wantDeleted: false,
},
{
name: "stale (before midday)",
step: 3,
createdAt: midday.Add(-1 * time.Hour), // before midday boundary
wantStep: 0,
wantDeleted: true,
},
{
name: "fresh but no entry type",
step: 2,
createdAt: now,
hasEntry: false,
wantStep: 0,
wantDeleted: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gr := &mockGachaRepo{
stepupStep: tt.step,
stepupTime: tt.createdAt,
stepupErr: tt.stepupErr,
hasEntryType: tt.hasEntry,
}
svc := newTestGachaService(gr, &mockUserRepoGacha{}, newMockCharacterRepo())
status, err := svc.GetStepupStatus(1, 1, now)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if status.Step != tt.wantStep {
t.Errorf("Step = %d, want %d", status.Step, tt.wantStep)
}
if gr.deletedStepup != tt.wantDeleted {
t.Errorf("deletedStepup = %v, want %v", gr.deletedStepup, tt.wantDeleted)
}
})
}
}
func TestGachaService_GetBoxInfo(t *testing.T) {
gr := &mockGachaRepo{
boxEntryIDs: []uint32{10, 20, 30},
}
svc := newTestGachaService(gr, &mockUserRepoGacha{}, newMockCharacterRepo())
ids, err := svc.GetBoxInfo(1, 1)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(ids) != 3 {
t.Errorf("Got %d entry IDs, want 3", len(ids))
}
}
func TestGachaService_ResetBox(t *testing.T) {
gr := &mockGachaRepo{}
svc := newTestGachaService(gr, &mockUserRepoGacha{}, newMockCharacterRepo())
err := svc.ResetBox(1, 1)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if !gr.deletedBox {
t.Error("Expected box entries to be deleted")
}
}
func TestGachaService_Transact_NetcafeCoins(t *testing.T) {
cr := newMockCharacterRepo()
cr.ints["netcafe_points"] = 5000
gr := &mockGachaRepo{
txItemType: 17,
txItemNumber: 100,
txRolls: 1,
}
svc := newTestGachaService(gr, &mockUserRepoGacha{}, cr)
rolls, err := svc.transact(1, 1, 1, 0)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if rolls != 1 {
t.Errorf("Rolls = %d, want 1", rolls)
}
// Netcafe points should have been reduced
if cr.ints["netcafe_points"] != 4900 {
t.Errorf("Netcafe points = %d, want 4900", cr.ints["netcafe_points"])
}
}
func TestGachaService_SpendGachaCoin_TrialFirst(t *testing.T) {
ur := &mockUserRepoGacha{trialCoins: 100}
svc := newTestGachaService(&mockGachaRepo{}, ur, newMockCharacterRepo())
svc.spendGachaCoin(1, 50)
// Should have used trial coins, not premium
}
func TestGachaService_SpendGachaCoin_PremiumFallback(t *testing.T) {
ur := &mockUserRepoGacha{trialCoins: 10}
svc := newTestGachaService(&mockGachaRepo{}, ur, newMockCharacterRepo())
svc.spendGachaCoin(1, 50)
// Should have used premium coins since trial < quantity
}