Update maintenance Component for Skills
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
package maintenance
|
||||
|
||||
import (
|
||||
"bamort/logger"
|
||||
"bamort/models"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MigrateSkillCategoriesToRelations migrates existing Category/Difficulty fields to the relational model
|
||||
func MigrateSkillCategoriesToRelations(db *gorm.DB) error {
|
||||
logger.Info("Starting migration of skill categories to relational model...")
|
||||
|
||||
// Get all skills with existing category/difficulty data
|
||||
var skills []models.Skill
|
||||
if err := db.Where("category IS NOT NULL AND category != ''").Find(&skills).Error; err != nil {
|
||||
return fmt.Errorf("failed to fetch skills: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Found %d skills to migrate", len(skills))
|
||||
|
||||
migrated := 0
|
||||
skipped := 0
|
||||
errors := 0
|
||||
|
||||
for _, skill := range skills {
|
||||
if err := migrateSkillCategoryDifficulty(db, &skill); err != nil {
|
||||
logger.Error("Failed to migrate skill %d (%s): %s", skill.ID, skill.Name, err.Error())
|
||||
errors++
|
||||
continue
|
||||
}
|
||||
migrated++
|
||||
}
|
||||
|
||||
logger.Info("Migration completed: %d migrated, %d skipped, %d errors", migrated, skipped, errors)
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateSkillCategoryDifficulty(db *gorm.DB, skill *models.Skill) error {
|
||||
// Check if already migrated
|
||||
var count int64
|
||||
if err := db.Model(&models.SkillCategoryDifficulty{}).Where("skill_id = ?", skill.ID).Count(&count).Error; err != nil {
|
||||
return fmt.Errorf("failed to check existing migration: %w", err)
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
logger.Debug("Skill %d (%s) already has category difficulties, skipping", skill.ID, skill.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find or create skill category
|
||||
var skillCategory models.SkillCategory
|
||||
categoryName := strings.TrimSpace(skill.Category)
|
||||
if categoryName == "" {
|
||||
categoryName = "Alltag" // Default category
|
||||
}
|
||||
|
||||
err := db.Where("name = ? AND game_system = ?", categoryName, skill.GameSystem).First(&skillCategory).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// Create new category if it doesn't exist
|
||||
// First, find a valid source (we'll use the skill's source or a default)
|
||||
var source models.Source
|
||||
if skill.SourceID > 0 {
|
||||
db.First(&source, skill.SourceID)
|
||||
} else {
|
||||
// Use default source (KOD)
|
||||
db.Where("code = ?", "KOD").First(&source)
|
||||
}
|
||||
|
||||
if source.ID == 0 {
|
||||
return fmt.Errorf("no valid source found for category creation")
|
||||
}
|
||||
|
||||
skillCategory = models.SkillCategory{
|
||||
Name: categoryName,
|
||||
GameSystem: skill.GameSystem,
|
||||
SourceID: source.ID,
|
||||
Quelle: source.Code,
|
||||
}
|
||||
if err := db.Create(&skillCategory).Error; err != nil {
|
||||
return fmt.Errorf("failed to create skill category: %w", err)
|
||||
}
|
||||
logger.Debug("Created new skill category: %s", categoryName)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to find skill category: %w", err)
|
||||
}
|
||||
|
||||
// Find or create skill difficulty
|
||||
var skillDifficulty models.SkillDifficulty
|
||||
difficultyName := strings.TrimSpace(skill.Difficulty)
|
||||
if difficultyName == "" {
|
||||
difficultyName = "normal" // Default difficulty
|
||||
}
|
||||
|
||||
err = db.Where("name = ? AND game_system = ?", difficultyName, skill.GameSystem).First(&skillDifficulty).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
skillDifficulty = models.SkillDifficulty{
|
||||
Name: difficultyName,
|
||||
GameSystem: skill.GameSystem,
|
||||
}
|
||||
if err := db.Create(&skillDifficulty).Error; err != nil {
|
||||
return fmt.Errorf("failed to create skill difficulty: %w", err)
|
||||
}
|
||||
logger.Debug("Created new skill difficulty: %s", difficultyName)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to find skill difficulty: %w", err)
|
||||
}
|
||||
|
||||
// Create SkillCategoryDifficulty relationship
|
||||
scd := models.SkillCategoryDifficulty{
|
||||
SkillID: skill.ID,
|
||||
SkillCategoryID: skillCategory.ID,
|
||||
SkillDifficultyID: skillDifficulty.ID,
|
||||
LearnCost: getDefaultLearnCost(difficultyName),
|
||||
SDifficulty: difficultyName,
|
||||
SCategory: categoryName,
|
||||
}
|
||||
|
||||
if err := db.Create(&scd).Error; err != nil {
|
||||
return fmt.Errorf("failed to create skill category difficulty: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("Migrated skill %d (%s): category=%s, difficulty=%s", skill.ID, skill.Name, categoryName, difficultyName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDefaultLearnCost returns default LE cost based on difficulty
|
||||
func getDefaultLearnCost(difficulty string) int {
|
||||
switch strings.ToLower(difficulty) {
|
||||
case "leicht", "easy":
|
||||
return 5
|
||||
case "normal", "standard":
|
||||
return 10
|
||||
case "schwer", "hard":
|
||||
return 20
|
||||
case "sehr schwer", "very hard":
|
||||
return 30
|
||||
default:
|
||||
return 10
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package maintenance
|
||||
|
||||
import (
|
||||
"bamort/database"
|
||||
"bamort/models"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func setupTestEnvironment(t *testing.T) {
|
||||
original := os.Getenv("ENVIRONMENT")
|
||||
os.Setenv("ENVIRONMENT", "test")
|
||||
t.Cleanup(func() {
|
||||
if original != "" {
|
||||
os.Setenv("ENVIRONMENT", original)
|
||||
} else {
|
||||
os.Unsetenv("ENVIRONMENT")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrateSkillCategoriesToRelations(t *testing.T) {
|
||||
setupTestEnvironment(t)
|
||||
|
||||
// Reset and setup fresh test database to avoid interference from other tests
|
||||
database.ResetTestDB()
|
||||
database.SetupTestDB()
|
||||
testDB := database.DB
|
||||
|
||||
// Ensure database is valid
|
||||
if testDB == nil {
|
||||
t.Fatal("Database connection is nil")
|
||||
}
|
||||
|
||||
// Create a test source - use unique code to avoid conflicts
|
||||
source := models.Source{
|
||||
Code: "TSTMIG1",
|
||||
Name: "Test Migration Source",
|
||||
GameSystem: "midgard",
|
||||
IsActive: true,
|
||||
}
|
||||
if err := testDB.Create(&source).Error; err != nil {
|
||||
t.Fatalf("Failed to create test source: %v", err)
|
||||
}
|
||||
|
||||
// Create test skills with old-style category/difficulty - use unique names to avoid conflicts
|
||||
testSkills := []models.Skill{
|
||||
{
|
||||
Name: "TestMigSkill_Schwimmen",
|
||||
Category: "Körper",
|
||||
Difficulty: "leicht",
|
||||
GameSystem: "midgard",
|
||||
Initialwert: 12,
|
||||
Improvable: true,
|
||||
Bonuseigenschaft: "Gw",
|
||||
SourceID: source.ID,
|
||||
},
|
||||
{
|
||||
Name: "TestMigSkill_Klettern",
|
||||
Category: "Körper",
|
||||
Difficulty: "normal",
|
||||
GameSystem: "midgard",
|
||||
Initialwert: 10,
|
||||
Improvable: true,
|
||||
Bonuseigenschaft: "Gw",
|
||||
SourceID: source.ID,
|
||||
},
|
||||
{
|
||||
Name: "TestMigSkill_LesenSchreiben",
|
||||
Category: "Wissen",
|
||||
Difficulty: "schwer",
|
||||
GameSystem: "midgard",
|
||||
Initialwert: 0,
|
||||
Improvable: true,
|
||||
Bonuseigenschaft: "In",
|
||||
SourceID: source.ID,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range testSkills {
|
||||
if err := testDB.Create(&testSkills[i]).Error; err != nil {
|
||||
t.Fatalf("Failed to create test skill: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Run migration
|
||||
if err := MigrateSkillCategoriesToRelations(testDB); err != nil {
|
||||
t.Fatalf("Migration failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify migration results
|
||||
for _, skill := range testSkills {
|
||||
var scds []models.SkillCategoryDifficulty
|
||||
err := testDB.Preload("SkillCategory").Preload("SkillDifficulty").
|
||||
Where("skill_id = ?", skill.ID).Find(&scds).Error
|
||||
if err != nil {
|
||||
t.Errorf("Failed to find migrated data for skill %s: %v", skill.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(scds) == 0 {
|
||||
t.Errorf("No SkillCategoryDifficulty created for skill %s", skill.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
scd := scds[0]
|
||||
if scd.SkillCategory.Name != skill.Category {
|
||||
t.Errorf("Category mismatch for %s: expected %s, got %s",
|
||||
skill.Name, skill.Category, scd.SkillCategory.Name)
|
||||
}
|
||||
|
||||
if scd.SkillDifficulty.Name != skill.Difficulty {
|
||||
t.Errorf("Difficulty mismatch for %s: expected %s, got %s",
|
||||
skill.Name, skill.Difficulty, scd.SkillDifficulty.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Test idempotency - running migration again should not create duplicates
|
||||
if err := MigrateSkillCategoriesToRelations(testDB); err != nil {
|
||||
t.Fatalf("Second migration failed: %v", err)
|
||||
}
|
||||
|
||||
for _, skill := range testSkills {
|
||||
var count int64
|
||||
testDB.Model(&models.SkillCategoryDifficulty{}).Where("skill_id = ?", skill.ID).Count(&count)
|
||||
if count != 1 {
|
||||
t.Errorf("Idempotency check failed for %s: expected 1 record, got %d", skill.Name, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDefaultLearnCost(t *testing.T) {
|
||||
tests := []struct {
|
||||
difficulty string
|
||||
expected int
|
||||
}{
|
||||
{"leicht", 5},
|
||||
{"easy", 5},
|
||||
{"normal", 10},
|
||||
{"standard", 10},
|
||||
{"schwer", 20},
|
||||
{"hard", 20},
|
||||
{"sehr schwer", 30},
|
||||
{"very hard", 30},
|
||||
{"unknown", 10}, // default
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.difficulty, func(t *testing.T) {
|
||||
result := getDefaultLearnCost(tt.difficulty)
|
||||
if result != tt.expected {
|
||||
t.Errorf("getDefaultLearnCost(%s) = %d, expected %d",
|
||||
tt.difficulty, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateSkillCategoryDifficulty_NoCategory(t *testing.T) {
|
||||
setupTestEnvironment(t)
|
||||
|
||||
// Reset and setup fresh test database to avoid interference from other tests
|
||||
database.ResetTestDB()
|
||||
database.SetupTestDB()
|
||||
testDB := database.DB
|
||||
|
||||
// Ensure database is valid
|
||||
if testDB == nil {
|
||||
t.Fatal("Database connection is nil")
|
||||
}
|
||||
|
||||
// Use existing source or create one with a unique code
|
||||
var source models.Source
|
||||
err := testDB.Where("code = ?", "KOD").First(&source).Error
|
||||
if err != nil {
|
||||
// Create a test source if KOD doesn't exist
|
||||
source = models.Source{
|
||||
Code: "TSTMIG2",
|
||||
Name: "Test Migration Source 2",
|
||||
GameSystem: "midgard",
|
||||
IsActive: true,
|
||||
}
|
||||
if err := testDB.Create(&source).Error; err != nil {
|
||||
t.Fatalf("Failed to create test source: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create skill without category (should use default) - use unique name
|
||||
skill := models.Skill{
|
||||
Name: "TestMigSkill_NoCategory",
|
||||
Category: "", // Empty category
|
||||
Difficulty: "", // Empty difficulty
|
||||
GameSystem: "midgard",
|
||||
Initialwert: 10,
|
||||
SourceID: source.ID,
|
||||
}
|
||||
if err := testDB.Create(&skill).Error; err != nil {
|
||||
t.Fatalf("Failed to create test skill: %v", err)
|
||||
}
|
||||
|
||||
// Migrate
|
||||
if err := migrateSkillCategoryDifficulty(testDB, &skill); err != nil {
|
||||
t.Fatalf("Migration failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify defaults were used
|
||||
var scd models.SkillCategoryDifficulty
|
||||
err = testDB.Preload("SkillCategory").Preload("SkillDifficulty").
|
||||
Where("skill_id = ?", skill.ID).First(&scd).Error
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to find migrated data: %v", err)
|
||||
}
|
||||
|
||||
if scd.SkillCategory.Name != "Alltag" {
|
||||
t.Errorf("Expected default category 'Alltag', got %s", scd.SkillCategory.Name)
|
||||
}
|
||||
|
||||
if scd.SkillDifficulty.Name != "normal" {
|
||||
t.Errorf("Expected default difficulty 'normal', got %s", scd.SkillDifficulty.Name)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user