cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
29
fork

Configure Feed

Select the types of activity you want to include in your feed.

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}