fork
Configure Feed
Select the types of activity you want to include in your feed.
cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
fork
Configure Feed
Select the types of activity you want to include in your feed.
1package handlers
2
3import (
4 "context"
5 "fmt"
6 "strings"
7 "testing"
8 "time"
9
10 "github.com/stormlightlabs/noteleaf/internal/models"
11 "github.com/stormlightlabs/noteleaf/internal/public"
12 "github.com/stormlightlabs/noteleaf/internal/services"
13 "github.com/stormlightlabs/noteleaf/internal/store"
14)
15
16func TestPublicationHandler(t *testing.T) {
17 t.Run("sessionFromConfig", func(t *testing.T) {
18 t.Run("returns error when DID is missing", func(t *testing.T) {
19 config := &store.Config{
20 ATProtoDID: "",
21 ATProtoHandle: "test.bsky.social",
22 ATProtoAccessJWT: "access_token",
23 ATProtoRefreshJWT: "refresh_token",
24 }
25
26 _, err := sessionFromConfig(config)
27 if err == nil {
28 t.Error("Expected error when DID is missing")
29 }
30 })
31
32 t.Run("returns error when AccessJWT is missing", func(t *testing.T) {
33 config := &store.Config{
34 ATProtoDID: "did:plc:test123",
35 ATProtoHandle: "test.bsky.social",
36 ATProtoAccessJWT: "",
37 ATProtoRefreshJWT: "refresh_token",
38 }
39
40 _, err := sessionFromConfig(config)
41 if err == nil {
42 t.Error("Expected error when AccessJWT is missing")
43 }
44 })
45
46 t.Run("returns error when RefreshJWT is missing", func(t *testing.T) {
47 config := &store.Config{
48 ATProtoDID: "did:plc:test123",
49 ATProtoHandle: "test.bsky.social",
50 ATProtoAccessJWT: "access_token",
51 ATProtoRefreshJWT: "",
52 }
53
54 _, err := sessionFromConfig(config)
55 if err == nil {
56 t.Error("Expected error when RefreshJWT is missing")
57 }
58 })
59
60 t.Run("successfully creates session from complete config", func(t *testing.T) {
61 expiresAt := time.Now().Add(2 * time.Hour)
62 config := &store.Config{
63 ATProtoDID: "did:plc:test123",
64 ATProtoHandle: "test.bsky.social",
65 ATProtoAccessJWT: "access_token",
66 ATProtoRefreshJWT: "refresh_token",
67 ATProtoPDSURL: "https://bsky.social",
68 ATProtoExpiresAt: expiresAt.Format("2006-01-02T15:04:05Z07:00"),
69 }
70
71 session, err := sessionFromConfig(config)
72 if err != nil {
73 t.Errorf("Expected no error, got %v", err)
74 }
75
76 if session.DID != config.ATProtoDID {
77 t.Errorf("Expected DID '%s', got '%s'", config.ATProtoDID, session.DID)
78 }
79 if session.Handle != config.ATProtoHandle {
80 t.Errorf("Expected Handle '%s', got '%s'", config.ATProtoHandle, session.Handle)
81 }
82 if session.AccessJWT != config.ATProtoAccessJWT {
83 t.Errorf("Expected AccessJWT '%s', got '%s'", config.ATProtoAccessJWT, session.AccessJWT)
84 }
85 if session.RefreshJWT != config.ATProtoRefreshJWT {
86 t.Errorf("Expected RefreshJWT '%s', got '%s'", config.ATProtoRefreshJWT, session.RefreshJWT)
87 }
88 if session.PDSURL != config.ATProtoPDSURL {
89 t.Errorf("Expected PDSURL '%s', got '%s'", config.ATProtoPDSURL, session.PDSURL)
90 }
91 if !session.Authenticated {
92 t.Error("Expected session to be authenticated")
93 }
94 })
95
96 t.Run("handles missing expiry time gracefully", func(t *testing.T) {
97 config := &store.Config{
98 ATProtoDID: "did:plc:test123",
99 ATProtoHandle: "test.bsky.social",
100 ATProtoAccessJWT: "access_token",
101 ATProtoRefreshJWT: "refresh_token",
102 ATProtoExpiresAt: "",
103 }
104
105 session, err := sessionFromConfig(config)
106 if err != nil {
107 t.Errorf("Expected no error, got %v", err)
108 }
109
110 if session.ExpiresAt.After(time.Now()) {
111 t.Error("Expected ExpiresAt to be in the past when not provided")
112 }
113 })
114
115 t.Run("handles invalid expiry time format gracefully", func(t *testing.T) {
116 config := &store.Config{
117 ATProtoDID: "did:plc:test123",
118 ATProtoHandle: "test.bsky.social",
119 ATProtoAccessJWT: "access_token",
120 ATProtoRefreshJWT: "refresh_token",
121 ATProtoExpiresAt: "invalid-timestamp",
122 }
123
124 session, err := sessionFromConfig(config)
125 if err != nil {
126 t.Errorf("Expected no error, got %v", err)
127 }
128
129 if session.ExpiresAt.After(time.Now()) {
130 t.Error("Expected ExpiresAt to be in the past when parse fails")
131 }
132 })
133 })
134
135 t.Run("Auth", func(t *testing.T) {
136 t.Run("validates required parameters", func(t *testing.T) {
137 _ = NewHandlerTestSuite(t)
138 handler := CreateHandler(t, NewPublicationHandler)
139 ctx := context.Background()
140
141 err := handler.Auth(ctx, "", "password")
142 if err == nil {
143 t.Error("Expected error when handle is empty")
144 }
145
146 err = handler.Auth(ctx, "handle", "")
147 if err == nil {
148 t.Error("Expected error when password is empty")
149 }
150 })
151
152 })
153
154 t.Run("GetAuthStatus", func(t *testing.T) {
155 t.Run("returns not authenticated when no session", func(t *testing.T) {
156 _ = NewHandlerTestSuite(t)
157 handler := CreateHandler(t, NewPublicationHandler)
158
159 status := handler.GetAuthStatus()
160 if status != "Not authenticated" {
161 t.Errorf("Expected 'Not authenticated', got '%s'", status)
162 }
163 })
164
165 t.Run("returns authenticated status with session", func(t *testing.T) {
166 _ = NewHandlerTestSuite(t)
167 handler := CreateHandler(t, NewPublicationHandler)
168
169 session := &services.Session{
170 DID: "did:plc:test123",
171 Handle: "test.bsky.social",
172 AccessJWT: "access_token",
173 RefreshJWT: "refresh_token",
174 PDSURL: "https://bsky.social",
175 ExpiresAt: time.Now().Add(2 * time.Hour),
176 Authenticated: true,
177 }
178
179 err := handler.atproto.RestoreSession(session)
180 if err != nil {
181 t.Fatalf("Failed to restore session: %v", err)
182 }
183
184 status := handler.GetAuthStatus()
185 expectedStatus := "Authenticated as test.bsky.social"
186 if status != expectedStatus {
187 t.Errorf("Expected '%s', got '%s'", expectedStatus, status)
188 }
189 })
190 })
191
192 t.Run("GetLastAuthenticatedHandle", func(t *testing.T) {
193 t.Run("returns empty string when no config", func(t *testing.T) {
194 handler := &PublicationHandler{
195 config: nil,
196 }
197
198 handle := handler.GetLastAuthenticatedHandle()
199 if handle != "" {
200 t.Errorf("Expected empty string, got '%s'", handle)
201 }
202 })
203
204 t.Run("returns empty string when handle not set", func(t *testing.T) {
205 handler := &PublicationHandler{
206 config: &store.Config{},
207 }
208
209 handle := handler.GetLastAuthenticatedHandle()
210 if handle != "" {
211 t.Errorf("Expected empty string, got '%s'", handle)
212 }
213 })
214
215 t.Run("returns handle from config", func(t *testing.T) {
216 expectedHandle := "test.bsky.social"
217 handler := &PublicationHandler{
218 config: &store.Config{
219 ATProtoHandle: expectedHandle,
220 },
221 }
222
223 handle := handler.GetLastAuthenticatedHandle()
224 if handle != expectedHandle {
225 t.Errorf("Expected '%s', got '%s'", expectedHandle, handle)
226 }
227 })
228
229 t.Run("returns handle after successful authentication", func(t *testing.T) {
230 suite := NewHandlerTestSuite(t)
231 defer suite.Cleanup()
232
233 handler := CreateHandler(t, NewPublicationHandler)
234 ctx := context.Background()
235
236 mock := services.SetupSuccessfulAuthMocks()
237 handler.atproto = mock
238
239 err := handler.Auth(ctx, "user.bsky.social", "password123")
240 suite.AssertNoError(err, "authentication should succeed")
241
242 handle := handler.GetLastAuthenticatedHandle()
243 if handle != "user.bsky.social" {
244 t.Errorf("Expected 'user.bsky.social', got '%s'", handle)
245 }
246 })
247 })
248
249 t.Run("NewPublicationHandler", func(t *testing.T) {
250 t.Run("creates handler successfully", func(t *testing.T) {
251 suite := NewHandlerTestSuite(t)
252 defer suite.Cleanup()
253
254 handler, err := NewPublicationHandler()
255 if err != nil {
256 t.Fatalf("Expected no error creating handler, got %v", err)
257 }
258 defer handler.Close()
259
260 if handler.db == nil {
261 t.Error("Expected database to be initialized")
262 }
263 if handler.config == nil {
264 t.Error("Expected config to be initialized")
265 }
266 if handler.repos == nil {
267 t.Error("Expected repos to be initialized")
268 }
269 if handler.atproto == nil {
270 t.Error("Expected atproto service to be initialized")
271 }
272 })
273
274 t.Run("restores session from config when available", func(t *testing.T) {
275 suite := NewHandlerTestSuite(t)
276 defer suite.Cleanup()
277
278 config, err := store.LoadConfig()
279 if err != nil {
280 t.Fatalf("Failed to load config: %v", err)
281 }
282
283 config.ATProtoDID = "did:plc:test123"
284 config.ATProtoHandle = "test.bsky.social"
285 config.ATProtoAccessJWT = "access_token"
286 config.ATProtoRefreshJWT = "refresh_token"
287 config.ATProtoPDSURL = "https://bsky.social"
288 config.ATProtoExpiresAt = time.Now().Add(2 * time.Hour).Format("2006-01-02T15:04:05Z07:00")
289
290 err = store.SaveConfig(config)
291 if err != nil {
292 t.Fatalf("Failed to save config: %v", err)
293 }
294
295 handler, err := NewPublicationHandler()
296 if err != nil {
297 t.Fatalf("Expected no error creating handler, got %v", err)
298 }
299 defer handler.Close()
300
301 if !handler.atproto.IsAuthenticated() {
302 t.Error("Expected handler to be authenticated after restoring from config")
303 }
304
305 session, err := handler.atproto.GetSession()
306 if err != nil {
307 t.Errorf("Expected to get session, got error: %v", err)
308 }
309 if session.DID != config.ATProtoDID {
310 t.Errorf("Expected DID '%s', got '%s'", config.ATProtoDID, session.DID)
311 }
312 })
313
314 t.Run("handles empty config gracefully", func(t *testing.T) {
315 suite := NewHandlerTestSuite(t)
316 defer suite.Cleanup()
317
318 handler, err := NewPublicationHandler()
319 if err != nil {
320 t.Fatalf("Expected no error creating handler, got %v", err)
321 }
322 defer handler.Close()
323
324 if handler.atproto.IsAuthenticated() {
325 t.Error("Expected handler to not be authenticated with empty config")
326 }
327 })
328 })
329
330 t.Run("Close", func(t *testing.T) {
331 t.Run("cleans up resources properly", func(t *testing.T) {
332 suite := NewHandlerTestSuite(t)
333 defer suite.Cleanup()
334
335 handler, err := NewPublicationHandler()
336 if err != nil {
337 t.Fatalf("Expected no error creating handler, got %v", err)
338 }
339
340 err = handler.Close()
341 if err != nil {
342 t.Errorf("Expected no error on close, got %v", err)
343 }
344 })
345 })
346
347 t.Run("documentToMarkdown", func(t *testing.T) {
348 t.Run("converts simple document with text blocks", func(t *testing.T) {
349 doc := services.DocumentWithMeta{
350 Document: public.Document{
351 Pages: []public.LinearDocument{
352 {
353 Blocks: []public.BlockWrap{
354 {
355 Type: "pub.leaflet.pages.linearDocument#block",
356 Block: public.TextBlock{
357 Type: "pub.leaflet.pages.linearDocument#textBlock",
358 Plaintext: "Hello world",
359 },
360 },
361 },
362 },
363 },
364 },
365 }
366
367 markdown, err := documentToMarkdown(doc)
368 if err != nil {
369 t.Fatalf("Expected no error, got %v", err)
370 }
371
372 if markdown != "Hello world" {
373 t.Errorf("Expected 'Hello world', got '%s'", markdown)
374 }
375 })
376
377 t.Run("converts document with headers", func(t *testing.T) {
378 doc := services.DocumentWithMeta{
379 Document: public.Document{
380 Pages: []public.LinearDocument{
381 {
382 Blocks: []public.BlockWrap{
383 {
384 Type: "pub.leaflet.pages.linearDocument#block",
385 Block: public.HeaderBlock{
386 Type: "pub.leaflet.pages.linearDocument#headerBlock",
387 Level: 1,
388 Plaintext: "Main Title",
389 },
390 },
391 {
392 Type: "pub.leaflet.pages.linearDocument#block",
393 Block: public.TextBlock{
394 Type: "pub.leaflet.pages.linearDocument#textBlock",
395 Plaintext: "Content here",
396 },
397 },
398 },
399 },
400 },
401 },
402 }
403
404 markdown, err := documentToMarkdown(doc)
405 if err != nil {
406 t.Fatalf("Expected no error, got %v", err)
407 }
408
409 expected := "# Main Title\n\nContent here"
410 if markdown != expected {
411 t.Errorf("Expected '%s', got '%s'", expected, markdown)
412 }
413 })
414
415 t.Run("converts document with code blocks", func(t *testing.T) {
416 doc := services.DocumentWithMeta{
417 Document: public.Document{
418 Pages: []public.LinearDocument{
419 {
420 Blocks: []public.BlockWrap{
421 {
422 Type: "pub.leaflet.pages.linearDocument#block",
423 Block: public.CodeBlock{
424 Type: "pub.leaflet.pages.linearDocument#codeBlock",
425 Plaintext: "fmt.Println(\"hello\")",
426 Language: "go",
427 },
428 },
429 },
430 },
431 },
432 },
433 }
434
435 markdown, err := documentToMarkdown(doc)
436 if err != nil {
437 t.Fatalf("Expected no error, got %v", err)
438 }
439
440 expected := "```go\nfmt.Println(\"hello\")\n```"
441 if markdown != expected {
442 t.Errorf("Expected '%s', got '%s'", expected, markdown)
443 }
444 })
445
446 t.Run("converts document with multiple pages", func(t *testing.T) {
447 doc := services.DocumentWithMeta{
448 Document: public.Document{
449 Pages: []public.LinearDocument{
450 {
451 Blocks: []public.BlockWrap{
452 {
453 Type: "pub.leaflet.pages.linearDocument#block",
454 Block: public.TextBlock{
455 Type: "pub.leaflet.pages.linearDocument#textBlock",
456 Plaintext: "Page one",
457 },
458 },
459 },
460 },
461 {
462 Blocks: []public.BlockWrap{
463 {
464 Type: "pub.leaflet.pages.linearDocument#block",
465 Block: public.TextBlock{
466 Type: "pub.leaflet.pages.linearDocument#textBlock",
467 Plaintext: "Page two",
468 },
469 },
470 },
471 },
472 },
473 },
474 }
475
476 markdown, err := documentToMarkdown(doc)
477 if err != nil {
478 t.Fatalf("Expected no error, got %v", err)
479 }
480
481 expected := "Page one\n\nPage two"
482 if markdown != expected {
483 t.Errorf("Expected '%s', got '%s'", expected, markdown)
484 }
485 })
486
487 t.Run("handles empty document", func(t *testing.T) {
488 doc := services.DocumentWithMeta{
489 Document: public.Document{
490 Pages: []public.LinearDocument{},
491 },
492 }
493
494 markdown, err := documentToMarkdown(doc)
495 if err != nil {
496 t.Fatalf("Expected no error, got %v", err)
497 }
498
499 if markdown != "" {
500 t.Errorf("Expected empty string, got '%s'", markdown)
501 }
502 })
503 })
504
505 t.Run("Pull", func(t *testing.T) {
506 t.Run("returns error when not authenticated", func(t *testing.T) {
507 suite := NewHandlerTestSuite(t)
508 defer suite.Cleanup()
509
510 handler := CreateHandler(t, NewPublicationHandler)
511 ctx := context.Background()
512
513 err := handler.Pull(ctx)
514 if err == nil {
515 t.Error("Expected error when not authenticated")
516 }
517
518 expectedMsg := "not authenticated"
519 if err != nil && !strings.Contains(err.Error(), expectedMsg) {
520 t.Errorf("Expected error message to contain '%s', got '%s'", expectedMsg, err.Error())
521 }
522 })
523 })
524
525 t.Run("List", func(t *testing.T) {
526 t.Run("lists all leaflet notes", func(t *testing.T) {
527 suite := NewHandlerTestSuite(t)
528 defer suite.Cleanup()
529
530 handler := CreateHandler(t, NewPublicationHandler)
531 ctx := context.Background()
532
533 rkey1 := "test_rkey_1"
534 cid1 := "test_cid_1"
535 publishedAt := time.Now()
536
537 note1 := &models.Note{
538 Title: "Published Note",
539 Content: "Content 1",
540 LeafletRKey: &rkey1,
541 LeafletCID: &cid1,
542 PublishedAt: &publishedAt,
543 IsDraft: false,
544 }
545
546 _, err := handler.repos.Notes.Create(ctx, note1)
547 suite.AssertNoError(err, "create published note")
548
549 rkey2 := "test_rkey_2"
550 cid2 := "test_cid_2"
551 note2 := &models.Note{
552 Title: "Draft Note",
553 Content: "Content 2",
554 LeafletRKey: &rkey2,
555 LeafletCID: &cid2,
556 IsDraft: true,
557 }
558
559 _, err = handler.repos.Notes.Create(ctx, note2)
560 suite.AssertNoError(err, "create draft note")
561
562 err = handler.List(ctx, "all")
563 suite.AssertNoError(err, "list all notes")
564
565 err = handler.List(ctx, "")
566 suite.AssertNoError(err, "list with empty filter")
567 })
568
569 t.Run("lists only published notes", func(t *testing.T) {
570 suite := NewHandlerTestSuite(t)
571 defer suite.Cleanup()
572
573 handler := CreateHandler(t, NewPublicationHandler)
574 ctx := context.Background()
575
576 rkey := "published_rkey"
577 cid := "published_cid"
578 publishedAt := time.Now()
579
580 note := &models.Note{
581 Title: "Published Note",
582 Content: "Content",
583 LeafletRKey: &rkey,
584 LeafletCID: &cid,
585 PublishedAt: &publishedAt,
586 IsDraft: false,
587 }
588
589 _, err := handler.repos.Notes.Create(ctx, note)
590 suite.AssertNoError(err, "create published note")
591
592 err = handler.List(ctx, "published")
593 suite.AssertNoError(err, "list published notes")
594 })
595
596 t.Run("lists only draft notes", func(t *testing.T) {
597 suite := NewHandlerTestSuite(t)
598 defer suite.Cleanup()
599
600 handler := CreateHandler(t, NewPublicationHandler)
601 ctx := context.Background()
602
603 rkey := "draft_rkey"
604 cid := "draft_cid"
605
606 note := &models.Note{
607 Title: "Draft Note",
608 Content: "Content",
609 LeafletRKey: &rkey,
610 LeafletCID: &cid,
611 IsDraft: true,
612 }
613
614 _, err := handler.repos.Notes.Create(ctx, note)
615 suite.AssertNoError(err, "create draft note")
616
617 err = handler.List(ctx, "draft")
618 suite.AssertNoError(err, "list draft notes")
619 })
620
621 t.Run("handles empty results gracefully", func(t *testing.T) {
622 suite := NewHandlerTestSuite(t)
623 defer suite.Cleanup()
624
625 handler := CreateHandler(t, NewPublicationHandler)
626 ctx := context.Background()
627
628 err := handler.List(ctx, "all")
629 suite.AssertNoError(err, "list with no notes")
630 })
631
632 t.Run("returns error for invalid filter", func(t *testing.T) {
633 suite := NewHandlerTestSuite(t)
634 defer suite.Cleanup()
635
636 handler := CreateHandler(t, NewPublicationHandler)
637 ctx := context.Background()
638
639 err := handler.List(ctx, "invalid_filter")
640 if err == nil {
641 t.Error("Expected error for invalid filter")
642 }
643
644 expectedMsg := "invalid filter"
645 if err != nil && !strings.Contains(err.Error(), expectedMsg) {
646 t.Errorf("Expected error message to contain '%s', got '%s'", expectedMsg, err.Error())
647 }
648 })
649
650 t.Run("only lists notes with leaflet metadata", func(t *testing.T) {
651 suite := NewHandlerTestSuite(t)
652 defer suite.Cleanup()
653
654 handler := CreateHandler(t, NewPublicationHandler)
655 ctx := context.Background()
656
657 regularNote := &models.Note{
658 Title: "Regular Note",
659 Content: "No leaflet data",
660 }
661
662 _, err := handler.repos.Notes.Create(ctx, regularNote)
663 suite.AssertNoError(err, "create regular note")
664
665 rkey := "leaflet_rkey"
666 cid := "leaflet_cid"
667 leafletNote := &models.Note{
668 Title: "Leaflet Note",
669 Content: "Has leaflet data",
670 LeafletRKey: &rkey,
671 LeafletCID: &cid,
672 IsDraft: false,
673 }
674
675 _, err = handler.repos.Notes.Create(ctx, leafletNote)
676 suite.AssertNoError(err, "create leaflet note")
677
678 err = handler.List(ctx, "all")
679 suite.AssertNoError(err, "list all leaflet notes")
680 })
681 })
682
683 t.Run("Post", func(t *testing.T) {
684 t.Run("returns error when not authenticated", func(t *testing.T) {
685 suite := NewHandlerTestSuite(t)
686 defer suite.Cleanup()
687
688 handler := CreateHandler(t, NewPublicationHandler)
689 ctx := context.Background()
690
691 err := handler.Post(ctx, 1, false)
692 if err == nil {
693 t.Error("Expected error when not authenticated")
694 }
695
696 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
697 t.Errorf("Expected 'not authenticated' error, got '%v'", err)
698 }
699 })
700
701 t.Run("returns error when note does not exist", func(t *testing.T) {
702 suite := NewHandlerTestSuite(t)
703 defer suite.Cleanup()
704
705 handler := CreateHandler(t, NewPublicationHandler)
706 ctx := context.Background()
707
708 session := &services.Session{
709 DID: "did:plc:test123",
710 Handle: "test.bsky.social",
711 AccessJWT: "access_token",
712 RefreshJWT: "refresh_token",
713 PDSURL: "https://bsky.social",
714 ExpiresAt: time.Now().Add(2 * time.Hour),
715 Authenticated: true,
716 }
717
718 err := handler.atproto.RestoreSession(session)
719 if err != nil {
720 t.Fatalf("Failed to restore session: %v", err)
721 }
722
723 err = handler.Post(ctx, 999, false)
724 if err == nil {
725 t.Error("Expected error when note does not exist")
726 }
727
728 if err != nil && !strings.Contains(err.Error(), "failed to get note") {
729 t.Errorf("Expected 'failed to get note' error, got '%v'", err)
730 }
731 })
732
733 t.Run("returns error when note already published", func(t *testing.T) {
734 suite := NewHandlerTestSuite(t)
735 defer suite.Cleanup()
736
737 handler := CreateHandler(t, NewPublicationHandler)
738 ctx := context.Background()
739
740 rkey := "existing_rkey"
741 cid := "existing_cid"
742 note := &models.Note{
743 Title: "Already Published",
744 Content: "Test content",
745 LeafletRKey: &rkey,
746 LeafletCID: &cid,
747 }
748
749 id, err := handler.repos.Notes.Create(ctx, note)
750 suite.AssertNoError(err, "create note")
751
752 session := &services.Session{
753 DID: "did:plc:test123",
754 Handle: "test.bsky.social",
755 AccessJWT: "access_token",
756 RefreshJWT: "refresh_token",
757 PDSURL: "https://bsky.social",
758 ExpiresAt: time.Now().Add(2 * time.Hour),
759 Authenticated: true,
760 }
761
762 err = handler.atproto.RestoreSession(session)
763 if err != nil {
764 t.Fatalf("Failed to restore session: %v", err)
765 }
766
767 err = handler.Post(ctx, id, false)
768 if err == nil {
769 t.Error("Expected error when note already published")
770 }
771
772 if err != nil && !strings.Contains(err.Error(), "already published") {
773 t.Errorf("Expected 'already published' error, got '%v'", err)
774 }
775 })
776
777 t.Run("handles markdown conversion errors", func(t *testing.T) {
778 suite := NewHandlerTestSuite(t)
779 defer suite.Cleanup()
780
781 handler := CreateHandler(t, NewPublicationHandler)
782 ctx := context.Background()
783
784 note := &models.Note{
785 Title: "Test Note",
786 Content: "# Valid markdown",
787 }
788
789 id, err := handler.repos.Notes.Create(ctx, note)
790 suite.AssertNoError(err, "create note")
791
792 session := &services.Session{
793 DID: "did:plc:test123",
794 Handle: "test.bsky.social",
795 AccessJWT: "access_token",
796 RefreshJWT: "refresh_token",
797 PDSURL: "https://bsky.social",
798 ExpiresAt: time.Now().Add(2 * time.Hour),
799 Authenticated: true,
800 }
801
802 err = handler.atproto.RestoreSession(session)
803 if err != nil {
804 t.Fatalf("Failed to restore session: %v", err)
805 }
806
807 err = handler.Post(ctx, id, false)
808 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
809 if !strings.Contains(err.Error(), "failed to post document") && !strings.Contains(err.Error(), "failed to get session") {
810 t.Logf("Got expected error during post: %v", err)
811 }
812 }
813 })
814
815 t.Run("sets correct draft status", func(t *testing.T) {
816 suite := NewHandlerTestSuite(t)
817 defer suite.Cleanup()
818
819 handler := CreateHandler(t, NewPublicationHandler)
820 ctx := context.Background()
821
822 note := &models.Note{
823 Title: "Draft Note",
824 Content: "# Test content",
825 }
826
827 id, err := handler.repos.Notes.Create(ctx, note)
828 suite.AssertNoError(err, "create note")
829
830 session := &services.Session{
831 DID: "did:plc:test123",
832 Handle: "test.bsky.social",
833 AccessJWT: "access_token",
834 RefreshJWT: "refresh_token",
835 PDSURL: "https://bsky.social",
836 ExpiresAt: time.Now().Add(2 * time.Hour),
837 Authenticated: true,
838 }
839
840 err = handler.atproto.RestoreSession(session)
841 if err != nil {
842 t.Fatalf("Failed to restore session: %v", err)
843 }
844
845 err = handler.Post(ctx, id, true)
846
847 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
848 t.Logf("Got error during post (expected for external service call): %v", err)
849 }
850 })
851 })
852
853 t.Run("Patch", func(t *testing.T) {
854 t.Run("returns error when not authenticated", func(t *testing.T) {
855 suite := NewHandlerTestSuite(t)
856 defer suite.Cleanup()
857
858 handler := CreateHandler(t, NewPublicationHandler)
859 ctx := context.Background()
860
861 err := handler.Patch(ctx, 1)
862 if err == nil {
863 t.Error("Expected error when not authenticated")
864 }
865
866 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
867 t.Errorf("Expected 'not authenticated' error, got '%v'", err)
868 }
869 })
870
871 t.Run("returns error when note does not exist", func(t *testing.T) {
872 suite := NewHandlerTestSuite(t)
873 defer suite.Cleanup()
874
875 handler := CreateHandler(t, NewPublicationHandler)
876 ctx := context.Background()
877
878 session := &services.Session{
879 DID: "did:plc:test123",
880 Handle: "test.bsky.social",
881 AccessJWT: "access_token",
882 RefreshJWT: "refresh_token",
883 PDSURL: "https://bsky.social",
884 ExpiresAt: time.Now().Add(2 * time.Hour),
885 Authenticated: true,
886 }
887
888 err := handler.atproto.RestoreSession(session)
889 if err != nil {
890 t.Fatalf("Failed to restore session: %v", err)
891 }
892
893 err = handler.Patch(ctx, 999)
894 if err == nil {
895 t.Error("Expected error when note does not exist")
896 }
897
898 if err != nil && !strings.Contains(err.Error(), "failed to get note") {
899 t.Errorf("Expected 'failed to get note' error, got '%v'", err)
900 }
901 })
902
903 t.Run("returns error when note not published", func(t *testing.T) {
904 suite := NewHandlerTestSuite(t)
905 defer suite.Cleanup()
906
907 handler := CreateHandler(t, NewPublicationHandler)
908 ctx := context.Background()
909
910 note := &models.Note{
911 Title: "Not Published",
912 Content: "Test content",
913 }
914
915 id, err := handler.repos.Notes.Create(ctx, note)
916 suite.AssertNoError(err, "create note")
917
918 session := &services.Session{
919 DID: "did:plc:test123",
920 Handle: "test.bsky.social",
921 AccessJWT: "access_token",
922 RefreshJWT: "refresh_token",
923 PDSURL: "https://bsky.social",
924 ExpiresAt: time.Now().Add(2 * time.Hour),
925 Authenticated: true,
926 }
927
928 err = handler.atproto.RestoreSession(session)
929 if err != nil {
930 t.Fatalf("Failed to restore session: %v", err)
931 }
932
933 err = handler.Patch(ctx, id)
934 if err == nil {
935 t.Error("Expected error when note not published")
936 }
937
938 if err != nil && !strings.Contains(err.Error(), "not published") {
939 t.Errorf("Expected 'not published' error, got '%v'", err)
940 }
941 })
942
943 t.Run("handles published note with existing metadata", func(t *testing.T) {
944 suite := NewHandlerTestSuite(t)
945 defer suite.Cleanup()
946
947 handler := CreateHandler(t, NewPublicationHandler)
948 ctx := context.Background()
949
950 rkey := "existing_rkey"
951 cid := "existing_cid"
952 publishedAt := time.Now().Add(-24 * time.Hour)
953 note := &models.Note{
954 Title: "Published Note",
955 Content: "# Updated content",
956 LeafletRKey: &rkey,
957 LeafletCID: &cid,
958 PublishedAt: &publishedAt,
959 IsDraft: false,
960 }
961
962 id, err := handler.repos.Notes.Create(ctx, note)
963 suite.AssertNoError(err, "create note")
964
965 session := &services.Session{
966 DID: "did:plc:test123",
967 Handle: "test.bsky.social",
968 AccessJWT: "access_token",
969 RefreshJWT: "refresh_token",
970 PDSURL: "https://bsky.social",
971 ExpiresAt: time.Now().Add(2 * time.Hour),
972 Authenticated: true,
973 }
974
975 err = handler.atproto.RestoreSession(session)
976 if err != nil {
977 t.Fatalf("Failed to restore session: %v", err)
978 }
979
980 err = handler.Patch(ctx, id)
981
982 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
983 t.Logf("Got error during patch (expected for external service call): %v", err)
984 }
985 })
986
987 t.Run("handles draft note", func(t *testing.T) {
988 suite := NewHandlerTestSuite(t)
989 defer suite.Cleanup()
990
991 handler := CreateHandler(t, NewPublicationHandler)
992 ctx := context.Background()
993
994 rkey := "draft_rkey"
995 cid := "draft_cid"
996 note := &models.Note{
997 Title: "Draft Note",
998 Content: "# Draft content",
999 LeafletRKey: &rkey,
1000 LeafletCID: &cid,
1001 IsDraft: true,
1002 }
1003
1004 id, err := handler.repos.Notes.Create(ctx, note)
1005 suite.AssertNoError(err, "create note")
1006
1007 session := &services.Session{
1008 DID: "did:plc:test123",
1009 Handle: "test.bsky.social",
1010 AccessJWT: "access_token",
1011 RefreshJWT: "refresh_token",
1012 PDSURL: "https://bsky.social",
1013 ExpiresAt: time.Now().Add(2 * time.Hour),
1014 Authenticated: true,
1015 }
1016
1017 err = handler.atproto.RestoreSession(session)
1018 if err != nil {
1019 t.Fatalf("Failed to restore session: %v", err)
1020 }
1021
1022 err = handler.Patch(ctx, id)
1023
1024 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
1025 t.Logf("Got error during patch (expected for external service call): %v", err)
1026 }
1027 })
1028
1029 t.Run("handles markdown conversion errors", func(t *testing.T) {
1030 suite := NewHandlerTestSuite(t)
1031 defer suite.Cleanup()
1032
1033 handler := CreateHandler(t, NewPublicationHandler)
1034 ctx := context.Background()
1035
1036 rkey := "test_rkey"
1037 cid := "test_cid"
1038 note := &models.Note{
1039 Title: "Test Note",
1040 Content: "# Valid markdown",
1041 LeafletRKey: &rkey,
1042 LeafletCID: &cid,
1043 }
1044
1045 id, err := handler.repos.Notes.Create(ctx, note)
1046 suite.AssertNoError(err, "create note")
1047
1048 session := &services.Session{
1049 DID: "did:plc:test123",
1050 Handle: "test.bsky.social",
1051 AccessJWT: "access_token",
1052 RefreshJWT: "refresh_token",
1053 PDSURL: "https://bsky.social",
1054 ExpiresAt: time.Now().Add(2 * time.Hour),
1055 Authenticated: true,
1056 }
1057
1058 err = handler.atproto.RestoreSession(session)
1059 if err != nil {
1060 t.Fatalf("Failed to restore session: %v", err)
1061 }
1062
1063 err = handler.Patch(ctx, id)
1064
1065 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
1066 t.Logf("Got error during patch (expected for external service call): %v", err)
1067 }
1068 })
1069 })
1070
1071 t.Run("PostPreview", func(t *testing.T) {
1072 t.Run("returns error when not authenticated", func(t *testing.T) {
1073 suite := NewHandlerTestSuite(t)
1074 defer suite.Cleanup()
1075
1076 handler := CreateHandler(t, NewPublicationHandler)
1077 ctx := context.Background()
1078
1079 err := handler.PostPreview(ctx, 1, false, "", false)
1080 if err == nil {
1081 t.Error("Expected error when not authenticated")
1082 }
1083
1084 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
1085 t.Errorf("Expected 'not authenticated' error, got '%v'", err)
1086 }
1087 })
1088
1089 t.Run("returns error when note does not exist", func(t *testing.T) {
1090 suite := NewHandlerTestSuite(t)
1091 defer suite.Cleanup()
1092
1093 handler := CreateHandler(t, NewPublicationHandler)
1094 ctx := context.Background()
1095
1096 session := &services.Session{
1097 DID: "did:plc:test123",
1098 Handle: "test.bsky.social",
1099 AccessJWT: "access_token",
1100 RefreshJWT: "refresh_token",
1101 PDSURL: "https://bsky.social",
1102 ExpiresAt: time.Now().Add(2 * time.Hour),
1103 Authenticated: true,
1104 }
1105
1106 err := handler.atproto.RestoreSession(session)
1107 if err != nil {
1108 t.Fatalf("Failed to restore session: %v", err)
1109 }
1110
1111 err = handler.PostPreview(ctx, 999, false, "", false)
1112 if err == nil {
1113 t.Error("Expected error when note does not exist")
1114 }
1115
1116 if err != nil && !strings.Contains(err.Error(), "failed to get note") {
1117 t.Errorf("Expected 'failed to get note' error, got '%v'", err)
1118 }
1119 })
1120
1121 t.Run("returns error when note already published", func(t *testing.T) {
1122 suite := NewHandlerTestSuite(t)
1123 defer suite.Cleanup()
1124
1125 handler := CreateHandler(t, NewPublicationHandler)
1126 ctx := context.Background()
1127
1128 rkey := "existing_rkey"
1129 cid := "existing_cid"
1130 note := &models.Note{
1131 Title: "Already Published",
1132 Content: "# Test content",
1133 LeafletRKey: &rkey,
1134 LeafletCID: &cid,
1135 }
1136
1137 id, err := handler.repos.Notes.Create(ctx, note)
1138 suite.AssertNoError(err, "create note")
1139
1140 session := &services.Session{
1141 DID: "did:plc:test123",
1142 Handle: "test.bsky.social",
1143 AccessJWT: "access_token",
1144 RefreshJWT: "refresh_token",
1145 PDSURL: "https://bsky.social",
1146 ExpiresAt: time.Now().Add(2 * time.Hour),
1147 Authenticated: true,
1148 }
1149
1150 err = handler.atproto.RestoreSession(session)
1151 if err != nil {
1152 t.Fatalf("Failed to restore session: %v", err)
1153 }
1154
1155 err = handler.PostPreview(ctx, id, false, "", false)
1156 if err == nil {
1157 t.Error("Expected error when note already published")
1158 }
1159
1160 if err != nil && !strings.Contains(err.Error(), "already published") {
1161 t.Errorf("Expected 'already published' error, got '%v'", err)
1162 }
1163 })
1164
1165 t.Run("shows preview for valid note", func(t *testing.T) {
1166 suite := NewHandlerTestSuite(t)
1167 defer suite.Cleanup()
1168
1169 handler := CreateHandler(t, NewPublicationHandler)
1170 ctx := context.Background()
1171
1172 note := &models.Note{
1173 Title: "Test Note",
1174 Content: "# Test content\n\nThis is a test.",
1175 }
1176
1177 id, err := handler.repos.Notes.Create(ctx, note)
1178 suite.AssertNoError(err, "create note")
1179
1180 mock := services.NewMockATProtoService()
1181 mock.IsAuthenticatedVal = true
1182 mock.Session = &services.Session{
1183 DID: "did:plc:test123",
1184 Handle: "test.bsky.social",
1185 AccessJWT: "access_token",
1186 RefreshJWT: "refresh_token",
1187 PDSURL: "https://bsky.social",
1188 ExpiresAt: time.Now().Add(2 * time.Hour),
1189 Authenticated: true,
1190 }
1191 handler.atproto = mock
1192
1193 err = handler.PostPreview(ctx, id, false, "", false)
1194 suite.AssertNoError(err, "preview should succeed")
1195 })
1196
1197 t.Run("shows preview for draft", func(t *testing.T) {
1198 suite := NewHandlerTestSuite(t)
1199 defer suite.Cleanup()
1200
1201 handler := CreateHandler(t, NewPublicationHandler)
1202 ctx := context.Background()
1203
1204 note := &models.Note{
1205 Title: "Draft Note",
1206 Content: "# Draft content",
1207 }
1208
1209 id, err := handler.repos.Notes.Create(ctx, note)
1210 suite.AssertNoError(err, "create note")
1211
1212 mock := services.NewMockATProtoService()
1213 mock.IsAuthenticatedVal = true
1214 mock.Session = &services.Session{
1215 DID: "did:plc:test123",
1216 Handle: "test.bsky.social",
1217 AccessJWT: "access_token",
1218 RefreshJWT: "refresh_token",
1219 PDSURL: "https://bsky.social",
1220 ExpiresAt: time.Now().Add(2 * time.Hour),
1221 Authenticated: true,
1222 }
1223 handler.atproto = mock
1224
1225 err = handler.PostPreview(ctx, id, true, "", false)
1226 suite.AssertNoError(err, "preview draft should succeed")
1227 })
1228 })
1229
1230 t.Run("PostValidate", func(t *testing.T) {
1231 t.Run("returns error when not authenticated", func(t *testing.T) {
1232 suite := NewHandlerTestSuite(t)
1233 defer suite.Cleanup()
1234
1235 handler := CreateHandler(t, NewPublicationHandler)
1236 ctx := context.Background()
1237
1238 err := handler.PostValidate(ctx, 1, false, "", false)
1239 if err == nil {
1240 t.Error("Expected error when not authenticated")
1241 }
1242
1243 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
1244 t.Errorf("Expected 'not authenticated' error, got '%v'", err)
1245 }
1246 })
1247
1248 t.Run("validates markdown conversion successfully", func(t *testing.T) {
1249 suite := NewHandlerTestSuite(t)
1250 defer suite.Cleanup()
1251
1252 handler := CreateHandler(t, NewPublicationHandler)
1253 ctx := context.Background()
1254
1255 note := &models.Note{
1256 Title: "Test Note",
1257 Content: "# Test content\n\nValid markdown here.",
1258 }
1259
1260 id, err := handler.repos.Notes.Create(ctx, note)
1261 suite.AssertNoError(err, "create note")
1262
1263 mock := services.NewMockATProtoService()
1264 mock.IsAuthenticatedVal = true
1265 mock.Session = &services.Session{
1266 DID: "did:plc:test123",
1267 Handle: "test.bsky.social",
1268 AccessJWT: "access_token",
1269 RefreshJWT: "refresh_token",
1270 PDSURL: "https://bsky.social",
1271 ExpiresAt: time.Now().Add(2 * time.Hour),
1272 Authenticated: true,
1273 }
1274 handler.atproto = mock
1275
1276 err = handler.PostValidate(ctx, id, false, "", false)
1277 suite.AssertNoError(err, "validation should succeed")
1278 })
1279 })
1280
1281 t.Run("PatchPreview", func(t *testing.T) {
1282 t.Run("returns error when not authenticated", func(t *testing.T) {
1283 suite := NewHandlerTestSuite(t)
1284 defer suite.Cleanup()
1285
1286 handler := CreateHandler(t, NewPublicationHandler)
1287 ctx := context.Background()
1288
1289 err := handler.PatchPreview(ctx, 1, "", false)
1290 if err == nil {
1291 t.Error("Expected error when not authenticated")
1292 }
1293
1294 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
1295 t.Errorf("Expected 'not authenticated' error, got '%v'", err)
1296 }
1297 })
1298
1299 t.Run("returns error when note does not exist", func(t *testing.T) {
1300 suite := NewHandlerTestSuite(t)
1301 defer suite.Cleanup()
1302
1303 handler := CreateHandler(t, NewPublicationHandler)
1304 ctx := context.Background()
1305
1306 session := &services.Session{
1307 DID: "did:plc:test123",
1308 Handle: "test.bsky.social",
1309 AccessJWT: "access_token",
1310 RefreshJWT: "refresh_token",
1311 PDSURL: "https://bsky.social",
1312 ExpiresAt: time.Now().Add(2 * time.Hour),
1313 Authenticated: true,
1314 }
1315
1316 err := handler.atproto.RestoreSession(session)
1317 if err != nil {
1318 t.Fatalf("Failed to restore session: %v", err)
1319 }
1320
1321 err = handler.PatchPreview(ctx, 999, "", false)
1322 if err == nil {
1323 t.Error("Expected error when note does not exist")
1324 }
1325
1326 if err != nil && !strings.Contains(err.Error(), "failed to get note") {
1327 t.Errorf("Expected 'failed to get note' error, got '%v'", err)
1328 }
1329 })
1330
1331 t.Run("returns error when note not published", func(t *testing.T) {
1332 suite := NewHandlerTestSuite(t)
1333 defer suite.Cleanup()
1334
1335 handler := CreateHandler(t, NewPublicationHandler)
1336 ctx := context.Background()
1337
1338 note := &models.Note{
1339 Title: "Not Published",
1340 Content: "# Test content",
1341 }
1342
1343 id, err := handler.repos.Notes.Create(ctx, note)
1344 suite.AssertNoError(err, "create note")
1345
1346 session := &services.Session{
1347 DID: "did:plc:test123",
1348 Handle: "test.bsky.social",
1349 AccessJWT: "access_token",
1350 RefreshJWT: "refresh_token",
1351 PDSURL: "https://bsky.social",
1352 ExpiresAt: time.Now().Add(2 * time.Hour),
1353 Authenticated: true,
1354 }
1355
1356 err = handler.atproto.RestoreSession(session)
1357 if err != nil {
1358 t.Fatalf("Failed to restore session: %v", err)
1359 }
1360
1361 err = handler.PatchPreview(ctx, id, "", false)
1362 if err == nil {
1363 t.Error("Expected error when note not published")
1364 }
1365
1366 if err != nil && !strings.Contains(err.Error(), "not published") {
1367 t.Errorf("Expected 'not published' error, got '%v'", err)
1368 }
1369 })
1370
1371 t.Run("shows preview for published note", func(t *testing.T) {
1372 suite := NewHandlerTestSuite(t)
1373 defer suite.Cleanup()
1374
1375 handler := CreateHandler(t, NewPublicationHandler)
1376 ctx := context.Background()
1377
1378 rkey := "test_rkey"
1379 cid := "test_cid"
1380 publishedAt := time.Now().Add(-24 * time.Hour)
1381 note := &models.Note{
1382 Title: "Published Note",
1383 Content: "# Updated content",
1384 LeafletRKey: &rkey,
1385 LeafletCID: &cid,
1386 PublishedAt: &publishedAt,
1387 IsDraft: false,
1388 }
1389
1390 id, err := handler.repos.Notes.Create(ctx, note)
1391 suite.AssertNoError(err, "create note")
1392
1393 mock := services.NewMockATProtoService()
1394 mock.IsAuthenticatedVal = true
1395 mock.Session = &services.Session{
1396 DID: "did:plc:test123",
1397 Handle: "test.bsky.social",
1398 AccessJWT: "access_token",
1399 RefreshJWT: "refresh_token",
1400 PDSURL: "https://bsky.social",
1401 ExpiresAt: time.Now().Add(2 * time.Hour),
1402 Authenticated: true,
1403 }
1404 handler.atproto = mock
1405
1406 err = handler.PatchPreview(ctx, id, "", false)
1407 suite.AssertNoError(err, "preview should succeed")
1408 })
1409 })
1410
1411 t.Run("PatchValidate", func(t *testing.T) {
1412 t.Run("returns error when not authenticated", func(t *testing.T) {
1413 suite := NewHandlerTestSuite(t)
1414 defer suite.Cleanup()
1415
1416 handler := CreateHandler(t, NewPublicationHandler)
1417 ctx := context.Background()
1418
1419 err := handler.PatchValidate(ctx, 1, "", false)
1420 if err == nil {
1421 t.Error("Expected error when not authenticated")
1422 }
1423
1424 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
1425 t.Errorf("Expected 'not authenticated' error, got '%v'", err)
1426 }
1427 })
1428
1429 t.Run("validates markdown conversion successfully", func(t *testing.T) {
1430 suite := NewHandlerTestSuite(t)
1431 defer suite.Cleanup()
1432
1433 handler := CreateHandler(t, NewPublicationHandler)
1434 ctx := context.Background()
1435
1436 rkey := "test_rkey"
1437 cid := "test_cid"
1438 note := &models.Note{
1439 Title: "Published Note",
1440 Content: "# Updated content\n\nValid markdown here.",
1441 LeafletRKey: &rkey,
1442 LeafletCID: &cid,
1443 IsDraft: false,
1444 }
1445
1446 id, err := handler.repos.Notes.Create(ctx, note)
1447 suite.AssertNoError(err, "create note")
1448
1449 mock := services.NewMockATProtoService()
1450 mock.IsAuthenticatedVal = true
1451 mock.Session = &services.Session{
1452 DID: "did:plc:test123",
1453 Handle: "test.bsky.social",
1454 AccessJWT: "access_token",
1455 RefreshJWT: "refresh_token",
1456 PDSURL: "https://bsky.social",
1457 ExpiresAt: time.Now().Add(2 * time.Hour),
1458 Authenticated: true,
1459 }
1460 handler.atproto = mock
1461
1462 err = handler.PatchValidate(ctx, id, "", false)
1463 suite.AssertNoError(err, "validation should succeed")
1464 })
1465 })
1466
1467 t.Run("Delete", func(t *testing.T) {
1468 t.Run("returns error when not authenticated", func(t *testing.T) {
1469 suite := NewHandlerTestSuite(t)
1470 defer suite.Cleanup()
1471
1472 handler := CreateHandler(t, NewPublicationHandler)
1473 ctx := context.Background()
1474
1475 err := handler.Delete(ctx, 1)
1476 if err == nil {
1477 t.Error("Expected error when not authenticated")
1478 }
1479
1480 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
1481 t.Errorf("Expected 'not authenticated' error, got '%v'", err)
1482 }
1483 })
1484
1485 t.Run("returns error when note does not exist", func(t *testing.T) {
1486 suite := NewHandlerTestSuite(t)
1487 defer suite.Cleanup()
1488
1489 handler := CreateHandler(t, NewPublicationHandler)
1490 ctx := context.Background()
1491
1492 session := &services.Session{
1493 DID: "did:plc:test123",
1494 Handle: "test.bsky.social",
1495 AccessJWT: "access_token",
1496 RefreshJWT: "refresh_token",
1497 PDSURL: "https://bsky.social",
1498 ExpiresAt: time.Now().Add(2 * time.Hour),
1499 Authenticated: true,
1500 }
1501
1502 err := handler.atproto.RestoreSession(session)
1503 if err != nil {
1504 t.Fatalf("Failed to restore session: %v", err)
1505 }
1506
1507 err = handler.Delete(ctx, 999)
1508 if err == nil {
1509 t.Error("Expected error when note does not exist")
1510 }
1511
1512 if err != nil && !strings.Contains(err.Error(), "failed to get note") {
1513 t.Errorf("Expected 'failed to get note' error, got '%v'", err)
1514 }
1515 })
1516
1517 t.Run("returns error when note not published", func(t *testing.T) {
1518 suite := NewHandlerTestSuite(t)
1519 defer suite.Cleanup()
1520
1521 handler := CreateHandler(t, NewPublicationHandler)
1522 ctx := context.Background()
1523
1524 note := &models.Note{
1525 Title: "Not Published",
1526 Content: "Test content",
1527 }
1528
1529 id, err := handler.repos.Notes.Create(ctx, note)
1530 suite.AssertNoError(err, "create note")
1531
1532 session := &services.Session{
1533 DID: "did:plc:test123",
1534 Handle: "test.bsky.social",
1535 AccessJWT: "access_token",
1536 RefreshJWT: "refresh_token",
1537 PDSURL: "https://bsky.social",
1538 ExpiresAt: time.Now().Add(2 * time.Hour),
1539 Authenticated: true,
1540 }
1541
1542 err = handler.atproto.RestoreSession(session)
1543 if err != nil {
1544 t.Fatalf("Failed to restore session: %v", err)
1545 }
1546
1547 err = handler.Delete(ctx, id)
1548 if err == nil {
1549 t.Error("Expected error when note not published")
1550 }
1551
1552 if err != nil && !strings.Contains(err.Error(), "not published") {
1553 t.Errorf("Expected 'not published' error, got '%v'", err)
1554 }
1555 })
1556
1557 t.Run("handles published note", func(t *testing.T) {
1558 suite := NewHandlerTestSuite(t)
1559 defer suite.Cleanup()
1560
1561 handler := CreateHandler(t, NewPublicationHandler)
1562 ctx := context.Background()
1563
1564 rkey := "test_rkey"
1565 cid := "test_cid"
1566 publishedAt := time.Now().Add(-24 * time.Hour)
1567 note := &models.Note{
1568 Title: "Published Note",
1569 Content: "# Test content",
1570 LeafletRKey: &rkey,
1571 LeafletCID: &cid,
1572 PublishedAt: &publishedAt,
1573 IsDraft: false,
1574 }
1575
1576 id, err := handler.repos.Notes.Create(ctx, note)
1577 suite.AssertNoError(err, "create note")
1578
1579 session := &services.Session{
1580 DID: "did:plc:test123",
1581 Handle: "test.bsky.social",
1582 AccessJWT: "access_token",
1583 RefreshJWT: "refresh_token",
1584 PDSURL: "https://bsky.social",
1585 ExpiresAt: time.Now().Add(2 * time.Hour),
1586 Authenticated: true,
1587 }
1588
1589 err = handler.atproto.RestoreSession(session)
1590 if err != nil {
1591 t.Fatalf("Failed to restore session: %v", err)
1592 }
1593
1594 err = handler.Delete(ctx, id)
1595
1596 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
1597 t.Logf("Got error during delete (expected for external service call): %v", err)
1598 }
1599 })
1600
1601 t.Run("handles draft note", func(t *testing.T) {
1602 suite := NewHandlerTestSuite(t)
1603 defer suite.Cleanup()
1604
1605 handler := CreateHandler(t, NewPublicationHandler)
1606 ctx := context.Background()
1607
1608 rkey := "draft_rkey"
1609 cid := "draft_cid"
1610 note := &models.Note{
1611 Title: "Draft Note",
1612 Content: "# Draft content",
1613 LeafletRKey: &rkey,
1614 LeafletCID: &cid,
1615 IsDraft: true,
1616 }
1617
1618 id, err := handler.repos.Notes.Create(ctx, note)
1619 suite.AssertNoError(err, "create note")
1620
1621 session := &services.Session{
1622 DID: "did:plc:test123",
1623 Handle: "test.bsky.social",
1624 AccessJWT: "access_token",
1625 RefreshJWT: "refresh_token",
1626 PDSURL: "https://bsky.social",
1627 ExpiresAt: time.Now().Add(2 * time.Hour),
1628 Authenticated: true,
1629 }
1630
1631 err = handler.atproto.RestoreSession(session)
1632 if err != nil {
1633 t.Fatalf("Failed to restore session: %v", err)
1634 }
1635
1636 err = handler.Delete(ctx, id)
1637
1638 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
1639 t.Logf("Got error during delete (expected for external service call): %v", err)
1640 }
1641 })
1642
1643 t.Run("does not clear metadata when delete fails", func(t *testing.T) {
1644 suite := NewHandlerTestSuite(t)
1645 defer suite.Cleanup()
1646
1647 handler := CreateHandler(t, NewPublicationHandler)
1648 ctx := context.Background()
1649
1650 rkey := "test_rkey"
1651 cid := "test_cid"
1652 publishedAt := time.Now().Add(-24 * time.Hour)
1653 note := &models.Note{
1654 Title: "Test Note",
1655 Content: "# Test content",
1656 LeafletRKey: &rkey,
1657 LeafletCID: &cid,
1658 PublishedAt: &publishedAt,
1659 IsDraft: false,
1660 }
1661
1662 id, err := handler.repos.Notes.Create(ctx, note)
1663 suite.AssertNoError(err, "create note")
1664
1665 session := &services.Session{
1666 DID: "did:plc:test123",
1667 Handle: "test.bsky.social",
1668 AccessJWT: "access_token",
1669 RefreshJWT: "refresh_token",
1670 PDSURL: "https://bsky.social",
1671 ExpiresAt: time.Now().Add(2 * time.Hour),
1672 Authenticated: true,
1673 }
1674
1675 err = handler.atproto.RestoreSession(session)
1676 if err != nil {
1677 t.Fatalf("Failed to restore session: %v", err)
1678 }
1679
1680 err = handler.Delete(ctx, id)
1681 if err == nil {
1682 t.Fatal("Expected delete to fail with invalid token")
1683 }
1684
1685 if !strings.Contains(err.Error(), "failed to delete document") {
1686 t.Logf("Got error: %v", err)
1687 }
1688
1689 updatedNote, err := handler.repos.Notes.Get(ctx, id)
1690 if err != nil {
1691 t.Fatalf("Failed to get updated note: %v", err)
1692 }
1693
1694 if !updatedNote.HasLeafletAssociation() {
1695 t.Error("Note should still have leaflet association after failed delete")
1696 }
1697
1698 if updatedNote.LeafletRKey == nil || updatedNote.LeafletCID == nil {
1699 t.Error("Note metadata should not be cleared after failed delete")
1700 }
1701 })
1702 })
1703
1704 t.Run("Push", func(t *testing.T) {
1705 t.Run("returns error when not authenticated", func(t *testing.T) {
1706 suite := NewHandlerTestSuite(t)
1707 defer suite.Cleanup()
1708
1709 handler := CreateHandler(t, NewPublicationHandler)
1710 ctx := context.Background()
1711
1712 err := handler.Push(ctx, []int64{1, 2, 3}, false, false)
1713 if err == nil {
1714 t.Error("Expected error when not authenticated")
1715 }
1716
1717 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
1718 t.Errorf("Expected 'not authenticated' error, got '%v'", err)
1719 }
1720 })
1721
1722 t.Run("returns error when no note IDs provided", func(t *testing.T) {
1723 suite := NewHandlerTestSuite(t)
1724 defer suite.Cleanup()
1725
1726 handler := CreateHandler(t, NewPublicationHandler)
1727 ctx := context.Background()
1728
1729 session := &services.Session{
1730 DID: "did:plc:test123",
1731 Handle: "test.bsky.social",
1732 AccessJWT: "access_token",
1733 RefreshJWT: "refresh_token",
1734 PDSURL: "https://bsky.social",
1735 ExpiresAt: time.Now().Add(2 * time.Hour),
1736 Authenticated: true,
1737 }
1738
1739 err := handler.atproto.RestoreSession(session)
1740 if err != nil {
1741 t.Fatalf("Failed to restore session: %v", err)
1742 }
1743
1744 err = handler.Push(ctx, []int64{}, false, false)
1745 if err == nil {
1746 t.Error("Expected error when no note IDs provided")
1747 }
1748
1749 if err != nil && !strings.Contains(err.Error(), "no note IDs provided") {
1750 t.Errorf("Expected 'no note IDs provided' error, got '%v'", err)
1751 }
1752 })
1753
1754 t.Run("handles note not found error", func(t *testing.T) {
1755 suite := NewHandlerTestSuite(t)
1756 defer suite.Cleanup()
1757
1758 handler := CreateHandler(t, NewPublicationHandler)
1759 ctx := context.Background()
1760
1761 session := &services.Session{
1762 DID: "did:plc:test123",
1763 Handle: "test.bsky.social",
1764 AccessJWT: "access_token",
1765 RefreshJWT: "refresh_token",
1766 PDSURL: "https://bsky.social",
1767 ExpiresAt: time.Now().Add(2 * time.Hour),
1768 Authenticated: true,
1769 }
1770
1771 err := handler.atproto.RestoreSession(session)
1772 if err != nil {
1773 t.Fatalf("Failed to restore session: %v", err)
1774 }
1775
1776 err = handler.Push(ctx, []int64{999}, false, false)
1777 if err == nil {
1778 t.Error("Expected error when note not found")
1779 }
1780
1781 if err != nil && !strings.Contains(err.Error(), "error(s)") {
1782 t.Errorf("Expected error about failures, got '%v'", err)
1783 }
1784 })
1785
1786 t.Run("attempts to create notes without leaflet association", func(t *testing.T) {
1787 suite := NewHandlerTestSuite(t)
1788 defer suite.Cleanup()
1789
1790 handler := CreateHandler(t, NewPublicationHandler)
1791 ctx := context.Background()
1792
1793 note1 := &models.Note{
1794 Title: "New Note 1",
1795 Content: "# Content 1",
1796 }
1797 note2 := &models.Note{
1798 Title: "New Note 2",
1799 Content: "# Content 2",
1800 }
1801
1802 id1, err := handler.repos.Notes.Create(ctx, note1)
1803 suite.AssertNoError(err, "create note 1")
1804
1805 id2, err := handler.repos.Notes.Create(ctx, note2)
1806 suite.AssertNoError(err, "create note 2")
1807
1808 session := &services.Session{
1809 DID: "did:plc:test123",
1810 Handle: "test.bsky.social",
1811 AccessJWT: "access_token",
1812 RefreshJWT: "refresh_token",
1813 PDSURL: "https://bsky.social",
1814 ExpiresAt: time.Now().Add(2 * time.Hour),
1815 Authenticated: true,
1816 }
1817
1818 err = handler.atproto.RestoreSession(session)
1819 if err != nil {
1820 t.Fatalf("Failed to restore session: %v", err)
1821 }
1822
1823 err = handler.Push(ctx, []int64{id1, id2}, false, false)
1824
1825 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
1826 t.Logf("Got error during push (expected for external service call): %v", err)
1827 }
1828 })
1829
1830 t.Run("attempts to update notes with leaflet association", func(t *testing.T) {
1831 suite := NewHandlerTestSuite(t)
1832 defer suite.Cleanup()
1833
1834 handler := CreateHandler(t, NewPublicationHandler)
1835 ctx := context.Background()
1836
1837 rkey1 := "rkey1"
1838 cid1 := "cid1"
1839 publishedAt1 := time.Now().Add(-24 * time.Hour)
1840 note1 := &models.Note{
1841 Title: "Published Note 1",
1842 Content: "# Content 1",
1843 LeafletRKey: &rkey1,
1844 LeafletCID: &cid1,
1845 PublishedAt: &publishedAt1,
1846 IsDraft: false,
1847 }
1848
1849 rkey2 := "rkey2"
1850 cid2 := "cid2"
1851 note2 := &models.Note{
1852 Title: "Draft Note 2",
1853 Content: "# Content 2",
1854 LeafletRKey: &rkey2,
1855 LeafletCID: &cid2,
1856 IsDraft: true,
1857 }
1858
1859 id1, err := handler.repos.Notes.Create(ctx, note1)
1860 suite.AssertNoError(err, "create note 1")
1861
1862 id2, err := handler.repos.Notes.Create(ctx, note2)
1863 suite.AssertNoError(err, "create note 2")
1864
1865 session := &services.Session{
1866 DID: "did:plc:test123",
1867 Handle: "test.bsky.social",
1868 AccessJWT: "access_token",
1869 RefreshJWT: "refresh_token",
1870 PDSURL: "https://bsky.social",
1871 ExpiresAt: time.Now().Add(2 * time.Hour),
1872 Authenticated: true,
1873 }
1874
1875 err = handler.atproto.RestoreSession(session)
1876 if err != nil {
1877 t.Fatalf("Failed to restore session: %v", err)
1878 }
1879
1880 err = handler.Push(ctx, []int64{id1, id2}, false, false)
1881
1882 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
1883 t.Logf("Got error during push (expected for external service call): %v", err)
1884 }
1885 })
1886
1887 t.Run("handles mixed create and update operations", func(t *testing.T) {
1888 suite := NewHandlerTestSuite(t)
1889 defer suite.Cleanup()
1890
1891 handler := CreateHandler(t, NewPublicationHandler)
1892 ctx := context.Background()
1893
1894 newNote := &models.Note{
1895 Title: "New Note",
1896 Content: "# New content",
1897 }
1898
1899 rkey := "existing_rkey"
1900 cid := "existing_cid"
1901 publishedAt := time.Now().Add(-24 * time.Hour)
1902 existingNote := &models.Note{
1903 Title: "Existing Note",
1904 Content: "# Updated content",
1905 LeafletRKey: &rkey,
1906 LeafletCID: &cid,
1907 PublishedAt: &publishedAt,
1908 IsDraft: false,
1909 }
1910
1911 newID, err := handler.repos.Notes.Create(ctx, newNote)
1912 suite.AssertNoError(err, "create new note")
1913
1914 existingID, err := handler.repos.Notes.Create(ctx, existingNote)
1915 suite.AssertNoError(err, "create existing note")
1916
1917 session := &services.Session{
1918 DID: "did:plc:test123",
1919 Handle: "test.bsky.social",
1920 AccessJWT: "access_token",
1921 RefreshJWT: "refresh_token",
1922 PDSURL: "https://bsky.social",
1923 ExpiresAt: time.Now().Add(2 * time.Hour),
1924 Authenticated: true,
1925 }
1926
1927 err = handler.atproto.RestoreSession(session)
1928 if err != nil {
1929 t.Fatalf("Failed to restore session: %v", err)
1930 }
1931
1932 err = handler.Push(ctx, []int64{newID, existingID}, false, false)
1933
1934 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
1935 t.Logf("Got error during push (expected for external service call): %v", err)
1936 }
1937 })
1938
1939 t.Run("continues processing after individual failures", func(t *testing.T) {
1940 suite := NewHandlerTestSuite(t)
1941 defer suite.Cleanup()
1942
1943 handler := CreateHandler(t, NewPublicationHandler)
1944 ctx := context.Background()
1945
1946 note1 := &models.Note{
1947 Title: "Valid Note",
1948 Content: "# Content",
1949 }
1950
1951 id1, err := handler.repos.Notes.Create(ctx, note1)
1952 suite.AssertNoError(err, "create valid note")
1953
1954 session := &services.Session{
1955 DID: "did:plc:test123",
1956 Handle: "test.bsky.social",
1957 AccessJWT: "access_token",
1958 RefreshJWT: "refresh_token",
1959 PDSURL: "https://bsky.social",
1960 ExpiresAt: time.Now().Add(2 * time.Hour),
1961 Authenticated: true,
1962 }
1963
1964 err = handler.atproto.RestoreSession(session)
1965 if err != nil {
1966 t.Fatalf("Failed to restore session: %v", err)
1967 }
1968
1969 invalidID := int64(999)
1970 err = handler.Push(ctx, []int64{id1, invalidID}, false, false)
1971
1972 if err == nil {
1973 t.Error("Expected error due to invalid note ID")
1974 }
1975
1976 if err != nil && !strings.Contains(err.Error(), "error(s)") {
1977 t.Errorf("Expected error message about failures, got '%v'", err)
1978 }
1979 })
1980
1981 t.Run("respects draft flag for new notes", func(t *testing.T) {
1982 suite := NewHandlerTestSuite(t)
1983 defer suite.Cleanup()
1984
1985 handler := CreateHandler(t, NewPublicationHandler)
1986 ctx := context.Background()
1987
1988 note := &models.Note{
1989 Title: "Draft Note",
1990 Content: "# Draft content",
1991 }
1992
1993 id, err := handler.repos.Notes.Create(ctx, note)
1994 suite.AssertNoError(err, "create note")
1995
1996 session := &services.Session{
1997 DID: "did:plc:test123",
1998 Handle: "test.bsky.social",
1999 AccessJWT: "access_token",
2000 RefreshJWT: "refresh_token",
2001 PDSURL: "https://bsky.social",
2002 ExpiresAt: time.Now().Add(2 * time.Hour),
2003 Authenticated: true,
2004 }
2005
2006 err = handler.atproto.RestoreSession(session)
2007 if err != nil {
2008 t.Fatalf("Failed to restore session: %v", err)
2009 }
2010
2011 err = handler.Push(ctx, []int64{id}, true, false)
2012
2013 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
2014 t.Logf("Got error during push (expected for external service call): %v", err)
2015 }
2016 })
2017 })
2018
2019 t.Run("Read", func(t *testing.T) {
2020 t.Run("returns error when no publications exist", func(t *testing.T) {
2021 suite := NewHandlerTestSuite(t)
2022 defer suite.Cleanup()
2023
2024 handler := CreateHandler(t, NewPublicationHandler)
2025 ctx := context.Background()
2026
2027 err := handler.Read(ctx, "")
2028 if err == nil {
2029 t.Error("Expected error when no publications exist")
2030 }
2031
2032 if !strings.Contains(err.Error(), "note not found") {
2033 t.Errorf("Expected 'note not found' error, got '%v'", err)
2034 }
2035 })
2036
2037 t.Run("returns error for non-existent numeric ID", func(t *testing.T) {
2038 suite := NewHandlerTestSuite(t)
2039 defer suite.Cleanup()
2040
2041 handler := CreateHandler(t, NewPublicationHandler)
2042 ctx := context.Background()
2043
2044 err := handler.Read(ctx, "999")
2045 if err == nil {
2046 t.Error("Expected error for non-existent ID")
2047 }
2048
2049 if !strings.Contains(err.Error(), "failed to get publication by ID") {
2050 t.Errorf("Expected 'failed to get publication by ID' error, got '%v'", err)
2051 }
2052 })
2053
2054 t.Run("returns error when note by ID is not a publication", func(t *testing.T) {
2055 suite := NewHandlerTestSuite(t)
2056 defer suite.Cleanup()
2057
2058 handler := CreateHandler(t, NewPublicationHandler)
2059 ctx := context.Background()
2060
2061 regularNote := &models.Note{
2062 Title: "Regular Note",
2063 Content: "# Not a publication",
2064 }
2065
2066 id, err := handler.repos.Notes.Create(ctx, regularNote)
2067 suite.AssertNoError(err, "create regular note")
2068
2069 err = handler.Read(ctx, fmt.Sprintf("%d", id))
2070 if err == nil {
2071 t.Error("Expected error when note is not a publication")
2072 }
2073
2074 if !strings.Contains(err.Error(), "not a publication") {
2075 t.Errorf("Expected 'not a publication' error, got '%v'", err)
2076 }
2077 })
2078
2079 t.Run("returns error for non-existent rkey", func(t *testing.T) {
2080 suite := NewHandlerTestSuite(t)
2081 defer suite.Cleanup()
2082
2083 handler := CreateHandler(t, NewPublicationHandler)
2084 ctx := context.Background()
2085
2086 err := handler.Read(ctx, "nonexistent_rkey")
2087 if err == nil {
2088 t.Error("Expected error for non-existent rkey")
2089 }
2090
2091 if !strings.Contains(err.Error(), "failed to get publication by rkey") {
2092 t.Errorf("Expected 'failed to get publication by rkey' error, got '%v'", err)
2093 }
2094 })
2095 })
2096
2097 t.Run("Auth Success Path", func(t *testing.T) {
2098 suite := NewHandlerTestSuite(t)
2099 defer suite.Cleanup()
2100
2101 handler := CreateHandler(t, NewPublicationHandler)
2102 ctx := context.Background()
2103
2104 mock := services.SetupSuccessfulAuthMocks()
2105 handler.atproto = mock
2106
2107 err := handler.Auth(ctx, "test.bsky.social", "password123")
2108 suite.AssertNoError(err, "authentication should succeed")
2109
2110 if !handler.atproto.IsAuthenticated() {
2111 t.Error("Expected handler to be authenticated after successful auth")
2112 }
2113
2114 session, err := handler.atproto.GetSession()
2115 suite.AssertNoError(err, "get session should succeed")
2116
2117 if session.Handle != "test.bsky.social" {
2118 t.Errorf("Expected handle 'test.bsky.social', got '%s'", session.Handle)
2119 }
2120
2121 if session.DID == "" {
2122 t.Error("Expected DID to be set")
2123 }
2124 })
2125
2126 t.Run("Pull Success Path", func(t *testing.T) {
2127 suite := NewHandlerTestSuite(t)
2128 defer suite.Cleanup()
2129
2130 handler := CreateHandler(t, NewPublicationHandler)
2131 ctx := context.Background()
2132
2133 mock := services.SetupSuccessfulPullMocks()
2134 handler.atproto = mock
2135
2136 err := handler.Pull(ctx)
2137 suite.AssertNoError(err, "pull should succeed")
2138
2139 notes, err := handler.repos.Notes.GetLeafletNotes(ctx)
2140 suite.AssertNoError(err, "get leaflet notes should succeed")
2141
2142 if len(notes) != 1 {
2143 t.Errorf("Expected 1 note created, got %d", len(notes))
2144 }
2145
2146 if notes[0].Title != "Test Document" {
2147 t.Errorf("Expected title 'Test Document', got '%s'", notes[0].Title)
2148 }
2149
2150 if notes[0].LeafletRKey == nil || *notes[0].LeafletRKey != "test_rkey" {
2151 t.Error("Expected leaflet rkey to be set correctly")
2152 }
2153 })
2154
2155 t.Run("Post Success Path", func(t *testing.T) {
2156 suite := NewHandlerTestSuite(t)
2157 defer suite.Cleanup()
2158
2159 handler := CreateHandler(t, NewPublicationHandler)
2160 ctx := context.Background()
2161
2162 mock := services.NewMockATProtoService()
2163 mock.IsAuthenticatedVal = true
2164 mock.Session = &services.Session{
2165 DID: "did:plc:test123",
2166 Handle: "test.bsky.social",
2167 AccessJWT: "mock_access",
2168 RefreshJWT: "mock_refresh",
2169 PDSURL: "https://bsky.social",
2170 ExpiresAt: time.Now().Add(2 * time.Hour),
2171 Authenticated: true,
2172 }
2173 handler.atproto = mock
2174
2175 note := &models.Note{
2176 Title: "Test Post",
2177 Content: "# Test Content\n\nThis is a test.",
2178 }
2179
2180 id, err := handler.repos.Notes.Create(ctx, note)
2181 suite.AssertNoError(err, "create note should succeed")
2182
2183 err = handler.Post(ctx, id, false)
2184 suite.AssertNoError(err, "post should succeed")
2185
2186 updatedNote, err := handler.repos.Notes.Get(ctx, id)
2187 suite.AssertNoError(err, "get updated note should succeed")
2188
2189 if updatedNote.LeafletRKey == nil || *updatedNote.LeafletRKey != "mock_rkey_123" {
2190 t.Error("Expected leaflet rkey to be set after post")
2191 }
2192
2193 if updatedNote.LeafletCID == nil || *updatedNote.LeafletCID != "mock_cid_456" {
2194 t.Error("Expected leaflet cid to be set after post")
2195 }
2196
2197 if updatedNote.IsDraft {
2198 t.Error("Expected note to be marked as published")
2199 }
2200
2201 if updatedNote.PublishedAt == nil {
2202 t.Error("Expected published at to be set")
2203 }
2204 })
2205
2206 t.Run("Post Draft Success Path", func(t *testing.T) {
2207 suite := NewHandlerTestSuite(t)
2208 defer suite.Cleanup()
2209
2210 handler := CreateHandler(t, NewPublicationHandler)
2211 ctx := context.Background()
2212
2213 mock := services.NewMockATProtoService()
2214 mock.IsAuthenticatedVal = true
2215 mock.Session = &services.Session{
2216 DID: "did:plc:test123",
2217 Handle: "test.bsky.social",
2218 AccessJWT: "mock_access",
2219 RefreshJWT: "mock_refresh",
2220 PDSURL: "https://bsky.social",
2221 ExpiresAt: time.Now().Add(2 * time.Hour),
2222 Authenticated: true,
2223 }
2224 handler.atproto = mock
2225
2226 note := &models.Note{
2227 Title: "Test Draft",
2228 Content: "# Draft Content",
2229 }
2230
2231 id, err := handler.repos.Notes.Create(ctx, note)
2232 suite.AssertNoError(err, "create note should succeed")
2233
2234 err = handler.Post(ctx, id, true)
2235 suite.AssertNoError(err, "post draft should succeed")
2236
2237 updatedNote, err := handler.repos.Notes.Get(ctx, id)
2238 suite.AssertNoError(err, "get updated note should succeed")
2239
2240 if !updatedNote.IsDraft {
2241 t.Error("Expected note to be marked as draft")
2242 }
2243
2244 if updatedNote.PublishedAt != nil {
2245 t.Error("Expected published at to be nil for draft")
2246 }
2247 })
2248
2249 t.Run("Patch Success Path", func(t *testing.T) {
2250 suite := NewHandlerTestSuite(t)
2251 defer suite.Cleanup()
2252
2253 handler := CreateHandler(t, NewPublicationHandler)
2254 ctx := context.Background()
2255
2256 mock := services.NewMockATProtoService()
2257 mock.IsAuthenticatedVal = true
2258 mock.Session = &services.Session{
2259 DID: "did:plc:test123",
2260 Handle: "test.bsky.social",
2261 AccessJWT: "mock_access",
2262 RefreshJWT: "mock_refresh",
2263 PDSURL: "https://bsky.social",
2264 ExpiresAt: time.Now().Add(2 * time.Hour),
2265 Authenticated: true,
2266 }
2267 handler.atproto = mock
2268
2269 rkey := "existing_rkey"
2270 cid := "existing_cid"
2271 publishedAt := time.Now().Add(-24 * time.Hour)
2272 note := &models.Note{
2273 Title: "Updated Note",
2274 Content: "# Updated Content",
2275 LeafletRKey: &rkey,
2276 LeafletCID: &cid,
2277 PublishedAt: &publishedAt,
2278 IsDraft: false,
2279 }
2280
2281 id, err := handler.repos.Notes.Create(ctx, note)
2282 suite.AssertNoError(err, "create note should succeed")
2283
2284 err = handler.Patch(ctx, id)
2285 suite.AssertNoError(err, "patch should succeed")
2286
2287 updatedNote, err := handler.repos.Notes.Get(ctx, id)
2288 suite.AssertNoError(err, "get updated note should succeed")
2289
2290 if updatedNote.LeafletCID == nil || *updatedNote.LeafletCID != "mock_cid_updated_789" {
2291 t.Error("Expected leaflet cid to be updated after patch")
2292 }
2293 })
2294
2295 t.Run("Delete Success Path", func(t *testing.T) {
2296 suite := NewHandlerTestSuite(t)
2297 defer suite.Cleanup()
2298
2299 handler := CreateHandler(t, NewPublicationHandler)
2300 ctx := context.Background()
2301
2302 mock := services.NewMockATProtoService()
2303 mock.IsAuthenticatedVal = true
2304 mock.Session = &services.Session{
2305 DID: "did:plc:test123",
2306 Handle: "test.bsky.social",
2307 AccessJWT: "mock_access",
2308 RefreshJWT: "mock_refresh",
2309 PDSURL: "https://bsky.social",
2310 ExpiresAt: time.Now().Add(2 * time.Hour),
2311 Authenticated: true,
2312 }
2313 handler.atproto = mock
2314
2315 rkey := "test_rkey"
2316 cid := "test_cid"
2317 publishedAt := time.Now().Add(-24 * time.Hour)
2318 note := &models.Note{
2319 Title: "Note to Delete",
2320 Content: "# Content",
2321 LeafletRKey: &rkey,
2322 LeafletCID: &cid,
2323 PublishedAt: &publishedAt,
2324 IsDraft: false,
2325 }
2326
2327 id, err := handler.repos.Notes.Create(ctx, note)
2328 suite.AssertNoError(err, "create note should succeed")
2329
2330 err = handler.Delete(ctx, id)
2331 suite.AssertNoError(err, "delete should succeed")
2332
2333 updatedNote, err := handler.repos.Notes.Get(ctx, id)
2334 suite.AssertNoError(err, "get updated note should succeed")
2335
2336 if updatedNote.LeafletRKey != nil {
2337 t.Error("Expected leaflet rkey to be cleared after delete")
2338 }
2339
2340 if updatedNote.LeafletCID != nil {
2341 t.Error("Expected leaflet cid to be cleared after delete")
2342 }
2343
2344 if updatedNote.PublishedAt != nil {
2345 t.Error("Expected published at to be cleared after delete")
2346 }
2347
2348 if updatedNote.IsDraft {
2349 t.Error("Expected draft flag to be false after delete")
2350 }
2351 })
2352}