Files

157 lines
6.9 KiB
Go

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")
}
}
func TestBackupReadConcurrencyRange(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.ReadConcurrency = 8
if err := ValidateJob(job); err != nil {
t.Fatalf("valid read concurrency rejected: %v", err)
}
job.ReadConcurrency = 65
if err := ValidateJob(job); err == nil {
t.Fatal("excessive read concurrency accepted")
}
}
func TestRsyncJobDoesNotRequireRepository(t *testing.T) {
c := DefaultConfig()
c.Jobs = []Job{{SchemaVersion: 1, ID: "rsync-job", Name: "Mirror", Type: JobRsync, Enabled: true, Sources: []Source{{Path: "/mnt/user/data"}}, Rsync: RsyncOptions{Target: "/mnt/remotes/backup", Overwrite: true}}}
if err := ValidateConfig(c); err != nil {
t.Fatalf("valid rsync job rejected: %v", err)
}
}
func TestRsyncJobRejectsOverlappingPaths(t *testing.T) {
job := Job{SchemaVersion: 1, ID: "rsync-job", Name: "Mirror", Type: JobRsync, Sources: []Source{{Path: "/mnt/user/data"}}, Rsync: RsyncOptions{Target: "/mnt/user/data/copy"}}
if err := ValidateJob(job); err == nil {
t.Fatal("rsync target inside source accepted")
}
}
func TestValidateRestoreTaskRejectsDelimiterInjection(t *testing.T) {
task := RestoreTask{RepositoryID: "repo", SnapshotID: "abc123\x00/", Target: "/mnt/user/restore", Includes: []string{"/mnt/user/data"}}
if err := ValidateRestoreTask(task); err == nil {
t.Fatal("NUL-delimited snapshot injection accepted")
}
task.SnapshotID = "abc123"
task.InPlace = true
task.Includes = nil
if err := ValidateRestoreTask(task); err == nil {
t.Fatal("in-place restore without an include accepted")
}
}
func TestValidateConfigRejectsDuplicateManagedMountPoint(t *testing.T) {
c := DefaultConfig()
c.Repositories = []Repository{
{SchemaVersion: 1, ID: "one", Name: "One", Type: RepositorySMB, Location: "/mnt/remotes/shared", PasswordRef: "one-password", Mount: &MountConfig{Managed: true, Remote: "//one/share", MountPoint: "/mnt/remotes/shared"}},
{SchemaVersion: 1, ID: "two", Name: "Two", Type: RepositoryNFS, Location: "/mnt/remotes/shared", PasswordRef: "two-password", Mount: &MountConfig{Managed: true, Remote: "two:/share", MountPoint: "/mnt/remotes/shared"}},
}
if err := ValidateConfig(c); err == nil {
t.Fatal("duplicate managed mount point accepted")
}
}
func TestValidateConfigRejectsUnsafeRestoreRoot(t *testing.T) {
c := DefaultConfig()
c.Settings.RestoreRoot = "/etc/urbm-restores"
if err := ValidateConfig(c); err == nil {
t.Fatal("restoreRoot outside Unraid storage roots accepted")
}
c.Settings.RestoreRoot = "/mnt/user/urbm-restores"
if err := ValidateConfig(c); err != nil {
t.Fatalf("safe restoreRoot rejected: %v", err)
}
}