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
build: persistence layer tests
desertthunder.dev
5 months ago
5bd45ba7
a982f381
+1625
-1
5 changed files
expand all
collapse all
unified
split
internal
models
models_test.go
store
config_test.go
database.go
database_test.go
migration_test.go
+653
internal/models/models_test.go
···
1
1
+
package models
2
2
+
3
3
+
import (
4
4
+
"encoding/json"
5
5
+
"testing"
6
6
+
"time"
7
7
+
)
8
8
+
9
9
+
func TestModels(t *testing.T) {
10
10
+
t.Run("Task Model", func(t *testing.T) {
11
11
+
t.Run("Model Interface Implementation", func(t *testing.T) {
12
12
+
task := &Task{
13
13
+
ID: 1,
14
14
+
UUID: "test-uuid",
15
15
+
Description: "Test task",
16
16
+
Status: "pending",
17
17
+
Entry: time.Now(),
18
18
+
Modified: time.Now(),
19
19
+
}
20
20
+
21
21
+
if task.GetID() != 1 {
22
22
+
t.Errorf("Expected ID 1, got %d", task.GetID())
23
23
+
}
24
24
+
25
25
+
task.SetID(2)
26
26
+
if task.GetID() != 2 {
27
27
+
t.Errorf("Expected ID 2 after SetID, got %d", task.GetID())
28
28
+
}
29
29
+
30
30
+
if task.GetTableName() != "tasks" {
31
31
+
t.Errorf("Expected table name 'tasks', got '%s'", task.GetTableName())
32
32
+
}
33
33
+
34
34
+
createdAt := time.Now()
35
35
+
task.SetCreatedAt(createdAt)
36
36
+
if !task.GetCreatedAt().Equal(createdAt) {
37
37
+
t.Errorf("Expected created at %v, got %v", createdAt, task.GetCreatedAt())
38
38
+
}
39
39
+
40
40
+
updatedAt := time.Now().Add(time.Hour)
41
41
+
task.SetUpdatedAt(updatedAt)
42
42
+
if !task.GetUpdatedAt().Equal(updatedAt) {
43
43
+
t.Errorf("Expected updated at %v, got %v", updatedAt, task.GetUpdatedAt())
44
44
+
}
45
45
+
})
46
46
+
47
47
+
t.Run("Status Methods", func(t *testing.T) {
48
48
+
testCases := []struct {
49
49
+
status string
50
50
+
isCompleted bool
51
51
+
isPending bool
52
52
+
isDeleted bool
53
53
+
}{
54
54
+
{"pending", false, true, false},
55
55
+
{"completed", true, false, false},
56
56
+
{"deleted", false, false, true},
57
57
+
{"unknown", false, false, false},
58
58
+
}
59
59
+
60
60
+
for _, tc := range testCases {
61
61
+
task := &Task{Status: tc.status}
62
62
+
63
63
+
if task.IsCompleted() != tc.isCompleted {
64
64
+
t.Errorf("Status %s: expected IsCompleted %v, got %v", tc.status, tc.isCompleted, task.IsCompleted())
65
65
+
}
66
66
+
if task.IsPending() != tc.isPending {
67
67
+
t.Errorf("Status %s: expected IsPending %v, got %v", tc.status, tc.isPending, task.IsPending())
68
68
+
}
69
69
+
if task.IsDeleted() != tc.isDeleted {
70
70
+
t.Errorf("Status %s: expected IsDeleted %v, got %v", tc.status, tc.isDeleted, task.IsDeleted())
71
71
+
}
72
72
+
}
73
73
+
})
74
74
+
75
75
+
t.Run("Priority Methods", func(t *testing.T) {
76
76
+
task := &Task{}
77
77
+
78
78
+
if task.HasPriority() {
79
79
+
t.Error("Task with empty priority should return false for HasPriority")
80
80
+
}
81
81
+
82
82
+
task.Priority = "A"
83
83
+
if !task.HasPriority() {
84
84
+
t.Error("Task with priority should return true for HasPriority")
85
85
+
}
86
86
+
})
87
87
+
88
88
+
t.Run("Tags Marshaling", func(t *testing.T) {
89
89
+
task := &Task{}
90
90
+
91
91
+
result, err := task.MarshalTags()
92
92
+
if err != nil {
93
93
+
t.Fatalf("MarshalTags failed: %v", err)
94
94
+
}
95
95
+
if result != "" {
96
96
+
t.Errorf("Expected empty string for empty tags, got '%s'", result)
97
97
+
}
98
98
+
99
99
+
task.Tags = []string{"work", "urgent", "project-x"}
100
100
+
result, err = task.MarshalTags()
101
101
+
if err != nil {
102
102
+
t.Fatalf("MarshalTags failed: %v", err)
103
103
+
}
104
104
+
105
105
+
expected := `["work","urgent","project-x"]`
106
106
+
if result != expected {
107
107
+
t.Errorf("Expected %s, got %s", expected, result)
108
108
+
}
109
109
+
110
110
+
newTask := &Task{}
111
111
+
err = newTask.UnmarshalTags(result)
112
112
+
if err != nil {
113
113
+
t.Fatalf("UnmarshalTags failed: %v", err)
114
114
+
}
115
115
+
116
116
+
if len(newTask.Tags) != 3 {
117
117
+
t.Errorf("Expected 3 tags, got %d", len(newTask.Tags))
118
118
+
}
119
119
+
if newTask.Tags[0] != "work" || newTask.Tags[1] != "urgent" || newTask.Tags[2] != "project-x" {
120
120
+
t.Errorf("Tags not unmarshaled correctly: %v", newTask.Tags)
121
121
+
}
122
122
+
123
123
+
emptyTask := &Task{}
124
124
+
err = emptyTask.UnmarshalTags("")
125
125
+
if err != nil {
126
126
+
t.Fatalf("UnmarshalTags with empty string failed: %v", err)
127
127
+
}
128
128
+
if emptyTask.Tags != nil {
129
129
+
t.Error("Expected nil tags for empty string")
130
130
+
}
131
131
+
})
132
132
+
133
133
+
t.Run("Annotations Marshaling", func(t *testing.T) {
134
134
+
task := &Task{}
135
135
+
136
136
+
result, err := task.MarshalAnnotations()
137
137
+
if err != nil {
138
138
+
t.Fatalf("MarshalAnnotations failed: %v", err)
139
139
+
}
140
140
+
if result != "" {
141
141
+
t.Errorf("Expected empty string for empty annotations, got '%s'", result)
142
142
+
}
143
143
+
144
144
+
task.Annotations = []string{"Note 1", "Note 2", "Important reminder"}
145
145
+
result, err = task.MarshalAnnotations()
146
146
+
if err != nil {
147
147
+
t.Fatalf("MarshalAnnotations failed: %v", err)
148
148
+
}
149
149
+
150
150
+
expected := `["Note 1","Note 2","Important reminder"]`
151
151
+
if result != expected {
152
152
+
t.Errorf("Expected %s, got %s", expected, result)
153
153
+
}
154
154
+
155
155
+
newTask := &Task{}
156
156
+
err = newTask.UnmarshalAnnotations(result)
157
157
+
if err != nil {
158
158
+
t.Fatalf("UnmarshalAnnotations failed: %v", err)
159
159
+
}
160
160
+
161
161
+
if len(newTask.Annotations) != 3 {
162
162
+
t.Errorf("Expected 3 annotations, got %d", len(newTask.Annotations))
163
163
+
}
164
164
+
if newTask.Annotations[0] != "Note 1" || newTask.Annotations[1] != "Note 2" || newTask.Annotations[2] != "Important reminder" {
165
165
+
t.Errorf("Annotations not unmarshaled correctly: %v", newTask.Annotations)
166
166
+
}
167
167
+
168
168
+
emptyTask := &Task{}
169
169
+
err = emptyTask.UnmarshalAnnotations("")
170
170
+
if err != nil {
171
171
+
t.Fatalf("UnmarshalAnnotations with empty string failed: %v", err)
172
172
+
}
173
173
+
if emptyTask.Annotations != nil {
174
174
+
t.Error("Expected nil annotations for empty string")
175
175
+
}
176
176
+
})
177
177
+
178
178
+
t.Run("JSON Marshaling", func(t *testing.T) {
179
179
+
now := time.Now()
180
180
+
due := now.Add(24 * time.Hour)
181
181
+
task := &Task{
182
182
+
ID: 1,
183
183
+
UUID: "test-uuid",
184
184
+
Description: "Test task",
185
185
+
Status: "pending",
186
186
+
Priority: "A",
187
187
+
Project: "test-project",
188
188
+
Tags: []string{"work", "urgent"},
189
189
+
Due: &due,
190
190
+
Entry: now,
191
191
+
Modified: now,
192
192
+
Annotations: []string{"Note 1"},
193
193
+
}
194
194
+
195
195
+
data, err := json.Marshal(task)
196
196
+
if err != nil {
197
197
+
t.Fatalf("JSON marshal failed: %v", err)
198
198
+
}
199
199
+
200
200
+
var unmarshaled Task
201
201
+
err = json.Unmarshal(data, &unmarshaled)
202
202
+
if err != nil {
203
203
+
t.Fatalf("JSON unmarshal failed: %v", err)
204
204
+
}
205
205
+
206
206
+
if unmarshaled.ID != task.ID {
207
207
+
t.Errorf("Expected ID %d, got %d", task.ID, unmarshaled.ID)
208
208
+
}
209
209
+
if unmarshaled.UUID != task.UUID {
210
210
+
t.Errorf("Expected UUID %s, got %s", task.UUID, unmarshaled.UUID)
211
211
+
}
212
212
+
if unmarshaled.Description != task.Description {
213
213
+
t.Errorf("Expected description %s, got %s", task.Description, unmarshaled.Description)
214
214
+
}
215
215
+
})
216
216
+
})
217
217
+
218
218
+
t.Run("Movie Model", func(t *testing.T) {
219
219
+
t.Run("Model Interface Implementation", func(t *testing.T) {
220
220
+
movie := &Movie{
221
221
+
ID: 1,
222
222
+
Title: "Test Movie",
223
223
+
Year: 2023,
224
224
+
Added: time.Now(),
225
225
+
}
226
226
+
227
227
+
if movie.GetID() != 1 {
228
228
+
t.Errorf("Expected ID 1, got %d", movie.GetID())
229
229
+
}
230
230
+
231
231
+
movie.SetID(2)
232
232
+
if movie.GetID() != 2 {
233
233
+
t.Errorf("Expected ID 2 after SetID, got %d", movie.GetID())
234
234
+
}
235
235
+
236
236
+
if movie.GetTableName() != "movies" {
237
237
+
t.Errorf("Expected table name 'movies', got '%s'", movie.GetTableName())
238
238
+
}
239
239
+
240
240
+
createdAt := time.Now()
241
241
+
movie.SetCreatedAt(createdAt)
242
242
+
if !movie.GetCreatedAt().Equal(createdAt) {
243
243
+
t.Errorf("Expected created at %v, got %v", createdAt, movie.GetCreatedAt())
244
244
+
}
245
245
+
246
246
+
updatedAt := time.Now().Add(time.Hour)
247
247
+
movie.SetUpdatedAt(updatedAt)
248
248
+
if !movie.GetUpdatedAt().Equal(updatedAt) {
249
249
+
t.Errorf("Expected updated at %v, got %v", updatedAt, movie.GetUpdatedAt())
250
250
+
}
251
251
+
})
252
252
+
253
253
+
t.Run("Status Methods", func(t *testing.T) {
254
254
+
testCases := []struct {
255
255
+
status string
256
256
+
isWatched bool
257
257
+
isQueued bool
258
258
+
}{
259
259
+
{"queued", false, true},
260
260
+
{"watched", true, false},
261
261
+
{"removed", false, false},
262
262
+
{"unknown", false, false},
263
263
+
}
264
264
+
265
265
+
for _, tc := range testCases {
266
266
+
movie := &Movie{Status: tc.status}
267
267
+
268
268
+
if movie.IsWatched() != tc.isWatched {
269
269
+
t.Errorf("Status %s: expected IsWatched %v, got %v", tc.status, tc.isWatched, movie.IsWatched())
270
270
+
}
271
271
+
if movie.IsQueued() != tc.isQueued {
272
272
+
t.Errorf("Status %s: expected IsQueued %v, got %v", tc.status, tc.isQueued, movie.IsQueued())
273
273
+
}
274
274
+
}
275
275
+
})
276
276
+
277
277
+
t.Run("JSON Marshaling", func(t *testing.T) {
278
278
+
now := time.Now()
279
279
+
watched := now.Add(-24 * time.Hour)
280
280
+
movie := &Movie{
281
281
+
ID: 1,
282
282
+
Title: "Test Movie",
283
283
+
Year: 2023,
284
284
+
Status: "watched",
285
285
+
Rating: 8.5,
286
286
+
Notes: "Great movie!",
287
287
+
Added: now,
288
288
+
Watched: &watched,
289
289
+
}
290
290
+
291
291
+
data, err := json.Marshal(movie)
292
292
+
if err != nil {
293
293
+
t.Fatalf("JSON marshal failed: %v", err)
294
294
+
}
295
295
+
296
296
+
var unmarshaled Movie
297
297
+
err = json.Unmarshal(data, &unmarshaled)
298
298
+
if err != nil {
299
299
+
t.Fatalf("JSON unmarshal failed: %v", err)
300
300
+
}
301
301
+
302
302
+
if unmarshaled.ID != movie.ID {
303
303
+
t.Errorf("Expected ID %d, got %d", movie.ID, unmarshaled.ID)
304
304
+
}
305
305
+
if unmarshaled.Title != movie.Title {
306
306
+
t.Errorf("Expected title %s, got %s", movie.Title, unmarshaled.Title)
307
307
+
}
308
308
+
if unmarshaled.Rating != movie.Rating {
309
309
+
t.Errorf("Expected rating %f, got %f", movie.Rating, unmarshaled.Rating)
310
310
+
}
311
311
+
})
312
312
+
})
313
313
+
314
314
+
t.Run("TV Show Model", func(t *testing.T) {
315
315
+
t.Run("Model Interface Implementation", func(t *testing.T) {
316
316
+
tvShow := &TVShow{
317
317
+
ID: 1,
318
318
+
Title: "Test Show",
319
319
+
Added: time.Now(),
320
320
+
}
321
321
+
322
322
+
if tvShow.GetID() != 1 {
323
323
+
t.Errorf("Expected ID 1, got %d", tvShow.GetID())
324
324
+
}
325
325
+
326
326
+
tvShow.SetID(2)
327
327
+
if tvShow.GetID() != 2 {
328
328
+
t.Errorf("Expected ID 2 after SetID, got %d", tvShow.GetID())
329
329
+
}
330
330
+
331
331
+
if tvShow.GetTableName() != "tv_shows" {
332
332
+
t.Errorf("Expected table name 'tv_shows', got '%s'", tvShow.GetTableName())
333
333
+
}
334
334
+
335
335
+
createdAt := time.Now()
336
336
+
tvShow.SetCreatedAt(createdAt)
337
337
+
if !tvShow.GetCreatedAt().Equal(createdAt) {
338
338
+
t.Errorf("Expected created at %v, got %v", createdAt, tvShow.GetCreatedAt())
339
339
+
}
340
340
+
341
341
+
updatedAt := time.Now().Add(time.Hour)
342
342
+
tvShow.SetUpdatedAt(updatedAt)
343
343
+
if !tvShow.GetUpdatedAt().Equal(updatedAt) {
344
344
+
t.Errorf("Expected updated at %v, got %v", updatedAt, tvShow.GetUpdatedAt())
345
345
+
}
346
346
+
})
347
347
+
348
348
+
t.Run("Status Methods", func(t *testing.T) {
349
349
+
testCases := []struct {
350
350
+
status string
351
351
+
isWatching bool
352
352
+
isWatched bool
353
353
+
isQueued bool
354
354
+
}{
355
355
+
{"queued", false, false, true},
356
356
+
{"watching", true, false, false},
357
357
+
{"watched", false, true, false},
358
358
+
{"removed", false, false, false},
359
359
+
{"unknown", false, false, false},
360
360
+
}
361
361
+
362
362
+
for _, tc := range testCases {
363
363
+
tvShow := &TVShow{Status: tc.status}
364
364
+
365
365
+
if tvShow.IsWatching() != tc.isWatching {
366
366
+
t.Errorf("Status %s: expected IsWatching %v, got %v", tc.status, tc.isWatching, tvShow.IsWatching())
367
367
+
}
368
368
+
if tvShow.IsWatched() != tc.isWatched {
369
369
+
t.Errorf("Status %s: expected IsWatched %v, got %v", tc.status, tc.isWatched, tvShow.IsWatched())
370
370
+
}
371
371
+
if tvShow.IsQueued() != tc.isQueued {
372
372
+
t.Errorf("Status %s: expected IsQueued %v, got %v", tc.status, tc.isQueued, tvShow.IsQueued())
373
373
+
}
374
374
+
}
375
375
+
})
376
376
+
377
377
+
t.Run("JSON Marshaling", func(t *testing.T) {
378
378
+
now := time.Now()
379
379
+
lastWatched := now.Add(-24 * time.Hour)
380
380
+
tvShow := &TVShow{
381
381
+
ID: 1,
382
382
+
Title: "Test Show",
383
383
+
Season: 1,
384
384
+
Episode: 5,
385
385
+
Status: "watching",
386
386
+
Rating: 9.0,
387
387
+
Notes: "Amazing series!",
388
388
+
Added: now,
389
389
+
LastWatched: &lastWatched,
390
390
+
}
391
391
+
392
392
+
data, err := json.Marshal(tvShow)
393
393
+
if err != nil {
394
394
+
t.Fatalf("JSON marshal failed: %v", err)
395
395
+
}
396
396
+
397
397
+
var unmarshaled TVShow
398
398
+
err = json.Unmarshal(data, &unmarshaled)
399
399
+
if err != nil {
400
400
+
t.Fatalf("JSON unmarshal failed: %v", err)
401
401
+
}
402
402
+
403
403
+
if unmarshaled.ID != tvShow.ID {
404
404
+
t.Errorf("Expected ID %d, got %d", tvShow.ID, unmarshaled.ID)
405
405
+
}
406
406
+
if unmarshaled.Title != tvShow.Title {
407
407
+
t.Errorf("Expected title %s, got %s", tvShow.Title, unmarshaled.Title)
408
408
+
}
409
409
+
if unmarshaled.Season != tvShow.Season {
410
410
+
t.Errorf("Expected season %d, got %d", tvShow.Season, unmarshaled.Season)
411
411
+
}
412
412
+
if unmarshaled.Episode != tvShow.Episode {
413
413
+
t.Errorf("Expected episode %d, got %d", tvShow.Episode, unmarshaled.Episode)
414
414
+
}
415
415
+
})
416
416
+
})
417
417
+
418
418
+
t.Run("Book Model", func(t *testing.T) {
419
419
+
t.Run("Model Interface Implementation", func(t *testing.T) {
420
420
+
book := &Book{
421
421
+
ID: 1,
422
422
+
Title: "Test Book",
423
423
+
Added: time.Now(),
424
424
+
}
425
425
+
426
426
+
if book.GetID() != 1 {
427
427
+
t.Errorf("Expected ID 1, got %d", book.GetID())
428
428
+
}
429
429
+
430
430
+
book.SetID(2)
431
431
+
if book.GetID() != 2 {
432
432
+
t.Errorf("Expected ID 2 after SetID, got %d", book.GetID())
433
433
+
}
434
434
+
435
435
+
if book.GetTableName() != "books" {
436
436
+
t.Errorf("Expected table name 'books', got '%s'", book.GetTableName())
437
437
+
}
438
438
+
439
439
+
createdAt := time.Now()
440
440
+
book.SetCreatedAt(createdAt)
441
441
+
if !book.GetCreatedAt().Equal(createdAt) {
442
442
+
t.Errorf("Expected created at %v, got %v", createdAt, book.GetCreatedAt())
443
443
+
}
444
444
+
445
445
+
updatedAt := time.Now().Add(time.Hour)
446
446
+
book.SetUpdatedAt(updatedAt)
447
447
+
if !book.GetUpdatedAt().Equal(updatedAt) {
448
448
+
t.Errorf("Expected updated at %v, got %v", updatedAt, book.GetUpdatedAt())
449
449
+
}
450
450
+
})
451
451
+
452
452
+
t.Run("Status Methods", func(t *testing.T) {
453
453
+
testCases := []struct {
454
454
+
status string
455
455
+
isReading bool
456
456
+
isFinished bool
457
457
+
isQueued bool
458
458
+
}{
459
459
+
{"queued", false, false, true},
460
460
+
{"reading", true, false, false},
461
461
+
{"finished", false, true, false},
462
462
+
{"removed", false, false, false},
463
463
+
{"unknown", false, false, false},
464
464
+
}
465
465
+
466
466
+
for _, tc := range testCases {
467
467
+
book := &Book{Status: tc.status}
468
468
+
469
469
+
if book.IsReading() != tc.isReading {
470
470
+
t.Errorf("Status %s: expected IsReading %v, got %v", tc.status, tc.isReading, book.IsReading())
471
471
+
}
472
472
+
if book.IsFinished() != tc.isFinished {
473
473
+
t.Errorf("Status %s: expected IsFinished %v, got %v", tc.status, tc.isFinished, book.IsFinished())
474
474
+
}
475
475
+
if book.IsQueued() != tc.isQueued {
476
476
+
t.Errorf("Status %s: expected IsQueued %v, got %v", tc.status, tc.isQueued, book.IsQueued())
477
477
+
}
478
478
+
}
479
479
+
})
480
480
+
481
481
+
t.Run("Progress Methods", func(t *testing.T) {
482
482
+
book := &Book{Progress: 75}
483
483
+
484
484
+
if book.ProgressPercent() != 75 {
485
485
+
t.Errorf("Expected progress 75%%, got %d%%", book.ProgressPercent())
486
486
+
}
487
487
+
})
488
488
+
489
489
+
t.Run("JSON Marshaling", func(t *testing.T) {
490
490
+
now := time.Now()
491
491
+
started := now.Add(-7 * 24 * time.Hour)
492
492
+
finished := now.Add(-24 * time.Hour)
493
493
+
book := &Book{
494
494
+
ID: 1,
495
495
+
Title: "Test Book",
496
496
+
Author: "Test Author",
497
497
+
Status: "finished",
498
498
+
Progress: 100,
499
499
+
Pages: 300,
500
500
+
Rating: 4.5,
501
501
+
Notes: "Excellent read!",
502
502
+
Added: now,
503
503
+
Started: &started,
504
504
+
Finished: &finished,
505
505
+
}
506
506
+
507
507
+
data, err := json.Marshal(book)
508
508
+
if err != nil {
509
509
+
t.Fatalf("JSON marshal failed: %v", err)
510
510
+
}
511
511
+
512
512
+
var unmarshaled Book
513
513
+
err = json.Unmarshal(data, &unmarshaled)
514
514
+
if err != nil {
515
515
+
t.Fatalf("JSON unmarshal failed: %v", err)
516
516
+
}
517
517
+
518
518
+
if unmarshaled.ID != book.ID {
519
519
+
t.Errorf("Expected ID %d, got %d", book.ID, unmarshaled.ID)
520
520
+
}
521
521
+
if unmarshaled.Title != book.Title {
522
522
+
t.Errorf("Expected title %s, got %s", book.Title, unmarshaled.Title)
523
523
+
}
524
524
+
if unmarshaled.Author != book.Author {
525
525
+
t.Errorf("Expected author %s, got %s", book.Author, unmarshaled.Author)
526
526
+
}
527
527
+
if unmarshaled.Progress != book.Progress {
528
528
+
t.Errorf("Expected progress %d, got %d", book.Progress, unmarshaled.Progress)
529
529
+
}
530
530
+
if unmarshaled.Pages != book.Pages {
531
531
+
t.Errorf("Expected pages %d, got %d", book.Pages, unmarshaled.Pages)
532
532
+
}
533
533
+
})
534
534
+
})
535
535
+
536
536
+
t.Run("Interface Implementations", func(t *testing.T) {
537
537
+
t.Run("All models implement Model interface", func(t *testing.T) {
538
538
+
var models []Model
539
539
+
540
540
+
task := &Task{}
541
541
+
movie := &Movie{}
542
542
+
tvShow := &TVShow{}
543
543
+
book := &Book{}
544
544
+
545
545
+
models = append(models, task, movie, tvShow, book)
546
546
+
547
547
+
if len(models) != 4 {
548
548
+
t.Errorf("Expected 4 models, got %d", len(models))
549
549
+
}
550
550
+
551
551
+
// Test that all models have the required methods
552
552
+
for i, model := range models {
553
553
+
// Test ID methods
554
554
+
model.SetID(int64(i + 1))
555
555
+
if model.GetID() != int64(i+1) {
556
556
+
t.Errorf("Model %d: ID not set correctly", i)
557
557
+
}
558
558
+
559
559
+
// Test table name method
560
560
+
tableName := model.GetTableName()
561
561
+
if tableName == "" {
562
562
+
t.Errorf("Model %d: table name should not be empty", i)
563
563
+
}
564
564
+
565
565
+
// Test timestamp methods
566
566
+
now := time.Now()
567
567
+
model.SetCreatedAt(now)
568
568
+
model.SetUpdatedAt(now)
569
569
+
570
570
+
// Note: We don't test exact equality due to potential precision differences
571
571
+
if model.GetCreatedAt().IsZero() {
572
572
+
t.Errorf("Model %d: created at should not be zero", i)
573
573
+
}
574
574
+
if model.GetUpdatedAt().IsZero() {
575
575
+
t.Errorf("Model %d: updated at should not be zero", i)
576
576
+
}
577
577
+
}
578
578
+
})
579
579
+
})
580
580
+
581
581
+
t.Run("Errors & Edge cases", func(t *testing.T) {
582
582
+
t.Run("Marshaling Errors", func(t *testing.T) {
583
583
+
t.Run("UnmarshalTags handles invalid JSON", func(t *testing.T) {
584
584
+
task := &Task{}
585
585
+
err := task.UnmarshalTags(`{"invalid": "json"}`)
586
586
+
if err == nil {
587
587
+
t.Error("Expected error for invalid JSON, got nil")
588
588
+
}
589
589
+
})
590
590
+
591
591
+
t.Run("UnmarshalAnnotations handles invalid JSON", func(t *testing.T) {
592
592
+
task := &Task{}
593
593
+
err := task.UnmarshalAnnotations(`{"invalid": "json"}`)
594
594
+
if err == nil {
595
595
+
t.Error("Expected error for invalid JSON, got nil")
596
596
+
}
597
597
+
})
598
598
+
})
599
599
+
})
600
600
+
601
601
+
t.Run("Edge Cases", func(t *testing.T) {
602
602
+
t.Run("Task with nil slices", func(t *testing.T) {
603
603
+
task := &Task{
604
604
+
Tags: nil,
605
605
+
Annotations: nil,
606
606
+
}
607
607
+
608
608
+
tagsJSON, err := task.MarshalTags()
609
609
+
if err != nil {
610
610
+
t.Errorf("MarshalTags with nil slice failed: %v", err)
611
611
+
}
612
612
+
if tagsJSON != "" {
613
613
+
t.Errorf("Expected empty string for nil tags, got '%s'", tagsJSON)
614
614
+
}
615
615
+
616
616
+
annotationsJSON, err := task.MarshalAnnotations()
617
617
+
if err != nil {
618
618
+
t.Errorf("MarshalAnnotations with nil slice failed: %v", err)
619
619
+
}
620
620
+
if annotationsJSON != "" {
621
621
+
t.Errorf("Expected empty string for nil annotations, got '%s'", annotationsJSON)
622
622
+
}
623
623
+
})
624
624
+
625
625
+
t.Run("Models with zero values", func(t *testing.T) {
626
626
+
task := &Task{}
627
627
+
movie := &Movie{}
628
628
+
tvShow := &TVShow{}
629
629
+
book := &Book{}
630
630
+
631
631
+
// Test that zero values don't cause panics
632
632
+
if task.IsCompleted() || task.IsPending() || task.IsDeleted() {
633
633
+
t.Error("Zero value task should have false status methods")
634
634
+
}
635
635
+
636
636
+
if movie.IsWatched() || movie.IsQueued() {
637
637
+
t.Error("Zero value movie should have false status methods")
638
638
+
}
639
639
+
640
640
+
if tvShow.IsWatching() || tvShow.IsWatched() || tvShow.IsQueued() {
641
641
+
t.Error("Zero value TV show should have false status methods")
642
642
+
}
643
643
+
644
644
+
if book.IsReading() || book.IsFinished() || book.IsQueued() {
645
645
+
t.Error("Zero value book should have false status methods")
646
646
+
}
647
647
+
648
648
+
if book.ProgressPercent() != 0 {
649
649
+
t.Errorf("Zero value book should have 0%% progress, got %d%%", book.ProgressPercent())
650
650
+
}
651
651
+
})
652
652
+
})
653
653
+
}
+347
internal/store/config_test.go
···
1
1
+
package store
2
2
+
3
3
+
import (
4
4
+
"os"
5
5
+
"path/filepath"
6
6
+
"runtime"
7
7
+
"testing"
8
8
+
)
9
9
+
10
10
+
func TestDefaultConfig(t *testing.T) {
11
11
+
config := DefaultConfig()
12
12
+
13
13
+
if config == nil {
14
14
+
t.Fatal("DefaultConfig should not return nil")
15
15
+
}
16
16
+
17
17
+
expectedDefaults := map[string]interface{}{
18
18
+
"DateFormat": "2006-01-02",
19
19
+
"ColorScheme": "default",
20
20
+
"DefaultView": "list",
21
21
+
"AutoArchive": false,
22
22
+
"SyncEnabled": false,
23
23
+
"ExportFormat": "json",
24
24
+
}
25
25
+
26
26
+
if config.DateFormat != expectedDefaults["DateFormat"] {
27
27
+
t.Errorf("Expected DateFormat %s, got %s", expectedDefaults["DateFormat"], config.DateFormat)
28
28
+
}
29
29
+
if config.ColorScheme != expectedDefaults["ColorScheme"] {
30
30
+
t.Errorf("Expected ColorScheme %s, got %s", expectedDefaults["ColorScheme"], config.ColorScheme)
31
31
+
}
32
32
+
if config.DefaultView != expectedDefaults["DefaultView"] {
33
33
+
t.Errorf("Expected DefaultView %s, got %s", expectedDefaults["DefaultView"], config.DefaultView)
34
34
+
}
35
35
+
if config.AutoArchive != expectedDefaults["AutoArchive"] {
36
36
+
t.Errorf("Expected AutoArchive %v, got %v", expectedDefaults["AutoArchive"], config.AutoArchive)
37
37
+
}
38
38
+
if config.SyncEnabled != expectedDefaults["SyncEnabled"] {
39
39
+
t.Errorf("Expected SyncEnabled %v, got %v", expectedDefaults["SyncEnabled"], config.SyncEnabled)
40
40
+
}
41
41
+
if config.ExportFormat != expectedDefaults["ExportFormat"] {
42
42
+
t.Errorf("Expected ExportFormat %s, got %s", expectedDefaults["ExportFormat"], config.ExportFormat)
43
43
+
}
44
44
+
}
45
45
+
46
46
+
func TestConfigOperations(t *testing.T) {
47
47
+
tempDir, err := os.MkdirTemp("", "noteleaf-config-test-*")
48
48
+
if err != nil {
49
49
+
t.Fatalf("Failed to create temp directory: %v", err)
50
50
+
}
51
51
+
defer os.RemoveAll(tempDir)
52
52
+
53
53
+
originalGetConfigDir := GetConfigDir
54
54
+
GetConfigDir = func() (string, error) {
55
55
+
return tempDir, nil
56
56
+
}
57
57
+
defer func() { GetConfigDir = originalGetConfigDir }()
58
58
+
59
59
+
t.Run("SaveConfig creates config file", func(t *testing.T) {
60
60
+
config := DefaultConfig()
61
61
+
config.ColorScheme = "dark"
62
62
+
config.AutoArchive = true
63
63
+
64
64
+
err := SaveConfig(config)
65
65
+
if err != nil {
66
66
+
t.Fatalf("SaveConfig failed: %v", err)
67
67
+
}
68
68
+
69
69
+
configPath := filepath.Join(tempDir, ".noteleaf.conf.toml")
70
70
+
if _, err := os.Stat(configPath); os.IsNotExist(err) {
71
71
+
t.Error("Config file should exist after SaveConfig")
72
72
+
}
73
73
+
})
74
74
+
75
75
+
t.Run("LoadConfig reads existing config", func(t *testing.T) {
76
76
+
config, err := LoadConfig()
77
77
+
if err != nil {
78
78
+
t.Fatalf("LoadConfig failed: %v", err)
79
79
+
}
80
80
+
81
81
+
if config.ColorScheme != "dark" {
82
82
+
t.Errorf("Expected ColorScheme 'dark', got '%s'", config.ColorScheme)
83
83
+
}
84
84
+
if !config.AutoArchive {
85
85
+
t.Error("Expected AutoArchive to be true")
86
86
+
}
87
87
+
})
88
88
+
89
89
+
t.Run("LoadConfig creates default when file doesn't exist", func(t *testing.T) {
90
90
+
configPath := filepath.Join(tempDir, ".noteleaf.conf.toml")
91
91
+
os.Remove(configPath)
92
92
+
93
93
+
config, err := LoadConfig()
94
94
+
if err != nil {
95
95
+
t.Fatalf("LoadConfig failed: %v", err)
96
96
+
}
97
97
+
98
98
+
if config.ColorScheme != "default" {
99
99
+
t.Errorf("Expected default ColorScheme 'default', got '%s'", config.ColorScheme)
100
100
+
}
101
101
+
if config.AutoArchive {
102
102
+
t.Error("Expected AutoArchive to be false by default")
103
103
+
}
104
104
+
105
105
+
if _, err := os.Stat(configPath); os.IsNotExist(err) {
106
106
+
t.Error("Config file should be created when it doesn't exist")
107
107
+
}
108
108
+
})
109
109
+
110
110
+
t.Run("GetConfigPath returns correct path", func(t *testing.T) {
111
111
+
configPath, err := GetConfigPath()
112
112
+
if err != nil {
113
113
+
t.Fatalf("GetConfigPath failed: %v", err)
114
114
+
}
115
115
+
116
116
+
expectedPath := filepath.Join(tempDir, ".noteleaf.conf.toml")
117
117
+
if configPath != expectedPath {
118
118
+
t.Errorf("Expected config path %s, got %s", expectedPath, configPath)
119
119
+
}
120
120
+
})
121
121
+
}
122
122
+
123
123
+
func TestConfigPersistence(t *testing.T) {
124
124
+
tempDir, err := os.MkdirTemp("", "noteleaf-config-persist-test-*")
125
125
+
if err != nil {
126
126
+
t.Fatalf("Failed to create temp directory: %v", err)
127
127
+
}
128
128
+
defer os.RemoveAll(tempDir)
129
129
+
130
130
+
originalGetConfigDir := GetConfigDir
131
131
+
GetConfigDir = func() (string, error) {
132
132
+
return tempDir, nil
133
133
+
}
134
134
+
defer func() { GetConfigDir = originalGetConfigDir }()
135
135
+
136
136
+
t.Run("config values persist across save/load cycles", func(t *testing.T) {
137
137
+
originalConfig := &Config{
138
138
+
DateFormat: "01/02/2006",
139
139
+
ColorScheme: "custom",
140
140
+
DefaultView: "kanban",
141
141
+
DefaultPriority: "high",
142
142
+
AutoArchive: true,
143
143
+
SyncEnabled: true,
144
144
+
SyncEndpoint: "https://api.example.com",
145
145
+
SyncToken: "secret-token",
146
146
+
ExportFormat: "csv",
147
147
+
MovieAPIKey: "movie-key",
148
148
+
BookAPIKey: "book-key",
149
149
+
}
150
150
+
151
151
+
err := SaveConfig(originalConfig)
152
152
+
if err != nil {
153
153
+
t.Fatalf("SaveConfig failed: %v", err)
154
154
+
}
155
155
+
156
156
+
loadedConfig, err := LoadConfig()
157
157
+
if err != nil {
158
158
+
t.Fatalf("LoadConfig failed: %v", err)
159
159
+
}
160
160
+
161
161
+
if loadedConfig.DateFormat != originalConfig.DateFormat {
162
162
+
t.Errorf("DateFormat not preserved: expected %s, got %s", originalConfig.DateFormat, loadedConfig.DateFormat)
163
163
+
}
164
164
+
if loadedConfig.ColorScheme != originalConfig.ColorScheme {
165
165
+
t.Errorf("ColorScheme not preserved: expected %s, got %s", originalConfig.ColorScheme, loadedConfig.ColorScheme)
166
166
+
}
167
167
+
if loadedConfig.DefaultView != originalConfig.DefaultView {
168
168
+
t.Errorf("DefaultView not preserved: expected %s, got %s", originalConfig.DefaultView, loadedConfig.DefaultView)
169
169
+
}
170
170
+
if loadedConfig.DefaultPriority != originalConfig.DefaultPriority {
171
171
+
t.Errorf("DefaultPriority not preserved: expected %s, got %s", originalConfig.DefaultPriority, loadedConfig.DefaultPriority)
172
172
+
}
173
173
+
if loadedConfig.AutoArchive != originalConfig.AutoArchive {
174
174
+
t.Errorf("AutoArchive not preserved: expected %v, got %v", originalConfig.AutoArchive, loadedConfig.AutoArchive)
175
175
+
}
176
176
+
if loadedConfig.SyncEnabled != originalConfig.SyncEnabled {
177
177
+
t.Errorf("SyncEnabled not preserved: expected %v, got %v", originalConfig.SyncEnabled, loadedConfig.SyncEnabled)
178
178
+
}
179
179
+
if loadedConfig.SyncEndpoint != originalConfig.SyncEndpoint {
180
180
+
t.Errorf("SyncEndpoint not preserved: expected %s, got %s", originalConfig.SyncEndpoint, loadedConfig.SyncEndpoint)
181
181
+
}
182
182
+
if loadedConfig.SyncToken != originalConfig.SyncToken {
183
183
+
t.Errorf("SyncToken not preserved: expected %s, got %s", originalConfig.SyncToken, loadedConfig.SyncToken)
184
184
+
}
185
185
+
if loadedConfig.ExportFormat != originalConfig.ExportFormat {
186
186
+
t.Errorf("ExportFormat not preserved: expected %s, got %s", originalConfig.ExportFormat, loadedConfig.ExportFormat)
187
187
+
}
188
188
+
if loadedConfig.MovieAPIKey != originalConfig.MovieAPIKey {
189
189
+
t.Errorf("MovieAPIKey not preserved: expected %s, got %s", originalConfig.MovieAPIKey, loadedConfig.MovieAPIKey)
190
190
+
}
191
191
+
if loadedConfig.BookAPIKey != originalConfig.BookAPIKey {
192
192
+
t.Errorf("BookAPIKey not preserved: expected %s, got %s", originalConfig.BookAPIKey, loadedConfig.BookAPIKey)
193
193
+
}
194
194
+
})
195
195
+
}
196
196
+
197
197
+
func TestConfigErrorHandling(t *testing.T) {
198
198
+
t.Run("LoadConfig handles invalid TOML", func(t *testing.T) {
199
199
+
tempDir, err := os.MkdirTemp("", "noteleaf-config-error-test-*")
200
200
+
if err != nil {
201
201
+
t.Fatalf("Failed to create temp directory: %v", err)
202
202
+
}
203
203
+
defer os.RemoveAll(tempDir)
204
204
+
205
205
+
originalGetConfigDir := GetConfigDir
206
206
+
GetConfigDir = func() (string, error) {
207
207
+
return tempDir, nil
208
208
+
}
209
209
+
defer func() { GetConfigDir = originalGetConfigDir }()
210
210
+
211
211
+
configPath := filepath.Join(tempDir, ".noteleaf.conf.toml")
212
212
+
invalidTOML := `[invalid toml content`
213
213
+
err = os.WriteFile(configPath, []byte(invalidTOML), 0644)
214
214
+
if err != nil {
215
215
+
t.Fatalf("Failed to write invalid TOML: %v", err)
216
216
+
}
217
217
+
218
218
+
_, err = LoadConfig()
219
219
+
if err == nil {
220
220
+
t.Error("LoadConfig should fail with invalid TOML")
221
221
+
}
222
222
+
})
223
223
+
224
224
+
t.Run("SaveConfig handles directory creation failure", func(t *testing.T) {
225
225
+
originalGetConfigDir := GetConfigDir
226
226
+
GetConfigDir = func() (string, error) {
227
227
+
return "/invalid/path/that/cannot/be/created", nil
228
228
+
}
229
229
+
defer func() { GetConfigDir = originalGetConfigDir }()
230
230
+
231
231
+
config := DefaultConfig()
232
232
+
err := SaveConfig(config)
233
233
+
if err == nil {
234
234
+
t.Error("SaveConfig should fail when config directory cannot be accessed")
235
235
+
}
236
236
+
})
237
237
+
238
238
+
t.Run("GetConfigPath handles GetConfigDir error", func(t *testing.T) {
239
239
+
originalGetConfigDir := GetConfigDir
240
240
+
GetConfigDir = func() (string, error) {
241
241
+
return "", os.ErrPermission
242
242
+
}
243
243
+
defer func() { GetConfigDir = originalGetConfigDir }()
244
244
+
245
245
+
_, err := GetConfigPath()
246
246
+
if err == nil {
247
247
+
t.Error("GetConfigPath should fail when GetConfigDir fails")
248
248
+
}
249
249
+
})
250
250
+
}
251
251
+
252
252
+
func TestGetConfigDir(t *testing.T) {
253
253
+
t.Run("returns correct directory based on OS", func(t *testing.T) {
254
254
+
configDir, err := GetConfigDir()
255
255
+
if err != nil {
256
256
+
t.Fatalf("GetConfigDir failed: %v", err)
257
257
+
}
258
258
+
259
259
+
if configDir == "" {
260
260
+
t.Error("Config directory should not be empty")
261
261
+
}
262
262
+
263
263
+
if _, err := os.Stat(configDir); os.IsNotExist(err) {
264
264
+
t.Error("Config directory should be created if it doesn't exist")
265
265
+
}
266
266
+
267
267
+
if filepath.Base(configDir) != "noteleaf" {
268
268
+
t.Errorf("Config directory should end with 'noteleaf', got: %s", configDir)
269
269
+
}
270
270
+
})
271
271
+
272
272
+
t.Run("creates directory if it doesn't exist", func(t *testing.T) {
273
273
+
tempDir, err := os.MkdirTemp("", "noteleaf-test-*")
274
274
+
if err != nil {
275
275
+
t.Fatalf("Failed to create temp directory: %v", err)
276
276
+
}
277
277
+
defer os.RemoveAll(tempDir)
278
278
+
279
279
+
var originalEnv string
280
280
+
var envVar string
281
281
+
switch runtime.GOOS {
282
282
+
case "windows":
283
283
+
envVar = "APPDATA"
284
284
+
originalEnv = os.Getenv("APPDATA")
285
285
+
os.Setenv("APPDATA", tempDir)
286
286
+
default:
287
287
+
envVar = "XDG_CONFIG_HOME"
288
288
+
originalEnv = os.Getenv("XDG_CONFIG_HOME")
289
289
+
os.Setenv("XDG_CONFIG_HOME", tempDir)
290
290
+
}
291
291
+
defer os.Setenv(envVar, originalEnv)
292
292
+
293
293
+
configDir, err := GetConfigDir()
294
294
+
if err != nil {
295
295
+
t.Fatalf("GetConfigDir failed: %v", err)
296
296
+
}
297
297
+
298
298
+
expectedPath := filepath.Join(tempDir, "noteleaf")
299
299
+
if configDir != expectedPath {
300
300
+
t.Errorf("Expected config dir %s, got %s", expectedPath, configDir)
301
301
+
}
302
302
+
303
303
+
if _, err := os.Stat(configDir); os.IsNotExist(err) {
304
304
+
t.Error("Config directory should be created")
305
305
+
}
306
306
+
})
307
307
+
308
308
+
t.Run("handles missing environment variables", func(t *testing.T) {
309
309
+
switch runtime.GOOS {
310
310
+
case "windows":
311
311
+
originalAppData := os.Getenv("APPDATA")
312
312
+
os.Unsetenv("APPDATA")
313
313
+
defer os.Setenv("APPDATA", originalAppData)
314
314
+
315
315
+
_, err := GetConfigDir()
316
316
+
if err == nil {
317
317
+
t.Error("GetConfigDir should fail when APPDATA is not set on Windows")
318
318
+
}
319
319
+
default:
320
320
+
originalXDG := os.Getenv("XDG_CONFIG_HOME")
321
321
+
originalHome := os.Getenv("HOME")
322
322
+
os.Unsetenv("XDG_CONFIG_HOME")
323
323
+
324
324
+
tempHome, err := os.MkdirTemp("", "noteleaf-home-test-*")
325
325
+
if err != nil {
326
326
+
t.Fatalf("Failed to create temp home: %v", err)
327
327
+
}
328
328
+
defer os.RemoveAll(tempHome)
329
329
+
os.Setenv("HOME", tempHome)
330
330
+
331
331
+
defer func() {
332
332
+
os.Setenv("XDG_CONFIG_HOME", originalXDG)
333
333
+
os.Setenv("HOME", originalHome)
334
334
+
}()
335
335
+
336
336
+
configDir, err := GetConfigDir()
337
337
+
if err != nil {
338
338
+
t.Fatalf("GetConfigDir should work with HOME fallback: %v", err)
339
339
+
}
340
340
+
341
341
+
expectedPath := filepath.Join(tempHome, ".config", "noteleaf")
342
342
+
if configDir != expectedPath {
343
343
+
t.Errorf("Expected config dir %s, got %s", expectedPath, configDir)
344
344
+
}
345
345
+
}
346
346
+
})
347
347
+
}
+1
-1
internal/store/database.go
···
21
21
}
22
22
23
23
// GetConfigDir returns the appropriate configuration directory based on the OS
24
24
-
func GetConfigDir() (string, error) {
24
24
+
var GetConfigDir = func() (string, error) {
25
25
var configDir string
26
26
27
27
switch runtime.GOOS {
+231
internal/store/database_test.go
···
1
1
+
package store
2
2
+
3
3
+
import (
4
4
+
"os"
5
5
+
"path/filepath"
6
6
+
"testing"
7
7
+
)
8
8
+
9
9
+
func TestNewDatabase(t *testing.T) {
10
10
+
tempDir, err := os.MkdirTemp("", "noteleaf-db-test-*")
11
11
+
if err != nil {
12
12
+
t.Fatalf("Failed to create temp directory: %v", err)
13
13
+
}
14
14
+
defer os.RemoveAll(tempDir)
15
15
+
16
16
+
originalGetConfigDir := GetConfigDir
17
17
+
GetConfigDir = func() (string, error) {
18
18
+
return tempDir, nil
19
19
+
}
20
20
+
defer func() { GetConfigDir = originalGetConfigDir }()
21
21
+
22
22
+
t.Run("creates database successfully", func(t *testing.T) {
23
23
+
db, err := NewDatabase()
24
24
+
if err != nil {
25
25
+
t.Fatalf("NewDatabase failed: %v", err)
26
26
+
}
27
27
+
defer db.Close()
28
28
+
29
29
+
if db == nil {
30
30
+
t.Fatal("Database should not be nil")
31
31
+
}
32
32
+
33
33
+
dbPath := filepath.Join(tempDir, "noteleaf.db")
34
34
+
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
35
35
+
t.Error("Database file should exist")
36
36
+
}
37
37
+
38
38
+
if db.GetPath() != dbPath {
39
39
+
t.Errorf("Expected database path %s, got %s", dbPath, db.GetPath())
40
40
+
}
41
41
+
})
42
42
+
43
43
+
t.Run("enables foreign keys", func(t *testing.T) {
44
44
+
db, err := NewDatabase()
45
45
+
if err != nil {
46
46
+
t.Fatalf("NewDatabase failed: %v", err)
47
47
+
}
48
48
+
defer db.Close()
49
49
+
50
50
+
var foreignKeys int
51
51
+
err = db.QueryRow("PRAGMA foreign_keys").Scan(&foreignKeys)
52
52
+
if err != nil {
53
53
+
t.Fatalf("Failed to check foreign keys: %v", err)
54
54
+
}
55
55
+
56
56
+
if foreignKeys != 1 {
57
57
+
t.Error("Foreign keys should be enabled")
58
58
+
}
59
59
+
})
60
60
+
61
61
+
t.Run("enables WAL mode", func(t *testing.T) {
62
62
+
db, err := NewDatabase()
63
63
+
if err != nil {
64
64
+
t.Fatalf("NewDatabase failed: %v", err)
65
65
+
}
66
66
+
defer db.Close()
67
67
+
68
68
+
var journalMode string
69
69
+
err = db.QueryRow("PRAGMA journal_mode").Scan(&journalMode)
70
70
+
if err != nil {
71
71
+
t.Fatalf("Failed to check journal mode: %v", err)
72
72
+
}
73
73
+
74
74
+
if journalMode != "wal" {
75
75
+
t.Errorf("Expected WAL journal mode, got %s", journalMode)
76
76
+
}
77
77
+
})
78
78
+
79
79
+
t.Run("runs migrations", func(t *testing.T) {
80
80
+
db, err := NewDatabase()
81
81
+
if err != nil {
82
82
+
t.Fatalf("NewDatabase failed: %v", err)
83
83
+
}
84
84
+
defer db.Close()
85
85
+
86
86
+
var count int
87
87
+
err = db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='migrations'").Scan(&count)
88
88
+
if err != nil {
89
89
+
t.Fatalf("Failed to check migrations table: %v", err)
90
90
+
}
91
91
+
92
92
+
if count != 1 {
93
93
+
t.Error("Migrations table should exist")
94
94
+
}
95
95
+
96
96
+
var migrationCount int
97
97
+
err = db.QueryRow("SELECT COUNT(*) FROM migrations").Scan(&migrationCount)
98
98
+
if err != nil {
99
99
+
t.Fatalf("Failed to count migrations: %v", err)
100
100
+
}
101
101
+
102
102
+
if migrationCount == 0 {
103
103
+
t.Error("At least one migration should be applied")
104
104
+
}
105
105
+
})
106
106
+
107
107
+
t.Run("creates migration runner", func(t *testing.T) {
108
108
+
db, err := NewDatabase()
109
109
+
if err != nil {
110
110
+
t.Fatalf("NewDatabase failed: %v", err)
111
111
+
}
112
112
+
defer db.Close()
113
113
+
114
114
+
runner := db.NewMigrationRunner()
115
115
+
if runner == nil {
116
116
+
t.Error("Migration runner should not be nil")
117
117
+
}
118
118
+
})
119
119
+
120
120
+
t.Run("closes database connection", func(t *testing.T) {
121
121
+
db, err := NewDatabase()
122
122
+
if err != nil {
123
123
+
t.Fatalf("NewDatabase failed: %v", err)
124
124
+
}
125
125
+
126
126
+
err = db.Close()
127
127
+
if err != nil {
128
128
+
t.Errorf("Close should not return error: %v", err)
129
129
+
}
130
130
+
131
131
+
err = db.Ping()
132
132
+
if err == nil {
133
133
+
t.Error("Database should be closed and ping should fail")
134
134
+
}
135
135
+
})
136
136
+
}
137
137
+
138
138
+
func TestDatabaseErrorHandling(t *testing.T) {
139
139
+
t.Run("handles GetConfigDir error", func(t *testing.T) {
140
140
+
originalGetConfigDir := GetConfigDir
141
141
+
GetConfigDir = func() (string, error) {
142
142
+
return "", os.ErrPermission
143
143
+
}
144
144
+
defer func() { GetConfigDir = originalGetConfigDir }()
145
145
+
146
146
+
_, err := NewDatabase()
147
147
+
if err == nil {
148
148
+
t.Error("NewDatabase should fail when GetConfigDir fails")
149
149
+
}
150
150
+
})
151
151
+
152
152
+
t.Run("handles invalid database path", func(t *testing.T) {
153
153
+
originalGetConfigDir := GetConfigDir
154
154
+
GetConfigDir = func() (string, error) {
155
155
+
return "/invalid/path/that/does/not/exist", nil
156
156
+
}
157
157
+
defer func() { GetConfigDir = originalGetConfigDir }()
158
158
+
159
159
+
_, err := NewDatabase()
160
160
+
if err == nil {
161
161
+
t.Error("NewDatabase should fail with invalid database path")
162
162
+
}
163
163
+
})
164
164
+
}
165
165
+
166
166
+
func TestDatabaseIntegration(t *testing.T) {
167
167
+
tempDir, err := os.MkdirTemp("", "noteleaf-db-integration-test-*")
168
168
+
if err != nil {
169
169
+
t.Fatalf("Failed to create temp directory: %v", err)
170
170
+
}
171
171
+
defer os.RemoveAll(tempDir)
172
172
+
173
173
+
originalGetConfigDir := GetConfigDir
174
174
+
GetConfigDir = func() (string, error) {
175
175
+
return tempDir, nil
176
176
+
}
177
177
+
defer func() { GetConfigDir = originalGetConfigDir }()
178
178
+
179
179
+
t.Run("multiple database instances use same file", func(t *testing.T) {
180
180
+
db1, err := NewDatabase()
181
181
+
if err != nil {
182
182
+
t.Fatalf("First NewDatabase failed: %v", err)
183
183
+
}
184
184
+
defer db1.Close()
185
185
+
186
186
+
db2, err := NewDatabase()
187
187
+
if err != nil {
188
188
+
t.Fatalf("Second NewDatabase failed: %v", err)
189
189
+
}
190
190
+
defer db2.Close()
191
191
+
192
192
+
if db1.GetPath() != db2.GetPath() {
193
193
+
t.Error("Both database instances should use the same file path")
194
194
+
}
195
195
+
})
196
196
+
197
197
+
t.Run("database survives connection close and reopen", func(t *testing.T) {
198
198
+
db1, err := NewDatabase()
199
199
+
if err != nil {
200
200
+
t.Fatalf("NewDatabase failed: %v", err)
201
201
+
}
202
202
+
203
203
+
_, err = db1.Exec("CREATE TABLE IF NOT EXISTS test_table (id INTEGER PRIMARY KEY, name TEXT)")
204
204
+
if err != nil {
205
205
+
t.Fatalf("Failed to create test table: %v", err)
206
206
+
}
207
207
+
208
208
+
_, err = db1.Exec("INSERT INTO test_table (name) VALUES (?)", "test_value")
209
209
+
if err != nil {
210
210
+
t.Fatalf("Failed to insert test data: %v", err)
211
211
+
}
212
212
+
213
213
+
db1.Close()
214
214
+
215
215
+
db2, err := NewDatabase()
216
216
+
if err != nil {
217
217
+
t.Fatalf("Second NewDatabase failed: %v", err)
218
218
+
}
219
219
+
defer db2.Close()
220
220
+
221
221
+
var name string
222
222
+
err = db2.QueryRow("SELECT name FROM test_table WHERE id = 1").Scan(&name)
223
223
+
if err != nil {
224
224
+
t.Fatalf("Failed to query test data: %v", err)
225
225
+
}
226
226
+
227
227
+
if name != "test_value" {
228
228
+
t.Errorf("Expected 'test_value', got '%s'", name)
229
229
+
}
230
230
+
})
231
231
+
}
+393
internal/store/migration_test.go
···
1
1
+
package store
2
2
+
3
3
+
import (
4
4
+
"database/sql"
5
5
+
"embed"
6
6
+
"testing"
7
7
+
8
8
+
_ "github.com/mattn/go-sqlite3"
9
9
+
)
10
10
+
11
11
+
//go:embed sql/migrations
12
12
+
var testMigrationFiles embed.FS
13
13
+
14
14
+
func createTestDB(t *testing.T) *sql.DB {
15
15
+
db, err := sql.Open("sqlite3", ":memory:")
16
16
+
if err != nil {
17
17
+
t.Fatalf("Failed to create in-memory database: %v", err)
18
18
+
}
19
19
+
20
20
+
if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
21
21
+
t.Fatalf("Failed to enable foreign keys: %v", err)
22
22
+
}
23
23
+
24
24
+
t.Cleanup(func() {
25
25
+
db.Close()
26
26
+
})
27
27
+
28
28
+
return db
29
29
+
}
30
30
+
31
31
+
func TestNewMigrationRunner(t *testing.T) {
32
32
+
db := createTestDB(t)
33
33
+
34
34
+
runner := NewMigrationRunner(db, testMigrationFiles)
35
35
+
if runner == nil {
36
36
+
t.Fatal("NewMigrationRunner should not return nil")
37
37
+
}
38
38
+
39
39
+
if runner.db != db {
40
40
+
t.Error("Migration runner should store the database reference")
41
41
+
}
42
42
+
}
43
43
+
44
44
+
func TestMigrationRunner_RunMigrations(t *testing.T) {
45
45
+
t.Run("runs migrations successfully", func(t *testing.T) {
46
46
+
db := createTestDB(t)
47
47
+
runner := NewMigrationRunner(db, testMigrationFiles)
48
48
+
49
49
+
err := runner.RunMigrations()
50
50
+
if err != nil {
51
51
+
t.Fatalf("RunMigrations failed: %v", err)
52
52
+
}
53
53
+
54
54
+
var count int
55
55
+
err = db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='migrations'").Scan(&count)
56
56
+
if err != nil {
57
57
+
t.Fatalf("Failed to check migrations table: %v", err)
58
58
+
}
59
59
+
60
60
+
if count != 1 {
61
61
+
t.Error("Migrations table should exist after running migrations")
62
62
+
}
63
63
+
64
64
+
var migrationCount int
65
65
+
err = db.QueryRow("SELECT COUNT(*) FROM migrations").Scan(&migrationCount)
66
66
+
if err != nil {
67
67
+
t.Fatalf("Failed to count applied migrations: %v", err)
68
68
+
}
69
69
+
70
70
+
if migrationCount == 0 {
71
71
+
t.Error("At least one migration should be applied")
72
72
+
}
73
73
+
})
74
74
+
75
75
+
t.Run("skips already applied migrations", func(t *testing.T) {
76
76
+
db := createTestDB(t)
77
77
+
runner := NewMigrationRunner(db, testMigrationFiles)
78
78
+
79
79
+
err := runner.RunMigrations()
80
80
+
if err != nil {
81
81
+
t.Fatalf("First RunMigrations failed: %v", err)
82
82
+
}
83
83
+
84
84
+
var initialCount int
85
85
+
err = db.QueryRow("SELECT COUNT(*) FROM migrations").Scan(&initialCount)
86
86
+
if err != nil {
87
87
+
t.Fatalf("Failed to count migrations: %v", err)
88
88
+
}
89
89
+
90
90
+
err = runner.RunMigrations()
91
91
+
if err != nil {
92
92
+
t.Fatalf("Second RunMigrations failed: %v", err)
93
93
+
}
94
94
+
95
95
+
var finalCount int
96
96
+
err = db.QueryRow("SELECT COUNT(*) FROM migrations").Scan(&finalCount)
97
97
+
if err != nil {
98
98
+
t.Fatalf("Failed to count migrations after second run: %v", err)
99
99
+
}
100
100
+
101
101
+
if finalCount != initialCount {
102
102
+
t.Errorf("Expected %d migrations, got %d (migrations should not be re-applied)", initialCount, finalCount)
103
103
+
}
104
104
+
})
105
105
+
106
106
+
t.Run("creates expected tables", func(t *testing.T) {
107
107
+
db := createTestDB(t)
108
108
+
runner := NewMigrationRunner(db, testMigrationFiles)
109
109
+
110
110
+
err := runner.RunMigrations()
111
111
+
if err != nil {
112
112
+
t.Fatalf("RunMigrations failed: %v", err)
113
113
+
}
114
114
+
115
115
+
expectedTables := []string{"migrations", "tasks", "movies", "tv_shows", "books"}
116
116
+
117
117
+
for _, tableName := range expectedTables {
118
118
+
var count int
119
119
+
err = db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?", tableName).Scan(&count)
120
120
+
if err != nil {
121
121
+
t.Fatalf("Failed to check table %s: %v", tableName, err)
122
122
+
}
123
123
+
124
124
+
if count != 1 {
125
125
+
t.Errorf("Table %s should exist after migrations", tableName)
126
126
+
}
127
127
+
}
128
128
+
})
129
129
+
}
130
130
+
131
131
+
func TestMigrationRunner_GetAppliedMigrations(t *testing.T) {
132
132
+
t.Run("returns empty list when no migrations table", func(t *testing.T) {
133
133
+
db := createTestDB(t)
134
134
+
runner := NewMigrationRunner(db, testMigrationFiles)
135
135
+
136
136
+
migrations, err := runner.GetAppliedMigrations()
137
137
+
if err != nil {
138
138
+
t.Fatalf("GetAppliedMigrations failed: %v", err)
139
139
+
}
140
140
+
141
141
+
if len(migrations) != 0 {
142
142
+
t.Errorf("Expected 0 migrations, got %d", len(migrations))
143
143
+
}
144
144
+
})
145
145
+
146
146
+
t.Run("returns applied migrations", func(t *testing.T) {
147
147
+
db := createTestDB(t)
148
148
+
runner := NewMigrationRunner(db, testMigrationFiles)
149
149
+
150
150
+
// Run migrations first
151
151
+
err := runner.RunMigrations()
152
152
+
if err != nil {
153
153
+
t.Fatalf("RunMigrations failed: %v", err)
154
154
+
}
155
155
+
156
156
+
migrations, err := runner.GetAppliedMigrations()
157
157
+
if err != nil {
158
158
+
t.Fatalf("GetAppliedMigrations failed: %v", err)
159
159
+
}
160
160
+
161
161
+
if len(migrations) == 0 {
162
162
+
t.Error("Should have applied migrations")
163
163
+
}
164
164
+
165
165
+
for _, migration := range migrations {
166
166
+
if migration.Version == "" {
167
167
+
t.Error("Migration version should not be empty")
168
168
+
}
169
169
+
if !migration.Applied {
170
170
+
t.Error("Migration should be marked as applied")
171
171
+
}
172
172
+
if migration.AppliedAt == "" {
173
173
+
t.Error("Migration should have applied timestamp")
174
174
+
}
175
175
+
}
176
176
+
177
177
+
for i := 1; i < len(migrations); i++ {
178
178
+
if migrations[i-1].Version > migrations[i].Version {
179
179
+
t.Error("Migrations should be sorted by version")
180
180
+
}
181
181
+
}
182
182
+
})
183
183
+
}
184
184
+
185
185
+
func TestMigrationRunner_GetAvailableMigrations(t *testing.T) {
186
186
+
t.Run("returns available migrations from embedded files", func(t *testing.T) {
187
187
+
db := createTestDB(t)
188
188
+
runner := NewMigrationRunner(db, testMigrationFiles)
189
189
+
190
190
+
migrations, err := runner.GetAvailableMigrations()
191
191
+
if err != nil {
192
192
+
t.Fatalf("GetAvailableMigrations failed: %v", err)
193
193
+
}
194
194
+
195
195
+
if len(migrations) == 0 {
196
196
+
t.Error("Should have available migrations")
197
197
+
}
198
198
+
199
199
+
for _, migration := range migrations {
200
200
+
if migration.Version == "" {
201
201
+
t.Error("Migration version should not be empty")
202
202
+
}
203
203
+
if migration.UpSQL == "" {
204
204
+
t.Error("Migration should have up SQL")
205
205
+
}
206
206
+
// Note: Down SQL might be empty for some migrations, so we don't check it
207
207
+
}
208
208
+
209
209
+
for i := 1; i < len(migrations); i++ {
210
210
+
if migrations[i-1].Version > migrations[i].Version {
211
211
+
t.Error("Migrations should be sorted by version")
212
212
+
}
213
213
+
}
214
214
+
})
215
215
+
216
216
+
t.Run("includes both up and down SQL when available", func(t *testing.T) {
217
217
+
db := createTestDB(t)
218
218
+
runner := NewMigrationRunner(db, testMigrationFiles)
219
219
+
220
220
+
migrations, err := runner.GetAvailableMigrations()
221
221
+
if err != nil {
222
222
+
t.Fatalf("GetAvailableMigrations failed: %v", err)
223
223
+
}
224
224
+
225
225
+
var foundMigrationWithDown bool
226
226
+
for _, migration := range migrations {
227
227
+
if migration.UpSQL != "" && migration.DownSQL != "" {
228
228
+
foundMigrationWithDown = true
229
229
+
break
230
230
+
}
231
231
+
}
232
232
+
233
233
+
if !foundMigrationWithDown {
234
234
+
t.Log("Note: No migrations found with both up and down SQL - this may be expected")
235
235
+
}
236
236
+
})
237
237
+
}
238
238
+
239
239
+
func TestMigrationRunner_Rollback(t *testing.T) {
240
240
+
t.Run("fails when no migrations to rollback", func(t *testing.T) {
241
241
+
db := createTestDB(t)
242
242
+
runner := NewMigrationRunner(db, testMigrationFiles)
243
243
+
244
244
+
err := runner.Rollback()
245
245
+
if err == nil {
246
246
+
t.Error("Rollback should fail when no migrations are applied")
247
247
+
}
248
248
+
})
249
249
+
250
250
+
t.Run("rolls back last migration", func(t *testing.T) {
251
251
+
db := createTestDB(t)
252
252
+
runner := NewMigrationRunner(db, testMigrationFiles)
253
253
+
254
254
+
err := runner.RunMigrations()
255
255
+
if err != nil {
256
256
+
t.Fatalf("RunMigrations failed: %v", err)
257
257
+
}
258
258
+
259
259
+
var initialCount int
260
260
+
err = db.QueryRow("SELECT COUNT(*) FROM migrations").Scan(&initialCount)
261
261
+
if err != nil {
262
262
+
t.Fatalf("Failed to count migrations: %v", err)
263
263
+
}
264
264
+
265
265
+
if initialCount == 0 {
266
266
+
t.Skip("No migrations to rollback")
267
267
+
}
268
268
+
269
269
+
err = runner.Rollback()
270
270
+
if err != nil {
271
271
+
t.Fatalf("Rollback failed: %v", err)
272
272
+
}
273
273
+
274
274
+
var finalCount int
275
275
+
err = db.QueryRow("SELECT COUNT(*) FROM migrations").Scan(&finalCount)
276
276
+
if err != nil {
277
277
+
t.Fatalf("Failed to count migrations after rollback: %v", err)
278
278
+
}
279
279
+
280
280
+
if finalCount != initialCount-1 {
281
281
+
t.Errorf("Expected %d migrations after rollback, got %d", initialCount-1, finalCount)
282
282
+
}
283
283
+
})
284
284
+
}
285
285
+
286
286
+
func TestMigrationHelperFunctions(t *testing.T) {
287
287
+
t.Run("extractVersionFromFilename", func(t *testing.T) {
288
288
+
testCases := []struct {
289
289
+
filename string
290
290
+
expected string
291
291
+
}{
292
292
+
{"0000_create_migrations_table_up.sql", "0000"},
293
293
+
{"0001_create_all_tables_up.sql", "0001"},
294
294
+
{"0002_add_indexes_down.sql", "0002"},
295
295
+
{"invalid_filename.sql", "invalid"},
296
296
+
{"", ""},
297
297
+
}
298
298
+
299
299
+
for _, tc := range testCases {
300
300
+
result := extractVersionFromFilename(tc.filename)
301
301
+
if result != tc.expected {
302
302
+
t.Errorf("extractVersionFromFilename(%s): expected %s, got %s", tc.filename, tc.expected, result)
303
303
+
}
304
304
+
}
305
305
+
})
306
306
+
307
307
+
t.Run("extractNameFromFilename", func(t *testing.T) {
308
308
+
testCases := []struct {
309
309
+
filename string
310
310
+
expected string
311
311
+
}{
312
312
+
{"0000_create_migrations_table_up.sql", "create_migrations_table"},
313
313
+
{"0001_create_all_tables_up.sql", "create_all_tables"},
314
314
+
{"0002_add_indexes_down.sql", "add_indexes"},
315
315
+
{"invalid_filename.sql", ""},
316
316
+
{"0003_up.sql", ""},
317
317
+
{"", ""},
318
318
+
}
319
319
+
320
320
+
for _, tc := range testCases {
321
321
+
result := extractNameFromFilename(tc.filename)
322
322
+
if result != tc.expected {
323
323
+
t.Errorf("extractNameFromFilename(%s): expected %s, got %s", tc.filename, tc.expected, result)
324
324
+
}
325
325
+
}
326
326
+
})
327
327
+
}
328
328
+
329
329
+
func TestMigrationIntegration(t *testing.T) {
330
330
+
t.Run("full migration lifecycle", func(t *testing.T) {
331
331
+
db := createTestDB(t)
332
332
+
runner := NewMigrationRunner(db, testMigrationFiles)
333
333
+
334
334
+
available, err := runner.GetAvailableMigrations()
335
335
+
if err != nil {
336
336
+
t.Fatalf("GetAvailableMigrations failed: %v", err)
337
337
+
}
338
338
+
339
339
+
if len(available) == 0 {
340
340
+
t.Skip("No migrations available for testing")
341
341
+
}
342
342
+
343
343
+
err = runner.RunMigrations()
344
344
+
if err != nil {
345
345
+
t.Fatalf("RunMigrations failed: %v", err)
346
346
+
}
347
347
+
348
348
+
applied, err := runner.GetAppliedMigrations()
349
349
+
if err != nil {
350
350
+
t.Fatalf("GetAppliedMigrations failed: %v", err)
351
351
+
}
352
352
+
353
353
+
if len(applied) == 0 {
354
354
+
t.Error("No migrations were applied")
355
355
+
}
356
356
+
357
357
+
tables := []string{"tasks", "movies", "tv_shows", "books"}
358
358
+
for _, table := range tables {
359
359
+
var count int
360
360
+
err = db.QueryRow("SELECT COUNT(*) FROM " + table).Scan(&count)
361
361
+
if err != nil {
362
362
+
t.Errorf("Failed to query table %s: %v", table, err)
363
363
+
}
364
364
+
}
365
365
+
366
366
+
if len(applied) > 1 { // Only test rollback if we have more than one migration
367
367
+
err = runner.Rollback()
368
368
+
if err != nil {
369
369
+
t.Logf("Rollback failed (may be expected): %v", err)
370
370
+
}
371
371
+
}
372
372
+
})
373
373
+
374
374
+
t.Run("migration runner works with real database", func(t *testing.T) {
375
375
+
db := createTestDB(t)
376
376
+
runner := NewMigrationRunner(db, migrationFiles)
377
377
+
378
378
+
err := runner.RunMigrations()
379
379
+
if err != nil {
380
380
+
t.Fatalf("RunMigrations with real files failed: %v", err)
381
381
+
}
382
382
+
383
383
+
var count int
384
384
+
err = db.QueryRow("SELECT COUNT(*) FROM migrations").Scan(&count)
385
385
+
if err != nil {
386
386
+
t.Fatalf("Failed to count real migrations: %v", err)
387
387
+
}
388
388
+
389
389
+
if count == 0 {
390
390
+
t.Error("Real migrations should be applied")
391
391
+
}
392
392
+
})
393
393
+
}