1package config
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "syscall"
8 "testing"
9
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12)
13
14func configRoot(t *testing.T) string {
15 t.Helper()
16 root := t.TempDir()
17 t.Setenv("XDG_CONFIG_HOME", root)
18 return filepath.Join(root, "nefercap", "config")
19}
20
21func saveConfig(t *testing.T, path, text string) {
22 t.Helper()
23 require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o700))
24 require.NoError(t, os.WriteFile(path, []byte(text), 0o600))
25}
26
27func TestLoadDefault(t *testing.T) {
28 configRoot(t)
29 got, err := Load()
30 require.NoError(t, err)
31 assert.True(t, got.Grid)
32}
33
34func TestLoadGrid(t *testing.T) {
35 for _, tc := range []struct {
36 name, text string
37 grid bool
38 }{
39 {"empty", "", true},
40 {"comments", "# selector settings\n\n", true},
41 {"on", "selector.grid = on\n", true},
42 {"off", "selector.grid = off\n", false},
43 {"whitespace", " selector.grid\t= off \n", false},
44 {"CRLF", "# config\r\nselector.grid = off\r\n", false},
45 {"BOM", "\ufeffselector.grid = off\n", false},
46 } {
47 t.Run(tc.name, func(t *testing.T) {
48 path := configRoot(t)
49 saveConfig(t, path, tc.text)
50 got, err := Load()
51 require.NoError(t, err)
52 assert.Equal(t, tc.grid, got.Grid)
53 })
54 }
55}
56
57func TestLoadRejectsInvalid(t *testing.T) {
58 for _, tc := range []struct{ text, want string }{
59 {"selector.grid = maybe", "line 1: selector.grid must be on or off"},
60 {"selector.grid", "line 1: expected each of"},
61 {"selector.grid =", "line 1: selector.grid must be on or off"},
62 {"selector.other = on", "line 1: expected each of"},
63 {"selector.grid = on\nselector.grid = off", "line 2: expected each of"},
64 {"selector.freeze = yes", "line 1: selector.freeze must be on or off"},
65 {"selector.freeze = on\nselector.freeze = on", "line 2: expected each of"},
66 {strings.Repeat("#", maxBytes+1), "exceeds 4096 bytes"},
67 } {
68 t.Run(tc.text[:min(len(tc.text), 40)], func(t *testing.T) {
69 path := configRoot(t)
70 saveConfig(t, path, tc.text)
71 _, err := Load()
72 require.ErrorContains(t, err, tc.want)
73 assert.Contains(t, err.Error(), path)
74 })
75 }
76}
77
78func TestLoadHomeFallback(t *testing.T) {
79 home := t.TempDir()
80 t.Setenv("HOME", home)
81 t.Setenv("XDG_CONFIG_HOME", "")
82 saveConfig(t, filepath.Join(home, ".config", "nefercap", "config"), "selector.grid = off\n")
83 got, err := Load()
84 require.NoError(t, err)
85 assert.False(t, got.Grid)
86}
87
88func TestLoadRelativeXDGUsesHome(t *testing.T) {
89 home := t.TempDir()
90 t.Setenv("HOME", home)
91 t.Setenv("XDG_CONFIG_HOME", "relative-config")
92 saveConfig(t, filepath.Join(home, ".config", "nefercap", "config"), "selector.grid = off\n")
93 got, err := Load()
94 require.NoError(t, err)
95 assert.False(t, got.Grid)
96}
97
98func TestLoadNoConfigDirectoryUsesDefaults(t *testing.T) {
99 t.Setenv("HOME", "")
100 t.Setenv("XDG_CONFIG_HOME", "")
101 got, err := Load()
102 require.NoError(t, err)
103 assert.True(t, got.Grid)
104}
105
106func TestLoadFIFORejectedWithoutBlocking(t *testing.T) {
107 path := configRoot(t)
108 require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o700))
109 require.NoError(t, syscall.Mkfifo(path, 0o600))
110 _, err := Load()
111 require.ErrorContains(t, err, "expected a regular file")
112}
113
114func TestLoadSizeBoundary(t *testing.T) {
115 path := configRoot(t)
116 saveConfig(t, path, strings.Repeat("#", maxBytes))
117 got, err := Load()
118 require.NoError(t, err)
119 assert.True(t, got.Grid)
120}
121
122func TestLoadDirectoryRejected(t *testing.T) {
123 path := configRoot(t)
124 require.NoError(t, os.MkdirAll(path, 0o700))
125 _, err := Load()
126 require.ErrorContains(t, err, "expected a regular file")
127}
128
129func TestLoadPathsAndOutput(t *testing.T) {
130 home := t.TempDir()
131 t.Setenv("HOME", home)
132 path := configRoot(t)
133 saveConfig(t, path, "# c\nscreenshot.dir = ~/Pics/Shots\nvideo.dir=/var/tmp/vids/\nscreenshot.output = file+clipboard\nselector.grid = off\n")
134 got, err := Load()
135 require.NoError(t, err)
136 assert.Equal(t, Settings{Grid: false, Freeze: true, ScreenshotDir: filepath.Join(home, "Pics", "Shots"), VideoDir: "/var/tmp/vids", Output: OutputFileClipboard}, got)
137
138 saveConfig(t, path, "screenshot.dir = ~/\nscreenshot.output = clipboard\n")
139 got, err = Load()
140 require.NoError(t, err)
141 assert.Equal(t, home, got.ScreenshotDir)
142 assert.Equal(t, OutputClipboard, got.Output)
143 assert.Empty(t, got.VideoDir)
144}
145
146func TestLoadDefaultsOutputFile(t *testing.T) {
147 configRoot(t)
148 got, err := Load()
149 require.NoError(t, err)
150 assert.Equal(t, Settings{Grid: true, Freeze: true, Output: OutputFile}, got)
151}
152
153func TestLoadFreeze(t *testing.T) {
154 for _, tc := range []struct {
155 text string
156 freeze bool
157 }{
158 {"", true},
159 {"selector.freeze = on\n", true},
160 {"selector.freeze = off\n", false},
161 } {
162 t.Run(tc.text, func(t *testing.T) {
163 saveConfig(t, configRoot(t), tc.text)
164 got, err := Load()
165 require.NoError(t, err)
166 assert.Equal(t, tc.freeze, got.Freeze)
167 assert.True(t, got.Grid, "independent of selector.grid")
168 })
169 }
170}
171
172func TestLoadRejectsInvalidNewKeys(t *testing.T) {
173 for _, tc := range []struct{ text, want string }{
174 {"screenshot.dir = Pictures", "line 1: screenshot.dir must be an absolute path or start with ~/"},
175 {"video.dir = ./v", "line 1: video.dir must be an absolute path"},
176 {"video.dir = ~user/v", "line 1: video.dir must be an absolute path"},
177 {"video.dir = ~", "line 1: video.dir must be an absolute path"},
178 {"video.dir =", "line 1: video.dir must be a non-empty path"},
179 {"screenshot.output = both", "line 1: screenshot.output must be file, file+clipboard or clipboard"},
180 {"screenshot.output = File", "screenshot.output must be"},
181 {"screenshot.dir = /a\nscreenshot.dir = /b", "line 2: expected each of"},
182 {"screenshot.output = file\n\nscreenshot.output = file", "line 3: expected each of"},
183 {"video.dir = /a\nvideo.dir = /a", "line 2: expected each of"},
184 {"video.output = file", "line 1: expected each of"},
185 // The first problem in line order is reported.
186 {"selector.grid = maybe\nselector.grid = on", "line 1: selector.grid must be on or off"},
187 {"selector.grid = maybe\nnoequals", "line 1: selector.grid must be on or off"},
188 {"selector.grid = maybe\n= x", "line 1: selector.grid must be on or off"},
189 {"selector.grid = maybe\nscreenshot.dir = /a\nscreenshot.dir = /b", "line 1: selector.grid must be on or off"},
190 {"foo = 1\nbar", "line 1: expected each of"},
191 {"selector.grid = on\nselector.grid = maybe", "line 2: expected each of"},
192 // '#' only starts a comment at the start of a line; values stay raw.
193 {"selector.grid = on # c", "line 1: selector.grid must be on or off"},
194 {"screenshot.output = \"file\"", "line 1: screenshot.output must be"},
195 {"# caf\xe9\nselector.grid = on", "not valid UTF-8"},
196 } {
197 t.Run(tc.text, func(t *testing.T) {
198 t.Setenv("HOME", t.TempDir())
199 path := configRoot(t)
200 saveConfig(t, path, tc.text)
201 _, err := Load()
202 require.ErrorContains(t, err, tc.want)
203 assert.Contains(t, err.Error(), path)
204 })
205 }
206}