package cli_test import ( "bytes" "os" "path/filepath" "testing" "time" "git.hrafn.xyz/aether/gosick/internal/homesick/cli" "git.hrafn.xyz/aether/gosick/internal/homesick/version" git "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing/object" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) type CLISuite struct { suite.Suite homeDir string stdout *bytes.Buffer stderr *bytes.Buffer } func TestCLISuite(t *testing.T) { suite.Run(t, new(CLISuite)) } func (s *CLISuite) SetupTest() { s.homeDir = filepath.Join(s.T().TempDir(), "home") require.NoError(s.T(), os.MkdirAll(s.homeDir, 0o755)) require.NoError(s.T(), os.Setenv("HOME", s.homeDir)) s.stdout = &bytes.Buffer{} s.stderr = &bytes.Buffer{} } func (s *CLISuite) createCastleRepo(castle string) string { castleRoot := filepath.Join(s.homeDir, ".homesick", "repos", castle) repo, err := git.PlainInit(castleRoot, false) require.NoError(s.T(), err) filePath := filepath.Join(castleRoot, "home", ".vimrc") require.NoError(s.T(), os.MkdirAll(filepath.Dir(filePath), 0o755)) require.NoError(s.T(), os.WriteFile(filePath, []byte("set number\n"), 0o644)) wt, err := repo.Worktree() require.NoError(s.T(), err) _, err = wt.Add("home/.vimrc") require.NoError(s.T(), err) _, err = wt.Commit("initial", &git.CommitOptions{Author: &object.Signature{ Name: "Behavior Test", Email: "behavior@test.local", When: time.Now(), }}) require.NoError(s.T(), err) _, err = repo.CreateRemote(&config.RemoteConfig{Name: "origin", URLs: []string{"git://example.com/test.git"}}) require.NoError(s.T(), err) return castleRoot } func (s *CLISuite) TestRun_VersionAliases() { for _, args := range [][]string{{"-v"}, {"--version"}, {"version"}} { s.stdout.Reset() s.stderr.Reset() exitCode := cli.Run(args, s.stdout, s.stderr) require.Equal(s.T(), 0, exitCode) require.Equal(s.T(), version.String+"\n", s.stdout.String()) require.Empty(s.T(), s.stderr.String()) } } func (s *CLISuite) TestRun_ShowPath_DefaultCastle() { exitCode := cli.Run([]string{"show_path"}, s.stdout, s.stderr) require.Equal(s.T(), 0, exitCode) require.Equal(s.T(), filepath.Join(s.homeDir, ".homesick", "repos", "dotfiles")+"\n", s.stdout.String()) require.Empty(s.T(), s.stderr.String()) } func (s *CLISuite) TestRun_Cd_DefaultCastle() { exitCode := cli.Run([]string{"cd"}, s.stdout, s.stderr) require.Equal(s.T(), 0, exitCode) require.Equal(s.T(), filepath.Join(s.homeDir, ".homesick", "repos", "dotfiles")+"\n", s.stdout.String()) require.Empty(s.T(), s.stderr.String()) } func (s *CLISuite) TestRun_Cd_ExplicitCastle() { exitCode := cli.Run([]string{"cd", "work"}, s.stdout, s.stderr) require.Equal(s.T(), 0, exitCode) require.Equal(s.T(), filepath.Join(s.homeDir, ".homesick", "repos", "work")+"\n", s.stdout.String()) require.Empty(s.T(), s.stderr.String()) } func (s *CLISuite) TestRun_Exec_RunsCommandInCastleRoot() { castleRoot := filepath.Join(s.homeDir, ".homesick", "repos", "dotfiles") require.NoError(s.T(), os.MkdirAll(castleRoot, 0o755)) exitCode := cli.Run([]string{"exec", "dotfiles", "pwd"}, s.stdout, s.stderr) require.Equal(s.T(), 0, exitCode) require.Contains(s.T(), s.stdout.String(), castleRoot) require.Empty(s.T(), s.stderr.String()) } func (s *CLISuite) TestRun_Exec_WithPretend_DoesNotExecute() { castleRoot := filepath.Join(s.homeDir, ".homesick", "repos", "dotfiles") require.NoError(s.T(), os.MkdirAll(castleRoot, 0o755)) target := filepath.Join(castleRoot, "should-not-exist") exitCode := cli.Run([]string{"--pretend", "exec", "dotfiles", "touch should-not-exist"}, s.stdout, s.stderr) require.Equal(s.T(), 0, exitCode) require.NoFileExists(s.T(), target) require.Contains(s.T(), s.stdout.String(), "Would execute") require.Empty(s.T(), s.stderr.String()) } func (s *CLISuite) TestRun_Exec_WithDryRunAlias_DoesNotExecute() { castleRoot := filepath.Join(s.homeDir, ".homesick", "repos", "dotfiles") require.NoError(s.T(), os.MkdirAll(castleRoot, 0o755)) target := filepath.Join(castleRoot, "should-not-exist") exitCode := cli.Run([]string{"--dry-run", "exec", "dotfiles", "touch should-not-exist"}, s.stdout, s.stderr) require.Equal(s.T(), 0, exitCode) require.NoFileExists(s.T(), target) require.Contains(s.T(), s.stdout.String(), "Would execute") require.Empty(s.T(), s.stderr.String()) } func (s *CLISuite) TestRun_Exec_WithQuiet_SuppressesStatusOutput() { castleRoot := filepath.Join(s.homeDir, ".homesick", "repos", "dotfiles") require.NoError(s.T(), os.MkdirAll(castleRoot, 0o755)) exitCode := cli.Run([]string{"--quiet", "exec", "dotfiles", "true"}, s.stdout, s.stderr) require.Equal(s.T(), 0, exitCode) require.Empty(s.T(), s.stdout.String()) require.Empty(s.T(), s.stderr.String()) } func (s *CLISuite) TestRun_PullAll_NoCastlesIsNoop() { exitCode := cli.Run([]string{"pull", "--all"}, s.stdout, s.stderr) require.Equal(s.T(), 0, exitCode) require.Empty(s.T(), s.stderr.String()) } func (s *CLISuite) TestRun_Rc_HomesickrcRequiresForce() { castleRoot := filepath.Join(s.homeDir, ".homesick", "repos", "dotfiles") require.NoError(s.T(), os.MkdirAll(castleRoot, 0o755)) require.NoError(s.T(), os.WriteFile(filepath.Join(castleRoot, ".homesickrc"), []byte("# ruby\n"), 0o644)) exitCode := cli.Run([]string{"rc", "dotfiles"}, s.stdout, s.stderr) require.NotEqual(s.T(), 0, exitCode) require.Contains(s.T(), s.stderr.String(), "--force") } func (s *CLISuite) TestRun_Rc_WithForceRuns() { castleRoot := filepath.Join(s.homeDir, ".homesick", "repos", "dotfiles") require.NoError(s.T(), os.MkdirAll(castleRoot, 0o755)) require.NoError(s.T(), os.WriteFile(filepath.Join(castleRoot, ".homesickrc"), []byte("# ruby\n"), 0o644)) exitCode := cli.Run([]string{"rc", "--force", "dotfiles"}, s.stdout, s.stderr) require.Equal(s.T(), 0, exitCode) require.Empty(s.T(), s.stderr.String()) } func (s *CLISuite) TestRun_CloneSubcommandHelp() { exitCode := cli.Run([]string{"clone", "--help"}, s.stdout, s.stderr) require.Equal(s.T(), 0, exitCode) require.Contains(s.T(), s.stdout.String(), "clone") require.Contains(s.T(), s.stdout.String(), "URI") require.Empty(s.T(), s.stderr.String()) } func (s *CLISuite) TestRun_Help_UsesProgramNameAndDescription() { originalArgs := os.Args s.T().Cleanup(func() { os.Args = originalArgs }) os.Args = []string{"gosick"} exitCode := cli.Run([]string{"--help"}, s.stdout, s.stderr) require.Equal(s.T(), 0, exitCode) require.Contains(s.T(), s.stdout.String(), "Usage: gosick") require.NotContains(s.T(), s.stdout.String(), "Usage: homesick") require.Contains(s.T(), s.stdout.String(), "precious dotfiles") require.Empty(s.T(), s.stderr.String()) } func (s *CLISuite) TestRun_SymlinkAlias_MatchesLinkCommand() { castleRoot := filepath.Join(s.homeDir, ".homesick", "repos", "dotfiles") castleHome := filepath.Join(castleRoot, "home") require.NoError(s.T(), os.MkdirAll(castleHome, 0o755)) require.NoError(s.T(), os.WriteFile(filepath.Join(castleHome, ".vimrc"), []byte("set number\n"), 0o644)) exitCode := cli.Run([]string{"symlink", "dotfiles"}, s.stdout, s.stderr) require.Equal(s.T(), 0, exitCode) target := filepath.Join(s.homeDir, ".vimrc") info, err := os.Lstat(target) require.NoError(s.T(), err) require.True(s.T(), info.Mode()&os.ModeSymlink != 0) require.Empty(s.T(), s.stderr.String()) } func (s *CLISuite) TestRun_Commit_PositionalMessageCompatibility() { exitCode := cli.Run([]string{"--pretend", "commit", "dotfiles", "behavior-suite-commit"}, s.stdout, s.stderr) require.Equal(s.T(), 0, exitCode) require.Contains(s.T(), s.stdout.String(), "git commit -m behavior-suite-commit") require.Empty(s.T(), s.stderr.String()) } func (s *CLISuite) TestRun_List_NoArguments() { s.createCastleRepo("dotfiles") exitCode := cli.Run([]string{"list"}, s.stdout, s.stderr) require.Equal(s.T(), 0, exitCode) require.Contains(s.T(), s.stdout.String(), "dotfiles") require.Empty(s.T(), s.stderr.String()) } func (s *CLISuite) TestRun_Generate_CreatesNewCastle() { castlePath := filepath.Join(s.T().TempDir(), "my-castle") exitCode := cli.Run([]string{"generate", castlePath}, s.stdout, s.stderr) require.Equal(s.T(), 0, exitCode) require.DirExists(s.T(), filepath.Join(castlePath, ".git")) require.DirExists(s.T(), filepath.Join(castlePath, "home")) } func (s *CLISuite) TestRun_Clone_WithoutArgs() { exitCode := cli.Run([]string{"clone"}, s.stdout, s.stderr) // Clone requires arguments, should fail require.NotEqual(s.T(), 0, exitCode) } func (s *CLISuite) TestRun_Status_DefaultCastle() { castleRoot := s.createCastleRepo("dotfiles") require.NoError(s.T(), os.WriteFile(filepath.Join(castleRoot, "home", ".vimrc"), []byte("changed\n"), 0o644)) exitCode := cli.Run([]string{"status"}, s.stdout, s.stderr) require.Equal(s.T(), 0, exitCode) require.Contains(s.T(), s.stdout.String(), "modified:") require.Empty(s.T(), s.stderr.String()) } func (s *CLISuite) TestRun_Diff_DefaultCastle() { castleRoot := s.createCastleRepo("dotfiles") require.NoError(s.T(), os.WriteFile(filepath.Join(castleRoot, "home", ".vimrc"), []byte("changed\n"), 0o644)) exitCode := cli.Run([]string{"diff"}, s.stdout, s.stderr) require.Equal(s.T(), 0, exitCode) require.Contains(s.T(), s.stdout.String(), "diff --git") require.Empty(s.T(), s.stderr.String()) }