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.

at main 2352 lines 66 kB view raw
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}