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") } }