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>
This commit is contained in:
532
consent-service/internal/services/grade_service_test.go
Normal file
532
consent-service/internal/services/grade_service_test.go
Normal file
@@ -0,0 +1,532 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user