cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 465 lines 11 kB view raw
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}