Files
Erupe/server/api/endpoints_coverage_test.go
Houmgaor 6f7852cc12 feat(api): add GET /v2/server/info endpoint for launcher compatibility
Exposes the server's configured client mode in a form that mhf-outpost
and other launcher tools can consume to warn users when their local game
version does not match what the server expects.

Returns clientMode (raw, e.g. "G9.1") and manifestId (normalized for
mhf-outpost: lowercase, dots stripped, e.g. "g91"). No auth required.
2026-03-23 19:55:55 +01:00

446 lines
10 KiB
Go

package api
import (
"bytes"
"database/sql"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
cfg "erupe-ce/config"
"golang.org/x/crypto/bcrypt"
)
func TestVersionEndpoint(t *testing.T) {
logger := NewTestLogger(t)
c := NewTestConfig()
c.ClientMode = "ZZ"
server := &APIServer{
logger: logger,
erupeConfig: c,
}
req := httptest.NewRequest("GET", "/version", nil)
rec := httptest.NewRecorder()
server.Version(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want 200", rec.Code)
}
var resp VersionResponse
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("decode error: %v", err)
}
if resp.ClientMode != "ZZ" {
t.Errorf("ClientMode = %q, want ZZ", resp.ClientMode)
}
if resp.Name != "Erupe-CE" {
t.Errorf("Name = %q, want Erupe-CE", resp.Name)
}
}
func TestServerInfoEndpoint(t *testing.T) {
tests := []struct {
clientMode string
wantID string
}{
{"ZZ", "zz"},
{"GG", "gg"},
{"G10.1", "g101"},
{"G9.1", "g91"},
{"FW.5", "fw5"},
}
for _, tt := range tests {
t.Run(tt.clientMode, func(t *testing.T) {
logger := NewTestLogger(t)
c := NewTestConfig()
c.ClientMode = tt.clientMode
server := &APIServer{
logger: logger,
erupeConfig: c,
}
req := httptest.NewRequest("GET", "/v2/server/info", nil)
rec := httptest.NewRecorder()
server.ServerInfo(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want 200", rec.Code)
}
if ct := rec.Header().Get("Content-Type"); ct != "application/json" {
t.Errorf("Content-Type = %q, want application/json", ct)
}
var resp ServerInfoResponse
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("decode error: %v", err)
}
if resp.ClientMode != tt.clientMode {
t.Errorf("ClientMode = %q, want %q", resp.ClientMode, tt.clientMode)
}
if resp.ManifestID != tt.wantID {
t.Errorf("ManifestID = %q, want %q", resp.ManifestID, tt.wantID)
}
if resp.Name != "Erupe-CE" {
t.Errorf("Name = %q, want Erupe-CE", resp.Name)
}
})
}
}
func TestLandingPageEndpoint_Enabled(t *testing.T) {
logger := NewTestLogger(t)
c := NewTestConfig()
c.API.LandingPage = cfg.LandingPage{
Enabled: true,
Title: "Test Server",
Content: "<p>Welcome</p>",
}
server := &APIServer{
logger: logger,
erupeConfig: c,
}
req := httptest.NewRequest("GET", "/", nil)
rec := httptest.NewRecorder()
server.LandingPage(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want 200", rec.Code)
}
if ct := rec.Header().Get("Content-Type"); ct != "text/html; charset=utf-8" {
t.Errorf("Content-Type = %q", ct)
}
}
func TestLandingPageEndpoint_Disabled(t *testing.T) {
logger := NewTestLogger(t)
c := NewTestConfig()
c.API.LandingPage = cfg.LandingPage{Enabled: false}
server := &APIServer{
logger: logger,
erupeConfig: c,
}
req := httptest.NewRequest("GET", "/", nil)
rec := httptest.NewRecorder()
server.LandingPage(rec, req)
if rec.Code != http.StatusNotFound {
t.Errorf("status = %d, want 404", rec.Code)
}
}
func TestLoginEndpoint_Success(t *testing.T) {
logger := NewTestLogger(t)
c := NewTestConfig()
hash, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.MinCost)
server := &APIServer{
logger: logger,
erupeConfig: c,
userRepo: &mockAPIUserRepo{
credentialsID: 1,
credentialsPassword: string(hash),
credentialsRights: 30,
lastLogin: time.Now(),
returnExpiry: time.Now().Add(time.Hour * 24 * 30),
},
sessionRepo: &mockAPISessionRepo{
createTokenID: 42,
},
charRepo: &mockAPICharacterRepo{
characters: []Character{
{ID: 1, Name: "TestHunter", HR: 100},
},
},
}
body, _ := json.Marshal(map[string]string{
"username": "testuser",
"password": "password123",
})
req := httptest.NewRequest("POST", "/login", bytes.NewReader(body))
rec := httptest.NewRecorder()
server.Login(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want 200", rec.Code)
}
var resp AuthData
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("decode error: %v", err)
}
if resp.User.TokenID != 42 {
t.Errorf("TokenID = %d, want 42", resp.User.TokenID)
}
if len(resp.Characters) != 1 {
t.Errorf("Characters count = %d, want 1", len(resp.Characters))
}
}
func TestLoginEndpoint_UsernameNotFound(t *testing.T) {
logger := NewTestLogger(t)
c := NewTestConfig()
server := &APIServer{
logger: logger,
erupeConfig: c,
userRepo: &mockAPIUserRepo{
credentialsErr: sql.ErrNoRows,
},
}
body, _ := json.Marshal(map[string]string{
"username": "nonexistent",
"password": "password123",
})
req := httptest.NewRequest("POST", "/login", bytes.NewReader(body))
rec := httptest.NewRecorder()
server.Login(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("status = %d, want 400", rec.Code)
}
var errResp ErrorResponse
if err := json.NewDecoder(rec.Body).Decode(&errResp); err != nil {
t.Fatalf("decode error: %v", err)
}
if errResp.Error != "invalid_username" {
t.Errorf("error = %q, want invalid_username", errResp.Error)
}
}
func TestLoginEndpoint_WrongPassword(t *testing.T) {
logger := NewTestLogger(t)
c := NewTestConfig()
hash, _ := bcrypt.GenerateFromPassword([]byte("correct"), bcrypt.MinCost)
server := &APIServer{
logger: logger,
erupeConfig: c,
userRepo: &mockAPIUserRepo{
credentialsID: 1,
credentialsPassword: string(hash),
lastLogin: time.Now(),
returnExpiry: time.Now().Add(time.Hour * 24 * 30),
},
}
body, _ := json.Marshal(map[string]string{
"username": "testuser",
"password": "wrongpassword",
})
req := httptest.NewRequest("POST", "/login", bytes.NewReader(body))
rec := httptest.NewRecorder()
server.Login(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("status = %d, want 400", rec.Code)
}
var errResp ErrorResponse
if err := json.NewDecoder(rec.Body).Decode(&errResp); err != nil {
t.Fatalf("decode error: %v", err)
}
if errResp.Error != "invalid_password" {
t.Errorf("error = %q, want invalid_password", errResp.Error)
}
}
func TestRegisterEndpoint_Success(t *testing.T) {
logger := NewTestLogger(t)
c := NewTestConfig()
server := &APIServer{
logger: logger,
erupeConfig: c,
userRepo: &mockAPIUserRepo{
registerID: 1,
registerRights: 30,
lastLogin: time.Now(),
returnExpiry: time.Now().Add(time.Hour * 24 * 30),
},
sessionRepo: &mockAPISessionRepo{
createTokenID: 10,
},
charRepo: &mockAPICharacterRepo{},
}
body, _ := json.Marshal(map[string]string{
"username": "newuser",
"password": "password123",
})
req := httptest.NewRequest("POST", "/register", bytes.NewReader(body))
rec := httptest.NewRecorder()
server.Register(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want 200", rec.Code)
}
var resp AuthData
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("decode error: %v", err)
}
if resp.User.Rights != 30 {
t.Errorf("Rights = %d, want 30", resp.User.Rights)
}
}
func TestCreateCharacterEndpoint_Success(t *testing.T) {
logger := NewTestLogger(t)
c := NewTestConfig()
server := &APIServer{
logger: logger,
erupeConfig: c,
sessionRepo: &mockAPISessionRepo{
userID: 1,
},
charRepo: &mockAPICharacterRepo{
newCharacter: Character{ID: 5, Name: "NewChar"},
},
}
body, _ := json.Marshal(map[string]string{
"token": "valid-token",
})
req := httptest.NewRequest("POST", "/character/create", bytes.NewReader(body))
rec := httptest.NewRecorder()
server.CreateCharacter(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want 200", rec.Code)
}
}
func TestCreateCharacterEndpoint_InvalidToken(t *testing.T) {
logger := NewTestLogger(t)
c := NewTestConfig()
server := &APIServer{
logger: logger,
erupeConfig: c,
sessionRepo: &mockAPISessionRepo{
userIDErr: sql.ErrNoRows,
},
}
body, _ := json.Marshal(map[string]string{
"token": "invalid",
})
req := httptest.NewRequest("POST", "/character/create", bytes.NewReader(body))
rec := httptest.NewRecorder()
server.CreateCharacter(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("status = %d, want 401", rec.Code)
}
}
func TestDeleteCharacterEndpoint_NewChar(t *testing.T) {
logger := NewTestLogger(t)
c := NewTestConfig()
server := &APIServer{
logger: logger,
erupeConfig: c,
sessionRepo: &mockAPISessionRepo{
userID: 1,
},
charRepo: &mockAPICharacterRepo{
isNewResult: true,
},
}
body, _ := json.Marshal(map[string]interface{}{
"token": "valid-token",
"charId": 5,
})
req := httptest.NewRequest("POST", "/character/delete", bytes.NewReader(body))
rec := httptest.NewRecorder()
server.DeleteCharacter(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want 200", rec.Code)
}
}
func TestDeleteCharacterEndpoint_FinalizedChar(t *testing.T) {
logger := NewTestLogger(t)
c := NewTestConfig()
server := &APIServer{
logger: logger,
erupeConfig: c,
sessionRepo: &mockAPISessionRepo{
userID: 1,
},
charRepo: &mockAPICharacterRepo{
isNewResult: false,
},
}
body, _ := json.Marshal(map[string]interface{}{
"token": "valid-token",
"charId": 5,
})
req := httptest.NewRequest("POST", "/character/delete", bytes.NewReader(body))
rec := httptest.NewRecorder()
server.DeleteCharacter(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want 200", rec.Code)
}
}
func TestExportSaveEndpoint_Success(t *testing.T) {
logger := NewTestLogger(t)
c := NewTestConfig()
server := &APIServer{
logger: logger,
erupeConfig: c,
sessionRepo: &mockAPISessionRepo{
userID: 1,
},
charRepo: &mockAPICharacterRepo{
exportResult: map[string]interface{}{
"name": "TestHunter",
"hr": 100,
},
},
}
body, _ := json.Marshal(map[string]interface{}{
"token": "valid-token",
"charId": 1,
})
req := httptest.NewRequest("POST", "/character/export", bytes.NewReader(body))
rec := httptest.NewRecorder()
server.ExportSave(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want 200", rec.Code)
}
var resp ExportData
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("decode error: %v", err)
}
if resp.Character["name"] != "TestHunter" {
t.Errorf("character name = %v, want TestHunter", resp.Character["name"])
}
}