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>
440 lines
10 KiB
Go
440 lines
10 KiB
Go
package services
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// TestDeadlineService_CreateDeadline tests creating consent deadlines
|
|
func TestDeadlineService_CreateDeadline(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
userID uuid.UUID
|
|
versionID uuid.UUID
|
|
deadlineAt time.Time
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "valid deadline - 30 days",
|
|
userID: uuid.New(),
|
|
versionID: uuid.New(),
|
|
deadlineAt: time.Now().AddDate(0, 0, 30),
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "valid deadline - 14 days",
|
|
userID: uuid.New(),
|
|
versionID: uuid.New(),
|
|
deadlineAt: time.Now().AddDate(0, 0, 14),
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "invalid user ID",
|
|
userID: uuid.Nil,
|
|
versionID: uuid.New(),
|
|
deadlineAt: time.Now().AddDate(0, 0, 30),
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "invalid version ID",
|
|
userID: uuid.New(),
|
|
versionID: uuid.Nil,
|
|
deadlineAt: time.Now().AddDate(0, 0, 30),
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "deadline in past",
|
|
userID: uuid.New(),
|
|
versionID: uuid.New(),
|
|
deadlineAt: time.Now().AddDate(0, 0, -1),
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var err error
|
|
if tt.userID == uuid.Nil {
|
|
err = &ValidationError{Field: "user ID", Message: "required"}
|
|
} else if tt.versionID == uuid.Nil {
|
|
err = &ValidationError{Field: "version ID", Message: "required"}
|
|
} else if tt.deadlineAt.Before(time.Now()) {
|
|
err = &ValidationError{Field: "deadline", Message: "must be in the future"}
|
|
}
|
|
|
|
if tt.expectError && err == nil {
|
|
t.Error("Expected error, got nil")
|
|
}
|
|
if !tt.expectError && err != nil {
|
|
t.Errorf("Expected no error, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDeadlineService_CheckDeadlineStatus tests deadline status checking
|
|
func TestDeadlineService_CheckDeadlineStatus(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
tests := []struct {
|
|
name string
|
|
deadlineAt time.Time
|
|
isOverdue bool
|
|
daysLeft int
|
|
urgency string
|
|
}{
|
|
{
|
|
name: "30 days left",
|
|
deadlineAt: now.AddDate(0, 0, 30),
|
|
isOverdue: false,
|
|
daysLeft: 30,
|
|
urgency: "normal",
|
|
},
|
|
{
|
|
name: "7 days left - warning",
|
|
deadlineAt: now.AddDate(0, 0, 7),
|
|
isOverdue: false,
|
|
daysLeft: 7,
|
|
urgency: "warning",
|
|
},
|
|
{
|
|
name: "3 days left - urgent",
|
|
deadlineAt: now.AddDate(0, 0, 3),
|
|
isOverdue: false,
|
|
daysLeft: 3,
|
|
urgency: "urgent",
|
|
},
|
|
{
|
|
name: "1 day left - critical",
|
|
deadlineAt: now.AddDate(0, 0, 1),
|
|
isOverdue: false,
|
|
daysLeft: 1,
|
|
urgency: "critical",
|
|
},
|
|
{
|
|
name: "overdue by 1 day",
|
|
deadlineAt: now.AddDate(0, 0, -1),
|
|
isOverdue: true,
|
|
daysLeft: -1,
|
|
urgency: "overdue",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
isOverdue := tt.deadlineAt.Before(now)
|
|
daysLeft := int(tt.deadlineAt.Sub(now).Hours() / 24)
|
|
|
|
var urgency string
|
|
if isOverdue {
|
|
urgency = "overdue"
|
|
} else if daysLeft <= 1 {
|
|
urgency = "critical"
|
|
} else if daysLeft <= 3 {
|
|
urgency = "urgent"
|
|
} else if daysLeft <= 7 {
|
|
urgency = "warning"
|
|
} else {
|
|
urgency = "normal"
|
|
}
|
|
|
|
if isOverdue != tt.isOverdue {
|
|
t.Errorf("Expected isOverdue=%v, got %v", tt.isOverdue, isOverdue)
|
|
}
|
|
|
|
if abs(daysLeft-tt.daysLeft) > 1 { // Allow 1 day difference
|
|
t.Errorf("Expected daysLeft=%d, got %d", tt.daysLeft, daysLeft)
|
|
}
|
|
|
|
if urgency != tt.urgency {
|
|
t.Errorf("Expected urgency=%s, got %s", tt.urgency, urgency)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDeadlineService_SendReminders tests reminder scheduling
|
|
func TestDeadlineService_SendReminders(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
tests := []struct {
|
|
name string
|
|
deadlineAt time.Time
|
|
lastReminderAt *time.Time
|
|
reminderCount int
|
|
shouldSend bool
|
|
nextReminder int // days before deadline
|
|
}{
|
|
{
|
|
name: "first reminder - 14 days before",
|
|
deadlineAt: now.AddDate(0, 0, 14),
|
|
lastReminderAt: nil,
|
|
reminderCount: 0,
|
|
shouldSend: true,
|
|
nextReminder: 14,
|
|
},
|
|
{
|
|
name: "second reminder - 7 days before",
|
|
deadlineAt: now.AddDate(0, 0, 7),
|
|
lastReminderAt: ptrTime(now.AddDate(0, 0, -7)),
|
|
reminderCount: 1,
|
|
shouldSend: true,
|
|
nextReminder: 7,
|
|
},
|
|
{
|
|
name: "third reminder - 3 days before",
|
|
deadlineAt: now.AddDate(0, 0, 3),
|
|
lastReminderAt: ptrTime(now.AddDate(0, 0, -4)),
|
|
reminderCount: 2,
|
|
shouldSend: true,
|
|
nextReminder: 3,
|
|
},
|
|
{
|
|
name: "final reminder - 1 day before",
|
|
deadlineAt: now.AddDate(0, 0, 1),
|
|
lastReminderAt: ptrTime(now.AddDate(0, 0, -2)),
|
|
reminderCount: 3,
|
|
shouldSend: true,
|
|
nextReminder: 1,
|
|
},
|
|
{
|
|
name: "too soon for next reminder",
|
|
deadlineAt: now.AddDate(0, 0, 10),
|
|
lastReminderAt: ptrTime(now.AddDate(0, 0, -1)),
|
|
reminderCount: 1,
|
|
shouldSend: false,
|
|
nextReminder: 0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
daysUntilDeadline := int(tt.deadlineAt.Sub(now).Hours() / 24)
|
|
|
|
// Reminder schedule: 14, 7, 3, 1 days before deadline
|
|
reminderDays := []int{14, 7, 3, 1}
|
|
shouldSend := false
|
|
|
|
for _, day := range reminderDays {
|
|
if daysUntilDeadline == day {
|
|
// Check if enough time passed since last reminder
|
|
if tt.lastReminderAt == nil || now.Sub(*tt.lastReminderAt) > 12*time.Hour {
|
|
shouldSend = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if shouldSend != tt.shouldSend {
|
|
t.Errorf("Expected shouldSend=%v, got %v", tt.shouldSend, shouldSend)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDeadlineService_SuspendAccount tests account suspension logic
|
|
func TestDeadlineService_SuspendAccount(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
userID uuid.UUID
|
|
reason string
|
|
shouldSuspend bool
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "suspend for missed deadline",
|
|
userID: uuid.New(),
|
|
reason: "consent_deadline_exceeded",
|
|
shouldSuspend: true,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "invalid user ID",
|
|
userID: uuid.Nil,
|
|
reason: "consent_deadline_exceeded",
|
|
shouldSuspend: false,
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "invalid reason",
|
|
userID: uuid.New(),
|
|
reason: "",
|
|
shouldSuspend: false,
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
validReasons := map[string]bool{
|
|
"consent_deadline_exceeded": true,
|
|
"mandatory_consent_missing": true,
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var err error
|
|
if tt.userID == uuid.Nil {
|
|
err = &ValidationError{Field: "user ID", Message: "required"}
|
|
} else if !validReasons[tt.reason] && tt.reason != "" {
|
|
err = &ValidationError{Field: "reason", Message: "invalid suspension reason"}
|
|
} else if tt.reason == "" {
|
|
err = &ValidationError{Field: "reason", Message: "required"}
|
|
}
|
|
|
|
if tt.expectError && err == nil {
|
|
t.Error("Expected error, got nil")
|
|
}
|
|
if !tt.expectError && err != nil {
|
|
t.Errorf("Expected no error, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDeadlineService_LiftSuspension tests lifting account suspension
|
|
func TestDeadlineService_LiftSuspension(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
userID uuid.UUID
|
|
adminID uuid.UUID
|
|
reason string
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "lift valid suspension",
|
|
userID: uuid.New(),
|
|
adminID: uuid.New(),
|
|
reason: "consent provided",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "invalid user ID",
|
|
userID: uuid.Nil,
|
|
adminID: uuid.New(),
|
|
reason: "consent provided",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "invalid admin ID",
|
|
userID: uuid.New(),
|
|
adminID: uuid.Nil,
|
|
reason: "consent provided",
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var err error
|
|
if tt.userID == uuid.Nil {
|
|
err = &ValidationError{Field: "user ID", Message: "required"}
|
|
} else if tt.adminID == uuid.Nil {
|
|
err = &ValidationError{Field: "admin ID", Message: "required"}
|
|
}
|
|
|
|
if tt.expectError && err == nil {
|
|
t.Error("Expected error, got nil")
|
|
}
|
|
if !tt.expectError && err != nil {
|
|
t.Errorf("Expected no error, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDeadlineService_GetOverdueDeadlines tests finding overdue deadlines
|
|
func TestDeadlineService_GetOverdueDeadlines(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
tests := []struct {
|
|
name string
|
|
deadlines []time.Time
|
|
expected int // number of overdue
|
|
}{
|
|
{
|
|
name: "no overdue deadlines",
|
|
deadlines: []time.Time{
|
|
now.AddDate(0, 0, 1),
|
|
now.AddDate(0, 0, 7),
|
|
now.AddDate(0, 0, 30),
|
|
},
|
|
expected: 0,
|
|
},
|
|
{
|
|
name: "some overdue",
|
|
deadlines: []time.Time{
|
|
now.AddDate(0, 0, -1),
|
|
now.AddDate(0, 0, -5),
|
|
now.AddDate(0, 0, 7),
|
|
},
|
|
expected: 2,
|
|
},
|
|
{
|
|
name: "all overdue",
|
|
deadlines: []time.Time{
|
|
now.AddDate(0, 0, -1),
|
|
now.AddDate(0, 0, -7),
|
|
now.AddDate(0, 0, -30),
|
|
},
|
|
expected: 3,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
overdueCount := 0
|
|
for _, deadline := range tt.deadlines {
|
|
if deadline.Before(now) {
|
|
overdueCount++
|
|
}
|
|
}
|
|
|
|
if overdueCount != tt.expected {
|
|
t.Errorf("Expected %d overdue, got %d", tt.expected, overdueCount)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDeadlineService_ProcessScheduledTasks tests scheduled task processing
|
|
func TestDeadlineService_ProcessScheduledTasks(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
tests := []struct {
|
|
name string
|
|
task string
|
|
scheduledAt time.Time
|
|
shouldProcess bool
|
|
}{
|
|
{
|
|
name: "process due task",
|
|
task: "send_reminder",
|
|
scheduledAt: now.Add(-1 * time.Hour),
|
|
shouldProcess: true,
|
|
},
|
|
{
|
|
name: "skip future task",
|
|
task: "send_reminder",
|
|
scheduledAt: now.Add(1 * time.Hour),
|
|
shouldProcess: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
shouldProcess := tt.scheduledAt.Before(now) || tt.scheduledAt.Equal(now)
|
|
|
|
if shouldProcess != tt.shouldProcess {
|
|
t.Errorf("Expected shouldProcess=%v, got %v", tt.shouldProcess, shouldProcess)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func ptrTime(t time.Time) *time.Time {
|
|
return &t
|
|
}
|