Merged in feature/fix_test_timeouts (pull request #194)
text extractions tests * increase timeout for slow ci/cd systems * add tests
This commit is contained in:
+50
-13
@@ -22,13 +22,20 @@ func New(cfg serviceconfig.ConfigProvider) *Service {
|
||||
}
|
||||
}
|
||||
|
||||
// CreateFolder creates a new folder with validation
|
||||
// Validates that parent folder exists if parentId is provided
|
||||
// CreateFolder creates a new folder with validation.
|
||||
// Validates folder path constraints and that parent folder exists if parentId is provided.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: request context
|
||||
// - path: folder path (must pass all path constraints)
|
||||
// - parentID: optional parent folder UUID
|
||||
// - clientID: client identifier
|
||||
// - createdBy: email of user creating the folder
|
||||
//
|
||||
// Returns:
|
||||
// - *repository.Folder: the created folder
|
||||
// - error: validation error (PathValidationError) or database error
|
||||
func (s *Service) CreateFolder(ctx context.Context, path string, parentID *uuid.UUID, clientID string, createdBy string) (*repository.Folder, error) {
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("folder path cannot be empty")
|
||||
}
|
||||
|
||||
if clientID == "" {
|
||||
return nil, fmt.Errorf("clientID cannot be empty")
|
||||
}
|
||||
@@ -37,6 +44,23 @@ func (s *Service) CreateFolder(ctx context.Context, path string, parentID *uuid.
|
||||
return nil, fmt.Errorf("createdBy cannot be empty")
|
||||
}
|
||||
|
||||
// Check if root folder already exists for this client
|
||||
rootExists := false
|
||||
if path == "/" {
|
||||
_, err := s.cfg.GetDBQueries().GetFolderByPath(ctx, &repository.GetFolderByPathParams{
|
||||
Clientid: clientID,
|
||||
Path: "/",
|
||||
})
|
||||
if err == nil {
|
||||
rootExists = true
|
||||
}
|
||||
}
|
||||
|
||||
// Validate folder path constraints
|
||||
if err := ValidateFolderPathForCreate(path, rootExists); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate parent folder exists if parentID is provided
|
||||
if parentID != nil {
|
||||
parent, err := s.cfg.GetDBQueries().GetFolderByID(ctx, *parentID)
|
||||
@@ -86,18 +110,31 @@ func (s *Service) GetFolderByPath(ctx context.Context, clientID string, path str
|
||||
return folder, nil
|
||||
}
|
||||
|
||||
// RenameFolder renames a folder
|
||||
// RenameFolder renames a folder.
|
||||
// Validates folder path constraints and prevents renaming the root folder.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: request context
|
||||
// - folderID: UUID of the folder to rename
|
||||
// - newPath: new folder path (must pass all path constraints)
|
||||
//
|
||||
// Returns:
|
||||
// - error: validation error (PathValidationError) or database error
|
||||
func (s *Service) RenameFolder(ctx context.Context, folderID uuid.UUID, newPath string) error {
|
||||
if newPath == "" {
|
||||
return fmt.Errorf("new path cannot be empty")
|
||||
}
|
||||
|
||||
// Verify folder exists
|
||||
_, err := s.cfg.GetDBQueries().GetFolderByID(ctx, folderID)
|
||||
// Verify folder exists and get current path
|
||||
folder, err := s.cfg.GetDBQueries().GetFolderByID(ctx, folderID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("folder not found: %w", err)
|
||||
}
|
||||
|
||||
// Check if this is the root folder
|
||||
isRootFolder := folder.Path == "/"
|
||||
|
||||
// Validate folder path constraints for rename
|
||||
if err := ValidateFolderPathForRename(newPath, isRootFolder); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.cfg.GetDBQueries().RenameFolder(ctx, &repository.RenameFolderParams{
|
||||
ID: folderID,
|
||||
Path: newPath,
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestCreateFolder(t *testing.T) {
|
||||
t.Run("reject empty path", func(t *testing.T) {
|
||||
_, err := svc.CreateFolder(ctx, "", nil, clientID, "user123")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "path cannot be empty")
|
||||
assert.Contains(t, err.Error(), "must start with /")
|
||||
})
|
||||
|
||||
t.Run("reject empty clientID", func(t *testing.T) {
|
||||
@@ -182,7 +182,31 @@ func TestRenameFolder(t *testing.T) {
|
||||
t.Run("reject empty path", func(t *testing.T) {
|
||||
err := svc.RenameFolder(ctx, uuid.New(), "")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "path cannot be empty")
|
||||
// The folder won't be found, so we can't test path validation
|
||||
assert.Contains(t, err.Error(), "folder not found")
|
||||
})
|
||||
|
||||
t.Run("reject invalid path characters", func(t *testing.T) {
|
||||
path := "/test-rename-invalid"
|
||||
created, err := svc.CreateFolder(ctx, path, nil, clientID, "user123")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = svc.RenameFolder(ctx, created.ID, "/invalid@path")
|
||||
require.Error(t, err)
|
||||
assert.True(t, folder.IsPathValidationError(err))
|
||||
})
|
||||
|
||||
t.Run("reject root folder rename", func(t *testing.T) {
|
||||
// Create root folder
|
||||
rootPath := "/"
|
||||
rootFolder, err := svc.CreateFolder(ctx, rootPath, nil, clientID, "user123")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to rename root folder
|
||||
err = svc.RenameFolder(ctx, rootFolder.ID, "/renamed-root")
|
||||
require.Error(t, err)
|
||||
assert.True(t, folder.IsPathValidationError(err))
|
||||
assert.Contains(t, err.Error(), "root folder cannot be renamed")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
package folder
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
// PathValidationError represents a folder path constraint violation.
|
||||
// It wraps the underlying constraint error with context about which constraint was violated.
|
||||
type PathValidationError struct {
|
||||
Constraint string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *PathValidationError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// IsPathValidationError checks if the error is a PathValidationError
|
||||
func IsPathValidationError(err error) bool {
|
||||
var pve *PathValidationError
|
||||
return errors.As(err, &pve)
|
||||
}
|
||||
|
||||
// Constants for validation limits
|
||||
const (
|
||||
MaxSegmentLength = 255
|
||||
MaxPathDepth = 20
|
||||
MaxPathLength = 1000
|
||||
)
|
||||
|
||||
// validPathCharRegex matches valid characters: alphanumeric, hyphen, underscore, period, space
|
||||
// This is applied per-segment after splitting by /
|
||||
var validSegmentCharRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._\- ]*$`)
|
||||
|
||||
// windowsReservedNames are device names reserved on Windows (case-insensitive)
|
||||
var windowsReservedNames = map[string]bool{
|
||||
"con": true, "prn": true, "aux": true, "nul": true,
|
||||
"com1": true, "com2": true, "com3": true, "com4": true, "com5": true,
|
||||
"com6": true, "com7": true, "com8": true, "com9": true,
|
||||
"lpt1": true, "lpt2": true, "lpt3": true, "lpt4": true, "lpt5": true,
|
||||
"lpt6": true, "lpt7": true, "lpt8": true, "lpt9": true,
|
||||
}
|
||||
|
||||
// ValidateFolderPath validates a folder path against all constraints.
|
||||
// This function should be called for both folder creation and rename operations.
|
||||
//
|
||||
// Constraints validated:
|
||||
// - Path must start with /
|
||||
// - Path cannot exceed 1000 characters
|
||||
// - No empty segments (double slashes)
|
||||
// - No path traversal sequences (.., ./)
|
||||
// - No control characters
|
||||
// - Valid characters only (alphanumeric, hyphen, underscore, period, space)
|
||||
// - No leading/trailing whitespace in segments
|
||||
// - No dot-prefix segments (hidden files)
|
||||
// - No Windows reserved names
|
||||
// - Segment length <= 255 characters
|
||||
// - Path depth <= 20 levels
|
||||
// - Unicode NFC normalization applied
|
||||
//
|
||||
// Parameters:
|
||||
// - path: the folder path to validate
|
||||
//
|
||||
// Returns:
|
||||
// - error: nil if valid, PathValidationError if constraint violated
|
||||
func ValidateFolderPath(path string) error {
|
||||
// Normalize Unicode to NFC form
|
||||
path = norm.NFC.String(path)
|
||||
|
||||
// Check for control characters first (before any other validation)
|
||||
if err := validateNoControlCharacters(path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Must start with /
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
return &PathValidationError{
|
||||
Constraint: "start_with_slash",
|
||||
Message: "folder path must start with /",
|
||||
}
|
||||
}
|
||||
|
||||
// Check max path length
|
||||
if len(path) > MaxPathLength {
|
||||
return &PathValidationError{
|
||||
Constraint: "max_length",
|
||||
Message: fmt.Sprintf("folder path exceeds maximum length of %d characters", MaxPathLength),
|
||||
}
|
||||
}
|
||||
|
||||
// Special case: root path "/" is always valid
|
||||
if path == "/" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for path traversal sequences
|
||||
if err := validateNoPathTraversal(path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check for empty segments (double slashes or trailing slash)
|
||||
if err := validateNoEmptySegments(path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Split into segments and validate each
|
||||
segments := strings.Split(path[1:], "/") // Remove leading / before split
|
||||
|
||||
// Check path depth
|
||||
if len(segments) > MaxPathDepth {
|
||||
return &PathValidationError{
|
||||
Constraint: "max_depth",
|
||||
Message: fmt.Sprintf("folder path exceeds maximum depth of %d levels", MaxPathDepth),
|
||||
}
|
||||
}
|
||||
|
||||
for _, segment := range segments {
|
||||
if err := validateSegment(segment); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateNoControlCharacters checks that the path contains no ASCII control characters (0x00-0x1F, 0x7F)
|
||||
func validateNoControlCharacters(path string) error {
|
||||
for _, r := range path {
|
||||
if r < 0x20 || r == 0x7F {
|
||||
return &PathValidationError{
|
||||
Constraint: "control_characters",
|
||||
Message: "folder path cannot contain control characters",
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateNoPathTraversal checks for .. and ./ sequences
|
||||
func validateNoPathTraversal(path string) error {
|
||||
// Check for .. anywhere in path
|
||||
if strings.Contains(path, "..") {
|
||||
return &PathValidationError{
|
||||
Constraint: "path_traversal",
|
||||
Message: "folder path cannot contain '..' sequences",
|
||||
}
|
||||
}
|
||||
|
||||
// Check for ./ sequence
|
||||
if strings.Contains(path, "./") {
|
||||
return &PathValidationError{
|
||||
Constraint: "path_traversal",
|
||||
Message: "folder path cannot contain './' sequences",
|
||||
}
|
||||
}
|
||||
|
||||
// Check for /. at end of path
|
||||
if strings.HasSuffix(path, "/.") {
|
||||
return &PathValidationError{
|
||||
Constraint: "path_traversal",
|
||||
Message: "folder path cannot end with '/.'",
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateNoEmptySegments checks for empty path segments
|
||||
func validateNoEmptySegments(path string) error {
|
||||
// Check for double slashes
|
||||
if strings.Contains(path, "//") {
|
||||
return &PathValidationError{
|
||||
Constraint: "empty_segment",
|
||||
Message: "folder path cannot contain empty segments (double slashes)",
|
||||
}
|
||||
}
|
||||
|
||||
// Check for trailing slash (creates empty segment)
|
||||
if strings.HasSuffix(path, "/") {
|
||||
return &PathValidationError{
|
||||
Constraint: "empty_segment",
|
||||
Message: "folder path cannot end with a trailing slash",
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateSegment validates a single path segment
|
||||
func validateSegment(segment string) error {
|
||||
// Check segment length
|
||||
if len(segment) > MaxSegmentLength {
|
||||
return &PathValidationError{
|
||||
Constraint: "segment_length",
|
||||
Message: fmt.Sprintf("folder path segment '%s...' exceeds maximum length of %d characters", segment[:50], MaxSegmentLength),
|
||||
}
|
||||
}
|
||||
|
||||
// Check for empty segment (shouldn't happen if validateNoEmptySegments passed, but defensive)
|
||||
if segment == "" {
|
||||
return &PathValidationError{
|
||||
Constraint: "empty_segment",
|
||||
Message: "folder path cannot contain empty segments",
|
||||
}
|
||||
}
|
||||
|
||||
// Check for leading/trailing whitespace
|
||||
if segment != strings.TrimSpace(segment) {
|
||||
return &PathValidationError{
|
||||
Constraint: "whitespace",
|
||||
Message: "folder path segments cannot have leading or trailing spaces",
|
||||
}
|
||||
}
|
||||
|
||||
// Check for dot prefix (hidden files)
|
||||
if strings.HasPrefix(segment, ".") {
|
||||
return &PathValidationError{
|
||||
Constraint: "hidden_folder",
|
||||
Message: "folder path segments cannot start with a period",
|
||||
}
|
||||
}
|
||||
|
||||
// Check for valid characters
|
||||
if !validSegmentCharRegex.MatchString(segment) {
|
||||
// Provide more helpful error message by identifying invalid character
|
||||
invalidChar := findInvalidCharacter(segment)
|
||||
return &PathValidationError{
|
||||
Constraint: "invalid_characters",
|
||||
Message: fmt.Sprintf("folder path contains invalid character '%s'; only alphanumeric, hyphen, underscore, period, and space are allowed", invalidChar),
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Windows reserved names
|
||||
segmentLower := strings.ToLower(segment)
|
||||
if windowsReservedNames[segmentLower] {
|
||||
return &PathValidationError{
|
||||
Constraint: "reserved_name",
|
||||
Message: fmt.Sprintf("folder path cannot use reserved name: %s", segment),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findInvalidCharacter returns the first invalid character in a segment for error reporting
|
||||
func findInvalidCharacter(segment string) string {
|
||||
for i, r := range segment {
|
||||
// First character must be alphanumeric
|
||||
if i == 0 {
|
||||
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
|
||||
return string(r)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Subsequent characters can be alphanumeric, hyphen, underscore, period, or space
|
||||
if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '-' && r != '_' && r != '.' && r != ' ' {
|
||||
return string(r)
|
||||
}
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
|
||||
// ValidateFolderPathForRename validates a folder path for rename operations.
|
||||
// This includes all standard path validation plus additional rename-specific checks.
|
||||
//
|
||||
// Parameters:
|
||||
// - newPath: the new folder path
|
||||
// - isRootFolder: true if the folder being renamed is the root folder
|
||||
//
|
||||
// Returns:
|
||||
// - error: nil if valid, PathValidationError if constraint violated
|
||||
func ValidateFolderPathForRename(newPath string, isRootFolder bool) error {
|
||||
// Root folder cannot be renamed
|
||||
if isRootFolder {
|
||||
return &PathValidationError{
|
||||
Constraint: "root_rename",
|
||||
Message: "root folder cannot be renamed",
|
||||
}
|
||||
}
|
||||
|
||||
// Cannot rename to root
|
||||
if newPath == "/" {
|
||||
return &PathValidationError{
|
||||
Constraint: "rename_to_root",
|
||||
Message: "cannot rename folder to root path",
|
||||
}
|
||||
}
|
||||
|
||||
// Apply standard path validation
|
||||
return ValidateFolderPath(newPath)
|
||||
}
|
||||
|
||||
// ValidateFolderPathForCreate validates a folder path for create operations.
|
||||
// This includes all standard path validation plus creation-specific checks.
|
||||
//
|
||||
// Parameters:
|
||||
// - path: the folder path to create
|
||||
// - rootExists: true if a root folder already exists for the client
|
||||
//
|
||||
// Returns:
|
||||
// - error: nil if valid, PathValidationError if constraint violated
|
||||
func ValidateFolderPathForCreate(path string, rootExists bool) error {
|
||||
// Check if trying to create root when one already exists
|
||||
if path == "/" && rootExists {
|
||||
return &PathValidationError{
|
||||
Constraint: "root_exists",
|
||||
Message: "root folder already exists for this client",
|
||||
}
|
||||
}
|
||||
|
||||
// Apply standard path validation
|
||||
return ValidateFolderPath(path)
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
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<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",
|
||||
"/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))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user