go-io/local/medium_test.go
Virgil c713bafd48
Some checks failed
CI / test (push) Failing after 3s
CI / auto-fix (push) Failing after 0s
CI / auto-merge (push) Failing after 0s
refactor(ax): align remaining AX examples and names
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-31 14:27:58 +00:00

473 lines
12 KiB
Go

package local
import (
"io"
"io/fs"
"syscall"
"testing"
core "dappco.re/go/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLocal_New_ResolvesRoot_Good(t *testing.T) {
root := t.TempDir()
localMedium, err := New(root)
assert.NoError(t, err)
resolved, err := resolveSymlinksPath(root)
require.NoError(t, err)
assert.Equal(t, resolved, localMedium.filesystemRoot)
}
func TestLocal_Path_Sandboxed_Good(t *testing.T) {
localMedium := &Medium{filesystemRoot: "/home/user"}
assert.Equal(t, "/home/user/file.txt", localMedium.sandboxedPath("file.txt"))
assert.Equal(t, "/home/user/dir/file.txt", localMedium.sandboxedPath("dir/file.txt"))
assert.Equal(t, "/home/user", localMedium.sandboxedPath(""))
assert.Equal(t, "/home/user/file.txt", localMedium.sandboxedPath("../file.txt"))
assert.Equal(t, "/home/user/file.txt", localMedium.sandboxedPath("dir/../file.txt"))
assert.Equal(t, "/home/user/etc/passwd", localMedium.sandboxedPath("/etc/passwd"))
}
func TestLocal_Path_RootFilesystem_Good(t *testing.T) {
localMedium := &Medium{filesystemRoot: "/"}
assert.Equal(t, "/etc/passwd", localMedium.sandboxedPath("/etc/passwd"))
assert.Equal(t, "/home/user/file.txt", localMedium.sandboxedPath("/home/user/file.txt"))
workingDirectory := currentWorkingDir()
assert.Equal(t, core.Path(workingDirectory, "file.txt"), localMedium.sandboxedPath("file.txt"))
}
func TestLocal_ReadWrite_Basic_Good(t *testing.T) {
root := t.TempDir()
localMedium, _ := New(root)
err := localMedium.Write("test.txt", "hello")
assert.NoError(t, err)
content, err := localMedium.Read("test.txt")
assert.NoError(t, err)
assert.Equal(t, "hello", content)
err = localMedium.Write("a/b/c.txt", "nested")
assert.NoError(t, err)
content, err = localMedium.Read("a/b/c.txt")
assert.NoError(t, err)
assert.Equal(t, "nested", content)
_, err = localMedium.Read("nope.txt")
assert.Error(t, err)
}
func TestLocal_EnsureDir_Basic_Good(t *testing.T) {
root := t.TempDir()
localMedium, _ := New(root)
err := localMedium.EnsureDir("one/two/three")
assert.NoError(t, err)
info, err := localMedium.Stat("one/two/three")
assert.NoError(t, err)
assert.True(t, info.IsDir())
}
func TestLocal_IsDir_Basic_Good(t *testing.T) {
root := t.TempDir()
localMedium, _ := New(root)
_ = localMedium.EnsureDir("mydir")
_ = localMedium.Write("myfile", "x")
assert.True(t, localMedium.IsDir("mydir"))
assert.False(t, localMedium.IsDir("myfile"))
assert.False(t, localMedium.IsDir("nope"))
assert.False(t, localMedium.IsDir(""))
}
func TestLocal_IsFile_Basic_Good(t *testing.T) {
root := t.TempDir()
localMedium, _ := New(root)
_ = localMedium.EnsureDir("mydir")
_ = localMedium.Write("myfile", "x")
assert.True(t, localMedium.IsFile("myfile"))
assert.False(t, localMedium.IsFile("mydir"))
assert.False(t, localMedium.IsFile("nope"))
assert.False(t, localMedium.IsFile(""))
}
func TestLocal_Exists_Basic_Good(t *testing.T) {
root := t.TempDir()
localMedium, _ := New(root)
_ = localMedium.Write("exists", "x")
assert.True(t, localMedium.Exists("exists"))
assert.False(t, localMedium.Exists("nope"))
}
func TestLocal_List_Basic_Good(t *testing.T) {
root := t.TempDir()
localMedium, _ := New(root)
_ = localMedium.Write("a.txt", "a")
_ = localMedium.Write("b.txt", "b")
_ = localMedium.EnsureDir("subdir")
entries, err := localMedium.List("")
assert.NoError(t, err)
assert.Len(t, entries, 3)
}
func TestLocal_Stat_Basic_Good(t *testing.T) {
root := t.TempDir()
localMedium, _ := New(root)
_ = localMedium.Write("file", "content")
info, err := localMedium.Stat("file")
assert.NoError(t, err)
assert.Equal(t, int64(7), info.Size())
}
func TestLocal_Delete_Basic_Good(t *testing.T) {
root := t.TempDir()
localMedium, _ := New(root)
_ = localMedium.Write("todelete", "x")
assert.True(t, localMedium.Exists("todelete"))
err := localMedium.Delete("todelete")
assert.NoError(t, err)
assert.False(t, localMedium.Exists("todelete"))
}
func TestLocal_DeleteAll_Basic_Good(t *testing.T) {
root := t.TempDir()
localMedium, _ := New(root)
_ = localMedium.Write("dir/sub/file", "x")
err := localMedium.DeleteAll("dir")
assert.NoError(t, err)
assert.False(t, localMedium.Exists("dir"))
}
func TestLocal_Delete_ProtectedHomeViaSymlinkEnv_Bad(t *testing.T) {
realHome := t.TempDir()
linkParent := t.TempDir()
homeLink := core.Path(linkParent, "home-link")
require.NoError(t, syscall.Symlink(realHome, homeLink))
t.Setenv("HOME", homeLink)
localMedium, err := New("/")
require.NoError(t, err)
err = localMedium.Delete(realHome)
require.Error(t, err)
assert.DirExists(t, realHome)
}
func TestLocal_DeleteAll_ProtectedHomeViaEnv_Bad(t *testing.T) {
tempHome := t.TempDir()
t.Setenv("HOME", tempHome)
localMedium, err := New("/")
require.NoError(t, err)
err = localMedium.DeleteAll(tempHome)
require.Error(t, err)
assert.DirExists(t, tempHome)
}
func TestLocal_Rename_Basic_Good(t *testing.T) {
root := t.TempDir()
localMedium, _ := New(root)
_ = localMedium.Write("old", "x")
err := localMedium.Rename("old", "new")
assert.NoError(t, err)
assert.False(t, localMedium.Exists("old"))
assert.True(t, localMedium.Exists("new"))
}
func TestLocal_Delete_Good(t *testing.T) {
testRoot := t.TempDir()
localMedium, err := New(testRoot)
assert.NoError(t, err)
err = localMedium.Write("file.txt", "content")
assert.NoError(t, err)
assert.True(t, localMedium.IsFile("file.txt"))
err = localMedium.Delete("file.txt")
assert.NoError(t, err)
assert.False(t, localMedium.IsFile("file.txt"))
err = localMedium.EnsureDir("emptydir")
assert.NoError(t, err)
err = localMedium.Delete("emptydir")
assert.NoError(t, err)
assert.False(t, localMedium.IsDir("emptydir"))
}
func TestLocal_Delete_NotEmpty_Bad(t *testing.T) {
testRoot := t.TempDir()
localMedium, err := New(testRoot)
assert.NoError(t, err)
err = localMedium.Write("mydir/file.txt", "content")
assert.NoError(t, err)
err = localMedium.Delete("mydir")
assert.Error(t, err)
}
func TestLocal_DeleteAll_Good(t *testing.T) {
testRoot := t.TempDir()
localMedium, err := New(testRoot)
assert.NoError(t, err)
err = localMedium.Write("mydir/file1.txt", "content1")
assert.NoError(t, err)
err = localMedium.Write("mydir/subdir/file2.txt", "content2")
assert.NoError(t, err)
err = localMedium.DeleteAll("mydir")
assert.NoError(t, err)
assert.False(t, localMedium.Exists("mydir"))
assert.False(t, localMedium.Exists("mydir/file1.txt"))
assert.False(t, localMedium.Exists("mydir/subdir/file2.txt"))
}
func TestLocal_Rename_Good(t *testing.T) {
testRoot := t.TempDir()
localMedium, err := New(testRoot)
assert.NoError(t, err)
err = localMedium.Write("old.txt", "content")
assert.NoError(t, err)
err = localMedium.Rename("old.txt", "new.txt")
assert.NoError(t, err)
assert.False(t, localMedium.IsFile("old.txt"))
assert.True(t, localMedium.IsFile("new.txt"))
content, err := localMedium.Read("new.txt")
assert.NoError(t, err)
assert.Equal(t, "content", content)
}
func TestLocal_Rename_TraversalSanitised_Good(t *testing.T) {
testRoot := t.TempDir()
localMedium, err := New(testRoot)
assert.NoError(t, err)
err = localMedium.Write("file.txt", "content")
assert.NoError(t, err)
err = localMedium.Rename("file.txt", "../escaped.txt")
assert.NoError(t, err)
assert.False(t, localMedium.Exists("file.txt"))
assert.True(t, localMedium.Exists("escaped.txt"))
}
func TestLocal_List_Good(t *testing.T) {
testRoot := t.TempDir()
localMedium, err := New(testRoot)
assert.NoError(t, err)
err = localMedium.Write("file1.txt", "content1")
assert.NoError(t, err)
err = localMedium.Write("file2.txt", "content2")
assert.NoError(t, err)
err = localMedium.EnsureDir("subdir")
assert.NoError(t, err)
entries, err := localMedium.List(".")
assert.NoError(t, err)
assert.Len(t, entries, 3)
names := make(map[string]bool)
for _, entry := range entries {
names[entry.Name()] = true
}
assert.True(t, names["file1.txt"])
assert.True(t, names["file2.txt"])
assert.True(t, names["subdir"])
}
func TestLocal_Stat_Good(t *testing.T) {
testRoot := t.TempDir()
localMedium, err := New(testRoot)
assert.NoError(t, err)
err = localMedium.Write("file.txt", "hello world")
assert.NoError(t, err)
info, err := localMedium.Stat("file.txt")
assert.NoError(t, err)
assert.Equal(t, "file.txt", info.Name())
assert.Equal(t, int64(11), info.Size())
assert.False(t, info.IsDir())
err = localMedium.EnsureDir("mydir")
assert.NoError(t, err)
info, err = localMedium.Stat("mydir")
assert.NoError(t, err)
assert.Equal(t, "mydir", info.Name())
assert.True(t, info.IsDir())
}
func TestLocal_Exists_Good(t *testing.T) {
testRoot := t.TempDir()
localMedium, err := New(testRoot)
assert.NoError(t, err)
assert.False(t, localMedium.Exists("nonexistent"))
err = localMedium.Write("file.txt", "content")
assert.NoError(t, err)
assert.True(t, localMedium.Exists("file.txt"))
err = localMedium.EnsureDir("mydir")
assert.NoError(t, err)
assert.True(t, localMedium.Exists("mydir"))
}
func TestLocal_IsDir_Good(t *testing.T) {
testRoot := t.TempDir()
localMedium, err := New(testRoot)
assert.NoError(t, err)
err = localMedium.Write("file.txt", "content")
assert.NoError(t, err)
assert.False(t, localMedium.IsDir("file.txt"))
err = localMedium.EnsureDir("mydir")
assert.NoError(t, err)
assert.True(t, localMedium.IsDir("mydir"))
assert.False(t, localMedium.IsDir("nonexistent"))
}
func TestLocal_ReadStream_Basic_Good(t *testing.T) {
root := t.TempDir()
localMedium, _ := New(root)
content := "streaming content"
err := localMedium.Write("stream.txt", content)
assert.NoError(t, err)
reader, err := localMedium.ReadStream("stream.txt")
assert.NoError(t, err)
defer reader.Close()
limitReader := io.LimitReader(reader, 9)
data, err := io.ReadAll(limitReader)
assert.NoError(t, err)
assert.Equal(t, "streaming", string(data))
}
func TestLocal_WriteStream_Basic_Good(t *testing.T) {
root := t.TempDir()
localMedium, _ := New(root)
writer, err := localMedium.WriteStream("output.txt")
assert.NoError(t, err)
_, err = io.Copy(writer, core.NewReader("piped data"))
assert.NoError(t, err)
err = writer.Close()
assert.NoError(t, err)
content, err := localMedium.Read("output.txt")
assert.NoError(t, err)
assert.Equal(t, "piped data", content)
}
func TestLocal_Path_TraversalSandbox_Good(t *testing.T) {
localMedium := &Medium{filesystemRoot: "/sandbox"}
assert.Equal(t, "/sandbox/file.txt", localMedium.sandboxedPath("../../../file.txt"))
assert.Equal(t, "/sandbox/target", localMedium.sandboxedPath("dir/../../target"))
assert.Equal(t, "/sandbox/.ssh/id_rsa", localMedium.sandboxedPath(".ssh/id_rsa"))
assert.Equal(t, "/sandbox/id_rsa", localMedium.sandboxedPath(".ssh/../id_rsa"))
assert.Equal(t, "/sandbox/file\x00.txt", localMedium.sandboxedPath("file\x00.txt"))
}
func TestLocal_ValidatePath_SymlinkEscape_Bad(t *testing.T) {
root := t.TempDir()
localMedium, err := New(root)
assert.NoError(t, err)
outside := t.TempDir()
outsideFile := core.Path(outside, "secret.txt")
outsideMedium, err := New("/")
require.NoError(t, err)
err = outsideMedium.Write(outsideFile, "secret")
assert.NoError(t, err)
_, err = localMedium.validatePath("../outside.txt")
assert.NoError(t, err)
linkPath := core.Path(root, "evil_link")
err = syscall.Symlink(outside, linkPath)
assert.NoError(t, err)
_, err = localMedium.validatePath("evil_link/secret.txt")
assert.Error(t, err)
assert.ErrorIs(t, err, fs.ErrPermission)
err = localMedium.EnsureDir("inner")
assert.NoError(t, err)
innerDir := core.Path(root, "inner")
nestedLink := core.Path(innerDir, "nested_evil")
err = syscall.Symlink(outside, nestedLink)
assert.NoError(t, err)
_, err = localMedium.validatePath("inner/nested_evil/secret.txt")
assert.Error(t, err)
assert.ErrorIs(t, err, fs.ErrPermission)
}
func TestLocal_EmptyPaths_Good(t *testing.T) {
root := t.TempDir()
localMedium, err := New(root)
assert.NoError(t, err)
_, err = localMedium.Read("")
assert.Error(t, err)
err = localMedium.Write("", "content")
assert.Error(t, err)
err = localMedium.EnsureDir("")
assert.NoError(t, err)
assert.False(t, localMedium.IsDir(""))
assert.True(t, localMedium.Exists(""))
entries, err := localMedium.List("")
assert.NoError(t, err)
assert.NotNil(t, entries)
}