cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists ๐Ÿƒ
charm leaflet readability golang

build: add error path tests for storage abstractions

+552 -3
+163
internal/store/config_test.go
··· 221 221 } 222 222 }) 223 223 224 + t.Run("LoadConfig handles file read permission error", func(t *testing.T) { 225 + if runtime.GOOS == "windows" { 226 + t.Skip("Permission test not reliable on Windows") 227 + } 228 + 229 + tempDir, err := os.MkdirTemp("", "noteleaf-config-perm-test-*") 230 + if err != nil { 231 + t.Fatalf("Failed to create temp directory: %v", err) 232 + } 233 + defer os.RemoveAll(tempDir) 234 + 235 + originalGetConfigDir := GetConfigDir 236 + GetConfigDir = func() (string, error) { 237 + return tempDir, nil 238 + } 239 + defer func() { GetConfigDir = originalGetConfigDir }() 240 + 241 + configPath := filepath.Join(tempDir, ".noteleaf.conf.toml") 242 + validTOML := `color_scheme = "dark"` 243 + err = os.WriteFile(configPath, []byte(validTOML), 0644) 244 + if err != nil { 245 + t.Fatalf("Failed to write config file: %v", err) 246 + } 247 + 248 + err = os.Chmod(configPath, 0000) 249 + if err != nil { 250 + t.Fatalf("Failed to change file permissions: %v", err) 251 + } 252 + defer os.Chmod(configPath, 0644) 253 + 254 + _, err = LoadConfig() 255 + if err == nil { 256 + t.Error("LoadConfig should fail when config file is not readable") 257 + } 258 + }) 259 + 260 + t.Run("LoadConfig handles GetConfigDir error", func(t *testing.T) { 261 + originalGetConfigDir := GetConfigDir 262 + GetConfigDir = func() (string, error) { 263 + return "", os.ErrPermission 264 + } 265 + defer func() { GetConfigDir = originalGetConfigDir }() 266 + 267 + _, err := LoadConfig() 268 + if err == nil { 269 + t.Error("LoadConfig should fail when GetConfigDir fails") 270 + } 271 + }) 272 + 273 + t.Run("LoadConfig handles SaveConfig failure when creating default", func(t *testing.T) { 274 + tempDir, err := os.MkdirTemp("", "noteleaf-config-save-fail-test-*") 275 + if err != nil { 276 + t.Fatalf("Failed to create temp directory: %v", err) 277 + } 278 + defer os.RemoveAll(tempDir) 279 + 280 + _ = filepath.Join(tempDir, ".noteleaf.conf.toml") 281 + 282 + callCount := 0 283 + originalGetConfigDir := GetConfigDir 284 + GetConfigDir = func() (string, error) { 285 + callCount++ 286 + if callCount == 1 { 287 + return tempDir, nil 288 + } 289 + return "", os.ErrPermission 290 + } 291 + defer func() { GetConfigDir = originalGetConfigDir }() 292 + 293 + _, err = LoadConfig() 294 + if err == nil { 295 + t.Error("LoadConfig should fail when SaveConfig fails during default config creation") 296 + } 297 + }) 298 + 224 299 t.Run("SaveConfig handles directory creation failure", func(t *testing.T) { 225 300 originalGetConfigDir := GetConfigDir 226 301 GetConfigDir = func() (string, error) { ··· 235 310 } 236 311 }) 237 312 313 + t.Run("SaveConfig handles file write permission error", func(t *testing.T) { 314 + if runtime.GOOS == "windows" { 315 + t.Skip("Permission test not reliable on Windows") 316 + } 317 + 318 + tempDir, err := os.MkdirTemp("", "noteleaf-config-write-perm-test-*") 319 + if err != nil { 320 + t.Fatalf("Failed to create temp directory: %v", err) 321 + } 322 + defer os.RemoveAll(tempDir) 323 + 324 + originalGetConfigDir := GetConfigDir 325 + GetConfigDir = func() (string, error) { 326 + return tempDir, nil 327 + } 328 + defer func() { GetConfigDir = originalGetConfigDir }() 329 + 330 + err = os.Chmod(tempDir, 0555) 331 + if err != nil { 332 + t.Fatalf("Failed to change directory permissions: %v", err) 333 + } 334 + defer os.Chmod(tempDir, 0755) 335 + 336 + config := DefaultConfig() 337 + err = SaveConfig(config) 338 + if err == nil { 339 + t.Error("SaveConfig should fail when directory is not writable") 340 + } 341 + }) 342 + 238 343 t.Run("GetConfigPath handles GetConfigDir error", func(t *testing.T) { 239 344 originalGetConfigDir := GetConfigDir 240 345 GetConfigDir = func() (string, error) { ··· 342 447 if configDir != expectedPath { 343 448 t.Errorf("Expected config dir %s, got %s", expectedPath, configDir) 344 449 } 450 + } 451 + }) 452 + 453 + t.Run("handles HOME directory lookup failure on Unix", func(t *testing.T) { 454 + if runtime.GOOS == "windows" { 455 + t.Skip("HOME directory test not applicable on Windows") 456 + } 457 + 458 + originalXDG := os.Getenv("XDG_CONFIG_HOME") 459 + originalHome := os.Getenv("HOME") 460 + os.Unsetenv("XDG_CONFIG_HOME") 461 + os.Unsetenv("HOME") 462 + 463 + defer func() { 464 + os.Setenv("XDG_CONFIG_HOME", originalXDG) 465 + os.Setenv("HOME", originalHome) 466 + }() 467 + 468 + _, err := GetConfigDir() 469 + if err == nil { 470 + t.Error("GetConfigDir should fail when both XDG_CONFIG_HOME and HOME are not available") 471 + } 472 + }) 473 + 474 + t.Run("handles directory creation permission failure", func(t *testing.T) { 475 + if runtime.GOOS == "windows" { 476 + t.Skip("Permission test not reliable on Windows") 477 + } 478 + 479 + tempParent, err := os.MkdirTemp("", "noteleaf-parent-test-*") 480 + if err != nil { 481 + t.Fatalf("Failed to create temp parent directory: %v", err) 482 + } 483 + defer os.RemoveAll(tempParent) 484 + 485 + err = os.Chmod(tempParent, 0555) 486 + if err != nil { 487 + t.Fatalf("Failed to change parent directory permissions: %v", err) 488 + } 489 + defer os.Chmod(tempParent, 0755) 490 + 491 + var originalEnv string 492 + var envVar string 493 + switch runtime.GOOS { 494 + case "windows": 495 + envVar = "APPDATA" 496 + originalEnv = os.Getenv("APPDATA") 497 + os.Setenv("APPDATA", tempParent) 498 + default: 499 + envVar = "XDG_CONFIG_HOME" 500 + originalEnv = os.Getenv("XDG_CONFIG_HOME") 501 + os.Setenv("XDG_CONFIG_HOME", tempParent) 502 + } 503 + defer os.Setenv(envVar, originalEnv) 504 + 505 + _, err = GetConfigDir() 506 + if err == nil { 507 + t.Error("GetConfigDir should fail when directory creation is not permitted") 345 508 } 346 509 }) 347 510 }
+71
internal/store/database_test.go
··· 3 3 import ( 4 4 "os" 5 5 "path/filepath" 6 + "runtime" 6 7 "testing" 7 8 ) 8 9 ··· 159 160 _, err := NewDatabase() 160 161 if err == nil { 161 162 t.Error("NewDatabase should fail with invalid database path") 163 + } 164 + }) 165 + 166 + t.Run("handles invalid database connection", func(t *testing.T) { 167 + originalGetConfigDir := GetConfigDir 168 + GetConfigDir = func() (string, error) { 169 + return "/dev/null", nil 170 + } 171 + defer func() { GetConfigDir = originalGetConfigDir }() 172 + 173 + _, err := NewDatabase() 174 + if err == nil { 175 + t.Error("NewDatabase should fail when database path is invalid") 176 + } 177 + }) 178 + 179 + t.Run("handles migration failure during database creation", func(t *testing.T) { 180 + tempDir, err := os.MkdirTemp("", "noteleaf-db-migration-fail-test-*") 181 + if err != nil { 182 + t.Fatalf("Failed to create temp directory: %v", err) 183 + } 184 + defer os.RemoveAll(tempDir) 185 + 186 + originalGetConfigDir := GetConfigDir 187 + GetConfigDir = func() (string, error) { 188 + return tempDir, nil 189 + } 190 + defer func() { GetConfigDir = originalGetConfigDir }() 191 + 192 + dbPath := filepath.Join(tempDir, "noteleaf.db") 193 + 194 + // Create a corrupted database file that will cause issues 195 + corruptedSQL := "this is not valid SQL and will cause failures" 196 + err = os.WriteFile(dbPath, []byte(corruptedSQL), 0644) 197 + if err != nil { 198 + t.Fatalf("Failed to create corrupted database file: %v", err) 199 + } 200 + 201 + _, err = NewDatabase() 202 + if err == nil { 203 + t.Error("NewDatabase should fail when database file is corrupted") 204 + } 205 + }) 206 + 207 + t.Run("handles database file permission error", func(t *testing.T) { 208 + if runtime.GOOS == "windows" { 209 + t.Skip("Permission test not reliable on Windows") 210 + } 211 + 212 + tempDir, err := os.MkdirTemp("", "noteleaf-db-perm-test-*") 213 + if err != nil { 214 + t.Fatalf("Failed to create temp directory: %v", err) 215 + } 216 + defer os.RemoveAll(tempDir) 217 + 218 + originalGetConfigDir := GetConfigDir 219 + GetConfigDir = func() (string, error) { 220 + return tempDir, nil 221 + } 222 + defer func() { GetConfigDir = originalGetConfigDir }() 223 + 224 + err = os.Chmod(tempDir, 0555) 225 + if err != nil { 226 + t.Fatalf("Failed to change directory permissions: %v", err) 227 + } 228 + defer os.Chmod(tempDir, 0755) 229 + 230 + _, err = NewDatabase() 231 + if err == nil { 232 + t.Error("NewDatabase should fail when database directory is not writable") 162 233 } 163 234 }) 164 235 }
+9 -3
internal/store/migration.go
··· 2 2 3 3 import ( 4 4 "database/sql" 5 - "embed" 6 5 "fmt" 6 + "io/fs" 7 7 "sort" 8 8 "strings" 9 9 ) ··· 18 18 AppliedAt string 19 19 } 20 20 21 + // FileSystem interface for reading migration files 22 + type FileSystem interface { 23 + ReadDir(name string) ([]fs.DirEntry, error) 24 + ReadFile(name string) ([]byte, error) 25 + } 26 + 21 27 // MigrationRunner handles database migrations 22 28 type MigrationRunner struct { 23 29 db *sql.DB 24 - migrationFiles embed.FS 30 + migrationFiles FileSystem 25 31 } 26 32 27 33 // NewMigrationRunner creates a new migration runner 28 - func NewMigrationRunner(db *sql.DB, files embed.FS) *MigrationRunner { 34 + func NewMigrationRunner(db *sql.DB, files FileSystem) *MigrationRunner { 29 35 return &MigrationRunner{ 30 36 db: db, 31 37 migrationFiles: files,
+309
internal/store/migration_test.go
··· 3 3 import ( 4 4 "database/sql" 5 5 "embed" 6 + "fmt" 7 + "io/fs" 6 8 "testing" 7 9 8 10 _ "github.com/mattn/go-sqlite3" ··· 11 13 //go:embed sql/migrations 12 14 var testMigrationFiles embed.FS 13 15 16 + type fakeMigrationFS struct { 17 + shouldFailRead bool 18 + invalidSQL bool 19 + hasNewMigrations bool 20 + } 21 + 22 + type fakeDirEntry struct { 23 + name string 24 + } 25 + 26 + func (f fakeDirEntry) Name() string { return f.name } 27 + func (f fakeDirEntry) IsDir() bool { return false } 28 + func (f fakeDirEntry) Type() fs.FileMode { return 0 } 29 + func (f fakeDirEntry) Info() (fs.FileInfo, error) { return nil, fmt.Errorf("info not available") } 30 + 31 + func (f *fakeMigrationFS) ReadDir(name string) ([]fs.DirEntry, error) { 32 + if name == "sql/migrations" { 33 + entries := []fs.DirEntry{ 34 + fakeDirEntry{name: "0000_create_migrations_table_up.sql"}, 35 + } 36 + if f.hasNewMigrations { 37 + entries = append(entries, 38 + fakeDirEntry{name: "0001_test_migration_up.sql"}, 39 + fakeDirEntry{name: "0001_test_migration_down.sql"}, 40 + ) 41 + } 42 + return entries, nil 43 + } 44 + return nil, fmt.Errorf("directory not found: %s", name) 45 + } 46 + 47 + func (f *fakeMigrationFS) ReadFile(name string) ([]byte, error) { 48 + if f.shouldFailRead { 49 + return nil, fmt.Errorf("simulated read failure") 50 + } 51 + if f.invalidSQL { 52 + return []byte("INVALID SQL SYNTAX GOES HERE AND MAKES DATABASE SAD"), nil 53 + } 54 + if name == "sql/migrations/0000_create_migrations_table_up.sql" { 55 + return []byte("CREATE TABLE migrations (version TEXT PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP);"), nil 56 + } 57 + if name == "sql/migrations/0001_test_migration_up.sql" { 58 + return []byte("CREATE TABLE test_table (id INTEGER PRIMARY KEY);"), nil 59 + } 60 + if name == "sql/migrations/0001_test_migration_down.sql" { 61 + return []byte("DROP TABLE IF EXISTS test_table;"), nil 62 + } 63 + return nil, fmt.Errorf("file not found: %s", name) 64 + } 65 + 14 66 func createTestDB(t *testing.T) *sql.DB { 15 67 db, err := sql.Open("sqlite3", ":memory:") 16 68 if err != nil { ··· 72 124 } 73 125 }) 74 126 127 + t.Run("handles migration directory read failure", func(t *testing.T) { 128 + db := createTestDB(t) 129 + 130 + emptyFS := embed.FS{} 131 + runner := NewMigrationRunner(db, emptyFS) 132 + 133 + err := runner.RunMigrations() 134 + if err == nil { 135 + t.Error("RunMigrations should fail when migration directory cannot be read") 136 + } 137 + }) 138 + 139 + t.Run("handles migration table check failure", func(t *testing.T) { 140 + db := createTestDB(t) 141 + db.Close() 142 + 143 + runner := NewMigrationRunner(db, testMigrationFiles) 144 + err := runner.RunMigrations() 145 + if err == nil { 146 + t.Error("RunMigrations should fail when database connection is closed") 147 + } 148 + }) 149 + 150 + t.Run("handles migration file read failure", func(t *testing.T) { 151 + db := createTestDB(t) 152 + 153 + fakeFS := &fakeMigrationFS{shouldFailRead: true, hasNewMigrations: true} 154 + runner := NewMigrationRunner(db, fakeFS) 155 + 156 + err := runner.RunMigrations() 157 + if err == nil { 158 + t.Error("RunMigrations should fail when migration file cannot be read") 159 + } 160 + }) 161 + 162 + t.Run("handles invalid SQL in migration file", func(t *testing.T) { 163 + db := createTestDB(t) 164 + 165 + fakeFS := &fakeMigrationFS{invalidSQL: true, hasNewMigrations: true} 166 + runner := NewMigrationRunner(db, fakeFS) 167 + 168 + err := runner.RunMigrations() 169 + if err == nil { 170 + t.Error("RunMigrations should fail when migration contains invalid SQL") 171 + } 172 + }) 173 + 174 + t.Run("handles migration record insertion failure", func(t *testing.T) { 175 + db := createTestDB(t) 176 + runner := NewMigrationRunner(db, testMigrationFiles) 177 + 178 + err := runner.RunMigrations() 179 + if err != nil { 180 + t.Fatalf("First RunMigrations failed: %v", err) 181 + } 182 + 183 + _, err = db.Exec("DROP TABLE migrations") 184 + if err != nil { 185 + t.Fatalf("Failed to drop migrations table: %v", err) 186 + } 187 + 188 + _, err = db.Exec("CREATE TABLE migrations (version TEXT PRIMARY KEY CHECK(length(version) < 0))") 189 + if err != nil { 190 + t.Fatalf("Failed to create migrations table with constraint: %v", err) 191 + } 192 + 193 + err = runner.RunMigrations() 194 + if err == nil { 195 + t.Error("RunMigrations should fail when migration record cannot be inserted") 196 + } 197 + }) 198 + 75 199 t.Run("skips already applied migrations", func(t *testing.T) { 76 200 db := createTestDB(t) 77 201 runner := NewMigrationRunner(db, testMigrationFiles) ··· 143 267 } 144 268 }) 145 269 270 + t.Run("handles database connection failure", func(t *testing.T) { 271 + db := createTestDB(t) 272 + db.Close() 273 + runner := NewMigrationRunner(db, testMigrationFiles) 274 + 275 + _, err := runner.GetAppliedMigrations() 276 + if err == nil { 277 + t.Error("GetAppliedMigrations should fail when database connection is closed") 278 + } 279 + }) 280 + 281 + t.Run("handles query execution failure", func(t *testing.T) { 282 + db := createTestDB(t) 283 + runner := NewMigrationRunner(db, testMigrationFiles) 284 + 285 + err := runner.RunMigrations() 286 + if err != nil { 287 + t.Fatalf("RunMigrations failed: %v", err) 288 + } 289 + 290 + // Close the database to trigger a query failure 291 + db.Close() 292 + 293 + _, err = runner.GetAppliedMigrations() 294 + if err == nil { 295 + t.Error("GetAppliedMigrations should fail when database is closed") 296 + } 297 + }) 298 + 299 + t.Run("handles row scan failure", func(t *testing.T) { 300 + db := createTestDB(t) 301 + runner := NewMigrationRunner(db, testMigrationFiles) 302 + 303 + err := runner.RunMigrations() 304 + if err != nil { 305 + t.Fatalf("RunMigrations failed: %v", err) 306 + } 307 + 308 + // Insert a record with NULL applied_at which should cause scan issues 309 + _, err = db.Exec("INSERT INTO migrations (version, applied_at) VALUES ('test', NULL)") 310 + if err != nil { 311 + t.Fatalf("Failed to insert NULL migration record: %v", err) 312 + } 313 + 314 + _, err = runner.GetAppliedMigrations() 315 + if err == nil { 316 + t.Error("GetAppliedMigrations should fail when scanning NULL applied_at field") 317 + } 318 + }) 319 + 146 320 t.Run("returns applied migrations", func(t *testing.T) { 147 321 db := createTestDB(t) 148 322 runner := NewMigrationRunner(db, testMigrationFiles) ··· 213 387 } 214 388 }) 215 389 390 + t.Run("handles migration directory read failure", func(t *testing.T) { 391 + db := createTestDB(t) 392 + 393 + emptyFS := embed.FS{} 394 + runner := NewMigrationRunner(db, emptyFS) 395 + 396 + _, err := runner.GetAvailableMigrations() 397 + if err == nil { 398 + t.Error("GetAvailableMigrations should fail when migration directory cannot be read") 399 + } 400 + }) 401 + 402 + t.Run("handles migration file read failure", func(t *testing.T) { 403 + db := createTestDB(t) 404 + 405 + fakeFS := &fakeMigrationFS{shouldFailRead: true} 406 + runner := NewMigrationRunner(db, fakeFS) 407 + 408 + _, err := runner.GetAvailableMigrations() 409 + if err == nil { 410 + t.Error("GetAvailableMigrations should fail when migration file cannot be read") 411 + } 412 + }) 413 + 216 414 t.Run("includes both up and down SQL when available", func(t *testing.T) { 217 415 db := createTestDB(t) 218 416 runner := NewMigrationRunner(db, testMigrationFiles) ··· 244 442 err := runner.Rollback() 245 443 if err == nil { 246 444 t.Error("Rollback should fail when no migrations are applied") 445 + } 446 + }) 447 + 448 + t.Run("handles database connection failure", func(t *testing.T) { 449 + db := createTestDB(t) 450 + runner := NewMigrationRunner(db, testMigrationFiles) 451 + 452 + err := runner.RunMigrations() 453 + if err != nil { 454 + t.Fatalf("RunMigrations failed: %v", err) 455 + } 456 + 457 + db.Close() 458 + 459 + err = runner.Rollback() 460 + if err == nil { 461 + t.Error("Rollback should fail when database connection is closed") 462 + } 463 + }) 464 + 465 + t.Run("handles migration directory read failure during rollback", func(t *testing.T) { 466 + db := createTestDB(t) 467 + runner := NewMigrationRunner(db, testMigrationFiles) 468 + 469 + err := runner.RunMigrations() 470 + if err != nil { 471 + t.Fatalf("RunMigrations failed: %v", err) 472 + } 473 + 474 + emptyFS := embed.FS{} 475 + runner.migrationFiles = emptyFS 476 + 477 + err = runner.Rollback() 478 + if err == nil { 479 + t.Error("Rollback should fail when migration directory cannot be read") 480 + } 481 + }) 482 + 483 + t.Run("handles missing down migration file", func(t *testing.T) { 484 + db := createTestDB(t) 485 + runner := NewMigrationRunner(db, testMigrationFiles) 486 + 487 + err := runner.RunMigrations() 488 + if err != nil { 489 + t.Fatalf("RunMigrations failed: %v", err) 490 + } 491 + 492 + fakeFS := &fakeMigrationFS{} 493 + runner.migrationFiles = fakeFS 494 + 495 + err = runner.Rollback() 496 + if err == nil { 497 + t.Error("Rollback should fail when down migration file is not found") 498 + } 499 + }) 500 + 501 + t.Run("handles down migration file read failure", func(t *testing.T) { 502 + db := createTestDB(t) 503 + 504 + fakeFS := &fakeMigrationFS{} 505 + runner := NewMigrationRunner(db, fakeFS) 506 + 507 + err := runner.RunMigrations() 508 + if err != nil { 509 + t.Fatalf("RunMigrations failed: %v", err) 510 + } 511 + 512 + fakeFS.shouldFailRead = true 513 + 514 + err = runner.Rollback() 515 + if err == nil { 516 + t.Error("Rollback should fail when down migration file cannot be read") 517 + } 518 + }) 519 + 520 + t.Run("handles invalid down migration SQL", func(t *testing.T) { 521 + db := createTestDB(t) 522 + 523 + fakeFS := &fakeMigrationFS{} 524 + runner := NewMigrationRunner(db, fakeFS) 525 + 526 + err := runner.RunMigrations() 527 + if err != nil { 528 + t.Fatalf("RunMigrations failed: %v", err) 529 + } 530 + 531 + fakeFS.invalidSQL = true 532 + 533 + err = runner.Rollback() 534 + if err == nil { 535 + t.Error("Rollback should fail when down migration contains invalid SQL") 536 + } 537 + }) 538 + 539 + t.Run("handles migration record deletion failure", func(t *testing.T) { 540 + db := createTestDB(t) 541 + runner := NewMigrationRunner(db, testMigrationFiles) 542 + 543 + err := runner.RunMigrations() 544 + if err != nil { 545 + t.Fatalf("RunMigrations failed: %v", err) 546 + } 547 + 548 + _, err = db.Exec("DROP TABLE migrations") 549 + if err != nil { 550 + t.Fatalf("Failed to drop migrations table: %v", err) 551 + } 552 + 553 + err = runner.Rollback() 554 + if err == nil { 555 + t.Error("Rollback should fail when migration record cannot be deleted") 247 556 } 248 557 }) 249 558