test(repos): add SQL integration tests for 17 untested repo files

Add 148 integration tests exercising actual SQL against PostgreSQL for
all previously untested repository files. Includes 6 new fixture helpers
in testhelpers_db.go and CI PostgreSQL service configuration.

Discovered and documented existing RecordPurchase SQL bug (ambiguous
column reference in ON CONFLICT clause).
This commit is contained in:
Houmgaor
2026-02-24 16:57:47 +01:00
parent c5fd0444f4
commit f9d4252860
19 changed files with 3309 additions and 0 deletions

View File

@@ -29,6 +29,22 @@ jobs:
name: Test name: Test
runs-on: ubuntu-latest 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: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -42,6 +58,12 @@ jobs:
- name: Run Tests with Race Detector and Coverage - name: Run Tests with Race Detector and Coverage
run: go test -race -coverprofile=coverage.out ./... -timeout=10m 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 - name: Check coverage threshold
run: | run: |

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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])
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -243,6 +243,89 @@ func CreateTestGuild(t *testing.T, db *sqlx.DB, leaderCharID uint32, name string
return guildID 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. // SetTestDB assigns a database to a Server and initializes all repositories.
// Use this in integration tests instead of setting s.server.db directly. // Use this in integration tests instead of setting s.server.db directly.
func SetTestDB(s *Server, db *sqlx.DB) { func SetTestDB(s *Server, db *sqlx.DB) {