From 7c36a1332715f8371c345351e886c5335d08be05 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 22 Apr 2026 10:29:40 +0200 Subject: [PATCH] updated API tests --- backend/api/api_test.go | 628 +++++----------------------------------- 1 file changed, 76 insertions(+), 552 deletions(-) diff --git a/backend/api/api_test.go b/backend/api/api_test.go index d41b373..a4b62d4 100644 --- a/backend/api/api_test.go +++ b/backend/api/api_test.go @@ -1,577 +1,101 @@ package api import ( - "bamort/bmrt/character" + "bamort/bmrt/ptpl" "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" + "os" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// 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 +func setupTestEnvironment(t *testing.T) { + t.Helper() + original := os.Getenv("ENVIRONMENT") + os.Setenv("ENVIRONMENT", "test") + t.Cleanup(func() { + if original != "" { + os.Setenv("ENVIRONMENT", original) + } else { + os.Unsetenv("ENVIRONMENT") } - - 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"}) }) + database.SetupTestDB(true, true) + t.Cleanup(database.ResetTestDB) - 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) + require.NoError(t, database.MigrateStructure()) + require.NoError(t, user.MigrateStructure()) + require.NoError(t, ptpl.MigrateStructure()) - // 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)) + gin.SetMode(gin.TestMode) } -func TestFallbackValueDetection(t *testing.T) { - database.SetupTestDB(true) // Setup test database - t.Cleanup(database.ResetTestDB) - // Initialize a Gin router +func buildRouter() *gin.Engine { 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) - } - }) - } + ptpl.RegisterRoutes(protected) + ptpl.RegisterPublicRoutes(r) + return r +} + +// TestPublicInfo verifies the public /public/ptpl/info endpoint (no auth required). +func TestPublicInfo(t *testing.T) { + setupTestEnvironment(t) + r := buildRouter() + + req, _ := http.NewRequest(http.MethodGet, "/public/ptpl/info", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var body map[string]string + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + assert.Equal(t, "ptpl", body["module"]) + assert.Equal(t, "active", body["status"]) +} + +// TestProtectedRouteWithoutToken verifies that /api/ptpl returns 401 without a JWT. +func TestProtectedRouteWithoutToken(t *testing.T) { + setupTestEnvironment(t) + r := buildRouter() + + req, _ := http.NewRequest(http.MethodGet, "/api/ptpl", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusUnauthorized, w.Code) +} + +// TestProtectedRouteWithToken verifies that /api/ptpl returns 200 with a valid JWT. +func TestProtectedRouteWithToken(t *testing.T) { + setupTestEnvironment(t) + r := buildRouter() + + u := &user.User{ + Username: "apitest", + DisplayName: "API Test", + PasswordHash: "irrelevant", + Email: "apitest@test.local", + Role: user.RoleStandardUser, + } + require.NoError(t, database.DB.Create(u).Error) + token := user.GenerateToken(u) + + req, _ := http.NewRequest(http.MethodGet, "/api/ptpl", nil) + req.Header.Set("Authorization", "Bearer "+token) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var items []ptpl.PtplItem + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &items)) + assert.Empty(t, items) }