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