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>
421 lines
12 KiB
Go
421 lines
12 KiB
Go
package services
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/breakpilot/consent-service/internal/models"
|
|
)
|
|
|
|
// TestDSRRequestTypeLabel tests label generation for request types
|
|
func TestDSRRequestTypeLabel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
reqType models.DSRRequestType
|
|
expected string
|
|
}{
|
|
{"access type", models.DSRTypeAccess, "Auskunftsanfrage (Art. 15)"},
|
|
{"rectification type", models.DSRTypeRectification, "Berichtigungsanfrage (Art. 16)"},
|
|
{"erasure type", models.DSRTypeErasure, "Löschanfrage (Art. 17)"},
|
|
{"restriction type", models.DSRTypeRestriction, "Einschränkungsanfrage (Art. 18)"},
|
|
{"portability type", models.DSRTypePortability, "Datenübertragung (Art. 20)"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.reqType.Label()
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %s, got %s", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDSRRequestTypeDeadlineDays tests deadline calculation for different request types
|
|
func TestDSRRequestTypeDeadlineDays(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
reqType models.DSRRequestType
|
|
expectedDays int
|
|
}{
|
|
{"access has 30 days", models.DSRTypeAccess, 30},
|
|
{"portability has 30 days", models.DSRTypePortability, 30},
|
|
{"rectification has 14 days", models.DSRTypeRectification, 14},
|
|
{"erasure has 14 days", models.DSRTypeErasure, 14},
|
|
{"restriction has 14 days", models.DSRTypeRestriction, 14},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.reqType.DeadlineDays()
|
|
if result != tt.expectedDays {
|
|
t.Errorf("Expected %d days, got %d", tt.expectedDays, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDSRRequestTypeIsExpedited tests expedited flag for request types
|
|
func TestDSRRequestTypeIsExpedited(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
reqType models.DSRRequestType
|
|
isExpedited bool
|
|
}{
|
|
{"access not expedited", models.DSRTypeAccess, false},
|
|
{"portability not expedited", models.DSRTypePortability, false},
|
|
{"rectification is expedited", models.DSRTypeRectification, true},
|
|
{"erasure is expedited", models.DSRTypeErasure, true},
|
|
{"restriction is expedited", models.DSRTypeRestriction, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.reqType.IsExpedited()
|
|
if result != tt.isExpedited {
|
|
t.Errorf("Expected IsExpedited=%v, got %v", tt.isExpedited, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDSRStatusLabel tests label generation for statuses
|
|
func TestDSRStatusLabel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
status models.DSRStatus
|
|
expected string
|
|
}{
|
|
{"intake status", models.DSRStatusIntake, "Eingang"},
|
|
{"identity verification", models.DSRStatusIdentityVerification, "Identitätsprüfung"},
|
|
{"processing status", models.DSRStatusProcessing, "In Bearbeitung"},
|
|
{"completed status", models.DSRStatusCompleted, "Abgeschlossen"},
|
|
{"rejected status", models.DSRStatusRejected, "Abgelehnt"},
|
|
{"cancelled status", models.DSRStatusCancelled, "Storniert"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.status.Label()
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %s, got %s", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidDSRRequestType tests request type validation
|
|
func TestValidDSRRequestType(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
reqType string
|
|
valid bool
|
|
}{
|
|
{"valid access", "access", true},
|
|
{"valid rectification", "rectification", true},
|
|
{"valid erasure", "erasure", true},
|
|
{"valid restriction", "restriction", true},
|
|
{"valid portability", "portability", true},
|
|
{"invalid type", "invalid", false},
|
|
{"empty type", "", false},
|
|
{"random string", "delete_everything", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := models.IsValidDSRRequestType(tt.reqType)
|
|
if result != tt.valid {
|
|
t.Errorf("Expected IsValidDSRRequestType=%v for %s, got %v", tt.valid, tt.reqType, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidDSRStatus tests status validation
|
|
func TestValidDSRStatus(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
status string
|
|
valid bool
|
|
}{
|
|
{"valid intake", "intake", true},
|
|
{"valid identity_verification", "identity_verification", true},
|
|
{"valid processing", "processing", true},
|
|
{"valid completed", "completed", true},
|
|
{"valid rejected", "rejected", true},
|
|
{"valid cancelled", "cancelled", true},
|
|
{"invalid status", "invalid", false},
|
|
{"empty status", "", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := models.IsValidDSRStatus(tt.status)
|
|
if result != tt.valid {
|
|
t.Errorf("Expected IsValidDSRStatus=%v for %s, got %v", tt.valid, tt.status, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDSRStatusTransitionValidation tests allowed status transitions
|
|
func TestDSRStatusTransitionValidation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
fromStatus models.DSRStatus
|
|
toStatus models.DSRStatus
|
|
allowed bool
|
|
}{
|
|
// From intake
|
|
{"intake to identity_verification", models.DSRStatusIntake, models.DSRStatusIdentityVerification, true},
|
|
{"intake to processing", models.DSRStatusIntake, models.DSRStatusProcessing, true},
|
|
{"intake to rejected", models.DSRStatusIntake, models.DSRStatusRejected, true},
|
|
{"intake to cancelled", models.DSRStatusIntake, models.DSRStatusCancelled, true},
|
|
{"intake to completed invalid", models.DSRStatusIntake, models.DSRStatusCompleted, false},
|
|
|
|
// From identity_verification
|
|
{"identity to processing", models.DSRStatusIdentityVerification, models.DSRStatusProcessing, true},
|
|
{"identity to rejected", models.DSRStatusIdentityVerification, models.DSRStatusRejected, true},
|
|
{"identity to cancelled", models.DSRStatusIdentityVerification, models.DSRStatusCancelled, true},
|
|
|
|
// From processing
|
|
{"processing to completed", models.DSRStatusProcessing, models.DSRStatusCompleted, true},
|
|
{"processing to rejected", models.DSRStatusProcessing, models.DSRStatusRejected, true},
|
|
{"processing to intake invalid", models.DSRStatusProcessing, models.DSRStatusIntake, false},
|
|
|
|
// From completed
|
|
{"completed to anything invalid", models.DSRStatusCompleted, models.DSRStatusProcessing, false},
|
|
|
|
// From rejected
|
|
{"rejected to anything invalid", models.DSRStatusRejected, models.DSRStatusProcessing, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := testIsValidStatusTransition(tt.fromStatus, tt.toStatus)
|
|
if result != tt.allowed {
|
|
t.Errorf("Expected transition %s->%s allowed=%v, got %v",
|
|
tt.fromStatus, tt.toStatus, tt.allowed, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// testIsValidStatusTransition is a test helper for validating status transitions
|
|
// This mirrors the logic in dsr_service.go for testing purposes
|
|
func testIsValidStatusTransition(from, to models.DSRStatus) bool {
|
|
validTransitions := map[models.DSRStatus][]models.DSRStatus{
|
|
models.DSRStatusIntake: {
|
|
models.DSRStatusIdentityVerification,
|
|
models.DSRStatusProcessing,
|
|
models.DSRStatusRejected,
|
|
models.DSRStatusCancelled,
|
|
},
|
|
models.DSRStatusIdentityVerification: {
|
|
models.DSRStatusProcessing,
|
|
models.DSRStatusRejected,
|
|
models.DSRStatusCancelled,
|
|
},
|
|
models.DSRStatusProcessing: {
|
|
models.DSRStatusCompleted,
|
|
models.DSRStatusRejected,
|
|
models.DSRStatusCancelled,
|
|
},
|
|
models.DSRStatusCompleted: {},
|
|
models.DSRStatusRejected: {},
|
|
models.DSRStatusCancelled: {},
|
|
}
|
|
|
|
allowed, exists := validTransitions[from]
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
for _, s := range allowed {
|
|
if s == to {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// TestCalculateDeadline tests deadline calculation
|
|
func TestCalculateDeadline(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
reqType models.DSRRequestType
|
|
expectedDays int
|
|
}{
|
|
{"access 30 days", models.DSRTypeAccess, 30},
|
|
{"erasure 14 days", models.DSRTypeErasure, 14},
|
|
{"rectification 14 days", models.DSRTypeRectification, 14},
|
|
{"restriction 14 days", models.DSRTypeRestriction, 14},
|
|
{"portability 30 days", models.DSRTypePortability, 30},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
now := time.Now()
|
|
deadline := now.AddDate(0, 0, tt.expectedDays)
|
|
days := tt.reqType.DeadlineDays()
|
|
|
|
if days != tt.expectedDays {
|
|
t.Errorf("Expected %d days, got %d", tt.expectedDays, days)
|
|
}
|
|
|
|
// Verify deadline is approximately correct (within 1 day due to test timing)
|
|
calculatedDeadline := now.AddDate(0, 0, days)
|
|
diff := calculatedDeadline.Sub(deadline)
|
|
if diff > time.Hour*24 || diff < -time.Hour*24 {
|
|
t.Errorf("Deadline calculation off by more than a day")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCreateDSRRequest_Validation tests validation of create request
|
|
func TestCreateDSRRequest_Validation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
request models.CreateDSRRequest
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "valid access request",
|
|
request: models.CreateDSRRequest{
|
|
RequestType: "access",
|
|
RequesterEmail: "test@example.com",
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "valid erasure request with name",
|
|
request: models.CreateDSRRequest{
|
|
RequestType: "erasure",
|
|
RequesterEmail: "test@example.com",
|
|
RequesterName: stringPtr("Max Mustermann"),
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "missing email",
|
|
request: models.CreateDSRRequest{
|
|
RequestType: "access",
|
|
},
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "invalid request type",
|
|
request: models.CreateDSRRequest{
|
|
RequestType: "invalid_type",
|
|
RequesterEmail: "test@example.com",
|
|
},
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "empty request type",
|
|
request: models.CreateDSRRequest{
|
|
RequestType: "",
|
|
RequesterEmail: "test@example.com",
|
|
},
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := testValidateCreateDSRRequest(tt.request)
|
|
hasError := err != nil
|
|
|
|
if hasError != tt.expectError {
|
|
t.Errorf("Expected error=%v, got error=%v (err: %v)", tt.expectError, hasError, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// testValidateCreateDSRRequest is a test helper for validating create DSR requests
|
|
func testValidateCreateDSRRequest(req models.CreateDSRRequest) error {
|
|
if req.RequesterEmail == "" {
|
|
return &dsrValidationError{"requester_email is required"}
|
|
}
|
|
if !models.IsValidDSRRequestType(req.RequestType) {
|
|
return &dsrValidationError{"invalid request_type"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type dsrValidationError struct {
|
|
Message string
|
|
}
|
|
|
|
func (e *dsrValidationError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// TestDSRTemplateTypes tests the template types
|
|
func TestDSRTemplateTypes(t *testing.T) {
|
|
expectedTemplates := []string{
|
|
"dsr_receipt_access",
|
|
"dsr_receipt_rectification",
|
|
"dsr_receipt_erasure",
|
|
"dsr_receipt_restriction",
|
|
"dsr_receipt_portability",
|
|
"dsr_identity_request",
|
|
"dsr_processing_started",
|
|
"dsr_processing_update",
|
|
"dsr_clarification_request",
|
|
"dsr_completed_access",
|
|
"dsr_completed_rectification",
|
|
"dsr_completed_erasure",
|
|
"dsr_completed_restriction",
|
|
"dsr_completed_portability",
|
|
"dsr_restriction_lifted",
|
|
"dsr_rejected_identity",
|
|
"dsr_rejected_exception",
|
|
"dsr_rejected_unfounded",
|
|
"dsr_deadline_warning",
|
|
}
|
|
|
|
// This test documents the expected template types
|
|
// The actual templates are created in database migration
|
|
for _, template := range expectedTemplates {
|
|
if template == "" {
|
|
t.Error("Template type should not be empty")
|
|
}
|
|
}
|
|
|
|
if len(expectedTemplates) != 19 {
|
|
t.Errorf("Expected 19 template types, got %d", len(expectedTemplates))
|
|
}
|
|
}
|
|
|
|
// TestErasureExceptionTypes tests Art. 17(3) exception types
|
|
func TestErasureExceptionTypes(t *testing.T) {
|
|
exceptions := []struct {
|
|
code string
|
|
description string
|
|
}{
|
|
{"art_17_3_a", "Meinungs- und Informationsfreiheit"},
|
|
{"art_17_3_b", "Rechtliche Verpflichtung"},
|
|
{"art_17_3_c", "Öffentliches Interesse im Gesundheitsbereich"},
|
|
{"art_17_3_d", "Archivzwecke, wissenschaftliche/historische Forschung"},
|
|
{"art_17_3_e", "Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen"},
|
|
}
|
|
|
|
if len(exceptions) != 5 {
|
|
t.Errorf("Expected 5 Art. 17(3) exceptions, got %d", len(exceptions))
|
|
}
|
|
|
|
for _, ex := range exceptions {
|
|
if ex.code == "" || ex.description == "" {
|
|
t.Error("Exception code and description should not be empty")
|
|
}
|
|
}
|
|
}
|
|
|
|
// stringPtr returns a pointer to the given string
|
|
func stringPtr(s string) *string {
|
|
return &s
|
|
}
|