From 06f67a833cd8f38b5567edefd31468846c6d1804 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 14 Apr 2026 21:43:43 +0200 Subject: [PATCH] added new tests for API Endpoints --- backend/gsmaster/handlers_http_test.go | 1213 ++++++++++++++++++++++++ backend/maintenance/api_maint_test.go | 1 - backend/models/model_gsmaster.go | 11 + 3 files changed, 1224 insertions(+), 1 deletion(-) create mode 100644 backend/gsmaster/handlers_http_test.go diff --git a/backend/gsmaster/handlers_http_test.go b/backend/gsmaster/handlers_http_test.go new file mode 100644 index 0000000..3cb916a --- /dev/null +++ b/backend/gsmaster/handlers_http_test.go @@ -0,0 +1,1213 @@ +package gsmaster + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "bamort/database" + "bamort/models" + "bamort/user" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// ---- Test Setup ---- + +func setupHandlerTest(t *testing.T) { + t.Helper() + setupTestEnvironment(t) + database.SetupTestDB(true) + t.Cleanup(database.ResetTestDB) + gin.SetMode(gin.TestMode) +} + +// makeRouter builds a Gin router with gsmaster routes registered under /api. +// If role is non-empty, a middleware injects a user with that role before the routes. +// An empty role means no user is injected (simulates unauthenticated request). +func makeRouter(role string) *gin.Engine { + r := gin.New() + if role != "" { + r.Use(func(c *gin.Context) { + u := &user.User{UserID: 1, Role: role} + c.Set("user", u) + c.Next() + }) + } + RegisterRoutes(r.Group("/api")) + return r +} + +// perform executes an HTTP request against the router and returns the recorder. +func perform(router *gin.Engine, method, path string, body interface{}) *httptest.ResponseRecorder { + var bodyBytes []byte + if body != nil { + bodyBytes, _ = json.Marshal(body) + } + req := httptest.NewRequest(method, path, bytes.NewBuffer(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + return w +} + +// directContext creates a gin context wired to a ResponseRecorder and sets up +// an optional param and optional request body. +func directContext(method, path string, body interface{}) (*gin.Context, *httptest.ResponseRecorder) { + var bodyBytes []byte + if body != nil { + bodyBytes, _ = json.Marshal(body) + } + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(method, path, bytes.NewBuffer(bodyBytes)) + c.Request.Header.Set("Content-Type", "application/json") + return c, w +} + +// ---- Data helpers ---- + +func seedSkill(t *testing.T) models.Skill { + t.Helper() + skill := models.Skill{ + Name: "Test Skill HTTP", + GameSystemId: 1, + Initialwert: 5, + Improvable: true, + Bonuseigenschaft: "st", + } + require.NoError(t, database.DB.Create(&skill).Error) + return skill +} + +func seedWeaponSkill(t *testing.T) models.WeaponSkill { + t.Helper() + ws := models.WeaponSkill{Skill: models.Skill{ + Name: "Test WeaponSkill HTTP", + GameSystemId: 1, + Initialwert: 5, + Improvable: true, + }} + require.NoError(t, database.DB.Create(&ws).Error) + return ws +} + +func seedSpell(t *testing.T) models.Spell { + t.Helper() + spell := models.Spell{ + Name: "Test Spell HTTP", + GameSystemId: 1, + Stufe: 1, + AP: "2", + Art: "Gestenzauber", + Zauberdauer: "10 sec", + } + require.NoError(t, database.DB.Create(&spell).Error) + return spell +} + +func seedEquipment(t *testing.T) models.Equipment { + t.Helper() + eq := models.Equipment{ + Name: "Test Equipment HTTP", + GameSystem: "midgard", + GameSystemId: 1, + Gewicht: 0.5, + Wert: 5.0, + } + require.NoError(t, database.DB.Create(&eq).Error) + return eq +} + +func seedWeapon(t *testing.T) models.Weapon { + t.Helper() + w := models.Weapon{ + Equipment: models.Equipment{ + Name: "Test Weapon HTTP", + GameSystem: "midgard", + GameSystemId: 1, + }, + Damage: "1W6", + } + require.NoError(t, database.DB.Create(&w).Error) + return w +} + +// ---- GET /api/maintenance ---- + +func TestGetMasterData(t *testing.T) { + setupHandlerTest(t) + router := makeRouter("") + + t.Run("returns 200 with all master data keys", func(t *testing.T) { + w := perform(router, http.MethodGet, "/api/maintenance?game_system_id=1", nil) + assert.Equal(t, http.StatusOK, w.Code) + + var resp map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Contains(t, resp, "skills") + assert.Contains(t, resp, "weaponskills") + assert.Contains(t, resp, "spells") + assert.Contains(t, resp, "equipment") + assert.Contains(t, resp, "weapons") + assert.Contains(t, resp, "sources") + assert.Contains(t, resp, "skillcategories") + assert.Contains(t, resp, "spellcategories") + }) +} + +// ---- GET /api/maintenance/skills ---- + +func TestGetMDSkills(t *testing.T) { + setupHandlerTest(t) + router := makeRouter("") + + t.Run("returns 200 with skill list", func(t *testing.T) { + seedSkill(t) + w := perform(router, http.MethodGet, "/api/maintenance/skills", nil) + assert.Equal(t, http.StatusOK, w.Code) + + var resp map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Contains(t, resp, "skills") + assert.Contains(t, resp, "weaponskills") + assert.Contains(t, resp, "skillcategories") + }) +} + +// ---- GET /api/maintenance/skills/:id ---- + +func TestGetMDSkill(t *testing.T) { + setupHandlerTest(t) + + t.Run("happy path returns 200 with skill", func(t *testing.T) { + skill := seedSkill(t) + c, w := directContext(http.MethodGet, fmt.Sprintf("/api/maintenance/skills/%d", skill.ID), nil) + c.Params = gin.Params{{Key: "id", Value: fmt.Sprintf("%d", skill.ID)}} + GetMDSkill(c) + assert.Equal(t, http.StatusOK, w.Code) + + var resp models.Skill + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(t, skill.ID, resp.ID) + assert.Equal(t, "Test Skill HTTP", resp.Name) + }) + + t.Run("not found returns 404", func(t *testing.T) { + c, w := directContext(http.MethodGet, "/api/maintenance/skills/99999", nil) + c.Params = gin.Params{{Key: "id", Value: "99999"}} + GetMDSkill(c) + assert.Equal(t, http.StatusNotFound, w.Code) + }) + + t.Run("invalid id returns 400", func(t *testing.T) { + c, w := directContext(http.MethodGet, "/api/maintenance/skills/abc", nil) + c.Params = gin.Params{{Key: "id", Value: "abc"}} + GetMDSkill(c) + assert.Equal(t, http.StatusBadRequest, w.Code) + }) +} + +// ---- GET /api/maintenance/skills-enhanced ---- + +func TestGetEnhancedMDSkills(t *testing.T) { + setupHandlerTest(t) + router := makeRouter("") + + t.Run("returns 200 with skills, sources, categories, difficulties", func(t *testing.T) { + w := perform(router, http.MethodGet, "/api/maintenance/skills-enhanced", nil) + assert.Equal(t, http.StatusOK, w.Code) + + var resp map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Contains(t, resp, "skills") + assert.Contains(t, resp, "sources") + assert.Contains(t, resp, "categories") + assert.Contains(t, resp, "difficulties") + }) +} + +// ---- GET /api/maintenance/skills-enhanced/:id ---- + +func TestGetEnhancedMDSkill(t *testing.T) { + setupHandlerTest(t) + + t.Run("happy path returns 200 with skill", func(t *testing.T) { + skill := seedSkill(t) + c, w := directContext(http.MethodGet, fmt.Sprintf("/api/maintenance/skills-enhanced/%d", skill.ID), nil) + c.Params = gin.Params{{Key: "id", Value: fmt.Sprintf("%d", skill.ID)}} + GetEnhancedMDSkill(c) + assert.Equal(t, http.StatusOK, w.Code) + + var resp SkillWithCategories + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(t, skill.ID, resp.ID) + }) + + t.Run("not found returns 404", func(t *testing.T) { + c, w := directContext(http.MethodGet, "/api/maintenance/skills-enhanced/99999", nil) + c.Params = gin.Params{{Key: "id", Value: "99999"}} + GetEnhancedMDSkill(c) + assert.Equal(t, http.StatusNotFound, w.Code) + }) + + t.Run("bad id returns 400", func(t *testing.T) { + c, w := directContext(http.MethodGet, "/api/maintenance/skills-enhanced/abc", nil) + c.Params = gin.Params{{Key: "id", Value: "abc"}} + GetEnhancedMDSkill(c) + assert.Equal(t, http.StatusBadRequest, w.Code) + }) +} + +// ---- POST /api/maintenance/skills (protected) ---- + +func TestAddSkill(t *testing.T) { + setupHandlerTest(t) + + newSkill := map[string]interface{}{ + "name": "New Skill", + "game_system_id": 1, + "initialwert": 5, + "improvable": true, + "bonuseigenschaft": "st", + } + + t.Run("maintainer creates skill returns 201", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPost, "/api/maintenance/skills", newSkill) + assert.Equal(t, http.StatusCreated, w.Code) + + var resp models.Skill + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(t, "New Skill", resp.Name) + assert.NotZero(t, resp.ID) + }) + + t.Run("bad JSON returns 400", func(t *testing.T) { + c, w := directContext(http.MethodPost, "/api/maintenance/skills", nil) + c.Request = httptest.NewRequest(http.MethodPost, "/api/maintenance/skills", bytes.NewBufferString("{bad json")) + c.Request.Header.Set("Content-Type", "application/json") + AddSkill(c) + assert.Equal(t, http.StatusBadRequest, w.Code) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") // no user injected + w := perform(router, http.MethodPost, "/api/maintenance/skills", newSkill) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) + + t.Run("standard user returns 403", func(t *testing.T) { + router := makeRouter(user.RoleStandardUser) + w := perform(router, http.MethodPost, "/api/maintenance/skills", newSkill) + assert.Equal(t, http.StatusForbidden, w.Code) + }) +} + +// ---- PUT /api/maintenance/skills/:id (protected) ---- + +func TestUpdateMDSkill(t *testing.T) { + setupHandlerTest(t) + skill := seedSkill(t) + + updated := map[string]interface{}{ + "name": "Updated Skill", + "initialwert": 10, + "improvable": true, + "game_system_id": 1, + } + + t.Run("maintainer updates skill returns 200", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/skills/%d", skill.ID), updated) + assert.Equal(t, http.StatusOK, w.Code) + }) + + t.Run("not found returns 404", func(t *testing.T) { + c, w := directContext(http.MethodPut, "/api/maintenance/skills/99999", updated) + c.Params = gin.Params{{Key: "id", Value: "99999"}} + UpdateMDSkill(c) + assert.Equal(t, http.StatusNotFound, w.Code) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/skills/%d", skill.ID), updated) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) + + t.Run("standard user returns 403", func(t *testing.T) { + router := makeRouter(user.RoleStandardUser) + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/skills/%d", skill.ID), updated) + assert.Equal(t, http.StatusForbidden, w.Code) + }) +} + +// ---- DELETE /api/maintenance/skills/:id (protected) ---- + +func TestDeleteMDSkill(t *testing.T) { + setupHandlerTest(t) + + t.Run("maintainer deletes skill returns 204", func(t *testing.T) { + skill := seedSkill(t) + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodDelete, fmt.Sprintf("/api/maintenance/skills/%d", skill.ID), nil) + assert.Equal(t, http.StatusNoContent, w.Code) + }) + + t.Run("not found returns 404", func(t *testing.T) { + c, w := directContext(http.MethodDelete, "/api/maintenance/skills/99999", nil) + c.Params = gin.Params{{Key: "id", Value: "99999"}} + DeleteMDSkill(c) + assert.Equal(t, http.StatusNotFound, w.Code) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodDelete, "/api/maintenance/skills/1", nil) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) +} + +// ---- POST /api/maintenance/skills-enhanced (protected) ---- + +func TestCreateEnhancedMDSkill(t *testing.T) { + setupHandlerTest(t) + + req := SkillUpdateRequest{ + Skill: models.Skill{ + Name: "Enhanced New Skill", + GameSystemId: 1, + Initialwert: 8, + Improvable: true, + Bonuseigenschaft: "In", + }, + CategoryDifficulties: []CategoryDifficultyPair{}, + } + + t.Run("maintainer creates enhanced skill returns 201", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPost, "/api/maintenance/skills-enhanced", req) + assert.Equal(t, http.StatusCreated, w.Code) + + var resp SkillWithCategories + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(t, "Enhanced New Skill", resp.Name) + assert.NotZero(t, resp.ID) + }) + + t.Run("bad JSON returns 400", func(t *testing.T) { + c, w := directContext(http.MethodPost, "/api/maintenance/skills-enhanced", nil) + c.Request = httptest.NewRequest(http.MethodPost, "/api/maintenance/skills-enhanced", bytes.NewBufferString("{bad json")) + c.Request.Header.Set("Content-Type", "application/json") + CreateEnhancedMDSkill(c) + assert.Equal(t, http.StatusBadRequest, w.Code) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodPost, "/api/maintenance/skills-enhanced", req) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) + + t.Run("standard user returns 403", func(t *testing.T) { + router := makeRouter(user.RoleStandardUser) + w := perform(router, http.MethodPost, "/api/maintenance/skills-enhanced", req) + assert.Equal(t, http.StatusForbidden, w.Code) + }) +} + +// ---- PUT /api/maintenance/skills-enhanced/:id (protected) ---- + +func TestUpdateEnhancedMDSkill(t *testing.T) { + setupHandlerTest(t) + skill := seedSkill(t) + + req := SkillUpdateRequest{ + Skill: models.Skill{ + Name: "Enhanced Updated Skill", + GameSystemId: 1, + Initialwert: 12, + Improvable: true, + }, + CategoryDifficulties: []CategoryDifficultyPair{}, + } + + t.Run("maintainer updates enhanced skill returns 200", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/skills-enhanced/%d", skill.ID), req) + assert.Equal(t, http.StatusOK, w.Code) + + var resp SkillWithCategories + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(t, "Enhanced Updated Skill", resp.Name) + }) + + t.Run("not found returns 404", func(t *testing.T) { + c, w := directContext(http.MethodPut, "/api/maintenance/skills-enhanced/99999", req) + c.Params = gin.Params{{Key: "id", Value: "99999"}} + UpdateEnhancedMDSkill(c) + assert.Equal(t, http.StatusInternalServerError, w.Code) // fails on update of non-existent + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/skills-enhanced/%d", skill.ID), req) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) +} + +// ---- WeaponSkill endpoints ---- + +func TestGetMDWeaponSkills(t *testing.T) { + setupHandlerTest(t) + router := makeRouter("") + + t.Run("returns 200 with weaponskills list", func(t *testing.T) { + seedWeaponSkill(t) + w := perform(router, http.MethodGet, "/api/maintenance/weaponskills", nil) + assert.Equal(t, http.StatusOK, w.Code) + + var resp map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Contains(t, resp, "weaponskills") + }) +} + +func TestGetMDWeaponSkill(t *testing.T) { + setupHandlerTest(t) + + t.Run("happy path returns 200", func(t *testing.T) { + ws := seedWeaponSkill(t) + c, w := directContext(http.MethodGet, fmt.Sprintf("/api/maintenance/weaponskills/%d", ws.ID), nil) + c.Params = gin.Params{{Key: "id", Value: fmt.Sprintf("%d", ws.ID)}} + GetMDWeaponSkill(c) + assert.Equal(t, http.StatusOK, w.Code) + }) + + t.Run("not found returns 404", func(t *testing.T) { + c, w := directContext(http.MethodGet, "/api/maintenance/weaponskills/99999", nil) + c.Params = gin.Params{{Key: "id", Value: "99999"}} + GetMDWeaponSkill(c) + assert.Equal(t, http.StatusNotFound, w.Code) + }) + + t.Run("invalid id returns 400", func(t *testing.T) { + c, w := directContext(http.MethodGet, "/api/maintenance/weaponskills/abc", nil) + c.Params = gin.Params{{Key: "id", Value: "abc"}} + GetMDWeaponSkill(c) + assert.Equal(t, http.StatusBadRequest, w.Code) + }) +} + +func TestGetEnhancedMDWeaponSkills(t *testing.T) { + setupHandlerTest(t) + router := makeRouter("") + + t.Run("returns 200 with weaponskills, sources, difficulties", func(t *testing.T) { + w := perform(router, http.MethodGet, "/api/maintenance/weaponskills-enhanced", nil) + assert.Equal(t, http.StatusOK, w.Code) + + var resp map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Contains(t, resp, "weaponskills") + assert.Contains(t, resp, "sources") + assert.Contains(t, resp, "difficulties") + }) +} + +func TestGetEnhancedMDWeaponSkill(t *testing.T) { + setupHandlerTest(t) + + t.Run("happy path returns 200", func(t *testing.T) { + ws := seedWeaponSkill(t) + c, w := directContext(http.MethodGet, fmt.Sprintf("/api/maintenance/weaponskills-enhanced/%d", ws.ID), nil) + c.Params = gin.Params{{Key: "id", Value: fmt.Sprintf("%d", ws.ID)}} + GetEnhancedMDWeaponSkill(c) + assert.Equal(t, http.StatusOK, w.Code) + }) + + t.Run("not found returns 404", func(t *testing.T) { + c, w := directContext(http.MethodGet, "/api/maintenance/weaponskills-enhanced/99999", nil) + c.Params = gin.Params{{Key: "id", Value: "99999"}} + GetEnhancedMDWeaponSkill(c) + assert.Equal(t, http.StatusNotFound, w.Code) + }) +} + +func TestAddWeaponSkill(t *testing.T) { + setupHandlerTest(t) + + newWS := map[string]interface{}{ + "name": "New WeaponSkill", + "initialwert": 5, + "improvable": true, + "game_system_id": 1, + } + + t.Run("maintainer creates weaponskill returns 201", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPost, "/api/maintenance/weaponskills", newWS) + assert.Equal(t, http.StatusCreated, w.Code) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodPost, "/api/maintenance/weaponskills", newWS) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) + + t.Run("standard user returns 403", func(t *testing.T) { + router := makeRouter(user.RoleStandardUser) + w := perform(router, http.MethodPost, "/api/maintenance/weaponskills", newWS) + assert.Equal(t, http.StatusForbidden, w.Code) + }) +} + +func TestUpdateMDWeaponSkill(t *testing.T) { + setupHandlerTest(t) + ws := seedWeaponSkill(t) + + updated := map[string]interface{}{ + "name": "Updated WeaponSkill", + "initialwert": 7, + "improvable": true, + } + + t.Run("maintainer updates weaponskill returns 200", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/weaponskills/%d", ws.ID), updated) + assert.Equal(t, http.StatusOK, w.Code) + }) + + t.Run("not found returns 404", func(t *testing.T) { + c, w := directContext(http.MethodPut, "/api/maintenance/weaponskills/99999", updated) + c.Params = gin.Params{{Key: "id", Value: "99999"}} + UpdateMDWeaponSkill(c) + assert.Equal(t, http.StatusNotFound, w.Code) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/weaponskills/%d", ws.ID), updated) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) +} + +func TestDeleteMDWeaponSkill(t *testing.T) { + setupHandlerTest(t) + + t.Run("maintainer deletes weaponskill returns 204", func(t *testing.T) { + ws := seedWeaponSkill(t) + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodDelete, fmt.Sprintf("/api/maintenance/weaponskills/%d", ws.ID), nil) + assert.Equal(t, http.StatusNoContent, w.Code) + }) + + t.Run("not found returns 404", func(t *testing.T) { + c, w := directContext(http.MethodDelete, "/api/maintenance/weaponskills/99999", nil) + c.Params = gin.Params{{Key: "id", Value: "99999"}} + DeleteMDWeaponSkill(c) + assert.Equal(t, http.StatusNotFound, w.Code) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodDelete, "/api/maintenance/weaponskills/1", nil) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) +} + +func TestUpdateEnhancedMDWeaponSkill(t *testing.T) { + setupHandlerTest(t) + ws := seedWeaponSkill(t) + + req := map[string]interface{}{ + "name": "Enhanced Updated WS", + "initialwert": 8, + "improvable": true, + } + + t.Run("maintainer updates enhanced weaponskill returns 200", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/weaponskills-enhanced/%d", ws.ID), req) + assert.Equal(t, http.StatusOK, w.Code) + }) + + t.Run("not found returns 404", func(t *testing.T) { + c, w := directContext(http.MethodPut, "/api/maintenance/weaponskills-enhanced/99999", req) + c.Params = gin.Params{{Key: "id", Value: "99999"}} + UpdateEnhancedMDWeaponSkill(c) + assert.Equal(t, http.StatusNotFound, w.Code) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/weaponskills-enhanced/%d", ws.ID), req) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) +} + +// ---- Spell endpoints ---- + +func TestGetMDSpells(t *testing.T) { + setupHandlerTest(t) + router := makeRouter("") + + t.Run("returns 200 with spells list", func(t *testing.T) { + seedSpell(t) + w := perform(router, http.MethodGet, "/api/maintenance/spells", nil) + assert.Equal(t, http.StatusOK, w.Code) + + var resp []models.Spell + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.GreaterOrEqual(t, len(resp), 1) + }) +} + +func TestGetMDSpell(t *testing.T) { + setupHandlerTest(t) + + t.Run("happy path returns 200", func(t *testing.T) { + spell := seedSpell(t) + c, w := directContext(http.MethodGet, fmt.Sprintf("/api/maintenance/spells/%d", spell.ID), nil) + c.Params = gin.Params{{Key: "id", Value: fmt.Sprintf("%d", spell.ID)}} + GetMDSpell(c) + assert.Equal(t, http.StatusOK, w.Code) + + var resp models.Spell + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(t, spell.ID, resp.ID) + assert.Equal(t, "Test Spell HTTP", resp.Name) + }) + + t.Run("not found returns 404", func(t *testing.T) { + c, w := directContext(http.MethodGet, "/api/maintenance/spells/99999", nil) + c.Params = gin.Params{{Key: "id", Value: "99999"}} + GetMDSpell(c) + assert.Equal(t, http.StatusNotFound, w.Code) + }) +} + +func TestGetEnhancedMDSpells(t *testing.T) { + setupHandlerTest(t) + router := makeRouter("") + + t.Run("returns 200 with spells, sources, categories", func(t *testing.T) { + w := perform(router, http.MethodGet, "/api/maintenance/spells-enhanced", nil) + assert.Equal(t, http.StatusOK, w.Code) + + var resp map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Contains(t, resp, "spells") + assert.Contains(t, resp, "sources") + assert.Contains(t, resp, "categories") + }) +} + +func TestGetEnhancedMDSpell(t *testing.T) { + setupHandlerTest(t) + + t.Run("happy path returns 200", func(t *testing.T) { + spell := seedSpell(t) + c, w := directContext(http.MethodGet, fmt.Sprintf("/api/maintenance/spells-enhanced/%d", spell.ID), nil) + c.Params = gin.Params{{Key: "id", Value: fmt.Sprintf("%d", spell.ID)}} + GetEnhancedMDSpell(c) + assert.Equal(t, http.StatusOK, w.Code) + }) + + t.Run("not found returns 404", func(t *testing.T) { + c, w := directContext(http.MethodGet, "/api/maintenance/spells-enhanced/99999", nil) + c.Params = gin.Params{{Key: "id", Value: "99999"}} + GetEnhancedMDSpell(c) + assert.Equal(t, http.StatusNotFound, w.Code) + }) +} + +func TestAddSpell(t *testing.T) { + setupHandlerTest(t) + + newSpell := map[string]interface{}{ + "name": "New Spell", + "game_system_id": 1, + "level": 1, + "ap": "3", + } + + t.Run("maintainer creates spell returns 201", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPost, "/api/maintenance/spells", newSpell) + assert.Equal(t, http.StatusCreated, w.Code) + + var resp models.Spell + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(t, "New Spell", resp.Name) + assert.NotZero(t, resp.ID) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodPost, "/api/maintenance/spells", newSpell) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) + + t.Run("standard user returns 403", func(t *testing.T) { + router := makeRouter(user.RoleStandardUser) + w := perform(router, http.MethodPost, "/api/maintenance/spells", newSpell) + assert.Equal(t, http.StatusForbidden, w.Code) + }) +} + +func TestUpdateMDSpell(t *testing.T) { + setupHandlerTest(t) + spell := seedSpell(t) + + updated := map[string]interface{}{ + "name": "Updated Spell", + "level": 2, + } + + t.Run("maintainer updates spell returns 200", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/spells/%d", spell.ID), updated) + assert.Equal(t, http.StatusOK, w.Code) + }) + + t.Run("not found returns 404", func(t *testing.T) { + c, w := directContext(http.MethodPut, "/api/maintenance/spells/99999", updated) + c.Params = gin.Params{{Key: "id", Value: "99999"}} + UpdateMDSpell(c) + assert.Equal(t, http.StatusNotFound, w.Code) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/spells/%d", spell.ID), updated) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) +} + +func TestUpdateEnhancedMDSpell(t *testing.T) { + setupHandlerTest(t) + spell := seedSpell(t) + + req := SpellUpdateRequest{ + Spell: models.Spell{ + Name: "Enhanced Updated Spell", + Stufe: 3, + }, + } + + t.Run("maintainer updates enhanced spell returns 200", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/spells-enhanced/%d", spell.ID), req) + assert.Equal(t, http.StatusOK, w.Code) + }) + + t.Run("not found returns 500", func(t *testing.T) { + // UpdateEnhancedMDSpell does a silent no-op update then fails to fetch → 500 + c, w := directContext(http.MethodPut, "/api/maintenance/spells-enhanced/99999", req) + c.Params = gin.Params{{Key: "id", Value: "99999"}} + UpdateEnhancedMDSpell(c) + assert.Equal(t, http.StatusInternalServerError, w.Code) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/spells-enhanced/%d", spell.ID), req) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) +} + +func TestDeleteMDSpell(t *testing.T) { + setupHandlerTest(t) + + t.Run("maintainer deletes spell returns 204", func(t *testing.T) { + spell := seedSpell(t) + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodDelete, fmt.Sprintf("/api/maintenance/spells/%d", spell.ID), nil) + assert.Equal(t, http.StatusNoContent, w.Code) + }) + + t.Run("not found returns 404", func(t *testing.T) { + c, w := directContext(http.MethodDelete, "/api/maintenance/spells/99999", nil) + c.Params = gin.Params{{Key: "id", Value: "99999"}} + DeleteMDSpell(c) + assert.Equal(t, http.StatusNotFound, w.Code) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodDelete, "/api/maintenance/spells/1", nil) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) +} + +// ---- Equipment endpoints (require game_system_id) ---- + +func TestGetMDEquipments(t *testing.T) { + setupHandlerTest(t) + router := makeRouter("") + + t.Run("returns 200 with equipment list", func(t *testing.T) { + seedEquipment(t) + w := perform(router, http.MethodGet, "/api/maintenance/equipment?game_system_id=1", nil) + assert.Equal(t, http.StatusOK, w.Code) + + var resp []models.Equipment + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.GreaterOrEqual(t, len(resp), 1) + }) + + t.Run("invalid game_system_id returns 400", func(t *testing.T) { + w := perform(router, http.MethodGet, "/api/maintenance/equipment?game_system_id=abc", nil) + assert.Equal(t, http.StatusBadRequest, w.Code) + }) +} + +func TestGetMDEquipment(t *testing.T) { + setupHandlerTest(t) + router := makeRouter("") + + t.Run("happy path returns 200", func(t *testing.T) { + eq := seedEquipment(t) + w := perform(router, http.MethodGet, fmt.Sprintf("/api/maintenance/equipment/%d?game_system_id=1", eq.ID), nil) + assert.Equal(t, http.StatusOK, w.Code) + + var resp models.Equipment + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(t, eq.ID, resp.ID) + assert.Equal(t, "Test Equipment HTTP", resp.Name) + }) + + t.Run("not found returns 404", func(t *testing.T) { + w := perform(router, http.MethodGet, "/api/maintenance/equipment/99999?game_system_id=1", nil) + assert.Equal(t, http.StatusNotFound, w.Code) + }) +} + +func TestGetEnhancedMDEquipment(t *testing.T) { + setupHandlerTest(t) + router := makeRouter("") + + t.Run("returns 200 with equipment and sources", func(t *testing.T) { + w := perform(router, http.MethodGet, "/api/maintenance/equipment-enhanced?game_system_id=1", nil) + assert.Equal(t, http.StatusOK, w.Code) + + var resp map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Contains(t, resp, "equipment") + assert.Contains(t, resp, "sources") + }) +} + +func TestGetEnhancedMDEquipmentItem(t *testing.T) { + setupHandlerTest(t) + router := makeRouter("") + + t.Run("happy path returns 200", func(t *testing.T) { + eq := seedEquipment(t) + w := perform(router, http.MethodGet, fmt.Sprintf("/api/maintenance/equipment-enhanced/%d?game_system_id=1", eq.ID), nil) + assert.Equal(t, http.StatusOK, w.Code) + }) + + t.Run("not found returns 404", func(t *testing.T) { + w := perform(router, http.MethodGet, "/api/maintenance/equipment-enhanced/99999?game_system_id=1", nil) + assert.Equal(t, http.StatusNotFound, w.Code) + }) +} + +func TestAddEquipment(t *testing.T) { + setupHandlerTest(t) + + newEq := map[string]interface{}{ + "name": "New Equipment", + "gewicht": 1.0, + "wert": 10.0, + } + + t.Run("maintainer creates equipment returns 201", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPost, "/api/maintenance/equipment?game_system_id=1", newEq) + assert.Equal(t, http.StatusCreated, w.Code) + + var resp models.Equipment + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(t, "New Equipment", resp.Name) + assert.NotZero(t, resp.ID) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodPost, "/api/maintenance/equipment?game_system_id=1", newEq) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) + + t.Run("standard user returns 403", func(t *testing.T) { + router := makeRouter(user.RoleStandardUser) + w := perform(router, http.MethodPost, "/api/maintenance/equipment?game_system_id=1", newEq) + assert.Equal(t, http.StatusForbidden, w.Code) + }) +} + +func TestUpdateMDEquipment(t *testing.T) { + setupHandlerTest(t) + eq := seedEquipment(t) + + updated := map[string]interface{}{ + "name": "Updated Equipment", + "gewicht": 2.0, + "wert": 20.0, + } + + t.Run("maintainer updates equipment returns 200", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/equipment/%d?game_system_id=1", eq.ID), updated) + assert.Equal(t, http.StatusOK, w.Code) + }) + + t.Run("not found returns 404", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPut, "/api/maintenance/equipment/99999?game_system_id=1", updated) + assert.Equal(t, http.StatusNotFound, w.Code) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/equipment/%d?game_system_id=1", eq.ID), updated) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) +} + +func TestUpdateEnhancedMDEquipmentItem(t *testing.T) { + setupHandlerTest(t) + eq := seedEquipment(t) + + req := EquipmentUpdateRequest{ + Equipment: models.Equipment{ + Name: "Enhanced Updated Equipment", + Gewicht: 3.0, + Wert: 30.0, + }, + } + + t.Run("maintainer updates enhanced equipment returns 200", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/equipment-enhanced/%d?game_system_id=1", eq.ID), req) + assert.Equal(t, http.StatusOK, w.Code) + }) + + t.Run("not found returns 500", func(t *testing.T) { + // UpdateEnhancedMDEquipmentItem does a silent no-op update then fails to fetch → 500 + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPut, "/api/maintenance/equipment-enhanced/99999?game_system_id=1", req) + assert.Equal(t, http.StatusInternalServerError, w.Code) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/equipment-enhanced/%d?game_system_id=1", eq.ID), req) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) +} + +func TestDeleteMDEquipment(t *testing.T) { + setupHandlerTest(t) + + t.Run("maintainer deletes equipment returns 204", func(t *testing.T) { + eq := seedEquipment(t) + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodDelete, fmt.Sprintf("/api/maintenance/equipment/%d?game_system_id=1", eq.ID), nil) + assert.Equal(t, http.StatusNoContent, w.Code) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodDelete, "/api/maintenance/equipment/1?game_system_id=1", nil) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) +} + +// ---- Weapon endpoints (require game_system_id) ---- + +func TestGetMDWeapons(t *testing.T) { + setupHandlerTest(t) + router := makeRouter("") + + t.Run("returns 200 with weapons list", func(t *testing.T) { + seedWeapon(t) + w := perform(router, http.MethodGet, "/api/maintenance/weapons?game_system_id=1", nil) + assert.Equal(t, http.StatusOK, w.Code) + + var resp []models.Weapon + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.GreaterOrEqual(t, len(resp), 1) + }) + + t.Run("invalid game_system_id returns 400", func(t *testing.T) { + w := perform(router, http.MethodGet, "/api/maintenance/weapons?game_system_id=abc", nil) + assert.Equal(t, http.StatusBadRequest, w.Code) + }) +} + +func TestGetMDWeapon(t *testing.T) { + setupHandlerTest(t) + router := makeRouter("") + + t.Run("happy path returns 200", func(t *testing.T) { + weapon := seedWeapon(t) + w := perform(router, http.MethodGet, fmt.Sprintf("/api/maintenance/weapons/%d?game_system_id=1", weapon.ID), nil) + assert.Equal(t, http.StatusOK, w.Code) + + var resp models.Weapon + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(t, weapon.ID, resp.ID) + assert.Equal(t, "Test Weapon HTTP", resp.Name) + }) + + t.Run("not found returns 404", func(t *testing.T) { + w := perform(router, http.MethodGet, "/api/maintenance/weapons/99999?game_system_id=1", nil) + assert.Equal(t, http.StatusNotFound, w.Code) + }) +} + +func TestGetEnhancedMDWeapons(t *testing.T) { + setupHandlerTest(t) + router := makeRouter("") + + t.Run("returns 200 with weapons and sources", func(t *testing.T) { + w := perform(router, http.MethodGet, "/api/maintenance/weapons-enhanced?game_system_id=1", nil) + assert.Equal(t, http.StatusOK, w.Code) + + var resp map[string]interface{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Contains(t, resp, "weapons") + assert.Contains(t, resp, "sources") + }) +} + +func TestGetEnhancedMDWeapon(t *testing.T) { + setupHandlerTest(t) + router := makeRouter("") + + t.Run("happy path returns 200", func(t *testing.T) { + weapon := seedWeapon(t) + w := perform(router, http.MethodGet, fmt.Sprintf("/api/maintenance/weapons-enhanced/%d?game_system_id=1", weapon.ID), nil) + assert.Equal(t, http.StatusOK, w.Code) + }) + + t.Run("not found returns 404", func(t *testing.T) { + w := perform(router, http.MethodGet, "/api/maintenance/weapons-enhanced/99999?game_system_id=1", nil) + assert.Equal(t, http.StatusNotFound, w.Code) + }) +} + +func TestAddWeapon(t *testing.T) { + setupHandlerTest(t) + + newWeapon := map[string]interface{}{ + "name": "New Weapon", + "damage": "1W8", + } + + t.Run("maintainer creates weapon returns 201", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPost, "/api/maintenance/weapons?game_system_id=1", newWeapon) + assert.Equal(t, http.StatusCreated, w.Code) + + var resp models.Weapon + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(t, "New Weapon", resp.Name) + assert.NotZero(t, resp.ID) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodPost, "/api/maintenance/weapons?game_system_id=1", newWeapon) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) + + t.Run("standard user returns 403", func(t *testing.T) { + router := makeRouter(user.RoleStandardUser) + w := perform(router, http.MethodPost, "/api/maintenance/weapons?game_system_id=1", newWeapon) + assert.Equal(t, http.StatusForbidden, w.Code) + }) +} + +func TestUpdateMDWeapon(t *testing.T) { + setupHandlerTest(t) + weapon := seedWeapon(t) + + updated := map[string]interface{}{ + "name": "Updated Weapon", + "damage": "2W6", + } + + t.Run("maintainer updates weapon returns 200", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/weapons/%d?game_system_id=1", weapon.ID), updated) + assert.Equal(t, http.StatusOK, w.Code) + }) + + t.Run("not found returns 404", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPut, "/api/maintenance/weapons/99999?game_system_id=1", updated) + assert.Equal(t, http.StatusNotFound, w.Code) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/weapons/%d?game_system_id=1", weapon.ID), updated) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) +} + +func TestUpdateEnhancedMDWeapon(t *testing.T) { + setupHandlerTest(t) + weapon := seedWeapon(t) + + req := map[string]interface{}{ + "name": "Enhanced Updated Weapon", + "damage": "3W4", + } + + t.Run("maintainer updates enhanced weapon returns 200", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/weapons-enhanced/%d?game_system_id=1", weapon.ID), req) + assert.Equal(t, http.StatusOK, w.Code) + }) + + t.Run("not found returns 404", func(t *testing.T) { + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodPut, "/api/maintenance/weapons-enhanced/99999?game_system_id=1", req) + assert.Equal(t, http.StatusNotFound, w.Code) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodPut, fmt.Sprintf("/api/maintenance/weapons-enhanced/%d?game_system_id=1", weapon.ID), req) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) +} + +func TestDeleteMDWeapon(t *testing.T) { + setupHandlerTest(t) + + t.Run("maintainer deletes weapon returns 204", func(t *testing.T) { + weapon := seedWeapon(t) + router := makeRouter(user.RoleMaintainer) + w := perform(router, http.MethodDelete, fmt.Sprintf("/api/maintenance/weapons/%d?game_system_id=1", weapon.ID), nil) + assert.Equal(t, http.StatusNoContent, w.Code) + }) + + t.Run("no auth returns 401", func(t *testing.T) { + router := makeRouter("") + w := perform(router, http.MethodDelete, "/api/maintenance/weapons/1?game_system_id=1", nil) + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) +} diff --git a/backend/maintenance/api_maint_test.go b/backend/maintenance/api_maint_test.go index 5aa4d78..d4a048c 100644 --- a/backend/maintenance/api_maint_test.go +++ b/backend/maintenance/api_maint_test.go @@ -57,7 +57,6 @@ func TestGetMasterData(t *testing.T) { character.RegisterRoutes(protected) gsmaster.RegisterRoutes(protected) RegisterRoutes(protected) - protected.GET("/maintenance", gsmaster.GetMasterData) protected.GET("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "Test OK"}) }) diff --git a/backend/models/model_gsmaster.go b/backend/models/model_gsmaster.go index 6ebd0b2..3352873 100644 --- a/backend/models/model_gsmaster.go +++ b/backend/models/model_gsmaster.go @@ -397,6 +397,17 @@ func (object *Spell) Save() error { return nil } +func (object *Spell) Delete() error { + result := database.DB.Delete(&object, object.ID) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return fmt.Errorf("no record found with ID %v", object.ID) + } + return nil +} + // SelectSpells gibt alle Zauber zurück, optional gefiltert nach einem Feld func SelectSpells(opts ...string) ([]Spell, error) { fieldName := ""