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