Files
gosick/internal/homesick/core/helpers_test.go
2026-03-21 20:52:13 +00:00

280 lines
8.4 KiB
Go

package core
import (
"bytes"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
type errReader struct{}
func (errReader) Read(_ []byte) (int, error) {
return 0, errors.New("boom")
}
type errWriter struct{}
func (errWriter) Write(_ []byte) (int, error) {
return 0, errors.New("boom")
}
func TestRunGitPretendWritesStatus(t *testing.T) {
stdout := &bytes.Buffer{}
app := &App{Stdout: stdout, Stderr: bytes.NewBuffer(nil), Pretend: true}
err := app.runGit("/tmp", "status")
require.NoError(t, err)
require.Contains(t, stdout.String(), "Would execute git status in /tmp")
}
func TestActionVerb(t *testing.T) {
app := &App{Pretend: true}
require.Equal(t, "Would execute", app.actionVerb())
app.Pretend = false
require.Equal(t, "Executing", app.actionVerb())
}
func TestSayStatusHonorsQuiet(t *testing.T) {
stdout := &bytes.Buffer{}
app := &App{Stdout: stdout, Quiet: true}
app.sayStatus("git", "status")
require.Empty(t, stdout.String())
app.Quiet = false
app.sayStatus("git", "status")
require.Contains(t, stdout.String(), "git: status")
}
func TestUnlinkPath(t *testing.T) {
t.Run("missing destination", func(t *testing.T) {
err := unlinkPath(filepath.Join(t.TempDir(), "does-not-exist"))
require.NoError(t, err)
})
t.Run("regular file is preserved", func(t *testing.T) {
dir := t.TempDir()
target := filepath.Join(dir, "regular")
require.NoError(t, os.WriteFile(target, []byte("x"), 0o644))
err := unlinkPath(target)
require.NoError(t, err)
require.FileExists(t, target)
})
t.Run("symlink is removed", func(t *testing.T) {
dir := t.TempDir()
source := filepath.Join(dir, "source")
destination := filepath.Join(dir, "dest")
require.NoError(t, os.WriteFile(source, []byte("x"), 0o644))
require.NoError(t, os.Symlink(source, destination))
err := unlinkPath(destination)
require.NoError(t, err)
_, statErr := os.Lstat(destination)
require.ErrorIs(t, statErr, os.ErrNotExist)
})
}
func TestLinkPath(t *testing.T) {
t.Run("existing symlink to same source is no-op", func(t *testing.T) {
dir := t.TempDir()
source := filepath.Join(dir, "source")
destination := filepath.Join(dir, "dest")
require.NoError(t, os.WriteFile(source, []byte("x"), 0o644))
absSource, err := filepath.Abs(source)
require.NoError(t, err)
require.NoError(t, os.Symlink(absSource, destination))
app := &App{Stdout: bytes.NewBuffer(nil), Stderr: bytes.NewBuffer(nil)}
err = app.linkPath(source, destination)
require.NoError(t, err)
})
t.Run("conflict without force errors", func(t *testing.T) {
dir := t.TempDir()
source := filepath.Join(dir, "source")
destination := filepath.Join(dir, "dest")
require.NoError(t, os.WriteFile(source, []byte("x"), 0o644))
require.NoError(t, os.WriteFile(destination, []byte("existing"), 0o644))
app := &App{Stdout: bytes.NewBuffer(nil), Stderr: bytes.NewBuffer(nil)}
err := app.linkPath(source, destination)
require.Error(t, err)
require.Contains(t, err.Error(), "exists")
})
t.Run("force replaces existing destination", func(t *testing.T) {
dir := t.TempDir()
source := filepath.Join(dir, "source")
destination := filepath.Join(dir, "dest")
require.NoError(t, os.WriteFile(source, []byte("x"), 0o644))
require.NoError(t, os.WriteFile(destination, []byte("existing"), 0o644))
app := &App{Stdout: bytes.NewBuffer(nil), Stderr: bytes.NewBuffer(nil), Force: true}
err := app.linkPath(source, destination)
require.NoError(t, err)
info, statErr := os.Lstat(destination)
require.NoError(t, statErr)
require.True(t, info.Mode()&os.ModeSymlink != 0)
})
t.Run("create destination parent error includes context", func(t *testing.T) {
dir := t.TempDir()
source := filepath.Join(dir, "source")
blocker := filepath.Join(dir, "blocker")
require.NoError(t, os.WriteFile(source, []byte("x"), 0o644))
require.NoError(t, os.WriteFile(blocker, []byte("x"), 0o644))
app := &App{Stdout: bytes.NewBuffer(nil), Stderr: bytes.NewBuffer(nil)}
err := app.linkPath(source, filepath.Join(blocker, "dest"))
require.Error(t, err)
require.Contains(t, err.Error(), "create destination parent")
})
}
func TestReadSubdirsAndMatchesIgnoredDir(t *testing.T) {
dir := t.TempDir()
meta := filepath.Join(dir, ".homesick_subdir")
require.NoError(t, os.WriteFile(meta, []byte(" .config/myapp \n\n"), 0o644))
subdirs, err := readSubdirs(meta)
require.NoError(t, err)
require.Equal(t, []string{filepath.Clean(".config/myapp")}, subdirs)
castleHome := filepath.Join(dir, "castle", "home")
candidate := filepath.Join(castleHome, ".config")
ignored, err := matchesIgnoredDir(castleHome, candidate, subdirs)
require.NoError(t, err)
require.True(t, ignored)
notIgnored, err := matchesIgnoredDir(castleHome, filepath.Join(castleHome, ".vim"), subdirs)
require.NoError(t, err)
require.False(t, notIgnored)
}
func TestReadSubdirsReadErrorIncludesContext(t *testing.T) {
_, err := readSubdirs(t.TempDir())
require.Error(t, err)
require.Contains(t, err.Error(), "read subdirs")
}
func TestPullAndPushDefaultCastlePretend(t *testing.T) {
dir := t.TempDir()
stdout := &bytes.Buffer{}
app := &App{
HomeDir: dir,
ReposDir: filepath.Join(dir, ".homesick", "repos"),
Stdout: stdout,
Stderr: bytes.NewBuffer(nil),
Pretend: true,
}
require.NoError(t, app.Pull(""))
require.NoError(t, app.Push(""))
out := stdout.String()
require.Contains(t, out, "git pull")
require.Contains(t, out, "git push")
require.Contains(t, out, filepath.Join(app.ReposDir, "dotfiles"))
}
func TestGenerateRequiresPath(t *testing.T) {
app := &App{Stdout: bytes.NewBuffer(nil), Stderr: bytes.NewBuffer(nil)}
err := app.Generate(" ")
require.Error(t, err)
require.Contains(t, err.Error(), "generate requires PATH")
}
func TestLinkAndUnlinkDefaultCastle(t *testing.T) {
dir := t.TempDir()
homeDir := filepath.Join(dir, "home")
reposDir := filepath.Join(homeDir, ".homesick", "repos")
castleHome := filepath.Join(reposDir, "dotfiles", "home")
require.NoError(t, os.MkdirAll(castleHome, 0o755))
source := filepath.Join(castleHome, ".vimrc")
require.NoError(t, os.WriteFile(source, []byte("set number\n"), 0o644))
app := &App{HomeDir: homeDir, ReposDir: reposDir, Stdout: bytes.NewBuffer(nil), Stderr: bytes.NewBuffer(nil)}
require.NoError(t, app.Link(""))
destination := filepath.Join(homeDir, ".vimrc")
info, err := os.Lstat(destination)
require.NoError(t, err)
require.True(t, info.Mode()&os.ModeSymlink != 0)
require.NoError(t, app.Unlink(""))
_, err = os.Lstat(destination)
require.ErrorIs(t, err, os.ErrNotExist)
}
func TestLinkAndUnlinkCastleMissingError(t *testing.T) {
dir := t.TempDir()
app := &App{
HomeDir: filepath.Join(dir, "home"),
ReposDir: filepath.Join(dir, "home", ".homesick", "repos"),
Stdout: bytes.NewBuffer(nil),
Stderr: bytes.NewBuffer(nil),
}
err := app.LinkCastle("missing")
require.Error(t, err)
require.Contains(t, err.Error(), "could not symlink")
err = app.UnlinkCastle("missing")
require.Error(t, err)
require.Contains(t, err.Error(), "could not symlink")
}
func TestConfirmDestroyResponses(t *testing.T) {
stdout := &bytes.Buffer{}
app := &App{Stdout: stdout, Stdin: strings.NewReader("yes\n")}
ok, err := app.confirmDestroy("dotfiles")
require.NoError(t, err)
require.True(t, ok)
require.Contains(t, stdout.String(), "Destroy castle \"dotfiles\"?")
stdout.Reset()
app.Stdin = strings.NewReader("n\n")
ok, err = app.confirmDestroy("dotfiles")
require.NoError(t, err)
require.False(t, ok)
}
func TestConfirmDestroyReadError(t *testing.T) {
app := &App{Stdout: bytes.NewBuffer(nil), Stdin: errReader{}}
ok, err := app.confirmDestroy("dotfiles")
require.Error(t, err)
require.False(t, ok)
require.Contains(t, err.Error(), "read destroy confirmation")
}
func TestConfirmDestroyWriteError(t *testing.T) {
app := &App{Stdout: errWriter{}, Stdin: strings.NewReader("yes\n")}
ok, err := app.confirmDestroy("dotfiles")
require.Error(t, err)
require.False(t, ok)
require.Contains(t, err.Error(), "write destroy prompt")
}
func TestExecAllWrapsCastleError(t *testing.T) {
dir := t.TempDir()
homeDir := filepath.Join(dir, "home")
reposDir := filepath.Join(homeDir, ".homesick", "repos")
require.NoError(t, os.MkdirAll(filepath.Join(reposDir, "broken", ".git"), 0o755))
app := &App{HomeDir: homeDir, ReposDir: reposDir, Stdout: bytes.NewBuffer(nil), Stderr: bytes.NewBuffer(nil)}
err := app.ExecAll([]string{"exit 3"})
require.Error(t, err)
require.Contains(t, err.Error(), "exec_all failed for \"broken\"")
}