package api import ( "bytes" "encoding/json" "encoding/xml" "net/http" "net/http/httptest" "strings" "testing" cfg "erupe-ce/config" "erupe-ce/common/gametime" "go.uber.org/zap" ) // TestLauncherEndpoint tests the /launcher endpoint func TestLauncherEndpoint(t *testing.T) { logger := NewTestLogger(t) defer func() { _ = logger.Sync() }() c := NewTestConfig() c.API.Banners = []cfg.APISignBanner{ {Src: "http://example.com/banner1.jpg", Link: "http://example.com"}, } c.API.Messages = []cfg.APISignMessage{ {Message: "Welcome to Erupe", Date: 0, Kind: 0, Link: "http://example.com"}, } c.API.Links = []cfg.APISignLink{ {Name: "Forum", Icon: "forum", Link: "http://forum.example.com"}, } server := &APIServer{ logger: logger, erupeConfig: c, db: nil, } // Create test request req, err := http.NewRequest("GET", "/launcher", nil) if err != nil { t.Fatalf("Failed to create request: %v", err) } // Create response recorder recorder := httptest.NewRecorder() // Call handler server.Launcher(recorder, req) // Check response status if recorder.Code != http.StatusOK { t.Errorf("Handler returned wrong status code: got %v want %v", recorder.Code, http.StatusOK) } // Check Content-Type header if contentType := recorder.Header().Get("Content-Type"); contentType != "application/json" { t.Errorf("Content-Type header = %v, want application/json", contentType) } // Parse response var respData LauncherResponse if err := json.NewDecoder(recorder.Body).Decode(&respData); err != nil { t.Fatalf("Failed to decode response: %v", err) } // Verify response content if len(respData.Banners) != 1 { t.Errorf("Number of banners = %d, want 1", len(respData.Banners)) } if len(respData.Messages) != 1 { t.Errorf("Number of messages = %d, want 1", len(respData.Messages)) } if len(respData.Links) != 1 { t.Errorf("Number of links = %d, want 1", len(respData.Links)) } } // TestLauncherEndpointEmptyConfig tests launcher with empty config func TestLauncherEndpointEmptyConfig(t *testing.T) { logger := NewTestLogger(t) defer func() { _ = logger.Sync() }() c := NewTestConfig() c.API.Banners = []cfg.APISignBanner{} c.API.Messages = []cfg.APISignMessage{} c.API.Links = []cfg.APISignLink{} server := &APIServer{ logger: logger, erupeConfig: c, } req := httptest.NewRequest("GET", "/launcher", nil) recorder := httptest.NewRecorder() server.Launcher(recorder, req) var respData LauncherResponse _ = json.NewDecoder(recorder.Body).Decode(&respData) if respData.Banners == nil { t.Error("Banners should not be nil, should be empty slice") } if respData.Messages == nil { t.Error("Messages should not be nil, should be empty slice") } if respData.Links == nil { t.Error("Links should not be nil, should be empty slice") } } // TestLoginEndpointInvalidJSON tests login with invalid JSON func TestLoginEndpointInvalidJSON(t *testing.T) { logger := NewTestLogger(t) defer func() { _ = logger.Sync() }() c := NewTestConfig() server := &APIServer{ logger: logger, erupeConfig: c, db: nil, } // Invalid JSON invalidJSON := `{"username": "test", "password": ` req := httptest.NewRequest("POST", "/login", strings.NewReader(invalidJSON)) req.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() server.Login(recorder, req) if recorder.Code != http.StatusBadRequest { t.Errorf("Expected status %d, got %d", http.StatusBadRequest, recorder.Code) } } // TestLoginEndpointEmptyCredentials tests login with empty credentials func TestLoginEndpointEmptyCredentials(t *testing.T) { logger := NewTestLogger(t) defer func() { _ = logger.Sync() }() c := NewTestConfig() server := &APIServer{ logger: logger, erupeConfig: c, db: nil, } tests := []struct { name string username string password string wantPanic bool // Note: will panic without real DB }{ {"EmptyUsername", "", "password", true}, {"EmptyPassword", "username", "", true}, {"BothEmpty", "", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantPanic { t.Skip("Skipping - requires real database connection") } body := struct { Username string `json:"username"` Password string `json:"password"` }{ Username: tt.username, Password: tt.password, } bodyBytes, _ := json.Marshal(body) req := httptest.NewRequest("POST", "/login", bytes.NewReader(bodyBytes)) recorder := httptest.NewRecorder() // Note: Without a database, this will fail server.Login(recorder, req) // Should fail (400 or 500 depending on DB availability) if recorder.Code < http.StatusBadRequest { t.Errorf("Should return error status for test: %s", tt.name) } }) } } // TestRegisterEndpointInvalidJSON tests register with invalid JSON func TestRegisterEndpointInvalidJSON(t *testing.T) { logger := NewTestLogger(t) defer func() { _ = logger.Sync() }() c := NewTestConfig() server := &APIServer{ logger: logger, erupeConfig: c, db: nil, } invalidJSON := `{"username": "test"` req := httptest.NewRequest("POST", "/register", strings.NewReader(invalidJSON)) recorder := httptest.NewRecorder() server.Register(recorder, req) if recorder.Code != http.StatusBadRequest { t.Errorf("Expected status %d, got %d", http.StatusBadRequest, recorder.Code) } } // TestRegisterEndpointEmptyCredentials tests register with empty fields func TestRegisterEndpointEmptyCredentials(t *testing.T) { logger := NewTestLogger(t) defer func() { _ = logger.Sync() }() c := NewTestConfig() server := &APIServer{ logger: logger, erupeConfig: c, db: nil, } tests := []struct { name string username string password string wantCode int }{ {"EmptyUsername", "", "password", http.StatusBadRequest}, {"EmptyPassword", "username", "", http.StatusBadRequest}, {"BothEmpty", "", "", http.StatusBadRequest}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { body := struct { Username string `json:"username"` Password string `json:"password"` }{ Username: tt.username, Password: tt.password, } bodyBytes, _ := json.Marshal(body) req := httptest.NewRequest("POST", "/register", bytes.NewReader(bodyBytes)) recorder := httptest.NewRecorder() // Validating empty credentials check only (no database call) server.Register(recorder, req) // Empty credentials should return 400 if recorder.Code != tt.wantCode { t.Logf("Got status %d, want %d - %s", recorder.Code, tt.wantCode, tt.name) } }) } } // TestCreateCharacterEndpointInvalidJSON tests create character with invalid JSON func TestCreateCharacterEndpointInvalidJSON(t *testing.T) { logger := NewTestLogger(t) defer func() { _ = logger.Sync() }() c := NewTestConfig() server := &APIServer{ logger: logger, erupeConfig: c, db: nil, } invalidJSON := `{"token": ` req := httptest.NewRequest("POST", "/character/create", strings.NewReader(invalidJSON)) recorder := httptest.NewRecorder() server.CreateCharacter(recorder, req) if recorder.Code != http.StatusBadRequest { t.Errorf("Expected status %d, got %d", http.StatusBadRequest, recorder.Code) } } // TestDeleteCharacterEndpointInvalidJSON tests delete character with invalid JSON func TestDeleteCharacterEndpointInvalidJSON(t *testing.T) { logger := NewTestLogger(t) defer func() { _ = logger.Sync() }() c := NewTestConfig() server := &APIServer{ logger: logger, erupeConfig: c, db: nil, } invalidJSON := `{"token": "test"` req := httptest.NewRequest("POST", "/character/delete", strings.NewReader(invalidJSON)) recorder := httptest.NewRecorder() server.DeleteCharacter(recorder, req) if recorder.Code != http.StatusBadRequest { t.Errorf("Expected status %d, got %d", http.StatusBadRequest, recorder.Code) } } // TestExportSaveEndpointInvalidJSON tests export save with invalid JSON func TestExportSaveEndpointInvalidJSON(t *testing.T) { logger := NewTestLogger(t) defer func() { _ = logger.Sync() }() c := NewTestConfig() server := &APIServer{ logger: logger, erupeConfig: c, db: nil, } invalidJSON := `{"token": ` req := httptest.NewRequest("POST", "/character/export", strings.NewReader(invalidJSON)) recorder := httptest.NewRecorder() server.ExportSave(recorder, req) if recorder.Code != http.StatusBadRequest { t.Errorf("Expected status %d, got %d", http.StatusBadRequest, recorder.Code) } } // TestScreenShotEndpointDisabled tests screenshot endpoint when disabled func TestScreenShotEndpointDisabled(t *testing.T) { logger := NewTestLogger(t) defer func() { _ = logger.Sync() }() c := NewTestConfig() c.Screenshots.Enabled = false server := &APIServer{ logger: logger, erupeConfig: c, db: nil, } req := httptest.NewRequest("POST", "/api/ss/bbs/upload.php", nil) recorder := httptest.NewRecorder() server.ScreenShot(recorder, req) // Parse XML response var result struct { XMLName xml.Name `xml:"result"` Code string `xml:"code"` } _ = xml.NewDecoder(recorder.Body).Decode(&result) if result.Code != "400" { t.Errorf("Expected code 400, got %s", result.Code) } } // TestScreenShotEndpointInvalidMethod tests screenshot endpoint with invalid method func TestScreenShotEndpointInvalidMethod(t *testing.T) { t.Skip("Screenshot endpoint doesn't have proper control flow for early returns") // The ScreenShot function doesn't exit early on method check, so it continues // to try to decode image from nil body which causes panic // This would need refactoring of the endpoint to fix } // TestScreenShotGetInvalidToken tests screenshot get with invalid token func TestScreenShotGetInvalidToken(t *testing.T) { logger := NewTestLogger(t) defer func() { _ = logger.Sync() }() c := NewTestConfig() server := &APIServer{ logger: logger, erupeConfig: c, db: nil, } tests := []struct { name string token string }{ {"EmptyToken", ""}, {"InvalidCharactersToken", "../../etc/passwd"}, {"SpecialCharactersToken", "token@!#$"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest("GET", "/api/ss/bbs/"+tt.token, nil) recorder := httptest.NewRecorder() // Set up the URL variable manually since we're not using gorilla/mux if tt.token == "" { server.ScreenShotGet(recorder, req) // Empty token should fail if recorder.Code != http.StatusBadRequest { t.Logf("Empty token returned status %d", recorder.Code) } } }) } } // TestNewAuthDataStructure tests the newAuthData helper function func TestNewAuthDataStructure(t *testing.T) { t.Skip("newAuthData requires database for getReturnExpiry - needs integration test") logger := NewTestLogger(t) defer func() { _ = logger.Sync() }() c := NewTestConfig() c.DebugOptions.MaxLauncherHR = false c.HideLoginNotice = false c.LoginNotices = []string{"Notice 1", "Notice 2"} server := &APIServer{ logger: logger, erupeConfig: c, db: nil, } characters := []Character{ { ID: 1, Name: "Char1", IsFemale: false, Weapon: 0, HR: 5, GR: 0, }, } authData := server.newAuthData(1, 0, 1, "test-token", characters) if authData.User.TokenID != 1 { t.Errorf("Token ID = %d, want 1", authData.User.TokenID) } if authData.User.Token != "test-token" { t.Errorf("Token = %s, want test-token", authData.User.Token) } if len(authData.Characters) != 1 { t.Errorf("Number of characters = %d, want 1", len(authData.Characters)) } if authData.MezFes == nil { t.Error("MezFes should not be nil") } if authData.PatchServer != c.API.PatchServer { t.Errorf("PatchServer = %s, want %s", authData.PatchServer, c.API.PatchServer) } if len(authData.Notices) == 0 { t.Error("Notices should not be empty when HideLoginNotice is false") } } // TestNewAuthDataDebugMode tests newAuthData with debug mode enabled func TestNewAuthDataDebugMode(t *testing.T) { t.Skip("newAuthData requires database for getReturnExpiry - needs integration test") logger := NewTestLogger(t) defer func() { _ = logger.Sync() }() c := NewTestConfig() c.DebugOptions.MaxLauncherHR = true server := &APIServer{ logger: logger, erupeConfig: c, db: nil, } characters := []Character{ { ID: 1, Name: "Char1", IsFemale: false, Weapon: 0, HR: 100, // High HR GR: 0, }, } authData := server.newAuthData(1, 0, 1, "token", characters) if authData.Characters[0].HR != 7 { t.Errorf("Debug mode should set HR to 7, got %d", authData.Characters[0].HR) } } // TestNewAuthDataMezFesConfiguration tests MezFes configuration in newAuthData func TestNewAuthDataMezFesConfiguration(t *testing.T) { t.Skip("newAuthData requires database for getReturnExpiry - needs integration test") logger := NewTestLogger(t) defer func() { _ = logger.Sync() }() c := NewTestConfig() c.GameplayOptions.MezFesSoloTickets = 150 c.GameplayOptions.MezFesGroupTickets = 75 c.GameplayOptions.MezFesSwitchMinigame = true server := &APIServer{ logger: logger, erupeConfig: c, db: nil, } authData := server.newAuthData(1, 0, 1, "token", []Character{}) if authData.MezFes.SoloTickets != 150 { t.Errorf("SoloTickets = %d, want 150", authData.MezFes.SoloTickets) } if authData.MezFes.GroupTickets != 75 { t.Errorf("GroupTickets = %d, want 75", authData.MezFes.GroupTickets) } // Check that minigame stall is switched if authData.MezFes.Stalls[4] != 2 { t.Errorf("Minigame stall should be 2 when MezFesSwitchMinigame is true, got %d", authData.MezFes.Stalls[4]) } } // TestNewAuthDataHideNotices tests notice hiding in newAuthData func TestNewAuthDataHideNotices(t *testing.T) { t.Skip("newAuthData requires database for getReturnExpiry - needs integration test") logger := NewTestLogger(t) defer func() { _ = logger.Sync() }() c := NewTestConfig() c.HideLoginNotice = true c.LoginNotices = []string{"Notice 1", "Notice 2"} server := &APIServer{ logger: logger, erupeConfig: c, db: nil, } authData := server.newAuthData(1, 0, 1, "token", []Character{}) if len(authData.Notices) != 0 { t.Errorf("Notices should be empty when HideLoginNotice is true, got %d", len(authData.Notices)) } } // TestNewAuthDataTimestamps tests timestamp generation in newAuthData func TestNewAuthDataTimestamps(t *testing.T) { t.Skip("newAuthData requires database for getReturnExpiry - needs integration test") logger := NewTestLogger(t) defer func() { _ = logger.Sync() }() c := NewTestConfig() server := &APIServer{ logger: logger, erupeConfig: c, db: nil, } authData := server.newAuthData(1, 0, 1, "token", []Character{}) // Timestamps should be reasonable (within last minute and next 30 days) now := uint32(gametime.Adjusted().Unix()) if authData.CurrentTS < now-60 || authData.CurrentTS > now+60 { t.Errorf("CurrentTS not within reasonable range: %d vs %d", authData.CurrentTS, now) } if authData.ExpiryTS < now { t.Errorf("ExpiryTS should be in future") } } // BenchmarkLauncherEndpoint benchmarks the launcher endpoint func BenchmarkLauncherEndpoint(b *testing.B) { logger, _ := zap.NewDevelopment() defer func() { _ = logger.Sync() }() c := NewTestConfig() server := &APIServer{ logger: logger, erupeConfig: c, } b.ResetTimer() for i := 0; i < b.N; i++ { req := httptest.NewRequest("GET", "/launcher", nil) recorder := httptest.NewRecorder() server.Launcher(recorder, req) } } // BenchmarkNewAuthData benchmarks the newAuthData function func BenchmarkNewAuthData(b *testing.B) { logger, _ := zap.NewDevelopment() defer func() { _ = logger.Sync() }() c := NewTestConfig() server := &APIServer{ logger: logger, erupeConfig: c, } characters := make([]Character, 16) for i := 0; i < 16; i++ { characters[i] = Character{ ID: uint32(i + 1), Name: "Character", IsFemale: i%2 == 0, Weapon: uint32(i % 14), HR: uint32(100 + i), GR: 0, } } b.ResetTimer() for i := 0; i < b.N; i++ { _ = server.newAuthData(1, 0, 1, "token", characters) } }