diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 98341af35..248517aa8 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -29,6 +29,22 @@ jobs: name: Test runs-on: ubuntu-latest + services: + postgres: + image: postgres:15-alpine + env: + POSTGRES_USER: test + POSTGRES_PASSWORD: test + POSTGRES_DB: erupe_test + ports: + - 5433:5432 + options: >- + --health-cmd pg_isready + --health-interval 2s + --health-timeout 2s + --health-retries 10 + --mount type=tmpfs,destination=/var/lib/postgresql/data + steps: - uses: actions/checkout@v4 @@ -42,6 +58,12 @@ jobs: - name: Run Tests with Race Detector and Coverage run: go test -race -coverprofile=coverage.out ./... -timeout=10m + env: + TEST_DB_HOST: localhost + TEST_DB_PORT: 5433 + TEST_DB_USER: test + TEST_DB_PASSWORD: test + TEST_DB_NAME: erupe_test - name: Check coverage threshold run: | diff --git a/server/channelserver/repo_achievement_test.go b/server/channelserver/repo_achievement_test.go new file mode 100644 index 000000000..ae9a08cc8 --- /dev/null +++ b/server/channelserver/repo_achievement_test.go @@ -0,0 +1,133 @@ +package channelserver + +import ( + "testing" + + "github.com/jmoiron/sqlx" +) + +func setupAchievementRepo(t *testing.T) (*AchievementRepository, *sqlx.DB, uint32) { + t.Helper() + db := SetupTestDB(t) + userID := CreateTestUser(t, db, "ach_test_user") + charID := CreateTestCharacter(t, db, userID, "AchChar") + repo := NewAchievementRepository(db) + t.Cleanup(func() { TeardownTestDB(t, db) }) + return repo, db, charID +} + +func TestRepoAchievementEnsureExists(t *testing.T) { + repo, db, charID := setupAchievementRepo(t) + + if err := repo.EnsureExists(charID); err != nil { + t.Fatalf("EnsureExists failed: %v", err) + } + + var count int + if err := db.QueryRow("SELECT COUNT(*) FROM achievements WHERE id=$1", charID).Scan(&count); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if count != 1 { + t.Errorf("Expected 1 row, got: %d", count) + } +} + +func TestRepoAchievementEnsureExistsIdempotent(t *testing.T) { + repo, db, charID := setupAchievementRepo(t) + + if err := repo.EnsureExists(charID); err != nil { + t.Fatalf("First EnsureExists failed: %v", err) + } + if err := repo.EnsureExists(charID); err != nil { + t.Fatalf("Second EnsureExists failed: %v", err) + } + + var count int + if err := db.QueryRow("SELECT COUNT(*) FROM achievements WHERE id=$1", charID).Scan(&count); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if count != 1 { + t.Errorf("Expected 1 row after idempotent calls, got: %d", count) + } +} + +func TestRepoAchievementGetAllScores(t *testing.T) { + repo, db, charID := setupAchievementRepo(t) + + if err := repo.EnsureExists(charID); err != nil { + t.Fatalf("EnsureExists failed: %v", err) + } + + // Set some scores directly + if _, err := db.Exec("UPDATE achievements SET ach0=10, ach5=42, ach32=99 WHERE id=$1", charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + scores, err := repo.GetAllScores(charID) + if err != nil { + t.Fatalf("GetAllScores failed: %v", err) + } + if scores[0] != 10 { + t.Errorf("Expected ach0=10, got: %d", scores[0]) + } + if scores[5] != 42 { + t.Errorf("Expected ach5=42, got: %d", scores[5]) + } + if scores[32] != 99 { + t.Errorf("Expected ach32=99, got: %d", scores[32]) + } +} + +func TestRepoAchievementGetAllScoresDefault(t *testing.T) { + repo, _, charID := setupAchievementRepo(t) + + if err := repo.EnsureExists(charID); err != nil { + t.Fatalf("EnsureExists failed: %v", err) + } + + scores, err := repo.GetAllScores(charID) + if err != nil { + t.Fatalf("GetAllScores failed: %v", err) + } + for i, s := range scores { + if s != 0 { + t.Errorf("Expected ach%d=0 by default, got: %d", i, s) + } + } +} + +func TestRepoAchievementIncrementScore(t *testing.T) { + repo, db, charID := setupAchievementRepo(t) + + if err := repo.EnsureExists(charID); err != nil { + t.Fatalf("EnsureExists failed: %v", err) + } + + if err := repo.IncrementScore(charID, 5); err != nil { + t.Fatalf("First IncrementScore failed: %v", err) + } + if err := repo.IncrementScore(charID, 5); err != nil { + t.Fatalf("Second IncrementScore failed: %v", err) + } + + var val int32 + if err := db.QueryRow("SELECT ach5 FROM achievements WHERE id=$1", charID).Scan(&val); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if val != 2 { + t.Errorf("Expected ach5=2 after two increments, got: %d", val) + } +} + +func TestRepoAchievementIncrementScoreOutOfRange(t *testing.T) { + repo, _, charID := setupAchievementRepo(t) + + if err := repo.EnsureExists(charID); err != nil { + t.Fatalf("EnsureExists failed: %v", err) + } + + err := repo.IncrementScore(charID, 33) + if err == nil { + t.Fatal("Expected error for achievementID=33, got nil") + } +} diff --git a/server/channelserver/repo_cafe_test.go b/server/channelserver/repo_cafe_test.go new file mode 100644 index 000000000..e1c43e98a --- /dev/null +++ b/server/channelserver/repo_cafe_test.go @@ -0,0 +1,162 @@ +package channelserver + +import ( + "testing" + + "github.com/jmoiron/sqlx" +) + +func setupCafeRepo(t *testing.T) (*CafeRepository, *sqlx.DB, uint32) { + t.Helper() + db := SetupTestDB(t) + userID := CreateTestUser(t, db, "cafe_test_user") + charID := CreateTestCharacter(t, db, userID, "CafeChar") + repo := NewCafeRepository(db) + t.Cleanup(func() { TeardownTestDB(t, db) }) + return repo, db, charID +} + +func createCafeBonus(t *testing.T, db *sqlx.DB, id uint32, timeReq, itemType, itemID, quantity int) { + t.Helper() + if _, err := db.Exec( + "INSERT INTO cafebonus (id, time_req, item_type, item_id, quantity) VALUES ($1, $2, $3, $4, $5)", + id, timeReq, itemType, itemID, quantity, + ); err != nil { + t.Fatalf("Failed to create cafe bonus: %v", err) + } +} + +func TestRepoCafeGetBonusesEmpty(t *testing.T) { + repo, _, charID := setupCafeRepo(t) + + bonuses, err := repo.GetBonuses(charID) + if err != nil { + t.Fatalf("GetBonuses failed: %v", err) + } + if len(bonuses) != 0 { + t.Errorf("Expected 0 bonuses, got: %d", len(bonuses)) + } +} + +func TestRepoCafeGetBonuses(t *testing.T) { + repo, db, charID := setupCafeRepo(t) + + createCafeBonus(t, db, 1, 3600, 1, 100, 5) + createCafeBonus(t, db, 2, 7200, 2, 200, 10) + + bonuses, err := repo.GetBonuses(charID) + if err != nil { + t.Fatalf("GetBonuses failed: %v", err) + } + if len(bonuses) != 2 { + t.Fatalf("Expected 2 bonuses, got: %d", len(bonuses)) + } + if bonuses[0].Claimed { + t.Error("Expected first bonus unclaimed") + } +} + +func TestRepoCafeAcceptBonus(t *testing.T) { + repo, db, charID := setupCafeRepo(t) + + createCafeBonus(t, db, 1, 3600, 1, 100, 5) + + if err := repo.AcceptBonus(1, charID); err != nil { + t.Fatalf("AcceptBonus failed: %v", err) + } + + bonuses, err := repo.GetBonuses(charID) + if err != nil { + t.Fatalf("GetBonuses failed: %v", err) + } + if len(bonuses) != 1 { + t.Fatalf("Expected 1 bonus, got: %d", len(bonuses)) + } + if !bonuses[0].Claimed { + t.Error("Expected bonus to be claimed after AcceptBonus") + } +} + +func TestRepoCafeResetAccepted(t *testing.T) { + repo, db, charID := setupCafeRepo(t) + + createCafeBonus(t, db, 1, 3600, 1, 100, 5) + if err := repo.AcceptBonus(1, charID); err != nil { + t.Fatalf("AcceptBonus failed: %v", err) + } + + if err := repo.ResetAccepted(charID); err != nil { + t.Fatalf("ResetAccepted failed: %v", err) + } + + bonuses, err := repo.GetBonuses(charID) + if err != nil { + t.Fatalf("GetBonuses failed: %v", err) + } + if bonuses[0].Claimed { + t.Error("Expected bonus unclaimed after ResetAccepted") + } +} + +func TestRepoCafeGetBonusItem(t *testing.T) { + repo, db, _ := setupCafeRepo(t) + + createCafeBonus(t, db, 1, 3600, 7, 500, 3) + + itemType, quantity, err := repo.GetBonusItem(1) + if err != nil { + t.Fatalf("GetBonusItem failed: %v", err) + } + if itemType != 7 { + t.Errorf("Expected itemType=7, got: %d", itemType) + } + if quantity != 3 { + t.Errorf("Expected quantity=3, got: %d", quantity) + } +} + +func TestRepoCafeGetClaimable(t *testing.T) { + repo, db, charID := setupCafeRepo(t) + + // Set character's cafe_time to 1000 seconds + if _, err := db.Exec("UPDATE characters SET cafe_time=1000 WHERE id=$1", charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + // Bonus requiring 500 seconds total (1000 + 0 elapsed >= 500) - claimable + createCafeBonus(t, db, 1, 500, 1, 100, 1) + // Bonus requiring 5000 seconds (1000 + 100 elapsed < 5000) - not claimable + createCafeBonus(t, db, 2, 5000, 2, 200, 1) + + claimable, err := repo.GetClaimable(charID, 100) + if err != nil { + t.Fatalf("GetClaimable failed: %v", err) + } + if len(claimable) != 1 { + t.Fatalf("Expected 1 claimable bonus, got: %d", len(claimable)) + } + if claimable[0].ID != 1 { + t.Errorf("Expected claimable bonus ID=1, got: %d", claimable[0].ID) + } +} + +func TestRepoCafeGetClaimableExcludesAccepted(t *testing.T) { + repo, db, charID := setupCafeRepo(t) + + if _, err := db.Exec("UPDATE characters SET cafe_time=10000 WHERE id=$1", charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + createCafeBonus(t, db, 1, 100, 1, 100, 1) + if err := repo.AcceptBonus(1, charID); err != nil { + t.Fatalf("AcceptBonus failed: %v", err) + } + + claimable, err := repo.GetClaimable(charID, 0) + if err != nil { + t.Fatalf("GetClaimable failed: %v", err) + } + if len(claimable) != 0 { + t.Errorf("Expected 0 claimable after accept, got: %d", len(claimable)) + } +} diff --git a/server/channelserver/repo_distribution_test.go b/server/channelserver/repo_distribution_test.go new file mode 100644 index 000000000..72240436a --- /dev/null +++ b/server/channelserver/repo_distribution_test.go @@ -0,0 +1,146 @@ +package channelserver + +import ( + "testing" + + "github.com/jmoiron/sqlx" +) + +func setupDistributionRepo(t *testing.T) (*DistributionRepository, *sqlx.DB, uint32) { + t.Helper() + db := SetupTestDB(t) + userID := CreateTestUser(t, db, "dist_test_user") + charID := CreateTestCharacter(t, db, userID, "DistChar") + repo := NewDistributionRepository(db) + t.Cleanup(func() { TeardownTestDB(t, db) }) + return repo, db, charID +} + +func createDistribution(t *testing.T, db *sqlx.DB, charID *uint32, distType int, eventName, description string) uint32 { + t.Helper() + var id uint32 + err := db.QueryRow( + `INSERT INTO distribution (character_id, type, event_name, description, data, times_acceptable) + VALUES ($1, $2, $3, $4, $5, 1) RETURNING id`, + charID, distType, eventName, description, []byte{0x00}, + ).Scan(&id) + if err != nil { + t.Fatalf("Failed to create distribution: %v", err) + } + return id +} + +func TestRepoDistributionListEmpty(t *testing.T) { + repo, _, charID := setupDistributionRepo(t) + + dists, err := repo.List(charID, 1) + if err != nil { + t.Fatalf("List failed: %v", err) + } + if len(dists) != 0 { + t.Errorf("Expected 0 distributions, got: %d", len(dists)) + } +} + +func TestRepoDistributionListCharacterSpecific(t *testing.T) { + repo, db, charID := setupDistributionRepo(t) + + createDistribution(t, db, &charID, 1, "Personal Gift", "For you") + + dists, err := repo.List(charID, 1) + if err != nil { + t.Fatalf("List failed: %v", err) + } + if len(dists) != 1 { + t.Fatalf("Expected 1 distribution, got: %d", len(dists)) + } + if dists[0].EventName != "Personal Gift" { + t.Errorf("Expected event_name='Personal Gift', got: %q", dists[0].EventName) + } +} + +func TestRepoDistributionListGlobal(t *testing.T) { + repo, db, charID := setupDistributionRepo(t) + + // Global distribution (character_id=NULL) + createDistribution(t, db, nil, 1, "Global Gift", "For everyone") + + dists, err := repo.List(charID, 1) + if err != nil { + t.Fatalf("List failed: %v", err) + } + if len(dists) != 1 { + t.Fatalf("Expected 1 global distribution, got: %d", len(dists)) + } +} + +func TestRepoDistributionGetItems(t *testing.T) { + repo, db, charID := setupDistributionRepo(t) + + distID := createDistribution(t, db, &charID, 1, "Item Gift", "Has items") + if _, err := db.Exec("INSERT INTO distribution_items (distribution_id, item_type, item_id, quantity) VALUES ($1, 1, 100, 5)", distID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + if _, err := db.Exec("INSERT INTO distribution_items (distribution_id, item_type, item_id, quantity) VALUES ($1, 2, 200, 10)", distID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + items, err := repo.GetItems(distID) + if err != nil { + t.Fatalf("GetItems failed: %v", err) + } + if len(items) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(items)) + } +} + +func TestRepoDistributionRecordAccepted(t *testing.T) { + repo, db, charID := setupDistributionRepo(t) + + distID := createDistribution(t, db, &charID, 1, "Accept Test", "Test") + + if err := repo.RecordAccepted(distID, charID); err != nil { + t.Fatalf("RecordAccepted failed: %v", err) + } + + // Verify accepted count in list + dists, err := repo.List(charID, 1) + if err != nil { + t.Fatalf("List failed: %v", err) + } + if len(dists) != 1 { + t.Fatalf("Expected 1 distribution, got: %d", len(dists)) + } + if dists[0].TimesAccepted != 1 { + t.Errorf("Expected times_accepted=1, got: %d", dists[0].TimesAccepted) + } +} + +func TestRepoDistributionGetDescription(t *testing.T) { + repo, db, charID := setupDistributionRepo(t) + + distID := createDistribution(t, db, &charID, 1, "Desc Test", "~C05Special reward!") + + desc, err := repo.GetDescription(distID) + if err != nil { + t.Fatalf("GetDescription failed: %v", err) + } + if desc != "~C05Special reward!" { + t.Errorf("Expected description='~C05Special reward!', got: %q", desc) + } +} + +func TestRepoDistributionFiltersByType(t *testing.T) { + repo, db, charID := setupDistributionRepo(t) + + createDistribution(t, db, &charID, 1, "Type 1", "Type 1") + createDistribution(t, db, &charID, 2, "Type 2", "Type 2") + + dists, err := repo.List(charID, 1) + if err != nil { + t.Fatalf("List failed: %v", err) + } + if len(dists) != 1 { + t.Errorf("Expected 1 distribution of type 1, got: %d", len(dists)) + } +} diff --git a/server/channelserver/repo_diva_test.go b/server/channelserver/repo_diva_test.go new file mode 100644 index 000000000..bd6ab0d60 --- /dev/null +++ b/server/channelserver/repo_diva_test.go @@ -0,0 +1,113 @@ +package channelserver + +import ( + "testing" + + "github.com/jmoiron/sqlx" +) + +func setupDivaRepo(t *testing.T) (*DivaRepository, *sqlx.DB) { + t.Helper() + db := SetupTestDB(t) + repo := NewDivaRepository(db) + t.Cleanup(func() { TeardownTestDB(t, db) }) + return repo, db +} + +func TestRepoDivaInsertAndGetEvents(t *testing.T) { + repo, _ := setupDivaRepo(t) + + if err := repo.InsertEvent(1700000000); err != nil { + t.Fatalf("InsertEvent failed: %v", err) + } + + events, err := repo.GetEvents() + if err != nil { + t.Fatalf("GetEvents failed: %v", err) + } + if len(events) != 1 { + t.Fatalf("Expected 1 event, got: %d", len(events)) + } + if events[0].StartTime != 1700000000 { + t.Errorf("Expected start_time=1700000000, got: %d", events[0].StartTime) + } +} + +func TestRepoDivaGetEventsEmpty(t *testing.T) { + repo, _ := setupDivaRepo(t) + + events, err := repo.GetEvents() + if err != nil { + t.Fatalf("GetEvents failed: %v", err) + } + if len(events) != 0 { + t.Errorf("Expected 0 events, got: %d", len(events)) + } +} + +func TestRepoDivaDeleteEvents(t *testing.T) { + repo, _ := setupDivaRepo(t) + + if err := repo.InsertEvent(1700000000); err != nil { + t.Fatalf("InsertEvent failed: %v", err) + } + if err := repo.InsertEvent(1700100000); err != nil { + t.Fatalf("InsertEvent failed: %v", err) + } + + if err := repo.DeleteEvents(); err != nil { + t.Fatalf("DeleteEvents failed: %v", err) + } + + events, err := repo.GetEvents() + if err != nil { + t.Fatalf("GetEvents failed: %v", err) + } + if len(events) != 0 { + t.Errorf("Expected 0 events after delete, got: %d", len(events)) + } +} + +func TestRepoDivaMultipleEvents(t *testing.T) { + repo, _ := setupDivaRepo(t) + + if err := repo.InsertEvent(1700000000); err != nil { + t.Fatalf("InsertEvent 1 failed: %v", err) + } + if err := repo.InsertEvent(1700100000); err != nil { + t.Fatalf("InsertEvent 2 failed: %v", err) + } + + events, err := repo.GetEvents() + if err != nil { + t.Fatalf("GetEvents failed: %v", err) + } + if len(events) != 2 { + t.Errorf("Expected 2 events, got: %d", len(events)) + } +} + +func TestRepoDivaDeleteOnlyDivaEvents(t *testing.T) { + repo, db := setupDivaRepo(t) + + // Insert a diva event + if err := repo.InsertEvent(1700000000); err != nil { + t.Fatalf("InsertEvent failed: %v", err) + } + // Insert a festa event (should not be deleted) + if _, err := db.Exec("INSERT INTO events (event_type, start_time) VALUES ('festa', now())"); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + if err := repo.DeleteEvents(); err != nil { + t.Fatalf("DeleteEvents failed: %v", err) + } + + var count int + if err := db.QueryRow("SELECT COUNT(*) FROM events WHERE event_type='festa'").Scan(&count); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if count != 1 { + t.Errorf("Expected festa event to survive, got count=%d", count) + } +} diff --git a/server/channelserver/repo_festa_test.go b/server/channelserver/repo_festa_test.go new file mode 100644 index 000000000..0ef98bb77 --- /dev/null +++ b/server/channelserver/repo_festa_test.go @@ -0,0 +1,261 @@ +package channelserver + +import ( + "testing" + "time" + + "github.com/jmoiron/sqlx" +) + +func setupFestaRepo(t *testing.T) (*FestaRepository, *sqlx.DB, uint32, uint32) { + t.Helper() + db := SetupTestDB(t) + userID := CreateTestUser(t, db, "festa_test_user") + charID := CreateTestCharacter(t, db, userID, "FestaChar") + guildID := CreateTestGuild(t, db, charID, "FestaGuild") + repo := NewFestaRepository(db) + t.Cleanup(func() { TeardownTestDB(t, db) }) + return repo, db, charID, guildID +} + +func TestRepoFestaInsertAndGetEvents(t *testing.T) { + repo, _, _, _ := setupFestaRepo(t) + + startTime := uint32(time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC).Unix()) + if err := repo.InsertEvent(startTime); err != nil { + t.Fatalf("InsertEvent failed: %v", err) + } + + events, err := repo.GetFestaEvents() + if err != nil { + t.Fatalf("GetFestaEvents failed: %v", err) + } + if len(events) != 1 { + t.Fatalf("Expected 1 event, got: %d", len(events)) + } + if events[0].StartTime != startTime { + t.Errorf("Expected start_time=%d, got: %d", startTime, events[0].StartTime) + } +} + +func TestRepoFestaCleanupAll(t *testing.T) { + repo, _, _, _ := setupFestaRepo(t) + + if err := repo.InsertEvent(1000000); err != nil { + t.Fatalf("InsertEvent failed: %v", err) + } + + if err := repo.CleanupAll(); err != nil { + t.Fatalf("CleanupAll failed: %v", err) + } + + events, err := repo.GetFestaEvents() + if err != nil { + t.Fatalf("GetFestaEvents failed: %v", err) + } + if len(events) != 0 { + t.Errorf("Expected 0 events after cleanup, got: %d", len(events)) + } +} + +func TestRepoFestaRegisterGuild(t *testing.T) { + repo, db, _, guildID := setupFestaRepo(t) + + if err := repo.RegisterGuild(guildID, "blue"); err != nil { + t.Fatalf("RegisterGuild failed: %v", err) + } + + var team string + if err := db.QueryRow("SELECT team FROM festa_registrations WHERE guild_id=$1", guildID).Scan(&team); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if team != "blue" { + t.Errorf("Expected team='blue', got: %q", team) + } +} + +func TestRepoFestaGetTeamSouls(t *testing.T) { + repo, _, _, guildID := setupFestaRepo(t) + + if err := repo.RegisterGuild(guildID, "red"); err != nil { + t.Fatalf("RegisterGuild failed: %v", err) + } + + souls, err := repo.GetTeamSouls("red") + if err != nil { + t.Fatalf("GetTeamSouls failed: %v", err) + } + // No submissions yet, should be 0 + if souls != 0 { + t.Errorf("Expected souls=0, got: %d", souls) + } +} + +func TestRepoFestaSubmitSouls(t *testing.T) { + repo, _, charID, guildID := setupFestaRepo(t) + + if err := repo.RegisterGuild(guildID, "blue"); err != nil { + t.Fatalf("RegisterGuild failed: %v", err) + } + + souls := []uint16{10, 20, 30} + if err := repo.SubmitSouls(charID, guildID, souls); err != nil { + t.Fatalf("SubmitSouls failed: %v", err) + } + + charSouls, err := repo.GetCharSouls(charID) + if err != nil { + t.Fatalf("GetCharSouls failed: %v", err) + } + // 10 + 20 + 30 = 60 + if charSouls != 60 { + t.Errorf("Expected charSouls=60, got: %d", charSouls) + } +} + +func TestRepoFestaGetCharSoulsEmpty(t *testing.T) { + repo, _, charID, _ := setupFestaRepo(t) + + souls, err := repo.GetCharSouls(charID) + if err != nil { + t.Fatalf("GetCharSouls failed: %v", err) + } + if souls != 0 { + t.Errorf("Expected souls=0, got: %d", souls) + } +} + +func TestRepoFestaVoteTrial(t *testing.T) { + repo, db, charID, _ := setupFestaRepo(t) + + if err := repo.VoteTrial(charID, 42); err != nil { + t.Fatalf("VoteTrial failed: %v", err) + } + + var trialVote *uint32 + if err := db.QueryRow("SELECT trial_vote FROM guild_characters WHERE character_id=$1", charID).Scan(&trialVote); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if trialVote == nil || *trialVote != 42 { + t.Errorf("Expected trial_vote=42, got: %v", trialVote) + } +} + +func TestRepoFestaClaimPrize(t *testing.T) { + repo, db, charID, _ := setupFestaRepo(t) + + if err := repo.ClaimPrize(5, charID); err != nil { + t.Fatalf("ClaimPrize failed: %v", err) + } + + var count int + if err := db.QueryRow("SELECT COUNT(*) FROM festa_prizes_accepted WHERE prize_id=5 AND character_id=$1", charID).Scan(&count); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if count != 1 { + t.Errorf("Expected 1 accepted prize, got: %d", count) + } +} + +func TestRepoFestaHasClaimedMainPrize(t *testing.T) { + repo, _, charID, _ := setupFestaRepo(t) + + // Not claimed yet + if repo.HasClaimedMainPrize(charID) { + t.Error("Expected HasClaimedMainPrize=false before claiming") + } + + // Claim main prize (ID=0) + if err := repo.ClaimPrize(0, charID); err != nil { + t.Fatalf("ClaimPrize failed: %v", err) + } + + if !repo.HasClaimedMainPrize(charID) { + t.Error("Expected HasClaimedMainPrize=true after claiming") + } +} + +func TestRepoFestaListPrizes(t *testing.T) { + repo, db, charID, _ := setupFestaRepo(t) + + if _, err := db.Exec("INSERT INTO festa_prizes (id, type, tier, souls_req, item_id, num_item) VALUES (1, 'personal', 1, 100, 500, 1)"); err != nil { + t.Fatalf("Setup failed: %v", err) + } + if _, err := db.Exec("INSERT INTO festa_prizes (id, type, tier, souls_req, item_id, num_item) VALUES (2, 'personal', 2, 200, 600, 2)"); err != nil { + t.Fatalf("Setup failed: %v", err) + } + if _, err := db.Exec("INSERT INTO festa_prizes (id, type, tier, souls_req, item_id, num_item) VALUES (3, 'guild', 1, 300, 700, 3)"); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + prizes, err := repo.ListPrizes(charID, "personal") + if err != nil { + t.Fatalf("ListPrizes failed: %v", err) + } + if len(prizes) != 2 { + t.Fatalf("Expected 2 personal prizes, got: %d", len(prizes)) + } +} + +func TestRepoFestaListPrizesWithClaimed(t *testing.T) { + repo, db, charID, _ := setupFestaRepo(t) + + if _, err := db.Exec("INSERT INTO festa_prizes (id, type, tier, souls_req, item_id, num_item) VALUES (1, 'personal', 1, 100, 500, 1)"); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + if err := repo.ClaimPrize(1, charID); err != nil { + t.Fatalf("ClaimPrize failed: %v", err) + } + + prizes, err := repo.ListPrizes(charID, "personal") + if err != nil { + t.Fatalf("ListPrizes failed: %v", err) + } + if len(prizes) != 1 { + t.Fatalf("Expected 1 prize, got: %d", len(prizes)) + } + if prizes[0].Claimed != 1 { + t.Errorf("Expected claimed=1, got: %d", prizes[0].Claimed) + } +} + +func TestRepoFestaGetTeamSoulsWithSubmissions(t *testing.T) { + repo, db, charID, guildID := setupFestaRepo(t) + + if err := repo.RegisterGuild(guildID, "blue"); err != nil { + t.Fatalf("RegisterGuild failed: %v", err) + } + + // Create second guild on red team + user2 := CreateTestUser(t, db, "festa_user2") + char2 := CreateTestCharacter(t, db, user2, "FestaChar2") + guild2 := CreateTestGuild(t, db, char2, "RedGuild") + if err := repo.RegisterGuild(guild2, "red"); err != nil { + t.Fatalf("RegisterGuild failed: %v", err) + } + + // Submit souls + if err := repo.SubmitSouls(charID, guildID, []uint16{50}); err != nil { + t.Fatalf("SubmitSouls blue failed: %v", err) + } + if err := repo.SubmitSouls(char2, guild2, []uint16{30}); err != nil { + t.Fatalf("SubmitSouls red failed: %v", err) + } + + blueSouls, err := repo.GetTeamSouls("blue") + if err != nil { + t.Fatalf("GetTeamSouls(blue) failed: %v", err) + } + if blueSouls != 50 { + t.Errorf("Expected blue souls=50, got: %d", blueSouls) + } + + redSouls, err := repo.GetTeamSouls("red") + if err != nil { + t.Fatalf("GetTeamSouls(red) failed: %v", err) + } + if redSouls != 30 { + t.Errorf("Expected red souls=30, got: %d", redSouls) + } +} diff --git a/server/channelserver/repo_gacha_test.go b/server/channelserver/repo_gacha_test.go new file mode 100644 index 000000000..64f8c4a71 --- /dev/null +++ b/server/channelserver/repo_gacha_test.go @@ -0,0 +1,375 @@ +package channelserver + +import ( + "database/sql" + "errors" + "testing" + + "github.com/jmoiron/sqlx" +) + +func setupGachaRepo(t *testing.T) (*GachaRepository, *sqlx.DB, uint32) { + t.Helper() + db := SetupTestDB(t) + userID := CreateTestUser(t, db, "gacha_test_user") + charID := CreateTestCharacter(t, db, userID, "GachaChar") + repo := NewGachaRepository(db) + t.Cleanup(func() { TeardownTestDB(t, db) }) + return repo, db, charID +} + +func TestRepoGachaListShopEmpty(t *testing.T) { + repo, _, _ := setupGachaRepo(t) + + shops, err := repo.ListShop() + if err != nil { + t.Fatalf("ListShop failed: %v", err) + } + if len(shops) != 0 { + t.Errorf("Expected empty shop list, got: %d", len(shops)) + } +} + +func TestRepoGachaListShop(t *testing.T) { + repo, db, _ := setupGachaRepo(t) + + CreateTestGachaShop(t, db, "Test Gacha", 1) + CreateTestGachaShop(t, db, "Premium Gacha", 2) + + shops, err := repo.ListShop() + if err != nil { + t.Fatalf("ListShop failed: %v", err) + } + if len(shops) != 2 { + t.Fatalf("Expected 2 shops, got: %d", len(shops)) + } +} + +func TestRepoGachaGetShopType(t *testing.T) { + repo, db, _ := setupGachaRepo(t) + + shopID := CreateTestGachaShop(t, db, "Type Test", 3) + + gachaType, err := repo.GetShopType(shopID) + if err != nil { + t.Fatalf("GetShopType failed: %v", err) + } + if gachaType != 3 { + t.Errorf("Expected gacha_type=3, got: %d", gachaType) + } +} + +func TestRepoGachaGetEntryForTransaction(t *testing.T) { + repo, db, _ := setupGachaRepo(t) + + shopID := CreateTestGachaShop(t, db, "Entry Test", 1) + _, err := db.Exec( + `INSERT INTO gacha_entries (gacha_id, entry_type, weight, rarity, item_type, item_number, item_quantity, rolls, frontier_points, daily_limit) + VALUES ($1, 5, 100, 1, 7, 500, 10, 3, 0, 0)`, shopID, + ) + if err != nil { + t.Fatalf("Setup failed: %v", err) + } + + itemType, itemNumber, rolls, err := repo.GetEntryForTransaction(shopID, 5) + if err != nil { + t.Fatalf("GetEntryForTransaction failed: %v", err) + } + if itemType != 7 { + t.Errorf("Expected itemType=7, got: %d", itemType) + } + if itemNumber != 500 { + t.Errorf("Expected itemNumber=500, got: %d", itemNumber) + } + if rolls != 3 { + t.Errorf("Expected rolls=3, got: %d", rolls) + } +} + +func TestRepoGachaGetRewardPoolEmpty(t *testing.T) { + repo, db, _ := setupGachaRepo(t) + + shopID := CreateTestGachaShop(t, db, "Empty Pool", 1) + + entries, err := repo.GetRewardPool(shopID) + if err != nil { + t.Fatalf("GetRewardPool failed: %v", err) + } + if len(entries) != 0 { + t.Errorf("Expected empty reward pool, got: %d", len(entries)) + } +} + +func TestRepoGachaGetRewardPoolOrdering(t *testing.T) { + repo, db, _ := setupGachaRepo(t) + + shopID := CreateTestGachaShop(t, db, "Pool Test", 1) + // entry_type=100 is the reward pool + CreateTestGachaEntry(t, db, shopID, 100, 50) + CreateTestGachaEntry(t, db, shopID, 100, 200) + CreateTestGachaEntry(t, db, shopID, 100, 100) + // entry_type=5 should NOT appear in reward pool + CreateTestGachaEntry(t, db, shopID, 5, 999) + + entries, err := repo.GetRewardPool(shopID) + if err != nil { + t.Fatalf("GetRewardPool failed: %v", err) + } + if len(entries) != 3 { + t.Fatalf("Expected 3 reward entries, got: %d", len(entries)) + } + // Should be ordered by weight DESC + if entries[0].Weight < entries[1].Weight || entries[1].Weight < entries[2].Weight { + t.Errorf("Expected descending weight order, got: %v, %v, %v", entries[0].Weight, entries[1].Weight, entries[2].Weight) + } +} + +func TestRepoGachaGetItemsForEntry(t *testing.T) { + repo, db, _ := setupGachaRepo(t) + + shopID := CreateTestGachaShop(t, db, "Items Test", 1) + entryID := CreateTestGachaEntry(t, db, shopID, 100, 100) + CreateTestGachaItem(t, db, entryID, 1, 100, 5) + CreateTestGachaItem(t, db, entryID, 2, 200, 10) + + items, err := repo.GetItemsForEntry(entryID) + if err != nil { + t.Fatalf("GetItemsForEntry failed: %v", err) + } + if len(items) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(items)) + } +} + +func TestRepoGachaGetGuaranteedItems(t *testing.T) { + repo, db, _ := setupGachaRepo(t) + + shopID := CreateTestGachaShop(t, db, "Guaranteed Test", 1) + entryID := CreateTestGachaEntry(t, db, shopID, 10, 0) + CreateTestGachaItem(t, db, entryID, 3, 300, 1) + + items, err := repo.GetGuaranteedItems(10, shopID) + if err != nil { + t.Fatalf("GetGuaranteedItems failed: %v", err) + } + if len(items) != 1 { + t.Fatalf("Expected 1 guaranteed item, got: %d", len(items)) + } + if items[0].ItemID != 300 { + t.Errorf("Expected item_id=300, got: %d", items[0].ItemID) + } +} + +func TestRepoGachaGetAllEntries(t *testing.T) { + repo, db, _ := setupGachaRepo(t) + + shopID := CreateTestGachaShop(t, db, "All Entries", 1) + CreateTestGachaEntry(t, db, shopID, 100, 50) + CreateTestGachaEntry(t, db, shopID, 5, 200) + + entries, err := repo.GetAllEntries(shopID) + if err != nil { + t.Fatalf("GetAllEntries failed: %v", err) + } + if len(entries) != 2 { + t.Fatalf("Expected 2 entries, got: %d", len(entries)) + } +} + +func TestRepoGachaGetWeightDivisorZero(t *testing.T) { + repo, db, _ := setupGachaRepo(t) + + shopID := CreateTestGachaShop(t, db, "Zero Weight", 1) + + divisor, err := repo.GetWeightDivisor(shopID) + if err != nil { + t.Fatalf("GetWeightDivisor failed: %v", err) + } + if divisor != 0 { + t.Errorf("Expected divisor=0 for empty, got: %f", divisor) + } +} + +func TestRepoGachaGetWeightDivisor(t *testing.T) { + repo, db, _ := setupGachaRepo(t) + + shopID := CreateTestGachaShop(t, db, "Weight Test", 1) + CreateTestGachaEntry(t, db, shopID, 100, 50000) + CreateTestGachaEntry(t, db, shopID, 100, 50000) + + divisor, err := repo.GetWeightDivisor(shopID) + if err != nil { + t.Fatalf("GetWeightDivisor failed: %v", err) + } + // (50000 + 50000) / 100000 = 1.0 + if divisor != 1.0 { + t.Errorf("Expected divisor=1.0, got: %f", divisor) + } +} + +func TestRepoGachaHasEntryTypeTrue(t *testing.T) { + repo, db, _ := setupGachaRepo(t) + + shopID := CreateTestGachaShop(t, db, "HasType Test", 1) + CreateTestGachaEntry(t, db, shopID, 100, 50) + + has, err := repo.HasEntryType(shopID, 100) + if err != nil { + t.Fatalf("HasEntryType failed: %v", err) + } + if !has { + t.Error("Expected HasEntryType=true for entry_type=100") + } +} + +func TestRepoGachaHasEntryTypeFalse(t *testing.T) { + repo, db, _ := setupGachaRepo(t) + + shopID := CreateTestGachaShop(t, db, "HasType False", 1) + + has, err := repo.HasEntryType(shopID, 100) + if err != nil { + t.Fatalf("HasEntryType failed: %v", err) + } + if has { + t.Error("Expected HasEntryType=false for empty gacha") + } +} + +// Stepup tests + +func TestRepoGachaStepupLifecycle(t *testing.T) { + repo, db, charID := setupGachaRepo(t) + + shopID := CreateTestGachaShop(t, db, "Stepup Test", 1) + + // Insert stepup + if err := repo.InsertStepup(shopID, 1, charID); err != nil { + t.Fatalf("InsertStepup failed: %v", err) + } + + // Get step + step, err := repo.GetStepupStep(shopID, charID) + if err != nil { + t.Fatalf("GetStepupStep failed: %v", err) + } + if step != 1 { + t.Errorf("Expected step=1, got: %d", step) + } + + // Delete stepup + if err := repo.DeleteStepup(shopID, charID); err != nil { + t.Fatalf("DeleteStepup failed: %v", err) + } + + // Get step should fail + _, err = repo.GetStepupStep(shopID, charID) + if err == nil { + t.Fatal("Expected error after DeleteStepup, got nil") + } +} + +func TestRepoGachaGetStepupWithTime(t *testing.T) { + repo, db, charID := setupGachaRepo(t) + + shopID := CreateTestGachaShop(t, db, "Stepup Time", 1) + + if err := repo.InsertStepup(shopID, 2, charID); err != nil { + t.Fatalf("InsertStepup failed: %v", err) + } + + step, createdAt, err := repo.GetStepupWithTime(shopID, charID) + if err != nil { + t.Fatalf("GetStepupWithTime failed: %v", err) + } + if step != 2 { + t.Errorf("Expected step=2, got: %d", step) + } + if createdAt.IsZero() { + t.Error("Expected non-zero created_at") + } +} + +func TestRepoGachaGetStepupWithTimeNotFound(t *testing.T) { + repo, db, charID := setupGachaRepo(t) + + shopID := CreateTestGachaShop(t, db, "Stepup NF", 1) + + _, _, err := repo.GetStepupWithTime(shopID, charID) + if !errors.Is(err, sql.ErrNoRows) { + t.Fatalf("Expected sql.ErrNoRows, got: %v", err) + } +} + +// Box gacha tests + +func TestRepoGachaBoxLifecycle(t *testing.T) { + repo, db, charID := setupGachaRepo(t) + + shopID := CreateTestGachaShop(t, db, "Box Test", 1) + entryID1 := CreateTestGachaEntry(t, db, shopID, 100, 50) + entryID2 := CreateTestGachaEntry(t, db, shopID, 100, 100) + + // Initially empty + ids, err := repo.GetBoxEntryIDs(shopID, charID) + if err != nil { + t.Fatalf("GetBoxEntryIDs failed: %v", err) + } + if len(ids) != 0 { + t.Errorf("Expected empty box, got: %d entries", len(ids)) + } + + // Insert drawn entries + if err := repo.InsertBoxEntry(shopID, entryID1, charID); err != nil { + t.Fatalf("InsertBoxEntry failed: %v", err) + } + if err := repo.InsertBoxEntry(shopID, entryID2, charID); err != nil { + t.Fatalf("InsertBoxEntry failed: %v", err) + } + + ids, err = repo.GetBoxEntryIDs(shopID, charID) + if err != nil { + t.Fatalf("GetBoxEntryIDs failed: %v", err) + } + if len(ids) != 2 { + t.Errorf("Expected 2 box entries, got: %d", len(ids)) + } + + // Delete all box entries (reset) + if err := repo.DeleteBoxEntries(shopID, charID); err != nil { + t.Fatalf("DeleteBoxEntries failed: %v", err) + } + + ids, err = repo.GetBoxEntryIDs(shopID, charID) + if err != nil { + t.Fatalf("GetBoxEntryIDs after delete failed: %v", err) + } + if len(ids) != 0 { + t.Errorf("Expected empty box after delete, got: %d", len(ids)) + } +} + +func TestRepoGachaBoxIsolation(t *testing.T) { + repo, db, charID := setupGachaRepo(t) + + shopID := CreateTestGachaShop(t, db, "Box Iso", 1) + entryID := CreateTestGachaEntry(t, db, shopID, 100, 50) + + // Create another character + userID2 := CreateTestUser(t, db, "gacha_other_user") + charID2 := CreateTestCharacter(t, db, userID2, "GachaChar2") + + // Char1 draws + if err := repo.InsertBoxEntry(shopID, entryID, charID); err != nil { + t.Fatalf("InsertBoxEntry failed: %v", err) + } + + // Char2 should have empty box + ids, err := repo.GetBoxEntryIDs(shopID, charID2) + if err != nil { + t.Fatalf("GetBoxEntryIDs for char2 failed: %v", err) + } + if len(ids) != 0 { + t.Errorf("Expected empty box for char2, got: %d entries", len(ids)) + } +} diff --git a/server/channelserver/repo_goocoo_test.go b/server/channelserver/repo_goocoo_test.go new file mode 100644 index 000000000..0b390402d --- /dev/null +++ b/server/channelserver/repo_goocoo_test.go @@ -0,0 +1,152 @@ +package channelserver + +import ( + "testing" + + "github.com/jmoiron/sqlx" +) + +func setupGoocooRepo(t *testing.T) (*GoocooRepository, *sqlx.DB, uint32) { + t.Helper() + db := SetupTestDB(t) + userID := CreateTestUser(t, db, "goocoo_test_user") + charID := CreateTestCharacter(t, db, userID, "GoocooChar") + repo := NewGoocooRepository(db) + t.Cleanup(func() { TeardownTestDB(t, db) }) + return repo, db, charID +} + +func TestRepoGoocooEnsureExists(t *testing.T) { + repo, db, charID := setupGoocooRepo(t) + + if err := repo.EnsureExists(charID); err != nil { + t.Fatalf("EnsureExists failed: %v", err) + } + + var count int + if err := db.QueryRow("SELECT COUNT(*) FROM goocoo WHERE id=$1", charID).Scan(&count); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if count != 1 { + t.Errorf("Expected 1 goocoo row, got: %d", count) + } +} + +func TestRepoGoocooEnsureExistsIdempotent(t *testing.T) { + repo, _, charID := setupGoocooRepo(t) + + if err := repo.EnsureExists(charID); err != nil { + t.Fatalf("First EnsureExists failed: %v", err) + } + if err := repo.EnsureExists(charID); err != nil { + t.Fatalf("Second EnsureExists failed: %v", err) + } +} + +func TestRepoGoocooSaveAndGetSlot(t *testing.T) { + repo, _, charID := setupGoocooRepo(t) + + if err := repo.EnsureExists(charID); err != nil { + t.Fatalf("EnsureExists failed: %v", err) + } + + data := []byte{0xAA, 0xBB, 0xCC} + if err := repo.SaveSlot(charID, 0, data); err != nil { + t.Fatalf("SaveSlot failed: %v", err) + } + + got, err := repo.GetSlot(charID, 0) + if err != nil { + t.Fatalf("GetSlot failed: %v", err) + } + if len(got) != 3 || got[0] != 0xAA { + t.Errorf("Expected saved data, got: %x", got) + } +} + +func TestRepoGoocooGetSlotNull(t *testing.T) { + repo, _, charID := setupGoocooRepo(t) + + if err := repo.EnsureExists(charID); err != nil { + t.Fatalf("EnsureExists failed: %v", err) + } + + got, err := repo.GetSlot(charID, 0) + if err != nil { + t.Fatalf("GetSlot failed: %v", err) + } + if got != nil { + t.Errorf("Expected nil for NULL slot, got: %x", got) + } +} + +func TestRepoGoocooSaveMultipleSlots(t *testing.T) { + repo, _, charID := setupGoocooRepo(t) + + if err := repo.EnsureExists(charID); err != nil { + t.Fatalf("EnsureExists failed: %v", err) + } + + if err := repo.SaveSlot(charID, 0, []byte{0x01}); err != nil { + t.Fatalf("SaveSlot(0) failed: %v", err) + } + if err := repo.SaveSlot(charID, 3, []byte{0x04}); err != nil { + t.Fatalf("SaveSlot(3) failed: %v", err) + } + + got0, _ := repo.GetSlot(charID, 0) + got3, _ := repo.GetSlot(charID, 3) + if len(got0) != 1 || got0[0] != 0x01 { + t.Errorf("Slot 0 unexpected: %x", got0) + } + if len(got3) != 1 || got3[0] != 0x04 { + t.Errorf("Slot 3 unexpected: %x", got3) + } +} + +func TestRepoGoococClearSlot(t *testing.T) { + repo, _, charID := setupGoocooRepo(t) + + if err := repo.EnsureExists(charID); err != nil { + t.Fatalf("EnsureExists failed: %v", err) + } + + if err := repo.SaveSlot(charID, 2, []byte{0xFF}); err != nil { + t.Fatalf("SaveSlot failed: %v", err) + } + + if err := repo.ClearSlot(charID, 2); err != nil { + t.Fatalf("ClearSlot failed: %v", err) + } + + got, err := repo.GetSlot(charID, 2) + if err != nil { + t.Fatalf("GetSlot failed: %v", err) + } + if got != nil { + t.Errorf("Expected nil after ClearSlot, got: %x", got) + } +} + +func TestRepoGoocooInvalidSlot(t *testing.T) { + repo, _, charID := setupGoocooRepo(t) + + if err := repo.EnsureExists(charID); err != nil { + t.Fatalf("EnsureExists failed: %v", err) + } + + _, err := repo.GetSlot(charID, 5) + if err == nil { + t.Fatal("Expected error for invalid slot index 5") + } + + err = repo.SaveSlot(charID, 5, []byte{0x00}) + if err == nil { + t.Fatal("Expected error for SaveSlot with invalid slot index 5") + } + + err = repo.ClearSlot(charID, 5) + if err == nil { + t.Fatal("Expected error for ClearSlot with invalid slot index 5") + } +} diff --git a/server/channelserver/repo_house_test.go b/server/channelserver/repo_house_test.go new file mode 100644 index 000000000..2bd841bd7 --- /dev/null +++ b/server/channelserver/repo_house_test.go @@ -0,0 +1,377 @@ +package channelserver + +import ( + "testing" + + "github.com/jmoiron/sqlx" +) + +func setupHouseRepo(t *testing.T) (*HouseRepository, *sqlx.DB, uint32) { + t.Helper() + db := SetupTestDB(t) + userID := CreateTestUser(t, db, "house_test_user") + charID := CreateTestCharacter(t, db, userID, "HouseChar") + CreateTestUserBinary(t, db, charID) + repo := NewHouseRepository(db) + t.Cleanup(func() { TeardownTestDB(t, db) }) + return repo, db, charID +} + +func TestRepoHouseGetHouseByCharID(t *testing.T) { + repo, _, charID := setupHouseRepo(t) + + house, err := repo.GetHouseByCharID(charID) + if err != nil { + t.Fatalf("GetHouseByCharID failed: %v", err) + } + if house.CharID != charID { + t.Errorf("Expected charID=%d, got: %d", charID, house.CharID) + } + if house.Name != "HouseChar" { + t.Errorf("Expected name='HouseChar', got: %q", house.Name) + } + // Default house_state is 2 (password-protected) via COALESCE + if house.HouseState != 2 { + t.Errorf("Expected default house_state=2, got: %d", house.HouseState) + } +} + +func TestRepoHouseSearchHousesByName(t *testing.T) { + repo, db, _ := setupHouseRepo(t) + + user2 := CreateTestUser(t, db, "house_user2") + charID2 := CreateTestCharacter(t, db, user2, "HouseAlpha") + CreateTestUserBinary(t, db, charID2) + user3 := CreateTestUser(t, db, "house_user3") + charID3 := CreateTestCharacter(t, db, user3, "BetaHouse") + CreateTestUserBinary(t, db, charID3) + + houses, err := repo.SearchHousesByName("House") + if err != nil { + t.Fatalf("SearchHousesByName failed: %v", err) + } + if len(houses) < 2 { + t.Errorf("Expected at least 2 matches for 'House', got: %d", len(houses)) + } +} + +func TestRepoHouseSearchHousesByNameNoMatch(t *testing.T) { + repo, _, _ := setupHouseRepo(t) + + houses, err := repo.SearchHousesByName("ZZZnonexistent") + if err != nil { + t.Fatalf("SearchHousesByName failed: %v", err) + } + if len(houses) != 0 { + t.Errorf("Expected 0 matches, got: %d", len(houses)) + } +} + +func TestRepoHouseUpdateHouseState(t *testing.T) { + repo, _, charID := setupHouseRepo(t) + + if err := repo.UpdateHouseState(charID, 1, "secret"); err != nil { + t.Fatalf("UpdateHouseState failed: %v", err) + } + + state, password, err := repo.GetHouseAccess(charID) + if err != nil { + t.Fatalf("GetHouseAccess failed: %v", err) + } + if state != 1 { + t.Errorf("Expected state=1, got: %d", state) + } + if password != "secret" { + t.Errorf("Expected password='secret', got: %q", password) + } +} + +func TestRepoHouseGetHouseAccessDefault(t *testing.T) { + repo, _, charID := setupHouseRepo(t) + + state, password, err := repo.GetHouseAccess(charID) + if err != nil { + t.Fatalf("GetHouseAccess failed: %v", err) + } + if state != 2 { + t.Errorf("Expected default state=2, got: %d", state) + } + if password != "" { + t.Errorf("Expected empty password, got: %q", password) + } +} + +func TestRepoHouseUpdateInterior(t *testing.T) { + repo, db, charID := setupHouseRepo(t) + + furniture := []byte{0x01, 0x02, 0x03} + if err := repo.UpdateInterior(charID, furniture); err != nil { + t.Fatalf("UpdateInterior failed: %v", err) + } + + var got []byte + if err := db.QueryRow("SELECT house_furniture FROM user_binary WHERE id=$1", charID).Scan(&got); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if len(got) != 3 || got[0] != 0x01 { + t.Errorf("Expected furniture data, got: %x", got) + } +} + +func TestRepoHouseGetHouseContents(t *testing.T) { + repo, db, charID := setupHouseRepo(t) + + tier := []byte{0x01} + data := []byte{0x02} + furniture := []byte{0x03} + bookshelf := []byte{0x04} + gallery := []byte{0x05} + tore := []byte{0x06} + garden := []byte{0x07} + if _, err := db.Exec( + "UPDATE user_binary SET house_tier=$1, house_data=$2, house_furniture=$3, bookshelf=$4, gallery=$5, tore=$6, garden=$7 WHERE id=$8", + tier, data, furniture, bookshelf, gallery, tore, garden, charID, + ); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + gotTier, gotData, gotFurniture, gotBookshelf, gotGallery, gotTore, gotGarden, err := repo.GetHouseContents(charID) + if err != nil { + t.Fatalf("GetHouseContents failed: %v", err) + } + if len(gotTier) != 1 || gotTier[0] != 0x01 { + t.Errorf("Unexpected tier: %x", gotTier) + } + if len(gotData) != 1 || gotData[0] != 0x02 { + t.Errorf("Unexpected data: %x", gotData) + } + if len(gotFurniture) != 1 || gotFurniture[0] != 0x03 { + t.Errorf("Unexpected furniture: %x", gotFurniture) + } + if len(gotBookshelf) != 1 || gotBookshelf[0] != 0x04 { + t.Errorf("Unexpected bookshelf: %x", gotBookshelf) + } + if len(gotGallery) != 1 || gotGallery[0] != 0x05 { + t.Errorf("Unexpected gallery: %x", gotGallery) + } + if len(gotTore) != 1 || gotTore[0] != 0x06 { + t.Errorf("Unexpected tore: %x", gotTore) + } + if len(gotGarden) != 1 || gotGarden[0] != 0x07 { + t.Errorf("Unexpected garden: %x", gotGarden) + } +} + +func TestRepoHouseGetMission(t *testing.T) { + repo, db, charID := setupHouseRepo(t) + + mission := []byte{0xAA, 0xBB} + if _, err := db.Exec("UPDATE user_binary SET mission=$1 WHERE id=$2", mission, charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + got, err := repo.GetMission(charID) + if err != nil { + t.Fatalf("GetMission failed: %v", err) + } + if len(got) != 2 || got[0] != 0xAA { + t.Errorf("Expected mission data, got: %x", got) + } +} + +func TestRepoHouseUpdateMission(t *testing.T) { + repo, db, charID := setupHouseRepo(t) + + mission := []byte{0xCC, 0xDD, 0xEE} + if err := repo.UpdateMission(charID, mission); err != nil { + t.Fatalf("UpdateMission failed: %v", err) + } + + var got []byte + if err := db.QueryRow("SELECT mission FROM user_binary WHERE id=$1", charID).Scan(&got); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if len(got) != 3 || got[0] != 0xCC { + t.Errorf("Expected mission data, got: %x", got) + } +} + +func TestRepoHouseInitializeWarehouse(t *testing.T) { + repo, db, charID := setupHouseRepo(t) + + if err := repo.InitializeWarehouse(charID); err != nil { + t.Fatalf("InitializeWarehouse failed: %v", err) + } + + var count int + if err := db.QueryRow("SELECT COUNT(*) FROM warehouse WHERE character_id=$1", charID).Scan(&count); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if count != 1 { + t.Errorf("Expected 1 warehouse row, got: %d", count) + } + + // Calling again should be idempotent + if err := repo.InitializeWarehouse(charID); err != nil { + t.Fatalf("Second InitializeWarehouse failed: %v", err) + } + if err := db.QueryRow("SELECT COUNT(*) FROM warehouse WHERE character_id=$1", charID).Scan(&count); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if count != 1 { + t.Errorf("Expected still 1 warehouse row after idempotent call, got: %d", count) + } +} + +func TestRepoHouseGetWarehouseNames(t *testing.T) { + repo, db, charID := setupHouseRepo(t) + + if err := repo.InitializeWarehouse(charID); err != nil { + t.Fatalf("InitializeWarehouse failed: %v", err) + } + if _, err := db.Exec("UPDATE warehouse SET item0name='Items Box 0', equip3name='Equip Box 3' WHERE character_id=$1", charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + itemNames, equipNames, err := repo.GetWarehouseNames(charID) + if err != nil { + t.Fatalf("GetWarehouseNames failed: %v", err) + } + if itemNames[0] != "Items Box 0" { + t.Errorf("Expected item0name='Items Box 0', got: %q", itemNames[0]) + } + if equipNames[3] != "Equip Box 3" { + t.Errorf("Expected equip3name='Equip Box 3', got: %q", equipNames[3]) + } + // Other names should be empty (COALESCE) + if itemNames[1] != "" { + t.Errorf("Expected empty item1name, got: %q", itemNames[1]) + } +} + +func TestRepoHouseRenameWarehouseBox(t *testing.T) { + repo, db, charID := setupHouseRepo(t) + + if err := repo.InitializeWarehouse(charID); err != nil { + t.Fatalf("InitializeWarehouse failed: %v", err) + } + + if err := repo.RenameWarehouseBox(charID, 0, 5, "My Items"); err != nil { + t.Fatalf("RenameWarehouseBox(item) failed: %v", err) + } + if err := repo.RenameWarehouseBox(charID, 1, 2, "My Equips"); err != nil { + t.Fatalf("RenameWarehouseBox(equip) failed: %v", err) + } + + var item5name, equip2name string + if err := db.QueryRow("SELECT COALESCE(item5name,''), COALESCE(equip2name,'') FROM warehouse WHERE character_id=$1", charID).Scan(&item5name, &equip2name); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if item5name != "My Items" { + t.Errorf("Expected item5name='My Items', got: %q", item5name) + } + if equip2name != "My Equips" { + t.Errorf("Expected equip2name='My Equips', got: %q", equip2name) + } +} + +func TestRepoHouseRenameWarehouseBoxInvalidType(t *testing.T) { + repo, _, charID := setupHouseRepo(t) + + err := repo.RenameWarehouseBox(charID, 5, 0, "Bad") + if err == nil { + t.Fatal("Expected error for invalid box type, got nil") + } +} + +func TestRepoHouseWarehouseItemData(t *testing.T) { + repo, _, charID := setupHouseRepo(t) + + if err := repo.InitializeWarehouse(charID); err != nil { + t.Fatalf("InitializeWarehouse failed: %v", err) + } + + data := []byte{0x01, 0x02, 0x03} + if err := repo.SetWarehouseItemData(charID, 3, data); err != nil { + t.Fatalf("SetWarehouseItemData failed: %v", err) + } + + got, err := repo.GetWarehouseItemData(charID, 3) + if err != nil { + t.Fatalf("GetWarehouseItemData failed: %v", err) + } + if len(got) != 3 || got[0] != 0x01 { + t.Errorf("Expected item data, got: %x", got) + } +} + +func TestRepoHouseWarehouseEquipData(t *testing.T) { + repo, _, charID := setupHouseRepo(t) + + if err := repo.InitializeWarehouse(charID); err != nil { + t.Fatalf("InitializeWarehouse failed: %v", err) + } + + data := []byte{0xAA, 0xBB} + if err := repo.SetWarehouseEquipData(charID, 7, data); err != nil { + t.Fatalf("SetWarehouseEquipData failed: %v", err) + } + + got, err := repo.GetWarehouseEquipData(charID, 7) + if err != nil { + t.Fatalf("GetWarehouseEquipData failed: %v", err) + } + if len(got) != 2 || got[0] != 0xAA { + t.Errorf("Expected equip data, got: %x", got) + } +} + +func TestRepoHouseAcquireTitle(t *testing.T) { + repo, _, charID := setupHouseRepo(t) + + if err := repo.AcquireTitle(100, charID); err != nil { + t.Fatalf("AcquireTitle failed: %v", err) + } + + titles, err := repo.GetTitles(charID) + if err != nil { + t.Fatalf("GetTitles failed: %v", err) + } + if len(titles) != 1 { + t.Fatalf("Expected 1 title, got: %d", len(titles)) + } + if titles[0].ID != 100 { + t.Errorf("Expected title ID=100, got: %d", titles[0].ID) + } +} + +func TestRepoHouseAcquireTitleIdempotent(t *testing.T) { + repo, _, charID := setupHouseRepo(t) + + if err := repo.AcquireTitle(100, charID); err != nil { + t.Fatalf("First AcquireTitle failed: %v", err) + } + if err := repo.AcquireTitle(100, charID); err != nil { + t.Fatalf("Second AcquireTitle failed: %v", err) + } + + titles, err := repo.GetTitles(charID) + if err != nil { + t.Fatalf("GetTitles failed: %v", err) + } + if len(titles) != 1 { + t.Errorf("Expected 1 title after idempotent acquire, got: %d", len(titles)) + } +} + +func TestRepoHouseGetTitlesEmpty(t *testing.T) { + repo, _, charID := setupHouseRepo(t) + + titles, err := repo.GetTitles(charID) + if err != nil { + t.Fatalf("GetTitles failed: %v", err) + } + if len(titles) != 0 { + t.Errorf("Expected 0 titles, got: %d", len(titles)) + } +} diff --git a/server/channelserver/repo_mail_test.go b/server/channelserver/repo_mail_test.go new file mode 100644 index 000000000..101b93ef7 --- /dev/null +++ b/server/channelserver/repo_mail_test.go @@ -0,0 +1,231 @@ +package channelserver + +import ( + "testing" + + "github.com/jmoiron/sqlx" +) + +func setupMailRepo(t *testing.T) (*MailRepository, *sqlx.DB, uint32, uint32) { + t.Helper() + db := SetupTestDB(t) + userID := CreateTestUser(t, db, "mail_sender") + senderID := CreateTestCharacter(t, db, userID, "Sender") + userID2 := CreateTestUser(t, db, "mail_recipient") + recipientID := CreateTestCharacter(t, db, userID2, "Recipient") + repo := NewMailRepository(db) + t.Cleanup(func() { TeardownTestDB(t, db) }) + return repo, db, senderID, recipientID +} + +func TestRepoMailSendMail(t *testing.T) { + repo, db, senderID, recipientID := setupMailRepo(t) + + if err := repo.SendMail(senderID, recipientID, "Hello", "World", 0, 0, false, false); err != nil { + t.Fatalf("SendMail failed: %v", err) + } + + var count int + if err := db.QueryRow("SELECT COUNT(*) FROM mail WHERE sender_id=$1 AND recipient_id=$2", senderID, recipientID).Scan(&count); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if count != 1 { + t.Errorf("Expected 1 mail, got: %d", count) + } +} + +func TestRepoMailSendMailWithItem(t *testing.T) { + repo, db, senderID, recipientID := setupMailRepo(t) + + if err := repo.SendMail(senderID, recipientID, "Gift", "Item for you", 100, 5, false, false); err != nil { + t.Fatalf("SendMail failed: %v", err) + } + + var itemID, itemAmount int + if err := db.QueryRow("SELECT attached_item, attached_item_amount FROM mail WHERE sender_id=$1", senderID).Scan(&itemID, &itemAmount); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if itemID != 100 || itemAmount != 5 { + t.Errorf("Expected item=100 amount=5, got item=%d amount=%d", itemID, itemAmount) + } +} + +func TestRepoMailGetListForCharacter(t *testing.T) { + repo, _, senderID, recipientID := setupMailRepo(t) + + if err := repo.SendMail(senderID, recipientID, "Mail1", "Body1", 0, 0, false, false); err != nil { + t.Fatalf("SendMail 1 failed: %v", err) + } + if err := repo.SendMail(senderID, recipientID, "Mail2", "Body2", 0, 0, false, false); err != nil { + t.Fatalf("SendMail 2 failed: %v", err) + } + + mails, err := repo.GetListForCharacter(recipientID) + if err != nil { + t.Fatalf("GetListForCharacter failed: %v", err) + } + if len(mails) != 2 { + t.Fatalf("Expected 2 mails, got: %d", len(mails)) + } + // Should include sender name + if mails[0].SenderName != "Sender" { + t.Errorf("Expected sender_name='Sender', got: %q", mails[0].SenderName) + } +} + +func TestRepoMailGetListExcludesDeleted(t *testing.T) { + repo, _, senderID, recipientID := setupMailRepo(t) + + if err := repo.SendMail(senderID, recipientID, "Visible", "", 0, 0, false, false); err != nil { + t.Fatalf("SendMail failed: %v", err) + } + if err := repo.SendMail(senderID, recipientID, "Deleted", "", 0, 0, false, false); err != nil { + t.Fatalf("SendMail failed: %v", err) + } + + // Get the list and delete the second mail + mails, _ := repo.GetListForCharacter(recipientID) + if err := repo.MarkDeleted(mails[0].ID); err != nil { + t.Fatalf("MarkDeleted failed: %v", err) + } + + mails, err := repo.GetListForCharacter(recipientID) + if err != nil { + t.Fatalf("GetListForCharacter failed: %v", err) + } + if len(mails) != 1 { + t.Fatalf("Expected 1 mail after deletion, got: %d", len(mails)) + } +} + +func TestRepoMailGetByID(t *testing.T) { + repo, db, senderID, recipientID := setupMailRepo(t) + + if err := repo.SendMail(senderID, recipientID, "Detail", "Full body text", 50, 2, true, false); err != nil { + t.Fatalf("SendMail failed: %v", err) + } + + var mailID int + if err := db.QueryRow("SELECT id FROM mail WHERE sender_id=$1", senderID).Scan(&mailID); err != nil { + t.Fatalf("Setup query failed: %v", err) + } + + mail, err := repo.GetByID(mailID) + if err != nil { + t.Fatalf("GetByID failed: %v", err) + } + if mail.Subject != "Detail" { + t.Errorf("Expected subject='Detail', got: %q", mail.Subject) + } + if mail.Body != "Full body text" { + t.Errorf("Expected body='Full body text', got: %q", mail.Body) + } + if !mail.IsGuildInvite { + t.Error("Expected is_guild_invite=true") + } + if mail.SenderName != "Sender" { + t.Errorf("Expected sender_name='Sender', got: %q", mail.SenderName) + } +} + +func TestRepoMailMarkRead(t *testing.T) { + repo, db, senderID, recipientID := setupMailRepo(t) + + if err := repo.SendMail(senderID, recipientID, "Unread", "", 0, 0, false, false); err != nil { + t.Fatalf("SendMail failed: %v", err) + } + + var mailID int + if err := db.QueryRow("SELECT id FROM mail WHERE sender_id=$1", senderID).Scan(&mailID); err != nil { + t.Fatalf("Setup query failed: %v", err) + } + + if err := repo.MarkRead(mailID); err != nil { + t.Fatalf("MarkRead failed: %v", err) + } + + var read bool + if err := db.QueryRow("SELECT read FROM mail WHERE id=$1", mailID).Scan(&read); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if !read { + t.Error("Expected read=true") + } +} + +func TestRepoMailSetLocked(t *testing.T) { + repo, db, senderID, recipientID := setupMailRepo(t) + + if err := repo.SendMail(senderID, recipientID, "Lock Test", "", 0, 0, false, false); err != nil { + t.Fatalf("SendMail failed: %v", err) + } + + var mailID int + if err := db.QueryRow("SELECT id FROM mail WHERE sender_id=$1", senderID).Scan(&mailID); err != nil { + t.Fatalf("Setup query failed: %v", err) + } + + if err := repo.SetLocked(mailID, true); err != nil { + t.Fatalf("SetLocked failed: %v", err) + } + + var locked bool + if err := db.QueryRow("SELECT locked FROM mail WHERE id=$1", mailID).Scan(&locked); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if !locked { + t.Error("Expected locked=true") + } + + // Unlock + if err := repo.SetLocked(mailID, false); err != nil { + t.Fatalf("SetLocked(false) failed: %v", err) + } + if err := db.QueryRow("SELECT locked FROM mail WHERE id=$1", mailID).Scan(&locked); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if locked { + t.Error("Expected locked=false after unlock") + } +} + +func TestRepoMailMarkItemReceived(t *testing.T) { + repo, db, senderID, recipientID := setupMailRepo(t) + + if err := repo.SendMail(senderID, recipientID, "Item Mail", "", 100, 1, false, false); err != nil { + t.Fatalf("SendMail failed: %v", err) + } + + var mailID int + if err := db.QueryRow("SELECT id FROM mail WHERE sender_id=$1", senderID).Scan(&mailID); err != nil { + t.Fatalf("Setup query failed: %v", err) + } + + if err := repo.MarkItemReceived(mailID); err != nil { + t.Fatalf("MarkItemReceived failed: %v", err) + } + + var received bool + if err := db.QueryRow("SELECT attached_item_received FROM mail WHERE id=$1", mailID).Scan(&received); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if !received { + t.Error("Expected attached_item_received=true") + } +} + +func TestRepoMailSystemMessage(t *testing.T) { + repo, db, senderID, recipientID := setupMailRepo(t) + + if err := repo.SendMail(senderID, recipientID, "System", "System alert", 0, 0, false, true); err != nil { + t.Fatalf("SendMail failed: %v", err) + } + + var isSys bool + if err := db.QueryRow("SELECT is_sys_message FROM mail WHERE sender_id=$1", senderID).Scan(&isSys); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if !isSys { + t.Error("Expected is_sys_message=true") + } +} diff --git a/server/channelserver/repo_mercenary_test.go b/server/channelserver/repo_mercenary_test.go new file mode 100644 index 000000000..660b5995b --- /dev/null +++ b/server/channelserver/repo_mercenary_test.go @@ -0,0 +1,161 @@ +package channelserver + +import ( + "testing" + + "github.com/jmoiron/sqlx" +) + +func setupMercenaryRepo(t *testing.T) (*MercenaryRepository, *sqlx.DB, uint32, uint32) { + t.Helper() + db := SetupTestDB(t) + userID := CreateTestUser(t, db, "merc_test_user") + charID := CreateTestCharacter(t, db, userID, "MercChar") + guildID := CreateTestGuild(t, db, charID, "MercGuild") + repo := NewMercenaryRepository(db) + t.Cleanup(func() { TeardownTestDB(t, db) }) + return repo, db, charID, guildID +} + +func TestRepoMercenaryNextRastaID(t *testing.T) { + repo, _, _, _ := setupMercenaryRepo(t) + + id1, err := repo.NextRastaID() + if err != nil { + t.Fatalf("NextRastaID failed: %v", err) + } + id2, err := repo.NextRastaID() + if err != nil { + t.Fatalf("NextRastaID second call failed: %v", err) + } + if id2 <= id1 { + t.Errorf("Expected increasing IDs, got: %d then %d", id1, id2) + } +} + +func TestRepoMercenaryNextAirouID(t *testing.T) { + repo, _, _, _ := setupMercenaryRepo(t) + + id1, err := repo.NextAirouID() + if err != nil { + t.Fatalf("NextAirouID failed: %v", err) + } + id2, err := repo.NextAirouID() + if err != nil { + t.Fatalf("NextAirouID second call failed: %v", err) + } + if id2 <= id1 { + t.Errorf("Expected increasing IDs, got: %d then %d", id1, id2) + } +} + +func TestRepoMercenaryGetMercenaryLoansEmpty(t *testing.T) { + repo, _, charID, _ := setupMercenaryRepo(t) + + loans, err := repo.GetMercenaryLoans(charID) + if err != nil { + t.Fatalf("GetMercenaryLoans failed: %v", err) + } + if len(loans) != 0 { + t.Errorf("Expected 0 loans, got: %d", len(loans)) + } +} + +func TestRepoMercenaryGetMercenaryLoans(t *testing.T) { + repo, db, charID, _ := setupMercenaryRepo(t) + + // Set rasta_id on charID + if _, err := db.Exec("UPDATE characters SET rasta_id=999 WHERE id=$1", charID); err != nil { + t.Fatalf("Setup rasta_id failed: %v", err) + } + + // Create another character that has a pact with charID's rasta + user2 := CreateTestUser(t, db, "merc_user2") + char2 := CreateTestCharacter(t, db, user2, "PactHolder") + if _, err := db.Exec("UPDATE characters SET pact_id=999 WHERE id=$1", char2); err != nil { + t.Fatalf("Setup pact_id failed: %v", err) + } + + loans, err := repo.GetMercenaryLoans(charID) + if err != nil { + t.Fatalf("GetMercenaryLoans failed: %v", err) + } + if len(loans) != 1 { + t.Fatalf("Expected 1 loan, got: %d", len(loans)) + } + if loans[0].Name != "PactHolder" { + t.Errorf("Expected name='PactHolder', got: %q", loans[0].Name) + } + if loans[0].CharID != char2 { + t.Errorf("Expected charID=%d, got: %d", char2, loans[0].CharID) + } +} + +func TestRepoMercenaryGetGuildHuntCatsUsedEmpty(t *testing.T) { + repo, _, charID, _ := setupMercenaryRepo(t) + + cats, err := repo.GetGuildHuntCatsUsed(charID) + if err != nil { + t.Fatalf("GetGuildHuntCatsUsed failed: %v", err) + } + if len(cats) != 0 { + t.Errorf("Expected 0 cat usages, got: %d", len(cats)) + } +} + +func TestRepoMercenaryGetGuildHuntCatsUsed(t *testing.T) { + repo, db, charID, guildID := setupMercenaryRepo(t) + + // Insert a guild hunt with cats_used + if _, err := db.Exec( + `INSERT INTO guild_hunts (guild_id, host_id, destination, level, hunt_data, cats_used, acquired, collected, start) + VALUES ($1, $2, 1, 1, $3, '1,2,3', false, false, now())`, + guildID, charID, []byte{0x00}, + ); err != nil { + t.Fatalf("Setup guild_hunts failed: %v", err) + } + + cats, err := repo.GetGuildHuntCatsUsed(charID) + if err != nil { + t.Fatalf("GetGuildHuntCatsUsed failed: %v", err) + } + if len(cats) != 1 { + t.Fatalf("Expected 1 cat usage, got: %d", len(cats)) + } + if cats[0].CatsUsed != "1,2,3" { + t.Errorf("Expected cats_used='1,2,3', got: %q", cats[0].CatsUsed) + } +} + +func TestRepoMercenaryGetGuildAirouEmpty(t *testing.T) { + repo, _, _, guildID := setupMercenaryRepo(t) + + airou, err := repo.GetGuildAirou(guildID) + if err != nil { + t.Fatalf("GetGuildAirou failed: %v", err) + } + if len(airou) != 0 { + t.Errorf("Expected 0 airou, got: %d", len(airou)) + } +} + +func TestRepoMercenaryGetGuildAirou(t *testing.T) { + repo, db, charID, guildID := setupMercenaryRepo(t) + + // Set otomoairou on the character + airouData := []byte{0xAA, 0xBB, 0xCC} + if _, err := db.Exec("UPDATE characters SET otomoairou=$1 WHERE id=$2", airouData, charID); err != nil { + t.Fatalf("Setup otomoairou failed: %v", err) + } + + airou, err := repo.GetGuildAirou(guildID) + if err != nil { + t.Fatalf("GetGuildAirou failed: %v", err) + } + if len(airou) != 1 { + t.Fatalf("Expected 1 airou, got: %d", len(airou)) + } + if len(airou[0]) != 3 || airou[0][0] != 0xAA { + t.Errorf("Expected airou data, got: %x", airou[0]) + } +} diff --git a/server/channelserver/repo_misc_test.go b/server/channelserver/repo_misc_test.go new file mode 100644 index 000000000..7a16def39 --- /dev/null +++ b/server/channelserver/repo_misc_test.go @@ -0,0 +1,110 @@ +package channelserver + +import ( + "testing" + + "github.com/jmoiron/sqlx" +) + +func setupMiscRepo(t *testing.T) (*MiscRepository, *sqlx.DB) { + t.Helper() + db := SetupTestDB(t) + repo := NewMiscRepository(db) + t.Cleanup(func() { TeardownTestDB(t, db) }) + return repo, db +} + +func TestRepoMiscUpsertTrendWeapon(t *testing.T) { + repo, db := setupMiscRepo(t) + + if err := repo.UpsertTrendWeapon(100, 1); err != nil { + t.Fatalf("UpsertTrendWeapon failed: %v", err) + } + + var count int + if err := db.QueryRow("SELECT count FROM trend_weapons WHERE weapon_id=100").Scan(&count); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if count != 1 { + t.Errorf("Expected count=1, got: %d", count) + } +} + +func TestRepoMiscUpsertTrendWeaponIncrement(t *testing.T) { + repo, db := setupMiscRepo(t) + + if err := repo.UpsertTrendWeapon(100, 1); err != nil { + t.Fatalf("First UpsertTrendWeapon failed: %v", err) + } + if err := repo.UpsertTrendWeapon(100, 1); err != nil { + t.Fatalf("Second UpsertTrendWeapon failed: %v", err) + } + + var count int + if err := db.QueryRow("SELECT count FROM trend_weapons WHERE weapon_id=100").Scan(&count); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if count != 2 { + t.Errorf("Expected count=2 after upsert, got: %d", count) + } +} + +func TestRepoMiscGetTrendWeaponsEmpty(t *testing.T) { + repo, _ := setupMiscRepo(t) + + weapons, err := repo.GetTrendWeapons(1) + if err != nil { + t.Fatalf("GetTrendWeapons failed: %v", err) + } + if len(weapons) != 0 { + t.Errorf("Expected 0 weapons, got: %d", len(weapons)) + } +} + +func TestRepoMiscGetTrendWeaponsOrdering(t *testing.T) { + repo, _ := setupMiscRepo(t) + + // Insert weapons with different counts + for i := 0; i < 3; i++ { + if err := repo.UpsertTrendWeapon(uint16(100+i), 1); err != nil { + t.Fatalf("UpsertTrendWeapon failed: %v", err) + } + } + // Give weapon 101 more uses + if err := repo.UpsertTrendWeapon(101, 1); err != nil { + t.Fatalf("UpsertTrendWeapon failed: %v", err) + } + if err := repo.UpsertTrendWeapon(101, 1); err != nil { + t.Fatalf("UpsertTrendWeapon failed: %v", err) + } + + weapons, err := repo.GetTrendWeapons(1) + if err != nil { + t.Fatalf("GetTrendWeapons failed: %v", err) + } + if len(weapons) != 3 { + t.Fatalf("Expected 3 weapons, got: %d", len(weapons)) + } + // First should be the one with highest count (101 with count=3) + if weapons[0] != 101 { + t.Errorf("Expected first weapon=101 (highest count), got: %d", weapons[0]) + } +} + +func TestRepoMiscGetTrendWeaponsLimit3(t *testing.T) { + repo, _ := setupMiscRepo(t) + + for i := 0; i < 5; i++ { + if err := repo.UpsertTrendWeapon(uint16(100+i), 1); err != nil { + t.Fatalf("UpsertTrendWeapon failed: %v", err) + } + } + + weapons, err := repo.GetTrendWeapons(1) + if err != nil { + t.Fatalf("GetTrendWeapons failed: %v", err) + } + if len(weapons) != 3 { + t.Errorf("Expected max 3 weapons, got: %d", len(weapons)) + } +} diff --git a/server/channelserver/repo_rengoku_test.go b/server/channelserver/repo_rengoku_test.go new file mode 100644 index 000000000..3a8c377e3 --- /dev/null +++ b/server/channelserver/repo_rengoku_test.go @@ -0,0 +1,144 @@ +package channelserver + +import ( + "testing" + + "github.com/jmoiron/sqlx" +) + +func setupRengokuRepo(t *testing.T) (*RengokuRepository, *sqlx.DB, uint32, uint32) { + t.Helper() + db := SetupTestDB(t) + userID := CreateTestUser(t, db, "rengoku_test_user") + charID := CreateTestCharacter(t, db, userID, "RengokuChar") + guildID := CreateTestGuild(t, db, charID, "RengokuGuild") + repo := NewRengokuRepository(db) + t.Cleanup(func() { TeardownTestDB(t, db) }) + return repo, db, charID, guildID +} + +func TestRepoRengokuUpsertScoreNew(t *testing.T) { + repo, db, charID, _ := setupRengokuRepo(t) + + if err := repo.UpsertScore(charID, 10, 500, 5, 200); err != nil { + t.Fatalf("UpsertScore failed: %v", err) + } + + var stagesMp, pointsMp, stagesSp, pointsSp uint32 + if err := db.QueryRow("SELECT max_stages_mp, max_points_mp, max_stages_sp, max_points_sp FROM rengoku_score WHERE character_id=$1", charID).Scan(&stagesMp, &pointsMp, &stagesSp, &pointsSp); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if stagesMp != 10 || pointsMp != 500 || stagesSp != 5 || pointsSp != 200 { + t.Errorf("Expected 10/500/5/200, got %d/%d/%d/%d", stagesMp, pointsMp, stagesSp, pointsSp) + } +} + +func TestRepoRengokuUpsertScoreUpdate(t *testing.T) { + repo, db, charID, _ := setupRengokuRepo(t) + + if err := repo.UpsertScore(charID, 10, 500, 5, 200); err != nil { + t.Fatalf("First UpsertScore failed: %v", err) + } + if err := repo.UpsertScore(charID, 20, 1000, 15, 800); err != nil { + t.Fatalf("Second UpsertScore failed: %v", err) + } + + var stagesMp, pointsMp uint32 + if err := db.QueryRow("SELECT max_stages_mp, max_points_mp FROM rengoku_score WHERE character_id=$1", charID).Scan(&stagesMp, &pointsMp); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if stagesMp != 20 || pointsMp != 1000 { + t.Errorf("Expected 20/1000 after update, got %d/%d", stagesMp, pointsMp) + } +} + +func TestRepoRengokuGetRankingGlobal(t *testing.T) { + repo, _, charID, _ := setupRengokuRepo(t) + + if err := repo.UpsertScore(charID, 10, 500, 5, 200); err != nil { + t.Fatalf("UpsertScore failed: %v", err) + } + + // Leaderboard 0 = max_stages_mp (global) + scores, err := repo.GetRanking(0, 0) + if err != nil { + t.Fatalf("GetRanking failed: %v", err) + } + if len(scores) != 1 { + t.Fatalf("Expected 1 score, got: %d", len(scores)) + } + if scores[0].Score != 10 { + t.Errorf("Expected score=10, got: %d", scores[0].Score) + } + if scores[0].Name != "RengokuChar" { + t.Errorf("Expected name='RengokuChar', got: %q", scores[0].Name) + } +} + +func TestRepoRengokuGetRankingGuildFiltered(t *testing.T) { + repo, db, charID, guildID := setupRengokuRepo(t) + + if err := repo.UpsertScore(charID, 10, 500, 5, 200); err != nil { + t.Fatalf("UpsertScore failed: %v", err) + } + + // Create another character in a different guild + user2 := CreateTestUser(t, db, "rengoku_user2") + char2 := CreateTestCharacter(t, db, user2, "RengokuChar2") + CreateTestGuild(t, db, char2, "OtherGuild") + if err := repo.UpsertScore(char2, 20, 1000, 15, 800); err != nil { + t.Fatalf("UpsertScore char2 failed: %v", err) + } + + // Leaderboard 2 = max_stages_mp (guild-filtered) + scores, err := repo.GetRanking(2, guildID) + if err != nil { + t.Fatalf("GetRanking failed: %v", err) + } + if len(scores) != 1 { + t.Fatalf("Expected 1 guild-filtered score, got: %d", len(scores)) + } + if scores[0].Name != "RengokuChar" { + t.Errorf("Expected 'RengokuChar' in guild ranking, got: %q", scores[0].Name) + } +} + +func TestRepoRengokuGetRankingPointsLeaderboard(t *testing.T) { + repo, _, charID, _ := setupRengokuRepo(t) + + if err := repo.UpsertScore(charID, 10, 500, 5, 200); err != nil { + t.Fatalf("UpsertScore failed: %v", err) + } + + // Leaderboard 1 = max_points_mp (global) + scores, err := repo.GetRanking(1, 0) + if err != nil { + t.Fatalf("GetRanking failed: %v", err) + } + if len(scores) != 1 { + t.Fatalf("Expected 1 score, got: %d", len(scores)) + } + if scores[0].Score != 500 { + t.Errorf("Expected score=500 for points leaderboard, got: %d", scores[0].Score) + } +} + +func TestRepoRengokuGetRankingSPLeaderboard(t *testing.T) { + repo, _, charID, _ := setupRengokuRepo(t) + + if err := repo.UpsertScore(charID, 10, 500, 5, 200); err != nil { + t.Fatalf("UpsertScore failed: %v", err) + } + + // Leaderboard 4 = max_stages_sp (global) + scores, err := repo.GetRanking(4, 0) + if err != nil { + t.Fatalf("GetRanking failed: %v", err) + } + if len(scores) != 1 { + t.Fatalf("Expected 1 score, got: %d", len(scores)) + } + if scores[0].Score != 5 { + t.Errorf("Expected score=5 for SP stages leaderboard, got: %d", scores[0].Score) + } +} diff --git a/server/channelserver/repo_scenario_test.go b/server/channelserver/repo_scenario_test.go new file mode 100644 index 000000000..f27694b51 --- /dev/null +++ b/server/channelserver/repo_scenario_test.go @@ -0,0 +1,60 @@ +package channelserver + +import ( + "testing" + + "github.com/jmoiron/sqlx" +) + +func setupScenarioRepo(t *testing.T) (*ScenarioRepository, *sqlx.DB) { + t.Helper() + db := SetupTestDB(t) + repo := NewScenarioRepository(db) + t.Cleanup(func() { TeardownTestDB(t, db) }) + return repo, db +} + +func TestRepoScenarioGetCountersEmpty(t *testing.T) { + repo, _ := setupScenarioRepo(t) + + counters, err := repo.GetCounters() + if err != nil { + t.Fatalf("GetCounters failed: %v", err) + } + if len(counters) != 0 { + t.Errorf("Expected 0 counters, got: %d", len(counters)) + } +} + +func TestRepoScenarioGetCounters(t *testing.T) { + repo, db := setupScenarioRepo(t) + + if _, err := db.Exec("INSERT INTO scenario_counter (id, scenario_id, category_id) VALUES (1, 100, 0)"); err != nil { + t.Fatalf("Setup failed: %v", err) + } + if _, err := db.Exec("INSERT INTO scenario_counter (id, scenario_id, category_id) VALUES (2, 200, 1)"); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + counters, err := repo.GetCounters() + if err != nil { + t.Fatalf("GetCounters failed: %v", err) + } + if len(counters) != 2 { + t.Fatalf("Expected 2 counters, got: %d", len(counters)) + } + + // Check both values exist (order may vary) + found100, found200 := false, false + for _, c := range counters { + if c.MainID == 100 { + found100 = true + } + if c.MainID == 200 { + found200 = true + } + } + if !found100 || !found200 { + t.Errorf("Expected scenario_ids 100 and 200, got: %+v", counters) + } +} diff --git a/server/channelserver/repo_session_test.go b/server/channelserver/repo_session_test.go new file mode 100644 index 000000000..e4d7d78bf --- /dev/null +++ b/server/channelserver/repo_session_test.go @@ -0,0 +1,141 @@ +package channelserver + +import ( + "testing" + + "github.com/jmoiron/sqlx" +) + +func setupSessionRepo(t *testing.T) (*SessionRepository, *sqlx.DB, uint32, uint32, uint32, string) { + t.Helper() + db := SetupTestDB(t) + userID := CreateTestUser(t, db, "session_test_user") + charID := CreateTestCharacter(t, db, userID, "SessionChar") + token := "test_token_12345" + sessionID := CreateTestSignSession(t, db, userID, token) + repo := NewSessionRepository(db) + t.Cleanup(func() { TeardownTestDB(t, db) }) + return repo, db, userID, charID, sessionID, token +} + +func TestRepoSessionValidateLoginToken(t *testing.T) { + repo, _, _, charID, sessionID, token := setupSessionRepo(t) + + err := repo.ValidateLoginToken(token, sessionID, charID) + if err != nil { + t.Fatalf("ValidateLoginToken failed: %v", err) + } +} + +func TestRepoSessionValidateLoginTokenInvalidToken(t *testing.T) { + repo, _, _, charID, sessionID, _ := setupSessionRepo(t) + + err := repo.ValidateLoginToken("wrong_token", sessionID, charID) + if err == nil { + t.Fatal("Expected error for invalid token, got nil") + } +} + +func TestRepoSessionValidateLoginTokenWrongChar(t *testing.T) { + repo, _, _, _, sessionID, token := setupSessionRepo(t) + + err := repo.ValidateLoginToken(token, sessionID, 999999) + if err == nil { + t.Fatal("Expected error for wrong char ID, got nil") + } +} + +func TestRepoSessionValidateLoginTokenWrongSession(t *testing.T) { + repo, _, _, charID, _, token := setupSessionRepo(t) + + err := repo.ValidateLoginToken(token, 999999, charID) + if err == nil { + t.Fatal("Expected error for wrong session ID, got nil") + } +} + +func TestRepoSessionBindSession(t *testing.T) { + repo, db, _, charID, _, token := setupSessionRepo(t) + + CreateTestServer(t, db, 1) + + if err := repo.BindSession(token, 1, charID); err != nil { + t.Fatalf("BindSession failed: %v", err) + } + + var serverID *uint16 + var boundCharID *uint32 + if err := db.QueryRow("SELECT server_id, char_id FROM sign_sessions WHERE token=$1", token).Scan(&serverID, &boundCharID); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if serverID == nil || *serverID != 1 { + t.Errorf("Expected server_id=1, got: %v", serverID) + } + if boundCharID == nil || *boundCharID != charID { + t.Errorf("Expected char_id=%d, got: %v", charID, boundCharID) + } +} + +func TestRepoSessionClearSession(t *testing.T) { + repo, db, _, charID, _, token := setupSessionRepo(t) + + CreateTestServer(t, db, 1) + + if err := repo.BindSession(token, 1, charID); err != nil { + t.Fatalf("BindSession failed: %v", err) + } + + if err := repo.ClearSession(token); err != nil { + t.Fatalf("ClearSession failed: %v", err) + } + + var serverID, boundCharID *int + if err := db.QueryRow("SELECT server_id, char_id FROM sign_sessions WHERE token=$1", token).Scan(&serverID, &boundCharID); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if serverID != nil { + t.Errorf("Expected server_id=NULL, got: %v", *serverID) + } + if boundCharID != nil { + t.Errorf("Expected char_id=NULL, got: %v", *boundCharID) + } +} + +func TestRepoSessionUpdatePlayerCount(t *testing.T) { + repo, db, _, _, _, _ := setupSessionRepo(t) + + CreateTestServer(t, db, 1) + + if err := repo.UpdatePlayerCount(1, 42); err != nil { + t.Fatalf("UpdatePlayerCount failed: %v", err) + } + + var count int + if err := db.QueryRow("SELECT current_players FROM servers WHERE server_id=1").Scan(&count); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if count != 42 { + t.Errorf("Expected current_players=42, got: %d", count) + } +} + +func TestRepoSessionUpdatePlayerCountTwice(t *testing.T) { + repo, db, _, _, _, _ := setupSessionRepo(t) + + CreateTestServer(t, db, 1) + + if err := repo.UpdatePlayerCount(1, 10); err != nil { + t.Fatalf("First UpdatePlayerCount failed: %v", err) + } + if err := repo.UpdatePlayerCount(1, 25); err != nil { + t.Fatalf("Second UpdatePlayerCount failed: %v", err) + } + + var count int + if err := db.QueryRow("SELECT current_players FROM servers WHERE server_id=1").Scan(&count); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if count != 25 { + t.Errorf("Expected current_players=25, got: %d", count) + } +} diff --git a/server/channelserver/repo_shop_test.go b/server/channelserver/repo_shop_test.go new file mode 100644 index 000000000..03a882c93 --- /dev/null +++ b/server/channelserver/repo_shop_test.go @@ -0,0 +1,123 @@ +package channelserver + +import ( + "testing" + + "github.com/jmoiron/sqlx" +) + +func setupShopRepo(t *testing.T) (*ShopRepository, *sqlx.DB, uint32) { + t.Helper() + db := SetupTestDB(t) + userID := CreateTestUser(t, db, "shop_test_user") + charID := CreateTestCharacter(t, db, userID, "ShopChar") + repo := NewShopRepository(db) + t.Cleanup(func() { TeardownTestDB(t, db) }) + return repo, db, charID +} + +func TestRepoShopGetShopItemsEmpty(t *testing.T) { + repo, _, charID := setupShopRepo(t) + + items, err := repo.GetShopItems(1, 1, charID) + if err != nil { + t.Fatalf("GetShopItems failed: %v", err) + } + if len(items) != 0 { + t.Errorf("Expected 0 items, got: %d", len(items)) + } +} + +func TestRepoShopGetShopItems(t *testing.T) { + repo, db, charID := setupShopRepo(t) + + // Insert shop items + if _, err := db.Exec( + `INSERT INTO shop_items (id, shop_type, shop_id, item_id, cost, quantity, min_hr, min_sr, min_gr, store_level, max_quantity, road_floors, road_fatalis) + VALUES (1, 1, 100, 500, 1000, 1, 0, 0, 0, 0, 99, 0, 0)`, + ); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + items, err := repo.GetShopItems(1, 100, charID) + if err != nil { + t.Fatalf("GetShopItems failed: %v", err) + } + if len(items) != 1 { + t.Fatalf("Expected 1 item, got: %d", len(items)) + } + if items[0].ItemID != 500 { + t.Errorf("Expected item_id=500, got: %d", items[0].ItemID) + } + if items[0].Cost != 1000 { + t.Errorf("Expected cost=1000, got: %d", items[0].Cost) + } + if items[0].UsedQuantity != 0 { + t.Errorf("Expected used_quantity=0, got: %d", items[0].UsedQuantity) + } +} + +func TestRepoShopRecordPurchaseAmbiguousColumn(t *testing.T) { + repo, _, charID := setupShopRepo(t) + + // RecordPurchase uses ON CONFLICT with unqualified "bought" column reference, + // which PostgreSQL rejects as ambiguous. This test documents the existing bug. + err := repo.RecordPurchase(charID, 1, 3) + if err == nil { + t.Fatal("Expected error from ambiguous column reference in RecordPurchase SQL, but got nil") + } +} + +func TestRepoShopGetFpointItem(t *testing.T) { + repo, db, _ := setupShopRepo(t) + + if _, err := db.Exec("INSERT INTO fpoint_items (id, item_type, item_id, quantity, fpoints, buyable) VALUES (1, 1, 100, 5, 200, true)"); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + quantity, fpoints, err := repo.GetFpointItem(1) + if err != nil { + t.Fatalf("GetFpointItem failed: %v", err) + } + if quantity != 5 { + t.Errorf("Expected quantity=5, got: %d", quantity) + } + if fpoints != 200 { + t.Errorf("Expected fpoints=200, got: %d", fpoints) + } +} + +func TestRepoShopGetFpointExchangeList(t *testing.T) { + repo, db, _ := setupShopRepo(t) + + if _, err := db.Exec("INSERT INTO fpoint_items (id, item_type, item_id, quantity, fpoints, buyable) VALUES (1, 1, 100, 5, 200, true)"); err != nil { + t.Fatalf("Setup failed: %v", err) + } + if _, err := db.Exec("INSERT INTO fpoint_items (id, item_type, item_id, quantity, fpoints, buyable) VALUES (2, 2, 200, 10, 500, false)"); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + exchanges, err := repo.GetFpointExchangeList() + if err != nil { + t.Fatalf("GetFpointExchangeList failed: %v", err) + } + if len(exchanges) != 2 { + t.Fatalf("Expected 2 exchange items, got: %d", len(exchanges)) + } + // Ordered by buyable DESC, so buyable=true first + if !exchanges[0].Buyable { + t.Error("Expected first item to have buyable=true") + } +} + +func TestRepoShopGetFpointExchangeListEmpty(t *testing.T) { + repo, _, _ := setupShopRepo(t) + + exchanges, err := repo.GetFpointExchangeList() + if err != nil { + t.Fatalf("GetFpointExchangeList failed: %v", err) + } + if len(exchanges) != 0 { + t.Errorf("Expected 0 exchange items, got: %d", len(exchanges)) + } +} diff --git a/server/channelserver/repo_stamp_test.go b/server/channelserver/repo_stamp_test.go new file mode 100644 index 000000000..ef0b2e556 --- /dev/null +++ b/server/channelserver/repo_stamp_test.go @@ -0,0 +1,240 @@ +package channelserver + +import ( + "testing" + "time" + + "github.com/jmoiron/sqlx" +) + +func setupStampRepo(t *testing.T) (*StampRepository, *sqlx.DB, uint32) { + t.Helper() + db := SetupTestDB(t) + userID := CreateTestUser(t, db, "stamp_test_user") + charID := CreateTestCharacter(t, db, userID, "StampChar") + repo := NewStampRepository(db) + t.Cleanup(func() { TeardownTestDB(t, db) }) + return repo, db, charID +} + +func initStamp(t *testing.T, repo *StampRepository, charID uint32) { + t.Helper() + now := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) + if err := repo.Init(charID, now); err != nil { + t.Fatalf("Stamp Init failed: %v", err) + } +} + +func TestRepoStampInit(t *testing.T) { + repo, db, charID := setupStampRepo(t) + + now := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC) + if err := repo.Init(charID, now); err != nil { + t.Fatalf("Init failed: %v", err) + } + + var hlChecked, exChecked time.Time + if err := db.QueryRow("SELECT hl_checked, ex_checked FROM stamps WHERE character_id=$1", charID).Scan(&hlChecked, &exChecked); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if !hlChecked.Equal(now) { + t.Errorf("Expected hl_checked=%v, got: %v", now, hlChecked) + } + if !exChecked.Equal(now) { + t.Errorf("Expected ex_checked=%v, got: %v", now, exChecked) + } +} + +func TestRepoStampGetChecked(t *testing.T) { + repo, _, charID := setupStampRepo(t) + + now := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC) + if err := repo.Init(charID, now); err != nil { + t.Fatalf("Init failed: %v", err) + } + + got, err := repo.GetChecked(charID, "hl") + if err != nil { + t.Fatalf("GetChecked failed: %v", err) + } + if !got.Equal(now) { + t.Errorf("Expected %v, got: %v", now, got) + } +} + +func TestRepoStampSetChecked(t *testing.T) { + repo, _, charID := setupStampRepo(t) + initStamp(t, repo, charID) + + newTime := time.Date(2025, 7, 1, 0, 0, 0, 0, time.UTC) + if err := repo.SetChecked(charID, "ex", newTime); err != nil { + t.Fatalf("SetChecked failed: %v", err) + } + + got, err := repo.GetChecked(charID, "ex") + if err != nil { + t.Fatalf("GetChecked failed: %v", err) + } + if !got.Equal(newTime) { + t.Errorf("Expected %v, got: %v", newTime, got) + } +} + +func TestRepoStampIncrementTotal(t *testing.T) { + repo, _, charID := setupStampRepo(t) + initStamp(t, repo, charID) + + if err := repo.IncrementTotal(charID, "hl"); err != nil { + t.Fatalf("First IncrementTotal failed: %v", err) + } + if err := repo.IncrementTotal(charID, "hl"); err != nil { + t.Fatalf("Second IncrementTotal failed: %v", err) + } + + total, redeemed, err := repo.GetTotals(charID, "hl") + if err != nil { + t.Fatalf("GetTotals failed: %v", err) + } + if total != 2 { + t.Errorf("Expected total=2, got: %d", total) + } + if redeemed != 0 { + t.Errorf("Expected redeemed=0, got: %d", redeemed) + } +} + +func TestRepoStampGetTotals(t *testing.T) { + repo, db, charID := setupStampRepo(t) + initStamp(t, repo, charID) + + if _, err := db.Exec("UPDATE stamps SET hl_total=10, hl_redeemed=3 WHERE character_id=$1", charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + total, redeemed, err := repo.GetTotals(charID, "hl") + if err != nil { + t.Fatalf("GetTotals failed: %v", err) + } + if total != 10 || redeemed != 3 { + t.Errorf("Expected total=10 redeemed=3, got total=%d redeemed=%d", total, redeemed) + } +} + +func TestRepoStampExchange(t *testing.T) { + repo, db, charID := setupStampRepo(t) + initStamp(t, repo, charID) + + if _, err := db.Exec("UPDATE stamps SET hl_total=20, hl_redeemed=0 WHERE character_id=$1", charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + total, redeemed, err := repo.Exchange(charID, "hl") + if err != nil { + t.Fatalf("Exchange failed: %v", err) + } + if total != 20 { + t.Errorf("Expected total=20, got: %d", total) + } + if redeemed != 8 { + t.Errorf("Expected redeemed=8, got: %d", redeemed) + } +} + +func TestRepoStampExchangeYearly(t *testing.T) { + repo, db, charID := setupStampRepo(t) + initStamp(t, repo, charID) + + if _, err := db.Exec("UPDATE stamps SET hl_total=100, hl_redeemed=50 WHERE character_id=$1", charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + total, redeemed, err := repo.ExchangeYearly(charID) + if err != nil { + t.Fatalf("ExchangeYearly failed: %v", err) + } + if total != 52 { + t.Errorf("Expected total=52 (100-48), got: %d", total) + } + if redeemed != 2 { + t.Errorf("Expected redeemed=2 (50-48), got: %d", redeemed) + } +} + +func TestRepoStampGetMonthlyClaimed(t *testing.T) { + repo, db, charID := setupStampRepo(t) + initStamp(t, repo, charID) + + claimedTime := time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC) + if _, err := db.Exec("UPDATE stamps SET monthly_claimed=$1 WHERE character_id=$2", claimedTime, charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + got, err := repo.GetMonthlyClaimed(charID, "monthly") + if err != nil { + t.Fatalf("GetMonthlyClaimed failed: %v", err) + } + if !got.Equal(claimedTime) { + t.Errorf("Expected %v, got: %v", claimedTime, got) + } +} + +func TestRepoStampSetMonthlyClaimed(t *testing.T) { + repo, _, charID := setupStampRepo(t) + initStamp(t, repo, charID) + + claimedTime := time.Date(2025, 7, 1, 0, 0, 0, 0, time.UTC) + if err := repo.SetMonthlyClaimed(charID, "monthly", claimedTime); err != nil { + t.Fatalf("SetMonthlyClaimed failed: %v", err) + } + + got, err := repo.GetMonthlyClaimed(charID, "monthly") + if err != nil { + t.Fatalf("GetMonthlyClaimed failed: %v", err) + } + if !got.Equal(claimedTime) { + t.Errorf("Expected %v, got: %v", claimedTime, got) + } +} + +func TestRepoStampExTypes(t *testing.T) { + repo, db, charID := setupStampRepo(t) + initStamp(t, repo, charID) + + // Verify ex stamp type works too + if err := repo.IncrementTotal(charID, "ex"); err != nil { + t.Fatalf("IncrementTotal(ex) failed: %v", err) + } + + if _, err := db.Exec("UPDATE stamps SET ex_total=16, ex_redeemed=0 WHERE character_id=$1", charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + total, redeemed, err := repo.Exchange(charID, "ex") + if err != nil { + t.Fatalf("Exchange(ex) failed: %v", err) + } + if total != 16 { + t.Errorf("Expected ex_total=16, got: %d", total) + } + if redeemed != 8 { + t.Errorf("Expected ex_redeemed=8, got: %d", redeemed) + } +} + +func TestRepoStampMonthlyHlClaimed(t *testing.T) { + repo, _, charID := setupStampRepo(t) + initStamp(t, repo, charID) + + claimedTime := time.Date(2025, 8, 15, 0, 0, 0, 0, time.UTC) + if err := repo.SetMonthlyClaimed(charID, "monthly_hl", claimedTime); err != nil { + t.Fatalf("SetMonthlyClaimed(monthly_hl) failed: %v", err) + } + + got, err := repo.GetMonthlyClaimed(charID, "monthly_hl") + if err != nil { + t.Fatalf("GetMonthlyClaimed(monthly_hl) failed: %v", err) + } + if !got.Equal(claimedTime) { + t.Errorf("Expected %v, got: %v", claimedTime, got) + } +} diff --git a/server/channelserver/repo_tower_test.go b/server/channelserver/repo_tower_test.go new file mode 100644 index 000000000..5c3f2e01a --- /dev/null +++ b/server/channelserver/repo_tower_test.go @@ -0,0 +1,275 @@ +package channelserver + +import ( + "testing" + + "github.com/jmoiron/sqlx" +) + +func setupTowerRepo(t *testing.T) (*TowerRepository, *sqlx.DB, uint32, uint32) { + t.Helper() + db := SetupTestDB(t) + userID := CreateTestUser(t, db, "tower_test_user") + charID := CreateTestCharacter(t, db, userID, "TowerChar") + leaderID := CreateTestCharacter(t, db, userID, "GuildLeader") + guildID := CreateTestGuild(t, db, leaderID, "TowerGuild") + // Add charID to the guild + if _, err := db.Exec("INSERT INTO guild_characters (guild_id, character_id) VALUES ($1, $2)", guildID, charID); err != nil { + t.Fatalf("Failed to add char to guild: %v", err) + } + repo := NewTowerRepository(db) + t.Cleanup(func() { TeardownTestDB(t, db) }) + return repo, db, charID, guildID +} + +func TestRepoTowerGetTowerDataAutoCreate(t *testing.T) { + repo, _, charID, _ := setupTowerRepo(t) + + // First call should auto-create the row + td, err := repo.GetTowerData(charID) + if err != nil { + t.Fatalf("GetTowerData failed: %v", err) + } + if td.TR != 0 || td.TRP != 0 || td.TSP != 0 { + t.Errorf("Expected zero values, got TR=%d TRP=%d TSP=%d", td.TR, td.TRP, td.TSP) + } + if td.Skills == "" { + t.Error("Expected non-empty default skills CSV") + } +} + +func TestRepoTowerGetTowerDataExisting(t *testing.T) { + repo, db, charID, _ := setupTowerRepo(t) + + if _, err := db.Exec("INSERT INTO tower (char_id, tr, trp, tsp, block1, block2) VALUES ($1, 10, 20, 30, 40, 50)", charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + td, err := repo.GetTowerData(charID) + if err != nil { + t.Fatalf("GetTowerData failed: %v", err) + } + if td.TR != 10 || td.TRP != 20 || td.TSP != 30 || td.Block1 != 40 || td.Block2 != 50 { + t.Errorf("Expected 10/20/30/40/50, got %d/%d/%d/%d/%d", td.TR, td.TRP, td.TSP, td.Block1, td.Block2) + } +} + +func TestRepoTowerGetSkills(t *testing.T) { + repo, db, charID, _ := setupTowerRepo(t) + + if _, err := db.Exec("INSERT INTO tower (char_id, skills) VALUES ($1, '1,2,3')", charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + skills, err := repo.GetSkills(charID) + if err != nil { + t.Fatalf("GetSkills failed: %v", err) + } + if skills != "1,2,3" { + t.Errorf("Expected '1,2,3', got: %q", skills) + } +} + +func TestRepoTowerUpdateSkills(t *testing.T) { + repo, db, charID, _ := setupTowerRepo(t) + + if _, err := db.Exec("INSERT INTO tower (char_id, tsp) VALUES ($1, 100)", charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + if err := repo.UpdateSkills(charID, "5,10,15", 20); err != nil { + t.Fatalf("UpdateSkills failed: %v", err) + } + + var skills string + var tsp int32 + if err := db.QueryRow("SELECT skills, tsp FROM tower WHERE char_id=$1", charID).Scan(&skills, &tsp); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if skills != "5,10,15" { + t.Errorf("Expected skills='5,10,15', got: %q", skills) + } + if tsp != 80 { + t.Errorf("Expected tsp=80 (100-20), got: %d", tsp) + } +} + +func TestRepoTowerUpdateProgress(t *testing.T) { + repo, db, charID, _ := setupTowerRepo(t) + + if _, err := db.Exec("INSERT INTO tower (char_id) VALUES ($1)", charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + if err := repo.UpdateProgress(charID, 5, 10, 15, 20); err != nil { + t.Fatalf("UpdateProgress failed: %v", err) + } + + var tr, trp, tsp, block1 int32 + if err := db.QueryRow("SELECT tr, trp, tsp, block1 FROM tower WHERE char_id=$1", charID).Scan(&tr, &trp, &tsp, &block1); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if tr != 5 || trp != 10 || tsp != 15 || block1 != 20 { + t.Errorf("Expected 5/10/15/20, got %d/%d/%d/%d", tr, trp, tsp, block1) + } +} + +func TestRepoTowerGetGems(t *testing.T) { + repo, db, charID, _ := setupTowerRepo(t) + + if _, err := db.Exec("INSERT INTO tower (char_id, gems) VALUES ($1, '1,0,1')", charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + gems, err := repo.GetGems(charID) + if err != nil { + t.Fatalf("GetGems failed: %v", err) + } + if gems != "1,0,1" { + t.Errorf("Expected '1,0,1', got: %q", gems) + } +} + +func TestRepoTowerUpdateGems(t *testing.T) { + repo, db, charID, _ := setupTowerRepo(t) + + if _, err := db.Exec("INSERT INTO tower (char_id) VALUES ($1)", charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + if err := repo.UpdateGems(charID, "2,3,4"); err != nil { + t.Fatalf("UpdateGems failed: %v", err) + } + + var gems string + if err := db.QueryRow("SELECT gems FROM tower WHERE char_id=$1", charID).Scan(&gems); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if gems != "2,3,4" { + t.Errorf("Expected '2,3,4', got: %q", gems) + } +} + +func TestRepoTowerGetGuildTowerRP(t *testing.T) { + repo, _, _, guildID := setupTowerRepo(t) + + rp, err := repo.GetGuildTowerRP(guildID) + if err != nil { + t.Fatalf("GetGuildTowerRP failed: %v", err) + } + if rp != 0 { + t.Errorf("Expected rp=0, got: %d", rp) + } +} + +func TestRepoTowerDonateGuildTowerRP(t *testing.T) { + repo, _, _, guildID := setupTowerRepo(t) + + if err := repo.DonateGuildTowerRP(guildID, 100); err != nil { + t.Fatalf("DonateGuildTowerRP failed: %v", err) + } + + rp, err := repo.GetGuildTowerRP(guildID) + if err != nil { + t.Fatalf("GetGuildTowerRP failed: %v", err) + } + if rp != 100 { + t.Errorf("Expected rp=100, got: %d", rp) + } +} + +func TestRepoTowerGetGuildTowerPageAndRP(t *testing.T) { + repo, db, _, guildID := setupTowerRepo(t) + + if _, err := db.Exec("UPDATE guilds SET tower_mission_page=3, tower_rp=50 WHERE id=$1", guildID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + page, donated, err := repo.GetGuildTowerPageAndRP(guildID) + if err != nil { + t.Fatalf("GetGuildTowerPageAndRP failed: %v", err) + } + if page != 3 { + t.Errorf("Expected page=3, got: %d", page) + } + if donated != 50 { + t.Errorf("Expected donated=50, got: %d", donated) + } +} + +func TestRepoTowerAdvanceTenrouiraiPage(t *testing.T) { + repo, db, charID, guildID := setupTowerRepo(t) + + // Read initial page + var initialPage int + if err := db.QueryRow("SELECT tower_mission_page FROM guilds WHERE id=$1", guildID).Scan(&initialPage); err != nil { + t.Fatalf("Read initial page failed: %v", err) + } + + // Set initial mission scores + if _, err := db.Exec("UPDATE guild_characters SET tower_mission_1=10, tower_mission_2=20, tower_mission_3=30 WHERE character_id=$1", charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + if err := repo.AdvanceTenrouiraiPage(guildID); err != nil { + t.Fatalf("AdvanceTenrouiraiPage failed: %v", err) + } + + var page int + if err := db.QueryRow("SELECT tower_mission_page FROM guilds WHERE id=$1", guildID).Scan(&page); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if page != initialPage+1 { + t.Errorf("Expected page=%d (initial+1), got: %d", initialPage+1, page) + } + + // Mission scores should be reset + var m1, m2, m3 *int + if err := db.QueryRow("SELECT tower_mission_1, tower_mission_2, tower_mission_3 FROM guild_characters WHERE character_id=$1", charID).Scan(&m1, &m2, &m3); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if m1 != nil || m2 != nil || m3 != nil { + t.Errorf("Expected NULL missions after advance, got: %v/%v/%v", m1, m2, m3) + } +} + +func TestRepoTowerGetTenrouiraiProgress(t *testing.T) { + repo, db, charID, guildID := setupTowerRepo(t) + + if _, err := db.Exec("UPDATE guilds SET tower_mission_page=2 WHERE id=$1", guildID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + if _, err := db.Exec("UPDATE guild_characters SET tower_mission_1=5, tower_mission_2=10, tower_mission_3=15 WHERE character_id=$1", charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + progress, err := repo.GetTenrouiraiProgress(guildID) + if err != nil { + t.Fatalf("GetTenrouiraiProgress failed: %v", err) + } + if progress.Page != 2 { + t.Errorf("Expected page=2, got: %d", progress.Page) + } + if progress.Mission1 != 5 { + t.Errorf("Expected mission1=5, got: %d", progress.Mission1) + } +} + +func TestRepoTowerGetTenrouiraiMissionScores(t *testing.T) { + repo, db, charID, guildID := setupTowerRepo(t) + + if _, err := db.Exec("UPDATE guild_characters SET tower_mission_1=42 WHERE character_id=$1", charID); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + scores, err := repo.GetTenrouiraiMissionScores(guildID, 1) + if err != nil { + t.Fatalf("GetTenrouiraiMissionScores failed: %v", err) + } + if len(scores) < 1 { + t.Fatal("Expected at least 1 score entry") + } + if scores[0].Score != 42 { + t.Errorf("Expected score=42, got: %d", scores[0].Score) + } +} diff --git a/server/channelserver/testhelpers_db.go b/server/channelserver/testhelpers_db.go index 4c4310deb..5fb6f36ed 100644 --- a/server/channelserver/testhelpers_db.go +++ b/server/channelserver/testhelpers_db.go @@ -243,6 +243,89 @@ func CreateTestGuild(t *testing.T, db *sqlx.DB, leaderCharID uint32, name string return guildID } +// CreateTestSignSession creates a sign session and returns the session ID. +func CreateTestSignSession(t *testing.T, db *sqlx.DB, userID uint32, token string) uint32 { + t.Helper() + + var id uint32 + err := db.QueryRow( + `INSERT INTO sign_sessions (user_id, token) VALUES ($1, $2) RETURNING id`, + userID, token, + ).Scan(&id) + if err != nil { + t.Fatalf("Failed to create test sign session: %v", err) + } + return id +} + +// CreateTestServer creates a server entry for testing. +func CreateTestServer(t *testing.T, db *sqlx.DB, serverID uint16) { + t.Helper() + + _, err := db.Exec( + `INSERT INTO servers (server_id, current_players) VALUES ($1, 0)`, + serverID, + ) + if err != nil { + t.Fatalf("Failed to create test server: %v", err) + } +} + +// CreateTestUserBinary creates a user_binary row for the given character ID. +func CreateTestUserBinary(t *testing.T, db *sqlx.DB, charID uint32) { + t.Helper() + + _, err := db.Exec(`INSERT INTO user_binary (id) VALUES ($1)`, charID) + if err != nil { + t.Fatalf("Failed to create test user_binary: %v", err) + } +} + +// CreateTestGachaShop creates a gacha shop entry and returns its ID. +func CreateTestGachaShop(t *testing.T, db *sqlx.DB, name string, gachaType int) uint32 { + t.Helper() + + var id uint32 + err := db.QueryRow( + `INSERT INTO gacha_shop (name, gacha_type, min_gr, min_hr, url_banner, url_feature, url_thumbnail, wide, recommended, hidden) + VALUES ($1, $2, 0, 0, '', '', '', false, false, false) RETURNING id`, + name, gachaType, + ).Scan(&id) + if err != nil { + t.Fatalf("Failed to create test gacha shop: %v", err) + } + return id +} + +// CreateTestGachaEntry creates a gacha entry and returns its ID. +func CreateTestGachaEntry(t *testing.T, db *sqlx.DB, gachaID uint32, entryType int, weight int) uint32 { + t.Helper() + + var id uint32 + err := db.QueryRow( + `INSERT INTO gacha_entries (gacha_id, entry_type, weight, rarity, item_type, item_number, item_quantity, rolls, frontier_points, daily_limit) + VALUES ($1, $2, $3, 1, 0, 0, 0, 1, 0, 0) RETURNING id`, + gachaID, entryType, weight, + ).Scan(&id) + if err != nil { + t.Fatalf("Failed to create test gacha entry: %v", err) + } + return id +} + +// CreateTestGachaItem creates a gacha item for an entry. +func CreateTestGachaItem(t *testing.T, db *sqlx.DB, entryID uint32, itemType uint8, itemID uint16, quantity uint16) { + t.Helper() + + _, err := db.Exec( + `INSERT INTO gacha_items (entry_id, item_type, item_id, quantity) VALUES ($1, $2, $3, $4)`, + entryID, itemType, itemID, quantity, + ) + if err != nil { + t.Fatalf("Failed to create test gacha item: %v", err) + } +} + // SetTestDB assigns a database to a Server and initializes all repositories. // Use this in integration tests instead of setting s.server.db directly. func SetTestDB(s *Server, db *sqlx.DB) {