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