cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
1package utils
2
3import (
4 "bytes"
5 "os"
6 "runtime"
7 "strings"
8 "testing"
9
10 "github.com/charmbracelet/log"
11)
12
13func getConfigPath() string {
14 switch runtime.GOOS {
15 case "windows":
16 appData := os.Getenv("APPDATA")
17 if appData != "" {
18 return appData + "\\noteleaf"
19 }
20 return "C:\\noteleaf"
21 default:
22 configHome := os.Getenv("XDG_CONFIG_HOME")
23 if configHome != "" {
24 return configHome + "/noteleaf"
25 }
26 return os.Getenv("HOME") + "/.config/noteleaf"
27 }
28}
29
30func simulateWindowsConfigPath() string {
31 appData := os.Getenv("APPDATA")
32 if appData != "" {
33 return appData + "\\noteleaf"
34 }
35 return "C:\\noteleaf"
36}
37
38func getBookStatusDisplay(status string) string {
39 switch status {
40 case "reading":
41 return "currently reading"
42 case "finished":
43 return "completed"
44 case "queued":
45 return "to be read"
46 default:
47 return status
48 }
49}
50
51func classifyMediaType(link string) string {
52 if strings.HasPrefix(link, "/m/") {
53 return "movie"
54 } else if strings.HasPrefix(link, "/tv/") {
55 return "tv"
56 }
57 return "unknown"
58}
59
60func validatePriority(priority string) bool {
61 switch strings.ToLower(priority) {
62 case "high", "medium", "low":
63 return true
64 case "h", "m", "l":
65 return true
66 case "1", "2", "3", "4", "5":
67 return true
68 default:
69 return false
70 }
71}
72
73func TestLogger(t *testing.T) {
74 t.Run("New", func(t *testing.T) {
75 t.Run("creates logger with info level", func(t *testing.T) {
76 logger := NewLogger("info", "text")
77 if logger == nil {
78 t.Fatal("Logger should not be nil")
79 }
80
81 if logger.GetLevel() != log.InfoLevel {
82 t.Errorf("Expected InfoLevel, got %v", logger.GetLevel())
83 }
84 })
85
86 t.Run("creates logger with debug level", func(t *testing.T) {
87 logger := NewLogger("debug", "text")
88 if logger.GetLevel() != log.DebugLevel {
89 t.Errorf("Expected DebugLevel, got %v", logger.GetLevel())
90 }
91 })
92
93 t.Run("creates logger with warn level", func(t *testing.T) {
94 logger := NewLogger("warn", "text")
95 if logger.GetLevel() != log.WarnLevel {
96 t.Errorf("Expected WarnLevel, got %v", logger.GetLevel())
97 }
98 })
99
100 t.Run("creates logger with warning level alias", func(t *testing.T) {
101 logger := NewLogger("warning", "text")
102 if logger.GetLevel() != log.WarnLevel {
103 t.Errorf("Expected WarnLevel, got %v", logger.GetLevel())
104 }
105 })
106
107 t.Run("creates logger with error level", func(t *testing.T) {
108 logger := NewLogger("error", "text")
109 if logger.GetLevel() != log.ErrorLevel {
110 t.Errorf("Expected ErrorLevel, got %v", logger.GetLevel())
111 }
112 })
113
114 t.Run("defaults to info level for invalid level", func(t *testing.T) {
115 logger := NewLogger("invalid", "text")
116 if logger.GetLevel() != log.InfoLevel {
117 t.Errorf("Expected InfoLevel for invalid input, got %v", logger.GetLevel())
118 }
119 })
120
121 t.Run("handles case insensitive levels", func(t *testing.T) {
122 logger := NewLogger("DEBUG", "text")
123 if logger.GetLevel() != log.DebugLevel {
124 t.Errorf("Expected DebugLevel for uppercase input, got %v", logger.GetLevel())
125 }
126 })
127
128 t.Run("creates logger with json format", func(t *testing.T) {
129 var buf bytes.Buffer
130 logger := NewLogger("info", "json")
131 logger.SetOutput(&buf)
132
133 logger.Info("test message")
134 output := buf.String()
135
136 if !strings.Contains(output, "{") || !strings.Contains(output, "}") {
137 t.Error("Expected JSON formatted output")
138 }
139 })
140
141 t.Run("creates logger with text format", func(t *testing.T) {
142 var buf bytes.Buffer
143 logger := NewLogger("info", "text")
144 logger.SetOutput(&buf)
145
146 logger.Info("test message")
147 output := buf.String()
148
149 if strings.Contains(output, "{") && strings.Contains(output, "}") {
150 t.Error("Expected text formatted output, not JSON")
151 }
152 })
153
154 t.Run("text format includes timestamp", func(t *testing.T) {
155 var buf bytes.Buffer
156 logger := NewLogger("info", "text")
157 logger.SetOutput(&buf)
158
159 logger.Info("test message")
160 output := buf.String()
161
162 if !strings.Contains(output, ":") {
163 t.Error("Expected timestamp in text format output")
164 }
165 })
166 })
167
168 t.Run("Get", func(t *testing.T) {
169 t.Run("returns global logger when set", func(t *testing.T) {
170 originalLogger := Logger
171 defer func() { Logger = originalLogger }()
172
173 testLogger := NewLogger("debug", "json")
174 Logger = testLogger
175
176 retrieved := GetLogger()
177 if retrieved != testLogger {
178 t.Error("GetLogger should return the global logger")
179 }
180 })
181
182 t.Run("creates default logger when global is nil", func(t *testing.T) {
183 originalLogger := Logger
184 defer func() { Logger = originalLogger }()
185
186 Logger = nil
187
188 retrieved := GetLogger()
189 if retrieved == nil {
190 t.Fatal("GetLogger should create a default logger")
191 }
192
193 if retrieved.GetLevel() != log.InfoLevel {
194 t.Error("Default logger should have InfoLevel")
195 }
196
197 if Logger != retrieved {
198 t.Error("Global logger should be set after GetLogger call")
199 }
200 })
201
202 t.Run("subsequent calls return same logger", func(t *testing.T) {
203 originalLogger := Logger
204 defer func() { Logger = originalLogger }()
205
206 Logger = nil
207
208 logger1 := GetLogger()
209 logger2 := GetLogger()
210
211 if logger1 != logger2 {
212 t.Error("Subsequent GetLogger calls should return the same instance")
213 }
214 })
215 })
216
217 t.Run("Integration", func(t *testing.T) {
218 t.Run("logger writes to stderr by default", func(t *testing.T) {
219 oldStderr := os.Stderr
220 r, w, _ := os.Pipe()
221 os.Stderr = w
222
223 logger := NewLogger("info", "text")
224 logger.Info("test message")
225
226 w.Close()
227 os.Stderr = oldStderr
228
229 var buf bytes.Buffer
230 buf.ReadFrom(r)
231 output := buf.String()
232
233 if !strings.Contains(output, "test message") {
234 t.Error("Logger should write to stderr by default")
235 }
236 })
237
238 t.Run("logger respects level filtering", func(t *testing.T) {
239 var buf bytes.Buffer
240 logger := NewLogger("error", "text")
241 logger.SetOutput(&buf)
242
243 logger.Debug("debug message")
244 logger.Info("info message")
245 logger.Warn("warn message")
246 logger.Error("error message")
247
248 output := buf.String()
249
250 if strings.Contains(output, "debug message") {
251 t.Error("Debug message should be filtered out at error level")
252 }
253 if strings.Contains(output, "info message") {
254 t.Error("Info message should be filtered out at error level")
255 }
256 if strings.Contains(output, "warn message") {
257 t.Error("Warn message should be filtered out at error level")
258 }
259 if !strings.Contains(output, "error message") {
260 t.Error("Error message should be included at error level")
261 }
262 })
263
264 t.Run("global logger persists between function calls", func(t *testing.T) {
265 originalLogger := Logger
266 defer func() { Logger = originalLogger }()
267
268 Logger = NewLogger("debug", "json")
269
270 retrieved := GetLogger()
271
272 if retrieved.GetLevel() != log.DebugLevel {
273 t.Error("Global logger settings should persist")
274 }
275 })
276 })
277}
278
279func TestTitlecase(t *testing.T) {
280 tests := []struct {
281 name string
282 input string
283 expected string
284 }{
285 {
286 name: "single word lowercase",
287 input: "hello",
288 expected: "Hello",
289 },
290 {
291 name: "single word uppercase",
292 input: "HELLO",
293 expected: "HELLO",
294 },
295 {
296 name: "multiple words",
297 input: "hello world",
298 expected: "Hello World",
299 },
300 {
301 name: "mixed case",
302 input: "hELLo WoRLD",
303 expected: "HELLo WoRLD",
304 },
305 {
306 name: "with punctuation",
307 input: "hello, world!",
308 expected: "Hello, World!",
309 },
310 {
311 name: "empty string",
312 input: "",
313 expected: "",
314 },
315 {
316 name: "with numbers",
317 input: "hello 123 world",
318 expected: "Hello 123 World",
319 },
320 {
321 name: "with special characters",
322 input: "hello-world_test",
323 expected: "Hello-World_test",
324 },
325 {
326 name: "already title case",
327 input: "Hello World",
328 expected: "Hello World",
329 },
330 {
331 name: "single character",
332 input: "a",
333 expected: "A",
334 },
335 {
336 name: "apostrophes",
337 input: "it's a beautiful day",
338 expected: "It's A Beautiful Day",
339 },
340 }
341
342 for _, tt := range tests {
343 t.Run(tt.name, func(t *testing.T) {
344 result := Titlecase(tt.input)
345 if result != tt.expected {
346 t.Errorf("Titlecase(%q) = %q, expected %q", tt.input, result, tt.expected)
347 }
348 })
349 }
350}
351
352func TestPlatformSpecificPaths(t *testing.T) {
353 t.Run("Windows Path Handling", func(t *testing.T) {
354 if runtime.GOOS == "windows" {
355 t.Run("APPDATA Environment Variable", func(t *testing.T) {
356 appData := os.Getenv("APPDATA")
357 if appData == "" {
358 t.Skip("APPDATA environment variable not set")
359 }
360
361 path := getConfigPath()
362 if !strings.Contains(path, appData) {
363 t.Errorf("Expected config path to contain APPDATA path %s, got %s", appData, path)
364 }
365 })
366 } else {
367 t.Run("Simulated Windows Path Handling", func(t *testing.T) {
368 originalAppData := os.Getenv("APPDATA")
369 defer os.Setenv("APPDATA", originalAppData)
370
371 os.Setenv("APPDATA", "C:\\Users\\Test\\AppData\\Roaming")
372
373 testPath := simulateWindowsConfigPath()
374 expected := "C:\\Users\\Test\\AppData\\Roaming"
375 if !strings.Contains(testPath, expected) {
376 t.Errorf("Expected Windows config path to contain %s, got %s", expected, testPath)
377 }
378 })
379 }
380 })
381
382 t.Run("Unix-like Path Handling", func(t *testing.T) {
383 if runtime.GOOS != "windows" {
384 t.Run("XDG Config Home", func(t *testing.T) {
385 originalConfigHome := os.Getenv("XDG_CONFIG_HOME")
386 defer os.Setenv("XDG_CONFIG_HOME", originalConfigHome)
387
388 testConfigHome := "/tmp/test-config"
389 os.Setenv("XDG_CONFIG_HOME", testConfigHome)
390
391 path := getConfigPath()
392 if !strings.Contains(path, testConfigHome) {
393 t.Errorf("Expected config path to contain XDG_CONFIG_HOME %s, got %s", testConfigHome, path)
394 }
395 })
396 }
397 })
398}
399
400func TestStatusFieldMatching(t *testing.T) {
401 t.Run("Book Status Field Access", func(t *testing.T) {
402 tests := []struct {
403 status string
404 expected string
405 }{
406 {"reading", "currently reading"},
407 {"finished", "completed"},
408 {"queued", "to be read"},
409 }
410
411 for _, tt := range tests {
412 t.Run(tt.status, func(t *testing.T) {
413 result := getBookStatusDisplay(tt.status)
414 if !strings.Contains(result, tt.expected) {
415 t.Errorf("Expected status display for %s to contain %s, got %s", tt.status, tt.expected, result)
416 }
417 })
418 }
419 })
420}
421
422func TestMediaTypeMatching(t *testing.T) {
423 t.Run("Media Type Classification", func(t *testing.T) {
424 tests := []struct {
425 link string
426 expectedType string
427 }{
428 {"/m/some-movie", "movie"},
429 {"/tv/some-show", "tv"},
430 {"/other/link", "unknown"},
431 }
432
433 for _, tt := range tests {
434 t.Run(tt.link, func(t *testing.T) {
435 result := classifyMediaType(tt.link)
436 if result != tt.expectedType {
437 t.Errorf("Expected media type %s for link %s, got %s", tt.expectedType, tt.link, result)
438 }
439 })
440 }
441 })
442}
443
444func TestTaskPriorityValidation(t *testing.T) {
445 t.Run("Priority String Validation", func(t *testing.T) {
446 tests := []struct {
447 priority string
448 valid bool
449 }{
450 {"high", true}, {"medium", true}, {"low", true},
451 {"H", true}, {"M", true}, {"L", true},
452 {"1", true}, {"5", true},
453 {"invalid", false},
454 }
455
456 for _, tt := range tests {
457 t.Run(tt.priority, func(t *testing.T) {
458 isValid := validatePriority(tt.priority)
459 if isValid != tt.valid {
460 t.Errorf("Expected priority %s to be valid=%t, got %t", tt.priority, tt.valid, isValid)
461 }
462 })
463 }
464 })
465}