cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
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}