Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@

binary_data/*
.DS_Store

.mongo-data
38 changes: 27 additions & 11 deletions restapi/restapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,18 @@ func sendError(w http.ResponseWriter, statusCode int, message string) {
sendJSON(w, statusCode, map[string]string{"error": message})
}

// resolveBandDisplayName mirrors in-game JSON leaderboard naming:
// band name → "<username>'s Band" → "Unnamed Band".
func resolveBandDisplayName(bandName, username string) string {
if bandName != "" {
return bandName
}
if username != "" {
return username + "'s Band"
}
return "Unnamed Band"
}

// Handles the health check endpoint to verify if the database is reachable.
// If somehow the DB has gone down, this will return a 503 Service Unavailable status so clients know that the service is not operational.
func HealthHandler(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -481,14 +493,20 @@ func LeaderboardHandler(w http.ResponseWriter, r *http.Request) {
}
}

// fetch all names at onnce in a single shot
// fetch all names at once in a single shot
ctx := context.TODO()
bandNameMap, err := database.GetBandNamesByOwnerPIDs(ctx, database.GocentralDatabase, bandPIDs)
if err != nil {
log.Println("Error fetching band names:", err)
bandNameMap = make(map[int]string)
}

bandOwnerUsernames, err := database.GetUsernamesByPIDs(ctx, database.GocentralDatabase, bandPIDs)
if err != nil {
log.Println("Error fetching band owner usernames:", err)
bandOwnerUsernames = make(map[int]string)
}

userNameMap, err := database.GetConsolePrefixedUsernamesByPIDs(ctx, database.GocentralDatabase, userPIDs)
if err != nil {
log.Println("Error fetching usernames:", err)
Expand All @@ -505,11 +523,7 @@ func LeaderboardHandler(w http.ResponseWriter, r *http.Request) {
var entryName string

if isBandScore {
if name, ok := bandNameMap[score.OwnerPID]; ok {
entryName = name
} else {
entryName = "Unnamed Band"
}
entryName = resolveBandDisplayName(bandNameMap[score.OwnerPID], bandOwnerUsernames[score.OwnerPID])
} else {
if name, ok := userNameMap[score.OwnerPID]; ok {
entryName = name
Expand Down Expand Up @@ -620,6 +634,12 @@ func BattleLeaderboardHandler(w http.ResponseWriter, r *http.Request) {
bandNameMap = make(map[int]string)
}

bandOwnerUsernames, err := database.GetUsernamesByPIDs(ctx, database.GocentralDatabase, bandPIDs)
if err != nil {
log.Println("Error fetching band owner usernames:", err)
bandOwnerUsernames = make(map[int]string)
}

userNameMap, err := database.GetConsolePrefixedUsernamesByPIDs(ctx, database.GocentralDatabase, userPIDs)
if err != nil {
log.Println("Error fetching usernames:", err)
Expand All @@ -634,11 +654,7 @@ func BattleLeaderboardHandler(w http.ResponseWriter, r *http.Request) {
var entryName string

if isBandScore {
if name, ok := bandNameMap[score.OwnerPID]; ok {
entryName = name
} else {
entryName = "Unnamed Band"
}
entryName = resolveBandDisplayName(bandNameMap[score.OwnerPID], bandOwnerUsernames[score.OwnerPID])
} else {
if name, ok := userNameMap[score.OwnerPID]; ok {
entryName = name
Expand Down
160 changes: 160 additions & 0 deletions tests/rest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,166 @@ func TestBattleLeaderboardHandler_MissingBattleID(t *testing.T) {
}
}

// Tests band display-name fallbacks on the song leaderboard (role_id 10).
func TestLeaderboardHandler_BandNameFallback(t *testing.T) {
ctx := context.Background()
scoresCollection := database.GocentralDatabase.Collection("scores")
usersCollection := database.GocentralDatabase.Collection("users")
bandsCollection := database.GocentralDatabase.Collection("bands")

testSongID := 777001
namedOwnerPID := 777010
noBandOwnerPID := 777011
emptyBandOwnerPID := 777012
orphanOwnerPID := 777013

usersCollection.InsertOne(ctx, map[string]interface{}{
"pid": namedOwnerPID, "username": "named_owner", "console_type": 1,
})
usersCollection.InsertOne(ctx, map[string]interface{}{
"pid": noBandOwnerPID, "username": "noband_owner", "console_type": 1,
})
usersCollection.InsertOne(ctx, map[string]interface{}{
"pid": emptyBandOwnerPID, "username": "emptyband_owner", "console_type": 1,
})

bandsCollection.InsertOne(ctx, map[string]interface{}{
"owner_pid": namedOwnerPID, "band_id": 777100, "name": "The Real Band", "console_type": 1,
})
bandsCollection.InsertOne(ctx, map[string]interface{}{
"owner_pid": emptyBandOwnerPID, "band_id": 777101, "name": "", "console_type": 1,
})

scores := []map[string]interface{}{
{"pid": namedOwnerPID, "song_id": testSongID, "role_id": 10, "score": 4000, "stars": 5, "diff_id": 2, "notespct": 95, "instrument_mask": 0},
{"pid": noBandOwnerPID, "song_id": testSongID, "role_id": 10, "score": 3000, "stars": 5, "diff_id": 2, "notespct": 95, "instrument_mask": 0},
{"pid": emptyBandOwnerPID, "song_id": testSongID, "role_id": 10, "score": 2000, "stars": 5, "diff_id": 2, "notespct": 95, "instrument_mask": 0},
{"pid": orphanOwnerPID, "song_id": testSongID, "role_id": 10, "score": 1000, "stars": 5, "diff_id": 2, "notespct": 95, "instrument_mask": 0},
}
for _, score := range scores {
scoresCollection.InsertOne(ctx, score)
}

defer func() {
scoresCollection.DeleteMany(ctx, bson.M{"song_id": testSongID})
bandsCollection.DeleteMany(ctx, bson.M{"owner_pid": bson.M{"$in": []int{namedOwnerPID, emptyBandOwnerPID}}})
usersCollection.DeleteMany(ctx, bson.M{"pid": bson.M{"$in": []int{namedOwnerPID, noBandOwnerPID, emptyBandOwnerPID}}})
}()

req := httptest.NewRequest("GET", "/leaderboard?song_id=777001&role_id=10", nil)
rr := httptest.NewRecorder()
restapi.LeaderboardHandler(rr, req)

if rr.Code != http.StatusOK {
t.Fatalf("Expected status 200, got %d (body: %s)", rr.Code, rr.Body.String())
}

var response map[string][]restapi.LeaderboardEntry
decodeResponse(t, rr, &response)
leaderboard := response["leaderboard"]
if len(leaderboard) != 4 {
t.Fatalf("Expected 4 entries, got %d", len(leaderboard))
}

byPID := map[int]string{}
for _, entry := range leaderboard {
byPID[entry.PID] = entry.Name
}

if byPID[namedOwnerPID] != "The Real Band" {
t.Errorf("named band: got %q, want %q", byPID[namedOwnerPID], "The Real Band")
}
if byPID[noBandOwnerPID] != "noband_owner's Band" {
t.Errorf("missing band: got %q, want %q", byPID[noBandOwnerPID], "noband_owner's Band")
}
if byPID[emptyBandOwnerPID] != "emptyband_owner's Band" {
t.Errorf("empty band name: got %q, want %q", byPID[emptyBandOwnerPID], "emptyband_owner's Band")
}
if byPID[orphanOwnerPID] != "Unnamed Band" {
t.Errorf("orphan pid: got %q, want %q", byPID[orphanOwnerPID], "Unnamed Band")
}
}

// Tests band display-name fallbacks on the battle leaderboard (role_id 10).
func TestBattleLeaderboardHandler_BandNameFallback(t *testing.T) {
ctx := context.Background()
scoresCollection := database.GocentralDatabase.Collection("scores")
usersCollection := database.GocentralDatabase.Collection("users")
bandsCollection := database.GocentralDatabase.Collection("bands")

testBattleID := 777002
namedOwnerPID := 777020
noBandOwnerPID := 777021
emptyBandOwnerPID := 777022
orphanOwnerPID := 777023

usersCollection.InsertOne(ctx, map[string]interface{}{
"pid": namedOwnerPID, "username": "battle_named", "console_type": 1,
})
usersCollection.InsertOne(ctx, map[string]interface{}{
"pid": noBandOwnerPID, "username": "battle_noband", "console_type": 1,
})
usersCollection.InsertOne(ctx, map[string]interface{}{
"pid": emptyBandOwnerPID, "username": "battle_empty", "console_type": 1,
})

bandsCollection.InsertOne(ctx, map[string]interface{}{
"owner_pid": namedOwnerPID, "band_id": 777200, "name": "Battle Band", "console_type": 1,
})
bandsCollection.InsertOne(ctx, map[string]interface{}{
"owner_pid": emptyBandOwnerPID, "band_id": 777201, "name": "", "console_type": 1,
})

scores := []map[string]interface{}{
{"pid": namedOwnerPID, "battle_id": testBattleID, "song_id": 100, "role_id": 10, "score": 4000, "stars": 5, "diff_id": 2, "notespct": 95, "instrument_mask": 0},
{"pid": noBandOwnerPID, "battle_id": testBattleID, "song_id": 100, "role_id": 10, "score": 3000, "stars": 5, "diff_id": 2, "notespct": 95, "instrument_mask": 0},
{"pid": emptyBandOwnerPID, "battle_id": testBattleID, "song_id": 100, "role_id": 10, "score": 2000, "stars": 5, "diff_id": 2, "notespct": 95, "instrument_mask": 0},
{"pid": orphanOwnerPID, "battle_id": testBattleID, "song_id": 100, "role_id": 10, "score": 1000, "stars": 5, "diff_id": 2, "notespct": 95, "instrument_mask": 0},
}
for _, score := range scores {
scoresCollection.InsertOne(ctx, score)
}

defer func() {
scoresCollection.DeleteMany(ctx, bson.M{"battle_id": testBattleID})
bandsCollection.DeleteMany(ctx, bson.M{"owner_pid": bson.M{"$in": []int{namedOwnerPID, emptyBandOwnerPID}}})
usersCollection.DeleteMany(ctx, bson.M{"pid": bson.M{"$in": []int{namedOwnerPID, noBandOwnerPID, emptyBandOwnerPID}}})
}()

req := httptest.NewRequest("GET", "/battle_leaderboard?battle_id=777002", nil)
rr := httptest.NewRecorder()
restapi.BattleLeaderboardHandler(rr, req)

if rr.Code != http.StatusOK {
t.Fatalf("Expected status 200, got %d (body: %s)", rr.Code, rr.Body.String())
}

var response map[string][]restapi.BattleLeaderboardEntry
decodeResponse(t, rr, &response)
leaderboard := response["leaderboard"]
if len(leaderboard) != 4 {
t.Fatalf("Expected 4 entries, got %d", len(leaderboard))
}

byPID := map[int]string{}
for _, entry := range leaderboard {
byPID[entry.PID] = entry.Name
}

if byPID[namedOwnerPID] != "Battle Band" {
t.Errorf("named band: got %q, want %q", byPID[namedOwnerPID], "Battle Band")
}
if byPID[noBandOwnerPID] != "battle_noband's Band" {
t.Errorf("missing band: got %q, want %q", byPID[noBandOwnerPID], "battle_noband's Band")
}
if byPID[emptyBandOwnerPID] != "battle_empty's Band" {
t.Errorf("empty band name: got %q, want %q", byPID[emptyBandOwnerPID], "battle_empty's Band")
}
if byPID[orphanOwnerPID] != "Unnamed Band" {
t.Errorf("orphan pid: got %q, want %q", byPID[orphanOwnerPID], "Unnamed Band")
}
}

// Tests the list banned players endpoint
func TestListBannedPlayersHandler(t *testing.T) {
rr := makeRequest(t, "GET", "/admin/bans", nil, restapi.ListBannedPlayersHandler)
Expand Down