Files
Roberto Jiménez Sánchez 047499a363 Provisioning: introduce concept of provisioning extras (#104981)
* Spike: Extras

* Attempt to wire it up

* Hack

* Fix issue with jobs

* Wire more things up

* Fix more wiring stuff

* Remove webhook secret key from main registration

* Move secret encryption also outside register

* Add TODOs in code

* Add more explanations

* Move connectors to different package

* Move pull request job into webhooks

* Separate registration

* Remove duplicate files

* Fix missing function

* Extract webhook repository logic out of the core github repository

* Use status patcher in webhook connector

* Fix change in go mod

* Change hooks signature

* Remove TODOs

* Remove Webhook methos from go-git

* Remove leftover

* Fix mistake in OpenAPI spec

* Fix some tests

* Fix some issues

* Fix linting
2025-05-13 09:50:43 +02:00

1484 lines
40 KiB
Go

package repository
import (
"context"
"errors"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
field "k8s.io/apimachinery/pkg/util/validation/field"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
)
func TestLocalResolver(t *testing.T) {
// Create a temporary directory structure
tempDir := t.TempDir()
// Create directory structure with multiple levels
dirs := []string{
"level1",
"level1/level2",
"level1/level2/level3",
"another/path",
}
for _, dir := range dirs {
dirPath := filepath.Join(tempDir, dir)
err := os.MkdirAll(dirPath, 0750)
require.NoError(t, err)
}
// Create some files at different levels
files := map[string]string{
"root.txt": "root content",
"level1/file1.txt": "level 1 content",
"level1/level2/file2.txt": "level 2 content",
"level1/level2/level3/file3.txt": "level 3 content",
"another/path/file.txt": "another path content",
}
for path, content := range files {
filePath := filepath.Join(tempDir, path)
err := os.WriteFile(filePath, []byte(content), 0644)
require.NoError(t, err)
}
// Create resolver with the temp directory as permitted prefix
resolver := &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
HomePath: "./",
}
// Test resolving paths
for _, dir := range dirs {
fullPath, err := resolver.LocalPath(filepath.Join(tempDir, dir))
require.NoError(t, err)
require.Equal(t, filepath.Join(tempDir, dir), fullPath)
}
// Test repository with the temp directory
repo := NewLocal(&provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
}, resolver)
// Verify we can read the tree
tree, err := repo.ReadTree(context.Background(), "")
require.NoError(t, err)
// Collect all paths from the tree
paths := make([]string, 0, len(tree))
for _, item := range tree {
paths = append(paths, item.Path)
}
// Sort paths for consistent comparison
sort.Strings(paths)
// Verify all directories and files are present
expectedPaths := []string{
"another",
"another/path",
"another/path/file.txt",
"level1",
"level1/file1.txt",
"level1/level2",
"level1/level2/file2.txt",
"level1/level2/level3",
"level1/level2/level3/file3.txt",
"root.txt",
}
require.Equal(t, expectedPaths, paths)
// Test reading a specific file
file, err := repo.Read(context.Background(), "level1/level2/file2.txt", "")
require.NoError(t, err)
require.Equal(t, "level1/level2/file2.txt", file.Path)
require.Equal(t, []byte("level 2 content"), file.Data)
}
func TestLocal(t *testing.T) {
// Valid paths test cases
for _, tc := range []struct {
Name string
Path string
PermittedPrefixes []string
ExpectedPath string
}{
{"relative path", "devenv/test", []string{"/home/grafana"}, "/home/grafana/devenv/test/"},
{"absolute path", "/devenv/test", []string{"/devenv"}, "/devenv/test/"},
{"relative path with multiple prefixes", "devenv/test", []string{"/home/grafana", "/devenv"}, "/home/grafana/devenv/test/"},
{"absolute path with multiple prefixes", "/devenv/test", []string{"/home/grafana", "/devenv"}, "/devenv/test/"},
} {
t.Run("valid: "+tc.Name, func(t *testing.T) {
r := NewLocal(&provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tc.Path,
},
},
}, &LocalFolderResolver{PermittedPrefixes: tc.PermittedPrefixes, HomePath: "/home/grafana"})
assert.Equal(t, tc.ExpectedPath, r.path, "expected path to be resolved")
for _, err := range r.Validate() {
assert.Fail(t, "unexpected validation failure", "unexpected validation error on field %s: %s", err.Field, err.ErrorBody())
}
})
}
// Invalid paths test cases
for _, tc := range []struct {
Name string
Path string
PermittedPrefixes []string
}{
{"no configured paths", "invalid/path", nil},
{"path traversal escape", "../../etc/passwd", []string{"/home/grafana"}},
{"unconfigured prefix", "invalid/path", []string{"devenv", "/tmp", "test"}},
} {
t.Run("invalid: "+tc.Name, func(t *testing.T) {
r := NewLocal(&provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tc.Path,
},
},
}, &LocalFolderResolver{PermittedPrefixes: tc.PermittedPrefixes, HomePath: "/home/grafana"})
require.Empty(t, r.path, "no path should be resolved")
errs := r.Validate()
require.NotEmpty(t, errs, "expected validation errors")
for _, err := range errs {
if !assert.Equal(t, "spec.local.path", err.Field) {
assert.FailNow(t, "unexpected validation failure", "unexpected validation error on field %s: %s", err.Field, err.ErrorBody())
}
}
})
}
}
func TestLocalRepository_Test(t *testing.T) {
// Test cases for the Test method
testCases := []struct {
name string
path string
pathExists bool
expectedCode int
expectedResult bool
}{
{
name: "valid path that exists",
path: "valid/path/",
pathExists: true,
expectedCode: http.StatusOK,
expectedResult: true,
},
{
name: "valid path that doesn't exist",
path: "valid/nonexistent",
pathExists: false,
expectedCode: http.StatusBadRequest,
expectedResult: false,
},
{
name: "invalid path with path traversal",
path: "../../../etc/passwd",
pathExists: false,
expectedCode: http.StatusBadRequest,
expectedResult: false,
},
{
name: "invalid path with special characters",
path: "path/with/*/wildcards",
pathExists: false,
expectedCode: http.StatusBadRequest,
expectedResult: false,
},
{
name: "empty path",
path: "",
pathExists: false,
expectedCode: http.StatusBadRequest,
expectedResult: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create a temporary directory for testing
tempDir := t.TempDir()
// Setup the test directory if needed
testPath := filepath.Join(tempDir, tc.path)
if tc.pathExists {
err := os.MkdirAll(testPath, 0750)
require.NoError(t, err, "Failed to create test directory")
}
// Create a resolver that permits the temp directory
resolver := &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
HomePath: tempDir,
}
// Create the repository with the test path
repo := NewLocal(&provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tc.path,
},
},
}, resolver)
// If we're testing a valid path, set it to our test path
if tc.path != "" {
repo.path = testPath
}
// Call the Test method
results, err := repo.Test(context.Background())
// Verify results
require.NoError(t, err, "Test method should not return an error")
assert.Equal(t, tc.expectedResult, results.Success, "Success flag should match expected")
assert.Equal(t, tc.expectedCode, results.Code, "Status code should match expected")
})
}
}
func TestLocalRepository_Validate(t *testing.T) {
testCases := []struct {
name string
config *provisioning.LocalRepositoryConfig
permittedPath string
expectedErrs []field.Error
}{
{
name: "valid configuration",
config: &provisioning.LocalRepositoryConfig{Path: "valid/path"},
permittedPath: "valid",
expectedErrs: nil,
},
{
name: "missing local config",
config: nil,
permittedPath: "valid",
expectedErrs: []field.Error{
{
Type: field.ErrorTypeRequired,
Field: "spec.local",
},
},
},
{
name: "empty path",
config: &provisioning.LocalRepositoryConfig{Path: ""},
permittedPath: "valid",
expectedErrs: []field.Error{
{
Type: field.ErrorTypeRequired,
Field: "spec.local.path",
Detail: "must enter a path to local file",
BadValue: "",
},
},
},
{
name: "path not in permitted prefixes",
config: &provisioning.LocalRepositoryConfig{Path: "invalid/path"},
permittedPath: "valid",
expectedErrs: []field.Error{
{
Type: field.ErrorTypeInvalid,
Field: "spec.local.path",
BadValue: "invalid/path",
Detail: "the path given ('invalid/path') is invalid for a local repository (the path matches no permitted prefix)",
},
},
},
{
name: "unsafe path with directory traversal",
config: &provisioning.LocalRepositoryConfig{Path: "../../../etc/passwd"},
permittedPath: "valid",
expectedErrs: []field.Error{
{
Type: field.ErrorTypeInvalid,
Field: "spec.local.path",
BadValue: "../../../etc/passwd",
Detail: "path contains traversal attempt (./ or ../)",
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create a temporary directory for testing
tempDir := t.TempDir()
permittedPath := filepath.Join(tempDir, tc.permittedPath)
// Create the permitted directory
if tc.permittedPath != "" {
err := os.MkdirAll(permittedPath, 0750)
require.NoError(t, err, "Failed to create permitted directory")
}
// Create a resolver that permits the specific path
resolver := &LocalFolderResolver{
PermittedPrefixes: []string{permittedPath},
HomePath: tempDir,
}
// Create repository config
repoConfig := &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: tc.config,
},
}
// Create the repository
repo := NewLocal(repoConfig, resolver)
// Call the Validate method
errors := repo.Validate()
// Verify results
if tc.expectedErrs == nil {
assert.Empty(t, errors, "Expected no validation errors")
} else {
assert.Len(t, errors, len(tc.expectedErrs), "Number of validation errors should match expected")
for i, expectedErr := range tc.expectedErrs {
assert.Equal(t, expectedErr.Type, errors[i].Type, "Error type should match")
assert.Equal(t, expectedErr.Field, errors[i].Field, "Error field should match")
assert.Equal(t, expectedErr.Detail, errors[i].Detail, "Error detail should match")
assert.Equal(t, expectedErr.BadValue, errors[i].BadValue, "Error bad value should match")
}
}
})
}
}
func TestInvalidLocalFolderError(t *testing.T) {
testCases := []struct {
name string
path string
additionalInfo string
expectedMsg string
expectedStatus metav1.Status
}{
{
name: "basic error",
path: "/invalid/path",
additionalInfo: "not allowed",
expectedMsg: "the path given ('/invalid/path') is invalid for a local repository (not allowed)",
expectedStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusBadRequest,
Reason: metav1.StatusReasonBadRequest,
Message: "the path given ('/invalid/path') is invalid for a local repository (not allowed)",
},
},
{
name: "empty path",
path: "",
additionalInfo: "path cannot be empty",
expectedMsg: "the path given ('') is invalid for a local repository (path cannot be empty)",
expectedStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusBadRequest,
Reason: metav1.StatusReasonBadRequest,
Message: "the path given ('') is invalid for a local repository (path cannot be empty)",
},
},
{
name: "no permitted prefixes",
path: "/some/path",
additionalInfo: "no permitted prefixes were configured",
expectedMsg: "the path given ('/some/path') is invalid for a local repository (no permitted prefixes were configured)",
expectedStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusBadRequest,
Reason: metav1.StatusReasonBadRequest,
Message: "the path given ('/some/path') is invalid for a local repository (no permitted prefixes were configured)",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create the error
err := &InvalidLocalFolderError{
Path: tc.path,
AdditionalInfo: tc.additionalInfo,
}
// Test Error() method
assert.Equal(t, tc.expectedMsg, err.Error(), "Error message should match expected")
// Test Status() method
status := err.Status()
assert.Equal(t, tc.expectedStatus.Status, status.Status, "Status should match")
assert.Equal(t, tc.expectedStatus.Code, status.Code, "Status code should match")
assert.Equal(t, tc.expectedStatus.Reason, status.Reason, "Status reason should match")
assert.Equal(t, tc.expectedStatus.Message, status.Message, "Status message should match")
// Verify it implements the expected interfaces
var apiStatus apierrors.APIStatus
assert.True(t, errors.As(err, &apiStatus), "Should implement APIStatus interface")
})
}
}
func TestLocalRepository_Delete(t *testing.T) {
testCases := []struct {
name string
setup func(t *testing.T) (string, *localRepository)
path string
ref string
comment string
expectedErr error
}{
{
name: "delete existing file",
setup: func(t *testing.T) (string, *localRepository) {
// Create a temporary directory for testing
tempDir := t.TempDir()
// Create a test file
testFilePath := filepath.Join(tempDir, "test-file.txt")
err := os.WriteFile(testFilePath, []byte("test content"), 0600)
require.NoError(t, err)
// Create repository with the temp directory as permitted prefix
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "test-file.txt",
ref: "",
comment: "test delete",
expectedErr: nil,
},
{
name: "delete non-existent file",
setup: func(t *testing.T) (string, *localRepository) {
// Create a temporary directory for testing
tempDir := t.TempDir()
// Create repository with the temp directory as permitted prefix
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "non-existent-file.txt",
ref: "",
comment: "test delete non-existent",
expectedErr: os.ErrNotExist,
},
{
name: "delete with ref not supported",
setup: func(t *testing.T) (string, *localRepository) {
// Create a temporary directory for testing
tempDir := t.TempDir()
// Create repository with the temp directory as permitted prefix
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "test-file.txt",
ref: "main",
comment: "test delete with ref",
expectedErr: apierrors.NewBadRequest("local repository does not support ref"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Setup test environment
_, repo := tc.setup(t)
// Execute the delete operation
err := repo.Delete(context.Background(), tc.path, tc.ref, tc.comment)
// Verify results
if tc.expectedErr != nil {
require.Error(t, err)
if errors.Is(tc.expectedErr, os.ErrNotExist) {
assert.True(t, errors.Is(err, os.ErrNotExist), "Expected os.ErrNotExist error")
} else {
assert.Equal(t, tc.expectedErr.Error(), err.Error(), "Error message should match expected")
}
} else {
require.NoError(t, err)
// Verify the file was actually deleted
_, statErr := os.Stat(filepath.Join(repo.path, tc.path))
assert.True(t, errors.Is(statErr, os.ErrNotExist), "File should be deleted")
}
})
}
}
func TestLocalRepository_Update(t *testing.T) {
testCases := []struct {
name string
setup func(t *testing.T) (string, *localRepository)
path string
ref string
data []byte
comment string
expectedErr error
}{
{
name: "update existing file",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
// Create a file to update
filePath := filepath.Join(tempDir, "existing-file.txt")
require.NoError(t, os.WriteFile(filePath, []byte("initial content"), 0600))
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "existing-file.txt",
ref: "",
data: []byte("updated content"),
comment: "",
expectedErr: nil,
},
{
name: "update existing directory as a file",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
// Create a directory
dirPath := filepath.Join(tempDir, "existing-dir")
require.NoError(t, os.MkdirAll(dirPath, 0700))
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "existing-dir",
ref: "",
data: []byte("file content"),
comment: "",
expectedErr: apierrors.NewBadRequest("path exists but it is a directory"),
},
{
name: "update non-existent file",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "non-existent-file.txt",
ref: "",
data: []byte("content"),
comment: "",
expectedErr: ErrFileNotFound,
},
{
name: "update directory",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
// Create a directory
dirPath := filepath.Join(tempDir, "test-dir")
require.NoError(t, os.MkdirAll(dirPath, 0700))
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "test-dir/",
ref: "",
data: []byte("content"),
comment: "",
expectedErr: apierrors.NewBadRequest("cannot update a directory"),
},
{
name: "update with ref",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
// Create a file to update
filePath := filepath.Join(tempDir, "test-file.txt")
require.NoError(t, os.WriteFile(filePath, []byte("initial content"), 0600))
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "test-file.txt",
ref: "main",
data: []byte("updated content"),
comment: "test update with ref",
expectedErr: apierrors.NewBadRequest("local repository does not support ref"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Setup test environment
_, repo := tc.setup(t)
// Execute the update operation
err := repo.Update(context.Background(), tc.path, tc.ref, tc.data, tc.comment)
// Verify results
if tc.expectedErr != nil {
require.Error(t, err)
assert.Equal(t, tc.expectedErr.Error(), err.Error(), "Error message should match expected")
} else {
require.NoError(t, err)
// Verify the file was actually updated
updatedContent, readErr := os.ReadFile(filepath.Join(repo.path, tc.path))
require.NoError(t, readErr)
assert.Equal(t, tc.data, updatedContent, "File content should be updated")
}
})
}
}
func TestLocalRepository_Write(t *testing.T) {
testCases := []struct {
name string
setup func(t *testing.T) (string, *localRepository)
path string
ref string
data []byte
comment string
expectedErr error
}{
{
name: "write new file",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "new-file.txt",
data: []byte("new content"),
comment: "test write new file",
},
{
name: "overwrite existing file",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
// Create a file to be overwritten
existingFilePath := filepath.Join(tempDir, "existing-file.txt")
require.NoError(t, os.WriteFile(existingFilePath, []byte("original content"), 0600))
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "existing-file.txt",
data: []byte("updated content"),
comment: "test overwrite existing file",
},
{
name: "create directory",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "new-dir/",
data: nil,
comment: "test create directory",
},
{
name: "create file in nested directory",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "nested/dir/file.txt",
data: []byte("nested file content"),
comment: "test create file in nested directory",
},
{
name: "write with ref should fail",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "test-file.txt",
ref: "main",
data: []byte("content with ref"),
comment: "test write with ref",
expectedErr: apierrors.NewBadRequest("local repository does not support ref"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Setup test environment
_, repo := tc.setup(t)
// Execute the write operation
err := repo.Write(context.Background(), tc.path, tc.ref, tc.data, tc.comment)
// Verify results
if tc.expectedErr != nil {
require.Error(t, err)
assert.Equal(t, tc.expectedErr.Error(), err.Error(), "Error message should match expected")
} else {
require.NoError(t, err)
// Verify the file or directory was created
targetPath := filepath.Join(repo.path, tc.path)
// Check if it's a directory
if strings.HasSuffix(tc.path, "/") || tc.data == nil {
info, statErr := os.Stat(targetPath)
require.NoError(t, statErr)
assert.True(t, info.IsDir(), "Path should be a directory")
} else {
// Verify file content
//nolint:gosec
content, readErr := os.ReadFile(targetPath)
require.NoError(t, readErr)
assert.Equal(t, tc.data, content, "File content should match written data")
}
}
})
}
}
func TestLocalRepository_Create(t *testing.T) {
testCases := []struct {
name string
setup func(t *testing.T) (string, *localRepository)
path string
ref string
data []byte
comment string
expectedErr error
}{
{
name: "create new file",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "new-file.txt",
data: []byte("new content"),
comment: "test create new file",
},
{
name: "create file in nested directory",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "nested/dir/new-file.txt",
data: []byte("nested content"),
comment: "test create file in nested directory",
},
{
name: "create directory",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "new-dir/",
data: nil,
comment: "test create directory",
},
{
name: "create file that already exists",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
// Create a file that will conflict
existingFilePath := filepath.Join(tempDir, "existing-file.txt")
require.NoError(t, os.WriteFile(existingFilePath, []byte("original content"), 0600))
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "existing-file.txt",
data: []byte("new content"),
comment: "test create existing file",
expectedErr: apierrors.NewAlreadyExists(schema.GroupResource{}, "existing-file.txt"),
},
{
name: "create directory with data",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "invalid-dir/",
data: []byte("directory with data"),
comment: "test create directory with data",
expectedErr: apierrors.NewBadRequest("data cannot be provided for a directory"),
},
{
name: "create with ref",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "file-with-ref.txt",
ref: "main",
data: []byte("content with ref"),
comment: "test create with ref",
expectedErr: apierrors.NewBadRequest("local repository does not support ref"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Setup test environment
_, repo := tc.setup(t)
// Execute the create operation
err := repo.Create(context.Background(), tc.path, tc.ref, tc.data, tc.comment)
// Verify results
if tc.expectedErr != nil {
require.Error(t, err)
assert.Equal(t, tc.expectedErr.Error(), err.Error(), "Error message should match expected")
} else {
require.NoError(t, err)
// Verify the file or directory was created
targetPath := filepath.Join(repo.path, tc.path)
// Check if it's a directory
if strings.HasSuffix(tc.path, "/") || tc.data == nil {
info, statErr := os.Stat(targetPath)
require.NoError(t, statErr)
assert.True(t, info.IsDir(), "Path should be a directory")
} else {
// Verify file content
//nolint:gosec
content, readErr := os.ReadFile(targetPath)
require.NoError(t, readErr)
assert.Equal(t, tc.data, content, "File content should match written data")
}
}
})
}
}
func TestLocalRepository_Read(t *testing.T) {
testCases := []struct {
name string
setup func(t *testing.T) (string, *localRepository)
path string
ref string
expectedErr error
expected *FileInfo
}{
{
name: "read existing file",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
// Create a file to read
filePath := filepath.Join(tempDir, "test-file.txt")
fileContent := []byte("test content")
require.NoError(t, os.WriteFile(filePath, fileContent, 0600))
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "test-file.txt",
expected: &FileInfo{
Path: "test-file.txt",
Modified: &metav1.Time{Time: time.Now()},
Data: []byte("test content"),
Hash: "1eebdf4fdc9fc7bf283031b93f9aef3338de9052",
},
},
{
name: "read non-existent file",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "non-existent-file.txt",
expectedErr: ErrFileNotFound,
},
{
name: "read with ref should fail",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
// Create a file to read
filePath := filepath.Join(tempDir, "test-file.txt")
fileContent := []byte("test content")
require.NoError(t, os.WriteFile(filePath, fileContent, 0600))
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "test-file.txt",
ref: "main",
expectedErr: apierrors.NewBadRequest("local repository does not support ref"),
},
{
name: "read existing directory",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
// Create a directory to read
dirPath := filepath.Join(tempDir, "test-dir")
require.NoError(t, os.Mkdir(dirPath, 0750))
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
path: "test-dir",
expected: &FileInfo{
Path: "test-dir",
Modified: &metav1.Time{Time: time.Now()},
},
expectedErr: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Setup test environment
_, repo := tc.setup(t)
// Execute the read operation
data, err := repo.Read(context.Background(), tc.path, tc.ref)
// Verify results
if tc.expectedErr != nil {
require.Error(t, err)
assert.Equal(t, tc.expectedErr.Error(), err.Error(), "Error message should match expected")
} else {
require.NoError(t, err)
assert.Equal(t, tc.expected.Path, data.Path, "Path should match expected")
assert.NotNil(t, data.Modified, "Modified time should not be nil")
assert.Equal(t, tc.expected.Data, data.Data, "Data should match expected")
assert.Equal(t, tc.expected.Hash, data.Hash, "Hash should match expected")
assert.Empty(t, data.Ref, "Ref should be empty")
}
})
}
}
func TestLocalRepository_ReadTree(t *testing.T) {
testCases := []struct {
name string
setup func(t *testing.T) (string, *localRepository)
ref string
expectedErr error
expected []FileTreeEntry
}{
{
name: "read empty directory",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
expected: []FileTreeEntry{},
expectedErr: nil,
},
{
name: "read directory with files",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
// Create a file structure
require.NoError(t, os.WriteFile(filepath.Join(tempDir, "file1.txt"), []byte("content1"), 0600))
require.NoError(t, os.WriteFile(filepath.Join(tempDir, "file2.txt"), []byte("content2"), 0600))
require.NoError(t, os.MkdirAll(filepath.Join(tempDir, "subdir"), 0700))
require.NoError(t, os.WriteFile(filepath.Join(tempDir, "subdir", "file3.txt"), []byte("content3"), 0600))
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
expected: []FileTreeEntry{
{Path: "file1.txt", Blob: true, Size: 8},
{Path: "file2.txt", Blob: true, Size: 8},
{Path: "subdir", Blob: false},
{Path: "subdir/file3.txt", Blob: true, Size: 8},
},
expectedErr: nil,
},
{
name: "read with ref",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: tempDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: tempDir,
}
return tempDir, repo
},
ref: "main",
expectedErr: apierrors.NewBadRequest("local repository does not support ref"),
},
{
name: "read non-existent directory",
setup: func(t *testing.T) (string, *localRepository) {
tempDir := t.TempDir()
nonExistentDir := filepath.Join(tempDir, "non-existent")
repo := &localRepository{
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: nonExistentDir,
},
},
},
resolver: &LocalFolderResolver{
PermittedPrefixes: []string{tempDir},
},
path: nonExistentDir,
}
return tempDir, repo
},
expected: []FileTreeEntry{},
expectedErr: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Setup test environment
_, repo := tc.setup(t)
// Execute the readTree operation
entries, err := repo.ReadTree(context.Background(), tc.ref)
// Verify results
if tc.expectedErr != nil {
require.Error(t, err)
assert.Equal(t, tc.expectedErr.Error(), err.Error(), "Error message should match expected")
} else {
require.NoError(t, err)
if len(tc.expected) == 0 {
assert.Empty(t, entries, "Expected empty entries")
} else {
// Sort both expected and actual entries by path for comparison
sort.Slice(entries, func(i, j int) bool {
return entries[i].Path < entries[j].Path
})
// We need to verify each entry individually since hash values will be different
assert.Equal(t, len(tc.expected), len(entries), "Number of entries should match")
for i, expected := range tc.expected {
if i < len(entries) {
assert.Equal(t, expected.Path, entries[i].Path, "Path should match")
assert.Equal(t, expected.Blob, entries[i].Blob, "Blob flag should match")
if expected.Blob {
assert.Equal(t, expected.Size, entries[i].Size, "Size should match")
assert.NotEmpty(t, entries[i].Hash, "Hash should not be empty for files")
}
}
}
}
}
})
}
}
func TestLocalRepository_Config(t *testing.T) {
testCases := []struct {
name string
config *provisioning.Repository
}{
{
name: "returns the same config that was provided",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Local: &provisioning.LocalRepositoryConfig{
Path: "/some/path",
},
},
},
},
{
name: "returns nil config",
config: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create repository with the test config
repo := &localRepository{
config: tc.config,
}
// Call the Config method
result := repo.Config()
// Verify the result is the same as the input config
assert.Equal(t, tc.config, result, "Config() should return the same config that was provided")
})
}
}