1package updater
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "github.com/bnema/dumber/internal/application/port"
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12)
13
14func TestApplier_SelfUpdateBlockedReason(t *testing.T) {
15 // Create a temporary directory for test state
16 tmpDir := t.TempDir()
17 applier := NewApplier(tmpDir)
18
19 ctx := context.Background()
20
21 // Get the reason - the actual result depends on the environment
22 reason := applier.SelfUpdateBlockedReason(ctx)
23
24 // Verify it returns a valid reason type
25 validReasons := []port.SelfUpdateBlockedReason{
26 port.SelfUpdateAllowed,
27 port.SelfUpdateBlockedFlatpak,
28 port.SelfUpdateBlockedPacman,
29 port.SelfUpdateBlockedNotWritable,
30 }
31
32 found := false
33 for _, valid := range validReasons {
34 if reason == valid {
35 found = true
36 break
37 }
38 }
39 assert.True(t, found, "SelfUpdateBlockedReason returned invalid reason: %q", reason)
40}
41
42func TestApplier_CanSelfUpdate_ConsistentWithReason(t *testing.T) {
43 tmpDir := t.TempDir()
44 applier := NewApplier(tmpDir)
45 ctx := context.Background()
46
47 canUpdate := applier.CanSelfUpdate(ctx)
48 reason := applier.SelfUpdateBlockedReason(ctx)
49
50 // If CanSelfUpdate returns true, reason should be SelfUpdateAllowed
51 if canUpdate {
52 assert.Equal(t, port.SelfUpdateAllowed, reason,
53 "CanSelfUpdate=true but SelfUpdateBlockedReason=%q", reason)
54 } else {
55 // If CanSelfUpdate returns false, reason should NOT be SelfUpdateAllowed
56 assert.NotEqual(t, port.SelfUpdateAllowed, reason,
57 "CanSelfUpdate=false but SelfUpdateBlockedReason is empty")
58 }
59}
60
61func TestApplier_StagingPaths(t *testing.T) {
62 tmpDir := t.TempDir()
63 applier := NewApplier(tmpDir)
64
65 // Test staging directory path
66 stagingDir := applier.stagingDir()
67 assert.Equal(t, filepath.Join(tmpDir, stagingDirName), stagingDir)
68
69 // Test staged binary path
70 stagedPath := applier.stagedBinaryPath()
71 assert.Equal(t, filepath.Join(tmpDir, stagingDirName, stagedBinaryName), stagedPath)
72}
73
74func TestApplier_HasStagedUpdate(t *testing.T) {
75 tmpDir := t.TempDir()
76 applier := NewApplier(tmpDir)
77 ctx := context.Background()
78
79 // Initially no staged update
80 assert.False(t, applier.HasStagedUpdate(ctx), "should have no staged update initially")
81
82 // Create staging directory and file
83 stagingDir := applier.stagingDir()
84 require.NoError(t, os.MkdirAll(stagingDir, 0o755))
85
86 stagedPath := applier.stagedBinaryPath()
87
88 // Create a non-executable file (should not count as staged)
89 require.NoError(t, os.WriteFile(stagedPath, []byte("test"), 0o644))
90 assert.False(t, applier.HasStagedUpdate(ctx), "non-executable file should not count as staged")
91
92 // Make it executable
93 require.NoError(t, os.Chmod(stagedPath, 0o755))
94 assert.True(t, applier.HasStagedUpdate(ctx), "executable file should count as staged")
95}
96
97func TestApplier_ClearStagedUpdate(t *testing.T) {
98 tmpDir := t.TempDir()
99 applier := NewApplier(tmpDir)
100 ctx := context.Background()
101
102 // Create staged update
103 stagingDir := applier.stagingDir()
104 require.NoError(t, os.MkdirAll(stagingDir, 0o755))
105 stagedPath := applier.stagedBinaryPath()
106 require.NoError(t, os.WriteFile(stagedPath, []byte("test"), 0o755))
107
108 assert.True(t, applier.HasStagedUpdate(ctx), "should have staged update")
109
110 // Clear it
111 require.NoError(t, applier.ClearStagedUpdate(ctx))
112
113 assert.False(t, applier.HasStagedUpdate(ctx), "should have no staged update after clear")
114
115 // Verify directory was removed
116 _, err := os.Stat(stagingDir)
117 assert.True(t, os.IsNotExist(err), "staging directory should be removed")
118}
119
120func TestApplier_StageUpdate(t *testing.T) {
121 tmpDir := t.TempDir()
122 applier := NewApplier(tmpDir)
123 ctx := context.Background()
124
125 // Create a fake binary to stage
126 srcBinary := filepath.Join(tmpDir, "new-binary")
127 binaryContent := []byte("#!/bin/sh\necho 'new version'\n")
128 require.NoError(t, os.WriteFile(srcBinary, binaryContent, 0o755))
129
130 // Stage the update
131 require.NoError(t, applier.StageUpdate(ctx, srcBinary))
132
133 // Verify it was staged
134 assert.True(t, applier.HasStagedUpdate(ctx), "should have staged update")
135
136 // Verify content was copied
137 stagedContent, err := os.ReadFile(applier.stagedBinaryPath())
138 require.NoError(t, err)
139 assert.Equal(t, binaryContent, stagedContent, "staged content should match source")
140
141 // Verify permissions
142 info, err := os.Stat(applier.stagedBinaryPath())
143 require.NoError(t, err)
144 assert.Equal(t, os.FileMode(0o755), info.Mode().Perm(), "staged binary should be executable")
145}
146
147func TestNewApplierFromXDG(t *testing.T) {
148 // This test verifies NewApplierFromXDG doesn't panic and returns valid applier
149 applier, err := NewApplierFromXDG()
150 require.NoError(t, err)
151 require.NotNil(t, applier)
152
153 // Verify it has a valid state directory
154 assert.NotEmpty(t, applier.stateDir)
155}
156
157func TestSelfUpdateBlockedReason_Constants(t *testing.T) {
158 // Verify the constants are distinct
159 reasons := []port.SelfUpdateBlockedReason{
160 port.SelfUpdateAllowed,
161 port.SelfUpdateBlockedFlatpak,
162 port.SelfUpdateBlockedPacman,
163 port.SelfUpdateBlockedNotWritable,
164 }
165
166 seen := make(map[port.SelfUpdateBlockedReason]bool)
167 for _, r := range reasons {
168 assert.False(t, seen[r], "duplicate reason: %q", r)
169 seen[r] = true
170 }
171
172 // Verify SelfUpdateAllowed is empty string
173 assert.Equal(t, port.SelfUpdateAllowed, port.SelfUpdateBlockedReason(""),
174 "SelfUpdateAllowed should be empty string")
175}