cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists ๐Ÿƒ
charm leaflet readability golang

feat: book handlers

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