bluesky viewer in the terminal
1package export
2
3import (
4 "encoding/csv"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10 "time"
11
12 "github.com/stormlightlabs/skypanel/cli/internal/store"
13)
14
15// createTestPosts generates sample posts for testing
16func createTestPosts() []*store.PostModel {
17 now := time.Now()
18 posts := []*store.PostModel{
19 {
20 URI: "at://did:plc:test1/app.bsky.feed.post/1",
21 AuthorDID: "did:plc:author1",
22 Text: "First test post",
23 FeedID: "feed-1",
24 IndexedAt: now.Add(-2 * time.Hour),
25 },
26 {
27 URI: "at://did:plc:test2/app.bsky.feed.post/2",
28 AuthorDID: "did:plc:author2",
29 Text: "Second test post",
30 FeedID: "feed-1",
31 IndexedAt: now.Add(-1 * time.Hour),
32 },
33 {
34 URI: "at://did:plc:test3/app.bsky.feed.post/3",
35 AuthorDID: "did:plc:author3",
36 Text: "Third test post with special chars: \"quotes\", commas, and\nnewlines",
37 FeedID: "feed-2",
38 IndexedAt: now,
39 },
40 }
41
42 for i, post := range posts {
43 post.SetID(string(rune('a' + i)))
44 post.SetCreatedAt(now.Add(time.Duration(-i) * time.Hour))
45 post.SetUpdatedAt(now)
46 }
47
48 return posts
49}
50
51// TestToJSON_Success verifies JSON export with valid data
52func TestToJSON_Success(t *testing.T) {
53 posts := createTestPosts()
54
55 tmpDir := t.TempDir()
56 filename := filepath.Join(tmpDir, "test.json")
57
58 err := ToJSON(filename, posts)
59 if err != nil {
60 t.Fatalf("ToJSON failed: %v", err)
61 }
62
63 if _, err := os.Stat(filename); os.IsNotExist(err) {
64 t.Fatal("exported file does not exist")
65 }
66
67 data, err := os.ReadFile(filename)
68 if err != nil {
69 t.Fatalf("failed to read exported file: %v", err)
70 }
71
72 var exportedPosts []ExportPost
73 if err := json.Unmarshal(data, &exportedPosts); err != nil {
74 t.Fatalf("failed to parse JSON: %v", err)
75 }
76
77 if len(exportedPosts) != 3 {
78 t.Errorf("expected 3 posts, got %d", len(exportedPosts))
79 }
80
81 if exportedPosts[0].URI != "at://did:plc:test1/app.bsky.feed.post/1" {
82 t.Errorf("unexpected URI: %s", exportedPosts[0].URI)
83 }
84 if exportedPosts[0].Text != "First test post" {
85 t.Errorf("unexpected text: %s", exportedPosts[0].Text)
86 }
87}
88
89// TestToJSON_EmptyPosts verifies JSON export with empty slice
90func TestToJSON_EmptyPosts(t *testing.T) {
91 tmpDir := t.TempDir()
92 filename := filepath.Join(tmpDir, "empty.json")
93
94 err := ToJSON(filename, []*store.PostModel{})
95 if err != nil {
96 t.Fatalf("ToJSON failed: %v", err)
97 }
98
99 data, err := os.ReadFile(filename)
100 if err != nil {
101 t.Fatalf("failed to read exported file: %v", err)
102 }
103
104 var exportedPosts []ExportPost
105 if err := json.Unmarshal(data, &exportedPosts); err != nil {
106 t.Fatalf("failed to parse JSON: %v", err)
107 }
108
109 if len(exportedPosts) != 0 {
110 t.Errorf("expected 0 posts, got %d", len(exportedPosts))
111 }
112}
113
114// TestToJSON_InvalidPath verifies error handling for invalid file paths
115func TestToJSON_InvalidPath(t *testing.T) {
116 posts := createTestPosts()
117
118 err := ToJSON("/invalid/path/that/does/not/exist/test.json", posts)
119 if err == nil {
120 t.Error("expected error for invalid path, got nil")
121 }
122}
123
124// TestToCSV_Success verifies CSV export with valid data
125func TestToCSV_Success(t *testing.T) {
126 posts := createTestPosts()
127
128 tmpDir := t.TempDir()
129 filename := filepath.Join(tmpDir, "test.csv")
130
131 err := ToCSV(filename, posts)
132 if err != nil {
133 t.Fatalf("ToCSV failed: %v", err)
134 }
135
136 if _, err := os.Stat(filename); os.IsNotExist(err) {
137 t.Fatal("exported file does not exist")
138 }
139
140 file, err := os.Open(filename)
141 if err != nil {
142 t.Fatalf("failed to open exported file: %v", err)
143 }
144 defer file.Close()
145
146 reader := csv.NewReader(file)
147 records, err := reader.ReadAll()
148 if err != nil {
149 t.Fatalf("failed to parse CSV: %v", err)
150 }
151
152 if len(records) != 4 {
153 t.Errorf("expected 4 rows (header + 3 data), got %d", len(records))
154 }
155
156 expectedHeader := []string{"ID", "URI", "AuthorDID", "Text", "FeedID", "IndexedAt", "CreatedAt"}
157 for i, col := range expectedHeader {
158 if records[0][i] != col {
159 t.Errorf("header column %d: expected %s, got %s", i, col, records[0][i])
160 }
161 }
162
163 if records[1][1] != "at://did:plc:test1/app.bsky.feed.post/1" {
164 t.Errorf("unexpected URI in row 1: %s", records[1][1])
165 }
166 if records[1][3] != "First test post" {
167 t.Errorf("unexpected text in row 1: %s", records[1][3])
168 }
169}
170
171// TestToCSV_SpecialCharacters verifies CSV escaping of special characters
172func TestToCSV_SpecialCharacters(t *testing.T) {
173 posts := createTestPosts()
174
175 tmpDir := t.TempDir()
176 filename := filepath.Join(tmpDir, "special.csv")
177
178 err := ToCSV(filename, posts)
179 if err != nil {
180 t.Fatalf("ToCSV failed: %v", err)
181 }
182
183 file, err := os.Open(filename)
184 if err != nil {
185 t.Fatalf("failed to open exported file: %v", err)
186 }
187 defer file.Close()
188
189 reader := csv.NewReader(file)
190 records, err := reader.ReadAll()
191 if err != nil {
192 t.Fatalf("failed to parse CSV: %v", err)
193 }
194
195 specialText := records[3][3]
196 if !strings.Contains(specialText, "quotes") {
197 t.Error("special characters not properly preserved in CSV")
198 }
199 if !strings.Contains(specialText, "newlines") {
200 t.Error("newlines not properly preserved in CSV")
201 }
202}
203
204// TestToCSV_EmptyPosts verifies CSV export with empty slice
205func TestToCSV_EmptyPosts(t *testing.T) {
206 tmpDir := t.TempDir()
207 filename := filepath.Join(tmpDir, "empty.csv")
208
209 err := ToCSV(filename, []*store.PostModel{})
210 if err != nil {
211 t.Fatalf("ToCSV failed: %v", err)
212 }
213
214 file, err := os.Open(filename)
215 if err != nil {
216 t.Fatalf("failed to open exported file: %v", err)
217 }
218 defer file.Close()
219
220 reader := csv.NewReader(file)
221 records, err := reader.ReadAll()
222 if err != nil {
223 t.Fatalf("failed to parse CSV: %v", err)
224 }
225
226 if len(records) != 1 {
227 t.Errorf("expected 1 row (header only), got %d", len(records))
228 }
229}
230
231// TestToCSV_InvalidPath verifies error handling for invalid file paths
232func TestToCSV_InvalidPath(t *testing.T) {
233 posts := createTestPosts()
234
235 err := ToCSV("/invalid/path/that/does/not/exist/test.csv", posts)
236 if err == nil {
237 t.Error("expected error for invalid path, got nil")
238 }
239}
240
241// TestToTXT_Success verifies TXT export with valid data
242func TestToTXT_Success(t *testing.T) {
243 posts := createTestPosts()
244
245 tmpDir := t.TempDir()
246 filename := filepath.Join(tmpDir, "test.txt")
247
248 err := ToTXT(filename, posts)
249 if err != nil {
250 t.Fatalf("ToTXT failed: %v", err)
251 }
252
253 if _, err := os.Stat(filename); os.IsNotExist(err) {
254 t.Fatal("exported file does not exist")
255 }
256
257 data, err := os.ReadFile(filename)
258 if err != nil {
259 t.Fatalf("failed to read exported file: %v", err)
260 }
261
262 content := string(data)
263
264 if !strings.Contains(content, "Post #1") {
265 t.Error("missing post number")
266 }
267 if !strings.Contains(content, "at://did:plc:test1/app.bsky.feed.post/1") {
268 t.Error("missing URI")
269 }
270 if !strings.Contains(content, "First test post") {
271 t.Error("missing post text")
272 }
273 if !strings.Contains(content, "did:plc:author1") {
274 t.Error("missing author DID")
275 }
276 if !strings.Contains(content, strings.Repeat("-", 80)) {
277 t.Error("missing separator")
278 }
279}
280
281// TestToTXT_EmptyPosts verifies TXT export with empty slice
282func TestToTXT_EmptyPosts(t *testing.T) {
283 tmpDir := t.TempDir()
284 filename := filepath.Join(tmpDir, "empty.txt")
285
286 err := ToTXT(filename, []*store.PostModel{})
287 if err != nil {
288 t.Fatalf("ToTXT failed: %v", err)
289 }
290
291 data, err := os.ReadFile(filename)
292 if err != nil {
293 t.Fatalf("failed to read exported file: %v", err)
294 }
295
296 if len(data) != 0 {
297 t.Errorf("expected empty file, got %d bytes", len(data))
298 }
299}
300
301// TestToTXT_InvalidPath verifies error handling for invalid file paths
302func TestToTXT_InvalidPath(t *testing.T) {
303 posts := createTestPosts()
304
305 err := ToTXT("/invalid/path/that/does/not/exist/test.txt", posts)
306 if err == nil {
307 t.Error("expected error for invalid path, got nil")
308 }
309}
310
311// TestToTXT_MultiplePostsFormatting verifies proper formatting of multiple posts
312func TestToTXT_MultiplePostsFormatting(t *testing.T) {
313 posts := createTestPosts()
314
315 tmpDir := t.TempDir()
316 filename := filepath.Join(tmpDir, "multiple.txt")
317
318 err := ToTXT(filename, posts)
319 if err != nil {
320 t.Fatalf("ToTXT failed: %v", err)
321 }
322
323 data, err := os.ReadFile(filename)
324 if err != nil {
325 t.Fatalf("failed to read exported file: %v", err)
326 }
327
328 content := string(data)
329
330 for i := 1; i <= 3; i++ {
331 postNum := "Post #" + string(rune('0'+i))
332 if !strings.Contains(content, postNum) {
333 t.Errorf("missing %s", postNum)
334 }
335 }
336
337 separatorCount := strings.Count(content, strings.Repeat("-", 80))
338 if separatorCount != 3 {
339 t.Errorf("expected 3 separators, got %d", separatorCount)
340 }
341}
342
343// TestConvertPosts verifies post model conversion
344func TestConvertPosts(t *testing.T) {
345 posts := createTestPosts()
346
347 exportPosts := convertPosts(posts)
348
349 if len(exportPosts) != len(posts) {
350 t.Errorf("expected %d posts, got %d", len(posts), len(exportPosts))
351 }
352
353 for i := range posts {
354 if exportPosts[i].ID != posts[i].ID() {
355 t.Errorf("post %d: ID mismatch", i)
356 }
357 if exportPosts[i].URI != posts[i].URI {
358 t.Errorf("post %d: URI mismatch", i)
359 }
360 if exportPosts[i].Text != posts[i].Text {
361 t.Errorf("post %d: Text mismatch", i)
362 }
363 if exportPosts[i].AuthorDID != posts[i].AuthorDID {
364 t.Errorf("post %d: AuthorDID mismatch", i)
365 }
366 if exportPosts[i].FeedID != posts[i].FeedID {
367 t.Errorf("post %d: FeedID mismatch", i)
368 }
369 }
370}
371
372// TestExportPost_JSONTags verifies JSON struct tags
373func TestExportPost_JSONTags(t *testing.T) {
374 now := time.Now()
375 post := ExportPost{
376 ID: "test-id",
377 URI: "at://test/uri",
378 AuthorDID: "did:plc:test",
379 Text: "test text",
380 FeedID: "feed-id",
381 IndexedAt: now,
382 CreatedAt: now,
383 }
384
385 data, err := json.Marshal(post)
386 if err != nil {
387 t.Fatalf("failed to marshal: %v", err)
388 }
389
390 content := string(data)
391
392 expectedFields := []string{
393 "\"id\":",
394 "\"uri\":",
395 "\"author_did\":",
396 "\"text\":",
397 "\"feed_id\":",
398 "\"indexed_at\":",
399 "\"created_at\":",
400 }
401
402 for _, field := range expectedFields {
403 if !strings.Contains(content, field) {
404 t.Errorf("missing expected JSON field: %s", field)
405 }
406 }
407}
408
409// TestToJSON_SinglePost verifies export with single post
410func TestToJSON_SinglePost(t *testing.T) {
411 now := time.Now()
412 post := &store.PostModel{
413 URI: "at://test/single",
414 AuthorDID: "did:plc:single",
415 Text: "Single post",
416 FeedID: "feed-1",
417 IndexedAt: now,
418 }
419 post.SetID("single-id")
420 post.SetCreatedAt(now)
421
422 tmpDir := t.TempDir()
423 filename := filepath.Join(tmpDir, "single.json")
424
425 err := ToJSON(filename, []*store.PostModel{post})
426 if err != nil {
427 t.Fatalf("ToJSON failed: %v", err)
428 }
429
430 data, err := os.ReadFile(filename)
431 if err != nil {
432 t.Fatalf("failed to read file: %v", err)
433 }
434
435 var exportedPosts []ExportPost
436 if err := json.Unmarshal(data, &exportedPosts); err != nil {
437 t.Fatalf("failed to parse JSON: %v", err)
438 }
439
440 if len(exportedPosts) != 1 {
441 t.Errorf("expected 1 post, got %d", len(exportedPosts))
442 }
443
444 if exportedPosts[0].Text != "Single post" {
445 t.Errorf("unexpected text: %s", exportedPosts[0].Text)
446 }
447}
448
449// createTestProfile generates a sample profile for testing
450func createTestProfile() *store.ActorProfile {
451 return &store.ActorProfile{
452 Did: "did:plc:test123",
453 Handle: "testuser.bsky.social",
454 DisplayName: "Test User",
455 Description: "This is a test profile description",
456 FollowersCount: 100,
457 FollowsCount: 50,
458 PostsCount: 25,
459 CreatedAt: "2024-01-01T00:00:00Z",
460 }
461}
462
463// TestProfileToJSON_Success verifies profile JSON export with valid data
464func TestProfileToJSON_Success(t *testing.T) {
465 profile := createTestProfile()
466
467 tmpDir := t.TempDir()
468 filename := filepath.Join(tmpDir, "profile.json")
469
470 err := ProfileToJSON(filename, profile)
471 if err != nil {
472 t.Fatalf("ProfileToJSON failed: %v", err)
473 }
474
475 if _, err := os.Stat(filename); os.IsNotExist(err) {
476 t.Fatal("exported file does not exist")
477 }
478
479 data, err := os.ReadFile(filename)
480 if err != nil {
481 t.Fatalf("failed to read exported file: %v", err)
482 }
483
484 var exportedProfile store.ActorProfile
485 if err := json.Unmarshal(data, &exportedProfile); err != nil {
486 t.Fatalf("failed to parse JSON: %v", err)
487 }
488
489 if exportedProfile.Did != profile.Did {
490 t.Errorf("expected DID %s, got %s", profile.Did, exportedProfile.Did)
491 }
492 if exportedProfile.Handle != profile.Handle {
493 t.Errorf("expected handle %s, got %s", profile.Handle, exportedProfile.Handle)
494 }
495 if exportedProfile.DisplayName != profile.DisplayName {
496 t.Errorf("expected display name %s, got %s", profile.DisplayName, exportedProfile.DisplayName)
497 }
498 if exportedProfile.FollowersCount != profile.FollowersCount {
499 t.Errorf("expected followers %d, got %d", profile.FollowersCount, exportedProfile.FollowersCount)
500 }
501}
502
503// TestProfileToJSON_InvalidPath verifies error handling for invalid file paths
504func TestProfileToJSON_InvalidPath(t *testing.T) {
505 profile := createTestProfile()
506
507 err := ProfileToJSON("/invalid/path/that/does/not/exist/profile.json", profile)
508 if err == nil {
509 t.Error("expected error for invalid path, got nil")
510 }
511}
512
513// TestProfileToTXT_Success verifies profile TXT export with valid data
514func TestProfileToTXT_Success(t *testing.T) {
515 profile := createTestProfile()
516
517 tmpDir := t.TempDir()
518 filename := filepath.Join(tmpDir, "profile.txt")
519
520 err := ProfileToTXT(filename, profile)
521 if err != nil {
522 t.Fatalf("ProfileToTXT failed: %v", err)
523 }
524
525 if _, err := os.Stat(filename); os.IsNotExist(err) {
526 t.Fatal("exported file does not exist")
527 }
528
529 data, err := os.ReadFile(filename)
530 if err != nil {
531 t.Fatalf("failed to read exported file: %v", err)
532 }
533
534 content := string(data)
535
536 if !strings.Contains(content, "@"+profile.Handle) {
537 t.Error("missing handle")
538 }
539 if !strings.Contains(content, profile.DisplayName) {
540 t.Error("missing display name")
541 }
542 if !strings.Contains(content, profile.Did) {
543 t.Error("missing DID")
544 }
545 if !strings.Contains(content, profile.Description) {
546 t.Error("missing description")
547 }
548 if !strings.Contains(content, "Followers: 100") {
549 t.Error("missing followers count")
550 }
551 if !strings.Contains(content, "Following: 50") {
552 t.Error("missing following count")
553 }
554 if !strings.Contains(content, "Posts: 25") {
555 t.Error("missing posts count")
556 }
557 if !strings.Contains(content, strings.Repeat("=", 80)) {
558 t.Error("missing separator")
559 }
560}
561
562// TestProfileToTXT_MinimalProfile verifies TXT export with minimal profile data
563func TestProfileToTXT_MinimalProfile(t *testing.T) {
564 profile := &store.ActorProfile{
565 Did: "did:plc:minimal",
566 Handle: "minimal.bsky.social",
567 }
568
569 tmpDir := t.TempDir()
570 filename := filepath.Join(tmpDir, "minimal.txt")
571
572 err := ProfileToTXT(filename, profile)
573 if err != nil {
574 t.Fatalf("ProfileToTXT failed: %v", err)
575 }
576
577 data, err := os.ReadFile(filename)
578 if err != nil {
579 t.Fatalf("failed to read exported file: %v", err)
580 }
581
582 content := string(data)
583
584 if !strings.Contains(content, "did:plc:minimal") {
585 t.Error("missing DID")
586 }
587 if !strings.Contains(content, "@minimal.bsky.social") {
588 t.Error("missing handle")
589 }
590
591 if strings.Contains(content, "Display Name:") && profile.DisplayName == "" {
592 t.Error("should not show empty display name label")
593 }
594}
595
596// TestProfileToTXT_InvalidPath verifies error handling for invalid file paths
597func TestProfileToTXT_InvalidPath(t *testing.T) {
598 profile := createTestProfile()
599
600 err := ProfileToTXT("/invalid/path/that/does/not/exist/profile.txt", profile)
601 if err == nil {
602 t.Error("expected error for invalid path, got nil")
603 }
604}
605
606// createTestFeedViewPost generates a sample FeedViewPost for testing
607func createTestFeedViewPost() *store.FeedViewPost {
608 return &store.FeedViewPost{
609 Post: &store.PostView{
610 Uri: "at://did:plc:test123/app.bsky.feed.post/abc123",
611 Cid: "bafyreic3test",
612 Author: &store.ActorProfile{
613 Did: "did:plc:author123",
614 Handle: "testauthor.bsky.social",
615 DisplayName: "Test Author",
616 },
617 Record: map[string]any{
618 "text": "This is a test post with some content",
619 "createdAt": "2024-01-01T12:00:00Z",
620 },
621 LikeCount: 42,
622 RepostCount: 10,
623 ReplyCount: 5,
624 QuoteCount: 2,
625 IndexedAt: "2024-01-01T12:00:00Z",
626 },
627 }
628}
629
630// TestFeedViewPostToJSON_Success verifies FeedViewPost JSON export with valid data
631func TestFeedViewPostToJSON_Success(t *testing.T) {
632 post := createTestFeedViewPost()
633
634 tmpDir := t.TempDir()
635 filename := filepath.Join(tmpDir, "post.json")
636
637 err := FeedViewPostToJSON(filename, post)
638 if err != nil {
639 t.Fatalf("FeedViewPostToJSON failed: %v", err)
640 }
641
642 if _, err := os.Stat(filename); os.IsNotExist(err) {
643 t.Fatal("exported file does not exist")
644 }
645
646 data, err := os.ReadFile(filename)
647 if err != nil {
648 t.Fatalf("failed to read exported file: %v", err)
649 }
650
651 var exportedPost store.FeedViewPost
652 if err := json.Unmarshal(data, &exportedPost); err != nil {
653 t.Fatalf("failed to parse JSON: %v", err)
654 }
655
656 if exportedPost.Post.Uri != post.Post.Uri {
657 t.Errorf("expected URI %s, got %s", post.Post.Uri, exportedPost.Post.Uri)
658 }
659 if exportedPost.Post.Author.Handle != post.Post.Author.Handle {
660 t.Errorf("expected handle %s, got %s", post.Post.Author.Handle, exportedPost.Post.Author.Handle)
661 }
662 if exportedPost.Post.LikeCount != post.Post.LikeCount {
663 t.Errorf("expected like count %d, got %d", post.Post.LikeCount, exportedPost.Post.LikeCount)
664 }
665}
666
667// TestFeedViewPostToJSON_InvalidPath verifies error handling for invalid file paths
668func TestFeedViewPostToJSON_InvalidPath(t *testing.T) {
669 post := createTestFeedViewPost()
670
671 err := FeedViewPostToJSON("/invalid/path/that/does/not/exist/post.json", post)
672 if err == nil {
673 t.Error("expected error for invalid path, got nil")
674 }
675}
676
677// TestFeedViewPostToTXT_Success verifies FeedViewPost TXT export with valid data
678func TestFeedViewPostToTXT_Success(t *testing.T) {
679 post := createTestFeedViewPost()
680
681 tmpDir := t.TempDir()
682 filename := filepath.Join(tmpDir, "post.txt")
683
684 err := FeedViewPostToTXT(filename, post)
685 if err != nil {
686 t.Fatalf("FeedViewPostToTXT failed: %v", err)
687 }
688
689 if _, err := os.Stat(filename); os.IsNotExist(err) {
690 t.Fatal("exported file does not exist")
691 }
692
693 data, err := os.ReadFile(filename)
694 if err != nil {
695 t.Fatalf("failed to read exported file: %v", err)
696 }
697
698 content := string(data)
699
700 if !strings.Contains(content, "@"+post.Post.Author.Handle) {
701 t.Error("missing author handle")
702 }
703 if !strings.Contains(content, post.Post.Author.DisplayName) {
704 t.Error("missing author display name")
705 }
706 if !strings.Contains(content, post.Post.Uri) {
707 t.Error("missing post URI")
708 }
709 if !strings.Contains(content, post.Post.Cid) {
710 t.Error("missing post CID")
711 }
712 if !strings.Contains(content, "This is a test post") {
713 t.Error("missing post text")
714 }
715 if !strings.Contains(content, "Likes: 42") {
716 t.Error("missing like count")
717 }
718 if !strings.Contains(content, "Reposts: 10") {
719 t.Error("missing repost count")
720 }
721 if !strings.Contains(content, "Replies: 5") {
722 t.Error("missing reply count")
723 }
724 if !strings.Contains(content, "Quotes: 2") {
725 t.Error("missing quote count")
726 }
727 if !strings.Contains(content, strings.Repeat("=", 80)) {
728 t.Error("missing separator")
729 }
730}
731
732// TestFeedViewPostToTXT_WithRepost verifies TXT export with repost reason
733func TestFeedViewPostToTXT_WithRepost(t *testing.T) {
734 post := createTestFeedViewPost()
735 post.Reason = &store.ReasonView{
736 Type: "app.bsky.feed.defs#reasonRepost",
737 By: &store.ActorProfile{
738 Did: "did:plc:reposter",
739 Handle: "reposter.bsky.social",
740 DisplayName: "Reposter",
741 },
742 IndexedAt: "2024-01-01T12:00:00Z",
743 }
744
745 tmpDir := t.TempDir()
746 filename := filepath.Join(tmpDir, "repost.txt")
747
748 err := FeedViewPostToTXT(filename, post)
749 if err != nil {
750 t.Fatalf("FeedViewPostToTXT failed: %v", err)
751 }
752
753 data, err := os.ReadFile(filename)
754 if err != nil {
755 t.Fatalf("failed to read exported file: %v", err)
756 }
757
758 content := string(data)
759
760 if !strings.Contains(content, "Reposted by: @reposter.bsky.social") {
761 t.Error("missing repost information")
762 }
763}
764
765// TestFeedViewPostToTXT_MinimalPost verifies TXT export with minimal post data
766func TestFeedViewPostToTXT_MinimalPost(t *testing.T) {
767 post := &store.FeedViewPost{
768 Post: &store.PostView{
769 Uri: "at://did:plc:minimal/app.bsky.feed.post/xyz",
770 Cid: "bafyreicminimal",
771 Author: &store.ActorProfile{
772 Did: "did:plc:minimal",
773 Handle: "minimal.bsky.social",
774 },
775 Record: map[string]any{},
776 IndexedAt: "2024-01-01T00:00:00Z",
777 },
778 }
779
780 tmpDir := t.TempDir()
781 filename := filepath.Join(tmpDir, "minimal.txt")
782
783 err := FeedViewPostToTXT(filename, post)
784 if err != nil {
785 t.Fatalf("FeedViewPostToTXT failed: %v", err)
786 }
787
788 data, err := os.ReadFile(filename)
789 if err != nil {
790 t.Fatalf("failed to read exported file: %v", err)
791 }
792
793 content := string(data)
794
795 if !strings.Contains(content, "@minimal.bsky.social") {
796 t.Error("missing handle")
797 }
798 if !strings.Contains(content, "at://did:plc:minimal") {
799 t.Error("missing URI")
800 }
801}
802
803// TestFeedViewPostToTXT_InvalidPath verifies error handling for invalid file paths
804func TestFeedViewPostToTXT_InvalidPath(t *testing.T) {
805 post := createTestFeedViewPost()
806
807 err := FeedViewPostToTXT("/invalid/path/that/does/not/exist/post.txt", post)
808 if err == nil {
809 t.Error("expected error for invalid path, got nil")
810 }
811}