mirror of
https://github.com/ivuorinen/gibidify.git
synced 2026-01-26 03:24:05 +00:00
* chore(ci): update go to 1.25, add permissions and envs * fix(ci): update pr-lint.yml * chore: update go, fix linting * fix: tests and linting * fix(lint): lint fixes, renovate should now pass * fix: updates, security upgrades * chore: workflow updates, lint * fix: more lint, checkmake, and other fixes * fix: more lint, convert scripts to POSIX compliant * fix: simplify codeql workflow * tests: increase test coverage, fix found issues * fix(lint): editorconfig checking, add to linters * fix(lint): shellcheck, add to linters * fix(lint): apply cr comment suggestions * fix(ci): remove step-security/harden-runner * fix(lint): remove duplication, apply cr fixes * fix(ci): tests in CI/CD pipeline * chore(lint): deduplication of strings * fix(lint): apply cr comment suggestions * fix(ci): actionlint * fix(lint): apply cr comment suggestions * chore: lint, add deps management
369 lines
8.5 KiB
Go
369 lines
8.5 KiB
Go
package gibidiutils
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestGetBaseName(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
absPath string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "normal path",
|
|
absPath: "/home/user/project",
|
|
expected: "project",
|
|
},
|
|
{
|
|
name: "path with trailing slash",
|
|
absPath: "/home/user/project/",
|
|
expected: "project",
|
|
},
|
|
{
|
|
name: "root path",
|
|
absPath: "/",
|
|
expected: "/",
|
|
},
|
|
{
|
|
name: "current directory",
|
|
absPath: ".",
|
|
expected: "output",
|
|
},
|
|
{
|
|
name: testEmptyPath,
|
|
absPath: "",
|
|
expected: "output",
|
|
},
|
|
{
|
|
name: "file path",
|
|
absPath: "/home/user/file.txt",
|
|
expected: "file.txt",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := GetBaseName(tt.absPath)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateSourcePath(t *testing.T) {
|
|
// Create a temp directory for testing
|
|
tempDir := t.TempDir()
|
|
tempFile := filepath.Join(tempDir, "test.txt")
|
|
require.NoError(t, os.WriteFile(tempFile, []byte("test"), 0o600))
|
|
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: testEmptyPath,
|
|
path: "",
|
|
expectedError: "source path is required",
|
|
},
|
|
{
|
|
name: testPathTraversalAttempt,
|
|
path: "../../../etc/passwd",
|
|
expectedError: testPathTraversalDetected,
|
|
},
|
|
{
|
|
name: "path with double dots",
|
|
path: "/home/../etc/passwd",
|
|
expectedError: testPathTraversalDetected,
|
|
},
|
|
{
|
|
name: "non-existent path",
|
|
path: "/definitely/does/not/exist",
|
|
expectedError: "does not exist",
|
|
},
|
|
{
|
|
name: "file instead of directory",
|
|
path: tempFile,
|
|
expectedError: "must be a directory",
|
|
},
|
|
{
|
|
name: "valid directory",
|
|
path: tempDir,
|
|
expectedError: "",
|
|
},
|
|
{
|
|
name: "valid relative path",
|
|
path: ".",
|
|
expectedError: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := ValidateSourcePath(tt.path)
|
|
|
|
if tt.expectedError != "" {
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.expectedError)
|
|
|
|
// Check if it's a StructuredError
|
|
var structErr *StructuredError
|
|
if errors.As(err, &structErr) {
|
|
assert.NotEmpty(t, structErr.Code)
|
|
assert.NotEqual(t, ErrorTypeUnknown, structErr.Type)
|
|
}
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateDestinationPath(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: testEmptyPath,
|
|
path: "",
|
|
expectedError: "destination path is required",
|
|
},
|
|
{
|
|
name: testPathTraversalAttempt,
|
|
path: "../../etc/passwd",
|
|
expectedError: testPathTraversalDetected,
|
|
},
|
|
{
|
|
name: "absolute path traversal",
|
|
path: "/home/../../../etc/passwd",
|
|
expectedError: testPathTraversalDetected,
|
|
},
|
|
{
|
|
name: "valid new file",
|
|
path: filepath.Join(tempDir, "newfile.txt"),
|
|
expectedError: "",
|
|
},
|
|
{
|
|
name: "valid relative path",
|
|
path: "output.txt",
|
|
expectedError: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := ValidateDestinationPath(tt.path)
|
|
|
|
if tt.expectedError != "" {
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.expectedError)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateConfigPath(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
validConfig := filepath.Join(tempDir, "config.yaml")
|
|
require.NoError(t, os.WriteFile(validConfig, []byte("key: value"), 0o600))
|
|
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: testEmptyPath,
|
|
path: "",
|
|
expectedError: "", // Empty config path is allowed
|
|
},
|
|
{
|
|
name: testPathTraversalAttempt,
|
|
path: "../../../etc/config.yaml",
|
|
expectedError: testPathTraversalDetected,
|
|
},
|
|
// ValidateConfigPath doesn't check if file exists or is regular file
|
|
// It only checks for path traversal
|
|
{
|
|
name: "valid config file",
|
|
path: validConfig,
|
|
expectedError: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := ValidateConfigPath(tt.path)
|
|
|
|
if tt.expectedError != "" {
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.expectedError)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetAbsolutePath is already covered in paths_test.go
|
|
|
|
func TestValidationErrorTypes(t *testing.T) {
|
|
t.Run("source path validation errors", func(t *testing.T) {
|
|
// Test empty source
|
|
err := ValidateSourcePath("")
|
|
assert.Error(t, err)
|
|
var structErrEmptyPath *StructuredError
|
|
if errors.As(err, &structErrEmptyPath) {
|
|
assert.Equal(t, ErrorTypeValidation, structErrEmptyPath.Type)
|
|
assert.Equal(t, CodeValidationRequired, structErrEmptyPath.Code)
|
|
}
|
|
|
|
// Test path traversal
|
|
err = ValidateSourcePath("../../../etc")
|
|
assert.Error(t, err)
|
|
var structErrTraversal *StructuredError
|
|
if errors.As(err, &structErrTraversal) {
|
|
assert.Equal(t, ErrorTypeValidation, structErrTraversal.Type)
|
|
assert.Equal(t, CodeValidationPath, structErrTraversal.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("destination path validation errors", func(t *testing.T) {
|
|
// Test empty destination
|
|
err := ValidateDestinationPath("")
|
|
assert.Error(t, err)
|
|
var structErrEmptyDest *StructuredError
|
|
if errors.As(err, &structErrEmptyDest) {
|
|
assert.Equal(t, ErrorTypeValidation, structErrEmptyDest.Type)
|
|
assert.Equal(t, CodeValidationRequired, structErrEmptyDest.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("config path validation errors", func(t *testing.T) {
|
|
// Test path traversal in config
|
|
err := ValidateConfigPath("../../etc/config.yaml")
|
|
assert.Error(t, err)
|
|
var structErrTraversalInConfig *StructuredError
|
|
if errors.As(err, &structErrTraversalInConfig) {
|
|
assert.Equal(t, ErrorTypeValidation, structErrTraversalInConfig.Type)
|
|
assert.Equal(t, CodeValidationPath, structErrTraversalInConfig.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPathSecurityChecks(t *testing.T) {
|
|
// Test various path traversal attempts
|
|
traversalPaths := []string{
|
|
"../etc/passwd",
|
|
"../../root/.ssh/id_rsa",
|
|
"/home/../../../etc/shadow",
|
|
"./../../sensitive/data",
|
|
"foo/../../../bar",
|
|
}
|
|
|
|
for _, path := range traversalPaths {
|
|
t.Run("source_"+path, func(t *testing.T) {
|
|
err := ValidateSourcePath(path)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), testPathTraversal)
|
|
})
|
|
|
|
t.Run("dest_"+path, func(t *testing.T) {
|
|
err := ValidateDestinationPath(path)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), testPathTraversal)
|
|
})
|
|
|
|
t.Run("config_"+path, func(t *testing.T) {
|
|
err := ValidateConfigPath(path)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), testPathTraversal)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSpecialPaths(t *testing.T) {
|
|
t.Run("GetBaseName with special paths", func(t *testing.T) {
|
|
specialPaths := map[string]string{
|
|
"/": "/",
|
|
"": "output",
|
|
".": "output",
|
|
"..": "..",
|
|
"/.": "output", // filepath.Base("/.") returns "." which matches the output condition
|
|
"/..": "..",
|
|
"//": "/",
|
|
"///": "/",
|
|
}
|
|
|
|
for path, expected := range specialPaths {
|
|
result := GetBaseName(path)
|
|
assert.Equal(t, expected, result, "Path: %s", path)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPathNormalization(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
t.Run("source path normalization", func(t *testing.T) {
|
|
// Create nested directory
|
|
nestedDir := filepath.Join(tempDir, "a", "b", "c")
|
|
require.NoError(t, os.MkdirAll(nestedDir, 0o750))
|
|
|
|
// Test path with redundant separators
|
|
redundantPath := tempDir + string(
|
|
os.PathSeparator,
|
|
) + string(
|
|
os.PathSeparator,
|
|
) + "a" + string(
|
|
os.PathSeparator,
|
|
) + "b" + string(
|
|
os.PathSeparator,
|
|
) + "c"
|
|
err := ValidateSourcePath(redundantPath)
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestPathValidationConcurrency(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
// Test concurrent path validation
|
|
paths := []string{
|
|
tempDir,
|
|
".",
|
|
"/tmp",
|
|
}
|
|
|
|
errChan := make(chan error, len(paths)*2)
|
|
|
|
for _, path := range paths {
|
|
go func(p string) {
|
|
errChan <- ValidateSourcePath(p)
|
|
}(path)
|
|
|
|
go func(p string) {
|
|
errChan <- ValidateDestinationPath(p + "/output.txt")
|
|
}(path)
|
|
}
|
|
|
|
// Collect results
|
|
for i := 0; i < len(paths)*2; i++ {
|
|
<-errChan
|
|
}
|
|
|
|
// No assertions needed - test passes if no panic/race
|
|
}
|