This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/consent-service/internal/services/grade_service_test.go
Benjamin Admin bfdaf63ba9 fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

533 lines
12 KiB
Go

package services
import (
"testing"
"time"
"github.com/breakpilot/consent-service/internal/models"
"github.com/google/uuid"
)
// TestValidateGrade tests grade validation
func TestValidateGrade(t *testing.T) {
schoolYearID := uuid.New()
gradeScaleID := uuid.New()
tests := []struct {
name string
grade models.Grade
expectValid bool
}{
{
name: "valid grade 1",
grade: models.Grade{
StudentID: uuid.New(),
SubjectID: uuid.New(),
TeacherID: uuid.New(),
SchoolYearID: schoolYearID,
GradeScaleID: gradeScaleID,
Value: 1.0,
Type: models.GradeTypeExam,
Weight: 1.0,
Date: time.Now(),
Semester: 1,
},
expectValid: true,
},
{
name: "valid grade 6",
grade: models.Grade{
StudentID: uuid.New(),
SubjectID: uuid.New(),
TeacherID: uuid.New(),
SchoolYearID: schoolYearID,
GradeScaleID: gradeScaleID,
Value: 6.0,
Type: models.GradeTypeOral,
Weight: 0.5,
Date: time.Now(),
Semester: 2,
},
expectValid: true,
},
{
name: "valid grade with plus (1.3)",
grade: models.Grade{
StudentID: uuid.New(),
SubjectID: uuid.New(),
TeacherID: uuid.New(),
SchoolYearID: schoolYearID,
GradeScaleID: gradeScaleID,
Value: 1.3,
Type: models.GradeTypeTest,
Weight: 0.25,
Date: time.Now(),
Semester: 1,
},
expectValid: true,
},
{
name: "invalid grade 0",
grade: models.Grade{
StudentID: uuid.New(),
SubjectID: uuid.New(),
TeacherID: uuid.New(),
SchoolYearID: schoolYearID,
GradeScaleID: gradeScaleID,
Value: 0.0,
Type: models.GradeTypeExam,
Weight: 1.0,
Date: time.Now(),
Semester: 1,
},
expectValid: false,
},
{
name: "invalid grade 7",
grade: models.Grade{
StudentID: uuid.New(),
SubjectID: uuid.New(),
TeacherID: uuid.New(),
SchoolYearID: schoolYearID,
GradeScaleID: gradeScaleID,
Value: 7.0,
Type: models.GradeTypeExam,
Weight: 1.0,
Date: time.Now(),
Semester: 1,
},
expectValid: false,
},
{
name: "missing student ID",
grade: models.Grade{
StudentID: uuid.Nil,
SubjectID: uuid.New(),
TeacherID: uuid.New(),
SchoolYearID: schoolYearID,
GradeScaleID: gradeScaleID,
Value: 2.0,
Type: models.GradeTypeExam,
Weight: 1.0,
Date: time.Now(),
Semester: 1,
},
expectValid: false,
},
{
name: "invalid weight negative",
grade: models.Grade{
StudentID: uuid.New(),
SubjectID: uuid.New(),
TeacherID: uuid.New(),
SchoolYearID: schoolYearID,
GradeScaleID: gradeScaleID,
Value: 2.0,
Type: models.GradeTypeExam,
Weight: -0.5,
Date: time.Now(),
Semester: 1,
},
expectValid: false,
},
{
name: "invalid semester 0",
grade: models.Grade{
StudentID: uuid.New(),
SubjectID: uuid.New(),
TeacherID: uuid.New(),
SchoolYearID: schoolYearID,
GradeScaleID: gradeScaleID,
Value: 2.0,
Type: models.GradeTypeExam,
Weight: 1.0,
Date: time.Now(),
Semester: 0,
},
expectValid: false,
},
{
name: "invalid semester 3",
grade: models.Grade{
StudentID: uuid.New(),
SubjectID: uuid.New(),
TeacherID: uuid.New(),
SchoolYearID: schoolYearID,
GradeScaleID: gradeScaleID,
Value: 2.0,
Type: models.GradeTypeExam,
Weight: 1.0,
Date: time.Now(),
Semester: 3,
},
expectValid: false,
},
{
name: "invalid type",
grade: models.Grade{
StudentID: uuid.New(),
SubjectID: uuid.New(),
TeacherID: uuid.New(),
SchoolYearID: schoolYearID,
GradeScaleID: gradeScaleID,
Value: 2.0,
Type: "invalid_type",
Weight: 1.0,
Date: time.Now(),
Semester: 1,
},
expectValid: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
isValid := validateGrade(tt.grade)
if isValid != tt.expectValid {
t.Errorf("expected valid=%v, got valid=%v", tt.expectValid, isValid)
}
})
}
}
// validateGrade validates a grade
func validateGrade(grade models.Grade) bool {
if grade.StudentID == uuid.Nil {
return false
}
if grade.SubjectID == uuid.Nil {
return false
}
if grade.TeacherID == uuid.Nil {
return false
}
// German grading scale: 1 (best) to 6 (worst)
if grade.Value < 1.0 || grade.Value > 6.0 {
return false
}
if grade.Weight < 0 {
return false
}
if grade.Semester < 1 || grade.Semester > 2 {
return false
}
// Validate type
validTypes := map[string]bool{
models.GradeTypeExam: true,
models.GradeTypeTest: true,
models.GradeTypeOral: true,
models.GradeTypeHomework: true,
models.GradeTypeProject: true,
models.GradeTypeParticipation: true,
models.GradeTypeSemester: true,
models.GradeTypeFinal: true,
}
if !validTypes[grade.Type] {
return false
}
return true
}
// TestCalculateWeightedAverage tests weighted average calculation
func TestCalculateWeightedAverage(t *testing.T) {
tests := []struct {
name string
grades []models.Grade
expectedAverage float64
}{
{
name: "simple average equal weights",
grades: []models.Grade{
{Value: 1.0, Weight: 1.0},
{Value: 2.0, Weight: 1.0},
{Value: 3.0, Weight: 1.0},
},
expectedAverage: 2.0,
},
{
name: "weighted average different weights",
grades: []models.Grade{
{Value: 1.0, Weight: 2.0}, // Exam counts double
{Value: 3.0, Weight: 1.0},
},
// (1*2 + 3*1) / (2+1) = 5/3 = 1.67
expectedAverage: 1.67,
},
{
name: "single grade",
grades: []models.Grade{
{Value: 2.5, Weight: 1.0},
},
expectedAverage: 2.5,
},
{
name: "empty grades",
grades: []models.Grade{},
expectedAverage: 0.0,
},
{
name: "all same grades",
grades: []models.Grade{
{Value: 2.0, Weight: 1.0},
{Value: 2.0, Weight: 1.0},
{Value: 2.0, Weight: 1.0},
},
expectedAverage: 2.0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
avg := calculateWeightedAverage(tt.grades)
// Allow small floating point differences
if avg < tt.expectedAverage-0.01 || avg > tt.expectedAverage+0.01 {
t.Errorf("expected average=%.2f, got average=%.2f", tt.expectedAverage, avg)
}
})
}
}
// calculateWeightedAverage calculates weighted average of grades
func calculateWeightedAverage(grades []models.Grade) float64 {
if len(grades) == 0 {
return 0.0
}
var weightedSum float64
var totalWeight float64
for _, g := range grades {
weightedSum += g.Value * g.Weight
totalWeight += g.Weight
}
if totalWeight == 0 {
return 0.0
}
avg := weightedSum / totalWeight
// Round to 2 decimal places
return float64(int(avg*100)) / 100
}
// TestGradeDistribution tests grade distribution calculation
func TestGradeDistribution(t *testing.T) {
tests := []struct {
name string
grades []models.Grade
expectedDist map[int]int
}{
{
name: "varied distribution",
grades: []models.Grade{
{Value: 1.0}, {Value: 1.3},
{Value: 2.0}, {Value: 2.0}, {Value: 2.7},
{Value: 3.0}, {Value: 3.0}, {Value: 3.0},
{Value: 4.0}, {Value: 4.3},
{Value: 5.0},
},
expectedDist: map[int]int{
1: 2, // 1.0, 1.3 (rounded: 1, 1)
2: 2, // 2.0, 2.0 (rounded: 2, 2)
3: 4, // 2.7, 3.0, 3.0, 3.0 (rounded: 3, 3, 3, 3)
4: 2, // 4.0, 4.3 (rounded: 4, 4)
5: 1, // 5.0 (rounded: 5)
6: 0,
},
},
{
name: "empty grades",
grades: []models.Grade{},
expectedDist: map[int]int{1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0},
},
{
name: "all same grade",
grades: []models.Grade{
{Value: 2.0},
{Value: 2.0},
{Value: 2.0},
},
expectedDist: map[int]int{1: 0, 2: 3, 3: 0, 4: 0, 5: 0, 6: 0},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dist := calculateGradeDistribution(tt.grades)
for grade, count := range tt.expectedDist {
if dist[grade] != count {
t.Errorf("grade %d: expected count=%d, got count=%d", grade, count, dist[grade])
}
}
})
}
}
// calculateGradeDistribution calculates how many grades fall into each category
func calculateGradeDistribution(grades []models.Grade) map[int]int {
dist := map[int]int{1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0}
for _, g := range grades {
// Round to nearest integer for distribution
roundedGrade := int(g.Value + 0.5)
if roundedGrade < 1 {
roundedGrade = 1
}
if roundedGrade > 6 {
roundedGrade = 6
}
dist[roundedGrade]++
}
return dist
}
// TestGradePointConversion tests conversion between grades and points (Oberstufe)
func TestGradePointConversion(t *testing.T) {
tests := []struct {
name string
grade float64
expectedPoints int
}{
{"grade 1.0 = 15 points", 1.0, 15},
{"grade 1.3 = 14 points", 1.3, 14},
{"grade 1.7 = 13 points", 1.7, 13},
{"grade 2.0 = 12 points", 2.0, 12},
{"grade 2.3 = 11 points", 2.3, 11},
{"grade 2.7 = 10 points", 2.7, 10},
{"grade 3.0 = 9 points", 3.0, 9},
{"grade 3.3 = 8 points", 3.3, 8},
{"grade 3.7 = 7 points", 3.7, 7},
{"grade 4.0 = 6 points", 4.0, 6},
{"grade 4.3 = 5 points", 4.3, 5},
{"grade 4.7 = 4 points", 4.7, 4},
{"grade 5.0 = 3 points", 5.0, 3},
{"grade 5.3 = 2 points", 5.3, 2},
{"grade 5.7 = 1 point", 5.7, 1},
{"grade 6.0 = 0 points", 6.0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
points := gradeToPoints(tt.grade)
if points != tt.expectedPoints {
t.Errorf("expected points=%d, got points=%d", tt.expectedPoints, points)
}
})
}
}
// gradeToPoints converts German grade (1-6) to Oberstufe points (0-15)
func gradeToPoints(grade float64) int {
// Mapping based on German school system
if grade <= 1.0 {
return 15
} else if grade <= 1.3 {
return 14
} else if grade <= 1.7 {
return 13
} else if grade <= 2.0 {
return 12
} else if grade <= 2.3 {
return 11
} else if grade <= 2.7 {
return 10
} else if grade <= 3.0 {
return 9
} else if grade <= 3.3 {
return 8
} else if grade <= 3.7 {
return 7
} else if grade <= 4.0 {
return 6
} else if grade <= 4.3 {
return 5
} else if grade <= 4.7 {
return 4
} else if grade <= 5.0 {
return 3
} else if grade <= 5.3 {
return 2
} else if grade <= 5.7 {
return 1
}
return 0
}
// TestFindBestAndWorstGrade tests finding best and worst grades
func TestFindBestAndWorstGrade(t *testing.T) {
tests := []struct {
name string
grades []models.Grade
expectedBest float64
expectedWorst float64
}{
{
name: "varied grades",
grades: []models.Grade{
{Value: 2.0},
{Value: 1.0},
{Value: 3.0},
{Value: 5.0},
{Value: 2.0},
},
expectedBest: 1.0,
expectedWorst: 5.0,
},
{
name: "all same",
grades: []models.Grade{
{Value: 2.0},
{Value: 2.0},
},
expectedBest: 2.0,
expectedWorst: 2.0,
},
{
name: "single grade",
grades: []models.Grade{
{Value: 3.0},
},
expectedBest: 3.0,
expectedWorst: 3.0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
best, worst := findBestAndWorstGrade(tt.grades)
if best != tt.expectedBest {
t.Errorf("expected best=%.1f, got best=%.1f", tt.expectedBest, best)
}
if worst != tt.expectedWorst {
t.Errorf("expected worst=%.1f, got worst=%.1f", tt.expectedWorst, worst)
}
})
}
}
// findBestAndWorstGrade finds the best (lowest) and worst (highest) grade
func findBestAndWorstGrade(grades []models.Grade) (best, worst float64) {
if len(grades) == 0 {
return 0, 0
}
best = grades[0].Value
worst = grades[0].Value
for _, g := range grades[1:] {
if g.Value < best {
best = g.Value
}
if g.Value > worst {
worst = g.Value
}
}
return best, worst
}