cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 1828 lines 50 kB view raw
1package services 2 3import ( 4 "context" 5 "encoding/json" 6 "strings" 7 "testing" 8 "time" 9 10 "github.com/fxamacker/cbor/v2" 11 "github.com/stormlightlabs/noteleaf/internal/public" 12) 13 14func TestATProtoService(t *testing.T) { 15 t.Run("NewATProtoService", func(t *testing.T) { 16 t.Run("creates service with default configuration", func(t *testing.T) { 17 svc := NewATProtoService() 18 19 if svc == nil { 20 t.Fatal("Expected service to be created, got nil") 21 } 22 23 if svc.pdsURL != "https://bsky.social" { 24 t.Errorf("Expected pdsURL to be 'https://bsky.social', got '%s'", svc.pdsURL) 25 } 26 27 if svc.client == nil { 28 t.Fatal("Expected client to be initialized, got nil") 29 } 30 31 if svc.client.Host != "https://bsky.social" { 32 t.Errorf("Expected client Host to be 'https://bsky.social', got '%s'", svc.client.Host) 33 } 34 }) 35 }) 36 37 t.Run("Authenticate", func(t *testing.T) { 38 t.Run("validates required parameters", func(t *testing.T) { 39 svc := NewATProtoService() 40 ctx := context.Background() 41 42 err := svc.Authenticate(ctx, "", "password") 43 if err == nil { 44 t.Error("Expected error for empty handle, got nil") 45 } 46 47 err = svc.Authenticate(ctx, "handle", "") 48 if err == nil { 49 t.Error("Expected error for empty password, got nil") 50 } 51 52 err = svc.Authenticate(ctx, "", "") 53 if err == nil { 54 t.Error("Expected error for empty handle and password, got nil") 55 } 56 }) 57 }) 58 59 t.Run("IsAuthenticated", func(t *testing.T) { 60 t.Run("returns false when no session exists", func(t *testing.T) { 61 svc := NewATProtoService() 62 63 if svc.IsAuthenticated() { 64 t.Error("Expected IsAuthenticated to return false for new service") 65 } 66 }) 67 68 t.Run("returns false when session is not authenticated", func(t *testing.T) { 69 svc := NewATProtoService() 70 svc.session = &Session{ 71 Handle: "test.bsky.social", 72 Authenticated: false, 73 } 74 75 if svc.IsAuthenticated() { 76 t.Error("Expected IsAuthenticated to return false for unauthenticated session") 77 } 78 }) 79 80 t.Run("returns true when session is authenticated", func(t *testing.T) { 81 svc := NewATProtoService() 82 svc.session = &Session{ 83 Handle: "test.bsky.social", 84 Authenticated: true, 85 } 86 87 if !svc.IsAuthenticated() { 88 t.Error("Expected IsAuthenticated to return true for authenticated session") 89 } 90 }) 91 }) 92 93 t.Run("GetSession", func(t *testing.T) { 94 t.Run("returns error when not authenticated", func(t *testing.T) { 95 svc := NewATProtoService() 96 97 session, err := svc.GetSession() 98 if err == nil { 99 t.Error("Expected error when getting session without authentication") 100 } 101 if session != nil { 102 t.Error("Expected nil session when not authenticated") 103 } 104 }) 105 106 t.Run("returns session when authenticated", func(t *testing.T) { 107 svc := NewATProtoService() 108 expectedSession := &Session{ 109 DID: "did:plc:test123", 110 Handle: "test.bsky.social", 111 AccessJWT: "access_token", 112 RefreshJWT: "refresh_token", 113 PDSURL: "https://bsky.social", 114 ExpiresAt: time.Now().Add(2 * time.Hour), 115 Authenticated: true, 116 } 117 svc.session = expectedSession 118 119 session, err := svc.GetSession() 120 if err != nil { 121 t.Errorf("Expected no error, got %v", err) 122 } 123 if session == nil { 124 t.Fatal("Expected session to be returned, got nil") 125 } 126 if session.DID != expectedSession.DID { 127 t.Errorf("Expected DID '%s', got '%s'", expectedSession.DID, session.DID) 128 } 129 if session.Handle != expectedSession.Handle { 130 t.Errorf("Expected Handle '%s', got '%s'", expectedSession.Handle, session.Handle) 131 } 132 }) 133 }) 134 135 t.Run("RefreshToken", func(t *testing.T) { 136 t.Run("returns error when no session exists", func(t *testing.T) { 137 svc := NewATProtoService() 138 ctx := context.Background() 139 140 err := svc.RefreshToken(ctx) 141 if err == nil { 142 t.Error("Expected error when refreshing without session") 143 } 144 }) 145 146 t.Run("returns error when refresh token is empty", func(t *testing.T) { 147 svc := NewATProtoService() 148 ctx := context.Background() 149 svc.session = &Session{ 150 Handle: "test.bsky.social", 151 RefreshJWT: "", 152 } 153 154 err := svc.RefreshToken(ctx) 155 if err == nil { 156 t.Error("Expected error when refreshing with empty refresh token") 157 } 158 }) 159 }) 160 161 t.Run("RestoreSession", func(t *testing.T) { 162 t.Run("returns error when session is nil", func(t *testing.T) { 163 svc := NewATProtoService() 164 165 err := svc.RestoreSession(nil) 166 if err == nil { 167 t.Error("Expected error when restoring nil session") 168 } 169 }) 170 171 t.Run("returns error when session missing DID", func(t *testing.T) { 172 svc := NewATProtoService() 173 session := &Session{ 174 DID: "", 175 Handle: "test.bsky.social", 176 AccessJWT: "access_token", 177 RefreshJWT: "refresh_token", 178 } 179 180 err := svc.RestoreSession(session) 181 if err == nil { 182 t.Error("Expected error when session missing DID") 183 } 184 }) 185 186 t.Run("returns error when session missing AccessJWT", func(t *testing.T) { 187 svc := NewATProtoService() 188 session := &Session{ 189 DID: "did:plc:test123", 190 Handle: "test.bsky.social", 191 AccessJWT: "", 192 RefreshJWT: "refresh_token", 193 } 194 195 err := svc.RestoreSession(session) 196 if err == nil { 197 t.Error("Expected error when session missing AccessJWT") 198 } 199 }) 200 201 t.Run("returns error when session missing RefreshJWT", func(t *testing.T) { 202 svc := NewATProtoService() 203 session := &Session{ 204 DID: "did:plc:test123", 205 Handle: "test.bsky.social", 206 AccessJWT: "access_token", 207 RefreshJWT: "", 208 } 209 210 err := svc.RestoreSession(session) 211 if err == nil { 212 t.Error("Expected error when session missing RefreshJWT") 213 } 214 }) 215 216 t.Run("successfully restores valid session", func(t *testing.T) { 217 svc := NewATProtoService() 218 session := &Session{ 219 DID: "did:plc:test123", 220 Handle: "test.bsky.social", 221 AccessJWT: "access_token", 222 RefreshJWT: "refresh_token", 223 PDSURL: "https://test.pds.example", 224 ExpiresAt: time.Now().Add(2 * time.Hour), 225 Authenticated: true, 226 } 227 228 err := svc.RestoreSession(session) 229 if err != nil { 230 t.Errorf("Expected no error, got %v", err) 231 } 232 233 if !svc.IsAuthenticated() { 234 t.Error("Expected service to be authenticated after restore") 235 } 236 237 restoredSession, err := svc.GetSession() 238 if err != nil { 239 t.Errorf("Expected to get session, got error: %v", err) 240 } 241 if restoredSession.DID != session.DID { 242 t.Errorf("Expected DID '%s', got '%s'", session.DID, restoredSession.DID) 243 } 244 if restoredSession.Handle != session.Handle { 245 t.Errorf("Expected Handle '%s', got '%s'", session.Handle, restoredSession.Handle) 246 } 247 }) 248 249 t.Run("updates client authentication", func(t *testing.T) { 250 svc := NewATProtoService() 251 session := &Session{ 252 DID: "did:plc:test123", 253 Handle: "test.bsky.social", 254 AccessJWT: "access_token", 255 RefreshJWT: "refresh_token", 256 PDSURL: "https://test.pds.example", 257 ExpiresAt: time.Now().Add(2 * time.Hour), 258 Authenticated: true, 259 } 260 261 err := svc.RestoreSession(session) 262 if err != nil { 263 t.Errorf("Expected no error, got %v", err) 264 } 265 266 if svc.client.Auth == nil { 267 t.Fatal("Expected client Auth to be set") 268 } 269 if svc.client.Auth.Did != session.DID { 270 t.Errorf("Expected client DID '%s', got '%s'", session.DID, svc.client.Auth.Did) 271 } 272 if svc.client.Auth.AccessJwt != session.AccessJWT { 273 t.Errorf("Expected client AccessJwt '%s', got '%s'", session.AccessJWT, svc.client.Auth.AccessJwt) 274 } 275 }) 276 277 t.Run("updates PDS URL when provided", func(t *testing.T) { 278 svc := NewATProtoService() 279 customPDS := "https://custom.pds.example" 280 session := &Session{ 281 DID: "did:plc:test123", 282 Handle: "test.bsky.social", 283 AccessJWT: "access_token", 284 RefreshJWT: "refresh_token", 285 PDSURL: customPDS, 286 ExpiresAt: time.Now().Add(2 * time.Hour), 287 Authenticated: true, 288 } 289 290 err := svc.RestoreSession(session) 291 if err != nil { 292 t.Errorf("Expected no error, got %v", err) 293 } 294 295 if svc.pdsURL != customPDS { 296 t.Errorf("Expected pdsURL '%s', got '%s'", customPDS, svc.pdsURL) 297 } 298 if svc.client.Host != customPDS { 299 t.Errorf("Expected client Host '%s', got '%s'", customPDS, svc.client.Host) 300 } 301 }) 302 }) 303 304 t.Run("Close", func(t *testing.T) { 305 t.Run("clears session", func(t *testing.T) { 306 svc := NewATProtoService() 307 svc.session = &Session{ 308 Handle: "test.bsky.social", 309 Authenticated: true, 310 } 311 312 err := svc.Close() 313 if err != nil { 314 t.Errorf("Expected no error on close, got %v", err) 315 } 316 if svc.session != nil { 317 t.Error("Expected session to be nil after close") 318 } 319 }) 320 321 t.Run("handles nil session gracefully", func(t *testing.T) { 322 svc := NewATProtoService() 323 svc.session = nil 324 325 err := svc.Close() 326 if err != nil { 327 t.Errorf("Expected no error on close with nil session, got %v", err) 328 } 329 }) 330 }) 331 332 t.Run("PullDocuments", func(t *testing.T) { 333 t.Run("returns error when not authenticated", func(t *testing.T) { 334 svc := NewATProtoService() 335 ctx := context.Background() 336 337 docs, err := svc.PullDocuments(ctx) 338 if err == nil { 339 t.Error("Expected error when pulling documents without authentication") 340 } 341 if docs != nil { 342 t.Error("Expected nil documents when not authenticated") 343 } 344 if err.Error() != "not authenticated" { 345 t.Errorf("Expected 'not authenticated' error, got '%v'", err) 346 } 347 }) 348 349 t.Run("returns error when session not authenticated", func(t *testing.T) { 350 svc := NewATProtoService() 351 ctx := context.Background() 352 svc.session = &Session{ 353 Handle: "test.bsky.social", 354 Authenticated: false, 355 } 356 357 docs, err := svc.PullDocuments(ctx) 358 if err == nil { 359 t.Error("Expected error when pulling documents with unauthenticated session") 360 } 361 if docs != nil { 362 t.Error("Expected nil documents when session not authenticated") 363 } 364 }) 365 366 t.Run("returns error when context cancelled", func(t *testing.T) { 367 svc := NewATProtoService() 368 svc.session = &Session{ 369 DID: "did:plc:test123", 370 Handle: "test.bsky.social", 371 AccessJWT: "access_token", 372 RefreshJWT: "refresh_token", 373 Authenticated: true, 374 } 375 376 ctx, cancel := context.WithCancel(context.Background()) 377 cancel() 378 379 docs, err := svc.PullDocuments(ctx) 380 if err == nil { 381 t.Error("Expected error when context is cancelled") 382 } 383 if docs != nil { 384 t.Error("Expected nil documents when context is cancelled") 385 } 386 }) 387 388 t.Run("returns empty list when no documents exist", func(t *testing.T) { 389 svc := NewATProtoService() 390 svc.session = &Session{ 391 DID: "did:plc:test123", 392 Handle: "test.bsky.social", 393 AccessJWT: "access_token", 394 RefreshJWT: "refresh_token", 395 Authenticated: true, 396 } 397 ctx := context.Background() 398 399 docs, err := svc.PullDocuments(ctx) 400 401 if err != nil && err.Error() == "not authenticated" { 402 t.Error("Authentication check should pass, but got authentication error") 403 } 404 405 _ = docs 406 }) 407 }) 408 409 t.Run("ListPublications", func(t *testing.T) { 410 t.Run("returns error when not authenticated", func(t *testing.T) { 411 svc := NewATProtoService() 412 ctx := context.Background() 413 414 pubs, err := svc.ListPublications(ctx) 415 if err == nil { 416 t.Error("Expected error when listing publications without authentication") 417 } 418 if pubs != nil { 419 t.Error("Expected nil publications when not authenticated") 420 } 421 if err.Error() != "not authenticated" { 422 t.Errorf("Expected 'not authenticated' error, got '%v'", err) 423 } 424 }) 425 426 t.Run("returns error when session not authenticated", func(t *testing.T) { 427 svc := NewATProtoService() 428 ctx := context.Background() 429 svc.session = &Session{ 430 Handle: "test.bsky.social", 431 Authenticated: false, 432 } 433 434 pubs, err := svc.ListPublications(ctx) 435 if err == nil { 436 t.Error("Expected error when listing publications with unauthenticated session") 437 } 438 if pubs != nil { 439 t.Error("Expected nil publications when session not authenticated") 440 } 441 }) 442 443 t.Run("returns error when context cancelled", func(t *testing.T) { 444 svc := NewATProtoService() 445 svc.session = &Session{ 446 DID: "did:plc:test123", 447 Handle: "test.bsky.social", 448 AccessJWT: "access_token", 449 RefreshJWT: "refresh_token", 450 Authenticated: true, 451 } 452 453 ctx, cancel := context.WithCancel(context.Background()) 454 cancel() 455 456 pubs, err := svc.ListPublications(ctx) 457 if err == nil { 458 t.Error("Expected error when context is cancelled") 459 } 460 if pubs != nil { 461 t.Error("Expected nil publications when context is cancelled") 462 } 463 }) 464 465 t.Run("returns error when context timeout", func(t *testing.T) { 466 svc := NewATProtoService() 467 svc.session = &Session{ 468 DID: "did:plc:test123", 469 Handle: "test.bsky.social", 470 AccessJWT: "access_token", 471 RefreshJWT: "refresh_token", 472 Authenticated: true, 473 } 474 475 ctx, cancel := context.WithTimeout(context.Background(), 1) 476 defer cancel() 477 time.Sleep(2 * time.Millisecond) 478 479 pubs, err := svc.ListPublications(ctx) 480 if err == nil { 481 t.Error("Expected error when context times out") 482 } 483 if pubs != nil { 484 t.Error("Expected nil publications when context times out") 485 } 486 }) 487 488 t.Run("returns empty list when no publications exist", func(t *testing.T) { 489 svc := NewATProtoService() 490 svc.session = &Session{ 491 DID: "did:plc:test123", 492 Handle: "test.bsky.social", 493 AccessJWT: "access_token", 494 RefreshJWT: "refresh_token", 495 Authenticated: true, 496 } 497 ctx := context.Background() 498 499 pubs, err := svc.ListPublications(ctx) 500 501 if err != nil && err.Error() == "not authenticated" { 502 t.Error("Authentication check should pass, but got authentication error") 503 } 504 505 _ = pubs 506 }) 507 }) 508 509 t.Run("GetDefaultPublication", func(t *testing.T) { 510 t.Run("returns error when not authenticated", func(t *testing.T) { 511 svc := NewATProtoService() 512 ctx := context.Background() 513 514 uri, err := svc.GetDefaultPublication(ctx) 515 if err == nil { 516 t.Error("Expected error when getting default publication without authentication") 517 } 518 if uri != "" { 519 t.Errorf("Expected empty URI, got %s", uri) 520 } 521 if !strings.Contains(err.Error(), "not authenticated") { 522 t.Errorf("Expected 'not authenticated' error, got '%v'", err) 523 } 524 }) 525 526 t.Run("returns error when session not authenticated", func(t *testing.T) { 527 svc := NewATProtoService() 528 ctx := context.Background() 529 svc.session = &Session{ 530 Handle: "test.bsky.social", 531 Authenticated: false, 532 } 533 534 uri, err := svc.GetDefaultPublication(ctx) 535 if err == nil { 536 t.Error("Expected error when getting default publication with unauthenticated session") 537 } 538 if uri != "" { 539 t.Errorf("Expected empty URI, got %s", uri) 540 } 541 }) 542 543 t.Run("returns error when no publications exist", func(t *testing.T) { 544 svc := NewATProtoService() 545 svc.session = &Session{ 546 DID: "did:plc:test123", 547 Handle: "test.bsky.social", 548 AccessJWT: "access_token", 549 RefreshJWT: "refresh_token", 550 Authenticated: true, 551 } 552 ctx := context.Background() 553 554 _, err := svc.GetDefaultPublication(ctx) 555 if err == nil { 556 t.Error("Expected error when getting default publication") 557 } 558 // With invalid credentials, we expect either auth error or no publications error 559 if !strings.Contains(err.Error(), "no publications found") && 560 !strings.Contains(err.Error(), "Authentication") && 561 !strings.Contains(err.Error(), "AuthMissing") && 562 !strings.Contains(err.Error(), "failed to fetch repository") { 563 t.Errorf("Expected authentication or 'no publications found' error, got '%v'", err) 564 } 565 }) 566 567 t.Run("returns error when context cancelled", func(t *testing.T) { 568 svc := NewATProtoService() 569 svc.session = &Session{ 570 DID: "did:plc:test123", 571 Handle: "test.bsky.social", 572 AccessJWT: "access_token", 573 RefreshJWT: "refresh_token", 574 Authenticated: true, 575 } 576 577 ctx, cancel := context.WithCancel(context.Background()) 578 cancel() 579 580 uri, err := svc.GetDefaultPublication(ctx) 581 if err == nil { 582 t.Error("Expected error when context is cancelled") 583 } 584 if uri != "" { 585 t.Errorf("Expected empty URI when error occurs, got %s", uri) 586 } 587 }) 588 }) 589 590 t.Run("Authentication Error Scenarios", func(t *testing.T) { 591 t.Run("returns error with context timeout", func(t *testing.T) { 592 svc := NewATProtoService() 593 ctx, cancel := context.WithTimeout(context.Background(), 1) 594 defer cancel() 595 time.Sleep(2 * time.Millisecond) 596 597 err := svc.Authenticate(ctx, "test.bsky.social", "password") 598 if err == nil { 599 t.Error("Expected error when context times out") 600 } 601 }) 602 603 t.Run("returns error with cancelled context", func(t *testing.T) { 604 svc := NewATProtoService() 605 ctx, cancel := context.WithCancel(context.Background()) 606 cancel() 607 608 err := svc.Authenticate(ctx, "test.bsky.social", "password") 609 if err == nil { 610 t.Error("Expected error when context is cancelled") 611 } 612 }) 613 614 t.Run("validates both handle and password together", func(t *testing.T) { 615 svc := NewATProtoService() 616 ctx := context.Background() 617 618 testCases := []struct { 619 name string 620 handle string 621 password string 622 }{ 623 {"empty handle", "", "password"}, 624 {"empty password", "handle", ""}, 625 {"both empty", "", ""}, 626 } 627 628 for _, tc := range testCases { 629 t.Run(tc.name, func(t *testing.T) { 630 err := svc.Authenticate(ctx, tc.handle, tc.password) 631 if err == nil { 632 t.Errorf("Expected error for %s", tc.name) 633 } 634 if !svc.IsAuthenticated() == false { 635 t.Error("Expected service to not be authenticated after error") 636 } 637 }) 638 } 639 }) 640 }) 641 642 t.Run("RefreshToken Error Scenarios", func(t *testing.T) { 643 t.Run("returns error with cancelled context", func(t *testing.T) { 644 svc := NewATProtoService() 645 svc.session = &Session{ 646 DID: "did:plc:test123", 647 Handle: "test.bsky.social", 648 AccessJWT: "access_token", 649 RefreshJWT: "refresh_token", 650 Authenticated: true, 651 } 652 ctx, cancel := context.WithCancel(context.Background()) 653 cancel() 654 655 err := svc.RefreshToken(ctx) 656 if err == nil { 657 t.Error("Expected error when context is cancelled") 658 } 659 }) 660 661 t.Run("returns error with timeout context", func(t *testing.T) { 662 svc := NewATProtoService() 663 svc.session = &Session{ 664 DID: "did:plc:test123", 665 Handle: "test.bsky.social", 666 AccessJWT: "access_token", 667 RefreshJWT: "refresh_token", 668 Authenticated: true, 669 } 670 ctx, cancel := context.WithTimeout(context.Background(), 1) 671 defer cancel() 672 time.Sleep(2 * time.Millisecond) 673 674 err := svc.RefreshToken(ctx) 675 if err == nil { 676 t.Error("Expected error when context times out") 677 } 678 }) 679 }) 680 681 t.Run("PostDocument", func(t *testing.T) { 682 t.Run("returns error when not authenticated", func(t *testing.T) { 683 svc := NewATProtoService() 684 ctx := context.Background() 685 686 doc := public.Document{ 687 Title: "Test Document", 688 } 689 690 result, err := svc.PostDocument(ctx, doc, false) 691 if err == nil { 692 t.Error("Expected error when posting document without authentication") 693 } 694 if result != nil { 695 t.Error("Expected nil result when not authenticated") 696 } 697 if err.Error() != "not authenticated" { 698 t.Errorf("Expected 'not authenticated' error, got '%v'", err) 699 } 700 }) 701 702 t.Run("returns error when session not authenticated", func(t *testing.T) { 703 svc := NewATProtoService() 704 ctx := context.Background() 705 svc.session = &Session{ 706 Handle: "test.bsky.social", 707 Authenticated: false, 708 } 709 710 doc := public.Document{ 711 Title: "Test Document", 712 } 713 714 result, err := svc.PostDocument(ctx, doc, false) 715 if err == nil { 716 t.Error("Expected error when posting document with unauthenticated session") 717 } 718 if result != nil { 719 t.Error("Expected nil result when session not authenticated") 720 } 721 }) 722 723 t.Run("returns error when document title is empty", func(t *testing.T) { 724 svc := NewATProtoService() 725 ctx := context.Background() 726 svc.session = &Session{ 727 DID: "did:plc:test123", 728 Handle: "test.bsky.social", 729 AccessJWT: "access_token", 730 RefreshJWT: "refresh_token", 731 Authenticated: true, 732 } 733 734 doc := public.Document{ 735 Title: "", 736 } 737 738 result, err := svc.PostDocument(ctx, doc, false) 739 if err == nil { 740 t.Error("Expected error when document title is empty") 741 } 742 if result != nil { 743 t.Error("Expected nil result when title is empty") 744 } 745 if err.Error() != "document title is required" { 746 t.Errorf("Expected 'document title is required' error, got '%v'", err) 747 } 748 }) 749 750 t.Run("returns error when context cancelled", func(t *testing.T) { 751 svc := NewATProtoService() 752 svc.session = &Session{ 753 DID: "did:plc:test123", 754 Handle: "test.bsky.social", 755 AccessJWT: "access_token", 756 RefreshJWT: "refresh_token", 757 Authenticated: true, 758 } 759 760 ctx, cancel := context.WithCancel(context.Background()) 761 cancel() 762 763 doc := public.Document{ 764 Title: "Test Document", 765 } 766 767 result, err := svc.PostDocument(ctx, doc, false) 768 if err == nil { 769 t.Error("Expected error when context is cancelled") 770 } 771 if result != nil { 772 t.Error("Expected nil result when context is cancelled") 773 } 774 }) 775 776 t.Run("returns error when context timeout", func(t *testing.T) { 777 svc := NewATProtoService() 778 svc.session = &Session{ 779 DID: "did:plc:test123", 780 Handle: "test.bsky.social", 781 AccessJWT: "access_token", 782 RefreshJWT: "refresh_token", 783 Authenticated: true, 784 } 785 786 ctx, cancel := context.WithTimeout(context.Background(), 1) 787 defer cancel() 788 time.Sleep(2 * time.Millisecond) 789 790 doc := public.Document{ 791 Title: "Test Document", 792 } 793 794 result, err := svc.PostDocument(ctx, doc, false) 795 if err == nil { 796 t.Error("Expected error when context times out") 797 } 798 if result != nil { 799 t.Error("Expected nil result when context times out") 800 } 801 }) 802 803 t.Run("validates draft parameter sets correct collection", func(t *testing.T) { 804 svc := NewATProtoService() 805 svc.session = &Session{ 806 DID: "did:plc:test123", 807 Handle: "test.bsky.social", 808 AccessJWT: "access_token", 809 RefreshJWT: "refresh_token", 810 Authenticated: true, 811 } 812 ctx := context.Background() 813 814 doc := public.Document{ 815 Title: "Test Document", 816 } 817 818 _, err := svc.PostDocument(ctx, doc, true) 819 820 if err != nil && err.Error() == "not authenticated" { 821 t.Error("Authentication check should pass, but got authentication error") 822 } 823 }) 824 825 t.Run("validates published parameter sets correct collection", func(t *testing.T) { 826 svc := NewATProtoService() 827 svc.session = &Session{ 828 DID: "did:plc:test123", 829 Handle: "test.bsky.social", 830 AccessJWT: "access_token", 831 RefreshJWT: "refresh_token", 832 Authenticated: true, 833 } 834 ctx := context.Background() 835 836 doc := public.Document{ 837 Title: "Test Document", 838 } 839 840 _, err := svc.PostDocument(ctx, doc, false) 841 842 if err != nil && err.Error() == "not authenticated" { 843 t.Error("Authentication check should pass, but got authentication error") 844 } 845 }) 846 }) 847 848 t.Run("PatchDocument", func(t *testing.T) { 849 t.Run("returns error when not authenticated", func(t *testing.T) { 850 svc := NewATProtoService() 851 ctx := context.Background() 852 853 doc := public.Document{ 854 Title: "Updated Document", 855 } 856 857 result, err := svc.PatchDocument(ctx, "test-rkey", doc, false) 858 if err == nil { 859 t.Error("Expected error when patching document without authentication") 860 } 861 if result != nil { 862 t.Error("Expected nil result when not authenticated") 863 } 864 if err.Error() != "not authenticated" { 865 t.Errorf("Expected 'not authenticated' error, got '%v'", err) 866 } 867 }) 868 869 t.Run("returns error when session not authenticated", func(t *testing.T) { 870 svc := NewATProtoService() 871 ctx := context.Background() 872 svc.session = &Session{ 873 Handle: "test.bsky.social", 874 Authenticated: false, 875 } 876 877 doc := public.Document{ 878 Title: "Updated Document", 879 } 880 881 result, err := svc.PatchDocument(ctx, "test-rkey", doc, false) 882 if err == nil { 883 t.Error("Expected error when patching document with unauthenticated session") 884 } 885 if result != nil { 886 t.Error("Expected nil result when session not authenticated") 887 } 888 }) 889 890 t.Run("returns error when rkey is empty", func(t *testing.T) { 891 svc := NewATProtoService() 892 ctx := context.Background() 893 svc.session = &Session{ 894 DID: "did:plc:test123", 895 Handle: "test.bsky.social", 896 AccessJWT: "access_token", 897 RefreshJWT: "refresh_token", 898 Authenticated: true, 899 } 900 901 doc := public.Document{ 902 Title: "Updated Document", 903 } 904 905 result, err := svc.PatchDocument(ctx, "", doc, false) 906 if err == nil { 907 t.Error("Expected error when rkey is empty") 908 } 909 if result != nil { 910 t.Error("Expected nil result when rkey is empty") 911 } 912 if err.Error() != "rkey is required" { 913 t.Errorf("Expected 'rkey is required' error, got '%v'", err) 914 } 915 }) 916 917 t.Run("returns error when document title is empty", func(t *testing.T) { 918 svc := NewATProtoService() 919 ctx := context.Background() 920 svc.session = &Session{ 921 DID: "did:plc:test123", 922 Handle: "test.bsky.social", 923 AccessJWT: "access_token", 924 RefreshJWT: "refresh_token", 925 Authenticated: true, 926 } 927 928 doc := public.Document{ 929 Title: "", 930 } 931 932 result, err := svc.PatchDocument(ctx, "test-rkey", doc, false) 933 if err == nil { 934 t.Error("Expected error when document title is empty") 935 } 936 if result != nil { 937 t.Error("Expected nil result when title is empty") 938 } 939 if err.Error() != "document title is required" { 940 t.Errorf("Expected 'document title is required' error, got '%v'", err) 941 } 942 }) 943 944 t.Run("returns error when context cancelled", func(t *testing.T) { 945 svc := NewATProtoService() 946 svc.session = &Session{ 947 DID: "did:plc:test123", 948 Handle: "test.bsky.social", 949 AccessJWT: "access_token", 950 RefreshJWT: "refresh_token", 951 Authenticated: true, 952 } 953 954 ctx, cancel := context.WithCancel(context.Background()) 955 cancel() 956 957 doc := public.Document{ 958 Title: "Updated Document", 959 } 960 961 result, err := svc.PatchDocument(ctx, "test-rkey", doc, false) 962 if err == nil { 963 t.Error("Expected error when context is cancelled") 964 } 965 if result != nil { 966 t.Error("Expected nil result when context is cancelled") 967 } 968 }) 969 970 t.Run("returns error when context timeout", func(t *testing.T) { 971 svc := NewATProtoService() 972 svc.session = &Session{ 973 DID: "did:plc:test123", 974 Handle: "test.bsky.social", 975 AccessJWT: "access_token", 976 RefreshJWT: "refresh_token", 977 Authenticated: true, 978 } 979 980 ctx, cancel := context.WithTimeout(context.Background(), 1) 981 defer cancel() 982 time.Sleep(2 * time.Millisecond) 983 984 doc := public.Document{ 985 Title: "Updated Document", 986 } 987 988 result, err := svc.PatchDocument(ctx, "test-rkey", doc, false) 989 if err == nil { 990 t.Error("Expected error when context times out") 991 } 992 if result != nil { 993 t.Error("Expected nil result when context times out") 994 } 995 }) 996 997 t.Run("validates draft parameter sets correct collection", func(t *testing.T) { 998 svc := NewATProtoService() 999 svc.session = &Session{ 1000 DID: "did:plc:test123", 1001 Handle: "test.bsky.social", 1002 AccessJWT: "access_token", 1003 RefreshJWT: "refresh_token", 1004 Authenticated: true, 1005 } 1006 ctx := context.Background() 1007 1008 doc := public.Document{ 1009 Title: "Updated Document", 1010 } 1011 1012 _, err := svc.PatchDocument(ctx, "test-rkey", doc, true) 1013 1014 if err != nil && err.Error() == "not authenticated" { 1015 t.Error("Authentication check should pass, but got authentication error") 1016 } 1017 }) 1018 1019 t.Run("validates published parameter sets correct collection", func(t *testing.T) { 1020 svc := NewATProtoService() 1021 svc.session = &Session{ 1022 DID: "did:plc:test123", 1023 Handle: "test.bsky.social", 1024 AccessJWT: "access_token", 1025 RefreshJWT: "refresh_token", 1026 Authenticated: true, 1027 } 1028 ctx := context.Background() 1029 1030 doc := public.Document{ 1031 Title: "Updated Document", 1032 } 1033 1034 _, err := svc.PatchDocument(ctx, "test-rkey", doc, false) 1035 1036 if err != nil && err.Error() == "not authenticated" { 1037 t.Error("Authentication check should pass, but got authentication error") 1038 } 1039 }) 1040 }) 1041 1042 t.Run("DeleteDocument", func(t *testing.T) { 1043 t.Run("returns error when not authenticated", func(t *testing.T) { 1044 svc := NewATProtoService() 1045 ctx := context.Background() 1046 1047 err := svc.DeleteDocument(ctx, "test-rkey", false) 1048 if err == nil { 1049 t.Error("Expected error when deleting document without authentication") 1050 } 1051 if err.Error() != "not authenticated" { 1052 t.Errorf("Expected 'not authenticated' error, got '%v'", err) 1053 } 1054 }) 1055 1056 t.Run("returns error when session not authenticated", func(t *testing.T) { 1057 svc := NewATProtoService() 1058 ctx := context.Background() 1059 svc.session = &Session{ 1060 Handle: "test.bsky.social", 1061 Authenticated: false, 1062 } 1063 1064 err := svc.DeleteDocument(ctx, "test-rkey", false) 1065 if err == nil { 1066 t.Error("Expected error when deleting document with unauthenticated session") 1067 } 1068 }) 1069 1070 t.Run("returns error when rkey is empty", func(t *testing.T) { 1071 svc := NewATProtoService() 1072 ctx := context.Background() 1073 svc.session = &Session{ 1074 DID: "did:plc:test123", 1075 Handle: "test.bsky.social", 1076 AccessJWT: "access_token", 1077 RefreshJWT: "refresh_token", 1078 Authenticated: true, 1079 } 1080 1081 err := svc.DeleteDocument(ctx, "", false) 1082 if err == nil { 1083 t.Error("Expected error when rkey is empty") 1084 } 1085 if err.Error() != "rkey is required" { 1086 t.Errorf("Expected 'rkey is required' error, got '%v'", err) 1087 } 1088 }) 1089 1090 t.Run("returns error when context cancelled", func(t *testing.T) { 1091 svc := NewATProtoService() 1092 svc.session = &Session{ 1093 DID: "did:plc:test123", 1094 Handle: "test.bsky.social", 1095 AccessJWT: "access_token", 1096 RefreshJWT: "refresh_token", 1097 Authenticated: true, 1098 } 1099 1100 ctx, cancel := context.WithCancel(context.Background()) 1101 cancel() 1102 1103 err := svc.DeleteDocument(ctx, "test-rkey", false) 1104 if err == nil { 1105 t.Error("Expected error when context is cancelled") 1106 } 1107 }) 1108 1109 t.Run("returns error when context timeout", func(t *testing.T) { 1110 svc := NewATProtoService() 1111 svc.session = &Session{ 1112 DID: "did:plc:test123", 1113 Handle: "test.bsky.social", 1114 AccessJWT: "access_token", 1115 RefreshJWT: "refresh_token", 1116 Authenticated: true, 1117 } 1118 1119 ctx, cancel := context.WithTimeout(context.Background(), 1) 1120 defer cancel() 1121 time.Sleep(2 * time.Millisecond) 1122 1123 err := svc.DeleteDocument(ctx, "test-rkey", false) 1124 if err == nil { 1125 t.Error("Expected error when context times out") 1126 } 1127 }) 1128 1129 t.Run("validates draft parameter sets correct collection", func(t *testing.T) { 1130 svc := NewATProtoService() 1131 svc.session = &Session{ 1132 DID: "did:plc:test123", 1133 Handle: "test.bsky.social", 1134 AccessJWT: "access_token", 1135 RefreshJWT: "refresh_token", 1136 Authenticated: true, 1137 } 1138 ctx := context.Background() 1139 1140 err := svc.DeleteDocument(ctx, "test-rkey", true) 1141 1142 if err != nil && err.Error() == "not authenticated" { 1143 t.Error("Authentication check should pass, but got authentication error") 1144 } 1145 }) 1146 1147 t.Run("validates published parameter sets correct collection", func(t *testing.T) { 1148 svc := NewATProtoService() 1149 svc.session = &Session{ 1150 DID: "did:plc:test123", 1151 Handle: "test.bsky.social", 1152 AccessJWT: "access_token", 1153 RefreshJWT: "refresh_token", 1154 Authenticated: true, 1155 } 1156 ctx := context.Background() 1157 1158 err := svc.DeleteDocument(ctx, "test-rkey", false) 1159 1160 if err != nil && err.Error() == "not authenticated" { 1161 t.Error("Authentication check should pass, but got authentication error") 1162 } 1163 }) 1164 }) 1165 1166 t.Run("Session Management Edge Cases", func(t *testing.T) { 1167 t.Run("GetSession returns distinct error for nil session", func(t *testing.T) { 1168 svc := NewATProtoService() 1169 1170 session, err := svc.GetSession() 1171 if err == nil { 1172 t.Error("Expected error when getting nil session") 1173 } 1174 if session != nil { 1175 t.Error("Expected nil session when not authenticated") 1176 } 1177 expectedMsg := "not authenticated" 1178 if !strings.Contains(err.Error(), expectedMsg) { 1179 t.Errorf("Expected error message to contain '%s', got '%v'", expectedMsg, err) 1180 } 1181 }) 1182 1183 t.Run("RestoreSession validates all required fields", func(t *testing.T) { 1184 svc := NewATProtoService() 1185 1186 testCases := []struct { 1187 name string 1188 session *Session 1189 }{ 1190 { 1191 name: "missing DID", 1192 session: &Session{ 1193 DID: "", 1194 Handle: "test.bsky.social", 1195 AccessJWT: "access", 1196 RefreshJWT: "refresh", 1197 }, 1198 }, 1199 { 1200 name: "missing AccessJWT", 1201 session: &Session{ 1202 DID: "did:plc:test", 1203 Handle: "test.bsky.social", 1204 AccessJWT: "", 1205 RefreshJWT: "refresh", 1206 }, 1207 }, 1208 { 1209 name: "missing RefreshJWT", 1210 session: &Session{ 1211 DID: "did:plc:test", 1212 Handle: "test.bsky.social", 1213 AccessJWT: "access", 1214 RefreshJWT: "", 1215 }, 1216 }, 1217 } 1218 1219 for _, tc := range testCases { 1220 t.Run(tc.name, func(t *testing.T) { 1221 err := svc.RestoreSession(tc.session) 1222 if err == nil { 1223 t.Errorf("Expected error for %s", tc.name) 1224 } 1225 if !strings.Contains(err.Error(), "session missing required fields") { 1226 t.Errorf("Expected 'session missing required fields' error, got: %v", err) 1227 } 1228 }) 1229 } 1230 }) 1231 1232 t.Run("RestoreSession preserves empty PDSURL", func(t *testing.T) { 1233 svc := NewATProtoService() 1234 defaultPDSURL := svc.pdsURL 1235 1236 session := &Session{ 1237 DID: "did:plc:test123", 1238 Handle: "test.bsky.social", 1239 AccessJWT: "access_token", 1240 RefreshJWT: "refresh_token", 1241 PDSURL: "", 1242 ExpiresAt: time.Now().Add(2 * time.Hour), 1243 Authenticated: true, 1244 } 1245 1246 err := svc.RestoreSession(session) 1247 if err != nil { 1248 t.Errorf("Expected no error, got %v", err) 1249 } 1250 1251 if svc.pdsURL != defaultPDSURL { 1252 t.Errorf("Expected pdsURL to remain default when session PDSURL is empty, got '%s'", svc.pdsURL) 1253 } 1254 }) 1255 }) 1256 1257 t.Run("PostDocument Validation", func(t *testing.T) { 1258 t.Run("validates title before marshaling", func(t *testing.T) { 1259 svc := NewATProtoService() 1260 svc.session = &Session{ 1261 DID: "did:plc:test123", 1262 Handle: "test.bsky.social", 1263 AccessJWT: "access_token", 1264 RefreshJWT: "refresh_token", 1265 Authenticated: true, 1266 } 1267 ctx := context.Background() 1268 1269 doc := public.Document{ 1270 Title: "", 1271 } 1272 1273 result, err := svc.PostDocument(ctx, doc, false) 1274 if err == nil { 1275 t.Error("Expected error when title is empty") 1276 } 1277 if result != nil { 1278 t.Error("Expected nil result when validation fails") 1279 } 1280 if !strings.Contains(err.Error(), "document title is required") { 1281 t.Errorf("Expected 'document title is required' error, got: %v", err) 1282 } 1283 }) 1284 1285 t.Run("sets correct collection for draft", func(t *testing.T) { 1286 svc := NewATProtoService() 1287 svc.session = &Session{ 1288 DID: "did:plc:test123", 1289 Handle: "test.bsky.social", 1290 AccessJWT: "access_token", 1291 RefreshJWT: "refresh_token", 1292 Authenticated: true, 1293 } 1294 ctx := context.Background() 1295 1296 doc := public.Document{ 1297 Title: "Test Draft", 1298 } 1299 1300 _, err := svc.PostDocument(ctx, doc, true) 1301 1302 if err != nil && strings.Contains(err.Error(), "document title is required") { 1303 t.Error("Title validation should pass") 1304 } 1305 }) 1306 1307 t.Run("sets correct collection for published", func(t *testing.T) { 1308 svc := NewATProtoService() 1309 svc.session = &Session{ 1310 DID: "did:plc:test123", 1311 Handle: "test.bsky.social", 1312 AccessJWT: "access_token", 1313 RefreshJWT: "refresh_token", 1314 Authenticated: true, 1315 } 1316 ctx := context.Background() 1317 1318 doc := public.Document{ 1319 Title: "Test Published", 1320 } 1321 1322 _, err := svc.PostDocument(ctx, doc, false) 1323 1324 if err != nil && strings.Contains(err.Error(), "document title is required") { 1325 t.Error("Title validation should pass") 1326 } 1327 }) 1328 }) 1329 1330 t.Run("PatchDocument Validation", func(t *testing.T) { 1331 t.Run("validates rkey before title", func(t *testing.T) { 1332 svc := NewATProtoService() 1333 svc.session = &Session{ 1334 DID: "did:plc:test123", 1335 Handle: "test.bsky.social", 1336 AccessJWT: "access_token", 1337 RefreshJWT: "refresh_token", 1338 Authenticated: true, 1339 } 1340 ctx := context.Background() 1341 1342 doc := public.Document{ 1343 Title: "Valid Title", 1344 } 1345 1346 result, err := svc.PatchDocument(ctx, "", doc, false) 1347 if err == nil { 1348 t.Error("Expected error when rkey is empty") 1349 } 1350 if result != nil { 1351 t.Error("Expected nil result when rkey validation fails") 1352 } 1353 if !strings.Contains(err.Error(), "rkey is required") { 1354 t.Errorf("Expected 'rkey is required' error, got: %v", err) 1355 } 1356 }) 1357 1358 t.Run("validates title after rkey", func(t *testing.T) { 1359 svc := NewATProtoService() 1360 svc.session = &Session{ 1361 DID: "did:plc:test123", 1362 Handle: "test.bsky.social", 1363 AccessJWT: "access_token", 1364 RefreshJWT: "refresh_token", 1365 Authenticated: true, 1366 } 1367 ctx := context.Background() 1368 1369 doc := public.Document{ 1370 Title: "", 1371 } 1372 1373 result, err := svc.PatchDocument(ctx, "valid-rkey", doc, false) 1374 if err == nil { 1375 t.Error("Expected error when title is empty") 1376 } 1377 if result != nil { 1378 t.Error("Expected nil result when title validation fails") 1379 } 1380 if !strings.Contains(err.Error(), "document title is required") { 1381 t.Errorf("Expected 'document title is required' error, got: %v", err) 1382 } 1383 }) 1384 1385 t.Run("sets correct collection for draft", func(t *testing.T) { 1386 svc := NewATProtoService() 1387 svc.session = &Session{ 1388 DID: "did:plc:test123", 1389 Handle: "test.bsky.social", 1390 AccessJWT: "access_token", 1391 RefreshJWT: "refresh_token", 1392 Authenticated: true, 1393 } 1394 ctx := context.Background() 1395 1396 doc := public.Document{ 1397 Title: "Test Draft", 1398 } 1399 1400 _, err := svc.PatchDocument(ctx, "test-rkey", doc, true) 1401 1402 if err != nil && strings.Contains(err.Error(), "document title is required") { 1403 t.Error("Title validation should pass") 1404 } 1405 }) 1406 1407 t.Run("sets correct collection for published", func(t *testing.T) { 1408 svc := NewATProtoService() 1409 svc.session = &Session{ 1410 DID: "did:plc:test123", 1411 Handle: "test.bsky.social", 1412 AccessJWT: "access_token", 1413 RefreshJWT: "refresh_token", 1414 Authenticated: true, 1415 } 1416 ctx := context.Background() 1417 1418 doc := public.Document{ 1419 Title: "Test Published", 1420 } 1421 1422 _, err := svc.PatchDocument(ctx, "test-rkey", doc, false) 1423 1424 if err != nil && strings.Contains(err.Error(), "document title is required") { 1425 t.Error("Title validation should pass") 1426 } 1427 }) 1428 }) 1429 1430 t.Run("DeleteDocument Validation", func(t *testing.T) { 1431 t.Run("validates rkey before attempting delete", func(t *testing.T) { 1432 svc := NewATProtoService() 1433 svc.session = &Session{ 1434 DID: "did:plc:test123", 1435 Handle: "test.bsky.social", 1436 AccessJWT: "access_token", 1437 RefreshJWT: "refresh_token", 1438 Authenticated: true, 1439 } 1440 ctx := context.Background() 1441 1442 err := svc.DeleteDocument(ctx, "", false) 1443 if err == nil { 1444 t.Error("Expected error when rkey is empty") 1445 } 1446 if !strings.Contains(err.Error(), "rkey is required") { 1447 t.Errorf("Expected 'rkey is required' error, got: %v", err) 1448 } 1449 }) 1450 1451 t.Run("uses correct collection for draft", func(t *testing.T) { 1452 svc := NewATProtoService() 1453 svc.session = &Session{ 1454 DID: "did:plc:test123", 1455 Handle: "test.bsky.social", 1456 AccessJWT: "access_token", 1457 RefreshJWT: "refresh_token", 1458 Authenticated: true, 1459 } 1460 ctx := context.Background() 1461 1462 err := svc.DeleteDocument(ctx, "test-rkey", true) 1463 1464 if err != nil && strings.Contains(err.Error(), "rkey is required") { 1465 t.Error("Rkey validation should pass") 1466 } 1467 }) 1468 1469 t.Run("uses correct collection for published", func(t *testing.T) { 1470 svc := NewATProtoService() 1471 svc.session = &Session{ 1472 DID: "did:plc:test123", 1473 Handle: "test.bsky.social", 1474 AccessJWT: "access_token", 1475 RefreshJWT: "refresh_token", 1476 Authenticated: true, 1477 } 1478 ctx := context.Background() 1479 1480 err := svc.DeleteDocument(ctx, "test-rkey", false) 1481 1482 if err != nil && strings.Contains(err.Error(), "rkey is required") { 1483 t.Error("Rkey validation should pass") 1484 } 1485 }) 1486 }) 1487 1488 t.Run("Concurrent Operations", func(t *testing.T) { 1489 t.Run("Close can be called multiple times", func(t *testing.T) { 1490 svc := NewATProtoService() 1491 svc.session = &Session{ 1492 Handle: "test.bsky.social", 1493 Authenticated: true, 1494 } 1495 1496 err1 := svc.Close() 1497 if err1 != nil { 1498 t.Errorf("First close should succeed: %v", err1) 1499 } 1500 1501 err2 := svc.Close() 1502 if err2 != nil { 1503 t.Errorf("Second close should succeed: %v", err2) 1504 } 1505 }) 1506 1507 t.Run("IsAuthenticated after Close returns false", func(t *testing.T) { 1508 svc := NewATProtoService() 1509 svc.session = &Session{ 1510 Handle: "test.bsky.social", 1511 Authenticated: true, 1512 } 1513 1514 if !svc.IsAuthenticated() { 1515 t.Error("Expected IsAuthenticated to return true before close") 1516 } 1517 1518 err := svc.Close() 1519 if err != nil { 1520 t.Errorf("Close failed: %v", err) 1521 } 1522 1523 if svc.IsAuthenticated() { 1524 t.Error("Expected IsAuthenticated to return false after close") 1525 } 1526 }) 1527 }) 1528 1529 t.Run("CBOR Conversion Functions", func(t *testing.T) { 1530 t.Run("convertCBORToJSONCompatible handles simple map", func(t *testing.T) { 1531 input := map[any]any{ 1532 "key1": "value1", 1533 "key2": 42, 1534 "key3": true, 1535 } 1536 1537 result := convertCBORToJSONCompatible(input) 1538 1539 mapResult, ok := result.(map[string]any) 1540 if !ok { 1541 t.Fatal("Expected result to be map[string]any") 1542 } 1543 1544 if mapResult["key1"] != "value1" { 1545 t.Errorf("Expected key1='value1', got '%v'", mapResult["key1"]) 1546 } 1547 if mapResult["key2"] != 42 { 1548 t.Errorf("Expected key2=42, got %v", mapResult["key2"]) 1549 } 1550 if mapResult["key3"] != true { 1551 t.Errorf("Expected key3=true, got %v", mapResult["key3"]) 1552 } 1553 }) 1554 1555 t.Run("convertCBORToJSONCompatible handles nested maps", func(t *testing.T) { 1556 input := map[any]any{ 1557 "outer": map[any]any{ 1558 "inner": map[any]any{ 1559 "deep": "value", 1560 }, 1561 }, 1562 } 1563 1564 result := convertCBORToJSONCompatible(input) 1565 1566 mapResult, ok := result.(map[string]any) 1567 if !ok { 1568 t.Fatal("Expected result to be map[string]any") 1569 } 1570 1571 outer, ok := mapResult["outer"].(map[string]any) 1572 if !ok { 1573 t.Fatal("Expected outer to be map[string]any") 1574 } 1575 1576 inner, ok := outer["inner"].(map[string]any) 1577 if !ok { 1578 t.Fatal("Expected inner to be map[string]any") 1579 } 1580 1581 if inner["deep"] != "value" { 1582 t.Errorf("Expected deep='value', got '%v'", inner["deep"]) 1583 } 1584 }) 1585 1586 t.Run("convertCBORToJSONCompatible handles arrays", func(t *testing.T) { 1587 input := []any{ 1588 "string", 1589 42, 1590 map[any]any{"nested": "map"}, 1591 []any{"nested", "array"}, 1592 } 1593 1594 result := convertCBORToJSONCompatible(input) 1595 1596 arrayResult, ok := result.([]any) 1597 if !ok { 1598 t.Fatal("Expected result to be []any") 1599 } 1600 1601 if len(arrayResult) != 4 { 1602 t.Fatalf("Expected 4 elements, got %d", len(arrayResult)) 1603 } 1604 1605 if arrayResult[0] != "string" { 1606 t.Errorf("Expected arrayResult[0]='string', got '%v'", arrayResult[0]) 1607 } 1608 1609 nestedMap, ok := arrayResult[2].(map[string]any) 1610 if !ok { 1611 t.Fatal("Expected arrayResult[2] to be map[string]any") 1612 } 1613 if nestedMap["nested"] != "map" { 1614 t.Errorf("Expected nested='map', got '%v'", nestedMap["nested"]) 1615 } 1616 1617 nestedArray, ok := arrayResult[3].([]any) 1618 if !ok { 1619 t.Fatal("Expected arrayResult[3] to be []any") 1620 } 1621 if len(nestedArray) != 2 { 1622 t.Errorf("Expected nested array length 2, got %d", len(nestedArray)) 1623 } 1624 }) 1625 1626 t.Run("convertJSONToCBORCompatible handles simple map", func(t *testing.T) { 1627 input := map[string]any{ 1628 "key1": "value1", 1629 "key2": 42, 1630 "key3": true, 1631 } 1632 1633 result := convertJSONToCBORCompatible(input) 1634 1635 mapResult, ok := result.(map[any]any) 1636 if !ok { 1637 t.Fatal("Expected result to be map[any]any") 1638 } 1639 1640 if mapResult["key1"] != "value1" { 1641 t.Errorf("Expected key1='value1', got '%v'", mapResult["key1"]) 1642 } 1643 if mapResult["key2"] != 42 { 1644 t.Errorf("Expected key2=42, got %v", mapResult["key2"]) 1645 } 1646 if mapResult["key3"] != true { 1647 t.Errorf("Expected key3=true, got %v", mapResult["key3"]) 1648 } 1649 }) 1650 1651 t.Run("convertJSONToCBORCompatible handles nested maps", func(t *testing.T) { 1652 input := map[string]any{ 1653 "outer": map[string]any{ 1654 "inner": map[string]any{ 1655 "deep": "value", 1656 }, 1657 }, 1658 } 1659 1660 result := convertJSONToCBORCompatible(input) 1661 1662 mapResult, ok := result.(map[any]any) 1663 if !ok { 1664 t.Fatal("Expected result to be map[any]any") 1665 } 1666 1667 outer, ok := mapResult["outer"].(map[any]any) 1668 if !ok { 1669 t.Fatal("Expected outer to be map[any]any") 1670 } 1671 1672 inner, ok := outer["inner"].(map[any]any) 1673 if !ok { 1674 t.Fatal("Expected inner to be map[any]any") 1675 } 1676 1677 if inner["deep"] != "value" { 1678 t.Errorf("Expected deep='value', got '%v'", inner["deep"]) 1679 } 1680 }) 1681 1682 t.Run("convertJSONToCBORCompatible handles arrays", func(t *testing.T) { 1683 input := []any{ 1684 "string", 1685 42, 1686 map[string]any{"nested": "map"}, 1687 []any{"nested", "array"}, 1688 } 1689 1690 result := convertJSONToCBORCompatible(input) 1691 1692 arrayResult, ok := result.([]any) 1693 if !ok { 1694 t.Fatal("Expected result to be []any") 1695 } 1696 1697 if len(arrayResult) != 4 { 1698 t.Fatalf("Expected 4 elements, got %d", len(arrayResult)) 1699 } 1700 1701 if arrayResult[0] != "string" { 1702 t.Errorf("Expected arrayResult[0]='string', got '%v'", arrayResult[0]) 1703 } 1704 1705 nestedMap, ok := arrayResult[2].(map[any]any) 1706 if !ok { 1707 t.Fatal("Expected arrayResult[2] to be map[any]any") 1708 } 1709 if nestedMap["nested"] != "map" { 1710 t.Errorf("Expected nested='map', got '%v'", nestedMap["nested"]) 1711 } 1712 1713 nestedArray, ok := arrayResult[3].([]any) 1714 if !ok { 1715 t.Fatal("Expected arrayResult[3] to be []any") 1716 } 1717 if len(nestedArray) != 2 { 1718 t.Errorf("Expected nested array length 2, got %d", len(nestedArray)) 1719 } 1720 }) 1721 1722 t.Run("round-trip conversion preserves data", func(t *testing.T) { 1723 original := map[string]any{ 1724 "title": "Test Document", 1725 "author": "did:plc:test123", 1726 "content": []any{"paragraph1", "paragraph2"}, 1727 "metadata": map[string]any{ 1728 "tags": []any{"test", "document"}, 1729 "published": true, 1730 "count": 42, 1731 }, 1732 } 1733 1734 cborCompatible := convertJSONToCBORCompatible(original) 1735 jsonCompatible := convertCBORToJSONCompatible(cborCompatible) 1736 1737 originalJSON, err := json.Marshal(original) 1738 if err != nil { 1739 t.Fatalf("Failed to marshal original: %v", err) 1740 } 1741 1742 resultJSON, err := json.Marshal(jsonCompatible) 1743 if err != nil { 1744 t.Fatalf("Failed to marshal result: %v", err) 1745 } 1746 1747 if string(originalJSON) != string(resultJSON) { 1748 t.Errorf("Round-trip conversion changed data.\nOriginal: %s\nResult: %s", originalJSON, resultJSON) 1749 } 1750 }) 1751 1752 t.Run("Document conversion through CBOR preserves structure", func(t *testing.T) { 1753 doc := public.Document{ 1754 Type: public.TypeDocument, 1755 Title: "Test Document", 1756 Pages: []public.LinearDocument{ 1757 { 1758 Type: public.TypeLinearDocument, 1759 Blocks: []public.BlockWrap{ 1760 { 1761 Type: public.TypeBlock, 1762 Block: public.TextBlock{ 1763 Type: public.TypeTextBlock, 1764 Plaintext: "Hello, world!", 1765 }, 1766 }, 1767 }, 1768 }, 1769 }, 1770 PublishedAt: time.Now().UTC().Format(time.RFC3339), 1771 } 1772 1773 jsonBytes, err := json.Marshal(doc) 1774 if err != nil { 1775 t.Fatalf("Failed to marshal document to JSON: %v", err) 1776 } 1777 1778 var jsonData map[string]any 1779 if err := json.Unmarshal(jsonBytes, &jsonData); err != nil { 1780 t.Fatalf("Failed to unmarshal JSON to map: %v", err) 1781 } 1782 1783 cborCompatible := convertJSONToCBORCompatible(jsonData) 1784 1785 cborBytes, err := cbor.Marshal(cborCompatible) 1786 if err != nil { 1787 t.Fatalf("Failed to marshal to CBOR: %v", err) 1788 } 1789 1790 var cborData any 1791 if err := cbor.Unmarshal(cborBytes, &cborData); err != nil { 1792 t.Fatalf("Failed to unmarshal CBOR: %v", err) 1793 } 1794 1795 jsonCompatible := convertCBORToJSONCompatible(cborData) 1796 1797 finalJSONBytes, err := json.Marshal(jsonCompatible) 1798 if err != nil { 1799 t.Fatalf("Failed to marshal final JSON: %v", err) 1800 } 1801 1802 var finalDoc public.Document 1803 if err := json.Unmarshal(finalJSONBytes, &finalDoc); err != nil { 1804 t.Fatalf("Failed to unmarshal final document: %v", err) 1805 } 1806 1807 if finalDoc.Title != doc.Title { 1808 t.Errorf("Title changed: expected '%s', got '%s'", doc.Title, finalDoc.Title) 1809 } 1810 1811 if len(finalDoc.Pages) != len(doc.Pages) { 1812 t.Errorf("Pages length changed: expected %d, got %d", len(doc.Pages), len(finalDoc.Pages)) 1813 } 1814 1815 if len(finalDoc.Pages) > 0 && len(finalDoc.Pages[0].Blocks) > 0 { 1816 if textBlock, ok := finalDoc.Pages[0].Blocks[0].Block.(public.TextBlock); ok { 1817 expectedBlock := doc.Pages[0].Blocks[0].Block.(public.TextBlock) 1818 if textBlock.Plaintext != expectedBlock.Plaintext { 1819 t.Errorf("Block plaintext changed: expected '%s', got '%s'", 1820 expectedBlock.Plaintext, textBlock.Plaintext) 1821 } 1822 } else { 1823 t.Error("Expected Block to be TextBlock") 1824 } 1825 } 1826 }) 1827 }) 1828}