Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions internal/diff/compare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,66 @@ func TestCompareSnapshots_EmptySnapshots(t *testing.T) {
assert.Equal(t, 0, result.TotalExtra())
assert.Equal(t, 0, result.TotalChanged())
}

func TestCompareSnapshotToRemote_WithMacOSPrefs(t *testing.T) {
isolateHome(t)
system := &snapshot.Snapshot{
MacOSPrefs: []snapshot.MacOSPref{
{Domain: "com.apple.dock", Key: "autohide", Value: "false"},
},
}
remote := &config.RemoteConfig{
MacOSPrefs: []config.RemoteMacOSPref{
{Domain: "com.apple.dock", Key: "autohide", Value: "true"},
{Domain: "com.apple.dock", Key: "tilesize", Value: "48"},
},
}

result := CompareSnapshotToRemote(system, remote, Source{Kind: "remote", Path: "user/cfg"})

require.NotNil(t, result.MacOS)
assert.Len(t, result.MacOS.Changed, 1)
assert.Equal(t, "autohide", result.MacOS.Changed[0].Key)
assert.Equal(t, "false", result.MacOS.Changed[0].System)
assert.Equal(t, "true", result.MacOS.Changed[0].Reference)
assert.Len(t, result.MacOS.Missing, 1)
assert.Equal(t, "tilesize", result.MacOS.Missing[0].Key)
}

func TestDiffDotfiles_GitSuffixNormalization(t *testing.T) {
isolateHome(t)

// Same repo, one with .git suffix and one without — should not report a change.
result := diffDotfiles(
"https://github.com/user/dotfiles.git",
"https://github.com/user/dotfiles",
)
assert.Nil(t, result.RepoChanged, "trailing .git suffix should be normalized away")
}

func TestDiffDotfiles_DifferentRepos(t *testing.T) {
isolateHome(t)

result := diffDotfiles(
"https://github.com/user/dotfiles-old",
"https://github.com/user/dotfiles-new",
)
require.NotNil(t, result.RepoChanged)
assert.Equal(t, "https://github.com/user/dotfiles-old", result.RepoChanged.System)
assert.Equal(t, "https://github.com/user/dotfiles-new", result.RepoChanged.Reference)
}

func TestDiffDotfiles_BothEmpty(t *testing.T) {
isolateHome(t)
result := diffDotfiles("", "")
assert.Nil(t, result.RepoChanged)
assert.False(t, result.Dirty)
assert.False(t, result.Unpushed)
}

func TestDiffDotfiles_EmptyReference(t *testing.T) {
isolateHome(t)
// Reference is empty → no change reported even if system has one.
result := diffDotfiles("https://github.com/user/dotfiles", "")
assert.Nil(t, result.RepoChanged)
}
95 changes: 95 additions & 0 deletions internal/diff/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,98 @@ func TestDiffResult_NilSections(t *testing.T) {
assert.Equal(t, 0, r.TotalChanged())
assert.False(t, r.HasChanges())
}

func TestDiffResult_HasChanges_DotfilesDirty(t *testing.T) {
r := &DiffResult{Dotfiles: &DotfilesDiff{Dirty: true}}
assert.True(t, r.HasChanges())
}

func TestDiffResult_HasChanges_DotfilesUnpushed(t *testing.T) {
r := &DiffResult{Dotfiles: &DotfilesDiff{Unpushed: true}}
assert.True(t, r.HasChanges())
}

func TestDiffResult_HasChanges_DotfilesRepoChanged(t *testing.T) {
r := &DiffResult{Dotfiles: &DotfilesDiff{RepoChanged: &ValueChange{System: "a", Reference: "b"}}}
assert.True(t, r.HasChanges())
}

func TestDiffResult_HasChanges_DotfilesEmptyNoChange(t *testing.T) {
r := &DiffResult{Dotfiles: &DotfilesDiff{}}
assert.False(t, r.HasChanges())
}

func TestDiffResult_HasChanges_Shell(t *testing.T) {
r := &DiffResult{Shell: &ShellDiff{ThemeChanged: true}}
assert.True(t, r.HasChanges())
}

func TestDiffResult_TotalChanged_DotfilesRepoChanged(t *testing.T) {
r := &DiffResult{Dotfiles: &DotfilesDiff{RepoChanged: &ValueChange{System: "old", Reference: "new"}}}
assert.Equal(t, 1, r.TotalChanged())
}

func TestDiffResult_TotalChanged_DotfilesNilRepoChanged(t *testing.T) {
r := &DiffResult{Dotfiles: &DotfilesDiff{Dirty: true}}
assert.Equal(t, 0, r.TotalChanged())
}

func TestDiffResult_TotalChanged_Shell(t *testing.T) {
r := &DiffResult{Shell: &ShellDiff{PluginsChanged: true}}
assert.Equal(t, 1, r.TotalChanged())
}

func TestDiffResult_TotalChanged_DotfilesAndShell(t *testing.T) {
r := &DiffResult{
Dotfiles: &DotfilesDiff{RepoChanged: &ValueChange{System: "a", Reference: "b"}},
Shell: &ShellDiff{ThemeChanged: true},
}
assert.Equal(t, 2, r.TotalChanged())
}

func TestToSet(t *testing.T) {
tests := []struct {
name string
input []string
wantKeys []string
}{
{"nil input", nil, nil},
{"empty input", []string{}, nil},
{"single item", []string{"a"}, []string{"a"}},
{"multiple items", []string{"a", "b", "c"}, []string{"a", "b", "c"}},
{"duplicates collapse", []string{"a", "a", "b"}, []string{"a", "b"}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ToSet(tt.input)
assert.Equal(t, len(tt.wantKeys), len(got))
for _, k := range tt.wantKeys {
assert.True(t, got[k], "expected key %q in set", k)
}
})
}
}

func TestPluginsEqual(t *testing.T) {
tests := []struct {
name string
a, b []string
want bool
}{
{"both nil", nil, nil, true},
{"both empty", []string{}, []string{}, true},
{"identical order", []string{"git", "z"}, []string{"git", "z"}, true},
{"different order", []string{"z", "git"}, []string{"git", "z"}, true},
{"different length", []string{"a"}, []string{"a", "b"}, false},
{"different elements", []string{"a", "b"}, []string{"a", "c"}, false},
// len guard (2==2) passes, but the set built from ["a","a"] lacks "b" → false.
{"duplicates in a", []string{"a", "a"}, []string{"a", "b"}, false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, PluginsEqual(tt.a, tt.b))
})
}
}
167 changes: 167 additions & 0 deletions internal/diff/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,170 @@ func TestFormatTerminal_PackagesOnly(t *testing.T) {
FormatTerminal(result, true)
})
}

func TestFormatTerminal_DotfilesSection(t *testing.T) {
tests := []struct {
name string
result *DiffResult
}{
{
name: "repo changed",
result: &DiffResult{
Source: Source{Kind: "remote", Path: "user/slug"},
Dotfiles: &DotfilesDiff{RepoChanged: &ValueChange{System: "old-repo", Reference: "new-repo"}},
},
},
{
name: "dirty",
result: &DiffResult{
Source: Source{Kind: "local", Path: "test.json"},
Dotfiles: &DotfilesDiff{Dirty: true},
},
},
{
name: "unpushed",
result: &DiffResult{
Source: Source{Kind: "local", Path: "test.json"},
Dotfiles: &DotfilesDiff{Unpushed: true},
},
},
{
name: "all dotfiles conditions",
result: &DiffResult{
Source: Source{Kind: "file", Path: "snap.json"},
Dotfiles: &DotfilesDiff{
RepoChanged: &ValueChange{System: "a", Reference: "b"},
Dirty: true,
Unpushed: true,
},
},
},
{
name: "empty dotfiles skips section",
result: &DiffResult{
Source: Source{Kind: "local", Path: "test.json"},
Dotfiles: &DotfilesDiff{},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.NotPanics(t, func() {
FormatTerminal(tt.result, false)
})
})
}
}

func TestFormatTerminal_ShellSection(t *testing.T) {
tests := []struct {
name string
result *DiffResult
}{
{
name: "theme changed",
result: &DiffResult{
Source: Source{Kind: "remote", Path: "user/slug"},
Shell: &ShellDiff{
ThemeChanged: true,
LocalTheme: "robbyrussell",
ReferenceTheme: "agnoster",
},
},
},
{
name: "plugins changed",
result: &DiffResult{
Source: Source{Kind: "remote", Path: "user/slug"},
Shell: &ShellDiff{
PluginsChanged: true,
LocalPlugins: []string{"git"},
ReferencePlugins: []string{"git", "z"},
},
},
},
{
name: "theme and plugins changed",
result: &DiffResult{
Source: Source{Kind: "remote", Path: "user/slug"},
Shell: &ShellDiff{
ThemeChanged: true,
LocalTheme: "",
ReferenceTheme: "agnoster",
PluginsChanged: true,
LocalPlugins: nil,
ReferencePlugins: []string{"git"},
},
},
},
{
name: "shell diff with no changes skips section",
result: &DiffResult{
Source: Source{Kind: "remote", Path: "user/slug"},
Shell: &ShellDiff{ThemeChanged: false, PluginsChanged: false},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.NotPanics(t, func() {
FormatTerminal(tt.result, false)
})
})
}
}

func TestFormatJSON_WithShellAndDotfiles(t *testing.T) {
result := &DiffResult{
Source: Source{Kind: "remote", Path: "user/cfg"},
Shell: &ShellDiff{
ThemeChanged: true,
LocalTheme: "robbyrussell",
ReferenceTheme: "agnoster",
},
Dotfiles: &DotfilesDiff{
RepoChanged: &ValueChange{System: "old", Reference: "new"},
},
}

data, err := FormatJSON(result)
require.NoError(t, err)

var parsed map[string]interface{}
err = json.Unmarshal(data, &parsed)
require.NoError(t, err)

assert.Contains(t, parsed, "shell")
assert.Contains(t, parsed, "dotfiles")

// Shell adds 1 to TotalChanged; dotfiles.RepoChanged adds 1 → total 2
summary := parsed["summary"].(map[string]interface{})
assert.Equal(t, float64(2), summary["changed"])
}

func TestFormatJSON_WithDevToolsSection(t *testing.T) {
result := &DiffResult{
Source: Source{Kind: "local", Path: "snap.json"},
DevTools: &DevToolDiff{
Missing: []string{"rust"},
Extra: []string{"python"},
Changed: []DevToolDelta{{Name: "go", System: "1.22", Reference: "1.24"}},
Common: 2,
},
}

data, err := FormatJSON(result)
require.NoError(t, err)

var parsed map[string]interface{}
err = json.Unmarshal(data, &parsed)
require.NoError(t, err)

assert.Contains(t, parsed, "dev_tools")
summary := parsed["summary"].(map[string]interface{})
assert.Equal(t, float64(1), summary["missing"])
assert.Equal(t, float64(1), summary["extra"])
assert.Equal(t, float64(1), summary["changed"])
}
12 changes: 9 additions & 3 deletions internal/permissions/screen_recording_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ import (
"github.com/stretchr/testify/assert"
)

// TestHasScreenRecordingPermission_Returns verifies the function returns a bool.
// On non-darwin/non-cgo builds the stub always returns false; on darwin+cgo
// the result depends on system permissions, so we only check the type.
func TestHasScreenRecordingPermission_Returns(t *testing.T) {
result := HasScreenRecordingPermission()
assert.IsType(t, true, result)
}

// TestOpenScreenRecordingSettings is intentionally omitted: the function's only
// effect is opening System Settings UI on macOS, which cannot be meaningfully
// unit-tested without side-effecting the developer's machine.
// TestOpenScreenRecordingSettings_Returns verifies the non-darwin/non-cgo stub
// is a no-op and returns nil.
func TestOpenScreenRecordingSettings_Returns(t *testing.T) {
err := OpenScreenRecordingSettings()
assert.NoError(t, err)
}
29 changes: 29 additions & 0 deletions internal/search/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,35 @@ func TestQueryAPI_CaskAndNpmFlags(t *testing.T) {
}
}

// ---------------------------------------------------------------------------
// getAPIBase env var handling
// ---------------------------------------------------------------------------

func TestGetAPIBase_DefaultURL(t *testing.T) {
t.Setenv("OPENBOOT_API_URL", "")
base := getAPIBase()
assert.Equal(t, "https://openboot.dev/api", base)
}

func TestGetAPIBase_ValidHTTPSOverride(t *testing.T) {
t.Setenv("OPENBOOT_API_URL", "https://staging.openboot.dev")
base := getAPIBase()
assert.Equal(t, "https://staging.openboot.dev/api", base)
}

func TestGetAPIBase_ValidLocalhostOverride(t *testing.T) {
t.Setenv("OPENBOOT_API_URL", "http://localhost:8080")
base := getAPIBase()
assert.Equal(t, "http://localhost:8080/api", base)
}

func TestGetAPIBase_DisallowedURLFallsBack(t *testing.T) {
// Plain HTTP to a non-loopback host must not be accepted.
t.Setenv("OPENBOOT_API_URL", "http://evil.example.com")
base := getAPIBase()
assert.Equal(t, "https://openboot.dev/api", base)
}

// ---------------------------------------------------------------------------
// Package field mapping
// ---------------------------------------------------------------------------
Expand Down
Loading