cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
1package store
2
3import (
4 "database/sql"
5 "fmt"
6 "os"
7 "path/filepath"
8 "runtime"
9 "strings"
10 "testing"
11
12 "github.com/stormlightlabs/noteleaf/internal/shared"
13)
14
15func withTempDirs(t *testing.T) string {
16 t.Helper()
17 tempDir, cleanup := shared.CreateTempDir("noteleaf-db-test-*", t)
18 t.Cleanup(func() { cleanup() })
19
20 origConfig, origData := GetConfigDir, GetDataDir
21 GetConfigDir = func() (string, error) { return tempDir, nil }
22 GetDataDir = func() (string, error) { return tempDir, nil }
23 t.Cleanup(func() {
24 GetConfigDir, GetDataDir = origConfig, origData
25 })
26
27 return tempDir
28}
29
30func TestNewDatabase(t *testing.T) {
31 tempDir := withTempDirs(t)
32
33 t.Run("creates database file", func(t *testing.T) {
34 db, err := NewDatabase()
35 if err != nil {
36 t.Fatalf("NewDatabase failed: %v", err)
37 }
38 defer db.Close()
39
40 dbPath := filepath.Join(tempDir, "noteleaf.db")
41 if _, err := os.Stat(dbPath); os.IsNotExist(err) {
42 t.Errorf("expected database file at %s", dbPath)
43 }
44 })
45
46 t.Run("foreign keys enabled", func(t *testing.T) {
47 db, _ := NewDatabase()
48 defer db.Close()
49
50 var fk int
51 if err := db.QueryRow("PRAGMA foreign_keys").Scan(&fk); err != nil {
52 t.Fatalf("query failed: %v", err)
53 }
54 if fk != 1 {
55 t.Errorf("expected foreign_keys=1, got %d", fk)
56 }
57 })
58
59 t.Run("WAL enabled", func(t *testing.T) {
60 db, _ := NewDatabase()
61 defer db.Close()
62
63 var mode string
64 if err := db.QueryRow("PRAGMA journal_mode").Scan(&mode); err != nil {
65 t.Fatalf("query failed: %v", err)
66 }
67 if strings.ToLower(mode) != "wal" {
68 t.Errorf("expected wal, got %s", mode)
69 }
70 })
71}
72
73func TestNewDatabase_ErrorPaths(t *testing.T) {
74 t.Run("sql.Open fails", func(t *testing.T) {
75 orig := sqlOpen
76 sqlOpen = func(driver, dsn string) (*sql.DB, error) {
77 return nil, fmt.Errorf("boom")
78 }
79 t.Cleanup(func() { sqlOpen = orig })
80
81 _, err := NewDatabase()
82 if err == nil || !strings.Contains(err.Error(), "failed to open database") {
83 t.Errorf("expected open error, got %v", err)
84 }
85 })
86
87 t.Run("foreign_keys pragma fails", func(t *testing.T) {
88 orig := pragmaExec
89 pragmaExec = func(db *sql.DB, stmt string) (sql.Result, error) {
90 if strings.Contains(stmt, "foreign_keys") {
91 return nil, fmt.Errorf("fk fail")
92 }
93 return orig(db, stmt)
94 }
95 t.Cleanup(func() { pragmaExec = orig })
96
97 _, err := NewDatabase()
98 if err == nil || !strings.Contains(err.Error(), "failed to enable foreign keys") {
99 t.Errorf("expected foreign key error, got %v", err)
100 }
101 })
102
103 t.Run("WAL pragma fails", func(t *testing.T) {
104 orig := pragmaExec
105 pragmaExec = func(db *sql.DB, stmt string) (sql.Result, error) {
106 if strings.Contains(stmt, "journal_mode") {
107 return nil, fmt.Errorf("wal fail")
108 }
109 return orig(db, stmt)
110 }
111 t.Cleanup(func() { pragmaExec = orig })
112
113 _, err := NewDatabase()
114 if err == nil || !strings.Contains(err.Error(), "failed to enable WAL mode") {
115 t.Errorf("expected WAL error, got %v", err)
116 }
117 })
118
119 t.Run("migration runner fails", func(t *testing.T) {
120 orig := createMigrationRunner
121 createMigrationRunner = func(db *sql.DB, fs FileSystem) *MigrationRunner {
122 return &MigrationRunner{runFn: func() error { return fmt.Errorf("migration fail") }}
123 }
124 t.Cleanup(func() { createMigrationRunner = orig })
125
126 _, err := NewDatabase()
127 if err == nil || !strings.Contains(err.Error(), "failed to run migrations") {
128 t.Errorf("expected migration error, got %v", err)
129 }
130 })
131
132 t.Run("permission denied on config dir", func(t *testing.T) {
133 if runtime.GOOS == "windows" {
134 t.Skip("not reliable on Windows")
135 }
136 dir := withTempDirs(t)
137 os.Chmod(dir, 0555) // make read-only
138 defer os.Chmod(dir, 0755)
139
140 GetConfigDir = func() (string, error) { return dir, nil }
141
142 _, err := NewDatabase()
143 if err == nil {
144 t.Error("expected mkdir fail due to permission denied")
145 }
146 })
147}
148
149func TestGetConfigDir_AllBranches(t *testing.T) {
150 tmp := t.TempDir()
151
152 t.Run("windows success", func(t *testing.T) {
153 origGOOS := getRuntime
154 getRuntime = func() string { return "windows" }
155 defer func() { getRuntime = origGOOS }()
156
157 os.Setenv("APPDATA", tmp)
158 defer os.Unsetenv("APPDATA")
159
160 dir, err := GetConfigDir()
161 if err != nil {
162 t.Fatalf("unexpected error: %v", err)
163 }
164 expected := filepath.Join(tmp, "noteleaf")
165 if dir != expected {
166 t.Errorf("expected %s, got %s", expected, dir)
167 }
168 })
169
170 t.Run("windows missing APPDATA", func(t *testing.T) {
171 origGOOS := getRuntime
172 getRuntime = func() string { return "windows" }
173 defer func() { getRuntime = origGOOS }()
174
175 os.Unsetenv("APPDATA")
176
177 _, err := GetConfigDir()
178 if err == nil || !strings.Contains(err.Error(), "APPDATA") {
179 t.Errorf("expected APPDATA error, got %v", err)
180 }
181 })
182
183 t.Run("darwin success", func(t *testing.T) {
184 origGOOS, origHome := getRuntime, getHomeDir
185 getRuntime = func() string { return "darwin" }
186 getHomeDir = func() (string, error) { return tmp, nil }
187 defer func() {
188 getRuntime = origGOOS
189 getHomeDir = origHome
190 }()
191
192 dir, err := GetConfigDir()
193 if err != nil {
194 t.Fatalf("unexpected error: %v", err)
195 }
196 expected := filepath.Join(tmp, "Library", "Application Support", "noteleaf")
197 if dir != expected {
198 t.Errorf("expected %s, got %s", expected, dir)
199 }
200 })
201
202 t.Run("linux default with XDG_CONFIG_HOME unset", func(t *testing.T) {
203 origGOOS, origHome := getRuntime, getHomeDir
204 getRuntime = func() string { return "linux" }
205 getHomeDir = func() (string, error) { return tmp, nil }
206 defer func() {
207 getRuntime = origGOOS
208 getHomeDir = origHome
209 }()
210
211 os.Unsetenv("XDG_CONFIG_HOME")
212
213 dir, err := GetConfigDir()
214 if err != nil {
215 t.Fatalf("unexpected error: %v", err)
216 }
217 expected := filepath.Join(tmp, ".config", "noteleaf")
218 if dir != expected {
219 t.Errorf("expected %s, got %s", expected, dir)
220 }
221 })
222}
223
224func TestGetDataDir_AllBranches(t *testing.T) {
225 tmp := t.TempDir()
226
227 t.Run("NOTELEAF_DATA_DIR overrides", func(t *testing.T) {
228 os.Setenv("NOTELEAF_DATA_DIR", tmp)
229 defer os.Unsetenv("NOTELEAF_DATA_DIR")
230
231 dir, err := GetDataDir()
232 if err != nil {
233 t.Fatalf("unexpected error: %v", err)
234 }
235 if dir != tmp {
236 t.Errorf("expected %s, got %s", tmp, dir)
237 }
238 })
239
240 t.Run("windows success", func(t *testing.T) {
241 origGOOS := getRuntime
242 getRuntime = func() string { return "windows" }
243 defer func() { getRuntime = origGOOS }()
244
245 os.Setenv("LOCALAPPDATA", tmp)
246 defer os.Unsetenv("LOCALAPPDATA")
247
248 dir, err := GetDataDir()
249 if err != nil {
250 t.Fatalf("unexpected error: %v", err)
251 }
252 expected := filepath.Join(tmp, "noteleaf")
253 if dir != expected {
254 t.Errorf("expected %s, got %s", expected, dir)
255 }
256 })
257
258 t.Run("windows missing LOCALAPPDATA", func(t *testing.T) {
259 origGOOS := getRuntime
260 getRuntime = func() string { return "windows" }
261 defer func() { getRuntime = origGOOS }()
262
263 os.Unsetenv("LOCALAPPDATA")
264
265 _, err := GetDataDir()
266 if err == nil || !strings.Contains(err.Error(), "LOCALAPPDATA") {
267 t.Errorf("expected LOCALAPPDATA error, got %v", err)
268 }
269 })
270
271 t.Run("darwin success", func(t *testing.T) {
272 origGOOS, origHome := getRuntime, getHomeDir
273 getRuntime = func() string { return "darwin" }
274 getHomeDir = func() (string, error) { return tmp, nil }
275 defer func() {
276 getRuntime = origGOOS
277 getHomeDir = origHome
278 }()
279
280 dir, err := GetDataDir()
281 if err != nil {
282 t.Fatalf("unexpected error: %v", err)
283 }
284 expected := filepath.Join(tmp, "Library", "Application Support", "noteleaf")
285 if dir != expected {
286 t.Errorf("expected %s, got %s", expected, dir)
287 }
288 })
289
290 t.Run("linux default with XDG_DATA_HOME unset", func(t *testing.T) {
291 origGOOS, origHome := getRuntime, getHomeDir
292 getRuntime = func() string { return "linux" }
293 getHomeDir = func() (string, error) { return tmp, nil }
294 defer func() {
295 getRuntime = origGOOS
296 getHomeDir = origHome
297 }()
298
299 os.Unsetenv("XDG_DATA_HOME")
300
301 dir, err := GetDataDir()
302 if err != nil {
303 t.Fatalf("unexpected error: %v", err)
304 }
305 expected := filepath.Join(tmp, ".local", "share", "noteleaf")
306 if dir != expected {
307 t.Errorf("expected %s, got %s", expected, dir)
308 }
309 })
310}