package training import ( "testing" "github.com/google/uuid" ) // ============================================================================= // buildContentPrompt Tests // ============================================================================= func TestBuildContentPrompt_ContainsModuleCode(t *testing.T) { module := TrainingModule{ ModuleCode: "CP-TRAIN-001", Title: "DSGVO Grundlagen", Description: "Basis-Schulung", RegulationArea: RegulationDSGVO, DurationMinutes: 30, } prompt := buildContentPrompt(module, "de") if !containsSubstring(prompt, "CP-TRAIN-001") { t.Error("Prompt should contain module code") } } func TestBuildContentPrompt_ContainsTitle(t *testing.T) { module := TrainingModule{ ModuleCode: "CP-001", Title: "DSGVO Grundlagen", RegulationArea: RegulationDSGVO, DurationMinutes: 30, } prompt := buildContentPrompt(module, "de") if !containsSubstring(prompt, "DSGVO Grundlagen") { t.Error("Prompt should contain module title") } } func TestBuildContentPrompt_ContainsRegulationLabel(t *testing.T) { tests := []struct { name string area RegulationArea expected string }{ {"DSGVO", RegulationDSGVO, "Datenschutz-Grundverordnung"}, {"NIS2", RegulationNIS2, "NIS-2-Richtlinie"}, {"ISO27001", RegulationISO27001, "ISO 27001"}, {"AIAct", RegulationAIAct, "AI Act"}, {"GeschGehG", RegulationGeschGehG, "Geschaeftsgeheimnisgesetz"}, {"HinSchG", RegulationHinSchG, "Hinweisgeberschutzgesetz"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { module := TrainingModule{ ModuleCode: "CP-001", Title: "Test Module", RegulationArea: tt.area, DurationMinutes: 30, } prompt := buildContentPrompt(module, "de") if !containsSubstring(prompt, tt.expected) { t.Errorf("Prompt should contain regulation label '%s' for area '%s'", tt.expected, tt.area) } }) } } func TestBuildContentPrompt_ContainsDuration(t *testing.T) { module := TrainingModule{ ModuleCode: "CP-001", Title: "Test", RegulationArea: RegulationDSGVO, DurationMinutes: 45, } prompt := buildContentPrompt(module, "de") if !containsSubstring(prompt, "45 Minuten") { t.Error("Prompt should contain duration in minutes") } } func TestBuildContentPrompt_UnknownRegulationArea(t *testing.T) { module := TrainingModule{ ModuleCode: "CP-001", Title: "Test", RegulationArea: RegulationArea("custom_regulation"), DurationMinutes: 30, } prompt := buildContentPrompt(module, "de") if !containsSubstring(prompt, "custom_regulation") { t.Error("Unknown regulation area should fall back to raw string") } } // ============================================================================= // buildQuizPrompt Tests // ============================================================================= func TestBuildQuizPrompt_ContainsQuestionCount(t *testing.T) { module := TrainingModule{ ModuleCode: "CP-001", Title: "Test Module", RegulationArea: RegulationDSGVO, } prompt := buildQuizPrompt(module, "", 10) if !containsSubstring(prompt, "10") { t.Error("Quiz prompt should contain question count") } } func TestBuildQuizPrompt_ContainsContentContext(t *testing.T) { module := TrainingModule{ ModuleCode: "CP-001", Title: "Test", RegulationArea: RegulationDSGVO, } prompt := buildQuizPrompt(module, "This is the module content about DSGVO.", 5) if !containsSubstring(prompt, "This is the module content about DSGVO.") { t.Error("Quiz prompt should include content context") } } func TestBuildQuizPrompt_TruncatesLongContent(t *testing.T) { module := TrainingModule{ ModuleCode: "CP-001", Title: "Test", RegulationArea: RegulationDSGVO, } // Create content longer than 3000 chars longContent := "" for i := 0; i < 400; i++ { longContent += "ABCDEFGHIJ" // 10 chars * 400 = 4000 chars } prompt := buildQuizPrompt(module, longContent, 5) if containsSubstring(prompt, longContent) { t.Error("Quiz prompt should truncate content longer than 3000 chars") } if !containsSubstring(prompt, "...") { t.Error("Truncated content should end with '...'") } } func TestBuildQuizPrompt_EmptyContent(t *testing.T) { module := TrainingModule{ ModuleCode: "CP-001", Title: "Test", RegulationArea: RegulationDSGVO, } prompt := buildQuizPrompt(module, "", 5) if containsSubstring(prompt, "Schulungsinhalt als Kontext") { t.Error("Empty content should not add context section") } } // ============================================================================= // parseQuizResponse Tests // ============================================================================= func TestParseQuizResponse_ValidJSON(t *testing.T) { moduleID := uuid.New() response := `[ { "question": "Was ist die DSGVO?", "options": ["EU-Verordnung", "Bundesgesetz", "Landesgesetz", "Internationale Konvention"], "correct_index": 0, "explanation": "Die DSGVO ist eine EU-Verordnung.", "difficulty": "easy" } ]` questions, err := parseQuizResponse(response, moduleID) if err != nil { t.Fatalf("Expected no error, got %v", err) } if len(questions) != 1 { t.Fatalf("Expected 1 question, got %d", len(questions)) } if questions[0].Question != "Was ist die DSGVO?" { t.Errorf("Expected question text, got '%s'", questions[0].Question) } if questions[0].CorrectIndex != 0 { t.Errorf("Expected correct_index 0, got %d", questions[0].CorrectIndex) } if questions[0].Difficulty != DifficultyEasy { t.Errorf("Expected difficulty 'easy', got '%s'", questions[0].Difficulty) } if questions[0].ModuleID != moduleID { t.Error("Module ID should be set on parsed question") } if !questions[0].IsActive { t.Error("Parsed questions should be active by default") } } func TestParseQuizResponse_InvalidJSON(t *testing.T) { moduleID := uuid.New() _, err := parseQuizResponse("not valid json at all", moduleID) if err == nil { t.Error("Expected error for invalid JSON") } } func TestParseQuizResponse_JSONWithSurroundingText(t *testing.T) { moduleID := uuid.New() response := `Here are the questions: [ { "question": "Test?", "options": ["A", "B", "C", "D"], "correct_index": 1, "explanation": "B is correct.", "difficulty": "medium" } ] I hope these are helpful!` questions, err := parseQuizResponse(response, moduleID) if err != nil { t.Fatalf("Expected no error, got %v", err) } if len(questions) != 1 { t.Fatalf("Expected 1 question, got %d", len(questions)) } } func TestParseQuizResponse_SkipsMalformedOptions(t *testing.T) { moduleID := uuid.New() response := `[ { "question": "Good question?", "options": ["A", "B", "C", "D"], "correct_index": 0, "explanation": "A is correct.", "difficulty": "easy" }, { "question": "Bad question?", "options": ["A", "B"], "correct_index": 0, "explanation": "Only 2 options.", "difficulty": "easy" } ]` questions, err := parseQuizResponse(response, moduleID) if err != nil { t.Fatalf("Expected no error, got %v", err) } if len(questions) != 1 { t.Errorf("Expected 1 valid question (malformed should be skipped), got %d", len(questions)) } } func TestParseQuizResponse_SkipsInvalidCorrectIndex(t *testing.T) { moduleID := uuid.New() response := `[ { "question": "Bad index?", "options": ["A", "B", "C", "D"], "correct_index": 5, "explanation": "Index out of range.", "difficulty": "medium" } ]` questions, err := parseQuizResponse(response, moduleID) if err != nil { t.Fatalf("Expected no error, got %v", err) } if len(questions) != 0 { t.Errorf("Expected 0 questions (invalid index should be skipped), got %d", len(questions)) } } func TestParseQuizResponse_NegativeCorrectIndex(t *testing.T) { moduleID := uuid.New() response := `[ { "question": "Negative index?", "options": ["A", "B", "C", "D"], "correct_index": -1, "explanation": "Negative index.", "difficulty": "easy" } ]` questions, err := parseQuizResponse(response, moduleID) if err != nil { t.Fatalf("Expected no error, got %v", err) } if len(questions) != 0 { t.Errorf("Expected 0 questions (negative index should be skipped), got %d", len(questions)) } } func TestParseQuizResponse_DefaultsDifficultyToMedium(t *testing.T) { moduleID := uuid.New() response := `[ { "question": "Test?", "options": ["A", "B", "C", "D"], "correct_index": 0, "explanation": "A is correct.", "difficulty": "unknown_difficulty" } ]` questions, err := parseQuizResponse(response, moduleID) if err != nil { t.Fatalf("Expected no error, got %v", err) } if len(questions) != 1 { t.Fatalf("Expected 1 question, got %d", len(questions)) } if questions[0].Difficulty != DifficultyMedium { t.Errorf("Expected difficulty to default to 'medium', got '%s'", questions[0].Difficulty) } } func TestParseQuizResponse_MultipleQuestions(t *testing.T) { moduleID := uuid.New() response := `[ {"question":"Q1?","options":["A","B","C","D"],"correct_index":0,"explanation":"","difficulty":"easy"}, {"question":"Q2?","options":["A","B","C","D"],"correct_index":1,"explanation":"","difficulty":"medium"}, {"question":"Q3?","options":["A","B","C","D"],"correct_index":2,"explanation":"","difficulty":"hard"} ]` questions, err := parseQuizResponse(response, moduleID) if err != nil { t.Fatalf("Expected no error, got %v", err) } if len(questions) != 3 { t.Errorf("Expected 3 questions, got %d", len(questions)) } } func TestParseQuizResponse_EmptyArray(t *testing.T) { moduleID := uuid.New() questions, err := parseQuizResponse("[]", moduleID) if err != nil { t.Fatalf("Expected no error, got %v", err) } if len(questions) != 0 { t.Errorf("Expected 0 questions, got %d", len(questions)) } } // ============================================================================= // truncateText Tests // ============================================================================= func TestTruncateText_ShortText(t *testing.T) { result := truncateText("hello", 100) if result != "hello" { t.Errorf("Short text should not be truncated, got '%s'", result) } } func TestTruncateText_ExactLength(t *testing.T) { result := truncateText("12345", 5) if result != "12345" { t.Errorf("Text at exact max length should not be truncated, got '%s'", result) } } func TestTruncateText_LongText(t *testing.T) { result := truncateText("1234567890", 5) if result != "12345..." { t.Errorf("Expected '12345...', got '%s'", result) } } func TestTruncateText_EmptyString(t *testing.T) { result := truncateText("", 10) if result != "" { t.Errorf("Empty string should remain empty, got '%s'", result) } } // ============================================================================= // System Prompt Tests // ============================================================================= func TestGetContentSystemPrompt_German(t *testing.T) { prompt := getContentSystemPrompt("de") if !containsSubstring(prompt, "Compliance-Schulungsinhalte") { t.Error("German system prompt should mention Compliance-Schulungsinhalte") } if !containsSubstring(prompt, "Markdown") { t.Error("System prompt should mention Markdown format") } } func TestGetContentSystemPrompt_English(t *testing.T) { prompt := getContentSystemPrompt("en") if !containsSubstring(prompt, "compliance training content") { t.Error("English system prompt should mention compliance training content") } } func TestGetQuizSystemPrompt_ContainsJSONFormat(t *testing.T) { prompt := getQuizSystemPrompt() if !containsSubstring(prompt, "JSON") { t.Error("Quiz system prompt should mention JSON format") } if !containsSubstring(prompt, "correct_index") { t.Error("Quiz system prompt should show correct_index field") } } // ============================================================================= // buildBlockContentPrompt Tests // ============================================================================= func TestBuildBlockContentPrompt_ContainsModuleInfo(t *testing.T) { module := TrainingModule{ ModuleCode: "BLK-AUTH-001", Title: "Authentication Controls", DurationMinutes: 45, } controls := []CanonicalControlSummary{ { ControlID: "AUTH-001", Title: "Multi-Factor Authentication", Objective: "Ensure MFA is enabled", Requirements: []string{"Enable MFA for all users"}, }, } prompt := buildBlockContentPrompt(module, controls, "de") if !containsSubstring(prompt, "BLK-AUTH-001") { t.Error("Block prompt should contain module code") } if !containsSubstring(prompt, "Authentication Controls") { t.Error("Block prompt should contain module title") } if !containsSubstring(prompt, "45 Minuten") { t.Error("Block prompt should contain duration") } } func TestBuildBlockContentPrompt_ContainsControlDetails(t *testing.T) { module := TrainingModule{ ModuleCode: "BLK-001", Title: "Test", DurationMinutes: 30, } controls := []CanonicalControlSummary{ { ControlID: "CTRL-001", Title: "Test Control", Objective: "Test objective", Requirements: []string{"Req 1", "Req 2"}, }, } prompt := buildBlockContentPrompt(module, controls, "de") if !containsSubstring(prompt, "CTRL-001") { t.Error("Prompt should contain control ID") } if !containsSubstring(prompt, "Test Control") { t.Error("Prompt should contain control title") } if !containsSubstring(prompt, "Test objective") { t.Error("Prompt should contain control objective") } if !containsSubstring(prompt, "Req 1") { t.Error("Prompt should contain control requirements") } } func TestBuildBlockContentPrompt_EnglishVersion(t *testing.T) { module := TrainingModule{ ModuleCode: "BLK-001", Title: "Test", DurationMinutes: 30, } controls := []CanonicalControlSummary{} prompt := buildBlockContentPrompt(module, controls, "en") if !containsSubstring(prompt, "Create training material") { t.Error("English prompt should use English text") } } func TestBuildBlockContentPrompt_MultipleControls(t *testing.T) { module := TrainingModule{ ModuleCode: "BLK-001", Title: "Test", DurationMinutes: 30, } controls := []CanonicalControlSummary{ {ControlID: "CTRL-001", Title: "First Control", Objective: "Obj 1"}, {ControlID: "CTRL-002", Title: "Second Control", Objective: "Obj 2"}, {ControlID: "CTRL-003", Title: "Third Control", Objective: "Obj 3"}, } prompt := buildBlockContentPrompt(module, controls, "de") if !containsSubstring(prompt, "3 Sicherheits-Controls") { t.Error("Prompt should mention the count of controls") } if !containsSubstring(prompt, "Control 1") { t.Error("Prompt should number controls") } if !containsSubstring(prompt, "Control 3") { t.Error("Prompt should include all controls") } } // ============================================================================= // Helpers // ============================================================================= func containsSubstring(s, substr string) bool { return len(s) >= len(substr) && searchSubstring(s, substr) } func searchSubstring(s, substr string) bool { if len(substr) == 0 { return true } for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false }