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>
398 lines
9.9 KiB
Go
398 lines
9.9 KiB
Go
package services
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestMonthsBetween(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
start time.Time
|
|
end time.Time
|
|
expected int
|
|
}{
|
|
{
|
|
name: "Same day",
|
|
start: time.Date(2025, 1, 15, 0, 0, 0, 0, time.UTC),
|
|
end: time.Date(2025, 1, 15, 0, 0, 0, 0, time.UTC),
|
|
expected: 0,
|
|
},
|
|
{
|
|
name: "Less than one month",
|
|
start: time.Date(2025, 1, 15, 0, 0, 0, 0, time.UTC),
|
|
end: time.Date(2025, 2, 10, 0, 0, 0, 0, time.UTC),
|
|
expected: 0,
|
|
},
|
|
{
|
|
name: "Exactly one month",
|
|
start: time.Date(2025, 1, 15, 0, 0, 0, 0, time.UTC),
|
|
end: time.Date(2025, 2, 15, 0, 0, 0, 0, time.UTC),
|
|
expected: 1,
|
|
},
|
|
{
|
|
name: "One month and one day",
|
|
start: time.Date(2025, 1, 15, 0, 0, 0, 0, time.UTC),
|
|
end: time.Date(2025, 2, 16, 0, 0, 0, 0, time.UTC),
|
|
expected: 1,
|
|
},
|
|
{
|
|
name: "Two months",
|
|
start: time.Date(2025, 1, 15, 0, 0, 0, 0, time.UTC),
|
|
end: time.Date(2025, 3, 15, 0, 0, 0, 0, time.UTC),
|
|
expected: 2,
|
|
},
|
|
{
|
|
name: "Five months exactly",
|
|
start: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
end: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
|
|
expected: 5,
|
|
},
|
|
{
|
|
name: "Year boundary",
|
|
start: time.Date(2024, 11, 15, 0, 0, 0, 0, time.UTC),
|
|
end: time.Date(2025, 2, 15, 0, 0, 0, 0, time.UTC),
|
|
expected: 3,
|
|
},
|
|
{
|
|
name: "Leap year February to March",
|
|
start: time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC),
|
|
end: time.Date(2024, 3, 29, 0, 0, 0, 0, time.UTC),
|
|
expected: 1,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := monthsBetween(tt.start, tt.end)
|
|
if result != tt.expected {
|
|
t.Errorf("monthsBetween(%v, %v) = %d, expected %d",
|
|
tt.start.Format("2006-01-02"), tt.end.Format("2006-01-02"),
|
|
result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCarryoverLogic(t *testing.T) {
|
|
// Test the carryover calculation logic
|
|
tests := []struct {
|
|
name string
|
|
currentBalance int
|
|
monthlyAllowance int
|
|
maxBalance int
|
|
monthsSinceRenewal int
|
|
expectedNewBalance int
|
|
}{
|
|
{
|
|
name: "Normal renewal - add allowance",
|
|
currentBalance: 50,
|
|
monthlyAllowance: 30,
|
|
maxBalance: 150,
|
|
monthsSinceRenewal: 1,
|
|
expectedNewBalance: 80,
|
|
},
|
|
{
|
|
name: "Two months missed",
|
|
currentBalance: 50,
|
|
monthlyAllowance: 30,
|
|
maxBalance: 150,
|
|
monthsSinceRenewal: 2,
|
|
expectedNewBalance: 110,
|
|
},
|
|
{
|
|
name: "Cap at max balance",
|
|
currentBalance: 140,
|
|
monthlyAllowance: 30,
|
|
maxBalance: 150,
|
|
monthsSinceRenewal: 1,
|
|
expectedNewBalance: 150,
|
|
},
|
|
{
|
|
name: "Already at max - no change",
|
|
currentBalance: 150,
|
|
monthlyAllowance: 30,
|
|
maxBalance: 150,
|
|
monthsSinceRenewal: 1,
|
|
expectedNewBalance: 150,
|
|
},
|
|
{
|
|
name: "Multiple months - cap applies",
|
|
currentBalance: 100,
|
|
monthlyAllowance: 30,
|
|
maxBalance: 150,
|
|
monthsSinceRenewal: 5,
|
|
expectedNewBalance: 150,
|
|
},
|
|
{
|
|
name: "Empty balance - add one month",
|
|
currentBalance: 0,
|
|
monthlyAllowance: 30,
|
|
maxBalance: 150,
|
|
monthsSinceRenewal: 1,
|
|
expectedNewBalance: 30,
|
|
},
|
|
{
|
|
name: "Empty balance - add five months",
|
|
currentBalance: 0,
|
|
monthlyAllowance: 30,
|
|
maxBalance: 150,
|
|
monthsSinceRenewal: 5,
|
|
expectedNewBalance: 150,
|
|
},
|
|
{
|
|
name: "Standard plan - normal case",
|
|
currentBalance: 200,
|
|
monthlyAllowance: 100,
|
|
maxBalance: 500,
|
|
monthsSinceRenewal: 1,
|
|
expectedNewBalance: 300,
|
|
},
|
|
{
|
|
name: "Premium plan - Fair Use",
|
|
currentBalance: 1000,
|
|
monthlyAllowance: 1000,
|
|
maxBalance: 5000,
|
|
monthsSinceRenewal: 1,
|
|
expectedNewBalance: 2000,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Simulate the carryover logic
|
|
newBalance := tt.currentBalance
|
|
for i := 0; i < tt.monthsSinceRenewal; i++ {
|
|
newBalance += tt.monthlyAllowance
|
|
if newBalance > tt.maxBalance {
|
|
newBalance = tt.maxBalance
|
|
break
|
|
}
|
|
}
|
|
|
|
if newBalance != tt.expectedNewBalance {
|
|
t.Errorf("Carryover for balance=%d, allowance=%d, max=%d, months=%d = %d, expected %d",
|
|
tt.currentBalance, tt.monthlyAllowance, tt.maxBalance, tt.monthsSinceRenewal,
|
|
newBalance, tt.expectedNewBalance)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTaskBalanceAfterConsumption(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
currentBalance int
|
|
tasksToConsume int
|
|
expectedBalance int
|
|
shouldBeAllowed bool
|
|
}{
|
|
{
|
|
name: "Normal consumption",
|
|
currentBalance: 50,
|
|
tasksToConsume: 1,
|
|
expectedBalance: 49,
|
|
shouldBeAllowed: true,
|
|
},
|
|
{
|
|
name: "Last task",
|
|
currentBalance: 1,
|
|
tasksToConsume: 1,
|
|
expectedBalance: 0,
|
|
shouldBeAllowed: true,
|
|
},
|
|
{
|
|
name: "Empty balance - not allowed",
|
|
currentBalance: 0,
|
|
tasksToConsume: 1,
|
|
expectedBalance: 0,
|
|
shouldBeAllowed: false,
|
|
},
|
|
{
|
|
name: "Multiple tasks",
|
|
currentBalance: 50,
|
|
tasksToConsume: 5,
|
|
expectedBalance: 45,
|
|
shouldBeAllowed: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Test if allowed
|
|
allowed := tt.currentBalance > 0
|
|
if allowed != tt.shouldBeAllowed {
|
|
t.Errorf("Task allowed with balance=%d: got %v, expected %v",
|
|
tt.currentBalance, allowed, tt.shouldBeAllowed)
|
|
}
|
|
|
|
// Test balance calculation
|
|
if allowed {
|
|
newBalance := tt.currentBalance - tt.tasksToConsume
|
|
if newBalance != tt.expectedBalance {
|
|
t.Errorf("Balance after consuming %d tasks from %d: got %d, expected %d",
|
|
tt.tasksToConsume, tt.currentBalance, newBalance, tt.expectedBalance)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTaskServiceErrors(t *testing.T) {
|
|
// Test error constants
|
|
if ErrTaskLimitReached == nil {
|
|
t.Error("ErrTaskLimitReached should not be nil")
|
|
}
|
|
if ErrTaskLimitReached.Error() != "TASK_LIMIT_REACHED" {
|
|
t.Errorf("ErrTaskLimitReached should be 'TASK_LIMIT_REACHED', got '%s'", ErrTaskLimitReached.Error())
|
|
}
|
|
|
|
if ErrNoSubscription == nil {
|
|
t.Error("ErrNoSubscription should not be nil")
|
|
}
|
|
if ErrNoSubscription.Error() != "NO_SUBSCRIPTION" {
|
|
t.Errorf("ErrNoSubscription should be 'NO_SUBSCRIPTION', got '%s'", ErrNoSubscription.Error())
|
|
}
|
|
}
|
|
|
|
func TestRenewalDateCalculation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
lastRenewal time.Time
|
|
monthsToAdd int
|
|
expectedRenewal time.Time
|
|
}{
|
|
{
|
|
name: "Add one month",
|
|
lastRenewal: time.Date(2025, 1, 15, 0, 0, 0, 0, time.UTC),
|
|
monthsToAdd: 1,
|
|
expectedRenewal: time.Date(2025, 2, 15, 0, 0, 0, 0, time.UTC),
|
|
},
|
|
{
|
|
name: "Add three months",
|
|
lastRenewal: time.Date(2025, 1, 15, 0, 0, 0, 0, time.UTC),
|
|
monthsToAdd: 3,
|
|
expectedRenewal: time.Date(2025, 4, 15, 0, 0, 0, 0, time.UTC),
|
|
},
|
|
{
|
|
name: "Year boundary",
|
|
lastRenewal: time.Date(2024, 11, 15, 0, 0, 0, 0, time.UTC),
|
|
monthsToAdd: 3,
|
|
expectedRenewal: time.Date(2025, 2, 15, 0, 0, 0, 0, time.UTC),
|
|
},
|
|
{
|
|
name: "End of month adjustment",
|
|
lastRenewal: time.Date(2025, 1, 31, 0, 0, 0, 0, time.UTC),
|
|
monthsToAdd: 1,
|
|
// Go's AddDate handles this - February doesn't have 31 days
|
|
expectedRenewal: time.Date(2025, 3, 3, 0, 0, 0, 0, time.UTC), // Feb 31 -> March 3
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.lastRenewal.AddDate(0, tt.monthsToAdd, 0)
|
|
if !result.Equal(tt.expectedRenewal) {
|
|
t.Errorf("AddDate(%v, %d months) = %v, expected %v",
|
|
tt.lastRenewal.Format("2006-01-02"), tt.monthsToAdd,
|
|
result.Format("2006-01-02"), tt.expectedRenewal.Format("2006-01-02"))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFairUseModeLogic(t *testing.T) {
|
|
// Test that Fair Use mode always allows tasks regardless of balance
|
|
tests := []struct {
|
|
name string
|
|
fairUseMode bool
|
|
balance int
|
|
shouldAllow bool
|
|
}{
|
|
{
|
|
name: "Fair Use - zero balance still allowed",
|
|
fairUseMode: true,
|
|
balance: 0,
|
|
shouldAllow: true,
|
|
},
|
|
{
|
|
name: "Fair Use - normal balance allowed",
|
|
fairUseMode: true,
|
|
balance: 1000,
|
|
shouldAllow: true,
|
|
},
|
|
{
|
|
name: "Not Fair Use - zero balance not allowed",
|
|
fairUseMode: false,
|
|
balance: 0,
|
|
shouldAllow: false,
|
|
},
|
|
{
|
|
name: "Not Fair Use - positive balance allowed",
|
|
fairUseMode: false,
|
|
balance: 50,
|
|
shouldAllow: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Simulate the check logic
|
|
var allowed bool
|
|
if tt.fairUseMode {
|
|
allowed = true // Fair Use always allows
|
|
} else {
|
|
allowed = tt.balance > 0
|
|
}
|
|
|
|
if allowed != tt.shouldAllow {
|
|
t.Errorf("FairUseMode=%v, balance=%d: allowed=%v, expected=%v",
|
|
tt.fairUseMode, tt.balance, allowed, tt.shouldAllow)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBalanceDecrementLogic(t *testing.T) {
|
|
// Test that Fair Use mode doesn't decrement balance
|
|
tests := []struct {
|
|
name string
|
|
fairUseMode bool
|
|
initialBalance int
|
|
expectedAfter int
|
|
}{
|
|
{
|
|
name: "Normal plan - decrement",
|
|
fairUseMode: false,
|
|
initialBalance: 50,
|
|
expectedAfter: 49,
|
|
},
|
|
{
|
|
name: "Fair Use - no decrement",
|
|
fairUseMode: true,
|
|
initialBalance: 1000,
|
|
expectedAfter: 1000,
|
|
},
|
|
{
|
|
name: "Normal plan - last task",
|
|
fairUseMode: false,
|
|
initialBalance: 1,
|
|
expectedAfter: 0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
newBalance := tt.initialBalance
|
|
if !tt.fairUseMode {
|
|
newBalance = tt.initialBalance - 1
|
|
}
|
|
|
|
if newBalance != tt.expectedAfter {
|
|
t.Errorf("FairUseMode=%v, initial=%d: got %d, expected %d",
|
|
tt.fairUseMode, tt.initialBalance, newBalance, tt.expectedAfter)
|
|
}
|
|
})
|
|
}
|
|
}
|