diff --git a/cmd/root_test.go b/cmd/root_test.go index 2c510e8..d4feb3c 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -76,6 +76,61 @@ func TestRunRecord(t *testing.T) { }) } +func TestRunRecordWithExplicitTestDirPath(t *testing.T) { + root := t.TempDir() + wantDir := filepath.Join(root, "e2e") + if err := os.MkdirAll(wantDir, 0o755); err != nil { + t.Fatalf("MkdirAll() error = %v", err) + } + + withWorkingDir(t, root, func() { + stdout, stderr := captureOutput(t, func() { + if got := Run([]string{"record", filepath.Join("e2e", "suite", "spec")}); got != 0 { + t.Fatalf("Run() code = %d, want %d", got, 0) + } + }) + + createdPath := filepath.Join(wantDir, "suite", "spec") + if stdout != prefixed(createdPath+"\n") { + t.Fatalf("stdout = %q, want %q", stdout, prefixed(createdPath+"\n")) + } + if stderr != "" { + t.Fatalf("stderr = %q, want empty", stderr) + } + + if _, err := os.Stat(createdPath); err != nil { + t.Fatalf("Stat() error = %v", err) + } + }) +} + +func TestRunRecordRejectsAbsolutePathOutsideTestDir(t *testing.T) { + root := t.TempDir() + wantDir := filepath.Join(root, "e2e") + if err := os.MkdirAll(wantDir, 0o755); err != nil { + t.Fatalf("MkdirAll() error = %v", err) + } + + outside := filepath.Join(root, "outside", "spec") + withWorkingDir(t, root, func() { + stdout, stderr := captureOutput(t, func() { + if got := Run([]string{"record", outside}); got != 1 { + t.Fatalf("Run() code = %d, want %d", got, 1) + } + }) + + if stdout != "" { + t.Fatalf("stdout = %q, want empty", stdout) + } + if !strings.Contains(stderr, "must be inside test directory") { + t.Fatalf("stderr = %q, want test directory error", stderr) + } + if _, err := os.Stat(outside); !os.IsNotExist(err) { + t.Fatalf("Stat(%q) error = %v, want not exists", outside, err) + } + }) +} + func TestRunRecordMissingPath(t *testing.T) { stdout, stderr := captureOutput(t, func() { if got := Run([]string{"record"}); got != 1 { diff --git a/internal/miro/record.go b/internal/miro/record.go index f1e1817..206323b 100644 --- a/internal/miro/record.go +++ b/internal/miro/record.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" ) // Record creates the requested directory path under the resolved test directory. @@ -13,10 +14,44 @@ func Record(path string) (string, error) { return "", fmt.Errorf("failed to resolve test directory: %v", err) } - target := filepath.Join(testDir, path) + target, err := resolveRecordTarget(testDir, path) + if err != nil { + return "", err + } + if err := os.MkdirAll(target, 0o755); err != nil { return "", err } return target, nil } + +func resolveRecordTarget(testDir, path string) (string, error) { + absTestDir, err := filepath.Abs(testDir) + if err != nil { + return "", fmt.Errorf("failed to resolve test directory path: %v", err) + } + + absPath, err := filepath.Abs(path) + if err != nil { + return "", fmt.Errorf("failed to resolve record path: %v", err) + } + + // path rel to testDir + relToTestDir, err := filepath.Rel(absTestDir, absPath) + if err == nil && isWithinBase(relToTestDir) { + return filepath.Join(absTestDir, relToTestDir), nil + } + + // not rel to test dir and absolute path given + if filepath.IsAbs(path) { + return "", fmt.Errorf("record path %q must be inside test directory %q", path, absTestDir) + } + + return filepath.Join(absTestDir, path), nil +} + +// is the rel path going "outside" +func isWithinBase(rel string) bool { + return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator))) +} diff --git a/internal/miro/record_test.go b/internal/miro/record_test.go new file mode 100644 index 0000000..1b22776 --- /dev/null +++ b/internal/miro/record_test.go @@ -0,0 +1,75 @@ +package miro + +import ( + "os" + "path/filepath" + "testing" +) + +func TestRecordCreatesRelativePath(t *testing.T) { + root := t.TempDir() + testDir := filepath.Join(root, "e2e") + mustMkdirAll(t, testDir) + + got := withWorkingDir(t, root, func() string { + path, err := Record(filepath.Join("a", "b", "c") + string(os.PathSeparator)) + if err != nil { + t.Fatalf("Record() error = %v", err) + } + return path + }) + + want := filepath.Join(testDir, "a", "b", "c") + if got != want { + t.Fatalf("Record() = %q, want %q", got, want) + } + + info, err := os.Stat(want) + if err != nil { + t.Fatalf("Stat() error = %v", err) + } + if !info.IsDir() { + t.Fatalf("created path %q is not a directory", want) + } +} + +func TestRecordStripsExplicitTestDirPrefix(t *testing.T) { + root := t.TempDir() + testDir := filepath.Join(root, "e2e") + mustMkdirAll(t, testDir) + + got := withWorkingDir(t, root, func() string { + path, err := Record(filepath.Join("e2e", "a", "b", "c")) + if err != nil { + t.Fatalf("Record() error = %v", err) + } + return path + }) + + want := filepath.Join(testDir, "a", "b", "c") + if got != want { + t.Fatalf("Record() = %q, want %q", got, want) + } + + if _, err := os.Stat(want); err != nil { + t.Fatalf("Stat() error = %v", err) + } +} + +func TestRecordRejectsAbsolutePathOutsideTestDir(t *testing.T) { + root := t.TempDir() + mustMkdirAll(t, filepath.Join(root, "e2e")) + outside := filepath.Join(root, "outside", "a", "b", "c") + + err := withWorkingDir(t, root, func() error { + _, err := Record(outside) + return err + }) + + if err == nil { + t.Fatal("Record() error = nil, want error") + } + if _, statErr := os.Stat(outside); !os.IsNotExist(statErr) { + t.Fatalf("Stat(%q) error = %v, want not exists", outside, statErr) + } +}