575 lines
16 KiB
Go
575 lines
16 KiB
Go
package bossraid
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/tolelom/tolchain/core"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests for pure functions and validation logic
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestGenerateToken_Uniqueness(t *testing.T) {
|
|
tokens := make(map[string]bool, 100)
|
|
for i := 0; i < 100; i++ {
|
|
tok, err := generateToken()
|
|
if err != nil {
|
|
t.Fatalf("generateToken() failed: %v", err)
|
|
}
|
|
if len(tok) != 64 { // 32 bytes = 64 hex chars
|
|
t.Errorf("token length = %d, want 64", len(tok))
|
|
}
|
|
if tokens[tok] {
|
|
t.Errorf("duplicate token generated: %s", tok)
|
|
}
|
|
tokens[tok] = true
|
|
}
|
|
}
|
|
|
|
func TestGenerateToken_IsValidHex(t *testing.T) {
|
|
tok, err := generateToken()
|
|
if err != nil {
|
|
t.Fatalf("generateToken() failed: %v", err)
|
|
}
|
|
for _, c := range tok {
|
|
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
|
|
t.Errorf("token contains non-hex char: %c", c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests for RegisterServer input validation
|
|
// Note: RequestEntry calls CheckStaleSlots() before validation, which needs
|
|
// a non-nil repo, so we test its validation via the mock-based tests below.
|
|
// RegisterServer validates before DB access, so we can test directly.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestRegisterServer_Validation_EmptyServerName(t *testing.T) {
|
|
svc := &Service{}
|
|
_, err := svc.RegisterServer("", "instance1", 10)
|
|
if err == nil {
|
|
t.Error("RegisterServer with empty serverName should fail")
|
|
}
|
|
}
|
|
|
|
func TestRegisterServer_Validation_EmptyInstanceID(t *testing.T) {
|
|
svc := &Service{}
|
|
_, err := svc.RegisterServer("Dedi1", "", 10)
|
|
if err == nil {
|
|
t.Error("RegisterServer with empty instanceID should fail")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests for model constants and JSON serialization
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestRoomStatus_Constants(t *testing.T) {
|
|
tests := []struct {
|
|
status RoomStatus
|
|
want string
|
|
}{
|
|
{StatusWaiting, "waiting"},
|
|
{StatusInProgress, "in_progress"},
|
|
{StatusCompleted, "completed"},
|
|
{StatusFailed, "failed"},
|
|
{StatusRewardFailed, "reward_failed"},
|
|
}
|
|
for _, tt := range tests {
|
|
if string(tt.status) != tt.want {
|
|
t.Errorf("status %v = %q, want %q", tt.status, string(tt.status), tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSlotStatus_Constants(t *testing.T) {
|
|
tests := []struct {
|
|
status SlotStatus
|
|
want string
|
|
}{
|
|
{SlotIdle, "idle"},
|
|
{SlotWaiting, "waiting"},
|
|
{SlotInProgress, "in_progress"},
|
|
}
|
|
for _, tt := range tests {
|
|
if string(tt.status) != tt.want {
|
|
t.Errorf("slot status %v = %q, want %q", tt.status, string(tt.status), tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDefaultMaxPlayers(t *testing.T) {
|
|
if defaultMaxPlayers != 3 {
|
|
t.Errorf("defaultMaxPlayers = %d, want 3", defaultMaxPlayers)
|
|
}
|
|
}
|
|
|
|
func TestBossRoom_PlayersJSON_RoundTrip(t *testing.T) {
|
|
usernames := []string{"alice", "bob", "charlie"}
|
|
data, err := json.Marshal(usernames)
|
|
if err != nil {
|
|
t.Fatalf("marshal failed: %v", err)
|
|
}
|
|
|
|
room := BossRoom{
|
|
Players: string(data),
|
|
}
|
|
|
|
var parsed []string
|
|
if err := json.Unmarshal([]byte(room.Players), &parsed); err != nil {
|
|
t.Fatalf("unmarshal failed: %v", err)
|
|
}
|
|
|
|
if len(parsed) != 3 {
|
|
t.Fatalf("parsed player count = %d, want 3", len(parsed))
|
|
}
|
|
for i, want := range usernames {
|
|
if parsed[i] != want {
|
|
t.Errorf("parsed[%d] = %q, want %q", i, parsed[i], want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPlayerReward_JSONRoundTrip(t *testing.T) {
|
|
rewards := []PlayerReward{
|
|
{Username: "alice", TokenAmount: 100, Experience: 50},
|
|
{Username: "bob", TokenAmount: 200, Experience: 75, Assets: nil},
|
|
}
|
|
|
|
data, err := json.Marshal(rewards)
|
|
if err != nil {
|
|
t.Fatalf("marshal rewards failed: %v", err)
|
|
}
|
|
|
|
var parsed []PlayerReward
|
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
t.Fatalf("unmarshal rewards failed: %v", err)
|
|
}
|
|
|
|
if len(parsed) != 2 {
|
|
t.Fatalf("parsed reward count = %d, want 2", len(parsed))
|
|
}
|
|
if parsed[0].Username != "alice" || parsed[0].TokenAmount != 100 || parsed[0].Experience != 50 {
|
|
t.Errorf("parsed[0] = %+v, unexpected values", parsed[0])
|
|
}
|
|
}
|
|
|
|
func TestRewardResult_JSONRoundTrip(t *testing.T) {
|
|
results := []RewardResult{
|
|
{Username: "alice", Success: true},
|
|
{Username: "bob", Success: false, Error: "insufficient balance"},
|
|
}
|
|
|
|
data, err := json.Marshal(results)
|
|
if err != nil {
|
|
t.Fatalf("marshal failed: %v", err)
|
|
}
|
|
|
|
var parsed []RewardResult
|
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
t.Fatalf("unmarshal failed: %v", err)
|
|
}
|
|
|
|
if len(parsed) != 2 {
|
|
t.Fatalf("got %d results, want 2", len(parsed))
|
|
}
|
|
if !parsed[0].Success {
|
|
t.Error("parsed[0].Success should be true")
|
|
}
|
|
if parsed[1].Success {
|
|
t.Error("parsed[1].Success should be false")
|
|
}
|
|
if parsed[1].Error != "insufficient balance" {
|
|
t.Errorf("parsed[1].Error = %q, want %q", parsed[1].Error, "insufficient balance")
|
|
}
|
|
}
|
|
|
|
func TestEntryTokenData_JSONRoundTrip(t *testing.T) {
|
|
data := entryTokenData{
|
|
Username: "player1",
|
|
SessionName: "Dedi1_Room_01",
|
|
}
|
|
|
|
b, err := json.Marshal(data)
|
|
if err != nil {
|
|
t.Fatalf("marshal failed: %v", err)
|
|
}
|
|
|
|
var parsed entryTokenData
|
|
if err := json.Unmarshal(b, &parsed); err != nil {
|
|
t.Fatalf("unmarshal failed: %v", err)
|
|
}
|
|
|
|
if parsed.Username != data.Username {
|
|
t.Errorf("Username = %q, want %q", parsed.Username, data.Username)
|
|
}
|
|
if parsed.SessionName != data.SessionName {
|
|
t.Errorf("SessionName = %q, want %q", parsed.SessionName, data.SessionName)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests for Service constructor and callback setters
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestNewService_NilParams(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
if svc == nil {
|
|
t.Error("NewService should return non-nil service")
|
|
}
|
|
}
|
|
|
|
func TestSetRewardGranter(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
svc.SetRewardGranter(func(username string, tokenAmount uint64, assets []core.MintAssetPayload) (string, error) {
|
|
return "", nil
|
|
})
|
|
if svc.rewardGrant == nil {
|
|
t.Error("rewardGrant should be set after SetRewardGranter")
|
|
}
|
|
}
|
|
|
|
func TestSetExpGranter(t *testing.T) {
|
|
svc := NewService(nil, nil)
|
|
svc.SetExpGranter(func(username string, exp int) error {
|
|
return nil
|
|
})
|
|
if svc.expGrant == nil {
|
|
t.Error("expGrant should be set after SetExpGranter")
|
|
}
|
|
}
|
|
|
|
func TestStaleTimeout_Value(t *testing.T) {
|
|
if staleTimeout != 30*time.Second {
|
|
t.Errorf("staleTimeout = %v, want 30s", staleTimeout)
|
|
}
|
|
}
|
|
|
|
func TestEntryTokenTTL_Value(t *testing.T) {
|
|
if entryTokenTTL != 5*time.Minute {
|
|
t.Errorf("entryTokenTTL = %v, want 5m", entryTokenTTL)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests using mock repository for deeper logic testing
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// mockRepo implements the methods needed by testableService to test
|
|
// business logic without a real database.
|
|
type mockRepo struct {
|
|
rooms map[string]*BossRoom
|
|
activeCounts map[string]int64
|
|
nextID uint
|
|
}
|
|
|
|
func newMockRepo() *mockRepo {
|
|
return &mockRepo{
|
|
rooms: make(map[string]*BossRoom),
|
|
activeCounts: make(map[string]int64),
|
|
nextID: 1,
|
|
}
|
|
}
|
|
|
|
// testableService mirrors the validation and state-transition logic of Service
|
|
// but uses an in-memory mock repository instead of GORM + MySQL.
|
|
// This lets us test business rules without external dependencies.
|
|
type testableService struct {
|
|
repo *mockRepo
|
|
rewardGrant func(username string, tokenAmount uint64, assets []core.MintAssetPayload) (string, error)
|
|
}
|
|
|
|
func (s *testableService) requestEntry(usernames []string, bossID int) (*BossRoom, error) {
|
|
if len(usernames) == 0 {
|
|
return nil, fmt.Errorf("empty players")
|
|
}
|
|
if len(usernames) > 3 {
|
|
return nil, fmt.Errorf("too many players")
|
|
}
|
|
seen := make(map[string]bool, len(usernames))
|
|
for _, u := range usernames {
|
|
if seen[u] {
|
|
return nil, fmt.Errorf("duplicate: %s", u)
|
|
}
|
|
seen[u] = true
|
|
}
|
|
|
|
for _, u := range usernames {
|
|
if s.repo.activeCounts[u] > 0 {
|
|
return nil, fmt.Errorf("player %s already active", u)
|
|
}
|
|
}
|
|
|
|
playersJSON, _ := json.Marshal(usernames)
|
|
sessionName := fmt.Sprintf("test_session_%d", s.repo.nextID)
|
|
room := &BossRoom{
|
|
ID: s.repo.nextID,
|
|
SessionName: sessionName,
|
|
BossID: bossID,
|
|
Status: StatusWaiting,
|
|
MaxPlayers: defaultMaxPlayers,
|
|
Players: string(playersJSON),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
s.repo.nextID++
|
|
s.repo.rooms[sessionName] = room
|
|
return room, nil
|
|
}
|
|
|
|
func (s *testableService) completeRaid(sessionName string, rewards []PlayerReward) (*BossRoom, []RewardResult, error) {
|
|
room, ok := s.repo.rooms[sessionName]
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("room not found")
|
|
}
|
|
if room.Status != StatusInProgress {
|
|
return nil, nil, fmt.Errorf("wrong status: %s", room.Status)
|
|
}
|
|
|
|
var players []string
|
|
if err := json.Unmarshal([]byte(room.Players), &players); err != nil {
|
|
return nil, nil, fmt.Errorf("parse players: %w", err)
|
|
}
|
|
playerSet := make(map[string]bool, len(players))
|
|
for _, p := range players {
|
|
playerSet[p] = true
|
|
}
|
|
for _, r := range rewards {
|
|
if !playerSet[r.Username] {
|
|
return nil, nil, fmt.Errorf("%s is not a room member", r.Username)
|
|
}
|
|
}
|
|
|
|
now := time.Now()
|
|
room.Status = StatusCompleted
|
|
room.CompletedAt = &now
|
|
|
|
var results []RewardResult
|
|
if s.rewardGrant != nil {
|
|
for _, r := range rewards {
|
|
_, grantErr := s.rewardGrant(r.Username, r.TokenAmount, r.Assets)
|
|
res := RewardResult{Username: r.Username, Success: grantErr == nil}
|
|
if grantErr != nil {
|
|
res.Error = grantErr.Error()
|
|
}
|
|
results = append(results, res)
|
|
}
|
|
}
|
|
return room, results, nil
|
|
}
|
|
|
|
func (s *testableService) failRaid(sessionName string) (*BossRoom, error) {
|
|
room, ok := s.repo.rooms[sessionName]
|
|
if !ok {
|
|
return nil, fmt.Errorf("room not found")
|
|
}
|
|
if room.Status != StatusWaiting && room.Status != StatusInProgress {
|
|
return nil, fmt.Errorf("wrong status: %s", room.Status)
|
|
}
|
|
now := time.Now()
|
|
room.Status = StatusFailed
|
|
room.CompletedAt = &now
|
|
return room, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mock-based tests for business logic
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestMock_RequestEntry_Success(t *testing.T) {
|
|
svc := &testableService{repo: newMockRepo()}
|
|
|
|
room, err := svc.requestEntry([]string{"p1", "p2"}, 1)
|
|
if err != nil {
|
|
t.Fatalf("requestEntry failed: %v", err)
|
|
}
|
|
if room.Status != StatusWaiting {
|
|
t.Errorf("Status = %q, want %q", room.Status, StatusWaiting)
|
|
}
|
|
if room.BossID != 1 {
|
|
t.Errorf("BossID = %d, want 1", room.BossID)
|
|
}
|
|
if room.MaxPlayers != 3 {
|
|
t.Errorf("MaxPlayers = %d, want 3", room.MaxPlayers)
|
|
}
|
|
}
|
|
|
|
func TestMock_RequestEntry_PlayerAlreadyActive(t *testing.T) {
|
|
repo := newMockRepo()
|
|
repo.activeCounts["p1"] = 1
|
|
svc := &testableService{repo: repo}
|
|
|
|
_, err := svc.requestEntry([]string{"p1", "p2"}, 1)
|
|
if err == nil {
|
|
t.Error("expected error for already-active player")
|
|
}
|
|
}
|
|
|
|
func TestMock_CompleteRaid_Success(t *testing.T) {
|
|
svc := &testableService{repo: newMockRepo()}
|
|
room, _ := svc.requestEntry([]string{"p1", "p2"}, 1)
|
|
room.Status = StatusInProgress
|
|
|
|
completed, _, err := svc.completeRaid(room.SessionName, []PlayerReward{
|
|
{Username: "p1", TokenAmount: 100},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("completeRaid failed: %v", err)
|
|
}
|
|
if completed.Status != StatusCompleted {
|
|
t.Errorf("Status = %q, want %q", completed.Status, StatusCompleted)
|
|
}
|
|
}
|
|
|
|
func TestMock_CompleteRaid_WrongStatus(t *testing.T) {
|
|
svc := &testableService{repo: newMockRepo()}
|
|
room, _ := svc.requestEntry([]string{"p1"}, 1)
|
|
// still in "waiting" status
|
|
|
|
_, _, err := svc.completeRaid(room.SessionName, nil)
|
|
if err == nil {
|
|
t.Error("expected error for wrong status")
|
|
}
|
|
}
|
|
|
|
func TestMock_CompleteRaid_InvalidRecipient(t *testing.T) {
|
|
svc := &testableService{repo: newMockRepo()}
|
|
room, _ := svc.requestEntry([]string{"p1"}, 1)
|
|
room.Status = StatusInProgress
|
|
|
|
_, _, err := svc.completeRaid(room.SessionName, []PlayerReward{
|
|
{Username: "stranger", TokenAmount: 100},
|
|
})
|
|
if err == nil {
|
|
t.Error("expected error for non-member reward recipient")
|
|
}
|
|
}
|
|
|
|
func TestMock_CompleteRaid_WithRewardGranter(t *testing.T) {
|
|
grantCalls := 0
|
|
svc := &testableService{
|
|
repo: newMockRepo(),
|
|
rewardGrant: func(username string, tokenAmount uint64, assets []core.MintAssetPayload) (string, error) {
|
|
grantCalls++
|
|
return "", nil
|
|
},
|
|
}
|
|
room, _ := svc.requestEntry([]string{"p1"}, 1)
|
|
room.Status = StatusInProgress
|
|
|
|
_, results, err := svc.completeRaid(room.SessionName, []PlayerReward{
|
|
{Username: "p1", TokenAmount: 50},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("completeRaid failed: %v", err)
|
|
}
|
|
if grantCalls != 1 {
|
|
t.Errorf("grant calls = %d, want 1", grantCalls)
|
|
}
|
|
if len(results) != 1 || !results[0].Success {
|
|
t.Errorf("expected 1 successful result, got %+v", results)
|
|
}
|
|
}
|
|
|
|
func TestMock_CompleteRaid_RewardFailure(t *testing.T) {
|
|
svc := &testableService{
|
|
repo: newMockRepo(),
|
|
rewardGrant: func(username string, tokenAmount uint64, assets []core.MintAssetPayload) (string, error) {
|
|
return "", fmt.Errorf("chain error")
|
|
},
|
|
}
|
|
room, _ := svc.requestEntry([]string{"p1"}, 1)
|
|
room.Status = StatusInProgress
|
|
|
|
completed, results, err := svc.completeRaid(room.SessionName, []PlayerReward{
|
|
{Username: "p1", TokenAmount: 50},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("completeRaid should not fail when granter fails: %v", err)
|
|
}
|
|
if completed.Status != StatusCompleted {
|
|
t.Errorf("room should still be completed despite reward failure")
|
|
}
|
|
if len(results) != 1 || results[0].Success {
|
|
t.Error("expected failed reward result")
|
|
}
|
|
if results[0].Error == "" {
|
|
t.Error("expected error message in result")
|
|
}
|
|
}
|
|
|
|
func TestMock_FailRaid_FromWaiting(t *testing.T) {
|
|
svc := &testableService{repo: newMockRepo()}
|
|
room, _ := svc.requestEntry([]string{"p1"}, 1)
|
|
|
|
failed, err := svc.failRaid(room.SessionName)
|
|
if err != nil {
|
|
t.Fatalf("failRaid failed: %v", err)
|
|
}
|
|
if failed.Status != StatusFailed {
|
|
t.Errorf("Status = %q, want %q", failed.Status, StatusFailed)
|
|
}
|
|
}
|
|
|
|
func TestMock_FailRaid_FromInProgress(t *testing.T) {
|
|
svc := &testableService{repo: newMockRepo()}
|
|
room, _ := svc.requestEntry([]string{"p1"}, 1)
|
|
room.Status = StatusInProgress
|
|
|
|
failed, err := svc.failRaid(room.SessionName)
|
|
if err != nil {
|
|
t.Fatalf("failRaid failed: %v", err)
|
|
}
|
|
if failed.Status != StatusFailed {
|
|
t.Errorf("Status = %q, want %q", failed.Status, StatusFailed)
|
|
}
|
|
}
|
|
|
|
func TestMock_FailRaid_FromCompleted(t *testing.T) {
|
|
svc := &testableService{repo: newMockRepo()}
|
|
room, _ := svc.requestEntry([]string{"p1"}, 1)
|
|
room.Status = StatusCompleted
|
|
|
|
_, err := svc.failRaid(room.SessionName)
|
|
if err == nil {
|
|
t.Error("expected error failing completed raid")
|
|
}
|
|
}
|
|
|
|
func TestMock_FullLifecycle(t *testing.T) {
|
|
svc := &testableService{repo: newMockRepo()}
|
|
|
|
// Create room
|
|
room, err := svc.requestEntry([]string{"p1", "p2"}, 1)
|
|
if err != nil {
|
|
t.Fatalf("requestEntry: %v", err)
|
|
}
|
|
if room.Status != StatusWaiting {
|
|
t.Fatalf("expected waiting, got %s", room.Status)
|
|
}
|
|
|
|
// Start raid
|
|
room.Status = StatusInProgress
|
|
|
|
// Complete raid
|
|
completed, _, err := svc.completeRaid(room.SessionName, []PlayerReward{
|
|
{Username: "p1", TokenAmount: 10},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("completeRaid: %v", err)
|
|
}
|
|
if completed.Status != StatusCompleted {
|
|
t.Errorf("expected completed, got %s", completed.Status)
|
|
}
|
|
|
|
// Cannot fail a completed raid
|
|
_, err = svc.failRaid(room.SessionName)
|
|
if err == nil {
|
|
t.Error("expected error failing completed raid")
|
|
}
|
|
}
|