mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 23:54:33 +01:00
fix(repo): detect silent save failures + partial daily mission stubs
SaveColumn and SaveMercenary now check RowsAffected() and return a wrapped ErrCharacterNotFound when 0 rows are updated, preventing silent data loss when a character ID is missing or mismatched. AdjustInt already detects this via its RETURNING scan — no change. Daily mission packet structs (Get/SetDailyMission*) now parse the AckHandle instead of returning NOT IMPLEMENTED, letting handlers send empty-list success ACKs and avoiding client softlocks. Also adds tests for dashboard stats endpoint and for five guild repo methods (SetAllianceRecruiting, RolloverDailyRP, AddWeeklyBonusUsers, InsertKillLog, ClearTreasureHunt) that had no coverage.
This commit is contained in:
153
server/api/dashboard_test.go
Normal file
153
server/api/dashboard_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestDashboardStatsJSON_NoDB verifies the stats endpoint returns valid JSON
|
||||
// with safe zero values when no database is configured.
|
||||
func TestDashboardStatsJSON_NoDB(t *testing.T) {
|
||||
logger := NewTestLogger(t)
|
||||
defer func() { _ = logger.Sync() }()
|
||||
|
||||
server := &APIServer{
|
||||
logger: logger,
|
||||
erupeConfig: NewTestConfig(),
|
||||
startTime: time.Now().Add(-5 * time.Minute),
|
||||
// db intentionally nil
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/dashboard/stats", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
server.DashboardStatsJSON(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
ct := rec.Header().Get("Content-Type")
|
||||
if !strings.HasPrefix(ct, "application/json") {
|
||||
t.Errorf("Expected Content-Type application/json, got %q", ct)
|
||||
}
|
||||
|
||||
var stats DashboardStats
|
||||
if err := json.NewDecoder(rec.Body).Decode(&stats); err != nil {
|
||||
t.Fatalf("Failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
// Verify required fields are present and have expected zero-DB values.
|
||||
if stats.ServerVersion == "" {
|
||||
t.Error("Expected non-empty ServerVersion")
|
||||
}
|
||||
if stats.Uptime == "" || stats.Uptime == "unknown" {
|
||||
// startTime is set so uptime should be computed, not "unknown".
|
||||
t.Errorf("Expected computed uptime, got %q", stats.Uptime)
|
||||
}
|
||||
if stats.TotalAccounts != 0 {
|
||||
t.Errorf("Expected TotalAccounts=0 without DB, got %d", stats.TotalAccounts)
|
||||
}
|
||||
if stats.TotalCharacters != 0 {
|
||||
t.Errorf("Expected TotalCharacters=0 without DB, got %d", stats.TotalCharacters)
|
||||
}
|
||||
if stats.OnlinePlayers != 0 {
|
||||
t.Errorf("Expected OnlinePlayers=0 without DB, got %d", stats.OnlinePlayers)
|
||||
}
|
||||
if stats.DatabaseOK {
|
||||
t.Error("Expected DatabaseOK=false without DB")
|
||||
}
|
||||
if stats.Channels != nil {
|
||||
t.Errorf("Expected nil Channels without DB, got %v", stats.Channels)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDashboardStatsJSON_UptimeUnknown verifies "unknown" uptime when startTime is zero.
|
||||
func TestDashboardStatsJSON_UptimeUnknown(t *testing.T) {
|
||||
logger := NewTestLogger(t)
|
||||
defer func() { _ = logger.Sync() }()
|
||||
|
||||
server := &APIServer{
|
||||
logger: logger,
|
||||
erupeConfig: NewTestConfig(),
|
||||
// startTime is zero value
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/dashboard/stats", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
server.DashboardStatsJSON(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
var stats DashboardStats
|
||||
if err := json.NewDecoder(rec.Body).Decode(&stats); err != nil {
|
||||
t.Fatalf("Failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if stats.Uptime != "unknown" {
|
||||
t.Errorf("Expected Uptime='unknown' for zero startTime, got %q", stats.Uptime)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDashboardStatsJSON_JSONShape validates every field of the DashboardStats payload.
|
||||
func TestDashboardStatsJSON_JSONShape(t *testing.T) {
|
||||
logger := NewTestLogger(t)
|
||||
defer func() { _ = logger.Sync() }()
|
||||
|
||||
server := &APIServer{
|
||||
logger: logger,
|
||||
erupeConfig: NewTestConfig(),
|
||||
startTime: time.Now(),
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/dashboard/stats", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
server.DashboardStatsJSON(rec, req)
|
||||
|
||||
// Decode into a raw map so we can check key presence independent of type.
|
||||
var raw map[string]interface{}
|
||||
if err := json.NewDecoder(rec.Body).Decode(&raw); err != nil {
|
||||
t.Fatalf("Failed to decode response as raw map: %v", err)
|
||||
}
|
||||
|
||||
requiredKeys := []string{
|
||||
"uptime", "serverVersion", "clientMode",
|
||||
"onlinePlayers", "totalAccounts", "totalCharacters",
|
||||
"databaseOK",
|
||||
}
|
||||
for _, key := range requiredKeys {
|
||||
if _, ok := raw[key]; !ok {
|
||||
t.Errorf("Missing required JSON key %q", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFormatDuration covers the human-readable duration formatter.
|
||||
func TestFormatDuration(t *testing.T) {
|
||||
tests := []struct {
|
||||
d time.Duration
|
||||
want string
|
||||
}{
|
||||
{10 * time.Second, "10s"},
|
||||
{90 * time.Second, "1m 30s"},
|
||||
{2*time.Hour + 15*time.Minute + 5*time.Second, "2h 15m 5s"},
|
||||
{25*time.Hour + 3*time.Minute + 0*time.Second, "1d 1h 3m 0s"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.want, func(t *testing.T) {
|
||||
got := formatDuration(tt.d)
|
||||
if got != tt.want {
|
||||
t.Errorf("formatDuration(%v) = %q, want %q", tt.d, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user