mirror of
https://github.com/yarlson/lnk.git
synced 2025-08-31 18:01:41 +02:00
refactor(cmd): improve testability and error handling in CLI commands
This commit is contained in:
42
cmd/add.go
42
cmd/add.go
@@ -8,28 +8,26 @@ import (
|
||||
"github.com/yarlson/lnk/internal/core"
|
||||
)
|
||||
|
||||
var addCmd = &cobra.Command{
|
||||
Use: "add <file>",
|
||||
Short: "✨ Add a file to lnk management",
|
||||
Long: "Moves a file to the lnk repository and creates a symlink in its place.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
filePath := args[0]
|
||||
func newAddCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "add <file>",
|
||||
Short: "✨ Add a file to lnk management",
|
||||
Long: "Moves a file to the lnk repository and creates a symlink in its place.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
filePath := args[0]
|
||||
|
||||
lnk := core.NewLnk()
|
||||
if err := lnk.Add(filePath); err != nil {
|
||||
return fmt.Errorf("failed to add file: %w", err)
|
||||
}
|
||||
lnk := core.NewLnk()
|
||||
if err := lnk.Add(filePath); err != nil {
|
||||
return fmt.Errorf("failed to add file: %w", err)
|
||||
}
|
||||
|
||||
basename := filepath.Base(filePath)
|
||||
fmt.Printf("✨ \033[1mAdded %s to lnk\033[0m\n", basename)
|
||||
fmt.Printf(" 🔗 \033[90m%s\033[0m → \033[36m~/.config/lnk/%s\033[0m\n", filePath, basename)
|
||||
fmt.Printf(" 📝 Use \033[1mlnk push\033[0m to sync to remote\n")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(addCmd)
|
||||
basename := filepath.Base(filePath)
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "✨ \033[1mAdded %s to lnk\033[0m\n", basename)
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 🔗 \033[90m%s\033[0m → \033[36m~/.config/lnk/%s\033[0m\n", filePath, basename)
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 📝 Use \033[1mlnk push\033[0m to sync to remote\n")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
64
cmd/init.go
64
cmd/init.go
@@ -7,39 +7,39 @@ import (
|
||||
"github.com/yarlson/lnk/internal/core"
|
||||
)
|
||||
|
||||
var initCmd = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "🎯 Initialize a new lnk repository",
|
||||
Long: "Creates the lnk directory and initializes a Git repository for managing dotfiles.",
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
remote, _ := cmd.Flags().GetString("remote")
|
||||
func newInitCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "🎯 Initialize a new lnk repository",
|
||||
Long: "Creates the lnk directory and initializes a Git repository for managing dotfiles.",
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
remote, _ := cmd.Flags().GetString("remote")
|
||||
|
||||
lnk := core.NewLnk()
|
||||
if err := lnk.InitWithRemote(remote); err != nil {
|
||||
return fmt.Errorf("failed to initialize lnk: %w", err)
|
||||
}
|
||||
lnk := core.NewLnk()
|
||||
if err := lnk.InitWithRemote(remote); err != nil {
|
||||
return fmt.Errorf("failed to initialize lnk: %w", err)
|
||||
}
|
||||
|
||||
if remote != "" {
|
||||
fmt.Printf("🎯 \033[1mInitialized lnk repository\033[0m\n")
|
||||
fmt.Printf(" 📦 Cloned from: \033[36m%s\033[0m\n", remote)
|
||||
fmt.Printf(" 📁 Location: \033[90m~/.config/lnk\033[0m\n")
|
||||
fmt.Printf("\n💡 \033[33mNext steps:\033[0m\n")
|
||||
fmt.Printf(" • Run \033[1mlnk pull\033[0m to restore symlinks\n")
|
||||
fmt.Printf(" • Use \033[1mlnk add <file>\033[0m to manage new files\n")
|
||||
} else {
|
||||
fmt.Printf("🎯 \033[1mInitialized empty lnk repository\033[0m\n")
|
||||
fmt.Printf(" 📁 Location: \033[90m~/.config/lnk\033[0m\n")
|
||||
fmt.Printf("\n💡 \033[33mNext steps:\033[0m\n")
|
||||
fmt.Printf(" • Run \033[1mlnk add <file>\033[0m to start managing dotfiles\n")
|
||||
fmt.Printf(" • Add a remote with: \033[1mgit remote add origin <url>\033[0m\n")
|
||||
}
|
||||
if remote != "" {
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "🎯 \033[1mInitialized lnk repository\033[0m\n")
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 📦 Cloned from: \033[36m%s\033[0m\n", remote)
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 📁 Location: \033[90m~/.config/lnk\033[0m\n")
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\n💡 \033[33mNext steps:\033[0m\n")
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " • Run \033[1mlnk pull\033[0m to restore symlinks\n")
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " • Use \033[1mlnk add <file>\033[0m to manage new files\n")
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "🎯 \033[1mInitialized empty lnk repository\033[0m\n")
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 📁 Location: \033[90m~/.config/lnk\033[0m\n")
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\n💡 \033[33mNext steps:\033[0m\n")
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " • Run \033[1mlnk add <file>\033[0m to start managing dotfiles\n")
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " • Add a remote with: \033[1mgit remote add origin <url>\033[0m\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
initCmd.Flags().StringP("remote", "r", "", "Clone from remote URL instead of creating empty repository")
|
||||
rootCmd.AddCommand(initCmd)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringP("remote", "r", "", "Clone from remote URL instead of creating empty repository")
|
||||
return cmd
|
||||
}
|
||||
|
62
cmd/pull.go
62
cmd/pull.go
@@ -7,39 +7,37 @@ import (
|
||||
"github.com/yarlson/lnk/internal/core"
|
||||
)
|
||||
|
||||
var pullCmd = &cobra.Command{
|
||||
Use: "pull",
|
||||
Short: "⬇️ Pull changes from remote and restore symlinks",
|
||||
Long: "Fetches changes from remote repository and automatically restores symlinks for all managed files.",
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
lnk := core.NewLnk()
|
||||
restored, err := lnk.Pull()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to pull changes: %w", err)
|
||||
}
|
||||
|
||||
if len(restored) > 0 {
|
||||
fmt.Printf("⬇️ \033[1;32mSuccessfully pulled changes\033[0m\n")
|
||||
fmt.Printf(" 🔗 Restored \033[1m%d symlink", len(restored))
|
||||
if len(restored) > 1 {
|
||||
fmt.Printf("s")
|
||||
func newPullCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "pull",
|
||||
Short: "⬇️ Pull changes from remote and restore symlinks",
|
||||
Long: "Fetches changes from remote repository and automatically restores symlinks for all managed files.",
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
lnk := core.NewLnk()
|
||||
restored, err := lnk.Pull()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to pull changes: %w", err)
|
||||
}
|
||||
fmt.Printf("\033[0m:\n")
|
||||
for _, file := range restored {
|
||||
fmt.Printf(" ✨ \033[36m%s\033[0m\n", file)
|
||||
|
||||
if len(restored) > 0 {
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "⬇️ \033[1;32mSuccessfully pulled changes\033[0m\n")
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 🔗 Restored \033[1m%d symlink", len(restored))
|
||||
if len(restored) > 1 {
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "s")
|
||||
}
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\033[0m:\n")
|
||||
for _, file := range restored {
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " ✨ \033[36m%s\033[0m\n", file)
|
||||
}
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\n 🎉 Your dotfiles are synced and ready!\n")
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "⬇️ \033[1;32mSuccessfully pulled changes\033[0m\n")
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " ✅ All symlinks already in place\n")
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 🎉 Everything is up to date!\n")
|
||||
}
|
||||
fmt.Printf("\n 🎉 Your dotfiles are synced and ready!\n")
|
||||
} else {
|
||||
fmt.Printf("⬇️ \033[1;32mSuccessfully pulled changes\033[0m\n")
|
||||
fmt.Printf(" ✅ All symlinks already in place\n")
|
||||
fmt.Printf(" 🎉 Everything is up to date!\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(pullCmd)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
48
cmd/push.go
48
cmd/push.go
@@ -7,31 +7,29 @@ import (
|
||||
"github.com/yarlson/lnk/internal/core"
|
||||
)
|
||||
|
||||
var pushCmd = &cobra.Command{
|
||||
Use: "push [message]",
|
||||
Short: "🚀 Push local changes to remote repository",
|
||||
Long: "Stages all changes, creates a sync commit with the provided message, and pushes to remote.",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
message := "lnk: sync configuration files"
|
||||
if len(args) > 0 {
|
||||
message = args[0]
|
||||
}
|
||||
func newPushCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "push [message]",
|
||||
Short: "🚀 Push local changes to remote repository",
|
||||
Long: "Stages all changes, creates a sync commit with the provided message, and pushes to remote.",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
message := "lnk: sync configuration files"
|
||||
if len(args) > 0 {
|
||||
message = args[0]
|
||||
}
|
||||
|
||||
lnk := core.NewLnk()
|
||||
if err := lnk.Push(message); err != nil {
|
||||
return fmt.Errorf("failed to push changes: %w", err)
|
||||
}
|
||||
lnk := core.NewLnk()
|
||||
if err := lnk.Push(message); err != nil {
|
||||
return fmt.Errorf("failed to push changes: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("🚀 \033[1;32mSuccessfully pushed changes\033[0m\n")
|
||||
fmt.Printf(" 💾 Commit: \033[90m%s\033[0m\n", message)
|
||||
fmt.Printf(" 📡 Synced to remote\n")
|
||||
fmt.Printf(" ✨ Your dotfiles are up to date!\n")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(pushCmd)
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "🚀 \033[1;32mSuccessfully pushed changes\033[0m\n")
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 💾 Commit: \033[90m%s\033[0m\n", message)
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 📡 Synced to remote\n")
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " ✨ Your dotfiles are up to date!\n")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
42
cmd/rm.go
42
cmd/rm.go
@@ -8,28 +8,26 @@ import (
|
||||
"github.com/yarlson/lnk/internal/core"
|
||||
)
|
||||
|
||||
var rmCmd = &cobra.Command{
|
||||
Use: "rm <file>",
|
||||
Short: "🗑️ Remove a file from lnk management",
|
||||
Long: "Removes a symlink and restores the original file from the lnk repository.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
filePath := args[0]
|
||||
func newRemoveCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "rm <file>",
|
||||
Short: "🗑️ Remove a file from lnk management",
|
||||
Long: "Removes a symlink and restores the original file from the lnk repository.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
filePath := args[0]
|
||||
|
||||
lnk := core.NewLnk()
|
||||
if err := lnk.Remove(filePath); err != nil {
|
||||
return fmt.Errorf("failed to remove file: %w", err)
|
||||
}
|
||||
lnk := core.NewLnk()
|
||||
if err := lnk.Remove(filePath); err != nil {
|
||||
return fmt.Errorf("failed to remove file: %w", err)
|
||||
}
|
||||
|
||||
basename := filepath.Base(filePath)
|
||||
fmt.Printf("🗑️ \033[1mRemoved %s from lnk\033[0m\n", basename)
|
||||
fmt.Printf(" ↩️ \033[90m~/.config/lnk/%s\033[0m → \033[36m%s\033[0m\n", basename, filePath)
|
||||
fmt.Printf(" 📄 Original file restored\n")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(rmCmd)
|
||||
basename := filepath.Base(filePath)
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "🗑️ \033[1mRemoved %s from lnk\033[0m\n", basename)
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " ↩️ \033[90m~/.config/lnk/%s\033[0m → \033[36m%s\033[0m\n", basename, filePath)
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 📄 Original file restored\n")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
26
cmd/root.go
26
cmd/root.go
@@ -12,10 +12,12 @@ var (
|
||||
buildTime = "unknown"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "lnk",
|
||||
Short: "🔗 Dotfiles, linked. No fluff.",
|
||||
Long: `🔗 Lnk - Git-native dotfiles management that doesn't suck.
|
||||
// NewRootCommand creates a new root command (testable)
|
||||
func NewRootCommand() *cobra.Command {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "lnk",
|
||||
Short: "🔗 Dotfiles, linked. No fluff.",
|
||||
Long: `🔗 Lnk - Git-native dotfiles management that doesn't suck.
|
||||
|
||||
Move your dotfiles to ~/.config/lnk, symlink them back, and use Git like normal.
|
||||
That's it.
|
||||
@@ -28,17 +30,29 @@ That's it.
|
||||
lnk pull # Get latest changes
|
||||
|
||||
🎯 Simple, fast, and Git-native.`,
|
||||
SilenceUsage: true,
|
||||
SilenceUsage: true,
|
||||
Version: fmt.Sprintf("%s (built %s)", version, buildTime),
|
||||
}
|
||||
|
||||
// Add subcommands
|
||||
rootCmd.AddCommand(newInitCmd())
|
||||
rootCmd.AddCommand(newAddCmd())
|
||||
rootCmd.AddCommand(newRemoveCmd())
|
||||
rootCmd.AddCommand(newStatusCmd())
|
||||
rootCmd.AddCommand(newPushCmd())
|
||||
rootCmd.AddCommand(newPullCmd())
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
// SetVersion sets the version information for the CLI
|
||||
func SetVersion(v, bt string) {
|
||||
version = v
|
||||
buildTime = bt
|
||||
rootCmd.Version = fmt.Sprintf("%s (built %s)", version, buildTime)
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
rootCmd := NewRootCommand()
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
|
@@ -7,52 +7,50 @@ import (
|
||||
"github.com/yarlson/lnk/internal/core"
|
||||
)
|
||||
|
||||
var statusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "📊 Show repository sync status",
|
||||
Long: "Display how many commits ahead/behind the local repository is relative to the remote.",
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
lnk := core.NewLnk()
|
||||
status, err := lnk.Status()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get status: %w", err)
|
||||
}
|
||||
func newStatusCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "📊 Show repository sync status",
|
||||
Long: "Display how many commits ahead/behind the local repository is relative to the remote.",
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
lnk := core.NewLnk()
|
||||
status, err := lnk.Status()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get status: %w", err)
|
||||
}
|
||||
|
||||
if status.Ahead == 0 && status.Behind == 0 {
|
||||
fmt.Printf("✅ \033[1;32mRepository is up to date\033[0m\n")
|
||||
fmt.Printf(" 📡 Synced with \033[36m%s\033[0m\n", status.Remote)
|
||||
} else {
|
||||
fmt.Printf("📊 \033[1mRepository Status\033[0m\n")
|
||||
fmt.Printf(" 📡 Remote: \033[36m%s\033[0m\n", status.Remote)
|
||||
fmt.Printf("\n")
|
||||
if status.Ahead == 0 && status.Behind == 0 {
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "✅ \033[1;32mRepository is up to date\033[0m\n")
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 📡 Synced with \033[36m%s\033[0m\n", status.Remote)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "📊 \033[1mRepository Status\033[0m\n")
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 📡 Remote: \033[36m%s\033[0m\n", status.Remote)
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\n")
|
||||
|
||||
if status.Ahead > 0 {
|
||||
commitText := "commit"
|
||||
if status.Ahead > 1 {
|
||||
commitText = "commits"
|
||||
if status.Ahead > 0 {
|
||||
commitText := "commit"
|
||||
if status.Ahead > 1 {
|
||||
commitText = "commits"
|
||||
}
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " ⬆️ \033[1;33m%d %s ahead\033[0m - ready to push\n", status.Ahead, commitText)
|
||||
}
|
||||
fmt.Printf(" ⬆️ \033[1;33m%d %s ahead\033[0m - ready to push\n", status.Ahead, commitText)
|
||||
}
|
||||
if status.Behind > 0 {
|
||||
commitText := "commit"
|
||||
if status.Behind > 1 {
|
||||
commitText = "commits"
|
||||
if status.Behind > 0 {
|
||||
commitText := "commit"
|
||||
if status.Behind > 1 {
|
||||
commitText = "commits"
|
||||
}
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " ⬇️ \033[1;31m%d %s behind\033[0m - run \033[1mlnk pull\033[0m\n", status.Behind, commitText)
|
||||
}
|
||||
|
||||
if status.Ahead > 0 && status.Behind == 0 {
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\n💡 Run \033[1mlnk push\033[0m to sync your changes")
|
||||
} else if status.Behind > 0 {
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\n💡 Run \033[1mlnk pull\033[0m to get latest changes")
|
||||
}
|
||||
fmt.Printf(" ⬇️ \033[1;31m%d %s behind\033[0m - run \033[1mlnk pull\033[0m\n", status.Behind, commitText)
|
||||
}
|
||||
|
||||
if status.Ahead > 0 && status.Behind == 0 {
|
||||
fmt.Printf("\n💡 Run \033[1mlnk push\033[0m to sync your changes")
|
||||
} else if status.Behind > 0 {
|
||||
fmt.Printf("\n💡 Run \033[1mlnk pull\033[0m to get latest changes")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(statusCmd)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
336
test/cli_test.go
Normal file
336
test/cli_test.go
Normal file
@@ -0,0 +1,336 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/yarlson/lnk/cmd"
|
||||
)
|
||||
|
||||
type CLITestSuite struct {
|
||||
suite.Suite
|
||||
tempDir string
|
||||
originalDir string
|
||||
stdout *bytes.Buffer
|
||||
stderr *bytes.Buffer
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) SetupTest() {
|
||||
// Create temp directory and change to it
|
||||
tempDir, err := os.MkdirTemp("", "lnk-cli-test-*")
|
||||
suite.Require().NoError(err)
|
||||
suite.tempDir = tempDir
|
||||
|
||||
originalDir, err := os.Getwd()
|
||||
suite.Require().NoError(err)
|
||||
suite.originalDir = originalDir
|
||||
|
||||
err = os.Chdir(tempDir)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Set XDG_CONFIG_HOME to temp directory
|
||||
suite.T().Setenv("XDG_CONFIG_HOME", tempDir)
|
||||
|
||||
// Capture output
|
||||
suite.stdout = &bytes.Buffer{}
|
||||
suite.stderr = &bytes.Buffer{}
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TearDownTest() {
|
||||
err := os.Chdir(suite.originalDir)
|
||||
suite.Require().NoError(err)
|
||||
err = os.RemoveAll(suite.tempDir)
|
||||
suite.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) runCommand(args ...string) error {
|
||||
rootCmd := cmd.NewRootCommand()
|
||||
rootCmd.SetOut(suite.stdout)
|
||||
rootCmd.SetErr(suite.stderr)
|
||||
rootCmd.SetArgs(args)
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestInitCommand() {
|
||||
err := suite.runCommand("init")
|
||||
suite.NoError(err)
|
||||
|
||||
// Check output
|
||||
output := suite.stdout.String()
|
||||
suite.Contains(output, "Initialized empty lnk repository")
|
||||
suite.Contains(output, "Location:")
|
||||
suite.Contains(output, "Next steps:")
|
||||
suite.Contains(output, "lnk add <file>")
|
||||
|
||||
// Verify actual effect
|
||||
lnkDir := filepath.Join(suite.tempDir, "lnk")
|
||||
suite.DirExists(lnkDir)
|
||||
|
||||
gitDir := filepath.Join(lnkDir, ".git")
|
||||
suite.DirExists(gitDir)
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestInitWithRemote() {
|
||||
err := suite.runCommand("init", "-r", "https://github.com/user/dotfiles.git")
|
||||
// This will fail because we don't have a real remote, but that's expected
|
||||
suite.Error(err)
|
||||
suite.Contains(err.Error(), "git clone failed")
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestAddCommand() {
|
||||
// Initialize first
|
||||
err := suite.runCommand("init")
|
||||
suite.Require().NoError(err)
|
||||
suite.stdout.Reset()
|
||||
|
||||
// Create test file
|
||||
testFile := filepath.Join(suite.tempDir, ".bashrc")
|
||||
err = os.WriteFile(testFile, []byte("export PATH=/usr/local/bin:$PATH"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test add command
|
||||
err = suite.runCommand("add", testFile)
|
||||
suite.NoError(err)
|
||||
|
||||
// Check output
|
||||
output := suite.stdout.String()
|
||||
suite.Contains(output, "Added .bashrc to lnk")
|
||||
suite.Contains(output, "→")
|
||||
suite.Contains(output, "sync to remote")
|
||||
|
||||
// Verify symlink was created
|
||||
info, err := os.Lstat(testFile)
|
||||
suite.NoError(err)
|
||||
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink)
|
||||
|
||||
// Verify file exists in repo
|
||||
repoFile := filepath.Join(suite.tempDir, "lnk", ".bashrc")
|
||||
suite.FileExists(repoFile)
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestRemoveCommand() {
|
||||
// Setup: init and add a file
|
||||
_ = suite.runCommand("init")
|
||||
testFile := filepath.Join(suite.tempDir, ".vimrc")
|
||||
_ = os.WriteFile(testFile, []byte("set number"), 0644)
|
||||
_ = suite.runCommand("add", testFile)
|
||||
suite.stdout.Reset()
|
||||
|
||||
// Test remove command
|
||||
err := suite.runCommand("rm", testFile)
|
||||
suite.NoError(err)
|
||||
|
||||
// Check output
|
||||
output := suite.stdout.String()
|
||||
suite.Contains(output, "Removed .vimrc from lnk")
|
||||
suite.Contains(output, "→")
|
||||
suite.Contains(output, "Original file restored")
|
||||
|
||||
// Verify symlink is gone and regular file is restored
|
||||
info, err := os.Lstat(testFile)
|
||||
suite.NoError(err)
|
||||
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink) // Not a symlink
|
||||
|
||||
// Verify content is preserved
|
||||
content, err := os.ReadFile(testFile)
|
||||
suite.NoError(err)
|
||||
suite.Equal("set number", string(content))
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestStatusCommand() {
|
||||
// Initialize first
|
||||
err := suite.runCommand("init")
|
||||
suite.Require().NoError(err)
|
||||
suite.stdout.Reset()
|
||||
|
||||
// Test status without remote - should fail
|
||||
err = suite.runCommand("status")
|
||||
suite.Error(err)
|
||||
suite.Contains(err.Error(), "no remote configured")
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestErrorHandling() {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantErr bool
|
||||
errContains string
|
||||
outContains string
|
||||
}{
|
||||
{
|
||||
name: "add nonexistent file",
|
||||
args: []string{"add", "/nonexistent/file"},
|
||||
wantErr: true,
|
||||
errContains: "File does not exist",
|
||||
},
|
||||
{
|
||||
name: "status without init",
|
||||
args: []string{"status"},
|
||||
wantErr: true,
|
||||
errContains: "Lnk repository not initialized",
|
||||
},
|
||||
{
|
||||
name: "help command",
|
||||
args: []string{"--help"},
|
||||
wantErr: false,
|
||||
outContains: "Lnk - Git-native dotfiles management",
|
||||
},
|
||||
{
|
||||
name: "version command",
|
||||
args: []string{"--version"},
|
||||
wantErr: false,
|
||||
outContains: "lnk version",
|
||||
},
|
||||
{
|
||||
name: "init help",
|
||||
args: []string{"init", "--help"},
|
||||
wantErr: false,
|
||||
outContains: "Creates the lnk directory",
|
||||
},
|
||||
{
|
||||
name: "add help",
|
||||
args: []string{"add", "--help"},
|
||||
wantErr: false,
|
||||
outContains: "Moves a file to the lnk repository",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
suite.Run(tt.name, func() {
|
||||
suite.stdout.Reset()
|
||||
suite.stderr.Reset()
|
||||
|
||||
err := suite.runCommand(tt.args...)
|
||||
|
||||
if tt.wantErr {
|
||||
suite.Error(err, "Expected error for %s", tt.name)
|
||||
if tt.errContains != "" {
|
||||
suite.Contains(err.Error(), tt.errContains, "Wrong error message for %s", tt.name)
|
||||
}
|
||||
} else {
|
||||
suite.NoError(err, "Unexpected error for %s", tt.name)
|
||||
}
|
||||
|
||||
if tt.outContains != "" {
|
||||
output := suite.stdout.String()
|
||||
suite.Contains(output, tt.outContains, "Expected output not found for %s", tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestCompleteWorkflow() {
|
||||
// Test realistic user workflow
|
||||
steps := []struct {
|
||||
name string
|
||||
args []string
|
||||
setup func()
|
||||
verify func(output string)
|
||||
}{
|
||||
{
|
||||
name: "initialize repository",
|
||||
args: []string{"init"},
|
||||
verify: func(output string) {
|
||||
suite.Contains(output, "Initialized empty lnk repository")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add config file",
|
||||
args: []string{"add", ".bashrc"},
|
||||
setup: func() {
|
||||
testFile := filepath.Join(suite.tempDir, ".bashrc")
|
||||
_ = os.WriteFile(testFile, []byte("export PATH=/usr/local/bin:$PATH"), 0644)
|
||||
},
|
||||
verify: func(output string) {
|
||||
suite.Contains(output, "Added .bashrc to lnk")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add another file",
|
||||
args: []string{"add", ".vimrc"},
|
||||
setup: func() {
|
||||
testFile := filepath.Join(suite.tempDir, ".vimrc")
|
||||
_ = os.WriteFile(testFile, []byte("set number"), 0644)
|
||||
},
|
||||
verify: func(output string) {
|
||||
suite.Contains(output, "Added .vimrc to lnk")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove file",
|
||||
args: []string{"rm", ".vimrc"},
|
||||
verify: func(output string) {
|
||||
suite.Contains(output, "Removed .vimrc from lnk")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, step := range steps {
|
||||
suite.Run(step.name, func() {
|
||||
if step.setup != nil {
|
||||
step.setup()
|
||||
}
|
||||
|
||||
suite.stdout.Reset()
|
||||
suite.stderr.Reset()
|
||||
|
||||
err := suite.runCommand(step.args...)
|
||||
suite.NoError(err, "Step %s failed: %v", step.name, err)
|
||||
|
||||
output := suite.stdout.String()
|
||||
if step.verify != nil {
|
||||
step.verify(output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestRemoveUnmanagedFile() {
|
||||
// Initialize repository
|
||||
_ = suite.runCommand("init")
|
||||
|
||||
// Create a regular file (not managed by lnk)
|
||||
testFile := filepath.Join(suite.tempDir, ".regularfile")
|
||||
_ = os.WriteFile(testFile, []byte("content"), 0644)
|
||||
|
||||
// Try to remove it
|
||||
err := suite.runCommand("rm", testFile)
|
||||
suite.Error(err)
|
||||
suite.Contains(err.Error(), "File is not managed by lnk")
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestAddDirectory() {
|
||||
// Initialize repository
|
||||
_ = suite.runCommand("init")
|
||||
suite.stdout.Reset()
|
||||
|
||||
// Create a directory with files
|
||||
testDir := filepath.Join(suite.tempDir, ".config")
|
||||
_ = os.MkdirAll(testDir, 0755)
|
||||
configFile := filepath.Join(testDir, "app.conf")
|
||||
_ = os.WriteFile(configFile, []byte("setting=value"), 0644)
|
||||
|
||||
// Add the directory
|
||||
err := suite.runCommand("add", testDir)
|
||||
suite.NoError(err)
|
||||
|
||||
// Check output
|
||||
output := suite.stdout.String()
|
||||
suite.Contains(output, "Added .config to lnk")
|
||||
|
||||
// Verify directory is now a symlink
|
||||
info, err := os.Lstat(testDir)
|
||||
suite.NoError(err)
|
||||
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink)
|
||||
|
||||
// Verify directory exists in repo
|
||||
repoDir := filepath.Join(suite.tempDir, "lnk", ".config")
|
||||
suite.DirExists(repoDir)
|
||||
}
|
||||
|
||||
func TestCLISuite(t *testing.T) {
|
||||
suite.Run(t, new(CLITestSuite))
|
||||
}
|
316
test/core_test.go
Normal file
316
test/core_test.go
Normal file
@@ -0,0 +1,316 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/yarlson/lnk/internal/core"
|
||||
)
|
||||
|
||||
type CoreTestSuite struct {
|
||||
suite.Suite
|
||||
tempDir string
|
||||
originalDir string
|
||||
lnk *core.Lnk
|
||||
}
|
||||
|
||||
func (suite *CoreTestSuite) SetupTest() {
|
||||
// Create temporary directory for each test
|
||||
tempDir, err := os.MkdirTemp("", "lnk-test-*")
|
||||
suite.Require().NoError(err)
|
||||
suite.tempDir = tempDir
|
||||
|
||||
// Change to temp directory
|
||||
originalDir, err := os.Getwd()
|
||||
suite.Require().NoError(err)
|
||||
suite.originalDir = originalDir
|
||||
|
||||
err = os.Chdir(tempDir)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Set XDG_CONFIG_HOME to temp directory
|
||||
suite.T().Setenv("XDG_CONFIG_HOME", tempDir)
|
||||
|
||||
// Initialize Lnk instance
|
||||
suite.lnk = core.NewLnk()
|
||||
}
|
||||
|
||||
func (suite *CoreTestSuite) TearDownTest() {
|
||||
// Return to original directory
|
||||
err := os.Chdir(suite.originalDir)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Clean up temp directory
|
||||
err = os.RemoveAll(suite.tempDir)
|
||||
suite.Require().NoError(err)
|
||||
}
|
||||
|
||||
// Test core initialization functionality
|
||||
func (suite *CoreTestSuite) TestCoreInit() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Check that the lnk directory was created
|
||||
lnkDir := filepath.Join(suite.tempDir, "lnk")
|
||||
suite.DirExists(lnkDir)
|
||||
|
||||
// Check that Git repo was initialized
|
||||
gitDir := filepath.Join(lnkDir, ".git")
|
||||
suite.DirExists(gitDir)
|
||||
}
|
||||
|
||||
// Test core add/remove functionality with files
|
||||
func (suite *CoreTestSuite) TestCoreFileOperations() {
|
||||
// Initialize first
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a test file
|
||||
testFile := filepath.Join(suite.tempDir, ".bashrc")
|
||||
content := "export PATH=$PATH:/usr/local/bin"
|
||||
err = os.WriteFile(testFile, []byte(content), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add the file
|
||||
err = suite.lnk.Add(testFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Verify symlink and repo file
|
||||
info, err := os.Lstat(testFile)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink)
|
||||
|
||||
repoFile := filepath.Join(suite.tempDir, "lnk", ".bashrc")
|
||||
suite.FileExists(repoFile)
|
||||
|
||||
// Verify content is preserved
|
||||
repoContent, err := os.ReadFile(repoFile)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(content, string(repoContent))
|
||||
|
||||
// Test remove
|
||||
err = suite.lnk.Remove(testFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Verify symlink is gone and regular file is restored
|
||||
info, err = os.Lstat(testFile)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink) // Not a symlink
|
||||
|
||||
// Verify content is preserved
|
||||
restoredContent, err := os.ReadFile(testFile)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(content, string(restoredContent))
|
||||
}
|
||||
|
||||
// Test core add/remove functionality with directories
|
||||
func (suite *CoreTestSuite) TestCoreDirectoryOperations() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a directory with files
|
||||
testDir := filepath.Join(suite.tempDir, "testdir")
|
||||
err = os.MkdirAll(testDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
testFile := filepath.Join(testDir, "config.txt")
|
||||
content := "test config"
|
||||
err = os.WriteFile(testFile, []byte(content), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add the directory
|
||||
err = suite.lnk.Add(testDir)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Verify directory is now a symlink
|
||||
info, err := os.Lstat(testDir)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink)
|
||||
|
||||
// Verify directory exists in repo
|
||||
repoDir := filepath.Join(suite.tempDir, "lnk", "testdir")
|
||||
suite.DirExists(repoDir)
|
||||
|
||||
// Remove the directory
|
||||
err = suite.lnk.Remove(testDir)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Verify symlink is gone and regular directory is restored
|
||||
info, err = os.Lstat(testDir)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink) // Not a symlink
|
||||
suite.True(info.IsDir()) // Is a directory
|
||||
|
||||
// Verify content is preserved
|
||||
restoredContent, err := os.ReadFile(testFile)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(content, string(restoredContent))
|
||||
}
|
||||
|
||||
// Test .lnk file tracking functionality
|
||||
func (suite *CoreTestSuite) TestLnkFileTracking() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add multiple items
|
||||
testFile := filepath.Join(suite.tempDir, ".bashrc")
|
||||
err = os.WriteFile(testFile, []byte("export PATH=/usr/local/bin:$PATH"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
err = suite.lnk.Add(testFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
testDir := filepath.Join(suite.tempDir, ".ssh")
|
||||
err = os.MkdirAll(testDir, 0700)
|
||||
suite.Require().NoError(err)
|
||||
configFile := filepath.Join(testDir, "config")
|
||||
err = os.WriteFile(configFile, []byte("Host example.com"), 0600)
|
||||
suite.Require().NoError(err)
|
||||
err = suite.lnk.Add(testDir)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Check .lnk file contains both entries
|
||||
lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk")
|
||||
suite.FileExists(lnkFile)
|
||||
|
||||
lnkContent, err := os.ReadFile(lnkFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(string(lnkContent)), "\n")
|
||||
suite.Len(lines, 2)
|
||||
suite.Contains(lines, ".bashrc")
|
||||
suite.Contains(lines, ".ssh")
|
||||
|
||||
// Remove one item and verify tracking is updated
|
||||
err = suite.lnk.Remove(testFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
lnkContent, err = os.ReadFile(lnkFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
lines = strings.Split(strings.TrimSpace(string(lnkContent)), "\n")
|
||||
suite.Len(lines, 1)
|
||||
suite.Contains(lines, ".ssh")
|
||||
suite.NotContains(lines, ".bashrc")
|
||||
}
|
||||
|
||||
// Test XDG_CONFIG_HOME fallback
|
||||
func (suite *CoreTestSuite) TestXDGConfigHomeFallback() {
|
||||
// Test fallback to ~/.config/lnk when XDG_CONFIG_HOME is not set
|
||||
suite.T().Setenv("XDG_CONFIG_HOME", "")
|
||||
|
||||
homeDir := filepath.Join(suite.tempDir, "home")
|
||||
err := os.MkdirAll(homeDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
suite.T().Setenv("HOME", homeDir)
|
||||
|
||||
lnk := core.NewLnk()
|
||||
err = lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Check that the lnk directory was created under ~/.config/lnk
|
||||
expectedDir := filepath.Join(homeDir, ".config", "lnk")
|
||||
suite.DirExists(expectedDir)
|
||||
}
|
||||
|
||||
// Test symlink restoration (pull functionality)
|
||||
func (suite *CoreTestSuite) TestSymlinkRestoration() {
|
||||
_ = suite.lnk.Init()
|
||||
|
||||
// Create a file in the repo directly (simulating a pull)
|
||||
repoFile := filepath.Join(suite.tempDir, "lnk", ".bashrc")
|
||||
content := "export PATH=$PATH:/usr/local/bin"
|
||||
err := os.WriteFile(repoFile, []byte(content), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create .lnk file to track it
|
||||
lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk")
|
||||
err = os.WriteFile(lnkFile, []byte(".bashrc\n"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Get home directory for the test
|
||||
homeDir, err := os.UserHomeDir()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
targetFile := filepath.Join(homeDir, ".bashrc")
|
||||
|
||||
// Clean up the test file after the test
|
||||
defer func() {
|
||||
_ = os.Remove(targetFile)
|
||||
}()
|
||||
|
||||
// Test symlink restoration
|
||||
restored, err := suite.lnk.RestoreSymlinks()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Should have restored the symlink
|
||||
suite.Len(restored, 1)
|
||||
suite.Equal(".bashrc", restored[0])
|
||||
|
||||
// Check that file is now a symlink
|
||||
info, err := os.Lstat(targetFile)
|
||||
suite.NoError(err)
|
||||
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink)
|
||||
}
|
||||
|
||||
// Test error conditions
|
||||
func (suite *CoreTestSuite) TestErrorConditions() {
|
||||
// Test add nonexistent file
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.lnk.Add("/nonexistent/file")
|
||||
suite.Error(err)
|
||||
suite.Contains(err.Error(), "File does not exist")
|
||||
|
||||
// Test remove unmanaged file
|
||||
testFile := filepath.Join(suite.tempDir, ".regularfile")
|
||||
err = os.WriteFile(testFile, []byte("content"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.lnk.Remove(testFile)
|
||||
suite.Error(err)
|
||||
suite.Contains(err.Error(), "File is not managed by lnk")
|
||||
|
||||
// Test status without remote
|
||||
_, err = suite.lnk.Status()
|
||||
suite.Error(err)
|
||||
suite.Contains(err.Error(), "no remote configured")
|
||||
}
|
||||
|
||||
// Test git operations
|
||||
func (suite *CoreTestSuite) TestGitOperations() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add a file to create a commit
|
||||
testFile := filepath.Join(suite.tempDir, ".bashrc")
|
||||
content := "export PATH=$PATH:/usr/local/bin"
|
||||
err = os.WriteFile(testFile, []byte(content), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.lnk.Add(testFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Check that Git commit was made
|
||||
commits, err := suite.lnk.GetCommits()
|
||||
suite.Require().NoError(err)
|
||||
suite.Len(commits, 1)
|
||||
suite.Contains(commits[0], "lnk: added .bashrc")
|
||||
|
||||
// Test add remote
|
||||
err = suite.lnk.AddRemote("origin", "https://github.com/test/dotfiles.git")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test status with remote
|
||||
status, err := suite.lnk.Status()
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(1, status.Ahead)
|
||||
suite.Equal(0, status.Behind)
|
||||
}
|
||||
|
||||
func TestCoreSuite(t *testing.T) {
|
||||
suite.Run(t, new(CoreTestSuite))
|
||||
}
|
@@ -1,718 +0,0 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/yarlson/lnk/internal/core"
|
||||
)
|
||||
|
||||
type LnkIntegrationTestSuite struct {
|
||||
suite.Suite
|
||||
tempDir string
|
||||
originalDir string
|
||||
lnk *core.Lnk
|
||||
}
|
||||
|
||||
func (suite *LnkIntegrationTestSuite) SetupTest() {
|
||||
// Create temporary directory for each test
|
||||
tempDir, err := os.MkdirTemp("", "lnk-test-*")
|
||||
suite.Require().NoError(err)
|
||||
suite.tempDir = tempDir
|
||||
|
||||
// Change to temp directory
|
||||
originalDir, err := os.Getwd()
|
||||
suite.Require().NoError(err)
|
||||
suite.originalDir = originalDir
|
||||
|
||||
err = os.Chdir(tempDir)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Set XDG_CONFIG_HOME to temp directory
|
||||
suite.T().Setenv("XDG_CONFIG_HOME", tempDir)
|
||||
|
||||
// Initialize Lnk instance
|
||||
suite.lnk = core.NewLnk()
|
||||
}
|
||||
|
||||
func (suite *LnkIntegrationTestSuite) TearDownTest() {
|
||||
// Return to original directory
|
||||
err := os.Chdir(suite.originalDir)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Clean up temp directory
|
||||
err = os.RemoveAll(suite.tempDir)
|
||||
suite.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (suite *LnkIntegrationTestSuite) TestInit() {
|
||||
// Test that init creates the directory and Git repo
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Check that the lnk directory was created
|
||||
lnkDir := filepath.Join(suite.tempDir, "lnk")
|
||||
suite.DirExists(lnkDir)
|
||||
|
||||
// Check that Git repo was initialized
|
||||
gitDir := filepath.Join(lnkDir, ".git")
|
||||
suite.DirExists(gitDir)
|
||||
|
||||
// Verify it's a non-bare repo
|
||||
configPath := filepath.Join(gitDir, "config")
|
||||
suite.FileExists(configPath)
|
||||
|
||||
// Verify the default branch is set to 'main'
|
||||
cmd := exec.Command("git", "symbolic-ref", "HEAD")
|
||||
cmd.Dir = lnkDir
|
||||
output, err := cmd.Output()
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal("refs/heads/main", strings.TrimSpace(string(output)))
|
||||
}
|
||||
|
||||
func (suite *LnkIntegrationTestSuite) TestAddFile() {
|
||||
// Initialize first
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a test file
|
||||
testFile := filepath.Join(suite.tempDir, ".bashrc")
|
||||
content := "export PATH=$PATH:/usr/local/bin"
|
||||
err = os.WriteFile(testFile, []byte(content), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add the file
|
||||
err = suite.lnk.Add(testFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Check that the original file is now a symlink
|
||||
info, err := os.Lstat(testFile)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink)
|
||||
|
||||
// Check that the file exists in the repo
|
||||
repoFile := filepath.Join(suite.tempDir, "lnk", ".bashrc")
|
||||
suite.FileExists(repoFile)
|
||||
|
||||
// Check that the content is preserved
|
||||
repoContent, err := os.ReadFile(repoFile)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(content, string(repoContent))
|
||||
|
||||
// Check that symlink points to the correct location
|
||||
linkTarget, err := os.Readlink(testFile)
|
||||
suite.Require().NoError(err)
|
||||
expectedTarget, err := filepath.Rel(filepath.Dir(testFile), repoFile)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(expectedTarget, linkTarget)
|
||||
|
||||
// Check that Git commit was made
|
||||
commits, err := suite.lnk.GetCommits()
|
||||
suite.Require().NoError(err)
|
||||
suite.Len(commits, 1)
|
||||
suite.Contains(commits[0], "lnk: added .bashrc")
|
||||
}
|
||||
|
||||
func (suite *LnkIntegrationTestSuite) TestRemoveFile() {
|
||||
// Initialize and add a file first
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
testFile := filepath.Join(suite.tempDir, ".vimrc")
|
||||
content := "set number"
|
||||
err = os.WriteFile(testFile, []byte(content), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.lnk.Add(testFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Now remove the file
|
||||
err = suite.lnk.Remove(testFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Check that the symlink is gone and regular file is restored
|
||||
info, err := os.Lstat(testFile)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink) // Not a symlink
|
||||
|
||||
// Check that content is preserved
|
||||
restoredContent, err := os.ReadFile(testFile)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(content, string(restoredContent))
|
||||
|
||||
// Check that file is removed from repo
|
||||
repoFile := filepath.Join(suite.tempDir, "lnk", ".vimrc")
|
||||
suite.NoFileExists(repoFile)
|
||||
|
||||
// Check that Git commit was made
|
||||
commits, err := suite.lnk.GetCommits()
|
||||
suite.Require().NoError(err)
|
||||
suite.Len(commits, 2) // add + remove
|
||||
suite.Contains(commits[0], "lnk: removed .vimrc")
|
||||
suite.Contains(commits[1], "lnk: added .vimrc")
|
||||
}
|
||||
|
||||
func (suite *LnkIntegrationTestSuite) TestAddNonexistentFile() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.lnk.Add("/nonexistent/file")
|
||||
suite.Error(err)
|
||||
suite.Contains(err.Error(), "File does not exist")
|
||||
}
|
||||
|
||||
func (suite *LnkIntegrationTestSuite) TestAddDirectory() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a directory with files
|
||||
testDir := filepath.Join(suite.tempDir, "testdir")
|
||||
err = os.MkdirAll(testDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add files to the directory
|
||||
testFile1 := filepath.Join(testDir, "file1.txt")
|
||||
err = os.WriteFile(testFile1, []byte("content1"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
testFile2 := filepath.Join(testDir, "file2.txt")
|
||||
err = os.WriteFile(testFile2, []byte("content2"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add the directory - should now succeed
|
||||
err = suite.lnk.Add(testDir)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Check that the directory is now a symlink
|
||||
info, err := os.Lstat(testDir)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink)
|
||||
|
||||
// Check that the directory exists in the repo
|
||||
repoDir := filepath.Join(suite.tempDir, "lnk", "testdir")
|
||||
suite.DirExists(repoDir)
|
||||
|
||||
// Check that files are preserved
|
||||
repoFile1 := filepath.Join(repoDir, "file1.txt")
|
||||
repoFile2 := filepath.Join(repoDir, "file2.txt")
|
||||
suite.FileExists(repoFile1)
|
||||
suite.FileExists(repoFile2)
|
||||
|
||||
content1, err := os.ReadFile(repoFile1)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal("content1", string(content1))
|
||||
|
||||
content2, err := os.ReadFile(repoFile2)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal("content2", string(content2))
|
||||
|
||||
// Check that .lnk file was created and contains the directory
|
||||
lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk")
|
||||
suite.FileExists(lnkFile)
|
||||
|
||||
lnkContent, err := os.ReadFile(lnkFile)
|
||||
suite.Require().NoError(err)
|
||||
suite.Contains(string(lnkContent), "testdir")
|
||||
|
||||
// Check that Git commit was made
|
||||
commits, err := suite.lnk.GetCommits()
|
||||
suite.Require().NoError(err)
|
||||
suite.Len(commits, 1)
|
||||
suite.Contains(commits[0], "lnk: added testdir")
|
||||
}
|
||||
|
||||
func (suite *LnkIntegrationTestSuite) TestRemoveDirectory() {
|
||||
// Initialize and add a directory first
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
testDir := filepath.Join(suite.tempDir, "testdir")
|
||||
err = os.MkdirAll(testDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
testFile := filepath.Join(testDir, "config.txt")
|
||||
content := "test config"
|
||||
err = os.WriteFile(testFile, []byte(content), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.lnk.Add(testDir)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Now remove the directory
|
||||
err = suite.lnk.Remove(testDir)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Check that the symlink is gone and regular directory is restored
|
||||
info, err := os.Lstat(testDir)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink) // Not a symlink
|
||||
suite.True(info.IsDir()) // Is a directory
|
||||
|
||||
// Check that content is preserved
|
||||
restoredContent, err := os.ReadFile(testFile)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(content, string(restoredContent))
|
||||
|
||||
// Check that directory is removed from repo
|
||||
repoDir := filepath.Join(suite.tempDir, "lnk", "testdir")
|
||||
suite.NoDirExists(repoDir)
|
||||
|
||||
// Check that .lnk file no longer contains the directory
|
||||
lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk")
|
||||
if suite.FileExists(lnkFile) {
|
||||
lnkContent, err := os.ReadFile(lnkFile)
|
||||
suite.Require().NoError(err)
|
||||
suite.NotContains(string(lnkContent), "testdir")
|
||||
}
|
||||
|
||||
// Check that Git commit was made
|
||||
commits, err := suite.lnk.GetCommits()
|
||||
suite.Require().NoError(err)
|
||||
suite.Len(commits, 2) // add + remove
|
||||
suite.Contains(commits[0], "lnk: removed testdir")
|
||||
suite.Contains(commits[1], "lnk: added testdir")
|
||||
}
|
||||
|
||||
func (suite *LnkIntegrationTestSuite) TestLnkFileTracking() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add a file
|
||||
testFile := filepath.Join(suite.tempDir, ".bashrc")
|
||||
err = os.WriteFile(testFile, []byte("export PATH=/usr/local/bin:$PATH"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.lnk.Add(testFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add a directory
|
||||
testDir := filepath.Join(suite.tempDir, ".ssh")
|
||||
err = os.MkdirAll(testDir, 0700)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
configFile := filepath.Join(testDir, "config")
|
||||
err = os.WriteFile(configFile, []byte("Host example.com"), 0600)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.lnk.Add(testDir)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Check .lnk file contains both entries
|
||||
lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk")
|
||||
suite.FileExists(lnkFile)
|
||||
|
||||
lnkContent, err := os.ReadFile(lnkFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(string(lnkContent)), "\n")
|
||||
suite.Len(lines, 2)
|
||||
suite.Contains(lines, ".bashrc")
|
||||
suite.Contains(lines, ".ssh")
|
||||
|
||||
// Remove a file and check .lnk is updated
|
||||
err = suite.lnk.Remove(testFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
lnkContent, err = os.ReadFile(lnkFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
lines = strings.Split(strings.TrimSpace(string(lnkContent)), "\n")
|
||||
suite.Len(lines, 1)
|
||||
suite.Contains(lines, ".ssh")
|
||||
suite.NotContains(lines, ".bashrc")
|
||||
}
|
||||
|
||||
func (suite *LnkIntegrationTestSuite) TestPullWithDirectories() {
|
||||
// Initialize repo
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add remote for pull to work
|
||||
err = suite.lnk.AddRemote("origin", "https://github.com/test/dotfiles.git")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a directory and .lnk file in the repo directly to simulate a pull
|
||||
repoDir := filepath.Join(suite.tempDir, "lnk", ".config")
|
||||
err = os.MkdirAll(repoDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
configFile := filepath.Join(repoDir, "app.conf")
|
||||
content := "setting=value"
|
||||
err = os.WriteFile(configFile, []byte(content), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create .lnk file
|
||||
lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk")
|
||||
err = os.WriteFile(lnkFile, []byte(".config\n"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Get home directory for the test
|
||||
homeDir, err := os.UserHomeDir()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
targetDir := filepath.Join(homeDir, ".config")
|
||||
|
||||
// Clean up the test directory after the test
|
||||
defer func() {
|
||||
_ = os.RemoveAll(targetDir)
|
||||
}()
|
||||
|
||||
// Create a regular directory in home to simulate conflict scenario
|
||||
err = os.MkdirAll(targetDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
err = os.WriteFile(filepath.Join(targetDir, "different.conf"), []byte("different"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Pull should restore symlinks and handle conflicts
|
||||
restored, err := suite.lnk.Pull()
|
||||
// In tests, pull will fail because we don't have real remotes, but that's expected
|
||||
// We can still test the symlink restoration part
|
||||
if err != nil {
|
||||
suite.Contains(err.Error(), "git pull failed")
|
||||
// Test symlink restoration directly
|
||||
restored, err = suite.lnk.RestoreSymlinks()
|
||||
suite.Require().NoError(err)
|
||||
}
|
||||
|
||||
// Should have restored the symlink
|
||||
suite.GreaterOrEqual(len(restored), 1)
|
||||
if len(restored) > 0 {
|
||||
suite.Equal(".config", restored[0])
|
||||
}
|
||||
|
||||
// Check that directory is back to being a symlink
|
||||
info, err := os.Lstat(targetDir)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink)
|
||||
|
||||
// Check content is preserved from repo
|
||||
repoContent, err := os.ReadFile(configFile)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(content, string(repoContent))
|
||||
}
|
||||
|
||||
func (suite *LnkIntegrationTestSuite) TestRemoveNonSymlink() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a regular file (not managed by lnk)
|
||||
testFile := filepath.Join(suite.tempDir, ".regularfile")
|
||||
err = os.WriteFile(testFile, []byte("content"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.lnk.Remove(testFile)
|
||||
suite.Error(err)
|
||||
suite.Contains(err.Error(), "File is not managed by lnk")
|
||||
}
|
||||
|
||||
func (suite *LnkIntegrationTestSuite) TestXDGConfigHomeFallback() {
|
||||
// Test fallback to ~/.config/lnk when XDG_CONFIG_HOME is not set
|
||||
suite.T().Setenv("XDG_CONFIG_HOME", "")
|
||||
|
||||
homeDir := filepath.Join(suite.tempDir, "home")
|
||||
err := os.MkdirAll(homeDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
suite.T().Setenv("HOME", homeDir)
|
||||
|
||||
lnk := core.NewLnk()
|
||||
err = lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Check that the lnk directory was created under ~/.config/lnk
|
||||
expectedDir := filepath.Join(homeDir, ".config", "lnk")
|
||||
suite.DirExists(expectedDir)
|
||||
}
|
||||
|
||||
func (suite *LnkIntegrationTestSuite) TestInitWithRemote() {
|
||||
// Test that init with remote adds the origin remote
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
remoteURL := "https://github.com/user/dotfiles.git"
|
||||
err = suite.lnk.AddRemote("origin", remoteURL)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Verify the remote was added by checking git config
|
||||
lnkDir := filepath.Join(suite.tempDir, "lnk")
|
||||
cmd := exec.Command("git", "remote", "get-url", "origin")
|
||||
cmd.Dir = lnkDir
|
||||
|
||||
output, err := cmd.Output()
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(remoteURL, strings.TrimSpace(string(output)))
|
||||
}
|
||||
|
||||
func (suite *LnkIntegrationTestSuite) TestInitIdempotent() {
|
||||
// Test that running init multiple times is safe
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
lnkDir := filepath.Join(suite.tempDir, "lnk")
|
||||
|
||||
// Add a file to the repo to ensure it's not lost
|
||||
testFile := filepath.Join(lnkDir, "test.txt")
|
||||
err = os.WriteFile(testFile, []byte("test content"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Run init again - should be idempotent
|
||||
err = suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// File should still exist
|
||||
suite.FileExists(testFile)
|
||||
content, err := os.ReadFile(testFile)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal("test content", string(content))
|
||||
}
|
||||
|
||||
func (suite *LnkIntegrationTestSuite) TestInitWithExistingRemote() {
|
||||
// Test init with remote when remote already exists (same URL)
|
||||
remoteURL := "https://github.com/user/dotfiles.git"
|
||||
|
||||
// First init with remote
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
err = suite.lnk.AddRemote("origin", remoteURL)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Init again with same remote should be idempotent
|
||||
err = suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
err = suite.lnk.AddRemote("origin", remoteURL)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Verify remote is still correct
|
||||
lnkDir := filepath.Join(suite.tempDir, "lnk")
|
||||
cmd := exec.Command("git", "remote", "get-url", "origin")
|
||||
cmd.Dir = lnkDir
|
||||
output, err := cmd.Output()
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(remoteURL, strings.TrimSpace(string(output)))
|
||||
}
|
||||
|
||||
func (suite *LnkIntegrationTestSuite) TestInitWithDifferentRemote() {
|
||||
// Test init with different remote when remote already exists
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add first remote
|
||||
err = suite.lnk.AddRemote("origin", "https://github.com/user/dotfiles.git")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Try to add different remote - should error
|
||||
err = suite.lnk.AddRemote("origin", "https://github.com/user/other-repo.git")
|
||||
suite.Error(err)
|
||||
suite.Contains(err.Error(), "already exists with different URL")
|
||||
}
|
||||
|
||||
func (suite *LnkIntegrationTestSuite) TestInitWithNonLnkRepo() {
|
||||
// Test init when directory contains a non-lnk Git repository
|
||||
lnkDir := filepath.Join(suite.tempDir, "lnk")
|
||||
err := os.MkdirAll(lnkDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a non-lnk git repo in the lnk directory
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = lnkDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add some content to make it look like a real repo
|
||||
testFile := filepath.Join(lnkDir, "important-file.txt")
|
||||
err = os.WriteFile(testFile, []byte("important data"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Configure git and commit
|
||||
cmd = exec.Command("git", "config", "user.name", "Test User")
|
||||
cmd.Dir = lnkDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
cmd = exec.Command("git", "config", "user.email", "test@example.com")
|
||||
cmd.Dir = lnkDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
cmd = exec.Command("git", "add", "important-file.txt")
|
||||
cmd.Dir = lnkDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
cmd = exec.Command("git", "commit", "-m", "important commit")
|
||||
cmd.Dir = lnkDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Now try to init lnk - should error to protect existing repo
|
||||
err = suite.lnk.Init()
|
||||
suite.Error(err)
|
||||
suite.Contains(err.Error(), "contains an existing Git repository")
|
||||
|
||||
// Verify the original file is still there
|
||||
suite.FileExists(testFile)
|
||||
}
|
||||
|
||||
// TestSyncStatus tests the status command functionality
|
||||
func (suite *LnkIntegrationTestSuite) TestSyncStatus() {
|
||||
// Initialize repo with remote
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.lnk.AddRemote("origin", "https://github.com/test/dotfiles.git")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add a file to create some local changes
|
||||
testFile := filepath.Join(suite.tempDir, ".bashrc")
|
||||
content := "export PATH=$PATH:/usr/local/bin"
|
||||
err = os.WriteFile(testFile, []byte(content), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.lnk.Add(testFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Get status - should show 1 commit ahead
|
||||
status, err := suite.lnk.Status()
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(1, status.Ahead)
|
||||
suite.Equal(0, status.Behind)
|
||||
suite.Equal("origin/main", status.Remote)
|
||||
}
|
||||
|
||||
// TestSyncPush tests the push command functionality
|
||||
func (suite *LnkIntegrationTestSuite) TestSyncPush() {
|
||||
// Initialize repo
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add remote for push to work
|
||||
err = suite.lnk.AddRemote("origin", "https://github.com/test/dotfiles.git")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add a file
|
||||
testFile := filepath.Join(suite.tempDir, ".vimrc")
|
||||
content := "set number"
|
||||
err = os.WriteFile(testFile, []byte(content), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.lnk.Add(testFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add another file for a second commit
|
||||
testFile2 := filepath.Join(suite.tempDir, ".gitconfig")
|
||||
content2 := "[user]\n name = Test User"
|
||||
err = os.WriteFile(testFile2, []byte(content2), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.lnk.Add(testFile2)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Modify one of the files to create uncommitted changes
|
||||
repoFile := filepath.Join(suite.tempDir, "lnk", ".vimrc")
|
||||
modifiedContent := "set number\nset relativenumber"
|
||||
err = os.WriteFile(repoFile, []byte(modifiedContent), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Push should stage all changes and create a sync commit
|
||||
message := "Updated configuration files"
|
||||
err = suite.lnk.Push(message)
|
||||
// In tests, push will fail because we don't have real remotes, but that's expected
|
||||
// The important part is that it stages and commits changes
|
||||
if err != nil {
|
||||
suite.Contains(err.Error(), "git push failed")
|
||||
}
|
||||
|
||||
// Check that a sync commit was made (even if push failed)
|
||||
commits, err := suite.lnk.GetCommits()
|
||||
suite.Require().NoError(err)
|
||||
suite.GreaterOrEqual(len(commits), 3) // at least 2 add commits + 1 sync commit
|
||||
suite.Contains(commits[0], message) // Latest commit should contain our message
|
||||
}
|
||||
|
||||
// TestSyncPull tests the pull command functionality
|
||||
func (suite *LnkIntegrationTestSuite) TestSyncPull() {
|
||||
// Initialize repo
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add remote for pull to work
|
||||
err = suite.lnk.AddRemote("origin", "https://github.com/test/dotfiles.git")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Pull should attempt to pull from remote (will fail in tests but that's expected)
|
||||
_, err = suite.lnk.Pull()
|
||||
// In tests, pull will fail because we don't have real remotes, but that's expected
|
||||
suite.Error(err)
|
||||
suite.Contains(err.Error(), "git pull failed")
|
||||
|
||||
// Test RestoreSymlinks functionality separately
|
||||
// Create a file in the repo directly
|
||||
repoFile := filepath.Join(suite.tempDir, "lnk", ".bashrc")
|
||||
content := "export PATH=$PATH:/usr/local/bin"
|
||||
err = os.WriteFile(repoFile, []byte(content), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test that RestoreSymlinks can be called (even if it doesn't restore anything in this test setup)
|
||||
restored, err := suite.lnk.RestoreSymlinks()
|
||||
suite.Require().NoError(err)
|
||||
// In this test setup, it might not restore anything, and that's okay for Phase 1
|
||||
suite.GreaterOrEqual(len(restored), 0)
|
||||
}
|
||||
|
||||
// TestSyncStatusNoRemote tests status when no remote is configured
|
||||
func (suite *LnkIntegrationTestSuite) TestSyncStatusNoRemote() {
|
||||
// Initialize repo without remote
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Status should indicate no remote
|
||||
_, err = suite.lnk.Status()
|
||||
suite.Error(err)
|
||||
suite.Contains(err.Error(), "no remote configured")
|
||||
}
|
||||
|
||||
// TestSyncPushWithModifiedFiles tests push when files are modified
|
||||
func (suite *LnkIntegrationTestSuite) TestSyncPushWithModifiedFiles() {
|
||||
// Initialize repo and add a file
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add remote for push to work
|
||||
err = suite.lnk.AddRemote("origin", "https://github.com/test/dotfiles.git")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
testFile := filepath.Join(suite.tempDir, ".bashrc")
|
||||
content := "export PATH=$PATH:/usr/local/bin"
|
||||
err = os.WriteFile(testFile, []byte(content), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
err = suite.lnk.Add(testFile)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Modify the file in the repo (simulate editing managed file)
|
||||
repoFile := filepath.Join(suite.tempDir, "lnk", ".bashrc")
|
||||
modifiedContent := "export PATH=$PATH:/usr/local/bin\nexport EDITOR=vim"
|
||||
err = os.WriteFile(repoFile, []byte(modifiedContent), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Push should detect and commit the changes
|
||||
message := "Updated bashrc with editor setting"
|
||||
err = suite.lnk.Push(message)
|
||||
// In tests, push will fail because we don't have real remotes, but that's expected
|
||||
if err != nil {
|
||||
suite.Contains(err.Error(), "git push failed")
|
||||
}
|
||||
|
||||
// Check that changes were committed (even if push failed)
|
||||
commits, err := suite.lnk.GetCommits()
|
||||
suite.Require().NoError(err)
|
||||
suite.GreaterOrEqual(len(commits), 2) // add commit + sync commit
|
||||
suite.Contains(commits[0], message)
|
||||
}
|
||||
|
||||
func TestLnkIntegrationSuite(t *testing.T) {
|
||||
suite.Run(t, new(LnkIntegrationTestSuite))
|
||||
}
|
Reference in New Issue
Block a user