1package platform
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8)
9
10func TestActivateDevelopmentEnvironment(t *testing.T) {
11 originalCwd, err := os.Getwd()
12 if err != nil {
13 t.Fatal(err)
14 }
15 t.Cleanup(func() {
16 if err := os.Chdir(originalCwd); err != nil {
17 t.Errorf("restore working directory: %v", err)
18 }
19 })
20
21 distinctCwd := t.TempDir()
22 tests := []struct {
23 name string
24 environment string
25 cwd string
26 wantErr bool
27 check func(*testing.T, string)
28 }{
29 {
30 name: "unset preserves existing XDG roots",
31 check: func(t *testing.T, _ string) {
32 t.Helper()
33 if got := os.Getenv("XDG_CONFIG_HOME"); got != "/existing/config" {
34 t.Errorf("XDG_CONFIG_HOME = %q", got)
35 }
36 if got := os.Getenv("XDG_STATE_HOME"); got != "/existing/state" {
37 t.Errorf("XDG_STATE_HOME = %q", got)
38 }
39 if got := os.Getenv("XDG_RUNTIME_DIR"); got != "/existing/runtime" {
40 t.Errorf("XDG_RUNTIME_DIR = %q", got)
41 }
42 },
43 },
44 {
45 name: "valid name overrides XDG roots",
46 environment: "dev",
47 check: func(t *testing.T, cwd string) {
48 t.Helper()
49 root := filepath.Join(cwd, ".dev", "dev")
50 if got := os.Getenv("XDG_CONFIG_HOME"); got != filepath.Join(root, "config") {
51 t.Errorf("XDG_CONFIG_HOME = %q", got)
52 }
53 if got := os.Getenv("XDG_STATE_HOME"); got != filepath.Join(root, "state") {
54 t.Errorf("XDG_STATE_HOME = %q", got)
55 }
56 if got := os.Getenv("XDG_RUNTIME_DIR"); got != filepath.Join(root, "runtime") {
57 t.Errorf("XDG_RUNTIME_DIR = %q", got)
58 }
59 if got, want := os.Getenv("VEV_ENV_ROOT"), filepath.Join(cwd, ".dev"); got != want {
60 t.Errorf("VEV_ENV_ROOT = %q, want %q", got, want)
61 }
62 if _, err := os.Stat(root); !os.IsNotExist(err) {
63 t.Errorf("activation created root or returned unexpected error: %v", err)
64 }
65 },
66 },
67 {
68 name: "resolved root survives cwd change",
69 environment: "persisted",
70 check: func(t *testing.T, cwd string) {
71 t.Helper()
72 root := filepath.Join(cwd, ".dev", "persisted")
73 other := t.TempDir()
74 if err := os.Chdir(other); err != nil {
75 t.Fatal(err)
76 }
77 if err := ActivateDevelopmentEnvironment(); err != nil {
78 t.Fatal(err)
79 }
80 if got := os.Getenv("XDG_STATE_HOME"); got != filepath.Join(root, "state") {
81 t.Errorf("XDG_STATE_HOME after cwd change = %q", got)
82 }
83 },
84 },
85 {name: "distinct name alpha", environment: "alpha", cwd: distinctCwd},
86 {name: "distinct name beta", environment: "beta", cwd: distinctCwd},
87 {name: "reject traversal", environment: "../live", wantErr: true},
88 {name: "reject separator", environment: "a/b", wantErr: true},
89 {name: "reject whitespace", environment: "two words", wantErr: true},
90 {name: "reject leading punctuation", environment: ".hidden", wantErr: true},
91 {name: "reject 65 characters", environment: strings.Repeat("a", 65), wantErr: true},
92 }
93
94 for _, tt := range tests {
95 t.Run(tt.name, func(t *testing.T) {
96 cwd := tt.cwd
97 if cwd == "" {
98 cwd = t.TempDir()
99 }
100 if err := os.Chdir(cwd); err != nil {
101 t.Fatal(err)
102 }
103 canonicalCwd, getwdErr := os.Getwd()
104 if getwdErr != nil {
105 t.Fatal(getwdErr)
106 }
107 t.Setenv("VEV_ENV", tt.environment)
108 t.Setenv("VEV_ENV_ROOT", "")
109 t.Setenv("XDG_CONFIG_HOME", "/existing/config")
110 t.Setenv("XDG_STATE_HOME", "/existing/state")
111 t.Setenv("XDG_RUNTIME_DIR", "/existing/runtime")
112
113 err := ActivateDevelopmentEnvironment()
114 if tt.wantErr {
115 if err == nil || !strings.Contains(err.Error(), "VEV_ENV") {
116 t.Fatalf("ActivateDevelopmentEnvironment() error = %v", err)
117 }
118 for name, want := range map[string]string{
119 "XDG_CONFIG_HOME": "/existing/config",
120 "XDG_STATE_HOME": "/existing/state",
121 "XDG_RUNTIME_DIR": "/existing/runtime",
122 "VEV_ENV_ROOT": "",
123 } {
124 if got := os.Getenv(name); got != want {
125 t.Errorf("%s changed to %q, want %q", name, got, want)
126 }
127 }
128 return
129 }
130 if err != nil {
131 t.Fatal(err)
132 }
133 if tt.check != nil {
134 tt.check(t, canonicalCwd)
135 }
136 })
137 }
138}
139
140func TestDevelopmentEnvironmentTempDir(t *testing.T) {
141 absoluteBase := filepath.Join(t.TempDir(), ".dev")
142 tests := []struct {
143 name string
144 environment string
145 base string
146 want string
147 wantActive bool
148 }{
149 {name: "inactive without environment", base: absoluteBase},
150 {name: "inactive without inherited base", environment: "work"},
151 {
152 name: "active environment",
153 environment: "work",
154 base: absoluteBase,
155 want: filepath.Join(absoluteBase, "work", "tmp"),
156 wantActive: true,
157 },
158 }
159
160 for _, tt := range tests {
161 t.Run(tt.name, func(t *testing.T) {
162 t.Setenv("VEV_ENV", tt.environment)
163 t.Setenv("VEV_ENV_ROOT", tt.base)
164
165 got, active := DevelopmentEnvironmentTempDir()
166 if got != tt.want || active != tt.wantActive {
167 t.Fatalf("DevelopmentEnvironmentTempDir() = (%q, %v), want (%q, %v)", got, active, tt.want, tt.wantActive)
168 }
169 if active && !filepath.IsAbs(got) {
170 t.Errorf("DevelopmentEnvironmentTempDir() = %q, want absolute path", got)
171 }
172 })
173 }
174}
175
176func TestActivateDevelopmentEnvironmentRecomputesProfileFromInheritedBase(t *testing.T) {
177 originalCwd, err := os.Getwd()
178 if err != nil {
179 t.Fatal(err)
180 }
181 t.Cleanup(func() {
182 if err := os.Chdir(originalCwd); err != nil {
183 t.Errorf("restore working directory: %v", err)
184 }
185 })
186
187 if err := os.Chdir(t.TempDir()); err != nil {
188 t.Fatal(err)
189 }
190 invocationCwd, err := os.Getwd()
191 if err != nil {
192 t.Fatal(err)
193 }
194 t.Setenv("VEV_ENV", "alpha")
195 t.Setenv("VEV_ENV_ROOT", "")
196 if err := ActivateDevelopmentEnvironment(); err != nil {
197 t.Fatal(err)
198 }
199 base := filepath.Join(invocationCwd, ".dev")
200 if got := os.Getenv("VEV_ENV_ROOT"); got != base {
201 t.Fatalf("VEV_ENV_ROOT = %q, want %q", got, base)
202 }
203
204 if err := os.Chdir(t.TempDir()); err != nil {
205 t.Fatal(err)
206 }
207 if err := os.Setenv("VEV_ENV", "beta"); err != nil {
208 t.Fatal(err)
209 }
210 if err := ActivateDevelopmentEnvironment(); err != nil {
211 t.Fatal(err)
212 }
213
214 betaRoot := filepath.Join(base, "beta")
215 for name, want := range map[string]string{
216 "VEV_ENV_ROOT": base,
217 "XDG_CONFIG_HOME": filepath.Join(betaRoot, "config"),
218 "XDG_STATE_HOME": filepath.Join(betaRoot, "state"),
219 "XDG_RUNTIME_DIR": filepath.Join(betaRoot, "runtime"),
220 } {
221 if got := os.Getenv(name); got != want {
222 t.Errorf("%s = %q, want %q", name, got, want)
223 }
224 }
225}
226
227func TestStateDir(t *testing.T) {
228 tests := []struct {
229 name string
230 xdgState string
231 home string
232 want func(*testing.T, string, string) string
233 }{
234 {
235 name: "xdg state home",
236 xdgState: filepath.Join(t.TempDir(), "xdg-state"),
237 home: filepath.Join(t.TempDir(), "home"),
238 want: func(_ *testing.T, xdgState, _ string) string {
239 return filepath.Join(xdgState, "vev")
240 },
241 },
242 {
243 name: "home fallback",
244 home: filepath.Join(t.TempDir(), "home"),
245 want: func(_ *testing.T, _, home string) string {
246 return filepath.Join(home, ".local", "state", "vev")
247 },
248 },
249 {
250 name: "temp fallback without home",
251 want: func(t *testing.T, _, _ string) string {
252 t.Helper()
253 return filepath.Join(os.TempDir(), "vev-state-")
254 },
255 },
256 }
257
258 for _, tt := range tests {
259 t.Run(tt.name, func(t *testing.T) {
260 t.Setenv("XDG_STATE_HOME", tt.xdgState)
261 t.Setenv("HOME", tt.home)
262
263 want := tt.want(t, tt.xdgState, tt.home)
264 got := StateDir()
265 if strings.HasSuffix(want, "vev-state-") {
266 if !strings.HasPrefix(got, want) {
267 t.Fatalf("StateDir() = %q, want prefix %q", got, want)
268 }
269 return
270 }
271 if got != want {
272 t.Fatalf("StateDir() = %q, want %q", got, want)
273 }
274 })
275 }
276}
277
278func TestConfigPath(t *testing.T) {
279 tests := []struct {
280 name string
281 xdgConfig string
282 home string
283 want func(*testing.T, string, string) string
284 }{
285 {
286 name: "xdg config home",
287 xdgConfig: filepath.Join(t.TempDir(), "xdg-config"),
288 home: filepath.Join(t.TempDir(), "home"),
289 want: func(_ *testing.T, xdgConfig, _ string) string {
290 return filepath.Join(xdgConfig, "vev", "config")
291 },
292 },
293 {
294 name: "home fallback",
295 home: filepath.Join(t.TempDir(), "home"),
296 want: func(_ *testing.T, _, home string) string {
297 return filepath.Join(home, ".config", "vev", "config")
298 },
299 },
300 {
301 name: "temp fallback without home",
302 want: func(t *testing.T, _, _ string) string {
303 t.Helper()
304 return filepath.Join(os.TempDir(), "vev-config-")
305 },
306 },
307 }
308
309 for _, tt := range tests {
310 t.Run(tt.name, func(t *testing.T) {
311 t.Setenv("XDG_CONFIG_HOME", tt.xdgConfig)
312 t.Setenv("HOME", tt.home)
313
314 want := tt.want(t, tt.xdgConfig, tt.home)
315 got := ConfigPath()
316 if strings.HasSuffix(want, "vev-config-") {
317 if !strings.HasPrefix(got, want) || filepath.Base(got) != "config" {
318 t.Fatalf("ConfigPath() = %q, want prefix %q and base config", got, want)
319 }
320 return
321 }
322 if got != want {
323 t.Fatalf("ConfigPath() = %q, want %q", got, want)
324 }
325 })
326 }
327}
328
329func TestDirOrHome(t *testing.T) {
330 existing := t.TempDir()
331 home := filepath.Join(t.TempDir(), "home")
332 nonexistent := filepath.Join(t.TempDir(), "missing")
333
334 tests := []struct {
335 name string
336 dir string
337 home string
338 want string
339 }{
340 {name: "existing dir", dir: existing, home: home, want: existing},
341 {name: "non-existent dir falls back to home", dir: nonexistent, home: home, want: home},
342 {name: "non-existent dir without home falls back to root", dir: nonexistent, want: "/"},
343 }
344
345 for _, tt := range tests {
346 t.Run(tt.name, func(t *testing.T) {
347 t.Setenv("HOME", tt.home)
348
349 if got := DirOrHome(tt.dir); got != tt.want {
350 t.Fatalf("DirOrHome(%q) = %q, want %q", tt.dir, got, tt.want)
351 }
352 })
353 }
354}