package folder import ( "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestValidateFolderPath_ValidPaths(t *testing.T) { validPaths := []string{ "/", "/foo", "/foo/bar", "/foo/bar/baz", "/my-folder", "/my_folder", "/my folder", "/My Folder", "/folder.name", "/2024", "/folder123", "/a", "/A", "/foo bar/baz qux", "/documents/2024/Q1", "/client-data/reports_2024/monthly.reports", } for _, path := range validPaths { t.Run(path, func(t *testing.T) { err := ValidateFolderPath(path) assert.NoError(t, err, "path should be valid: %s", path) }) } } func TestValidateFolderPath_MustStartWithSlash(t *testing.T) { invalidPaths := []string{ "foo", "foo/bar", "./foo", "../foo", " /foo", } for _, path := range invalidPaths { t.Run(path, func(t *testing.T) { err := ValidateFolderPath(path) require.Error(t, err) var pve *PathValidationError require.ErrorAs(t, err, &pve) assert.Equal(t, "start_with_slash", pve.Constraint) }) } } func TestValidateFolderPath_PathTraversal(t *testing.T) { tests := []struct { path string wantErr bool }{ {"/foo/bar", false}, {"/foo/../bar", true}, {"/../etc/passwd", true}, {"/foo/./bar", true}, {"/foo/..", true}, {"/foo/bar/..", true}, {"/foo/.", true}, {"/..foo", true}, // starts with dot, blocked by hidden folder rule {"/foo..bar", true}, // contains .. {"/foo...bar", true}, // contains .. {"/foo/bar..baz", true}, // contains .. } for _, tt := range tests { t.Run(tt.path, func(t *testing.T) { err := ValidateFolderPath(tt.path) if tt.wantErr { require.Error(t, err, "path should be invalid: %s", tt.path) } else { assert.NoError(t, err, "path should be valid: %s", tt.path) } }) } } func TestValidateFolderPath_EmptySegments(t *testing.T) { invalidPaths := []string{ "//foo", "/foo//bar", "/foo/bar//", "/foo/", "/foo/bar/", "///", } for _, path := range invalidPaths { t.Run(path, func(t *testing.T) { err := ValidateFolderPath(path) require.Error(t, err, "path should be invalid: %s", path) var pve *PathValidationError require.ErrorAs(t, err, &pve) assert.Equal(t, "empty_segment", pve.Constraint) }) } } func TestValidateFolderPath_LeadingTrailingWhitespace(t *testing.T) { invalidPaths := []string{ "/ foo", "/foo ", "/ foo ", "/foo/ bar", "/foo /bar", "/ foo", "/foo ", } for _, path := range invalidPaths { t.Run(path, func(t *testing.T) { err := ValidateFolderPath(path) require.Error(t, err, "path should be invalid: %s", path) var pve *PathValidationError require.ErrorAs(t, err, &pve) assert.Equal(t, "whitespace", pve.Constraint) }) } } func TestValidateFolderPath_HiddenFolders(t *testing.T) { invalidPaths := []string{ "/.hidden", "/foo/.secret", "/.git", "/foo/.config/bar", } for _, path := range invalidPaths { t.Run(path, func(t *testing.T) { err := ValidateFolderPath(path) require.Error(t, err, "path should be invalid: %s", path) var pve *PathValidationError require.ErrorAs(t, err, &pve) // Could be hidden_folder or path_traversal depending on the case assert.Contains(t, []string{"hidden_folder", "path_traversal"}, pve.Constraint) }) } } func TestValidateFolderPath_InvalidCharacters(t *testing.T) { invalidPaths := []string{ "/foo@bar", "/foo#bar", "/foo\\bar", "/foo", "/foo\"bar", "/foo'bar", "/foo:bar", "/foo*bar", "/foo?bar", "/foo|bar", "/foo`bar", "/foo~bar", "/foo!bar", "/foo$bar", "/foo%bar", "/foo^bar", "/foo&bar", "/foo+bar", "/foo=bar", "/foo[bar]", "/foo{bar}", "/foo;bar", "/foo,bar", } for _, path := range invalidPaths { t.Run(path, func(t *testing.T) { err := ValidateFolderPath(path) require.Error(t, err, "path should be invalid: %s", path) var pve *PathValidationError require.ErrorAs(t, err, &pve) assert.Equal(t, "invalid_characters", pve.Constraint) }) } } func TestValidateFolderPath_WindowsReservedNames(t *testing.T) { reservedNames := []string{ "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "con", "prn", "aux", "nul", // lowercase "Con", "Prn", "Aux", "Nul", // mixed case } for _, name := range reservedNames { t.Run(name, func(t *testing.T) { // Test as root segment err := ValidateFolderPath("/" + name) require.Error(t, err, "reserved name should be invalid: /%s", name) var pve *PathValidationError require.ErrorAs(t, err, &pve) assert.Equal(t, "reserved_name", pve.Constraint) // Test as nested segment err = ValidateFolderPath("/foo/" + name + "/bar") require.Error(t, err, "reserved name in path should be invalid: /foo/%s/bar", name) require.ErrorAs(t, err, &pve) assert.Equal(t, "reserved_name", pve.Constraint) }) } } func TestValidateFolderPath_SegmentLength(t *testing.T) { // Valid: exactly 255 characters validSegment := strings.Repeat("a", 255) err := ValidateFolderPath("/" + validSegment) assert.NoError(t, err, "255 character segment should be valid") // Invalid: 256 characters invalidSegment := strings.Repeat("a", 256) err = ValidateFolderPath("/" + invalidSegment) require.Error(t, err) var pve *PathValidationError require.ErrorAs(t, err, &pve) assert.Equal(t, "segment_length", pve.Constraint) } func TestValidateFolderPath_PathDepth(t *testing.T) { // Valid: exactly 20 levels segments := make([]string, 20) for i := range segments { segments[i] = "a" } validPath := "/" + strings.Join(segments, "/") err := ValidateFolderPath(validPath) assert.NoError(t, err, "20 level path should be valid") // Invalid: 21 levels segments = make([]string, 21) for i := range segments { segments[i] = "a" } invalidPath := "/" + strings.Join(segments, "/") err = ValidateFolderPath(invalidPath) require.Error(t, err) var pve *PathValidationError require.ErrorAs(t, err, &pve) assert.Equal(t, "max_depth", pve.Constraint) } func TestValidateFolderPath_MaxLength(t *testing.T) { // Valid: exactly 1000 characters using multiple segments (each <=255 chars) // 4 segments of 249 chars each = /249/249/249/249 = 1 + 249 + 1 + 249 + 1 + 249 + 1 + 249 = 1000 seg249 := strings.Repeat("a", 249) validPath := "/" + seg249 + "/" + seg249 + "/" + seg249 + "/" + seg249 assert.Equal(t, 1000, len(validPath), "sanity check: path should be 1000 chars") err := ValidateFolderPath(validPath) assert.NoError(t, err, "1000 character path should be valid") // Invalid: 1001 characters invalidPath := validPath + "a" err = ValidateFolderPath(invalidPath) require.Error(t, err) var pve *PathValidationError require.ErrorAs(t, err, &pve) assert.Equal(t, "max_length", pve.Constraint) } func TestValidateFolderPath_ControlCharacters(t *testing.T) { controlChars := []rune{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x7F, } for _, char := range controlChars { t.Run(string(char), func(t *testing.T) { path := "/foo" + string(char) + "bar" err := ValidateFolderPath(path) require.Error(t, err, "control character 0x%02X should be invalid", char) var pve *PathValidationError require.ErrorAs(t, err, &pve) assert.Equal(t, "control_characters", pve.Constraint) }) } } func TestValidateFolderPath_SegmentMustStartWithAlphanumeric(t *testing.T) { invalidPaths := []string{ "/-foo", "/_foo", "/ foo", // leading space "/foo/-bar", "/foo/_bar", } for _, path := range invalidPaths { t.Run(path, func(t *testing.T) { err := ValidateFolderPath(path) require.Error(t, err, "path should be invalid: %s", path) }) } } func TestValidateFolderPathForRename(t *testing.T) { t.Run("root folder cannot be renamed", func(t *testing.T) { err := ValidateFolderPathForRename("/new-name", true) require.Error(t, err) var pve *PathValidationError require.ErrorAs(t, err, &pve) assert.Equal(t, "root_rename", pve.Constraint) }) t.Run("cannot rename to root", func(t *testing.T) { err := ValidateFolderPathForRename("/", false) require.Error(t, err) var pve *PathValidationError require.ErrorAs(t, err, &pve) assert.Equal(t, "rename_to_root", pve.Constraint) }) t.Run("valid rename", func(t *testing.T) { err := ValidateFolderPathForRename("/new-folder", false) assert.NoError(t, err) }) } func TestValidateFolderPathForCreate(t *testing.T) { t.Run("cannot create root when exists", func(t *testing.T) { err := ValidateFolderPathForCreate("/", true) require.Error(t, err) var pve *PathValidationError require.ErrorAs(t, err, &pve) assert.Equal(t, "root_exists", pve.Constraint) }) t.Run("can create root when not exists", func(t *testing.T) { err := ValidateFolderPathForCreate("/", false) assert.NoError(t, err) }) t.Run("valid create", func(t *testing.T) { err := ValidateFolderPathForCreate("/new-folder", true) assert.NoError(t, err) }) } func TestIsPathValidationError(t *testing.T) { t.Run("PathValidationError returns true", func(t *testing.T) { err := &PathValidationError{Constraint: "test", Message: "test message"} assert.True(t, IsPathValidationError(err)) }) t.Run("other error returns false", func(t *testing.T) { err := assert.AnError assert.False(t, IsPathValidationError(err)) }) t.Run("nil returns false", func(t *testing.T) { assert.False(t, IsPathValidationError(nil)) }) }