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 main 684 lines 18 kB view raw
1package handlers 2 3import ( 4 "bytes" 5 "context" 6 "os" 7 "path/filepath" 8 "runtime" 9 "strings" 10 "testing" 11 "time" 12 13 "github.com/stormlightlabs/noteleaf/internal/store" 14) 15 16func createTestDir(t *testing.T) string { 17 tempDir, err := os.MkdirTemp("", "noteleaf-test-*") 18 if err != nil { 19 t.Fatalf("Failed to create temp dir: %v", err) 20 } 21 22 oldNoteleafConfig := os.Getenv("NOTELEAF_CONFIG") 23 oldNoteleafDataDir := os.Getenv("NOTELEAF_DATA_DIR") 24 os.Setenv("NOTELEAF_CONFIG", filepath.Join(tempDir, ".noteleaf.conf.toml")) 25 os.Setenv("NOTELEAF_DATA_DIR", tempDir) 26 27 t.Cleanup(func() { 28 os.Setenv("NOTELEAF_CONFIG", oldNoteleafConfig) 29 os.Setenv("NOTELEAF_DATA_DIR", oldNoteleafDataDir) 30 os.RemoveAll(tempDir) 31 }) 32 33 return tempDir 34} 35 36func getDbPath(config *store.Config) string { 37 var dbPath string 38 if config.DatabasePath != "" { 39 dbPath = config.DatabasePath 40 } else if config.DataDir != "" { 41 dbPath = filepath.Join(config.DataDir, "noteleaf.db") 42 } else { 43 dataDir, _ := store.GetDataDir() 44 dbPath = filepath.Join(dataDir, "noteleaf.db") 45 } 46 47 return dbPath 48} 49 50func TestSetup(t *testing.T) { 51 t.Run("creates database and config files", func(t *testing.T) { 52 _ = createTestDir(t) 53 ctx := context.Background() 54 55 err := Setup(ctx, []string{}) 56 if err != nil { 57 t.Errorf("Setup failed: %v", err) 58 } 59 60 config, err := store.LoadConfig() 61 if err != nil { 62 t.Fatalf("Failed to load config: %v", err) 63 } 64 65 dbPath := getDbPath(config) 66 67 if _, err := os.Stat(dbPath); os.IsNotExist(err) { 68 t.Error("Database file was not created") 69 } 70 71 configPath, err := store.GetConfigPath() 72 if err != nil { 73 t.Fatalf("Failed to get config path: %v", err) 74 } 75 76 if _, err := os.Stat(configPath); os.IsNotExist(err) { 77 t.Error("Config file was not created") 78 } 79 80 }) 81 82 t.Run("handles existing database gracefully", func(t *testing.T) { 83 _ = createTestDir(t) 84 ctx := context.Background() 85 86 err1 := Setup(ctx, []string{}) 87 if err1 != nil { 88 t.Errorf("First setup failed: %v", err1) 89 } 90 91 err2 := Setup(ctx, []string{}) 92 if err2 != nil { 93 t.Errorf("Second setup should not fail: %v", err2) 94 } 95 96 }) 97 98 t.Run("initializes migrations", func(t *testing.T) { 99 _ = createTestDir(t) 100 ctx := context.Background() 101 102 err := Setup(ctx, []string{}) 103 if err != nil { 104 t.Errorf("Setup failed: %v", err) 105 } 106 107 db, err := store.NewDatabase() 108 if err != nil { 109 t.Fatalf("Failed to connect to database: %v", err) 110 } 111 defer db.Close() 112 113 runner := store.NewMigrationRunner(db) 114 migrations, err := runner.GetAppliedMigrations() 115 if err != nil { 116 t.Fatalf("Failed to get migrations: %v", err) 117 } 118 119 if len(migrations) == 0 { 120 t.Error("No migrations were applied") 121 } 122 123 var count int 124 err = db.QueryRow("SELECT COUNT(*) FROM migrations").Scan(&count) 125 if err != nil { 126 t.Errorf("Failed to query migrations table: %v", err) 127 } 128 129 if count == 0 { 130 t.Error("Migrations table is empty") 131 } 132 133 }) 134} 135 136func TestReset(t *testing.T) { 137 t.Run("removes database and config files", func(t *testing.T) { 138 _ = createTestDir(t) 139 ctx := context.Background() 140 141 err := Setup(ctx, []string{}) 142 if err != nil { 143 t.Fatalf("Setup failed: %v", err) 144 } 145 146 // Determine database path using the same logic as Setup 147 config, err := store.LoadConfig() 148 if err != nil { 149 t.Fatalf("Failed to load config: %v", err) 150 } 151 152 dbPath := getDbPath(config) 153 154 configPath, err := store.GetConfigPath() 155 if err != nil { 156 t.Fatalf("Failed to get config path: %v", err) 157 } 158 159 if _, err := os.Stat(dbPath); os.IsNotExist(err) { 160 t.Fatal("Database should exist before reset") 161 } 162 163 if _, err := os.Stat(configPath); os.IsNotExist(err) { 164 t.Fatal("Config should exist before reset") 165 } 166 167 err = Reset(ctx, []string{}) 168 if err != nil { 169 t.Errorf("Reset failed: %v", err) 170 } 171 172 if _, err := os.Stat(dbPath); !os.IsNotExist(err) { 173 t.Error("Database file should be removed after reset") 174 } 175 176 if _, err := os.Stat(configPath); !os.IsNotExist(err) { 177 t.Error("Config file should be removed after reset") 178 } 179 180 }) 181 182 t.Run("handles non-existent files gracefully", func(t *testing.T) { 183 _ = createTestDir(t) 184 ctx := context.Background() 185 186 err := Reset(ctx, []string{}) 187 if err != nil { 188 t.Errorf("Reset should handle non-existent files: %v", err) 189 } 190 191 }) 192} 193 194func TestStatus(t *testing.T) { 195 t.Run("reports status when setup", func(t *testing.T) { 196 _ = createTestDir(t) 197 ctx := context.Background() 198 var buf bytes.Buffer 199 200 err := Setup(ctx, []string{}) 201 if err != nil { 202 t.Fatalf("Setup failed: %v", err) 203 } 204 205 err = Status(ctx, []string{}, &buf) 206 if err != nil { 207 t.Errorf("Status failed: %v", err) 208 } 209 210 }) 211 212 t.Run("reports status when not setup", func(t *testing.T) { 213 _ = createTestDir(t) 214 ctx := context.Background() 215 var buf bytes.Buffer 216 217 err := Status(ctx, []string{}, &buf) 218 if err != nil { 219 t.Errorf("Status should not fail when not setup: %v", err) 220 } 221 222 }) 223 224 t.Run("shows migration information", func(t *testing.T) { 225 _ = createTestDir(t) 226 ctx := context.Background() 227 var buf bytes.Buffer 228 229 err := Setup(ctx, []string{}) 230 if err != nil { 231 t.Fatalf("Setup failed: %v", err) 232 } 233 234 err = Status(ctx, []string{}, &buf) 235 if err != nil { 236 t.Errorf("Status failed: %v", err) 237 } 238 239 }) 240} 241 242func TestSetupErrorHandling(t *testing.T) { 243 t.Run("handles GetConfigDir error", func(t *testing.T) { 244 originalXDG := os.Getenv("XDG_CONFIG_HOME") 245 originalHome := os.Getenv("HOME") 246 originalNoteleafConfig := os.Getenv("NOTELEAF_CONFIG") 247 originalNoteleafDataDir := os.Getenv("NOTELEAF_DATA_DIR") 248 249 os.Unsetenv("NOTELEAF_CONFIG") 250 os.Unsetenv("NOTELEAF_DATA_DIR") 251 252 if runtime.GOOS == "windows" { 253 originalAppData := os.Getenv("APPDATA") 254 os.Unsetenv("APPDATA") 255 defer os.Setenv("APPDATA", originalAppData) 256 } else { 257 os.Unsetenv("XDG_CONFIG_HOME") 258 os.Unsetenv("HOME") 259 } 260 261 defer func() { 262 os.Setenv("XDG_CONFIG_HOME", originalXDG) 263 os.Setenv("HOME", originalHome) 264 os.Setenv("NOTELEAF_CONFIG", originalNoteleafConfig) 265 os.Setenv("NOTELEAF_DATA_DIR", originalNoteleafDataDir) 266 }() 267 268 ctx := context.Background() 269 err := Setup(ctx, []string{}) 270 271 if err == nil { 272 t.Error("Setup should fail when GetConfigDir fails") 273 } 274 if !strings.Contains(err.Error(), "failed to get config directory") && !strings.Contains(err.Error(), "failed to load config") { 275 t.Errorf("Expected config directory error, got: %v", err) 276 } 277 }) 278 279 t.Run("handles database creation error", func(t *testing.T) { 280 tempDir, err := os.MkdirTemp("", "noteleaf-readonly-test-*") 281 if err != nil { 282 t.Fatalf("Failed to create temp dir: %v", err) 283 } 284 defer os.RemoveAll(tempDir) 285 286 if err := os.Chmod(tempDir, 0444); err != nil { 287 t.Fatalf("Failed to make directory read-only: %v", err) 288 } 289 290 defer os.Chmod(tempDir, 0755) 291 292 oldNoteleafConfig := os.Getenv("NOTELEAF_CONFIG") 293 oldNoteleafDataDir := os.Getenv("NOTELEAF_DATA_DIR") 294 os.Setenv("NOTELEAF_CONFIG", filepath.Join(tempDir, ".noteleaf.conf.toml")) 295 os.Setenv("NOTELEAF_DATA_DIR", tempDir) 296 defer func() { 297 os.Setenv("NOTELEAF_CONFIG", oldNoteleafConfig) 298 os.Setenv("NOTELEAF_DATA_DIR", oldNoteleafDataDir) 299 }() 300 301 ctx := context.Background() 302 err = Setup(ctx, []string{}) 303 304 if err == nil { 305 t.Error("Setup should fail when database creation fails") 306 } 307 if !strings.Contains(err.Error(), "failed to initialize database") && !strings.Contains(err.Error(), "failed to create configuration") && !strings.Contains(err.Error(), "failed to load configuration") { 308 t.Errorf("Expected database initialization or configuration error, got: %v", err) 309 } 310 }) 311 312 t.Run("handles config loading error", func(t *testing.T) { 313 tempDir, err := os.MkdirTemp("", "noteleaf-config-error-test-*") 314 if err != nil { 315 t.Fatalf("Failed to create temp dir: %v", err) 316 } 317 defer os.RemoveAll(tempDir) 318 319 configPath := filepath.Join(tempDir, ".noteleaf.conf.toml") 320 invalidTOML := `[invalid toml content` 321 if err := os.WriteFile(configPath, []byte(invalidTOML), 0644); err != nil { 322 t.Fatalf("Failed to write invalid config: %v", err) 323 } 324 325 oldNoteleafConfig := os.Getenv("NOTELEAF_CONFIG") 326 oldNoteleafDataDir := os.Getenv("NOTELEAF_DATA_DIR") 327 os.Setenv("NOTELEAF_CONFIG", configPath) 328 os.Setenv("NOTELEAF_DATA_DIR", tempDir) 329 defer func() { 330 os.Setenv("NOTELEAF_CONFIG", oldNoteleafConfig) 331 os.Setenv("NOTELEAF_DATA_DIR", oldNoteleafDataDir) 332 }() 333 334 ctx := context.Background() 335 err = Setup(ctx, []string{}) 336 337 if err == nil { 338 t.Error("Setup should fail when config loading fails") 339 } 340 if !strings.Contains(err.Error(), "failed to create configuration") && !strings.Contains(err.Error(), "failed to parse") { 341 t.Errorf("Expected configuration error, got: %v", err) 342 } 343 }) 344} 345 346func TestResetErrorHandling(t *testing.T) { 347 t.Run("handles GetConfigDir error", func(t *testing.T) { 348 originalXDG := os.Getenv("XDG_CONFIG_HOME") 349 originalHome := os.Getenv("HOME") 350 351 if runtime.GOOS == "windows" { 352 originalAppData := os.Getenv("APPDATA") 353 os.Unsetenv("APPDATA") 354 defer os.Setenv("APPDATA", originalAppData) 355 } else { 356 os.Unsetenv("XDG_CONFIG_HOME") 357 os.Unsetenv("HOME") 358 } 359 360 defer func() { 361 os.Setenv("XDG_CONFIG_HOME", originalXDG) 362 os.Setenv("HOME", originalHome) 363 }() 364 365 ctx := context.Background() 366 err := Reset(ctx, []string{}) 367 368 if err == nil { 369 t.Error("Reset should fail when directory access fails") 370 } 371 if !strings.Contains(err.Error(), "failed to get config directory") && !strings.Contains(err.Error(), "failed to get data directory") { 372 t.Errorf("Expected config or data directory error, got: %v", err) 373 } 374 }) 375 376 t.Run("handles GetConfigPath error", func(t *testing.T) { 377 tempDir, err := os.MkdirTemp("", "noteleaf-reset-error-test-*") 378 if err != nil { 379 t.Fatalf("Failed to create temp dir: %v", err) 380 } 381 defer os.RemoveAll(tempDir) 382 383 dbPath := filepath.Join(tempDir, "noteleaf.db") 384 if err := os.WriteFile(dbPath, []byte("test"), 0644); err != nil { 385 t.Fatalf("Failed to create test db file: %v", err) 386 } 387 388 if err := os.Chmod(tempDir, 0444); err != nil { 389 t.Fatalf("Failed to make directory read-only: %v", err) 390 } 391 392 defer os.Chmod(tempDir, 0755) 393 394 oldNoteleafConfig := os.Getenv("NOTELEAF_CONFIG") 395 oldNoteleafDataDir := os.Getenv("NOTELEAF_DATA_DIR") 396 os.Setenv("NOTELEAF_CONFIG", filepath.Join(tempDir, ".noteleaf.conf.toml")) 397 os.Setenv("NOTELEAF_DATA_DIR", tempDir) 398 defer func() { 399 os.Setenv("NOTELEAF_CONFIG", oldNoteleafConfig) 400 os.Setenv("NOTELEAF_DATA_DIR", oldNoteleafDataDir) 401 }() 402 403 ctx := context.Background() 404 err = Reset(ctx, []string{}) 405 406 if err == nil { 407 t.Error("Reset should fail when file removal fails") 408 } 409 if !strings.Contains(err.Error(), "failed to remove") && !strings.Contains(err.Error(), "failed to get config path") { 410 t.Errorf("Expected removal or config path error, got: %v", err) 411 } 412 }) 413} 414 415func TestStatusErrorHandling(t *testing.T) { 416 t.Run("handles GetConfigDir error", func(t *testing.T) { 417 originalXDG := os.Getenv("XDG_CONFIG_HOME") 418 originalHome := os.Getenv("HOME") 419 420 if runtime.GOOS == "windows" { 421 originalAppData := os.Getenv("APPDATA") 422 os.Unsetenv("APPDATA") 423 defer os.Setenv("APPDATA", originalAppData) 424 } else { 425 os.Unsetenv("XDG_CONFIG_HOME") 426 os.Unsetenv("HOME") 427 } 428 429 defer func() { 430 os.Setenv("XDG_CONFIG_HOME", originalXDG) 431 os.Setenv("HOME", originalHome) 432 }() 433 434 var buf bytes.Buffer 435 ctx := context.Background() 436 err := Status(ctx, []string{}, &buf) 437 438 if err == nil { 439 t.Error("Status should fail when GetConfigDir fails") 440 } 441 if !strings.Contains(err.Error(), "failed to get config directory") { 442 t.Errorf("Expected config directory error, got: %v", err) 443 } 444 }) 445 446 t.Run("handles database connection error", func(t *testing.T) { 447 _ = createTestDir(t) 448 ctx := context.Background() 449 450 err := Setup(ctx, []string{}) 451 if err != nil { 452 t.Fatalf("Setup failed: %v", err) 453 } 454 455 // Get the actual database path from config to ensure we corrupt the right file 456 config, err := store.LoadConfig() 457 if err != nil { 458 t.Fatalf("Failed to load config: %v", err) 459 } 460 461 dbPath := getDbPath(config) 462 463 os.Remove(dbPath) 464 465 if err := os.MkdirAll(dbPath, 0755); err != nil { 466 t.Fatalf("Failed to create directory: %v", err) 467 } 468 469 var buf bytes.Buffer 470 err = Status(ctx, []string{}, &buf) 471 if err == nil { 472 t.Error("Status should fail when database connection fails") 473 } else if !strings.Contains(err.Error(), "failed to connect to database") && !strings.Contains(err.Error(), "failed to open database") && !strings.Contains(err.Error(), "failed to load config") { 474 t.Errorf("Expected database connection or config error, got: %v", err) 475 } 476 }) 477 478 t.Run("handles migration errors", func(t *testing.T) { 479 _ = createTestDir(t) 480 ctx := context.Background() 481 482 err := Setup(ctx, []string{}) 483 if err != nil { 484 t.Fatalf("Setup failed: %v", err) 485 } 486 487 // Corrupt the migrations table to cause GetAppliedMigrations to fail 488 db, err := store.NewDatabase() 489 if err != nil { 490 t.Fatalf("Failed to connect to database: %v", err) 491 } 492 493 _, err = db.Exec("DROP TABLE migrations") 494 if err != nil { 495 t.Fatalf("Failed to drop migrations table: %v", err) 496 } 497 498 _, err = db.Exec("CREATE TABLE migrations (invalid_schema TEXT)") 499 if err != nil { 500 t.Fatalf("Failed to create corrupted migrations table: %v", err) 501 } 502 db.Close() 503 504 var buf bytes.Buffer 505 err = Status(ctx, []string{}, &buf) 506 if err == nil { 507 t.Error("Status should fail when migration queries fail") 508 } 509 if !strings.Contains(err.Error(), "failed to get") && !strings.Contains(err.Error(), "migrations") { 510 t.Errorf("Expected migration-related error, got: %v", err) 511 } 512 }) 513} 514 515func TestErrorScenarios(t *testing.T) { 516 errorTests := []struct { 517 name string 518 setupFunc func(t *testing.T) (cleanup func()) 519 handlerFunc func(ctx context.Context, args []string) error 520 expectError bool 521 errorSubstr string 522 }{ 523 { 524 name: "Setup with invalid environment", 525 setupFunc: func(t *testing.T) func() { 526 if runtime.GOOS == "windows" { 527 original := os.Getenv("APPDATA") 528 os.Unsetenv("APPDATA") 529 return func() { os.Setenv("APPDATA", original) } 530 } else { 531 originalXDG := os.Getenv("XDG_CONFIG_HOME") 532 originalHome := os.Getenv("HOME") 533 os.Unsetenv("XDG_CONFIG_HOME") 534 os.Unsetenv("HOME") 535 return func() { 536 os.Setenv("XDG_CONFIG_HOME", originalXDG) 537 os.Setenv("HOME", originalHome) 538 } 539 } 540 }, 541 handlerFunc: Setup, 542 expectError: true, 543 errorSubstr: "config directory", 544 }, 545 { 546 name: "Reset with invalid environment", 547 setupFunc: func(t *testing.T) func() { 548 if runtime.GOOS == "windows" { 549 original := os.Getenv("APPDATA") 550 os.Unsetenv("APPDATA") 551 return func() { os.Setenv("APPDATA", original) } 552 } else { 553 originalXDG := os.Getenv("XDG_CONFIG_HOME") 554 originalHome := os.Getenv("HOME") 555 os.Unsetenv("XDG_CONFIG_HOME") 556 os.Unsetenv("HOME") 557 return func() { 558 os.Setenv("XDG_CONFIG_HOME", originalXDG) 559 os.Setenv("HOME", originalHome) 560 } 561 } 562 }, 563 handlerFunc: Reset, 564 expectError: true, 565 errorSubstr: "data directory", 566 }, 567 { 568 name: "Status with invalid environment", 569 setupFunc: func(t *testing.T) func() { 570 if runtime.GOOS == "windows" { 571 original := os.Getenv("APPDATA") 572 os.Unsetenv("APPDATA") 573 return func() { os.Setenv("APPDATA", original) } 574 } else { 575 originalXDG := os.Getenv("XDG_CONFIG_HOME") 576 originalHome := os.Getenv("HOME") 577 os.Unsetenv("XDG_CONFIG_HOME") 578 os.Unsetenv("HOME") 579 return func() { 580 os.Setenv("XDG_CONFIG_HOME", originalXDG) 581 os.Setenv("HOME", originalHome) 582 } 583 } 584 }, 585 handlerFunc: func(ctx context.Context, args []string) error { 586 var buf bytes.Buffer 587 return Status(ctx, args, &buf) 588 }, 589 expectError: true, 590 errorSubstr: "config directory", 591 }, 592 } 593 594 for _, tt := range errorTests { 595 t.Run(tt.name, func(t *testing.T) { 596 cleanup := tt.setupFunc(t) 597 defer cleanup() 598 599 ctx := context.Background() 600 err := tt.handlerFunc(ctx, []string{}) 601 602 if tt.expectError && err == nil { 603 t.Errorf("Expected error containing %q, got nil", tt.errorSubstr) 604 } else if !tt.expectError && err != nil { 605 t.Errorf("Expected no error, got: %v", err) 606 } else if tt.expectError && err != nil && !strings.Contains(err.Error(), tt.errorSubstr) { 607 t.Errorf("Expected error containing %q, got: %v", tt.errorSubstr, err) 608 } 609 }) 610 } 611} 612 613func TestIntegration(t *testing.T) { 614 t.Run("full setup-reset-status cycle", func(t *testing.T) { 615 _ = createTestDir(t) 616 ctx := context.Background() 617 618 var buf bytes.Buffer 619 err := Status(ctx, []string{}, &buf) 620 if err != nil { 621 t.Errorf("Initial status failed: %v", err) 622 } 623 624 err = Setup(ctx, []string{}) 625 if err != nil { 626 t.Errorf("Setup failed: %v", err) 627 } 628 629 var buf2 bytes.Buffer 630 err = Status(ctx, []string{}, &buf2) 631 if err != nil { 632 t.Errorf("Status after setup failed: %v", err) 633 } 634 635 config, _ := store.LoadConfig() 636 dbPath := getDbPath(config) 637 638 if _, err := os.Stat(dbPath); os.IsNotExist(err) { 639 t.Error("Database should exist after setup") 640 } 641 642 err = Reset(ctx, []string{}) 643 if err != nil { 644 t.Errorf("Reset failed: %v", err) 645 } 646 647 if _, err := os.Stat(dbPath); !os.IsNotExist(err) { 648 t.Error("Database should not exist after reset") 649 } 650 651 var buf3 bytes.Buffer 652 err = Status(ctx, []string{}, &buf3) 653 if err != nil { 654 t.Errorf("Status after reset failed: %v", err) 655 } 656 657 }) 658 659 t.Run("concurrent operations", func(t *testing.T) { 660 _ = createTestDir(t) 661 ctx := context.Background() 662 663 err := Setup(ctx, []string{}) 664 if err != nil { 665 t.Fatalf("Setup failed: %v", err) 666 } 667 668 done := make(chan error, 3) 669 670 for range 3 { 671 go func() { 672 var buf bytes.Buffer 673 time.Sleep(time.Millisecond * 10) 674 done <- Status(ctx, []string{}, &buf) 675 }() 676 } 677 678 for i := range 3 { 679 if err := <-done; err != nil { 680 t.Errorf("Concurrent status operation %d failed: %v", i, err) 681 } 682 } 683 }) 684}