๐Ÿ Tiny CLI to post simultaneously to Mastodon and Bluesky
1
fork

Configure Feed

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

`gofmt` and `golangci-lint` stuff

+112 -21
+2
auth/auth_test.go
··· 9 9 ) 10 10 11 11 func TestSaveAndLoadFromFile(t *testing.T) { 12 + t.Parallel() 12 13 email := "me@server.org" 13 14 password := "forty two" 14 15 token := "forty-two" ··· 94 95 } 95 96 96 97 func TestLoadBackwardCompatibility(t *testing.T) { 98 + t.Parallel() 97 99 pth := filepath.Join("..", "testdata", "encrypted.pickle") 98 100 got, err := loadFromFile(pth, "admin") 99 101 if err != nil {
+1
auth/backward_test.go
··· 7 7 ) 8 8 9 9 func TestUnpickle(t *testing.T) { 10 + t.Parallel() 10 11 pth := filepath.Join("..", "testdata", "auth.pickle") 11 12 f, err := os.Open(pth) 12 13 if err != nil {
+2 -2
client/bluesky.go
··· 13 13 14 14 "golang.org/x/sync/errgroup" 15 15 16 - "tangled.org/cuducos.me/not-my-ex/auth" 17 - "tangled.org/cuducos.me/not-my-ex/post" 18 16 "github.com/bluesky-social/indigo/api/atproto" 19 17 "github.com/bluesky-social/indigo/api/bsky" 20 18 "github.com/bluesky-social/indigo/atproto/atclient" 21 19 "github.com/bluesky-social/indigo/atproto/syntax" 22 20 "github.com/bluesky-social/indigo/lex/util" 21 + "tangled.org/cuducos.me/not-my-ex/auth" 22 + "tangled.org/cuducos.me/not-my-ex/post" 23 23 ) 24 24 25 25 var (
+11
client/bluesky_test.go
··· 8 8 ) 9 9 10 10 func TestBuildFacetsURL(t *testing.T) { 11 + t.Parallel() 11 12 src := []byte("check out https://example.com today") 12 13 got := buildFacets(src) 13 14 if len(got) != 1 { ··· 29 30 } 30 31 31 32 func TestBuildFacetsHashtag(t *testing.T) { 33 + t.Parallel() 32 34 src := []byte("loving #golang today") 33 35 got := buildFacets(src) 34 36 if len(got) != 1 { ··· 44 46 } 45 47 46 48 func TestBuildFacetsHashtagAllDigitsSkipped(t *testing.T) { 49 + t.Parallel() 47 50 src := []byte("issue #42 is fixed") 48 51 got := buildFacets(src) 49 52 if len(got) != 0 { ··· 52 55 } 53 56 54 57 func TestBuildFacetsHashtagInsideURLSkipped(t *testing.T) { 58 + t.Parallel() 55 59 src := []byte("see https://example.com/wiki/CBOR#42 for details") 56 60 got := buildFacets(src) 57 61 if len(got) != 1 { ··· 63 67 } 64 68 65 69 func TestBuildFacetsURLAndHashtag(t *testing.T) { 70 + t.Parallel() 66 71 src := []byte("โœจ example mentioning @atproto.com to #share the URL ๐Ÿ‘จโ€โค๏ธโ€๐Ÿ‘จ https://en.wikipedia.org/wiki/CBOR#42.") 67 72 got := buildFacets(src) 68 73 if len(got) != 2 { ··· 82 87 } 83 88 84 89 func TestBuildFacetsHashtagTrailingPunctuationStripped(t *testing.T) { 90 + t.Parallel() 85 91 src := []byte("nice #golang. period") 86 92 got := buildFacets(src) 87 93 if len(got) != 1 { ··· 93 99 } 94 100 95 101 func TestBuildFacetsEmpty(t *testing.T) { 102 + t.Parallel() 96 103 got := buildFacets([]byte("no links or hashtags here")) 97 104 if len(got) != 0 { 98 105 t.Errorf("expected 0 facets, got %d", len(got)) ··· 100 107 } 101 108 102 109 func TestWithRetrySuccess(t *testing.T) { 110 + t.Parallel() 103 111 var n int 104 112 err := withRetry(context.Background(), 3, time.Millisecond, func() error { 105 113 n++ ··· 114 122 } 115 123 116 124 func TestWithRetryEventualSuccess(t *testing.T) { 125 + t.Parallel() 117 126 var n int 118 127 err := withRetry(context.Background(), 5, time.Millisecond, func() error { 119 128 n++ ··· 131 140 } 132 141 133 142 func TestWithRetryExhausted(t *testing.T) { 143 + t.Parallel() 134 144 var n int 135 145 err := withRetry(context.Background(), 3, time.Millisecond, func() error { 136 146 n++ ··· 145 155 } 146 156 147 157 func TestWithRetryContextCancel(t *testing.T) { 158 + t.Parallel() 148 159 ctx, cancel := context.WithCancel(context.Background()) 149 160 cancel() 150 161 err := withRetry(ctx, 5, time.Millisecond, func() error {
+22 -5
client/card_test.go
··· 20 20 } 21 21 22 22 func TestParseOGMeta(t *testing.T) { 23 + t.Parallel() 23 24 m := parseOGMeta(readTestdata(t, "card.html")) 24 25 if m["og:title"] != "Sample Title" { 25 26 t.Errorf("expected og:title to be %q, got %q", "Sample Title", m["og:title"]) ··· 37 38 srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 38 39 if r.URL.Path == "/image.jpeg" { 39 40 w.Header().Set("Content-Type", "image/jpeg") 40 - w.Write(thumb) 41 + if _, err := w.Write(thumb); err != nil { 42 + t.Errorf("error writing thumbnail: %v", err) 43 + } 41 44 return 42 45 } 43 46 w.Header().Set("Content-Type", "text/html") 44 - w.Write([]byte(strings.ReplaceAll(string(html), "SERVER_URL", srv.URL))) 47 + if _, err := w.Write([]byte(strings.ReplaceAll(string(html), "SERVER_URL", srv.URL))); err != nil { 48 + t.Errorf("error writing html: %v", err) 49 + } 45 50 })) 46 51 return srv 47 52 } ··· 51 56 b := readTestdata(t, name) 52 57 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 53 58 w.Header().Set("Content-Type", "text/html") 54 - w.Write(b) 59 + if _, err := w.Write(b); err != nil { 60 + t.Errorf("error writing html: %v", err) 61 + } 55 62 })) 56 63 } 57 64 58 65 func TestFetchCard(t *testing.T) { 66 + t.Parallel() 59 67 thumb := readTestdata(t, "image.jpeg") 60 68 srv := newCardServer(t) 61 69 defer srv.Close() ··· 79 87 } 80 88 81 89 func TestFetchCardMissingTitle(t *testing.T) { 90 + t.Parallel() 82 91 srv := newStaticServer(t, "card_no_title.html") 83 92 defer srv.Close() 84 93 ··· 88 97 } 89 98 90 99 func TestFetchCardMissingURL(t *testing.T) { 100 + t.Parallel() 91 101 srv := newStaticServer(t, "card_no_url.html") 92 102 defer srv.Close() 93 103 ··· 97 107 } 98 108 99 109 func TestFetchCardHTTPError(t *testing.T) { 110 + t.Parallel() 100 111 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 101 112 w.WriteHeader(http.StatusNotFound) 102 113 })) ··· 108 119 } 109 120 110 121 func TestFetchCardNoThumbnail(t *testing.T) { 122 + t.Parallel() 111 123 srv := newStaticServer(t, "card_no_thumbnail.html") 112 124 defer srv.Close() 113 125 ··· 121 133 } 122 134 123 135 func TestFetchCardRelativeImage(t *testing.T) { 136 + t.Parallel() 124 137 thumb := readTestdata(t, "image.jpeg") 125 138 html := readTestdata(t, "card_relative_image.html") 126 139 var srv *httptest.Server 127 140 srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 128 141 if r.URL.Path == "/image.jpeg" { 129 142 w.Header().Set("Content-Type", "image/jpeg") 130 - w.Write(thumb) 143 + if _, err := w.Write(thumb); err != nil { 144 + t.Errorf("error writing thumbnail: %v", err) 145 + } 131 146 return 132 147 } 133 148 w.Header().Set("Content-Type", "text/html") 134 - w.Write([]byte(strings.ReplaceAll(string(html), "SERVER_URL", srv.URL))) 149 + if _, err := w.Write([]byte(strings.ReplaceAll(string(html), "SERVER_URL", srv.URL))); err != nil { 150 + t.Errorf("error writing html: %v", err) 151 + } 135 152 })) 136 153 defer srv.Close() 137 154
+18 -4
client/mastodon_test.go
··· 38 38 } 39 39 40 40 func TestMastodonUploadMedia(t *testing.T) { 41 + t.Parallel() 41 42 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 42 43 if r.Method != "POST" || r.URL.Path != "/api/v2/media" { 43 44 t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) ··· 45 46 return 46 47 } 47 48 w.Header().Set("Content-Type", "application/json") 48 - json.NewEncoder(w).Encode(mastodonMedia{ID: "42"}) 49 + if err := json.NewEncoder(w).Encode(mastodonMedia{ID: "42"}); err != nil { 50 + t.Errorf("error encoding json: %v", err) 51 + } 49 52 })) 50 53 defer srv.Close() 51 54 ··· 62 65 } 63 66 64 67 func TestMastodonUploadMediaServerError(t *testing.T) { 68 + t.Parallel() 65 69 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 66 70 w.WriteHeader(http.StatusUnprocessableEntity) 67 - w.Write([]byte("file too large")) 71 + if _, err := w.Write([]byte("file too large")); err != nil { 72 + t.Errorf("error writing response: %v", err) 73 + } 68 74 })) 69 75 defer srv.Close() 70 76 ··· 76 82 } 77 83 78 84 func TestMastodonUploadMediaAsyncProcessing(t *testing.T) { 85 + t.Parallel() 79 86 var n atomic.Int32 80 87 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 81 88 if r.Method == "POST" && r.URL.Path == "/api/v2/media" { 82 89 w.Header().Set("Content-Type", "application/json") 83 90 w.WriteHeader(http.StatusAccepted) 84 - json.NewEncoder(w).Encode(mastodonMedia{ID: "99"}) 91 + if err := json.NewEncoder(w).Encode(mastodonMedia{ID: "99"}); err != nil { 92 + t.Errorf("error encoding json: %v", err) 93 + } 85 94 return 86 95 } 87 96 if r.Method == "GET" { ··· 108 117 } 109 118 110 119 func TestMastodonWaitMediaReady(t *testing.T) { 120 + t.Parallel() 111 121 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 112 122 w.WriteHeader(http.StatusOK) 113 123 })) ··· 120 130 } 121 131 122 132 func TestMastodonWaitMediaContextCancelled(t *testing.T) { 133 + t.Parallel() 123 134 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 124 135 w.WriteHeader(http.StatusPartialContent) 125 136 })) ··· 139 150 } 140 151 141 152 func TestMastodonUploadMediaAltText(t *testing.T) { 153 + t.Parallel() 142 154 var b []byte 143 155 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 144 156 b, _ = io.ReadAll(r.Body) 145 157 w.Header().Set("Content-Type", "application/json") 146 - json.NewEncoder(w).Encode(mastodonMedia{ID: "1"}) 158 + if err := json.NewEncoder(w).Encode(mastodonMedia{ID: "1"}); err != nil { 159 + t.Errorf("error encoding json: %v", err) 160 + } 147 161 })) 148 162 defer srv.Close() 149 163
+2 -2
cmd/cmd.go
··· 8 8 "strings" 9 9 "time" 10 10 11 + "github.com/spf13/cobra" 12 + "golang.org/x/sync/errgroup" 11 13 "tangled.org/cuducos.me/not-my-ex/auth" 12 14 "tangled.org/cuducos.me/not-my-ex/post" 13 - "github.com/spf13/cobra" 14 - "golang.org/x/sync/errgroup" 15 15 ) 16 16 17 17 var (
+1 -1
cmd/config.go
··· 4 4 "context" 5 5 "slices" 6 6 7 + "golang.org/x/sync/errgroup" 7 8 "tangled.org/cuducos.me/not-my-ex/auth" 8 9 "tangled.org/cuducos.me/not-my-ex/client" 9 - "golang.org/x/sync/errgroup" 10 10 ) 11 11 12 12 type Config struct {
+7 -1
cmd/post_test.go
··· 7 7 ) 8 8 9 9 func TestPostContentFromArgsLiteralText(t *testing.T) { 10 + t.Parallel() 10 11 got, err := postContentFromArgs([]string{"hello world"}) 11 12 if err != nil { 12 13 t.Fatalf("expected no error, got %s", err) ··· 17 18 } 18 19 19 20 func TestPostContentFromArgsFile(t *testing.T) { 21 + t.Parallel() 20 22 f, err := os.CreateTemp(t.TempDir(), "post-*.txt") 21 23 if err != nil { 22 24 t.Fatalf("expected no error creating temp file, got %s", err) ··· 24 26 if _, err := f.WriteString("from file"); err != nil { 25 27 t.Fatalf("expected no error writing temp file, got %s", err) 26 28 } 27 - f.Close() 29 + if err := f.Close(); err != nil { 30 + t.Fatalf("error closing temp file: %v", err) 31 + } 28 32 29 33 got, err := postContentFromArgs([]string{f.Name()}) 30 34 if err != nil { ··· 36 40 } 37 41 38 42 func TestPostContentFromArgsNoArgs(t *testing.T) { 43 + t.Parallel() 39 44 got, err := postContentFromArgs([]string{}) 40 45 if err != nil { 41 46 t.Fatalf("expected no error, got %s", err) ··· 46 51 } 47 52 48 53 func TestPostContentFromFileNotExistTreatedAsText(t *testing.T) { 54 + t.Parallel() 49 55 pth := filepath.Join(t.TempDir(), "does-not-exist.txt") 50 56 got, err := postContentFromArgs([]string{pth}) 51 57 if err != nil {
+36 -6
post/media_test.go
··· 6 6 ) 7 7 8 8 func TestMediaDimensionsPNG(t *testing.T) { 9 + t.Parallel() 9 10 m, err := newMedia(filepath.Join("..", "testdata", "image.png"), "", 0) 10 11 if err != nil { 11 12 t.Fatalf("expected no error opening PNG, got %s", err) 12 13 } 13 - defer m.Close() 14 + t.Cleanup(func() { 15 + if err := m.Close(); err != nil { 16 + t.Errorf("error closing PNG media: %v", err) 17 + } 18 + }) 14 19 15 20 w, h, err := m.Dimensions() 16 21 if err != nil { ··· 25 30 } 26 31 27 32 func TestMediaDimensionsJPEG(t *testing.T) { 33 + t.Parallel() 28 34 m, err := newMedia(filepath.Join("..", "testdata", "image.jpeg"), "", 0) 29 35 if err != nil { 30 36 t.Fatalf("expected no error opening JPEG, got %s", err) 31 37 } 32 - defer m.Close() 38 + t.Cleanup(func() { 39 + if err := m.Close(); err != nil { 40 + t.Errorf("error closing JPEG media: %v", err) 41 + } 42 + }) 33 43 34 44 w, h, err := m.Dimensions() 35 45 if err != nil { ··· 44 54 } 45 55 46 56 func TestNewMediaSizeLimit(t *testing.T) { 57 + t.Parallel() 47 58 _, err := newMedia(filepath.Join("..", "testdata", "image.png"), "", 1) 48 59 if err == nil { 49 60 t.Error("expected error for image exceeding size limit, got nil") ··· 51 62 } 52 63 53 64 func TestNewMediasOrder(t *testing.T) { 65 + t.Parallel() 54 66 png := filepath.Join("..", "testdata", "image.png") 55 67 jpeg := filepath.Join("..", "testdata", "image.jpeg") 56 68 pths := []string{png, jpeg, png, jpeg} ··· 60 72 t.Fatalf("expected no error, got %s", err) 61 73 } 62 74 for i, m := range got { 63 - defer m.Close() 75 + m := m 76 + t.Cleanup(func() { 77 + if err := m.Close(); err != nil { 78 + t.Errorf("error closing media item %d: %v", i, err) 79 + } 80 + }) 64 81 w, _, err := m.Dimensions() 65 82 if err != nil { 66 83 t.Fatalf("expected no error reading dimensions for item %d, got %s", i, err) ··· 78 95 } 79 96 80 97 func TestNewMediasAltTexts(t *testing.T) { 98 + t.Parallel() 81 99 png := filepath.Join("..", "testdata", "image.png") 82 100 pths := []string{png, png} 83 101 alts := []string{"first", "second"} ··· 87 105 t.Fatalf("expected no error, got %s", err) 88 106 } 89 107 for i, m := range got { 90 - defer m.Close() 108 + m := m 109 + t.Cleanup(func() { 110 + if err := m.Close(); err != nil { 111 + t.Errorf("error closing media item %d: %v", i, err) 112 + } 113 + }) 91 114 if m.Alt != alts[i] { 92 115 t.Errorf("expected alt text to be %q for item %d, got %q", alts[i], i, m.Alt) 93 116 } ··· 95 118 } 96 119 97 120 func TestNewMediasPartialAltTexts(t *testing.T) { 121 + t.Parallel() 98 122 png := filepath.Join("..", "testdata", "image.png") 99 123 pths := []string{png, png} 100 124 alts := []string{"only-first"} ··· 103 127 if err != nil { 104 128 t.Fatalf("expected no error, got %s", err) 105 129 } 106 - defer got[0].Close() 107 - defer got[1].Close() 130 + t.Cleanup(func() { 131 + if err := got[0].Close(); err != nil { 132 + t.Errorf("error closing media item 0: %v", err) 133 + } 134 + if err := got[1].Close(); err != nil { 135 + t.Errorf("error closing media item 1: %v", err) 136 + } 137 + }) 108 138 if got[0].Alt != "only-first" { 109 139 t.Errorf("expected first alt to be %q, got %q", "only-first", got[0].Alt) 110 140 }
+10
post/post_test.go
··· 7 7 ) 8 8 9 9 func TestNewLanguageValid(t *testing.T) { 10 + t.Parallel() 10 11 for _, c := range []string{"en", "pt", "FR", " de "} { 11 12 got, err := newLanguage(c) 12 13 if err != nil { ··· 19 20 } 20 21 21 22 func TestNewLanguageInvalid(t *testing.T) { 23 + t.Parallel() 22 24 for _, c := range []string{"", "e", "eng", "e1", "42"} { 23 25 if _, err := newLanguage(c); err == nil { 24 26 t.Errorf("expected error for %q, got nil", c) ··· 27 29 } 28 30 29 31 func TestNewPostTooLong(t *testing.T) { 32 + t.Parallel() 30 33 _, err := NewPost(strings.Repeat("a", 301), nil, "en", 300, false) 31 34 if err == nil { 32 35 t.Fatal("expected error for text exceeding limit, got nil") ··· 37 40 } 38 41 39 42 func TestNewPostMultiByteWithinLimit(t *testing.T) { 43 + t.Parallel() 40 44 // 300 CJK characters = 900 bytes, should not exceed the 300-character limit 41 45 p, err := NewPost(strings.Repeat("ไฝ ", 300), nil, "zh", 300, false) 42 46 if err != nil { ··· 48 52 } 49 53 50 54 func TestNewPostMultiByteTooLong(t *testing.T) { 55 + t.Parallel() 51 56 // 301 CJK characters should exceed the 300-character limit 52 57 _, err := NewPost(strings.Repeat("ไฝ ", 301), nil, "zh", 300, false) 53 58 if err == nil { ··· 56 61 } 57 62 58 63 func TestNewPostTooLongSavesDraft(t *testing.T) { 64 + t.Parallel() 59 65 _, err := NewPost(strings.Repeat("a", 301), nil, "en", 300, false) 60 66 if err == nil { 61 67 t.Fatal("expected error, got nil") ··· 73 79 } 74 80 75 81 func TestNewPostValid(t *testing.T) { 82 + t.Parallel() 76 83 p, err := NewPost("forty-two", nil, "en", 300, false) 77 84 if err != nil { 78 85 t.Fatalf("expected no error, got %s", err) ··· 86 93 } 87 94 88 95 func TestNewPostTrimsWhitespace(t *testing.T) { 96 + t.Parallel() 89 97 p, err := NewPost(" hello ", nil, "en", 300, false) 90 98 if err != nil { 91 99 t.Fatalf("expected no error, got %s", err) ··· 96 104 } 97 105 98 106 func TestPostEmpty(t *testing.T) { 107 + t.Parallel() 99 108 cases := []struct { 100 109 txt string 101 110 media []*Media ··· 113 122 } 114 123 115 124 func TestPostEmptyWithMedia(t *testing.T) { 125 + t.Parallel() 116 126 p := &Post{Text: "", Media: []*Media{{Mime: "image/png"}}} 117 127 if p.Empty() { 118 128 t.Error("expected non-empty post with media, got empty")