cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
29
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 45b93e8b8739f75beca6952c05f7889faef41b24 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}