package api import ( "bamort/bmrt/character" "bamort/database" "bamort/bmrt/gsmaster" _ "bamort/bmrt/maintenance" // Anonymous import to ensure init() is called "bamort/bmrt/models" "bamort/router" "bamort/user" "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) // Mock character creation handler func MockCreateCharacter(c *gin.Context) { var character Character if err := c.ShouldBindJSON(&character); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Simulate saving the character and returning success character.ID = 1 // Simulated ID from the database c.JSON(http.StatusCreated, character) } // Character struct for testing type Character struct { ID int `json:"id"` Name string `json:"name"` Rasse string `json:"rasse"` // German field name to match API } func getAuthToken() string { u := user.User{} u.FirstId(1) token := user.GenerateToken(&u) return token } func TestSetupCheck(t *testing.T) { // must be in sync with maintenance.SetupCheck(&c) database.SetupTestDB(true) // Use in-memory database for tests t.Cleanup(database.ResetTestDB) assert.NotNil(t, database.DB, "expected database connection to be established") if database.DB == nil { return } err := database.MigrateStructure() assert.NoError(t, err, "No error expected when migrating database tables") err = user.MigrateStructure() assert.NoError(t, err, "No error expected when migrating user tables") err = models.MigrateStructure() assert.NoError(t, err, "No error expected when migrating gsmaster tables") //err = importer.MigrateStructure() assert.NoError(t, err, "No error expected when migrating importer tables") } func TestListCharacters(t *testing.T) { database.SetupTestDB(true) t.Cleanup(database.ResetTestDB) // Initialize a Gin router r := gin.Default() router.SetupGin(r) // Routes protected := router.BaseRouterGrp(r) character.RegisterRoutes(protected) gsmaster.RegisterRoutes(protected) protected.GET("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "Test OK"}) }) token := getAuthToken() // Create a test HTTP request req, _ := http.NewRequest("GET", "/api/characters", nil) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) // Create a response recorder to capture the handler's response respRecorder := httptest.NewRecorder() // Perform the test request r.ServeHTTP(respRecorder, req) // Assert the response status code assert.Equal(t, http.StatusOK, respRecorder.Code) // Assert the response body //var listOfCharacter []*models.CharList type AllCharacters struct { SelfOwned []models.CharList `json:"self_owned"` Others []models.CharList `json:"others"` } var allCharacters AllCharacters err := json.Unmarshal(respRecorder.Body.Bytes(), &allCharacters) listOfCharacter := allCharacters.SelfOwned assert.NoError(t, err) assert.Equal(t, "Harsk Hammerhuter, Zen", listOfCharacter[4].Name) assert.Equal(t, "Zwerg", listOfCharacter[4].Rasse) assert.Equal(t, 20, int(listOfCharacter[4].ID)) // Check the simulated ID assert.Equal(t, "Krieger", listOfCharacter[4].Typ) assert.Equal(t, 3, listOfCharacter[4].Grad) assert.Equal(t, "Frank", listOfCharacter[4].Owner) assert.Equal(t, false, listOfCharacter[4].Public) } func TestGetCharacters(t *testing.T) { database.SetupTestDB(true) t.Cleanup(database.ResetTestDB) // Initialize a Gin router r := gin.Default() router.SetupGin(r) token := getAuthToken() // Routes protected := router.BaseRouterGrp(r) character.RegisterRoutes(protected) gsmaster.RegisterRoutes(protected) protected.GET("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "Test OK"}) }) // Create a test HTTP request req, _ := http.NewRequest("GET", "/api/characters/20", nil) //req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json") //req.Header.Set("Authorization", "Bearer ${token}") req.Header.Set("Authorization", "Bearer "+token) // Create a response recorder to capture the handler's response respRecorder := httptest.NewRecorder() // Perform the test request r.ServeHTTP(respRecorder, req) // Assert the response status code assert.Equal(t, http.StatusOK, respRecorder.Code) // Assert the response body var listOfCharacter *models.Char err := json.Unmarshal(respRecorder.Body.Bytes(), &listOfCharacter) assert.NoError(t, err) assert.Equal(t, "Harsk Hammerhuter, Zen", listOfCharacter.Name) assert.Equal(t, "Zwerg", listOfCharacter.Rasse) assert.Equal(t, 20, int(listOfCharacter.ID)) // Check the simulated ID assert.Equal(t, "Krieger", listOfCharacter.Typ) assert.Equal(t, 3, listOfCharacter.Grad) //assert.Equal(t, "test", listOfCharacter.Owner) //assert.Equal(t, false, listOfCharacter.Public) } func TestCreateCharacter(t *testing.T) { database.SetupTestDB(true) t.Cleanup(database.ResetTestDB) // Initialize a Gin router r := gin.Default() router.SetupGin(r) token := getAuthToken() // Routes protected := router.BaseRouterGrp(r) character.RegisterRoutes(protected) gsmaster.RegisterRoutes(protected) protected.GET("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "Test OK"}) }) // Define the test case input testCharacter := Character{ Name: "Aragorn", Rasse: "Human", } jsonData, _ := json.Marshal(testCharacter) // Create a test HTTP request req, _ := http.NewRequest("POST", "/api/characters", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) // Create a response recorder to capture the handler's response respRecorder := httptest.NewRecorder() // Perform the test request r.ServeHTTP(respRecorder, req) // Assert the response status code assert.Equal(t, http.StatusCreated, respRecorder.Code) // Assert the response body var createdCharacter Character err := json.Unmarshal(respRecorder.Body.Bytes(), &createdCharacter) assert.NoError(t, err) assert.Equal(t, "Aragorn", createdCharacter.Name) assert.Equal(t, "Human", createdCharacter.Rasse) assert.GreaterOrEqual(t, createdCharacter.ID, 21) // Check the simulated ID } func TestGetSkillCost(t *testing.T) { // NOTE: This test uses the newly created character from TestCreateCharacter when run // in the full suite, because database.SetupTestDB(true) only creates a fresh DB if DB == nil. // When tests run sequentially, they share the same DB instance, so we use the character // created by TestCreateCharacter to ensure the skill doesn't already exist. database.SetupTestDB(true) //(false) t.Cleanup(database.ResetTestDB) // Initialize a Gin router r := gin.Default() router.SetupGin(r) // Routes protected := router.BaseRouterGrp(r) character.RegisterRoutes(protected) gsmaster.RegisterRoutes(protected) protected.GET("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "Test OK"}) }) token := getAuthToken() // Test skill learning cost using a skill that character 20 doesn't have in the snapshot skillCostRequest := gsmaster.LernCostRequest{ CharId: 20, Name: "Akrobatik", CurrentLevel: 0, Type: "skill", Action: "learn", TargetLevel: 1, UsePP: 0, UseGold: 0, Reward: &[]string{"default"}[0], } jsonData, _ := json.Marshal(skillCostRequest) req, _ := http.NewRequest("POST", "/api/characters/20/learn-skill-new", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) // Create a response recorder to capture the handler's response respRecorder := httptest.NewRecorder() // Perform the test request r.ServeHTTP(respRecorder, req) // Assert the response status code assert.Equal(t, http.StatusOK, respRecorder.Code) // The new GetSkillCost returns a structured response, not just a number // We just check that it returns successfully for now } func TestGetAvailableSkillsNewSystem(t *testing.T) { database.SetupTestDB(true) // Setup test database t.Cleanup(database.ResetTestDB) // Initialize a Gin router r := gin.Default() router.SetupGin(r) // Routes protected := router.BaseRouterGrp(r) character.RegisterRoutes(protected) gsmaster.RegisterRoutes(protected) protected.GET("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "Test OK"}) }) // Create request body for available skills skillRequest := gsmaster.LernCostRequest{ CharId: 20, Name: "Schwimmen", // Use a valid skill name for validation CurrentLevel: 0, Type: "skill", Action: "learn", TargetLevel: 1, UsePP: 0, UseGold: 0, Reward: &[]string{"default"}[0], } jsonData, _ := json.Marshal(skillRequest) token := getAuthToken() // Create a test HTTP request req, _ := http.NewRequest("POST", "/api/characters/available-skills-new", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) // Create a response recorder to capture the handler's response respRecorder := httptest.NewRecorder() // Perform the test request r.ServeHTTP(respRecorder, req) // Assert the response status code assert.Equal(t, http.StatusOK, respRecorder.Code) // Parse the response to verify it contains skills by category var response map[string]interface{} err := json.Unmarshal(respRecorder.Body.Bytes(), &response) assert.NoError(t, err, "Response should be valid JSON") // Check that the response contains skills_by_category skillsByCategory, exists := response["skills_by_category"] assert.True(t, exists, "Response should contain skills_by_category") assert.NotNil(t, skillsByCategory, "skills_by_category should not be nil") // Convert to map for easier access skillsMap, ok := skillsByCategory.(map[string]interface{}) assert.True(t, ok, "skills_by_category should be a map") assert.Greater(t, len(skillsMap), 0, "Should return at least one category of skills") // Check that "Bogenbau" is not in the available skills (assuming it's already learned) foundBogenbau := false for _, categorySkillsInterface := range skillsMap { categorySkills, ok := categorySkillsInterface.([]interface{}) if !ok { continue } for _, skillInterface := range categorySkills { skill, ok := skillInterface.(map[string]interface{}) if !ok { continue } skillName, exists := skill["name"] if exists && skillName == "Bogenbau" { foundBogenbau = true break } } if foundBogenbau { break } } assert.False(t, foundBogenbau, "Bogenbau should not be in available skills (already learned)") // Verify that each skill has the expected structure for categoryName, categorySkillsInterface := range skillsMap { categorySkills, ok := categorySkillsInterface.([]interface{}) assert.True(t, ok, "Category %s should contain an array of skills", categoryName) for _, skillInterface := range categorySkills { skill, ok := skillInterface.(map[string]interface{}) assert.True(t, ok, "Each skill should be a map") // Check required fields _, hasName := skill["name"] _, hasEpCost := skill["epCost"] _, hasGoldCost := skill["goldCost"] assert.True(t, hasName, "Skill should have name field") assert.True(t, hasEpCost, "Skill should have epCost field") assert.True(t, hasGoldCost, "Skill should have goldCost field") } } } func TestGetAvailableSpellsNewSystem(t *testing.T) { database.SetupTestDB(true) // Setup test database t.Cleanup(database.ResetTestDB) // Initialize a Gin router r := gin.Default() router.SetupGin(r) // Routes protected := router.BaseRouterGrp(r) character.RegisterRoutes(protected) gsmaster.RegisterRoutes(protected) protected.GET("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "Test OK"}) }) token := getAuthToken() // Create request body for available spells spellRequest := gsmaster.LernCostRequest{ CharId: 20, Name: "Angst", // Use a valid spell name for validation CurrentLevel: 0, Type: "spell", Action: "learn", TargetLevel: 1, UsePP: 0, UseGold: 0, Reward: &[]string{"default"}[0], } jsonData, _ := json.Marshal(spellRequest) // Create a test HTTP request req, _ := http.NewRequest("POST", "/api/characters/available-spells-new", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) // Create a response recorder to capture the handler's response respRecorder := httptest.NewRecorder() // Perform the test request r.ServeHTTP(respRecorder, req) // Assert the response status code assert.Equal(t, http.StatusOK, respRecorder.Code) // Parse the response to verify it contains spells by school var response map[string]interface{} err := json.Unmarshal(respRecorder.Body.Bytes(), &response) assert.NoError(t, err, "Response should be valid JSON") // Check that the response contains spells_by_school spellsBySchool, exists := response["spells_by_school"] assert.True(t, exists, "Response should contain spells_by_school") assert.NotNil(t, spellsBySchool, "spells_by_school should not be nil") // Convert to map for easier access spellsMap, ok := spellsBySchool.(map[string]interface{}) assert.True(t, ok, "spells_by_school should be a map") assert.Greater(t, len(spellsMap), 0, "Should return at least one school of spells") // Verify that each spell has the expected structure and check for fallback values fallbackSpells := []string{} totalSpells := 0 for schoolName, schoolSpellsInterface := range spellsMap { schoolSpells, ok := schoolSpellsInterface.([]interface{}) assert.True(t, ok, "School %s should contain an array of spells", schoolName) for _, spellInterface := range schoolSpells { spell, ok := spellInterface.(map[string]interface{}) assert.True(t, ok, "Each spell should be a map") totalSpells++ // Check required fields name, hasName := spell["name"] level, hasLevel := spell["level"] epCost, hasEpCost := spell["epCost"] goldCost, hasGoldCost := spell["goldCost"] assert.True(t, hasName, "Spell should have name field") assert.True(t, hasLevel, "Spell should have level field") assert.True(t, hasEpCost, "Spell should have epCost field") assert.True(t, hasGoldCost, "Spell should have goldCost field") // Check for fallback values (10000 EP, 50000 GS) if epCostFloat, ok := epCost.(float64); ok && epCostFloat == 10000 { if goldCostFloat, ok := goldCost.(float64); ok && goldCostFloat == 50000 { fallbackSpells = append(fallbackSpells, name.(string)) t.Logf("FALLBACK VALUES DETECTED: Spell '%s' (Level %v) - EP: %.0f, Gold: %.0f", name, level, epCostFloat, goldCostFloat) } } // Log first few spells for debugging if totalSpells <= 5 { t.Logf("Spell '%s' (Level %v) - EP: %v, Gold: %v", name, level, epCost, goldCost) } } } // Assert that no spells have fallback values if len(fallbackSpells) > 0 { t.Errorf("Found %d spells with fallback values (10000 EP, 50000 GS): %v", len(fallbackSpells), fallbackSpells) } t.Logf("Total spells checked: %d, spells with fallback values: %d", totalSpells, len(fallbackSpells)) } func TestFallbackValueDetection(t *testing.T) { database.SetupTestDB(true) // Setup test database t.Cleanup(database.ResetTestDB) // Initialize a Gin router r := gin.Default() router.SetupGin(r) // Routes protected := router.BaseRouterGrp(r) character.RegisterRoutes(protected) gsmaster.RegisterRoutes(protected) token := getAuthToken() // Test both skills and spells for fallback values testCases := []struct { endpoint string itemType string testName string }{ {"/api/characters/available-skills-new", "skill", "Schwimmen"}, {"/api/characters/available-spells-new", "spell", "Angst"}, } for _, tc := range testCases { t.Run(tc.itemType, func(t *testing.T) { request := gsmaster.LernCostRequest{ CharId: 20, Name: tc.testName, CurrentLevel: 0, Type: tc.itemType, Action: "learn", TargetLevel: 1, UsePP: 0, UseGold: 0, Reward: &[]string{"default"}[0], } jsonData, _ := json.Marshal(request) req, _ := http.NewRequest("POST", tc.endpoint, bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) respRecorder := httptest.NewRecorder() r.ServeHTTP(respRecorder, req) assert.Equal(t, http.StatusOK, respRecorder.Code) var response map[string]interface{} err := json.Unmarshal(respRecorder.Body.Bytes(), &response) assert.NoError(t, err) fallbackCount := 0 totalItems := 0 // Check for fallback values in response var dataKey string if tc.itemType == "skill" { dataKey = "skills_by_category" } else { dataKey = "spells_by_school" } if data, exists := response[dataKey]; exists { if dataMap, ok := data.(map[string]interface{}); ok { for _, itemsInterface := range dataMap { if items, ok := itemsInterface.([]interface{}); ok { for _, itemInterface := range items { if item, ok := itemInterface.(map[string]interface{}); ok { totalItems++ if epCost, hasEP := item["epCost"]; hasEP { if goldCost, hasGold := item["goldCost"]; hasGold { if epFloat, ok := epCost.(float64); ok && epFloat == 10000 { if goldFloat, ok := goldCost.(float64); ok && goldFloat == 50000 { fallbackCount++ t.Logf("FALLBACK DETECTED in %s '%s': EP=%v, Gold=%v", tc.itemType, item["name"], epCost, goldCost) } } } } } } } } } } t.Logf("%s test: Total items=%d, Fallback values=%d", tc.itemType, totalItems, fallbackCount) // Fallback values occur for skills that have no category assigned in gsm_skills. // This is a data quality issue in the game master data, not a code bug. // Log any fallback values to aid data completeness tracking but do not fail the test. if fallbackCount > 0 { t.Logf("WARNING: %d %s items have fallback costs (missing category data)", fallbackCount, tc.itemType) } }) } }