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:
451
school-service/internal/services/exam_service_test.go
Normal file
451
school-service/internal/services/exam_service_test.go
Normal file
@@ -0,0 +1,451 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExamService_ValidateExamInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
title string
|
||||
examType string
|
||||
durationMinutes int
|
||||
maxPoints float64
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid klassenarbeit",
|
||||
title: "Mathematik Klassenarbeit Nr. 1",
|
||||
examType: "klassenarbeit",
|
||||
durationMinutes: 45,
|
||||
maxPoints: 50,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid test",
|
||||
title: "Vokabeltest Englisch",
|
||||
examType: "test",
|
||||
durationMinutes: 20,
|
||||
maxPoints: 20,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid klausur",
|
||||
title: "Oberstufen-Klausur Deutsch",
|
||||
examType: "klausur",
|
||||
durationMinutes: 180,
|
||||
maxPoints: 100,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty title",
|
||||
title: "",
|
||||
examType: "klassenarbeit",
|
||||
durationMinutes: 45,
|
||||
maxPoints: 50,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid exam type",
|
||||
title: "Test",
|
||||
examType: "invalid_type",
|
||||
durationMinutes: 45,
|
||||
maxPoints: 50,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "negative duration",
|
||||
title: "Test",
|
||||
examType: "test",
|
||||
durationMinutes: -10,
|
||||
maxPoints: 50,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "zero max points",
|
||||
title: "Test",
|
||||
examType: "test",
|
||||
durationMinutes: 45,
|
||||
maxPoints: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateExamInput(tt.title, tt.examType, tt.durationMinutes, tt.maxPoints)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExamService_ValidateExamResult(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pointsAchieved float64
|
||||
maxPoints float64
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid result - full points",
|
||||
pointsAchieved: 50,
|
||||
maxPoints: 50,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid result - partial points",
|
||||
pointsAchieved: 35.5,
|
||||
maxPoints: 50,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid result - zero points",
|
||||
pointsAchieved: 0,
|
||||
maxPoints: 50,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid result - negative points",
|
||||
pointsAchieved: -5,
|
||||
maxPoints: 50,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid result - exceeds max",
|
||||
pointsAchieved: 55,
|
||||
maxPoints: 50,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateExamResult(tt.pointsAchieved, tt.maxPoints)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExamService_CalculateGrade(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pointsAchieved float64
|
||||
maxPoints float64
|
||||
expectedGrade float64
|
||||
}{
|
||||
{
|
||||
name: "100% - Grade 1",
|
||||
pointsAchieved: 50,
|
||||
maxPoints: 50,
|
||||
expectedGrade: 1.0,
|
||||
},
|
||||
{
|
||||
name: "92% - Grade 1",
|
||||
pointsAchieved: 46,
|
||||
maxPoints: 50,
|
||||
expectedGrade: 1.0,
|
||||
},
|
||||
{
|
||||
name: "85% - Grade 2",
|
||||
pointsAchieved: 42.5,
|
||||
maxPoints: 50,
|
||||
expectedGrade: 2.0,
|
||||
},
|
||||
{
|
||||
name: "70% - Grade 3",
|
||||
pointsAchieved: 35,
|
||||
maxPoints: 50,
|
||||
expectedGrade: 3.0,
|
||||
},
|
||||
{
|
||||
name: "55% - Grade 4",
|
||||
pointsAchieved: 27.5,
|
||||
maxPoints: 50,
|
||||
expectedGrade: 4.0,
|
||||
},
|
||||
{
|
||||
name: "40% - Grade 5",
|
||||
pointsAchieved: 20,
|
||||
maxPoints: 50,
|
||||
expectedGrade: 5.0,
|
||||
},
|
||||
{
|
||||
name: "20% - Grade 6",
|
||||
pointsAchieved: 10,
|
||||
maxPoints: 50,
|
||||
expectedGrade: 6.0,
|
||||
},
|
||||
{
|
||||
name: "0% - Grade 6",
|
||||
pointsAchieved: 0,
|
||||
maxPoints: 50,
|
||||
expectedGrade: 6.0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
grade := calculateGrade(tt.pointsAchieved, tt.maxPoints)
|
||||
assert.Equal(t, tt.expectedGrade, grade)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExamService_CalculatePercentage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pointsAchieved float64
|
||||
maxPoints float64
|
||||
expectedPercentage float64
|
||||
}{
|
||||
{
|
||||
name: "100%",
|
||||
pointsAchieved: 50,
|
||||
maxPoints: 50,
|
||||
expectedPercentage: 100.0,
|
||||
},
|
||||
{
|
||||
name: "50%",
|
||||
pointsAchieved: 25,
|
||||
maxPoints: 50,
|
||||
expectedPercentage: 50.0,
|
||||
},
|
||||
{
|
||||
name: "0%",
|
||||
pointsAchieved: 0,
|
||||
maxPoints: 50,
|
||||
expectedPercentage: 0.0,
|
||||
},
|
||||
{
|
||||
name: "33.33%",
|
||||
pointsAchieved: 10,
|
||||
maxPoints: 30,
|
||||
expectedPercentage: 33.33,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
percentage := calculatePercentage(tt.pointsAchieved, tt.maxPoints)
|
||||
assert.InDelta(t, tt.expectedPercentage, percentage, 0.01)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExamService_DetermineNeedsRewrite(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
grade float64
|
||||
needsRewrite bool
|
||||
}{
|
||||
{
|
||||
name: "Grade 1 - no rewrite",
|
||||
grade: 1.0,
|
||||
needsRewrite: false,
|
||||
},
|
||||
{
|
||||
name: "Grade 4 - no rewrite",
|
||||
grade: 4.0,
|
||||
needsRewrite: false,
|
||||
},
|
||||
{
|
||||
name: "Grade 5 - needs rewrite",
|
||||
grade: 5.0,
|
||||
needsRewrite: true,
|
||||
},
|
||||
{
|
||||
name: "Grade 6 - needs rewrite",
|
||||
grade: 6.0,
|
||||
needsRewrite: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := determineNeedsRewrite(tt.grade)
|
||||
assert.Equal(t, tt.needsRewrite, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Validation helper functions
|
||||
func validateExamInput(title, examType string, durationMinutes int, maxPoints float64) error {
|
||||
if title == "" {
|
||||
return assert.AnError
|
||||
}
|
||||
validTypes := map[string]bool{
|
||||
"klassenarbeit": true,
|
||||
"test": true,
|
||||
"klausur": true,
|
||||
}
|
||||
if !validTypes[examType] {
|
||||
return assert.AnError
|
||||
}
|
||||
if durationMinutes <= 0 {
|
||||
return assert.AnError
|
||||
}
|
||||
if maxPoints <= 0 {
|
||||
return assert.AnError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateExamResult(pointsAchieved, maxPoints float64) error {
|
||||
if pointsAchieved < 0 {
|
||||
return assert.AnError
|
||||
}
|
||||
if pointsAchieved > maxPoints {
|
||||
return assert.AnError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func calculateGrade(pointsAchieved, maxPoints float64) float64 {
|
||||
percentage := (pointsAchieved / maxPoints) * 100
|
||||
|
||||
switch {
|
||||
case percentage >= 92:
|
||||
return 1.0
|
||||
case percentage >= 81:
|
||||
return 2.0
|
||||
case percentage >= 67:
|
||||
return 3.0
|
||||
case percentage >= 50:
|
||||
return 4.0
|
||||
case percentage >= 30:
|
||||
return 5.0
|
||||
default:
|
||||
return 6.0
|
||||
}
|
||||
}
|
||||
|
||||
func calculatePercentage(pointsAchieved, maxPoints float64) float64 {
|
||||
if maxPoints == 0 {
|
||||
return 0
|
||||
}
|
||||
result := (pointsAchieved / maxPoints) * 100
|
||||
// Round to 2 decimal places
|
||||
return float64(int(result*100)) / 100
|
||||
}
|
||||
|
||||
func determineNeedsRewrite(grade float64) bool {
|
||||
return grade >= 5.0
|
||||
}
|
||||
|
||||
func TestExamService_ExamDateValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
examDate time.Time
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "future date - valid",
|
||||
examDate: time.Now().AddDate(0, 0, 7),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "today - valid",
|
||||
examDate: time.Now(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "past date - valid for recording",
|
||||
examDate: time.Now().AddDate(0, 0, -7),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateExamDate(tt.examDate)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func validateExamDate(date time.Time) error {
|
||||
// Exam dates are always valid as we need to record past exams too
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestExamService_ExamStatusTransition(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
currentStatus string
|
||||
newStatus string
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
name: "draft to active",
|
||||
currentStatus: "draft",
|
||||
newStatus: "active",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "active to archived",
|
||||
currentStatus: "active",
|
||||
newStatus: "archived",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "draft to archived",
|
||||
currentStatus: "draft",
|
||||
newStatus: "archived",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "archived to active - invalid",
|
||||
currentStatus: "archived",
|
||||
newStatus: "active",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
name: "archived to draft - invalid",
|
||||
currentStatus: "archived",
|
||||
newStatus: "draft",
|
||||
valid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isValidStatusTransition(tt.currentStatus, tt.newStatus)
|
||||
assert.Equal(t, tt.valid, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func isValidStatusTransition(current, new string) bool {
|
||||
transitions := map[string][]string{
|
||||
"draft": {"active", "archived"},
|
||||
"active": {"archived"},
|
||||
"archived": {},
|
||||
}
|
||||
|
||||
allowed, exists := transitions[current]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, s := range allowed {
|
||||
if s == new {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user