cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 514 lines 14 kB view raw
1package main 2 3import ( 4 "context" 5 "strings" 6 "testing" 7 8 "github.com/spf13/cobra" 9 "github.com/stormlightlabs/noteleaf/internal/handlers" 10 "github.com/stormlightlabs/noteleaf/internal/shared" 11) 12 13func createTestPublicationHandler(t *testing.T) (*handlers.PublicationHandler, func()) { 14 cleanup := setupCommandTest(t) 15 handler, err := handlers.NewPublicationHandler() 16 if err != nil { 17 cleanup() 18 t.Fatalf("Failed to create test publication handler: %v", err) 19 } 20 return handler, func() { 21 handler.Close() 22 cleanup() 23 } 24} 25 26func TestPublicationCommand(t *testing.T) { 27 t.Run("CommandGroup Interface", func(t *testing.T) { 28 handler, cleanup := createTestPublicationHandler(t) 29 defer cleanup() 30 31 var _ CommandGroup = NewPublicationCommand(handler) 32 }) 33 34 t.Run("Create", func(t *testing.T) { 35 t.Run("creates command with correct structure", func(t *testing.T) { 36 handler, cleanup := createTestPublicationHandler(t) 37 defer cleanup() 38 39 cmd := NewPublicationCommand(handler).Create() 40 41 if cmd == nil { 42 t.Fatal("Create returned nil") 43 } 44 if cmd.Use != "pub" { 45 t.Errorf("Expected Use to be 'pub', got '%s'", cmd.Use) 46 } 47 if cmd.Short != "Manage leaflet publication sync" { 48 t.Errorf("Expected Short to be 'Manage leaflet publication sync', got '%s'", cmd.Short) 49 } 50 if !cmd.HasSubCommands() { 51 t.Error("Expected command to have subcommands") 52 } 53 }) 54 55 t.Run("has all expected subcommands", func(t *testing.T) { 56 handler, cleanup := createTestPublicationHandler(t) 57 defer cleanup() 58 59 cmd := NewPublicationCommand(handler).Create() 60 subcommands := cmd.Commands() 61 subcommandNames := make([]string, len(subcommands)) 62 for i, subcmd := range subcommands { 63 subcommandNames[i] = subcmd.Use 64 } 65 66 expectedSubcommands := []string{ 67 "auth [handle]", 68 "pull", 69 "list [--published|--draft|--all] [--interactive]", 70 "read [identifier]", 71 "status", 72 "post [note-id]", 73 "patch [note-id]", 74 "push [note-ids...] [--file files...]", 75 } 76 77 for _, expected := range expectedSubcommands { 78 if !findSubcommand(subcommandNames, expected) { 79 t.Errorf("Expected subcommand '%s' not found in %v", expected, subcommandNames) 80 } 81 } 82 }) 83 }) 84 85 t.Run("Status Command", func(t *testing.T) { 86 t.Run("shows not authenticated initially", func(t *testing.T) { 87 handler, cleanup := createTestPublicationHandler(t) 88 defer cleanup() 89 90 cmd := NewPublicationCommand(handler).Create() 91 cmd.SetArgs([]string{"status"}) 92 err := cmd.Execute() 93 94 if err != nil { 95 t.Errorf("status command failed: %v", err) 96 } 97 }) 98 }) 99 100 t.Run("List Command", func(t *testing.T) { 101 t.Run("default filter", func(t *testing.T) { 102 handler, cleanup := createTestPublicationHandler(t) 103 defer cleanup() 104 105 cmd := NewPublicationCommand(handler).Create() 106 cmd.SetArgs([]string{"list"}) 107 err := cmd.Execute() 108 109 if err != nil { 110 t.Errorf("list command failed: %v", err) 111 } 112 }) 113 114 t.Run("with published flag", func(t *testing.T) { 115 handler, cleanup := createTestPublicationHandler(t) 116 defer cleanup() 117 118 cmd := NewPublicationCommand(handler).Create() 119 cmd.SetArgs([]string{"list", "--published"}) 120 err := cmd.Execute() 121 122 if err != nil { 123 t.Errorf("list --published failed: %v", err) 124 } 125 }) 126 127 t.Run("with draft flag", func(t *testing.T) { 128 handler, cleanup := createTestPublicationHandler(t) 129 defer cleanup() 130 131 cmd := NewPublicationCommand(handler).Create() 132 cmd.SetArgs([]string{"list", "--draft"}) 133 err := cmd.Execute() 134 135 if err != nil { 136 t.Errorf("list --draft failed: %v", err) 137 } 138 }) 139 140 t.Run("with all flag", func(t *testing.T) { 141 handler, cleanup := createTestPublicationHandler(t) 142 defer cleanup() 143 144 cmd := NewPublicationCommand(handler).Create() 145 cmd.SetArgs([]string{"list", "--all"}) 146 err := cmd.Execute() 147 148 if err != nil { 149 t.Errorf("list --all failed: %v", err) 150 } 151 }) 152 153 t.Run("published takes precedence over draft", func(t *testing.T) { 154 handler, cleanup := createTestPublicationHandler(t) 155 defer cleanup() 156 157 cmd := NewPublicationCommand(handler).Create() 158 cmd.SetArgs([]string{"list", "--published", "--draft"}) 159 err := cmd.Execute() 160 161 if err != nil { 162 t.Errorf("list with multiple flags failed: %v", err) 163 } 164 }) 165 }) 166 167 t.Run("Read Command", func(t *testing.T) { 168 t.Run("reads without identifier", func(t *testing.T) { 169 handler, cleanup := createTestPublicationHandler(t) 170 defer cleanup() 171 172 cmd := NewPublicationCommand(handler).Create() 173 cmd.SetArgs([]string{"read"}) 174 err := cmd.Execute() 175 176 if err == nil { 177 t.Error("Expected read to fail when no publications exist") 178 } 179 180 shared.AssertErrorContains(t, err, "note not found", "") 181 }) 182 183 t.Run("fails with non-existent note ID", func(t *testing.T) { 184 handler, cleanup := createTestPublicationHandler(t) 185 defer cleanup() 186 187 cmd := NewPublicationCommand(handler).Create() 188 cmd.SetArgs([]string{"read", "999"}) 189 err := cmd.Execute() 190 191 shared.AssertError(t, err, "read to fail with non-existent ID") 192 }) 193 194 t.Run("fails with non-existent rkey", func(t *testing.T) { 195 handler, cleanup := createTestPublicationHandler(t) 196 defer cleanup() 197 198 cmd := NewPublicationCommand(handler).Create() 199 cmd.SetArgs([]string{"read", "3jxxxxxxxxxxxx"}) 200 err := cmd.Execute() 201 202 if err == nil { 203 t.Error("Expected read to fail with non-existent rkey") 204 } 205 }) 206 207 t.Run("accepts optional identifier argument", func(t *testing.T) { 208 handler, cleanup := createTestPublicationHandler(t) 209 defer cleanup() 210 211 cmd := NewPublicationCommand(handler).Create() 212 subcommands := cmd.Commands() 213 214 var readCmd *cobra.Command 215 for _, subcmd := range subcommands { 216 if strings.HasPrefix(subcmd.Use, "read") { 217 readCmd = subcmd 218 break 219 } 220 } 221 222 if readCmd == nil { 223 t.Fatal("read command not found") 224 } 225 226 if readCmd.Use != "read [identifier]" { 227 t.Errorf("Expected Use to be 'read [identifier]', got '%s'", readCmd.Use) 228 } 229 }) 230 }) 231 232 t.Run("Pull Command", func(t *testing.T) { 233 t.Run("fails when not authenticated", func(t *testing.T) { 234 handler, cleanup := createTestPublicationHandler(t) 235 defer cleanup() 236 237 cmd := NewPublicationCommand(handler).Create() 238 cmd.SetArgs([]string{"pull"}) 239 err := cmd.Execute() 240 241 if err == nil { 242 t.Error("Expected pull to fail when not authenticated") 243 } 244 if err != nil && !strings.Contains(err.Error(), "not authenticated") { 245 t.Errorf("Expected 'not authenticated' error, got: %v", err) 246 } 247 }) 248 }) 249 250 t.Run("Post Command", func(t *testing.T) { 251 t.Run("requires note ID argument", func(t *testing.T) { 252 handler, cleanup := createTestPublicationHandler(t) 253 defer cleanup() 254 255 cmd := NewPublicationCommand(handler).Create() 256 cmd.SetArgs([]string{"post"}) 257 err := cmd.Execute() 258 259 if err == nil { 260 t.Error("Expected error for missing note ID") 261 } 262 }) 263 264 t.Run("rejects invalid note ID", func(t *testing.T) { 265 handler, cleanup := createTestPublicationHandler(t) 266 defer cleanup() 267 268 cmd := NewPublicationCommand(handler).Create() 269 cmd.SetArgs([]string{"post", "not-a-number"}) 270 err := cmd.Execute() 271 272 if err == nil { 273 t.Error("Expected error for invalid note ID") 274 } 275 if !strings.Contains(err.Error(), "invalid note ID") { 276 t.Errorf("Expected 'invalid note ID' error, got: %v", err) 277 } 278 }) 279 280 t.Run("fails when not authenticated", func(t *testing.T) { 281 handler, cleanup := createTestPublicationHandler(t) 282 defer cleanup() 283 284 cmd := NewPublicationCommand(handler).Create() 285 cmd.SetArgs([]string{"post", "123"}) 286 err := cmd.Execute() 287 288 if err == nil { 289 t.Error("Expected post to fail when not authenticated") 290 } 291 if !strings.Contains(err.Error(), "not authenticated") { 292 t.Errorf("Expected 'not authenticated' error, got: %v", err) 293 } 294 }) 295 296 t.Run("preview mode fails when not authenticated", func(t *testing.T) { 297 handler, cleanup := createTestPublicationHandler(t) 298 defer cleanup() 299 300 cmd := NewPublicationCommand(handler).Create() 301 cmd.SetArgs([]string{"post", "123", "--preview"}) 302 err := cmd.Execute() 303 304 if err == nil { 305 t.Error("Expected post --preview to fail when not authenticated") 306 } 307 if !strings.Contains(err.Error(), "not authenticated") { 308 t.Errorf("Expected 'not authenticated' error, got: %v", err) 309 } 310 }) 311 312 t.Run("validate mode fails when not authenticated", func(t *testing.T) { 313 handler, cleanup := createTestPublicationHandler(t) 314 defer cleanup() 315 316 cmd := NewPublicationCommand(handler).Create() 317 cmd.SetArgs([]string{"post", "123", "--validate"}) 318 err := cmd.Execute() 319 320 if err == nil { 321 t.Error("Expected post --validate to fail when not authenticated") 322 } 323 if !strings.Contains(err.Error(), "not authenticated") { 324 t.Errorf("Expected 'not authenticated' error, got: %v", err) 325 } 326 }) 327 328 t.Run("accepts draft flag", func(t *testing.T) { 329 handler, cleanup := createTestPublicationHandler(t) 330 defer cleanup() 331 332 cmd := NewPublicationCommand(handler).Create() 333 cmd.SetArgs([]string{"post", "123", "--draft"}) 334 err := cmd.Execute() 335 336 if err == nil { 337 t.Error("Expected post --draft to fail when not authenticated") 338 } 339 if !strings.Contains(err.Error(), "not authenticated") { 340 t.Errorf("Expected 'not authenticated' error, got: %v", err) 341 } 342 }) 343 344 t.Run("accepts preview and draft flags together", func(t *testing.T) { 345 handler, cleanup := createTestPublicationHandler(t) 346 defer cleanup() 347 348 cmd := NewPublicationCommand(handler).Create() 349 cmd.SetArgs([]string{"post", "123", "--preview", "--draft"}) 350 err := cmd.Execute() 351 352 if err == nil { 353 t.Error("Expected post --preview --draft to fail when not authenticated") 354 } 355 if !strings.Contains(err.Error(), "not authenticated") { 356 t.Errorf("Expected 'not authenticated' error, got: %v", err) 357 } 358 }) 359 }) 360 361 t.Run("Patch Command", func(t *testing.T) { 362 t.Run("requires note ID argument", func(t *testing.T) { 363 handler, cleanup := createTestPublicationHandler(t) 364 defer cleanup() 365 366 cmd := NewPublicationCommand(handler).Create() 367 cmd.SetArgs([]string{"patch"}) 368 err := cmd.Execute() 369 370 if err == nil { 371 t.Error("Expected error for missing note ID") 372 } 373 }) 374 375 t.Run("rejects invalid note ID", func(t *testing.T) { 376 handler, cleanup := createTestPublicationHandler(t) 377 defer cleanup() 378 379 cmd := NewPublicationCommand(handler).Create() 380 cmd.SetArgs([]string{"patch", "not-a-number"}) 381 err := cmd.Execute() 382 383 if err == nil { 384 t.Error("Expected error for invalid note ID") 385 } 386 if !strings.Contains(err.Error(), "invalid note ID") { 387 t.Errorf("Expected 'invalid note ID' error, got: %v", err) 388 } 389 }) 390 391 t.Run("fails when not authenticated", func(t *testing.T) { 392 handler, cleanup := createTestPublicationHandler(t) 393 defer cleanup() 394 395 cmd := NewPublicationCommand(handler).Create() 396 cmd.SetArgs([]string{"patch", "123"}) 397 err := cmd.Execute() 398 399 if err == nil { 400 t.Error("Expected patch to fail when not authenticated") 401 } 402 if !strings.Contains(err.Error(), "not authenticated") { 403 t.Errorf("Expected 'not authenticated' error, got: %v", err) 404 } 405 }) 406 407 t.Run("preview mode fails when not authenticated", func(t *testing.T) { 408 handler, cleanup := createTestPublicationHandler(t) 409 defer cleanup() 410 411 cmd := NewPublicationCommand(handler).Create() 412 cmd.SetArgs([]string{"patch", "123", "--preview"}) 413 err := cmd.Execute() 414 415 if err == nil { 416 t.Error("Expected patch --preview to fail when not authenticated") 417 } 418 if !strings.Contains(err.Error(), "not authenticated") { 419 t.Errorf("Expected 'not authenticated' error, got: %v", err) 420 } 421 }) 422 423 t.Run("validate mode fails when not authenticated", func(t *testing.T) { 424 handler, cleanup := createTestPublicationHandler(t) 425 defer cleanup() 426 427 cmd := NewPublicationCommand(handler).Create() 428 cmd.SetArgs([]string{"patch", "123", "--validate"}) 429 err := cmd.Execute() 430 431 if err == nil { 432 t.Error("Expected patch --validate to fail when not authenticated") 433 } 434 if !strings.Contains(err.Error(), "not authenticated") { 435 t.Errorf("Expected 'not authenticated' error, got: %v", err) 436 } 437 }) 438 }) 439 440 t.Run("Command Help", func(t *testing.T) { 441 t.Run("root help", func(t *testing.T) { 442 handler, cleanup := createTestPublicationHandler(t) 443 defer cleanup() 444 445 cmd := NewPublicationCommand(handler).Create() 446 cmd.SetArgs([]string{"help"}) 447 err := cmd.Execute() 448 449 if err != nil { 450 t.Errorf("help command failed: %v", err) 451 } 452 }) 453 454 t.Run("auth help", func(t *testing.T) { 455 handler, cleanup := createTestPublicationHandler(t) 456 defer cleanup() 457 458 cmd := NewPublicationCommand(handler).Create() 459 cmd.SetArgs([]string{"auth", "--help"}) 460 err := cmd.Execute() 461 462 if err != nil { 463 t.Errorf("auth help failed: %v", err) 464 } 465 }) 466 }) 467 468 t.Run("Command Aliases", func(t *testing.T) { 469 t.Run("list alias ls works", func(t *testing.T) { 470 handler, cleanup := createTestPublicationHandler(t) 471 defer cleanup() 472 473 cmd := NewPublicationCommand(handler).Create() 474 cmd.SetArgs([]string{"ls"}) 475 err := cmd.Execute() 476 477 if err != nil { 478 t.Errorf("list alias 'ls' failed: %v", err) 479 } 480 }) 481 }) 482 483 t.Run("Handler Validation", func(t *testing.T) { 484 t.Run("auth validates empty handle", func(t *testing.T) { 485 handler, cleanup := createTestPublicationHandler(t) 486 defer cleanup() 487 488 ctx := context.Background() 489 err := handler.Auth(ctx, "", "password") 490 491 if err == nil { 492 t.Error("Expected error for empty handle") 493 } 494 if !strings.Contains(err.Error(), "handle is required") { 495 t.Errorf("Expected 'handle is required' error, got: %v", err) 496 } 497 }) 498 499 t.Run("auth validates empty password", func(t *testing.T) { 500 handler, cleanup := createTestPublicationHandler(t) 501 defer cleanup() 502 503 ctx := context.Background() 504 err := handler.Auth(ctx, "test.bsky.social", "") 505 506 if err == nil { 507 t.Error("Expected error for empty password") 508 } 509 if !strings.Contains(err.Error(), "password is required") { 510 t.Errorf("Expected 'password is required' error, got: %v", err) 511 } 512 }) 513 }) 514}