Files
2026-05-27 22:12:14 -05:00

226 lines
7.4 KiB
Go

package bootstrap
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
)
func TestDeployManagedFileBacksUpExistingUnmanagedFile(t *testing.T) {
dir := t.TempDir()
target := filepath.Join(dir, "profile.ps1")
if err := os.WriteFile(target, []byte("custom"), 0o644); err != nil {
t.Fatal(err)
}
deployment, err := DeployManagedFile("profile", target, []byte("managed"))
if err != nil {
t.Fatal(err)
}
if !deployment.Changed || deployment.Backup == "" {
t.Fatalf("deployment = %#v, want changed with backup", deployment)
}
if got, _ := os.ReadFile(target); string(got) != "managed" {
t.Fatalf("target = %q", got)
}
if got, _ := os.ReadFile(deployment.Backup); string(got) != "custom" {
t.Fatalf("backup = %q", got)
}
deployment, err = DeployManagedFile("profile", target, []byte("managed"))
if err != nil {
t.Fatal(err)
}
if deployment.Changed {
t.Fatal("identical managed file should be left untouched")
}
}
func TestDeployConfigAssetsWritesExpectedTargets(t *testing.T) {
root := t.TempDir()
paths := UserPaths{
Home: filepath.Join(root, "home"),
LocalAppData: filepath.Join(root, "local"),
Documents: filepath.Join(root, "docs"),
}
deployments, err := DeployConfigAssets(paths)
if err != nil {
t.Fatal(err)
}
if len(deployments) != 3 {
t.Fatalf("deployments = %d, want 3", len(deployments))
}
profile := filepath.Join(paths.Documents, "PowerShell", "Microsoft.PowerShell_profile.ps1")
data, err := os.ReadFile(profile)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(data), "oh-my-posh") || !strings.Contains(string(data), "fnm env") {
t.Fatalf("profile missing expected initialization: %s", data)
}
}
func TestDefaultUserPathsUsesHomeAndDocuments(t *testing.T) {
paths, err := DefaultUserPaths()
if err != nil {
t.Fatal(err)
}
if paths.Home == "" || paths.LocalAppData == "" || paths.Documents == "" {
t.Fatalf("paths should be populated: %#v", paths)
}
if !strings.HasSuffix(paths.Documents, "Documents") {
t.Fatalf("documents path = %q, want Documents suffix", paths.Documents)
}
}
func TestDefaultUserPathsFallsBackWhenLocalAppDataMissing(t *testing.T) {
t.Setenv("LOCALAPPDATA", "")
paths, err := DefaultUserPaths()
if err != nil {
t.Fatal(err)
}
want := filepath.Join(paths.Home, "AppData", "Local")
if paths.LocalAppData != want {
t.Fatalf("local app data = %q, want %q", paths.LocalAppData, want)
}
}
func TestDeployConfigAssetsReportsDeploymentError(t *testing.T) {
root := t.TempDir()
homeFile := filepath.Join(root, "home")
if err := os.WriteFile(homeFile, []byte("not a directory"), 0o644); err != nil {
t.Fatal(err)
}
_, err := DeployConfigAssets(UserPaths{
Home: homeFile,
LocalAppData: filepath.Join(root, "local"),
Documents: filepath.Join(root, "docs"),
})
if err == nil {
t.Fatal("expected deployment error when home path is a file")
}
}
func TestDeployManagedFileReportsReadErrorForDirectoryTarget(t *testing.T) {
target := t.TempDir()
if _, err := DeployManagedFile("directory", target, []byte("managed")); err == nil {
t.Fatal("expected read error for directory target")
}
}
func TestDeployManagedFileReportsDirectoryCreationError(t *testing.T) {
root := t.TempDir()
parentFile := filepath.Join(root, "parent")
if err := os.WriteFile(parentFile, []byte("not a directory"), 0o644); err != nil {
t.Fatal(err)
}
target := filepath.Join(parentFile, "profile.ps1")
if _, err := DeployManagedFile("profile", target, []byte("managed")); err == nil {
t.Fatal("expected directory creation error")
}
}
func TestNextBackupPathUsesNumberedSuffix(t *testing.T) {
target := filepath.Join(t.TempDir(), "profile.ps1")
if err := os.WriteFile(target+".bak", []byte("backup"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(target+".bak.1", []byte("backup"), 0o644); err != nil {
t.Fatal(err)
}
if got, want := NextBackupPath(target), target+".bak.2"; got != want {
t.Fatalf("backup path = %q, want %q", got, want)
}
}
func TestConfigureNeovimLeavesManagedCheckoutAlone(t *testing.T) {
root := t.TempDir()
target := filepath.Join(root, "local", "nvim")
if err := os.MkdirAll(target, 0o755); err != nil {
t.Fatal(err)
}
runner := &fakeRunner{responses: map[string]CommandResult{
commandKey("git", "-C", target, "config", "--get", "remote.origin.url"): {Stdout: "https://github.com/JMR-dev/nvim-config.git\n"},
}, defaultErr: true}
issue := ConfigureNeovim(context.Background(), runner, UserPaths{LocalAppData: filepath.Join(root, "local")})
if !IsEmptyIssue(issue) {
t.Fatalf("issue = %#v", issue)
}
if runner.called("git", "clone", "https://github.com/JMR-dev/nvim-config.git", target) {
t.Fatal("managed checkout should not be cloned over")
}
}
func TestConfigureNeovimClonesMissingConfig(t *testing.T) {
root := t.TempDir()
target := filepath.Join(root, "local", "nvim")
runner := &fakeRunner{}
issue := ConfigureNeovim(context.Background(), runner, UserPaths{LocalAppData: filepath.Join(root, "local")})
if !IsEmptyIssue(issue) {
t.Fatalf("issue = %#v", issue)
}
if !runner.called("git", "clone", "https://github.com/JMR-dev/nvim-config.git", target) {
t.Fatal("expected missing Neovim config to be cloned")
}
}
func TestConfigureNeovimBacksUpUnmanagedConfig(t *testing.T) {
root := t.TempDir()
local := filepath.Join(root, "local")
target := filepath.Join(local, "nvim")
if err := os.MkdirAll(target, 0o755); err != nil {
t.Fatal(err)
}
runner := &fakeRunner{responses: map[string]CommandResult{
commandKey("git", "-C", target, "config", "--get", "remote.origin.url"): {Err: os.ErrNotExist},
}}
issue := ConfigureNeovim(context.Background(), runner, UserPaths{LocalAppData: local})
if !IsEmptyIssue(issue) {
t.Fatalf("issue = %#v", issue)
}
if _, err := os.Stat(target + ".bak"); err != nil {
t.Fatalf("expected unmanaged config backup: %v", err)
}
if !runner.called("git", "clone", "https://github.com/JMR-dev/nvim-config.git", target) {
t.Fatal("expected clone after backing up unmanaged config")
}
}
func TestConfigureNeovimReportsCloneFailure(t *testing.T) {
root := t.TempDir()
target := filepath.Join(root, "local", "nvim")
runner := &fakeRunner{responses: map[string]CommandResult{
commandKey("git", "clone", "https://github.com/JMR-dev/nvim-config.git", target): {Err: os.ErrPermission, Stderr: "denied"},
}}
issue := ConfigureNeovim(context.Background(), runner, UserPaths{LocalAppData: filepath.Join(root, "local")})
if IsEmptyIssue(issue) || !strings.Contains(issue.Err.Error(), "denied") {
t.Fatalf("issue = %#v, want clone failure", issue)
}
}
func TestFileSummaryCoversAllDeploymentStatuses(t *testing.T) {
summary := FileSummary([]AssetDeployment{
{Target: "same"},
{Target: "new", Changed: true},
{Target: "backup", Changed: true, Backup: "backup.bak"},
})
joined := strings.Join(summary, "\n")
for _, want := range []string{"already managed", "written", "backed up"} {
if !strings.Contains(joined, want) {
t.Fatalf("summary missing %q: %s", want, joined)
}
}
}
func TestPathContainsPathEntryUsesCleanCaseInsensitiveComparison(t *testing.T) {
entry := filepath.Join("C:\\", "Tools", "bin")
value := strings.Join([]string{filepath.Join("C:\\", "Other"), strings.ToLower(entry) + string(os.PathSeparator)}, string(os.PathListSeparator))
if !pathContainsPathEntry(value, entry) {
t.Fatalf("%q should contain %q", value, entry)
}
if pathContainsPathEntry(value, filepath.Join("C:\\", "Missing")) {
t.Fatal("unexpected missing path match")
}
}