package model import "testing" func TestValidateConfig(t *testing.T) { c := DefaultConfig() if c.Settings.ShowLiveLog { t.Fatal("live log must be hidden by default") } c.Repositories = append(c.Repositories, Repository{SchemaVersion: 1, ID: "repo", Name: "Local", Type: RepositoryLocal, Location: "/tmp/repo", PasswordRef: "password"}) c.Jobs = append(c.Jobs, Job{SchemaVersion: 1, ID: "job", Name: "Share", Type: JobShare, Enabled: true, RepositoryID: "repo", Sources: []Source{{Path: "/mnt/user/data"}}, Schedule: Schedule{Cron: "0 2 * * *"}, Consistency: Consistency{Mode: "live"}, Compression: "auto", Retention: Retention{Daily: 7, Weekly: 4, Monthly: 12}, ShutdownSecs: 120}) if err := ValidateConfig(c); err != nil { t.Fatalf("valid config rejected: %v", err) } c.Jobs[0].Sources[0].Path = "relative" if err := ValidateConfig(c); err == nil { t.Fatal("relative source path accepted") } } func TestNormalizeConfigReplacesNtfyAndAddsUnraid(t *testing.T) { c := DefaultConfig() c.Notifications = []NotificationTarget{{SchemaVersion: 1, ID: "old-ntfy", Name: "ntfy", Type: "ntfy", Enabled: true}} c = NormalizeConfig(c) if len(c.Notifications) != 1 || c.Notifications[0].Type != "unraid" || !c.Notifications[0].Enabled { t.Fatalf("notifications = %#v", c.Notifications) } } func TestValidateGotifyNotification(t *testing.T) { target := NotificationTarget{SchemaVersion: 1, ID: "gotify", Name: "Gotify", Type: "gotify", Enabled: true, URL: "https://gotify.example.test", TokenRef: "gotify-token", Events: []string{"failed"}} if err := ValidateNotification(target); err != nil { t.Fatal(err) } target.URL = "javascript:alert(1)" if err := ValidateNotification(target); err == nil { t.Fatal("unsafe Gotify URL accepted") } } func TestRepositoryIsolation(t *testing.T) { c := DefaultConfig() c.Jobs = []Job{{SchemaVersion: 1, ID: "job", Name: "Broken", Type: JobFlash, RepositoryID: "missing", Consistency: Consistency{Mode: "live"}, Compression: "auto"}} if err := ValidateConfig(c); err == nil { t.Fatal("unknown repository reference accepted") } } func TestRepositoryCanBeSharedByJobs(t *testing.T) { c := DefaultConfig() c.Repositories = []Repository{{SchemaVersion: 1, ID: "repo", Name: "Local", Type: RepositoryLocal, Location: "/tmp/repo", PasswordRef: "password"}} base := Job{SchemaVersion: 1, Name: "Flash", Type: JobFlash, RepositoryID: "repo", Consistency: Consistency{Mode: "live"}, Compression: "auto", Retention: Retention{KeepWithinDays: 30}} first, second := base, base first.ID, second.ID = "first", "second" c.Jobs = []Job{first, second} if err := ValidateConfig(c); err != nil { t.Fatalf("shared repository rejected: %v", err) } } func TestDockerJobRequiresWorkloadSources(t *testing.T) { job := Job{SchemaVersion: 1, ID: "docker-job", Name: "Docker", Type: JobDocker, RepositoryID: "repo", Sources: []Source{{Path: "/mnt/user/appdata"}}, Consistency: Consistency{Mode: "live"}, Compression: "auto", Retention: Retention{KeepWithinDays: 30}} if err := ValidateJob(job); err == nil { t.Fatal("docker path source accepted without workload selection") } job.Sources = []Source{{WorkloadID: "plex"}} if err := ValidateJob(job); err != nil { t.Fatalf("valid docker workload rejected: %v", err) } } func TestRetentionRequiresAtLeastOneRule(t *testing.T) { job := Job{SchemaVersion: 1, ID: "job", Name: "Share", Type: JobShare, RepositoryID: "repo", Sources: []Source{{Path: "/mnt/user/data"}}, Consistency: Consistency{Mode: "live"}, Compression: "auto"} if err := ValidateJob(job); err == nil { t.Fatal("empty retention policy accepted") } job.Retention.KeepWithinDays = 30 if err := ValidateJob(job); err != nil { t.Fatalf("age retention rejected: %v", err) } } func TestBackupCPUCoresRange(t *testing.T) { job := Job{SchemaVersion: 1, ID: "job", Name: "Share", Type: JobShare, RepositoryID: "repo", Sources: []Source{{Path: "/mnt/user/data"}}, Consistency: Consistency{Mode: "live"}, Compression: "auto", Retention: Retention{KeepWithinDays: 30}} job.CPUCores = 2 if err := ValidateJob(job); err != nil { t.Fatalf("valid CPU limit rejected: %v", err) } job.CPUCores = 257 if err := ValidateJob(job); err == nil { t.Fatal("excessive CPU limit accepted") } }