package service import ( "context" "fmt" "git.casaderoll.de/michael/urbm/internal/notify" "git.casaderoll.de/michael/urbm/internal/platform" "git.casaderoll.de/michael/urbm/internal/queue" "git.casaderoll.de/michael/urbm/internal/restic" "log/slog" "os" "path/filepath" "strings" "testing" "time" "git.casaderoll.de/michael/urbm/internal/model" ) func TestNotificationMessageIncludesBackupStatistics(t *testing.T) { started := time.Date(2026, 6, 14, 12, 0, 0, 0, time.UTC) finished := started.Add(2*time.Minute + 8*time.Second) run := model.Run{TaskType: "backup", Status: "success", StartedAt: &started, FinishedAt: &finished, SnapshotID: "abc123456789abcdef", BytesAdded: 1536, FilesNew: 7, FilesChanged: 2, BytesProcessed: 5 * 1024 * 1024, FilesProcessed: 120, RetentionRemoved: 3, RetentionAfter: 14} message := notificationMessage("Freigaben", "LV-426", run, finished) for _, expected := range []string{"πŸ“Š Backup-Zusammenfassung", "πŸ“¦ Repository: LV-426", "πŸ—‚οΈ Job: Freigaben", "πŸ“Έ Snapshot: abc123456789", "⏱️ Dauer: 2 Min. 8 Sek.", "πŸ“‚ Dateien: 120 verarbeitet", "πŸ’Ύ Verarbeitet: 5.0 MiB", "βž• Neu: 7 Dateien", "✏️ GeΓ€ndert: 2 Dateien", "πŸ“€ Neu gespeichert: 1.5 KiB", "🧹 Aufbewahrung: 3 Snapshot(s) entfernt", "πŸ“Έ Verbleibende Snapshots: 14", "πŸ” Ziel: Restic Repository", "βœ… Status: Erfolgreich"} { if !strings.Contains(message, expected) { t.Errorf("message %q does not contain %q", message, expected) } } } func TestNotificationTitleIncludesRepositoryAndStatus(t *testing.T) { run := model.Run{TaskType: "backup", Status: "failed"} title := notificationTitle("Freigaben", "LV-426", run) if title != "❌ URBM Backup: LV-426" { t.Fatalf("title = %q", title) } } func TestNotificationMessageIncludesPruneSummary(t *testing.T) { started := time.Date(2026, 6, 22, 3, 0, 0, 0, time.UTC) finished := started.Add(42 * time.Second) run := model.Run{TaskType: "prune", Status: "success", StartedAt: &started, FinishedAt: &finished, Message: "prune completed", RepositoryBefore: 100 * 1024 * 1024, RepositoryAfter: 75 * 1024 * 1024, RepositoryFreed: 25 * 1024 * 1024} message := notificationMessage("Repository-Bereinigung", "LV-426", run, finished) for _, expected := range []string{"🧹 Prune-Zusammenfassung", "πŸ“¦ Repository: LV-426", "πŸ—‚οΈ Job: Repository-Bereinigung", "⏱️ Dauer: 42 Sekunden", "πŸ“¦ Vorher: 100.0 MiB", "πŸ“¦ Nachher: 75.0 MiB", "🧹 Freigegeben: 25.0 MiB", "πŸ” Ziel: Restic Repository", "βœ… Status: Erfolgreich"} { if !strings.Contains(message, expected) { t.Errorf("message %q does not contain %q", message, expected) } } if strings.Contains(message, "⚠️ Details: prune completed") { t.Fatalf("message contains redundant prune completion details: %q", message) } } type checkSecrets struct{} func (checkSecrets) Get(string) (string, error) { return "test", nil } func TestCheckSendsSuccessAndFailureNotifications(t *testing.T) { for _, code := range []int{0, 1} { t.Run(fmt.Sprint(code), func(t *testing.T) { dir := t.TempDir() binary := filepath.Join(dir, "restic") if err := os.WriteFile(binary, []byte(fmt.Sprintf("#!/bin/sh\necho check-result >&2\nexit %d\n", code)), 0700); err != nil { t.Fatal(err) } var sent []string s := &Service{ config: model.Config{Repositories: []model.Repository{{ID: "repo", Name: "test", Type: model.RepositoryLocal, Location: dir}}, Notifications: []model.NotificationTarget{{Type: "unraid", Enabled: true, Events: []string{"success", "failed"}}}}, repositoryLocks: map[string]chan struct{}{}, restic: &restic.Runner{Binary: binary, RuntimeDir: dir, Secrets: checkSecrets{}}, mounts: &platform.MountManager{}, queue: queue.New(nil, nil), log: slog.Default(), notifier: ¬ify.Sender{RunCommand: func(_ context.Context, _ string, args ...string) error { sent = append(sent, strings.Join(args, " ")) return nil }}, } result := s.execute(context.Background(), model.Run{ID: "run", JobID: "repo", TaskType: "check"}) want := "success" if code != 0 { want = "failed" } if result.Status != want || len(sent) != 1 { t.Fatalf("status=%s messages=%v", result.Status, sent) } if !strings.Contains(sent[0], "URBM") { t.Fatal(sent) } }) } }