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
feat: book handlers
desertthunder.dev
5 months ago
ec2cb6a6
cc80a0f9
+2480
-1276
7 changed files
expand all
collapse all
unified
split
cmd
cli
commands.go
handlers
books.go
books_test.go
notes_test.go
tasks_test.go
internal
ui
book_list.go
book_list_test.go
+67
-12
cmd/cli/commands.go
···
188
188
Short: "Manage reading list",
189
189
}
190
190
191
191
+
// book add - Search and add book to reading list
192
192
+
addCmd := &cobra.Command{
193
193
+
Use: "add [search query...]",
194
194
+
Short: "Search and add book to reading list",
195
195
+
Long: `Search for books and add them to your reading list.
196
196
+
197
197
+
By default, shows search results in a simple list format where you can select by number.
198
198
+
Use the -i flag for an interactive interface with navigation keys.`,
199
199
+
RunE: func(cmd *cobra.Command, args []string) error {
200
200
+
interactive, _ := cmd.Flags().GetBool("interactive")
201
201
+
return handlers.SearchAndAddWithOptions(cmd.Context(), args, interactive)
202
202
+
},
203
203
+
}
204
204
+
addCmd.Flags().BoolP("interactive", "i", false, "Use interactive interface for book selection")
205
205
+
root.AddCommand(addCmd)
206
206
+
207
207
+
// book list - Show reading queue with progress
191
208
root.AddCommand(&cobra.Command{
192
192
-
Use: "add [title]",
193
193
-
Short: "Add book to reading list",
194
194
-
Args: cobra.MinimumNArgs(1),
209
209
+
Use: "list [--all|--reading|--finished|--queued]",
210
210
+
Short: "Show reading queue with progress",
211
211
+
RunE: func(cmd *cobra.Command, args []string) error {
212
212
+
return handlers.ListBooks(cmd.Context(), args)
213
213
+
},
214
214
+
})
215
215
+
216
216
+
// book reading - Mark book as currently reading (alias for update status)
217
217
+
root.AddCommand(&cobra.Command{
218
218
+
Use: "reading <id>",
219
219
+
Short: "Mark book as currently reading",
220
220
+
Args: cobra.ExactArgs(1),
221
221
+
RunE: func(cmd *cobra.Command, args []string) error {
222
222
+
return handlers.UpdateBookStatus(cmd.Context(), []string{args[0], "reading"})
223
223
+
},
224
224
+
})
225
225
+
226
226
+
// book finished - Mark book as completed
227
227
+
root.AddCommand(&cobra.Command{
228
228
+
Use: "finished <id>",
229
229
+
Short: "Mark book as completed",
230
230
+
Aliases: []string{"read"},
231
231
+
Args: cobra.ExactArgs(1),
232
232
+
RunE: func(cmd *cobra.Command, args []string) error {
233
233
+
return handlers.UpdateBookStatus(cmd.Context(), []string{args[0], "finished"})
234
234
+
},
235
235
+
})
236
236
+
237
237
+
// book remove - Remove from reading list
238
238
+
root.AddCommand(&cobra.Command{
239
239
+
Use: "remove <id>",
240
240
+
Short: "Remove from reading list",
241
241
+
Aliases: []string{"rm"},
242
242
+
Args: cobra.ExactArgs(1),
243
243
+
RunE: func(cmd *cobra.Command, args []string) error {
244
244
+
return handlers.UpdateBookStatus(cmd.Context(), []string{args[0], "removed"})
245
245
+
},
246
246
+
})
247
247
+
248
248
+
// book progress - Update reading progress percentage
249
249
+
root.AddCommand(&cobra.Command{
250
250
+
Use: "progress <id> <percentage>",
251
251
+
Short: "Update reading progress percentage (0-100)",
252
252
+
Args: cobra.ExactArgs(2),
195
253
RunE: func(cmd *cobra.Command, args []string) error {
196
196
-
title := args[0]
197
197
-
fmt.Printf("Adding book: %s\n", title)
198
198
-
// TODO: Implement book addition
199
199
-
return nil
254
254
+
return handlers.UpdateBookProgress(cmd.Context(), args)
200
255
},
201
256
})
202
257
258
258
+
// book update - Update book status
203
259
root.AddCommand(&cobra.Command{
204
204
-
Use: "list",
205
205
-
Short: "List books in reading list",
260
260
+
Use: "update <id> <status>",
261
261
+
Short: "Update book status (queued|reading|finished|removed)",
262
262
+
Args: cobra.ExactArgs(2),
206
263
RunE: func(cmd *cobra.Command, args []string) error {
207
207
-
fmt.Println("Listing books...")
208
208
-
// TODO: Implement book listing
209
209
-
return nil
264
264
+
return handlers.UpdateBookStatus(cmd.Context(), args)
210
265
},
211
266
})
212
267
+407
cmd/handlers/books.go
···
1
1
+
package handlers
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"fmt"
6
6
+
"os"
7
7
+
"slices"
8
8
+
"time"
9
9
+
10
10
+
"github.com/stormlightlabs/noteleaf/internal/models"
11
11
+
"github.com/stormlightlabs/noteleaf/internal/repo"
12
12
+
"github.com/stormlightlabs/noteleaf/internal/services"
13
13
+
"github.com/stormlightlabs/noteleaf/internal/store"
14
14
+
"github.com/stormlightlabs/noteleaf/internal/ui"
15
15
+
)
16
16
+
17
17
+
// BookHandler handles all book-related commands
18
18
+
type BookHandler struct {
19
19
+
db *store.Database
20
20
+
config *store.Config
21
21
+
repos *repo.Repositories
22
22
+
service *services.BookService
23
23
+
}
24
24
+
25
25
+
// NewBookHandler creates a new book handler
26
26
+
func NewBookHandler() (*BookHandler, error) {
27
27
+
db, err := store.NewDatabase()
28
28
+
if err != nil {
29
29
+
return nil, fmt.Errorf("failed to initialize database: %w", err)
30
30
+
}
31
31
+
32
32
+
config, err := store.LoadConfig()
33
33
+
if err != nil {
34
34
+
return nil, fmt.Errorf("failed to load configuration: %w", err)
35
35
+
}
36
36
+
37
37
+
repos := repo.NewRepositories(db.DB)
38
38
+
service := services.NewBookService()
39
39
+
40
40
+
return &BookHandler{
41
41
+
db: db,
42
42
+
config: config,
43
43
+
repos: repos,
44
44
+
service: service,
45
45
+
}, nil
46
46
+
}
47
47
+
48
48
+
// Close cleans up resources
49
49
+
func (h *BookHandler) Close() error {
50
50
+
if err := h.service.Close(); err != nil {
51
51
+
return fmt.Errorf("failed to close service: %w", err)
52
52
+
}
53
53
+
return h.db.Close()
54
54
+
}
55
55
+
56
56
+
// SearchAndAdd searches for books and allows user to select and add to queue
57
57
+
func SearchAndAdd(ctx context.Context, args []string) error {
58
58
+
handler, err := NewBookHandler()
59
59
+
if err != nil {
60
60
+
return fmt.Errorf("failed to initialize book handler: %w", err)
61
61
+
}
62
62
+
defer handler.Close()
63
63
+
64
64
+
return handler.searchAndAdd(ctx, args)
65
65
+
}
66
66
+
67
67
+
// SearchAndAddWithOptions searches for books with interactive option
68
68
+
func SearchAndAddWithOptions(ctx context.Context, args []string, interactive bool) error {
69
69
+
handler, err := NewBookHandler()
70
70
+
if err != nil {
71
71
+
return fmt.Errorf("failed to initialize book handler: %w", err)
72
72
+
}
73
73
+
defer handler.Close()
74
74
+
75
75
+
return handler.searchAndAddWithOptions(ctx, args, interactive)
76
76
+
}
77
77
+
78
78
+
func (h *BookHandler) searchAndAdd(ctx context.Context, args []string) error {
79
79
+
if len(args) == 0 {
80
80
+
return fmt.Errorf("usage: book add <search query> [-i for interactive mode]")
81
81
+
}
82
82
+
83
83
+
interactive := false
84
84
+
searchArgs := args
85
85
+
if len(args) > 0 && args[len(args)-1] == "-i" {
86
86
+
interactive = true
87
87
+
searchArgs = args[:len(args)-1]
88
88
+
}
89
89
+
90
90
+
if len(searchArgs) == 0 {
91
91
+
return fmt.Errorf("search query cannot be empty")
92
92
+
}
93
93
+
94
94
+
query := searchArgs[0]
95
95
+
if len(searchArgs) > 1 {
96
96
+
for _, arg := range searchArgs[1:] {
97
97
+
query += " " + arg
98
98
+
}
99
99
+
}
100
100
+
101
101
+
return h.searchAndAddWithOptions(ctx, searchArgs, interactive)
102
102
+
}
103
103
+
104
104
+
func (h *BookHandler) searchAndAddWithOptions(ctx context.Context, args []string, interactive bool) error {
105
105
+
if len(args) == 0 {
106
106
+
return fmt.Errorf("usage: book add <search query>")
107
107
+
}
108
108
+
109
109
+
query := args[0]
110
110
+
if len(args) > 1 {
111
111
+
for _, arg := range args[1:] {
112
112
+
query += " " + arg
113
113
+
}
114
114
+
}
115
115
+
116
116
+
if interactive {
117
117
+
bookList := ui.NewBookList(h.service, h.repos.Books, ui.BookListOptions{
118
118
+
Output: os.Stdout,
119
119
+
Input: os.Stdin,
120
120
+
Static: false,
121
121
+
})
122
122
+
return bookList.SearchAndSelect(ctx, query)
123
123
+
}
124
124
+
125
125
+
fmt.Printf("Searching for books: %s\n", query)
126
126
+
fmt.Print("Loading...")
127
127
+
128
128
+
results, err := h.service.Search(ctx, query, 1, 5)
129
129
+
if err != nil {
130
130
+
fmt.Println(" failed!")
131
131
+
return fmt.Errorf("search failed: %w", err)
132
132
+
}
133
133
+
134
134
+
fmt.Println(" done!")
135
135
+
fmt.Println()
136
136
+
137
137
+
if len(results) == 0 {
138
138
+
fmt.Println("No books found.")
139
139
+
return nil
140
140
+
}
141
141
+
142
142
+
fmt.Printf("Found %d result(s):\n\n", len(results))
143
143
+
for i, result := range results {
144
144
+
if book, ok := (*result).(*models.Book); ok {
145
145
+
fmt.Printf("[%d] %s", i+1, book.Title)
146
146
+
if book.Author != "" {
147
147
+
fmt.Printf(" by %s", book.Author)
148
148
+
}
149
149
+
if book.Notes != "" {
150
150
+
notes := book.Notes
151
151
+
if len(notes) > 80 {
152
152
+
notes = notes[:77] + "..."
153
153
+
}
154
154
+
fmt.Printf("\n %s", notes)
155
155
+
}
156
156
+
fmt.Println()
157
157
+
}
158
158
+
}
159
159
+
160
160
+
fmt.Print("\nEnter number to add (1-", len(results), "), or 0 to cancel: ")
161
161
+
162
162
+
var choice int
163
163
+
if _, err := fmt.Scanf("%d", &choice); err != nil {
164
164
+
return fmt.Errorf("invalid input")
165
165
+
}
166
166
+
167
167
+
if choice == 0 {
168
168
+
fmt.Println("Cancelled.")
169
169
+
return nil
170
170
+
}
171
171
+
172
172
+
if choice < 1 || choice > len(results) {
173
173
+
return fmt.Errorf("invalid choice: %d", choice)
174
174
+
}
175
175
+
176
176
+
// Add selected book
177
177
+
selectedBook, ok := (*results[choice-1]).(*models.Book)
178
178
+
if !ok {
179
179
+
return fmt.Errorf("error processing selected book")
180
180
+
}
181
181
+
182
182
+
if _, err := h.repos.Books.Create(ctx, selectedBook); err != nil {
183
183
+
return fmt.Errorf("failed to add book: %w", err)
184
184
+
}
185
185
+
186
186
+
fmt.Printf("โ Added book: %s", selectedBook.Title)
187
187
+
if selectedBook.Author != "" {
188
188
+
fmt.Printf(" by %s", selectedBook.Author)
189
189
+
}
190
190
+
fmt.Println()
191
191
+
192
192
+
return nil
193
193
+
}
194
194
+
195
195
+
// ListBooks lists all books in the queue
196
196
+
func ListBooks(ctx context.Context, args []string) error {
197
197
+
handler, err := NewBookHandler()
198
198
+
if err != nil {
199
199
+
return fmt.Errorf("failed to initialize book handler: %w", err)
200
200
+
}
201
201
+
defer handler.Close()
202
202
+
203
203
+
return handler.listBooks(ctx, args)
204
204
+
}
205
205
+
206
206
+
func (h *BookHandler) listBooks(ctx context.Context, args []string) error {
207
207
+
status := "queued"
208
208
+
if len(args) > 0 {
209
209
+
switch args[0] {
210
210
+
case "all", "--all", "-a":
211
211
+
status = ""
212
212
+
case "reading", "--reading", "-r":
213
213
+
status = "reading"
214
214
+
case "finished", "--finished", "-f":
215
215
+
status = "finished"
216
216
+
case "queued", "--queued", "-q":
217
217
+
status = "queued"
218
218
+
}
219
219
+
}
220
220
+
221
221
+
var books []*models.Book
222
222
+
var err error
223
223
+
224
224
+
if status == "" {
225
225
+
books, err = h.repos.Books.List(ctx, repo.BookListOptions{})
226
226
+
if err != nil {
227
227
+
return fmt.Errorf("failed to list books: %w", err)
228
228
+
}
229
229
+
} else {
230
230
+
switch status {
231
231
+
case "queued":
232
232
+
books, err = h.repos.Books.GetQueued(ctx)
233
233
+
case "reading":
234
234
+
books, err = h.repos.Books.GetReading(ctx)
235
235
+
case "finished":
236
236
+
books, err = h.repos.Books.GetFinished(ctx)
237
237
+
}
238
238
+
if err != nil {
239
239
+
return fmt.Errorf("failed to get %s books: %w", status, err)
240
240
+
}
241
241
+
}
242
242
+
243
243
+
if len(books) == 0 {
244
244
+
if status == "" {
245
245
+
fmt.Println("No books found")
246
246
+
} else {
247
247
+
fmt.Printf("No %s books found\n", status)
248
248
+
}
249
249
+
return nil
250
250
+
}
251
251
+
252
252
+
fmt.Printf("Found %d book(s):\n\n", len(books))
253
253
+
for _, book := range books {
254
254
+
h.printBook(book)
255
255
+
}
256
256
+
257
257
+
return nil
258
258
+
}
259
259
+
260
260
+
func UpdateBookStatus(ctx context.Context, args []string) error {
261
261
+
handler, err := NewBookHandler()
262
262
+
if err != nil {
263
263
+
return fmt.Errorf("failed to initialize book handler: %w", err)
264
264
+
}
265
265
+
defer handler.Close()
266
266
+
267
267
+
return handler.updateBookStatus(ctx, args)
268
268
+
}
269
269
+
270
270
+
func (h *BookHandler) updateBookStatus(ctx context.Context, args []string) error {
271
271
+
if len(args) < 2 {
272
272
+
return fmt.Errorf("usage: book update <id> <status>")
273
273
+
}
274
274
+
275
275
+
var bookID int64
276
276
+
if _, err := fmt.Sscanf(args[0], "%d", &bookID); err != nil {
277
277
+
return fmt.Errorf("invalid book ID: %s", args[0])
278
278
+
}
279
279
+
280
280
+
status := args[1]
281
281
+
validStatuses := []string{"queued", "reading", "finished", "removed"}
282
282
+
valid := slices.Contains(validStatuses, status)
283
283
+
if !valid {
284
284
+
return fmt.Errorf("invalid status: %s (valid: %v)", status, validStatuses)
285
285
+
}
286
286
+
287
287
+
book, err := h.repos.Books.Get(ctx, bookID)
288
288
+
if err != nil {
289
289
+
return fmt.Errorf("failed to get book: %w", err)
290
290
+
}
291
291
+
292
292
+
book.Status = status
293
293
+
if status == "reading" && book.Started == nil {
294
294
+
now := time.Now()
295
295
+
book.Started = &now
296
296
+
}
297
297
+
if status == "finished" && book.Finished == nil {
298
298
+
now := time.Now()
299
299
+
book.Finished = &now
300
300
+
book.Progress = 100
301
301
+
}
302
302
+
303
303
+
if err := h.repos.Books.Update(ctx, book); err != nil {
304
304
+
return fmt.Errorf("failed to update book: %w", err)
305
305
+
}
306
306
+
307
307
+
fmt.Printf("Book status updated: %s -> %s\n", book.Title, status)
308
308
+
return nil
309
309
+
}
310
310
+
311
311
+
// UpdateBookProgress updates a book's reading progress percentage
312
312
+
func UpdateBookProgress(ctx context.Context, args []string) error {
313
313
+
handler, err := NewBookHandler()
314
314
+
if err != nil {
315
315
+
return fmt.Errorf("failed to initialize book handler: %w", err)
316
316
+
}
317
317
+
defer handler.Close()
318
318
+
319
319
+
return handler.updateBookProgress(ctx, args)
320
320
+
}
321
321
+
322
322
+
func (h *BookHandler) updateBookProgress(ctx context.Context, args []string) error {
323
323
+
if len(args) < 2 {
324
324
+
return fmt.Errorf("usage: book progress <id> <percentage>")
325
325
+
}
326
326
+
327
327
+
var bookID int64
328
328
+
if _, err := fmt.Sscanf(args[0], "%d", &bookID); err != nil {
329
329
+
return fmt.Errorf("invalid book ID: %s", args[0])
330
330
+
}
331
331
+
332
332
+
var progress int
333
333
+
if _, err := fmt.Sscanf(args[1], "%d", &progress); err != nil {
334
334
+
return fmt.Errorf("invalid progress percentage: %s", args[1])
335
335
+
}
336
336
+
337
337
+
if progress < 0 || progress > 100 {
338
338
+
return fmt.Errorf("progress must be between 0 and 100, got %d", progress)
339
339
+
}
340
340
+
341
341
+
book, err := h.repos.Books.Get(ctx, bookID)
342
342
+
if err != nil {
343
343
+
return fmt.Errorf("failed to get book: %w", err)
344
344
+
}
345
345
+
346
346
+
book.Progress = progress
347
347
+
348
348
+
if progress == 0 && book.Status == "reading" {
349
349
+
book.Status = "queued"
350
350
+
book.Started = nil
351
351
+
} else if progress > 0 && book.Status == "queued" {
352
352
+
book.Status = "reading"
353
353
+
if book.Started == nil {
354
354
+
now := time.Now()
355
355
+
book.Started = &now
356
356
+
}
357
357
+
} else if progress == 100 {
358
358
+
book.Status = "finished"
359
359
+
if book.Finished == nil {
360
360
+
now := time.Now()
361
361
+
book.Finished = &now
362
362
+
}
363
363
+
}
364
364
+
365
365
+
if err := h.repos.Books.Update(ctx, book); err != nil {
366
366
+
return fmt.Errorf("failed to update book progress: %w", err)
367
367
+
}
368
368
+
369
369
+
fmt.Printf("Book progress updated: %s -> %d%%", book.Title, progress)
370
370
+
if book.Status != "queued" {
371
371
+
fmt.Printf(" (%s)", book.Status)
372
372
+
}
373
373
+
fmt.Println()
374
374
+
return nil
375
375
+
}
376
376
+
377
377
+
func (h *BookHandler) printBook(book *models.Book) {
378
378
+
fmt.Printf("[%d] %s", book.ID, book.Title)
379
379
+
380
380
+
if book.Author != "" {
381
381
+
fmt.Printf(" by %s", book.Author)
382
382
+
}
383
383
+
384
384
+
if book.Status != "queued" {
385
385
+
fmt.Printf(" (%s)", book.Status)
386
386
+
}
387
387
+
388
388
+
if book.Progress > 0 {
389
389
+
fmt.Printf(" [%d%%]", book.Progress)
390
390
+
}
391
391
+
392
392
+
if book.Rating > 0 {
393
393
+
fmt.Printf(" โ %.1f", book.Rating)
394
394
+
}
395
395
+
396
396
+
fmt.Println()
397
397
+
398
398
+
if book.Notes != "" {
399
399
+
notes := book.Notes
400
400
+
if len(notes) > 80 {
401
401
+
notes = notes[:77] + "..."
402
402
+
}
403
403
+
fmt.Printf(" %s\n", notes)
404
404
+
}
405
405
+
406
406
+
fmt.Println()
407
407
+
}
+754
cmd/handlers/books_test.go
···
1
1
+
package handlers
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"os"
6
6
+
"strconv"
7
7
+
"strings"
8
8
+
"testing"
9
9
+
"time"
10
10
+
11
11
+
"github.com/stormlightlabs/noteleaf/internal/models"
12
12
+
)
13
13
+
14
14
+
func setupBookTest(t *testing.T) (string, func()) {
15
15
+
tempDir, err := os.MkdirTemp("", "noteleaf-book-test-*")
16
16
+
if err != nil {
17
17
+
t.Fatalf("Failed to create temp dir: %v", err)
18
18
+
}
19
19
+
20
20
+
oldConfigHome := os.Getenv("XDG_CONFIG_HOME")
21
21
+
os.Setenv("XDG_CONFIG_HOME", tempDir)
22
22
+
23
23
+
cleanup := func() {
24
24
+
os.Setenv("XDG_CONFIG_HOME", oldConfigHome)
25
25
+
os.RemoveAll(tempDir)
26
26
+
}
27
27
+
28
28
+
ctx := context.Background()
29
29
+
err = Setup(ctx, []string{})
30
30
+
if err != nil {
31
31
+
cleanup()
32
32
+
t.Fatalf("Failed to setup database: %v", err)
33
33
+
}
34
34
+
35
35
+
return tempDir, cleanup
36
36
+
}
37
37
+
38
38
+
func createTestBook(t *testing.T, handler *BookHandler, ctx context.Context) *models.Book {
39
39
+
t.Helper()
40
40
+
if handler == nil {
41
41
+
t.Fatal("handler provided to createTestBook is nil")
42
42
+
}
43
43
+
book := &models.Book{
44
44
+
Title: "Test Book",
45
45
+
Author: "Test Author",
46
46
+
Status: "queued",
47
47
+
Added: time.Now(),
48
48
+
}
49
49
+
id, err := handler.repos.Books.Create(ctx, book)
50
50
+
if err != nil {
51
51
+
t.Fatalf("Failed to create test book: %v", err)
52
52
+
}
53
53
+
book.ID = id
54
54
+
return book
55
55
+
}
56
56
+
57
57
+
func TestBookHandler(t *testing.T) {
58
58
+
t.Run("New", func(t *testing.T) {
59
59
+
t.Run("creates handler successfully", func(t *testing.T) {
60
60
+
_, cleanup := setupBookTest(t)
61
61
+
defer cleanup()
62
62
+
63
63
+
handler, err := NewBookHandler()
64
64
+
if err != nil {
65
65
+
t.Fatalf("NewBookHandler failed: %v", err)
66
66
+
}
67
67
+
if handler == nil {
68
68
+
t.Fatal("Handler should not be nil")
69
69
+
}
70
70
+
defer handler.Close()
71
71
+
72
72
+
if handler.db == nil {
73
73
+
t.Error("Handler database should not be nil")
74
74
+
}
75
75
+
if handler.config == nil {
76
76
+
t.Error("Handler config should not be nil")
77
77
+
}
78
78
+
if handler.repos == nil {
79
79
+
t.Error("Handler repos should not be nil")
80
80
+
}
81
81
+
if handler.service == nil {
82
82
+
t.Error("Handler service should not be nil")
83
83
+
}
84
84
+
})
85
85
+
86
86
+
t.Run("handles database initialization error", func(t *testing.T) {
87
87
+
originalXDG := os.Getenv("XDG_CONFIG_HOME")
88
88
+
originalHome := os.Getenv("HOME")
89
89
+
90
90
+
os.Unsetenv("XDG_CONFIG_HOME")
91
91
+
os.Unsetenv("HOME")
92
92
+
defer func() {
93
93
+
os.Setenv("XDG_CONFIG_HOME", originalXDG)
94
94
+
os.Setenv("HOME", originalHome)
95
95
+
}()
96
96
+
97
97
+
handler, err := NewBookHandler()
98
98
+
if err == nil {
99
99
+
if handler != nil {
100
100
+
handler.Close()
101
101
+
}
102
102
+
t.Error("Expected error when database initialization fails")
103
103
+
}
104
104
+
})
105
105
+
})
106
106
+
107
107
+
t.Run("Search & Add", func(t *testing.T) {
108
108
+
_, cleanup := setupBookTest(t)
109
109
+
defer cleanup()
110
110
+
111
111
+
t.Run("fails with empty args", func(t *testing.T) {
112
112
+
ctx := context.Background()
113
113
+
args := []string{}
114
114
+
115
115
+
err := SearchAndAdd(ctx, args)
116
116
+
if err == nil {
117
117
+
t.Error("Expected error for empty args")
118
118
+
}
119
119
+
120
120
+
if !strings.Contains(err.Error(), "usage: book add") {
121
121
+
t.Errorf("Expected usage error, got: %v", err)
122
122
+
}
123
123
+
})
124
124
+
125
125
+
t.Run("fails with empty search", func(t *testing.T) {
126
126
+
ctx := context.Background()
127
127
+
args := []string{"-i"}
128
128
+
129
129
+
err := SearchAndAdd(ctx, args)
130
130
+
if err == nil {
131
131
+
t.Error("Expected error for empty search query")
132
132
+
}
133
133
+
134
134
+
if !strings.Contains(err.Error(), "search query cannot be empty") {
135
135
+
t.Errorf("Expected empty search query error, got: %v", err)
136
136
+
}
137
137
+
})
138
138
+
139
139
+
t.Run("with options", func(t *testing.T) {
140
140
+
_, cleanup := setupBookTest(t)
141
141
+
defer cleanup()
142
142
+
143
143
+
t.Run("fails with empty args", func(t *testing.T) {
144
144
+
ctx := context.Background()
145
145
+
args := []string{}
146
146
+
147
147
+
err := SearchAndAddWithOptions(ctx, args, false)
148
148
+
if err == nil {
149
149
+
t.Error("Expected error for empty args")
150
150
+
}
151
151
+
152
152
+
if !strings.Contains(err.Error(), "usage: book add") {
153
153
+
t.Errorf("Expected usage error, got: %v", err)
154
154
+
}
155
155
+
})
156
156
+
157
157
+
t.Run("handles search service errors", func(t *testing.T) {
158
158
+
ctx := context.Background()
159
159
+
args := []string{"test", "book"}
160
160
+
161
161
+
err := SearchAndAddWithOptions(ctx, args, false)
162
162
+
if err == nil {
163
163
+
t.Error("Expected error due to mocked service")
164
164
+
}
165
165
+
if strings.Contains(err.Error(), "usage:") {
166
166
+
t.Error("Should not show usage error for valid args")
167
167
+
}
168
168
+
})
169
169
+
170
170
+
})
171
171
+
})
172
172
+
173
173
+
t.Run("List", func(t *testing.T) {
174
174
+
_, cleanup := setupBookTest(t)
175
175
+
defer cleanup()
176
176
+
177
177
+
ctx := context.Background()
178
178
+
179
179
+
handler, err := NewBookHandler()
180
180
+
if err != nil {
181
181
+
t.Fatalf("Failed to create handler: %v", err)
182
182
+
}
183
183
+
defer handler.Close()
184
184
+
185
185
+
_ = createTestBook(t, handler, ctx)
186
186
+
187
187
+
book2 := &models.Book{
188
188
+
Title: "Reading Book",
189
189
+
Author: "Reading Author",
190
190
+
Status: "reading",
191
191
+
Added: time.Now(),
192
192
+
}
193
193
+
id2, err := handler.repos.Books.Create(ctx, book2)
194
194
+
if err != nil {
195
195
+
t.Fatalf("Failed to create book2: %v", err)
196
196
+
}
197
197
+
book2.ID = id2
198
198
+
199
199
+
book3 := &models.Book{
200
200
+
Title: "Finished Book",
201
201
+
Author: "Finished Author",
202
202
+
Status: "finished",
203
203
+
Added: time.Now(),
204
204
+
}
205
205
+
id3, err := handler.repos.Books.Create(ctx, book3)
206
206
+
if err != nil {
207
207
+
t.Fatalf("Failed to create book3: %v", err)
208
208
+
}
209
209
+
book3.ID = id3
210
210
+
211
211
+
t.Run("lists queued books by default", func(t *testing.T) {
212
212
+
args := []string{}
213
213
+
214
214
+
err := ListBooks(ctx, args)
215
215
+
if err != nil {
216
216
+
t.Errorf("ListBooks failed: %v", err)
217
217
+
}
218
218
+
})
219
219
+
220
220
+
t.Run("filters by status - all", func(t *testing.T) {
221
221
+
args := []string{"all"}
222
222
+
223
223
+
err := ListBooks(ctx, args)
224
224
+
if err != nil {
225
225
+
t.Errorf("ListBooks with status all failed: %v", err)
226
226
+
}
227
227
+
})
228
228
+
229
229
+
t.Run("filters by status - reading", func(t *testing.T) {
230
230
+
args := []string{"reading"}
231
231
+
232
232
+
err := ListBooks(ctx, args)
233
233
+
if err != nil {
234
234
+
t.Errorf("ListBooks with status reading failed: %v", err)
235
235
+
}
236
236
+
})
237
237
+
238
238
+
t.Run("filters by status - finished", func(t *testing.T) {
239
239
+
args := []string{"finished"}
240
240
+
241
241
+
err := ListBooks(ctx, args)
242
242
+
if err != nil {
243
243
+
t.Errorf("ListBooks with status finished failed: %v", err)
244
244
+
}
245
245
+
})
246
246
+
247
247
+
t.Run("filters by status - queued", func(t *testing.T) {
248
248
+
args := []string{"queued"}
249
249
+
250
250
+
err := ListBooks(ctx, args)
251
251
+
if err != nil {
252
252
+
t.Errorf("ListBooks with status queued failed: %v", err)
253
253
+
}
254
254
+
})
255
255
+
256
256
+
t.Run("handles various flag formats", func(t *testing.T) {
257
257
+
statusVariants := [][]string{
258
258
+
{"--all"}, {"-a"},
259
259
+
{"--reading"}, {"-r"},
260
260
+
{"--finished"}, {"-f"},
261
261
+
{"--queued"}, {"-q"},
262
262
+
}
263
263
+
264
264
+
for _, args := range statusVariants {
265
265
+
err := ListBooks(ctx, args)
266
266
+
if err != nil {
267
267
+
t.Errorf("ListBooks with args %v failed: %v", args, err)
268
268
+
}
269
269
+
}
270
270
+
})
271
271
+
})
272
272
+
273
273
+
t.Run("Update", func(t *testing.T) {
274
274
+
t.Run("Update status", func(t *testing.T) {
275
275
+
_, cleanup := setupBookTest(t)
276
276
+
defer cleanup()
277
277
+
278
278
+
ctx := context.Background()
279
279
+
280
280
+
handler, err := NewBookHandler()
281
281
+
if err != nil {
282
282
+
t.Fatalf("Failed to create handler: %v", err)
283
283
+
}
284
284
+
defer handler.Close()
285
285
+
286
286
+
book := createTestBook(t, handler, ctx)
287
287
+
288
288
+
t.Run("updates book status successfully", func(t *testing.T) {
289
289
+
args := []string{strconv.FormatInt(book.ID, 10), "reading"}
290
290
+
291
291
+
err := UpdateBookStatus(ctx, args)
292
292
+
if err != nil {
293
293
+
t.Errorf("UpdateBookStatus failed: %v", err)
294
294
+
}
295
295
+
296
296
+
updatedBook, err := handler.repos.Books.Get(ctx, book.ID)
297
297
+
if err != nil {
298
298
+
t.Fatalf("Failed to get updated book: %v", err)
299
299
+
}
300
300
+
301
301
+
if updatedBook.Status != "reading" {
302
302
+
t.Errorf("Expected status 'reading', got '%s'", updatedBook.Status)
303
303
+
}
304
304
+
305
305
+
if updatedBook.Started == nil {
306
306
+
t.Error("Expected started time to be set")
307
307
+
}
308
308
+
})
309
309
+
310
310
+
t.Run("updates to finished status", func(t *testing.T) {
311
311
+
args := []string{strconv.FormatInt(book.ID, 10), "finished"}
312
312
+
313
313
+
err := UpdateBookStatus(ctx, args)
314
314
+
if err != nil {
315
315
+
t.Errorf("UpdateBookStatus failed: %v", err)
316
316
+
}
317
317
+
318
318
+
updatedBook, err := handler.repos.Books.Get(ctx, book.ID)
319
319
+
if err != nil {
320
320
+
t.Fatalf("Failed to get updated book: %v", err)
321
321
+
}
322
322
+
323
323
+
if updatedBook.Status != "finished" {
324
324
+
t.Errorf("Expected status 'finished', got '%s'", updatedBook.Status)
325
325
+
}
326
326
+
327
327
+
if updatedBook.Finished == nil {
328
328
+
t.Error("Expected finished time to be set")
329
329
+
}
330
330
+
331
331
+
if updatedBook.Progress != 100 {
332
332
+
t.Errorf("Expected progress 100, got %d", updatedBook.Progress)
333
333
+
}
334
334
+
})
335
335
+
336
336
+
t.Run("fails with insufficient arguments", func(t *testing.T) {
337
337
+
args := []string{strconv.FormatInt(book.ID, 10)}
338
338
+
339
339
+
err := UpdateBookStatus(ctx, args)
340
340
+
if err == nil {
341
341
+
t.Error("Expected error for insufficient arguments")
342
342
+
}
343
343
+
344
344
+
if !strings.Contains(err.Error(), "usage: book update") {
345
345
+
t.Errorf("Expected usage error, got: %v", err)
346
346
+
}
347
347
+
})
348
348
+
349
349
+
t.Run("fails with invalid book ID", func(t *testing.T) {
350
350
+
args := []string{"invalid-id", "reading"}
351
351
+
352
352
+
err := UpdateBookStatus(ctx, args)
353
353
+
if err == nil {
354
354
+
t.Error("Expected error for invalid book ID")
355
355
+
}
356
356
+
357
357
+
if !strings.Contains(err.Error(), "invalid book ID") {
358
358
+
t.Errorf("Expected invalid book ID error, got: %v", err)
359
359
+
}
360
360
+
})
361
361
+
362
362
+
t.Run("fails with invalid status", func(t *testing.T) {
363
363
+
args := []string{strconv.FormatInt(book.ID, 10), "invalid-status"}
364
364
+
365
365
+
err := UpdateBookStatus(ctx, args)
366
366
+
if err == nil {
367
367
+
t.Error("Expected error for invalid status")
368
368
+
}
369
369
+
370
370
+
if !strings.Contains(err.Error(), "invalid status") {
371
371
+
t.Errorf("Expected invalid status error, got: %v", err)
372
372
+
}
373
373
+
})
374
374
+
375
375
+
t.Run("fails with non-existent book ID", func(t *testing.T) {
376
376
+
args := []string{"99999", "reading"}
377
377
+
378
378
+
err := UpdateBookStatus(ctx, args)
379
379
+
if err == nil {
380
380
+
t.Error("Expected error for non-existent book ID")
381
381
+
}
382
382
+
383
383
+
if !strings.Contains(err.Error(), "failed to get book") {
384
384
+
t.Errorf("Expected book not found error, got: %v", err)
385
385
+
}
386
386
+
})
387
387
+
388
388
+
t.Run("validates all status options", func(t *testing.T) {
389
389
+
validStatuses := []string{"queued", "reading", "finished", "removed"}
390
390
+
391
391
+
for _, status := range validStatuses {
392
392
+
args := []string{strconv.FormatInt(book.ID, 10), status}
393
393
+
394
394
+
err := UpdateBookStatus(ctx, args)
395
395
+
if err != nil {
396
396
+
t.Errorf("UpdateBookStatus with status %s failed: %v", status, err)
397
397
+
}
398
398
+
}
399
399
+
})
400
400
+
})
401
401
+
402
402
+
t.Run("progress", func(t *testing.T) {
403
403
+
_, cleanup := setupBookTest(t)
404
404
+
defer cleanup()
405
405
+
406
406
+
ctx := context.Background()
407
407
+
408
408
+
handler, err := NewBookHandler()
409
409
+
if err != nil {
410
410
+
t.Fatalf("Failed to create handler: %v", err)
411
411
+
}
412
412
+
defer handler.Close()
413
413
+
414
414
+
book := createTestBook(t, handler, ctx)
415
415
+
416
416
+
t.Run("updates progress successfully", func(t *testing.T) {
417
417
+
args := []string{strconv.FormatInt(book.ID, 10), "50"}
418
418
+
419
419
+
err := UpdateBookProgress(ctx, args)
420
420
+
if err != nil {
421
421
+
t.Errorf("UpdateBookProgress failed: %v", err)
422
422
+
}
423
423
+
424
424
+
updatedBook, err := handler.repos.Books.Get(ctx, book.ID)
425
425
+
if err != nil {
426
426
+
t.Fatalf("Failed to get updated book: %v", err)
427
427
+
}
428
428
+
429
429
+
if updatedBook.Progress != 50 {
430
430
+
t.Errorf("Expected progress 50, got %d", updatedBook.Progress)
431
431
+
}
432
432
+
433
433
+
if updatedBook.Status != "reading" {
434
434
+
t.Errorf("Expected status 'reading', got '%s'", updatedBook.Status)
435
435
+
}
436
436
+
437
437
+
if updatedBook.Started == nil {
438
438
+
t.Error("Expected started time to be set")
439
439
+
}
440
440
+
})
441
441
+
442
442
+
t.Run("auto-completes book at 100%", func(t *testing.T) {
443
443
+
args := []string{strconv.FormatInt(book.ID, 10), "100"}
444
444
+
445
445
+
err := UpdateBookProgress(ctx, args)
446
446
+
if err != nil {
447
447
+
t.Errorf("UpdateBookProgress failed: %v", err)
448
448
+
}
449
449
+
450
450
+
updatedBook, err := handler.repos.Books.Get(ctx, book.ID)
451
451
+
if err != nil {
452
452
+
t.Fatalf("Failed to get updated book: %v", err)
453
453
+
}
454
454
+
455
455
+
if updatedBook.Progress != 100 {
456
456
+
t.Errorf("Expected progress 100, got %d", updatedBook.Progress)
457
457
+
}
458
458
+
459
459
+
if updatedBook.Status != "finished" {
460
460
+
t.Errorf("Expected status 'finished', got '%s'", updatedBook.Status)
461
461
+
}
462
462
+
463
463
+
if updatedBook.Finished == nil {
464
464
+
t.Error("Expected finished time to be set")
465
465
+
}
466
466
+
})
467
467
+
468
468
+
t.Run("resets to queued at 0%", func(t *testing.T) {
469
469
+
book.Status = "reading"
470
470
+
now := time.Now()
471
471
+
book.Started = &now
472
472
+
handler.repos.Books.Update(ctx, book)
473
473
+
474
474
+
args := []string{strconv.FormatInt(book.ID, 10), "0"}
475
475
+
476
476
+
err := UpdateBookProgress(ctx, args)
477
477
+
if err != nil {
478
478
+
t.Errorf("UpdateBookProgress failed: %v", err)
479
479
+
}
480
480
+
481
481
+
updatedBook, err := handler.repos.Books.Get(ctx, book.ID)
482
482
+
if err != nil {
483
483
+
t.Fatalf("Failed to get updated book: %v", err)
484
484
+
}
485
485
+
486
486
+
if updatedBook.Progress != 0 {
487
487
+
t.Errorf("Expected progress 0, got %d", updatedBook.Progress)
488
488
+
}
489
489
+
490
490
+
if updatedBook.Status != "queued" {
491
491
+
t.Errorf("Expected status 'queued', got '%s'", updatedBook.Status)
492
492
+
}
493
493
+
494
494
+
if updatedBook.Started != nil {
495
495
+
t.Error("Expected started time to be nil")
496
496
+
}
497
497
+
})
498
498
+
499
499
+
t.Run("fails with insufficient arguments", func(t *testing.T) {
500
500
+
args := []string{strconv.FormatInt(book.ID, 10)}
501
501
+
502
502
+
err := UpdateBookProgress(ctx, args)
503
503
+
if err == nil {
504
504
+
t.Error("Expected error for insufficient arguments")
505
505
+
}
506
506
+
507
507
+
if !strings.Contains(err.Error(), "usage: book progress") {
508
508
+
t.Errorf("Expected usage error, got: %v", err)
509
509
+
}
510
510
+
})
511
511
+
512
512
+
t.Run("fails with invalid book ID", func(t *testing.T) {
513
513
+
args := []string{"invalid-id", "50"}
514
514
+
515
515
+
err := UpdateBookProgress(ctx, args)
516
516
+
if err == nil {
517
517
+
t.Error("Expected error for invalid book ID")
518
518
+
}
519
519
+
520
520
+
if !strings.Contains(err.Error(), "invalid book ID") {
521
521
+
t.Errorf("Expected invalid book ID error, got: %v", err)
522
522
+
}
523
523
+
})
524
524
+
525
525
+
t.Run("fails with invalid progress percentage", func(t *testing.T) {
526
526
+
args := []string{strconv.FormatInt(book.ID, 10), "invalid-progress"}
527
527
+
528
528
+
err := UpdateBookProgress(ctx, args)
529
529
+
if err == nil {
530
530
+
t.Error("Expected error for invalid progress percentage")
531
531
+
}
532
532
+
533
533
+
if !strings.Contains(err.Error(), "invalid progress percentage") {
534
534
+
t.Errorf("Expected invalid progress percentage error, got: %v", err)
535
535
+
}
536
536
+
})
537
537
+
538
538
+
t.Run("fails with progress out of range", func(t *testing.T) {
539
539
+
testCases := []string{"-1", "101", "150"}
540
540
+
541
541
+
for _, progress := range testCases {
542
542
+
args := []string{strconv.FormatInt(book.ID, 10), progress}
543
543
+
544
544
+
err := UpdateBookProgress(ctx, args)
545
545
+
if err == nil {
546
546
+
t.Errorf("Expected error for progress %s", progress)
547
547
+
}
548
548
+
549
549
+
if !strings.Contains(err.Error(), "progress must be between 0 and 100") {
550
550
+
t.Errorf("Expected range error for progress %s, got: %v", progress, err)
551
551
+
}
552
552
+
}
553
553
+
})
554
554
+
555
555
+
t.Run("fails with non-existent book ID", func(t *testing.T) {
556
556
+
args := []string{"99999", "50"}
557
557
+
558
558
+
err := UpdateBookProgress(ctx, args)
559
559
+
if err == nil {
560
560
+
t.Error("Expected error for non-existent book ID")
561
561
+
}
562
562
+
563
563
+
if !strings.Contains(err.Error(), "failed to get book") {
564
564
+
t.Errorf("Expected book not found error, got: %v", err)
565
565
+
}
566
566
+
})
567
567
+
})
568
568
+
})
569
569
+
570
570
+
t.Run("Close", func(t *testing.T) {
571
571
+
t.Run("closes handler resources", func(t *testing.T) {
572
572
+
_, cleanup := setupBookTest(t)
573
573
+
defer cleanup()
574
574
+
575
575
+
handler, err := NewBookHandler()
576
576
+
if err != nil {
577
577
+
t.Fatalf("NewBookHandler failed: %v", err)
578
578
+
}
579
579
+
580
580
+
err = handler.Close()
581
581
+
if err != nil {
582
582
+
t.Errorf("Close failed: %v", err)
583
583
+
}
584
584
+
})
585
585
+
})
586
586
+
587
587
+
t.Run("Print", func(t *testing.T) {
588
588
+
_, cleanup := setupBookTest(t)
589
589
+
defer cleanup()
590
590
+
591
591
+
handler, err := NewBookHandler()
592
592
+
if err != nil {
593
593
+
t.Fatalf("Failed to create handler: %v", err)
594
594
+
}
595
595
+
defer handler.Close()
596
596
+
597
597
+
now := time.Now()
598
598
+
book := &models.Book{
599
599
+
ID: 1,
600
600
+
Title: "Test Book",
601
601
+
Author: "Test Author",
602
602
+
Status: "reading",
603
603
+
Progress: 75,
604
604
+
Rating: 4.5,
605
605
+
Notes: "This is a test note that is longer than 80 characters to test the truncation functionality in the print method",
606
606
+
Added: now,
607
607
+
}
608
608
+
609
609
+
t.Run("printBook doesn't panic", func(t *testing.T) {
610
610
+
defer func() {
611
611
+
if r := recover(); r != nil {
612
612
+
t.Errorf("printBook panicked: %v", r)
613
613
+
}
614
614
+
}()
615
615
+
616
616
+
handler.printBook(book)
617
617
+
})
618
618
+
619
619
+
t.Run("handles book with minimal fields", func(t *testing.T) {
620
620
+
minimalBook := &models.Book{
621
621
+
ID: 2,
622
622
+
Title: "Minimal Book",
623
623
+
Status: "queued",
624
624
+
Added: now,
625
625
+
}
626
626
+
627
627
+
defer func() {
628
628
+
if r := recover(); r != nil {
629
629
+
t.Errorf("printBook panicked with minimal book: %v", r)
630
630
+
}
631
631
+
}()
632
632
+
633
633
+
handler.printBook(minimalBook)
634
634
+
})
635
635
+
})
636
636
+
637
637
+
t.Run("Error handling", func(t *testing.T) {
638
638
+
t.Run("handler creation fails with invalid environment", func(t *testing.T) {
639
639
+
originalXDG := os.Getenv("XDG_CONFIG_HOME")
640
640
+
originalHome := os.Getenv("HOME")
641
641
+
642
642
+
os.Unsetenv("XDG_CONFIG_HOME")
643
643
+
os.Unsetenv("HOME")
644
644
+
defer func() {
645
645
+
os.Setenv("XDG_CONFIG_HOME", originalXDG)
646
646
+
os.Setenv("HOME", originalHome)
647
647
+
}()
648
648
+
649
649
+
handler, err := NewBookHandler()
650
650
+
if err == nil {
651
651
+
if handler != nil {
652
652
+
handler.Close()
653
653
+
}
654
654
+
t.Error("Expected error when environment is invalid")
655
655
+
}
656
656
+
})
657
657
+
658
658
+
})
659
659
+
660
660
+
t.Run("Integration", func(t *testing.T) {
661
661
+
t.Run("full book lifecycle", func(t *testing.T) {
662
662
+
_, cleanup := setupBookTest(t)
663
663
+
defer cleanup()
664
664
+
665
665
+
ctx := context.Background()
666
666
+
667
667
+
handler, err := NewBookHandler()
668
668
+
if err != nil {
669
669
+
t.Fatalf("Failed to create handler: %v", err)
670
670
+
}
671
671
+
defer handler.Close()
672
672
+
673
673
+
book := createTestBook(t, handler, ctx)
674
674
+
675
675
+
if book.Status != "queued" {
676
676
+
t.Errorf("Expected initial status 'queued', got '%s'", book.Status)
677
677
+
}
678
678
+
679
679
+
err = UpdateBookProgress(ctx, []string{strconv.FormatInt(book.ID, 10), "25"})
680
680
+
if err != nil {
681
681
+
t.Errorf("Failed to update progress: %v", err)
682
682
+
}
683
683
+
684
684
+
updatedBook, err := handler.repos.Books.Get(ctx, book.ID)
685
685
+
if err != nil {
686
686
+
t.Fatalf("Failed to get updated book: %v", err)
687
687
+
}
688
688
+
689
689
+
if updatedBook.Status != "reading" {
690
690
+
t.Errorf("Expected status 'reading', got '%s'", updatedBook.Status)
691
691
+
}
692
692
+
693
693
+
err = UpdateBookProgress(ctx, []string{strconv.FormatInt(book.ID, 10), "100"})
694
694
+
if err != nil {
695
695
+
t.Errorf("Failed to complete book: %v", err)
696
696
+
}
697
697
+
698
698
+
completedBook, err := handler.repos.Books.Get(ctx, book.ID)
699
699
+
if err != nil {
700
700
+
t.Fatalf("Failed to get completed book: %v", err)
701
701
+
}
702
702
+
703
703
+
if completedBook.Status != "finished" {
704
704
+
t.Errorf("Expected status 'finished', got '%s'", completedBook.Status)
705
705
+
}
706
706
+
707
707
+
if completedBook.Progress != 100 {
708
708
+
t.Errorf("Expected progress 100, got %d", completedBook.Progress)
709
709
+
}
710
710
+
711
711
+
if completedBook.Finished == nil {
712
712
+
t.Error("Expected finished time to be set")
713
713
+
}
714
714
+
})
715
715
+
716
716
+
t.Run("concurrent book operations", func(t *testing.T) {
717
717
+
_, cleanup := setupBookTest(t)
718
718
+
defer cleanup()
719
719
+
720
720
+
ctx := context.Background()
721
721
+
722
722
+
handler, err := NewBookHandler()
723
723
+
if err != nil {
724
724
+
t.Fatalf("Failed to create handler: %v", err)
725
725
+
}
726
726
+
defer handler.Close()
727
727
+
728
728
+
book := createTestBook(t, handler, ctx)
729
729
+
730
730
+
done := make(chan error, 3)
731
731
+
732
732
+
go func() {
733
733
+
time.Sleep(time.Millisecond * 10)
734
734
+
done <- ListBooks(ctx, []string{})
735
735
+
}()
736
736
+
737
737
+
go func() {
738
738
+
time.Sleep(time.Millisecond * 15)
739
739
+
done <- UpdateBookProgress(ctx, []string{strconv.FormatInt(book.ID, 10), "50"})
740
740
+
}()
741
741
+
742
742
+
go func() {
743
743
+
time.Sleep(time.Millisecond * 20)
744
744
+
done <- UpdateBookStatus(ctx, []string{strconv.FormatInt(book.ID, 10), "finished"})
745
745
+
}()
746
746
+
747
747
+
for i := range 3 {
748
748
+
if err := <-done; err != nil {
749
749
+
t.Errorf("Concurrent operation %d failed: %v", i, err)
750
750
+
}
751
751
+
}
752
752
+
})
753
753
+
})
754
754
+
}
+655
-653
cmd/handlers/notes_test.go
···
42
42
return filePath
43
43
}
44
44
45
45
-
func TestNoteHandler_NewNoteHandler(t *testing.T) {
46
46
-
t.Run("creates handler successfully", func(t *testing.T) {
47
47
-
_, cleanup := setupNoteTest(t)
48
48
-
defer cleanup()
45
45
+
func TestNoteHandler(t *testing.T) {
46
46
+
t.Run("New", func(t *testing.T) {
47
47
+
t.Run("creates handler successfully", func(t *testing.T) {
48
48
+
_, cleanup := setupNoteTest(t)
49
49
+
defer cleanup()
49
50
50
50
-
handler, err := NewNoteHandler()
51
51
-
if err != nil {
52
52
-
t.Errorf("NewNoteHandler failed: %v", err)
53
53
-
}
54
54
-
if handler == nil {
55
55
-
t.Error("Handler should not be nil")
56
56
-
}
57
57
-
defer handler.Close()
51
51
+
handler, err := NewNoteHandler()
52
52
+
if err != nil {
53
53
+
t.Fatalf("NewNoteHandler failed: %v", err)
54
54
+
}
55
55
+
if handler == nil {
56
56
+
t.Fatal("Handler should not be nil")
57
57
+
}
58
58
+
defer handler.Close()
58
59
59
59
-
if handler.db == nil {
60
60
-
t.Error("Handler database should not be nil")
61
61
-
}
62
62
-
if handler.config == nil {
63
63
-
t.Error("Handler config should not be nil")
64
64
-
}
65
65
-
if handler.repos == nil {
66
66
-
t.Error("Handler repos should not be nil")
67
67
-
}
68
68
-
})
60
60
+
if handler.db == nil {
61
61
+
t.Error("Handler database should not be nil")
62
62
+
}
63
63
+
if handler.config == nil {
64
64
+
t.Error("Handler config should not be nil")
65
65
+
}
66
66
+
if handler.repos == nil {
67
67
+
t.Error("Handler repos should not be nil")
68
68
+
}
69
69
+
})
69
70
70
70
-
t.Run("handles database initialization error", func(t *testing.T) {
71
71
-
originalXDG := os.Getenv("XDG_CONFIG_HOME")
72
72
-
originalHome := os.Getenv("HOME")
71
71
+
t.Run("handles database initialization error", func(t *testing.T) {
72
72
+
originalXDG := os.Getenv("XDG_CONFIG_HOME")
73
73
+
originalHome := os.Getenv("HOME")
73
74
74
74
-
if runtime.GOOS == "windows" {
75
75
-
originalAppData := os.Getenv("APPDATA")
76
76
-
os.Unsetenv("APPDATA")
77
77
-
defer os.Setenv("APPDATA", originalAppData)
78
78
-
} else {
79
79
-
os.Unsetenv("XDG_CONFIG_HOME")
80
80
-
os.Unsetenv("HOME")
81
81
-
}
75
75
+
if runtime.GOOS == "windows" {
76
76
+
originalAppData := os.Getenv("APPDATA")
77
77
+
os.Unsetenv("APPDATA")
78
78
+
defer os.Setenv("APPDATA", originalAppData)
79
79
+
} else {
80
80
+
os.Unsetenv("XDG_CONFIG_HOME")
81
81
+
os.Unsetenv("HOME")
82
82
+
}
82
83
83
83
-
defer func() {
84
84
-
os.Setenv("XDG_CONFIG_HOME", originalXDG)
85
85
-
os.Setenv("HOME", originalHome)
86
86
-
}()
84
84
+
defer func() {
85
85
+
os.Setenv("XDG_CONFIG_HOME", originalXDG)
86
86
+
os.Setenv("HOME", originalHome)
87
87
+
}()
87
88
88
88
-
_, err := NewNoteHandler()
89
89
-
if err == nil {
90
90
-
t.Error("NewNoteHandler should fail when database initialization fails")
91
91
-
}
92
92
-
if !strings.Contains(err.Error(), "failed to initialize database") {
93
93
-
t.Errorf("Expected database error, got: %v", err)
94
94
-
}
89
89
+
_, err := NewNoteHandler()
90
90
+
if err == nil {
91
91
+
t.Error("NewNoteHandler should fail when database initialization fails")
92
92
+
}
93
93
+
if !strings.Contains(err.Error(), "failed to initialize database") {
94
94
+
t.Errorf("Expected database error, got: %v", err)
95
95
+
}
96
96
+
})
95
97
})
96
96
-
}
97
98
98
98
-
func TestNoteHandler_parseNoteContent(t *testing.T) {
99
99
-
handler := &NoteHandler{}
99
99
+
t.Run("parse content", func(t *testing.T) {
100
100
+
handler := &NoteHandler{}
100
101
101
101
-
testCases := []struct {
102
102
-
name string
103
103
-
input string
104
104
-
expectedTitle string
105
105
-
expectedContent string
106
106
-
expectedTags []string
107
107
-
}{
108
108
-
{
109
109
-
name: "note with title and tags",
110
110
-
input: `# My Test Note
102
102
+
testCases := []struct {
103
103
+
name string
104
104
+
input string
105
105
+
expectedTitle string
106
106
+
expectedContent string
107
107
+
expectedTags []string
108
108
+
}{
109
109
+
{
110
110
+
name: "note with title and tags",
111
111
+
input: `# My Test Note
111
112
112
113
This is the content.
113
114
114
115
<!-- Tags: personal, work, important -->`,
115
115
-
expectedTitle: "My Test Note",
116
116
-
expectedContent: `# My Test Note
116
116
+
expectedTitle: "My Test Note",
117
117
+
expectedContent: `# My Test Note
117
118
118
119
This is the content.
119
120
120
121
<!-- Tags: personal, work, important -->`,
121
121
-
expectedTags: []string{"personal", "work", "important"},
122
122
-
},
123
123
-
{
124
124
-
name: "note without title",
125
125
-
input: `Just some content here.
122
122
+
expectedTags: []string{"personal", "work", "important"},
123
123
+
},
124
124
+
{
125
125
+
name: "note without title",
126
126
+
input: `Just some content here.
126
127
127
128
No title heading.
128
129
129
130
<!-- Tags: test -->`,
130
130
-
expectedTitle: "",
131
131
-
expectedContent: `Just some content here.
131
131
+
expectedTitle: "",
132
132
+
expectedContent: `Just some content here.
132
133
133
134
No title heading.
134
135
135
136
<!-- Tags: test -->`,
136
136
-
expectedTags: []string{"test"},
137
137
-
},
138
138
-
{
139
139
-
name: "note without tags",
140
140
-
input: `# Title Only
137
137
+
expectedTags: []string{"test"},
138
138
+
},
139
139
+
{
140
140
+
name: "note without tags",
141
141
+
input: `# Title Only
141
142
142
143
Content without tags.`,
143
143
-
expectedTitle: "Title Only",
144
144
-
expectedContent: `# Title Only
144
144
+
expectedTitle: "Title Only",
145
145
+
expectedContent: `# Title Only
145
146
146
147
Content without tags.`,
147
147
-
expectedTags: nil,
148
148
-
},
149
149
-
{
150
150
-
name: "empty tags comment",
151
151
-
input: `# Test Note
148
148
+
expectedTags: nil,
149
149
+
},
150
150
+
{
151
151
+
name: "empty tags comment",
152
152
+
input: `# Test Note
152
153
153
154
Content here.
154
155
155
156
<!-- Tags: -->`,
156
156
-
expectedTitle: "Test Note",
157
157
-
expectedContent: `# Test Note
157
157
+
expectedTitle: "Test Note",
158
158
+
expectedContent: `# Test Note
158
159
159
160
Content here.
160
161
161
162
<!-- Tags: -->`,
162
162
-
expectedTags: nil,
163
163
-
},
164
164
-
{
165
165
-
name: "malformed tags comment",
166
166
-
input: `# Test Note
163
163
+
expectedTags: nil,
164
164
+
},
165
165
+
{
166
166
+
name: "malformed tags comment",
167
167
+
input: `# Test Note
167
168
168
169
Content here.
169
170
170
171
<!-- Tags: tag1, , tag2,, tag3 -->`,
171
171
-
expectedTitle: "Test Note",
172
172
-
expectedContent: `# Test Note
172
172
+
expectedTitle: "Test Note",
173
173
+
expectedContent: `# Test Note
173
174
174
175
Content here.
175
176
176
177
<!-- Tags: tag1, , tag2,, tag3 -->`,
177
177
-
expectedTags: []string{"tag1", "tag2", "tag3"},
178
178
-
},
179
179
-
{
180
180
-
name: "multiple headings",
181
181
-
input: `## Secondary Heading
178
178
+
expectedTags: []string{"tag1", "tag2", "tag3"},
179
179
+
},
180
180
+
{
181
181
+
name: "multiple headings",
182
182
+
input: `## Secondary Heading
182
183
183
184
# Main Title
184
185
185
186
Content here.`,
186
186
-
expectedTitle: "Main Title",
187
187
-
expectedContent: `## Secondary Heading
187
187
+
expectedTitle: "Main Title",
188
188
+
expectedContent: `## Secondary Heading
188
189
189
190
# Main Title
190
191
191
192
Content here.`,
192
192
-
expectedTags: nil,
193
193
-
},
194
194
-
}
193
193
+
expectedTags: nil,
194
194
+
},
195
195
+
}
195
196
196
196
-
for _, tc := range testCases {
197
197
-
t.Run(tc.name, func(t *testing.T) {
198
198
-
title, content, tags := handler.parseNoteContent(tc.input)
197
197
+
for _, tc := range testCases {
198
198
+
t.Run(tc.name, func(t *testing.T) {
199
199
+
title, content, tags := handler.parseNoteContent(tc.input)
199
200
200
200
-
if title != tc.expectedTitle {
201
201
-
t.Errorf("Expected title %q, got %q", tc.expectedTitle, title)
202
202
-
}
201
201
+
if title != tc.expectedTitle {
202
202
+
t.Errorf("Expected title %q, got %q", tc.expectedTitle, title)
203
203
+
}
203
204
204
204
-
if content != tc.expectedContent {
205
205
-
t.Errorf("Expected content %q, got %q", tc.expectedContent, content)
206
206
-
}
205
205
+
if content != tc.expectedContent {
206
206
+
t.Errorf("Expected content %q, got %q", tc.expectedContent, content)
207
207
+
}
207
208
208
208
-
if len(tags) != len(tc.expectedTags) {
209
209
-
t.Errorf("Expected %d tags, got %d", len(tc.expectedTags), len(tags))
210
210
-
}
209
209
+
if len(tags) != len(tc.expectedTags) {
210
210
+
t.Errorf("Expected %d tags, got %d", len(tc.expectedTags), len(tags))
211
211
+
}
211
212
212
212
-
for i, expectedTag := range tc.expectedTags {
213
213
-
if i >= len(tags) || tags[i] != expectedTag {
214
214
-
t.Errorf("Expected tag %q at position %d, got %q", expectedTag, i, tags[i])
213
213
+
for i, expectedTag := range tc.expectedTags {
214
214
+
if i >= len(tags) || tags[i] != expectedTag {
215
215
+
t.Errorf("Expected tag %q at position %d, got %q", expectedTag, i, tags[i])
216
216
+
}
215
217
}
216
216
-
}
217
217
-
})
218
218
-
}
219
219
-
}
218
218
+
})
219
219
+
}
220
220
+
})
220
221
221
221
-
func TestIsFile(t *testing.T) {
222
222
-
testCases := []struct {
223
223
-
name string
224
224
-
input string
225
225
-
expected bool
226
226
-
}{
227
227
-
{"file with extension", "test.md", true},
228
228
-
{"file with multiple extensions", "test.tar.gz", true},
229
229
-
{"path with slash", "/path/to/file", true},
230
230
-
{"path with backslash", "path\\to\\file", true},
231
231
-
{"relative path", "./file", true},
232
232
-
{"just text", "hello", false},
233
233
-
{"empty string", "", false},
234
234
-
}
222
222
+
t.Run("IsFile helper", func(t *testing.T) {
223
223
+
testCases := []struct {
224
224
+
name string
225
225
+
input string
226
226
+
expected bool
227
227
+
}{
228
228
+
{"file with extension", "test.md", true},
229
229
+
{"file with multiple extensions", "test.tar.gz", true},
230
230
+
{"path with slash", "/path/to/file", true},
231
231
+
{"path with backslash", "path\\to\\file", true},
232
232
+
{"relative path", "./file", true},
233
233
+
{"just text", "hello", false},
234
234
+
{"empty string", "", false},
235
235
+
}
235
236
236
236
-
tempDir, err := os.MkdirTemp("", "isfile-test-*")
237
237
-
if err != nil {
238
238
-
t.Fatalf("Failed to create temp dir: %v", err)
239
239
-
}
240
240
-
defer os.RemoveAll(tempDir)
237
237
+
tempDir, err := os.MkdirTemp("", "isfile-test-*")
238
238
+
if err != nil {
239
239
+
t.Fatalf("Failed to create temp dir: %v", err)
240
240
+
}
241
241
+
defer os.RemoveAll(tempDir)
241
242
242
242
-
existingFile := filepath.Join(tempDir, "existing")
243
243
-
err = os.WriteFile(existingFile, []byte("test"), 0644)
244
244
-
if err != nil {
245
245
-
t.Fatalf("Failed to create test file: %v", err)
246
246
-
}
243
243
+
existingFile := filepath.Join(tempDir, "existing")
244
244
+
err = os.WriteFile(existingFile, []byte("test"), 0644)
245
245
+
if err != nil {
246
246
+
t.Fatalf("Failed to create test file: %v", err)
247
247
+
}
247
248
248
248
-
testCases = append(testCases, struct {
249
249
-
name string
250
250
-
input string
251
251
-
expected bool
252
252
-
}{"existing file without extension", existingFile, true})
249
249
+
testCases = append(testCases, struct {
250
250
+
name string
251
251
+
input string
252
252
+
expected bool
253
253
+
}{"existing file without extension", existingFile, true})
253
254
254
254
-
for _, tc := range testCases {
255
255
-
t.Run(tc.name, func(t *testing.T) {
256
256
-
result := isFile(tc.input)
257
257
-
if result != tc.expected {
258
258
-
t.Errorf("isFile(%q) = %v, expected %v", tc.input, result, tc.expected)
259
259
-
}
260
260
-
})
261
261
-
}
262
262
-
}
255
255
+
for _, tc := range testCases {
256
256
+
t.Run(tc.name, func(t *testing.T) {
257
257
+
result := isFile(tc.input)
258
258
+
if result != tc.expected {
259
259
+
t.Errorf("isFile(%q) = %v, expected %v", tc.input, result, tc.expected)
260
260
+
}
261
261
+
})
262
262
+
}
263
263
+
})
263
264
264
264
-
func TestNoteHandler_getEditor(t *testing.T) {
265
265
-
handler := &NoteHandler{}
265
265
+
t.Run("getEditor", func(t *testing.T) {
266
266
+
handler := &NoteHandler{}
266
267
267
267
-
t.Run("uses EDITOR environment variable", func(t *testing.T) {
268
268
-
originalEditor := os.Getenv("EDITOR")
269
269
-
os.Setenv("EDITOR", "test-editor")
270
270
-
defer os.Setenv("EDITOR", originalEditor)
268
268
+
t.Run("uses EDITOR environment variable", func(t *testing.T) {
269
269
+
originalEditor := os.Getenv("EDITOR")
270
270
+
os.Setenv("EDITOR", "test-editor")
271
271
+
defer os.Setenv("EDITOR", originalEditor)
271
272
272
272
-
editor := handler.getEditor()
273
273
-
if editor != "test-editor" {
274
274
-
t.Errorf("Expected 'test-editor', got %q", editor)
275
275
-
}
276
276
-
})
273
273
+
editor := handler.getEditor()
274
274
+
if editor != "test-editor" {
275
275
+
t.Errorf("Expected 'test-editor', got %q", editor)
276
276
+
}
277
277
+
})
277
278
278
278
-
t.Run("finds available editor", func(t *testing.T) {
279
279
-
originalEditor := os.Getenv("EDITOR")
280
280
-
os.Unsetenv("EDITOR")
281
281
-
defer os.Setenv("EDITOR", originalEditor)
279
279
+
t.Run("finds available editor", func(t *testing.T) {
280
280
+
originalEditor := os.Getenv("EDITOR")
281
281
+
os.Unsetenv("EDITOR")
282
282
+
defer os.Setenv("EDITOR", originalEditor)
282
283
283
283
-
editor := handler.getEditor()
284
284
-
if editor == "" {
285
285
-
t.Skip("No common editors found on system, skipping test")
286
286
-
}
287
287
-
})
284
284
+
editor := handler.getEditor()
285
285
+
if editor == "" {
286
286
+
t.Skip("No common editors found on system, skipping test")
287
287
+
}
288
288
+
})
288
289
289
289
-
t.Run("returns empty when no editor available", func(t *testing.T) {
290
290
-
originalEditor := os.Getenv("EDITOR")
291
291
-
originalPath := os.Getenv("PATH")
290
290
+
t.Run("returns empty when no editor available", func(t *testing.T) {
291
291
+
originalEditor := os.Getenv("EDITOR")
292
292
+
originalPath := os.Getenv("PATH")
292
293
293
293
-
os.Unsetenv("EDITOR")
294
294
-
os.Setenv("PATH", "")
294
294
+
os.Unsetenv("EDITOR")
295
295
+
os.Setenv("PATH", "")
295
296
296
296
-
defer func() {
297
297
-
os.Setenv("EDITOR", originalEditor)
298
298
-
os.Setenv("PATH", originalPath)
299
299
-
}()
297
297
+
defer func() {
298
298
+
os.Setenv("EDITOR", originalEditor)
299
299
+
os.Setenv("PATH", originalPath)
300
300
+
}()
300
301
301
301
-
editor := handler.getEditor()
302
302
-
if editor != "" {
303
303
-
t.Errorf("Expected empty string when no editor available, got %q", editor)
304
304
-
}
302
302
+
editor := handler.getEditor()
303
303
+
if editor != "" {
304
304
+
t.Errorf("Expected empty string when no editor available, got %q", editor)
305
305
+
}
306
306
+
})
305
307
})
306
306
-
}
307
308
308
308
-
func TestNoteCreateErrorScenarios(t *testing.T) {
309
309
-
errorTests := []struct {
310
310
-
name string
311
311
-
setupFunc func(t *testing.T) (cleanup func())
312
312
-
args []string
313
313
-
expectError bool
314
314
-
errorSubstr string
315
315
-
}{
316
316
-
{
317
317
-
name: "database initialization error",
318
318
-
setupFunc: func(t *testing.T) func() {
319
319
-
if runtime.GOOS == "windows" {
320
320
-
original := os.Getenv("APPDATA")
321
321
-
os.Unsetenv("APPDATA")
322
322
-
return func() { os.Setenv("APPDATA", original) }
323
323
-
} else {
324
324
-
originalXDG := os.Getenv("XDG_CONFIG_HOME")
325
325
-
originalHome := os.Getenv("HOME")
326
326
-
os.Unsetenv("XDG_CONFIG_HOME")
327
327
-
os.Unsetenv("HOME")
328
328
-
return func() {
329
329
-
os.Setenv("XDG_CONFIG_HOME", originalXDG)
330
330
-
os.Setenv("HOME", originalHome)
309
309
+
t.Run("Create Errors", func(t *testing.T) {
310
310
+
errorTests := []struct {
311
311
+
name string
312
312
+
setupFunc func(t *testing.T) (cleanup func())
313
313
+
args []string
314
314
+
expectError bool
315
315
+
errorSubstr string
316
316
+
}{
317
317
+
{
318
318
+
name: "database initialization error",
319
319
+
setupFunc: func(t *testing.T) func() {
320
320
+
if runtime.GOOS == "windows" {
321
321
+
original := os.Getenv("APPDATA")
322
322
+
os.Unsetenv("APPDATA")
323
323
+
return func() { os.Setenv("APPDATA", original) }
324
324
+
} else {
325
325
+
originalXDG := os.Getenv("XDG_CONFIG_HOME")
326
326
+
originalHome := os.Getenv("HOME")
327
327
+
os.Unsetenv("XDG_CONFIG_HOME")
328
328
+
os.Unsetenv("HOME")
329
329
+
return func() {
330
330
+
os.Setenv("XDG_CONFIG_HOME", originalXDG)
331
331
+
os.Setenv("HOME", originalHome)
332
332
+
}
331
333
}
332
332
-
}
334
334
+
},
335
335
+
args: []string{"Test Note"},
336
336
+
expectError: true,
337
337
+
errorSubstr: "failed to initialize database",
333
338
},
334
334
-
args: []string{"Test Note"},
335
335
-
expectError: true,
336
336
-
errorSubstr: "failed to initialize database",
337
337
-
},
338
338
-
{
339
339
-
name: "note creation in database fails",
340
340
-
setupFunc: func(t *testing.T) func() {
341
341
-
tempDir, cleanup := setupNoteTest(t)
339
339
+
{
340
340
+
name: "note creation in database fails",
341
341
+
setupFunc: func(t *testing.T) func() {
342
342
+
tempDir, cleanup := setupNoteTest(t)
342
343
343
343
-
configDir := filepath.Join(tempDir, "noteleaf")
344
344
-
dbPath := filepath.Join(configDir, "noteleaf.db")
344
344
+
configDir := filepath.Join(tempDir, "noteleaf")
345
345
+
dbPath := filepath.Join(configDir, "noteleaf.db")
345
346
346
346
-
err := os.WriteFile(dbPath, []byte("invalid sqlite content"), 0644)
347
347
-
if err != nil {
348
348
-
t.Fatalf("Failed to corrupt database: %v", err)
349
349
-
}
347
347
+
err := os.WriteFile(dbPath, []byte("invalid sqlite content"), 0644)
348
348
+
if err != nil {
349
349
+
t.Fatalf("Failed to corrupt database: %v", err)
350
350
+
}
350
351
351
351
-
return cleanup
352
352
+
return cleanup
353
353
+
},
354
354
+
args: []string{"Test Note"},
355
355
+
expectError: true,
356
356
+
errorSubstr: "failed to initialize database",
352
357
},
353
353
-
args: []string{"Test Note"},
354
354
-
expectError: true,
355
355
-
errorSubstr: "failed to initialize database",
356
356
-
},
357
357
-
}
358
358
+
}
359
359
+
360
360
+
for _, tt := range errorTests {
361
361
+
t.Run(tt.name, func(t *testing.T) {
362
362
+
cleanup := tt.setupFunc(t)
363
363
+
defer cleanup()
364
364
+
365
365
+
oldStdin := os.Stdin
366
366
+
r, w, _ := os.Pipe()
367
367
+
os.Stdin = r
368
368
+
defer func() { os.Stdin = oldStdin }()
369
369
+
370
370
+
go func() {
371
371
+
w.WriteString("n\n")
372
372
+
w.Close()
373
373
+
}()
374
374
+
375
375
+
ctx := context.Background()
376
376
+
err := Create(ctx, tt.args)
377
377
+
378
378
+
if tt.expectError && err == nil {
379
379
+
t.Errorf("Expected error containing %q, got nil", tt.errorSubstr)
380
380
+
} else if !tt.expectError && err != nil {
381
381
+
t.Errorf("Expected no error, got: %v", err)
382
382
+
} else if tt.expectError && err != nil && !strings.Contains(err.Error(), tt.errorSubstr) {
383
383
+
t.Errorf("Expected error containing %q, got: %v", tt.errorSubstr, err)
384
384
+
}
385
385
+
})
386
386
+
}
387
387
+
})
358
388
359
359
-
for _, tt := range errorTests {
360
360
-
t.Run(tt.name, func(t *testing.T) {
361
361
-
cleanup := tt.setupFunc(t)
389
389
+
t.Run("Create (args)", func(t *testing.T) {
390
390
+
t.Run("creates note from title only", func(t *testing.T) {
391
391
+
_, cleanup := setupNoteTest(t)
362
392
defer cleanup()
363
393
364
394
oldStdin := os.Stdin
···
372
402
}()
373
403
374
404
ctx := context.Background()
375
375
-
err := Create(ctx, tt.args)
376
376
-
377
377
-
if tt.expectError && err == nil {
378
378
-
t.Errorf("Expected error containing %q, got nil", tt.errorSubstr)
379
379
-
} else if !tt.expectError && err != nil {
380
380
-
t.Errorf("Expected no error, got: %v", err)
381
381
-
} else if tt.expectError && err != nil && !strings.Contains(err.Error(), tt.errorSubstr) {
382
382
-
t.Errorf("Expected error containing %q, got: %v", tt.errorSubstr, err)
405
405
+
err := Create(ctx, []string{"Test Note"})
406
406
+
if err != nil {
407
407
+
t.Errorf("Create failed: %v", err)
383
408
}
384
409
})
385
385
-
}
386
386
-
}
387
410
388
388
-
func TestCreate_WithArgs(t *testing.T) {
389
389
-
t.Run("creates note from title only", func(t *testing.T) {
390
390
-
_, cleanup := setupNoteTest(t)
391
391
-
defer cleanup()
411
411
+
t.Run("creates note from title and content", func(t *testing.T) {
412
412
+
_, cleanup := setupNoteTest(t)
413
413
+
defer cleanup()
392
414
393
393
-
oldStdin := os.Stdin
394
394
-
r, w, _ := os.Pipe()
395
395
-
os.Stdin = r
396
396
-
defer func() { os.Stdin = oldStdin }()
415
415
+
oldStdin := os.Stdin
416
416
+
r, w, _ := os.Pipe()
417
417
+
os.Stdin = r
418
418
+
defer func() { os.Stdin = oldStdin }()
397
419
398
398
-
go func() {
399
399
-
w.WriteString("n\n")
400
400
-
w.Close()
401
401
-
}()
420
420
+
go func() {
421
421
+
w.WriteString("n\n")
422
422
+
w.Close()
423
423
+
}()
402
424
403
403
-
ctx := context.Background()
404
404
-
err := Create(ctx, []string{"Test Note"})
405
405
-
if err != nil {
406
406
-
t.Errorf("Create failed: %v", err)
407
407
-
}
408
408
-
})
425
425
+
ctx := context.Background()
426
426
+
err := Create(ctx, []string{"Test Note", "This", "is", "test", "content"})
427
427
+
if err != nil {
428
428
+
t.Errorf("Create failed: %v", err)
429
429
+
}
430
430
+
})
409
431
410
410
-
t.Run("creates note from title and content", func(t *testing.T) {
411
411
-
_, cleanup := setupNoteTest(t)
412
412
-
defer cleanup()
432
432
+
t.Run("handles database connection error", func(t *testing.T) {
433
433
+
tempDir, cleanup := setupNoteTest(t)
434
434
+
defer cleanup()
413
435
414
414
-
oldStdin := os.Stdin
415
415
-
r, w, _ := os.Pipe()
416
416
-
os.Stdin = r
417
417
-
defer func() { os.Stdin = oldStdin }()
436
436
+
configDir := filepath.Join(tempDir, "noteleaf")
437
437
+
dbPath := filepath.Join(configDir, "noteleaf.db")
438
438
+
os.Remove(dbPath)
418
439
419
419
-
go func() {
420
420
-
w.WriteString("n\n")
421
421
-
w.Close()
422
422
-
}()
440
440
+
os.MkdirAll(dbPath, 0755)
441
441
+
defer os.RemoveAll(dbPath)
423
442
424
424
-
ctx := context.Background()
425
425
-
err := Create(ctx, []string{"Test Note", "This", "is", "test", "content"})
426
426
-
if err != nil {
427
427
-
t.Errorf("Create failed: %v", err)
428
428
-
}
429
429
-
})
443
443
+
ctx := context.Background()
444
444
+
err := Create(ctx, []string{"Test Note"})
445
445
+
if err == nil {
446
446
+
t.Error("Create should fail when database is inaccessible")
447
447
+
}
448
448
+
})
430
449
431
431
-
t.Run("handles database connection error", func(t *testing.T) {
432
432
-
tempDir, cleanup := setupNoteTest(t)
433
433
-
defer cleanup()
450
450
+
t.Run("New is alias for Create", func(t *testing.T) {
451
451
+
_, cleanup := setupNoteTest(t)
452
452
+
defer cleanup()
434
453
435
435
-
configDir := filepath.Join(tempDir, "noteleaf")
436
436
-
dbPath := filepath.Join(configDir, "noteleaf.db")
437
437
-
os.Remove(dbPath)
454
454
+
oldStdin := os.Stdin
455
455
+
r, w, _ := os.Pipe()
456
456
+
os.Stdin = r
457
457
+
defer func() { os.Stdin = oldStdin }()
438
458
439
439
-
os.MkdirAll(dbPath, 0755)
440
440
-
defer os.RemoveAll(dbPath)
459
459
+
go func() {
460
460
+
w.WriteString("n\n")
461
461
+
w.Close()
462
462
+
}()
441
463
442
442
-
ctx := context.Background()
443
443
-
err := Create(ctx, []string{"Test Note"})
444
444
-
if err == nil {
445
445
-
t.Error("Create should fail when database is inaccessible")
446
446
-
}
464
464
+
ctx := context.Background()
465
465
+
err := New(ctx, []string{"Test Note via New"})
466
466
+
if err != nil {
467
467
+
t.Errorf("New failed: %v", err)
468
468
+
}
469
469
+
})
447
470
})
448
471
449
449
-
t.Run("New is alias for Create", func(t *testing.T) {
450
450
-
_, cleanup := setupNoteTest(t)
451
451
-
defer cleanup()
452
452
-
453
453
-
oldStdin := os.Stdin
454
454
-
r, w, _ := os.Pipe()
455
455
-
os.Stdin = r
456
456
-
defer func() { os.Stdin = oldStdin }()
457
457
-
458
458
-
go func() {
459
459
-
w.WriteString("n\n")
460
460
-
w.Close()
461
461
-
}()
462
462
-
463
463
-
ctx := context.Background()
464
464
-
err := New(ctx, []string{"Test Note via New"})
465
465
-
if err != nil {
466
466
-
t.Errorf("New failed: %v", err)
467
467
-
}
468
468
-
})
469
469
-
}
472
472
+
t.Run("Create from file", func(t *testing.T) {
473
473
+
t.Run("creates note from markdown file", func(t *testing.T) {
474
474
+
tempDir, cleanup := setupNoteTest(t)
475
475
+
defer cleanup()
470
476
471
471
-
func TestCreate_FromFile(t *testing.T) {
472
472
-
t.Run("creates note from markdown file", func(t *testing.T) {
473
473
-
tempDir, cleanup := setupNoteTest(t)
474
474
-
defer cleanup()
475
475
-
476
476
-
content := `# My Test Note
477
477
+
content := `# My Test Note
477
478
478
479
This is the content of my test note.
479
480
···
483
484
484
485
<!-- Tags: personal, work -->`
485
486
486
486
-
filePath := createTestMarkdownFile(t, tempDir, "test.md", content)
487
487
+
filePath := createTestMarkdownFile(t, tempDir, "test.md", content)
487
488
488
488
-
ctx := context.Background()
489
489
-
err := Create(ctx, []string{filePath})
490
490
-
if err != nil {
491
491
-
t.Errorf("Create from file failed: %v", err)
492
492
-
}
493
493
-
})
489
489
+
ctx := context.Background()
490
490
+
err := Create(ctx, []string{filePath})
491
491
+
if err != nil {
492
492
+
t.Errorf("Create from file failed: %v", err)
493
493
+
}
494
494
+
})
494
495
495
495
-
t.Run("handles non-existent file", func(t *testing.T) {
496
496
-
_, cleanup := setupNoteTest(t)
497
497
-
defer cleanup()
496
496
+
t.Run("handles non-existent file", func(t *testing.T) {
497
497
+
_, cleanup := setupNoteTest(t)
498
498
+
defer cleanup()
498
499
499
499
-
ctx := context.Background()
500
500
-
err := Create(ctx, []string{"/non/existent/file.md"})
501
501
-
if err == nil {
502
502
-
t.Error("Create should fail for non-existent file")
503
503
-
}
504
504
-
if !strings.Contains(err.Error(), "file does not exist") {
505
505
-
t.Errorf("Expected file not found error, got: %v", err)
506
506
-
}
507
507
-
})
500
500
+
ctx := context.Background()
501
501
+
err := Create(ctx, []string{"/non/existent/file.md"})
502
502
+
if err == nil {
503
503
+
t.Error("Create should fail for non-existent file")
504
504
+
}
505
505
+
if !strings.Contains(err.Error(), "file does not exist") {
506
506
+
t.Errorf("Expected file not found error, got: %v", err)
507
507
+
}
508
508
+
})
508
509
509
509
-
t.Run("handles empty file", func(t *testing.T) {
510
510
-
tempDir, cleanup := setupNoteTest(t)
511
511
-
defer cleanup()
510
510
+
t.Run("handles empty file", func(t *testing.T) {
511
511
+
tempDir, cleanup := setupNoteTest(t)
512
512
+
defer cleanup()
512
513
513
513
-
filePath := createTestMarkdownFile(t, tempDir, "empty.md", "")
514
514
+
filePath := createTestMarkdownFile(t, tempDir, "empty.md", "")
514
515
515
515
-
ctx := context.Background()
516
516
-
err := Create(ctx, []string{filePath})
517
517
-
if err == nil {
518
518
-
t.Error("Create should fail for empty file")
519
519
-
}
520
520
-
if !strings.Contains(err.Error(), "file is empty") {
521
521
-
t.Errorf("Expected empty file error, got: %v", err)
522
522
-
}
523
523
-
})
516
516
+
ctx := context.Background()
517
517
+
err := Create(ctx, []string{filePath})
518
518
+
if err == nil {
519
519
+
t.Error("Create should fail for empty file")
520
520
+
}
521
521
+
if !strings.Contains(err.Error(), "file is empty") {
522
522
+
t.Errorf("Expected empty file error, got: %v", err)
523
523
+
}
524
524
+
})
524
525
525
525
-
t.Run("handles whitespace-only file", func(t *testing.T) {
526
526
-
tempDir, cleanup := setupNoteTest(t)
527
527
-
defer cleanup()
526
526
+
t.Run("handles whitespace-only file", func(t *testing.T) {
527
527
+
tempDir, cleanup := setupNoteTest(t)
528
528
+
defer cleanup()
528
529
529
529
-
filePath := createTestMarkdownFile(t, tempDir, "whitespace.md", " \n\t \n ")
530
530
+
filePath := createTestMarkdownFile(t, tempDir, "whitespace.md", " \n\t \n ")
530
531
531
531
-
ctx := context.Background()
532
532
-
err := Create(ctx, []string{filePath})
533
533
-
if err == nil {
534
534
-
t.Error("Create should fail for whitespace-only file")
535
535
-
}
536
536
-
if !strings.Contains(err.Error(), "file is empty") {
537
537
-
t.Errorf("Expected empty file error, got: %v", err)
538
538
-
}
539
539
-
})
532
532
+
ctx := context.Background()
533
533
+
err := Create(ctx, []string{filePath})
534
534
+
if err == nil {
535
535
+
t.Error("Create should fail for whitespace-only file")
536
536
+
}
537
537
+
if !strings.Contains(err.Error(), "file is empty") {
538
538
+
t.Errorf("Expected empty file error, got: %v", err)
539
539
+
}
540
540
+
})
540
541
541
541
-
t.Run("creates note without title in file", func(t *testing.T) {
542
542
-
tempDir, cleanup := setupNoteTest(t)
543
543
-
defer cleanup()
542
542
+
t.Run("creates note without title in file", func(t *testing.T) {
543
543
+
tempDir, cleanup := setupNoteTest(t)
544
544
+
defer cleanup()
544
545
545
545
-
content := `This note has no title heading.
546
546
+
content := `This note has no title heading.
546
547
547
548
Just some content here.`
548
549
549
549
-
filePath := createTestMarkdownFile(t, tempDir, "notitle.md", content)
550
550
+
filePath := createTestMarkdownFile(t, tempDir, "notitle.md", content)
550
551
551
551
-
ctx := context.Background()
552
552
-
err := Create(ctx, []string{filePath})
553
553
-
if err != nil {
554
554
-
t.Errorf("Create from file without title failed: %v", err)
555
555
-
}
556
556
-
})
552
552
+
ctx := context.Background()
553
553
+
err := Create(ctx, []string{filePath})
554
554
+
if err != nil {
555
555
+
t.Errorf("Create from file without title failed: %v", err)
556
556
+
}
557
557
+
})
557
558
558
558
-
t.Run("handles file read error", func(t *testing.T) {
559
559
-
tempDir, cleanup := setupNoteTest(t)
560
560
-
defer cleanup()
559
559
+
t.Run("handles file read error", func(t *testing.T) {
560
560
+
tempDir, cleanup := setupNoteTest(t)
561
561
+
defer cleanup()
561
562
562
562
-
filePath := createTestMarkdownFile(t, tempDir, "unreadable.md", "test content")
563
563
-
err := os.Chmod(filePath, 0000)
564
564
-
if err != nil {
565
565
-
t.Fatalf("Failed to make file unreadable: %v", err)
566
566
-
}
567
567
-
defer os.Chmod(filePath, 0644)
563
563
+
filePath := createTestMarkdownFile(t, tempDir, "unreadable.md", "test content")
564
564
+
err := os.Chmod(filePath, 0000)
565
565
+
if err != nil {
566
566
+
t.Fatalf("Failed to make file unreadable: %v", err)
567
567
+
}
568
568
+
defer os.Chmod(filePath, 0644)
568
569
569
569
-
ctx := context.Background()
570
570
-
err = Create(ctx, []string{filePath})
571
571
-
if err == nil {
572
572
-
t.Error("Create should fail for unreadable file")
573
573
-
}
574
574
-
if !strings.Contains(err.Error(), "failed to read file") {
575
575
-
t.Errorf("Expected file read error, got: %v", err)
576
576
-
}
570
570
+
ctx := context.Background()
571
571
+
err = Create(ctx, []string{filePath})
572
572
+
if err == nil {
573
573
+
t.Error("Create should fail for unreadable file")
574
574
+
}
575
575
+
if !strings.Contains(err.Error(), "failed to read file") {
576
576
+
t.Errorf("Expected file read error, got: %v", err)
577
577
+
}
578
578
+
})
577
579
})
578
578
-
}
579
580
580
580
-
func TestCreate_Interactive(t *testing.T) {
581
581
-
t.Run("handles no editor configured", func(t *testing.T) {
582
582
-
_, cleanup := setupNoteTest(t)
583
583
-
defer cleanup()
581
581
+
t.Run("Interactive Create", func(t *testing.T) {
582
582
+
t.Run("handles no editor configured", func(t *testing.T) {
583
583
+
_, cleanup := setupNoteTest(t)
584
584
+
defer cleanup()
584
585
585
585
-
originalEditor := os.Getenv("EDITOR")
586
586
-
originalPath := os.Getenv("PATH")
587
587
-
os.Unsetenv("EDITOR")
588
588
-
os.Setenv("PATH", "")
589
589
-
defer func() {
590
590
-
os.Setenv("EDITOR", originalEditor)
591
591
-
os.Setenv("PATH", originalPath)
592
592
-
}()
586
586
+
originalEditor := os.Getenv("EDITOR")
587
587
+
originalPath := os.Getenv("PATH")
588
588
+
os.Unsetenv("EDITOR")
589
589
+
os.Setenv("PATH", "")
590
590
+
defer func() {
591
591
+
os.Setenv("EDITOR", originalEditor)
592
592
+
os.Setenv("PATH", originalPath)
593
593
+
}()
593
594
594
594
-
ctx := context.Background()
595
595
-
err := Create(ctx, []string{})
596
596
-
if err == nil {
597
597
-
t.Error("Create should fail when no editor is configured")
598
598
-
}
599
599
-
if !strings.Contains(err.Error(), "no editor configured") {
600
600
-
t.Errorf("Expected no editor error, got: %v", err)
601
601
-
}
602
602
-
})
595
595
+
ctx := context.Background()
596
596
+
err := Create(ctx, []string{})
597
597
+
if err == nil {
598
598
+
t.Error("Create should fail when no editor is configured")
599
599
+
}
600
600
+
if !strings.Contains(err.Error(), "no editor configured") {
601
601
+
t.Errorf("Expected no editor error, got: %v", err)
602
602
+
}
603
603
+
})
603
604
604
604
-
t.Run("handles editor command failure", func(t *testing.T) {
605
605
-
_, cleanup := setupNoteTest(t)
606
606
-
defer cleanup()
605
605
+
t.Run("handles editor command failure", func(t *testing.T) {
606
606
+
_, cleanup := setupNoteTest(t)
607
607
+
defer cleanup()
607
608
608
608
-
originalEditor := os.Getenv("EDITOR")
609
609
-
os.Setenv("EDITOR", "nonexistent-editor-12345")
610
610
-
defer os.Setenv("EDITOR", originalEditor)
609
609
+
originalEditor := os.Getenv("EDITOR")
610
610
+
os.Setenv("EDITOR", "nonexistent-editor-12345")
611
611
+
defer os.Setenv("EDITOR", originalEditor)
611
612
612
612
-
ctx := context.Background()
613
613
-
err := Create(ctx, []string{})
614
614
-
if err == nil {
615
615
-
t.Error("Create should fail when editor command fails")
616
616
-
}
617
617
-
if !strings.Contains(err.Error(), "failed to open editor") {
618
618
-
t.Errorf("Expected editor failure error, got: %v", err)
619
619
-
}
620
620
-
})
613
613
+
ctx := context.Background()
614
614
+
err := Create(ctx, []string{})
615
615
+
if err == nil {
616
616
+
t.Error("Create should fail when editor command fails")
617
617
+
}
618
618
+
if !strings.Contains(err.Error(), "failed to open editor") {
619
619
+
t.Errorf("Expected editor failure error, got: %v", err)
620
620
+
}
621
621
+
})
621
622
622
622
-
t.Run("creates note successfully with mocked editor", func(t *testing.T) {
623
623
-
_, cleanup := setupNoteTest(t)
624
624
-
defer cleanup()
623
623
+
t.Run("creates note successfully with mocked editor", func(t *testing.T) {
624
624
+
_, cleanup := setupNoteTest(t)
625
625
+
defer cleanup()
625
626
626
626
-
originalEditor := os.Getenv("EDITOR")
627
627
-
os.Setenv("EDITOR", "test-editor")
628
628
-
defer os.Setenv("EDITOR", originalEditor)
627
627
+
originalEditor := os.Getenv("EDITOR")
628
628
+
os.Setenv("EDITOR", "test-editor")
629
629
+
defer os.Setenv("EDITOR", originalEditor)
629
630
630
630
-
handler, err := NewNoteHandler()
631
631
-
if err != nil {
632
632
-
t.Fatalf("NewNoteHandler failed: %v", err)
633
633
-
}
634
634
-
defer handler.Close()
631
631
+
handler, err := NewNoteHandler()
632
632
+
if err != nil {
633
633
+
t.Fatalf("NewNoteHandler failed: %v", err)
634
634
+
}
635
635
+
defer handler.Close()
635
636
636
636
-
handler.openInEditorFunc = func(editor, filePath string) error {
637
637
-
content := `# Test Note
637
637
+
handler.openInEditorFunc = func(editor, filePath string) error {
638
638
+
content := `# Test Note
638
639
639
640
This is edited content.
640
641
641
642
<!-- Tags: test, created -->`
642
642
-
return os.WriteFile(filePath, []byte(content), 0644)
643
643
-
}
643
643
+
return os.WriteFile(filePath, []byte(content), 0644)
644
644
+
}
645
645
+
646
646
+
ctx := context.Background()
647
647
+
err = handler.createInteractive(ctx)
648
648
+
if err != nil {
649
649
+
t.Errorf("Interactive create failed: %v", err)
650
650
+
}
651
651
+
})
652
652
+
653
653
+
t.Run("handles editor cancellation", func(t *testing.T) {
654
654
+
_, cleanup := setupNoteTest(t)
655
655
+
defer cleanup()
656
656
+
657
657
+
originalEditor := os.Getenv("EDITOR")
658
658
+
os.Setenv("EDITOR", "test-editor")
659
659
+
defer os.Setenv("EDITOR", originalEditor)
660
660
+
661
661
+
handler, err := NewNoteHandler()
662
662
+
if err != nil {
663
663
+
t.Fatalf("NewNoteHandler failed: %v", err)
664
664
+
}
665
665
+
defer handler.Close()
666
666
+
667
667
+
handler.openInEditorFunc = func(editor, filePath string) error {
668
668
+
return nil
669
669
+
}
644
670
645
645
-
ctx := context.Background()
646
646
-
err = handler.createInteractive(ctx)
647
647
-
if err != nil {
648
648
-
t.Errorf("Interactive create failed: %v", err)
649
649
-
}
671
671
+
ctx := context.Background()
672
672
+
err = handler.createInteractive(ctx)
673
673
+
if err != nil {
674
674
+
t.Errorf("Interactive create should handle cancellation gracefully: %v", err)
675
675
+
}
676
676
+
})
650
677
})
651
678
652
652
-
t.Run("handles editor cancellation", func(t *testing.T) {
679
679
+
t.Run("Close", func(t *testing.T) {
653
680
_, cleanup := setupNoteTest(t)
654
681
defer cleanup()
655
682
656
656
-
originalEditor := os.Getenv("EDITOR")
657
657
-
os.Setenv("EDITOR", "test-editor")
658
658
-
defer os.Setenv("EDITOR", originalEditor)
659
659
-
660
683
handler, err := NewNoteHandler()
661
684
if err != nil {
662
685
t.Fatalf("NewNoteHandler failed: %v", err)
663
686
}
664
664
-
defer handler.Close()
665
687
666
666
-
handler.openInEditorFunc = func(editor, filePath string) error {
667
667
-
return nil
688
688
+
err = handler.Close()
689
689
+
if err != nil {
690
690
+
t.Errorf("Close should not return error: %v", err)
668
691
}
669
692
670
670
-
ctx := context.Background()
671
671
-
err = handler.createInteractive(ctx)
693
693
+
handler.db = nil
694
694
+
err = handler.Close()
672
695
if err != nil {
673
673
-
t.Errorf("Interactive create should handle cancellation gracefully: %v", err)
696
696
+
t.Errorf("Close should handle nil database gracefully: %v", err)
674
697
}
675
698
})
676
676
-
}
677
699
678
678
-
func TestNoteHandlerClosesResources(t *testing.T) {
679
679
-
_, cleanup := setupNoteTest(t)
680
680
-
defer cleanup()
681
681
-
682
682
-
handler, err := NewNoteHandler()
683
683
-
if err != nil {
684
684
-
t.Fatalf("NewNoteHandler failed: %v", err)
685
685
-
}
686
686
-
687
687
-
err = handler.Close()
688
688
-
if err != nil {
689
689
-
t.Errorf("Close should not return error: %v", err)
690
690
-
}
691
691
-
692
692
-
handler.db = nil
693
693
-
err = handler.Close()
694
694
-
if err != nil {
695
695
-
t.Errorf("Close should handle nil database gracefully: %v", err)
696
696
-
}
697
697
-
}
698
698
-
699
699
-
func TestEdit(t *testing.T) {
700
700
-
t.Run("validates argument count", func(t *testing.T) {
701
701
-
_, cleanup := setupNoteTest(t)
702
702
-
defer cleanup()
700
700
+
t.Run("Edit", func(t *testing.T) {
701
701
+
t.Run("validates argument count", func(t *testing.T) {
702
702
+
_, cleanup := setupNoteTest(t)
703
703
+
defer cleanup()
703
704
704
704
-
ctx := context.Background()
705
705
+
ctx := context.Background()
705
706
706
706
-
err := Edit(ctx, []string{})
707
707
-
if err == nil {
708
708
-
t.Error("Edit should fail with no arguments")
709
709
-
}
710
710
-
if !strings.Contains(err.Error(), "edit requires exactly one argument") {
711
711
-
t.Errorf("Expected argument count error, got: %v", err)
712
712
-
}
707
707
+
err := Edit(ctx, []string{})
708
708
+
if err == nil {
709
709
+
t.Error("Edit should fail with no arguments")
710
710
+
}
711
711
+
if !strings.Contains(err.Error(), "edit requires exactly one argument") {
712
712
+
t.Errorf("Expected argument count error, got: %v", err)
713
713
+
}
713
714
714
714
-
err = Edit(ctx, []string{"1", "2"})
715
715
-
if err == nil {
716
716
-
t.Error("Edit should fail with too many arguments")
717
717
-
}
718
718
-
if !strings.Contains(err.Error(), "edit requires exactly one argument") {
719
719
-
t.Errorf("Expected argument count error, got: %v", err)
720
720
-
}
721
721
-
})
715
715
+
err = Edit(ctx, []string{"1", "2"})
716
716
+
if err == nil {
717
717
+
t.Error("Edit should fail with too many arguments")
718
718
+
}
719
719
+
if !strings.Contains(err.Error(), "edit requires exactly one argument") {
720
720
+
t.Errorf("Expected argument count error, got: %v", err)
721
721
+
}
722
722
+
})
722
723
723
723
-
t.Run("validates note ID format", func(t *testing.T) {
724
724
-
_, cleanup := setupNoteTest(t)
725
725
-
defer cleanup()
724
724
+
t.Run("validates note ID format", func(t *testing.T) {
725
725
+
_, cleanup := setupNoteTest(t)
726
726
+
defer cleanup()
726
727
727
727
-
ctx := context.Background()
728
728
+
ctx := context.Background()
728
729
729
729
-
err := Edit(ctx, []string{"invalid"})
730
730
-
if err == nil {
731
731
-
t.Error("Edit should fail with invalid note ID")
732
732
-
}
733
733
-
if !strings.Contains(err.Error(), "invalid note ID") {
734
734
-
t.Errorf("Expected invalid ID error, got: %v", err)
735
735
-
}
730
730
+
err := Edit(ctx, []string{"invalid"})
731
731
+
if err == nil {
732
732
+
t.Error("Edit should fail with invalid note ID")
733
733
+
}
734
734
+
if !strings.Contains(err.Error(), "invalid note ID") {
735
735
+
t.Errorf("Expected invalid ID error, got: %v", err)
736
736
+
}
736
737
737
737
-
err = Edit(ctx, []string{"-1"})
738
738
-
if err == nil {
739
739
-
t.Error("Edit should fail with negative note ID")
740
740
-
}
738
738
+
err = Edit(ctx, []string{"-1"})
739
739
+
if err == nil {
740
740
+
t.Error("Edit should fail with negative note ID")
741
741
+
}
741
742
742
742
-
if !strings.Contains(err.Error(), "failed to get note") {
743
743
-
t.Errorf("Expected note not found error for negative ID, got: %v", err)
744
744
-
}
745
745
-
})
743
743
+
if !strings.Contains(err.Error(), "failed to get note") {
744
744
+
t.Errorf("Expected note not found error for negative ID, got: %v", err)
745
745
+
}
746
746
+
})
746
747
747
747
-
t.Run("handles non-existent note", func(t *testing.T) {
748
748
-
_, cleanup := setupNoteTest(t)
749
749
-
defer cleanup()
748
748
+
t.Run("handles non-existent note", func(t *testing.T) {
749
749
+
_, cleanup := setupNoteTest(t)
750
750
+
defer cleanup()
750
751
751
751
-
ctx := context.Background()
752
752
+
ctx := context.Background()
752
753
753
753
-
err := Edit(ctx, []string{"999"})
754
754
-
if err == nil {
755
755
-
t.Error("Edit should fail with non-existent note ID")
756
756
-
}
757
757
-
if !strings.Contains(err.Error(), "failed to get note") {
758
758
-
t.Errorf("Expected note not found error, got: %v", err)
759
759
-
}
760
760
-
})
754
754
+
err := Edit(ctx, []string{"999"})
755
755
+
if err == nil {
756
756
+
t.Error("Edit should fail with non-existent note ID")
757
757
+
}
758
758
+
if !strings.Contains(err.Error(), "failed to get note") {
759
759
+
t.Errorf("Expected note not found error, got: %v", err)
760
760
+
}
761
761
+
})
761
762
762
762
-
t.Run("handles no editor configured", func(t *testing.T) {
763
763
-
_, cleanup := setupNoteTest(t)
764
764
-
defer cleanup()
763
763
+
t.Run("handles no editor configured", func(t *testing.T) {
764
764
+
_, cleanup := setupNoteTest(t)
765
765
+
defer cleanup()
765
766
766
766
-
originalEditor := os.Getenv("EDITOR")
767
767
-
originalPath := os.Getenv("PATH")
768
768
-
os.Setenv("EDITOR", "")
769
769
-
os.Setenv("PATH", "")
770
770
-
defer func() {
771
771
-
os.Setenv("EDITOR", originalEditor)
772
772
-
os.Setenv("PATH", originalPath)
773
773
-
}()
767
767
+
originalEditor := os.Getenv("EDITOR")
768
768
+
originalPath := os.Getenv("PATH")
769
769
+
os.Setenv("EDITOR", "")
770
770
+
os.Setenv("PATH", "")
771
771
+
defer func() {
772
772
+
os.Setenv("EDITOR", originalEditor)
773
773
+
os.Setenv("PATH", originalPath)
774
774
+
}()
774
775
775
775
-
ctx := context.Background()
776
776
+
ctx := context.Background()
776
777
777
777
-
err := Create(ctx, []string{"Test Note", "Test content"})
778
778
-
if err != nil {
779
779
-
t.Fatalf("Failed to create test note: %v", err)
780
780
-
}
778
778
+
err := Create(ctx, []string{"Test Note", "Test content"})
779
779
+
if err != nil {
780
780
+
t.Fatalf("Failed to create test note: %v", err)
781
781
+
}
781
782
782
782
-
err = Edit(ctx, []string{"1"})
783
783
-
if err == nil {
784
784
-
t.Error("Edit should fail when no editor is configured")
785
785
-
}
783
783
+
err = Edit(ctx, []string{"1"})
784
784
+
if err == nil {
785
785
+
t.Error("Edit should fail when no editor is configured")
786
786
+
}
786
787
787
787
-
if !strings.Contains(err.Error(), "no editor configured") && !strings.Contains(err.Error(), "failed to open editor") {
788
788
-
t.Errorf("Expected no editor or editor failure error, got: %v", err)
789
789
-
}
790
790
-
})
788
788
+
if !strings.Contains(err.Error(), "no editor configured") && !strings.Contains(err.Error(), "failed to open editor") {
789
789
+
t.Errorf("Expected no editor or editor failure error, got: %v", err)
790
790
+
}
791
791
+
})
791
792
792
792
-
t.Run("handles editor command failure", func(t *testing.T) {
793
793
-
_, cleanup := setupNoteTest(t)
794
794
-
defer cleanup()
793
793
+
t.Run("handles editor command failure", func(t *testing.T) {
794
794
+
_, cleanup := setupNoteTest(t)
795
795
+
defer cleanup()
795
796
796
796
-
originalEditor := os.Getenv("EDITOR")
797
797
-
os.Setenv("EDITOR", "nonexistent-editor-12345")
798
798
-
defer os.Setenv("EDITOR", originalEditor)
797
797
+
originalEditor := os.Getenv("EDITOR")
798
798
+
os.Setenv("EDITOR", "nonexistent-editor-12345")
799
799
+
defer os.Setenv("EDITOR", originalEditor)
799
800
800
800
-
ctx := context.Background()
801
801
+
ctx := context.Background()
801
802
802
802
-
err := Create(ctx, []string{"Test Note", "Test content"})
803
803
-
if err != nil {
804
804
-
t.Fatalf("Failed to create test note: %v", err)
805
805
-
}
803
803
+
err := Create(ctx, []string{"Test Note", "Test content"})
804
804
+
if err != nil {
805
805
+
t.Fatalf("Failed to create test note: %v", err)
806
806
+
}
806
807
807
807
-
err = Edit(ctx, []string{"1"})
808
808
-
if err == nil {
809
809
-
t.Error("Edit should fail when editor command fails")
810
810
-
}
811
811
-
if !strings.Contains(err.Error(), "failed to open editor") {
812
812
-
t.Errorf("Expected editor failure error, got: %v", err)
813
813
-
}
814
814
-
})
808
808
+
err = Edit(ctx, []string{"1"})
809
809
+
if err == nil {
810
810
+
t.Error("Edit should fail when editor command fails")
811
811
+
}
812
812
+
if !strings.Contains(err.Error(), "failed to open editor") {
813
813
+
t.Errorf("Expected editor failure error, got: %v", err)
814
814
+
}
815
815
+
})
815
816
816
816
-
t.Run("edits note successfully with mocked editor", func(t *testing.T) {
817
817
-
_, cleanup := setupNoteTest(t)
818
818
-
defer cleanup()
817
817
+
t.Run("edits note successfully with mocked editor", func(t *testing.T) {
818
818
+
_, cleanup := setupNoteTest(t)
819
819
+
defer cleanup()
819
820
820
820
-
originalEditor := os.Getenv("EDITOR")
821
821
-
os.Setenv("EDITOR", "test-editor")
822
822
-
defer os.Setenv("EDITOR", originalEditor)
821
821
+
originalEditor := os.Getenv("EDITOR")
822
822
+
os.Setenv("EDITOR", "test-editor")
823
823
+
defer os.Setenv("EDITOR", originalEditor)
823
824
824
824
-
ctx := context.Background()
825
825
+
ctx := context.Background()
825
826
826
826
-
err := Create(ctx, []string{"Original Title", "Original content"})
827
827
-
if err != nil {
828
828
-
t.Fatalf("Failed to create test note: %v", err)
829
829
-
}
827
827
+
err := Create(ctx, []string{"Original Title", "Original content"})
828
828
+
if err != nil {
829
829
+
t.Fatalf("Failed to create test note: %v", err)
830
830
+
}
830
831
831
831
-
handler, err := NewNoteHandler()
832
832
-
if err != nil {
833
833
-
t.Fatalf("NewNoteHandler failed: %v", err)
834
834
-
}
835
835
-
defer handler.Close()
832
832
+
handler, err := NewNoteHandler()
833
833
+
if err != nil {
834
834
+
t.Fatalf("NewNoteHandler failed: %v", err)
835
835
+
}
836
836
+
defer handler.Close()
836
837
837
837
-
handler.openInEditorFunc = func(editor, filePath string) error {
838
838
-
newContent := `# Updated Title
838
838
+
handler.openInEditorFunc = func(editor, filePath string) error {
839
839
+
newContent := `# Updated Title
839
840
840
841
This is updated content.
841
842
842
843
<!-- Tags: updated, test -->`
843
843
-
return os.WriteFile(filePath, []byte(newContent), 0644)
844
844
-
}
844
844
+
return os.WriteFile(filePath, []byte(newContent), 0644)
845
845
+
}
845
846
846
846
-
err = handler.editNote(ctx, 1)
847
847
-
if err != nil {
848
848
-
t.Errorf("Edit should succeed with mocked editor: %v", err)
849
849
-
}
847
847
+
err = handler.editNote(ctx, 1)
848
848
+
if err != nil {
849
849
+
t.Errorf("Edit should succeed with mocked editor: %v", err)
850
850
+
}
850
851
851
851
-
note, err := handler.repos.Notes.Get(ctx, 1)
852
852
-
if err != nil {
853
853
-
t.Fatalf("Failed to get updated note: %v", err)
854
854
-
}
852
852
+
note, err := handler.repos.Notes.Get(ctx, 1)
853
853
+
if err != nil {
854
854
+
t.Fatalf("Failed to get updated note: %v", err)
855
855
+
}
855
856
856
856
-
if note.Title != "Updated Title" {
857
857
-
t.Errorf("Expected title 'Updated Title', got %q", note.Title)
858
858
-
}
857
857
+
if note.Title != "Updated Title" {
858
858
+
t.Errorf("Expected title 'Updated Title', got %q", note.Title)
859
859
+
}
859
860
860
860
-
if !strings.Contains(note.Content, "This is updated content") {
861
861
-
t.Errorf("Expected content to contain 'This is updated content', got %q", note.Content)
862
862
-
}
861
861
+
if !strings.Contains(note.Content, "This is updated content") {
862
862
+
t.Errorf("Expected content to contain 'This is updated content', got %q", note.Content)
863
863
+
}
863
864
864
864
-
expectedTags := []string{"updated", "test"}
865
865
-
if len(note.Tags) != len(expectedTags) {
866
866
-
t.Errorf("Expected %d tags, got %d", len(expectedTags), len(note.Tags))
867
867
-
}
868
868
-
for i, tag := range expectedTags {
869
869
-
if i >= len(note.Tags) || note.Tags[i] != tag {
870
870
-
t.Errorf("Expected tag %q at index %d, got %q", tag, i, note.Tags[i])
865
865
+
expectedTags := []string{"updated", "test"}
866
866
+
if len(note.Tags) != len(expectedTags) {
867
867
+
t.Errorf("Expected %d tags, got %d", len(expectedTags), len(note.Tags))
868
868
+
}
869
869
+
for i, tag := range expectedTags {
870
870
+
if i >= len(note.Tags) || note.Tags[i] != tag {
871
871
+
t.Errorf("Expected tag %q at index %d, got %q", tag, i, note.Tags[i])
872
872
+
}
871
873
}
872
872
-
}
873
873
-
})
874
874
+
})
874
875
875
875
-
t.Run("handles editor cancellation (no changes)", func(t *testing.T) {
876
876
-
_, cleanup := setupNoteTest(t)
877
877
-
defer cleanup()
876
876
+
t.Run("handles editor cancellation (no changes)", func(t *testing.T) {
877
877
+
_, cleanup := setupNoteTest(t)
878
878
+
defer cleanup()
878
879
879
879
-
originalEditor := os.Getenv("EDITOR")
880
880
-
os.Setenv("EDITOR", "test-editor")
881
881
-
defer os.Setenv("EDITOR", originalEditor)
880
880
+
originalEditor := os.Getenv("EDITOR")
881
881
+
os.Setenv("EDITOR", "test-editor")
882
882
+
defer os.Setenv("EDITOR", originalEditor)
882
883
883
883
-
ctx := context.Background()
884
884
+
ctx := context.Background()
884
885
885
885
-
err := Create(ctx, []string{"Test Note", "Test content"})
886
886
-
if err != nil {
887
887
-
t.Fatalf("Failed to create test note: %v", err)
888
888
-
}
886
886
+
err := Create(ctx, []string{"Test Note", "Test content"})
887
887
+
if err != nil {
888
888
+
t.Fatalf("Failed to create test note: %v", err)
889
889
+
}
889
890
890
890
-
handler, err := NewNoteHandler()
891
891
-
if err != nil {
892
892
-
t.Fatalf("NewNoteHandler failed: %v", err)
893
893
-
}
894
894
-
defer handler.Close()
891
891
+
handler, err := NewNoteHandler()
892
892
+
if err != nil {
893
893
+
t.Fatalf("NewNoteHandler failed: %v", err)
894
894
+
}
895
895
+
defer handler.Close()
895
896
896
896
-
handler.openInEditorFunc = func(editor, filePath string) error {
897
897
-
return nil
898
898
-
}
897
897
+
handler.openInEditorFunc = func(editor, filePath string) error {
898
898
+
return nil
899
899
+
}
899
900
900
900
-
err = handler.editNote(ctx, 1)
901
901
-
if err != nil {
902
902
-
t.Errorf("Edit should handle cancellation gracefully: %v", err)
903
903
-
}
901
901
+
err = handler.editNote(ctx, 1)
902
902
+
if err != nil {
903
903
+
t.Errorf("Edit should handle cancellation gracefully: %v", err)
904
904
+
}
904
905
905
905
-
note, err := handler.repos.Notes.Get(ctx, 1)
906
906
-
if err != nil {
907
907
-
t.Fatalf("Failed to get note: %v", err)
908
908
-
}
906
906
+
note, err := handler.repos.Notes.Get(ctx, 1)
907
907
+
if err != nil {
908
908
+
t.Fatalf("Failed to get note: %v", err)
909
909
+
}
909
910
910
910
-
if note.Title != "Test Note" {
911
911
-
t.Errorf("Expected title 'Test Note', got %q", note.Title)
912
912
-
}
911
911
+
if note.Title != "Test Note" {
912
912
+
t.Errorf("Expected title 'Test Note', got %q", note.Title)
913
913
+
}
913
914
914
914
-
if note.Content != "Test content" {
915
915
-
t.Errorf("Expected content 'Test content', got %q", note.Content)
916
916
-
}
915
915
+
if note.Content != "Test content" {
916
916
+
t.Errorf("Expected content 'Test content', got %q", note.Content)
917
917
+
}
918
918
+
})
917
919
})
918
920
}
+564
-582
cmd/handlers/tasks_test.go
···
37
37
return tempDir, cleanup
38
38
}
39
39
40
40
-
func TestTaskHandler_NewTaskHandler(t *testing.T) {
41
41
-
t.Run("creates handler successfully", func(t *testing.T) {
42
42
-
_, cleanup := setupTaskTest(t)
43
43
-
defer cleanup()
40
40
+
func TestTaskHandler(t *testing.T) {
41
41
+
t.Run("New", func(t *testing.T) {
42
42
+
t.Run("creates handler successfully", func(t *testing.T) {
43
43
+
_, cleanup := setupTaskTest(t)
44
44
+
defer cleanup()
44
45
45
45
-
handler, err := NewTaskHandler()
46
46
-
if err != nil {
47
47
-
t.Errorf("NewTaskHandler failed: %v", err)
48
48
-
}
49
49
-
if handler == nil {
50
50
-
t.Error("Handler should not be nil")
51
51
-
}
52
52
-
defer handler.Close()
46
46
+
handler, err := NewTaskHandler()
47
47
+
if err != nil {
48
48
+
t.Fatalf("NewTaskHandler failed: %v", err)
49
49
+
}
50
50
+
if handler == nil {
51
51
+
t.Fatal("Handler should not be nil")
52
52
+
}
53
53
+
defer handler.Close()
53
54
54
54
-
if handler.db == nil {
55
55
-
t.Error("Handler database should not be nil")
56
56
-
}
57
57
-
if handler.config == nil {
58
58
-
t.Error("Handler config should not be nil")
59
59
-
}
60
60
-
if handler.repos == nil {
61
61
-
t.Error("Handler repos should not be nil")
62
62
-
}
63
63
-
})
55
55
+
if handler.db == nil {
56
56
+
t.Error("Handler database should not be nil")
57
57
+
}
58
58
+
if handler.config == nil {
59
59
+
t.Error("Handler config should not be nil")
60
60
+
}
61
61
+
if handler.repos == nil {
62
62
+
t.Error("Handler repos should not be nil")
63
63
+
}
64
64
+
})
64
65
65
65
-
t.Run("handles database initialization error", func(t *testing.T) {
66
66
-
originalXDG := os.Getenv("XDG_CONFIG_HOME")
67
67
-
originalHome := os.Getenv("HOME")
66
66
+
t.Run("handles database initialization error", func(t *testing.T) {
67
67
+
originalXDG := os.Getenv("XDG_CONFIG_HOME")
68
68
+
originalHome := os.Getenv("HOME")
68
69
69
69
-
if runtime.GOOS == "windows" {
70
70
-
originalAppData := os.Getenv("APPDATA")
71
71
-
os.Unsetenv("APPDATA")
72
72
-
defer os.Setenv("APPDATA", originalAppData)
73
73
-
} else {
74
74
-
os.Unsetenv("XDG_CONFIG_HOME")
75
75
-
os.Unsetenv("HOME")
76
76
-
defer os.Setenv("XDG_CONFIG_HOME", originalXDG)
77
77
-
defer os.Setenv("HOME", originalHome)
78
78
-
}
70
70
+
if runtime.GOOS == "windows" {
71
71
+
originalAppData := os.Getenv("APPDATA")
72
72
+
os.Unsetenv("APPDATA")
73
73
+
defer os.Setenv("APPDATA", originalAppData)
74
74
+
} else {
75
75
+
os.Unsetenv("XDG_CONFIG_HOME")
76
76
+
os.Unsetenv("HOME")
77
77
+
defer os.Setenv("XDG_CONFIG_HOME", originalXDG)
78
78
+
defer os.Setenv("HOME", originalHome)
79
79
+
}
79
80
80
80
-
handler, err := NewTaskHandler()
81
81
-
if err == nil {
82
82
-
if handler != nil {
83
83
-
handler.Close()
81
81
+
handler, err := NewTaskHandler()
82
82
+
if err == nil {
83
83
+
if handler != nil {
84
84
+
handler.Close()
85
85
+
}
86
86
+
t.Error("Expected error when database initialization fails")
84
87
}
85
85
-
t.Error("Expected error when database initialization fails")
86
86
-
}
88
88
+
})
87
89
})
88
88
-
}
89
90
90
90
-
func TestCreateTask(t *testing.T) {
91
91
-
_, cleanup := setupTaskTest(t)
92
92
-
defer cleanup()
91
91
+
t.Run("Create", func(t *testing.T) {
92
92
+
_, cleanup := setupTaskTest(t)
93
93
+
defer cleanup()
93
94
94
94
-
t.Run("creates task successfully", func(t *testing.T) {
95
95
-
ctx := context.Background()
96
96
-
args := []string{"Buy groceries", "and", "cook dinner"}
95
95
+
t.Run("creates task successfully", func(t *testing.T) {
96
96
+
ctx := context.Background()
97
97
+
args := []string{"Buy groceries", "and", "cook dinner"}
97
98
98
98
-
err := CreateTask(ctx, args)
99
99
-
if err != nil {
100
100
-
t.Errorf("CreateTask failed: %v", err)
101
101
-
}
99
99
+
err := CreateTask(ctx, args)
100
100
+
if err != nil {
101
101
+
t.Errorf("CreateTask failed: %v", err)
102
102
+
}
102
103
103
103
-
// Verify task was created by listing tasks
104
104
-
handler, err := NewTaskHandler()
105
105
-
if err != nil {
106
106
-
t.Fatalf("Failed to create handler: %v", err)
107
107
-
}
108
108
-
defer handler.Close()
104
104
+
handler, err := NewTaskHandler()
105
105
+
if err != nil {
106
106
+
t.Fatalf("Failed to create handler: %v", err)
107
107
+
}
108
108
+
defer handler.Close()
109
109
110
110
-
tasks, err := handler.repos.Tasks.GetPending(ctx)
111
111
-
if err != nil {
112
112
-
t.Fatalf("Failed to get pending tasks: %v", err)
113
113
-
}
110
110
+
tasks, err := handler.repos.Tasks.GetPending(ctx)
111
111
+
if err != nil {
112
112
+
t.Fatalf("Failed to get pending tasks: %v", err)
113
113
+
}
114
114
115
115
-
if len(tasks) != 1 {
116
116
-
t.Errorf("Expected 1 task, got %d", len(tasks))
117
117
-
}
115
115
+
if len(tasks) != 1 {
116
116
+
t.Errorf("Expected 1 task, got %d", len(tasks))
117
117
+
}
118
118
119
119
-
task := tasks[0]
120
120
-
expectedDesc := "Buy groceries and cook dinner"
121
121
-
if task.Description != expectedDesc {
122
122
-
t.Errorf("Expected description '%s', got '%s'", expectedDesc, task.Description)
123
123
-
}
119
119
+
task := tasks[0]
120
120
+
expectedDesc := "Buy groceries and cook dinner"
121
121
+
if task.Description != expectedDesc {
122
122
+
t.Errorf("Expected description '%s', got '%s'", expectedDesc, task.Description)
123
123
+
}
124
124
125
125
-
if task.Status != "pending" {
126
126
-
t.Errorf("Expected status 'pending', got '%s'", task.Status)
127
127
-
}
125
125
+
if task.Status != "pending" {
126
126
+
t.Errorf("Expected status 'pending', got '%s'", task.Status)
127
127
+
}
128
128
129
129
-
if task.UUID == "" {
130
130
-
t.Error("Task should have a UUID")
131
131
-
}
132
132
-
})
129
129
+
if task.UUID == "" {
130
130
+
t.Error("Task should have a UUID")
131
131
+
}
132
132
+
})
133
133
134
134
-
t.Run("fails with empty description", func(t *testing.T) {
135
135
-
ctx := context.Background()
136
136
-
args := []string{}
134
134
+
t.Run("fails with empty description", func(t *testing.T) {
135
135
+
ctx := context.Background()
136
136
+
args := []string{}
137
137
138
138
-
err := CreateTask(ctx, args)
139
139
-
if err == nil {
140
140
-
t.Error("Expected error for empty description")
141
141
-
}
138
138
+
err := CreateTask(ctx, args)
139
139
+
if err == nil {
140
140
+
t.Error("Expected error for empty description")
141
141
+
}
142
142
143
143
-
if !strings.Contains(err.Error(), "task description required") {
144
144
-
t.Errorf("Expected error about required description, got: %v", err)
145
145
-
}
143
143
+
if !strings.Contains(err.Error(), "task description required") {
144
144
+
t.Errorf("Expected error about required description, got: %v", err)
145
145
+
}
146
146
+
})
146
147
})
147
147
-
}
148
148
-
149
149
-
func TestListTasks(t *testing.T) {
150
150
-
_, cleanup := setupTaskTest(t)
151
151
-
defer cleanup()
152
148
153
153
-
ctx := context.Background()
154
154
-
155
155
-
// Create test tasks
156
156
-
handler, err := NewTaskHandler()
157
157
-
if err != nil {
158
158
-
t.Fatalf("Failed to create handler: %v", err)
159
159
-
}
160
160
-
defer handler.Close()
161
161
-
162
162
-
// Create a pending task
163
163
-
task1 := &models.Task{
164
164
-
UUID: uuid.New().String(),
165
165
-
Description: "Task 1",
166
166
-
Status: "pending",
167
167
-
Priority: "A",
168
168
-
Project: "work",
169
169
-
}
170
170
-
_, err = handler.repos.Tasks.Create(ctx, task1)
171
171
-
if err != nil {
172
172
-
t.Fatalf("Failed to create task1: %v", err)
173
173
-
}
149
149
+
t.Run("List", func(t *testing.T) {
150
150
+
_, cleanup := setupTaskTest(t)
151
151
+
defer cleanup()
174
152
175
175
-
// Create a completed task
176
176
-
task2 := &models.Task{
177
177
-
UUID: uuid.New().String(),
178
178
-
Description: "Task 2",
179
179
-
Status: "completed",
180
180
-
}
181
181
-
_, err = handler.repos.Tasks.Create(ctx, task2)
182
182
-
if err != nil {
183
183
-
t.Fatalf("Failed to create task2: %v", err)
184
184
-
}
153
153
+
ctx := context.Background()
185
154
186
186
-
t.Run("lists pending tasks by default", func(t *testing.T) {
187
187
-
args := []string{}
188
188
-
189
189
-
err := ListTasks(ctx, args)
155
155
+
handler, err := NewTaskHandler()
190
156
if err != nil {
191
191
-
t.Errorf("ListTasks failed: %v", err)
157
157
+
t.Fatalf("Failed to create handler: %v", err)
192
158
}
193
193
-
})
194
194
-
195
195
-
t.Run("filters by status", func(t *testing.T) {
196
196
-
args := []string{"--status", "completed"}
159
159
+
defer handler.Close()
197
160
198
198
-
err := ListTasks(ctx, args)
199
199
-
if err != nil {
200
200
-
t.Errorf("ListTasks with status filter failed: %v", err)
161
161
+
task1 := &models.Task{
162
162
+
UUID: uuid.New().String(),
163
163
+
Description: "Task 1",
164
164
+
Status: "pending",
165
165
+
Priority: "A",
166
166
+
Project: "work",
201
167
}
202
202
-
})
203
203
-
204
204
-
t.Run("filters by priority", func(t *testing.T) {
205
205
-
args := []string{"--priority", "A"}
206
206
-
207
207
-
err := ListTasks(ctx, args)
168
168
+
_, err = handler.repos.Tasks.Create(ctx, task1)
208
169
if err != nil {
209
209
-
t.Errorf("ListTasks with priority filter failed: %v", err)
170
170
+
t.Fatalf("Failed to create task1: %v", err)
210
171
}
211
211
-
})
212
172
213
213
-
t.Run("filters by project", func(t *testing.T) {
214
214
-
args := []string{"--project", "work"}
215
215
-
216
216
-
err := ListTasks(ctx, args)
173
173
+
task2 := &models.Task{
174
174
+
UUID: uuid.New().String(),
175
175
+
Description: "Task 2",
176
176
+
Status: "completed",
177
177
+
}
178
178
+
_, err = handler.repos.Tasks.Create(ctx, task2)
217
179
if err != nil {
218
218
-
t.Errorf("ListTasks with project filter failed: %v", err)
180
180
+
t.Fatalf("Failed to create task2: %v", err)
219
181
}
220
220
-
})
221
182
222
222
-
t.Run("searches tasks", func(t *testing.T) {
223
223
-
args := []string{"--search", "Task"}
183
183
+
t.Run("lists pending tasks by default", func(t *testing.T) {
184
184
+
args := []string{}
224
185
225
225
-
err := ListTasks(ctx, args)
226
226
-
if err != nil {
227
227
-
t.Errorf("ListTasks with search failed: %v", err)
228
228
-
}
229
229
-
})
186
186
+
err := ListTasks(ctx, args)
187
187
+
if err != nil {
188
188
+
t.Errorf("ListTasks failed: %v", err)
189
189
+
}
190
190
+
})
230
191
231
231
-
t.Run("limits results", func(t *testing.T) {
232
232
-
args := []string{"--limit", "1"}
192
192
+
t.Run("filters by status", func(t *testing.T) {
193
193
+
args := []string{"--status", "completed"}
233
194
234
234
-
err := ListTasks(ctx, args)
235
235
-
if err != nil {
236
236
-
t.Errorf("ListTasks with limit failed: %v", err)
237
237
-
}
238
238
-
})
239
239
-
}
195
195
+
err := ListTasks(ctx, args)
196
196
+
if err != nil {
197
197
+
t.Errorf("ListTasks with status filter failed: %v", err)
198
198
+
}
199
199
+
})
240
200
241
241
-
func TestUpdateTask(t *testing.T) {
242
242
-
_, cleanup := setupTaskTest(t)
243
243
-
defer cleanup()
201
201
+
t.Run("filters by priority", func(t *testing.T) {
202
202
+
args := []string{"--priority", "A"}
244
203
245
245
-
ctx := context.Background()
204
204
+
err := ListTasks(ctx, args)
205
205
+
if err != nil {
206
206
+
t.Errorf("ListTasks with priority filter failed: %v", err)
207
207
+
}
208
208
+
})
246
209
247
247
-
// Create test task
248
248
-
handler, err := NewTaskHandler()
249
249
-
if err != nil {
250
250
-
t.Fatalf("Failed to create handler: %v", err)
251
251
-
}
252
252
-
defer handler.Close()
210
210
+
t.Run("filters by project", func(t *testing.T) {
211
211
+
args := []string{"--project", "work"}
253
212
254
254
-
task := &models.Task{
255
255
-
UUID: uuid.New().String(),
256
256
-
Description: "Original description",
257
257
-
Status: "pending",
258
258
-
}
259
259
-
id, err := handler.repos.Tasks.Create(ctx, task)
260
260
-
if err != nil {
261
261
-
t.Fatalf("Failed to create task: %v", err)
262
262
-
}
213
213
+
err := ListTasks(ctx, args)
214
214
+
if err != nil {
215
215
+
t.Errorf("ListTasks with project filter failed: %v", err)
216
216
+
}
217
217
+
})
263
218
264
264
-
t.Run("updates task by ID", func(t *testing.T) {
265
265
-
args := []string{strconv.FormatInt(id, 10), "--description", "Updated description"}
219
219
+
t.Run("searches tasks", func(t *testing.T) {
220
220
+
args := []string{"--search", "Task"}
266
221
267
267
-
err := UpdateTask(ctx, args)
268
268
-
if err != nil {
269
269
-
t.Errorf("UpdateTask failed: %v", err)
270
270
-
}
222
222
+
err := ListTasks(ctx, args)
223
223
+
if err != nil {
224
224
+
t.Errorf("ListTasks with search failed: %v", err)
225
225
+
}
226
226
+
})
271
227
272
272
-
// Verify update
273
273
-
updatedTask, err := handler.repos.Tasks.Get(ctx, id)
274
274
-
if err != nil {
275
275
-
t.Fatalf("Failed to get updated task: %v", err)
276
276
-
}
228
228
+
t.Run("limits results", func(t *testing.T) {
229
229
+
args := []string{"--limit", "1"}
277
230
278
278
-
if updatedTask.Description != "Updated description" {
279
279
-
t.Errorf("Expected description 'Updated description', got '%s'", updatedTask.Description)
280
280
-
}
231
231
+
err := ListTasks(ctx, args)
232
232
+
if err != nil {
233
233
+
t.Errorf("ListTasks with limit failed: %v", err)
234
234
+
}
235
235
+
})
281
236
})
282
237
283
283
-
t.Run("updates task by UUID", func(t *testing.T) {
284
284
-
args := []string{task.UUID, "--status", "completed"}
238
238
+
t.Run("Update", func(t *testing.T) {
239
239
+
_, cleanup := setupTaskTest(t)
240
240
+
defer cleanup()
241
241
+
242
242
+
ctx := context.Background()
285
243
286
286
-
err := UpdateTask(ctx, args)
244
244
+
// Create test task
245
245
+
handler, err := NewTaskHandler()
287
246
if err != nil {
288
288
-
t.Errorf("UpdateTask by UUID failed: %v", err)
247
247
+
t.Fatalf("Failed to create handler: %v", err)
289
248
}
249
249
+
defer handler.Close()
290
250
291
291
-
// Verify update
292
292
-
updatedTask, err := handler.repos.Tasks.GetByUUID(ctx, task.UUID)
251
251
+
task := &models.Task{
252
252
+
UUID: uuid.New().String(),
253
253
+
Description: "Original description",
254
254
+
Status: "pending",
255
255
+
}
256
256
+
id, err := handler.repos.Tasks.Create(ctx, task)
293
257
if err != nil {
294
294
-
t.Fatalf("Failed to get updated task by UUID: %v", err)
258
258
+
t.Fatalf("Failed to create task: %v", err)
295
259
}
296
260
297
297
-
if updatedTask.Status != "completed" {
298
298
-
t.Errorf("Expected status 'completed', got '%s'", updatedTask.Status)
299
299
-
}
300
300
-
})
261
261
+
t.Run("updates task by ID", func(t *testing.T) {
262
262
+
args := []string{strconv.FormatInt(id, 10), "--description", "Updated description"}
301
263
302
302
-
t.Run("updates multiple fields", func(t *testing.T) {
303
303
-
args := []string{
304
304
-
strconv.FormatInt(id, 10),
305
305
-
"--description", "Multiple updates",
306
306
-
"--priority", "B",
307
307
-
"--project", "test",
308
308
-
"--due", "2024-12-31",
309
309
-
}
264
264
+
err := UpdateTask(ctx, args)
265
265
+
if err != nil {
266
266
+
t.Errorf("UpdateTask failed: %v", err)
267
267
+
}
310
268
311
311
-
err := UpdateTask(ctx, args)
312
312
-
if err != nil {
313
313
-
t.Errorf("UpdateTask with multiple fields failed: %v", err)
314
314
-
}
269
269
+
updatedTask, err := handler.repos.Tasks.Get(ctx, id)
270
270
+
if err != nil {
271
271
+
t.Fatalf("Failed to get updated task: %v", err)
272
272
+
}
315
273
316
316
-
// Verify all updates
317
317
-
updatedTask, err := handler.repos.Tasks.Get(ctx, id)
318
318
-
if err != nil {
319
319
-
t.Fatalf("Failed to get updated task: %v", err)
320
320
-
}
274
274
+
if updatedTask.Description != "Updated description" {
275
275
+
t.Errorf("Expected description 'Updated description', got '%s'", updatedTask.Description)
276
276
+
}
277
277
+
})
321
278
322
322
-
if updatedTask.Description != "Multiple updates" {
323
323
-
t.Errorf("Expected description 'Multiple updates', got '%s'", updatedTask.Description)
324
324
-
}
325
325
-
if updatedTask.Priority != "B" {
326
326
-
t.Errorf("Expected priority 'B', got '%s'", updatedTask.Priority)
327
327
-
}
328
328
-
if updatedTask.Project != "test" {
329
329
-
t.Errorf("Expected project 'test', got '%s'", updatedTask.Project)
330
330
-
}
331
331
-
if updatedTask.Due == nil {
332
332
-
t.Error("Expected due date to be set")
333
333
-
}
334
334
-
})
279
279
+
t.Run("updates task by UUID", func(t *testing.T) {
280
280
+
args := []string{task.UUID, "--status", "completed"}
335
281
336
336
-
t.Run("adds and removes tags", func(t *testing.T) {
337
337
-
args := []string{
338
338
-
strconv.FormatInt(id, 10),
339
339
-
"--add-tag=work",
340
340
-
"--add-tag=urgent",
341
341
-
}
282
282
+
err := UpdateTask(ctx, args)
283
283
+
if err != nil {
284
284
+
t.Errorf("UpdateTask by UUID failed: %v", err)
285
285
+
}
342
286
343
343
-
err := UpdateTask(ctx, args)
344
344
-
if err != nil {
345
345
-
t.Errorf("UpdateTask with add tags failed: %v", err)
346
346
-
}
287
287
+
updatedTask, err := handler.repos.Tasks.GetByUUID(ctx, task.UUID)
288
288
+
if err != nil {
289
289
+
t.Fatalf("Failed to get updated task by UUID: %v", err)
290
290
+
}
347
291
348
348
-
// Verify tags added
349
349
-
updatedTask, err := handler.repos.Tasks.Get(ctx, id)
350
350
-
if err != nil {
351
351
-
t.Fatalf("Failed to get updated task: %v", err)
352
352
-
}
292
292
+
if updatedTask.Status != "completed" {
293
293
+
t.Errorf("Expected status 'completed', got '%s'", updatedTask.Status)
294
294
+
}
295
295
+
})
353
296
354
354
-
if len(updatedTask.Tags) != 2 {
355
355
-
t.Errorf("Expected 2 tags, got %d", len(updatedTask.Tags))
356
356
-
}
297
297
+
t.Run("updates multiple fields", func(t *testing.T) {
298
298
+
args := []string{
299
299
+
strconv.FormatInt(id, 10),
300
300
+
"--description", "Multiple updates",
301
301
+
"--priority", "B",
302
302
+
"--project", "test",
303
303
+
"--due", "2024-12-31",
304
304
+
}
357
305
358
358
-
// Remove a tag
359
359
-
args = []string{
360
360
-
strconv.FormatInt(id, 10),
361
361
-
"--remove-tag=urgent",
362
362
-
}
306
306
+
err := UpdateTask(ctx, args)
307
307
+
if err != nil {
308
308
+
t.Errorf("UpdateTask with multiple fields failed: %v", err)
309
309
+
}
363
310
364
364
-
err = UpdateTask(ctx, args)
365
365
-
if err != nil {
366
366
-
t.Errorf("UpdateTask with remove tag failed: %v", err)
367
367
-
}
311
311
+
// Verify all updates
312
312
+
updatedTask, err := handler.repos.Tasks.Get(ctx, id)
313
313
+
if err != nil {
314
314
+
t.Fatalf("Failed to get updated task: %v", err)
315
315
+
}
368
316
369
369
-
// Verify tag removed
370
370
-
updatedTask, err = handler.repos.Tasks.Get(ctx, id)
371
371
-
if err != nil {
372
372
-
t.Fatalf("Failed to get updated task: %v", err)
373
373
-
}
317
317
+
if updatedTask.Description != "Multiple updates" {
318
318
+
t.Errorf("Expected description 'Multiple updates', got '%s'", updatedTask.Description)
319
319
+
}
320
320
+
if updatedTask.Priority != "B" {
321
321
+
t.Errorf("Expected priority 'B', got '%s'", updatedTask.Priority)
322
322
+
}
323
323
+
if updatedTask.Project != "test" {
324
324
+
t.Errorf("Expected project 'test', got '%s'", updatedTask.Project)
325
325
+
}
326
326
+
if updatedTask.Due == nil {
327
327
+
t.Error("Expected due date to be set")
328
328
+
}
329
329
+
})
374
330
375
375
-
if len(updatedTask.Tags) != 1 {
376
376
-
t.Errorf("Expected 1 tag after removal, got %d", len(updatedTask.Tags))
377
377
-
}
331
331
+
t.Run("adds and removes tags", func(t *testing.T) {
332
332
+
args := []string{
333
333
+
strconv.FormatInt(id, 10),
334
334
+
"--add-tag=work",
335
335
+
"--add-tag=urgent",
336
336
+
}
378
337
379
379
-
if updatedTask.Tags[0] != "work" {
380
380
-
t.Errorf("Expected remaining tag 'work', got '%s'", updatedTask.Tags[0])
381
381
-
}
382
382
-
})
338
338
+
err := UpdateTask(ctx, args)
339
339
+
if err != nil {
340
340
+
t.Errorf("UpdateTask with add tags failed: %v", err)
341
341
+
}
383
342
384
384
-
t.Run("fails with missing task ID", func(t *testing.T) {
385
385
-
args := []string{}
343
343
+
updatedTask, err := handler.repos.Tasks.Get(ctx, id)
344
344
+
if err != nil {
345
345
+
t.Fatalf("Failed to get updated task: %v", err)
346
346
+
}
386
347
387
387
-
err := UpdateTask(ctx, args)
388
388
-
if err == nil {
389
389
-
t.Error("Expected error for missing task ID")
390
390
-
}
348
348
+
if len(updatedTask.Tags) != 2 {
349
349
+
t.Errorf("Expected 2 tags, got %d", len(updatedTask.Tags))
350
350
+
}
391
351
392
392
-
if !strings.Contains(err.Error(), "task ID required") {
393
393
-
t.Errorf("Expected error about required task ID, got: %v", err)
394
394
-
}
395
395
-
})
352
352
+
args = []string{
353
353
+
strconv.FormatInt(id, 10),
354
354
+
"--remove-tag=urgent",
355
355
+
}
396
356
397
397
-
t.Run("fails with invalid task ID", func(t *testing.T) {
398
398
-
args := []string{"99999", "--description", "test"}
357
357
+
err = UpdateTask(ctx, args)
358
358
+
if err != nil {
359
359
+
t.Errorf("UpdateTask with remove tag failed: %v", err)
360
360
+
}
399
361
400
400
-
err := UpdateTask(ctx, args)
401
401
-
if err == nil {
402
402
-
t.Error("Expected error for invalid task ID")
403
403
-
}
362
362
+
updatedTask, err = handler.repos.Tasks.Get(ctx, id)
363
363
+
if err != nil {
364
364
+
t.Fatalf("Failed to get updated task: %v", err)
365
365
+
}
404
366
405
405
-
if !strings.Contains(err.Error(), "failed to find task") {
406
406
-
t.Errorf("Expected error about task not found, got: %v", err)
407
407
-
}
408
408
-
})
409
409
-
}
367
367
+
if len(updatedTask.Tags) != 1 {
368
368
+
t.Errorf("Expected 1 tag after removal, got %d", len(updatedTask.Tags))
369
369
+
}
410
370
411
411
-
func TestDeleteTask(t *testing.T) {
412
412
-
_, cleanup := setupTaskTest(t)
413
413
-
defer cleanup()
371
371
+
if updatedTask.Tags[0] != "work" {
372
372
+
t.Errorf("Expected remaining tag 'work', got '%s'", updatedTask.Tags[0])
373
373
+
}
374
374
+
})
414
375
415
415
-
ctx := context.Background()
376
376
+
t.Run("fails with missing task ID", func(t *testing.T) {
377
377
+
args := []string{}
416
378
417
417
-
// Create test task
418
418
-
handler, err := NewTaskHandler()
419
419
-
if err != nil {
420
420
-
t.Fatalf("Failed to create handler: %v", err)
421
421
-
}
422
422
-
defer handler.Close()
379
379
+
err := UpdateTask(ctx, args)
380
380
+
if err == nil {
381
381
+
t.Error("Expected error for missing task ID")
382
382
+
}
423
383
424
424
-
task := &models.Task{
425
425
-
UUID: uuid.New().String(),
426
426
-
Description: "Task to delete",
427
427
-
Status: "pending",
428
428
-
}
429
429
-
id, err := handler.repos.Tasks.Create(ctx, task)
430
430
-
if err != nil {
431
431
-
t.Fatalf("Failed to create task: %v", err)
432
432
-
}
384
384
+
if !strings.Contains(err.Error(), "task ID required") {
385
385
+
t.Errorf("Expected error about required task ID, got: %v", err)
386
386
+
}
387
387
+
})
433
388
434
434
-
t.Run("deletes task by ID", func(t *testing.T) {
435
435
-
args := []string{strconv.FormatInt(id, 10)}
389
389
+
t.Run("fails with invalid task ID", func(t *testing.T) {
390
390
+
args := []string{"99999", "--description", "test"}
436
391
437
437
-
err := DeleteTask(ctx, args)
438
438
-
if err != nil {
439
439
-
t.Errorf("DeleteTask failed: %v", err)
440
440
-
}
392
392
+
err := UpdateTask(ctx, args)
393
393
+
if err == nil {
394
394
+
t.Error("Expected error for invalid task ID")
395
395
+
}
441
396
442
442
-
// Verify task was deleted
443
443
-
_, err = handler.repos.Tasks.Get(ctx, id)
444
444
-
if err == nil {
445
445
-
t.Error("Expected error when getting deleted task")
446
446
-
}
397
397
+
if !strings.Contains(err.Error(), "failed to find task") {
398
398
+
t.Errorf("Expected error about task not found, got: %v", err)
399
399
+
}
400
400
+
})
447
401
})
448
402
449
449
-
t.Run("deletes task by UUID", func(t *testing.T) {
450
450
-
// Create another task to delete by UUID
451
451
-
task2 := &models.Task{
403
403
+
t.Run("Delete", func(t *testing.T) {
404
404
+
_, cleanup := setupTaskTest(t)
405
405
+
defer cleanup()
406
406
+
407
407
+
ctx := context.Background()
408
408
+
409
409
+
handler, err := NewTaskHandler()
410
410
+
if err != nil {
411
411
+
t.Fatalf("Failed to create handler: %v", err)
412
412
+
}
413
413
+
defer handler.Close()
414
414
+
415
415
+
task := &models.Task{
452
416
UUID: uuid.New().String(),
453
453
-
Description: "Task to delete by UUID",
417
417
+
Description: "Task to delete",
454
418
Status: "pending",
455
419
}
456
456
-
_, err := handler.repos.Tasks.Create(ctx, task2)
420
420
+
id, err := handler.repos.Tasks.Create(ctx, task)
457
421
if err != nil {
458
458
-
t.Fatalf("Failed to create task2: %v", err)
422
422
+
t.Fatalf("Failed to create task: %v", err)
459
423
}
460
424
461
461
-
args := []string{task2.UUID}
425
425
+
t.Run("deletes task by ID", func(t *testing.T) {
426
426
+
args := []string{strconv.FormatInt(id, 10)}
462
427
463
463
-
err = DeleteTask(ctx, args)
464
464
-
if err != nil {
465
465
-
t.Errorf("DeleteTask by UUID failed: %v", err)
466
466
-
}
428
428
+
err := DeleteTask(ctx, args)
429
429
+
if err != nil {
430
430
+
t.Errorf("DeleteTask failed: %v", err)
431
431
+
}
467
432
468
468
-
// Verify task was deleted
469
469
-
_, err = handler.repos.Tasks.GetByUUID(ctx, task2.UUID)
470
470
-
if err == nil {
471
471
-
t.Error("Expected error when getting deleted task by UUID")
472
472
-
}
473
473
-
})
433
433
+
_, err = handler.repos.Tasks.Get(ctx, id)
434
434
+
if err == nil {
435
435
+
t.Error("Expected error when getting deleted task")
436
436
+
}
437
437
+
})
474
438
475
475
-
t.Run("fails with missing task ID", func(t *testing.T) {
476
476
-
args := []string{}
439
439
+
t.Run("deletes task by UUID", func(t *testing.T) {
440
440
+
task2 := &models.Task{
441
441
+
UUID: uuid.New().String(),
442
442
+
Description: "Task to delete by UUID",
443
443
+
Status: "pending",
444
444
+
}
445
445
+
_, err := handler.repos.Tasks.Create(ctx, task2)
446
446
+
if err != nil {
447
447
+
t.Fatalf("Failed to create task2: %v", err)
448
448
+
}
477
449
478
478
-
err := DeleteTask(ctx, args)
479
479
-
if err == nil {
480
480
-
t.Error("Expected error for missing task ID")
481
481
-
}
450
450
+
args := []string{task2.UUID}
482
451
483
483
-
if !strings.Contains(err.Error(), "task ID required") {
484
484
-
t.Errorf("Expected error about required task ID, got: %v", err)
485
485
-
}
486
486
-
})
452
452
+
err = DeleteTask(ctx, args)
453
453
+
if err != nil {
454
454
+
t.Errorf("DeleteTask by UUID failed: %v", err)
455
455
+
}
487
456
488
488
-
t.Run("fails with invalid task ID", func(t *testing.T) {
489
489
-
args := []string{"99999"}
457
457
+
_, err = handler.repos.Tasks.GetByUUID(ctx, task2.UUID)
458
458
+
if err == nil {
459
459
+
t.Error("Expected error when getting deleted task by UUID")
460
460
+
}
461
461
+
})
490
462
491
491
-
err := DeleteTask(ctx, args)
492
492
-
if err == nil {
493
493
-
t.Error("Expected error for invalid task ID")
494
494
-
}
463
463
+
t.Run("fails with missing task ID", func(t *testing.T) {
464
464
+
args := []string{}
495
465
496
496
-
if !strings.Contains(err.Error(), "failed to find task") {
497
497
-
t.Errorf("Expected error about task not found, got: %v", err)
498
498
-
}
499
499
-
})
500
500
-
}
466
466
+
err := DeleteTask(ctx, args)
467
467
+
if err == nil {
468
468
+
t.Error("Expected error for missing task ID")
469
469
+
}
501
470
502
502
-
func TestViewTask(t *testing.T) {
503
503
-
_, cleanup := setupTaskTest(t)
504
504
-
defer cleanup()
471
471
+
if !strings.Contains(err.Error(), "task ID required") {
472
472
+
t.Errorf("Expected error about required task ID, got: %v", err)
473
473
+
}
474
474
+
})
505
475
506
506
-
ctx := context.Background()
476
476
+
t.Run("fails with invalid task ID", func(t *testing.T) {
477
477
+
args := []string{"99999"}
507
478
508
508
-
// Create test task
509
509
-
handler, err := NewTaskHandler()
510
510
-
if err != nil {
511
511
-
t.Fatalf("Failed to create handler: %v", err)
512
512
-
}
513
513
-
defer handler.Close()
479
479
+
err := DeleteTask(ctx, args)
480
480
+
if err == nil {
481
481
+
t.Error("Expected error for invalid task ID")
482
482
+
}
514
483
515
515
-
now := time.Now()
516
516
-
task := &models.Task{
517
517
-
UUID: uuid.New().String(),
518
518
-
Description: "Task to view",
519
519
-
Status: "pending",
520
520
-
Priority: "A",
521
521
-
Project: "test",
522
522
-
Tags: []string{"work", "important"},
523
523
-
Entry: now,
524
524
-
Modified: now,
525
525
-
}
526
526
-
id, err := handler.repos.Tasks.Create(ctx, task)
527
527
-
if err != nil {
528
528
-
t.Fatalf("Failed to create task: %v", err)
529
529
-
}
530
530
-
531
531
-
t.Run("views task by ID", func(t *testing.T) {
532
532
-
args := []string{strconv.FormatInt(id, 10)}
533
533
-
534
534
-
err := ViewTask(ctx, args)
535
535
-
if err != nil {
536
536
-
t.Errorf("ViewTask failed: %v", err)
537
537
-
}
484
484
+
if !strings.Contains(err.Error(), "failed to find task") {
485
485
+
t.Errorf("Expected error about task not found, got: %v", err)
486
486
+
}
487
487
+
})
538
488
})
539
489
540
540
-
t.Run("views task by UUID", func(t *testing.T) {
541
541
-
args := []string{task.UUID}
490
490
+
t.Run("View", func(t *testing.T) {
491
491
+
_, cleanup := setupTaskTest(t)
492
492
+
defer cleanup()
542
493
543
543
-
err := ViewTask(ctx, args)
494
494
+
ctx := context.Background()
495
495
+
496
496
+
handler, err := NewTaskHandler()
544
497
if err != nil {
545
545
-
t.Errorf("ViewTask by UUID failed: %v", err)
498
498
+
t.Fatalf("Failed to create handler: %v", err)
546
499
}
547
547
-
})
548
548
-
549
549
-
t.Run("fails with missing task ID", func(t *testing.T) {
550
550
-
args := []string{}
500
500
+
defer handler.Close()
551
501
552
552
-
err := ViewTask(ctx, args)
553
553
-
if err == nil {
554
554
-
t.Error("Expected error for missing task ID")
502
502
+
now := time.Now()
503
503
+
task := &models.Task{
504
504
+
UUID: uuid.New().String(),
505
505
+
Description: "Task to view",
506
506
+
Status: "pending",
507
507
+
Priority: "A",
508
508
+
Project: "test",
509
509
+
Tags: []string{"work", "important"},
510
510
+
Entry: now,
511
511
+
Modified: now,
555
512
}
556
556
-
557
557
-
if !strings.Contains(err.Error(), "task ID required") {
558
558
-
t.Errorf("Expected error about required task ID, got: %v", err)
513
513
+
id, err := handler.repos.Tasks.Create(ctx, task)
514
514
+
if err != nil {
515
515
+
t.Fatalf("Failed to create task: %v", err)
559
516
}
560
560
-
})
561
517
562
562
-
t.Run("fails with invalid task ID", func(t *testing.T) {
563
563
-
args := []string{"99999"}
518
518
+
t.Run("views task by ID", func(t *testing.T) {
519
519
+
args := []string{strconv.FormatInt(id, 10)}
564
520
565
565
-
err := ViewTask(ctx, args)
566
566
-
if err == nil {
567
567
-
t.Error("Expected error for invalid task ID")
568
568
-
}
521
521
+
err := ViewTask(ctx, args)
522
522
+
if err != nil {
523
523
+
t.Errorf("ViewTask failed: %v", err)
524
524
+
}
525
525
+
})
569
526
570
570
-
if !strings.Contains(err.Error(), "failed to find task") {
571
571
-
t.Errorf("Expected error about task not found, got: %v", err)
572
572
-
}
573
573
-
})
574
574
-
}
527
527
+
t.Run("views task by UUID", func(t *testing.T) {
528
528
+
args := []string{task.UUID}
575
529
576
576
-
func TestDoneTask(t *testing.T) {
577
577
-
_, cleanup := setupTaskTest(t)
578
578
-
defer cleanup()
530
530
+
err := ViewTask(ctx, args)
531
531
+
if err != nil {
532
532
+
t.Errorf("ViewTask by UUID failed: %v", err)
533
533
+
}
534
534
+
})
579
535
580
580
-
ctx := context.Background()
536
536
+
t.Run("fails with missing task ID", func(t *testing.T) {
537
537
+
args := []string{}
581
538
582
582
-
// Create test task
583
583
-
handler, err := NewTaskHandler()
584
584
-
if err != nil {
585
585
-
t.Fatalf("Failed to create handler: %v", err)
586
586
-
}
587
587
-
defer handler.Close()
539
539
+
err := ViewTask(ctx, args)
540
540
+
if err == nil {
541
541
+
t.Error("Expected error for missing task ID")
542
542
+
}
588
543
589
589
-
task := &models.Task{
590
590
-
UUID: uuid.New().String(),
591
591
-
Description: "Task to complete",
592
592
-
Status: "pending",
593
593
-
}
594
594
-
id, err := handler.repos.Tasks.Create(ctx, task)
595
595
-
if err != nil {
596
596
-
t.Fatalf("Failed to create task: %v", err)
597
597
-
}
544
544
+
if !strings.Contains(err.Error(), "task ID required") {
545
545
+
t.Errorf("Expected error about required task ID, got: %v", err)
546
546
+
}
547
547
+
})
598
548
599
599
-
t.Run("marks task as done by ID", func(t *testing.T) {
600
600
-
args := []string{strconv.FormatInt(id, 10)}
549
549
+
t.Run("fails with invalid task ID", func(t *testing.T) {
550
550
+
args := []string{"99999"}
601
551
602
602
-
err := DoneTask(ctx, args)
603
603
-
if err != nil {
604
604
-
t.Errorf("DoneTask failed: %v", err)
605
605
-
}
552
552
+
err := ViewTask(ctx, args)
553
553
+
if err == nil {
554
554
+
t.Error("Expected error for invalid task ID")
555
555
+
}
606
556
607
607
-
// Verify task was marked as completed
608
608
-
completedTask, err := handler.repos.Tasks.Get(ctx, id)
609
609
-
if err != nil {
610
610
-
t.Fatalf("Failed to get completed task: %v", err)
611
611
-
}
612
612
-
613
613
-
if completedTask.Status != "completed" {
614
614
-
t.Errorf("Expected status 'completed', got '%s'", completedTask.Status)
615
615
-
}
616
616
-
617
617
-
if completedTask.End == nil {
618
618
-
t.Error("Expected end time to be set")
619
619
-
}
557
557
+
if !strings.Contains(err.Error(), "failed to find task") {
558
558
+
t.Errorf("Expected error about task not found, got: %v", err)
559
559
+
}
560
560
+
})
620
561
})
621
562
622
622
-
t.Run("handles already completed task", func(t *testing.T) {
623
623
-
// Create another task and complete it first
624
624
-
task2 := &models.Task{
625
625
-
UUID: uuid.New().String(),
626
626
-
Description: "Already completed task",
627
627
-
Status: "completed",
628
628
-
}
629
629
-
id2, err := handler.repos.Tasks.Create(ctx, task2)
630
630
-
if err != nil {
631
631
-
t.Fatalf("Failed to create task2: %v", err)
632
632
-
}
563
563
+
t.Run("Done", func(t *testing.T) {
564
564
+
_, cleanup := setupTaskTest(t)
565
565
+
defer cleanup()
633
566
634
634
-
args := []string{strconv.FormatInt(id2, 10)}
567
567
+
ctx := context.Background()
635
568
636
636
-
err = DoneTask(ctx, args)
569
569
+
handler, err := NewTaskHandler()
637
570
if err != nil {
638
638
-
t.Errorf("DoneTask on completed task failed: %v", err)
571
571
+
t.Fatalf("Failed to create handler: %v", err)
639
572
}
640
640
-
})
573
573
+
defer handler.Close()
641
574
642
642
-
t.Run("marks task as done by UUID", func(t *testing.T) {
643
643
-
// Create another pending task
644
644
-
task3 := &models.Task{
575
575
+
task := &models.Task{
645
576
UUID: uuid.New().String(),
646
646
-
Description: "Task to complete by UUID",
577
577
+
Description: "Task to complete",
647
578
Status: "pending",
648
579
}
649
649
-
_, err := handler.repos.Tasks.Create(ctx, task3)
580
580
+
id, err := handler.repos.Tasks.Create(ctx, task)
650
581
if err != nil {
651
651
-
t.Fatalf("Failed to create task3: %v", err)
582
582
+
t.Fatalf("Failed to create task: %v", err)
652
583
}
653
584
654
654
-
args := []string{task3.UUID}
585
585
+
t.Run("marks task as done by ID", func(t *testing.T) {
586
586
+
args := []string{strconv.FormatInt(id, 10)}
655
587
656
656
-
err = DoneTask(ctx, args)
657
657
-
if err != nil {
658
658
-
t.Errorf("DoneTask by UUID failed: %v", err)
659
659
-
}
588
588
+
err := DoneTask(ctx, args)
589
589
+
if err != nil {
590
590
+
t.Errorf("DoneTask failed: %v", err)
591
591
+
}
660
592
661
661
-
// Verify task was marked as completed
662
662
-
completedTask, err := handler.repos.Tasks.GetByUUID(ctx, task3.UUID)
663
663
-
if err != nil {
664
664
-
t.Fatalf("Failed to get completed task by UUID: %v", err)
665
665
-
}
593
593
+
completedTask, err := handler.repos.Tasks.Get(ctx, id)
594
594
+
if err != nil {
595
595
+
t.Fatalf("Failed to get completed task: %v", err)
596
596
+
}
666
597
667
667
-
if completedTask.Status != "completed" {
668
668
-
t.Errorf("Expected status 'completed', got '%s'", completedTask.Status)
669
669
-
}
598
598
+
if completedTask.Status != "completed" {
599
599
+
t.Errorf("Expected status 'completed', got '%s'", completedTask.Status)
600
600
+
}
670
601
671
671
-
if completedTask.End == nil {
672
672
-
t.Error("Expected end time to be set")
673
673
-
}
674
674
-
})
602
602
+
if completedTask.End == nil {
603
603
+
t.Error("Expected end time to be set")
604
604
+
}
605
605
+
})
675
606
676
676
-
t.Run("fails with missing task ID", func(t *testing.T) {
677
677
-
args := []string{}
607
607
+
t.Run("handles already completed task", func(t *testing.T) {
608
608
+
task2 := &models.Task{
609
609
+
UUID: uuid.New().String(),
610
610
+
Description: "Already completed task",
611
611
+
Status: "completed",
612
612
+
}
613
613
+
id2, err := handler.repos.Tasks.Create(ctx, task2)
614
614
+
if err != nil {
615
615
+
t.Fatalf("Failed to create task2: %v", err)
616
616
+
}
678
617
679
679
-
err := DoneTask(ctx, args)
680
680
-
if err == nil {
681
681
-
t.Error("Expected error for missing task ID")
682
682
-
}
618
618
+
args := []string{strconv.FormatInt(id2, 10)}
683
619
684
684
-
if !strings.Contains(err.Error(), "task ID required") {
685
685
-
t.Errorf("Expected error about required task ID, got: %v", err)
686
686
-
}
687
687
-
})
620
620
+
err = DoneTask(ctx, args)
621
621
+
if err != nil {
622
622
+
t.Errorf("DoneTask on completed task failed: %v", err)
623
623
+
}
624
624
+
})
688
625
689
689
-
t.Run("fails with invalid task ID", func(t *testing.T) {
690
690
-
args := []string{"99999"}
626
626
+
t.Run("marks task as done by UUID", func(t *testing.T) {
627
627
+
task3 := &models.Task{
628
628
+
UUID: uuid.New().String(),
629
629
+
Description: "Task to complete by UUID",
630
630
+
Status: "pending",
631
631
+
}
632
632
+
_, err := handler.repos.Tasks.Create(ctx, task3)
633
633
+
if err != nil {
634
634
+
t.Fatalf("Failed to create task3: %v", err)
635
635
+
}
691
636
692
692
-
err := DoneTask(ctx, args)
693
693
-
if err == nil {
694
694
-
t.Error("Expected error for invalid task ID")
695
695
-
}
637
637
+
args := []string{task3.UUID}
696
638
697
697
-
if !strings.Contains(err.Error(), "failed to find task") {
698
698
-
t.Errorf("Expected error about task not found, got: %v", err)
699
699
-
}
700
700
-
})
701
701
-
}
639
639
+
err = DoneTask(ctx, args)
640
640
+
if err != nil {
641
641
+
t.Errorf("DoneTask by UUID failed: %v", err)
642
642
+
}
702
643
703
703
-
func TestHelperFunctions(t *testing.T) {
704
704
-
t.Run("contains function", func(t *testing.T) {
705
705
-
slice := []string{"a", "b", "c"}
644
644
+
completedTask, err := handler.repos.Tasks.GetByUUID(ctx, task3.UUID)
645
645
+
if err != nil {
646
646
+
t.Fatalf("Failed to get completed task by UUID: %v", err)
647
647
+
}
706
648
707
707
-
if !contains(slice, "b") {
708
708
-
t.Error("Expected contains to return true for existing item")
709
709
-
}
649
649
+
if completedTask.Status != "completed" {
650
650
+
t.Errorf("Expected status 'completed', got '%s'", completedTask.Status)
651
651
+
}
710
652
711
711
-
if contains(slice, "d") {
712
712
-
t.Error("Expected contains to return false for non-existing item")
713
713
-
}
714
714
-
})
653
653
+
if completedTask.End == nil {
654
654
+
t.Error("Expected end time to be set")
655
655
+
}
656
656
+
})
715
657
716
716
-
t.Run("removeString function", func(t *testing.T) {
717
717
-
slice := []string{"a", "b", "c", "b"}
718
718
-
result := removeString(slice, "b")
658
658
+
t.Run("fails with missing task ID", func(t *testing.T) {
659
659
+
args := []string{}
719
660
720
720
-
if len(result) != 2 {
721
721
-
t.Errorf("Expected 2 items after removing 'b', got %d", len(result))
722
722
-
}
661
661
+
err := DoneTask(ctx, args)
662
662
+
if err == nil {
663
663
+
t.Error("Expected error for missing task ID")
664
664
+
}
723
665
724
724
-
if contains(result, "b") {
725
725
-
t.Error("Expected 'b' to be removed from slice")
726
726
-
}
666
666
+
if !strings.Contains(err.Error(), "task ID required") {
667
667
+
t.Errorf("Expected error about required task ID, got: %v", err)
668
668
+
}
669
669
+
})
727
670
728
728
-
if !contains(result, "a") || !contains(result, "c") {
729
729
-
t.Error("Expected 'a' and 'c' to remain in slice")
730
730
-
}
671
671
+
t.Run("fails with invalid task ID", func(t *testing.T) {
672
672
+
args := []string{"99999"}
673
673
+
674
674
+
err := DoneTask(ctx, args)
675
675
+
if err == nil {
676
676
+
t.Error("Expected error for invalid task ID")
677
677
+
}
678
678
+
679
679
+
if !strings.Contains(err.Error(), "failed to find task") {
680
680
+
t.Errorf("Expected error about task not found, got: %v", err)
681
681
+
}
682
682
+
})
731
683
})
732
732
-
}
733
684
734
734
-
func TestPrintFunctions(t *testing.T) {
735
735
-
_, cleanup := setupTaskTest(t)
736
736
-
defer cleanup()
685
685
+
t.Run("Helper", func(t *testing.T) {
686
686
+
t.Run("contains function", func(t *testing.T) {
687
687
+
slice := []string{"a", "b", "c"}
737
688
738
738
-
handler, err := NewTaskHandler()
739
739
-
if err != nil {
740
740
-
t.Fatalf("Failed to create handler: %v", err)
741
741
-
}
742
742
-
defer handler.Close()
689
689
+
if !contains(slice, "b") {
690
690
+
t.Error("Expected contains to return true for existing item")
691
691
+
}
743
692
744
744
-
now := time.Now()
745
745
-
due := now.Add(24 * time.Hour)
693
693
+
if contains(slice, "d") {
694
694
+
t.Error("Expected contains to return false for non-existing item")
695
695
+
}
696
696
+
})
746
697
747
747
-
task := &models.Task{
748
748
-
ID: 1,
749
749
-
UUID: uuid.New().String(),
750
750
-
Description: "Test task",
751
751
-
Status: "pending",
752
752
-
Priority: "A",
753
753
-
Project: "test",
754
754
-
Tags: []string{"work", "urgent"},
755
755
-
Due: &due,
756
756
-
Entry: now,
757
757
-
Modified: now,
758
758
-
}
698
698
+
t.Run("removeString function", func(t *testing.T) {
699
699
+
slice := []string{"a", "b", "c", "b"}
700
700
+
result := removeString(slice, "b")
701
701
+
702
702
+
if len(result) != 2 {
703
703
+
t.Errorf("Expected 2 items after removing 'b', got %d", len(result))
704
704
+
}
759
705
760
760
-
// Test that print functions don't panic
761
761
-
t.Run("printTask doesn't panic", func(t *testing.T) {
762
762
-
defer func() {
763
763
-
if r := recover(); r != nil {
764
764
-
t.Errorf("printTask panicked: %v", r)
706
706
+
if contains(result, "b") {
707
707
+
t.Error("Expected 'b' to be removed from slice")
765
708
}
766
766
-
}()
767
709
768
768
-
handler.printTask(task)
710
710
+
if !contains(result, "a") || !contains(result, "c") {
711
711
+
t.Error("Expected 'a' and 'c' to remain in slice")
712
712
+
}
713
713
+
})
769
714
})
770
715
771
771
-
t.Run("printTaskDetail doesn't panic", func(t *testing.T) {
772
772
-
defer func() {
773
773
-
if r := recover(); r != nil {
774
774
-
t.Errorf("printTaskDetail panicked: %v", r)
775
775
-
}
776
776
-
}()
716
716
+
t.Run("Print", func(t *testing.T) {
717
717
+
_, cleanup := setupTaskTest(t)
718
718
+
defer cleanup()
719
719
+
720
720
+
handler, err := NewTaskHandler()
721
721
+
if err != nil {
722
722
+
t.Fatalf("Failed to create handler: %v", err)
723
723
+
}
724
724
+
defer handler.Close()
725
725
+
726
726
+
now := time.Now()
727
727
+
due := now.Add(24 * time.Hour)
728
728
+
729
729
+
task := &models.Task{
730
730
+
ID: 1,
731
731
+
UUID: uuid.New().String(),
732
732
+
Description: "Test task",
733
733
+
Status: "pending",
734
734
+
Priority: "A",
735
735
+
Project: "test",
736
736
+
Tags: []string{"work", "urgent"},
737
737
+
Due: &due,
738
738
+
Entry: now,
739
739
+
Modified: now,
740
740
+
}
741
741
+
742
742
+
t.Run("printTask doesn't panic", func(t *testing.T) {
743
743
+
defer func() {
744
744
+
if r := recover(); r != nil {
745
745
+
t.Errorf("printTask panicked: %v", r)
746
746
+
}
747
747
+
}()
748
748
+
749
749
+
handler.printTask(task)
750
750
+
})
751
751
+
752
752
+
t.Run("printTaskDetail doesn't panic", func(t *testing.T) {
753
753
+
defer func() {
754
754
+
if r := recover(); r != nil {
755
755
+
t.Errorf("printTaskDetail panicked: %v", r)
756
756
+
}
757
757
+
}()
777
758
778
778
-
handler.printTaskDetail(task)
759
759
+
handler.printTaskDetail(task)
760
760
+
})
779
761
})
780
780
-
}
762
762
+
}
+18
-14
internal/ui/book_list.go
···
6
6
"io"
7
7
"os"
8
8
"strings"
9
9
+
"time"
9
10
10
11
tea "github.com/charmbracelet/bubbletea"
11
12
"github.com/charmbracelet/huh"
···
17
18
18
19
// BookListOptions configures the book list UI behavior
19
20
type BookListOptions struct {
20
20
-
Output io.Writer // Output destination (stdout for interactive, buffer for testing)
21
21
-
Input io.Reader // Input source (stdin for interactive, strings reader for testing)
22
22
-
StaticMode bool // Enable static mode for testing (no interactive components)
21
21
+
// Output destination (stdout for interactive, buffer for testing)
22
22
+
Output io.Writer
23
23
+
// Input source (stdin for interactive, strings reader for testing)
24
24
+
Input io.Reader
25
25
+
// Enable static mode (no interactive components)
26
26
+
Static bool
23
27
}
24
28
25
29
// BookList handles book search and selection UI
···
64
68
type bookAddedMsg *models.Book
65
69
66
70
func (m searchModel) Init() tea.Cmd {
67
67
-
return nil
71
71
+
return m.searchBooks(m.query)
68
72
}
69
73
70
74
func (m searchModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
···
106
110
case bookAddedMsg:
107
111
m.addedBook = (*models.Book)(msg)
108
112
m.confirmed = true
109
109
-
return m, tea.Quit
113
113
+
return m, tea.Tick(2*time.Second, func(t time.Time) tea.Msg {
114
114
+
return tea.Quit()
115
115
+
})
110
116
}
111
117
return m, nil
112
118
}
···
204
210
205
211
// SearchAndSelect searches for books with the given query and allows selection
206
212
func (bl *BookList) SearchAndSelect(ctx context.Context, query string) error {
207
207
-
if bl.opts.StaticMode {
208
208
-
return bl.searchAndSelectStatic(ctx, query)
213
213
+
if bl.opts.Static {
214
214
+
return bl.staticSelect(ctx, query)
209
215
}
210
216
211
217
model := searchModel{
···
219
225
220
226
program := tea.NewProgram(model, tea.WithInput(bl.opts.Input), tea.WithOutput(bl.opts.Output))
221
227
222
222
-
program.Send(tea.Cmd(model.searchBooks(query)))
223
223
-
224
228
_, err := program.Run()
225
229
return err
226
230
}
227
231
228
228
-
func (bl *BookList) searchAndSelectStatic(ctx context.Context, query string) error {
232
232
+
func (bl *BookList) staticSelect(ctx context.Context, query string) error {
229
233
results, err := bl.service.Search(ctx, query, 1, 10)
230
234
if err != nil {
231
235
fmt.Fprintf(bl.opts.Output, "Error: %s\n", err)
···
270
274
271
275
// InteractiveSearch provides an interactive search interface
272
276
func (bl *BookList) InteractiveSearch(ctx context.Context) error {
273
273
-
if bl.opts.StaticMode {
274
274
-
return bl.interactiveSearchStatic(ctx)
277
277
+
if bl.opts.Static {
278
278
+
return bl.staticSearch(ctx)
275
279
}
276
280
277
281
var query string
···
295
299
return bl.SearchAndSelect(ctx, query)
296
300
}
297
301
298
298
-
func (bl *BookList) interactiveSearchStatic(ctx context.Context) error {
302
302
+
func (bl *BookList) staticSearch(ctx context.Context) error {
299
303
fmt.Fprintf(bl.opts.Output, "Search for books: test query\n")
300
300
-
return bl.searchAndSelectStatic(ctx, "test query")
304
304
+
return bl.staticSelect(ctx, "test query")
301
305
}
+15
-15
internal/ui/book_list_test.go
···
40
40
t.Run("Options", func(t *testing.T) {
41
41
t.Run("default options", func(t *testing.T) {
42
42
opts := BookListOptions{}
43
43
-
if opts.StaticMode {
43
43
+
if opts.Static {
44
44
t.Error("StaticMode should default to false")
45
45
}
46
46
})
···
48
48
t.Run("static mode enabled", func(t *testing.T) {
49
49
var buf bytes.Buffer
50
50
opts := BookListOptions{
51
51
-
Output: &buf,
52
52
-
StaticMode: true,
51
51
+
Output: &buf,
52
52
+
Static: true,
53
53
}
54
54
55
55
-
if !opts.StaticMode {
55
55
+
if !opts.Static {
56
56
t.Error("StaticMode should be enabled")
57
57
}
58
58
if opts.Output != &buf {
···
73
73
service: service,
74
74
repo: nil,
75
75
opts: BookListOptions{
76
76
-
Output: &buf,
77
77
-
StaticMode: true,
76
76
+
Output: &buf,
77
77
+
Static: true,
78
78
},
79
79
}
80
80
81
81
-
err := bl.searchAndSelectStatic(context.Background(), "test query")
81
81
+
err := bl.staticSelect(context.Background(), "test query")
82
82
if err == nil {
83
83
t.Fatal("Expected error, got nil")
84
84
}
···
100
100
service: service,
101
101
repo: nil,
102
102
opts: BookListOptions{
103
103
-
Output: &buf,
104
104
-
StaticMode: true,
103
103
+
Output: &buf,
104
104
+
Static: true,
105
105
},
106
106
}
107
107
108
108
-
err := bl.searchAndSelectStatic(context.Background(), "nonexistent")
108
108
+
err := bl.staticSelect(context.Background(), "nonexistent")
109
109
if err != nil {
110
110
t.Fatalf("searchAndSelectStatic failed: %v", err)
111
111
}
···
134
134
// Skip repo operations for this test
135
135
// repo: nil,
136
136
opts: BookListOptions{
137
137
-
Output: &buf,
138
138
-
StaticMode: true,
137
137
+
Output: &buf,
138
138
+
Static: true,
139
139
},
140
140
}
141
141
···
193
193
service: service,
194
194
repo: nil,
195
195
opts: BookListOptions{
196
196
-
Output: &buf,
197
197
-
StaticMode: true,
196
196
+
Output: &buf,
197
197
+
Static: true,
198
198
},
199
199
}
200
200
201
201
-
err := bl.interactiveSearchStatic(context.Background())
201
201
+
err := bl.staticSearch(context.Background())
202
202
if err != nil {
203
203
t.Fatalf("InteractiveSearch failed: %v", err)
204
204
}