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>
729 lines
18 KiB
Go
729 lines
18 KiB
Go
package services
|
|
|
|
import (
|
|
"regexp"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// TestDocumentService_CreateDocument tests creating a new legal document
|
|
func TestDocumentService_CreateDocument(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
docType string
|
|
docName string
|
|
description string
|
|
isMandatory bool
|
|
expectError bool
|
|
errorContains string
|
|
}{
|
|
{
|
|
name: "valid mandatory document",
|
|
docType: "terms",
|
|
docName: "Terms of Service",
|
|
description: "Our terms and conditions",
|
|
isMandatory: true,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "valid optional document",
|
|
docType: "cookies",
|
|
docName: "Cookie Policy",
|
|
description: "How we use cookies",
|
|
isMandatory: false,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "empty document type",
|
|
docType: "",
|
|
docName: "Test Document",
|
|
description: "Test",
|
|
isMandatory: true,
|
|
expectError: true,
|
|
errorContains: "type",
|
|
},
|
|
{
|
|
name: "empty document name",
|
|
docType: "privacy",
|
|
docName: "",
|
|
description: "Test",
|
|
isMandatory: true,
|
|
expectError: true,
|
|
errorContains: "name",
|
|
},
|
|
{
|
|
name: "invalid document type",
|
|
docType: "invalid_type",
|
|
docName: "Test",
|
|
description: "Test",
|
|
isMandatory: false,
|
|
expectError: true,
|
|
errorContains: "type",
|
|
},
|
|
}
|
|
|
|
validTypes := map[string]bool{
|
|
"terms": true,
|
|
"privacy": true,
|
|
"cookies": true,
|
|
"community_guidelines": true,
|
|
"imprint": true,
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Validate inputs
|
|
var err error
|
|
if tt.docType == "" {
|
|
err = &ValidationError{Field: "type", Message: "required"}
|
|
} else if !validTypes[tt.docType] {
|
|
err = &ValidationError{Field: "type", Message: "invalid document type"}
|
|
} else if tt.docName == "" {
|
|
err = &ValidationError{Field: "name", Message: "required"}
|
|
}
|
|
|
|
// Assert
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Errorf("Expected error containing '%s', got nil", tt.errorContains)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("Expected no error, got %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDocumentService_UpdateDocument tests updating a document
|
|
func TestDocumentService_UpdateDocument(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
documentID uuid.UUID
|
|
newName string
|
|
newActive bool
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "valid update",
|
|
documentID: uuid.New(),
|
|
newName: "Updated Name",
|
|
newActive: true,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "deactivate document",
|
|
documentID: uuid.New(),
|
|
newName: "Test",
|
|
newActive: false,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "invalid document ID",
|
|
documentID: uuid.Nil,
|
|
newName: "Test",
|
|
newActive: true,
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var err error
|
|
if tt.documentID == uuid.Nil {
|
|
err = &ValidationError{Field: "document 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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDocumentService_CreateVersion tests creating a document version
|
|
func TestDocumentService_CreateVersion(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
documentID uuid.UUID
|
|
version string
|
|
language string
|
|
title string
|
|
content string
|
|
expectError bool
|
|
errorContains string
|
|
}{
|
|
{
|
|
name: "valid version - German",
|
|
documentID: uuid.New(),
|
|
version: "1.0.0",
|
|
language: "de",
|
|
title: "Nutzungsbedingungen",
|
|
content: "<h1>Terms</h1><p>Content...</p>",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "valid version - English",
|
|
documentID: uuid.New(),
|
|
version: "1.0.0",
|
|
language: "en",
|
|
title: "Terms of Service",
|
|
content: "<h1>Terms</h1><p>Content...</p>",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "invalid version format",
|
|
documentID: uuid.New(),
|
|
version: "1.0",
|
|
language: "de",
|
|
title: "Test",
|
|
content: "Content",
|
|
expectError: true,
|
|
errorContains: "version",
|
|
},
|
|
{
|
|
name: "invalid language",
|
|
documentID: uuid.New(),
|
|
version: "1.0.0",
|
|
language: "fr",
|
|
title: "Test",
|
|
content: "Content",
|
|
expectError: true,
|
|
errorContains: "language",
|
|
},
|
|
{
|
|
name: "empty title",
|
|
documentID: uuid.New(),
|
|
version: "1.0.0",
|
|
language: "de",
|
|
title: "",
|
|
content: "Content",
|
|
expectError: true,
|
|
errorContains: "title",
|
|
},
|
|
{
|
|
name: "empty content",
|
|
documentID: uuid.New(),
|
|
version: "1.0.0",
|
|
language: "de",
|
|
title: "Test",
|
|
content: "",
|
|
expectError: true,
|
|
errorContains: "content",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Validate semver format (X.Y.Z pattern)
|
|
validVersion := regexp.MustCompile(`^\d+\.\d+\.\d+$`).MatchString(tt.version)
|
|
validLanguage := tt.language == "de" || tt.language == "en"
|
|
|
|
var err error
|
|
if !validVersion {
|
|
err = &ValidationError{Field: "version", Message: "invalid format"}
|
|
} else if !validLanguage {
|
|
err = &ValidationError{Field: "language", Message: "must be 'de' or 'en'"}
|
|
} else if tt.title == "" {
|
|
err = &ValidationError{Field: "title", Message: "required"}
|
|
} else if tt.content == "" {
|
|
err = &ValidationError{Field: "content", Message: "required"}
|
|
}
|
|
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Errorf("Expected error containing '%s', got nil", tt.errorContains)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("Expected no error, got %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDocumentService_VersionStatusTransitions tests version status workflow
|
|
func TestDocumentService_VersionStatusTransitions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
fromStatus string
|
|
toStatus string
|
|
isAllowed bool
|
|
}{
|
|
// Valid transitions
|
|
{"draft to review", "draft", "review", true},
|
|
{"review to approved", "review", "approved", true},
|
|
{"review to rejected", "review", "rejected", true},
|
|
{"approved to published", "approved", "published", true},
|
|
{"approved to scheduled", "approved", "scheduled", true},
|
|
{"scheduled to published", "scheduled", "published", true},
|
|
{"published to archived", "published", "archived", true},
|
|
{"rejected to draft", "rejected", "draft", true},
|
|
|
|
// Invalid transitions
|
|
{"draft to published", "draft", "published", false},
|
|
{"draft to approved", "draft", "approved", false},
|
|
{"review to published", "review", "published", false},
|
|
{"published to draft", "published", "draft", false},
|
|
{"published to review", "published", "review", false},
|
|
{"archived to draft", "archived", "draft", false},
|
|
{"archived to published", "archived", "published", false},
|
|
}
|
|
|
|
// Define valid transitions
|
|
validTransitions := map[string][]string{
|
|
"draft": {"review"},
|
|
"review": {"approved", "rejected"},
|
|
"approved": {"published", "scheduled"},
|
|
"scheduled": {"published"},
|
|
"published": {"archived"},
|
|
"rejected": {"draft"},
|
|
"archived": {}, // terminal state
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Check if transition is allowed
|
|
allowed := false
|
|
if transitions, ok := validTransitions[tt.fromStatus]; ok {
|
|
for _, validTo := range transitions {
|
|
if validTo == tt.toStatus {
|
|
allowed = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if allowed != tt.isAllowed {
|
|
t.Errorf("Transition %s->%s: expected allowed=%v, got %v",
|
|
tt.fromStatus, tt.toStatus, tt.isAllowed, allowed)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDocumentService_PublishVersion tests publishing a version
|
|
func TestDocumentService_PublishVersion(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
versionID uuid.UUID
|
|
currentStatus string
|
|
expectError bool
|
|
errorContains string
|
|
}{
|
|
{
|
|
name: "publish approved version",
|
|
versionID: uuid.New(),
|
|
currentStatus: "approved",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "publish scheduled version",
|
|
versionID: uuid.New(),
|
|
currentStatus: "scheduled",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "cannot publish draft",
|
|
versionID: uuid.New(),
|
|
currentStatus: "draft",
|
|
expectError: true,
|
|
errorContains: "draft",
|
|
},
|
|
{
|
|
name: "cannot publish review",
|
|
versionID: uuid.New(),
|
|
currentStatus: "review",
|
|
expectError: true,
|
|
errorContains: "review",
|
|
},
|
|
{
|
|
name: "invalid version ID",
|
|
versionID: uuid.Nil,
|
|
currentStatus: "approved",
|
|
expectError: true,
|
|
errorContains: "ID",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var err error
|
|
if tt.versionID == uuid.Nil {
|
|
err = &ValidationError{Field: "version ID", Message: "required"}
|
|
} else if tt.currentStatus != "approved" && tt.currentStatus != "scheduled" {
|
|
err = &ValidationError{Field: "status", Message: "only approved or scheduled versions can be published"}
|
|
}
|
|
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Errorf("Expected error containing '%s', got nil", tt.errorContains)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("Expected no error, got %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDocumentService_ArchiveVersion tests archiving a version
|
|
func TestDocumentService_ArchiveVersion(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
versionID uuid.UUID
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "archive valid version",
|
|
versionID: uuid.New(),
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "invalid version ID",
|
|
versionID: uuid.Nil,
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var err error
|
|
if tt.versionID == uuid.Nil {
|
|
err = &ValidationError{Field: "version 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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDocumentService_DeleteVersion tests deleting a version
|
|
func TestDocumentService_DeleteVersion(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
versionID uuid.UUID
|
|
status string
|
|
canDelete bool
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "delete draft version",
|
|
versionID: uuid.New(),
|
|
status: "draft",
|
|
canDelete: true,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "delete rejected version",
|
|
versionID: uuid.New(),
|
|
status: "rejected",
|
|
canDelete: true,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "cannot delete published version",
|
|
versionID: uuid.New(),
|
|
status: "published",
|
|
canDelete: false,
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "cannot delete approved version",
|
|
versionID: uuid.New(),
|
|
status: "approved",
|
|
canDelete: false,
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "cannot delete archived version",
|
|
versionID: uuid.New(),
|
|
status: "archived",
|
|
canDelete: false,
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Only draft and rejected can be deleted
|
|
canDelete := tt.status == "draft" || tt.status == "rejected"
|
|
|
|
var err error
|
|
if !canDelete {
|
|
err = &ValidationError{Field: "status", Message: "only draft or rejected versions can be deleted"}
|
|
}
|
|
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Error("Expected error, got nil")
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("Expected no error, got %v", err)
|
|
}
|
|
}
|
|
|
|
if canDelete != tt.canDelete {
|
|
t.Errorf("Expected canDelete=%v, got %v", tt.canDelete, canDelete)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDocumentService_GetLatestVersion tests retrieving the latest version
|
|
func TestDocumentService_GetLatestVersion(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
documentID uuid.UUID
|
|
language string
|
|
status string
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "get latest German version",
|
|
documentID: uuid.New(),
|
|
language: "de",
|
|
status: "published",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "get latest English version",
|
|
documentID: uuid.New(),
|
|
language: "en",
|
|
status: "published",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "invalid document ID",
|
|
documentID: uuid.Nil,
|
|
language: "de",
|
|
status: "published",
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var err error
|
|
if tt.documentID == uuid.Nil {
|
|
err = &ValidationError{Field: "document 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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDocumentService_CompareVersions tests version comparison
|
|
func TestDocumentService_CompareVersions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
version1 string
|
|
version2 string
|
|
isDifferent bool
|
|
}{
|
|
{
|
|
name: "same version",
|
|
version1: "1.0.0",
|
|
version2: "1.0.0",
|
|
isDifferent: false,
|
|
},
|
|
{
|
|
name: "different major version",
|
|
version1: "2.0.0",
|
|
version2: "1.0.0",
|
|
isDifferent: true,
|
|
},
|
|
{
|
|
name: "different minor version",
|
|
version1: "1.1.0",
|
|
version2: "1.0.0",
|
|
isDifferent: true,
|
|
},
|
|
{
|
|
name: "different patch version",
|
|
version1: "1.0.1",
|
|
version2: "1.0.0",
|
|
isDifferent: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
isDifferent := tt.version1 != tt.version2
|
|
|
|
if isDifferent != tt.isDifferent {
|
|
t.Errorf("Expected isDifferent=%v, got %v", tt.isDifferent, isDifferent)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDocumentService_ScheduledPublishing tests scheduled publishing
|
|
func TestDocumentService_ScheduledPublishing(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
tests := []struct {
|
|
name string
|
|
scheduledAt time.Time
|
|
shouldPublish bool
|
|
}{
|
|
{
|
|
name: "scheduled for past - should publish",
|
|
scheduledAt: now.Add(-1 * time.Hour),
|
|
shouldPublish: true,
|
|
},
|
|
{
|
|
name: "scheduled for now - should publish",
|
|
scheduledAt: now,
|
|
shouldPublish: true,
|
|
},
|
|
{
|
|
name: "scheduled for future - should not publish",
|
|
scheduledAt: now.Add(1 * time.Hour),
|
|
shouldPublish: false,
|
|
},
|
|
{
|
|
name: "scheduled for tomorrow - should not publish",
|
|
scheduledAt: now.AddDate(0, 0, 1),
|
|
shouldPublish: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
shouldPublish := tt.scheduledAt.Before(now) || tt.scheduledAt.Equal(now)
|
|
|
|
if shouldPublish != tt.shouldPublish {
|
|
t.Errorf("Expected shouldPublish=%v, got %v", tt.shouldPublish, shouldPublish)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDocumentService_ApprovalWorkflow tests the approval workflow
|
|
func TestDocumentService_ApprovalWorkflow(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
action string
|
|
userRole string
|
|
isAllowed bool
|
|
}{
|
|
// Admin permissions
|
|
{"admin submit for review", "submit_review", "admin", true},
|
|
{"admin cannot approve", "approve", "admin", false},
|
|
{"admin can publish", "publish", "admin", true},
|
|
|
|
// DSB permissions
|
|
{"dsb can approve", "approve", "data_protection_officer", true},
|
|
{"dsb can reject", "reject", "data_protection_officer", true},
|
|
{"dsb can publish", "publish", "data_protection_officer", true},
|
|
|
|
// User permissions
|
|
{"user cannot submit", "submit_review", "user", false},
|
|
{"user cannot approve", "approve", "user", false},
|
|
{"user cannot publish", "publish", "user", false},
|
|
}
|
|
|
|
permissions := map[string]map[string]bool{
|
|
"admin": {
|
|
"submit_review": true,
|
|
"approve": false,
|
|
"reject": false,
|
|
"publish": true,
|
|
},
|
|
"data_protection_officer": {
|
|
"submit_review": true,
|
|
"approve": true,
|
|
"reject": true,
|
|
"publish": true,
|
|
},
|
|
"user": {
|
|
"submit_review": false,
|
|
"approve": false,
|
|
"reject": false,
|
|
"publish": false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
rolePerms, ok := permissions[tt.userRole]
|
|
if !ok {
|
|
t.Fatalf("Unknown role: %s", tt.userRole)
|
|
}
|
|
|
|
isAllowed := rolePerms[tt.action]
|
|
|
|
if isAllowed != tt.isAllowed {
|
|
t.Errorf("Role %s action %s: expected allowed=%v, got %v",
|
|
tt.userRole, tt.action, tt.isAllowed, isAllowed)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDocumentService_FourEyesPrinciple tests the four-eyes principle
|
|
func TestDocumentService_FourEyesPrinciple(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
createdBy uuid.UUID
|
|
approver uuid.UUID
|
|
approverRole string
|
|
canApprove bool
|
|
}{
|
|
{
|
|
name: "different users - DSB can approve",
|
|
createdBy: uuid.New(),
|
|
approver: uuid.New(),
|
|
approverRole: "data_protection_officer",
|
|
canApprove: true,
|
|
},
|
|
{
|
|
name: "same user - DSB cannot approve own",
|
|
createdBy: uuid.MustParse("123e4567-e89b-12d3-a456-426614174000"),
|
|
approver: uuid.MustParse("123e4567-e89b-12d3-a456-426614174000"),
|
|
approverRole: "data_protection_officer",
|
|
canApprove: false,
|
|
},
|
|
{
|
|
name: "same user - admin CAN approve own (exception)",
|
|
createdBy: uuid.MustParse("123e4567-e89b-12d3-a456-426614174000"),
|
|
approver: uuid.MustParse("123e4567-e89b-12d3-a456-426614174000"),
|
|
approverRole: "admin",
|
|
canApprove: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Four-eyes principle: DSB cannot approve their own work
|
|
// Exception: Admins can (for development/testing)
|
|
canApprove := tt.createdBy != tt.approver || tt.approverRole == "admin"
|
|
|
|
if canApprove != tt.canApprove {
|
|
t.Errorf("Expected canApprove=%v, got %v", tt.canApprove, canApprove)
|
|
}
|
|
})
|
|
}
|
|
}
|