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

refactor: add shims

+242 -200
+24 -15
internal/store/database.go
··· 11 _ "github.com/mattn/go-sqlite3" 12 ) 13 14 //go:embed sql/migrations 15 var migrationFiles embed.FS 16 ··· 24 var GetConfigDir = func() (string, error) { 25 var configDir string 26 27 - switch runtime.GOOS { 28 case "windows": 29 appData := os.Getenv("APPDATA") 30 if appData == "" { ··· 32 } 33 configDir = filepath.Join(appData, "noteleaf") 34 case "darwin": 35 - homeDir, err := os.UserHomeDir() 36 if err != nil { 37 return "", fmt.Errorf("failed to get user home directory: %w", err) 38 } ··· 40 default: 41 xdgConfigHome := os.Getenv("XDG_CONFIG_HOME") 42 if xdgConfigHome == "" { 43 - homeDir, err := os.UserHomeDir() 44 if err != nil { 45 return "", fmt.Errorf("failed to get user home directory: %w", err) 46 } ··· 49 configDir = filepath.Join(xdgConfigHome, "noteleaf") 50 } 51 52 - if err := os.MkdirAll(configDir, 0755); err != nil { 53 return "", fmt.Errorf("failed to create config directory: %w", err) 54 } 55 ··· 59 // GetDataDir returns the appropriate data directory based on [runtime.GOOS] or NOTELEAF_DATA_DIR 60 var GetDataDir = func() (string, error) { 61 if envDataDir := os.Getenv("NOTELEAF_DATA_DIR"); envDataDir != "" { 62 - if err := os.MkdirAll(envDataDir, 0755); err != nil { 63 return "", fmt.Errorf("failed to create data directory: %w", err) 64 } 65 return envDataDir, nil ··· 67 68 var dataDir string 69 70 - switch runtime.GOOS { 71 case "windows": 72 localAppData := os.Getenv("LOCALAPPDATA") 73 if localAppData == "" { ··· 75 } 76 dataDir = filepath.Join(localAppData, "noteleaf") 77 case "darwin": 78 - homeDir, err := os.UserHomeDir() 79 if err != nil { 80 return "", fmt.Errorf("failed to get user home directory: %w", err) 81 } ··· 83 default: 84 xdgDataHome := os.Getenv("XDG_DATA_HOME") 85 if xdgDataHome == "" { 86 - homeDir, err := os.UserHomeDir() 87 if err != nil { 88 return "", fmt.Errorf("failed to get user home directory: %w", err) 89 } ··· 92 dataDir = filepath.Join(xdgDataHome, "noteleaf") 93 } 94 95 - if err := os.MkdirAll(dataDir, 0755); err != nil { 96 return "", fmt.Errorf("failed to create data directory: %w", err) 97 } 98 ··· 118 if config.DatabasePath != "" { 119 dbPath = config.DatabasePath 120 dbDir := filepath.Dir(dbPath) 121 - if err := os.MkdirAll(dbDir, 0755); err != nil { 122 return nil, fmt.Errorf("failed to create database directory: %w", err) 123 } 124 } else if config.DataDir != "" { ··· 131 dbPath = filepath.Join(dataDir, "noteleaf.db") 132 } 133 134 - db, err := sql.Open("sqlite3", dbPath) 135 if err != nil { 136 return nil, fmt.Errorf("failed to open database: %w", err) 137 } 138 139 - if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil { 140 db.Close() 141 return nil, fmt.Errorf("failed to enable foreign keys: %w", err) 142 } 143 144 - if _, err := db.Exec("PRAGMA journal_mode = WAL"); err != nil { 145 db.Close() 146 return nil, fmt.Errorf("failed to enable WAL mode: %w", err) 147 } 148 149 database := &Database{DB: db, path: dbPath} 150 - runner := NewMigrationRunner(db, migrationFiles) 151 if err := runner.RunMigrations(); err != nil { 152 db.Close() 153 return nil, fmt.Errorf("failed to run migrations: %w", err) ··· 158 159 // NewMigrationRunnerFromDB creates a new migration runner from a Database instance 160 func (db *Database) NewMigrationRunner() *MigrationRunner { 161 - return NewMigrationRunner(db.DB, migrationFiles) 162 } 163 164 // GetPath returns the database file path
··· 11 _ "github.com/mattn/go-sqlite3" 12 ) 13 14 + var ( 15 + sqlOpen = sql.Open 16 + pragmaExec = func(db *sql.DB, stmt string) (sql.Result, error) { return db.Exec(stmt) } 17 + newMigrationRunner = NewMigrationRunner 18 + getRuntime = func() string { return runtime.GOOS } 19 + getHomeDir = os.UserHomeDir 20 + mkdirAll = os.MkdirAll 21 + ) 22 + 23 //go:embed sql/migrations 24 var migrationFiles embed.FS 25 ··· 33 var GetConfigDir = func() (string, error) { 34 var configDir string 35 36 + switch getRuntime() { 37 case "windows": 38 appData := os.Getenv("APPDATA") 39 if appData == "" { ··· 41 } 42 configDir = filepath.Join(appData, "noteleaf") 43 case "darwin": 44 + homeDir, err := getHomeDir() 45 if err != nil { 46 return "", fmt.Errorf("failed to get user home directory: %w", err) 47 } ··· 49 default: 50 xdgConfigHome := os.Getenv("XDG_CONFIG_HOME") 51 if xdgConfigHome == "" { 52 + homeDir, err := getHomeDir() 53 if err != nil { 54 return "", fmt.Errorf("failed to get user home directory: %w", err) 55 } ··· 58 configDir = filepath.Join(xdgConfigHome, "noteleaf") 59 } 60 61 + if err := mkdirAll(configDir, 0755); err != nil { 62 return "", fmt.Errorf("failed to create config directory: %w", err) 63 } 64 ··· 68 // GetDataDir returns the appropriate data directory based on [runtime.GOOS] or NOTELEAF_DATA_DIR 69 var GetDataDir = func() (string, error) { 70 if envDataDir := os.Getenv("NOTELEAF_DATA_DIR"); envDataDir != "" { 71 + if err := mkdirAll(envDataDir, 0755); err != nil { 72 return "", fmt.Errorf("failed to create data directory: %w", err) 73 } 74 return envDataDir, nil ··· 76 77 var dataDir string 78 79 + switch getRuntime() { 80 case "windows": 81 localAppData := os.Getenv("LOCALAPPDATA") 82 if localAppData == "" { ··· 84 } 85 dataDir = filepath.Join(localAppData, "noteleaf") 86 case "darwin": 87 + homeDir, err := getHomeDir() 88 if err != nil { 89 return "", fmt.Errorf("failed to get user home directory: %w", err) 90 } ··· 92 default: 93 xdgDataHome := os.Getenv("XDG_DATA_HOME") 94 if xdgDataHome == "" { 95 + homeDir, err := getHomeDir() 96 if err != nil { 97 return "", fmt.Errorf("failed to get user home directory: %w", err) 98 } ··· 101 dataDir = filepath.Join(xdgDataHome, "noteleaf") 102 } 103 104 + if err := mkdirAll(dataDir, 0755); err != nil { 105 return "", fmt.Errorf("failed to create data directory: %w", err) 106 } 107 ··· 127 if config.DatabasePath != "" { 128 dbPath = config.DatabasePath 129 dbDir := filepath.Dir(dbPath) 130 + if err := mkdirAll(dbDir, 0755); err != nil { 131 return nil, fmt.Errorf("failed to create database directory: %w", err) 132 } 133 } else if config.DataDir != "" { ··· 140 dbPath = filepath.Join(dataDir, "noteleaf.db") 141 } 142 143 + db, err := sqlOpen("sqlite3", dbPath) 144 if err != nil { 145 return nil, fmt.Errorf("failed to open database: %w", err) 146 } 147 148 + if _, err := pragmaExec(db, "PRAGMA foreign_keys = ON"); err != nil { 149 db.Close() 150 return nil, fmt.Errorf("failed to enable foreign keys: %w", err) 151 } 152 153 + if _, err := pragmaExec(db, "PRAGMA journal_mode = WAL"); err != nil { 154 db.Close() 155 return nil, fmt.Errorf("failed to enable WAL mode: %w", err) 156 } 157 158 database := &Database{DB: db, path: dbPath} 159 + runner := newMigrationRunner(db, migrationFiles) 160 if err := runner.RunMigrations(); err != nil { 161 db.Close() 162 return nil, fmt.Errorf("failed to run migrations: %w", err) ··· 167 168 // NewMigrationRunnerFromDB creates a new migration runner from a Database instance 169 func (db *Database) NewMigrationRunner() *MigrationRunner { 170 + return newMigrationRunner(db.DB, migrationFiles) 171 } 172 173 // GetPath returns the database file path
+205 -182
internal/store/database_test.go
··· 1 package store 2 3 import ( 4 "os" 5 "path/filepath" 6 "runtime" 7 "testing" 8 ) 9 10 - func TestNewDatabase(t *testing.T) { 11 tempDir, err := os.MkdirTemp("", "noteleaf-db-test-*") 12 if err != nil { 13 t.Fatalf("Failed to create temp directory: %v", err) 14 } 15 - defer os.RemoveAll(tempDir) 16 17 - originalGetConfigDir := GetConfigDir 18 - originalGetDataDir := GetDataDir 19 - GetConfigDir = func() (string, error) { 20 - return tempDir, nil 21 - } 22 - GetDataDir = func() (string, error) { 23 - return tempDir, nil 24 - } 25 - defer func() { 26 - GetConfigDir = originalGetConfigDir 27 - GetDataDir = originalGetDataDir 28 - }() 29 30 - t.Run("creates database successfully", func(t *testing.T) { 31 db, err := NewDatabase() 32 if err != nil { 33 t.Fatalf("NewDatabase failed: %v", err) 34 } 35 defer db.Close() 36 37 - if db == nil { 38 - t.Fatal("Database should not be nil") 39 - } 40 - 41 dbPath := filepath.Join(tempDir, "noteleaf.db") 42 if _, err := os.Stat(dbPath); os.IsNotExist(err) { 43 - t.Error("Database file should exist") 44 - } 45 - 46 - if db.GetPath() != dbPath { 47 - t.Errorf("Expected database path %s, got %s", dbPath, db.GetPath()) 48 } 49 }) 50 51 - t.Run("enables foreign keys", func(t *testing.T) { 52 - db, err := NewDatabase() 53 - if err != nil { 54 - t.Fatalf("NewDatabase failed: %v", err) 55 - } 56 defer db.Close() 57 58 - var foreignKeys int 59 - err = db.QueryRow("PRAGMA foreign_keys").Scan(&foreignKeys) 60 - if err != nil { 61 - t.Fatalf("Failed to check foreign keys: %v", err) 62 } 63 - 64 - if foreignKeys != 1 { 65 - t.Error("Foreign keys should be enabled") 66 } 67 }) 68 69 - t.Run("enables WAL mode", func(t *testing.T) { 70 - db, err := NewDatabase() 71 - if err != nil { 72 - t.Fatalf("NewDatabase failed: %v", err) 73 - } 74 defer db.Close() 75 76 - var journalMode string 77 - err = db.QueryRow("PRAGMA journal_mode").Scan(&journalMode) 78 - if err != nil { 79 - t.Fatalf("Failed to check journal mode: %v", err) 80 } 81 - 82 - if journalMode != "wal" { 83 - t.Errorf("Expected WAL journal mode, got %s", journalMode) 84 } 85 }) 86 - 87 - t.Run("runs migrations", func(t *testing.T) { 88 - db, err := NewDatabase() 89 - if err != nil { 90 - t.Fatalf("NewDatabase failed: %v", err) 91 - } 92 - defer db.Close() 93 94 - var count int 95 - err = db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='migrations'").Scan(&count) 96 - if err != nil { 97 - t.Fatalf("Failed to check migrations table: %v", err) 98 } 99 100 - if count != 1 { 101 - t.Error("Migrations table should exist") 102 } 103 104 - var migrationCount int 105 - err = db.QueryRow("SELECT COUNT(*) FROM migrations").Scan(&migrationCount) 106 - if err != nil { 107 - t.Fatalf("Failed to count migrations: %v", err) 108 } 109 110 - if migrationCount == 0 { 111 - t.Error("At least one migration should be applied") 112 } 113 }) 114 115 - t.Run("creates migration runner", func(t *testing.T) { 116 - db, err := NewDatabase() 117 - if err != nil { 118 - t.Fatalf("NewDatabase failed: %v", err) 119 } 120 - defer db.Close() 121 122 - runner := db.NewMigrationRunner() 123 - if runner == nil { 124 - t.Error("Migration runner should not be nil") 125 } 126 }) 127 128 - t.Run("closes database connection", func(t *testing.T) { 129 - db, err := NewDatabase() 130 - if err != nil { 131 - t.Fatalf("NewDatabase failed: %v", err) 132 } 133 - 134 - err = db.Close() 135 - if err != nil { 136 - t.Errorf("Close should not return error: %v", err) 137 - } 138 139 - err = db.Ping() 140 - if err == nil { 141 - t.Error("Database should be closed and ping should fail") 142 } 143 }) 144 - } 145 146 - func TestDatabaseErrorHandling(t *testing.T) { 147 - t.Run("handles GetConfigDir error", func(t *testing.T) { 148 - originalGetConfigDir := GetConfigDir 149 - GetConfigDir = func() (string, error) { 150 - return "", os.ErrPermission 151 } 152 - defer func() { GetConfigDir = originalGetConfigDir }() 153 154 _, err := NewDatabase() 155 if err == nil { 156 - t.Error("NewDatabase should fail when GetConfigDir fails") 157 } 158 }) 159 160 - t.Run("handles invalid database path", func(t *testing.T) { 161 - originalGetConfigDir := GetConfigDir 162 - GetConfigDir = func() (string, error) { 163 - return "/invalid/path/that/does/not/exist", nil 164 } 165 - defer func() { GetConfigDir = originalGetConfigDir }() 166 - 167 - _, err := NewDatabase() 168 - if err == nil { 169 - t.Error("NewDatabase should fail with invalid database path") 170 } 171 }) 172 173 - t.Run("handles invalid database connection", func(t *testing.T) { 174 - originalGetConfigDir := GetConfigDir 175 - GetConfigDir = func() (string, error) { 176 - return "/dev/null", nil 177 - } 178 - defer func() { GetConfigDir = originalGetConfigDir }() 179 180 - _, err := NewDatabase() 181 - if err == nil { 182 - t.Error("NewDatabase should fail when database path is invalid") 183 } 184 }) 185 186 - t.Run("handles database file permission error", func(t *testing.T) { 187 - if runtime.GOOS == "windows" { 188 - t.Skip("Permission test not reliable on Windows") 189 - } 190 191 - tempDir, err := os.MkdirTemp("", "noteleaf-db-perm-test-*") 192 if err != nil { 193 - t.Fatalf("Failed to create temp directory: %v", err) 194 } 195 - defer os.RemoveAll(tempDir) 196 197 - originalGetConfigDir := GetConfigDir 198 - GetConfigDir = func() (string, error) { 199 - return tempDir, nil 200 - } 201 - defer func() { GetConfigDir = originalGetConfigDir }() 202 203 - err = os.Chmod(tempDir, 0555) 204 if err != nil { 205 - t.Fatalf("Failed to change directory permissions: %v", err) 206 } 207 - defer os.Chmod(tempDir, 0755) 208 - 209 - _, err = NewDatabase() 210 - if err == nil { 211 - t.Error("NewDatabase should fail when database directory is not writable") 212 } 213 }) 214 } 215 216 - func TestDatabaseIntegration(t *testing.T) { 217 - tempDir, err := os.MkdirTemp("", "noteleaf-db-integration-test-*") 218 - if err != nil { 219 - t.Fatalf("Failed to create temp directory: %v", err) 220 - } 221 - defer os.RemoveAll(tempDir) 222 223 - originalGetConfigDir := GetConfigDir 224 - originalGetDataDir := GetDataDir 225 - GetConfigDir = func() (string, error) { 226 - return tempDir, nil 227 - } 228 - GetDataDir = func() (string, error) { 229 - return tempDir, nil 230 - } 231 - defer func() { 232 - GetConfigDir = originalGetConfigDir 233 - GetDataDir = originalGetDataDir 234 - }() 235 236 - t.Run("multiple database instances use same file", func(t *testing.T) { 237 - db1, err := NewDatabase() 238 if err != nil { 239 - t.Fatalf("First NewDatabase failed: %v", err) 240 } 241 - defer db1.Close() 242 243 - db2, err := NewDatabase() 244 if err != nil { 245 - t.Fatalf("Second NewDatabase failed: %v", err) 246 } 247 - defer db2.Close() 248 - 249 - if db1.GetPath() != db2.GetPath() { 250 - t.Error("Both database instances should use the same file path") 251 } 252 }) 253 254 - t.Run("database survives connection close and reopen", func(t *testing.T) { 255 - db1, err := NewDatabase() 256 - if err != nil { 257 - t.Fatalf("NewDatabase failed: %v", err) 258 - } 259 260 - _, err = db1.Exec("CREATE TABLE IF NOT EXISTS test_table (id INTEGER PRIMARY KEY, name TEXT)") 261 - if err != nil { 262 - t.Fatalf("Failed to create test table: %v", err) 263 - } 264 265 - _, err = db1.Exec("INSERT INTO test_table (name) VALUES (?)", "test_value") 266 - if err != nil { 267 - t.Fatalf("Failed to insert test data: %v", err) 268 } 269 270 - db1.Close() 271 272 - db2, err := NewDatabase() 273 if err != nil { 274 - t.Fatalf("Second NewDatabase failed: %v", err) 275 } 276 - defer db2.Close() 277 278 - var name string 279 - err = db2.QueryRow("SELECT name FROM test_table WHERE id = 1").Scan(&name) 280 if err != nil { 281 - t.Fatalf("Failed to query test data: %v", err) 282 } 283 - 284 - if name != "test_value" { 285 - t.Errorf("Expected 'test_value', got '%s'", name) 286 } 287 }) 288 }
··· 1 package store 2 3 import ( 4 + "database/sql" 5 + "fmt" 6 "os" 7 "path/filepath" 8 "runtime" 9 + "strings" 10 "testing" 11 ) 12 13 + func withTempDirs(t *testing.T) string { 14 + t.Helper() 15 tempDir, err := os.MkdirTemp("", "noteleaf-db-test-*") 16 if err != nil { 17 t.Fatalf("Failed to create temp directory: %v", err) 18 } 19 + t.Cleanup(func() { os.RemoveAll(tempDir) }) 20 + 21 + origConfig, origData := GetConfigDir, GetDataDir 22 + GetConfigDir = func() (string, error) { return tempDir, nil } 23 + GetDataDir = func() (string, error) { return tempDir, nil } 24 + t.Cleanup(func() { 25 + GetConfigDir, GetDataDir = origConfig, origData 26 + }) 27 28 + return tempDir 29 + } 30 + 31 + func TestNewDatabase(t *testing.T) { 32 + tempDir := withTempDirs(t) 33 34 + t.Run("creates database file", func(t *testing.T) { 35 db, err := NewDatabase() 36 if err != nil { 37 t.Fatalf("NewDatabase failed: %v", err) 38 } 39 defer db.Close() 40 41 dbPath := filepath.Join(tempDir, "noteleaf.db") 42 if _, err := os.Stat(dbPath); os.IsNotExist(err) { 43 + t.Errorf("expected database file at %s", dbPath) 44 } 45 }) 46 47 + t.Run("foreign keys enabled", func(t *testing.T) { 48 + db, _ := NewDatabase() 49 defer db.Close() 50 51 + var fk int 52 + if err := db.QueryRow("PRAGMA foreign_keys").Scan(&fk); err != nil { 53 + t.Fatalf("query failed: %v", err) 54 } 55 + if fk != 1 { 56 + t.Errorf("expected foreign_keys=1, got %d", fk) 57 } 58 }) 59 60 + t.Run("WAL enabled", func(t *testing.T) { 61 + db, _ := NewDatabase() 62 defer db.Close() 63 64 + var mode string 65 + if err := db.QueryRow("PRAGMA journal_mode").Scan(&mode); err != nil { 66 + t.Fatalf("query failed: %v", err) 67 } 68 + if strings.ToLower(mode) != "wal" { 69 + t.Errorf("expected wal, got %s", mode) 70 } 71 }) 72 + } 73 74 + func TestNewDatabase_ErrorPaths(t *testing.T) { 75 + t.Run("sql.Open fails", func(t *testing.T) { 76 + orig := sqlOpen 77 + sqlOpen = func(driver, dsn string) (*sql.DB, error) { 78 + return nil, fmt.Errorf("boom") 79 } 80 + t.Cleanup(func() { sqlOpen = orig }) 81 82 + _, err := NewDatabase() 83 + if err == nil || !strings.Contains(err.Error(), "failed to open database") { 84 + t.Errorf("expected open error, got %v", err) 85 } 86 + }) 87 88 + t.Run("foreign_keys pragma fails", func(t *testing.T) { 89 + orig := pragmaExec 90 + pragmaExec = func(db *sql.DB, stmt string) (sql.Result, error) { 91 + if strings.Contains(stmt, "foreign_keys") { 92 + return nil, fmt.Errorf("fk fail") 93 + } 94 + return orig(db, stmt) 95 } 96 + t.Cleanup(func() { pragmaExec = orig }) 97 98 + _, err := NewDatabase() 99 + if err == nil || !strings.Contains(err.Error(), "failed to enable foreign keys") { 100 + t.Errorf("expected foreign key error, got %v", err) 101 } 102 }) 103 104 + t.Run("WAL pragma fails", func(t *testing.T) { 105 + orig := pragmaExec 106 + pragmaExec = func(db *sql.DB, stmt string) (sql.Result, error) { 107 + if strings.Contains(stmt, "journal_mode") { 108 + return nil, fmt.Errorf("wal fail") 109 + } 110 + return orig(db, stmt) 111 } 112 + t.Cleanup(func() { pragmaExec = orig }) 113 114 + _, err := NewDatabase() 115 + if err == nil || !strings.Contains(err.Error(), "failed to enable WAL mode") { 116 + t.Errorf("expected WAL error, got %v", err) 117 } 118 }) 119 120 + t.Run("migration runner fails", func(t *testing.T) { 121 + orig := newMigrationRunner 122 + newMigrationRunner = func(db *sql.DB, fs FileSystem) *MigrationRunner { 123 + return &MigrationRunner{runFn: func() error { return fmt.Errorf("migration fail") }} 124 } 125 + t.Cleanup(func() { newMigrationRunner = orig }) 126 127 + _, err := NewDatabase() 128 + if err == nil || !strings.Contains(err.Error(), "failed to run migrations") { 129 + t.Errorf("expected migration error, got %v", err) 130 } 131 }) 132 133 + t.Run("permission denied on config dir", func(t *testing.T) { 134 + if runtime.GOOS == "windows" { 135 + t.Skip("not reliable on Windows") 136 } 137 + dir := withTempDirs(t) 138 + os.Chmod(dir, 0555) // make read-only 139 + defer os.Chmod(dir, 0755) 140 + 141 + GetConfigDir = func() (string, error) { return dir, nil } 142 143 _, err := NewDatabase() 144 if err == nil { 145 + t.Error("expected mkdir fail due to permission denied") 146 } 147 }) 148 + } 149 150 + func TestGetConfigDir_AllBranches(t *testing.T) { 151 + tmp := t.TempDir() 152 + 153 + t.Run("windows success", func(t *testing.T) { 154 + origGOOS := getRuntime 155 + getRuntime = func() string { return "windows" } 156 + defer func() { getRuntime = origGOOS }() 157 + 158 + os.Setenv("APPDATA", tmp) 159 + defer os.Unsetenv("APPDATA") 160 + 161 + dir, err := GetConfigDir() 162 + if err != nil { 163 + t.Fatalf("unexpected error: %v", err) 164 } 165 + expected := filepath.Join(tmp, "noteleaf") 166 + if dir != expected { 167 + t.Errorf("expected %s, got %s", expected, dir) 168 } 169 }) 170 171 + t.Run("windows missing APPDATA", func(t *testing.T) { 172 + origGOOS := getRuntime 173 + getRuntime = func() string { return "windows" } 174 + defer func() { getRuntime = origGOOS }() 175 176 + os.Unsetenv("APPDATA") 177 + 178 + _, err := GetConfigDir() 179 + if err == nil || !strings.Contains(err.Error(), "APPDATA") { 180 + t.Errorf("expected APPDATA error, got %v", err) 181 } 182 }) 183 184 + t.Run("darwin success", func(t *testing.T) { 185 + origGOOS, origHome := getRuntime, getHomeDir 186 + getRuntime = func() string { return "darwin" } 187 + getHomeDir = func() (string, error) { return tmp, nil } 188 + defer func() { 189 + getRuntime = origGOOS 190 + getHomeDir = origHome 191 + }() 192 193 + dir, err := GetConfigDir() 194 if err != nil { 195 + t.Fatalf("unexpected error: %v", err) 196 + } 197 + expected := filepath.Join(tmp, "Library", "Application Support", "noteleaf") 198 + if dir != expected { 199 + t.Errorf("expected %s, got %s", expected, dir) 200 } 201 + }) 202 203 + t.Run("linux default with XDG_CONFIG_HOME unset", func(t *testing.T) { 204 + origGOOS, origHome := getRuntime, getHomeDir 205 + getRuntime = func() string { return "linux" } 206 + getHomeDir = func() (string, error) { return tmp, nil } 207 + defer func() { 208 + getRuntime = origGOOS 209 + getHomeDir = origHome 210 + }() 211 212 + os.Unsetenv("XDG_CONFIG_HOME") 213 + 214 + dir, err := GetConfigDir() 215 if err != nil { 216 + t.Fatalf("unexpected error: %v", err) 217 } 218 + expected := filepath.Join(tmp, ".config", "noteleaf") 219 + if dir != expected { 220 + t.Errorf("expected %s, got %s", expected, dir) 221 } 222 }) 223 } 224 225 + func TestGetDataDir_AllBranches(t *testing.T) { 226 + tmp := t.TempDir() 227 228 + t.Run("NOTELEAF_DATA_DIR overrides", func(t *testing.T) { 229 + os.Setenv("NOTELEAF_DATA_DIR", tmp) 230 + defer os.Unsetenv("NOTELEAF_DATA_DIR") 231 232 + dir, err := GetDataDir() 233 if err != nil { 234 + t.Fatalf("unexpected error: %v", err) 235 } 236 + if dir != tmp { 237 + t.Errorf("expected %s, got %s", tmp, dir) 238 + } 239 + }) 240 241 + t.Run("windows success", func(t *testing.T) { 242 + origGOOS := getRuntime 243 + getRuntime = func() string { return "windows" } 244 + defer func() { getRuntime = origGOOS }() 245 + 246 + os.Setenv("LOCALAPPDATA", tmp) 247 + defer os.Unsetenv("LOCALAPPDATA") 248 + 249 + dir, err := GetDataDir() 250 if err != nil { 251 + t.Fatalf("unexpected error: %v", err) 252 } 253 + expected := filepath.Join(tmp, "noteleaf") 254 + if dir != expected { 255 + t.Errorf("expected %s, got %s", expected, dir) 256 } 257 }) 258 259 + t.Run("windows missing LOCALAPPDATA", func(t *testing.T) { 260 + origGOOS := getRuntime 261 + getRuntime = func() string { return "windows" } 262 + defer func() { getRuntime = origGOOS }() 263 264 + os.Unsetenv("LOCALAPPDATA") 265 266 + _, err := GetDataDir() 267 + if err == nil || !strings.Contains(err.Error(), "LOCALAPPDATA") { 268 + t.Errorf("expected LOCALAPPDATA error, got %v", err) 269 } 270 + }) 271 272 + t.Run("darwin success", func(t *testing.T) { 273 + origGOOS, origHome := getRuntime, getHomeDir 274 + getRuntime = func() string { return "darwin" } 275 + getHomeDir = func() (string, error) { return tmp, nil } 276 + defer func() { 277 + getRuntime = origGOOS 278 + getHomeDir = origHome 279 + }() 280 281 + dir, err := GetDataDir() 282 if err != nil { 283 + t.Fatalf("unexpected error: %v", err) 284 } 285 + expected := filepath.Join(tmp, "Library", "Application Support", "noteleaf") 286 + if dir != expected { 287 + t.Errorf("expected %s, got %s", expected, dir) 288 + } 289 + }) 290 291 + t.Run("linux default with XDG_DATA_HOME unset", func(t *testing.T) { 292 + origGOOS, origHome := getRuntime, getHomeDir 293 + getRuntime = func() string { return "linux" } 294 + getHomeDir = func() (string, error) { return tmp, nil } 295 + defer func() { 296 + getRuntime = origGOOS 297 + getHomeDir = origHome 298 + }() 299 + 300 + os.Unsetenv("XDG_DATA_HOME") 301 + 302 + dir, err := GetDataDir() 303 if err != nil { 304 + t.Fatalf("unexpected error: %v", err) 305 } 306 + expected := filepath.Join(tmp, ".local", "share", "noteleaf") 307 + if dir != expected { 308 + t.Errorf("expected %s, got %s", expected, dir) 309 } 310 }) 311 }
+13 -3
internal/store/migration.go
··· 28 type MigrationRunner struct { 29 db *sql.DB 30 migrationFiles FileSystem 31 } 32 33 // NewMigrationRunner creates a new migration runner 34 func NewMigrationRunner(db *sql.DB, files FileSystem) *MigrationRunner { 35 - return &MigrationRunner{ 36 db: db, 37 migrationFiles: files, 38 } 39 } 40 41 - // RunMigrations applies all pending migrations 42 func (mr *MigrationRunner) RunMigrations() error { 43 entries, err := mr.migrationFiles.ReadDir("sql/migrations") 44 if err != nil { 45 return fmt.Errorf("failed to read migrations directory: %w", err) ··· 214 return nil 215 } 216 217 - // extractVersionFromFilename extracts the 4-digit version from a migration filename 218 func extractVersionFromFilename(filename string) string { 219 parts := strings.Split(filename, "_") 220 if len(parts) > 0 {
··· 28 type MigrationRunner struct { 29 db *sql.DB 30 migrationFiles FileSystem 31 + runFn func() error // inject for testing 32 } 33 34 // NewMigrationRunner creates a new migration runner 35 func NewMigrationRunner(db *sql.DB, files FileSystem) *MigrationRunner { 36 + mr := &MigrationRunner{ 37 db: db, 38 migrationFiles: files, 39 } 40 + mr.runFn = mr.defaultRunMigrations 41 + return mr 42 } 43 44 + // RunMigrations applies all pending migrations (delegates to runFn) 45 func (mr *MigrationRunner) RunMigrations() error { 46 + if mr.runFn != nil { 47 + return mr.runFn() 48 + } 49 + return nil 50 + } 51 + 52 + func (mr *MigrationRunner) defaultRunMigrations() error { 53 entries, err := mr.migrationFiles.ReadDir("sql/migrations") 54 if err != nil { 55 return fmt.Errorf("failed to read migrations directory: %w", err) ··· 224 return nil 225 } 226 227 + // extractVersionFromFilename extracts the 4-digit version from a [Migration] filename 228 func extractVersionFromFilename(filename string) string { 229 parts := strings.Split(filename, "_") 230 if len(parts) > 0 {