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