Files
kandev/apps/backend/internal/worktree/copyfiles/copyfiles_test.go
T

853 lines
24 KiB
Go

package copyfiles
import (
"context"
"errors"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
)
func TestParse(t *testing.T) {
t.Parallel()
cases := []struct {
name string
in string
want []string
}{
{"empty", "", nil},
{"only comma", ",", nil},
{"single trimmed", " .env ", []string{".env"}},
{"two", ".env,.env.local", []string{".env", ".env.local"}},
{"dedupe", ".env, .env, .env.local", []string{".env", ".env.local"}},
{"empties dropped", " .env , , .env.local ", []string{".env", ".env.local"}},
{"brace keeps comma", "config/{local,dev}.yml", []string{"config/{local,dev}.yml"}},
{
"brace with siblings",
".env, config/{local,dev}.yml, .env.local",
[]string{".env", "config/{local,dev}.yml", ".env.local"},
},
{"nested braces", "{a,{b,c}}.txt", []string{"{a,{b,c}}.txt"}},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := Parse(tc.in)
if len(got) == 0 && len(tc.want) == 0 {
return
}
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("Parse(%q) = %#v, want %#v", tc.in, got, tc.want)
}
})
}
}
func writeFile(t *testing.T, path, content string, mode os.FileMode) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir parent: %v", err)
}
if err := os.WriteFile(path, []byte(content), mode); err != nil {
t.Fatalf("write %s: %v", path, err)
}
// WriteFile honors umask; force exact mode for predictable tests.
if err := os.Chmod(path, mode); err != nil {
t.Fatalf("chmod %s: %v", path, err)
}
}
func readFile(t *testing.T, path string) string {
t.Helper()
b, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
return string(b)
}
func TestCopy_LiteralFile(t *testing.T) {
t.Parallel()
src := t.TempDir()
dst := t.TempDir()
writeFile(t, filepath.Join(src, ".env"), "X=1", 0o600)
_, warnings, err := Copy(context.Background(), src, dst, []string{".env"}, nil)
if err != nil {
t.Fatalf("Copy err: %v", err)
}
if len(warnings) != 0 {
t.Fatalf("unexpected warnings: %v", warnings)
}
got := readFile(t, filepath.Join(dst, ".env"))
if got != "X=1" {
t.Fatalf("content = %q, want %q", got, "X=1")
}
info, err := os.Stat(filepath.Join(dst, ".env"))
if err != nil {
t.Fatalf("stat: %v", err)
}
if info.Mode().Perm() != 0o600 {
t.Fatalf("mode = %v, want 0o600", info.Mode().Perm())
}
}
func TestCopy_Glob(t *testing.T) {
t.Parallel()
src := t.TempDir()
dst := t.TempDir()
writeFile(t, filepath.Join(src, "a.local"), "A", 0o644)
writeFile(t, filepath.Join(src, "b.local"), "B", 0o644)
writeFile(t, filepath.Join(src, "c.txt"), "C", 0o644)
_, warnings, err := Copy(context.Background(), src, dst, []string{"*.local"}, nil)
if err != nil {
t.Fatalf("Copy err: %v", err)
}
if len(warnings) != 0 {
t.Fatalf("warnings: %v", warnings)
}
if readFile(t, filepath.Join(dst, "a.local")) != "A" {
t.Fatalf("a.local missing or wrong")
}
if readFile(t, filepath.Join(dst, "b.local")) != "B" {
t.Fatalf("b.local missing or wrong")
}
if _, err := os.Stat(filepath.Join(dst, "c.txt")); !os.IsNotExist(err) {
t.Fatalf("c.txt should not exist, err=%v", err)
}
}
func TestCopy_DirectoryRecursive(t *testing.T) {
t.Parallel()
src := t.TempDir()
dst := t.TempDir()
writeFile(t, filepath.Join(src, "config", "local.yml"), "y", 0o644)
writeFile(t, filepath.Join(src, "config", "sub", "dev.json"), "j", 0o644)
_, warnings, err := Copy(context.Background(), src, dst, []string{"config"}, nil)
if err != nil {
t.Fatalf("Copy err: %v", err)
}
if len(warnings) != 0 {
t.Fatalf("warnings: %v", warnings)
}
if readFile(t, filepath.Join(dst, "config", "local.yml")) != "y" {
t.Fatalf("local.yml missing")
}
if readFile(t, filepath.Join(dst, "config", "sub", "dev.json")) != "j" {
t.Fatalf("sub/dev.json missing")
}
}
func TestCopy_NestedFile(t *testing.T) {
t.Parallel()
src := t.TempDir()
dst := t.TempDir()
writeFile(t, filepath.Join(src, "config", "local.yml"), "y", 0o644)
_, warnings, err := Copy(context.Background(), src, dst, []string{"config/local.yml"}, nil)
if err != nil {
t.Fatalf("Copy err: %v", err)
}
if len(warnings) != 0 {
t.Fatalf("warnings: %v", warnings)
}
if readFile(t, filepath.Join(dst, "config", "local.yml")) != "y" {
t.Fatalf("nested file not copied")
}
}
func TestCopy_MissingPattern(t *testing.T) {
t.Parallel()
src := t.TempDir()
dst := t.TempDir()
_, warnings, err := Copy(context.Background(), src, dst, []string{".env"}, nil)
if err != nil {
t.Fatalf("Copy err: %v", err)
}
if len(warnings) != 1 {
t.Fatalf("warnings = %v, want 1", warnings)
}
if !strings.Contains(warnings[0], ".env") {
t.Fatalf("warning does not mention .env: %q", warnings[0])
}
entries, _ := os.ReadDir(dst)
if len(entries) != 0 {
t.Fatalf("dst not empty: %v", entries)
}
}
func TestCopy_DoubleStarRecursive(t *testing.T) {
t.Parallel()
src := t.TempDir()
dst := t.TempDir()
writeFile(t, filepath.Join(src, ".env"), "ROOT", 0o644)
writeFile(t, filepath.Join(src, "apps", "web", ".env"), "WEB", 0o644)
writeFile(t, filepath.Join(src, "apps", "backend", ".env"), "BACK", 0o644)
writeFile(t, filepath.Join(src, "apps", "backend", "nested", ".env"), "DEEP", 0o644)
writeFile(t, filepath.Join(src, "apps", "web", "ignore.txt"), "IGN", 0o644)
_, warnings, err := Copy(context.Background(), src, dst, []string{"**/.env"}, nil)
if err != nil {
t.Fatalf("Copy err: %v", err)
}
if len(warnings) != 0 {
t.Fatalf("warnings: %v", warnings)
}
cases := map[string]string{
".env": "ROOT",
"apps/web/.env": "WEB",
"apps/backend/.env": "BACK",
"apps/backend/nested/.env": "DEEP",
}
for rel, want := range cases {
got := readFile(t, filepath.Join(dst, filepath.FromSlash(rel)))
if got != want {
t.Fatalf("%s = %q, want %q", rel, got, want)
}
}
if _, err := os.Stat(filepath.Join(dst, "apps", "web", "ignore.txt")); !os.IsNotExist(err) {
t.Fatalf("ignore.txt should not exist, err=%v", err)
}
}
func TestCopy_DoubleStarScoped(t *testing.T) {
t.Parallel()
src := t.TempDir()
dst := t.TempDir()
writeFile(t, filepath.Join(src, "apps", "web", "config.yml"), "WEB", 0o644)
writeFile(t, filepath.Join(src, "apps", "backend", "deep", "config.yml"), "BACK", 0o644)
writeFile(t, filepath.Join(src, "services", "config.yml"), "SVC", 0o644)
_, warnings, err := Copy(context.Background(), src, dst, []string{"apps/**/config.yml"}, nil)
if err != nil {
t.Fatalf("Copy err: %v", err)
}
if len(warnings) != 0 {
t.Fatalf("warnings: %v", warnings)
}
if readFile(t, filepath.Join(dst, "apps", "web", "config.yml")) != "WEB" {
t.Fatalf("apps/web/config.yml missing")
}
if readFile(t, filepath.Join(dst, "apps", "backend", "deep", "config.yml")) != "BACK" {
t.Fatalf("apps/backend/deep/config.yml missing")
}
if _, err := os.Stat(filepath.Join(dst, "services", "config.yml")); !os.IsNotExist(err) {
t.Fatalf("services/config.yml should not exist, err=%v", err)
}
}
func TestCopy_BraceAlternation(t *testing.T) {
t.Parallel()
src := t.TempDir()
dst := t.TempDir()
writeFile(t, filepath.Join(src, ".env"), "E", 0o644)
writeFile(t, filepath.Join(src, ".env.local"), "L", 0o644)
writeFile(t, filepath.Join(src, ".envrc"), "R", 0o644)
_, warnings, err := Copy(context.Background(), src, dst, []string{".env{,.local}"}, nil)
if err != nil {
t.Fatalf("Copy err: %v", err)
}
if len(warnings) != 0 {
t.Fatalf("warnings: %v", warnings)
}
if readFile(t, filepath.Join(dst, ".env")) != "E" {
t.Fatalf(".env missing")
}
if readFile(t, filepath.Join(dst, ".env.local")) != "L" {
t.Fatalf(".env.local missing")
}
if _, err := os.Stat(filepath.Join(dst, ".envrc")); !os.IsNotExist(err) {
t.Fatalf(".envrc should not be copied, err=%v", err)
}
}
// TestCopy_ParseBraceRoundTrip exercises the real ingestion path: a
// comma-separated user spec containing a brace pattern with an internal comma
// must survive Parse and reach the glob engine intact.
func TestCopy_ParseBraceRoundTrip(t *testing.T) {
t.Parallel()
src := t.TempDir()
dst := t.TempDir()
writeFile(t, filepath.Join(src, ".env"), "E", 0o644)
writeFile(t, filepath.Join(src, "config", "local.yml"), "L", 0o644)
writeFile(t, filepath.Join(src, "config", "dev.yml"), "D", 0o644)
writeFile(t, filepath.Join(src, "config", "prod.yml"), "P", 0o644)
patterns := Parse(".env, config/{local,dev}.yml")
_, warnings, err := Copy(context.Background(), src, dst, patterns, nil)
if err != nil {
t.Fatalf("Copy err: %v", err)
}
if len(warnings) != 0 {
t.Fatalf("warnings: %v", warnings)
}
if readFile(t, filepath.Join(dst, ".env")) != "E" {
t.Fatalf(".env missing")
}
if readFile(t, filepath.Join(dst, "config", "local.yml")) != "L" {
t.Fatalf("config/local.yml missing")
}
if readFile(t, filepath.Join(dst, "config", "dev.yml")) != "D" {
t.Fatalf("config/dev.yml missing")
}
if _, err := os.Stat(filepath.Join(dst, "config", "prod.yml")); !os.IsNotExist(err) {
t.Fatalf("config/prod.yml should not be copied, err=%v", err)
}
}
func TestCopy_GlobNoMatch(t *testing.T) {
t.Parallel()
src := t.TempDir()
dst := t.TempDir()
writeFile(t, filepath.Join(src, "foo.txt"), "foo", 0o644)
_, warnings, err := Copy(context.Background(), src, dst, []string{"*.local"}, nil)
if err != nil {
t.Fatalf("Copy err: %v", err)
}
if len(warnings) != 1 {
t.Fatalf("warnings = %v, want 1", warnings)
}
}
func TestCopy_PathTraversal_Relative(t *testing.T) {
t.Parallel()
parent := t.TempDir()
src := filepath.Join(parent, "src")
if err := os.Mkdir(src, 0o755); err != nil {
t.Fatalf("mkdir src: %v", err)
}
dst := t.TempDir()
// Create a file outside src
writeFile(t, filepath.Join(parent, "escape.txt"), "leak", 0o644)
_, warnings, err := Copy(context.Background(), src, dst, []string{"../escape.txt"}, nil)
if err != nil {
t.Fatalf("Copy err: %v", err)
}
if len(warnings) == 0 {
t.Fatalf("expected warning for traversal")
}
if _, err := os.Stat(filepath.Join(dst, "escape.txt")); !os.IsNotExist(err) {
t.Fatalf("escape.txt should not be copied")
}
entries, _ := os.ReadDir(dst)
if len(entries) != 0 {
t.Fatalf("dst not empty: %v", entries)
}
}
func TestCopy_PathTraversal_Absolute(t *testing.T) {
t.Parallel()
parent := t.TempDir()
src := filepath.Join(parent, "src")
if err := os.Mkdir(src, 0o755); err != nil {
t.Fatalf("mkdir src: %v", err)
}
dst := t.TempDir()
outside := filepath.Join(parent, "abs_escape.txt")
writeFile(t, outside, "leak", 0o644)
_, warnings, err := Copy(context.Background(), src, dst, []string{outside}, nil)
if err != nil {
t.Fatalf("Copy err: %v", err)
}
if len(warnings) == 0 {
t.Fatalf("expected warning for absolute traversal")
}
entries, _ := os.ReadDir(dst)
if len(entries) != 0 {
t.Fatalf("dst not empty: %v", entries)
}
}
func TestCopy_SymlinkInside(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("symlink creation often requires privilege on Windows")
}
src := t.TempDir()
dst := t.TempDir()
writeFile(t, filepath.Join(src, "real.env"), "REAL=1", 0o644)
if err := os.Symlink("real.env", filepath.Join(src, ".env")); err != nil {
t.Skipf("symlink unsupported: %v", err)
}
_, warnings, err := Copy(context.Background(), src, dst, []string{".env"}, nil)
if err != nil {
t.Fatalf("Copy err: %v", err)
}
if len(warnings) != 0 {
t.Fatalf("warnings: %v", warnings)
}
got := readFile(t, filepath.Join(dst, ".env"))
if got != "REAL=1" {
t.Fatalf("content = %q, want REAL=1", got)
}
// Confirm target is a regular file, not a symlink
info, err := os.Lstat(filepath.Join(dst, ".env"))
if err != nil {
t.Fatalf("lstat: %v", err)
}
if info.Mode()&os.ModeSymlink != 0 {
t.Fatalf("target is symlink, want regular file")
}
}
func TestCopy_SymlinkOutside(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("symlink creation often requires privilege on Windows")
}
parent := t.TempDir()
src := filepath.Join(parent, "src")
if err := os.Mkdir(src, 0o755); err != nil {
t.Fatalf("mkdir src: %v", err)
}
dst := t.TempDir()
outside := filepath.Join(parent, "secret.txt")
writeFile(t, outside, "SECRET", 0o644)
if err := os.Symlink(outside, filepath.Join(src, "leak")); err != nil {
t.Skipf("symlink unsupported: %v", err)
}
_, warnings, err := Copy(context.Background(), src, dst, []string{"leak"}, nil)
if err != nil {
t.Fatalf("Copy err: %v", err)
}
if len(warnings) == 0 {
t.Fatalf("expected warning for symlink escaping src")
}
if _, err := os.Stat(filepath.Join(dst, "leak")); !os.IsNotExist(err) {
t.Fatalf("leak should not be copied")
}
}
func TestCopy_Idempotent(t *testing.T) {
t.Parallel()
src := t.TempDir()
dst := t.TempDir()
writeFile(t, filepath.Join(src, ".env"), "SRC", 0o644)
writeFile(t, filepath.Join(dst, ".env"), "DST_ORIGINAL", 0o644)
_, warnings, err := Copy(context.Background(), src, dst, []string{".env"}, nil)
if err != nil {
t.Fatalf("Copy err: %v", err)
}
if len(warnings) != 0 {
t.Fatalf("warnings: %v", warnings)
}
got := readFile(t, filepath.Join(dst, ".env"))
if got != "DST_ORIGINAL" {
t.Fatalf("content = %q, want DST_ORIGINAL (existing file should not be overwritten)", got)
}
}
func TestCopy_EmptyPatterns(t *testing.T) {
t.Parallel()
src := t.TempDir()
dst := t.TempDir()
for _, patterns := range [][]string{nil, {}} {
_, warnings, err := Copy(context.Background(), src, dst, patterns, nil)
if err != nil {
t.Fatalf("Copy err: %v", err)
}
if len(warnings) != 0 {
t.Fatalf("warnings: %v", warnings)
}
}
}
func TestCopy_NilLogger(t *testing.T) {
t.Parallel()
src := t.TempDir()
dst := t.TempDir()
writeFile(t, filepath.Join(src, ".env"), "X", 0o644)
defer func() {
if r := recover(); r != nil {
t.Fatalf("panic with nil logger: %v", r)
}
}()
if _, _, err := Copy(context.Background(), src, dst, []string{".env"}, nil); err != nil {
t.Fatalf("Copy err: %v", err)
}
}
func TestCopy_SymlinkedSourceDir(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("symlink permission/semantics differ on Windows")
}
realDir := t.TempDir()
linkedDir := filepath.Join(t.TempDir(), "linked")
if err := os.Symlink(realDir, linkedDir); err != nil {
t.Skipf("symlinks unsupported: %v", err)
}
writeFile(t, filepath.Join(realDir, ".env"), "X=1", 0o600)
target := t.TempDir()
_, warnings, err := Copy(context.Background(), linkedDir, target, []string{".env"}, nil)
if err != nil {
t.Fatalf("Copy err: %v", err)
}
if len(warnings) != 0 {
t.Fatalf("should not reject symlinked source dir, warnings: %v", warnings)
}
got := readFile(t, filepath.Join(target, ".env"))
if got != "X=1" {
t.Fatalf("content = %q, want %q", got, "X=1")
}
}
func TestCopy_MissingSourceDir(t *testing.T) {
t.Parallel()
_, _, err := Copy(context.Background(), "/nonexistent-dir-xyz", t.TempDir(), []string{".env"}, nil)
if err == nil {
t.Fatalf("expected error for missing source dir")
}
}
func TestCopy_ContextCancelled(t *testing.T) {
t.Parallel()
src := t.TempDir()
dst := t.TempDir()
writeFile(t, filepath.Join(src, ".env"), "X", 0o644)
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, _, err := Copy(ctx, src, dst, []string{".env"}, nil)
if err == nil {
t.Fatalf("expected error from cancelled context")
}
if !errors.Is(err, context.Canceled) {
t.Fatalf("err = %v, want wrapping context.Canceled", err)
}
if _, statErr := os.Stat(filepath.Join(dst, ".env")); !os.IsNotExist(statErr) {
t.Fatalf("file should not be copied when ctx cancelled, statErr=%v", statErr)
}
}
func TestPlan_HappyPath(t *testing.T) {
t.Parallel()
src := t.TempDir()
writeFile(t, filepath.Join(src, ".env"), "X=1", 0o600)
writeFile(t, filepath.Join(src, "config", "local.yml"), "y", 0o644)
entries, warnings, err := Plan(context.Background(), src,
[]string{".env", "config"}, nil)
if err != nil {
t.Fatalf("Plan err: %v", err)
}
if len(warnings) != 0 {
t.Fatalf("unexpected warnings: %v", warnings)
}
got := map[string]Entry{}
for _, e := range entries {
got[e.RelPath] = e
}
if string(got[".env"].Content) != "X=1" {
t.Errorf(".env content = %q, want %q", got[".env"].Content, "X=1")
}
if got[".env"].Mode.Perm() != 0o600 {
t.Errorf(".env mode = %v, want 0o600", got[".env"].Mode.Perm())
}
if string(got["config/local.yml"].Content) != "y" {
t.Errorf("config/local.yml content = %q, want %q", got["config/local.yml"].Content, "y")
}
}
// TestPlan_DoubleStarRecursive mirrors TestCopy_DoubleStarRecursive but goes
// through Plan to guard against future regressions in the planMode branch.
func TestPlan_DoubleStarRecursive(t *testing.T) {
t.Parallel()
src := t.TempDir()
writeFile(t, filepath.Join(src, ".env"), "root", 0o644)
writeFile(t, filepath.Join(src, "apps", "web", ".env"), "web", 0o644)
writeFile(t, filepath.Join(src, "apps", "backend", ".env"), "backend", 0o644)
writeFile(t, filepath.Join(src, "apps", "backend", "nested", ".env"), "nested", 0o644)
writeFile(t, filepath.Join(src, "README.md"), "readme", 0o644)
entries, warnings, err := Plan(context.Background(), src, []string{"**/.env"}, nil)
if err != nil {
t.Fatalf("Plan err: %v", err)
}
if len(warnings) != 0 {
t.Fatalf("unexpected warnings: %v", warnings)
}
got := map[string]string{}
for _, e := range entries {
got[e.RelPath] = string(e.Content)
}
want := map[string]string{
".env": "root",
"apps/web/.env": "web",
"apps/backend/.env": "backend",
"apps/backend/nested/.env": "nested",
}
for rel, content := range want {
if got[rel] != content {
t.Errorf("entry %q = %q, want %q", rel, got[rel], content)
}
}
if _, ok := got["README.md"]; ok {
t.Errorf("README.md should not be planned")
}
}
func TestPlan_OversizedFileSkipped(t *testing.T) {
t.Parallel()
src := t.TempDir()
// Write a payload exceeding MaxEntryBytes (5MB). Use a sparse-ish
// buffer to avoid stressing the test runner's RAM.
huge := make([]byte, MaxEntryBytes+1)
writeFile(t, filepath.Join(src, "big.bin"), string(huge), 0o644)
writeFile(t, filepath.Join(src, ".env"), "OK=1", 0o644)
entries, warnings, err := Plan(context.Background(), src,
[]string{"big.bin", ".env"}, nil)
if err != nil {
t.Fatalf("Plan err: %v", err)
}
if len(entries) != 1 || entries[0].RelPath != ".env" {
t.Fatalf("entries = %+v, expected just .env", entries)
}
foundCapWarning := false
for _, w := range warnings {
if strings.Contains(w, "big.bin") && strings.Contains(w, "cap") {
foundCapWarning = true
}
}
if !foundCapWarning {
t.Fatalf("expected cap warning for big.bin, got %v", warnings)
}
}
func TestPlan_PathTraversalRejected(t *testing.T) {
t.Parallel()
parent := t.TempDir()
src := filepath.Join(parent, "src")
if err := os.Mkdir(src, 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
writeFile(t, filepath.Join(parent, "escape.txt"), "leak", 0o644)
entries, warnings, err := Plan(context.Background(), src,
[]string{"../escape.txt"}, nil)
if err != nil {
t.Fatalf("Plan err: %v", err)
}
if len(entries) != 0 {
t.Fatalf("expected zero entries for traversal, got %+v", entries)
}
if len(warnings) == 0 {
t.Fatal("expected warning for traversal")
}
}
func TestWriteEntries_HappyPath(t *testing.T) {
t.Parallel()
dst := t.TempDir()
entries := []Entry{
{RelPath: ".env", Mode: 0o600, Content: []byte("X=1")},
{RelPath: "config/local.yml", Mode: 0o644, Content: []byte("y")},
}
copied, warnings, err := WriteEntries(context.Background(), dst, dst, entries, nil)
if err != nil {
t.Fatalf("WriteEntries err: %v", err)
}
if len(warnings) != 0 {
t.Fatalf("unexpected warnings: %v", warnings)
}
if len(copied) != 2 {
t.Fatalf("copied = %v, want 2 entries", copied)
}
if got := readFile(t, filepath.Join(dst, ".env")); got != "X=1" {
t.Errorf(".env content = %q", got)
}
info, err := os.Stat(filepath.Join(dst, ".env"))
if err != nil {
t.Fatalf("stat: %v", err)
}
if info.Mode().Perm() != 0o600 {
t.Errorf(".env mode = %v, want 0o600", info.Mode().Perm())
}
}
func TestWriteEntries_SkipIfExists(t *testing.T) {
t.Parallel()
dst := t.TempDir()
writeFile(t, filepath.Join(dst, ".env"), "PREEXISTING", 0o644)
copied, _, err := WriteEntries(context.Background(), dst, dst,
[]Entry{{RelPath: ".env", Mode: 0o600, Content: []byte("NEW")}}, nil)
if err != nil {
t.Fatalf("WriteEntries err: %v", err)
}
if len(copied) != 0 {
t.Errorf("copied = %v, want empty (skip-if-exists)", copied)
}
if got := readFile(t, filepath.Join(dst, ".env")); got != "PREEXISTING" {
t.Errorf("existing file overwritten: %q", got)
}
}
func TestWriteEntries_RejectsTargetOutsideContainmentRoot(t *testing.T) {
t.Parallel()
root := t.TempDir()
outside := t.TempDir() // sibling tmpdir, NOT under root
_, _, err := WriteEntries(context.Background(), root, outside,
[]Entry{{RelPath: ".env", Mode: 0o600, Content: []byte("X")}}, nil)
if err == nil {
t.Fatal("expected error when target lies outside containment root")
}
if !strings.Contains(err.Error(), "outside containment root") {
t.Errorf("err = %v, want containment-root rejection", err)
}
}
func TestWriteEntries_RejectsSymlinkedParentEscape(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("symlink creation often requires privilege on Windows")
}
parent := t.TempDir()
dst := filepath.Join(parent, "dst")
if err := os.Mkdir(dst, 0o755); err != nil {
t.Fatalf("mkdir dst: %v", err)
}
outside := filepath.Join(parent, "outside")
if err := os.Mkdir(outside, 0o755); err != nil {
t.Fatalf("mkdir outside: %v", err)
}
// A pre-existing symlinked directory inside the target — `dst/config`
// points at a directory completely outside the target root. Writing
// `config/sneaky.txt` via lexical join lands at `outside/sneaky.txt`
// before the EvalSymlinks guard was added.
if err := os.Symlink(outside, filepath.Join(dst, "config")); err != nil {
t.Skipf("symlink unsupported: %v", err)
}
copied, warnings, err := WriteEntries(context.Background(), dst, dst,
[]Entry{{RelPath: "config/sneaky.txt", Mode: 0o644, Content: []byte("PWN")}}, nil)
if err != nil {
t.Fatalf("WriteEntries err: %v", err)
}
if len(copied) != 0 {
t.Errorf("copied = %v, want zero (escape rejected)", copied)
}
if len(warnings) == 0 {
t.Error("expected warning for symlinked-parent escape")
}
if _, err := os.Stat(filepath.Join(outside, "sneaky.txt")); !os.IsNotExist(err) {
t.Errorf("sneaky.txt should NOT exist outside dst; stat err = %v", err)
}
}
func TestWriteEntries_RejectsTraversalEntries(t *testing.T) {
t.Parallel()
parent := t.TempDir()
dst := filepath.Join(parent, "dst")
if err := os.Mkdir(dst, 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
bad := []Entry{
{RelPath: "../escape.txt", Mode: 0o644, Content: []byte("leak")},
{RelPath: "/etc/passwd", Mode: 0o644, Content: []byte("leak")},
{RelPath: "", Mode: 0o644, Content: []byte("x")},
}
copied, warnings, err := WriteEntries(context.Background(), dst, dst, bad, nil)
if err != nil {
t.Fatalf("WriteEntries err: %v", err)
}
if len(copied) != 0 {
t.Errorf("copied = %v, want zero (all rejected)", copied)
}
if len(warnings) != 3 {
t.Errorf("warnings = %v, want 3", warnings)
}
if _, err := os.Stat(filepath.Join(parent, "escape.txt")); !os.IsNotExist(err) {
t.Errorf("escape.txt should not exist outside dst")
}
}
func TestCopy_ReturnsCopiedRelPaths(t *testing.T) {
t.Parallel()
src := t.TempDir()
dst := t.TempDir()
writeFile(t, filepath.Join(src, ".env"), "X=1", 0o644)
writeFile(t, filepath.Join(src, "config", "local.yml"), "y", 0o644)
writeFile(t, filepath.Join(src, "config", "sub", "dev.json"), "j", 0o644)
// Pre-existing dst file should be skipped and NOT appear in copied.
writeFile(t, filepath.Join(src, "already.txt"), "src", 0o644)
writeFile(t, filepath.Join(dst, "already.txt"), "preexisting", 0o644)
copied, warnings, err := Copy(context.Background(), src, dst,
[]string{".env", "config", "already.txt"}, nil)
if err != nil {
t.Fatalf("Copy err: %v", err)
}
if len(warnings) != 0 {
t.Fatalf("unexpected warnings: %v", warnings)
}
got := map[string]bool{}
for _, p := range copied {
got[p] = true
}
want := []string{".env", "config/local.yml", "config/sub/dev.json"}
for _, w := range want {
if !got[w] {
t.Errorf("expected copied list to contain %q, got %v", w, copied)
}
}
if got["already.txt"] {
t.Errorf("skip-if-exists entry should NOT appear in copied list, got %v", copied)
}
if len(copied) != len(want) {
t.Errorf("copied len = %d, want %d (list: %v)", len(copied), len(want), copied)
}
}