340 lines
10 KiB
Go
340 lines
10 KiB
Go
|
|
package character
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"bamort/database"
|
||
|
|
"bamort/models"
|
||
|
|
"bamort/testutils"
|
||
|
|
"bamort/user"
|
||
|
|
|
||
|
|
"github.com/gin-gonic/gin"
|
||
|
|
"github.com/stretchr/testify/require"
|
||
|
|
"gorm.io/gorm"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestOwnershipGuardsBlockMutations(t *testing.T) {
|
||
|
|
testutils.SetupTestEnvironment(t)
|
||
|
|
gin.SetMode(gin.TestMode)
|
||
|
|
|
||
|
|
database.SetupTestDB(true, true)
|
||
|
|
t.Cleanup(database.ResetTestDB)
|
||
|
|
|
||
|
|
require.NoError(t, models.MigrateStructure())
|
||
|
|
|
||
|
|
t.Run("UpdateCharacter blocks non-owner", func(t *testing.T) {
|
||
|
|
owner := ensureUserExists(t, 101)
|
||
|
|
char := createCharacterOwnedBy(t, owner.UserID)
|
||
|
|
originalName := char.Name
|
||
|
|
|
||
|
|
ctx, w := buildJSONContext(t, http.MethodPut, map[string]any{"name": "New Name"}, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||
|
|
|
||
|
|
UpdateCharacter(ctx)
|
||
|
|
|
||
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
||
|
|
reloaded := reloadCharacter(t, char.ID)
|
||
|
|
require.Equal(t, originalName, reloaded.Name)
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("DeleteCharacter blocks non-owner", func(t *testing.T) {
|
||
|
|
owner := ensureUserExists(t, 102)
|
||
|
|
char := createCharacterOwnedBy(t, owner.UserID)
|
||
|
|
|
||
|
|
ctx, w := buildJSONContext(t, http.MethodDelete, nil, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||
|
|
|
||
|
|
DeleteCharacter(ctx)
|
||
|
|
|
||
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
||
|
|
var exists models.Char
|
||
|
|
err := database.DB.First(&exists, char.ID).Error
|
||
|
|
require.NoError(t, err)
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("UpdateCharacterExperience blocks non-owner", func(t *testing.T) {
|
||
|
|
owner := ensureUserExists(t, 103)
|
||
|
|
char := createCharacterOwnedBy(t, owner.UserID)
|
||
|
|
seedExperience(t, char, 25)
|
||
|
|
|
||
|
|
ctx, w := buildJSONContext(t, http.MethodPost, map[string]any{"experience_points": 50}, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||
|
|
|
||
|
|
UpdateCharacterExperience(ctx)
|
||
|
|
|
||
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
||
|
|
reloaded := reloadCharacterWithPreloads(t, char.ID)
|
||
|
|
require.Equal(t, 25, reloaded.Erfahrungsschatz.EP)
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("UpdateCharacterWealth blocks non-owner", func(t *testing.T) {
|
||
|
|
owner := ensureUserExists(t, 104)
|
||
|
|
char := createCharacterOwnedBy(t, owner.UserID)
|
||
|
|
seedWealth(t, char, 5, 4, 3)
|
||
|
|
|
||
|
|
ctx, w := buildJSONContext(t, http.MethodPost, map[string]any{"goldstücke": 50}, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||
|
|
|
||
|
|
UpdateCharacterWealth(ctx)
|
||
|
|
|
||
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
||
|
|
reloaded := reloadCharacterWithPreloads(t, char.ID)
|
||
|
|
require.Equal(t, 5, reloaded.Vermoegen.Goldstuecke)
|
||
|
|
require.Equal(t, 4, reloaded.Vermoegen.Silberstuecke)
|
||
|
|
require.Equal(t, 3, reloaded.Vermoegen.Kupferstuecke)
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("LearnSkill blocks non-owner", func(t *testing.T) {
|
||
|
|
owner := ensureUserExists(t, 105)
|
||
|
|
char := createCharacterOwnedBy(t, owner.UserID)
|
||
|
|
before := countSkills(t, char.ID)
|
||
|
|
|
||
|
|
ctx, w := buildJSONContext(t, http.MethodPost, map[string]any{"name": "Test Skill", "target_level": 1, "type": "skill"}, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||
|
|
|
||
|
|
LearnSkill(ctx)
|
||
|
|
|
||
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
||
|
|
after := countSkills(t, char.ID)
|
||
|
|
require.Equal(t, before, after)
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("ImproveSkill blocks non-owner", func(t *testing.T) {
|
||
|
|
owner := ensureUserExists(t, 106)
|
||
|
|
char := createCharacterOwnedBy(t, owner.UserID)
|
||
|
|
seedSkill(t, char, "Athletik", 1, 0)
|
||
|
|
before := countSkills(t, char.ID)
|
||
|
|
|
||
|
|
payload := map[string]any{
|
||
|
|
"char_id": char.ID,
|
||
|
|
"name": "Athletik",
|
||
|
|
"current_level": 1,
|
||
|
|
"target_level": 2,
|
||
|
|
"type": "skill",
|
||
|
|
"action": "improve",
|
||
|
|
"reward": "default",
|
||
|
|
}
|
||
|
|
ctx, w := buildJSONContext(t, http.MethodPost, payload, char.UserID+1, nil)
|
||
|
|
|
||
|
|
ImproveSkill(ctx)
|
||
|
|
|
||
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
||
|
|
after := countSkills(t, char.ID)
|
||
|
|
require.Equal(t, before, after)
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("LearnSpell blocks non-owner", func(t *testing.T) {
|
||
|
|
owner := ensureUserExists(t, 107)
|
||
|
|
char := createCharacterOwnedBy(t, owner.UserID)
|
||
|
|
before := countSpells(t, char.ID)
|
||
|
|
|
||
|
|
ctx, w := buildJSONContext(t, http.MethodPost, map[string]any{"name": "Test Spell"}, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||
|
|
|
||
|
|
LearnSpell(ctx)
|
||
|
|
|
||
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
||
|
|
after := countSpells(t, char.ID)
|
||
|
|
require.Equal(t, before, after)
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("UpdatePracticePoints blocks non-owner", func(t *testing.T) {
|
||
|
|
owner := ensureUserExists(t, 108)
|
||
|
|
char := createCharacterOwnedBy(t, owner.UserID)
|
||
|
|
seedSkill(t, char, "Menschenkenntnis", 1, 2)
|
||
|
|
before := fetchSkillPp(t, char.ID, "Menschenkenntnis")
|
||
|
|
|
||
|
|
payload := []map[string]any{{"skill_name": "Menschenkenntnis", "amount": 0}}
|
||
|
|
ctx, w := buildJSONContext(t, http.MethodPost, payload, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||
|
|
|
||
|
|
UpdatePracticePoints(ctx)
|
||
|
|
|
||
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
||
|
|
after := fetchSkillPp(t, char.ID, "Menschenkenntnis")
|
||
|
|
require.Equal(t, before, after)
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("AddPracticePoint blocks non-owner", func(t *testing.T) {
|
||
|
|
owner := ensureUserExists(t, 109)
|
||
|
|
char := createCharacterOwnedBy(t, owner.UserID)
|
||
|
|
seedSkill(t, char, "Athletik", 1, 1)
|
||
|
|
before := fetchSkillPp(t, char.ID, "Athletik")
|
||
|
|
|
||
|
|
payload := map[string]any{"skill_name": "Athletik", "amount": 3}
|
||
|
|
ctx, w := buildJSONContext(t, http.MethodPost, payload, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||
|
|
|
||
|
|
AddPracticePoint(ctx)
|
||
|
|
|
||
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
||
|
|
after := fetchSkillPp(t, char.ID, "Athletik")
|
||
|
|
require.Equal(t, before, after)
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("UsePracticePoint blocks non-owner", func(t *testing.T) {
|
||
|
|
owner := ensureUserExists(t, 110)
|
||
|
|
char := createCharacterOwnedBy(t, owner.UserID)
|
||
|
|
seedSkill(t, char, "Athletik", 1, 2)
|
||
|
|
before := fetchSkillPp(t, char.ID, "Athletik")
|
||
|
|
|
||
|
|
payload := map[string]any{"skill_name": "Athletik", "amount": 1}
|
||
|
|
ctx, w := buildJSONContext(t, http.MethodPost, payload, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||
|
|
|
||
|
|
UsePracticePoint(ctx)
|
||
|
|
|
||
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
||
|
|
after := fetchSkillPp(t, char.ID, "Athletik")
|
||
|
|
require.Equal(t, before, after)
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("UpdateCharacterShares blocks non-owner", func(t *testing.T) {
|
||
|
|
owner := ensureUserExists(t, 111)
|
||
|
|
shareTarget := ensureUserExists(t, 112)
|
||
|
|
char := createCharacterOwnedBy(t, owner.UserID)
|
||
|
|
|
||
|
|
payload := map[string]any{"user_ids": []uint{shareTarget.UserID}}
|
||
|
|
ctx, w := buildJSONContext(t, http.MethodPost, payload, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||
|
|
|
||
|
|
UpdateCharacterShares(ctx)
|
||
|
|
|
||
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
||
|
|
var shares []models.CharShare
|
||
|
|
err := database.DB.Where("character_id = ?", char.ID).Find(&shares).Error
|
||
|
|
require.NoError(t, err)
|
||
|
|
require.Empty(t, shares)
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("UpdateCharacterImage blocks non-owner", func(t *testing.T) {
|
||
|
|
owner := ensureUserExists(t, 113)
|
||
|
|
char := createCharacterOwnedBy(t, owner.UserID)
|
||
|
|
char.Image = "initial.png"
|
||
|
|
require.NoError(t, database.DB.Save(&char).Error)
|
||
|
|
|
||
|
|
ctx, w := buildJSONContext(t, http.MethodPost, map[string]any{"image": "new.png"}, char.UserID+1, map[string]string{"id": fmt.Sprint(char.ID)})
|
||
|
|
|
||
|
|
UpdateCharacterImage(ctx)
|
||
|
|
|
||
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
||
|
|
reloaded := reloadCharacter(t, char.ID)
|
||
|
|
require.Equal(t, "initial.png", reloaded.Image)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func ensureUserExists(t *testing.T, id uint) user.User {
|
||
|
|
var existing user.User
|
||
|
|
err := database.DB.First(&existing, "user_id = ?", id).Error
|
||
|
|
if err == nil {
|
||
|
|
return existing
|
||
|
|
}
|
||
|
|
|
||
|
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||
|
|
require.NoError(t, err)
|
||
|
|
}
|
||
|
|
|
||
|
|
newUser := user.User{
|
||
|
|
UserID: id,
|
||
|
|
Username: fmt.Sprintf("user-%d-%d", id, time.Now().UnixNano()),
|
||
|
|
Email: fmt.Sprintf("user-%d@example.com", id),
|
||
|
|
Role: user.RoleStandardUser,
|
||
|
|
}
|
||
|
|
require.NoError(t, database.DB.Create(&newUser).Error)
|
||
|
|
return newUser
|
||
|
|
}
|
||
|
|
|
||
|
|
func createCharacterOwnedBy(t *testing.T, ownerID uint) models.Char {
|
||
|
|
char := models.Char{
|
||
|
|
BamortBase: models.BamortBase{Name: fmt.Sprintf("Char-%d", time.Now().UnixNano())},
|
||
|
|
UserID: ownerID,
|
||
|
|
Typ: "Krieger",
|
||
|
|
Rasse: "Mensch",
|
||
|
|
Grad: 1,
|
||
|
|
}
|
||
|
|
require.NoError(t, database.DB.Create(&char).Error)
|
||
|
|
return char
|
||
|
|
}
|
||
|
|
|
||
|
|
func seedExperience(t *testing.T, char models.Char, ep int) {
|
||
|
|
exp := models.Erfahrungsschatz{
|
||
|
|
BamortCharTrait: models.BamortCharTrait{CharacterID: char.ID, UserID: char.UserID},
|
||
|
|
EP: ep,
|
||
|
|
}
|
||
|
|
require.NoError(t, database.DB.Create(&exp).Error)
|
||
|
|
}
|
||
|
|
|
||
|
|
func seedWealth(t *testing.T, char models.Char, gold, silver, copper int) {
|
||
|
|
wealth := models.Vermoegen{
|
||
|
|
BamortCharTrait: models.BamortCharTrait{CharacterID: char.ID, UserID: char.UserID},
|
||
|
|
Goldstuecke: gold,
|
||
|
|
Silberstuecke: silver,
|
||
|
|
Kupferstuecke: copper,
|
||
|
|
}
|
||
|
|
require.NoError(t, database.DB.Create(&wealth).Error)
|
||
|
|
}
|
||
|
|
|
||
|
|
func seedSkill(t *testing.T, char models.Char, name string, level, pp int) {
|
||
|
|
skill := models.SkFertigkeit{
|
||
|
|
BamortCharTrait: models.BamortCharTrait{
|
||
|
|
BamortBase: models.BamortBase{Name: name},
|
||
|
|
CharacterID: char.ID,
|
||
|
|
UserID: char.UserID,
|
||
|
|
},
|
||
|
|
Fertigkeitswert: level,
|
||
|
|
Pp: pp,
|
||
|
|
Improvable: true,
|
||
|
|
Category: "Test",
|
||
|
|
}
|
||
|
|
require.NoError(t, database.DB.Create(&skill).Error)
|
||
|
|
}
|
||
|
|
|
||
|
|
func countSkills(t *testing.T, charID uint) int {
|
||
|
|
var skills []models.SkFertigkeit
|
||
|
|
require.NoError(t, database.DB.Where("character_id = ?", charID).Find(&skills).Error)
|
||
|
|
return len(skills)
|
||
|
|
}
|
||
|
|
|
||
|
|
func countSpells(t *testing.T, charID uint) int {
|
||
|
|
var spells []models.SkZauber
|
||
|
|
require.NoError(t, database.DB.Where("character_id = ?", charID).Find(&spells).Error)
|
||
|
|
return len(spells)
|
||
|
|
}
|
||
|
|
|
||
|
|
func fetchSkillPp(t *testing.T, charID uint, name string) int {
|
||
|
|
var skill models.SkFertigkeit
|
||
|
|
err := database.DB.Where("character_id = ? AND name = ?", charID, name).First(&skill).Error
|
||
|
|
require.NoError(t, err)
|
||
|
|
return skill.Pp
|
||
|
|
}
|
||
|
|
|
||
|
|
func reloadCharacter(t *testing.T, id uint) models.Char {
|
||
|
|
var char models.Char
|
||
|
|
require.NoError(t, database.DB.First(&char, id).Error)
|
||
|
|
return char
|
||
|
|
}
|
||
|
|
|
||
|
|
func reloadCharacterWithPreloads(t *testing.T, id uint) models.Char {
|
||
|
|
var char models.Char
|
||
|
|
require.NoError(t, database.DB.Preload("Erfahrungsschatz").Preload("Vermoegen").First(&char, id).Error)
|
||
|
|
return char
|
||
|
|
}
|
||
|
|
|
||
|
|
func buildJSONContext(t *testing.T, method string, body any, userID uint, params map[string]string) (*gin.Context, *httptest.ResponseRecorder) {
|
||
|
|
var buf bytes.Buffer
|
||
|
|
if body != nil {
|
||
|
|
require.NoError(t, json.NewEncoder(&buf).Encode(body))
|
||
|
|
}
|
||
|
|
|
||
|
|
req, err := http.NewRequest(method, "/", &buf)
|
||
|
|
require.NoError(t, err)
|
||
|
|
req.Header.Set("Content-Type", "application/json")
|
||
|
|
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
ctx, _ := gin.CreateTestContext(w)
|
||
|
|
ctx.Request = req
|
||
|
|
ctx.Set("userID", userID)
|
||
|
|
|
||
|
|
for k, v := range params {
|
||
|
|
ctx.Params = append(ctx.Params, gin.Param{Key: k, Value: v})
|
||
|
|
}
|
||
|
|
|
||
|
|
return ctx, w
|
||
|
|
}
|