editing and maintenance and user suggestions
* every user has a right of a username and a display name and has the right to change it * System information page now shows information about database user count, char count, and database schema version * more maintenance lists * show the right values in columns and fields * move similar from inside of frontend component functions to utility js when used multiple times * display help on mouse over * add more than one believe to character * make char name editable with better char info in headline * GiT Gifttoleranz value not calculated correctly * Bump backend to 0.2.3, frontend to 0.2.2
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
package config
|
||||
package appsystem
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -8,3 +8,7 @@ import (
|
||||
func Versionsinfo(c *gin.Context) {
|
||||
c.JSON(200, GetInfo())
|
||||
}
|
||||
|
||||
func SystemInfo(c *gin.Context) {
|
||||
c.JSON(200, GetInfo2())
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package config
|
||||
package appsystem
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// RegisterRoutes registers config-related routes (protected)
|
||||
func RegisterRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/version", Versionsinfo)
|
||||
r.GET("/systeminfo", SystemInfo)
|
||||
}
|
||||
|
||||
// RegisterPublicRoutes registers public config routes (no auth required)
|
||||
@@ -12,4 +13,5 @@ func RegisterPublicRoutes(r *gin.Engine) {
|
||||
// Public version endpoint - no authentication required
|
||||
public := r.Group("/api/public")
|
||||
public.GET("/version", Versionsinfo)
|
||||
public.GET("/systeminfo", SystemInfo)
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
package config
|
||||
package appsystem
|
||||
|
||||
import (
|
||||
"bamort/database"
|
||||
)
|
||||
|
||||
// Version is the application version
|
||||
const Version = "0.2.2"
|
||||
@@ -50,6 +54,22 @@ type Info struct {
|
||||
Version string `json:"version"`
|
||||
GitCommit string `json:"gitCommit"`
|
||||
}
|
||||
type Info2 struct {
|
||||
Version string `json:"version"`
|
||||
GitCommit string `json:"gitCommit"`
|
||||
UserCount int64 `json:"userCount"`
|
||||
CharCount int64 `json:"charCount"`
|
||||
DbVersion string `json:"dbVersion"`
|
||||
}
|
||||
|
||||
func getUserCount() int {
|
||||
// Placeholder implementation
|
||||
return 42
|
||||
}
|
||||
func getCharCount() int {
|
||||
// Placeholder implementation
|
||||
return 123456
|
||||
}
|
||||
|
||||
// GetInfo returns version information as a struct
|
||||
func GetInfo() Info {
|
||||
@@ -58,3 +78,20 @@ func GetInfo() Info {
|
||||
GitCommit: GitCommit,
|
||||
}
|
||||
}
|
||||
|
||||
func GetInfo2() Info2 {
|
||||
info := Info2{
|
||||
Version: Version,
|
||||
GitCommit: GitCommit,
|
||||
}
|
||||
db := database.DB
|
||||
if db == nil {
|
||||
return info
|
||||
}
|
||||
|
||||
db.Table("char_chars").Count(&info.CharCount)
|
||||
db.Table("users").Count(&info.UserCount)
|
||||
db.Raw("SELECT version FROM schema_version ORDER BY id DESC LIMIT 1").Scan(&info.DbVersion)
|
||||
|
||||
return info
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package config
|
||||
package appsystem
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"bamort/database"
|
||||
"bamort/testutils"
|
||||
)
|
||||
|
||||
func TestGetVersion(t *testing.T) {
|
||||
@@ -41,3 +44,24 @@ func TestGetInfo(t *testing.T) {
|
||||
t.Errorf("Expected info.Version %s, got %s", Version, info.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetInfo2UsesDatabaseValues(t *testing.T) {
|
||||
testutils.SetupTestEnvironment(t)
|
||||
database.ResetTestDB()
|
||||
t.Cleanup(database.ResetTestDB)
|
||||
database.SetupTestDB()
|
||||
|
||||
info := GetInfo2()
|
||||
|
||||
if info.CharCount <= 0 {
|
||||
t.Fatalf("expected character count from database, got %d", info.CharCount)
|
||||
}
|
||||
|
||||
if info.UserCount <= 0 {
|
||||
t.Fatalf("expected user count from database, got %d", info.UserCount)
|
||||
}
|
||||
|
||||
if info.DbVersion == "" {
|
||||
t.Fatalf("expected database schema version, got empty string")
|
||||
}
|
||||
}
|
||||
@@ -195,6 +195,7 @@ func ToFeChar(object *models.Char) *models.FeChar {
|
||||
feC.Fertigkeiten = skills
|
||||
feC.InnateSkills = innateSkills
|
||||
feC.CategorizedSkills = categories
|
||||
feC.Git = object.GetGiftToleranz()
|
||||
return feC
|
||||
}
|
||||
|
||||
|
||||
@@ -1073,9 +1073,11 @@ func TestGetDatasheetOptions(t *testing.T) {
|
||||
assert.Contains(t, socialClasses, "Mittelschicht")
|
||||
|
||||
faiths := response["faiths"].([]interface{})
|
||||
assert.Equal(t, 5, len(faiths))
|
||||
assert.Equal(t, 15, len(faiths))
|
||||
assert.Contains(t, faiths, "Druide")
|
||||
assert.Contains(t, faiths, "Keine")
|
||||
assert.Contains(t, faiths, "Torkin")
|
||||
assert.NotContains(t, faiths, "")
|
||||
|
||||
handedness := response["handedness"].([]interface{})
|
||||
assert.Equal(t, 3, len(handedness))
|
||||
|
||||
@@ -33,8 +33,9 @@ func GetCharacterShares(c *gin.Context) {
|
||||
// Get user details for each share
|
||||
type ShareWithUser struct {
|
||||
models.CharShare
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
var sharesWithUsers []ShareWithUser
|
||||
@@ -42,9 +43,10 @@ func GetCharacterShares(c *gin.Context) {
|
||||
var u user.User
|
||||
if err := u.FirstId(share.UserID); err == nil {
|
||||
sharesWithUsers = append(sharesWithUsers, ShareWithUser{
|
||||
CharShare: share,
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
CharShare: share,
|
||||
Username: u.Username,
|
||||
DisplayName: u.DisplayNameOrUsername(),
|
||||
Email: u.Email,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -128,17 +130,19 @@ func GetAvailableUsersForSharing(c *gin.Context) {
|
||||
|
||||
// Remove sensitive data
|
||||
type UserInfo struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
var userInfos []UserInfo
|
||||
for _, u := range users {
|
||||
userInfos = append(userInfos, UserInfo{
|
||||
UserID: u.UserID,
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
UserID: u.UserID,
|
||||
Username: u.Username,
|
||||
DisplayName: u.DisplayNameOrUsername(),
|
||||
Email: u.Email,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package character
|
||||
|
||||
import (
|
||||
"bamort/database"
|
||||
"bamort/models"
|
||||
"bamort/user"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setupShareHandlerTestEnvironment(t *testing.T) {
|
||||
original := os.Getenv("ENVIRONMENT")
|
||||
os.Setenv("ENVIRONMENT", "test")
|
||||
t.Cleanup(func() {
|
||||
if original != "" {
|
||||
os.Setenv("ENVIRONMENT", original)
|
||||
} else {
|
||||
os.Unsetenv("ENVIRONMENT")
|
||||
}
|
||||
})
|
||||
|
||||
database.SetupTestDB(true, true)
|
||||
t.Cleanup(database.ResetTestDB)
|
||||
|
||||
err := user.MigrateStructure()
|
||||
require.NoError(t, err, "Should migrate user structure")
|
||||
|
||||
err = models.MigrateStructure()
|
||||
require.NoError(t, err, "Should migrate models structure")
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
}
|
||||
|
||||
func createHashedUser(t *testing.T, username, password, email, displayName string) *user.User {
|
||||
u := &user.User{
|
||||
Username: username,
|
||||
PasswordHash: password,
|
||||
Email: email,
|
||||
DisplayName: displayName,
|
||||
}
|
||||
|
||||
hashed := md5.Sum([]byte(password))
|
||||
u.PasswordHash = hex.EncodeToString(hashed[:])
|
||||
|
||||
err := u.Create()
|
||||
require.NoError(t, err, "Should create user")
|
||||
return u
|
||||
}
|
||||
|
||||
func TestGetAvailableUsersForSharingReturnsDisplayNames(t *testing.T) {
|
||||
setupShareHandlerTestEnvironment(t)
|
||||
|
||||
owner := createHashedUser(t, "owneruser", "ownerpass", "owner@example.com", "")
|
||||
sharedUser := createHashedUser(t, "shareduser", "sharedpass", "shared@example.com", "Shared Display")
|
||||
|
||||
char := models.Char{
|
||||
BamortBase: models.BamortBase{Name: "Shared Character"},
|
||||
UserID: owner.UserID,
|
||||
}
|
||||
err := database.DB.Create(&char).Error
|
||||
require.NoError(t, err, "Should create character")
|
||||
|
||||
req, _ := http.NewRequest("GET", fmt.Sprintf("/api/characters/%d/available-users", char.ID), nil)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
c.Params = gin.Params{gin.Param{Key: "id", Value: fmt.Sprintf("%d", char.ID)}}
|
||||
c.Set("userID", owner.UserID)
|
||||
|
||||
GetAvailableUsersForSharing(c)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var response []map[string]interface{}
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, response, 1, "Only the non-owner user should be returned")
|
||||
|
||||
entry := response[0]
|
||||
assert.Equal(t, float64(sharedUser.UserID), entry["user_id"])
|
||||
assert.Equal(t, sharedUser.DisplayName, entry["display_name"])
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bamort/appsystem"
|
||||
"bamort/character"
|
||||
"bamort/config"
|
||||
"bamort/database"
|
||||
@@ -92,11 +93,11 @@ func main() {
|
||||
importer.RegisterRoutes(protected)
|
||||
pdfrender.RegisterRoutes(protected)
|
||||
transfer.RegisterRoutes(protected)
|
||||
config.RegisterRoutes(protected)
|
||||
appsystem.RegisterRoutes(protected)
|
||||
|
||||
// Register public routes (no authentication)
|
||||
pdfrender.RegisterPublicRoutes(r)
|
||||
config.RegisterPublicRoutes(r)
|
||||
appsystem.RegisterPublicRoutes(r)
|
||||
|
||||
logger.Info("API-Routen erfolgreich registriert")
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bamort/database"
|
||||
"bamort/models"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -19,23 +20,32 @@ func GetMiscLookupByKey(key string, order ...string) ([]models.MiscLookup, error
|
||||
func GetMiscLookupByKeyForSystem(key string, gameSystemId uint, order ...string) ([]models.MiscLookup, error) {
|
||||
var items []models.MiscLookup
|
||||
|
||||
orderKey := "value"
|
||||
orderBy := "value ASC"
|
||||
if len(order) > 0 && order[0] != "" {
|
||||
switch order[0] {
|
||||
case "id":
|
||||
orderKey = "id"
|
||||
orderBy = "id ASC"
|
||||
case "value":
|
||||
orderKey = "value"
|
||||
orderBy = "value ASC"
|
||||
case "source":
|
||||
orderKey = "source"
|
||||
orderBy = "source_id ASC, value ASC"
|
||||
case "source_value":
|
||||
orderKey = "source"
|
||||
orderBy = "source_id ASC, value ASC"
|
||||
default:
|
||||
orderKey = "value"
|
||||
orderBy = "value ASC"
|
||||
}
|
||||
}
|
||||
|
||||
gs := models.GetGameSystem(gameSystemId, "")
|
||||
if gs == nil {
|
||||
gs = models.GetGameSystem(0, "")
|
||||
}
|
||||
|
||||
query := database.DB.Where("`key` = ?", key)
|
||||
if gs.ID != 0 {
|
||||
@@ -57,9 +67,96 @@ func GetMiscLookupByKeyForSystem(key string, gameSystemId uint, order ...string)
|
||||
}
|
||||
}
|
||||
|
||||
if key == "faiths" {
|
||||
existingValues := make(map[string]struct{}, len(items))
|
||||
for i := range items {
|
||||
trimmed := strings.TrimSpace(items[i].Value)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
existingValues[trimmed] = struct{}{}
|
||||
}
|
||||
|
||||
believeItems, err := collectBeliefsForSystem(gs)
|
||||
if err != nil {
|
||||
return items, err
|
||||
}
|
||||
|
||||
additions := make([]models.MiscLookup, 0, len(believeItems))
|
||||
for _, believe := range believeItems {
|
||||
value := strings.TrimSpace(believe.Name)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, exists := existingValues[value]; exists {
|
||||
continue
|
||||
}
|
||||
existingValues[value] = struct{}{}
|
||||
|
||||
addition := models.MiscLookup{
|
||||
ID: believe.ID,
|
||||
Key: key,
|
||||
Value: value,
|
||||
SourceID: believe.SourceID,
|
||||
PageNumber: believe.PageNumber,
|
||||
GameSystem: believe.GameSystem,
|
||||
GameSystemId: believe.GameSystemId,
|
||||
}
|
||||
|
||||
if addition.GameSystemId == 0 && gs.ID != 0 {
|
||||
addition.GameSystemId = gs.ID
|
||||
}
|
||||
|
||||
additions = append(additions, addition)
|
||||
}
|
||||
|
||||
if len(additions) > 0 {
|
||||
items = append(items, additions...)
|
||||
sortMiscLookup(items, orderKey)
|
||||
}
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func collectBeliefsForSystem(gs *models.GameSystem) ([]models.Believe, error) {
|
||||
var believes []models.Believe
|
||||
if gs == nil {
|
||||
return believes, nil
|
||||
}
|
||||
|
||||
query := database.DB.Model(&models.Believe{})
|
||||
if gs.ID != 0 {
|
||||
query = query.Where("game_system_id = ?", gs.ID)
|
||||
}
|
||||
if gs.Name != "" {
|
||||
query = query.Or("game_system = ?", gs.Name)
|
||||
}
|
||||
|
||||
if err := query.Find(&believes).Error; err != nil {
|
||||
return believes, err
|
||||
}
|
||||
|
||||
return believes, nil
|
||||
}
|
||||
|
||||
func sortMiscLookup(items []models.MiscLookup, orderKey string) {
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
switch orderKey {
|
||||
case "id":
|
||||
return items[i].ID < items[j].ID
|
||||
case "source":
|
||||
if items[i].SourceID == items[j].SourceID {
|
||||
return items[i].Value < items[j].Value
|
||||
}
|
||||
return items[i].SourceID < items[j].SourceID
|
||||
default:
|
||||
return items[i].Value < items[j].Value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
// PopulateMiscLookupData populates initial misc lookup data if table is empty
|
||||
func PopulateMiscLookupData() error {
|
||||
|
||||
@@ -111,7 +111,7 @@ func TestPopulateMiscLookupData(t *testing.T) {
|
||||
"races": 5,
|
||||
"origins": 15,
|
||||
"social_classes": 4,
|
||||
"faiths": 5,
|
||||
"faiths": 15,
|
||||
"handedness": 3,
|
||||
}
|
||||
|
||||
@@ -133,6 +133,21 @@ func TestPopulateMiscLookupData(t *testing.T) {
|
||||
assert.Contains(t, raceValues, "Mensch")
|
||||
assert.Contains(t, raceValues, "Elf")
|
||||
|
||||
faiths, _ := GetMiscLookupByKey("faiths")
|
||||
faithValues := make([]string, len(faiths))
|
||||
for i, f := range faiths {
|
||||
faithValues[i] = f.Value
|
||||
}
|
||||
assert.Contains(t, faithValues, "Torkin")
|
||||
assert.NotContains(t, faithValues, "")
|
||||
mahalCount := 0
|
||||
for _, v := range faithValues {
|
||||
if v == "Mahal" {
|
||||
mahalCount++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 1, mahalCount)
|
||||
|
||||
/*
|
||||
// Second population should not duplicate data
|
||||
err = PopulateMiscLookupData()
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package maintenance
|
||||
|
||||
import (
|
||||
"bamort/database"
|
||||
"bamort/models"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type believeResponse struct {
|
||||
models.Believe
|
||||
SourceCode string `json:"source_code,omitempty"`
|
||||
}
|
||||
|
||||
type believeUpdateRequest struct {
|
||||
Name string `json:"name"`
|
||||
Beschreibung string `json:"beschreibung"`
|
||||
SourceID *uint `json:"source_id"`
|
||||
PageNumber *int `json:"page_number"`
|
||||
}
|
||||
|
||||
func resolveGameSystemOrDefault(c *gin.Context) *models.GameSystem {
|
||||
gsIDStr := c.Query("game_system_id")
|
||||
gsName := c.Query("game_system")
|
||||
|
||||
var gsID uint
|
||||
if gsIDStr != "" {
|
||||
id, err := strconv.ParseUint(gsIDStr, 10, 32)
|
||||
if err != nil {
|
||||
respondWithError(c, http.StatusBadRequest, "Invalid game_system_id")
|
||||
return nil
|
||||
}
|
||||
gsID = uint(id)
|
||||
}
|
||||
|
||||
gs := models.GetGameSystem(gsID, gsName)
|
||||
if gs == nil {
|
||||
respondWithError(c, http.StatusBadRequest, "Invalid game system")
|
||||
return nil
|
||||
}
|
||||
|
||||
return gs
|
||||
}
|
||||
|
||||
func GetBelieves(c *gin.Context) {
|
||||
gs := resolveGameSystemOrDefault(c)
|
||||
if gs == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var believes []models.Believe
|
||||
if err := database.DB.Where("game_system=? OR game_system_id=?", gs.Name, gs.ID).
|
||||
Order("name ASC").
|
||||
Find(&believes).Error; err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve believes")
|
||||
return
|
||||
}
|
||||
|
||||
var sources []models.Source
|
||||
if err := database.DB.Find(&sources).Error; err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve sources")
|
||||
return
|
||||
}
|
||||
|
||||
sourceMap := make(map[uint]string, len(sources))
|
||||
for _, source := range sources {
|
||||
sourceMap[source.ID] = source.Code
|
||||
}
|
||||
|
||||
enhanced := make([]believeResponse, len(believes))
|
||||
for i, believe := range believes {
|
||||
enhanced[i] = believeResponse{
|
||||
Believe: believe,
|
||||
SourceCode: sourceMap[believe.SourceID],
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"believes": enhanced,
|
||||
"sources": sources,
|
||||
})
|
||||
}
|
||||
|
||||
func UpdateBelieve(c *gin.Context) {
|
||||
gs := resolveGameSystemOrDefault(c)
|
||||
if gs == nil {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
respondWithError(c, http.StatusBadRequest, "Invalid ID")
|
||||
return
|
||||
}
|
||||
|
||||
var believe models.Believe
|
||||
if err := database.DB.First(&believe, uint(id)).Error; err != nil {
|
||||
respondWithError(c, http.StatusNotFound, "Believe not found")
|
||||
return
|
||||
}
|
||||
|
||||
var payload believeUpdateRequest
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
respondWithError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(payload.Name)
|
||||
if name == "" {
|
||||
respondWithError(c, http.StatusBadRequest, "name is required")
|
||||
return
|
||||
}
|
||||
|
||||
believe.Name = name
|
||||
believe.Beschreibung = payload.Beschreibung
|
||||
if payload.SourceID != nil {
|
||||
believe.SourceID = *payload.SourceID
|
||||
} else {
|
||||
believe.SourceID = 0
|
||||
}
|
||||
if payload.PageNumber != nil {
|
||||
believe.PageNumber = *payload.PageNumber
|
||||
} else {
|
||||
believe.PageNumber = 0
|
||||
}
|
||||
|
||||
believe.GameSystem = gs.Name
|
||||
believe.GameSystemId = gs.ID
|
||||
|
||||
if err := database.DB.Save(&believe).Error; err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to update believe")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, believeResponse{
|
||||
Believe: believe,
|
||||
SourceCode: lookupSourceCode(believe.SourceID),
|
||||
})
|
||||
}
|
||||
|
||||
func lookupSourceCode(sourceID uint) string {
|
||||
if sourceID == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var source models.Source
|
||||
if err := database.DB.Select("code").First(&source, sourceID).Error; err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return source.Code
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package maintenance
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"bamort/database"
|
||||
"bamort/models"
|
||||
"bamort/router"
|
||||
"bamort/testutils"
|
||||
"bamort/user"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setupMaintenanceTest(t *testing.T) (string, *gin.Engine, *models.GameSystem) {
|
||||
t.Helper()
|
||||
|
||||
testutils.SetupTestEnvironment(t)
|
||||
database.ResetTestDB()
|
||||
t.Cleanup(database.ResetTestDB)
|
||||
database.SetupTestDB(true)
|
||||
|
||||
var maintainer user.User
|
||||
require.NoError(t, database.DB.First(&maintainer, 1).Error)
|
||||
maintainer.Role = user.RoleMaintainer
|
||||
require.NoError(t, maintainer.Save())
|
||||
|
||||
token := user.GenerateToken(&maintainer)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.Default()
|
||||
router.SetupGin(r)
|
||||
protected := router.BaseRouterGrp(r)
|
||||
RegisterRoutes(protected)
|
||||
|
||||
gs := models.GetGameSystem(0, "")
|
||||
require.NotNil(t, gs)
|
||||
|
||||
return token, r, gs
|
||||
}
|
||||
|
||||
func createSource(t *testing.T, gs *models.GameSystem, code string) models.Source {
|
||||
t.Helper()
|
||||
|
||||
source := models.Source{
|
||||
Code: code,
|
||||
Name: fmt.Sprintf("Source %s", code),
|
||||
FullName: fmt.Sprintf("Source %s", code),
|
||||
GameSystem: gs.Name,
|
||||
GameSystemId: gs.ID,
|
||||
IsActive: true,
|
||||
}
|
||||
require.NoError(t, database.DB.Create(&source).Error)
|
||||
return source
|
||||
}
|
||||
|
||||
func createBelieve(t *testing.T, gs *models.GameSystem, source models.Source, name string) models.Believe {
|
||||
t.Helper()
|
||||
|
||||
believe := models.Believe{
|
||||
Name: name,
|
||||
Beschreibung: "Initial description",
|
||||
SourceID: source.ID,
|
||||
PageNumber: 7,
|
||||
GameSystem: gs.Name,
|
||||
GameSystemId: gs.ID,
|
||||
}
|
||||
require.NoError(t, database.DB.Create(&believe).Error)
|
||||
return believe
|
||||
}
|
||||
|
||||
func TestListBelievesReturnsData(t *testing.T) {
|
||||
token, router, gs := setupMaintenanceTest(t)
|
||||
source := createSource(t, gs, "TSTBEL")
|
||||
created := createBelieve(t, gs, source, "Test Believe One")
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "/api/maintenance/gsm-believes", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var payload struct {
|
||||
Believes []struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
SourceID uint `json:"source_id"`
|
||||
SourceCode string `json:"source_code"`
|
||||
} `json:"believes"`
|
||||
Sources []models.Source `json:"sources"`
|
||||
}
|
||||
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &payload))
|
||||
assert.NotEmpty(t, payload.Sources)
|
||||
|
||||
var found bool
|
||||
for _, b := range payload.Believes {
|
||||
if b.ID == created.ID {
|
||||
found = true
|
||||
assert.Equal(t, created.Name, b.Name)
|
||||
assert.Equal(t, source.ID, b.SourceID)
|
||||
assert.Equal(t, source.Code, b.SourceCode)
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, found, "expected created believe in response")
|
||||
}
|
||||
|
||||
func TestUpdateBelieve(t *testing.T) {
|
||||
token, router, gs := setupMaintenanceTest(t)
|
||||
sourceOld := createSource(t, gs, "OLD01")
|
||||
sourceNew := createSource(t, gs, "NEW01")
|
||||
created := createBelieve(t, gs, sourceOld, "Old Believe")
|
||||
|
||||
updateBody := map[string]interface{}{
|
||||
"name": "Updated Believe",
|
||||
"beschreibung": "Updated description",
|
||||
"source_id": sourceNew.ID,
|
||||
"page_number": 123,
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(updateBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/api/maintenance/gsm-believes/%d", created.ID), bytes.NewBuffer(bodyBytes))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var updated models.Believe
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &updated))
|
||||
assert.Equal(t, created.ID, updated.ID)
|
||||
assert.Equal(t, "Updated Believe", updated.Name)
|
||||
assert.Equal(t, "Updated description", updated.Beschreibung)
|
||||
assert.Equal(t, sourceNew.ID, updated.SourceID)
|
||||
assert.Equal(t, 123, updated.PageNumber)
|
||||
assert.Equal(t, gs.Name, updated.GameSystem)
|
||||
assert.Equal(t, gs.ID, updated.GameSystemId)
|
||||
|
||||
var stored models.Believe
|
||||
require.NoError(t, database.DB.First(&stored, created.ID).Error)
|
||||
assert.Equal(t, "Updated Believe", stored.Name)
|
||||
assert.Equal(t, sourceNew.ID, stored.SourceID)
|
||||
assert.Equal(t, 123, stored.PageNumber)
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
package maintenance
|
||||
|
||||
import (
|
||||
"bamort/database"
|
||||
"bamort/models"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func GetGameSystems(c *gin.Context) {
|
||||
var systems []models.GameSystem
|
||||
if err := database.DB.Order("code ASC").Find(&systems).Error; err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve game systems")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"game_systems": systems})
|
||||
}
|
||||
|
||||
type gameSystemUpdateRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func UpdateGameSystem(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
respondWithError(c, http.StatusBadRequest, "Invalid ID")
|
||||
return
|
||||
}
|
||||
|
||||
var gs models.GameSystem
|
||||
if err := database.DB.First(&gs, uint(id)).Error; err != nil {
|
||||
respondWithError(c, http.StatusNotFound, "Game system not found")
|
||||
return
|
||||
}
|
||||
|
||||
var payload gameSystemUpdateRequest
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
respondWithError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(payload.Name)
|
||||
if name != "" {
|
||||
gs.Name = name
|
||||
}
|
||||
gs.Description = payload.Description
|
||||
if payload.IsActive != nil {
|
||||
gs.IsActive = *payload.IsActive
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&gs).Error; err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to update game system")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gs)
|
||||
}
|
||||
|
||||
// --- Sources ---
|
||||
|
||||
type sourceUpdateRequest struct {
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
Edition string `json:"edition"`
|
||||
Publisher string `json:"publisher"`
|
||||
PublishYear int `json:"publish_year"`
|
||||
Description string `json:"description"`
|
||||
IsCore *bool `json:"is_core"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func GetLitSources(c *gin.Context) {
|
||||
gs := resolveGameSystemOrDefault(c)
|
||||
if gs == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var sources []models.Source
|
||||
if err := database.DB.Where("game_system=? OR game_system_id=?", gs.Name, gs.ID).
|
||||
Order("code ASC").
|
||||
Find(&sources).Error; err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve sources")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"sources": sources})
|
||||
}
|
||||
|
||||
func UpdateLitSource(c *gin.Context) {
|
||||
gs := resolveGameSystemOrDefault(c)
|
||||
if gs == nil {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
respondWithError(c, http.StatusBadRequest, "Invalid ID")
|
||||
return
|
||||
}
|
||||
|
||||
var src models.Source
|
||||
if err := database.DB.First(&src, uint(id)).Error; err != nil {
|
||||
respondWithError(c, http.StatusNotFound, "Source not found")
|
||||
return
|
||||
}
|
||||
|
||||
var payload sourceUpdateRequest
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
respondWithError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if name := strings.TrimSpace(payload.Name); name != "" {
|
||||
src.Name = name
|
||||
}
|
||||
src.FullName = payload.FullName
|
||||
src.Edition = payload.Edition
|
||||
src.Publisher = payload.Publisher
|
||||
src.PublishYear = payload.PublishYear
|
||||
src.Description = payload.Description
|
||||
if payload.IsCore != nil {
|
||||
src.IsCore = *payload.IsCore
|
||||
}
|
||||
if payload.IsActive != nil {
|
||||
src.IsActive = *payload.IsActive
|
||||
}
|
||||
src.GameSystem = gs.Name
|
||||
src.GameSystemId = gs.ID
|
||||
|
||||
if err := database.DB.Save(&src).Error; err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to update source")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, src)
|
||||
}
|
||||
|
||||
// --- Misc lookup ---
|
||||
|
||||
type miscUpdateRequest struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
SourceID *uint `json:"source_id"`
|
||||
PageNumber *int `json:"page_number"`
|
||||
}
|
||||
|
||||
func GetMisc(c *gin.Context) {
|
||||
gs := resolveGameSystemOrDefault(c)
|
||||
if gs == nil {
|
||||
return
|
||||
}
|
||||
|
||||
keyFilter := strings.TrimSpace(c.Query("key"))
|
||||
|
||||
var items []models.MiscLookup
|
||||
q := database.DB.Where("game_system=? OR game_system_id=?", gs.Name, gs.ID)
|
||||
if keyFilter != "" {
|
||||
q = q.Where("`key` = ?", keyFilter)
|
||||
}
|
||||
if err := q.Order("`key` ASC, value ASC").Find(&items).Error; err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve misc")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"misc": items})
|
||||
}
|
||||
|
||||
func UpdateMisc(c *gin.Context) {
|
||||
gs := resolveGameSystemOrDefault(c)
|
||||
if gs == nil {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
respondWithError(c, http.StatusBadRequest, "Invalid ID")
|
||||
return
|
||||
}
|
||||
|
||||
var item models.MiscLookup
|
||||
if err := database.DB.First(&item, uint(id)).Error; err != nil {
|
||||
respondWithError(c, http.StatusNotFound, "Misc entry not found")
|
||||
return
|
||||
}
|
||||
|
||||
var payload miscUpdateRequest
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
respondWithError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if key := strings.TrimSpace(payload.Key); key != "" {
|
||||
item.Key = key
|
||||
}
|
||||
if payload.Value != "" {
|
||||
item.Value = payload.Value
|
||||
}
|
||||
if payload.SourceID != nil {
|
||||
item.SourceID = *payload.SourceID
|
||||
}
|
||||
if payload.PageNumber != nil {
|
||||
item.PageNumber = *payload.PageNumber
|
||||
}
|
||||
item.GameSystem = gs.Name
|
||||
item.GameSystemId = gs.ID
|
||||
|
||||
if err := database.DB.Save(&item).Error; err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to update misc entry")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// --- Skill improvement cost2 ---
|
||||
|
||||
type skillImprovementUpdateRequest struct {
|
||||
CurrentLevel *int `json:"current_level"`
|
||||
TERequired *int `json:"te_required"`
|
||||
CategoryID *uint `json:"category_id"`
|
||||
DifficultyID *uint `json:"difficulty_id"`
|
||||
}
|
||||
|
||||
func GetSkillImprovementCost2(c *gin.Context) {
|
||||
var costs []models.SkillImprovementCost
|
||||
if err := database.DB.Order("current_level ASC").Find(&costs).Error; err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve skill improvement costs")
|
||||
return
|
||||
}
|
||||
|
||||
categoryNames, difficultyNames, err := loadSkillMetadata(costs)
|
||||
if err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve skill improvement costs")
|
||||
return
|
||||
}
|
||||
|
||||
responses := make([]skillImprovementCostResponse, len(costs))
|
||||
for i, cost := range costs {
|
||||
responses[i] = skillImprovementCostResponse{
|
||||
SkillImprovementCost: cost,
|
||||
CategoryName: categoryNames[cost.CategoryID],
|
||||
DifficultyName: difficultyNames[cost.DifficultyID],
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"costs": responses})
|
||||
}
|
||||
|
||||
func UpdateSkillImprovementCost2(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
respondWithError(c, http.StatusBadRequest, "Invalid ID")
|
||||
return
|
||||
}
|
||||
|
||||
var cost models.SkillImprovementCost
|
||||
if err := database.DB.First(&cost, uint(id)).Error; err != nil {
|
||||
respondWithError(c, http.StatusNotFound, "Skill improvement cost not found")
|
||||
return
|
||||
}
|
||||
|
||||
var payload skillImprovementUpdateRequest
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
respondWithError(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if payload.CurrentLevel != nil {
|
||||
cost.CurrentLevel = *payload.CurrentLevel
|
||||
}
|
||||
if payload.TERequired != nil {
|
||||
cost.TERequired = *payload.TERequired
|
||||
}
|
||||
if payload.CategoryID != nil {
|
||||
cost.CategoryID = *payload.CategoryID
|
||||
}
|
||||
if payload.DifficultyID != nil {
|
||||
cost.DifficultyID = *payload.DifficultyID
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&cost).Error; err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to update skill improvement cost")
|
||||
return
|
||||
}
|
||||
|
||||
categoryNames, difficultyNames, err := loadSkillMetadata([]models.SkillImprovementCost{cost})
|
||||
if err != nil {
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to update skill improvement cost")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, skillImprovementCostResponse{
|
||||
SkillImprovementCost: cost,
|
||||
CategoryName: categoryNames[cost.CategoryID],
|
||||
DifficultyName: difficultyNames[cost.DifficultyID],
|
||||
})
|
||||
}
|
||||
|
||||
type skillImprovementCostResponse struct {
|
||||
models.SkillImprovementCost
|
||||
CategoryName string `json:"category_name,omitempty"`
|
||||
DifficultyName string `json:"difficulty_name,omitempty"`
|
||||
}
|
||||
|
||||
func loadSkillMetadata(costs []models.SkillImprovementCost) (map[uint]string, map[uint]string, error) {
|
||||
categoryNames := make(map[uint]string)
|
||||
difficultyNames := make(map[uint]string)
|
||||
|
||||
if len(costs) == 0 {
|
||||
return categoryNames, difficultyNames, nil
|
||||
}
|
||||
|
||||
categoryIDs := make([]uint, 0)
|
||||
difficultyIDs := make([]uint, 0)
|
||||
seenCategories := make(map[uint]struct{})
|
||||
seenDifficulties := make(map[uint]struct{})
|
||||
|
||||
for _, cost := range costs {
|
||||
if _, ok := seenCategories[cost.CategoryID]; !ok {
|
||||
seenCategories[cost.CategoryID] = struct{}{}
|
||||
categoryIDs = append(categoryIDs, cost.CategoryID)
|
||||
}
|
||||
if _, ok := seenDifficulties[cost.DifficultyID]; !ok {
|
||||
seenDifficulties[cost.DifficultyID] = struct{}{}
|
||||
difficultyIDs = append(difficultyIDs, cost.DifficultyID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(categoryIDs) > 0 {
|
||||
var categories []models.SkillCategory
|
||||
if err := database.DB.Where("id IN ?", categoryIDs).Find(&categories).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, category := range categories {
|
||||
categoryNames[category.ID] = category.Name
|
||||
}
|
||||
}
|
||||
|
||||
if len(difficultyIDs) > 0 {
|
||||
var difficulties []models.SkillDifficulty
|
||||
if err := database.DB.Where("id IN ?", difficultyIDs).Find(&difficulties).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, difficulty := range difficulties {
|
||||
difficultyNames[difficulty.ID] = difficulty.Name
|
||||
}
|
||||
}
|
||||
|
||||
return categoryNames, difficultyNames, nil
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
package maintenance
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"bamort/database"
|
||||
"bamort/models"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func createGameSystem(t *testing.T, code string) models.GameSystem {
|
||||
t.Helper()
|
||||
gs := models.GameSystem{Code: code, Name: "Game System " + code, Description: "desc", IsActive: true}
|
||||
require.NoError(t, database.DB.Create(&gs).Error)
|
||||
return gs
|
||||
}
|
||||
|
||||
func TestListGameSystems(t *testing.T) {
|
||||
token, router, _ := setupMaintenanceTest(t)
|
||||
created := createGameSystem(t, "TSTGS")
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "/api/maintenance/game-systems", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var payload struct {
|
||||
GameSystems []models.GameSystem `json:"game_systems"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &payload))
|
||||
|
||||
var found bool
|
||||
for _, gs := range payload.GameSystems {
|
||||
if gs.ID == created.ID {
|
||||
found = true
|
||||
assert.Equal(t, created.Code, gs.Code)
|
||||
assert.Equal(t, created.Name, gs.Name)
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "expected created game system in response")
|
||||
}
|
||||
|
||||
func TestUpdateGameSystem(t *testing.T) {
|
||||
token, router, _ := setupMaintenanceTest(t)
|
||||
gs := createGameSystem(t, "UPDGS")
|
||||
|
||||
body := map[string]interface{}{
|
||||
"name": "Updated GS",
|
||||
"description": "Updated desc",
|
||||
"is_active": false,
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/api/maintenance/game-systems/%d", gs.ID), bytes.NewBuffer(bodyBytes))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var updated models.GameSystem
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &updated))
|
||||
assert.Equal(t, gs.ID, updated.ID)
|
||||
assert.Equal(t, "Updated GS", updated.Name)
|
||||
assert.Equal(t, "Updated desc", updated.Description)
|
||||
assert.False(t, updated.IsActive)
|
||||
}
|
||||
|
||||
func createLitSource(t *testing.T, gs models.GameSystem, code string) models.Source {
|
||||
t.Helper()
|
||||
src := models.Source{
|
||||
Code: code,
|
||||
Name: "Source " + code,
|
||||
FullName: "Full " + code,
|
||||
Edition: "1",
|
||||
Publisher: "Pub",
|
||||
PublishYear: 2025,
|
||||
Description: "Desc",
|
||||
IsCore: false,
|
||||
IsActive: true,
|
||||
GameSystem: gs.Name,
|
||||
GameSystemId: gs.ID,
|
||||
}
|
||||
require.NoError(t, database.DB.Create(&src).Error)
|
||||
return src
|
||||
}
|
||||
|
||||
func TestListLitSources(t *testing.T) {
|
||||
token, router, gs := setupMaintenanceTest(t)
|
||||
src := createLitSource(t, *gs, "SRC01")
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/maintenance/gsm-lit-sources", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var payload struct {
|
||||
Sources []models.Source `json:"sources"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &payload))
|
||||
|
||||
var found bool
|
||||
for _, s := range payload.Sources {
|
||||
if s.ID == src.ID {
|
||||
found = true
|
||||
assert.Equal(t, src.Code, s.Code)
|
||||
assert.Equal(t, src.Name, s.Name)
|
||||
}
|
||||
}
|
||||
assert.True(t, found)
|
||||
}
|
||||
|
||||
func TestUpdateLitSource(t *testing.T) {
|
||||
token, router, gs := setupMaintenanceTest(t)
|
||||
src := createLitSource(t, *gs, "SRCUPD")
|
||||
|
||||
body := map[string]interface{}{
|
||||
"name": "Updated Source",
|
||||
"full_name": "Updated Full",
|
||||
"edition": "2",
|
||||
"publisher": "NewPub",
|
||||
"publish_year": 2026,
|
||||
"description": "New Desc",
|
||||
"is_active": false,
|
||||
"is_core": true,
|
||||
}
|
||||
payload, _ := json.Marshal(body)
|
||||
|
||||
req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("/api/maintenance/gsm-lit-sources/%d", src.ID), bytes.NewBuffer(payload))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var updated models.Source
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &updated))
|
||||
assert.Equal(t, "Updated Source", updated.Name)
|
||||
assert.Equal(t, "Updated Full", updated.FullName)
|
||||
assert.Equal(t, "2", updated.Edition)
|
||||
assert.Equal(t, "NewPub", updated.Publisher)
|
||||
assert.Equal(t, 2026, updated.PublishYear)
|
||||
assert.False(t, updated.IsActive)
|
||||
assert.True(t, updated.IsCore)
|
||||
}
|
||||
|
||||
func createMisc(t *testing.T, gs models.GameSystem, key, value string) models.MiscLookup {
|
||||
t.Helper()
|
||||
m := models.MiscLookup{
|
||||
Key: key,
|
||||
Value: value,
|
||||
GameSystem: gs.Name,
|
||||
GameSystemId: gs.ID,
|
||||
}
|
||||
require.NoError(t, database.DB.Create(&m).Error)
|
||||
return m
|
||||
}
|
||||
|
||||
func TestListMisc(t *testing.T) {
|
||||
token, router, gs := setupMaintenanceTest(t)
|
||||
item := createMisc(t, *gs, "origin", "North")
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/maintenance/gsm-misc", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var payload struct {
|
||||
Items []models.MiscLookup `json:"misc"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &payload))
|
||||
|
||||
var found bool
|
||||
for _, it := range payload.Items {
|
||||
if it.ID == item.ID {
|
||||
found = true
|
||||
assert.Equal(t, "origin", it.Key)
|
||||
assert.Equal(t, "North", it.Value)
|
||||
}
|
||||
}
|
||||
assert.True(t, found)
|
||||
}
|
||||
|
||||
func TestUpdateMisc(t *testing.T) {
|
||||
token, router, gs := setupMaintenanceTest(t)
|
||||
item := createMisc(t, *gs, "race", "Human")
|
||||
|
||||
body := map[string]interface{}{
|
||||
"value": "Elf",
|
||||
}
|
||||
payload, _ := json.Marshal(body)
|
||||
|
||||
req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("/api/maintenance/gsm-misc/%d", item.ID), bytes.NewBuffer(payload))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var updated models.MiscLookup
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &updated))
|
||||
assert.Equal(t, "Elf", updated.Value)
|
||||
}
|
||||
|
||||
func createSkillImprovementCost(t *testing.T, gs models.GameSystem) models.SkillImprovementCost {
|
||||
t.Helper()
|
||||
cost := models.SkillImprovementCost{
|
||||
CurrentLevel: 5,
|
||||
TERequired: 2,
|
||||
CategoryID: 1,
|
||||
DifficultyID: 1,
|
||||
}
|
||||
require.NoError(t, database.DB.Create(&cost).Error)
|
||||
return cost
|
||||
}
|
||||
|
||||
func TestListSkillImprovementCost(t *testing.T) {
|
||||
token, router, gs := setupMaintenanceTest(t)
|
||||
created := createSkillImprovementCost(t, *gs)
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/maintenance/skill-improvement-cost2", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var payload struct {
|
||||
Costs []models.SkillImprovementCost `json:"costs"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &payload))
|
||||
|
||||
var found bool
|
||||
for _, c := range payload.Costs {
|
||||
if c.ID == created.ID {
|
||||
found = true
|
||||
assert.Equal(t, created.CurrentLevel, c.CurrentLevel)
|
||||
assert.Equal(t, created.TERequired, c.TERequired)
|
||||
}
|
||||
}
|
||||
assert.True(t, found)
|
||||
}
|
||||
|
||||
func TestUpdateSkillImprovementCost(t *testing.T) {
|
||||
token, router, gs := setupMaintenanceTest(t)
|
||||
created := createSkillImprovementCost(t, *gs)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"te_required": 5,
|
||||
"current_level": 6,
|
||||
}
|
||||
payload, _ := json.Marshal(body)
|
||||
|
||||
req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("/api/maintenance/skill-improvement-cost2/%d", created.ID), bytes.NewBuffer(payload))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var updated models.SkillImprovementCost
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &updated))
|
||||
assert.Equal(t, 5, updated.TERequired)
|
||||
assert.Equal(t, 6, updated.CurrentLevel)
|
||||
}
|
||||
@@ -10,6 +10,16 @@ func RegisterRoutes(r *gin.RouterGroup) {
|
||||
charGrp := r.Group("/maintenance")
|
||||
charGrp.Use(user.RequireMaintainer())
|
||||
{
|
||||
charGrp.GET("/gsm-believes", GetBelieves)
|
||||
charGrp.PUT("/gsm-believes/:id", UpdateBelieve)
|
||||
charGrp.GET("/game-systems", GetGameSystems)
|
||||
charGrp.PUT("/game-systems/:id", UpdateGameSystem)
|
||||
charGrp.GET("/gsm-lit-sources", GetLitSources)
|
||||
charGrp.PUT("/gsm-lit-sources/:id", UpdateLitSource)
|
||||
charGrp.GET("/gsm-misc", GetMisc)
|
||||
charGrp.PUT("/gsm-misc/:id", UpdateMisc)
|
||||
charGrp.GET("/skill-improvement-cost2", GetSkillImprovementCost2)
|
||||
charGrp.PUT("/skill-improvement-cost2/:id", UpdateSkillImprovementCost2)
|
||||
charGrp.GET("/setupcheck", SetupCheck)
|
||||
charGrp.GET("/setupcheck-dev", SetupCheckDev)
|
||||
charGrp.GET("/mktestdata", MakeTestdataFromLive)
|
||||
|
||||
@@ -131,6 +131,7 @@ type CharList struct {
|
||||
|
||||
type FeChar struct {
|
||||
Char
|
||||
Git int `json:"git"` // GiftToleranz
|
||||
CategorizedSkills map[string][]SkFertigkeit `json:"categorizedskills"`
|
||||
InnateSkills []SkFertigkeit `json:"innateskills"`
|
||||
}
|
||||
@@ -140,6 +141,11 @@ func (object *Char) TableName() string {
|
||||
return dbPrefix + "_" + "chars"
|
||||
}
|
||||
|
||||
func (object *Char) GetGiftToleranz() int {
|
||||
//30+(Konstitution/2)
|
||||
konstitution := object.GetAttributeValue("Ko")
|
||||
return 30 + (konstitution / 2)
|
||||
}
|
||||
func (object *Char) ensureGameSystem() {
|
||||
gs := GetGameSystem(object.GameSystemId, object.GameSystem)
|
||||
if gs == nil {
|
||||
@@ -259,7 +265,7 @@ func FindSharedCharList(userID uint) ([]CharList, error) {
|
||||
var chars []CharList
|
||||
gs := GetGameSystem(0, "midgard")
|
||||
err := database.DB.Table("char_chars").
|
||||
Select("char_chars.id, char_chars.name, char_chars.user_id, char_chars.rasse, char_chars.typ, char_chars.grad, char_chars.public, char_chars.game_system, char_chars.game_system_id, users.username as owner").
|
||||
Select("char_chars.id, char_chars.name, char_chars.user_id, char_chars.rasse, char_chars.typ, char_chars.grad, char_chars.public, char_chars.game_system, char_chars.game_system_id, COALESCE(NULLIF(users.display_name, ''), users.username) as owner").
|
||||
Joins("LEFT JOIN users ON char_chars.user_id = users.user_id").
|
||||
Joins("INNER JOIN char_shares ON char_shares.character_id = char_chars.id").
|
||||
Where("char_shares.user_id = ? AND (char_chars.game_system = ? OR char_chars.game_system_id = ?)", userID, gs.Name, gs.ID).
|
||||
@@ -274,7 +280,7 @@ func FindPublicCharList() ([]CharList, error) {
|
||||
var chars []CharList
|
||||
gs := GetGameSystem(0, "midgard")
|
||||
err := database.DB.Table("char_chars").
|
||||
Select("char_chars.id, char_chars.name, char_chars.user_id, char_chars.rasse, char_chars.typ, char_chars.grad, char_chars.public, char_chars.game_system, char_chars.game_system_id, users.username as owner").
|
||||
Select("char_chars.id, char_chars.name, char_chars.user_id, char_chars.rasse, char_chars.typ, char_chars.grad, char_chars.public, char_chars.game_system, char_chars.game_system_id, COALESCE(NULLIF(users.display_name, ''), users.username) as owner").
|
||||
Joins("LEFT JOIN users ON char_chars.user_id = users.user_id").
|
||||
Where("char_chars.public = ? AND (char_chars.game_system = ? OR char_chars.game_system_id = ?)", true, gs.Name, gs.ID).
|
||||
Find(&chars).Error
|
||||
@@ -289,7 +295,7 @@ func FindCharListByUserID(userID uint) ([]CharList, error) {
|
||||
var chars []CharList
|
||||
gs := GetGameSystem(0, "midgard")
|
||||
err := database.DB.Table("char_chars").
|
||||
Select("char_chars.id, char_chars.name, char_chars.user_id, char_chars.rasse, char_chars.typ, char_chars.grad, char_chars.public, char_chars.game_system, char_chars.game_system_id, users.username as owner").
|
||||
Select("char_chars.id, char_chars.name, char_chars.user_id, char_chars.rasse, char_chars.typ, char_chars.grad, char_chars.public, char_chars.game_system, char_chars.game_system_id, COALESCE(NULLIF(users.display_name, ''), users.username) as owner").
|
||||
Joins("LEFT JOIN users ON char_chars.user_id = users.user_id").
|
||||
Where("char_chars.user_id = ? AND (char_chars.game_system = ? OR char_chars.game_system_id = ?)", userID, gs.Name, gs.ID).
|
||||
Find(&chars).Error
|
||||
|
||||
@@ -13,8 +13,11 @@ import (
|
||||
func setupCharacterTestDB(t *testing.T) {
|
||||
database.SetupTestDB()
|
||||
|
||||
err := user.MigrateStructure()
|
||||
require.NoError(t, err, "Failed to migrate user structure")
|
||||
|
||||
// Migrate structures
|
||||
err := MigrateStructure()
|
||||
err = MigrateStructure()
|
||||
require.NoError(t, err, "Failed to migrate database structure")
|
||||
|
||||
// Clean up any existing test data
|
||||
@@ -355,6 +358,40 @@ func TestFindCharListByUserID_Success(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindPublicCharList_UsesOwnerDisplayName(t *testing.T) {
|
||||
setupCharacterTestDB(t)
|
||||
|
||||
displayOwner := &user.User{
|
||||
Username: "display-owner",
|
||||
DisplayName: "Display Owner",
|
||||
PasswordHash: "hash",
|
||||
Email: "display-owner@example.com",
|
||||
}
|
||||
err := displayOwner.Create()
|
||||
require.NoError(t, err, "User creation should succeed")
|
||||
|
||||
char := createTestChar("Public Char With Display Name")
|
||||
char.UserID = displayOwner.UserID
|
||||
char.Public = true
|
||||
|
||||
err = char.Create()
|
||||
require.NoError(t, err, "Character creation should succeed")
|
||||
|
||||
publicChars, err := FindPublicCharList()
|
||||
require.NoError(t, err, "FindPublicCharList should succeed")
|
||||
|
||||
var found *CharList
|
||||
for i := range publicChars {
|
||||
if publicChars[i].ID == char.ID {
|
||||
found = &publicChars[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, found, "Created public character should be present in list")
|
||||
assert.Equal(t, displayOwner.DisplayName, found.Owner, "Owner should use display name")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tests for Eigenschaft struct
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package transfer
|
||||
|
||||
import (
|
||||
"bamort/config"
|
||||
"bamort/appsystem"
|
||||
"bamort/database"
|
||||
"bamort/models"
|
||||
"bamort/user"
|
||||
@@ -90,7 +90,7 @@ func ExportDatabase(exportDir string) (*ExportResult, error) {
|
||||
}
|
||||
|
||||
export := DatabaseExport{
|
||||
Version: config.GetVersion(),
|
||||
Version: appsystem.GetVersion(),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ func ListUsers(c *gin.Context) {
|
||||
for i := range users {
|
||||
users[i].PasswordHash = ""
|
||||
users[i].ResetPwHash = nil
|
||||
users[i].DisplayName = users[i].DisplayNameOrUsername()
|
||||
}
|
||||
|
||||
logger.Info("Successfully fetched %d users", len(users))
|
||||
@@ -77,6 +78,7 @@ func GetUser(c *gin.Context) {
|
||||
// Remove sensitive data
|
||||
user.PasswordHash = ""
|
||||
user.ResetPwHash = nil
|
||||
user.DisplayName = user.DisplayNameOrUsername()
|
||||
|
||||
logger.Info("Successfully fetched user: %s (ID: %d)", user.Username, user.UserID)
|
||||
c.JSON(http.StatusOK, user)
|
||||
@@ -135,9 +137,10 @@ func UpdateUserRole(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "User role updated successfully",
|
||||
"user": gin.H{
|
||||
"id": user.UserID,
|
||||
"username": user.Username,
|
||||
"role": user.Role,
|
||||
"id": user.UserID,
|
||||
"username": user.Username,
|
||||
"display_name": user.DisplayNameOrUsername(),
|
||||
"role": user.Role,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -414,9 +415,10 @@ func ValidateResetToken(c *gin.Context) {
|
||||
|
||||
logger.Debug("Reset-Token gültig für Benutzer: %s", user.Username)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"valid": true,
|
||||
"username": user.Username,
|
||||
"expires": user.ResetPwHashExpires,
|
||||
"valid": true,
|
||||
"username": user.Username,
|
||||
"display_name": user.DisplayNameOrUsername(),
|
||||
"expires": user.ResetPwHashExpires,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -443,6 +445,7 @@ func GetUserProfile(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": user.UserID,
|
||||
"username": user.Username,
|
||||
"display_name": user.DisplayNameOrUsername(),
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
"preferred_language": user.PreferredLanguage,
|
||||
@@ -605,3 +608,51 @@ func UpdateLanguage(c *gin.Context) {
|
||||
"language": user.PreferredLanguage,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateDisplayName Handler to update user's display name
|
||||
func UpdateDisplayName(c *gin.Context) {
|
||||
logger.Debug("Starte Anzeigenamen-Aktualisierung...")
|
||||
|
||||
userID, exists := c.Get("userID")
|
||||
if !exists {
|
||||
logger.Error("Benutzer-ID nicht im Context gefunden")
|
||||
respondWithError(c, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
var input struct {
|
||||
DisplayName string `json:"display_name"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
logger.Error("Fehler beim Parsen des Anzeigenamens: %s", err.Error())
|
||||
respondWithError(c, http.StatusBadRequest, "Display name is required")
|
||||
return
|
||||
}
|
||||
|
||||
if utf8.RuneCountInString(input.DisplayName) > 30 {
|
||||
logger.Warn("Anzeigename zu lang: %d Zeichen", utf8.RuneCountInString(input.DisplayName))
|
||||
respondWithError(c, http.StatusBadRequest, "Display name must be at most 30 characters")
|
||||
return
|
||||
}
|
||||
|
||||
var user User
|
||||
if err := user.FirstId(userID.(uint)); err != nil {
|
||||
logger.Error("Benutzer mit ID %v nicht gefunden: %s", userID, err.Error())
|
||||
respondWithError(c, http.StatusNotFound, "User not found")
|
||||
return
|
||||
}
|
||||
|
||||
user.DisplayName = input.DisplayName
|
||||
if err := user.Save(); err != nil {
|
||||
logger.Error("Fehler beim Speichern des Anzeigenamens für Benutzer %s: %s", user.Username, err.Error())
|
||||
respondWithError(c, http.StatusInternalServerError, "Failed to update display name")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("Anzeigename erfolgreich aktualisiert für Benutzer: %s (ID: %d)", user.Username, user.UserID)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Display name updated successfully",
|
||||
"display_name": user.DisplayNameOrUsername(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"bamort/database"
|
||||
@@ -31,6 +32,83 @@ func setupTestEnvironment(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateDisplayName(t *testing.T) {
|
||||
setupHandlerTestEnvironment(t)
|
||||
|
||||
t.Run("Success - Update display name", func(t *testing.T) {
|
||||
user := createTestUser(t, "displayuser", "password123", "display@test.com")
|
||||
|
||||
requestData := map[string]interface{}{
|
||||
"display_name": "Neuer Anzeigename",
|
||||
}
|
||||
requestBody, _ := json.Marshal(requestData)
|
||||
|
||||
req, _ := http.NewRequest("PUT", "/api/user/display-name", bytes.NewBuffer(requestBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
c.Set("userID", user.UserID)
|
||||
|
||||
UpdateDisplayName(c)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Display name updated successfully", response["message"])
|
||||
assert.Equal(t, "Neuer Anzeigename", response["display_name"])
|
||||
|
||||
var updated User
|
||||
err = updated.FirstId(user.UserID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Neuer Anzeigename", updated.DisplayName)
|
||||
})
|
||||
|
||||
t.Run("Failure - Display name too long", func(t *testing.T) {
|
||||
user := createTestUser(t, "displayuser2", "password123", "display2@test.com")
|
||||
|
||||
requestData := map[string]interface{}{
|
||||
"display_name": strings.Repeat("a", 31),
|
||||
}
|
||||
requestBody, _ := json.Marshal(requestData)
|
||||
|
||||
req, _ := http.NewRequest("PUT", "/api/user/display-name", bytes.NewBuffer(requestBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
c.Set("userID", user.UserID)
|
||||
|
||||
UpdateDisplayName(c)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Display name must be at most 30 characters", response["error"])
|
||||
})
|
||||
|
||||
t.Run("Failure - No user ID in context", func(t *testing.T) {
|
||||
requestData := map[string]interface{}{
|
||||
"display_name": "Any",
|
||||
}
|
||||
requestBody, _ := json.Marshal(requestData)
|
||||
|
||||
req, _ := http.NewRequest("PUT", "/api/user/display-name", bytes.NewBuffer(requestBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
UpdateDisplayName(c)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
// setupHandlerTestEnvironment sets up the test environment for handler tests
|
||||
func setupHandlerTestEnvironment(t *testing.T) {
|
||||
setupTestEnvironment(t)
|
||||
@@ -89,6 +167,45 @@ func TestGetUserProfile(t *testing.T) {
|
||||
assert.Equal(t, float64(testUser.UserID), response["id"])
|
||||
})
|
||||
|
||||
t.Run("Success - Profile uses display name with fallback", func(t *testing.T) {
|
||||
userWithDisplay := createTestUser(t, "profiledisplay", "password123", "profiledisplay@test.com")
|
||||
|
||||
req, _ := http.NewRequest("GET", "/api/user/profile", nil)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
c.Set("userID", userWithDisplay.UserID)
|
||||
|
||||
GetUserProfile(c)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, userWithDisplay.Username, response["display_name"])
|
||||
|
||||
userWithDisplay.DisplayName = "Profile Display"
|
||||
require.NoError(t, userWithDisplay.Save())
|
||||
|
||||
req2, _ := http.NewRequest("GET", "/api/user/profile", nil)
|
||||
w2 := httptest.NewRecorder()
|
||||
c2, _ := gin.CreateTestContext(w2)
|
||||
c2.Request = req2
|
||||
c2.Set("userID", userWithDisplay.UserID)
|
||||
|
||||
GetUserProfile(c2)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w2.Code)
|
||||
|
||||
var response2 map[string]interface{}
|
||||
err = json.Unmarshal(w2.Body.Bytes(), &response2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, userWithDisplay.DisplayName, response2["display_name"])
|
||||
})
|
||||
|
||||
t.Run("Failure - No user ID in context", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/api/user/profile", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
@@ -3,6 +3,7 @@ package user
|
||||
import (
|
||||
"bamort/database"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -18,6 +19,7 @@ const (
|
||||
type User struct {
|
||||
UserID uint `gorm:"primaryKey" json:"id"`
|
||||
Username string `gorm:"unique" json:"username"`
|
||||
DisplayName string `gorm:"not null;default:''" json:"display_name"`
|
||||
PasswordHash string `json:"password"`
|
||||
Email string `gorm:"unique" json:"email"`
|
||||
Role string `gorm:"default:standard" json:"role"`
|
||||
@@ -33,6 +35,10 @@ func (object *User) Create() error {
|
||||
return fmt.Errorf("database connection is nil")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(object.DisplayName) == "" {
|
||||
object.DisplayName = object.Username
|
||||
}
|
||||
|
||||
err := database.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// Save the User record
|
||||
if err := tx.Create(&object).Error; err != nil {
|
||||
@@ -75,6 +81,10 @@ func (object *User) Save() error {
|
||||
return fmt.Errorf("database connection is nil")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(object.DisplayName) == "" {
|
||||
object.DisplayName = object.Username
|
||||
}
|
||||
|
||||
err := database.DB.Save(&object).Error
|
||||
if err != nil {
|
||||
// User found
|
||||
@@ -154,3 +164,10 @@ func (u *User) IsStandardUser() bool {
|
||||
func ValidateRole(role string) bool {
|
||||
return role == RoleStandardUser || role == RoleMaintainer || role == RoleAdmin
|
||||
}
|
||||
|
||||
func (u *User) DisplayNameOrUsername() string {
|
||||
if strings.TrimSpace(u.DisplayName) != "" {
|
||||
return u.DisplayName
|
||||
}
|
||||
return u.Username
|
||||
}
|
||||
|
||||
@@ -39,6 +39,21 @@ func TestUserRoleDefaults(t *testing.T) {
|
||||
assert.Equal(t, RoleStandardUser, user.Role, "New users should have standard role")
|
||||
}
|
||||
|
||||
// TestUserDisplayNameDefaultsToUsername ensures a user's display name falls back to the username
|
||||
func TestUserDisplayNameDefaultsToUsername(t *testing.T) {
|
||||
setupRoleTestEnvironment(t)
|
||||
|
||||
user := &User{
|
||||
Username: "display_user",
|
||||
PasswordHash: "hashedpw",
|
||||
Email: "display@example.com",
|
||||
}
|
||||
|
||||
err := user.Create()
|
||||
require.NoError(t, err, "Should create user")
|
||||
assert.Equal(t, user.Username, user.DisplayName, "DisplayName should default to username")
|
||||
}
|
||||
|
||||
// TestRoleValidation tests role validation
|
||||
func TestRoleValidation(t *testing.T) {
|
||||
assert.True(t, ValidateRole(RoleStandardUser), "standard should be valid")
|
||||
@@ -75,7 +90,7 @@ func TestListUsers(t *testing.T) {
|
||||
setupRoleTestEnvironment(t)
|
||||
|
||||
// Create test users
|
||||
admin := &User{Username: "listadmin", PasswordHash: "hash", Email: "listadmin@test.com", Role: RoleAdmin}
|
||||
admin := &User{Username: "listadmin", PasswordHash: "hash", Email: "listadmin@test.com", Role: RoleAdmin, DisplayName: "List Admin"}
|
||||
require.NoError(t, admin.Create())
|
||||
|
||||
standardUser := &User{Username: "listuser", PasswordHash: "hash", Email: "listuser@test.com", Role: RoleStandardUser}
|
||||
@@ -93,6 +108,27 @@ func TestListUsers(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code, "Admin should access list")
|
||||
|
||||
var response []map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
var adminEntry map[string]interface{}
|
||||
var standardEntry map[string]interface{}
|
||||
for _, entry := range response {
|
||||
switch entry["username"] {
|
||||
case admin.Username:
|
||||
adminEntry = entry
|
||||
case standardUser.Username:
|
||||
standardEntry = entry
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, adminEntry, "admin should be present in response")
|
||||
require.NotNil(t, standardEntry, "standard user should be present in response")
|
||||
|
||||
assert.Equal(t, admin.DisplayName, adminEntry["display_name"], "admin display name should be returned")
|
||||
assert.Equal(t, standardUser.Username, standardEntry["display_name"], "standard user should fall back to username when display name is empty")
|
||||
|
||||
// Test standard user access (should fail)
|
||||
router2 := gin.Default()
|
||||
router2.GET("/users", func(c *gin.Context) {
|
||||
|
||||
@@ -10,6 +10,7 @@ func RegisterRoutes(r *gin.RouterGroup) {
|
||||
{
|
||||
// Protected routes - require authentication
|
||||
userGroup.GET("/profile", GetUserProfile)
|
||||
userGroup.PUT("/display-name", UpdateDisplayName)
|
||||
userGroup.PUT("/email", UpdateEmail)
|
||||
userGroup.PUT("/password", UpdatePassword)
|
||||
userGroup.PUT("/language", UpdateLanguage)
|
||||
|
||||
Reference in New Issue
Block a user